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