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  }