| 1 | //go:build windows |
| 2 | |
| 3 | package proc |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "io" |
| 8 | "os/exec" |
| 9 | "testing" |
| 10 | "time" |
| 11 | ) |
| 12 | |
| 13 | // A launcher (cmd.exe) that spawns a long-lived grandchild (ping) which inherits |
| 14 | // the stdout pipe: killing only the direct child leaves the grandchild holding |
| 15 | // the pipe, so cmd.Wait blocks until the grandchild exits. KillTree must take |
| 16 | // the whole tree down so Wait returns promptly. |
| 17 | func TestKillTreeUnblocksWaitOnSurvivingGrandchild(t *testing.T) { |
| 18 | cmd := exec.Command("cmd", "/c", "ping", "-n", "30", "127.0.0.1") |
| 19 | HideWindow(cmd) |
| 20 | stdout, err := cmd.StdoutPipe() |
| 21 | if err != nil { |
| 22 | t.Fatalf("StdoutPipe: %v", err) |
| 23 | } |
| 24 | if err := cmd.Start(); err != nil { |
| 25 | t.Fatalf("Start: %v", err) |
| 26 | } |
| 27 | go func() { _, _ = io.Copy(io.Discard, stdout) }() |
| 28 | time.Sleep(500 * time.Millisecond) // let cmd.exe exec the ping grandchild |
| 29 | |
| 30 | KillTree(cmd) |
| 31 | |
| 32 | done := make(chan error, 1) |
| 33 | go func() { done <- cmd.Wait() }() |
| 34 | select { |
| 35 | case <-done: |
| 36 | case <-time.After(8 * time.Second): |
| 37 | t.Fatal("cmd.Wait blocked after KillTree — grandchild survived holding the pipe") |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | // StartTracked must create a Job Object for the started process, and KillTracked |
| 42 | // must take the whole tracked tree down, including descendants a plain taskkill |
| 43 | // /T would miss. |
| 44 | func TestKillTrackedReapsTrackedTree(t *testing.T) { |
| 45 | cmd := exec.Command("cmd", "/c", "ping", "-n", "30", "127.0.0.1") |
| 46 | HideWindow(cmd) |
| 47 | stdout, err := cmd.StdoutPipe() |
| 48 | if err != nil { |
| 49 | t.Fatalf("StdoutPipe: %v", err) |
| 50 | } |
| 51 | job, err := StartTracked(cmd) |
| 52 | if err != nil { |
| 53 | t.Fatalf("StartTracked: %v", err) |
| 54 | } |
| 55 | if job == 0 { |
| 56 | t.Fatal("StartTracked returned 0 — job object not created") |
| 57 | } |
| 58 | go func() { _, _ = io.Copy(io.Discard, stdout) }() |
| 59 | time.Sleep(500 * time.Millisecond) // let cmd.exe exec the ping grandchild into the job |
| 60 | |
| 61 | KillTracked(cmd, job) |
| 62 | |
| 63 | done := make(chan error, 1) |
| 64 | go func() { done <- cmd.Wait() }() |
| 65 | select { |
| 66 | case <-done: |
| 67 | case <-time.After(8 * time.Second): |
| 68 | t.Fatal("cmd.Wait blocked after KillTracked — tracked tree survived") |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | // StartTracked creates the child suspended to win the job-assignment race, so it |
| 73 | // must resume it or the process hangs forever. A child that exits with a known |
| 74 | // code proves the resume fired: Wait returns that code instead of timing out. |
| 75 | func TestStartTrackedResumesSuspendedChild(t *testing.T) { |
| 76 | cmd := exec.Command("cmd", "/c", "exit", "7") |
| 77 | HideWindow(cmd) |
| 78 | if _, err := StartTracked(cmd); err != nil { |
| 79 | t.Fatalf("StartTracked: %v", err) |
| 80 | } |
| 81 | |
| 82 | done := make(chan error, 1) |
| 83 | go func() { done <- cmd.Wait() }() |
| 84 | select { |
| 85 | case err := <-done: |
| 86 | var ee *exec.ExitError |
| 87 | if !errors.As(err, &ee) || ee.ExitCode() != 7 { |
| 88 | t.Fatalf("exit = %v, want exit status 7", err) |
| 89 | } |
| 90 | case <-time.After(8 * time.Second): |
| 91 | t.Fatal("cmd.Wait blocked — StartTracked left the child suspended") |
| 92 | } |
| 93 | } |
| 94 |