This repository was archived by the owner on Sep 9, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1k
/
Copy pathcmd.go
205 lines (173 loc) · 5.33 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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gps
import (
"bytes"
"context"
"fmt"
"os/exec"
"sync"
"sync/atomic"
"time"
"github.com/Masterminds/vcs"
"github.com/pkg/errors"
)
// 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
}
// noProgressError indicates that the monitored process was terminated due to
// exceeding exceeding the progress timeout.
type noProgressError struct {
timeout time.Duration
}
// killCmdError indicates that an error occurred while sending a kill signal to
// the monitored process.
type killCmdError struct {
err error
}
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 progress, as indicated by writing to stdout or
// stderr, 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()
}
err := c.cmd.Start()
if err != nil {
return err
}
ticker := time.NewTicker(c.timeout)
defer ticker.Stop()
// Atomic marker to track proc exit state. Guards against bad channel
// select receive order, where a tick or context cancellation could come
// in at the same time as process completion, but one of the former are
// picked first; in such a case, cmd.Process could(?) be nil by the time we
// call signal methods on it.
var isDone int32
done := make(chan error, 1)
go func() {
// Wait() can only be called once, so this must act as the completion
// indicator for both normal *and* signal-induced termination.
done <- c.cmd.Wait()
atomic.CompareAndSwapInt32(&isDone, 0, 1)
}()
var killerr error
selloop:
for {
select {
case err := <-done:
return err
case <-ticker.C:
if !atomic.CompareAndSwapInt32(&isDone, 1, 1) && c.hasTimedOut() {
if err := killProcess(c.cmd, &isDone); err != nil {
killerr = &killCmdError{err}
} else {
killerr = &noProgressError{c.timeout}
}
break selloop
}
case <-ctx.Done():
if !atomic.CompareAndSwapInt32(&isDone, 1, 1) {
if err := killProcess(c.cmd, &isDone); err != nil {
killerr = &killCmdError{err}
} else {
killerr = ctx.Err()
}
break selloop
}
}
}
// This is only reachable on the signal-induced termination path, so block
// until a message comes through the channel indicating that the command has
// exited.
//
// TODO(sdboyer) if the signaling process errored (resulting in a
// killCmdError stored in killerr), is it possible that this receive could
// block forever on some kind of hung process?
<-done
return killerr
}
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) {
c.cmd.Stderr = c.stdout
err := c.run(ctx)
return c.stdout.Bytes(), errors.Wrapf(err, "command failed: %v", c.cmd.Args)
}
// 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()
defer b.Unlock()
b.lastActivityStamp = time.Now()
return b.buf.Write(p)
}
func (b *activityBuffer) String() string {
b.Lock()
defer b.Unlock()
return b.buf.String()
}
func (b *activityBuffer) Bytes() []byte {
b.Lock()
defer b.Unlock()
return b.buf.Bytes()
}
func (b *activityBuffer) lastActivity() time.Time {
b.Lock()
defer b.Unlock()
return b.lastActivityStamp
}
func (e noProgressError) Error() string {
return fmt.Sprintf("command killed after %s of no activity", e.timeout)
}
func (e killCmdError) Error() string {
return fmt.Sprintf("error killing command: %s", e.err)
}
func runFromCwd(ctx context.Context, timeout time.Duration, cmd string, args ...string) ([]byte, error) {
c := newMonitoredCmd(exec.Command(cmd, args...), timeout)
return c.combinedOutput(ctx)
}
func runFromRepoDir(ctx context.Context, repo vcs.Repo, timeout time.Duration, cmd string, args ...string) ([]byte, error) {
c := newMonitoredCmd(repo.CmdFromDir(cmd, args...), timeout)
return c.combinedOutput(ctx)
}
const (
// expensiveCmdTimeout is meant to be used in a command that is expensive
// in terms of computation and we know it will take long or one that uses
// the network, such as clones, updates, ....
expensiveCmdTimeout = 2 * time.Minute
// defaultCmdTimeout is just an umbrella value for all other commands that
// should not take much.
defaultCmdTimeout = 30 * time.Second
)