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 }