github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/osutil/exec.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package osutil 21 22 import ( 23 "fmt" 24 "io" 25 "os" 26 "os/exec" 27 "syscall" 28 "time" 29 30 "gopkg.in/tomb.v2" 31 32 "github.com/snapcore/snapd/strutil" 33 ) 34 35 var cmdWaitTimeout = 5 * time.Second 36 37 // KillProcessGroup kills the process group associated with the given command. 38 // 39 // If the command hasn't had Setpgid set in its SysProcAttr, you'll probably end 40 // up killing yourself. 41 func KillProcessGroup(cmd *exec.Cmd) error { 42 pgid, err := syscallGetpgid(cmd.Process.Pid) 43 if err != nil { 44 return err 45 } 46 if pgid == 1 { 47 return fmt.Errorf("cannot kill pgid 1") 48 } 49 return syscallKill(-pgid, syscall.SIGKILL) 50 } 51 52 // RunAndWait runs a command for the given argv with the given environ added to 53 // os.Environ, killing it if it reaches timeout, or if the tomb is dying. 54 func RunAndWait(argv []string, env []string, timeout time.Duration, tomb *tomb.Tomb) ([]byte, error) { 55 if len(argv) == 0 { 56 return nil, fmt.Errorf("internal error: osutil.RunAndWait needs non-empty argv") 57 } 58 if timeout <= 0 { 59 return nil, fmt.Errorf("internal error: osutil.RunAndWait needs positive timeout") 60 } 61 if tomb == nil { 62 return nil, fmt.Errorf("internal error: osutil.RunAndWait needs non-nil tomb") 63 } 64 65 command := exec.Command(argv[0], argv[1:]...) 66 67 // setup a process group for the command so that we can kill parent 68 // and children on e.g. timeout 69 command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 70 command.Env = append(os.Environ(), env...) 71 72 // Make sure we can obtain stdout and stderror. Same buffer so they're 73 // combined. 74 buffer := strutil.NewLimitedBuffer(100, 10*1024) 75 command.Stdout = buffer 76 command.Stderr = buffer 77 78 // Actually run the command. 79 if err := command.Start(); err != nil { 80 return nil, err 81 } 82 83 // add timeout handling 84 killTimerCh := time.After(timeout) 85 86 commandCompleted := make(chan struct{}) 87 var commandError error 88 go func() { 89 // Wait for hook to complete 90 commandError = command.Wait() 91 close(commandCompleted) 92 }() 93 94 var abortOrTimeoutError error 95 select { 96 case <-commandCompleted: 97 // Command completed; it may or may not have been successful. 98 return buffer.Bytes(), commandError 99 case <-tomb.Dying(): 100 // Hook was aborted, process will get killed below 101 abortOrTimeoutError = fmt.Errorf("aborted") 102 case <-killTimerCh: 103 // Max timeout reached, process will get killed below 104 abortOrTimeoutError = fmt.Errorf("exceeded maximum runtime of %s", timeout) 105 } 106 107 // select above exited which means that aborted or killTimeout 108 // was reached. Kill the command and wait for command.Wait() 109 // to clean it up (but limit the wait with the cmdWaitTimer) 110 if err := KillProcessGroup(command); err != nil { 111 return nil, fmt.Errorf("cannot abort: %s", err) 112 } 113 select { 114 case <-time.After(cmdWaitTimeout): 115 // cmdWaitTimeout was reached, i.e. command.Wait() did not 116 // finish in a reasonable amount of time, we can not use 117 // buffer in this case so return without it. 118 return nil, fmt.Errorf("%v, but did not stop", abortOrTimeoutError) 119 case <-commandCompleted: 120 // cmd.Wait came back from waiting the killed process 121 break 122 } 123 fmt.Fprintf(buffer, "\n<%s>", abortOrTimeoutError) 124 125 return buffer.Bytes(), abortOrTimeoutError 126 } 127 128 type waitingReader struct { 129 reader io.Reader 130 cmd *exec.Cmd 131 } 132 133 func (r *waitingReader) Close() error { 134 if r.cmd.Process != nil { 135 r.cmd.Process.Kill() 136 } 137 return r.cmd.Wait() 138 } 139 140 func (r *waitingReader) Read(b []byte) (int, error) { 141 n, err := r.reader.Read(b) 142 if n == 0 && err == io.EOF { 143 err = r.Close() 144 if err == nil { 145 return 0, io.EOF 146 } 147 return 0, err 148 } 149 return n, err 150 } 151 152 // StreamCommand runs a the named program with the given arguments, 153 // streaming its standard output over the returned io.ReadCloser. 154 // 155 // The program will run until EOF is reached (at which point the 156 // ReadCloser is closed), or until the ReadCloser is explicitly closed. 157 func StreamCommand(name string, args ...string) (io.ReadCloser, error) { 158 cmd := exec.Command(name, args...) 159 pipe, err := cmd.StdoutPipe() 160 if err != nil { 161 return nil, err 162 } 163 cmd.Stderr = os.Stderr 164 165 if err := cmd.Start(); err != nil { 166 return nil, err 167 } 168 169 return &waitingReader{reader: pipe, cmd: cmd}, nil 170 }