Use ConPTY on Windows for pty-backed command execution

The per-platform getCmdHandlerPty split existed because the Unix side
had creack/pty and the Windows side had nothing — so it fell back to a
non-pty handler. Now that oscommands.StartPty provides a pty on both
platforms, the two files collapse into one cross-platform
implementation and the stub is gone.

cmdHandler grows a 'wait' field because the pty path on Windows spawns
via CreateProcess and never runs exec.Cmd.Start — so cmd.Wait wouldn't
work there. Non-pty handlers set wait = cmd.Wait; pty handlers set it
to the wait closure StartPty returns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-06-30 09:45:26 +02:00
parent 1935117141
commit 8c035ebe60
4 changed files with 23 additions and 39 deletions

View file

@ -157,7 +157,7 @@ func (self *CmdObj) ShouldStreamOutput() bool {
}
// when you call this, then call Run(), we'll use a PTY to run the command. Only
// has an effect if StreamOutput() was also called. Ignored on Windows.
// has an effect if StreamOutput() was also called.
func (self *CmdObj) UsePty() *CmdObj {
self.usePty = true

View file

@ -219,6 +219,10 @@ type cmdHandler struct {
stdoutPipe io.Reader
stdinPipe io.Writer
close func() error
// wait blocks until the child process exits. Needed as a separate
// field because the pty path on Windows spawns via CreateProcess and
// never runs *exec.Cmd.Start — so cmd.Wait wouldn't work there.
wait func() error
}
func (self *cmdObjRunner) runAndStream(cmdObj *CmdObj) error {
@ -274,7 +278,7 @@ func (self *cmdObjRunner) runAndStreamAux(
onRun(handler, cmdWriter)
err = cmd.Wait()
err = handler.wait()
self.log.Infof("%s (%s)", cmdObj.ToString(), time.Since(t))
@ -376,9 +380,7 @@ func (self *cmdObjRunner) processOutput(
responseChan := promptUserForCredential(askFor)
if responseChan == nil {
// Returning a nil channel means we should terminate the process.
// We achieve this by closing the pty that it's running in. Note that this won't
// work for the case where we're not running in a pty (i.e. on Windows), but
// in that case we'll never be prompted for credentials, so it's not a concern.
// We achieve this by closing the pty that it's running in.
if err := closeFunc(); err != nil {
self.log.Error(err)
}
@ -481,5 +483,21 @@ func (self *cmdObjRunner) getCmdHandlerNonPty(cmd *exec.Cmd) (*cmdHandler, error
stdoutPipe: stdoutReader,
stdinPipe: buf,
close: func() error { return nil },
wait: cmd.Wait,
}, nil
}
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
// Size will be adjusted by the caller if it cares; this just avoids a
// zero-size pty.
sp, err := StartPty(cmd, 80, 24)
if err != nil {
return nil, err
}
return &cmdHandler{
stdoutPipe: sp.Pty,
stdinPipe: sp.Pty,
close: sp.Pty.Close,
wait: sp.Wait,
}, nil
}

View file

@ -1,24 +0,0 @@
//go:build !windows
package oscommands
import (
"os/exec"
"github.com/creack/pty"
)
// we define this separately for windows and non-windows given that windows does
// not have great PTY support and we need a PTY to handle a credential request
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
ptmx, err := pty.Start(cmd)
if err != nil {
return nil, err
}
return &cmdHandler{
stdoutPipe: ptmx,
stdinPipe: ptmx,
close: ptmx.Close,
}, nil
}

View file

@ -1,10 +0,0 @@
package oscommands
import (
"os/exec"
)
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
// We don't have PTY support on Windows yet, so we just return a non-PTY handler.
return self.getCmdHandlerNonPty(cmd)
}