coder/pty/pty_other.go

166 lines
2.7 KiB
Go

//go:build !windows
package pty
import (
"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,
}
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
}
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.tty.Name()
}
func (p *otherPty) Input() ReadWriter {
return ReadWriter{
Reader: p.tty,
Writer: p.pty,
}
}
func (p *otherPty) Output() ReadWriter {
return ReadWriter{
Reader: p.pty,
Writer: p.tty,
}
}
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()
err2 := p.tty.Close()
if err == nil {
err = err2
}
if err != nil {
p.err = err
} else {
p.err = xerrors.New("pty: closed")
}
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) 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)
}