mirror of https://github.com/coder/coder.git
parent
b4c29f34c3
commit
09da3858ce
|
@ -31,6 +31,7 @@
|
||||||
"gonet",
|
"gonet",
|
||||||
"gossh",
|
"gossh",
|
||||||
"gsyslog",
|
"gsyslog",
|
||||||
|
"GTTY",
|
||||||
"hashicorp",
|
"hashicorp",
|
||||||
"hclsyntax",
|
"hclsyntax",
|
||||||
"httpapi",
|
"httpapi",
|
||||||
|
@ -67,6 +68,7 @@
|
||||||
"ntqry",
|
"ntqry",
|
||||||
"OIDC",
|
"OIDC",
|
||||||
"oneof",
|
"oneof",
|
||||||
|
"opty",
|
||||||
"paralleltest",
|
"paralleltest",
|
||||||
"parameterscopeid",
|
"parameterscopeid",
|
||||||
"pqtype",
|
"pqtype",
|
||||||
|
@ -76,6 +78,7 @@
|
||||||
"provisionerd",
|
"provisionerd",
|
||||||
"provisionersdk",
|
"provisionersdk",
|
||||||
"ptty",
|
"ptty",
|
||||||
|
"ptys",
|
||||||
"ptytest",
|
"ptytest",
|
||||||
"reconfig",
|
"reconfig",
|
||||||
"retrier",
|
"retrier",
|
||||||
|
@ -87,6 +90,7 @@
|
||||||
"sourcemapped",
|
"sourcemapped",
|
||||||
"Srcs",
|
"Srcs",
|
||||||
"stretchr",
|
"stretchr",
|
||||||
|
"STTY",
|
||||||
"stuntest",
|
"stuntest",
|
||||||
"tailbroker",
|
"tailbroker",
|
||||||
"tailcfg",
|
"tailcfg",
|
||||||
|
@ -105,6 +109,7 @@
|
||||||
"tfjson",
|
"tfjson",
|
||||||
"tfplan",
|
"tfplan",
|
||||||
"tfstate",
|
"tfstate",
|
||||||
|
"tios",
|
||||||
"tparallel",
|
"tparallel",
|
||||||
"trimprefix",
|
"trimprefix",
|
||||||
"tsdial",
|
"tsdial",
|
||||||
|
|
|
@ -374,7 +374,7 @@ func (a *agent) runStartupScript(ctx context.Context, script string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
writer, err := os.OpenFile(filepath.Join(os.TempDir(), "coder-startup-script.log"), os.O_CREATE|os.O_RDWR, 0600)
|
writer, err := os.OpenFile(filepath.Join(os.TempDir(), "coder-startup-script.log"), os.O_CREATE|os.O_RDWR, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("open startup script log file: %w", err)
|
return xerrors.Errorf("open startup script log file: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -537,6 +537,8 @@ func (a *agent) init(ctx context.Context) {
|
||||||
},
|
},
|
||||||
SubsystemHandlers: map[string]ssh.SubsystemHandler{
|
SubsystemHandlers: map[string]ssh.SubsystemHandler{
|
||||||
"sftp": func(session ssh.Session) {
|
"sftp": func(session ssh.Session) {
|
||||||
|
session.DisablePTYEmulation()
|
||||||
|
|
||||||
server, err := sftp.NewServer(session)
|
server, err := sftp.NewServer(session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Debug(session.Context(), "initialize sftp server", slog.Error(err))
|
a.logger.Debug(session.Context(), "initialize sftp server", slog.Error(err))
|
||||||
|
@ -661,7 +663,8 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
|
func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
|
||||||
cmd, err := a.createCommand(session.Context(), session.RawCommand(), session.Environ())
|
ctx := session.Context()
|
||||||
|
cmd, err := a.createCommand(ctx, session.RawCommand(), session.Environ())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -678,32 +681,34 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
|
||||||
|
|
||||||
sshPty, windowSize, isPty := session.Pty()
|
sshPty, windowSize, isPty := session.Pty()
|
||||||
if isPty {
|
if isPty {
|
||||||
|
// Disable minimal PTY emulation set by gliderlabs/ssh (NL-to-CRNL).
|
||||||
|
// See https://github.com/coder/coder/issues/3371.
|
||||||
|
session.DisablePTYEmulation()
|
||||||
|
|
||||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", sshPty.Term))
|
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", sshPty.Term))
|
||||||
|
|
||||||
// The pty package sets `SSH_TTY` on supported platforms.
|
// The pty package sets `SSH_TTY` on supported platforms.
|
||||||
ptty, process, err := pty.Start(cmd)
|
ptty, process, err := pty.Start(cmd, pty.WithPTYOption(
|
||||||
|
pty.WithSSHRequest(sshPty),
|
||||||
|
pty.WithLogger(slog.Stdlib(ctx, a.logger, slog.LevelInfo)),
|
||||||
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("start command: %w", err)
|
return xerrors.Errorf("start command: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
closeErr := ptty.Close()
|
closeErr := ptty.Close()
|
||||||
if closeErr != nil {
|
if closeErr != nil {
|
||||||
a.logger.Warn(context.Background(), "failed to close tty",
|
a.logger.Warn(ctx, "failed to close tty", slog.Error(closeErr))
|
||||||
slog.Error(closeErr))
|
|
||||||
if retErr == nil {
|
if retErr == nil {
|
||||||
retErr = closeErr
|
retErr = closeErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
err = ptty.Resize(uint16(sshPty.Window.Height), uint16(sshPty.Window.Width))
|
|
||||||
if err != nil {
|
|
||||||
return xerrors.Errorf("resize ptty: %w", err)
|
|
||||||
}
|
|
||||||
go func() {
|
go func() {
|
||||||
for win := range windowSize {
|
for win := range windowSize {
|
||||||
resizeErr := ptty.Resize(uint16(win.Height), uint16(win.Width))
|
resizeErr := ptty.Resize(uint16(win.Height), uint16(win.Width))
|
||||||
if resizeErr != nil {
|
if resizeErr != nil {
|
||||||
a.logger.Warn(context.Background(), "failed to resize tty", slog.Error(resizeErr))
|
a.logger.Warn(ctx, "failed to resize tty", slog.Error(resizeErr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -718,8 +723,7 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
|
||||||
// ExitErrors just mean the command we run returned a non-zero exit code, which is normal
|
// ExitErrors just mean the command we run returned a non-zero exit code, which is normal
|
||||||
// and not something to be concerned about. But, if it's something else, we should log it.
|
// and not something to be concerned about. But, if it's something else, we should log it.
|
||||||
if err != nil && !xerrors.As(err, &exitErr) {
|
if err != nil && !xerrors.As(err, &exitErr) {
|
||||||
a.logger.Warn(context.Background(), "wait error",
|
a.logger.Warn(ctx, "wait error", slog.Error(err))
|
||||||
slog.Error(err))
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,6 +195,13 @@ func ssh() *cobra.Command {
|
||||||
// shutdown of services.
|
// shutdown of services.
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
if validOut {
|
||||||
|
// Set initial window size.
|
||||||
|
width, height, err := term.GetSize(int(stdoutFile.Fd()))
|
||||||
|
if err == nil {
|
||||||
|
_ = sshSession.WindowChange(height, width)
|
||||||
|
}
|
||||||
|
}
|
||||||
err = sshSession.Wait()
|
err = sshSession.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If the connection drops unexpectedly, we get an ExitMissingError but no other
|
// If the connection drops unexpectedly, we get an ExitMissingError but no other
|
||||||
|
|
10
go.mod
10
go.mod
|
@ -51,6 +51,15 @@ replace github.com/tcnksm/go-httpstat => github.com/kylecarbs/go-httpstat v0.0.0
|
||||||
// https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main
|
// https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main
|
||||||
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20220907193453-fb5ba5ab658d
|
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20220907193453-fb5ba5ab658d
|
||||||
|
|
||||||
|
// Switch to our fork that imports fixes from http://github.com/tailscale/ssh.
|
||||||
|
// See: https://github.com/coder/coder/issues/3371
|
||||||
|
//
|
||||||
|
// Note that http://github.com/tailscale/ssh has been merged into the Tailscale
|
||||||
|
// repo as tailscale.com/tempfork/gliderlabs/ssh, however, we can't replace the
|
||||||
|
// subpath and it includes changes to golang.org/x/crypto/ssh as well which
|
||||||
|
// makes importing it directly a bit messy.
|
||||||
|
replace github.com/gliderlabs/ssh => github.com/coder/ssh v0.0.0-20220811105153-fcea99919338
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cdr.dev/slog v1.4.2-0.20220525200111-18dce5c2cd5f
|
cdr.dev/slog v1.4.2-0.20220525200111-18dce5c2cd5f
|
||||||
cloud.google.com/go/compute v1.7.0
|
cloud.google.com/go/compute v1.7.0
|
||||||
|
@ -126,6 +135,7 @@ require (
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.8.0
|
github.com/stretchr/testify v1.8.0
|
||||||
github.com/tabbed/pqtype v0.1.1
|
github.com/tabbed/pqtype v0.1.1
|
||||||
|
github.com/u-root/u-root v0.9.0
|
||||||
github.com/unrolled/secure v1.12.0
|
github.com/unrolled/secure v1.12.0
|
||||||
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1
|
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1
|
||||||
go.opentelemetry.io/otel v1.8.0
|
go.opentelemetry.io/otel v1.8.0
|
||||||
|
|
7
go.sum
7
go.sum
|
@ -352,6 +352,8 @@ github.com/coder/glog v1.0.1-0.20220322161911-7365fe7f2cd1 h1:UqBrPWSYvRI2s5RtOu
|
||||||
github.com/coder/glog v1.0.1-0.20220322161911-7365fe7f2cd1/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
github.com/coder/glog v1.0.1-0.20220322161911-7365fe7f2cd1/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||||
github.com/coder/retry v1.3.0 h1:5lAAwt/2Cm6lVmnfBY7sOMXcBOwcwJhmV5QGSELIVWY=
|
github.com/coder/retry v1.3.0 h1:5lAAwt/2Cm6lVmnfBY7sOMXcBOwcwJhmV5QGSELIVWY=
|
||||||
github.com/coder/retry v1.3.0/go.mod h1:tXuRgZgWjUnU5LZPT4lJh4ew2elUhexhlnXzrJWdyFY=
|
github.com/coder/retry v1.3.0/go.mod h1:tXuRgZgWjUnU5LZPT4lJh4ew2elUhexhlnXzrJWdyFY=
|
||||||
|
github.com/coder/ssh v0.0.0-20220811105153-fcea99919338 h1:tN5GKFT68YLVzJoA8AHuiMNJ0qlhoD3pGN3JY9gxSko=
|
||||||
|
github.com/coder/ssh v0.0.0-20220811105153-fcea99919338/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
|
||||||
github.com/coder/tailscale v1.1.1-0.20220907193453-fb5ba5ab658d h1:IQ8wJn8MfDS+sesYPpn3EDAyvoGMxFvyyE9uWtcfU6w=
|
github.com/coder/tailscale v1.1.1-0.20220907193453-fb5ba5ab658d h1:IQ8wJn8MfDS+sesYPpn3EDAyvoGMxFvyyE9uWtcfU6w=
|
||||||
github.com/coder/tailscale v1.1.1-0.20220907193453-fb5ba5ab658d/go.mod h1:MO+tWkQp2YIF3KBnnej/mQvgYccRS5Xk/IrEpZ4Z3BU=
|
github.com/coder/tailscale v1.1.1-0.20220907193453-fb5ba5ab658d/go.mod h1:MO+tWkQp2YIF3KBnnej/mQvgYccRS5Xk/IrEpZ4Z3BU=
|
||||||
github.com/coder/wireguard-go/tun/netstack v0.0.0-20220823170024-a78136eb0cab h1:9yEvRWXXfyKzXu8AqywCi+tFZAoqCy4wVcsXwuvZNMc=
|
github.com/coder/wireguard-go/tun/netstack v0.0.0-20220823170024-a78136eb0cab h1:9yEvRWXXfyKzXu8AqywCi+tFZAoqCy4wVcsXwuvZNMc=
|
||||||
|
@ -638,9 +640,6 @@ github.com/gin-gonic/gin v1.7.0 h1:jGB9xAJQ12AIGNB4HguylppmDK1Am9ppF7XnGXXJuoU=
|
||||||
github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
|
||||||
github.com/gliderlabs/ssh v0.3.4 h1:+AXBtim7MTKaLVPgvE+3mhewYRawNLTd+jEEz/wExZw=
|
|
||||||
github.com/gliderlabs/ssh v0.3.4/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
|
|
||||||
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
||||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
||||||
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||||
|
@ -1810,6 +1809,8 @@ github.com/tomarrell/wrapcheck/v2 v2.4.0/go.mod h1:68bQ/eJg55BROaRTbMjC7vuhL2Ogf
|
||||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||||
github.com/tommy-muehle/go-mnd/v2 v2.4.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
|
github.com/tommy-muehle/go-mnd/v2 v2.4.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
|
||||||
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
|
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
|
||||||
|
github.com/u-root/u-root v0.9.0 h1:1dpUzrE0FyKrNEjxpKFOkyveuV1f3T0Ko5CQg4gTkCg=
|
||||||
|
github.com/u-root/u-root v0.9.0/go.mod h1:ewc9w6JF1ayZCVC9Y5wsrUiCBw3nMmPC3QItvrEwmew=
|
||||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
||||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 h1:hl6sK6aFgTLISijk6xIzeqnPzQcsLqqvL6vEfTPinME=
|
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 h1:hl6sK6aFgTLISijk6xIzeqnPzQcsLqqvL6vEfTPinME=
|
||||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
||||||
|
|
35
pty/pty.go
35
pty/pty.go
|
@ -2,13 +2,19 @@ package pty
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/gliderlabs/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PTY is a minimal interface for interacting with a TTY.
|
// PTY is a minimal interface for interacting with a TTY.
|
||||||
type PTY interface {
|
type PTY interface {
|
||||||
io.Closer
|
io.Closer
|
||||||
|
|
||||||
|
// Name of the TTY. Example on Linux would be "/dev/pts/1".
|
||||||
|
Name() string
|
||||||
|
|
||||||
// Output handles TTY output.
|
// Output handles TTY output.
|
||||||
//
|
//
|
||||||
// cmd.SetOutput(pty.Output()) would be used to specify a command
|
// cmd.SetOutput(pty.Output()) would be used to specify a command
|
||||||
|
@ -35,7 +41,6 @@ type PTY interface {
|
||||||
// to Wait() on a process, this abstraction provides a goroutine-safe interface for interacting with
|
// to Wait() on a process, this abstraction provides a goroutine-safe interface for interacting with
|
||||||
// the process.
|
// the process.
|
||||||
type Process interface {
|
type Process interface {
|
||||||
|
|
||||||
// Wait for the command to complete. Returned error is as for exec.Cmd.Wait()
|
// Wait for the command to complete. Returned error is as for exec.Cmd.Wait()
|
||||||
Wait() error
|
Wait() error
|
||||||
|
|
||||||
|
@ -52,9 +57,33 @@ type WithFlags interface {
|
||||||
EchoEnabled() (bool, error)
|
EchoEnabled() (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Options represents a an option for a PTY.
|
||||||
|
type Option func(*ptyOptions)
|
||||||
|
|
||||||
|
type ptyOptions struct {
|
||||||
|
logger *log.Logger
|
||||||
|
sshReq *ssh.Pty
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSSHRequest applies the ssh.Pty request to the PTY.
|
||||||
|
//
|
||||||
|
// Only partially supported on Windows (e.g. window size).
|
||||||
|
func WithSSHRequest(req ssh.Pty) Option {
|
||||||
|
return func(opts *ptyOptions) {
|
||||||
|
opts.sshReq = &req
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogger sets a logger for logging errors.
|
||||||
|
func WithLogger(logger *log.Logger) Option {
|
||||||
|
return func(opts *ptyOptions) {
|
||||||
|
opts.logger = logger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// New constructs a new Pty.
|
// New constructs a new Pty.
|
||||||
func New() (PTY, error) {
|
func New(opts ...Option) (PTY, error) {
|
||||||
return newPty()
|
return newPty(opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadWriter is an implementation of io.ReadWriter that wraps two separate
|
// ReadWriter is an implementation of io.ReadWriter that wraps two separate
|
||||||
|
|
|
@ -2,12 +2,23 @@
|
||||||
|
|
||||||
package pty
|
package pty
|
||||||
|
|
||||||
import "golang.org/x/sys/unix"
|
import (
|
||||||
|
"github.com/u-root/u-root/pkg/termios"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
func (p *otherPty) EchoEnabled() (bool, error) {
|
func (p *otherPty) EchoEnabled() (echo bool, err error) {
|
||||||
termios, err := unix.IoctlGetTermios(int(p.pty.Fd()), unix.TCGETS)
|
err = p.control(p.pty, func(fd uintptr) error {
|
||||||
|
t, err := termios.GetTermios(fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
echo = (t.Lflag & unix.ECHO) != 0
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return (termios.Lflag & unix.ECHO) != 0, nil
|
return echo, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !windows
|
//go:build !windows
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package pty
|
package pty
|
||||||
|
|
||||||
|
@ -10,19 +9,42 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
|
"github.com/u-root/u-root/pkg/termios"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newPty() (PTY, error) {
|
func newPty(opt ...Option) (retPTY *otherPty, err error) {
|
||||||
|
var opts ptyOptions
|
||||||
|
for _, o := range opt {
|
||||||
|
o(&opts)
|
||||||
|
}
|
||||||
|
|
||||||
ptyFile, ttyFile, err := pty.Open()
|
ptyFile, ttyFile, err := pty.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
opty := &otherPty{
|
||||||
|
pty: ptyFile,
|
||||||
|
tty: ttyFile,
|
||||||
|
opts: opts,
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
_ = opty.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return &otherPty{
|
if opts.sshReq != nil {
|
||||||
pty: ptyFile,
|
err = opty.control(opty.tty, func(fd uintptr) error {
|
||||||
tty: ttyFile,
|
return applyTerminalModesToFd(opts.logger, fd, *opts.sshReq)
|
||||||
}, nil
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return opty, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type otherPty struct {
|
type otherPty struct {
|
||||||
|
@ -30,15 +52,40 @@ type otherPty struct {
|
||||||
closed bool
|
closed bool
|
||||||
err error
|
err error
|
||||||
pty, tty *os.File
|
pty, tty *os.File
|
||||||
|
opts ptyOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
type otherProcess struct {
|
func (p *otherPty) control(tty *os.File, fn func(fd uintptr) error) (err error) {
|
||||||
pty *os.File
|
defer func() {
|
||||||
cmd *exec.Cmd
|
// Always echo the close error for closed ptys.
|
||||||
|
p.mutex.Lock()
|
||||||
|
defer p.mutex.Unlock()
|
||||||
|
if p.closed {
|
||||||
|
err = p.err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// cmdDone protects access to cmdErr: anything reading cmdErr should read from cmdDone first.
|
rawConn, err := tty.SyscallConn()
|
||||||
cmdDone chan any
|
if err != nil {
|
||||||
cmdErr error
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctlErr error
|
||||||
|
err = rawConn.Control(func(fd uintptr) {
|
||||||
|
ctlErr = fn(fd)
|
||||||
|
})
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
return err
|
||||||
|
case ctlErr != nil:
|
||||||
|
return ctlErr
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *otherPty) Name() string {
|
||||||
|
return p.tty.Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *otherPty) Input() ReadWriter {
|
func (p *otherPty) Input() ReadWriter {
|
||||||
|
@ -56,14 +103,13 @@ func (p *otherPty) Output() ReadWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *otherPty) Resize(height uint16, width uint16) error {
|
func (p *otherPty) Resize(height uint16, width uint16) error {
|
||||||
p.mutex.Lock()
|
return p.control(p.pty, func(fd uintptr) error {
|
||||||
defer p.mutex.Unlock()
|
return termios.SetWinSize(fd, &termios.Winsize{
|
||||||
if p.closed {
|
Winsize: unix.Winsize{
|
||||||
return p.err
|
Row: height,
|
||||||
}
|
Col: width,
|
||||||
return pty.Setsize(p.pty, &pty.Winsize{
|
},
|
||||||
Rows: height,
|
})
|
||||||
Cols: width,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +137,15 @@ func (p *otherPty) Close() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type otherProcess struct {
|
||||||
|
pty *os.File
|
||||||
|
cmd *exec.Cmd
|
||||||
|
|
||||||
|
// cmdDone protects access to cmdErr: anything reading cmdErr should read from cmdDone first.
|
||||||
|
cmdDone chan any
|
||||||
|
cmdErr error
|
||||||
|
}
|
||||||
|
|
||||||
func (p *otherProcess) Wait() error {
|
func (p *otherProcess) Wait() error {
|
||||||
<-p.cmdDone
|
<-p.cmdDone
|
||||||
return p.cmdErr
|
return p.cmdErr
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build windows
|
//go:build windows
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package pty
|
package pty
|
||||||
|
|
||||||
|
@ -22,7 +21,12 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// See: https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session
|
// See: https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session
|
||||||
func newPty() (PTY, error) {
|
func newPty(opt ...Option) (PTY, error) {
|
||||||
|
var opts ptyOptions
|
||||||
|
for _, o := range opt {
|
||||||
|
o(&opts)
|
||||||
|
}
|
||||||
|
|
||||||
// We use the CreatePseudoConsole API which was introduced in build 17763
|
// We use the CreatePseudoConsole API which was introduced in build 17763
|
||||||
vsn := windows.RtlGetVersion()
|
vsn := windows.RtlGetVersion()
|
||||||
if vsn.MajorVersion < 10 ||
|
if vsn.MajorVersion < 10 ||
|
||||||
|
@ -32,30 +36,42 @@ func newPty() (PTY, error) {
|
||||||
return nil, xerrors.Errorf("pty not supported")
|
return nil, xerrors.Errorf("pty not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
ptyWindows := &ptyWindows{}
|
pty := &ptyWindows{
|
||||||
|
opts: opts,
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
ptyWindows.inputRead, ptyWindows.inputWrite, err = os.Pipe()
|
pty.inputRead, pty.inputWrite, err = os.Pipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ptyWindows.outputRead, ptyWindows.outputWrite, err = os.Pipe()
|
pty.outputRead, pty.outputWrite, err = os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
_ = pty.inputRead.Close()
|
||||||
|
_ = pty.inputWrite.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
consoleSize := uintptr(80) + (uintptr(80) << 16)
|
consoleSize := uintptr(80) + (uintptr(80) << 16)
|
||||||
|
if opts.sshReq != nil {
|
||||||
|
consoleSize = uintptr(opts.sshReq.Window.Width) + (uintptr(opts.sshReq.Window.Height) << 16)
|
||||||
|
}
|
||||||
ret, _, err := procCreatePseudoConsole.Call(
|
ret, _, err := procCreatePseudoConsole.Call(
|
||||||
consoleSize,
|
consoleSize,
|
||||||
uintptr(ptyWindows.inputRead.Fd()),
|
uintptr(pty.inputRead.Fd()),
|
||||||
uintptr(ptyWindows.outputWrite.Fd()),
|
uintptr(pty.outputWrite.Fd()),
|
||||||
0,
|
0,
|
||||||
uintptr(unsafe.Pointer(&ptyWindows.console)),
|
uintptr(unsafe.Pointer(&pty.console)),
|
||||||
)
|
)
|
||||||
if int32(ret) < 0 {
|
if int32(ret) < 0 {
|
||||||
|
_ = pty.Close()
|
||||||
return nil, xerrors.Errorf("create pseudo console (%d): %w", int32(ret), err)
|
return nil, xerrors.Errorf("create pseudo console (%d): %w", int32(ret), err)
|
||||||
}
|
}
|
||||||
return ptyWindows, nil
|
return pty, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ptyWindows struct {
|
type ptyWindows struct {
|
||||||
|
opts ptyOptions
|
||||||
console windows.Handle
|
console windows.Handle
|
||||||
|
|
||||||
outputWrite *os.File
|
outputWrite *os.File
|
||||||
|
@ -74,6 +90,13 @@ type windowsProcess struct {
|
||||||
proc *os.Process
|
proc *os.Process
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns the TTY name on Windows.
|
||||||
|
//
|
||||||
|
// Not implemented.
|
||||||
|
func (p *ptyWindows) Name() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (p *ptyWindows) Output() ReadWriter {
|
func (p *ptyWindows) Output() ReadWriter {
|
||||||
return ReadWriter{
|
return ReadWriter{
|
||||||
Reader: p.outputRead,
|
Reader: p.outputRead,
|
||||||
|
|
|
@ -21,19 +21,19 @@ import (
|
||||||
"github.com/coder/coder/testutil"
|
"github.com/coder/coder/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(t *testing.T) *PTY {
|
func New(t *testing.T, opts ...pty.Option) *PTY {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
ptty, err := pty.New()
|
ptty, err := pty.New(opts...)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return create(t, ptty, "cmd")
|
return create(t, ptty, "cmd")
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(t *testing.T, cmd *exec.Cmd) (*PTY, pty.Process) {
|
func Start(t *testing.T, cmd *exec.Cmd, opts ...pty.StartOption) (*PTY, pty.Process) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
ptty, ps, err := pty.Start(cmd)
|
ptty, ps, err := pty.Start(cmd, opts...)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
_ = ps.Kill()
|
_ = ps.Kill()
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package pty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gliderlabs/ssh"
|
||||||
|
"github.com/u-root/u-root/pkg/termios"
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// terminalModeFlagNames maps the SSH terminal mode flags to mnemonic
|
||||||
|
// names used by the termios package.
|
||||||
|
var terminalModeFlagNames = map[uint8]string{
|
||||||
|
gossh.VINTR: "intr",
|
||||||
|
gossh.VQUIT: "quit",
|
||||||
|
gossh.VERASE: "erase",
|
||||||
|
gossh.VKILL: "kill",
|
||||||
|
gossh.VEOF: "eof",
|
||||||
|
gossh.VEOL: "eol",
|
||||||
|
gossh.VEOL2: "eol2",
|
||||||
|
gossh.VSTART: "start",
|
||||||
|
gossh.VSTOP: "stop",
|
||||||
|
gossh.VSUSP: "susp",
|
||||||
|
gossh.VDSUSP: "dsusp",
|
||||||
|
gossh.VREPRINT: "rprnt",
|
||||||
|
gossh.VWERASE: "werase",
|
||||||
|
gossh.VLNEXT: "lnext",
|
||||||
|
gossh.VFLUSH: "flush",
|
||||||
|
gossh.VSWTCH: "swtch",
|
||||||
|
gossh.VSTATUS: "status",
|
||||||
|
gossh.VDISCARD: "discard",
|
||||||
|
gossh.IGNPAR: "ignpar",
|
||||||
|
gossh.PARMRK: "parmrk",
|
||||||
|
gossh.INPCK: "inpck",
|
||||||
|
gossh.ISTRIP: "istrip",
|
||||||
|
gossh.INLCR: "inlcr",
|
||||||
|
gossh.IGNCR: "igncr",
|
||||||
|
gossh.ICRNL: "icrnl",
|
||||||
|
gossh.IUCLC: "iuclc",
|
||||||
|
gossh.IXON: "ixon",
|
||||||
|
gossh.IXANY: "ixany",
|
||||||
|
gossh.IXOFF: "ixoff",
|
||||||
|
gossh.IMAXBEL: "imaxbel",
|
||||||
|
gossh.IUTF8: "iutf8",
|
||||||
|
gossh.ISIG: "isig",
|
||||||
|
gossh.ICANON: "icanon",
|
||||||
|
gossh.XCASE: "xcase",
|
||||||
|
gossh.ECHO: "echo",
|
||||||
|
gossh.ECHOE: "echoe",
|
||||||
|
gossh.ECHOK: "echok",
|
||||||
|
gossh.ECHONL: "echonl",
|
||||||
|
gossh.NOFLSH: "noflsh",
|
||||||
|
gossh.TOSTOP: "tostop",
|
||||||
|
gossh.IEXTEN: "iexten",
|
||||||
|
gossh.ECHOCTL: "echoctl",
|
||||||
|
gossh.ECHOKE: "echoke",
|
||||||
|
gossh.PENDIN: "pendin",
|
||||||
|
gossh.OPOST: "opost",
|
||||||
|
gossh.OLCUC: "olcuc",
|
||||||
|
gossh.ONLCR: "onlcr",
|
||||||
|
gossh.OCRNL: "ocrnl",
|
||||||
|
gossh.ONOCR: "onocr",
|
||||||
|
gossh.ONLRET: "onlret",
|
||||||
|
gossh.CS7: "cs7",
|
||||||
|
gossh.CS8: "cs8",
|
||||||
|
gossh.PARENB: "parenb",
|
||||||
|
gossh.PARODD: "parodd",
|
||||||
|
gossh.TTY_OP_ISPEED: "tty_op_ispeed",
|
||||||
|
gossh.TTY_OP_OSPEED: "tty_op_ospeed",
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyTerminalModesToFd applies the terminal settings from the SSH
|
||||||
|
// request to the given fd.
|
||||||
|
//
|
||||||
|
// This is based on code from Tailscale's tailssh package:
|
||||||
|
// https://github.com/tailscale/tailscale/blob/main/ssh/tailssh/incubator.go
|
||||||
|
func applyTerminalModesToFd(logger *log.Logger, fd uintptr, req ssh.Pty) error {
|
||||||
|
// Get the current TTY configuration.
|
||||||
|
tios, err := termios.GTTY(int(fd))
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("GTTY: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the modes from the SSH request.
|
||||||
|
tios.Row = req.Window.Height
|
||||||
|
tios.Col = req.Window.Width
|
||||||
|
|
||||||
|
for c, v := range req.Modes {
|
||||||
|
if c == gossh.TTY_OP_ISPEED {
|
||||||
|
tios.Ispeed = int(v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == gossh.TTY_OP_OSPEED {
|
||||||
|
tios.Ospeed = int(v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
k, ok := terminalModeFlagNames[c]
|
||||||
|
if !ok {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Printf("unknown terminal mode: %d", c)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := tios.CC[k]; ok {
|
||||||
|
tios.CC[k] = uint8(v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := tios.Opts[k]; ok {
|
||||||
|
tios.Opts[k] = v > 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if logger != nil {
|
||||||
|
logger.Printf("unsupported terminal mode: k=%s, c=%d, v=%d", k, c, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the new TTY configuration.
|
||||||
|
if _, err := tios.STTY(int(fd)); err != nil {
|
||||||
|
return xerrors.Errorf("STTY: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
18
pty/start.go
18
pty/start.go
|
@ -4,8 +4,22 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// StartOption represents a configuration option passed to Start.
|
||||||
|
type StartOption func(*startOptions)
|
||||||
|
|
||||||
|
type startOptions struct {
|
||||||
|
ptyOpts []Option
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPTYOption applies the given options to the underlying PTY.
|
||||||
|
func WithPTYOption(opts ...Option) StartOption {
|
||||||
|
return func(o *startOptions) {
|
||||||
|
o.ptyOpts = append(o.ptyOpts, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start the command in a TTY. The calling code must not use cmd after passing it to the PTY, and
|
// Start the command in a TTY. The calling code must not use cmd after passing it to the PTY, and
|
||||||
// instead rely on the returned Process to manage the command/process.
|
// instead rely on the returned Process to manage the command/process.
|
||||||
func Start(cmd *exec.Cmd) (PTY, Process, error) {
|
func Start(cmd *exec.Cmd, opt ...StartOption) (PTY, Process, error) {
|
||||||
return startPty(cmd)
|
return startPty(cmd, opt...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !windows
|
//go:build !windows
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package pty
|
package pty
|
||||||
|
|
||||||
|
@ -10,45 +9,49 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/creack/pty"
|
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func startPty(cmd *exec.Cmd) (PTY, Process, error) {
|
func startPty(cmd *exec.Cmd, opt ...StartOption) (retPTY *otherPty, proc Process, err error) {
|
||||||
ptty, tty, err := pty.Open()
|
var opts startOptions
|
||||||
if err != nil {
|
for _, o := range opt {
|
||||||
return nil, nil, xerrors.Errorf("open: %w", err)
|
o(&opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
opty, err := newPty(opts.ptyOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, xerrors.Errorf("newPty failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
origEnv := cmd.Env
|
||||||
|
if opty.opts.sshReq != nil {
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_TTY=%s", opty.Name()))
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_PTY=%s", tty.Name()))
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
Setsid: true,
|
Setsid: true,
|
||||||
Setctty: true,
|
Setctty: true,
|
||||||
}
|
}
|
||||||
cmd.Stdout = tty
|
cmd.Stdout = opty.tty
|
||||||
cmd.Stderr = tty
|
cmd.Stderr = opty.tty
|
||||||
cmd.Stdin = tty
|
cmd.Stdin = opty.tty
|
||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = ptty.Close()
|
_ = opty.Close()
|
||||||
_ = tty.Close()
|
|
||||||
if runtime.GOOS == "darwin" && strings.Contains(err.Error(), "bad file descriptor") {
|
if runtime.GOOS == "darwin" && strings.Contains(err.Error(), "bad file descriptor") {
|
||||||
// macOS has an obscure issue where the PTY occasionally closes
|
// macOS has an obscure issue where the PTY occasionally closes
|
||||||
// before it's used. It's unknown why this is, but creating a new
|
// before it's used. It's unknown why this is, but creating a new
|
||||||
// TTY resolves it.
|
// TTY resolves it.
|
||||||
return startPty(cmd)
|
cmd.Env = origEnv
|
||||||
|
return startPty(cmd, opt...)
|
||||||
}
|
}
|
||||||
return nil, nil, xerrors.Errorf("start: %w", err)
|
return nil, nil, xerrors.Errorf("start: %w", err)
|
||||||
}
|
}
|
||||||
oPty := &otherPty{
|
|
||||||
pty: ptty,
|
|
||||||
tty: tty,
|
|
||||||
}
|
|
||||||
oProcess := &otherProcess{
|
oProcess := &otherProcess{
|
||||||
pty: ptty,
|
pty: opty.pty,
|
||||||
cmd: cmd,
|
cmd: cmd,
|
||||||
cmdDone: make(chan any),
|
cmdDone: make(chan any),
|
||||||
}
|
}
|
||||||
go oProcess.waitInternal()
|
go oProcess.waitInternal()
|
||||||
return oPty, oProcess, nil
|
return opty, oProcess, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,13 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gliderlabs/ssh"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/goleak"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"go.uber.org/goleak"
|
"github.com/coder/coder/pty"
|
||||||
|
|
||||||
"github.com/coder/coder/pty/ptytest"
|
"github.com/coder/coder/pty/ptytest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -40,10 +41,16 @@ func TestStart(t *testing.T) {
|
||||||
assert.NotEqual(t, 0, exitErr.ExitCode())
|
assert.NotEqual(t, 0, exitErr.ExitCode())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("SSH_PTY", func(t *testing.T) {
|
t.Run("SSH_TTY", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
pty, ps := ptytest.Start(t, exec.Command("env"))
|
opts := pty.WithPTYOption(pty.WithSSHRequest(ssh.Pty{
|
||||||
pty.ExpectMatch("SSH_PTY=/dev/")
|
Window: ssh.Window{
|
||||||
|
Width: 80,
|
||||||
|
Height: 24,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
pty, ps := ptytest.Start(t, exec.Command("env"), opts)
|
||||||
|
pty.ExpectMatch("SSH_TTY=/dev/")
|
||||||
err := ps.Wait()
|
err := ps.Wait()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package pty
|
package pty
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -16,7 +17,12 @@ import (
|
||||||
|
|
||||||
// Allocates a PTY and starts the specified command attached to it.
|
// 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
|
// See: https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#creating-the-hosted-process
|
||||||
func startPty(cmd *exec.Cmd) (PTY, Process, error) {
|
func startPty(cmd *exec.Cmd, opt ...StartOption) (PTY, Process, error) {
|
||||||
|
var opts startOptions
|
||||||
|
for _, o := range opt {
|
||||||
|
o(&opts)
|
||||||
|
}
|
||||||
|
|
||||||
fullPath, err := exec.LookPath(cmd.Path)
|
fullPath, err := exec.LookPath(cmd.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
@ -39,11 +45,14 @@ func startPty(cmd *exec.Cmd) (PTY, Process, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
pty, err := newPty()
|
pty, err := newPty(opts.ptyOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
winPty := pty.(*ptyWindows)
|
winPty := pty.(*ptyWindows)
|
||||||
|
if winPty.opts.sshReq != nil {
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_TTY=%s", winPty.Name()))
|
||||||
|
}
|
||||||
|
|
||||||
attrs, err := windows.NewProcThreadAttributeList(1)
|
attrs, err := windows.NewProcThreadAttributeList(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in New Issue