1- package main
1+ // Package tandem provides a process manager utility for running multiple
2+ // processes and combining their output.
3+ package tandem
24
35import (
46 "encoding/json"
@@ -12,11 +14,15 @@ import (
1214 "sync"
1315 "syscall"
1416 "time"
17+
18+ "github.com/rosszurowski/tandem/ansi"
1519)
1620
1721var colors = []int {2 , 3 , 4 , 5 , 6 , 42 , 130 , 103 , 129 , 108 }
1822
19- type processManager struct {
23+ // ProcessManager manages a set of processes, combining their output and exiting
24+ // all of them gracefully when one of them exits.
25+ type ProcessManager struct {
2026 output * multiOutput
2127 procs []* process
2228 procWg sync.WaitGroup
@@ -26,21 +32,35 @@ type processManager struct {
2632 silent bool
2733}
2834
29- func newProcessManager (root string , timeout int , cmds []string , silent bool ) (* processManager , error ) {
30- pm := & processManager {
35+ // Config is the configuration for a process manager.
36+ type Config struct {
37+ Cmds []string // Shell commands to run
38+ Root string // Root directory for commands to run from
39+ Timeout int // Timeout in seconds for commands to exit gracefully before being killed. Defaults to 0.
40+ Silent bool // Whether to silence process management messages like "Starting..."
41+ }
42+
43+ // New creates a new process manager with the given configuration.
44+ func New (cfg Config ) (* ProcessManager , error ) {
45+ root , err := filepath .Abs (cfg .Root )
46+ if err != nil {
47+ return nil , fmt .Errorf ("could not get absolute path for directory: %v" , err )
48+ }
49+
50+ pm := & ProcessManager {
3151 output : & multiOutput {printProcName : true },
3252 procs : make ([]* process , 0 ),
33- timeout : time .Duration (timeout ) * time .Second ,
34- silent : silent ,
53+ timeout : time .Duration (cfg . Timeout ) * time .Second ,
54+ silent : cfg . Silent ,
3555 }
3656
3757 env := os .Environ ()
38- nodeBin := filepath .Join (root , "node_modules/.bin" )
58+ nodeBin := filepath .Join (cfg . Root , "node_modules/.bin" )
3959 if fi , err := os .Stat (nodeBin ); err == nil && fi .IsDir () {
4060 injectPathVal (env , nodeBin )
4161 }
4262
43- namedCmds , err := parseCommands (root , cmds )
63+ namedCmds , err := parseCommands (root , cfg . Cmds )
4464 if err != nil {
4565 return nil , err
4666 }
@@ -59,7 +79,8 @@ func newProcessManager(root string, timeout int, cmds []string, silent bool) (*p
5979 return pm , nil
6080}
6181
62- func (pm * processManager ) Run () {
82+ // Run starts all processes and waits for them to exit or be interrupted.
83+ func (pm * ProcessManager ) Run () {
6384 pm .done = make (chan bool , len (pm .procs ))
6485 pm .interrupted = make (chan os.Signal )
6586 signal .Notify (pm .interrupted , syscall .SIGINT , syscall .SIGTERM )
@@ -70,7 +91,7 @@ func (pm *processManager) Run() {
7091 pm .procWg .Wait ()
7192}
7293
73- func (pm * processManager ) runProcess (proc * process ) {
94+ func (pm * ProcessManager ) runProcess (proc * process ) {
7495 pm .procWg .Add (1 )
7596 go func () {
7697 defer pm .procWg .Done ()
@@ -79,21 +100,21 @@ func (pm *processManager) runProcess(proc *process) {
79100 }()
80101}
81102
82- func (pm * processManager ) waitForDoneOrInterrupt () {
103+ func (pm * ProcessManager ) waitForDoneOrInterrupt () {
83104 select {
84105 case <- pm .done :
85106 case <- pm .interrupted :
86107 }
87108}
88109
89- func (pm * processManager ) waitForTimeoutOrInterrupt () {
110+ func (pm * ProcessManager ) waitForTimeoutOrInterrupt () {
90111 select {
91112 case <- time .After (pm .timeout ):
92113 case <- pm .interrupted :
93114 }
94115}
95116
96- func (pm * processManager ) waitForExit () {
117+ func (pm * ProcessManager ) waitForExit () {
97118 pm .waitForDoneOrInterrupt ()
98119 for _ , proc := range pm .procs {
99120 go proc .Interrupt ()
@@ -151,6 +172,10 @@ func (p *process) signal(sig os.Signal) {
151172 }
152173}
153174
175+ func (p * process ) writeDebug (s string ) {
176+ p .writeLine ([]byte (ansi .Dim (s )))
177+ }
178+
154179func (p * process ) writeLine (b []byte ) {
155180 p .output .WriteLine (p , b )
156181}
@@ -163,30 +188,30 @@ func (p *process) Run() {
163188 p .output .PipeOutput (p )
164189 defer p .output .ClosePipe (p )
165190 if ! p .silent {
166- p .writeLine ([] byte ( dim ( "Starting..." )) )
191+ p .writeDebug ( "Starting..." )
167192 }
168193 if err := p .Cmd .Run (); err != nil {
169194 var exitErr * exec.ExitError
170195 if errors .As (err , & exitErr ) {
171196 if exitErr .ExitCode () == 1 {
172197 p .writeErr (err )
173198 } else {
174- p .writeLine ([]byte (dim (fmt .Sprintf ("exit status %d" , exitErr .ExitCode ()))))
199+ p .writeLine ([]byte (ansi . Dim (fmt .Sprintf ("exit status %d" , exitErr .ExitCode ()))))
175200 }
176201 return
177202 }
178203 p .writeErr (err )
179204 return
180205 }
181206 if ! p .silent {
182- p .writeLine ([] byte ( dim ( "Process exited" )) )
207+ p .writeDebug ( "Process exited" )
183208 }
184209}
185210
186211func (p * process ) Interrupt () {
187212 if p .Running () {
188213 if ! p .silent {
189- p .writeLine ([] byte ( dim ( "Interrupting..." )) )
214+ p .writeDebug ( "Interrupting..." )
190215 }
191216 p .signal (syscall .SIGINT )
192217 }
@@ -195,7 +220,7 @@ func (p *process) Interrupt() {
195220func (p * process ) Kill () {
196221 if p .Running () {
197222 if ! p .silent {
198- p .writeLine ([] byte ( dim ( "Killing..." )) )
223+ p .writeDebug ( "Killing..." )
199224 }
200225 p .signal (syscall .SIGKILL )
201226 }
0 commit comments