This repository has been archived by the owner on Feb 3, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathcmd.go
143 lines (120 loc) · 3.13 KB
/
cmd.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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package gps
import (
"bytes"
"context"
"fmt"
"os/exec"
"sync"
"time"
"github.com/Masterminds/vcs"
)
// monitoredCmd wraps a cmd and will keep monitoring the process until it
// finishes, the provided context is canceled, or a certain amount of time has
// passed and the command showed no signs of activity.
type monitoredCmd struct {
cmd *exec.Cmd
timeout time.Duration
stdout *activityBuffer
stderr *activityBuffer
}
func newMonitoredCmd(cmd *exec.Cmd, timeout time.Duration) *monitoredCmd {
stdout, stderr := newActivityBuffer(), newActivityBuffer()
cmd.Stdout, cmd.Stderr = stdout, stderr
return &monitoredCmd{
cmd: cmd,
timeout: timeout,
stdout: stdout,
stderr: stderr,
}
}
// run will wait for the command to finish and return the error, if any. If the
// command does not show any activity for more than the specified timeout the
// process will be killed.
func (c *monitoredCmd) run(ctx context.Context) error {
// Check for cancellation before even starting
if ctx.Err() != nil {
return ctx.Err()
}
ticker := time.NewTicker(c.timeout)
done := make(chan error, 1)
defer ticker.Stop()
err := c.cmd.Start()
if err != nil {
return err
}
go func() {
done <- c.cmd.Wait()
}()
for {
select {
case <-ticker.C:
if c.hasTimedOut() {
if err := c.cmd.Process.Kill(); err != nil {
return &killCmdError{err}
}
return &timeoutError{c.timeout}
}
case <-ctx.Done():
if err := c.cmd.Process.Kill(); err != nil {
return &killCmdError{err}
}
return ctx.Err()
case err := <-done:
return err
}
}
}
func (c *monitoredCmd) hasTimedOut() bool {
t := time.Now().Add(-c.timeout)
return c.stderr.lastActivity().Before(t) &&
c.stdout.lastActivity().Before(t)
}
func (c *monitoredCmd) combinedOutput(ctx context.Context) ([]byte, error) {
if err := c.run(ctx); err != nil {
return c.stderr.buf.Bytes(), err
}
return c.stdout.buf.Bytes(), nil
}
// activityBuffer is a buffer that keeps track of the last time a Write
// operation was performed on it.
type activityBuffer struct {
sync.Mutex
buf *bytes.Buffer
lastActivityStamp time.Time
}
func newActivityBuffer() *activityBuffer {
return &activityBuffer{
buf: bytes.NewBuffer(nil),
}
}
func (b *activityBuffer) Write(p []byte) (int, error) {
b.Lock()
b.lastActivityStamp = time.Now()
defer b.Unlock()
return b.buf.Write(p)
}
func (b *activityBuffer) lastActivity() time.Time {
b.Lock()
defer b.Unlock()
return b.lastActivityStamp
}
type timeoutError struct {
timeout time.Duration
}
func (e timeoutError) Error() string {
return fmt.Sprintf("command killed after %s of no activity", e.timeout)
}
type killCmdError struct {
err error
}
func (e killCmdError) Error() string {
return fmt.Sprintf("error killing command: %s", e.err)
}
func runFromCwd(ctx context.Context, cmd string, args ...string) ([]byte, error) {
c := newMonitoredCmd(exec.Command(cmd, args...), 2*time.Minute)
return c.combinedOutput(ctx)
}
func runFromRepoDir(ctx context.Context, repo vcs.Repo, cmd string, args ...string) ([]byte, error) {
c := newMonitoredCmd(repo.CmdFromDir(cmd, args...), 2*time.Minute)
return c.combinedOutput(ctx)
}