diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..97aa2fd --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,62 @@ +name: CI + +# yamllint disable-line rule:truthy +on: + check_run: + types: + - rerequested + - requested_action + pull_request: {} + push: + branches: + - master + tags: + - v* + +env: + GO111MODULE: "on" + +defaults: + run: + # NOTE: when testing on windows, we may need non-cygwin environment + # so we need to change shell when doing actual testing on windows + shell: bash + +jobs: + unit-test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + go: + - 1.13.x + - 1.14.x + - 1.15.x + os: + - ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + + - name: Lint + uses: golangci/golangci-lint-action@v2 + with: + version: v1.33 + + - name: Ensure tidy gomod + run: | + go mod download -x + go mod tidy + if ! git diff --exit-code + then + echo "go mod not tidy" + exit 1 + fi + + - name: Test crossbuild + run: | + sh test_crosscompile.sh diff --git a/.gitignore b/.gitignore index 1f0a99f..f0f95b2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ _go* _test* _obj +.idea diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..2693ec4 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,77 @@ +run: + deadline: 5m + tests: true + modules-download-mode: readonly + +output: + format: colored-line-number + print-issued-lines: true + print-linter-name: true + sort-results: true + +linters: + disable-all: true + enable: + - golint + - errcheck + - misspell + - deadcode + - govet + - typecheck + - lll + - megacheck + - varcheck + - unconvert + - bodyclose + - scopelint + - goimports + - ineffassign + - gofmt + - maligned + - goconst + - gocyclo + - unparam + - structcheck + - staticcheck + - gocritic + +linters-settings: + govet: + check-shadowing: true + golint: + min-confidence: 0 + misspell: + locale: US + unused: + check-exported: false + gocyclo: + min-complexity: 30 + goimports: + local-prefixes: github.com/creack/pty + lll: + line-length: 120 + # tab width in spaces. Default to 1. + tab-width: 4 + maligned: + suggest-new: true + +issues: + exclude-rules: + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec + - maligned + - lll + - scopelint + - goconst + + - path: kubehelper + linters: + - lll + + - text: "commentFormatting: put a space between `//` and comment text" + linters: + - gocritic diff --git a/README.md b/README.md index 50457e3..4dd3260 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/doc.go b/doc.go index 190cfbe..fd01d97 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") // Opens 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 e48deca..3a44393 100644 --- a/go.mod +++ b/go.mod @@ -2,3 +2,4 @@ module github.com/creack/pty go 1.13 +require golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..de9e09c --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/ioctl_solaris.go b/ioctl_solaris.go index f63985f..249686c 100644 --- a/ioctl_solaris.go +++ b/ioctl_solaris.go @@ -1,15 +1,16 @@ package pty import ( - "golang.org/x/sys/unix" "unsafe" + + "golang.org/x/sys/unix" ) const ( // see /usr/include/sys/stropts.h - I_PUSH = uintptr((int32('S')<<8 | 002)) - I_STR = uintptr((int32('S')<<8 | 010)) - I_FIND = uintptr((int32('S')<<8 | 013)) + I_PUSH = uintptr((int32('S')<<8 | 002)) + I_STR = uintptr((int32('S')<<8 | 010)) + I_FIND = uintptr((int32('S')<<8 | 013)) // see /usr/include/sys/ptms.h ISPTM = (int32('P') << 8) | 1 UNLKPT = (int32('P') << 8) | 2 diff --git a/pty_linux.go b/pty_linux.go index 4a833de..27a4c90 100644 --- a/pty_linux.go +++ b/pty_linux.go @@ -24,7 +24,7 @@ func open() (pty, tty *os.File, err error) { return nil, nil, err } - if err := unlockpt(p); err != nil { + if err = unlockpt(p); err != nil { return nil, nil, err } diff --git a/pty_unsupported.go b/pty_unsupported.go index ceb425b..6dfacf3 100644 --- a/pty_unsupported.go +++ b/pty_unsupported.go @@ -1,4 +1,4 @@ -// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd,!solaris +// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd,!solaris,!windows package pty diff --git a/pty_windows.go b/pty_windows.go new file mode 100644 index 0000000..18759a1 --- /dev/null +++ b/pty_windows.go @@ -0,0 +1,160 @@ +package pty + +import ( + "os" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + kernel32DLL *syscall.DLL + + createPseudoConsole *syscall.Proc + resizePseudoConsole *syscall.Proc + closePseudoConsole *syscall.Proc + getConsoleScreenBufferInfo *syscall.Proc + + initializeProcThreadAttributeList *syscall.Proc + updateProcThreadAttribute *syscall.Proc + deleteProcThreadAttributeList *syscall.Proc +) + +func init() { + var err error + kernel32DLL, err = syscall.LoadDLL("kernel32.dll") + if err != nil { + panic(err) + } + + createPseudoConsole = kernel32DLL.MustFindProc("CreatePseudoConsole") + resizePseudoConsole = kernel32DLL.MustFindProc("ResizePseudoConsole") + closePseudoConsole = kernel32DLL.MustFindProc("ClosePseudoConsole") + getConsoleScreenBufferInfo = kernel32DLL.MustFindProc("GetConsoleScreenBufferInfo") + + initializeProcThreadAttributeList = kernel32DLL.MustFindProc("InitializeProcThreadAttributeList") + updateProcThreadAttribute = kernel32DLL.MustFindProc("UpdateProcThreadAttribute") + deleteProcThreadAttributeList = kernel32DLL.MustFindProc("DeleteProcThreadAttributeList") +} + +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 + 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() + + _, _, 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 b079425..0ef9e0e 100644 --- a/run.go +++ b/run.go @@ -1,74 +1,14 @@ -// +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(c *exec.Cmd) (pty *os.File, err error) { +func Start(c *exec.Cmd) (pty Pty, err error) { return StartWithSize(c, 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(c *exec.Cmd, sz *Winsize) (pty *os.File, err 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 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) (pty *os.File, err error) { - pty, tty, err := Open() - if err != nil { - return nil, err - } - defer 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_unix.go b/run_unix.go new file mode 100644 index 0000000..c8a4c95 --- /dev/null +++ b/run_unix.go @@ -0,0 +1,68 @@ +// +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..1cd4365 --- /dev/null +++ b/run_windows.go @@ -0,0 +1,527 @@ +package pty + +import ( + "context" + "errors" + "io" + "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) { + // append console handler to new process + var ( + attrBufSize uint64 + si startupInfoEx + ) + + si.startupInfo.Cb = uint32(unsafe.Sizeof(si)) + + // get size of attr list + 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 + 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) { + 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 +} + +// copied from os/exec.Cmd +// 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/run_windows.s b/run_windows.s new file mode 100644 index 0000000..90ff4bc --- /dev/null +++ b/run_windows.s @@ -0,0 +1 @@ +// disable default -complete option to `go tool compile` on windows diff --git a/test_crosscompile.sh b/test_crosscompile.sh index c4b9e37..7218a14 100755 --- a/test_crosscompile.sh +++ b/test_crosscompile.sh @@ -32,9 +32,7 @@ cross netbsd amd64 386 arm 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/util.go b/util.go index 8fdde0b..e4eb15c 100644 --- a/util.go +++ b/util.go @@ -3,7 +3,6 @@ package pty import ( - "os" "syscall" "unsafe" ) @@ -11,7 +10,7 @@ import ( // 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 { +func InheritSize(pty Pty, tty Tty) error { size, err := GetsizeFull(pty) if err != nil { return err @@ -24,12 +23,12 @@ func InheritSize(pty, tty *os.File) error { } // Setsize resizes t to s. -func Setsize(t *os.File, ws *Winsize) error { +func Setsize(t FdHolder, ws *Winsize) error { return windowRectCall(ws, t.Fd(), syscall.TIOCSWINSZ) } // 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 err = windowRectCall(&ws, t.Fd(), syscall.TIOCGWINSZ) return &ws, err @@ -37,7 +36,7 @@ func GetsizeFull(t *os.File) (size *Winsize, err error) { // 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) { +func Getsize(t FdHolder) (rows, cols int, err error) { ws, err := GetsizeFull(t) return int(ws.Rows), int(ws.Cols), err } @@ -58,7 +57,7 @@ func windowRectCall(ws *Winsize, fd, a2 uintptr) error { uintptr(unsafe.Pointer(ws)), ) if errno != 0 { - return syscall.Errno(errno) + return errno } return nil } diff --git a/util_solaris.go b/util_solaris.go index e889692..b98dbb4 100644 --- a/util_solaris.go +++ b/util_solaris.go @@ -1,9 +1,6 @@ -// - package pty import ( - "os" "golang.org/x/sys/unix" ) @@ -21,7 +18,7 @@ type Winsize struct { } // 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 wsz *unix.Winsize wsz, err = unix.IoctlGetWinsize(int(t.Fd()), TIOCGWINSZ) @@ -33,7 +30,7 @@ func GetsizeFull(t *os.File) (size *Winsize, err error) { } // Get Windows Size -func Getsize(t *os.File) (rows, cols int, err error) { +func Getsize(t FdHolder) (rows, cols int, err error) { var wsz *unix.Winsize wsz, err = unix.IoctlGetWinsize(int(t.Fd()), TIOCGWINSZ) @@ -45,7 +42,7 @@ func Getsize(t *os.File) (rows, cols int, err error) { } // Setsize resizes t to s. -func Setsize(t *os.File, ws *Winsize) error { +func Setsize(t FdHolder, ws *Winsize) error { wsz := unix.Winsize{ws.Rows, ws.Cols, ws.X, ws.Y} return unix.IoctlSetWinsize(int(t.Fd()), TIOCSWINSZ, &wsz) } diff --git a/util_windows.go b/util_windows.go new file mode 100644 index 0000000..8958230 --- /dev/null +++ b/util_windows.go @@ -0,0 +1,55 @@ +package pty + +import ( + "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 { + _, _, 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) { + 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) +}