github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/updater/process/process.go (about) 1 // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package process 5 6 import ( 7 "fmt" 8 "os" 9 "runtime" 10 "syscall" 11 "time" 12 13 "github.com/keybase/go-ps" 14 ) 15 16 // Log is the logging interface for the process package 17 type Log interface { 18 Debugf(s string, args ...interface{}) 19 Infof(s string, args ...interface{}) 20 Warningf(s string, args ...interface{}) 21 Errorf(s string, args ...interface{}) 22 } 23 24 type processesFn func() ([]ps.Process, error) 25 type breakFn func([]ps.Process) bool 26 27 // FindProcesses returns processes containing string matching process path 28 func FindProcesses(matcher Matcher, wait time.Duration, delay time.Duration, log Log) ([]ps.Process, error) { 29 breakFn := func(procs []ps.Process) bool { 30 return len(procs) > 0 31 } 32 return findProcesses(matcher, breakFn, wait, delay, log) 33 } 34 35 // WaitForExit returns processes (if any) that are still running after wait 36 func WaitForExit(matcher Matcher, wait time.Duration, delay time.Duration, log Log) ([]ps.Process, error) { 37 breakFn := func(procs []ps.Process) bool { 38 return len(procs) == 0 39 } 40 return findProcesses(matcher, breakFn, wait, delay, log) 41 } 42 43 func findProcesses(matcher Matcher, breakFn breakFn, wait time.Duration, delay time.Duration, log Log) ([]ps.Process, error) { 44 start := time.Now() 45 for { 46 log.Debugf("Find process %s (%s < %s)", matcher.match, time.Since(start), wait) 47 procs, err := findProcessesWithFn(ps.Processes, matcher.Fn(), 0) 48 if err != nil { 49 return nil, err 50 } 51 if breakFn(procs) { 52 return procs, nil 53 } 54 if time.Since(start) >= wait { 55 break 56 } 57 time.Sleep(delay) 58 } 59 return nil, nil 60 } 61 62 // findProcessWithPID returns a process for a pid. 63 // Consider using os.FindProcess instead if suitable since this may be 64 // inefficient. 65 func findProcessWithPID(pid int) (ps.Process, error) { 66 matchPID := func(p ps.Process) bool { return p.Pid() == pid } 67 procs, err := findProcessesWithFn(ps.Processes, matchPID, 1) 68 if err != nil { 69 return nil, err 70 } 71 if len(procs) == 0 { 72 return nil, nil 73 } 74 return procs[0], nil 75 } 76 77 // Currently findProcessWithPID is only used in tests, ignore deadcode warning 78 var _ = findProcessWithPID 79 80 // findProcessesWithFn finds processes using match function. 81 // If max is != 0, then we will return that max number of processes. 82 func findProcessesWithFn(fn processesFn, matchFn MatchFn, max int) ([]ps.Process, error) { 83 processes, err := fn() 84 if err != nil { 85 return nil, fmt.Errorf("Error listing processes: %s", err) 86 } 87 if processes == nil { 88 return nil, nil 89 } 90 procs := []ps.Process{} 91 for _, p := range processes { 92 if matchFn(p) { 93 procs = append(procs, p) 94 } 95 if max != 0 && len(procs) >= max { 96 break 97 } 98 } 99 return procs, nil 100 } 101 102 // FindPIDsWithMatchFn returns pids for processes matching function 103 func FindPIDsWithMatchFn(matchFn MatchFn, log Log) ([]int, error) { 104 return findPIDsWithFn(ps.Processes, matchFn, log) 105 } 106 107 func findPIDsWithFn(fn processesFn, matchFn MatchFn, log Log) ([]int, error) { 108 procs, err := findProcessesWithFn(fn, matchFn, 0) 109 if err != nil { 110 log.Errorf("Error finding matching processes") 111 return nil, err 112 } 113 pids := []int{} 114 for _, p := range procs { 115 pids = append(pids, p.Pid()) 116 } 117 log.Debugf("Found %d matching processes with pids: %s", len(procs), pids) 118 return pids, nil 119 } 120 121 // TerminateAll stops all processes with executable names that contains the matching string. 122 // It returns the pids that were terminated. 123 // This method only logs errors, if you need error handling, you can should use a different implementation. 124 func TerminateAll(matcher Matcher, killDelay time.Duration, log Log) []int { 125 return TerminateAllWithProcessesFn(ps.Processes, matcher.Fn(), killDelay, log) 126 } 127 128 // TerminateAllWithProcessesFn stops processes processesFn that satify the matchFn. 129 // It returns the pids that were terminated. 130 // This method only logs errors, if you need error handling, you can should use a different implementation. 131 func TerminateAllWithProcessesFn(fn processesFn, matchFn MatchFn, killDelay time.Duration, log Log) (terminatedPids []int) { 132 pids, err := findPIDsWithFn(fn, matchFn, log) 133 if err != nil { 134 log.Errorf("Error finding process: %s", err) 135 return terminatedPids 136 } 137 if len(pids) == 0 { 138 return terminatedPids 139 } 140 for _, pid := range pids { 141 if err := TerminatePID(pid, killDelay, log); err != nil { 142 log.Errorf("Error terminating %d: %s", pid, err) 143 continue 144 } 145 log.Debugf("Successfully terminated %s", pid) 146 terminatedPids = append(terminatedPids, pid) 147 } 148 return terminatedPids 149 } 150 151 // TerminatePID is an overly simple way to terminate a PID. 152 // On darwin and linux, it calls SIGTERM, then waits a killDelay and then calls 153 // SIGKILL. We don't mind if we call SIGKILL on an already terminated process, 154 // since there could be a race anyway where the process exits right after we 155 // check if it's still running but before the SIGKILL. 156 // The killDelay is not used on windows. 157 func TerminatePID(pid int, killDelay time.Duration, log Log) error { 158 log.Debugf("Searching OS for %d to terminate", pid) 159 process, err := os.FindProcess(pid) 160 if err != nil { 161 return fmt.Errorf("Error finding OS process: %s", err) 162 } 163 if process == nil { 164 return fmt.Errorf("No process found with pid %d", pid) 165 } 166 167 // Sending SIGTERM is not supported on windows, so we can use process.Kill() 168 if runtime.GOOS == "windows" { 169 return process.Kill() 170 } 171 172 log.Debugf("Terminating: %#v", process) 173 err = process.Signal(syscall.SIGTERM) 174 if err != nil { 175 log.Warningf("Error sending terminate: %s", err) 176 } 177 log.Debugf("Waiting %s", killDelay) 178 time.Sleep(killDelay) 179 log.Debugf("Done waiting") 180 // Ignore SIGKILL error since it will be that the process wasn't running if 181 // the terminate above succeeded. If terminate didn't succeed above, then 182 // this SIGKILL is a measure of last resort, and an error would signify that 183 // something in the environment has gone terribly wrong. 184 _ = process.Kill() 185 return err 186 } 187 188 // KillAll kills all processes that match 189 func KillAll(matcher Matcher, log Log) (pids []int) { 190 pids, err := findPIDsWithFn(ps.Processes, matcher.Fn(), log) 191 if err != nil { 192 log.Errorf("Error finding process: %s", err) 193 return 194 } 195 if len(pids) == 0 { 196 return 197 } 198 for _, pid := range pids { 199 if err := KillPID(pid, log); err != nil { 200 log.Errorf("Error killing %d: %s", pid, err) 201 } 202 } 203 return 204 } 205 206 // KillPID kills process at pid (sends a SIGKILL on unix) 207 func KillPID(pid int, log Log) error { 208 log.Debugf("Searching OS for %d to kill", pid) 209 process, err := os.FindProcess(pid) 210 if err != nil { 211 return fmt.Errorf("Error finding OS process: %s", err) 212 } 213 if process == nil { 214 return fmt.Errorf("No process found with pid %d", pid) 215 } 216 return process.Kill() 217 }