github.com/vmware/govmomi@v0.51.0/toolbox/process/process.go (about)

     1  // © Broadcom. All Rights Reserved.
     2  // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package process
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"net"
    13  	"net/url"
    14  	"os"
    15  	"os/exec"
    16  	"path"
    17  	"path/filepath"
    18  	"strconv"
    19  	"strings"
    20  	"sync"
    21  	"sync/atomic"
    22  	"syscall"
    23  	"time"
    24  
    25  	"github.com/vmware/govmomi/toolbox/hgfs"
    26  	"github.com/vmware/govmomi/toolbox/vix"
    27  )
    28  
    29  var (
    30  	EscapeXML *strings.Replacer
    31  
    32  	shell = "/bin/sh"
    33  
    34  	defaultOwner = os.Getenv("USER")
    35  )
    36  
    37  func init() {
    38  	// See: VixToolsEscapeXMLString
    39  	chars := []string{
    40  		`"`,
    41  		"%",
    42  		"&",
    43  		"'",
    44  		"<",
    45  		">",
    46  	}
    47  
    48  	replace := make([]string, 0, len(chars)*2)
    49  
    50  	for _, c := range chars {
    51  		replace = append(replace, c)
    52  		replace = append(replace, url.QueryEscape(c))
    53  	}
    54  
    55  	EscapeXML = strings.NewReplacer(replace...)
    56  
    57  	// See procMgrPosix.c:ProcMgrStartProcess:
    58  	// Prefer bash -c as is uses exec() to replace itself,
    59  	// whereas bourne shell does a fork & exec, so two processes are started.
    60  	if sh, err := exec.LookPath("bash"); err != nil {
    61  		shell = sh
    62  	}
    63  
    64  	if defaultOwner == "" {
    65  		defaultOwner = "toolbox"
    66  	}
    67  }
    68  
    69  // IO encapsulates IO for Go functions and OS commands such that they can interact via the OperationsManager
    70  // without file system disk IO.
    71  type IO struct {
    72  	In struct {
    73  		io.Writer
    74  		io.Reader
    75  		io.Closer // Closer for the write side of the pipe, can be closed via hgfs ops (FileTranfserToGuest)
    76  	}
    77  
    78  	Out *bytes.Buffer
    79  	Err *bytes.Buffer
    80  }
    81  
    82  // State is the toolbox representation of the GuestProcessInfo type
    83  type State struct {
    84  	StartTime int64 // (keep first to ensure 64-bit alignment)
    85  	EndTime   int64 // (keep first to ensure 64-bit alignment)
    86  
    87  	Name     string
    88  	Args     string
    89  	Owner    string
    90  	Pid      int64
    91  	ExitCode int32
    92  
    93  	IO *IO
    94  }
    95  
    96  // WithIO enables toolbox Process IO without file system disk IO.
    97  func (p *Process) WithIO() *Process {
    98  	p.IO = &IO{
    99  		Out: new(bytes.Buffer),
   100  		Err: new(bytes.Buffer),
   101  	}
   102  
   103  	return p
   104  }
   105  
   106  // File implements the os.FileInfo interface to enable toolbox interaction with virtual files.
   107  type File struct {
   108  	io.Reader
   109  	io.Writer
   110  	io.Closer
   111  
   112  	name string
   113  	size int
   114  }
   115  
   116  // Name implementation of the os.FileInfo interface method.
   117  func (a *File) Name() string {
   118  	return a.name
   119  }
   120  
   121  // Size implementation of the os.FileInfo interface method.
   122  func (a *File) Size() int64 {
   123  	return int64(a.size)
   124  }
   125  
   126  // Mode implementation of the os.FileInfo interface method.
   127  func (a *File) Mode() os.FileMode {
   128  	if strings.HasSuffix(a.name, "stdin") {
   129  		return 0200
   130  	}
   131  	return 0400
   132  }
   133  
   134  // ModTime implementation of the os.FileInfo interface method.
   135  func (a *File) ModTime() time.Time {
   136  	return time.Now()
   137  }
   138  
   139  // IsDir implementation of the os.FileInfo interface method.
   140  func (a *File) IsDir() bool {
   141  	return false
   142  }
   143  
   144  // Sys implementation of the os.FileInfo interface method.
   145  func (a *File) Sys() any {
   146  	return nil
   147  }
   148  
   149  func (s *State) toXML() string {
   150  	const format = "<proc>" +
   151  		"<cmd>%s</cmd>" +
   152  		"<name>%s</name>" +
   153  		"<pid>%d</pid>" +
   154  		"<user>%s</user>" +
   155  		"<start>%d</start>" +
   156  		"<eCode>%d</eCode>" +
   157  		"<eTime>%d</eTime>" +
   158  		"</proc>"
   159  
   160  	name := filepath.Base(s.Name)
   161  
   162  	argv := []string{s.Name}
   163  
   164  	if len(s.Args) != 0 {
   165  		argv = append(argv, EscapeXML.Replace(s.Args))
   166  	}
   167  
   168  	args := strings.Join(argv, " ")
   169  
   170  	return fmt.Sprintf(format, name, args, s.Pid, s.Owner, s.StartTime, s.ExitCode, s.EndTime)
   171  }
   172  
   173  // Process managed by the process Manager.
   174  type Process struct {
   175  	State
   176  
   177  	Start func(*Process, *vix.StartProgramRequest) (int64, error)
   178  	Wait  func() error
   179  	Kill  context.CancelFunc
   180  
   181  	ctx context.Context
   182  }
   183  
   184  // Error can be returned by the Process.Wait function to propagate ExitCode to process State.
   185  type Error struct {
   186  	Err      error
   187  	ExitCode int32
   188  }
   189  
   190  func (e *Error) Error() string {
   191  	return e.Err.Error()
   192  }
   193  
   194  // Manager manages processes within the guest.
   195  // See: https://developer.broadcom.com/xapis/vsphere-web-services-api/latest/vim.vm.guest.Manager.html
   196  type Manager struct {
   197  	wg      sync.WaitGroup
   198  	mu      sync.Mutex
   199  	expire  time.Duration
   200  	entries map[int64]*Process
   201  	pids    sync.Pool
   202  }
   203  
   204  // NewManager creates a new process Manager instance.
   205  func NewManager() *Manager {
   206  	// We use pseudo PIDs that don't conflict with OS PIDs, so they can live in the same table.
   207  	// For the pseudo PIDs, we use a sync.Pool rather than a plain old counter to avoid the unlikely,
   208  	// but possible wrapping should such a counter exceed MaxInt64.
   209  	pid := int64(32768) // TODO: /proc/sys/kernel/pid_max
   210  
   211  	return &Manager{
   212  		expire:  time.Minute * 5,
   213  		entries: make(map[int64]*Process),
   214  		pids: sync.Pool{
   215  			New: func() any {
   216  				return atomic.AddInt64(&pid, 1)
   217  			},
   218  		},
   219  	}
   220  }
   221  
   222  // Start calls the Process.Start function, returning the pid on success or an error.
   223  // A goroutine is started that calls the Process.Wait function.  After Process.Wait has
   224  // returned, the process State EndTime and ExitCode fields are set.  The process state can be
   225  // queried via ListProcessesInGuest until it is removed, 5 minutes after Wait returns.
   226  func (m *Manager) Start(r *vix.StartProgramRequest, p *Process) (int64, error) {
   227  	p.Name = r.ProgramPath
   228  	p.Args = r.Arguments
   229  
   230  	// Owner is cosmetic, but useful for example with: govc guest.ps -U $uid
   231  	if p.Owner == "" {
   232  		p.Owner = defaultOwner
   233  	}
   234  
   235  	p.StartTime = time.Now().Unix()
   236  
   237  	p.ctx, p.Kill = context.WithCancel(context.Background())
   238  
   239  	pid, err := p.Start(p, r)
   240  	if err != nil {
   241  		return -1, err
   242  	}
   243  
   244  	if pid == 0 {
   245  		p.Pid = m.pids.Get().(int64) // pseudo pid for funcs
   246  	} else {
   247  		p.Pid = pid
   248  	}
   249  
   250  	m.mu.Lock()
   251  	m.entries[p.Pid] = p
   252  	m.mu.Unlock()
   253  
   254  	m.wg.Add(1)
   255  	go func() {
   256  		werr := p.Wait()
   257  
   258  		m.mu.Lock()
   259  		p.EndTime = time.Now().Unix()
   260  
   261  		if werr != nil {
   262  			rc := int32(1)
   263  			if xerr, ok := werr.(*Error); ok {
   264  				rc = xerr.ExitCode
   265  			}
   266  
   267  			p.ExitCode = rc
   268  		}
   269  
   270  		m.mu.Unlock()
   271  		m.wg.Done()
   272  		p.Kill() // cancel context for those waiting on p.ctx.Done()
   273  
   274  		// See: https://developer.broadcom.com/xapis/vsphere-web-services-api/latest/vim.vm.guest.ProcessManager.ProcessInfo.html
   275  		// "If the process was started using StartProgramInGuest then the process completion time
   276  		//  will be available if queried within 5 minutes after it completes."
   277  		<-time.After(m.expire)
   278  
   279  		m.mu.Lock()
   280  		delete(m.entries, p.Pid)
   281  		m.mu.Unlock()
   282  
   283  		if pid == 0 {
   284  			m.pids.Put(p.Pid) // pseudo pid can be reused now
   285  		}
   286  	}()
   287  
   288  	return p.Pid, nil
   289  }
   290  
   291  // Kill cancels the Process Context.
   292  // Returns true if pid exists in the process table, false otherwise.
   293  func (m *Manager) Kill(pid int64) bool {
   294  	m.mu.Lock()
   295  	entry, ok := m.entries[pid]
   296  	m.mu.Unlock()
   297  
   298  	if ok {
   299  		entry.Kill()
   300  		return true
   301  	}
   302  
   303  	return false
   304  }
   305  
   306  // ListProcesses marshals the process State for the given pids.
   307  // If no pids are specified, all current processes are included.
   308  // The return value can be used for responding to a VixMsgListProcessesExRequest.
   309  func (m *Manager) ListProcesses(pids []int64) []byte {
   310  	w := new(bytes.Buffer)
   311  
   312  	for _, p := range m.List(pids) {
   313  		_, _ = w.WriteString(p.toXML())
   314  	}
   315  
   316  	return w.Bytes()
   317  }
   318  
   319  // List the process State for the given pids.
   320  func (m *Manager) List(pids []int64) []State {
   321  	var list []State
   322  
   323  	m.mu.Lock()
   324  
   325  	if len(pids) == 0 {
   326  		for _, p := range m.entries {
   327  			list = append(list, p.State)
   328  		}
   329  	} else {
   330  		for _, id := range pids {
   331  			p, ok := m.entries[id]
   332  			if !ok {
   333  				continue
   334  			}
   335  
   336  			list = append(list, p.State)
   337  		}
   338  	}
   339  
   340  	m.mu.Unlock()
   341  
   342  	return list
   343  }
   344  
   345  type procFileInfo struct {
   346  	os.FileInfo
   347  }
   348  
   349  // Size returns hgfs.LargePacketMax such that InitiateFileTransferFromGuest can download a /proc/ file from the guest.
   350  // If we were to return the size '0' here, then a 'Content-Length: 0' header is returned by VC/ESX.
   351  func (p procFileInfo) Size() int64 {
   352  	return hgfs.LargePacketMax // Remember, Sully, when I promised to kill you last?  I lied.
   353  }
   354  
   355  // Stat implements hgfs.FileHandler.Stat
   356  func (m *Manager) Stat(u *url.URL) (os.FileInfo, error) {
   357  	name := path.Join("/proc", u.Path)
   358  
   359  	info, err := os.Stat(name)
   360  	if err == nil && info.Size() == 0 {
   361  		// This is a real /proc file
   362  		return &procFileInfo{info}, nil
   363  	}
   364  
   365  	dir, file := path.Split(u.Path)
   366  
   367  	pid, err := strconv.ParseInt(path.Base(dir), 10, 64)
   368  	if err != nil {
   369  		return nil, os.ErrNotExist
   370  	}
   371  
   372  	m.mu.Lock()
   373  	p := m.entries[pid]
   374  	m.mu.Unlock()
   375  
   376  	if p == nil || p.IO == nil {
   377  		return nil, os.ErrNotExist
   378  	}
   379  
   380  	pf := &File{
   381  		name:   name,
   382  		Closer: io.NopCloser(nil), // via hgfs, nop for stdout and stderr
   383  	}
   384  
   385  	var r *bytes.Buffer
   386  
   387  	switch file {
   388  	case "stdin":
   389  		pf.Writer = p.IO.In.Writer
   390  		pf.Closer = p.IO.In.Closer
   391  		return pf, nil
   392  	case "stdout":
   393  		r = p.IO.Out
   394  	case "stderr":
   395  		r = p.IO.Err
   396  	default:
   397  		return nil, os.ErrNotExist
   398  	}
   399  
   400  	select {
   401  	case <-p.ctx.Done():
   402  	case <-time.After(time.Second):
   403  		// The vmx guest RPC calls are queue based, serialized on the vmx side.
   404  		// There are 5 seconds between "ping" RPC calls and after a few misses,
   405  		// the vmx considers tools as not running.  In this case, the vmx would timeout
   406  		// a file transfer after 60 seconds.
   407  		//
   408  		// vix.FileAccessError is converted to a CannotAccessFile fault,
   409  		// so the client can choose to retry the transfer in this case.
   410  		// Would have preferred vix.ObjectIsBusy (EBUSY), but VC/ESX converts that
   411  		// to a general SystemErrorFault with nothing but a localized string message
   412  		// to check against: "<reason>vix error codes = (5, 0).</reason>"
   413  		// Is standard vmware-tools, EACCES is converted to a CannotAccessFile fault.
   414  		return nil, vix.Error(vix.FileAccessError)
   415  	}
   416  
   417  	pf.Reader = r
   418  	pf.size = r.Len()
   419  
   420  	return pf, nil
   421  }
   422  
   423  // Open implements hgfs.FileHandler.Open
   424  func (m *Manager) Open(u *url.URL, mode int32) (hgfs.File, error) {
   425  	info, err := m.Stat(u)
   426  	if err != nil {
   427  		return nil, err
   428  	}
   429  
   430  	pinfo, ok := info.(*File)
   431  
   432  	if !ok {
   433  		return nil, os.ErrNotExist // fall through to default os.Open
   434  	}
   435  
   436  	switch path.Base(u.Path) {
   437  	case "stdin":
   438  		if mode != hgfs.OpenModeWriteOnly {
   439  			return nil, vix.Error(vix.InvalidArg)
   440  		}
   441  	case "stdout", "stderr":
   442  		if mode != hgfs.OpenModeReadOnly {
   443  			return nil, vix.Error(vix.InvalidArg)
   444  		}
   445  	}
   446  
   447  	return pinfo, nil
   448  }
   449  
   450  type processFunc struct {
   451  	wg sync.WaitGroup
   452  
   453  	run func(context.Context, string) error
   454  
   455  	err error
   456  }
   457  
   458  // NewFunc creates a new Process, where the Start function calls the given run function within a goroutine.
   459  // The Wait function waits for the goroutine to finish and returns the error returned by run.
   460  // The run ctx param may be used to return early via the process Manager.Kill method.
   461  // The run args command is that of the VixMsgStartProgramRequest.Arguments field.
   462  func NewFunc(run func(ctx context.Context, args string) error) *Process {
   463  	f := &processFunc{run: run}
   464  
   465  	return &Process{
   466  		Start: f.start,
   467  		Wait:  f.wait,
   468  	}
   469  }
   470  
   471  // FuncIO is the Context key to access optional ProcessIO
   472  var FuncIO = struct {
   473  	key int64
   474  }{vix.CommandMagicWord}
   475  
   476  func (f *processFunc) start(p *Process, r *vix.StartProgramRequest) (int64, error) {
   477  	f.wg.Add(1)
   478  
   479  	var c io.Closer
   480  
   481  	if p.IO != nil {
   482  		pr, pw := io.Pipe()
   483  
   484  		p.IO.In.Reader, p.IO.In.Writer = pr, pw
   485  		c, p.IO.In.Closer = pr, pw
   486  
   487  		p.ctx = context.WithValue(p.ctx, FuncIO, p.IO)
   488  	}
   489  
   490  	go func() {
   491  		f.err = f.run(p.ctx, r.Arguments)
   492  
   493  		if p.IO != nil {
   494  			_ = c.Close()
   495  
   496  			if f.err != nil && p.IO.Err.Len() == 0 {
   497  				p.IO.Err.WriteString(f.err.Error())
   498  			}
   499  		}
   500  
   501  		f.wg.Done()
   502  	}()
   503  
   504  	return 0, nil
   505  }
   506  
   507  func (f *processFunc) wait() error {
   508  	f.wg.Wait()
   509  	return f.err
   510  }
   511  
   512  type processCmd struct {
   513  	cmd *exec.Cmd
   514  }
   515  
   516  // New creates a new Process, where the Start function use exec.CommandContext to create and start the process.
   517  // The Wait function waits for the process to finish and returns the error returned by exec.Cmd.Wait().
   518  // Prior to Wait returning, the exec.Cmd.Wait() error is used to set the Process.ExitCode, if error is of type exec.ExitError.
   519  // The ctx param may be used to kill the process via the process Manager.Kill method.
   520  // The VixMsgStartProgramRequest param fields are mapped to the exec.Cmd counterpart fields.
   521  // Processes are started within a sub-shell, allowing for i/o redirection, just as with the C version of vmware-tools.
   522  func New() *Process {
   523  	c := new(processCmd)
   524  
   525  	return &Process{
   526  		Start: c.start,
   527  		Wait:  c.wait,
   528  	}
   529  }
   530  
   531  func (c *processCmd) start(p *Process, r *vix.StartProgramRequest) (int64, error) {
   532  	name, err := exec.LookPath(r.ProgramPath)
   533  	if err != nil {
   534  		return -1, err
   535  	}
   536  	// #nosec: Subprocess launching with variable
   537  	// Note that processCmd is currently used only for testing.
   538  	c.cmd = exec.CommandContext(p.ctx, shell, "-c", fmt.Sprintf("%s %s", name, r.Arguments))
   539  	c.cmd.Dir = r.WorkingDir
   540  	c.cmd.Env = r.EnvVars
   541  
   542  	if p.IO != nil {
   543  		in, perr := c.cmd.StdinPipe()
   544  		if perr != nil {
   545  			return -1, perr
   546  		}
   547  
   548  		p.IO.In.Writer = in
   549  		p.IO.In.Closer = in
   550  
   551  		// Note we currently use a Buffer in addition to the os.Pipe so that:
   552  		// - Stat() can provide a size
   553  		// - FileTransferFromGuest won't block
   554  		// - Can't use the exec.Cmd.Std{out,err}Pipe methods since Wait() closes the pipes.
   555  		//   We could use os.Pipe directly, but toolbox needs to take care of closing both ends,
   556  		//   but also need to prevent FileTransferFromGuest from blocking.
   557  		c.cmd.Stdout = p.IO.Out
   558  		c.cmd.Stderr = p.IO.Err
   559  	}
   560  
   561  	err = c.cmd.Start()
   562  	if err != nil {
   563  		return -1, err
   564  	}
   565  
   566  	return int64(c.cmd.Process.Pid), nil
   567  }
   568  
   569  func (c *processCmd) wait() error {
   570  	err := c.cmd.Wait()
   571  	if err != nil {
   572  		xerr := &Error{
   573  			Err:      err,
   574  			ExitCode: 1,
   575  		}
   576  
   577  		if x, ok := err.(*exec.ExitError); ok {
   578  			if status, ok := x.Sys().(syscall.WaitStatus); ok {
   579  				xerr.ExitCode = int32(status.ExitStatus())
   580  			}
   581  		}
   582  
   583  		return xerr
   584  	}
   585  
   586  	return nil
   587  }
   588  
   589  // NewRoundTrip starts a Go function to implement a toolbox backed http.RoundTripper
   590  func NewRoundTrip() *Process {
   591  	return NewFunc(func(ctx context.Context, host string) error {
   592  		p, _ := ctx.Value(FuncIO).(*IO)
   593  
   594  		closers := []io.Closer{p.In.Closer}
   595  
   596  		defer func() {
   597  			for _, c := range closers {
   598  				_ = c.Close()
   599  			}
   600  		}()
   601  
   602  		c, err := new(net.Dialer).DialContext(ctx, "tcp", host)
   603  		if err != nil {
   604  			return err
   605  		}
   606  
   607  		closers = append(closers, c)
   608  
   609  		go func() {
   610  			<-ctx.Done()
   611  			if ctx.Err() == context.DeadlineExceeded {
   612  				_ = c.Close()
   613  			}
   614  		}()
   615  
   616  		_, err = io.Copy(c, p.In.Reader)
   617  		if err != nil {
   618  			return err
   619  		}
   620  
   621  		_, err = io.Copy(p.Out, c)
   622  		if err != nil {
   623  			return err
   624  		}
   625  
   626  		return nil
   627  	}).WithIO()
   628  }