mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Add workspace agent for SSH (#318)
* feat: Add workspace agent for SSH This adds the initial agent that supports TTY and execution over SSH. It functions across MacOS, Windows, and Linux. This does not handle the coderd interaction yet, but does setup a simple path forward. * Fix pty tests on Windows * Fix log race * Lock around dial error to fix log output * Fix context return early * fix: Leaking yamux session after HTTP handler is closed Closes #317. We depended on the context canceling the yamux connection, but this isn't a sync operation. Explicitly calling close ensures the handler waits for yamux to complete before exit. * Lock around close return * Force failure with log * Fix failed handler * Upgrade dep * Fix defer inside loops * Fix context cancel for HTTP requests * Fix resize
This commit is contained in:
+1
-1
@@ -45,7 +45,7 @@ func (p *otherPty) Output() io.ReadWriter {
|
||||
func (p *otherPty) Resize(cols uint16, rows uint16) error {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
return pty.Setsize(p.tty, &pty.Winsize{
|
||||
return pty.Setsize(p.pty, &pty.Winsize{
|
||||
Rows: rows,
|
||||
Cols: cols,
|
||||
})
|
||||
|
||||
+8
-5
@@ -96,12 +96,15 @@ func (p *ptyWindows) Close() error {
|
||||
return nil
|
||||
}
|
||||
p.closed = true
|
||||
|
||||
ret, _, err := procClosePseudoConsole.Call(uintptr(p.console))
|
||||
if ret != 0 {
|
||||
return xerrors.Errorf("close pseudo console: %w", err)
|
||||
}
|
||||
_ = p.outputWrite.Close()
|
||||
_ = p.outputRead.Close()
|
||||
_ = p.inputWrite.Close()
|
||||
_ = p.inputRead.Close()
|
||||
|
||||
ret, _, err := procClosePseudoConsole.Call(uintptr(p.console))
|
||||
if ret < 0 {
|
||||
return xerrors.Errorf("close pseudo console: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+11
-4
@@ -5,8 +5,10 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
@@ -28,10 +30,10 @@ func New(t *testing.T) *PTY {
|
||||
return create(t, ptty)
|
||||
}
|
||||
|
||||
func Start(t *testing.T, cmd *exec.Cmd) *PTY {
|
||||
ptty, err := pty.Start(cmd)
|
||||
func Start(t *testing.T, cmd *exec.Cmd) (*PTY, *os.Process) {
|
||||
ptty, ps, err := pty.Start(cmd)
|
||||
require.NoError(t, err)
|
||||
return create(t, ptty)
|
||||
return create(t, ptty), ps
|
||||
}
|
||||
|
||||
func create(t *testing.T, ptty pty.PTY) *PTY {
|
||||
@@ -86,10 +88,15 @@ func (p *PTY) ExpectMatch(str string) string {
|
||||
break
|
||||
}
|
||||
}
|
||||
p.t.Logf("matched %q = %q", str, stripAnsi.ReplaceAllString(buffer.String(), ""))
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (p *PTY) WriteLine(str string) {
|
||||
_, err := fmt.Fprintf(p.PTY.Input(), "%s\n", str)
|
||||
newline := "\n"
|
||||
if runtime.GOOS == "windows" {
|
||||
newline = "\r\n"
|
||||
}
|
||||
_, err := fmt.Fprintf(p.PTY.Input(), "%s%s", str, newline)
|
||||
require.NoError(p.t, err)
|
||||
}
|
||||
|
||||
+5
-2
@@ -1,7 +1,10 @@
|
||||
package pty
|
||||
|
||||
import "os/exec"
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Start(cmd *exec.Cmd) (PTY, error) {
|
||||
func Start(cmd *exec.Cmd) (PTY, *os.Process, error) {
|
||||
return startPty(cmd)
|
||||
}
|
||||
|
||||
+7
-5
@@ -4,6 +4,7 @@
|
||||
package pty
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
@@ -11,10 +12,10 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func startPty(cmd *exec.Cmd) (PTY, error) {
|
||||
func startPty(cmd *exec.Cmd) (PTY, *os.Process, error) {
|
||||
ptty, tty, err := pty.Open()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("open: %w", err)
|
||||
return nil, nil, xerrors.Errorf("open: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tty.Close()
|
||||
@@ -29,10 +30,11 @@ func startPty(cmd *exec.Cmd) (PTY, error) {
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
_ = ptty.Close()
|
||||
return nil, xerrors.Errorf("start: %w", err)
|
||||
return nil, nil, xerrors.Errorf("start: %w", err)
|
||||
}
|
||||
return &otherPty{
|
||||
oPty := &otherPty{
|
||||
pty: ptty,
|
||||
tty: tty,
|
||||
}, nil
|
||||
}
|
||||
return oPty, cmd.Process, nil
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/coder/coder/pty/ptytest"
|
||||
"go.uber.org/goleak"
|
||||
|
||||
"github.com/coder/coder/pty/ptytest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -19,7 +20,7 @@ func TestStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Echo", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
pty := ptytest.Start(t, exec.Command("echo", "test"))
|
||||
pty, _ := ptytest.Start(t, exec.Command("echo", "test"))
|
||||
pty.ExpectMatch("test")
|
||||
})
|
||||
}
|
||||
|
||||
+16
-11
@@ -11,47 +11,48 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Allocates a PTY and starts the specified command attached to it.
|
||||
// See: https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#creating-the-hosted-process
|
||||
func startPty(cmd *exec.Cmd) (PTY, error) {
|
||||
func startPty(cmd *exec.Cmd) (PTY, *os.Process, error) {
|
||||
fullPath, err := exec.LookPath(cmd.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
pathPtr, err := windows.UTF16PtrFromString(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
argsPtr, err := windows.UTF16PtrFromString(windows.ComposeCommandLine(cmd.Args))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if cmd.Dir == "" {
|
||||
cmd.Dir, err = os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
dirPtr, err := windows.UTF16PtrFromString(cmd.Dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
pty, err := newPty()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
winPty := pty.(*ptyWindows)
|
||||
|
||||
attrs, err := windows.NewProcThreadAttributeList(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
// Taken from: https://github.com/microsoft/hcsshim/blob/2314362e977aa03b3ed245a4beb12d00422af0e2/internal/winapi/process.go#L6
|
||||
err = attrs.Update(0x20016, unsafe.Pointer(winPty.console), unsafe.Sizeof(winPty.console))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
startupInfo := &windows.StartupInfoEx{}
|
||||
@@ -73,12 +74,16 @@ func startPty(cmd *exec.Cmd) (PTY, error) {
|
||||
&processInfo,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
defer windows.CloseHandle(processInfo.Thread)
|
||||
defer windows.CloseHandle(processInfo.Process)
|
||||
|
||||
return pty, nil
|
||||
process, err := os.FindProcess(int(processInfo.ProcessId))
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("find process %d: %w", processInfo.ProcessId, err)
|
||||
}
|
||||
return pty, process, nil
|
||||
}
|
||||
|
||||
// Taken from: https://github.com/microsoft/hcsshim/blob/7fbdca16f91de8792371ba22b7305bf4ca84170a/internal/exec/exec.go#L476
|
||||
|
||||
@@ -20,12 +20,12 @@ func TestStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Echo", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
pty := ptytest.Start(t, exec.Command("cmd.exe", "/c", "echo", "test"))
|
||||
pty, _ := ptytest.Start(t, exec.Command("cmd.exe", "/c", "echo", "test"))
|
||||
pty.ExpectMatch("test")
|
||||
})
|
||||
t.Run("Resize", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
pty := ptytest.Start(t, exec.Command("cmd.exe"))
|
||||
pty, _ := ptytest.Start(t, exec.Command("cmd.exe"))
|
||||
err := pty.Resize(100, 50)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user