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  }