turbo-editors/turbo-corepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated. 28d5985 · on main · k33g · 4h ago
shell_windows.go · 99 lines · 3.1 KBGo Blame HistoryRaw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//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)
	}
}