wasi: introduce platform.Select and use it for poll_oneoff (#1346)
Some checks failed
Release CLI / Pre-release build (push) Has been cancelled
Release CLI / Pre-release test (macos-12) (push) Has been cancelled
Release CLI / Pre-release test (ubuntu-22.04) (push) Has been cancelled
Release CLI / Pre-release test (windows-2022) (push) Has been cancelled
Release CLI / Release (push) Has been cancelled
Some checks failed
Release CLI / Pre-release build (push) Has been cancelled
Release CLI / Pre-release test (macos-12) (push) Has been cancelled
Release CLI / Pre-release test (ubuntu-22.04) (push) Has been cancelled
Release CLI / Pre-release test (windows-2022) (push) Has been cancelled
Release CLI / Release (push) Has been cancelled
The PR introduces the `platform.Select()` API, wrapping `select(2)` on POSIX and emulated in some cases on Windows. RATIONALE.md contains a full explanation of the approach followed in `poll_oneoff` to handle Stdin and the other types of file descriptors, and the clock subscriptions. It also introduces an abstraction (`StdioFilePoller`) to allow the simulation of different scenarios (waiting for input, input ready, timeout expired, etc.) when unit-testing interactive input. This closes #1317. Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
This commit is contained in:
90
RATIONALE.md
90
RATIONALE.md
@@ -907,6 +907,96 @@ See https://github.com/WebAssembly/stack-switching/discussions/38
|
||||
See https://github.com/WebAssembly/wasi-threads#what-can-be-skipped
|
||||
See https://slinkydeveloper.com/Kubernetes-controllers-A-New-Hope/
|
||||
|
||||
## poll_oneoff
|
||||
|
||||
`poll_oneoff` is a WASI API for waiting for I/O events on multiple handles.
|
||||
It is conceptually similar to the POSIX `poll(2)` syscall.
|
||||
The name is not `poll`, because it references [“the fact that this function is not efficient
|
||||
when used repeatedly with the same large set of handles”][poll_oneoff].
|
||||
|
||||
We chose to support this API in a handful of cases that work for regular files
|
||||
and standard input. We currently do not support other types of file descriptors such
|
||||
as socket handles.
|
||||
|
||||
### Clock Subscriptions
|
||||
|
||||
As detailed above in [sys.Nanosleep](#sysnanosleep), `poll_oneoff` handles
|
||||
relative clock subscriptions. In our implementation we use `sys.Nanosleep()`
|
||||
for this purpose in most cases, except when polling for interactive input
|
||||
from `os.Stdin` (see more details below).
|
||||
|
||||
### FdRead and FdWrite Subscriptions
|
||||
|
||||
When subscribing a file descriptor (except `Stdin`) for reads or writes,
|
||||
the implementation will generally return immediately with success, unless
|
||||
the file descriptor is unknown. The file descriptor is not checked further
|
||||
for new incoming data. Any timeout is cancelled, and the API call is able
|
||||
to return, unless there are subscriptions to `Stdin`: these are handled
|
||||
separately.
|
||||
|
||||
### FdRead and FdWrite Subscription to Stdin
|
||||
|
||||
Subscribing `Stdin` for reads (writes make no sense and cause an error),
|
||||
requires extra care: wazero allows to configure a custom reader for `Stdin`.
|
||||
|
||||
In general, if a custom reader is found, the behavior will be the same
|
||||
as for regular file descriptors: data is assumed to be present and
|
||||
a success is written back to the result buffer.
|
||||
|
||||
However, if the reader is detected to read from `os.Stdin`,
|
||||
a special code path is followed, invoking `platform.Select()`.
|
||||
|
||||
`platform.Select()` is a wrapper for `select(2)` on POSIX systems,
|
||||
and it is mocked for a handful of cases also on Windows.
|
||||
|
||||
### Select on POSIX
|
||||
|
||||
On POSIX systems,`select(2)` allows to wait for incoming data on a file
|
||||
descriptor, and block until either data becomes available or the timeout
|
||||
expires. It is not surprising that `select(2)` and `poll(2)` have lot in common:
|
||||
the main difference is how the file descriptor parameters are passed.
|
||||
|
||||
Usage of `platform.Select()` is only reserved for the standard input case, because
|
||||
|
||||
1. it is really only necessary to handle interactive input: otherwise,
|
||||
there is no way in Go to peek from Standard Input without actually
|
||||
reading (and thus consuming) from it;
|
||||
|
||||
2. if `Stdin` is connected to a pipe, it is ok in most cases to return
|
||||
with success immediately;
|
||||
|
||||
3. `platform.Select()` is currently a blocking call, irrespective of goroutines,
|
||||
because the underlying syscall is; thus, it is better to limit its usage.
|
||||
|
||||
So, if the subscription is for `os.Stdin` and the handle is detected
|
||||
to correspond to an interactive session, then `platform.Select()` will be
|
||||
invoked with a the `Stdin` handle *and* the timeout.
|
||||
|
||||
This also means that in this specific case, the timeout is uninterruptible,
|
||||
unless data becomes available on `Stdin` itself.
|
||||
|
||||
### Select on Windows
|
||||
|
||||
On Windows the `platform.Select()` is much more straightforward,
|
||||
and it really just replicates the behavior found in the general cases
|
||||
for `FdRead` subscriptions: in other words, the subscription to `Stdin`
|
||||
is immediately acknowledged.
|
||||
|
||||
The implementation also support a timeout, but in this case
|
||||
it relies on `time.Sleep()`, which notably, as compared to the POSIX
|
||||
case, interruptible and compatible with goroutines.
|
||||
|
||||
However, because `Stdin` subscriptions are always acknowledged
|
||||
without wait and because this code path is always followed only
|
||||
when at least one `Stdin` subscription is present, then the
|
||||
timeout is effectively always handled externally.
|
||||
|
||||
In any case, the behavior of `platform.Select` on Windows
|
||||
is sensibly different from the behavior on POSIX platforms;
|
||||
we plan to refine and further align it in semantics in the future.
|
||||
|
||||
[poll_oneoff]: https://github.com/WebAssembly/wasi-poll#why-is-the-function-called-poll_oneoff
|
||||
|
||||
## Signed encoding of integer global constant initializers
|
||||
|
||||
wazero treats integer global constant initializers signed as their interpretation is not known at declaration time. For
|
||||
|
||||
@@ -2,10 +2,11 @@ package wasi_snapshot_preview1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
internalsys "github.com/tetratelabs/wazero/internal/sys"
|
||||
"github.com/tetratelabs/wazero/internal/wasip1"
|
||||
"github.com/tetratelabs/wazero/internal/wasm"
|
||||
@@ -42,6 +43,13 @@ var pollOneoff = newHostFunc(
|
||||
"in", "out", "nsubscriptions", "result.nevents",
|
||||
)
|
||||
|
||||
type event struct {
|
||||
eventType byte
|
||||
userData []byte
|
||||
errno wasip1.Errno
|
||||
outOffset uint32
|
||||
}
|
||||
|
||||
func pollOneoffFn(ctx context.Context, mod api.Module, params []uint64) syscall.Errno {
|
||||
in := uint32(params[0])
|
||||
out := uint32(params[1])
|
||||
@@ -60,6 +68,11 @@ func pollOneoffFn(ctx context.Context, mod api.Module, params []uint64) syscall.
|
||||
return syscall.EFAULT
|
||||
}
|
||||
outBuf, ok := mem.Read(out, nsubscriptions*32)
|
||||
// zero-out all buffer before writing
|
||||
for i := range outBuf {
|
||||
outBuf[i] = 0
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return syscall.EFAULT
|
||||
}
|
||||
@@ -72,6 +85,17 @@ func pollOneoffFn(ctx context.Context, mod api.Module, params []uint64) syscall.
|
||||
|
||||
// Loop through all subscriptions and write their output.
|
||||
|
||||
// Extract FS context, used in the body of the for loop for FS access.
|
||||
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
|
||||
// Slice of events that are processed out of the loop (stdin subscribers).
|
||||
var stdinSubs []*event
|
||||
// The timeout is initialized at max Duration, the loop will find the minimum.
|
||||
var timeout time.Duration = 1<<63 - 1
|
||||
// Count of all the clock subscribers that have been already written back to outBuf.
|
||||
clockEvents := uint32(0)
|
||||
// Count of all the non-clock subscribers that have been already written back to outBuf.
|
||||
readySubs := uint32(0)
|
||||
|
||||
// Layout is subscription_u: Union
|
||||
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#subscription_u
|
||||
for i := uint32(0); i < nsubscriptions; i++ {
|
||||
@@ -79,81 +103,153 @@ func pollOneoffFn(ctx context.Context, mod api.Module, params []uint64) syscall.
|
||||
outOffset := i * 32
|
||||
|
||||
eventType := inBuf[inOffset+8] // +8 past userdata
|
||||
var errno syscall.Errno // errno for this specific event (1-byte)
|
||||
// +8 past userdata +8 contents_offset
|
||||
argBuf := inBuf[inOffset+8+8:]
|
||||
userData := inBuf[inOffset : inOffset+8]
|
||||
|
||||
evt := &event{
|
||||
eventType: eventType,
|
||||
userData: userData,
|
||||
errno: wasip1.ErrnoSuccess,
|
||||
outOffset: outOffset,
|
||||
}
|
||||
|
||||
switch eventType {
|
||||
case wasip1.EventTypeClock: // handle later
|
||||
// +8 past userdata +8 contents_offset
|
||||
errno = processClockEvent(ctx, mod, inBuf[inOffset+8+8:])
|
||||
case wasip1.EventTypeFdRead, wasip1.EventTypeFdWrite:
|
||||
// +8 past userdata +8 contents_offset
|
||||
errno = processFDEvent(mod, eventType, inBuf[inOffset+8+8:])
|
||||
clockEvents++
|
||||
newTimeout, err := processClockEvent(argBuf)
|
||||
if err != 0 {
|
||||
return err
|
||||
}
|
||||
// Min timeout.
|
||||
if newTimeout < timeout {
|
||||
timeout = newTimeout
|
||||
}
|
||||
// Ack the clock event to the outBuf.
|
||||
writeEvent(outBuf, evt)
|
||||
case wasip1.EventTypeFdRead:
|
||||
fd := le.Uint32(argBuf)
|
||||
if fd == internalsys.FdStdin {
|
||||
// if the fd is Stdin, do not ack yet,
|
||||
// append to a slice for delayed evaluation.
|
||||
stdinSubs = append(stdinSubs, evt)
|
||||
} else {
|
||||
evt.errno = processFDEventRead(fsc, fd)
|
||||
writeEvent(outBuf, evt)
|
||||
readySubs++
|
||||
}
|
||||
case wasip1.EventTypeFdWrite:
|
||||
fd := le.Uint32(argBuf)
|
||||
evt.errno = processFDEventWrite(fsc, fd)
|
||||
readySubs++
|
||||
writeEvent(outBuf, evt)
|
||||
default:
|
||||
return syscall.EINVAL
|
||||
}
|
||||
|
||||
// Write the event corresponding to the processed subscription.
|
||||
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-event-struct
|
||||
copy(outBuf, inBuf[inOffset:inOffset+8]) // userdata
|
||||
if errno != 0 {
|
||||
outBuf[outOffset+8] = byte(wasip1.ToErrno(errno)) // uint16, but safe as < 255
|
||||
} else { // special case ass ErrnoSuccess is zero
|
||||
outBuf[outOffset+8] = 0
|
||||
}
|
||||
outBuf[outOffset+9] = 0
|
||||
le.PutUint32(outBuf[outOffset+10:], uint32(eventType))
|
||||
// TODO: When FD events are supported, write outOffset+16
|
||||
}
|
||||
|
||||
// If there are subscribers with data ready, we have already written them to outBuf,
|
||||
// and we don't need to wait for the timeout: clear it.
|
||||
if readySubs != 0 {
|
||||
timeout = 0
|
||||
}
|
||||
|
||||
// If there are stdin subscribers, check for data with given timeout.
|
||||
if len(stdinSubs) > 0 {
|
||||
reader := getStdioFileReader(mod)
|
||||
// Wait for the timeout to expire, or for some data to become available on Stdin.
|
||||
stdinReady, err := reader.Poll(timeout)
|
||||
if err != nil {
|
||||
return platform.UnwrapOSError(err)
|
||||
}
|
||||
if stdinReady {
|
||||
// stdin has data ready to for reading, write back all the events
|
||||
for i := range stdinSubs {
|
||||
readySubs++
|
||||
evt := stdinSubs[i]
|
||||
evt.errno = 0
|
||||
writeEvent(outBuf, evt)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No subscribers, just wait for the given timeout.
|
||||
sysCtx := mod.(*wasm.ModuleInstance).Sys
|
||||
sysCtx.Nanosleep(int64(timeout))
|
||||
}
|
||||
|
||||
if readySubs != nsubscriptions {
|
||||
if !mod.Memory().WriteUint32Le(resultNevents, readySubs+clockEvents) {
|
||||
return syscall.EFAULT
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// processClockEvent supports only relative name events, as that's what's used
|
||||
// to implement sleep in various compilers including Rust, Zig and TinyGo.
|
||||
func processClockEvent(_ context.Context, mod api.Module, inBuf []byte) syscall.Errno {
|
||||
func processClockEvent(inBuf []byte) (time.Duration, syscall.Errno) {
|
||||
_ /* ID */ = le.Uint32(inBuf[0:8]) // See below
|
||||
timeout := le.Uint64(inBuf[8:16]) // nanos if relative
|
||||
_ /* precision */ = le.Uint64(inBuf[16:24]) // Unused
|
||||
flags := le.Uint16(inBuf[24:32])
|
||||
|
||||
var err syscall.Errno
|
||||
// subclockflags has only one flag defined: subscription_clock_abstime
|
||||
switch flags {
|
||||
case 0: // relative time
|
||||
case 1: // subscription_clock_abstime
|
||||
return syscall.ENOTSUP
|
||||
err = syscall.ENOTSUP
|
||||
default: // subclockflags has only one flag defined.
|
||||
return syscall.EINVAL
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
|
||||
// https://linux.die.net/man/3/clock_settime says relative timers are
|
||||
// unaffected. Since this function only supports relative timeout, we can
|
||||
// skip name ID validation and use a single sleep function.
|
||||
if err != 0 {
|
||||
return 0, err
|
||||
} else {
|
||||
// https://linux.die.net/man/3/clock_settime says relative timers are
|
||||
// unaffected. Since this function only supports relative timeout, we can
|
||||
// skip name ID validation and use a single sleep function.
|
||||
|
||||
sysCtx := mod.(*wasm.ModuleInstance).Sys
|
||||
sysCtx.Nanosleep(int64(timeout))
|
||||
return 0
|
||||
return time.Duration(timeout), 0
|
||||
}
|
||||
}
|
||||
|
||||
// processFDEvent returns a validation error or syscall.ENOTSUP as file or socket
|
||||
// subscriptions are not yet supported.
|
||||
func processFDEvent(mod api.Module, eventType byte, inBuf []byte) syscall.Errno {
|
||||
fd := le.Uint32(inBuf)
|
||||
// processFDEventRead returns ErrnoSuccess if the file exists and ErrnoBadf otherwise.
|
||||
func processFDEventRead(fsc *internalsys.FSContext, fd uint32) wasip1.Errno {
|
||||
if _, ok := fsc.LookupFile(fd); ok {
|
||||
return wasip1.ErrnoSuccess
|
||||
} else {
|
||||
return wasip1.ErrnoBadf
|
||||
}
|
||||
}
|
||||
|
||||
// processFDEventWrite returns ErrnoNotsup if the file exists and ErrnoBadf otherwise.
|
||||
func processFDEventWrite(fsc *internalsys.FSContext, fd uint32) wasip1.Errno {
|
||||
if internalsys.WriterForFile(fsc, fd) == nil {
|
||||
return wasip1.ErrnoBadf
|
||||
}
|
||||
return wasip1.ErrnoNotsup
|
||||
}
|
||||
|
||||
// writeEvent writes the event corresponding to the processed subscription.
|
||||
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-event-struct
|
||||
func writeEvent(outBuf []byte, evt *event) {
|
||||
copy(outBuf[evt.outOffset:], evt.userData) // userdata
|
||||
outBuf[evt.outOffset+8] = byte(evt.errno) // uint16, but safe as < 255
|
||||
outBuf[evt.outOffset+9] = 0
|
||||
le.PutUint32(outBuf[evt.outOffset+10:], uint32(evt.eventType))
|
||||
// TODO: When FD events are supported, write outOffset+16
|
||||
}
|
||||
|
||||
// getStdioFileReader extracts a StdioFileReader for FdStdin from the given api.Module instance.
|
||||
// and panics if this is not possible.
|
||||
func getStdioFileReader(mod api.Module) *internalsys.StdioFileReader {
|
||||
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
|
||||
|
||||
// Choose the best error, which falls back to unsupported, until we support
|
||||
// files.
|
||||
errno := syscall.ENOTSUP
|
||||
if eventType == wasip1.EventTypeFdRead {
|
||||
if f, ok := fsc.LookupFile(fd); ok {
|
||||
st, _ := f.Stat()
|
||||
// if fd is a pipe, then it is not a char device (a tty)
|
||||
if st.Mode&fs.ModeCharDevice != 0 {
|
||||
errno = syscall.EBADF
|
||||
}
|
||||
} else {
|
||||
errno = syscall.EBADF
|
||||
if file, ok := fsc.LookupFile(internalsys.FdStdin); ok {
|
||||
if reader, typeOk := file.File.(*internalsys.StdioFileReader); typeOk {
|
||||
return reader
|
||||
}
|
||||
} else if eventType == wasip1.EventTypeFdWrite && internalsys.WriterForFile(fsc, fd) == nil {
|
||||
errno = syscall.EBADF
|
||||
}
|
||||
|
||||
return errno
|
||||
panic("unexpected error: Stdin must always be a StdioFileReader")
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ package wasi_snapshot_preview1_test
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/internal/sys"
|
||||
@@ -30,7 +31,8 @@ func Test_pollOneoff(t *testing.T) {
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeClock, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
'?', // stopped after encoding
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, '?', // stopped after encoding
|
||||
}
|
||||
|
||||
in := uint32(0) // past in
|
||||
@@ -61,17 +63,6 @@ func Test_pollOneoff_Errors(t *testing.T) {
|
||||
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig())
|
||||
defer r.Close(testCtx)
|
||||
|
||||
// We aren't guaranteed to have a terminal device for os.Stdin, due to how
|
||||
// `go test` forks processes. Instead, we test if this is consistent. For
|
||||
// example, when run in a debugger, this could end up true.
|
||||
// See also `terminal_test.go`.
|
||||
expectedFdReadErr := wasip1.ErrnoNotsup
|
||||
if stat, err := os.Stdin.Stat(); err != nil {
|
||||
if stat.Mode()&fs.ModeCharDevice != 0 {
|
||||
expectedFdReadErr = wasip1.ErrnoBadf
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
in, out, nsubscriptions, resultNevents uint32
|
||||
@@ -121,29 +112,6 @@ func Test_pollOneoff_Errors(t *testing.T) {
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=0)
|
||||
<== (nevents=,errno=EINVAL)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "unsupported EventTypeFdRead",
|
||||
nsubscriptions: 1,
|
||||
mem: []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
wasip1.EventTypeFdRead, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
byte(sys.FdStdin), 0x0, 0x0, 0x0, // valid readable FD
|
||||
'?', // stopped after encoding
|
||||
},
|
||||
expectedErrno: wasip1.ErrnoSuccess,
|
||||
out: 128, // past in
|
||||
resultNevents: 512, // past out
|
||||
expectedMem: []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(expectedFdReadErr), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeFdRead, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
'?', // stopped after encoding
|
||||
},
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=1)
|
||||
<== (nevents=1,errno=ESUCCESS)
|
||||
`,
|
||||
},
|
||||
}
|
||||
@@ -172,7 +140,413 @@ func Test_pollOneoff_Errors(t *testing.T) {
|
||||
nevents, ok := mod.Memory().ReadUint32Le(tc.resultNevents)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, uint32(1), nevents)
|
||||
_ = nevents
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_pollOneoff_Stdin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in, out, nsubscriptions, resultNevents uint32
|
||||
mem []byte // at offset in
|
||||
stdioReader *sys.StdioFileReader
|
||||
expectedErrno wasip1.Errno
|
||||
expectedMem []byte // at offset out
|
||||
expectedLog string
|
||||
expectedNevents uint32
|
||||
}{
|
||||
{
|
||||
name: "Read without explicit timeout (no tty)",
|
||||
nsubscriptions: 1,
|
||||
expectedNevents: 1,
|
||||
stdioReader: sys.NewStdioFileReader(
|
||||
strings.NewReader("test"),
|
||||
stdinFileInfo(0o640),
|
||||
sys.PollerAlwaysReady), // isatty
|
||||
mem: fdReadSub,
|
||||
expectedErrno: wasip1.ErrnoSuccess,
|
||||
out: 128, // past in
|
||||
resultNevents: 512, // past out
|
||||
expectedMem: []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeFdRead, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
|
||||
'?', // stopped after encoding
|
||||
},
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=1)
|
||||
<== (nevents=1,errno=ESUCCESS)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "20ms timeout, fdread on tty (buffer ready): both events are written",
|
||||
nsubscriptions: 2,
|
||||
expectedNevents: 2,
|
||||
stdioReader: sys.NewStdioFileReader(
|
||||
strings.NewReader("test"),
|
||||
stdinFileInfo(fs.ModeDevice|fs.ModeCharDevice|0o640),
|
||||
sys.PollerAlwaysReady), // isatty
|
||||
mem: concat(
|
||||
clockNsSub(20*1000*1000),
|
||||
fdReadSub,
|
||||
singleton('?'),
|
||||
),
|
||||
expectedErrno: wasip1.ErrnoSuccess,
|
||||
out: 128, // past in
|
||||
resultNevents: 512, // past out
|
||||
expectedMem: []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeClock, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // pad to 32
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeFdRead, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // pad to 32
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
|
||||
'?', // stopped after encoding
|
||||
},
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=2)
|
||||
<== (nevents=2,errno=ESUCCESS)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "0ns timeout, fdread on tty (buffer ready): both are written",
|
||||
nsubscriptions: 2,
|
||||
expectedNevents: 2,
|
||||
stdioReader: sys.NewStdioFileReader(
|
||||
strings.NewReader("test"),
|
||||
stdinFileInfo(fs.ModeDevice|fs.ModeCharDevice|0o640),
|
||||
sys.PollerAlwaysReady), // isatty
|
||||
mem: concat(
|
||||
clockNsSub(20*1000*1000),
|
||||
fdReadSub,
|
||||
singleton('?'),
|
||||
),
|
||||
expectedErrno: wasip1.ErrnoSuccess,
|
||||
out: 128, // past in
|
||||
resultNevents: 512, // past out
|
||||
expectedMem: []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeClock, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // pad to 32
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeFdRead, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // pad to 32
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
|
||||
'?', // stopped after encoding
|
||||
},
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=2)
|
||||
<== (nevents=2,errno=ESUCCESS)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "0ns timeout, fdread on regular file: both events are written",
|
||||
nsubscriptions: 2,
|
||||
expectedNevents: 2,
|
||||
stdioReader: sys.NewStdioFileReader(
|
||||
strings.NewReader("test"),
|
||||
stdinFileInfo(0o640),
|
||||
sys.PollerAlwaysReady),
|
||||
mem: concat(
|
||||
clockNsSub(20*1000*1000),
|
||||
fdReadSub,
|
||||
singleton('?'),
|
||||
),
|
||||
expectedErrno: wasip1.ErrnoSuccess,
|
||||
out: 128, // past in
|
||||
resultNevents: 512, // past out
|
||||
expectedMem: []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeClock, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // pad to 32
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeFdRead, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // pad to 32
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
|
||||
'?', // stopped after encoding
|
||||
},
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=2)
|
||||
<== (nevents=2,errno=ESUCCESS)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "1ns timeout, fdread on regular file: both events are written",
|
||||
nsubscriptions: 2,
|
||||
expectedNevents: 2,
|
||||
stdioReader: sys.NewStdioFileReader(
|
||||
strings.NewReader("test"),
|
||||
stdinFileInfo(0o640),
|
||||
sys.PollerAlwaysReady),
|
||||
mem: concat(
|
||||
clockNsSub(20*1000*1000),
|
||||
fdReadSub,
|
||||
singleton('?'),
|
||||
),
|
||||
expectedErrno: wasip1.ErrnoSuccess,
|
||||
out: 128, // past in
|
||||
resultNevents: 512, // past out
|
||||
expectedMem: []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeClock, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // pad to 32
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeFdRead, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // pad to 32
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
|
||||
'?', // stopped after encoding
|
||||
},
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=2)
|
||||
<== (nevents=2,errno=ESUCCESS)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "20ms timeout, fdread on blocked tty: only clock event is written",
|
||||
nsubscriptions: 2,
|
||||
expectedNevents: 1,
|
||||
stdioReader: sys.NewStdioFileReader(
|
||||
newBlockingReader(t),
|
||||
stdinFileInfo(fs.ModeDevice|fs.ModeCharDevice|0o640),
|
||||
sys.PollerNeverReady),
|
||||
mem: concat(
|
||||
clockNsSub(20*1000*1000),
|
||||
fdReadSub,
|
||||
singleton('?'),
|
||||
),
|
||||
|
||||
expectedErrno: wasip1.ErrnoSuccess,
|
||||
out: 128, // past in
|
||||
resultNevents: 512, // past out
|
||||
expectedMem: []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeClock, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // pad to 32
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
|
||||
// 32 empty bytes
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
|
||||
'?', // stopped after encoding
|
||||
},
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=2)
|
||||
<== (nevents=1,errno=ESUCCESS)
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tconfig := wazero.NewModuleConfig().WithStdin(tc.stdioReader)
|
||||
mod, r, log := requireProxyModule(t, tconfig)
|
||||
defer r.Close(testCtx)
|
||||
defer log.Reset()
|
||||
|
||||
maskMemory(t, mod, 1024)
|
||||
|
||||
if tc.mem != nil {
|
||||
mod.Memory().Write(tc.in, tc.mem)
|
||||
}
|
||||
|
||||
requireErrnoResult(t, tc.expectedErrno, mod, wasip1.PollOneoffName, uint64(tc.in), uint64(tc.out),
|
||||
uint64(tc.nsubscriptions), uint64(tc.resultNevents))
|
||||
require.Equal(t, tc.expectedLog, "\n"+log.String())
|
||||
|
||||
out, ok := mod.Memory().Read(tc.out, uint32(len(tc.expectedMem)))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tc.expectedMem, out)
|
||||
|
||||
// Events should be written on success regardless of nested failure.
|
||||
if tc.expectedErrno == wasip1.ErrnoSuccess {
|
||||
nevents, ok := mod.Memory().ReadUint32Le(tc.resultNevents)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tc.expectedNevents, nevents)
|
||||
_ = nevents
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_pollOneoff_Zero(t *testing.T) {
|
||||
poller := &poller{ready: true}
|
||||
|
||||
tconfig := wazero.NewModuleConfig().WithStdin(sys.NewStdioFileReader(
|
||||
strings.NewReader("test"),
|
||||
stdinFileInfo(fs.ModeDevice|fs.ModeCharDevice|0o640),
|
||||
poller))
|
||||
|
||||
mod, r, log := requireProxyModule(t, tconfig)
|
||||
defer r.Close(testCtx)
|
||||
defer log.Reset()
|
||||
|
||||
maskMemory(t, mod, 1024)
|
||||
|
||||
out := uint32(128)
|
||||
nsubscriptions := 2
|
||||
resultNevents := uint32(512)
|
||||
|
||||
mod.Memory().Write(0,
|
||||
concat(
|
||||
clockNsSub(20*1000*1000),
|
||||
fdReadSub,
|
||||
singleton('?'),
|
||||
),
|
||||
)
|
||||
|
||||
expectedMem := []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeClock, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeFdRead, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // pad to 32
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
|
||||
'?', // stopped after encoding
|
||||
}
|
||||
|
||||
requireErrnoResult(t, wasip1.ErrnoSuccess, mod, wasip1.PollOneoffName, uint64(0), uint64(out),
|
||||
uint64(nsubscriptions), uint64(resultNevents))
|
||||
|
||||
outMem, ok := mod.Memory().Read(out, uint32(len(expectedMem)))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, expectedMem, outMem)
|
||||
|
||||
// Events should be written on success regardless of nested failure.
|
||||
nevents, ok := mod.Memory().ReadUint32Le(resultNevents)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, uint32(2), nevents)
|
||||
|
||||
// second run: simulate no more data on the fd
|
||||
poller.ready = false
|
||||
|
||||
mod.Memory().Write(0,
|
||||
concat(
|
||||
clockNsSub(20*1000*1000),
|
||||
fdReadSub,
|
||||
singleton('?'),
|
||||
),
|
||||
)
|
||||
|
||||
expectedMem = []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
byte(wasip1.ErrnoSuccess), 0x0, // errno is 16 bit
|
||||
wasip1.EventTypeClock, 0x0, 0x0, 0x0, // 4 bytes for type enum
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
|
||||
'?', // stopped after encoding
|
||||
}
|
||||
|
||||
requireErrnoResult(t, wasip1.ErrnoSuccess, mod, wasip1.PollOneoffName, uint64(0), uint64(out),
|
||||
uint64(nsubscriptions), uint64(resultNevents))
|
||||
|
||||
outMem, ok = mod.Memory().Read(out, uint32(len(expectedMem)))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, expectedMem, outMem)
|
||||
|
||||
nevents, ok = mod.Memory().ReadUint32Le(resultNevents)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, uint32(1), nevents)
|
||||
}
|
||||
|
||||
func singleton(b byte) []byte {
|
||||
return []byte{b}
|
||||
}
|
||||
|
||||
func concat(bytes ...[]byte) []byte {
|
||||
var res []byte
|
||||
for i := range bytes {
|
||||
res = append(res, bytes[i]...)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// subscription for a given timeout in ns
|
||||
func clockNsSub(ns uint64) []byte {
|
||||
return []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
wasip1.EventTypeClock, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // event type and padding
|
||||
wasip1.ClockIDMonotonic, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
byte(ns), byte(ns >> 8), byte(ns >> 16), byte(ns >> 24),
|
||||
byte(ns >> 32), byte(ns >> 40), byte(ns >> 48), byte(ns >> 56), // timeout (ns)
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // precision (ns)
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // flags
|
||||
}
|
||||
}
|
||||
|
||||
// subscription for an EventTypeFdRead on a given fd
|
||||
func fdReadSubFd(fd byte) []byte {
|
||||
return []byte{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
||||
wasip1.EventTypeFdRead, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
fd, 0x0, 0x0, 0x0, // valid readable FD
|
||||
}
|
||||
}
|
||||
|
||||
// subscription for an EventTypeFdRead on stdin
|
||||
var fdReadSub = fdReadSubFd(byte(sys.FdStdin))
|
||||
|
||||
type poller struct {
|
||||
ready bool
|
||||
}
|
||||
|
||||
func (p *poller) Poll(d time.Duration) (bool, error) {
|
||||
if p.ready {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <unistd.h>
|
||||
#include <stdbool.h>
|
||||
#include <sys/select.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
#define formatBool(b) ((b) ? "true" : "false")
|
||||
|
||||
@@ -37,7 +39,7 @@ void main_stat() {
|
||||
printf("/ isatty: %s\n", formatBool(isatty(3)));
|
||||
}
|
||||
|
||||
void main_poll() {
|
||||
void main_poll(int timeout, int millis) {
|
||||
int ret = 0;
|
||||
fd_set rfds;
|
||||
struct timeval tv;
|
||||
@@ -45,8 +47,8 @@ void main_poll() {
|
||||
FD_ZERO(&rfds);
|
||||
FD_SET(0, &rfds);
|
||||
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 0;
|
||||
tv.tv_sec = timeout;
|
||||
tv.tv_usec = millis*1000;
|
||||
ret = select(1, &rfds, NULL, NULL, &tv);
|
||||
if ((ret > 0) && FD_ISSET(0, &rfds)) {
|
||||
printf("STDIN\n");
|
||||
@@ -55,6 +57,19 @@ void main_poll() {
|
||||
}
|
||||
}
|
||||
|
||||
void main_sleepmillis(int millis) {
|
||||
struct timespec tim, tim2;
|
||||
tim.tv_sec = 0;
|
||||
tim.tv_nsec = millis * 1000000;
|
||||
|
||||
if(nanosleep(&tim , &tim2) < 0 ) {
|
||||
printf("ERR\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("OK\n");
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (strcmp(argv[1],"ls")==0) {
|
||||
bool repeat = false;
|
||||
@@ -65,7 +80,22 @@ int main(int argc, char** argv) {
|
||||
} else if (strcmp(argv[1],"stat")==0) {
|
||||
main_stat();
|
||||
} else if (strcmp(argv[1],"poll")==0) {
|
||||
main_poll();
|
||||
int timeout = 0;
|
||||
int usec = 0;
|
||||
if (argc > 2) {
|
||||
timeout = atoi(argv[2]);
|
||||
}
|
||||
if (argc > 3) {
|
||||
usec = atoi(argv[3]);
|
||||
}
|
||||
main_poll(timeout, usec);
|
||||
} else if (strcmp(argv[1],"sleepmillis")==0) {
|
||||
int timeout = 0;
|
||||
if (argc > 2) {
|
||||
timeout = atoi(argv[2]);
|
||||
}
|
||||
main_sleepmillis(timeout);
|
||||
|
||||
} else {
|
||||
fprintf(stderr, "unknown command: %s\n", argv[1]);
|
||||
return 1;
|
||||
|
||||
Binary file not shown.
@@ -3,15 +3,17 @@ package wasi_snapshot_preview1_test
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
|
||||
internalsys "github.com/tetratelabs/wazero/internal/sys"
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
"github.com/tetratelabs/wazero/sys"
|
||||
)
|
||||
@@ -217,18 +219,95 @@ func compileAndRun(t *testing.T, config wazero.ModuleConfig, bin []byte) (consol
|
||||
}
|
||||
|
||||
func Test_Poll(t *testing.T) {
|
||||
moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "poll")
|
||||
console := compileAndRun(t, moduleConfig, wasmZigCc)
|
||||
// The "real" expected behavior is to return "NOINPUT",
|
||||
// however the poll API is currently relying on stat'ing the file
|
||||
// descriptor for stdin which makes the behavior platform-specific
|
||||
// **during tests** and unfortunately hard to mock.
|
||||
// For now, we just make sure the result is consistent.
|
||||
if stat, err := os.Stdin.Stat(); err != nil {
|
||||
if stat.Mode()&fs.ModeCharDevice != 0 {
|
||||
require.Equal(t, "NOINPUT\n", console)
|
||||
return
|
||||
}
|
||||
// The following test cases replace Stdin with a custom reader.
|
||||
// For more precise coverage, see poll_test.go.
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
stdin io.Reader
|
||||
expectedOutput string
|
||||
expectedTimeout time.Duration
|
||||
}{
|
||||
{
|
||||
name: "custom reader, data ready, not tty",
|
||||
args: []string{"wasi", "poll"},
|
||||
stdin: internalsys.NewStdioFileReader(
|
||||
strings.NewReader("test"), // input ready
|
||||
stdinFileInfo(fs.ModeDevice|fs.ModeCharDevice|0o640),
|
||||
internalsys.PollerAlwaysReady),
|
||||
expectedOutput: "STDIN",
|
||||
expectedTimeout: 0 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
name: "custom reader, data ready, not tty, .5sec",
|
||||
args: []string{"wasi", "poll", "0", "500"},
|
||||
stdin: internalsys.NewStdioFileReader(
|
||||
strings.NewReader("test"), // input ready
|
||||
stdinFileInfo(fs.ModeDevice|fs.ModeCharDevice|0o640),
|
||||
internalsys.PollerAlwaysReady),
|
||||
expectedOutput: "STDIN",
|
||||
expectedTimeout: 0 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
name: "custom reader, data ready, tty, .5sec",
|
||||
args: []string{"wasi", "poll", "0", "500"},
|
||||
stdin: internalsys.NewStdioFileReader(
|
||||
strings.NewReader("test"), // input ready
|
||||
stdinFileInfo(fs.ModeDevice|fs.ModeCharDevice|0o640),
|
||||
internalsys.PollerAlwaysReady),
|
||||
expectedOutput: "STDIN",
|
||||
expectedTimeout: 0 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
name: "custom, blocking reader, no data, tty, .5sec",
|
||||
args: []string{"wasi", "poll", "0", "500"},
|
||||
stdin: internalsys.NewStdioFileReader(
|
||||
newBlockingReader(t), // simulate waiting for input
|
||||
stdinFileInfo(fs.ModeDevice|fs.ModeCharDevice|0o640),
|
||||
internalsys.PollerNeverReady),
|
||||
expectedOutput: "NOINPUT",
|
||||
expectedTimeout: 500 * time.Millisecond, // always timeouts
|
||||
},
|
||||
{
|
||||
name: "eofReader, not tty, .5sec",
|
||||
args: []string{"wasi", "poll", "0", "500"},
|
||||
stdin: internalsys.NewStdioFileReader(
|
||||
eofReader{}, // simulate waiting for input
|
||||
stdinFileInfo(fs.ModeDevice|fs.ModeCharDevice|0o640),
|
||||
internalsys.PollerAlwaysReady),
|
||||
expectedOutput: "STDIN",
|
||||
expectedTimeout: 0 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
moduleConfig := wazero.NewModuleConfig().WithArgs(tc.args...).
|
||||
WithStdin(tc.stdin)
|
||||
start := time.Now()
|
||||
console := compileAndRun(t, moduleConfig, wasmZigCc)
|
||||
elapsed := time.Since(start)
|
||||
require.True(t, elapsed >= tc.expectedTimeout)
|
||||
require.Equal(t, tc.expectedOutput+"\n", console)
|
||||
})
|
||||
}
|
||||
require.Equal(t, "STDIN\n", console)
|
||||
}
|
||||
|
||||
// eofReader is safer than reading from os.DevNull as it can never overrun operating system file descriptors.
|
||||
type eofReader struct{}
|
||||
|
||||
// Read implements io.Reader
|
||||
// Note: This doesn't use a pointer reference as it has no state and an empty struct doesn't allocate.
|
||||
func (eofReader) Read([]byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func Test_Sleep(t *testing.T) {
|
||||
moduleConfig := wazero.NewModuleConfig().WithArgs("wasi", "sleepmillis", "100").WithSysNanosleep()
|
||||
start := time.Now()
|
||||
console := compileAndRun(t, moduleConfig, wasmZigCc)
|
||||
require.True(t, time.Since(start) >= 100*time.Millisecond)
|
||||
require.Equal(t, "OK\n", console)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"io/fs"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
@@ -147,3 +149,31 @@ func requireErrnoResult(t *testing.T, expectedErrno wasip1.Errno, mod api.Closer
|
||||
errno := wasip1.Errno(results[0])
|
||||
require.Equal(t, expectedErrno, errno, "want %s but have %s", wasip1.ErrnoName(expectedErrno), wasip1.ErrnoName(errno))
|
||||
}
|
||||
|
||||
func newBlockingReader(t *testing.T) blockingReader {
|
||||
timeout, cancelFunc := context.WithTimeout(testCtx, 5*time.Second)
|
||||
t.Cleanup(cancelFunc)
|
||||
return blockingReader{ctx: timeout}
|
||||
}
|
||||
|
||||
// blockingReader is an io.Reader that never terminates its read
|
||||
// unless the embedded context is Done()
|
||||
type blockingReader struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// Read implements io.Reader
|
||||
func (b blockingReader) Read(p []byte) (n int, err error) {
|
||||
<-b.ctx.Done()
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// stdinFileInfo implements fs.FileInfo: it is only representing the mode because it is always stdin
|
||||
type stdinFileInfo uint32
|
||||
|
||||
func (stdinFileInfo) Name() string { return "stdin" }
|
||||
func (stdinFileInfo) Size() int64 { return 0 }
|
||||
func (s stdinFileInfo) Mode() fs.FileMode { return fs.FileMode(s) }
|
||||
func (stdinFileInfo) ModTime() time.Time { return time.Unix(0, 0) }
|
||||
func (stdinFileInfo) IsDir() bool { return false }
|
||||
func (stdinFileInfo) Sys() interface{} { return nil }
|
||||
|
||||
23
internal/platform/fdset.go
Normal file
23
internal/platform/fdset.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package platform
|
||||
|
||||
// Set adds the given fd to the set.
|
||||
func (f *FdSet) Set(fd int) {
|
||||
f.Bits[fd/nfdbits] |= (1 << (uintptr(fd) % nfdbits))
|
||||
}
|
||||
|
||||
// Clear removes the given fd from the set.
|
||||
func (f *FdSet) Clear(fd int) {
|
||||
f.Bits[fd/nfdbits] &^= (1 << (uintptr(fd) % nfdbits))
|
||||
}
|
||||
|
||||
// IsSet returns true when fd is in the set.
|
||||
func (f *FdSet) IsSet(fd int) bool {
|
||||
return f.Bits[fd/nfdbits]&(1<<(uintptr(fd)%nfdbits)) != 0
|
||||
}
|
||||
|
||||
// Zero clears the set.
|
||||
func (f *FdSet) Zero() {
|
||||
for i := range f.Bits {
|
||||
f.Bits[i] = 0
|
||||
}
|
||||
}
|
||||
8
internal/platform/fdset_darwin.go
Normal file
8
internal/platform/fdset_darwin.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package platform
|
||||
|
||||
import "syscall"
|
||||
|
||||
const nfdbits = 0x20
|
||||
|
||||
// FdSet re-exports syscall.FdSet with utility methods.
|
||||
type FdSet syscall.FdSet
|
||||
8
internal/platform/fdset_linux.go
Normal file
8
internal/platform/fdset_linux.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package platform
|
||||
|
||||
import "syscall"
|
||||
|
||||
const nfdbits = 0x40
|
||||
|
||||
// FdSet re-exports syscall.FdSet with utility methods.
|
||||
type FdSet syscall.FdSet
|
||||
109
internal/platform/fdset_test.go
Normal file
109
internal/platform/fdset_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
)
|
||||
|
||||
func TestFdSet(t *testing.T) {
|
||||
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
|
||||
t.Skip("not supported")
|
||||
}
|
||||
|
||||
allBitsSetAtIndex0 := FdSet{}
|
||||
allBitsSetAtIndex0.Bits[0] = -1
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
init FdSet
|
||||
exec func(fdSet *FdSet)
|
||||
expected FdSet
|
||||
}{
|
||||
{
|
||||
name: "all bits set",
|
||||
exec: func(fdSet *FdSet) {
|
||||
for fd := 0; fd < nfdbits; fd++ {
|
||||
fdSet.Set(fd)
|
||||
}
|
||||
},
|
||||
expected: allBitsSetAtIndex0,
|
||||
},
|
||||
{
|
||||
name: "all bits cleared",
|
||||
init: allBitsSetAtIndex0,
|
||||
exec: func(fdSet *FdSet) {
|
||||
for fd := 0; fd < nfdbits; fd++ {
|
||||
fdSet.Clear(fd)
|
||||
}
|
||||
},
|
||||
expected: FdSet{},
|
||||
},
|
||||
{
|
||||
name: "zero should clear all bits",
|
||||
init: allBitsSetAtIndex0,
|
||||
exec: func(fdSet *FdSet) {
|
||||
fdSet.Zero()
|
||||
},
|
||||
expected: FdSet{},
|
||||
},
|
||||
{
|
||||
name: "is-set should return true for all bits",
|
||||
init: allBitsSetAtIndex0,
|
||||
exec: func(fdSet *FdSet) {
|
||||
for i := range fdSet.Bits {
|
||||
require.True(t, fdSet.IsSet(i))
|
||||
}
|
||||
},
|
||||
expected: allBitsSetAtIndex0,
|
||||
},
|
||||
{
|
||||
name: "is-set should return true for all odd bits",
|
||||
init: FdSet{},
|
||||
exec: func(fdSet *FdSet) {
|
||||
for fd := 1; fd < nfdbits; fd += 2 {
|
||||
fdSet.Set(fd)
|
||||
}
|
||||
for fd := 0; fd < nfdbits; fd++ {
|
||||
isSet := fdSet.IsSet(fd)
|
||||
if fd&0x1 == 0x1 {
|
||||
require.True(t, isSet)
|
||||
} else {
|
||||
require.False(t, isSet)
|
||||
}
|
||||
}
|
||||
fdSet.Zero()
|
||||
},
|
||||
expected: FdSet{},
|
||||
},
|
||||
{
|
||||
name: "should clear all even bits",
|
||||
init: allBitsSetAtIndex0,
|
||||
exec: func(fdSet *FdSet) {
|
||||
for fd := 0; fd < nfdbits; fd += 2 {
|
||||
fdSet.Clear(fd)
|
||||
}
|
||||
for fd := 0; fd < nfdbits; fd++ {
|
||||
isSet := fdSet.IsSet(fd)
|
||||
if fd&0x1 == 0x1 {
|
||||
require.True(t, isSet)
|
||||
} else {
|
||||
require.False(t, isSet)
|
||||
}
|
||||
}
|
||||
fdSet.Zero()
|
||||
},
|
||||
expected: FdSet{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
x := tc.init
|
||||
tc.exec(&x)
|
||||
require.Equal(t, tc.expected, x)
|
||||
})
|
||||
}
|
||||
}
|
||||
10
internal/platform/fdset_unsupported.go
Normal file
10
internal/platform/fdset_unsupported.go
Normal file
@@ -0,0 +1,10 @@
|
||||
//go:build !darwin && !linux
|
||||
|
||||
package platform
|
||||
|
||||
const nfdbits = 0x40
|
||||
|
||||
// FdSet mocks syscall.FdSet on systems that do not support it.
|
||||
type FdSet struct {
|
||||
Bits [16]int64
|
||||
}
|
||||
@@ -47,10 +47,3 @@ var libc_futimens_trampoline_addr uintptr
|
||||
// Note: CGO mechanisms are used in darwin regardless of the CGO_ENABLED value
|
||||
// or the "cgo" build flag. See /RATIONALE.md for why.
|
||||
//go:cgo_import_dynamic libc_futimens futimens "/usr/lib/libSystem.B.dylib"
|
||||
|
||||
// syscall_syscall6 is a private symbol that we link below. We need to use this
|
||||
// instead of syscall.Syscall6 because the public syscall.Syscall6 won't work
|
||||
// when fn is an address.
|
||||
//
|
||||
//go:linkname syscall_syscall6 syscall.syscall6
|
||||
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
|
||||
|
||||
31
internal/platform/select.go
Normal file
31
internal/platform/select.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package platform
|
||||
|
||||
import "time"
|
||||
|
||||
// Select exposes the select(2) syscall.
|
||||
//
|
||||
// # Notes on Parameters
|
||||
//
|
||||
// For convenience, we expose a pointer to a time.Duration instead of a pointer to a syscall.Timeval.
|
||||
// It must be a pointer because `nil` means "wait forever".
|
||||
//
|
||||
// However, notice that select(2) may mutate the pointed Timeval on some platforms,
|
||||
// for instance if the call returns early.
|
||||
//
|
||||
// This implementation *will not* update the pointed time.Duration value accordingly.
|
||||
//
|
||||
// See also: https://github.com/golang/sys/blob/master/unix/syscall_unix_test.go#L606-L617
|
||||
//
|
||||
// # Notes on the Syscall
|
||||
//
|
||||
// Because this is a blocking syscall, it will also block the carrier thread of the goroutine,
|
||||
// preventing any means to support context cancellation directly.
|
||||
//
|
||||
// There are ways to obviate this issue. We outline here one idea, that is however not currently implemented.
|
||||
// A common approach to support context cancellation is to add a signal file descriptor to the set,
|
||||
// e.g. the read-end of a pipe or an eventfd on Linux.
|
||||
// When the context is canceled, we may unblock a Select call by writing to the fd, causing it to return immediately.
|
||||
// This however requires to do a bit of housekeeping to hide the "special" FD from the end-user.
|
||||
func Select(n int, r, w, e *FdSet, timeout *time.Duration) (int, error) {
|
||||
return syscall_select(n, r, w, e, timeout)
|
||||
}
|
||||
43
internal/platform/select_darwin.go
Normal file
43
internal/platform/select_darwin.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// syscall_select invokes select on Darwin, with the given timeout Duration.
|
||||
// We implement our own version instead of relying on syscall.Select because the latter
|
||||
// only returns the error and discards the result.
|
||||
func syscall_select(n int, r, w, e *FdSet, timeout *time.Duration) (int, error) {
|
||||
var t *syscall.Timeval
|
||||
if timeout != nil {
|
||||
tv := syscall.NsecToTimeval(timeout.Nanoseconds())
|
||||
t = &tv
|
||||
}
|
||||
result, _, errno := syscall_syscall6(
|
||||
libc_select_trampoline_addr,
|
||||
uintptr(n),
|
||||
uintptr(unsafe.Pointer(r)),
|
||||
uintptr(unsafe.Pointer(w)),
|
||||
uintptr(unsafe.Pointer(e)),
|
||||
uintptr(unsafe.Pointer(t)),
|
||||
0)
|
||||
res := int(result)
|
||||
if errno == 0 {
|
||||
return res, nil
|
||||
}
|
||||
return res, errno
|
||||
}
|
||||
|
||||
// libc_select_trampoline_addr is the address of the
|
||||
// `libc_select_trampoline` symbol, defined in `select_darwin.s`.
|
||||
//
|
||||
// We use this to invoke the syscall through syscall_syscall6 imported below.
|
||||
var libc_select_trampoline_addr uintptr
|
||||
|
||||
// Imports the select symbol from libc as `libc_select`.
|
||||
//
|
||||
// Note: CGO mechanisms are used in darwin regardless of the CGO_ENABLED value
|
||||
// or the "cgo" build flag. See /RATIONALE.md for why.
|
||||
//go:cgo_import_dynamic libc_select select "/usr/lib/libSystem.B.dylib"
|
||||
8
internal/platform/select_darwin.s
Normal file
8
internal/platform/select_darwin.s
Normal file
@@ -0,0 +1,8 @@
|
||||
// lifted from golang.org/x/sys unix
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT libc_select_trampoline<>(SB), NOSPLIT, $0-0
|
||||
JMP libc_select(SB)
|
||||
|
||||
GLOBL ·libc_select_trampoline_addr(SB), RODATA, $8
|
||||
DATA ·libc_select_trampoline_addr(SB)/8, $libc_select_trampoline<>(SB)
|
||||
16
internal/platform/select_linux.go
Normal file
16
internal/platform/select_linux.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// syscall_select invokes select on Unix (unless Darwin), with the given timeout Duration.
|
||||
func syscall_select(n int, r, w, e *FdSet, timeout *time.Duration) (int, error) {
|
||||
var t *syscall.Timeval
|
||||
if timeout != nil {
|
||||
tv := syscall.NsecToTimeval(timeout.Nanoseconds())
|
||||
t = &tv
|
||||
}
|
||||
return syscall.Select(n, (*syscall.FdSet)(r), (*syscall.FdSet)(w), (*syscall.FdSet)(e), t)
|
||||
}
|
||||
92
internal/platform/select_test.go
Normal file
92
internal/platform/select_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tetratelabs/wazero/internal/testing/require"
|
||||
)
|
||||
|
||||
func TestSelect(t *testing.T) {
|
||||
t.Run("should return immediately with no fds and duration 0", func(t *testing.T) {
|
||||
for {
|
||||
dur := time.Duration(0)
|
||||
n, err := Select(0, nil, nil, nil, &dur)
|
||||
if err == syscall.EINTR {
|
||||
t.Logf("Select interrupted")
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, n)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should wait for the given duration", func(t *testing.T) {
|
||||
dur := 250 * time.Millisecond
|
||||
var took time.Duration
|
||||
for {
|
||||
// On some platforms (e.g. Linux), the passed-in timeval is
|
||||
// updated by select(2). We are not accounting for this
|
||||
// in our implementation.
|
||||
start := time.Now()
|
||||
n, err := Select(0, nil, nil, nil, &dur)
|
||||
took = time.Since(start)
|
||||
if err == syscall.EINTR {
|
||||
t.Logf("Select interrupted after %v", took)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, n)
|
||||
break
|
||||
}
|
||||
|
||||
// On some platforms the actual timeout might be arbitrarily
|
||||
// less than requested.
|
||||
if took < dur {
|
||||
if runtime.GOOS == "linux" {
|
||||
// Linux promises to only return early if a file descriptor
|
||||
// becomes ready (not applicable here), or the call
|
||||
// is interrupted by a signal handler (explicitly retried in the loop above),
|
||||
// or the timeout expires.
|
||||
t.Errorf("Select: slept for %v, expected %v", took, dur)
|
||||
} else {
|
||||
t.Logf("Select: slept for %v, requested %v", took, dur)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should return 1 if a given FD has data", func(t *testing.T) {
|
||||
rr, ww, err := os.Pipe()
|
||||
require.NoError(t, err)
|
||||
defer rr.Close()
|
||||
defer ww.Close()
|
||||
|
||||
_, err = ww.Write([]byte("TEST"))
|
||||
require.NoError(t, err)
|
||||
|
||||
rFdSet := &FdSet{}
|
||||
fd := int(rr.Fd())
|
||||
rFdSet.Set(fd)
|
||||
|
||||
for {
|
||||
n, err := Select(fd+1, rFdSet, nil, nil, nil)
|
||||
if runtime.GOOS == "windows" {
|
||||
// Not implemented for fds != wasiFdStdin
|
||||
require.ErrorIs(t, err, syscall.ENOSYS)
|
||||
require.Equal(t, -1, n)
|
||||
break
|
||||
}
|
||||
if err == syscall.EINTR {
|
||||
t.Log("Select interrupted")
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, n)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
12
internal/platform/select_unsupported.go
Normal file
12
internal/platform/select_unsupported.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build !darwin && !linux && !windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func syscall_select(n int, r, w, e *FdSet, timeout *time.Duration) (int, error) {
|
||||
return -1, syscall.ENOSYS
|
||||
}
|
||||
31
internal/platform/select_windows.go
Normal file
31
internal/platform/select_windows.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// wasiFdStdin is the constant value for stdin on Wasi.
|
||||
// We need this constant because on Windows os.Stdin.Fd() != 0.
|
||||
const wasiFdStdin = 0
|
||||
|
||||
// syscall_select emulates the select syscall on Windows for two, well-known cases, returns syscall.ENOSYS for all others.
|
||||
// If r contains fd 0, then it immediately returns 1 (data ready on stdin) and r will have the fd 0 bit set.
|
||||
// If n==0 it will wait for the given timeout duration, but it will return syscall.ENOSYS if timeout is nil,
|
||||
// i.e. it won't block indefinitely.
|
||||
func syscall_select(n int, r, w, e *FdSet, timeout *time.Duration) (int, error) {
|
||||
if n == 0 {
|
||||
// don't block indefinitely
|
||||
if timeout == nil {
|
||||
return -1, syscall.ENOSYS
|
||||
}
|
||||
time.Sleep(*timeout)
|
||||
return 0, nil
|
||||
}
|
||||
if r.IsSet(wasiFdStdin) {
|
||||
r.Zero()
|
||||
r.Set(wasiFdStdin)
|
||||
return 1, nil
|
||||
}
|
||||
return -1, syscall.ENOSYS
|
||||
}
|
||||
13
internal/platform/syscall6_darwin.go
Normal file
13
internal/platform/syscall6_darwin.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
// syscall_syscall6 is a private symbol that we link below. We need to use this
|
||||
// instead of syscall.Syscall6 because the public syscall.Syscall6 won't work
|
||||
// when fn is an address.
|
||||
//
|
||||
//go:linkname syscall_syscall6 syscall.syscall6
|
||||
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
|
||||
@@ -61,21 +61,72 @@ func (w *stdioFileWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type stdioFileReader struct {
|
||||
r io.Reader
|
||||
s fs.FileInfo
|
||||
// StdioFilePoller is a strategy for polling a StdioFileReader for a given duration.
|
||||
// It returns true if the reader has data ready to be read, false and/or an error otherwise.
|
||||
type StdioFilePoller interface {
|
||||
Poll(duration time.Duration) (bool, error)
|
||||
}
|
||||
|
||||
// PollerDefaultStdin is a poller that checks standard input.
|
||||
var PollerDefaultStdin = &pollerDefaultStdin{}
|
||||
|
||||
type pollerDefaultStdin struct{}
|
||||
|
||||
// Poll implements StdioFilePoller for pollerDefaultStdin.
|
||||
func (*pollerDefaultStdin) Poll(duration time.Duration) (bool, error) {
|
||||
fdSet := platform.FdSet{}
|
||||
fdSet.Set(int(FdStdin))
|
||||
count, err := platform.Select(int(FdStdin+1), &fdSet, nil, nil, &duration)
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// PollerAlwaysReady is a poller that ignores the given timeout, and it returns true and no error.
|
||||
var PollerAlwaysReady = &pollerAlwaysReady{}
|
||||
|
||||
type pollerAlwaysReady struct{}
|
||||
|
||||
// Poll implements StdioFilePoller for pollerAlwaysReady.
|
||||
func (*pollerAlwaysReady) Poll(time.Duration) (bool, error) { return true, nil }
|
||||
|
||||
// PollerNeverReady is a poller that waits for the given duration, and it always returns false and no error.
|
||||
var PollerNeverReady = &pollerNeverReady{}
|
||||
|
||||
type pollerNeverReady struct{}
|
||||
|
||||
// Poll implements StdioFilePoller for pollerNeverReady.
|
||||
func (*pollerNeverReady) Poll(d time.Duration) (bool, error) { time.Sleep(d); return false, nil }
|
||||
|
||||
// StdioFileReader implements io.Reader for stdio files.
|
||||
type StdioFileReader struct {
|
||||
r io.Reader
|
||||
s fs.FileInfo
|
||||
poll StdioFilePoller
|
||||
}
|
||||
|
||||
// NewStdioFileReader is a constructor for StdioFileReader.
|
||||
func NewStdioFileReader(reader io.Reader, fileInfo fs.FileInfo, poll StdioFilePoller) *StdioFileReader {
|
||||
return &StdioFileReader{
|
||||
r: reader,
|
||||
s: fileInfo,
|
||||
poll: poll,
|
||||
}
|
||||
}
|
||||
|
||||
// Poll invokes the StdioFilePoller that was given at the NewStdioFileReader constructor.
|
||||
func (r *StdioFileReader) Poll(duration time.Duration) (bool, error) {
|
||||
return r.poll.Poll(duration)
|
||||
}
|
||||
|
||||
// Stat implements fs.File
|
||||
func (r *stdioFileReader) Stat() (fs.FileInfo, error) { return r.s, nil }
|
||||
func (r *StdioFileReader) Stat() (fs.FileInfo, error) { return r.s, nil }
|
||||
|
||||
// Read implements fs.File
|
||||
func (r *stdioFileReader) Read(p []byte) (n int, err error) {
|
||||
func (r *StdioFileReader) Read(p []byte) (n int, err error) {
|
||||
return r.r.Read(p)
|
||||
}
|
||||
|
||||
// Close implements fs.File
|
||||
func (r *stdioFileReader) Close() error {
|
||||
func (r *StdioFileReader) Close() error {
|
||||
// Don't actually close the underlying file, as we didn't open it!
|
||||
return nil
|
||||
}
|
||||
@@ -300,11 +351,17 @@ func stdinReader(r io.Reader) (*FileEntry, error) {
|
||||
if r == nil {
|
||||
r = eofReader{}
|
||||
}
|
||||
s, err := stdioStat(r, noopStdinStat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var freader *StdioFileReader
|
||||
if stdioFileReader, ok := r.(*StdioFileReader); ok {
|
||||
freader = stdioFileReader
|
||||
} else {
|
||||
s, err := stdioStat(r, noopStdinStat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
freader = NewStdioFileReader(r, s, PollerDefaultStdin)
|
||||
}
|
||||
return &FileEntry{Name: noopStdinStat.Name(), File: &stdioFileReader{r: r, s: s}}, nil
|
||||
return &FileEntry{Name: noopStdinStat.Name(), File: freader}, nil
|
||||
}
|
||||
|
||||
func stdioWriter(w io.Writer, defaultStat stdioFileInfo) (*FileEntry, error) {
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
|
||||
|
||||
var (
|
||||
noopStdin = &FileEntry{Name: "stdin", File: &stdioFileReader{r: eofReader{}, s: noopStdinStat}}
|
||||
noopStdin = &FileEntry{Name: "stdin", File: NewStdioFileReader(eofReader{}, noopStdinStat, PollerDefaultStdin)}
|
||||
noopStdout = &FileEntry{Name: "stdout", File: &stdioFileWriter{w: io.Discard, s: noopStdoutStat}}
|
||||
noopStderr = &FileEntry{Name: "stderr", File: &stdioFileWriter{w: io.Discard, s: noopStderrStat}}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user