turbo-editors/turbo-corepublic Fork 0
v1.0.2
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.

pty_windows.go · 205 lines · 7.8 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 17h ago1//go:build windows
2
3package terminal
4
5import (
6 "fmt"
7 "os"
8 "sync"
9 "unsafe"
10
11 "golang.org/x/sys/windows"
12)
13
14// windowsChild is a shell behind a Windows pseudo-console.
15//
16// A pseudo-console is not a device this process can read and write: it is an
17// object owned by conhost.exe, wired to two pipes of ours. What the shell
18// prints arrives on the output pipe as the same VT sequences a Unix shell
19// writes to a pty, which is the whole reason the emulator on this side needs
20// no Windows code at all; what we write to the input pipe reaches the shell
21// as keystrokes.
22//
23// Go's exec cannot start a process attached to a pseudo-console — the
24// attribute has to go through an extended STARTUPINFO — so the process is
25// created with CreateProcess directly and waited for with a handle.
26type windowsChild struct {
27 console windows.Handle
28 process windows.Handle
29 input *os.File // our end of the shell's standard input
30 output *os.File // our end of the shell's standard output and error
31 program string
32
33 closeConsole sync.Once
34 closeOnce sync.Once
35 closeErr error
36}
37
38// updateProcThreadAttribute is called by hand rather than through x/sys,
39// because the pseudo-console attribute's value is the handle *itself*, and
40// x/sys's wrapper takes an unsafe.Pointer — which would mean converting a
41// handle to one, the very conversion `go vet` exists to flag.
42var updateProcThreadAttribute = windows.NewLazySystemDLL("kernel32.dll").NewProc("UpdateProcThreadAttribute")
43
44// startChild creates a pseudo-console and starts the shell attached to it.
45func startChild(options Options) (child, error) {
46 var inputRead, inputWrite, outputRead, outputWrite windows.Handle
47 if err := windows.CreatePipe(&inputRead, &inputWrite, nil, 0); err != nil {
48 return nil, fmt.Errorf("terminal: creating the input pipe: %w", err)
49 }
50 if err := windows.CreatePipe(&outputRead, &outputWrite, nil, 0); err != nil {
51 closeHandles(inputRead, inputWrite)
52 return nil, fmt.Errorf("terminal: creating the output pipe: %w", err)
53 }
54
55 var console windows.Handle
56 size := coord(options.Width, options.Height)
57 if err := windows.CreatePseudoConsole(size, inputRead, outputWrite, 0, &console); err != nil {
58 closeHandles(inputRead, inputWrite, outputRead, outputWrite)
59 return nil, fmt.Errorf("terminal: creating the pseudo-console: %w", err)
60 }
61 // The console has duplicated the two ends it uses; ours are the other two.
62 closeHandles(inputRead, outputWrite)
63
64 program := shellOrDefault(options.Shell)
65 process, err := createProcess(console, program, options)
66 if err != nil {
67 windows.ClosePseudoConsole(console)
68 closeHandles(inputWrite, outputRead)
69 return nil, err
70 }
71
72 c := &windowsChild{
73 console: console,
74 process: process,
75 input: os.NewFile(uintptr(inputWrite), "pseudo-console input"),
76 output: os.NewFile(uintptr(outputRead), "pseudo-console output"),
77 program: program,
78 }
79 // conhost keeps the output pipe open for as long as the console exists,
80 // whether or not the shell is still running. Without this, a shell that
81 // exited would never give the reader its EOF, and a command run in a
82 // window would never be seen to finish.
83 go c.closeConsoleWhenTheShellExits()
84 return c, nil
85}
86
87// createProcess starts the shell attached to the pseudo-console.
88func createProcess(console windows.Handle, program string, options Options) (windows.Handle, error) {
89 attributes, err := pseudoConsoleAttributes(console)
90 if err != nil {
91 return 0, err
92 }
93 defer attributes.Delete()
94
95 commandLine, directory, err := encodedArguments(program, options)
96 if err != nil {
97 return 0, err
98 }
99 block := environmentBlock(environment(options.Env))
100
101 startup := windows.StartupInfoEx{ProcThreadAttributeList: attributes.List()}
102 startup.Cb = uint32(unsafe.Sizeof(startup))
103 var info windows.ProcessInformation
104
105 flags := uint32(windows.EXTENDED_STARTUPINFO_PRESENT | windows.CREATE_UNICODE_ENVIRONMENT)
106 if err := windows.CreateProcess(nil, commandLine, nil, nil, false, flags, &block[0], directory, &startup.StartupInfo, &info); err != nil {
107 return 0, fmt.Errorf("terminal: starting %s: %w", program, err)
108 }
109 windows.CloseHandle(info.Thread)
110 return info.Process, nil
111}
112
113// pseudoConsoleAttributes builds the one process attribute that attaches the
114// child to the console. The caller deletes it once the process has started.
115func pseudoConsoleAttributes(console windows.Handle) (*windows.ProcThreadAttributeListContainer, error) {
116 attributes, err := windows.NewProcThreadAttributeList(1)
117 if err != nil {
118 return nil, fmt.Errorf("terminal: preparing the process attributes: %w", err)
119 }
120
121 // The attribute's value is the console handle itself, sizeof(HPCON) wide.
122 if ret, _, err := updateProcThreadAttribute.Call(
123 uintptr(unsafe.Pointer(attributes.List())), 0, windows.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
124 uintptr(console), unsafe.Sizeof(console), 0, 0,
125 ); ret == 0 {
126 attributes.Delete()
127 return nil, fmt.Errorf("terminal: attaching the pseudo-console: %w", err)
128 }
129 return attributes, nil
130}
131
132// encodedArguments returns the command line and the working directory as the
133// UTF-16 strings CreateProcess reads; a nil directory means the current one.
134func encodedArguments(program string, options Options) (commandLine, directory *uint16, err error) {
135 commandLine, err = windows.UTF16PtrFromString(windowsCommandLine(program, options.Args))
136 if err != nil {
137 return nil, nil, fmt.Errorf("terminal: encoding the command line: %w", err)
138 }
139 if options.Dir == "" {
140 return commandLine, nil, nil
141 }
142 directory, err = windows.UTF16PtrFromString(options.Dir)
143 if err != nil {
144 return nil, nil, fmt.Errorf("terminal: encoding the directory: %w", err)
145 }
146 return commandLine, directory, nil
147}
148
149// closeConsoleWhenTheShellExits waits for the process and then closes the
150// pseudo-console, which is what makes conhost close its end of the output pipe
151// and the reader see io.EOF.
152func (c *windowsChild) closeConsoleWhenTheShellExits() {
153 _, _ = windows.WaitForSingleObject(c.process, windows.INFINITE)
154 c.closeConsole.Do(func() { windows.ClosePseudoConsole(c.console) })
155}
156
157// coord is a terminal size in the shape ResizePseudoConsole wants.
158func coord(width, height int) windows.Coord {
159 return windows.Coord{X: int16(max(width, 1)), Y: int16(max(height, 1))}
160}
161
162// closeHandles closes what it is given, ignoring handles that are already zero.
163func closeHandles(handles ...windows.Handle) {
164 for _, handle := range handles {
165 if handle != 0 {
166 windows.CloseHandle(handle)
167 }
168 }
169}
170
171// defaultShell returns the shell Windows names as the user's: %COMSPEC%,
172// which is cmd.exe. There is no $SHELL on Windows.
173func defaultShell() string { return windowsShell(os.Getenv("COMSPEC")) }
174
175func (c *windowsChild) Read(b []byte) (int, error) { return c.output.Read(b) }
176func (c *windowsChild) Write(b []byte) (int, error) { return c.input.Write(b) }
177
178// Resize tells conhost the console has changed size; it passes that on to the
179// shell as a window-size event, which is what a full-screen program repaints
180// on.
181func (c *windowsChild) Resize(width, height int) error {
182 return windows.ResizePseudoConsole(c.console, coord(width, height))
183}
184
185// Close ends the shell, the console and the pipes, in that order.
186//
187// The process goes first, so that closing the console does not wait on a
188// shell that ignores the end of its input; the console second, which is what
189// gives the reading goroutine its EOF; the pipes last. Closing twice is safe,
190// because the view closes the session and the waiter may already have closed
191// the console.
192func (c *windowsChild) Close() error {
193 c.closeOnce.Do(func() {
194 _ = windows.TerminateProcess(c.process, 1)
195 _, _ = windows.WaitForSingleObject(c.process, 5000)
196 c.closeConsole.Do(func() { windows.ClosePseudoConsole(c.console) })
197 c.closeErr = c.input.Close()
198 _ = c.output.Close()
199 windows.CloseHandle(c.process)
200 })
201 return c.closeErr
202}
203
204// Command returns the shell's name without its path.
205func (c *windowsChild) Command() string { return programName(c.program) }