-
Notifications
You must be signed in to change notification settings - Fork 601
/
Copy pathexec.go
175 lines (151 loc) · 4.77 KB
/
exec.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
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2017 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package osutil
import (
"fmt"
"io"
"os"
"os/exec"
"syscall"
"time"
"gopkg.in/tomb.v2"
"github.com/snapcore/snapd/strutil"
)
var (
syscallKill = syscall.Kill
syscallGetpgid = syscall.Getpgid
)
var cmdWaitTimeout = 5 * time.Second
// KillProcessGroup kills the process group associated with the given command.
//
// If the command hasn't had Setpgid set in its SysProcAttr, you'll probably end
// up killing yourself.
func KillProcessGroup(cmd *exec.Cmd) error {
pgid, err := syscallGetpgid(cmd.Process.Pid)
if err != nil {
return err
}
if pgid == 1 {
return fmt.Errorf("cannot kill pgid 1")
}
return syscallKill(-pgid, syscall.SIGKILL)
}
// RunAndWait runs a command for the given argv with the given environ added to
// os.Environ, killing it if it reaches timeout, or if the tomb is dying.
func RunAndWait(argv []string, env []string, timeout time.Duration, tomb *tomb.Tomb) ([]byte, error) {
if len(argv) == 0 {
return nil, fmt.Errorf("internal error: osutil.RunAndWait needs non-empty argv")
}
if timeout <= 0 {
return nil, fmt.Errorf("internal error: osutil.RunAndWait needs positive timeout")
}
if tomb == nil {
return nil, fmt.Errorf("internal error: osutil.RunAndWait needs non-nil tomb")
}
command := exec.Command(argv[0], argv[1:]...)
// setup a process group for the command so that we can kill parent
// and children on e.g. timeout
command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
command.Env = append(os.Environ(), env...)
// Make sure we can obtain stdout and stderror. Same buffer so they're
// combined.
buffer := strutil.NewLimitedBuffer(100, 10*1024)
command.Stdout = buffer
command.Stderr = buffer
// Actually run the command.
if err := command.Start(); err != nil {
return nil, err
}
// add timeout handling
killTimerCh := time.After(timeout)
commandCompleted := make(chan struct{})
var commandError error
go func() {
// Wait for hook to complete
commandError = command.Wait()
close(commandCompleted)
}()
var abortOrTimeoutError error
select {
case <-commandCompleted:
// Command completed; it may or may not have been successful.
return buffer.Bytes(), commandError
case <-tomb.Dying():
// Hook was aborted, process will get killed below
abortOrTimeoutError = fmt.Errorf("aborted")
case <-killTimerCh:
// Max timeout reached, process will get killed below
abortOrTimeoutError = fmt.Errorf("exceeded maximum runtime of %s", timeout)
}
// select above exited which means that aborted or killTimeout
// was reached. Kill the command and wait for command.Wait()
// to clean it up (but limit the wait with the cmdWaitTimer)
if err := KillProcessGroup(command); err != nil {
return nil, fmt.Errorf("cannot abort: %s", err)
}
select {
case <-time.After(cmdWaitTimeout):
// cmdWaitTimeout was reached, i.e. command.Wait() did not
// finish in a reasonable amount of time, we can not use
// buffer in this case so return without it.
return nil, fmt.Errorf("%v, but did not stop", abortOrTimeoutError)
case <-commandCompleted:
// cmd.Wait came back from waiting the killed process
break
}
fmt.Fprintf(buffer, "\n<%s>", abortOrTimeoutError)
return buffer.Bytes(), abortOrTimeoutError
}
type waitingReader struct {
reader io.Reader
cmd *exec.Cmd
}
func (r *waitingReader) Close() error {
if r.cmd.Process != nil {
r.cmd.Process.Kill()
}
return r.cmd.Wait()
}
func (r *waitingReader) Read(b []byte) (int, error) {
n, err := r.reader.Read(b)
if n == 0 && err == io.EOF {
err = r.Close()
if err == nil {
return 0, io.EOF
}
return 0, err
}
return n, err
}
// StreamCommand runs a the named program with the given arguments,
// streaming its standard output over the returned io.ReadCloser.
//
// The program will run until EOF is reached (at which point the
// ReadCloser is closed), or until the ReadCloser is explicitly closed.
func StreamCommand(name string, args ...string) (io.ReadCloser, error) {
cmd := exec.Command(name, args...)
pipe, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return nil, err
}
return &waitingReader{reader: pipe, cmd: cmd}, nil
}