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  }