Windows Pty (#206)

Add Windows Pty support, so the terminal works properly on windows
machines
This commit is contained in:
Sylvie Crowe 2024-08-09 18:49:35 -07:00 committed by GitHub
parent 4498ca8e9d
commit c192fe2663
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 98 additions and 59 deletions

View File

@ -8,6 +8,8 @@ vars:
BIN_DIR: "bin"
VERSION:
sh: node version.cjs
RM: '{{if eq OS "windows"}}powershell Remove-Item{{else}}rm{{end}}'
DATE: '{{if eq OS "windows"}}powershell date -UFormat{{else}}date{{end}}'
tasks:
electron:dev:
@ -50,7 +52,7 @@ tasks:
status:
- exit {{if eq OS "darwin"}}1{{else}}0{{end}}
cmds:
- cmd: rm dist/bin/wavesrv*
- cmd: '{{.RM}} "dist/bin/wavesrv*"'
ignore_error: true
- task: build:server:internal
vars:
@ -64,7 +66,7 @@ tasks:
status:
- exit {{if eq OS "darwin"}}0{{else}}1{{end}}
cmds:
- cmd: rm dist/bin/wavesrv*
- cmd: '{{.RM}} "dist/bin/wavesrv*"'
ignore_error: true
- task: build:server:internal
vars:
@ -77,7 +79,7 @@ tasks:
vars:
- GOARCH
cmds:
- CGO_ENABLED=1 GOARCH={{.GOARCH}} go build -tags "osusergo,netgo,sqlite_omit_load_extension" -ldflags "{{.GO_LDFLAGS}} -X main.BuildTime=$(date +'%Y%m%d%H%M') -X main.WaveVersion={{.VERSION}}" -o dist/bin/wavesrv.{{.GOARCH}}{{exeExt}} cmd/server/main-server.go
- CGO_ENABLED=1 GOARCH={{.GOARCH}} go build -tags "osusergo,netgo,sqlite_omit_load_extension" -ldflags "{{.GO_LDFLAGS}} -X main.BuildTime=$({{.DATE}} +'%Y%m%d%H%M') -X main.WaveVersion={{.VERSION}}" -o dist/bin/wavesrv.{{.GOARCH}}{{exeExt}} cmd/server/main-server.go
sources:
- "cmd/server/*.go"
- "pkg/**/*.go"
@ -133,7 +135,7 @@ tasks:
generates:
- dist/bin/wsh-{{.VERSION}}-{{.GOOS}}.{{.GOARCH}}{{.EXT}}
cmds:
- (CGO_ENABLED=0 GOOS={{.GOOS}} GOARCH={{.GOARCH}} go build -ldflags="-s -w -X main.BuildTime=$(date +'%Y%m%d%H%M')" -o dist/bin/wsh-{{.VERSION}}-{{.GOOS}}.{{.GOARCH}}{{.EXT}} cmd/wsh/main-wsh.go)
- (CGO_ENABLED=0 GOOS={{.GOOS}} GOARCH={{.GOARCH}} go build -ldflags="-s -w -X main.BuildTime=$({{.DATE}} +'%Y%m%d%H%M')" -o dist/bin/wsh-{{.VERSION}}-{{.GOOS}}.{{.GOARCH}}{{.EXT}} cmd/wsh/main-wsh.go)
deps:
- generate
- go:mod:tidy

View File

@ -226,6 +226,9 @@ func main() {
if pidStr != "" {
_, err := strconv.Atoi(pidStr)
if err == nil {
if BuildTime == "" {
BuildTime = "0"
}
// use fmt instead of log here to make sure it goes directly to stderr
fmt.Fprintf(os.Stderr, "WAVESRV-ESTART ws:%s web:%s version:%s buildtime:%s\n", wsListener.Addr(), webListener.Addr(), WaveVersion, BuildTime)
}

View File

@ -122,7 +122,7 @@ function getWaveSrvPath(): string {
if (process.platform === "win32") {
const winBinName = `${wavesrvBinName}.exe`;
const appPath = path.join(getGoAppBasePath(), "bin", winBinName);
return `& "${appPath}"`;
return `${appPath}`;
}
return path.join(getGoAppBasePath(), "bin", wavesrvBinName);
}

2
go.mod
View File

@ -44,3 +44,5 @@ require (
)
replace github.com/kevinburke/ssh_config => github.com/wavetermdev/ssh_config v0.0.0-20240306041034-17e2087ebde2
replace github.com/creack/pty => github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b

5
go.sum
View File

@ -3,8 +3,6 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4
github.com/alexflint/go-filemutex v1.3.0 h1:LgE+nTUWnQCyRKbpoceKZsPQbs84LivvgwUymZXdOcM=
github.com/alexflint/go-filemutex v1.3.0/go.mod h1:U0+VA/i30mGBlLCrFPGtTe9y6wGQfNAWPBTekHQ+c8A=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -48,6 +46,8 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b h1:cLGKfKb1uk0hxI0Q8L83UAJPpeJ+gSpn3cCU/tjd3eg=
github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b/go.mod h1:KO+FcPtyLAiRC0hJwreJVvfwc7vnNz77UxBTIGHdPVk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
@ -90,6 +90,7 @@ golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@ -17,7 +17,6 @@ import (
"sync"
"time"
"github.com/creack/pty"
"github.com/wavetermdev/thenextwave/pkg/eventbus"
"github.com/wavetermdev/thenextwave/pkg/filestore"
"github.com/wavetermdev/thenextwave/pkg/shellexec"
@ -329,7 +328,7 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
shellInputCh := make(chan *BlockInputUnion, 32)
bc.ShellInputCh = shellInputCh
messageCh := make(chan []byte, 32)
ptyBuffer := wshutil.MakePtyBuffer(wshutil.WaveOSCPrefix, bc.ShellProc.Pty, messageCh)
ptyBuffer := wshutil.MakePtyBuffer(wshutil.WaveOSCPrefix, bc.ShellProc.Cmd, messageCh)
outputCh := make(chan []byte, 32)
WshServerFactoryFn(messageCh, outputCh, wshrpc.RpcContext{BlockId: bc.BlockId, TabId: bc.TabId})
go func() {
@ -368,17 +367,13 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
// handles input from the shellInputCh, sent to pty
for ic := range shellInputCh {
if len(ic.InputData) > 0 {
bc.ShellProc.Pty.Write(ic.InputData)
bc.ShellProc.Cmd.Write(ic.InputData)
}
if ic.TermSize != nil {
log.Printf("SETTERMSIZE: %dx%d\n", ic.TermSize.Rows, ic.TermSize.Cols)
err := pty.Setsize(bc.ShellProc.Pty, &pty.Winsize{Rows: uint16(ic.TermSize.Rows), Cols: uint16(ic.TermSize.Cols)})
if err != nil {
log.Printf("error setting term size: %v\n", err)
}
err = bc.ShellProc.Cmd.SetSize(ic.TermSize.Rows, ic.TermSize.Cols)
if err != nil {
log.Printf("error setting remote SIGWINCH: %v\n", err)
log.Printf("error setting pty size: %v\n", err)
}
}
}

View File

@ -2,9 +2,9 @@ package shellexec
import (
"io"
"os"
"os/exec"
"github.com/creack/pty"
"golang.org/x/crypto/ssh"
)
@ -16,10 +16,12 @@ type ConnInterface interface {
StdoutPipe() (io.ReadCloser, error)
StderrPipe() (io.ReadCloser, error)
SetSize(w int, h int) error
pty.Pty
}
type CmdWrap struct {
Cmd *exec.Cmd
pty.Pty
}
func (cw CmdWrap) Kill() {
@ -54,13 +56,18 @@ func (cw CmdWrap) StderrPipe() (io.ReadCloser, error) {
}
func (cw CmdWrap) SetSize(w int, h int) error {
err := pty.Setsize(cw.Pty, &pty.Winsize{Rows: uint16(w), Cols: uint16(h)})
if err != nil {
return err
}
return nil
}
type SessionWrap struct {
Session *ssh.Session
StartCmd string
Tty *os.File
Tty pty.Tty
pty.Pty
}
func (sw SessionWrap) Kill() {

View File

@ -14,6 +14,7 @@ import (
"path/filepath"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
@ -24,7 +25,6 @@ import (
"github.com/wavetermdev/thenextwave/pkg/remote"
"github.com/wavetermdev/thenextwave/pkg/util/shellutil"
"github.com/wavetermdev/thenextwave/pkg/wavebase"
"golang.org/x/term"
)
type TermSize struct {
@ -41,7 +41,6 @@ type CommandOptsType struct {
type ShellProc struct {
Cmd ConnInterface
Pty *os.File
CloseOnce *sync.Once
DoneCh chan any // closed after proc.Wait() returns
WaitErr error // WaitErr is synchronized by DoneCh (written before DoneCh is closed) and CloseOnce
@ -52,7 +51,13 @@ func (sp *ShellProc) Close() {
go func() {
waitErr := sp.Cmd.Wait()
sp.SetWaitErrorAndSignalDone(waitErr)
sp.Pty.Close()
// windows cannot handle the pty being
// closed twice, so we let the pty
// close itself instead
if runtime.GOOS != "windows" {
sp.Cmd.Close()
}
}()
}
@ -115,6 +120,41 @@ func checkCwd(cwd string) error {
var userHostRe = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`)
type PipePty struct {
remoteStdinWrite *os.File
remoteStdoutRead *os.File
}
func (pp *PipePty) Fd() uintptr {
return pp.remoteStdinWrite.Fd()
}
func (pp *PipePty) Name() string {
return "pipe-pty"
}
func (pp *PipePty) Read(p []byte) (n int, err error) {
return pp.remoteStdoutRead.Read(p)
}
func (pp *PipePty) Write(p []byte) (n int, err error) {
return pp.remoteStdinWrite.Write(p)
}
func (pp *PipePty) Close() error {
err1 := pp.remoteStdinWrite.Close()
err2 := pp.remoteStdoutRead.Close()
if err1 != nil {
return err1
}
return err2
}
func (pp *PipePty) WriteString(s string) (n int, err error) {
return pp.Write([]byte(s))
}
func StartRemoteShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsType, remoteName string) (*ShellProc, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelFunc()
@ -158,14 +198,21 @@ func StartRemoteShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsT
if err != nil {
return nil, err
}
// todo: connect pty output, etc
// redirect to fake pty???
cmdPty, cmdTty, err := pty.Open()
remoteStdinRead, remoteStdinWriteOurs, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("opening new pty: %w", err)
return nil, err
}
remoteStdoutReadOurs, remoteStdoutWrite, err := os.Pipe()
if err != nil {
return nil, err
}
pipePty := &PipePty{
remoteStdinWrite: remoteStdinWriteOurs,
remoteStdoutRead: remoteStdoutReadOurs,
}
term.MakeRaw(int(cmdTty.Fd()))
if termSize.Rows == 0 || termSize.Cols == 0 {
termSize.Rows = shellutil.DefaultTermRows
termSize.Cols = shellutil.DefaultTermCols
@ -173,10 +220,9 @@ func StartRemoteShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsT
if termSize.Rows <= 0 || termSize.Cols <= 0 {
return nil, fmt.Errorf("invalid term size: %v", termSize)
}
pty.Setsize(cmdPty, &pty.Winsize{Rows: uint16(termSize.Rows), Cols: uint16(termSize.Cols)})
session.Stdin = cmdTty
session.Stdout = cmdTty
session.Stderr = cmdTty
session.Stdin = remoteStdinRead
session.Stdout = remoteStdoutWrite
session.Stderr = remoteStdoutWrite
for envKey, envVal := range cmdOpts.Env {
// note these might fail depending on server settings, but we still try
session.Setenv(envKey, envVal)
@ -184,13 +230,13 @@ func StartRemoteShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsT
session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil)
sessionWrap := SessionWrap{session, cmdCombined, cmdTty}
sessionWrap := SessionWrap{session, cmdCombined, pipePty, pipePty}
err = sessionWrap.Start()
if err != nil {
cmdPty.Close()
pipePty.Close()
return nil, err
}
return &ShellProc{Cmd: sessionWrap, Pty: cmdPty, CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
return &ShellProc{Cmd: sessionWrap, CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
}
func isZshShell(shellPath string) bool {
@ -216,7 +262,7 @@ func StartShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsType) (
// add --rcfile
// cant set -l or -i with --rcfile
shellOpts = append(shellOpts, "--rcfile", shellutil.GetBashRcFileOverride())
} else {
} else if runtime.GOOS != "windows" {
if cmdOpts.Login {
shellOpts = append(shellOpts, "-l")
}
@ -252,10 +298,6 @@ func StartShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsType) (
}
shellutil.UpdateCmdEnv(ecmd, envToAdd)
shellutil.UpdateCmdEnv(ecmd, cmdOpts.Env)
cmdPty, cmdTty, err := pty.Open()
if err != nil {
return nil, fmt.Errorf("opening new pty: %w", err)
}
if termSize.Rows == 0 || termSize.Cols == 0 {
termSize.Rows = shellutil.DefaultTermRows
termSize.Cols = shellutil.DefaultTermCols
@ -263,28 +305,17 @@ func StartShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsType) (
if termSize.Rows <= 0 || termSize.Cols <= 0 {
return nil, fmt.Errorf("invalid term size: %v", termSize)
}
pty.Setsize(cmdPty, &pty.Winsize{Rows: uint16(termSize.Rows), Cols: uint16(termSize.Cols)})
ecmd.Stdin = cmdTty
ecmd.Stdout = cmdTty
ecmd.Stderr = cmdTty
ecmd.SysProcAttr = &syscall.SysProcAttr{}
setSysProcAttrs(ecmd)
err = ecmd.Start()
cmdTty.Close()
cmdPty, err := pty.StartWithSize(ecmd, &pty.Winsize{Rows: uint16(termSize.Rows), Cols: uint16(termSize.Cols)})
if err != nil {
cmdPty.Close()
return nil, err
}
return &ShellProc{Cmd: CmdWrap{ecmd}, Pty: cmdPty, CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
return &ShellProc{Cmd: CmdWrap{ecmd, cmdPty}, CloseOnce: &sync.Once{}, DoneCh: make(chan any)}, nil
}
func RunSimpleCmdInPty(ecmd *exec.Cmd, termSize TermSize) ([]byte, error) {
ecmd.Env = os.Environ()
shellutil.UpdateCmdEnv(ecmd, shellutil.WaveshellLocalEnvVars(shellutil.DefaultTermType))
cmdPty, cmdTty, err := pty.Open()
if err != nil {
return nil, fmt.Errorf("opening new pty: %w", err)
}
if termSize.Rows == 0 || termSize.Cols == 0 {
termSize.Rows = shellutil.DefaultTermRows
termSize.Cols = shellutil.DefaultTermCols
@ -292,19 +323,14 @@ func RunSimpleCmdInPty(ecmd *exec.Cmd, termSize TermSize) ([]byte, error) {
if termSize.Rows <= 0 || termSize.Cols <= 0 {
return nil, fmt.Errorf("invalid term size: %v", termSize)
}
pty.Setsize(cmdPty, &pty.Winsize{Rows: uint16(termSize.Rows), Cols: uint16(termSize.Cols)})
ecmd.Stdin = cmdTty
ecmd.Stdout = cmdTty
ecmd.Stderr = cmdTty
ecmd.SysProcAttr = &syscall.SysProcAttr{}
setSysProcAttrs(ecmd)
err = ecmd.Start()
cmdTty.Close()
cmdPty, err := pty.StartWithSize(ecmd, &pty.Winsize{Rows: uint16(termSize.Rows), Cols: uint16(termSize.Cols)})
if err != nil {
cmdPty.Close()
return nil, err
}
if runtime.GOOS != "windows" {
defer cmdPty.Close()
}
ioDone := make(chan bool)
var outputBuf bytes.Buffer
go func() {

View File

@ -81,6 +81,9 @@ export PATH=$WAVETERM_WSHBINDIR:$PATH
)
func DetectLocalShellPath() string {
if runtime.GOOS == "windows" {
return "powershell.exe"
}
shellPath := GetMacUserShell()
if shellPath == "" {
shellPath = os.Getenv("SHELL")