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

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:
Edoardo Vacchi
2023-04-18 16:31:34 +02:00
committed by GitHub
parent ab78591915
commit ea336061c2
23 changed files with 1274 additions and 121 deletions

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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)
}

View File

@@ -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 }

View 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
}
}

View File

@@ -0,0 +1,8 @@
package platform
import "syscall"
const nfdbits = 0x20
// FdSet re-exports syscall.FdSet with utility methods.
type FdSet syscall.FdSet

View File

@@ -0,0 +1,8 @@
package platform
import "syscall"
const nfdbits = 0x40
// FdSet re-exports syscall.FdSet with utility methods.
type FdSet syscall.FdSet

View 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)
})
}
}

View 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
}

View File

@@ -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)

View 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)
}

View 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"

View 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)

View 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)
}

View 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
}
})
}

View 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
}

View 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
}

View 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)

View File

@@ -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) {

View File

@@ -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}}
)