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

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