From 7c6b04f28e901410a2c839a3b0063760acc1cb6b Mon Sep 17 00:00:00 2001 From: "guillaume.tardif" Date: Thu, 4 Jun 2020 16:31:06 +0200 Subject: [PATCH 1/2] make e2e TestKillChildOnCancel exec properly on windows --- tests/e2e/e2e_test.go | 10 +++++----- tests/framework/exec.go | 10 +++++++++- tests/framework/suite.go | 8 ++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 67e528f21..e968212b1 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -109,7 +109,7 @@ func (s *E2eSuite) TestSetupError() { func (s *E2eSuite) TestKillChildOnCancel() { It("should kill docker-classic if parent command is cancelled", func() { - out := s.NewCommand("ps", "-x").ExecOrDie() + out := s.ListProcessesCommand().ExecOrDie() Expect(out).NotTo(ContainSubstring("docker-classic")) dir := s.ConfigDir @@ -122,16 +122,16 @@ RUN sleep 100`), 0644)).To(Succeed()) _, err := ctx.Exec() errs <- err }() - err := WaitFor(time.Second, 3*time.Second, errs, func() bool { - out := s.NewCommand("ps", "-x").ExecOrDie() + err := WaitFor(time.Second, 10*time.Second, errs, func() bool { + out := s.ListProcessesCommand().ExecOrDie() return strings.Contains(out, "docker-classic") }) Expect(err).NotTo(HaveOccurred()) log.Println("Killing docker process") close(shutdown) - err = WaitFor(time.Second, 4*time.Second, nil, func() bool { - out := s.NewCommand("ps", "-x").ExecOrDie() + err = WaitFor(time.Second, 12*time.Second, nil, func() bool { + out := s.ListProcessesCommand().ExecOrDie() return !strings.Contains(out, "docker-classic") }) Expect(err).NotTo(HaveOccurred()) diff --git a/tests/framework/exec.go b/tests/framework/exec.go index b0494e899..899dab2fb 100644 --- a/tests/framework/exec.go +++ b/tests/framework/exec.go @@ -32,6 +32,7 @@ import ( "fmt" "io" "os/exec" + "runtime" "strings" "syscall" "time" @@ -176,7 +177,7 @@ func Execute(cmd *exec.Cmd, timeout <-chan time.Time) (string, error) { } case <-timeout: log.Debugf("%s %s timed-out", cmd.Path, strings.Join(cmd.Args[1:], " ")) - if err := cmd.Process.Signal(syscall.SIGTERM); err != nil { + if err := terminateProcess(cmd); err != nil { return "", err } return "", fmt.Errorf( @@ -189,6 +190,13 @@ func Execute(cmd *exec.Cmd, timeout <-chan time.Time) (string, error) { return stdout.String(), nil } +func terminateProcess(cmd *exec.Cmd) error { + if runtime.GOOS == "windows" { + return cmd.Process.Kill() + } + return cmd.Process.Signal(syscall.SIGTERM) +} + func mergeWriter(other io.Writer, buf io.Writer) io.Writer { if other != nil { return io.MultiWriter(other, buf) diff --git a/tests/framework/suite.go b/tests/framework/suite.go index 076e9a44a..b27e9a553 100644 --- a/tests/framework/suite.go +++ b/tests/framework/suite.go @@ -107,6 +107,14 @@ func (s *Suite) AfterTest(suite, test string) { _ = os.RemoveAll(s.ConfigDir) } +// ListProcessesCommand creates a command to list processes, "tasklist" on windows, "ps" otherwise. +func (s *Suite) ListProcessesCommand() *CmdContext { + if runtime.GOOS == "windows" { + return s.NewCommand("tasklist") + } + return s.NewCommand("ps") +} + // NewCommand creates a command context. func (s *Suite) NewCommand(command string, args ...string) *CmdContext { return &CmdContext{ From 995d04760888b829ff3048840919515fca5698b0 Mon Sep 17 00:00:00 2001 From: "guillaume.tardif" Date: Thu, 4 Jun 2020 16:32:21 +0200 Subject: [PATCH 2/2] Windows specific : ensure all child processes are killed when parent exits (also works when killing parent). From Desktop code --- cli/dockerclassic/job_windows.go | 93 ++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 cli/dockerclassic/job_windows.go diff --git a/cli/dockerclassic/job_windows.go b/cli/dockerclassic/job_windows.go new file mode 100644 index 000000000..e3c276e92 --- /dev/null +++ b/cli/dockerclassic/job_windows.go @@ -0,0 +1,93 @@ +package dockerclassic + +import ( + "fmt" + "os" + "syscall" + "unsafe" +) + +func init() { + if err := killSubProcessesOnClose(); err != nil { + fmt.Println("failed to create job:", err) + } +} + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") +) + +type jobObjectExtendedLimitInformation struct { + BasicLimitInformation struct { + PerProcessUserTimeLimit uint64 + PerJobUserTimeLimit uint64 + LimitFlags uint32 + MinimumWorkingSetSize uintptr + MaximumWorkingSetSize uintptr + ActiveProcessLimit uint32 + Affinity uintptr + PriorityClass uint32 + SchedulingClass uint32 + } + IoInfo struct { + ReadOperationCount uint64 + WriteOperationCount uint64 + OtherOperationCount uint64 + ReadTransferCount uint64 + WriteTransferCount uint64 + OtherTransferCount uint64 + } + ProcessMemoryLimit uintptr + JobMemoryLimit uintptr + PeakProcessMemoryUsed uintptr + PeakJobMemoryUsed uintptr +} + +// killSubProcessesOnClose will ensure on windows that all child processes of the current process are killed if parent is killed. +func killSubProcessesOnClose() error { + job, err := createJobObject() + if err != nil { + return err + } + info := jobObjectExtendedLimitInformation{} + info.BasicLimitInformation.LimitFlags = 0x2000 + if err := setInformationJobObject(job, info); err != nil { + _ = syscall.CloseHandle(job) + return err + } + proc, err := syscall.GetCurrentProcess() + if err != nil { + _ = syscall.CloseHandle(job) + return err + } + if err := assignProcessToJobObject(job, proc); err != nil { + _ = syscall.CloseHandle(job) + return err + } + return nil +} + +func createJobObject() (syscall.Handle, error) { + res, _, err := kernel32.NewProc("CreateJobObjectW").Call(uintptr(unsafe.Pointer(nil)), uintptr(unsafe.Pointer(nil))) + if res == 0 { + return syscall.InvalidHandle, os.NewSyscallError("CreateJobObject", err) + } + return syscall.Handle(res), nil +} + +func setInformationJobObject(job syscall.Handle, info jobObjectExtendedLimitInformation) error { + infoClass := uint32(9) + res, _, err := kernel32.NewProc("SetInformationJobObject").Call(uintptr(job), uintptr(infoClass), uintptr(unsafe.Pointer(&info)), uintptr(uint32(unsafe.Sizeof(info)))) + if res == 0 { + return os.NewSyscallError("SetInformationJobObject", err) + } + return nil +} + +func assignProcessToJobObject(job syscall.Handle, process syscall.Handle) error { + res, _, err := kernel32.NewProc("AssignProcessToJobObject").Call(uintptr(job), uintptr(process)) + if res == 0 { + return os.NewSyscallError("AssignProcessToJobObject", err) + } + return nil +}