1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
|
package linux
import (
"context"
"errors"
"io"
"os"
"time"
"golang.org/x/sys/unix"
)
// GetPollRevents poll for events on provided fd.
func GetPollRevents(fd int, timeout int, flags int) (int, int, error) {
pollFd := unix.PollFd{
Fd: int32(fd),
Events: int16(flags),
Revents: 0,
}
pollFds := []unix.PollFd{pollFd}
again:
n, err := unix.Poll(pollFds, timeout)
if err != nil {
if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) {
goto again
}
return -1, -1, err
}
return n, int(pollFds[0].Revents), err
}
// NewExecWrapper returns a new ReadWriteCloser wrapper for an os.File.
// The ctx is used to indicate when the executed process has ended, at which point any further Read calls will
// return io.EOF rather than potentially blocking on the poll syscall if the process is a shell that still has
// background processes running that are not producing any output.
func NewExecWrapper(ctx context.Context, f *os.File) io.ReadWriteCloser {
return &execWrapper{
ctx: ctx,
f: f,
}
}
// execWrapper implements a ReadWriteCloser wrapper for an os.File connected to a PTY.
type execWrapper struct {
f *os.File
ctx context.Context
finishDeadline time.Time
}
// Read uses the poll syscall with a timeout of 1s to check if there is any data to read.
// This avoids potentially blocking in the poll syscall in situations where the process is a shell that has
// background processes that are not producing any output.
// If the ctx has been cancelled before the poll starts then io.EOF error is returned.
func (w *execWrapper) Read(p []byte) (int, error) {
rawConn, err := w.f.SyscallConn()
if err != nil {
return 0, err
}
var opErr error
var n int
err = rawConn.Read(func(fd uintptr) bool {
for {
// Call poll() with 1s timeout, this prevents blocking if a shell process exits leaving
// background processes running that are not outputting anything.
_, revents, err := GetPollRevents(int(fd), 1000, (unix.POLLIN | unix.POLLPRI | unix.POLLERR | unix.POLLNVAL | unix.POLLHUP | unix.POLLRDHUP))
switch {
case err != nil:
opErr = err
case revents&unix.POLLERR > 0:
opErr = errors.New("Got POLLERR event")
case revents&unix.POLLNVAL > 0:
opErr = errors.New("Got POLLNVAL event")
case revents&(unix.POLLIN|unix.POLLPRI) > 0:
// If there is something to read then read it.
n, opErr = unix.Read(int(fd), p)
if opErr == nil && w.ctx.Err() != nil {
if w.finishDeadline.IsZero() {
// When the parent process finishes set a deadline to complete
// future reads by.
w.finishDeadline = time.Now().Add(time.Second)
} else if time.Now().After(w.finishDeadline) {
// If there is still output being received after the parent
// process has finished then return EOF to prevent background
// processes from keeping the reads ongoing.
opErr = io.EOF
}
}
case w.ctx.Err() != nil:
// Nothing to read after process exited then return EOF.
opErr = io.EOF
default:
continue
}
return true
}
})
if err != nil {
return n, err
}
return n, opErr
}
func (w *execWrapper) Write(p []byte) (int, error) {
return w.f.Write(p)
}
func (w *execWrapper) Close() error {
return w.f.Close()
}
|