| package libcontainerd |
| |
| import ( |
| "io" |
| "strings" |
| "syscall" |
| |
| "github.com/Microsoft/hcsshim" |
| "github.com/Sirupsen/logrus" |
| ) |
| |
| type container struct { |
| containerCommon |
| |
| // Platform specific fields are below here. There are none presently on Windows. |
| options []CreateOption |
| |
| // The ociSpec is required, as client.Create() needs a spec, |
| // but can be called from the RestartManager context which does not |
| // otherwise have access to the Spec |
| ociSpec Spec |
| } |
| |
| func (ctr *container) newProcess(friendlyName string) *process { |
| return &process{ |
| processCommon: processCommon{ |
| containerID: ctr.containerID, |
| friendlyName: friendlyName, |
| client: ctr.client, |
| }, |
| } |
| } |
| |
| func (ctr *container) start() error { |
| var err error |
| |
| // Start the container |
| logrus.Debugln("Starting container ", ctr.containerID) |
| if err = hcsshim.StartComputeSystem(ctr.containerID); err != nil { |
| logrus.Errorf("Failed to start compute system: %s", err) |
| return err |
| } |
| |
| createProcessParms := hcsshim.CreateProcessParams{ |
| EmulateConsole: ctr.ociSpec.Process.Terminal, |
| WorkingDirectory: ctr.ociSpec.Process.Cwd, |
| ConsoleSize: ctr.ociSpec.Process.InitialConsoleSize, |
| } |
| |
| // Configure the environment for the process |
| createProcessParms.Environment = setupEnvironmentVariables(ctr.ociSpec.Process.Env) |
| createProcessParms.CommandLine = strings.Join(ctr.ociSpec.Process.Args, " ") |
| |
| iopipe := &IOPipe{Terminal: ctr.ociSpec.Process.Terminal} |
| |
| // Start the command running in the container. Note we always tell HCS to |
| // create stdout as it's required regardless of '-i' or '-t' options, so that |
| // docker can always grab the output through logs. We also tell HCS to always |
| // create stdin, even if it's not used - it will be closed shortly. Stderr |
| // is only created if it we're not -t. |
| var pid uint32 |
| var stdout, stderr io.ReadCloser |
| pid, iopipe.Stdin, stdout, stderr, err = hcsshim.CreateProcessInComputeSystem( |
| ctr.containerID, |
| true, |
| true, |
| !ctr.ociSpec.Process.Terminal, |
| createProcessParms) |
| if err != nil { |
| logrus.Errorf("CreateProcessInComputeSystem() failed %s", err) |
| |
| // Explicitly terminate the compute system here. |
| if err2 := hcsshim.TerminateComputeSystem(ctr.containerID, hcsshim.TimeoutInfinite, "CreateProcessInComputeSystem failed"); err2 != nil { |
| // Ignore this error, there's not a lot we can do except log it |
| logrus.Warnf("Failed to TerminateComputeSystem after a failed CreateProcessInComputeSystem. Ignoring this.", err2) |
| } else { |
| logrus.Debugln("Cleaned up after failed CreateProcessInComputeSystem by calling TerminateComputeSystem") |
| } |
| return err |
| } |
| |
| // Convert io.ReadClosers to io.Readers |
| if stdout != nil { |
| iopipe.Stdout = openReaderFromPipe(stdout) |
| } |
| if stderr != nil { |
| iopipe.Stderr = openReaderFromPipe(stderr) |
| } |
| |
| // Save the PID |
| logrus.Debugf("Process started - PID %d", pid) |
| ctr.systemPid = uint32(pid) |
| |
| // Spin up a go routine waiting for exit to handle cleanup |
| go ctr.waitExit(pid, InitFriendlyName, true) |
| |
| ctr.client.appendContainer(ctr) |
| |
| if err := ctr.client.backend.AttachStreams(ctr.containerID, *iopipe); err != nil { |
| // OK to return the error here, as waitExit will handle tear-down in HCS |
| return err |
| } |
| |
| // Tell the docker engine that the container has started. |
| si := StateInfo{ |
| State: StateStart, |
| Pid: ctr.systemPid, // Not sure this is needed? Double-check monitor.go in daemon BUGBUG @jhowardmsft |
| } |
| return ctr.client.backend.StateChanged(ctr.containerID, si) |
| |
| } |
| |
| // waitExit runs as a goroutine waiting for the process to exit. It's |
| // equivalent to (in the linux containerd world) where events come in for |
| // state change notifications from containerd. |
| func (ctr *container) waitExit(pid uint32, processFriendlyName string, isFirstProcessToStart bool) error { |
| logrus.Debugln("waitExit on pid", pid) |
| |
| // Block indefinitely for the process to exit. |
| exitCode, err := hcsshim.WaitForProcessInComputeSystem(ctr.containerID, pid, hcsshim.TimeoutInfinite) |
| if err != nil { |
| if herr, ok := err.(*hcsshim.HcsError); ok && herr.Err != syscall.ERROR_BROKEN_PIPE { |
| logrus.Warnf("WaitForProcessInComputeSystem failed (container may have been killed): %s", err) |
| } |
| // Fall through here, do not return. This ensures we attempt to continue the |
| // shutdown in HCS nad tell the docker engine that the process/container |
| // has exited to avoid a container being dropped on the floor. |
| } |
| |
| // Assume the container has exited |
| si := StateInfo{ |
| State: StateExit, |
| ExitCode: uint32(exitCode), |
| Pid: pid, |
| ProcessID: processFriendlyName, |
| } |
| |
| // But it could have been an exec'd process which exited |
| if !isFirstProcessToStart { |
| si.State = StateExitProcess |
| } |
| |
| // If this is the init process, always call into vmcompute.dll to |
| // shutdown the container after we have completed. |
| if isFirstProcessToStart { |
| logrus.Debugf("Shutting down container %s", ctr.containerID) |
| // Explicit timeout here rather than hcsshim.TimeoutInfinte to avoid a |
| // (remote) possibility that ShutdownComputeSystem hangs indefinitely. |
| const shutdownTimeout = 5 * 60 * 1000 // 5 minutes |
| if err := hcsshim.ShutdownComputeSystem(ctr.containerID, shutdownTimeout, "waitExit"); err != nil { |
| if herr, ok := err.(*hcsshim.HcsError); !ok || |
| (herr.Err != hcsshim.ERROR_SHUTDOWN_IN_PROGRESS && |
| herr.Err != ErrorBadPathname && |
| herr.Err != syscall.ERROR_PATH_NOT_FOUND) { |
| logrus.Warnf("Ignoring error from ShutdownComputeSystem %s", err) |
| } |
| } else { |
| logrus.Debugf("Completed shutting down container %s", ctr.containerID) |
| } |
| |
| // BUGBUG - Is taking the lock necessary here? Should it just be taken for |
| // the deleteContainer call, not for the restart logic? @jhowardmsft |
| ctr.client.lock(ctr.containerID) |
| defer ctr.client.unlock(ctr.containerID) |
| |
| if si.State == StateExit && ctr.restartManager != nil { |
| restart, wait, err := ctr.restartManager.ShouldRestart(uint32(exitCode)) |
| if err != nil { |
| logrus.Error(err) |
| } else if restart { |
| si.State = StateRestart |
| ctr.restarting = true |
| go func() { |
| err := <-wait |
| ctr.restarting = false |
| if err != nil { |
| si.State = StateExit |
| if err := ctr.client.backend.StateChanged(ctr.containerID, si); err != nil { |
| logrus.Error(err) |
| } |
| logrus.Error(err) |
| } else { |
| ctr.client.Create(ctr.containerID, ctr.ociSpec, ctr.options...) |
| } |
| }() |
| } |
| } |
| |
| // Remove process from list if we have exited |
| // We need to do so here in case the Message Handler decides to restart it. |
| if si.State == StateExit { |
| ctr.client.deleteContainer(ctr.friendlyName) |
| } |
| } |
| |
| // Call into the backend to notify it of the state change. |
| logrus.Debugf("waitExit() calling backend.StateChanged %v", si) |
| if err := ctr.client.backend.StateChanged(ctr.containerID, si); err != nil { |
| logrus.Error(err) |
| } |
| |
| logrus.Debugln("waitExit() completed OK") |
| return nil |
| } |