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 }