-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b34e00b
commit 38b63e0
Showing
44 changed files
with
3,091 additions
and
730 deletions.
There are no files selected for viewing
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
package pty | ||
|
||
import ( | ||
"io" | ||
|
||
"golang.org/x/xerrors" | ||
) | ||
|
||
// ErrClosed is returned when a PTY is used after it has been closed. | ||
var ErrClosed = xerrors.New("pty: closed") | ||
|
||
// PTYCmd is an interface for interacting with a pseudo-TTY where we control | ||
// only one end, and the other end has been passed to a running os.Process. | ||
// nolint:revive | ||
type PTYCmd interface { | ||
io.Closer | ||
|
||
// Resize sets the size of the PTY. | ||
Resize(width int, height int) error | ||
|
||
// OutputReader returns an io.Reader for reading the output from the process | ||
// controlled by the pseudo-TTY | ||
OutputReader() io.Reader | ||
|
||
// InputWriter returns an io.Writer for writing into to the process | ||
// controlled by the pseudo-TTY | ||
InputWriter() io.Writer | ||
} | ||
|
||
// PTY is a minimal interface for interacting with pseudo-TTY where this | ||
// process retains access to _both_ ends of the pseudo-TTY (i.e. `ptm` & `pts` | ||
// on Linux). | ||
type PTY interface { | ||
io.Closer | ||
|
||
// Resize sets the size of the PTY. | ||
Resize(width int, height int) error | ||
|
||
// Name of the TTY. Example on Linux would be "/dev/pts/1". | ||
Name() string | ||
|
||
// Output handles TTY output. | ||
// | ||
// cmd.SetOutput(pty.Output()) would be used to specify a command | ||
// uses the output stream for writing. | ||
// | ||
// The same stream could be read to validate output. | ||
Output() io.ReadWriter | ||
|
||
// Input handles TTY input. | ||
// | ||
// cmd.SetInput(pty.Input()) would be used to specify a command | ||
// uses the PTY input for reading. | ||
// | ||
// The same stream would be used to provide user input: pty.Input().Write(...) | ||
Input() io.ReadWriter | ||
} | ||
|
||
// Process represents a process running in a PTY. We need to trigger special processing on the PTY | ||
// on process completion, meaning that we will have goroutines calling Wait() on the process. Since | ||
// the caller will also typically wait for the process, and it is not safe for multiple goroutines | ||
// to Wait() on a process, this abstraction provides a goroutine-safe interface for interacting with | ||
// the process. | ||
type Process interface { | ||
// Wait for the command to complete. Returned error is as for exec.Cmd.Wait() | ||
Wait() error | ||
|
||
// Kill the command process. Returned error is as for os.Process.Kill() | ||
Kill() error | ||
} | ||
|
||
// WithFlags represents a PTY whose flags can be inspected, in particular | ||
// to determine whether local echo is enabled. | ||
type WithFlags interface { | ||
PTY | ||
|
||
// EchoEnabled determines whether local echo is currently enabled for this terminal. | ||
EchoEnabled() (bool, error) | ||
} | ||
|
||
// Controllable represents a PTY that can be controlled via the syscall.RawConn | ||
// interface. | ||
type Controllable interface { | ||
PTY | ||
|
||
// ControlPTY allows the caller to control the PTY via the syscall.RawConn interface. | ||
ControlPTY(func(uintptr) error) error | ||
|
||
// ControlTTY allows the caller to control the TTY via the syscall.RawConn interface. | ||
ControlTTY(func(uintptr) error) error | ||
} | ||
|
||
// Options represents a an option for a PTY. | ||
type Option func(*ptyOptions) | ||
|
||
type ptyOptions struct { | ||
setSize bool | ||
|
||
height int | ||
width int | ||
} | ||
|
||
// WithSize sets the size of the PTY. | ||
func WithSize(width int, height int) Option { | ||
return func(opts *ptyOptions) { | ||
opts.setSize = true | ||
opts.height = height | ||
opts.width = width | ||
} | ||
} | ||
|
||
// New constructs a new Pty. | ||
func New(opts ...Option) (PTY, error) { | ||
return newPty(opts...) | ||
} | ||
|
||
// readWriter is an implementation of io.ReadWriter that wraps two separate | ||
// underlying file descriptors, one for reading and one for writing, and allows | ||
// them to be accessed separately. | ||
type readWriter struct { | ||
Reader io.Reader | ||
Writer io.Writer | ||
} | ||
|
||
func (rw readWriter) Read(p []byte) (int, error) { | ||
return rw.Reader.Read(p) | ||
} | ||
|
||
func (rw readWriter) Write(p []byte) (int, error) { | ||
return rw.Writer.Write(p) | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
//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.setSize { | ||
if err := opty.Resize(opts.width, opts.height); err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
return opty, err | ||
} | ||
|
||
type otherPty struct { | ||
mutex sync.Mutex | ||
closed bool | ||
err error | ||
pty, tty *os.File | ||
opts ptyOptions | ||
name string | ||
} | ||
|
||
func (p *otherPty) ControlPTY(fn func(fd uintptr) error) error { | ||
return p.control(p.pty, fn) | ||
} | ||
|
||
func (p *otherPty) ControlTTY(fn func(fd uintptr) error) error { | ||
return p.control(p.tty, fn) | ||
} | ||
|
||
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() io.ReadWriter { | ||
return readWriter{ | ||
Reader: p.tty, | ||
Writer: p.pty, | ||
} | ||
} | ||
|
||
func (p *otherPty) InputWriter() io.Writer { | ||
return p.pty | ||
} | ||
|
||
func (p *otherPty) Output() io.ReadWriter { | ||
return readWriter{ | ||
Reader: &ptmReader{p.pty}, | ||
Writer: p.tty, | ||
} | ||
} | ||
|
||
func (p *otherPty) OutputReader() io.Reader { | ||
return &ptmReader{p.pty} | ||
} | ||
|
||
func (p *otherPty) Resize(width int, height int) error { | ||
return p.control(p.pty, func(fd uintptr) error { | ||
return termios.SetWinSize(fd, &termios.Winsize{ | ||
Winsize: unix.Winsize{ | ||
Row: uint16(height), | ||
Col: uint16(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) 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 | ||
} |
Oops, something went wrong.