Skip to content

Commit 69a7781

Browse files
committed
feat: support windows conpty
1 parent 1a1da3b commit 69a7781

File tree

7 files changed

+122
-372
lines changed

7 files changed

+122
-372
lines changed

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module github.com/charmbracelet/ssh
22

3-
go 1.17
3+
go 1.21
4+
5+
toolchain go1.21.5
46

57
require (
68
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
@@ -10,3 +12,5 @@ require (
1012
)
1113

1214
require golang.org/x/sys v0.16.0
15+
16+
require github.com/charmbracelet/x/term v0.0.0-20240109162103-b354873f6f2c

go.sum

Lines changed: 2 additions & 365 deletions
Large diffs are not rendered by default.

pty.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"errors"
66
"io"
7+
"os/exec"
78
)
89

910
// ErrUnsupported is returned when the platform does not support PTY.
@@ -59,3 +60,12 @@ func (rw readWriterDelegate) Read(p []byte) (n int, err error) {
5960
func (rw readWriterDelegate) Write(p []byte) (n int, err error) {
6061
return rw.w.Write(p)
6162
}
63+
64+
// Start starts a *exec.Cmd attached to the Session. If a PTY is allocated,
65+
// it will use that for I/O.
66+
// On Windows, the process execution lifecycle is not managed by Go and has to
67+
// be managed manually. This means that c.Wait() won't work.
68+
// See https://github.com/charmbracelet/x/blob/main/term/conpty/conpty_windows.go#L155
69+
func (p *Pty) Start(c *exec.Cmd) error {
70+
return p.start(c)
71+
}

pty_other.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris
2-
// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris
1+
//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris && !windows
2+
// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris,!windows
33

4-
// TODO: support Windows
54
package ssh
65

76
import (
7+
"os/exec"
8+
89
"golang.org/x/crypto/ssh"
910
)
1011

@@ -26,6 +27,10 @@ func (i *impl) Close() error {
2627
return nil
2728
}
2829

30+
func (*impl) start(*exec.Cmd) error {
31+
return ErrUnsupported
32+
}
33+
2934
func newPty(Context, string, Window, ssh.TerminalModes) (impl, error) {
3035
return impl{}, ErrUnsupported
3136
}

pty_unix.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package ssh
66
import (
77
"fmt"
88
"os"
9+
"os/exec"
10+
"syscall"
911

1012
"github.com/creack/pty"
1113
"github.com/u-root/u-root/pkg/termios"
@@ -57,6 +59,13 @@ func (i *impl) Resize(w int, h int) (rErr error) {
5759
})
5860
}
5961

62+
func (i *impl) start(c *exec.Cmd) error {
63+
c.Stdin, c.Stdout, c.Stderr := i.Slave, i.Slave, i.Slave
64+
if c.SysProcAttr == nil {
65+
c.SysProcAttr = &syscall.SysProcAttr{}
66+
}
67+
}
68+
6069
func newPty(_ Context, _ string, win Window, modes ssh.TerminalModes) (_ impl, rErr error) {
6170
ptm, pts, err := pty.Open()
6271
if err != nil {

pty_windows.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//go:build windows
2+
// +build windows
3+
4+
package ssh
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"os/exec"
10+
"syscall"
11+
12+
"github.com/charmbracelet/x/term/conpty"
13+
"golang.org/x/crypto/ssh"
14+
"golang.org/x/sys/windows"
15+
)
16+
17+
type impl struct {
18+
Context
19+
*conpty.ConPty
20+
}
21+
22+
func (i *impl) Read(p []byte) (n int, err error) {
23+
return i.ConPty.Read(p)
24+
}
25+
26+
func (i *impl) Write(p []byte) (n int, err error) {
27+
return i.ConPty.Write(p)
28+
}
29+
30+
func (i *impl) Resize(w int, h int) error {
31+
return i.ConPty.Resize(w, h)
32+
}
33+
34+
func (i *impl) Close() error {
35+
return i.ConPty.Close()
36+
}
37+
38+
func (i *impl) start(c *exec.Cmd) error {
39+
pid, process, err := i.Spawn(c.Path, c.Args, &syscall.ProcAttr{
40+
Dir: c.Dir,
41+
Env: c.Env,
42+
Sys: c.SysProcAttr,
43+
})
44+
if err != nil {
45+
return err
46+
}
47+
48+
c.Process, err = os.FindProcess(pid)
49+
if err != nil {
50+
// If we can't find the process via os.FindProcess, terminate the
51+
// process as that's what we rely on for all further operations on the
52+
// object.
53+
if tErr := windows.TerminateProcess(process, 1); tErr != nil {
54+
return fmt.Errorf("failed to terminate process after process not found: %w", tErr)
55+
}
56+
return fmt.Errorf("failed to find process after starting: %w", err)
57+
}
58+
59+
type result struct {
60+
*os.ProcessState
61+
error
62+
}
63+
donec := make(chan result, 1)
64+
go func() {
65+
state, err := c.Process.Wait()
66+
donec <- result{state, err}
67+
}()
68+
go func() {
69+
select {
70+
case <-i.Context.Done():
71+
c.Err = windows.TerminateProcess(process, 1)
72+
case r := <-donec:
73+
c.ProcessState = r.ProcessState
74+
c.Err = r.error
75+
}
76+
}()
77+
78+
return nil
79+
}
80+
81+
func newPty(ctx Context, _ string, win Window, _ ssh.TerminalModes) (impl, error) {
82+
c, err := conpty.New(win.Width, win.Height, 0)
83+
if err != nil {
84+
return impl{}, err
85+
}
86+
87+
return impl{ctx, c}, nil
88+
}

session.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"errors"
55
"fmt"
66
"io"
7-
"log"
87
"net"
98
"sync"
109

@@ -383,14 +382,12 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
383382
}()
384383
req.Reply(ok, nil)
385384
case "window-change":
386-
log.Printf("window resize event")
387385
if sess.pty == nil {
388386
req.Reply(false, nil)
389387
continue
390388
}
391389
win, _, ok := parseWindow(req.Payload)
392390
if ok {
393-
log.Printf("window resize %dx%d", win.Width, win.Height)
394391
sess.pty.Window = win
395392
sess.winch <- win
396393
}

0 commit comments

Comments
 (0)