Skip to content

Commit 2fe10c6

Browse files
authored
Merge pull request golang#164 from erizocosmico/bugfix-84/hung-processes
Introduce monitoredCmd
2 parents 7fb04c9 + 7411b16 commit 2fe10c6

File tree

4 files changed

+168
-1
lines changed

4 files changed

+168
-1
lines changed

_testdata/cmd/echosleep.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"time"
7+
)
8+
9+
func main() {
10+
n := flag.Int("n", 1, "number of iterations before stopping")
11+
flag.Parse()
12+
13+
for i := 0; i < *n; i++ {
14+
fmt.Println("foo")
15+
time.Sleep(time.Duration(i) * 100 * time.Millisecond)
16+
}
17+
}

cmd.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package gps
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os/exec"
7+
"time"
8+
)
9+
10+
// monitoredCmd wraps a cmd and will keep monitoring the process until it
11+
// finishes or a certain amount of time has passed and the command showed
12+
// no signs of activity.
13+
type monitoredCmd struct {
14+
cmd *exec.Cmd
15+
timeout time.Duration
16+
stdout *activityBuffer
17+
stderr *activityBuffer
18+
}
19+
20+
func newMonitoredCmd(cmd *exec.Cmd, timeout time.Duration) *monitoredCmd {
21+
stdout := newActivityBuffer()
22+
stderr := newActivityBuffer()
23+
cmd.Stderr = stderr
24+
cmd.Stdout = stdout
25+
return &monitoredCmd{cmd, timeout, stdout, stderr}
26+
}
27+
28+
// run will wait for the command to finish and return the error, if any. If the
29+
// command does not show any activity for more than the specified timeout the
30+
// process will be killed.
31+
func (c *monitoredCmd) run() error {
32+
ticker := time.NewTicker(c.timeout)
33+
done := make(chan error, 1)
34+
defer ticker.Stop()
35+
go func() { done <- c.cmd.Run() }()
36+
37+
for {
38+
select {
39+
case <-ticker.C:
40+
if c.hasTimedOut() {
41+
if err := c.cmd.Process.Kill(); err != nil {
42+
return &killCmdError{err}
43+
}
44+
45+
return &timeoutError{c.timeout}
46+
}
47+
case err := <-done:
48+
return err
49+
}
50+
}
51+
}
52+
53+
func (c *monitoredCmd) hasTimedOut() bool {
54+
t := time.Now().Add(-c.timeout)
55+
return c.stderr.lastActivity.Before(t) &&
56+
c.stdout.lastActivity.Before(t)
57+
}
58+
59+
func (c *monitoredCmd) combinedOutput() ([]byte, error) {
60+
if err := c.run(); err != nil {
61+
return c.stderr.buf.Bytes(), err
62+
}
63+
64+
return c.stdout.buf.Bytes(), nil
65+
}
66+
67+
// activityBuffer is a buffer that keeps track of the last time a Write
68+
// operation was performed on it.
69+
type activityBuffer struct {
70+
buf *bytes.Buffer
71+
lastActivity time.Time
72+
}
73+
74+
func newActivityBuffer() *activityBuffer {
75+
return &activityBuffer{
76+
buf: bytes.NewBuffer(nil),
77+
}
78+
}
79+
80+
func (b *activityBuffer) Write(p []byte) (int, error) {
81+
b.lastActivity = time.Now()
82+
return b.buf.Write(p)
83+
}
84+
85+
type timeoutError struct {
86+
timeout time.Duration
87+
}
88+
89+
func (e timeoutError) Error() string {
90+
return fmt.Sprintf("command killed after %s of no activity", e.timeout)
91+
}
92+
93+
type killCmdError struct {
94+
err error
95+
}
96+
97+
func (e killCmdError) Error() string {
98+
return fmt.Sprintf("error killing command after timeout: %s", e.err)
99+
}

cmd_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package gps
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"testing"
8+
"time"
9+
)
10+
11+
func mkTestCmd(iterations int) *monitoredCmd {
12+
return newMonitoredCmd(
13+
exec.Command("./echosleep", "-n", fmt.Sprint(iterations)),
14+
200*time.Millisecond,
15+
)
16+
}
17+
18+
func TestMonitoredCmd(t *testing.T) {
19+
err := exec.Command("go", "build", "./_testdata/cmd/echosleep.go").Run()
20+
if err != nil {
21+
t.Errorf("Unable to build echosleep binary: %s", err)
22+
}
23+
defer os.Remove("./echosleep")
24+
25+
cmd := mkTestCmd(2)
26+
err = cmd.run()
27+
if err != nil {
28+
t.Errorf("Expected command not to fail: %s", err)
29+
}
30+
31+
expectedOutput := "foo\nfoo\n"
32+
if cmd.stdout.buf.String() != expectedOutput {
33+
t.Errorf("Unexpected output:\n\t(GOT): %s\n\t(WNT): %s", cmd.stdout.buf.String(), expectedOutput)
34+
}
35+
36+
cmd = mkTestCmd(10)
37+
err = cmd.run()
38+
if err == nil {
39+
t.Error("Expected command to fail")
40+
}
41+
42+
_, ok := err.(*timeoutError)
43+
if !ok {
44+
t.Errorf("Expected a timeout error, but got: %s", err)
45+
}
46+
47+
expectedOutput = "foo\nfoo\nfoo\nfoo\n"
48+
if cmd.stdout.buf.String() != expectedOutput {
49+
t.Errorf("Unexpected output:\n\t(GOT): %s\n\t(WNT): %s", cmd.stdout.buf.String(), expectedOutput)
50+
}
51+
}

glide.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)