mirror of https://github.com/coder/coder.git
203 lines
3.6 KiB
Go
203 lines
3.6 KiB
Go
//go:build !windows
|
|
|
|
package pty
|
|
|
|
import (
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"sync"
|
|
|
|
"github.com/creack/pty"
|
|
"github.com/u-root/u-root/pkg/termios"
|
|
"golang.org/x/sys/unix"
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
func newPty(opt ...Option) (retPTY *otherPty, err error) {
|
|
var opts ptyOptions
|
|
for _, o := range opt {
|
|
o(&opts)
|
|
}
|
|
|
|
ptyFile, ttyFile, err := pty.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
opty := &otherPty{
|
|
pty: ptyFile,
|
|
tty: ttyFile,
|
|
opts: opts,
|
|
name: ttyFile.Name(),
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
_ = opty.Close()
|
|
}
|
|
}()
|
|
|
|
if opts.sshReq != nil {
|
|
err = opty.control(opty.tty, func(fd uintptr) error {
|
|
return applyTerminalModesToFd(opts.logger, fd, *opts.sshReq)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return opty, nil
|
|
}
|
|
|
|
type otherPty struct {
|
|
mutex sync.Mutex
|
|
closed bool
|
|
err error
|
|
pty, tty *os.File
|
|
opts ptyOptions
|
|
name string
|
|
}
|
|
|
|
func (p *otherPty) control(tty *os.File, fn func(fd uintptr) error) (err error) {
|
|
defer func() {
|
|
// Always echo the close error for closed ptys.
|
|
p.mutex.Lock()
|
|
defer p.mutex.Unlock()
|
|
if p.closed {
|
|
err = p.err
|
|
}
|
|
}()
|
|
|
|
rawConn, err := tty.SyscallConn()
|
|
if err != nil {
|
|
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.name
|
|
}
|
|
|
|
func (p *otherPty) Input() ReadWriter {
|
|
return ReadWriter{
|
|
Reader: p.tty,
|
|
Writer: p.pty,
|
|
}
|
|
}
|
|
|
|
func (p *otherPty) InputWriter() io.Writer {
|
|
return p.pty
|
|
}
|
|
|
|
func (p *otherPty) Output() ReadWriter {
|
|
return ReadWriter{
|
|
Reader: &ptmReader{p.pty},
|
|
Writer: p.tty,
|
|
}
|
|
}
|
|
|
|
func (p *otherPty) OutputReader() io.Reader {
|
|
return &ptmReader{p.pty}
|
|
}
|
|
|
|
func (p *otherPty) Resize(height uint16, width uint16) error {
|
|
return p.control(p.pty, func(fd uintptr) error {
|
|
return termios.SetWinSize(fd, &termios.Winsize{
|
|
Winsize: unix.Winsize{
|
|
Row: height,
|
|
Col: width,
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
func (p *otherPty) Close() error {
|
|
p.mutex.Lock()
|
|
defer p.mutex.Unlock()
|
|
|
|
if p.closed {
|
|
return p.err
|
|
}
|
|
p.closed = true
|
|
|
|
err := p.pty.Close()
|
|
// tty is closed & unset if we Start() a new process
|
|
if p.tty != nil {
|
|
err2 := p.tty.Close()
|
|
if err == nil {
|
|
err = err2
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
p.err = err
|
|
} else {
|
|
p.err = ErrClosed
|
|
}
|
|
|
|
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 {
|
|
<-p.cmdDone
|
|
return p.cmdErr
|
|
}
|
|
|
|
func (p *otherProcess) Kill() error {
|
|
return p.cmd.Process.Kill()
|
|
}
|
|
|
|
func (p *otherProcess) Signal(sig os.Signal) error {
|
|
return p.cmd.Process.Signal(sig)
|
|
}
|
|
|
|
func (p *otherProcess) waitInternal() {
|
|
// The GC can garbage collect the TTY FD before the command
|
|
// has finished running. See:
|
|
// https://github.com/creack/pty/issues/127#issuecomment-932764012
|
|
p.cmdErr = p.cmd.Wait()
|
|
runtime.KeepAlive(p.pty)
|
|
close(p.cmdDone)
|
|
}
|
|
|
|
// ptmReader wraps a reference to the ptm side of a pseudo-TTY for portability
|
|
type ptmReader struct {
|
|
ptm io.Reader
|
|
}
|
|
|
|
func (r *ptmReader) Read(p []byte) (n int, err error) {
|
|
n, err = r.ptm.Read(p)
|
|
// output from the ptm will hit a PathErr when the process hangs up the
|
|
// other side (typically when the process exits, but could be earlier). For
|
|
// portability, and to fit with our use of io.Copy() to copy from the PTY,
|
|
// we want to translate this error into io.EOF
|
|
pathErr := &fs.PathError{}
|
|
if xerrors.As(err, &pathErr) {
|
|
return n, io.EOF
|
|
}
|
|
return n, err
|
|
}
|