123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611 |
- //go:build windows
- package hcs
- import (
- "context"
- "encoding/json"
- "errors"
- "io"
- "os"
- "sync"
- "syscall"
- "time"
- "github.com/Microsoft/hcsshim/internal/cow"
- "github.com/Microsoft/hcsshim/internal/log"
- "github.com/Microsoft/hcsshim/internal/oc"
- "github.com/Microsoft/hcsshim/internal/vmcompute"
- "go.opencensus.io/trace"
- )
- // ContainerError is an error encountered in HCS
- type Process struct {
- handleLock sync.RWMutex
- handle vmcompute.HcsProcess
- processID int
- system *System
- hasCachedStdio bool
- stdioLock sync.Mutex
- stdin io.WriteCloser
- stdout io.ReadCloser
- stderr io.ReadCloser
- callbackNumber uintptr
- killSignalDelivered bool
- closedWaitOnce sync.Once
- waitBlock chan struct{}
- exitCode int
- waitError error
- }
- var _ cow.Process = &Process{}
- func newProcess(process vmcompute.HcsProcess, processID int, computeSystem *System) *Process {
- return &Process{
- handle: process,
- processID: processID,
- system: computeSystem,
- waitBlock: make(chan struct{}),
- }
- }
- 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"
- 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) processSignalResult(ctx context.Context, err error) (bool, error) {
- switch err {
- case nil:
- return true, nil
- case ErrVmcomputeOperationInvalidState, ErrComputeSystemDoesNotExist, ErrElementNotFound:
- if !process.stopped() {
- // The process should be gone, but we have not received the notification.
- // After a second, force unblock the process wait to work around a possible
- // deadlock in the HCS.
- go func() {
- time.Sleep(time.Second)
- process.closedWaitOnce.Do(func() {
- log.G(ctx).WithError(err).Warn("force unblocking process waits")
- process.exitCode = -1
- process.waitError = err
- close(process.waitBlock)
- })
- }()
- }
- return false, nil
- default:
- return false, err
- }
- }
- // Signal signals the process with `options`.
- //
- // For LCOW `guestresource.SignalProcessOptionsLCOW`.
- //
- // For WCOW `guestresource.SignalProcessOptionsWCOW`.
- func (process *Process) Signal(ctx context.Context, options interface{}) (bool, error) {
- process.handleLock.RLock()
- defer process.handleLock.RUnlock()
- operation := "hcs::Process::Signal"
- if process.handle == 0 {
- return false, makeProcessError(process, operation, ErrAlreadyClosed, nil)
- }
- optionsb, err := json.Marshal(options)
- if err != nil {
- return false, err
- }
- resultJSON, err := vmcompute.HcsSignalProcess(ctx, process.handle, string(optionsb))
- events := processHcsResult(ctx, resultJSON)
- delivered, err := process.processSignalResult(ctx, err)
- if err != nil {
- err = makeProcessError(process, operation, err, events)
- }
- return delivered, err
- }
- // Kill signals the process to terminate but does not wait for it to finish terminating.
- func (process *Process) Kill(ctx context.Context) (bool, error) {
- process.handleLock.RLock()
- defer process.handleLock.RUnlock()
- operation := "hcs::Process::Kill"
- if process.handle == 0 {
- return false, makeProcessError(process, operation, ErrAlreadyClosed, nil)
- }
- if process.stopped() {
- return false, makeProcessError(process, operation, ErrProcessAlreadyStopped, nil)
- }
- if process.killSignalDelivered {
- // A kill signal has already been sent to this process. Sending a second
- // one offers no real benefit, as processes cannot stop themselves from
- // being terminated, once a TerminateProcess has been issued. Sending a
- // second kill may result in a number of errors (two of which detailed bellow)
- // and which we can avoid handling.
- return true, nil
- }
- // HCS serializes the signals sent to a target pid per compute system handle.
- // To avoid SIGKILL being serialized behind other signals, we open a new compute
- // system handle to deliver the kill signal.
- // If the calls to opening a new compute system handle fail, we forcefully
- // terminate the container itself so that no container is left behind
- hcsSystem, err := OpenComputeSystem(ctx, process.system.id)
- if err != nil {
- // log error and force termination of container
- log.G(ctx).WithField("err", err).Error("OpenComputeSystem() call failed")
- err = process.system.Terminate(ctx)
- // if the Terminate() call itself ever failed, log and return error
- if err != nil {
- log.G(ctx).WithField("err", err).Error("Terminate() call failed")
- return false, err
- }
- process.system.Close()
- return true, nil
- }
- defer hcsSystem.Close()
- newProcessHandle, err := hcsSystem.OpenProcess(ctx, process.Pid())
- if err != nil {
- // Return true only if the target process has either already
- // exited, or does not exist.
- if IsAlreadyStopped(err) {
- return true, nil
- } else {
- return false, err
- }
- }
- defer newProcessHandle.Close()
- resultJSON, err := vmcompute.HcsTerminateProcess(ctx, newProcessHandle.handle)
- if err != nil {
- // We still need to check these two cases, as processes may still be killed by an
- // external actor (human operator, OOM, random script etc).
- if errors.Is(err, os.ErrPermission) || IsAlreadyStopped(err) {
- // There are two cases where it should be safe to ignore an error returned
- // by HcsTerminateProcess. The first one is cause by the fact that
- // HcsTerminateProcess ends up calling TerminateProcess in the context
- // of a container. According to the TerminateProcess documentation:
- // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-terminateprocess#remarks
- // After a process has terminated, call to TerminateProcess with open
- // handles to the process fails with ERROR_ACCESS_DENIED (5) error code.
- // It's safe to ignore this error here. HCS should always have permissions
- // to kill processes inside any container. So an ERROR_ACCESS_DENIED
- // is unlikely to be anything else than what the ending remarks in the
- // documentation states.
- //
- // The second case is generated by hcs itself, if for any reason HcsTerminateProcess
- // is called twice in a very short amount of time. In such cases, hcs may return
- // HCS_E_PROCESS_ALREADY_STOPPED.
- return true, nil
- }
- }
- events := processHcsResult(ctx, resultJSON)
- delivered, err := newProcessHandle.processSignalResult(ctx, err)
- if err != nil {
- err = makeProcessError(newProcessHandle, operation, err, events)
- }
- process.killSignalDelivered = delivered
- return delivered, err
- }
- // waitBackground waits for the process exit notification. Once received sets
- // `process.waitError` (if any) and unblocks all `Wait` calls.
- //
- // This MUST be called exactly once per `process.handle` but `Wait` is safe to
- // call multiple times.
- func (process *Process) waitBackground() {
- operation := "hcs::Process::waitBackground"
- ctx, span := oc.StartSpan(context.Background(), operation)
- defer span.End()
- span.AddAttributes(
- trace.StringAttribute("cid", process.SystemID()),
- trace.Int64Attribute("pid", int64(process.processID)))
- var (
- err error
- exitCode = -1
- propertiesJSON string
- resultJSON string
- )
- err = waitForNotification(ctx, process.callbackNumber, hcsNotificationProcessExited, nil)
- if err != nil {
- err = makeProcessError(process, operation, err, nil)
- log.G(ctx).WithError(err).Error("failed wait")
- } else {
- process.handleLock.RLock()
- defer process.handleLock.RUnlock()
- // Make sure we didnt race with Close() here
- if process.handle != 0 {
- propertiesJSON, resultJSON, err = vmcompute.HcsGetProcessProperties(ctx, process.handle)
- events := processHcsResult(ctx, resultJSON)
- if err != nil {
- err = makeProcessError(process, operation, err, events)
- } else {
- properties := &processStatus{}
- err = json.Unmarshal([]byte(propertiesJSON), properties)
- if err != nil {
- err = makeProcessError(process, operation, err, nil)
- } else {
- if properties.LastWaitResult != 0 {
- log.G(ctx).WithField("wait-result", properties.LastWaitResult).Warning("non-zero last wait result")
- } else {
- exitCode = int(properties.ExitCode)
- }
- }
- }
- }
- }
- log.G(ctx).WithField("exitCode", exitCode).Debug("process exited")
- process.closedWaitOnce.Do(func() {
- process.exitCode = exitCode
- process.waitError = err
- close(process.waitBlock)
- })
- oc.SetSpanStatus(span, err)
- }
- // Wait waits for the process to exit. If the process has already exited returns
- // the previous error (if any).
- func (process *Process) Wait() error {
- <-process.waitBlock
- return process.waitError
- }
- // Exited returns if the process has stopped
- func (process *Process) stopped() bool {
- select {
- case <-process.waitBlock:
- return true
- default:
- return false
- }
- }
- // ResizeConsole resizes the console of the process.
- func (process *Process) ResizeConsole(ctx context.Context, width, height uint16) error {
- process.handleLock.RLock()
- defer process.handleLock.RUnlock()
- operation := "hcs::Process::ResizeConsole"
- 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
- }
- resultJSON, err := vmcompute.HcsModifyProcess(ctx, process.handle, string(modifyRequestb))
- events := processHcsResult(ctx, resultJSON)
- if err != nil {
- return makeProcessError(process, operation, err, events)
- }
- return nil
- }
- // ExitCode returns the exit code of the process. The process must have
- // already terminated.
- func (process *Process) ExitCode() (int, error) {
- if !process.stopped() {
- return -1, makeProcessError(process, "hcs::Process::ExitCode", ErrInvalidProcessState, nil)
- }
- if process.waitError != nil {
- return -1, process.waitError
- }
- return process.exitCode, nil
- }
- // StdioLegacy returns the stdin, stdout, and stderr pipes, respectively. Closing
- // these pipes does not close the underlying pipes. Once returned, these pipes
- // are the responsibility of the caller to close.
- func (process *Process) StdioLegacy() (_ io.WriteCloser, _ io.ReadCloser, _ io.ReadCloser, err error) {
- operation := "hcs::Process::StdioLegacy"
- ctx, span := oc.StartSpan(context.Background(), operation)
- defer span.End()
- defer func() { oc.SetSpanStatus(span, err) }()
- span.AddAttributes(
- trace.StringAttribute("cid", process.SystemID()),
- trace.Int64Attribute("pid", int64(process.processID)))
- process.handleLock.RLock()
- defer process.handleLock.RUnlock()
- if process.handle == 0 {
- return nil, nil, nil, makeProcessError(process, operation, ErrAlreadyClosed, nil)
- }
- process.stdioLock.Lock()
- defer process.stdioLock.Unlock()
- if process.hasCachedStdio {
- stdin, stdout, stderr := process.stdin, process.stdout, process.stderr
- process.stdin, process.stdout, process.stderr = nil, nil, nil
- process.hasCachedStdio = false
- return stdin, stdout, stderr, nil
- }
- processInfo, resultJSON, err := vmcompute.HcsGetProcessInfo(ctx, process.handle)
- events := processHcsResult(ctx, resultJSON)
- if err != nil {
- return nil, nil, nil, makeProcessError(process, operation, err, events)
- }
- pipes, err := makeOpenFiles([]syscall.Handle{processInfo.StdInput, processInfo.StdOutput, processInfo.StdError})
- if err != nil {
- return nil, nil, nil, makeProcessError(process, operation, err, nil)
- }
- return pipes[0], pipes[1], pipes[2], nil
- }
- // Stdio returns the stdin, stdout, and stderr pipes, respectively.
- // To close them, close the process handle, or use the `CloseStd*` functions.
- func (process *Process) Stdio() (stdin io.Writer, stdout, stderr io.Reader) {
- process.stdioLock.Lock()
- defer process.stdioLock.Unlock()
- return process.stdin, process.stdout, process.stderr
- }
- // 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(ctx context.Context) (err error) {
- operation := "hcs::Process::CloseStdin"
- ctx, span := trace.StartSpan(ctx, operation)
- defer span.End()
- defer func() { oc.SetSpanStatus(span, err) }()
- span.AddAttributes(
- trace.StringAttribute("cid", process.SystemID()),
- trace.Int64Attribute("pid", int64(process.processID)))
- process.handleLock.RLock()
- defer process.handleLock.RUnlock()
- if process.handle == 0 {
- return makeProcessError(process, operation, ErrAlreadyClosed, nil)
- }
- //HcsModifyProcess request to close stdin will fail if the process has already exited
- if !process.stopped() {
- modifyRequest := processModifyRequest{
- Operation: modifyCloseHandle,
- CloseHandle: &closeHandle{
- Handle: stdIn,
- },
- }
- modifyRequestb, err := json.Marshal(modifyRequest)
- if err != nil {
- return err
- }
- resultJSON, err := vmcompute.HcsModifyProcess(ctx, process.handle, string(modifyRequestb))
- events := processHcsResult(ctx, resultJSON)
- if err != nil {
- return makeProcessError(process, operation, err, events)
- }
- }
- process.stdioLock.Lock()
- defer process.stdioLock.Unlock()
- if process.stdin != nil {
- process.stdin.Close()
- process.stdin = nil
- }
- return nil
- }
- func (process *Process) CloseStdout(ctx context.Context) (err error) {
- ctx, span := oc.StartSpan(ctx, "hcs::Process::CloseStdout") //nolint:ineffassign,staticcheck
- defer span.End()
- defer func() { oc.SetSpanStatus(span, err) }()
- span.AddAttributes(
- trace.StringAttribute("cid", process.SystemID()),
- trace.Int64Attribute("pid", int64(process.processID)))
- process.handleLock.Lock()
- defer process.handleLock.Unlock()
- if process.handle == 0 {
- return nil
- }
- process.stdioLock.Lock()
- defer process.stdioLock.Unlock()
- if process.stdout != nil {
- process.stdout.Close()
- process.stdout = nil
- }
- return nil
- }
- func (process *Process) CloseStderr(ctx context.Context) (err error) {
- ctx, span := oc.StartSpan(ctx, "hcs::Process::CloseStderr") //nolint:ineffassign,staticcheck
- defer span.End()
- defer func() { oc.SetSpanStatus(span, err) }()
- span.AddAttributes(
- trace.StringAttribute("cid", process.SystemID()),
- trace.Int64Attribute("pid", int64(process.processID)))
- process.handleLock.Lock()
- defer process.handleLock.Unlock()
- if process.handle == 0 {
- return nil
- }
- process.stdioLock.Lock()
- defer process.stdioLock.Unlock()
- if process.stderr != nil {
- process.stderr.Close()
- process.stderr = nil
- }
- 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) {
- operation := "hcs::Process::Close"
- ctx, span := oc.StartSpan(context.Background(), operation)
- defer span.End()
- defer func() { oc.SetSpanStatus(span, err) }()
- span.AddAttributes(
- trace.StringAttribute("cid", process.SystemID()),
- trace.Int64Attribute("pid", int64(process.processID)))
- process.handleLock.Lock()
- defer process.handleLock.Unlock()
- // Don't double free this
- if process.handle == 0 {
- return nil
- }
- process.stdioLock.Lock()
- if process.stdin != nil {
- process.stdin.Close()
- process.stdin = nil
- }
- if process.stdout != nil {
- process.stdout.Close()
- process.stdout = nil
- }
- if process.stderr != nil {
- process.stderr.Close()
- process.stderr = nil
- }
- process.stdioLock.Unlock()
- if err = process.unregisterCallback(ctx); err != nil {
- return makeProcessError(process, operation, err, nil)
- }
- if err = vmcompute.HcsCloseProcess(ctx, process.handle); err != nil {
- return makeProcessError(process, operation, err, nil)
- }
- process.handle = 0
- process.closedWaitOnce.Do(func() {
- process.exitCode = -1
- process.waitError = ErrAlreadyClosed
- close(process.waitBlock)
- })
- return nil
- }
- func (process *Process) registerCallback(ctx context.Context) error {
- callbackContext := ¬ificationWatcherContext{
- channels: newProcessChannels(),
- systemID: process.SystemID(),
- processID: process.processID,
- }
- callbackMapLock.Lock()
- callbackNumber := nextCallback
- nextCallback++
- callbackMap[callbackNumber] = callbackContext
- callbackMapLock.Unlock()
- callbackHandle, err := vmcompute.HcsRegisterProcessCallback(ctx, process.handle, notificationWatcherCallback, callbackNumber)
- if err != nil {
- return err
- }
- callbackContext.handle = callbackHandle
- process.callbackNumber = callbackNumber
- return nil
- }
- func (process *Process) unregisterCallback(ctx context.Context) error {
- callbackNumber := process.callbackNumber
- callbackMapLock.RLock()
- callbackContext := callbackMap[callbackNumber]
- callbackMapLock.RUnlock()
- if callbackContext == nil {
- return nil
- }
- handle := callbackContext.handle
- if handle == 0 {
- return nil
- }
- // vmcompute.HcsUnregisterProcessCallback has its own synchronization to
- // wait for all callbacks to complete. We must NOT hold the callbackMapLock.
- err := vmcompute.HcsUnregisterProcessCallback(ctx, handle)
- if err != nil {
- return err
- }
- closeChannels(callbackContext.channels)
- callbackMapLock.Lock()
- delete(callbackMap, callbackNumber)
- callbackMapLock.Unlock()
- handle = 0 //nolint:ineffassign
- return nil
- }
|