container_windows.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. package libcontainerd
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "io/ioutil"
  7. "strings"
  8. "time"
  9. "github.com/Microsoft/hcsshim"
  10. "github.com/opencontainers/runtime-spec/specs-go"
  11. "github.com/sirupsen/logrus"
  12. "golang.org/x/sys/windows"
  13. )
  14. type container struct {
  15. containerCommon
  16. // Platform specific fields are below here. There are none presently on Windows.
  17. options []CreateOption
  18. // The ociSpec is required, as client.Create() needs a spec,
  19. // but can be called from the RestartManager context which does not
  20. // otherwise have access to the Spec
  21. ociSpec specs.Spec
  22. isWindows bool
  23. manualStopRequested bool
  24. hcsContainer hcsshim.Container
  25. }
  26. func (ctr *container) newProcess(friendlyName string) *process {
  27. return &process{
  28. processCommon: processCommon{
  29. containerID: ctr.containerID,
  30. friendlyName: friendlyName,
  31. client: ctr.client,
  32. },
  33. }
  34. }
  35. // start starts a created container.
  36. // Caller needs to lock container ID before calling this method.
  37. func (ctr *container) start(attachStdio StdioCallback) error {
  38. var err error
  39. // Start the container. If this is a servicing container, this call will block
  40. // until the container is done with the servicing execution.
  41. logrus.Debugln("libcontainerd: starting container ", ctr.containerID)
  42. if err = ctr.hcsContainer.Start(); err != nil {
  43. logrus.Errorf("libcontainerd: failed to start container: %s", err)
  44. ctr.debugGCS() // Before terminating!
  45. if err := ctr.terminate(); err != nil {
  46. logrus.Errorf("libcontainerd: failed to cleanup after a failed Start. %s", err)
  47. } else {
  48. logrus.Debugln("libcontainerd: cleaned up after failed Start by calling Terminate")
  49. }
  50. return err
  51. }
  52. defer ctr.debugGCS()
  53. // Note we always tell HCS to
  54. // create stdout as it's required regardless of '-i' or '-t' options, so that
  55. // docker can always grab the output through logs. We also tell HCS to always
  56. // create stdin, even if it's not used - it will be closed shortly. Stderr
  57. // is only created if it we're not -t.
  58. var (
  59. emulateConsole bool
  60. createStdErrPipe bool
  61. )
  62. if ctr.ociSpec.Process != nil {
  63. emulateConsole = ctr.ociSpec.Process.Terminal
  64. createStdErrPipe = !ctr.ociSpec.Process.Terminal && !ctr.ociSpec.Windows.Servicing
  65. }
  66. createProcessParms := &hcsshim.ProcessConfig{
  67. EmulateConsole: emulateConsole,
  68. WorkingDirectory: ctr.ociSpec.Process.Cwd,
  69. CreateStdInPipe: !ctr.ociSpec.Windows.Servicing,
  70. CreateStdOutPipe: !ctr.ociSpec.Windows.Servicing,
  71. CreateStdErrPipe: createStdErrPipe,
  72. }
  73. if ctr.ociSpec.Process != nil && ctr.ociSpec.Process.ConsoleSize != nil {
  74. createProcessParms.ConsoleSize[0] = uint(ctr.ociSpec.Process.ConsoleSize.Height)
  75. createProcessParms.ConsoleSize[1] = uint(ctr.ociSpec.Process.ConsoleSize.Width)
  76. }
  77. // Configure the environment for the process
  78. createProcessParms.Environment = setupEnvironmentVariables(ctr.ociSpec.Process.Env)
  79. if ctr.isWindows {
  80. createProcessParms.CommandLine = strings.Join(ctr.ociSpec.Process.Args, " ")
  81. } else {
  82. createProcessParms.CommandArgs = ctr.ociSpec.Process.Args
  83. }
  84. createProcessParms.User = ctr.ociSpec.Process.User.Username
  85. // LCOW requires the raw OCI spec passed through HCS and onwards to GCS for the utility VM.
  86. if !ctr.isWindows {
  87. ociBuf, err := json.Marshal(ctr.ociSpec)
  88. if err != nil {
  89. return err
  90. }
  91. ociRaw := json.RawMessage(ociBuf)
  92. createProcessParms.OCISpecification = &ociRaw
  93. }
  94. // Start the command running in the container.
  95. newProcess, err := ctr.hcsContainer.CreateProcess(createProcessParms)
  96. if err != nil {
  97. logrus.Errorf("libcontainerd: CreateProcess() failed %s", err)
  98. if err := ctr.terminate(); err != nil {
  99. logrus.Errorf("libcontainerd: failed to cleanup after a failed CreateProcess. %s", err)
  100. } else {
  101. logrus.Debugln("libcontainerd: cleaned up after failed CreateProcess by calling Terminate")
  102. }
  103. return err
  104. }
  105. pid := newProcess.Pid()
  106. // Save the hcs Process and PID
  107. ctr.process.friendlyName = InitFriendlyName
  108. ctr.process.hcsProcess = newProcess
  109. // If this is a servicing container, wait on the process synchronously here and
  110. // if it succeeds, wait for it cleanly shutdown and merge into the parent container.
  111. if ctr.ociSpec.Windows.Servicing {
  112. exitCode := ctr.waitProcessExitCode(&ctr.process)
  113. if exitCode != 0 {
  114. if err := ctr.terminate(); err != nil {
  115. logrus.Warnf("libcontainerd: terminating servicing container %s failed: %s", ctr.containerID, err)
  116. }
  117. return fmt.Errorf("libcontainerd: servicing container %s returned non-zero exit code %d", ctr.containerID, exitCode)
  118. }
  119. return ctr.hcsContainer.WaitTimeout(time.Minute * 5)
  120. }
  121. var stdout, stderr io.ReadCloser
  122. var stdin io.WriteCloser
  123. stdin, stdout, stderr, err = newProcess.Stdio()
  124. if err != nil {
  125. logrus.Errorf("libcontainerd: failed to get stdio pipes: %s", err)
  126. if err := ctr.terminate(); err != nil {
  127. logrus.Errorf("libcontainerd: failed to cleanup after a failed Stdio. %s", err)
  128. }
  129. return err
  130. }
  131. iopipe := &IOPipe{Terminal: ctr.ociSpec.Process.Terminal}
  132. iopipe.Stdin = createStdInCloser(stdin, newProcess)
  133. // Convert io.ReadClosers to io.Readers
  134. if stdout != nil {
  135. iopipe.Stdout = ioutil.NopCloser(&autoClosingReader{ReadCloser: stdout})
  136. }
  137. if stderr != nil {
  138. iopipe.Stderr = ioutil.NopCloser(&autoClosingReader{ReadCloser: stderr})
  139. }
  140. // Save the PID
  141. logrus.Debugf("libcontainerd: process started - PID %d", pid)
  142. ctr.systemPid = uint32(pid)
  143. // Spin up a go routine waiting for exit to handle cleanup
  144. go ctr.waitExit(&ctr.process, true)
  145. ctr.client.appendContainer(ctr)
  146. if err := attachStdio(*iopipe); err != nil {
  147. // OK to return the error here, as waitExit will handle tear-down in HCS
  148. return err
  149. }
  150. // Tell the docker engine that the container has started.
  151. si := StateInfo{
  152. CommonStateInfo: CommonStateInfo{
  153. State: StateStart,
  154. Pid: ctr.systemPid, // Not sure this is needed? Double-check monitor.go in daemon BUGBUG @jhowardmsft
  155. }}
  156. logrus.Debugf("libcontainerd: start() completed OK, %+v", si)
  157. return ctr.client.backend.StateChanged(ctr.containerID, si)
  158. }
  159. // waitProcessExitCode will wait for the given process to exit and return its error code.
  160. func (ctr *container) waitProcessExitCode(process *process) int {
  161. // Block indefinitely for the process to exit.
  162. err := process.hcsProcess.Wait()
  163. if err != nil {
  164. if herr, ok := err.(*hcsshim.ProcessError); ok && herr.Err != windows.ERROR_BROKEN_PIPE {
  165. logrus.Warnf("libcontainerd: Wait() failed (container may have been killed): %s", err)
  166. }
  167. // Fall through here, do not return. This ensures we attempt to continue the
  168. // shutdown in HCS and tell the docker engine that the process/container
  169. // has exited to avoid a container being dropped on the floor.
  170. }
  171. exitCode, err := process.hcsProcess.ExitCode()
  172. if err != nil {
  173. if herr, ok := err.(*hcsshim.ProcessError); ok && herr.Err != windows.ERROR_BROKEN_PIPE {
  174. logrus.Warnf("libcontainerd: unable to get exit code from container %s", ctr.containerID)
  175. }
  176. // Since we got an error retrieving the exit code, make sure that the code we return
  177. // doesn't incorrectly indicate success.
  178. exitCode = -1
  179. // Fall through here, do not return. This ensures we attempt to continue the
  180. // shutdown in HCS and tell the docker engine that the process/container
  181. // has exited to avoid a container being dropped on the floor.
  182. }
  183. return exitCode
  184. }
  185. // waitExit runs as a goroutine waiting for the process to exit. It's
  186. // equivalent to (in the linux containerd world) where events come in for
  187. // state change notifications from containerd.
  188. func (ctr *container) waitExit(process *process, isFirstProcessToStart bool) error {
  189. logrus.Debugln("libcontainerd: waitExit() on pid", process.systemPid)
  190. exitCode := ctr.waitProcessExitCode(process)
  191. // Lock the container while removing the process/container from the list
  192. ctr.client.lock(ctr.containerID)
  193. if !isFirstProcessToStart {
  194. ctr.cleanProcess(process.friendlyName)
  195. } else {
  196. ctr.client.deleteContainer(ctr.containerID)
  197. }
  198. // Unlock here so other threads are unblocked
  199. ctr.client.unlock(ctr.containerID)
  200. // Assume the container has exited
  201. si := StateInfo{
  202. CommonStateInfo: CommonStateInfo{
  203. State: StateExit,
  204. ExitCode: uint32(exitCode),
  205. Pid: process.systemPid,
  206. ProcessID: process.friendlyName,
  207. },
  208. UpdatePending: false,
  209. }
  210. // But it could have been an exec'd process which exited
  211. if !isFirstProcessToStart {
  212. si.State = StateExitProcess
  213. } else {
  214. // Pending updates is only applicable for WCOW
  215. if ctr.isWindows {
  216. updatePending, err := ctr.hcsContainer.HasPendingUpdates()
  217. if err != nil {
  218. logrus.Warnf("libcontainerd: HasPendingUpdates() failed (container may have been killed): %s", err)
  219. } else {
  220. si.UpdatePending = updatePending
  221. }
  222. }
  223. logrus.Debugf("libcontainerd: shutting down container %s", ctr.containerID)
  224. if err := ctr.shutdown(); err != nil {
  225. logrus.Debugf("libcontainerd: failed to shutdown container %s", ctr.containerID)
  226. } else {
  227. logrus.Debugf("libcontainerd: completed shutting down container %s", ctr.containerID)
  228. }
  229. if err := ctr.hcsContainer.Close(); err != nil {
  230. logrus.Error(err)
  231. }
  232. }
  233. if err := process.hcsProcess.Close(); err != nil {
  234. logrus.Errorf("libcontainerd: hcsProcess.Close(): %v", err)
  235. }
  236. // Call into the backend to notify it of the state change.
  237. logrus.Debugf("libcontainerd: waitExit() calling backend.StateChanged %+v", si)
  238. if err := ctr.client.backend.StateChanged(ctr.containerID, si); err != nil {
  239. logrus.Error(err)
  240. }
  241. logrus.Debugf("libcontainerd: waitExit() completed OK, %+v", si)
  242. return nil
  243. }
  244. // cleanProcess removes process from the map.
  245. // Caller needs to lock container ID before calling this method.
  246. func (ctr *container) cleanProcess(id string) {
  247. delete(ctr.processes, id)
  248. }
  249. // shutdown shuts down the container in HCS
  250. // Caller needs to lock container ID before calling this method.
  251. func (ctr *container) shutdown() error {
  252. const shutdownTimeout = time.Minute * 5
  253. err := ctr.hcsContainer.Shutdown()
  254. if hcsshim.IsPending(err) {
  255. // Explicit timeout to avoid a (remote) possibility that shutdown hangs indefinitely.
  256. err = ctr.hcsContainer.WaitTimeout(shutdownTimeout)
  257. } else if hcsshim.IsAlreadyStopped(err) {
  258. err = nil
  259. }
  260. if err != nil {
  261. logrus.Debugf("libcontainerd: error shutting down container %s %v calling terminate", ctr.containerID, err)
  262. if err := ctr.terminate(); err != nil {
  263. return err
  264. }
  265. return err
  266. }
  267. return nil
  268. }
  269. // terminate terminates the container in HCS
  270. // Caller needs to lock container ID before calling this method.
  271. func (ctr *container) terminate() error {
  272. const terminateTimeout = time.Minute * 5
  273. err := ctr.hcsContainer.Terminate()
  274. if hcsshim.IsPending(err) {
  275. err = ctr.hcsContainer.WaitTimeout(terminateTimeout)
  276. } else if hcsshim.IsAlreadyStopped(err) {
  277. err = nil
  278. }
  279. if err != nil {
  280. logrus.Debugf("libcontainerd: error terminating container %s %v", ctr.containerID, err)
  281. return err
  282. }
  283. return nil
  284. }