github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/updater/command/command.go (about) 1 // Copyright 2016 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package command 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "os" 11 "os/exec" 12 "strings" 13 "syscall" 14 "time" 15 ) 16 17 // Log is the logging interface for the command package 18 type Log interface { 19 Debugf(s string, args ...interface{}) 20 Infof(s string, args ...interface{}) 21 Warningf(s string, args ...interface{}) 22 Errorf(s string, args ...interface{}) 23 } 24 25 // Program is a program at path with arguments 26 type Program struct { 27 Path string 28 Args []string 29 } 30 31 // ArgsWith returns program args with passed in args 32 func (p Program) ArgsWith(args []string) []string { 33 if p.Args == nil || len(p.Args) == 0 { 34 return args 35 } 36 if len(args) == 0 { 37 return p.Args 38 } 39 return append(p.Args, args...) 40 } 41 42 // Result is the result of running a command 43 type Result struct { 44 Stdout bytes.Buffer 45 Stderr bytes.Buffer 46 Process *os.Process 47 } 48 49 // CombinedOutput returns Stdout and Stderr as a single string. 50 func (r Result) CombinedOutput() string { 51 strs := []string{} 52 if sout := r.Stdout.String(); sout != "" { 53 strs = append(strs, fmt.Sprintf("[stdout]: %s", sout)) 54 } 55 if serr := r.Stderr.String(); serr != "" { 56 strs = append(strs, fmt.Sprintf("[stderr]: %s", serr)) 57 } 58 return strings.Join(strs, ", ") 59 } 60 61 type execCmd func(name string, arg ...string) *exec.Cmd 62 63 // Exec runs a command and returns the stdout/err output and error if any 64 func Exec(name string, args []string, timeout time.Duration, log Log) (Result, error) { 65 return execWithFunc(name, args, nil, exec.Command, timeout, log) 66 } 67 68 // ExecWithEnv runs a command with an environment and returns the stdout/err output and error if any 69 func ExecWithEnv(name string, args []string, env []string, timeout time.Duration, log Log) (Result, error) { 70 return execWithFunc(name, args, env, exec.Command, timeout, log) 71 } 72 73 // exec runs a command and returns a Result and error if any. 74 // We will send TERM signal and wait 1 second or timeout, whichever is less, 75 // before calling KILL. 76 func execWithFunc(name string, args []string, env []string, execCmd execCmd, timeout time.Duration, log Log) (Result, error) { 77 var result Result 78 log.Debugf("Execute: %s %s", name, args) 79 if name == "" { 80 return result, fmt.Errorf("No command") 81 } 82 if timeout < 0 { 83 return result, fmt.Errorf("Invalid timeout: %s", timeout) 84 } 85 cmd := execCmd(name, args...) 86 if cmd == nil { 87 return result, fmt.Errorf("No command") 88 } 89 cmd.Stdout = &result.Stdout 90 cmd.Stderr = &result.Stderr 91 if env != nil { 92 cmd.Env = env 93 } 94 err := cmd.Start() 95 if err != nil { 96 return result, err 97 } 98 result.Process = cmd.Process 99 doneCh := make(chan error) 100 go func() { 101 doneCh <- cmd.Wait() 102 close(doneCh) 103 }() 104 // Wait for the command to finish or time out 105 select { 106 case cmdErr := <-doneCh: 107 log.Debugf("Executed %s %s", name, args) 108 return result, cmdErr 109 case <-time.After(timeout): 110 // Timed out 111 log.Warningf("Process timed out") 112 } 113 // If no process, nothing to kill 114 if cmd.Process == nil { 115 return result, fmt.Errorf("No process") 116 } 117 118 // Signal the process to terminate gracefully 119 // Wait a second or timeout for termination, whichever less 120 termWait := time.Second 121 if timeout < termWait { 122 termWait = timeout 123 } 124 log.Warningf("Command timed out, terminating (will wait %s before killing)", termWait) 125 err = cmd.Process.Signal(syscall.SIGTERM) 126 if err != nil { 127 log.Warningf("Error sending terminate: %s", err) 128 } 129 select { 130 case <-doneCh: 131 log.Warningf("Terminated") 132 case <-time.After(termWait): 133 // Bring out the big guns 134 log.Warningf("Command failed to terminate, killing") 135 if err := cmd.Process.Kill(); err != nil { 136 log.Warningf("Error trying to kill process: %s", err) 137 } else { 138 log.Warningf("Killed process") 139 } 140 } 141 return result, fmt.Errorf("Timed out") 142 } 143 144 // ExecForJSON runs a command (with timeout) expecting JSON output with obj interface 145 func ExecForJSON(command string, args []string, obj interface{}, timeout time.Duration, log Log) error { 146 result, err := execWithFunc(command, args, nil, exec.Command, timeout, log) 147 if err != nil { 148 return err 149 } 150 if err := json.NewDecoder(&result.Stdout).Decode(&obj); err != nil { 151 return fmt.Errorf("Error in result: %s", err) 152 } 153 return nil 154 }