1package hcs 2 3import ( 4 "context" 5 "encoding/json" 6 "io" 7 "sync" 8 "syscall" 9 "time" 10 11 "github.com/Microsoft/hcsshim/internal/log" 12 "github.com/Microsoft/hcsshim/internal/oc" 13 "github.com/Microsoft/hcsshim/internal/vmcompute" 14 "go.opencensus.io/trace" 15) 16 17// ContainerError is an error encountered in HCS 18type Process struct { 19 handleLock sync.RWMutex 20 handle vmcompute.HcsProcess 21 processID int 22 system *System 23 hasCachedStdio bool 24 stdioLock sync.Mutex 25 stdin io.WriteCloser 26 stdout io.ReadCloser 27 stderr io.ReadCloser 28 callbackNumber uintptr 29 30 closedWaitOnce sync.Once 31 waitBlock chan struct{} 32 exitCode int 33 waitError error 34} 35 36func newProcess(process vmcompute.HcsProcess, processID int, computeSystem *System) *Process { 37 return &Process{ 38 handle: process, 39 processID: processID, 40 system: computeSystem, 41 waitBlock: make(chan struct{}), 42 } 43} 44 45type processModifyRequest struct { 46 Operation string 47 ConsoleSize *consoleSize `json:",omitempty"` 48 CloseHandle *closeHandle `json:",omitempty"` 49} 50 51type consoleSize struct { 52 Height uint16 53 Width uint16 54} 55 56type closeHandle struct { 57 Handle string 58} 59 60type processStatus struct { 61 ProcessID uint32 62 Exited bool 63 ExitCode uint32 64 LastWaitResult int32 65} 66 67const stdIn string = "StdIn" 68 69const ( 70 modifyConsoleSize string = "ConsoleSize" 71 modifyCloseHandle string = "CloseHandle" 72) 73 74// Pid returns the process ID of the process within the container. 75func (process *Process) Pid() int { 76 return process.processID 77} 78 79// SystemID returns the ID of the process's compute system. 80func (process *Process) SystemID() string { 81 return process.system.ID() 82} 83 84func (process *Process) processSignalResult(ctx context.Context, err error) (bool, error) { 85 switch err { 86 case nil: 87 return true, nil 88 case ErrVmcomputeOperationInvalidState, ErrComputeSystemDoesNotExist, ErrElementNotFound: 89 select { 90 case <-process.waitBlock: 91 // The process exit notification has already arrived. 92 default: 93 // The process should be gone, but we have not received the notification. 94 // After a second, force unblock the process wait to work around a possible 95 // deadlock in the HCS. 96 go func() { 97 time.Sleep(time.Second) 98 process.closedWaitOnce.Do(func() { 99 log.G(ctx).WithError(err).Warn("force unblocking process waits") 100 process.exitCode = -1 101 process.waitError = err 102 close(process.waitBlock) 103 }) 104 }() 105 } 106 return false, nil 107 default: 108 return false, err 109 } 110} 111 112// Signal signals the process with `options`. 113// 114// For LCOW `guestrequest.SignalProcessOptionsLCOW`. 115// 116// For WCOW `guestrequest.SignalProcessOptionsWCOW`. 117func (process *Process) Signal(ctx context.Context, options interface{}) (bool, error) { 118 process.handleLock.RLock() 119 defer process.handleLock.RUnlock() 120 121 operation := "hcs::Process::Signal" 122 123 if process.handle == 0 { 124 return false, makeProcessError(process, operation, ErrAlreadyClosed, nil) 125 } 126 127 optionsb, err := json.Marshal(options) 128 if err != nil { 129 return false, err 130 } 131 132 resultJSON, err := vmcompute.HcsSignalProcess(ctx, process.handle, string(optionsb)) 133 events := processHcsResult(ctx, resultJSON) 134 delivered, err := process.processSignalResult(ctx, err) 135 if err != nil { 136 err = makeProcessError(process, operation, err, events) 137 } 138 return delivered, err 139} 140 141// Kill signals the process to terminate but does not wait for it to finish terminating. 142func (process *Process) Kill(ctx context.Context) (bool, error) { 143 process.handleLock.RLock() 144 defer process.handleLock.RUnlock() 145 146 operation := "hcs::Process::Kill" 147 148 if process.handle == 0 { 149 return false, makeProcessError(process, operation, ErrAlreadyClosed, nil) 150 } 151 152 resultJSON, err := vmcompute.HcsTerminateProcess(ctx, process.handle) 153 events := processHcsResult(ctx, resultJSON) 154 delivered, err := process.processSignalResult(ctx, err) 155 if err != nil { 156 err = makeProcessError(process, operation, err, events) 157 } 158 return delivered, err 159} 160 161// waitBackground waits for the process exit notification. Once received sets 162// `process.waitError` (if any) and unblocks all `Wait` calls. 163// 164// This MUST be called exactly once per `process.handle` but `Wait` is safe to 165// call multiple times. 166func (process *Process) waitBackground() { 167 operation := "hcs::Process::waitBackground" 168 ctx, span := trace.StartSpan(context.Background(), operation) 169 defer span.End() 170 span.AddAttributes( 171 trace.StringAttribute("cid", process.SystemID()), 172 trace.Int64Attribute("pid", int64(process.processID))) 173 174 var ( 175 err error 176 exitCode = -1 177 propertiesJSON string 178 resultJSON string 179 ) 180 181 err = waitForNotification(ctx, process.callbackNumber, hcsNotificationProcessExited, nil) 182 if err != nil { 183 err = makeProcessError(process, operation, err, nil) 184 log.G(ctx).WithError(err).Error("failed wait") 185 } else { 186 process.handleLock.RLock() 187 defer process.handleLock.RUnlock() 188 189 // Make sure we didnt race with Close() here 190 if process.handle != 0 { 191 propertiesJSON, resultJSON, err = vmcompute.HcsGetProcessProperties(ctx, process.handle) 192 events := processHcsResult(ctx, resultJSON) 193 if err != nil { 194 err = makeProcessError(process, operation, err, events) //nolint:ineffassign 195 } else { 196 properties := &processStatus{} 197 err = json.Unmarshal([]byte(propertiesJSON), properties) 198 if err != nil { 199 err = makeProcessError(process, operation, err, nil) //nolint:ineffassign 200 } else { 201 if properties.LastWaitResult != 0 { 202 log.G(ctx).WithField("wait-result", properties.LastWaitResult).Warning("non-zero last wait result") 203 } else { 204 exitCode = int(properties.ExitCode) 205 } 206 } 207 } 208 } 209 } 210 log.G(ctx).WithField("exitCode", exitCode).Debug("process exited") 211 212 process.closedWaitOnce.Do(func() { 213 process.exitCode = exitCode 214 process.waitError = err 215 close(process.waitBlock) 216 }) 217 oc.SetSpanStatus(span, err) 218} 219 220// Wait waits for the process to exit. If the process has already exited returns 221// the pervious error (if any). 222func (process *Process) Wait() error { 223 <-process.waitBlock 224 return process.waitError 225} 226 227// ResizeConsole resizes the console of the process. 228func (process *Process) ResizeConsole(ctx context.Context, width, height uint16) error { 229 process.handleLock.RLock() 230 defer process.handleLock.RUnlock() 231 232 operation := "hcs::Process::ResizeConsole" 233 234 if process.handle == 0 { 235 return makeProcessError(process, operation, ErrAlreadyClosed, nil) 236 } 237 238 modifyRequest := processModifyRequest{ 239 Operation: modifyConsoleSize, 240 ConsoleSize: &consoleSize{ 241 Height: height, 242 Width: width, 243 }, 244 } 245 246 modifyRequestb, err := json.Marshal(modifyRequest) 247 if err != nil { 248 return err 249 } 250 251 resultJSON, err := vmcompute.HcsModifyProcess(ctx, process.handle, string(modifyRequestb)) 252 events := processHcsResult(ctx, resultJSON) 253 if err != nil { 254 return makeProcessError(process, operation, err, events) 255 } 256 257 return nil 258} 259 260// ExitCode returns the exit code of the process. The process must have 261// already terminated. 262func (process *Process) ExitCode() (int, error) { 263 select { 264 case <-process.waitBlock: 265 if process.waitError != nil { 266 return -1, process.waitError 267 } 268 return process.exitCode, nil 269 default: 270 return -1, makeProcessError(process, "hcs::Process::ExitCode", ErrInvalidProcessState, nil) 271 } 272} 273 274// StdioLegacy returns the stdin, stdout, and stderr pipes, respectively. Closing 275// these pipes does not close the underlying pipes. Once returned, these pipes 276// are the responsibility of the caller to close. 277func (process *Process) StdioLegacy() (_ io.WriteCloser, _ io.ReadCloser, _ io.ReadCloser, err error) { 278 operation := "hcs::Process::StdioLegacy" 279 ctx, span := trace.StartSpan(context.Background(), operation) 280 defer span.End() 281 defer func() { oc.SetSpanStatus(span, err) }() 282 span.AddAttributes( 283 trace.StringAttribute("cid", process.SystemID()), 284 trace.Int64Attribute("pid", int64(process.processID))) 285 286 process.handleLock.RLock() 287 defer process.handleLock.RUnlock() 288 289 if process.handle == 0 { 290 return nil, nil, nil, makeProcessError(process, operation, ErrAlreadyClosed, nil) 291 } 292 293 process.stdioLock.Lock() 294 defer process.stdioLock.Unlock() 295 if process.hasCachedStdio { 296 stdin, stdout, stderr := process.stdin, process.stdout, process.stderr 297 process.stdin, process.stdout, process.stderr = nil, nil, nil 298 process.hasCachedStdio = false 299 return stdin, stdout, stderr, nil 300 } 301 302 processInfo, resultJSON, err := vmcompute.HcsGetProcessInfo(ctx, process.handle) 303 events := processHcsResult(ctx, resultJSON) 304 if err != nil { 305 return nil, nil, nil, makeProcessError(process, operation, err, events) 306 } 307 308 pipes, err := makeOpenFiles([]syscall.Handle{processInfo.StdInput, processInfo.StdOutput, processInfo.StdError}) 309 if err != nil { 310 return nil, nil, nil, makeProcessError(process, operation, err, nil) 311 } 312 313 return pipes[0], pipes[1], pipes[2], nil 314} 315 316// Stdio returns the stdin, stdout, and stderr pipes, respectively. 317// To close them, close the process handle. 318func (process *Process) Stdio() (stdin io.Writer, stdout, stderr io.Reader) { 319 process.stdioLock.Lock() 320 defer process.stdioLock.Unlock() 321 return process.stdin, process.stdout, process.stderr 322} 323 324// CloseStdin closes the write side of the stdin pipe so that the process is 325// notified on the read side that there is no more data in stdin. 326func (process *Process) CloseStdin(ctx context.Context) error { 327 process.handleLock.RLock() 328 defer process.handleLock.RUnlock() 329 330 operation := "hcs::Process::CloseStdin" 331 332 if process.handle == 0 { 333 return makeProcessError(process, operation, ErrAlreadyClosed, nil) 334 } 335 336 modifyRequest := processModifyRequest{ 337 Operation: modifyCloseHandle, 338 CloseHandle: &closeHandle{ 339 Handle: stdIn, 340 }, 341 } 342 343 modifyRequestb, err := json.Marshal(modifyRequest) 344 if err != nil { 345 return err 346 } 347 348 resultJSON, err := vmcompute.HcsModifyProcess(ctx, process.handle, string(modifyRequestb)) 349 events := processHcsResult(ctx, resultJSON) 350 if err != nil { 351 return makeProcessError(process, operation, err, events) 352 } 353 354 process.stdioLock.Lock() 355 if process.stdin != nil { 356 process.stdin.Close() 357 process.stdin = nil 358 } 359 process.stdioLock.Unlock() 360 361 return nil 362} 363 364func (process *Process) CloseStdout(ctx context.Context) (err error) { 365 ctx, span := trace.StartSpan(ctx, "hcs::Process::CloseStdout") //nolint:ineffassign,staticcheck 366 defer span.End() 367 defer func() { oc.SetSpanStatus(span, err) }() 368 span.AddAttributes( 369 trace.StringAttribute("cid", process.SystemID()), 370 trace.Int64Attribute("pid", int64(process.processID))) 371 372 process.handleLock.Lock() 373 defer process.handleLock.Unlock() 374 375 if process.handle == 0 { 376 return nil 377 } 378 379 process.stdioLock.Lock() 380 defer process.stdioLock.Unlock() 381 if process.stdout != nil { 382 process.stdout.Close() 383 process.stdout = nil 384 } 385 return nil 386} 387 388func (process *Process) CloseStderr(ctx context.Context) (err error) { 389 ctx, span := trace.StartSpan(ctx, "hcs::Process::CloseStderr") //nolint:ineffassign,staticcheck 390 defer span.End() 391 defer func() { oc.SetSpanStatus(span, err) }() 392 span.AddAttributes( 393 trace.StringAttribute("cid", process.SystemID()), 394 trace.Int64Attribute("pid", int64(process.processID))) 395 396 process.handleLock.Lock() 397 defer process.handleLock.Unlock() 398 399 if process.handle == 0 { 400 return nil 401 } 402 403 process.stdioLock.Lock() 404 defer process.stdioLock.Unlock() 405 if process.stderr != nil { 406 process.stderr.Close() 407 process.stderr = nil 408 409 } 410 return nil 411} 412 413// Close cleans up any state associated with the process but does not kill 414// or wait on it. 415func (process *Process) Close() (err error) { 416 operation := "hcs::Process::Close" 417 ctx, span := trace.StartSpan(context.Background(), operation) 418 defer span.End() 419 defer func() { oc.SetSpanStatus(span, err) }() 420 span.AddAttributes( 421 trace.StringAttribute("cid", process.SystemID()), 422 trace.Int64Attribute("pid", int64(process.processID))) 423 424 process.handleLock.Lock() 425 defer process.handleLock.Unlock() 426 427 // Don't double free this 428 if process.handle == 0 { 429 return nil 430 } 431 432 process.stdioLock.Lock() 433 if process.stdin != nil { 434 process.stdin.Close() 435 process.stdin = nil 436 } 437 if process.stdout != nil { 438 process.stdout.Close() 439 process.stdout = nil 440 } 441 if process.stderr != nil { 442 process.stderr.Close() 443 process.stderr = nil 444 } 445 process.stdioLock.Unlock() 446 447 if err = process.unregisterCallback(ctx); err != nil { 448 return makeProcessError(process, operation, err, nil) 449 } 450 451 if err = vmcompute.HcsCloseProcess(ctx, process.handle); err != nil { 452 return makeProcessError(process, operation, err, nil) 453 } 454 455 process.handle = 0 456 process.closedWaitOnce.Do(func() { 457 process.exitCode = -1 458 process.waitError = ErrAlreadyClosed 459 close(process.waitBlock) 460 }) 461 462 return nil 463} 464 465func (process *Process) registerCallback(ctx context.Context) error { 466 callbackContext := ¬ificationWatcherContext{ 467 channels: newProcessChannels(), 468 systemID: process.SystemID(), 469 processID: process.processID, 470 } 471 472 callbackMapLock.Lock() 473 callbackNumber := nextCallback 474 nextCallback++ 475 callbackMap[callbackNumber] = callbackContext 476 callbackMapLock.Unlock() 477 478 callbackHandle, err := vmcompute.HcsRegisterProcessCallback(ctx, process.handle, notificationWatcherCallback, callbackNumber) 479 if err != nil { 480 return err 481 } 482 callbackContext.handle = callbackHandle 483 process.callbackNumber = callbackNumber 484 485 return nil 486} 487 488func (process *Process) unregisterCallback(ctx context.Context) error { 489 callbackNumber := process.callbackNumber 490 491 callbackMapLock.RLock() 492 callbackContext := callbackMap[callbackNumber] 493 callbackMapLock.RUnlock() 494 495 if callbackContext == nil { 496 return nil 497 } 498 499 handle := callbackContext.handle 500 501 if handle == 0 { 502 return nil 503 } 504 505 // vmcompute.HcsUnregisterProcessCallback has its own synchronization to 506 // wait for all callbacks to complete. We must NOT hold the callbackMapLock. 507 err := vmcompute.HcsUnregisterProcessCallback(ctx, handle) 508 if err != nil { 509 return err 510 } 511 512 closeChannels(callbackContext.channels) 513 514 callbackMapLock.Lock() 515 delete(callbackMap, callbackNumber) 516 callbackMapLock.Unlock() 517 518 handle = 0 //nolint:ineffassign 519 520 return nil 521} 522