mirror of
https://github.com/lxc/incus
synced 2026-08-02 05:26:46 +00:00
119 lines
3.3 KiB
Go
119 lines
3.3 KiB
Go
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)
|
|
}
|
|
|
|
// Close closes the underlying os.File.
|
|
func (w *execWrapper) Close() error {
|
|
return w.f.Close()
|
|
}
|