//go:build windows package tools import ( "os" "os/exec" "syscall" "unsafe" "golang.org/x/sys/windows" ) // shellProgram is the shell Windows names as the user's: %COMSPEC%, which is // cmd.exe. func shellProgram() string { return windowsShell(os.Getenv("COMSPEC")) } // shellArgs makes cmd.exe run one command line and exit. /S is what makes its // quote handling predictable; see cmdExeCommandLine. func shellArgs(command string) []string { return []string{"/S", "/C", command} } // childAttributes gives the command a process group of its own and, more to // the point, a command line written the way cmd.exe reads it. // // Go's exec composes a command line by the C runtime's rules, which cmd.exe // does not follow: a `"` inside the command would arrive as `\"`. So the line // is written here in full and exec is told to use it as it is. func childAttributes(process *exec.Cmd, command string) *syscall.SysProcAttr { return &syscall.SysProcAttr{ CreationFlags: windows.CREATE_NEW_PROCESS_GROUP, CmdLine: cmdExeCommandLine(process.Path, command), } } // jobObject is the Windows answer to a process group: every process the // command starts is put in the job, and terminating the job ends them all. // // A job that could not be created leaves the command running as it would have // anyway, and kill falls back to the one process. type jobObject struct { process *os.Process job windows.Handle } // newGroup puts a started command in a job of its own. // // The process is assigned after it has started, so a child it spawned in the // meantime escapes the job. cmd.exe takes milliseconds to start a child and the // window is that wide; the alternative — starting suspended and resuming by // hand — is not something exec.Cmd allows. func newGroup(process *os.Process) group { g := jobObject{process: process} job, err := windows.CreateJobObject(nil, nil) if err != nil { return g } limits := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{} limits.BasicLimitInformation.LimitFlags = windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE if _, err := windows.SetInformationJobObject(job, windows.JobObjectExtendedLimitInformation, uintptr(unsafe.Pointer(&limits)), uint32(unsafe.Sizeof(limits))); err != nil { windows.CloseHandle(job) return g } handle, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(process.Pid)) if err != nil { windows.CloseHandle(job) return g } defer windows.CloseHandle(handle) if err := windows.AssignProcessToJobObject(job, handle); err != nil { windows.CloseHandle(job) return g } g.job = job return g } // kill terminates the job — the shell and everything it started — or, with no // job, the shell alone. func (g jobObject) kill() { if g.job != 0 { if err := windows.TerminateJobObject(g.job, 1); err == nil { return } } _ = g.process.Kill() } // release closes the job handle. The job was created to kill on close, so a // command that ended on its own has nothing left in it, and one that is still // running is ended — which is only reached from a Run that has finished. func (g jobObject) release() { if g.job != 0 { windows.CloseHandle(g.job) } }