forked from maxmcd/tcp-proxy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdaemon.go
114 lines (99 loc) · 3.36 KB
/
daemon.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// Start a command and redirect its stdout & stderr to a channel and a file respectively
// copied from https://stackoverflow.com/a/69958845/6088837
package main
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
)
type Daemon struct {
cancel func()
doneCh chan struct{}
cmdErr error
log chan string
}
func (d *Daemon) Start(command string, args ...string) <- chan string {
ctx, cancel := context.WithCancel(context.Background())
d.cancel = cancel
d.doneCh = make(chan struct{})
d.log = make(chan string, 100)
cmd := exec.CommandContext(ctx, command, args...)
fW, e := os.Create("command.log")
if e != nil {
panic(e)
}
outR, outW := io.Pipe()
mw := io.MultiWriter(outW, fW)
// mw := io.MultiWriter(outW, os.Stdout)
cmd.Stdout = mw
cmd.Stderr = mw
// MultiWriter could be blocked by one of the writters.
// As a result, cmd.Wait does not return.
// check out this comment: https://github.com/golang/go/issues/10338#issuecomment-115423168
// Use buffering to prevent MultiWriter from being blocked.
lines := make(chan string, 5000)
// Output lines producer goroutine.
// This goroutine typically exits when there the write
// end of the pipe is closed (which happens in the cmd.Wait goroutine).
go func() {
defer close(lines) // close on exit: when the scanner has no more to read, or has encountered an error.
scanner := bufio.NewScanner(outR)
for scanner.Scan() {
lines <- scanner.Text()
}
if err := scanner.Err(); err != nil {
d.log <- fmt.Sprint(err)
}
}()
// Start the command.
if err := cmd.Start(); err != nil {
panic(err)
}
// Goroutine that waits for the command to exit using cmd.Wait().
// It closes the doneCh to indicate to users of Daemon that
// the command has finished.
// It closes the write end of the pipe to free-up the output lines producer
// and, in turn, the output lines consumer goroutines.
//
// This goroutine must be run only after cmd.Start() returns,
// otherwise cmd.Run() may panic.
//
// The command can exit either:
// * normally with success;
// * with failure due to a command error; or
// * with failure due to Context cancelation.
go func() {
d.log <- "waiting..."
err := cmd.Wait()
d.log <- fmt.Sprint("command exited; error is:", err)
_ = outW.Close() // TODO: handle error from Close(); log it maybe.
d.cmdErr = err
close(d.doneCh)
}()
return lines
}
// Done returns a channel, which is closed when the command
// started by Start exits: either normally, due to a command error,
// or due to d.cancel.
// After the channel is closed, d.CmdErr() returns the error, if any,
// from the command's exit.
func (d *Daemon) Done() <-chan struct{} {
d.log <- "Done"
return d.doneCh
}
// CmdErr returns the error, if any, from the command's exit.
// Only valid after the channel returned by Done() has been closed.
func (d *Daemon) CmdErr() error {
return d.cmdErr
}
// Cancel causes the running command to exit preemptively.
// If Cancel is called after the command has already
// exited either naturally or due to a previous Cancel call,
// then Cancel has no effect.
func (d *Daemon) Cancel() {
d.log <- "Cancel"
d.cancel()
}