From 55c93ad4e2464ebf4be933a47c56501bb9ac5b78 Mon Sep 17 00:00:00 2001 From: Erik Brakkee Date: Mon, 22 Jul 2024 23:55:37 +0200 Subject: [PATCH] Windows terminal support using the termtest library. Should even support resizing. Fully untested. --- go.mod | 2 ++ go.sum | 5 +++ pkg/iowrappers/io.go | 12 +++++++ pkg/terminal/pty_windows.go | 71 +++++++++++++++++++++++++++++++++---- 4 files changed, 83 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 9f65ada..33d0529 100755 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module converge go 1.21 require ( + github.com/ActiveState/termtest/conpty v0.5.0 github.com/creack/pty v1.1.21 github.com/gliderlabs/ssh v0.3.7 github.com/gorilla/websocket v1.5.3 @@ -13,6 +14,7 @@ require ( ) require ( + github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/kr/fs v0.1.0 // indirect golang.org/x/sys v0.22.0 // indirect diff --git a/go.sum b/go.sum index b17c2df..1dcbe17 100755 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/ActiveState/termtest/conpty v0.5.0 h1:JLUe6YDs4Jw4xNPCU+8VwTpniYOGeKzQg4SM2YHQNA8= +github.com/ActiveState/termtest/conpty v0.5.0/go.mod h1:LO4208FLsxw6DcNZ1UtuGUMW+ga9PFtX4ntv8Ymg9og= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= @@ -36,6 +40,7 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200428200454-593003d681fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/iowrappers/io.go b/pkg/iowrappers/io.go index a215521..67620f0 100644 --- a/pkg/iowrappers/io.go +++ b/pkg/iowrappers/io.go @@ -10,3 +10,15 @@ type ReadWriteAddrCloser interface { RemoteAddr() net.Addr } + +type ReadWriterCombiner struct { + io.Reader + io.Writer +} + +func (rw *ReadWriterCombiner) Read(p []byte) (n int, err error) { + return rw.Reader.Read(p) +} +func (rw *ReadWriterCombiner) Write(p []byte) (n int, err error) { + return rw.Writer.Write(p) +} diff --git a/pkg/terminal/pty_windows.go b/pkg/terminal/pty_windows.go index bdf8b73..8b534c2 100644 --- a/pkg/terminal/pty_windows.go +++ b/pkg/terminal/pty_windows.go @@ -1,24 +1,81 @@ package terminal import ( + "fmt" + "github.com/ActiveState/termtest/conpty" "github.com/gliderlabs/ssh" "io" + "log" "os" - "os/exec" + "syscall" ) -var PtySpawner = Spawner(func(sshSession ssh.Session, env []string, name string, arg ...string) (Process, error) { - return nil, nil +var PtySpawner = Spawner(func(sshSession ssh.Session, env []string, name string, args ...string) (Process, error) { + _, winCh, isPty := sshSession.Pty() + if !isPty { + return nil, fmt.Errorf("ssh session is not a pty") + } + + cpty, err := conpty.New(80, 25) + if err != nil { + return nil, err + } + pid, _, err := cpty.Spawn( + "cmd.exe", + args, + &syscall.ProcAttr{ + Env: env, + }, + ) + if err != nil { + cpty.Close() + return nil, err + } + fmt.Printf("New process with pid %d spawned\n", pid) + process, err := os.FindProcess(pid) + if err != nil { + cpty.Close() + return nil, fmt.Errorf("Failed to find process: %v", err) + } + + go func() { + for win := range winCh { + err = cpty.Resize(uint16(win.Width), uint16(win.Height)) + if err != nil { + log.Printf("Feiled to resize terminal to %d x %d", win.Width, win.Height) + } + } + }() + return ptyProcess{ + cpty: cpty, + process: process, + }, nil }) type ptyProcess struct { - cmd *exec.Cmd - f *os.File + cpty *conpty.ConPty + process *os.Process +} + +func (proc ptyProcess) Read(p []byte) (n int, err error) { + return proc.cpty.OutPipe().Read(p) +} +func (proc ptyProcess) Write(p []byte) (n int, err error) { + return proc.Write(p) } func (p ptyProcess) Pipe() io.ReadWriter { - return p.f + return p } + func (p ptyProcess) Wait() error { - return p.cmd.Wait() + defer p.cpty.Close() + ps, err := p.process.Wait() + if err != nil { + return fmt.Errorf("Error waiting for process: %v", err) + } + if ps.ExitCode() != 0 { + return fmt.Errorf("exit code was: %d\n", ps.ExitCode()) + } + return nil }