-
-
Notifications
You must be signed in to change notification settings - Fork 262
Support pty on windows with ConPty api #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
2097845
Support pty on windows with ConPty api
80786e2
Unify Winsize and InheritSize for all platfoms
e9aa7ec
Pty: Replace io.StringWriter with simple func
5cd3e9a
Embed types and constants from external packages
f0bc158
Move GetSize to winsize
aee4483
Remove underline prefix for fields in WindowsPty
07b10c5
Use inline error check for Setsize in InheritSize
9ac8ea3
docs: Add windows requirements note
295ab95
Fix embedded windows types
83b9ce2
chore: Fix arg name in comment
8eb0e58
chore: Use LazyDLL and LazyProc
f5cfe5c
nit: Workaround go1.17 linkname relocation
294ecca
fix(conpty): Fix windows conpty syscall handling
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| //go:build windows | ||
| // +build windows | ||
|
|
||
| package pty | ||
|
|
||
| import ( | ||
| "context" | ||
| "io" | ||
| "os" | ||
| "syscall" | ||
|
|
||
| _ "unsafe" // for go:linkname | ||
| ) | ||
|
|
||
| // copied from os/exec.Cmd for platform compatibility | ||
| // we need to use startupInfoEx for pty support, but os/exec.Cmd only have | ||
| // support for startupInfo on windows, so we have to rewrite some internal | ||
| // logic for windows while keep its behavior compatible with other platforms. | ||
|
|
||
| // cmd represents an external command being prepared or run. | ||
| // | ||
| // A cmd cannot be reused after calling its Run, Output or CombinedOutput | ||
| // methods. | ||
| //go:linkname cmd os/exec.Cmd | ||
| type cmd struct { | ||
| // Path is the path of the command to run. | ||
| // | ||
| // This is the only field that must be set to a non-zero | ||
| // value. If Path is relative, it is evaluated relative | ||
| // to Dir. | ||
| Path string | ||
|
|
||
| // Args holds command line arguments, including the command as Args[0]. | ||
| // If the Args field is empty or nil, Run uses {Path}. | ||
| // | ||
| // In typical use, both Path and Args are set by calling Command. | ||
| Args []string | ||
|
|
||
| // Env specifies the environment of the process. | ||
| // Each entry is of the form "key=value". | ||
| // If Env is nil, the new process uses the current process's | ||
| // environment. | ||
| // If Env contains duplicate environment keys, only the last | ||
| // value in the slice for each duplicate key is used. | ||
| // As a special case on Windows, SYSTEMROOT is always added if | ||
| // missing and not explicitly set to the empty string. | ||
| Env []string | ||
|
|
||
| // Dir specifies the working directory of the command. | ||
| // If Dir is the empty string, Run runs the command in the | ||
| // calling process's current directory. | ||
| Dir string | ||
|
|
||
| // Stdin specifies the process's standard input. | ||
| // | ||
| // If Stdin is nil, the process reads from the null device (os.DevNull). | ||
| // | ||
| // If Stdin is an *os.File, the process's standard input is connected | ||
| // directly to that file. | ||
| // | ||
| // Otherwise, during the execution of the command a separate | ||
| // goroutine reads from Stdin and delivers that data to the command | ||
| // over a pipe. In this case, Wait does not complete until the goroutine | ||
| // stops copying, either because it has reached the end of Stdin | ||
| // (EOF or a read error) or because writing to the pipe returned an error. | ||
| Stdin io.Reader | ||
|
|
||
| // Stdout and Stderr specify the process's standard output and error. | ||
| // | ||
| // If either is nil, Run connects the corresponding file descriptor | ||
| // to the null device (os.DevNull). | ||
| // | ||
| // If either is an *os.File, the corresponding output from the process | ||
| // is connected directly to that file. | ||
| // | ||
| // Otherwise, during the execution of the command a separate goroutine | ||
| // reads from the process over a pipe and delivers that data to the | ||
| // corresponding Writer. In this case, Wait does not complete until the | ||
| // goroutine reaches EOF or encounters an error. | ||
| // | ||
| // If Stdout and Stderr are the same writer, and have a type that can | ||
| // be compared with ==, at most one goroutine at a time will call Write. | ||
| Stdout io.Writer | ||
| Stderr io.Writer | ||
|
|
||
| // ExtraFiles specifies additional open files to be inherited by the | ||
| // new process. It does not include standard input, standard output, or | ||
| // standard error. If non-nil, entry i becomes file descriptor 3+i. | ||
| // | ||
| // ExtraFiles is not supported on Windows. | ||
| ExtraFiles []*os.File | ||
|
|
||
| // SysProcAttr holds optional, operating system-specific attributes. | ||
| // Run passes it to os.StartProcess as the os.ProcAttr's Sys field. | ||
| SysProcAttr *syscall.SysProcAttr | ||
|
|
||
| // Process is the underlying process, once started. | ||
| Process *os.Process | ||
|
|
||
| // ProcessState contains information about an exited process, | ||
| // available after a call to Wait or Run. | ||
| ProcessState *os.ProcessState | ||
|
|
||
| ctx context.Context // nil means none | ||
| lookPathErr error // LookPath error, if any. | ||
| finished bool // when Wait was called | ||
| childFiles []*os.File | ||
| closeAfterStart []io.Closer | ||
| closeAfterWait []io.Closer | ||
| goroutine []func() error | ||
| errch chan error // one send per goroutine | ||
| waitDone chan struct{} | ||
| } | ||
|
|
||
| //go:linkname _cmd_closeDescriptors os/exec.(*Cmd).closeDescriptors | ||
| func _cmd_closeDescriptors(c *cmd, closers []io.Closer) | ||
|
|
||
| //go:linkname _cmd_envv os/exec.(*Cmd).envv | ||
| func _cmd_envv(c *cmd) ([]string, error) | ||
|
|
||
| //go:linkname _cmd_argv os/exec.(*Cmd).argv | ||
| func _cmd_argv(c *cmd) []string | ||
|
|
||
| //go:linkname lookExtensions os/exec.lookExtensions | ||
| func lookExtensions(path, dir string) (string, error) | ||
|
|
||
| //go:linkname dedupEnv os/exec.dedupEnv | ||
| func dedupEnv(env []string) []string | ||
|
|
||
| //go:linkname addCriticalEnv os/exec.addCriticalEnv | ||
| func addCriticalEnv(env []string) []string | ||
|
|
||
| //go:linkname newProcess os.newProcess | ||
| func newProcess(pid int, handle uintptr) *os.Process | ||
|
|
||
| //go:linkname execEnvDefault internal/syscall/execenv.Default | ||
| func execEnvDefault(sys *syscall.SysProcAttr) (env []string, err error) | ||
|
|
||
| //go:linkname createEnvBlock syscall.createEnvBlock | ||
| func createEnvBlock(envv []string) *uint16 | ||
|
|
||
| //go:linkname makeCmdLine syscall.makeCmdLine | ||
| func makeCmdLine(args []string) string | ||
|
|
||
| //go:linkname joinExeDirAndFName syscall.joinExeDirAndFName | ||
| func joinExeDirAndFName(dir, p string) (name string, err error) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // disable default -complete option to `go tool compile` on windows | ||
|
|
||
| // We are using `go:linkname` to support golang os/exec.Cmd for extended | ||
| // windows process creation (startupInfoEx), and that is not supported by its | ||
| // standard library implementation. | ||
|
|
||
| // By default, the go compiler will require all functions in *.go files | ||
| // with body defined, if that's not the case, we have to enable CGO to | ||
| // enable symbol lookup. One solution to disable that compile time check | ||
| // is to add some go assembly file to your project. | ||
|
|
||
| // For this project, we don't use CGO at all, and should not require users | ||
| // to set `CGO_ENABLED=1` when compiling their projects using this package. | ||
|
|
||
| // By adding this empty assembly file, the go compiler will enable symbol | ||
| // lookup, so that we can have functions with no body defined in *.go files. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| //go:build windows | ||
| // +build windows | ||
|
|
||
| package pty | ||
|
|
||
| import ( | ||
| "os" | ||
| "syscall" | ||
| "unsafe" | ||
| ) | ||
|
|
||
| var ( | ||
|
This conversation was marked as resolved.
|
||
| // NOTE(security): as noted by the comment of syscall.NewLazyDLL and syscall.LoadDLL | ||
| // user need to call internal/syscall/windows/sysdll.Add("kernel32.dll") to make sure | ||
| // the kernel32.dll is loaded from windows system path | ||
| // | ||
| // ref: https://pkg.go.dev/syscall@go1.13?GOOS=windows#LoadDLL | ||
| kernel32DLL = syscall.NewLazyDLL("kernel32.dll") | ||
|
|
||
| // https://docs.microsoft.com/en-us/windows/console/createpseudoconsole | ||
| createPseudoConsole = kernel32DLL.NewProc("CreatePseudoConsole") | ||
| closePseudoConsole = kernel32DLL.NewProc("ClosePseudoConsole") | ||
|
|
||
| deleteProcThreadAttributeList = kernel32DLL.NewProc("DeleteProcThreadAttributeList") | ||
| initializeProcThreadAttributeList = kernel32DLL.NewProc("InitializeProcThreadAttributeList") | ||
|
|
||
| // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute | ||
| updateProcThreadAttribute = kernel32DLL.NewProc("UpdateProcThreadAttribute") | ||
|
|
||
| resizePseudoConsole = kernel32DLL.NewProc("ResizePseudoConsole") | ||
| getConsoleScreenBufferInfo = kernel32DLL.NewProc("GetConsoleScreenBufferInfo") | ||
| ) | ||
|
|
||
| func open() (_ Pty, _ Tty, err error) { | ||
| pr, consoleW, err := os.Pipe() | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| consoleR, pw, err := os.Pipe() | ||
| if err != nil { | ||
| _ = consoleW.Close() | ||
| _ = pr.Close() | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| defer func() { | ||
| if err != nil { | ||
| _ = consoleW.Close() | ||
| _ = pr.Close() | ||
|
|
||
| _ = pw.Close() | ||
| _ = consoleR.Close() | ||
| } | ||
| }() | ||
|
|
||
| err = createPseudoConsole.Find() | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| var consoleHandle syscall.Handle | ||
| r1, _, err := createPseudoConsole.Call( | ||
| (windowsCoord{X: 80, Y: 30}).Pack(), // size: default 80x30 window | ||
| consoleR.Fd(), // console input | ||
| consoleW.Fd(), // console output | ||
| 0, // console flags, currently only PSEUDOCONSOLE_INHERIT_CURSOR supported | ||
| uintptr(unsafe.Pointer(&consoleHandle)), // console handler value return | ||
| ) | ||
| if r1 != 0 { | ||
| // S_OK: 0 | ||
| return nil, nil, os.NewSyscallError("CreatePseudoConsole", err) | ||
| } | ||
|
|
||
| return &WindowsPty{ | ||
| handle: uintptr(consoleHandle), | ||
| r: pr, | ||
| w: pw, | ||
| consoleR: consoleR, | ||
| consoleW: consoleW, | ||
| }, &WindowsTty{ | ||
| handle: uintptr(consoleHandle), | ||
| r: consoleR, | ||
| w: consoleW, | ||
| }, nil | ||
| } | ||
|
|
||
| var _ Pty = (*WindowsPty)(nil) | ||
|
|
||
| type WindowsPty struct { | ||
| handle uintptr | ||
| r, w *os.File | ||
|
|
||
| consoleR, consoleW *os.File | ||
| } | ||
|
|
||
| func (p *WindowsPty) Fd() uintptr { | ||
| return p.handle | ||
| } | ||
|
|
||
| func (p *WindowsPty) Read(data []byte) (int, error) { | ||
| return p.r.Read(data) | ||
| } | ||
|
|
||
| func (p *WindowsPty) Write(data []byte) (int, error) { | ||
| return p.w.Write(data) | ||
| } | ||
|
|
||
| func (p *WindowsPty) WriteString(s string) (int, error) { | ||
| return p.w.WriteString(s) | ||
| } | ||
|
|
||
| func (p *WindowsPty) InputPipe() *os.File { | ||
| return p.w | ||
| } | ||
|
|
||
| func (p *WindowsPty) OutputPipe() *os.File { | ||
| return p.r | ||
| } | ||
|
|
||
| func (p *WindowsPty) Close() error { | ||
| _ = p.r.Close() | ||
| _ = p.w.Close() | ||
|
|
||
| _ = p.consoleR.Close() | ||
| _ = p.consoleW.Close() | ||
|
|
||
| err := closePseudoConsole.Find() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| _, _, err = closePseudoConsole.Call(p.handle) | ||
| return err | ||
| } | ||
|
|
||
| var _ Tty = (*WindowsTty)(nil) | ||
|
|
||
| type WindowsTty struct { | ||
| handle uintptr | ||
| r, w *os.File | ||
| } | ||
|
|
||
| func (t *WindowsTty) Fd() uintptr { | ||
| return t.handle | ||
| } | ||
|
|
||
| func (t *WindowsTty) Read(p []byte) (int, error) { | ||
| return t.r.Read(p) | ||
| } | ||
|
|
||
| func (t *WindowsTty) Write(p []byte) (int, error) { | ||
| return t.w.Write(p) | ||
| } | ||
|
|
||
| func (t *WindowsTty) Close() error { | ||
| _ = t.r.Close() | ||
| return t.w.Close() | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.