| 🛟 Updated. 28d5985 k33g 15h ago | 1 | //go:build windows |
| 2 | |
| 3 | package tools |
| 4 | |
| 5 | import ( |
| 6 | "os" |
| 7 | "os/exec" |
| 8 | "syscall" |
| 9 | "unsafe" |
| 10 | |
| 11 | "golang.org/x/sys/windows" |
| 12 | ) |
| 13 | |
| 14 | // shellProgram is the shell Windows names as the user's: %COMSPEC%, which is |
| 15 | // cmd.exe. |
| 16 | func shellProgram() string { return windowsShell(os.Getenv("COMSPEC")) } |
| 17 | |
| 18 | // shellArgs makes cmd.exe run one command line and exit. /S is what makes its |
| 19 | // quote handling predictable; see cmdExeCommandLine. |
| 20 | func shellArgs(command string) []string { return []string{"/S", "/C", command} } |
| 21 | |
| 22 | // childAttributes gives the command a process group of its own and, more to |
| 23 | // the point, a command line written the way cmd.exe reads it. |
| 24 | // |
| 25 | // Go's exec composes a command line by the C runtime's rules, which cmd.exe |
| 26 | // does not follow: a `"` inside the command would arrive as `\"`. So the line |
| 27 | // is written here in full and exec is told to use it as it is. |
| 28 | func childAttributes(process *exec.Cmd, command string) *syscall.SysProcAttr { |
| 29 | return &syscall.SysProcAttr{ |
| 30 | CreationFlags: windows.CREATE_NEW_PROCESS_GROUP, |
| 31 | CmdLine: cmdExeCommandLine(process.Path, command), |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | // jobObject is the Windows answer to a process group: every process the |
| 36 | // command starts is put in the job, and terminating the job ends them all. |
| 37 | // |
| 38 | // A job that could not be created leaves the command running as it would have |
| 39 | // anyway, and kill falls back to the one process. |
| 40 | type jobObject struct { |
| 41 | process *os.Process |
| 42 | job windows.Handle |
| 43 | } |
| 44 | |
| 45 | // newGroup puts a started command in a job of its own. |
| 46 | // |
| 47 | // The process is assigned after it has started, so a child it spawned in the |
| 48 | // meantime escapes the job. cmd.exe takes milliseconds to start a child and the |
| 49 | // window is that wide; the alternative — starting suspended and resuming by |
| 50 | // hand — is not something exec.Cmd allows. |
| 51 | func newGroup(process *os.Process) group { |
| 52 | g := jobObject{process: process} |
| 53 | |
| 54 | job, err := windows.CreateJobObject(nil, nil) |
| 55 | if err != nil { |
| 56 | return g |
| 57 | } |
| 58 | limits := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{} |
| 59 | limits.BasicLimitInformation.LimitFlags = windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE |
| 60 | if _, err := windows.SetInformationJobObject(job, windows.JobObjectExtendedLimitInformation, |
| 61 | uintptr(unsafe.Pointer(&limits)), uint32(unsafe.Sizeof(limits))); err != nil { |
| 62 | windows.CloseHandle(job) |
| 63 | return g |
| 64 | } |
| 65 | |
| 66 | handle, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(process.Pid)) |
| 67 | if err != nil { |
| 68 | windows.CloseHandle(job) |
| 69 | return g |
| 70 | } |
| 71 | defer windows.CloseHandle(handle) |
| 72 | if err := windows.AssignProcessToJobObject(job, handle); err != nil { |
| 73 | windows.CloseHandle(job) |
| 74 | return g |
| 75 | } |
| 76 | |
| 77 | g.job = job |
| 78 | return g |
| 79 | } |
| 80 | |
| 81 | // kill terminates the job — the shell and everything it started — or, with no |
| 82 | // job, the shell alone. |
| 83 | func (g jobObject) kill() { |
| 84 | if g.job != 0 { |
| 85 | if err := windows.TerminateJobObject(g.job, 1); err == nil { |
| 86 | return |
| 87 | } |
| 88 | } |
| 89 | _ = g.process.Kill() |
| 90 | } |
| 91 | |
| 92 | // release closes the job handle. The job was created to kill on close, so a |
| 93 | // command that ended on its own has nothing left in it, and one that is still |
| 94 | // running is ended — which is only reached from a Run that has finished. |
| 95 | func (g jobObject) release() { |
| 96 | if g.job != 0 { |
| 97 | windows.CloseHandle(g.job) |
| 98 | } |
| 99 | } |