//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) }