...

Source file src/github.com/Microsoft/hcsshim/internal/cmd/cmd.go

Documentation: github.com/Microsoft/hcsshim/internal/cmd

     1  //go:build windows
     2  
     3  package cmd
     4  
     5  import (
     6  	"bytes"
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"strings"
    11  	"sync/atomic"
    12  	"time"
    13  
    14  	"github.com/Microsoft/hcsshim/internal/cow"
    15  	hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
    16  	"github.com/Microsoft/hcsshim/internal/log"
    17  	specs "github.com/opencontainers/runtime-spec/specs-go"
    18  	"github.com/sirupsen/logrus"
    19  	"golang.org/x/sync/errgroup"
    20  	"golang.org/x/sys/windows"
    21  )
    22  
    23  // CmdProcessRequest stores information on command requests made through this package.
    24  type CmdProcessRequest struct {
    25  	Args     []string
    26  	Workdir  string
    27  	Terminal bool
    28  	Stdin    string
    29  	Stdout   string
    30  	Stderr   string
    31  }
    32  
    33  // Cmd represents a command being prepared or run in a process host.
    34  type Cmd struct {
    35  	// Host is the process host in which to launch the process.
    36  	Host cow.ProcessHost
    37  
    38  	// The OCI spec for the process.
    39  	Spec *specs.Process
    40  
    41  	// Standard IO streams to relay to/from the process.
    42  	Stdin  io.Reader
    43  	Stdout io.Writer
    44  	Stderr io.Writer
    45  
    46  	// Log provides a logrus entry to use in logging IO copying status.
    47  	Log *logrus.Entry
    48  
    49  	// Context provides a context that terminates the process when it is done.
    50  	Context context.Context
    51  
    52  	// CopyAfterExitTimeout is the amount of time after process exit we allow the
    53  	// stdout, stderr relays to continue before forcibly closing them if not
    54  	// already completed. This is primarily a safety step against the HCS when
    55  	// it fails to send a close on the stdout, stderr pipes when the process
    56  	// exits and blocks the relay wait groups forever.
    57  	CopyAfterExitTimeout time.Duration
    58  
    59  	// Process is filled out after Start() returns.
    60  	Process cow.Process
    61  
    62  	// ExitState is filled out after Wait() (or Run() or Output()) completes.
    63  	ExitState *ExitState
    64  
    65  	iogrp     errgroup.Group
    66  	stdinErr  atomic.Value
    67  	allDoneCh chan struct{}
    68  }
    69  
    70  // ExitState contains whether a process has exited and with which exit code.
    71  type ExitState struct {
    72  	exited bool
    73  	code   int
    74  }
    75  
    76  // ExitCode returns the exit code of the process, or -1 if the exit code is not known.
    77  func (s *ExitState) ExitCode() int {
    78  	if !s.exited {
    79  		return -1
    80  	}
    81  	return s.code
    82  }
    83  
    84  // ExitError is used when a process exits with a non-zero exit code.
    85  type ExitError struct {
    86  	*ExitState
    87  }
    88  
    89  func (err *ExitError) Error() string {
    90  	return fmt.Sprintf("process exited with exit code %d", err.ExitCode())
    91  }
    92  
    93  // Additional fields to hcsschema.ProcessParameters used by LCOW
    94  type lcowProcessParameters struct {
    95  	hcsschema.ProcessParameters
    96  	OCIProcess *specs.Process `json:"OciProcess,omitempty"`
    97  }
    98  
    99  // escapeArgs makes a Windows-style escaped command line from a set of arguments
   100  func escapeArgs(args []string) string {
   101  	escapedArgs := make([]string, len(args))
   102  	for i, a := range args {
   103  		escapedArgs[i] = windows.EscapeArg(a)
   104  	}
   105  	return strings.Join(escapedArgs, " ")
   106  }
   107  
   108  // Command makes a Cmd for a given command and arguments.
   109  func Command(host cow.ProcessHost, name string, arg ...string) *Cmd {
   110  	cmd := &Cmd{
   111  		Host: host,
   112  		Spec: &specs.Process{
   113  			Args: append([]string{name}, arg...),
   114  		},
   115  		Log:       log.L.Dup(),
   116  		ExitState: &ExitState{},
   117  	}
   118  	if host.OS() == "windows" {
   119  		cmd.Spec.Cwd = `C:\`
   120  	} else {
   121  		cmd.Spec.Cwd = "/"
   122  		cmd.Spec.Env = []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}
   123  	}
   124  	return cmd
   125  }
   126  
   127  // CommandContext makes a Cmd for a given command and arguments. After
   128  // it is launched, the process is killed when the context becomes done.
   129  func CommandContext(ctx context.Context, host cow.ProcessHost, name string, arg ...string) *Cmd {
   130  	cmd := Command(host, name, arg...)
   131  	cmd.Context = ctx
   132  	cmd.Log = log.G(ctx)
   133  	return cmd
   134  }
   135  
   136  // Start starts a command. The caller must ensure that if Start succeeds,
   137  // Wait is eventually called to clean up resources.
   138  func (c *Cmd) Start() error {
   139  	c.allDoneCh = make(chan struct{})
   140  	var x interface{}
   141  	if !c.Host.IsOCI() {
   142  		wpp := &hcsschema.ProcessParameters{
   143  			CommandLine:      c.Spec.CommandLine,
   144  			User:             c.Spec.User.Username,
   145  			WorkingDirectory: c.Spec.Cwd,
   146  			EmulateConsole:   c.Spec.Terminal,
   147  			CreateStdInPipe:  c.Stdin != nil,
   148  			CreateStdOutPipe: c.Stdout != nil,
   149  			CreateStdErrPipe: c.Stderr != nil,
   150  		}
   151  
   152  		if c.Spec.CommandLine == "" {
   153  			if c.Host.OS() == "windows" {
   154  				wpp.CommandLine = escapeArgs(c.Spec.Args)
   155  			} else {
   156  				wpp.CommandArgs = c.Spec.Args
   157  			}
   158  		}
   159  
   160  		environment := make(map[string]string)
   161  		for _, v := range c.Spec.Env {
   162  			s := strings.SplitN(v, "=", 2)
   163  			if len(s) == 2 && len(s[1]) > 0 {
   164  				environment[s[0]] = s[1]
   165  			}
   166  		}
   167  		wpp.Environment = environment
   168  
   169  		if c.Spec.ConsoleSize != nil {
   170  			wpp.ConsoleSize = []int32{
   171  				int32(c.Spec.ConsoleSize.Height),
   172  				int32(c.Spec.ConsoleSize.Width),
   173  			}
   174  		}
   175  		x = wpp
   176  	} else {
   177  		lpp := &lcowProcessParameters{
   178  			ProcessParameters: hcsschema.ProcessParameters{
   179  				CreateStdInPipe:  c.Stdin != nil,
   180  				CreateStdOutPipe: c.Stdout != nil,
   181  				CreateStdErrPipe: c.Stderr != nil,
   182  			},
   183  			OCIProcess: c.Spec,
   184  		}
   185  		x = lpp
   186  	}
   187  	if c.Context != nil && c.Context.Err() != nil {
   188  		return c.Context.Err()
   189  	}
   190  	p, err := c.Host.CreateProcess(context.TODO(), x)
   191  	if err != nil {
   192  		return err
   193  	}
   194  	c.Process = p
   195  	if c.Log != nil {
   196  		c.Log = c.Log.WithField("pid", p.Pid())
   197  	}
   198  
   199  	// Start relaying process IO.
   200  	stdin, stdout, stderr := p.Stdio()
   201  	if c.Stdin != nil {
   202  		// Do not make stdin part of the error group because there is no way for
   203  		// us or the caller to reliably unblock the c.Stdin read when the
   204  		// process exits.
   205  		go func() {
   206  			_, err := relayIO(stdin, c.Stdin, c.Log, "stdin")
   207  			// Report the stdin copy error. If the process has exited, then the
   208  			// caller may never see it, but if the error was due to a failure in
   209  			// stdin read, then it is likely the process is still running.
   210  			if err != nil {
   211  				c.stdinErr.Store(err)
   212  			}
   213  			// Notify the process that there is no more input.
   214  			if err := p.CloseStdin(context.TODO()); err != nil && c.Log != nil {
   215  				c.Log.WithError(err).Warn("failed to close Cmd stdin")
   216  			}
   217  		}()
   218  	}
   219  
   220  	if c.Stdout != nil {
   221  		c.iogrp.Go(func() error {
   222  			_, err := relayIO(c.Stdout, stdout, c.Log, "stdout")
   223  			if err := p.CloseStdout(context.TODO()); err != nil {
   224  				c.Log.WithError(err).Warn("failed to close Cmd stdout")
   225  			}
   226  			return err
   227  		})
   228  	}
   229  
   230  	if c.Stderr != nil {
   231  		c.iogrp.Go(func() error {
   232  			_, err := relayIO(c.Stderr, stderr, c.Log, "stderr")
   233  			if err := p.CloseStderr(context.TODO()); err != nil {
   234  				c.Log.WithError(err).Warn("failed to close Cmd stderr")
   235  			}
   236  			return err
   237  		})
   238  	}
   239  
   240  	if c.Context != nil {
   241  		go func() {
   242  			select {
   243  			case <-c.Context.Done():
   244  				// Process.Kill (via Process.Signal) will not send an RPC if the
   245  				// provided context in is cancelled (bridge.AsyncRPC will end early)
   246  				ctx := c.Context
   247  				if ctx == nil {
   248  					ctx = context.Background()
   249  				}
   250  				kctx := log.Copy(context.Background(), ctx)
   251  				_, _ = c.Process.Kill(kctx)
   252  			case <-c.allDoneCh:
   253  			}
   254  		}()
   255  	}
   256  	return nil
   257  }
   258  
   259  // Wait waits for a command and its IO to complete and closes the underlying
   260  // process. It can only be called once. It returns an ExitError if the command
   261  // runs and returns a non-zero exit code.
   262  func (c *Cmd) Wait() error {
   263  	waitErr := c.Process.Wait()
   264  	if waitErr != nil && c.Log != nil {
   265  		c.Log.WithError(waitErr).Warn("process wait failed")
   266  	}
   267  	state := &ExitState{}
   268  	code, exitErr := c.Process.ExitCode()
   269  	if exitErr == nil {
   270  		state.exited = true
   271  		state.code = code
   272  	}
   273  	// Terminate the IO if the copy does not complete in the requested time.
   274  	if c.CopyAfterExitTimeout != 0 {
   275  		go func() {
   276  			t := time.NewTimer(c.CopyAfterExitTimeout)
   277  			defer t.Stop()
   278  			select {
   279  			case <-c.allDoneCh:
   280  			case <-t.C:
   281  				// Close the process to cancel any reads to stdout or stderr.
   282  				c.Process.Close()
   283  				if c.Log != nil {
   284  					c.Log.Warn("timed out waiting for stdio relay")
   285  				}
   286  			}
   287  		}()
   288  	}
   289  	ioErr := c.iogrp.Wait()
   290  	if ioErr == nil {
   291  		ioErr, _ = c.stdinErr.Load().(error)
   292  	}
   293  	close(c.allDoneCh)
   294  	c.Process.Close()
   295  	c.ExitState = state
   296  	if exitErr != nil {
   297  		return exitErr
   298  	}
   299  	if state.exited && state.code != 0 {
   300  		return &ExitError{state}
   301  	}
   302  	return ioErr
   303  }
   304  
   305  // Run is equivalent to Start followed by Wait.
   306  func (c *Cmd) Run() error {
   307  	err := c.Start()
   308  	if err != nil {
   309  		return err
   310  	}
   311  	return c.Wait()
   312  }
   313  
   314  // Output runs a command via Run and collects its stdout into a buffer,
   315  // which it returns.
   316  func (c *Cmd) Output() ([]byte, error) {
   317  	var b bytes.Buffer
   318  	c.Stdout = &b
   319  	err := c.Run()
   320  	return b.Bytes(), err
   321  }
   322  

View as plain text