| package hcs |
| |
| import ( |
| "encoding/json" |
| "io" |
| "sync" |
| "syscall" |
| "time" |
| |
| "github.com/Microsoft/hcsshim/internal/interop" |
| "github.com/Microsoft/hcsshim/internal/logfields" |
| "github.com/sirupsen/logrus" |
| ) |
| |
| // ContainerError is an error encountered in HCS |
| type Process struct { |
| handleLock sync.RWMutex |
| handle hcsProcess |
| processID int |
| system *System |
| cachedPipes *cachedPipes |
| callbackNumber uintptr |
| |
| logctx logrus.Fields |
| |
| closedWaitOnce sync.Once |
| waitBlock chan struct{} |
| waitError error |
| } |
| |
| func newProcess(process hcsProcess, processID int, computeSystem *System) *Process { |
| return &Process{ |
| handle: process, |
| processID: processID, |
| system: computeSystem, |
| logctx: logrus.Fields{ |
| logfields.ContainerID: computeSystem.ID(), |
| logfields.ProcessID: processID, |
| }, |
| waitBlock: make(chan struct{}), |
| } |
| } |
| |
| type cachedPipes struct { |
| stdIn syscall.Handle |
| stdOut syscall.Handle |
| stdErr syscall.Handle |
| } |
| |
| type processModifyRequest struct { |
| Operation string |
| ConsoleSize *consoleSize `json:",omitempty"` |
| CloseHandle *closeHandle `json:",omitempty"` |
| } |
| |
| type consoleSize struct { |
| Height uint16 |
| Width uint16 |
| } |
| |
| type closeHandle struct { |
| Handle string |
| } |
| |
| type ProcessStatus struct { |
| ProcessID uint32 |
| Exited bool |
| ExitCode uint32 |
| LastWaitResult int32 |
| } |
| |
| const ( |
| stdIn string = "StdIn" |
| stdOut string = "StdOut" |
| stdErr string = "StdErr" |
| ) |
| |
| const ( |
| modifyConsoleSize string = "ConsoleSize" |
| modifyCloseHandle string = "CloseHandle" |
| ) |
| |
| // Pid returns the process ID of the process within the container. |
| func (process *Process) Pid() int { |
| return process.processID |
| } |
| |
| // SystemID returns the ID of the process's compute system. |
| func (process *Process) SystemID() string { |
| return process.system.ID() |
| } |
| |
| func (process *Process) logOperationBegin(operation string) { |
| logOperationBegin( |
| process.logctx, |
| operation+" - Begin Operation") |
| } |
| |
| func (process *Process) logOperationEnd(operation string, err error) { |
| var result string |
| if err == nil { |
| result = "Success" |
| } else { |
| result = "Error" |
| } |
| |
| logOperationEnd( |
| process.logctx, |
| operation+" - End Operation - "+result, |
| err) |
| } |
| |
| // Signal signals the process with `options`. |
| // |
| // For LCOW `guestrequest.SignalProcessOptionsLCOW`. |
| // |
| // For WCOW `guestrequest.SignalProcessOptionsWCOW`. |
| func (process *Process) Signal(options interface{}) (err error) { |
| process.handleLock.RLock() |
| defer process.handleLock.RUnlock() |
| |
| operation := "hcsshim::Process::Signal" |
| process.logOperationBegin(operation) |
| defer func() { process.logOperationEnd(operation, err) }() |
| |
| if process.handle == 0 { |
| return makeProcessError(process, operation, ErrAlreadyClosed, nil) |
| } |
| |
| optionsb, err := json.Marshal(options) |
| if err != nil { |
| return err |
| } |
| |
| optionsStr := string(optionsb) |
| |
| var resultp *uint16 |
| syscallWatcher(process.logctx, func() { |
| err = hcsSignalProcess(process.handle, optionsStr, &resultp) |
| }) |
| events := processHcsResult(resultp) |
| if err != nil { |
| return makeProcessError(process, operation, err, events) |
| } |
| |
| return nil |
| } |
| |
| // Kill signals the process to terminate but does not wait for it to finish terminating. |
| func (process *Process) Kill() (err error) { |
| process.handleLock.RLock() |
| defer process.handleLock.RUnlock() |
| |
| operation := "hcsshim::Process::Kill" |
| process.logOperationBegin(operation) |
| defer func() { process.logOperationEnd(operation, err) }() |
| |
| if process.handle == 0 { |
| return makeProcessError(process, operation, ErrAlreadyClosed, nil) |
| } |
| |
| var resultp *uint16 |
| syscallWatcher(process.logctx, func() { |
| err = hcsTerminateProcess(process.handle, &resultp) |
| }) |
| events := processHcsResult(resultp) |
| if err != nil { |
| return makeProcessError(process, operation, err, events) |
| } |
| |
| return nil |
| } |
| |
| // waitBackground waits for the process exit notification. Once received sets |
| // `process.waitError` (if any) and unblocks all `Wait` and `WaitTimeout` calls. |
| // |
| // This MUST be called exactly once per `process.handle` but `Wait` and |
| // `WaitTimeout` are safe to call multiple times. |
| func (process *Process) waitBackground() { |
| process.waitError = waitForNotification(process.callbackNumber, hcsNotificationProcessExited, nil) |
| process.closedWaitOnce.Do(func() { |
| close(process.waitBlock) |
| }) |
| } |
| |
| // Wait waits for the process to exit. If the process has already exited returns |
| // the pervious error (if any). |
| func (process *Process) Wait() (err error) { |
| operation := "hcsshim::Process::Wait" |
| process.logOperationBegin(operation) |
| defer func() { process.logOperationEnd(operation, err) }() |
| |
| <-process.waitBlock |
| if process.waitError != nil { |
| return makeProcessError(process, operation, process.waitError, nil) |
| } |
| return nil |
| } |
| |
| // WaitTimeout waits for the process to exit or the duration to elapse. If the |
| // process has already exited returns the pervious error (if any). If a timeout |
| // occurs returns `ErrTimeout`. |
| func (process *Process) WaitTimeout(timeout time.Duration) (err error) { |
| operation := "hcssshim::Process::WaitTimeout" |
| process.logOperationBegin(operation) |
| defer func() { process.logOperationEnd(operation, err) }() |
| |
| select { |
| case <-process.waitBlock: |
| if process.waitError != nil { |
| return makeProcessError(process, operation, process.waitError, nil) |
| } |
| return nil |
| case <-time.After(timeout): |
| return makeProcessError(process, operation, ErrTimeout, nil) |
| } |
| } |
| |
| // ResizeConsole resizes the console of the process. |
| func (process *Process) ResizeConsole(width, height uint16) (err error) { |
| process.handleLock.RLock() |
| defer process.handleLock.RUnlock() |
| |
| operation := "hcsshim::Process::ResizeConsole" |
| process.logOperationBegin(operation) |
| defer func() { process.logOperationEnd(operation, err) }() |
| |
| if process.handle == 0 { |
| return makeProcessError(process, operation, ErrAlreadyClosed, nil) |
| } |
| |
| modifyRequest := processModifyRequest{ |
| Operation: modifyConsoleSize, |
| ConsoleSize: &consoleSize{ |
| Height: height, |
| Width: width, |
| }, |
| } |
| |
| modifyRequestb, err := json.Marshal(modifyRequest) |
| if err != nil { |
| return err |
| } |
| |
| modifyRequestStr := string(modifyRequestb) |
| |
| var resultp *uint16 |
| err = hcsModifyProcess(process.handle, modifyRequestStr, &resultp) |
| events := processHcsResult(resultp) |
| if err != nil { |
| return makeProcessError(process, operation, err, events) |
| } |
| |
| return nil |
| } |
| |
| func (process *Process) Properties() (_ *ProcessStatus, err error) { |
| process.handleLock.RLock() |
| defer process.handleLock.RUnlock() |
| |
| operation := "hcsshim::Process::Properties" |
| process.logOperationBegin(operation) |
| defer func() { process.logOperationEnd(operation, err) }() |
| |
| if process.handle == 0 { |
| return nil, makeProcessError(process, operation, ErrAlreadyClosed, nil) |
| } |
| |
| var ( |
| resultp *uint16 |
| propertiesp *uint16 |
| ) |
| syscallWatcher(process.logctx, func() { |
| err = hcsGetProcessProperties(process.handle, &propertiesp, &resultp) |
| }) |
| events := processHcsResult(resultp) |
| if err != nil { |
| return nil, makeProcessError(process, operation, err, events) |
| } |
| |
| if propertiesp == nil { |
| return nil, ErrUnexpectedValue |
| } |
| propertiesRaw := interop.ConvertAndFreeCoTaskMemBytes(propertiesp) |
| |
| properties := &ProcessStatus{} |
| if err := json.Unmarshal(propertiesRaw, properties); err != nil { |
| return nil, makeProcessError(process, operation, err, nil) |
| } |
| |
| return properties, nil |
| } |
| |
| // ExitCode returns the exit code of the process. The process must have |
| // already terminated. |
| func (process *Process) ExitCode() (_ int, err error) { |
| operation := "hcsshim::Process::ExitCode" |
| process.logOperationBegin(operation) |
| defer func() { process.logOperationEnd(operation, err) }() |
| |
| properties, err := process.Properties() |
| if err != nil { |
| return -1, makeProcessError(process, operation, err, nil) |
| } |
| |
| if properties.Exited == false { |
| return -1, makeProcessError(process, operation, ErrInvalidProcessState, nil) |
| } |
| |
| if properties.LastWaitResult != 0 { |
| logrus.WithFields(logrus.Fields{ |
| logfields.ContainerID: process.SystemID(), |
| logfields.ProcessID: process.processID, |
| "wait-result": properties.LastWaitResult, |
| }).Warn("hcsshim::Process::ExitCode - Non-zero last wait result") |
| return -1, nil |
| } |
| |
| return int(properties.ExitCode), nil |
| } |
| |
| // Stdio returns the stdin, stdout, and stderr pipes, respectively. Closing |
| // these pipes does not close the underlying pipes; it should be possible to |
| // call this multiple times to get multiple interfaces. |
| func (process *Process) Stdio() (_ io.WriteCloser, _ io.ReadCloser, _ io.ReadCloser, err error) { |
| process.handleLock.RLock() |
| defer process.handleLock.RUnlock() |
| |
| operation := "hcsshim::Process::Stdio" |
| process.logOperationBegin(operation) |
| defer func() { process.logOperationEnd(operation, err) }() |
| |
| if process.handle == 0 { |
| return nil, nil, nil, makeProcessError(process, operation, ErrAlreadyClosed, nil) |
| } |
| |
| var stdIn, stdOut, stdErr syscall.Handle |
| |
| if process.cachedPipes == nil { |
| var ( |
| processInfo hcsProcessInformation |
| resultp *uint16 |
| ) |
| err = hcsGetProcessInfo(process.handle, &processInfo, &resultp) |
| events := processHcsResult(resultp) |
| if err != nil { |
| return nil, nil, nil, makeProcessError(process, operation, err, events) |
| } |
| |
| stdIn, stdOut, stdErr = processInfo.StdInput, processInfo.StdOutput, processInfo.StdError |
| } else { |
| // Use cached pipes |
| stdIn, stdOut, stdErr = process.cachedPipes.stdIn, process.cachedPipes.stdOut, process.cachedPipes.stdErr |
| |
| // Invalidate the cache |
| process.cachedPipes = nil |
| } |
| |
| pipes, err := makeOpenFiles([]syscall.Handle{stdIn, stdOut, stdErr}) |
| if err != nil { |
| return nil, nil, nil, makeProcessError(process, operation, err, nil) |
| } |
| |
| return pipes[0], pipes[1], pipes[2], nil |
| } |
| |
| // CloseStdin closes the write side of the stdin pipe so that the process is |
| // notified on the read side that there is no more data in stdin. |
| func (process *Process) CloseStdin() (err error) { |
| process.handleLock.RLock() |
| defer process.handleLock.RUnlock() |
| |
| operation := "hcsshim::Process::CloseStdin" |
| process.logOperationBegin(operation) |
| defer func() { process.logOperationEnd(operation, err) }() |
| |
| if process.handle == 0 { |
| return makeProcessError(process, operation, ErrAlreadyClosed, nil) |
| } |
| |
| modifyRequest := processModifyRequest{ |
| Operation: modifyCloseHandle, |
| CloseHandle: &closeHandle{ |
| Handle: stdIn, |
| }, |
| } |
| |
| modifyRequestb, err := json.Marshal(modifyRequest) |
| if err != nil { |
| return err |
| } |
| |
| modifyRequestStr := string(modifyRequestb) |
| |
| var resultp *uint16 |
| err = hcsModifyProcess(process.handle, modifyRequestStr, &resultp) |
| events := processHcsResult(resultp) |
| if err != nil { |
| return makeProcessError(process, operation, err, events) |
| } |
| |
| return nil |
| } |
| |
| // Close cleans up any state associated with the process but does not kill |
| // or wait on it. |
| func (process *Process) Close() (err error) { |
| process.handleLock.Lock() |
| defer process.handleLock.Unlock() |
| |
| operation := "hcsshim::Process::Close" |
| process.logOperationBegin(operation) |
| defer func() { process.logOperationEnd(operation, err) }() |
| |
| // Don't double free this |
| if process.handle == 0 { |
| return nil |
| } |
| |
| if err = process.unregisterCallback(); err != nil { |
| return makeProcessError(process, operation, err, nil) |
| } |
| |
| if err = hcsCloseProcess(process.handle); err != nil { |
| return makeProcessError(process, operation, err, nil) |
| } |
| |
| process.handle = 0 |
| process.closedWaitOnce.Do(func() { |
| close(process.waitBlock) |
| }) |
| |
| return nil |
| } |
| |
| func (process *Process) registerCallback() error { |
| context := ¬ifcationWatcherContext{ |
| channels: newProcessChannels(), |
| systemID: process.SystemID(), |
| processID: process.processID, |
| } |
| |
| callbackMapLock.Lock() |
| callbackNumber := nextCallback |
| nextCallback++ |
| callbackMap[callbackNumber] = context |
| callbackMapLock.Unlock() |
| |
| var callbackHandle hcsCallback |
| err := hcsRegisterProcessCallback(process.handle, notificationWatcherCallback, callbackNumber, &callbackHandle) |
| if err != nil { |
| return err |
| } |
| context.handle = callbackHandle |
| process.callbackNumber = callbackNumber |
| |
| return nil |
| } |
| |
| func (process *Process) unregisterCallback() error { |
| callbackNumber := process.callbackNumber |
| |
| callbackMapLock.RLock() |
| context := callbackMap[callbackNumber] |
| callbackMapLock.RUnlock() |
| |
| if context == nil { |
| return nil |
| } |
| |
| handle := context.handle |
| |
| if handle == 0 { |
| return nil |
| } |
| |
| // hcsUnregisterProcessCallback has its own syncronization |
| // to wait for all callbacks to complete. We must NOT hold the callbackMapLock. |
| err := hcsUnregisterProcessCallback(handle) |
| if err != nil { |
| return err |
| } |
| |
| closeChannels(context.channels) |
| |
| callbackMapLock.Lock() |
| delete(callbackMap, callbackNumber) |
| callbackMapLock.Unlock() |
| |
| handle = 0 |
| |
| return nil |
| } |