turbo-editors/turbo-corepublic Fork 0
v1.0.0
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 v1.0.0 · k33g · 13h ago
pty_windows.go · 205 lines · 7.8 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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
//go:build windows

package terminal

import (
	"fmt"
	"os"
	"sync"
	"unsafe"

	"golang.org/x/sys/windows"
)

// windowsChild is a shell behind a Windows pseudo-console.
//
// A pseudo-console is not a device this process can read and write: it is an
// object owned by conhost.exe, wired to two pipes of ours. What the shell
// prints arrives on the output pipe as the same VT sequences a Unix shell
// writes to a pty, which is the whole reason the emulator on this side needs
// no Windows code at all; what we write to the input pipe reaches the shell
// as keystrokes.
//
// Go's exec cannot start a process attached to a pseudo-console — the
// attribute has to go through an extended STARTUPINFO — so the process is
// created with CreateProcess directly and waited for with a handle.
type windowsChild struct {
	console windows.Handle
	process windows.Handle
	input   *os.File // our end of the shell's standard input
	output  *os.File // our end of the shell's standard output and error
	program string

	closeConsole sync.Once
	closeOnce    sync.Once
	closeErr     error
}

// updateProcThreadAttribute is called by hand rather than through x/sys,
// because the pseudo-console attribute's value is the handle *itself*, and
// x/sys's wrapper takes an unsafe.Pointer — which would mean converting a
// handle to one, the very conversion `go vet` exists to flag.
var updateProcThreadAttribute = windows.NewLazySystemDLL("kernel32.dll").NewProc("UpdateProcThreadAttribute")

// startChild creates a pseudo-console and starts the shell attached to it.
func startChild(options Options) (child, error) {
	var inputRead, inputWrite, outputRead, outputWrite windows.Handle
	if err := windows.CreatePipe(&inputRead, &inputWrite, nil, 0); err != nil {
		return nil, fmt.Errorf("terminal: creating the input pipe: %w", err)
	}
	if err := windows.CreatePipe(&outputRead, &outputWrite, nil, 0); err != nil {
		closeHandles(inputRead, inputWrite)
		return nil, fmt.Errorf("terminal: creating the output pipe: %w", err)
	}

	var console windows.Handle
	size := coord(options.Width, options.Height)
	if err := windows.CreatePseudoConsole(size, inputRead, outputWrite, 0, &console); err != nil {
		closeHandles(inputRead, inputWrite, outputRead, outputWrite)
		return nil, fmt.Errorf("terminal: creating the pseudo-console: %w", err)
	}
	// The console has duplicated the two ends it uses; ours are the other two.
	closeHandles(inputRead, outputWrite)

	program := shellOrDefault(options.Shell)
	process, err := createProcess(console, program, options)
	if err != nil {
		windows.ClosePseudoConsole(console)
		closeHandles(inputWrite, outputRead)
		return nil, err
	}

	c := &windowsChild{
		console: console,
		process: process,
		input:   os.NewFile(uintptr(inputWrite), "pseudo-console input"),
		output:  os.NewFile(uintptr(outputRead), "pseudo-console output"),
		program: program,
	}
	// conhost keeps the output pipe open for as long as the console exists,
	// whether or not the shell is still running. Without this, a shell that
	// exited would never give the reader its EOF, and a command run in a
	// window would never be seen to finish.
	go c.closeConsoleWhenTheShellExits()
	return c, nil
}

// createProcess starts the shell attached to the pseudo-console.
func createProcess(console windows.Handle, program string, options Options) (windows.Handle, error) {
	attributes, err := pseudoConsoleAttributes(console)
	if err != nil {
		return 0, err
	}
	defer attributes.Delete()

	commandLine, directory, err := encodedArguments(program, options)
	if err != nil {
		return 0, err
	}
	block := environmentBlock(environment(options.Env))

	startup := windows.StartupInfoEx{ProcThreadAttributeList: attributes.List()}
	startup.Cb = uint32(unsafe.Sizeof(startup))
	var info windows.ProcessInformation

	flags := uint32(windows.EXTENDED_STARTUPINFO_PRESENT | windows.CREATE_UNICODE_ENVIRONMENT)
	if err := windows.CreateProcess(nil, commandLine, nil, nil, false, flags, &block[0], directory, &startup.StartupInfo, &info); err != nil {
		return 0, fmt.Errorf("terminal: starting %s: %w", program, err)
	}
	windows.CloseHandle(info.Thread)
	return info.Process, nil
}

// pseudoConsoleAttributes builds the one process attribute that attaches the
// child to the console. The caller deletes it once the process has started.
func pseudoConsoleAttributes(console windows.Handle) (*windows.ProcThreadAttributeListContainer, error) {
	attributes, err := windows.NewProcThreadAttributeList(1)
	if err != nil {
		return nil, fmt.Errorf("terminal: preparing the process attributes: %w", err)
	}

	// The attribute's value is the console handle itself, sizeof(HPCON) wide.
	if ret, _, err := updateProcThreadAttribute.Call(
		uintptr(unsafe.Pointer(attributes.List())), 0, windows.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
		uintptr(console), unsafe.Sizeof(console), 0, 0,
	); ret == 0 {
		attributes.Delete()
		return nil, fmt.Errorf("terminal: attaching the pseudo-console: %w", err)
	}
	return attributes, nil
}

// encodedArguments returns the command line and the working directory as the
// UTF-16 strings CreateProcess reads; a nil directory means the current one.
func encodedArguments(program string, options Options) (commandLine, directory *uint16, err error) {
	commandLine, err = windows.UTF16PtrFromString(windowsCommandLine(program, options.Args))
	if err != nil {
		return nil, nil, fmt.Errorf("terminal: encoding the command line: %w", err)
	}
	if options.Dir == "" {
		return commandLine, nil, nil
	}
	directory, err = windows.UTF16PtrFromString(options.Dir)
	if err != nil {
		return nil, nil, fmt.Errorf("terminal: encoding the directory: %w", err)
	}
	return commandLine, directory, nil
}

// closeConsoleWhenTheShellExits waits for the process and then closes the
// pseudo-console, which is what makes conhost close its end of the output pipe
// and the reader see io.EOF.
func (c *windowsChild) closeConsoleWhenTheShellExits() {
	_, _ = windows.WaitForSingleObject(c.process, windows.INFINITE)
	c.closeConsole.Do(func() { windows.ClosePseudoConsole(c.console) })
}

// coord is a terminal size in the shape ResizePseudoConsole wants.
func coord(width, height int) windows.Coord {
	return windows.Coord{X: int16(max(width, 1)), Y: int16(max(height, 1))}
}

// closeHandles closes what it is given, ignoring handles that are already zero.
func closeHandles(handles ...windows.Handle) {
	for _, handle := range handles {
		if handle != 0 {
			windows.CloseHandle(handle)
		}
	}
}

// defaultShell returns the shell Windows names as the user's: %COMSPEC%,
// which is cmd.exe. There is no $SHELL on Windows.
func defaultShell() string { return windowsShell(os.Getenv("COMSPEC")) }

func (c *windowsChild) Read(b []byte) (int, error)  { return c.output.Read(b) }
func (c *windowsChild) Write(b []byte) (int, error) { return c.input.Write(b) }

// Resize tells conhost the console has changed size; it passes that on to the
// shell as a window-size event, which is what a full-screen program repaints
// on.
func (c *windowsChild) Resize(width, height int) error {
	return windows.ResizePseudoConsole(c.console, coord(width, height))
}

// Close ends the shell, the console and the pipes, in that order.
//
// The process goes first, so that closing the console does not wait on a
// shell that ignores the end of its input; the console second, which is what
// gives the reading goroutine its EOF; the pipes last. Closing twice is safe,
// because the view closes the session and the waiter may already have closed
// the console.
func (c *windowsChild) Close() error {
	c.closeOnce.Do(func() {
		_ = windows.TerminateProcess(c.process, 1)
		_, _ = windows.WaitForSingleObject(c.process, 5000)
		c.closeConsole.Do(func() { windows.ClosePseudoConsole(c.console) })
		c.closeErr = c.input.Close()
		_ = c.output.Close()
		windows.CloseHandle(c.process)
	})
	return c.closeErr
}

// Command returns the shell's name without its path.
func (c *windowsChild) Command() string { return programName(c.program) }