diff --git a/Taskfile.yml b/Taskfile.yml index 2cb7569df..cf5f81e45 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -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 diff --git a/cmd/server/main-server.go b/cmd/server/main-server.go index 09497770c..92c2d77ff 100644 --- a/cmd/server/main-server.go +++ b/cmd/server/main-server.go @@ -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) } diff --git a/emain/emain.ts b/emain/emain.ts index 76877969c..06af37da6 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -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); } diff --git a/go.mod b/go.mod index 3e7c7a121..ffa7c50b7 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 4562141c9..947991b29 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 84a6a97d6..6c3c87316 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -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) } } } diff --git a/pkg/shellexec/conninterface.go b/pkg/shellexec/conninterface.go index 1fcaa8fef..a2d5a7dd1 100644 --- a/pkg/shellexec/conninterface.go +++ b/pkg/shellexec/conninterface.go @@ -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() { diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index 67a277d06..c5212f676 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -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 } - defer cmdPty.Close() + if runtime.GOOS != "windows" { + defer cmdPty.Close() + } ioDone := make(chan bool) var outputBuf bytes.Buffer go func() { diff --git a/pkg/util/shellutil/shellutil.go b/pkg/util/shellutil/shellutil.go index 81dd772c4..48a3bbfbf 100644 --- a/pkg/util/shellutil/shellutil.go +++ b/pkg/util/shellutil/shellutil.go @@ -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")