Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/charmbracelet/ssh

go 1.17
go 1.21

toolchain go1.21.5

require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
Expand All @@ -10,3 +12,5 @@ require (
)

require golang.org/x/sys v0.16.0

require github.com/charmbracelet/x/term v0.0.0-20240109162103-b354873f6f2c
367 changes: 2 additions & 365 deletions go.sum

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions pty.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"io"
"os/exec"
)

// ErrUnsupported is returned when the platform does not support PTY.
Expand Down Expand Up @@ -59,3 +60,12 @@ func (rw readWriterDelegate) Read(p []byte) (n int, err error) {
func (rw readWriterDelegate) Write(p []byte) (n int, err error) {
return rw.w.Write(p)
}

// Start starts a *exec.Cmd attached to the Session. If a PTY is allocated,
// it will use that for I/O.
// On Windows, the process execution lifecycle is not managed by Go and has to
// be managed manually. This means that c.Wait() won't work.
// See https://github.com/charmbracelet/x/blob/main/term/conpty/conpty_windows.go#L155
func (p *Pty) Start(c *exec.Cmd) error {
return p.start(c)
}
11 changes: 8 additions & 3 deletions pty_other.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris
// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris
//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris && !windows
// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris,!windows

// TODO: support Windows
package ssh

import (
"os/exec"

"golang.org/x/crypto/ssh"
)

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

func (*impl) start(*exec.Cmd) error {
return ErrUnsupported
}

func newPty(Context, string, Window, ssh.TerminalModes) (impl, error) {
return impl{}, ErrUnsupported
}
9 changes: 9 additions & 0 deletions pty_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package ssh
import (
"fmt"
"os"
"os/exec"
"syscall"

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

func (i *impl) start(c *exec.Cmd) error {
c.Stdin, c.Stdout, c.Stderr := i.Slave, i.Slave, i.Slave
if c.SysProcAttr == nil {
c.SysProcAttr = &syscall.SysProcAttr{}
}
}

func newPty(_ Context, _ string, win Window, modes ssh.TerminalModes) (_ impl, rErr error) {
ptm, pts, err := pty.Open()
if err != nil {
Expand Down
88 changes: 88 additions & 0 deletions pty_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//go:build windows
// +build windows

package ssh

import (
"fmt"
"os"
"os/exec"
"syscall"

"github.com/charmbracelet/x/term/conpty"
"golang.org/x/crypto/ssh"
"golang.org/x/sys/windows"
)

type impl struct {
Context
*conpty.ConPty
}

func (i *impl) Read(p []byte) (n int, err error) {
return i.ConPty.Read(p)
}

func (i *impl) Write(p []byte) (n int, err error) {
return i.ConPty.Write(p)
}

func (i *impl) Resize(w int, h int) error {
return i.ConPty.Resize(w, h)
}

func (i *impl) Close() error {
return i.ConPty.Close()
}

func (i *impl) start(c *exec.Cmd) error {
pid, process, err := i.Spawn(c.Path, c.Args, &syscall.ProcAttr{
Dir: c.Dir,
Env: c.Env,
Sys: c.SysProcAttr,
})
if err != nil {
return err
}

c.Process, err = os.FindProcess(pid)
if err != nil {
// If we can't find the process via os.FindProcess, terminate the
// process as that's what we rely on for all further operations on the
// object.
if tErr := windows.TerminateProcess(process, 1); tErr != nil {
return fmt.Errorf("failed to terminate process after process not found: %w", tErr)
}
return fmt.Errorf("failed to find process after starting: %w", err)
}

type result struct {
*os.ProcessState
error
}
donec := make(chan result, 1)
go func() {
state, err := c.Process.Wait()
donec <- result{state, err}
}()
go func() {
select {
case <-i.Context.Done():
c.Err = windows.TerminateProcess(process, 1)
case r := <-donec:
c.ProcessState = r.ProcessState
c.Err = r.error
}
}()

return nil
}

func newPty(ctx Context, _ string, win Window, _ ssh.TerminalModes) (impl, error) {
c, err := conpty.New(win.Width, win.Height, 0)
if err != nil {
return impl{}, err
}

return impl{ctx, c}, nil
}
3 changes: 0 additions & 3 deletions session.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"io"
"log"
"net"
"sync"

Expand Down Expand Up @@ -383,14 +382,12 @@ func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
}()
req.Reply(ok, nil)
case "window-change":
log.Printf("window resize event")
if sess.pty == nil {
req.Reply(false, nil)
continue
}
win, _, ok := parseWindow(req.Payload)
if ok {
log.Printf("window resize %dx%d", win.Width, win.Height)
sess.pty.Window = win
sess.winch <- win
}
Expand Down