From 2097845ab84e6e9d7caf1f94d042baf0aefdfd63 Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Tue, 24 Nov 2020 03:40:23 +0100 Subject: [PATCH 01/13] Support pty on windows with ConPty api This is an attempt to support pseudo-terminal on windows, and introduces major api change for platform compatibility. Signed-off-by: Jeffrey Stoke --- README.md | 2 +- cmd_windows.go | 146 ++++++++++++++ cmd_windows.s | 16 ++ doc.go | 32 +++- go.mod | 2 + go.sum | 2 + pty_unsupported.go | 4 +- pty_windows.go | 157 +++++++++++++++ run.go | 69 +------ run_unix.go | 69 +++++++ run_windows.go | 419 +++++++++++++++++++++++++++++++++++++++++ test_crosscompile.sh | 4 +- winsize.go | 24 --- winsize_unix.go | 26 ++- winsize_unsupported.go | 23 --- winsize_windows.go | 66 +++++++ 16 files changed, 938 insertions(+), 123 deletions(-) create mode 100644 cmd_windows.go create mode 100644 cmd_windows.s create mode 100644 go.sum create mode 100644 pty_windows.go create mode 100644 run_unix.go create mode 100644 run_windows.go delete mode 100644 winsize.go delete mode 100644 winsize_unsupported.go create mode 100644 winsize_windows.go diff --git a/README.md b/README.md index a4fe767..214f0f1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pty -Pty is a Go package for using unix pseudo-terminals. +Pty is a Go package for using unix pseudo-terminals and windows ConPty. ## Install diff --git a/cmd_windows.go b/cmd_windows.go new file mode 100644 index 0000000..ddbf885 --- /dev/null +++ b/cmd_windows.go @@ -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 (c *cmd) closeDescriptors(closers []io.Closer) + +//go:linkname (*cmd).envv os/exec.(*Cmd).envv +func (c *cmd) envv() ([]string, error) + +//go:linkname (*cmd).argv os/exec.(*Cmd).argv +func (c *cmd) argv() []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) diff --git a/cmd_windows.s b/cmd_windows.s new file mode 100644 index 0000000..caddc3c --- /dev/null +++ b/cmd_windows.s @@ -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. diff --git a/doc.go b/doc.go index 3c8b324..ef3a2c9 100644 --- a/doc.go +++ b/doc.go @@ -3,7 +3,7 @@ package pty import ( "errors" - "os" + "io" ) // ErrUnsupported is returned if a function is not @@ -11,6 +11,34 @@ import ( var ErrUnsupported = errors.New("unsupported") // Open a pty and its corresponding tty. -func Open() (pty, tty *os.File, err error) { +func Open() (Pty, Tty, error) { return open() } + +type ( + FdHolder interface { + Fd() uintptr + } + + // Pty for terminal control in current process + // for unix systems, the real type is *os.File + // for windows, the real type is a *WindowsPty for ConPTY handle + Pty interface { + // Fd intended to resize Tty of child process in current process + FdHolder + + // string writer is used to identify Pty and Tty + io.StringWriter + io.ReadWriteCloser + } + + // Tty for data i/o in child process + // for unix systems, the real type is *os.File + // for windows, the real type is a *WindowsTty, which is a combination of two pipe file + Tty interface { + // Fd only intended for manual InheritSize from Pty + FdHolder + + io.ReadWriteCloser + } +) diff --git a/go.mod b/go.mod index 7731235..91a1c03 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/creack/pty go 1.13 + +require golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..750c835 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI= +golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pty_unsupported.go b/pty_unsupported.go index 765523a..fb666df 100644 --- a/pty_unsupported.go +++ b/pty_unsupported.go @@ -1,5 +1,5 @@ -//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris -//+build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris +//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris && !windows +//+build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris,!windows package pty diff --git a/pty_windows.go b/pty_windows.go new file mode 100644 index 0000000..9fa7294 --- /dev/null +++ b/pty_windows.go @@ -0,0 +1,157 @@ +//go:build windows +//+build windows + +package pty + +import ( + "os" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + kernel32DLL *syscall.DLL +) + +func init() { + var err error + kernel32DLL, err = syscall.LoadDLL("kernel32.dll") + if err != nil { + panic(err) + } +} + +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() + } + }() + + var ( + consoleHandle syscall.Handle + defaultSize = &windows.Coord{X: 80, Y: 30} + ) + + // https://docs.microsoft.com/en-us/windows/console/createpseudoconsole + + createPseudoConsole, err := kernel32DLL.FindProc("CreatePseudoConsole") + if err != nil { + return nil, nil, os.NewSyscallError("CreatePseudoConsole", err) + } + + r1, _, err := createPseudoConsole.Call( + uintptr(unsafe.Pointer(defaultSize)), // 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() + + closePseudoConsole, err := kernel32DLL.FindProc("ClosePseudoConsole") + if err != nil { + return os.NewSyscallError("ClosePseudoConsole", 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() +} diff --git a/run.go b/run.go index 160001f..0ef9e0e 100644 --- a/run.go +++ b/run.go @@ -1,75 +1,14 @@ -//go:build !windows -//+build !windows - package pty import ( - "os" "os/exec" - "syscall" ) -// Start assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout, +// Start assigns a pseudo-terminal Tty to c.Stdin, c.Stdout, // and c.Stderr, calls c.Start, and returns the File of the tty's -// corresponding pty. +// corresponding Pty. // // Starts the process in a new session and sets the controlling terminal. -func Start(cmd *exec.Cmd) (*os.File, error) { - return StartWithSize(cmd, nil) -} - -// StartWithSize assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout, -// and c.Stderr, calls c.Start, and returns the File of the tty's -// corresponding pty. -// -// This will resize the pty to the specified size before starting the command. -// Starts the process in a new session and sets the controlling terminal. -func StartWithSize(cmd *exec.Cmd, ws *Winsize) (*os.File, error) { - if cmd.SysProcAttr == nil { - cmd.SysProcAttr = &syscall.SysProcAttr{} - } - cmd.SysProcAttr.Setsid = true - cmd.SysProcAttr.Setctty = true - return StartWithAttrs(cmd, ws, cmd.SysProcAttr) -} - -// StartWithAttrs assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout, -// and c.Stderr, calls c.Start, and returns the File of the tty's -// corresponding pty. -// -// This will resize the pty to the specified size before starting the command if a size is provided. -// The `attrs` parameter overrides the one set in c.SysProcAttr. -// -// This should generally not be needed. Used in some edge cases where it is needed to create a pty -// without a controlling terminal. -func StartWithAttrs(c *exec.Cmd, sz *Winsize, attrs *syscall.SysProcAttr) (*os.File, error) { - pty, tty, err := Open() - if err != nil { - return nil, err - } - defer func() { _ = tty.Close() }() // Best effort. - - if sz != nil { - if err := Setsize(pty, sz); err != nil { - _ = pty.Close() // Best effort. - return nil, err - } - } - if c.Stdout == nil { - c.Stdout = tty - } - if c.Stderr == nil { - c.Stderr = tty - } - if c.Stdin == nil { - c.Stdin = tty - } - - c.SysProcAttr = attrs - - if err := c.Start(); err != nil { - _ = pty.Close() // Best effort. - return nil, err - } - return pty, err +func Start(c *exec.Cmd) (pty Pty, err error) { + return StartWithSize(c, nil) } diff --git a/run_unix.go b/run_unix.go new file mode 100644 index 0000000..2b56db4 --- /dev/null +++ b/run_unix.go @@ -0,0 +1,69 @@ +//go:build !windows +//+build !windows + +package pty + +import ( + "os/exec" + "syscall" +) + +// StartWithSize assigns a pseudo-terminal Tty to c.Stdin, c.Stdout, +// and c.Stderr, calls c.Start, and returns the File of the tty's +// corresponding Pty. +// +// This will resize the Pty to the specified size before starting the command. +// Starts the process in a new session and sets the controlling terminal. +func StartWithSize(c *exec.Cmd, sz *Winsize) (Pty, error) { + if c.SysProcAttr == nil { + c.SysProcAttr = &syscall.SysProcAttr{} + } + c.SysProcAttr.Setsid = true + c.SysProcAttr.Setctty = true + return StartWithAttrs(c, sz, c.SysProcAttr) +} + +// StartWithAttrs assigns a pseudo-terminal Tty to c.Stdin, c.Stdout, +// and c.Stderr, calls c.Start, and returns the File of the tty's +// corresponding Pty. +// +// This will resize the Pty to the specified size before starting the command if a size is provided. +// The `attrs` parameter overrides the one set in c.SysProcAttr. +// +// This should generally not be needed. Used in some edge cases where it is needed to create a pty +// without a controlling terminal. +func StartWithAttrs(c *exec.Cmd, sz *Winsize, attrs *syscall.SysProcAttr) (Pty, error) { + pty, tty, err := open() + if err != nil { + return nil, err + } + defer func() { + // always close tty fds since it's being used in another process + // but pty is kept to resize tty + _ = tty.Close() + }() + + if sz != nil { + if err := Setsize(pty, sz); err != nil { + _ = pty.Close() + return nil, err + } + } + if c.Stdout == nil { + c.Stdout = tty + } + if c.Stderr == nil { + c.Stderr = tty + } + if c.Stdin == nil { + c.Stdin = tty + } + + c.SysProcAttr = attrs + + if err := c.Start(); err != nil { + _ = pty.Close() + return nil, err + } + return pty, err +} diff --git a/run_windows.go b/run_windows.go new file mode 100644 index 0000000..51fcd9f --- /dev/null +++ b/run_windows.go @@ -0,0 +1,419 @@ +//go:build windows +//+build windows + +package pty + +import ( + "errors" + "os" + "os/exec" + "runtime" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +type startupInfoEx struct { + startupInfo syscall.StartupInfo + lpAttrList syscall.Handle +} + +const ( + _PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016 +) + +// StartWithSize assigns a pseudo-terminal Tty to c.Stdin, c.Stdout, +// and c.Stderr, calls c.Start, and returns the File of the tty's +// corresponding Pty. +// +// This will resize the Pty to the specified size before starting the command. +// Starts the process in a new session and sets the controlling terminal. +func StartWithSize(c *exec.Cmd, sz *Winsize) (Pty, error) { + return StartWithAttrs(c, sz, c.SysProcAttr) +} + +// StartWithAttrs assigns a pseudo-terminal Tty to c.Stdin, c.Stdout, +// and c.Stderr, calls c.Start, and returns the File of the tty's +// corresponding Pty. +// +// This will resize the Pty to the specified size before starting the command if a size is provided. +// The `attrs` parameter overrides the one set in c.SysProcAttr. +// +// This should generally not be needed. Used in some edge cases where it is needed to create a pty +// without a controlling terminal. +func StartWithAttrs(c *exec.Cmd, sz *Winsize, attrs *syscall.SysProcAttr) (_ Pty, err error) { + pty, tty, err := open() + if err != nil { + return nil, err + } + + defer func() { + // unlike unix command exec, do not close tty unless error happened + if err != nil { + _ = tty.Close() + _ = pty.Close() + } + }() + + if sz != nil { + if err = Setsize(pty, sz); err != nil { + return nil, err + } + } + + // unlike unix command exec, do not set stdin/stdout/stderr + + c.SysProcAttr = attrs + + // do not use os/exec.Start since we need to append console handler to startup info + + err = start((*cmd)(unsafe.Pointer(c)), syscall.Handle(tty.Fd())) + if err != nil { + return nil, err + } + + return pty, err +} + +func createExtendedStartupInfo(consoleHandle syscall.Handle) (_ *startupInfoEx, err error) { + deleteProcThreadAttributeList, err := kernel32DLL.FindProc("DeleteProcThreadAttributeList") + if err != nil { + return nil, os.NewSyscallError("DeleteProcThreadAttributeList", err) + } + + // append console handler to new process + var ( + attrBufSize uint64 + si startupInfoEx + ) + + si.startupInfo.Cb = uint32(unsafe.Sizeof(si)) + + // get size of attr list + initializeProcThreadAttributeList, err := kernel32DLL.FindProc("InitializeProcThreadAttributeList") + if err != nil { + return nil, os.NewSyscallError("InitializeProcThreadAttributeList", err) + } + + r1, _, err := initializeProcThreadAttributeList.Call( + 0, // list ptr + 1, // list item count + 0, // dwFlags: reserved, MUST be 0 + uintptr(unsafe.Pointer(&attrBufSize)), + ) + if r1 == 0 { + // according to + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-initializeprocthreadattributelist + // which says: This initial call will return an error by design. This is expected behavior. + // + // so here we check the returned value of the attr buf size, if it's zero, we cannot update attribute list + if attrBufSize == 0 { + return nil, os.NewSyscallError("InitializeProcThreadAttributeList (size)", err) + } + } + + attrListBuf := make([]byte, attrBufSize) + si.lpAttrList = syscall.Handle(unsafe.Pointer(&attrListBuf[0])) + // create attr list with console handler + r1, _, err = initializeProcThreadAttributeList.Call( + uintptr(si.lpAttrList), // attr list buf + 1, // list item count + 0, // dwFlags: reserved, MUST be 0 + uintptr(unsafe.Pointer(&attrBufSize)), // size of the list + ) + if r1 == 0 { + // false + return nil, os.NewSyscallError("InitializeProcThreadAttributeList (create)", err) + } + + // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute + updateProcThreadAttribute, err := kernel32DLL.FindProc("UpdateProcThreadAttribute") + if err != nil { + return nil, os.NewSyscallError("UpdateProcThreadAttribute", err) + } + + r1, _, err = updateProcThreadAttribute.Call( + uintptr(si.lpAttrList), // buf list + 0, // dwFlags: reserved, MUST be 0 + _PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, + uintptr(consoleHandle), + unsafe.Sizeof(consoleHandle), + 0, + 0, + ) + if r1 == 0 { + // false + _, _, _ = deleteProcThreadAttributeList.Call(uintptr(si.lpAttrList)) + return nil, os.NewSyscallError("UpdateProcThreadAttribute", err) + } + + return &si, nil +} + +// copied from os/exec.(*Cmd).Start +// start starts the specified command but does not wait for it to complete. +// +// If Start returns successfully, the c.Process field will be set. +// +// The Wait method will return the exit code and release associated resources +// once the command exits. +func start(c *cmd, consoleHandle syscall.Handle) error { + if c.lookPathErr != nil { + c.closeDescriptors(c.closeAfterStart) + c.closeDescriptors(c.closeAfterWait) + return c.lookPathErr + } + if runtime.GOOS == "windows" { + lp, err := lookExtensions(c.Path, c.Dir) + if err != nil { + c.closeDescriptors(c.closeAfterStart) + c.closeDescriptors(c.closeAfterWait) + return err + } + c.Path = lp + } + if c.Process != nil { + return errors.New("exec: already started") + } + if c.ctx != nil { + select { + case <-c.ctx.Done(): + c.closeDescriptors(c.closeAfterStart) + c.closeDescriptors(c.closeAfterWait) + return c.ctx.Err() + default: + } + } + + //c.childFiles = make([]*os.File, 0, 3+len(c.ExtraFiles)) + //type F func() (*os.File, error) + //for _, setupFd := range []F{c.stdin, c.stdout, c.stderr} { + // fd, err := setupFd() + // if err != nil { + // c.closeDescriptors(c.closeAfterStart) + // c.closeDescriptors(c.closeAfterWait) + // return err + // } + // c.childFiles = append(c.childFiles, fd) + //} + //c.childFiles = append(c.childFiles, c.ExtraFiles...) + + envv, err := c.envv() + if err != nil { + return err + } + + c.Process, err = startProcess(c.Path, c.argv(), &os.ProcAttr{ + Dir: c.Dir, + Files: c.childFiles, + Env: addCriticalEnv(dedupEnv(envv)), + Sys: c.SysProcAttr, + }, consoleHandle) + if err != nil { + c.closeDescriptors(c.closeAfterStart) + c.closeDescriptors(c.closeAfterWait) + return err + } + + c.closeDescriptors(c.closeAfterStart) + + // Don't allocate the channel unless there are goroutines to fire. + if len(c.goroutine) > 0 { + c.errch = make(chan error, len(c.goroutine)) + for _, fn := range c.goroutine { + go func(fn func() error) { + c.errch <- fn() + }(fn) + } + } + + if c.ctx != nil { + c.waitDone = make(chan struct{}) + go func() { + select { + case <-c.ctx.Done(): + _ = c.Process.Kill() + case <-c.waitDone: + } + }() + } + + return nil +} + +// copied from os.startProcess, add consoleHandle arg +func startProcess(name string, argv []string, attr *os.ProcAttr, consoleHandle syscall.Handle) (p *os.Process, err error) { + // If there is no SysProcAttr (ie. no Chroot or changed + // UID/GID), double-check existence of the directory we want + // to chdir into. We can make the error clearer this way. + if attr != nil && attr.Sys == nil && attr.Dir != "" { + if _, err := os.Stat(attr.Dir); err != nil { + pe := err.(*os.PathError) + pe.Op = "chdir" + return nil, pe + } + } + + sysattr := &syscall.ProcAttr{ + Dir: attr.Dir, + Env: attr.Env, + Sys: attr.Sys, + } + if sysattr.Env == nil { + sysattr.Env, err = execEnvDefault(sysattr.Sys) + if err != nil { + return nil, err + } + } + sysattr.Files = make([]uintptr, 0, len(attr.Files)) + for _, f := range attr.Files { + sysattr.Files = append(sysattr.Files, f.Fd()) + } + + pid, h, e := syscallStartProcess(name, argv, sysattr, consoleHandle) + + // Make sure we don't run the finalizers of attr.Files. + runtime.KeepAlive(attr) + + if e != nil { + return nil, &os.PathError{"fork/exec", name, e} + } + + return newProcess(pid, h), nil +} + +//go:linkname zeroProcAttr syscall.zeroProcAttr +var zeroProcAttr syscall.ProcAttr + +//go:linkname zeroSysProcAttr syscall.zeroSysProcAttr +var zeroSysProcAttr syscall.SysProcAttr + +// copied from syscall.StartProcess, add consoleHandle arg +func syscallStartProcess(argv0 string, argv []string, attr *syscall.ProcAttr, consoleHandle syscall.Handle) (pid int, handle uintptr, err error) { + deleteProcThreadAttributeList, err := kernel32DLL.FindProc("DeleteProcThreadAttributeList") + if err != nil { + return 0, 0, os.NewSyscallError("DeleteProcThreadAttributeList", err) + } + + if len(argv0) == 0 { + return 0, 0, syscall.EWINDOWS + } + if attr == nil { + attr = &zeroProcAttr + } + sys := attr.Sys + if sys == nil { + sys = &zeroSysProcAttr + } + + //if len(attr.Files) > 3 { + // return 0, 0, syscall.EWINDOWS + //} + //if len(attr.Files) < 3 { + // return 0, 0, syscall.EINVAL + //} + + if len(attr.Dir) != 0 { + // StartProcess assumes that argv0 is relative to attr.Dir, + // because it implies Chdir(attr.Dir) before executing argv0. + // Windows CreateProcess assumes the opposite: it looks for + // argv0 relative to the current directory, and, only once the new + // process is started, it does Chdir(attr.Dir). We are adjusting + // for that difference here by making argv0 absolute. + var err error + argv0, err = joinExeDirAndFName(attr.Dir, argv0) + if err != nil { + return 0, 0, err + } + } + argv0p, err := syscall.UTF16PtrFromString(argv0) + if err != nil { + return 0, 0, err + } + + var cmdline string + // Windows CreateProcess takes the command line as a single string: + // use attr.CmdLine if set, else build the command line by escaping + // and joining each argument with spaces + if sys.CmdLine != "" { + cmdline = sys.CmdLine + } else { + cmdline = makeCmdLine(argv) + } + + var argvp *uint16 + if len(cmdline) != 0 { + argvp, err = syscall.UTF16PtrFromString(cmdline) + if err != nil { + return 0, 0, err + } + } + + var dirp *uint16 + if len(attr.Dir) != 0 { + dirp, err = syscall.UTF16PtrFromString(attr.Dir) + if err != nil { + return 0, 0, err + } + } + + // Acquire the fork lock so that no other threads + // create new fds that are not yet close-on-exec + // before we fork. + syscall.ForkLock.Lock() + defer syscall.ForkLock.Unlock() + + //p, _ := syscall.GetCurrentProcess() + //fd := make([]syscall.Handle, len(attr.Files)) + //for i := range attr.Files { + // if attr.Files[i] > 0 { + // err := syscall.DuplicateHandle(p, syscall.Handle(attr.Files[i]), p, &fd[i], 0, true, syscall.DUPLICATE_SAME_ACCESS) + // if err != nil { + // return 0, 0, err + // } + // defer syscall.CloseHandle(syscall.Handle(fd[i])) + // } + //} + + // replaced default syscall.StartupInfo with custom startupInfEx for console handle + //si := new(syscall.StartupInfo) + //si.Cb = uint32(unsafe.Sizeof(*si)) + si, err := createExtendedStartupInfo(consoleHandle) + if err != nil { + return 0, 0, err + } + // add finalizer for attribute list cleanup, best effort + runtime.SetFinalizer(si, func(si *startupInfoEx) { + _, _, _ = deleteProcThreadAttributeList.Call(uintptr(si.lpAttrList)) + }) + + si.startupInfo.Flags = syscall.STARTF_USESTDHANDLES + if sys.HideWindow { + si.startupInfo.Flags |= syscall.STARTF_USESHOWWINDOW + si.startupInfo.ShowWindow = syscall.SW_HIDE + } + //si.StdInput = fd[0] + //si.StdOutput = fd[1] + //si.StdErr = fd[2] + + pi := new(syscall.ProcessInformation) + + flags := sys.CreationFlags | syscall.CREATE_UNICODE_ENVIRONMENT + + // add startupInfoEx flag + flags = flags | windows.EXTENDED_STARTUPINFO_PRESENT + + // ignore security attrs since both Process and Thread handles are not inheritable for conPty + if sys.Token != 0 { + err = syscall.CreateProcessAsUser(sys.Token, argv0p, argvp, nil, nil, false, flags, createEnvBlock(attr.Env), dirp, &si.startupInfo, pi) + } else { + err = syscall.CreateProcess(argv0p, argvp, nil, nil, false, flags, createEnvBlock(attr.Env), dirp, &si.startupInfo, pi) + } + if err != nil { + return 0, 0, err + } + defer syscall.CloseHandle(syscall.Handle(pi.Thread)) + + return int(pi.ProcessId), uintptr(pi.Process), nil +} diff --git a/test_crosscompile.sh b/test_crosscompile.sh index bbab6b2..3078787 100755 --- a/test_crosscompile.sh +++ b/test_crosscompile.sh @@ -32,9 +32,7 @@ cross netbsd amd64 386 arm arm64 cross openbsd amd64 386 arm arm64 cross dragonfly amd64 cross solaris amd64 - -# Not expected to work but should still compile. -cross windows amd64 386 arm +cross windows amd64 386 arm # TODO: Fix compilation error on openbsd/arm. # TODO: Merge the solaris PR. diff --git a/winsize.go b/winsize.go deleted file mode 100644 index 9660a93..0000000 --- a/winsize.go +++ /dev/null @@ -1,24 +0,0 @@ -package pty - -import "os" - -// InheritSize applies the terminal size of pty to tty. This should be run -// in a signal handler for syscall.SIGWINCH to automatically resize the tty when -// the pty receives a window size change notification. -func InheritSize(pty, tty *os.File) error { - size, err := GetsizeFull(pty) - if err != nil { - return err - } - if err := Setsize(tty, size); err != nil { - return err - } - return nil -} - -// Getsize returns the number of rows (lines) and cols (positions -// in each line) in terminal t. -func Getsize(t *os.File) (rows, cols int, err error) { - ws, err := GetsizeFull(t) - return int(ws.Rows), int(ws.Cols), err -} diff --git a/winsize_unix.go b/winsize_unix.go index f358e90..5968260 100644 --- a/winsize_unix.go +++ b/winsize_unix.go @@ -4,7 +4,6 @@ package pty import ( - "os" "syscall" "unsafe" ) @@ -18,13 +17,13 @@ type Winsize struct { } // Setsize resizes t to s. -func Setsize(t *os.File, ws *Winsize) error { +func Setsize(t FdHolder, ws *Winsize) error { //nolint:gosec // Expected unsafe pointer for Syscall call. return ioctl(t.Fd(), syscall.TIOCSWINSZ, uintptr(unsafe.Pointer(ws))) } // GetsizeFull returns the full terminal size description. -func GetsizeFull(t *os.File) (size *Winsize, err error) { +func GetsizeFull(t FdHolder) (size *Winsize, err error) { var ws Winsize //nolint:gosec // Expected unsafe pointer for Syscall call. @@ -33,3 +32,24 @@ func GetsizeFull(t *os.File) (size *Winsize, err error) { } return &ws, nil } + +// InheritSize applies the terminal size of pty to tty. This should be run +// in a signal handler for syscall.SIGWINCH to automatically resize the tty when +// the pty receives a window size change notification. +func InheritSize(pty Pty, tty Tty) error { + size, err := GetsizeFull(pty) + if err != nil { + return err + } + if err := Setsize(tty, size); err != nil { + return err + } + return nil +} + +// Getsize returns the number of rows (lines) and cols (positions +// in each line) in terminal t. +func Getsize(t FdHolder) (rows, cols int, err error) { + ws, err := GetsizeFull(t) + return int(ws.Rows), int(ws.Cols), err +} diff --git a/winsize_unsupported.go b/winsize_unsupported.go deleted file mode 100644 index c4bff44..0000000 --- a/winsize_unsupported.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build windows -//+build windows - -package pty - -import ( - "os" -) - -// Winsize is a dummy struct to enable compilation on unsupported platforms. -type Winsize struct { - Rows, Cols, X, Y uint -} - -// Setsize resizes t to s. -func Setsize(*os.File, *Winsize) error { - return ErrUnsupported -} - -// GetsizeFull returns the full terminal size description. -func GetsizeFull(*os.File) (*Winsize, error) { - return nil, ErrUnsupported -} diff --git a/winsize_windows.go b/winsize_windows.go new file mode 100644 index 0000000..ebc5b7e --- /dev/null +++ b/winsize_windows.go @@ -0,0 +1,66 @@ +package pty + +import ( + "os" + "unsafe" + + "golang.org/x/sys/windows" +) + +// InheritSize applies the terminal size of pty to tty. This should be run +// in a signal handler for syscall.SIGWINCH to automatically resize the tty when +// the pty receives a window size change notification. +func InheritSize(pty Pty, tty Tty) error { + size, err := GetsizeFull(pty) + if err != nil { + return err + } + err = Setsize(tty, size) + if err != nil { + return err + } + return nil +} + +// Setsize resizes t to s. +func Setsize(t FdHolder, ws *Winsize) error { + resizePseudoConsole, err := kernel32DLL.FindProc("ResizePseudoConsole") + if err != nil { + return os.NewSyscallError("ResizePseudoConsole", err) + } + + _, _, err = resizePseudoConsole.Call( + t.Fd(), + uintptr(unsafe.Pointer(&windows.Coord{X: int16(ws.Cols), Y: int16(ws.Rows)})), + ) + return err +} + +// GetsizeFull returns the full terminal size description. +func GetsizeFull(t FdHolder) (size *Winsize, err error) { + getConsoleScreenBufferInfo, err := kernel32DLL.FindProc("GetConsoleScreenBufferInfo") + if err != nil { + return nil, os.NewSyscallError("GetConsoleScreenBufferInfo", err) + } + + var info windows.ConsoleScreenBufferInfo + _, _, err = getConsoleScreenBufferInfo.Call(t.Fd(), uintptr(unsafe.Pointer(&info))) + return &Winsize{ + Rows: uint16(info.Window.Bottom - info.Window.Top + 1), + Cols: uint16(info.Window.Right - info.Window.Left + 1), + }, err +} + +// Getsize returns the number of rows (lines) and cols (positions +// in each line) in terminal t. +func Getsize(t FdHolder) (rows, cols int, err error) { + ws, err := GetsizeFull(t) + return int(ws.Rows), int(ws.Cols), err +} + +type Winsize struct { + Rows uint16 // ws_row: Number of rows (in cells) + Cols uint16 // ws_col: Number of columns (in cells) + X uint16 // ws_xpixel: Width in pixels (not supported) + Y uint16 // ws_ypixel: Height in pixels (not supported) +} From 80786e2f56afaf8cb59c11f521f149079006b1a0 Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Mon, 31 May 2021 10:38:31 +0200 Subject: [PATCH 02/13] Unify Winsize and InheritSize for all platfoms Signed-off-by: Jeffrey Stoke --- winsize.go | 24 ++++++++++++++++++++++++ winsize_unix.go | 22 ---------------------- winsize_windows.go | 22 ---------------------- 3 files changed, 24 insertions(+), 44 deletions(-) create mode 100644 winsize.go diff --git a/winsize.go b/winsize.go new file mode 100644 index 0000000..c0f2bf9 --- /dev/null +++ b/winsize.go @@ -0,0 +1,24 @@ +package pty + +// Winsize describes the terminal size. +type Winsize struct { + Rows uint16 // ws_row: Number of rows (in cells) + Cols uint16 // ws_col: Number of columns (in cells) + X uint16 // ws_xpixel: Width in pixels + Y uint16 // ws_ypixel: Height in pixels +} + +// InheritSize applies the terminal size of pty to tty. This should be run +// in a signal handler for syscall.SIGWINCH to automatically resize the tty when +// the pty receives a window size change notification. +func InheritSize(pty Pty, tty Tty) error { + size, err := GetsizeFull(pty) + if err != nil { + return err + } + err = Setsize(tty, size) + if err != nil { + return err + } + return nil +} diff --git a/winsize_unix.go b/winsize_unix.go index 5968260..0a4f681 100644 --- a/winsize_unix.go +++ b/winsize_unix.go @@ -8,14 +8,6 @@ import ( "unsafe" ) -// Winsize describes the terminal size. -type Winsize struct { - Rows uint16 // ws_row: Number of rows (in cells) - Cols uint16 // ws_col: Number of columns (in cells) - X uint16 // ws_xpixel: Width in pixels - Y uint16 // ws_ypixel: Height in pixels -} - // Setsize resizes t to s. func Setsize(t FdHolder, ws *Winsize) error { //nolint:gosec // Expected unsafe pointer for Syscall call. @@ -33,20 +25,6 @@ func GetsizeFull(t FdHolder) (size *Winsize, err error) { return &ws, nil } -// InheritSize applies the terminal size of pty to tty. This should be run -// in a signal handler for syscall.SIGWINCH to automatically resize the tty when -// the pty receives a window size change notification. -func InheritSize(pty Pty, tty Tty) error { - size, err := GetsizeFull(pty) - if err != nil { - return err - } - if err := Setsize(tty, size); err != nil { - return err - } - return nil -} - // Getsize returns the number of rows (lines) and cols (positions // in each line) in terminal t. func Getsize(t FdHolder) (rows, cols int, err error) { diff --git a/winsize_windows.go b/winsize_windows.go index ebc5b7e..ec9c052 100644 --- a/winsize_windows.go +++ b/winsize_windows.go @@ -7,21 +7,6 @@ import ( "golang.org/x/sys/windows" ) -// InheritSize applies the terminal size of pty to tty. This should be run -// in a signal handler for syscall.SIGWINCH to automatically resize the tty when -// the pty receives a window size change notification. -func InheritSize(pty Pty, tty Tty) error { - size, err := GetsizeFull(pty) - if err != nil { - return err - } - err = Setsize(tty, size) - if err != nil { - return err - } - return nil -} - // Setsize resizes t to s. func Setsize(t FdHolder, ws *Winsize) error { resizePseudoConsole, err := kernel32DLL.FindProc("ResizePseudoConsole") @@ -57,10 +42,3 @@ func Getsize(t FdHolder) (rows, cols int, err error) { ws, err := GetsizeFull(t) return int(ws.Rows), int(ws.Cols), err } - -type Winsize struct { - Rows uint16 // ws_row: Number of rows (in cells) - Cols uint16 // ws_col: Number of columns (in cells) - X uint16 // ws_xpixel: Width in pixels (not supported) - Y uint16 // ws_ypixel: Height in pixels (not supported) -} From e9aa7ec88b1fa2ad1efec599f90811d26ec754dc Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Mon, 31 May 2021 10:40:04 +0200 Subject: [PATCH 03/13] Pty: Replace io.StringWriter with simple func For backward compatibility Signed-off-by: Jeffrey Stoke --- doc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc.go b/doc.go index ef3a2c9..c56777a 100644 --- a/doc.go +++ b/doc.go @@ -27,8 +27,8 @@ type ( // Fd intended to resize Tty of child process in current process FdHolder - // string writer is used to identify Pty and Tty - io.StringWriter + // WriteString is only used to identify Pty and Tty + WriteString(s string) (n int, err error) io.ReadWriteCloser } From 5cd3e9a3d78f1abb1ccf74b653d0034ca1792eaf Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Mon, 31 May 2021 10:49:12 +0200 Subject: [PATCH 04/13] Embed types and constants from external packages Remove module dependency golang.org/x/sys/windows Signed-off-by: Jeffrey Stoke --- go.mod | 2 -- go.sum | 2 -- pty_windows.go | 4 +--- run_windows.go | 6 +++--- winsize_windows.go | 22 +++++++++++++++++++--- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 91a1c03..7731235 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/creack/pty go 1.13 - -require golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea diff --git a/go.sum b/go.sum index 750c835..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI= -golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pty_windows.go b/pty_windows.go index 9fa7294..80b620f 100644 --- a/pty_windows.go +++ b/pty_windows.go @@ -7,8 +7,6 @@ import ( "os" "syscall" "unsafe" - - "golang.org/x/sys/windows" ) var ( @@ -48,7 +46,7 @@ func open() (_ Pty, _ Tty, err error) { var ( consoleHandle syscall.Handle - defaultSize = &windows.Coord{X: 80, Y: 30} + defaultSize = &windowsCoord{X: 80, Y: 30} ) // https://docs.microsoft.com/en-us/windows/console/createpseudoconsole diff --git a/run_windows.go b/run_windows.go index 51fcd9f..f4db1bb 100644 --- a/run_windows.go +++ b/run_windows.go @@ -10,8 +10,6 @@ import ( "runtime" "syscall" "unsafe" - - "golang.org/x/sys/windows" ) type startupInfoEx struct { @@ -20,6 +18,8 @@ type startupInfoEx struct { } const ( + _EXTENDED_STARTUPINFO_PRESENT = 0x00080000 + _PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016 ) @@ -402,7 +402,7 @@ func syscallStartProcess(argv0 string, argv []string, attr *syscall.ProcAttr, co flags := sys.CreationFlags | syscall.CREATE_UNICODE_ENVIRONMENT // add startupInfoEx flag - flags = flags | windows.EXTENDED_STARTUPINFO_PRESENT + flags = flags | _EXTENDED_STARTUPINFO_PRESENT // ignore security attrs since both Process and Thread handles are not inheritable for conPty if sys.Token != 0 { diff --git a/winsize_windows.go b/winsize_windows.go index ec9c052..ab5dc86 100644 --- a/winsize_windows.go +++ b/winsize_windows.go @@ -3,8 +3,24 @@ package pty import ( "os" "unsafe" +) + +// types from golang.org/x/sys/windows +type ( + // copy of https://pkg.go.dev/golang.org/x/sys/windows#Coord + windowsCoord struct { + X int16 + Y int16 + } - "golang.org/x/sys/windows" + // copy of https://pkg.go.dev/golang.org/x/sys/windows#ConsoleScreenBufferInfo + windowsConsoleScreenBufferInfo struct { + Size Coord + CursorPosition Coord + Attributes uint16 + Window SmallRect + MaximumWindowSize Coord + } ) // Setsize resizes t to s. @@ -16,7 +32,7 @@ func Setsize(t FdHolder, ws *Winsize) error { _, _, err = resizePseudoConsole.Call( t.Fd(), - uintptr(unsafe.Pointer(&windows.Coord{X: int16(ws.Cols), Y: int16(ws.Rows)})), + uintptr(unsafe.Pointer(&windowsCoord{X: int16(ws.Cols), Y: int16(ws.Rows)})), ) return err } @@ -28,7 +44,7 @@ func GetsizeFull(t FdHolder) (size *Winsize, err error) { return nil, os.NewSyscallError("GetConsoleScreenBufferInfo", err) } - var info windows.ConsoleScreenBufferInfo + var info windowsConsoleScreenBufferInfo _, _, err = getConsoleScreenBufferInfo.Call(t.Fd(), uintptr(unsafe.Pointer(&info))) return &Winsize{ Rows: uint16(info.Window.Bottom - info.Window.Top + 1), From f0bc1585e3cb9fd8faf11175fccc4dbb0e210e6f Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Mon, 31 May 2021 10:58:22 +0200 Subject: [PATCH 05/13] Move GetSize to winsize All platforms share the same GetSize implementation Signed-off-by: Jeffrey Stoke --- winsize.go | 7 +++++++ winsize_unix.go | 7 ------- winsize_windows.go | 7 ------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/winsize.go b/winsize.go index c0f2bf9..4027eda 100644 --- a/winsize.go +++ b/winsize.go @@ -22,3 +22,10 @@ func InheritSize(pty Pty, tty Tty) error { } return nil } + +// Getsize returns the number of rows (lines) and cols (positions +// in each line) in terminal t. +func Getsize(t FdHolder) (rows, cols int, err error) { + ws, err := GetsizeFull(t) + return int(ws.Rows), int(ws.Cols), err +} diff --git a/winsize_unix.go b/winsize_unix.go index 0a4f681..2c3c0ff 100644 --- a/winsize_unix.go +++ b/winsize_unix.go @@ -24,10 +24,3 @@ func GetsizeFull(t FdHolder) (size *Winsize, err error) { } return &ws, nil } - -// Getsize returns the number of rows (lines) and cols (positions -// in each line) in terminal t. -func Getsize(t FdHolder) (rows, cols int, err error) { - ws, err := GetsizeFull(t) - return int(ws.Rows), int(ws.Cols), err -} diff --git a/winsize_windows.go b/winsize_windows.go index ab5dc86..143e9d9 100644 --- a/winsize_windows.go +++ b/winsize_windows.go @@ -51,10 +51,3 @@ func GetsizeFull(t FdHolder) (size *Winsize, err error) { Cols: uint16(info.Window.Right - info.Window.Left + 1), }, err } - -// Getsize returns the number of rows (lines) and cols (positions -// in each line) in terminal t. -func Getsize(t FdHolder) (rows, cols int, err error) { - ws, err := GetsizeFull(t) - return int(ws.Rows), int(ws.Cols), err -} From aee4483a55022d4f0f23751545f80661868bdc5d Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Mon, 31 May 2021 10:59:15 +0200 Subject: [PATCH 06/13] Remove underline prefix for fields in WindowsPty Keep it constant with other platforms Signed-off-by: Jeffrey Stoke --- pty_windows.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pty_windows.go b/pty_windows.go index 80b620f..50e5198 100644 --- a/pty_windows.go +++ b/pty_windows.go @@ -72,8 +72,8 @@ func open() (_ Pty, _ Tty, err error) { handle: uintptr(consoleHandle), r: pr, w: pw, - _consoleR: consoleR, - _consoleW: consoleW, + consoleR: consoleR, + consoleW: consoleW, }, &WindowsTty{ handle: uintptr(consoleHandle), r: consoleR, @@ -87,7 +87,7 @@ type WindowsPty struct { handle uintptr r, w *os.File - _consoleR, _consoleW *os.File + consoleR, consoleW *os.File } func (p *WindowsPty) Fd() uintptr { @@ -118,8 +118,8 @@ func (p *WindowsPty) Close() error { _ = p.r.Close() _ = p.w.Close() - _ = p._consoleR.Close() - _ = p._consoleW.Close() + _ = p.consoleR.Close() + _ = p.consoleW.Close() closePseudoConsole, err := kernel32DLL.FindProc("ClosePseudoConsole") if err != nil { From 07b10c56f86ee558d6e01529e7ff45f1ebf60e97 Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Mon, 31 May 2021 11:00:50 +0200 Subject: [PATCH 07/13] Use inline error check for Setsize in InheritSize Signed-off-by: Jeffrey Stoke --- winsize.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winsize.go b/winsize.go index 4027eda..7d3d1fc 100644 --- a/winsize.go +++ b/winsize.go @@ -16,8 +16,8 @@ func InheritSize(pty Pty, tty Tty) error { if err != nil { return err } - err = Setsize(tty, size) - if err != nil { + + if err := Setsize(tty, size); err != nil { return err } return nil From 9ac8ea31101cf9d7a25556aadec218b798d09456 Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Mon, 31 May 2021 11:06:11 +0200 Subject: [PATCH 08/13] docs: Add windows requirements note Signed-off-by: Jeffrey Stoke --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 214f0f1..df4bcce 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ go get github.com/creack/pty Note that those examples are for demonstration purpose only, to showcase how to use the library. They are not meant to be used in any kind of production environment. +__NOTE:__ This package requires `ConPty` support on windows platform, please make sure your windows system meet [these requirements](https://docs.microsoft.com/en-us/windows/console/createpseudoconsole#requirements) + ### Command ```go From 295ab954b3811da326130776e692cd9481fc490b Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Mon, 31 May 2021 11:12:29 +0200 Subject: [PATCH 09/13] Fix embedded windows types Add missing SmallRect Signed-off-by: Jeffrey Stoke --- winsize_windows.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/winsize_windows.go b/winsize_windows.go index 143e9d9..e0faee5 100644 --- a/winsize_windows.go +++ b/winsize_windows.go @@ -13,13 +13,21 @@ type ( Y int16 } + // copy of https://pkg.go.dev/golang.org/x/sys/windows#SmallRect + windowsSmallRect struct { + Left int16 + Top int16 + Right int16 + Bottom int16 + } + // copy of https://pkg.go.dev/golang.org/x/sys/windows#ConsoleScreenBufferInfo windowsConsoleScreenBufferInfo struct { - Size Coord - CursorPosition Coord + Size windowsCoord + CursorPosition windowsCoord Attributes uint16 - Window SmallRect - MaximumWindowSize Coord + Window windowsSmallRect + MaximumWindowSize windowsCoord } ) From 83b9ce2b718ea10c2c77d19da37d030ebe4d73ff Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Sun, 29 Aug 2021 11:15:24 +0200 Subject: [PATCH 10/13] chore: Fix arg name in comment winsize: w to ws Signed-off-by: Jeffrey Stoke --- winsize_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winsize_windows.go b/winsize_windows.go index e0faee5..566cf8b 100644 --- a/winsize_windows.go +++ b/winsize_windows.go @@ -31,7 +31,7 @@ type ( } ) -// Setsize resizes t to s. +// Setsize resizes t to ws. func Setsize(t FdHolder, ws *Winsize) error { resizePseudoConsole, err := kernel32DLL.FindProc("ResizePseudoConsole") if err != nil { From 8eb0e5860838ae2ec5d5e1de8d922c9128773152 Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Sun, 29 Aug 2021 11:51:53 +0200 Subject: [PATCH 11/13] chore: Use LazyDLL and LazyProc Always call LazyProc.Find() before calling LazyProc.Call() to avoid panic due to compulsory mustFind() call in Call() Signed-off-by: Jeffrey Stoke --- pty_windows.go | 48 +++++++++++++++++++++++++++------------------- run_windows.go | 31 ++++++++++++------------------ winsize_windows.go | 9 ++++----- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/pty_windows.go b/pty_windows.go index 50e5198..7cf12ee 100644 --- a/pty_windows.go +++ b/pty_windows.go @@ -1,5 +1,5 @@ //go:build windows -//+build windows +// +build windows package pty @@ -10,16 +10,26 @@ import ( ) var ( - kernel32DLL *syscall.DLL -) + // 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") -func init() { - var err error - kernel32DLL, err = syscall.LoadDLL("kernel32.dll") - if err != nil { - panic(err) - } -} + // 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() @@ -49,11 +59,9 @@ func open() (_ Pty, _ Tty, err error) { defaultSize = &windowsCoord{X: 80, Y: 30} ) - // https://docs.microsoft.com/en-us/windows/console/createpseudoconsole - - createPseudoConsole, err := kernel32DLL.FindProc("CreatePseudoConsole") + err = createPseudoConsole.Find() if err != nil { - return nil, nil, os.NewSyscallError("CreatePseudoConsole", err) + return nil, nil, err } r1, _, err := createPseudoConsole.Call( @@ -69,10 +77,10 @@ func open() (_ Pty, _ Tty, err error) { } return &WindowsPty{ - handle: uintptr(consoleHandle), - r: pr, - w: pw, - consoleR: consoleR, + handle: uintptr(consoleHandle), + r: pr, + w: pw, + consoleR: consoleR, consoleW: consoleW, }, &WindowsTty{ handle: uintptr(consoleHandle), @@ -121,9 +129,9 @@ func (p *WindowsPty) Close() error { _ = p.consoleR.Close() _ = p.consoleW.Close() - closePseudoConsole, err := kernel32DLL.FindProc("ClosePseudoConsole") + err := closePseudoConsole.Find() if err != nil { - return os.NewSyscallError("ClosePseudoConsole", err) + return err } _, _, err = closePseudoConsole.Call(p.handle) diff --git a/run_windows.go b/run_windows.go index f4db1bb..8f362b5 100644 --- a/run_windows.go +++ b/run_windows.go @@ -1,5 +1,5 @@ //go:build windows -//+build windows +// +build windows package pty @@ -77,11 +77,6 @@ func StartWithAttrs(c *exec.Cmd, sz *Winsize, attrs *syscall.SysProcAttr) (_ Pty } func createExtendedStartupInfo(consoleHandle syscall.Handle) (_ *startupInfoEx, err error) { - deleteProcThreadAttributeList, err := kernel32DLL.FindProc("DeleteProcThreadAttributeList") - if err != nil { - return nil, os.NewSyscallError("DeleteProcThreadAttributeList", err) - } - // append console handler to new process var ( attrBufSize uint64 @@ -91,9 +86,9 @@ func createExtendedStartupInfo(consoleHandle syscall.Handle) (_ *startupInfoEx, si.startupInfo.Cb = uint32(unsafe.Sizeof(si)) // get size of attr list - initializeProcThreadAttributeList, err := kernel32DLL.FindProc("InitializeProcThreadAttributeList") + err = initializeProcThreadAttributeList.Find() if err != nil { - return nil, os.NewSyscallError("InitializeProcThreadAttributeList", err) + return nil, err } r1, _, err := initializeProcThreadAttributeList.Call( @@ -127,10 +122,9 @@ func createExtendedStartupInfo(consoleHandle syscall.Handle) (_ *startupInfoEx, return nil, os.NewSyscallError("InitializeProcThreadAttributeList (create)", err) } - // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute - updateProcThreadAttribute, err := kernel32DLL.FindProc("UpdateProcThreadAttribute") + err = updateProcThreadAttribute.Find() if err != nil { - return nil, os.NewSyscallError("UpdateProcThreadAttribute", err) + return nil, err } r1, _, err = updateProcThreadAttribute.Call( @@ -144,7 +138,9 @@ func createExtendedStartupInfo(consoleHandle syscall.Handle) (_ *startupInfoEx, ) if r1 == 0 { // false - _, _, _ = deleteProcThreadAttributeList.Call(uintptr(si.lpAttrList)) + if deleteProcThreadAttributeList.Find() == nil { + _, _, _ = deleteProcThreadAttributeList.Call(uintptr(si.lpAttrList)) + } return nil, os.NewSyscallError("UpdateProcThreadAttribute", err) } @@ -277,7 +273,7 @@ func startProcess(name string, argv []string, attr *os.ProcAttr, consoleHandle s runtime.KeepAlive(attr) if e != nil { - return nil, &os.PathError{"fork/exec", name, e} + return nil, &os.PathError{Op: "fork/exec", Path: name, Err: e} } return newProcess(pid, h), nil @@ -291,11 +287,6 @@ var zeroSysProcAttr syscall.SysProcAttr // copied from syscall.StartProcess, add consoleHandle arg func syscallStartProcess(argv0 string, argv []string, attr *syscall.ProcAttr, consoleHandle syscall.Handle) (pid int, handle uintptr, err error) { - deleteProcThreadAttributeList, err := kernel32DLL.FindProc("DeleteProcThreadAttributeList") - if err != nil { - return 0, 0, os.NewSyscallError("DeleteProcThreadAttributeList", err) - } - if len(argv0) == 0 { return 0, 0, syscall.EWINDOWS } @@ -385,7 +376,9 @@ func syscallStartProcess(argv0 string, argv []string, attr *syscall.ProcAttr, co } // add finalizer for attribute list cleanup, best effort runtime.SetFinalizer(si, func(si *startupInfoEx) { - _, _, _ = deleteProcThreadAttributeList.Call(uintptr(si.lpAttrList)) + if deleteProcThreadAttributeList.Find() == nil { + _, _, _ = deleteProcThreadAttributeList.Call(uintptr(si.lpAttrList)) + } }) si.startupInfo.Flags = syscall.STARTF_USESTDHANDLES diff --git a/winsize_windows.go b/winsize_windows.go index 566cf8b..a4b850f 100644 --- a/winsize_windows.go +++ b/winsize_windows.go @@ -1,7 +1,6 @@ package pty import ( - "os" "unsafe" ) @@ -33,9 +32,9 @@ type ( // Setsize resizes t to ws. func Setsize(t FdHolder, ws *Winsize) error { - resizePseudoConsole, err := kernel32DLL.FindProc("ResizePseudoConsole") + err := resizePseudoConsole.Find() if err != nil { - return os.NewSyscallError("ResizePseudoConsole", err) + return err } _, _, err = resizePseudoConsole.Call( @@ -47,9 +46,9 @@ func Setsize(t FdHolder, ws *Winsize) error { // GetsizeFull returns the full terminal size description. func GetsizeFull(t FdHolder) (size *Winsize, err error) { - getConsoleScreenBufferInfo, err := kernel32DLL.FindProc("GetConsoleScreenBufferInfo") + err = getConsoleScreenBufferInfo.Find() if err != nil { - return nil, os.NewSyscallError("GetConsoleScreenBufferInfo", err) + return nil, err } var info windowsConsoleScreenBufferInfo From f5cfe5cc429745ad6fb5127ccedaef2d85630642 Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Mon, 30 Aug 2021 15:31:50 +0200 Subject: [PATCH 12/13] nit: Workaround go1.17 linkname relocation Signed-off-by: Jeffrey Stoke --- cmd_windows.go | 14 +++++++------- run_windows.go | 26 +++++++++++++------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cmd_windows.go b/cmd_windows.go index ddbf885..5e74ed4 100644 --- a/cmd_windows.go +++ b/cmd_windows.go @@ -1,5 +1,5 @@ //go:build windows -//+build windows +// +build windows package pty @@ -112,14 +112,14 @@ type cmd struct { waitDone chan struct{} } -//go:linkname (*cmd).closeDescriptors os/exec.(*Cmd).closeDescriptors -func (c *cmd) closeDescriptors(closers []io.Closer) +//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 (c *cmd) envv() ([]string, error) +//go:linkname _cmd_envv os/exec.(*Cmd).envv +func _cmd_envv(c *cmd) ([]string, error) -//go:linkname (*cmd).argv os/exec.(*Cmd).argv -func (c *cmd) argv() []string +//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) diff --git a/run_windows.go b/run_windows.go index 8f362b5..48e17ef 100644 --- a/run_windows.go +++ b/run_windows.go @@ -156,15 +156,15 @@ func createExtendedStartupInfo(consoleHandle syscall.Handle) (_ *startupInfoEx, // once the command exits. func start(c *cmd, consoleHandle syscall.Handle) error { if c.lookPathErr != nil { - c.closeDescriptors(c.closeAfterStart) - c.closeDescriptors(c.closeAfterWait) + _cmd_closeDescriptors(c, c.closeAfterStart) + _cmd_closeDescriptors(c, c.closeAfterWait) return c.lookPathErr } if runtime.GOOS == "windows" { lp, err := lookExtensions(c.Path, c.Dir) if err != nil { - c.closeDescriptors(c.closeAfterStart) - c.closeDescriptors(c.closeAfterWait) + _cmd_closeDescriptors(c, c.closeAfterStart) + _cmd_closeDescriptors(c, c.closeAfterWait) return err } c.Path = lp @@ -175,8 +175,8 @@ func start(c *cmd, consoleHandle syscall.Handle) error { if c.ctx != nil { select { case <-c.ctx.Done(): - c.closeDescriptors(c.closeAfterStart) - c.closeDescriptors(c.closeAfterWait) + _cmd_closeDescriptors(c, c.closeAfterStart) + _cmd_closeDescriptors(c, c.closeAfterWait) return c.ctx.Err() default: } @@ -187,32 +187,32 @@ func start(c *cmd, consoleHandle syscall.Handle) error { //for _, setupFd := range []F{c.stdin, c.stdout, c.stderr} { // fd, err := setupFd() // if err != nil { - // c.closeDescriptors(c.closeAfterStart) - // c.closeDescriptors(c.closeAfterWait) + // closeDescriptors(c, c.closeAfterStart) + // closeDescriptors(c, c.closeAfterWait) // return err // } // c.childFiles = append(c.childFiles, fd) //} //c.childFiles = append(c.childFiles, c.ExtraFiles...) - envv, err := c.envv() + envv, err := _cmd_envv(c) if err != nil { return err } - c.Process, err = startProcess(c.Path, c.argv(), &os.ProcAttr{ + c.Process, err = startProcess(c.Path, _cmd_argv(c), &os.ProcAttr{ Dir: c.Dir, Files: c.childFiles, Env: addCriticalEnv(dedupEnv(envv)), Sys: c.SysProcAttr, }, consoleHandle) if err != nil { - c.closeDescriptors(c.closeAfterStart) - c.closeDescriptors(c.closeAfterWait) + _cmd_closeDescriptors(c, c.closeAfterStart) + _cmd_closeDescriptors(c, c.closeAfterWait) return err } - c.closeDescriptors(c.closeAfterStart) + _cmd_closeDescriptors(c, c.closeAfterStart) // Don't allocate the channel unless there are goroutines to fire. if len(c.goroutine) > 0 { From 294eccab62ed63a1d000f7fe7a548eccc165a7b0 Mon Sep 17 00:00:00 2001 From: Jeffrey Stoke Date: Tue, 31 Aug 2021 18:25:22 +0200 Subject: [PATCH 13/13] fix(conpty): Fix windows conpty syscall handling * COORD passed as uintptr for pty creation and resizing This should fix unexpected error when creating and resize terminal * Check return value of syscalls to determine error state instead of using err directly This should resolve "The operation completed successfully." problem. photostorm contributed these fixes, I just removed some redundant variable declarations and made this commit. Signed-off-by: Jeffrey Stoke Co-authored-by: photostorm <3271896+photostorm@users.noreply.github.com> Co-authored-by: Jeffrey Stoke --- pty_windows.go | 8 ++------ winsize_windows.go | 25 ++++++++++++++++++++----- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/pty_windows.go b/pty_windows.go index 7cf12ee..717f1eb 100644 --- a/pty_windows.go +++ b/pty_windows.go @@ -54,18 +54,14 @@ func open() (_ Pty, _ Tty, err error) { } }() - var ( - consoleHandle syscall.Handle - defaultSize = &windowsCoord{X: 80, Y: 30} - ) - err = createPseudoConsole.Find() if err != nil { return nil, nil, err } + var consoleHandle syscall.Handle r1, _, err := createPseudoConsole.Call( - uintptr(unsafe.Pointer(defaultSize)), // size: default 80x30 window + (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 diff --git a/winsize_windows.go b/winsize_windows.go index a4b850f..c6826ea 100644 --- a/winsize_windows.go +++ b/winsize_windows.go @@ -1,6 +1,7 @@ package pty import ( + "os" "unsafe" ) @@ -30,6 +31,10 @@ type ( } ) +func (c windowsCoord) Pack() uintptr { + return uintptr((int32(c.Y) << 16) | int32(c.X)) +} + // Setsize resizes t to ws. func Setsize(t FdHolder, ws *Winsize) error { err := resizePseudoConsole.Find() @@ -37,11 +42,16 @@ func Setsize(t FdHolder, ws *Winsize) error { return err } - _, _, err = resizePseudoConsole.Call( + r1, _, err := resizePseudoConsole.Call( t.Fd(), - uintptr(unsafe.Pointer(&windowsCoord{X: int16(ws.Cols), Y: int16(ws.Rows)})), + (windowsCoord{X: int16(ws.Cols), Y: int16(ws.Rows)}).Pack(), ) - return err + if r1 != 0 { + // S_OK: 0 + return os.NewSyscallError("ResizePseudoConsole", err) + } + + return nil } // GetsizeFull returns the full terminal size description. @@ -52,9 +62,14 @@ func GetsizeFull(t FdHolder) (size *Winsize, err error) { } var info windowsConsoleScreenBufferInfo - _, _, err = getConsoleScreenBufferInfo.Call(t.Fd(), uintptr(unsafe.Pointer(&info))) + r1, _, err := getConsoleScreenBufferInfo.Call(t.Fd(), uintptr(unsafe.Pointer(&info))) + if r1 != 0 { + // S_OK: 0 + return nil, os.NewSyscallError("GetConsoleScreenBufferInfo", err) + } + return &Winsize{ Rows: uint16(info.Window.Bottom - info.Window.Top + 1), Cols: uint16(info.Window.Right - info.Window.Left + 1), - }, err + }, nil }