mirror of
https://github.com/wazero/wazero
synced 2026-06-21 14:12:37 +00:00
5500f5c252
poll_oneoff only polled stdin for blocking fd read subscriptions, silently dropping non-stdin pollable fds. Generalize the deferred polling to poll each blocking fd individually, tracking remaining time budget across iterations so total wall time never exceeds the requested timeout. Changes: - Generalize poll_oneoff blocking fd handling: track each deferred subscription's file and poll it individually instead of only stdin - Track elapsed time per poll so N blocking fds complete within 1x timeout - Add test for poll_oneoff with non-stdin pollable fd Fixes: #1500 Signed-off-by: Christian Stewart <christian@aperture.us>
891 lines
27 KiB
Go
891 lines
27 KiB
Go
package wasi_snapshot_preview1_test
|
|
|
|
import (
|
|
"io/fs"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tetratelabs/wazero"
|
|
"github.com/tetratelabs/wazero/api"
|
|
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
|
|
"github.com/tetratelabs/wazero/internal/sys"
|
|
"github.com/tetratelabs/wazero/internal/testing/require"
|
|
"github.com/tetratelabs/wazero/internal/wasip1"
|
|
"github.com/tetratelabs/wazero/internal/wasm"
|
|
sysapi "github.com/tetratelabs/wazero/sys"
|
|
)
|
|
|
|
func Test_pollOneoff(t *testing.T) {
|
|
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig())
|
|
defer r.Close(testCtx)
|
|
|
|
mem := []byte{
|
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // userdata
|
|
wasip1.EventTypeClock, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // event type and padding
|
|
wasip1.ClockIDMonotonic, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // clockID
|
|
0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // timeout (ns)
|
|
0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // precision (ns)
|
|
0x00, 0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // flags (relative)
|
|
'?', // stopped after encoding
|
|
}
|
|
|
|
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, '?', // stopped after encoding
|
|
}
|
|
|
|
in := uint32(0) // past in
|
|
out := uint32(128) // past in
|
|
nsubscriptions := uint32(1)
|
|
resultNevents := uint32(512) // past out
|
|
|
|
maskMemory(t, mod, 1024)
|
|
mod.Memory().Write(in, mem)
|
|
|
|
requireErrnoResult(t, wasip1.ErrnoSuccess, mod, wasip1.PollOneoffName, uint64(in), uint64(out), uint64(nsubscriptions),
|
|
uint64(resultNevents))
|
|
require.Equal(t, `
|
|
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=1)
|
|
<== (nevents=1,errno=ESUCCESS)
|
|
`, "\n"+log.String())
|
|
|
|
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, nsubscriptions, nevents)
|
|
}
|
|
|
|
func Test_pollOneoff_Errors(t *testing.T) {
|
|
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig())
|
|
defer r.Close(testCtx)
|
|
|
|
tests := []struct {
|
|
name string
|
|
in, out, nsubscriptions, resultNevents uint32
|
|
mem []byte // at offset in
|
|
expectedErrno wasip1.Errno
|
|
expectedMem []byte // at offset out
|
|
expectedLog string
|
|
}{
|
|
{
|
|
name: "in out of range",
|
|
in: wasm.MemoryPageSize,
|
|
nsubscriptions: 1,
|
|
out: 128, // past in
|
|
resultNevents: 512, // past out
|
|
expectedErrno: wasip1.ErrnoFault,
|
|
expectedLog: `
|
|
==> wasi_snapshot_preview1.poll_oneoff(in=65536,out=128,nsubscriptions=1)
|
|
<== (nevents=,errno=EFAULT)
|
|
`,
|
|
},
|
|
{
|
|
name: "out out of range",
|
|
out: wasm.MemoryPageSize,
|
|
resultNevents: 512, // past out
|
|
nsubscriptions: 1,
|
|
expectedErrno: wasip1.ErrnoFault,
|
|
expectedLog: `
|
|
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=65536,nsubscriptions=1)
|
|
<== (nevents=,errno=EFAULT)
|
|
`,
|
|
},
|
|
{
|
|
name: "resultNevents out of range",
|
|
resultNevents: wasm.MemoryPageSize,
|
|
nsubscriptions: 1,
|
|
expectedErrno: wasip1.ErrnoFault,
|
|
expectedLog: `
|
|
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=0,nsubscriptions=1)
|
|
<== (nevents=,errno=EFAULT)
|
|
`,
|
|
},
|
|
{
|
|
name: "nsubscriptions zero",
|
|
out: 128, // past in
|
|
resultNevents: 512, // past out
|
|
expectedErrno: wasip1.ErrnoInval,
|
|
expectedLog: `
|
|
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=0)
|
|
<== (nevents=,errno=EINVAL)
|
|
`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
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, uint32(1), nevents)
|
|
_ = nevents
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_pollOneoff_Stdin(t *testing.T) {
|
|
w, r, err := os.Pipe()
|
|
require.NoError(t, err)
|
|
defer w.Close()
|
|
defer r.Close()
|
|
_, _ = w.Write([]byte("wazero"))
|
|
|
|
tests := []struct {
|
|
name string
|
|
in, out, nsubscriptions, resultNevents uint32
|
|
mem []byte // at offset in
|
|
stdin experimentalsys.File
|
|
expectedErrno wasip1.Errno
|
|
expectedMem []byte // at offset out
|
|
expectedLog string
|
|
expectedNevents uint32
|
|
}{
|
|
{
|
|
name: "Read without explicit timeout (no tty)",
|
|
nsubscriptions: 1,
|
|
expectedNevents: 1,
|
|
stdin: &sys.StdinFile{Reader: strings.NewReader("test")},
|
|
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,
|
|
stdin: &ttyStdinFile{StdinFile: sys.StdinFile{Reader: strings.NewReader("test")}},
|
|
mem: concat(
|
|
clockNsSub(20*1000*1000),
|
|
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.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,
|
|
stdin: &ttyStdinFile{StdinFile: sys.StdinFile{Reader: strings.NewReader("test")}},
|
|
mem: concat(
|
|
clockNsSub(20*1000*1000),
|
|
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.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,
|
|
stdin: &sys.StdinFile{Reader: strings.NewReader("test")},
|
|
mem: concat(
|
|
clockNsSub(20*1000*1000),
|
|
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.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,
|
|
stdin: &sys.StdinFile{Reader: strings.NewReader("test")},
|
|
mem: concat(
|
|
clockNsSub(20*1000*1000),
|
|
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.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,
|
|
stdin: &neverReadyTtyStdinFile{StdinFile: sys.StdinFile{Reader: newBlockingReader(t)}},
|
|
mem: concat(
|
|
clockNsSub(20*1000*1000),
|
|
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.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)
|
|
`,
|
|
},
|
|
{
|
|
name: "pollable pipe, multiple subs, events returned out of order",
|
|
nsubscriptions: 3,
|
|
expectedNevents: 3,
|
|
mem: concat(
|
|
fdReadSub,
|
|
clockNsSub(20*1000*1000),
|
|
// Illegal file fd with custom user data to recognize it in the event buffer.
|
|
fdReadSubFdWithUserData(100, []byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77})),
|
|
stdin: &sys.StdinFile{Reader: w},
|
|
expectedErrno: wasip1.ErrnoSuccess,
|
|
out: 128, // past in
|
|
resultNevents: 512, // past out
|
|
expectedMem: []byte{
|
|
// Clock is acknowledged first.
|
|
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,
|
|
|
|
// Then an illegal file with custom user data.
|
|
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, // userdata
|
|
byte(wasip1.ErrnoBadf), 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,
|
|
|
|
// Stdin pipes are delayed to invoke sysfs.poll
|
|
// thus, they are written back last.
|
|
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=3)
|
|
<== (nevents=3,errno=ESUCCESS)
|
|
`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig())
|
|
defer r.Close(testCtx)
|
|
defer log.Reset()
|
|
|
|
setStdin(t, mod, tc.stdin)
|
|
|
|
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 setStdin(t *testing.T, mod api.Module, stdin experimentalsys.File) {
|
|
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
|
|
f, ok := fsc.LookupFile(sys.FdStdin)
|
|
require.True(t, ok)
|
|
f.File = stdin
|
|
}
|
|
|
|
func Test_pollOneoff_Zero(t *testing.T) {
|
|
poller := &pollStdinFile{StdinFile: sys.StdinFile{Reader: strings.NewReader("test")}, ready: true}
|
|
|
|
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig())
|
|
defer r.Close(testCtx)
|
|
defer log.Reset()
|
|
|
|
setStdin(t, mod, poller)
|
|
|
|
maskMemory(t, mod, 1024)
|
|
|
|
out := uint32(128)
|
|
nsubscriptions := 2
|
|
resultNevents := uint32(512)
|
|
|
|
mod.Memory().Write(0,
|
|
concat(
|
|
clockNsSub(20*1000*1000),
|
|
fdReadSub,
|
|
),
|
|
)
|
|
|
|
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,
|
|
),
|
|
)
|
|
|
|
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)
|
|
}
|
|
|
|
// Test_pollOneoff_NonStdinPollable verifies that poll_oneoff correctly polls
|
|
// non-stdin fds that implement Pollable (e.g. custom fs.FS mounts).
|
|
func Test_pollOneoff_NonStdinPollable(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a file so we can open it and get an fd.
|
|
require.NoError(t, os.WriteFile(tmpDir+"/test.txt", []byte("data"), 0o600))
|
|
|
|
cfg := wazero.NewModuleConfig().WithFSConfig(
|
|
wazero.NewFSConfig().WithDirMount(tmpDir, "/"),
|
|
)
|
|
mod, r, log := requireProxyModule(t, cfg)
|
|
defer r.Close(testCtx)
|
|
defer log.Reset()
|
|
|
|
fd := requirePollOpenFile(t, mod, "test.txt")
|
|
|
|
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
|
|
entry, ok := fsc.LookupFile(fd)
|
|
require.True(t, ok)
|
|
entry.File = &pollableFile{ready: true}
|
|
|
|
maskMemory(t, mod, 1024)
|
|
|
|
out := uint32(128)
|
|
resultNevents := uint32(512)
|
|
|
|
// Subscribe to fd read on our custom pollable fd.
|
|
mod.Memory().Write(0, fdReadSubFd(byte(fd)))
|
|
|
|
requireErrnoResult(t, wasip1.ErrnoSuccess, mod, wasip1.PollOneoffName,
|
|
uint64(0), uint64(out), uint64(1), uint64(resultNevents))
|
|
|
|
require.Equal(t, `
|
|
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=1)
|
|
<== (nevents=1,errno=ESUCCESS)
|
|
`, "\n"+log.String())
|
|
|
|
// Verify the event was written with success.
|
|
outMem, ok := mod.Memory().Read(out, 32)
|
|
require.True(t, ok)
|
|
require.Equal(t, byte(wasip1.ErrnoSuccess), outMem[8])
|
|
require.Equal(t, byte(wasip1.EventTypeFdRead), outMem[10])
|
|
|
|
nevents, ok := mod.Memory().ReadUint32Le(resultNevents)
|
|
require.True(t, ok)
|
|
require.Equal(t, uint32(1), nevents)
|
|
}
|
|
|
|
func Test_pollOneoff_NonStdinPollUnsupported(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
require.NoError(t, os.WriteFile(tmpDir+"/test.txt", []byte("data"), 0o600))
|
|
|
|
cfg := wazero.NewModuleConfig().WithFSConfig(
|
|
wazero.NewFSConfig().WithDirMount(tmpDir, "/"),
|
|
)
|
|
|
|
tests := []struct {
|
|
name string
|
|
file experimentalsys.File
|
|
}{
|
|
{
|
|
name: "file does not implement Pollable",
|
|
file: experimentalsys.UnimplementedFile{},
|
|
},
|
|
{
|
|
name: "file Poll returns ENOSYS",
|
|
file: &pollableFile{errno: experimentalsys.ENOSYS},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
mod, r, log := requireProxyModule(t, cfg)
|
|
defer r.Close(testCtx)
|
|
defer log.Reset()
|
|
|
|
fd := requirePollOpenFile(t, mod, "test.txt")
|
|
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
|
|
entry, ok := fsc.LookupFile(fd)
|
|
require.True(t, ok)
|
|
if tc.file != nil {
|
|
entry.File = tc.file
|
|
}
|
|
|
|
maskMemory(t, mod, 1024)
|
|
|
|
out := uint32(128)
|
|
resultNevents := uint32(512)
|
|
mod.Memory().Write(0, fdReadSubFd(byte(fd)))
|
|
|
|
requireErrnoResult(t, wasip1.ErrnoSuccess, mod, wasip1.PollOneoffName,
|
|
uint64(0), uint64(out), uint64(1), uint64(resultNevents))
|
|
|
|
require.Equal(t, `
|
|
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=1)
|
|
<== (nevents=1,errno=ESUCCESS)
|
|
`, "\n"+log.String())
|
|
|
|
outMem, ok := mod.Memory().Read(out, 32)
|
|
require.True(t, ok)
|
|
require.Equal(t, byte(wasip1.ErrnoNotsup), outMem[8])
|
|
require.Equal(t, byte(wasip1.EventTypeFdRead), outMem[10])
|
|
|
|
nevents, ok := mod.Memory().ReadUint32Le(resultNevents)
|
|
require.True(t, ok)
|
|
require.Equal(t, uint32(1), nevents)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_pollOneoff_NonStdinPollableNotReady(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
require.NoError(t, os.WriteFile(tmpDir+"/test.txt", []byte("data"), 0o600))
|
|
|
|
cfg := wazero.NewModuleConfig().WithFSConfig(
|
|
wazero.NewFSConfig().WithDirMount(tmpDir, "/"),
|
|
)
|
|
mod, r, log := requireProxyModule(t, cfg)
|
|
defer r.Close(testCtx)
|
|
defer log.Reset()
|
|
|
|
fd := requirePollOpenFile(t, mod, "test.txt")
|
|
poller := &pollableFile{}
|
|
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
|
|
entry, ok := fsc.LookupFile(fd)
|
|
require.True(t, ok)
|
|
entry.File = poller
|
|
|
|
maskMemory(t, mod, 1024)
|
|
|
|
out := uint32(128)
|
|
resultNevents := uint32(512)
|
|
mod.Memory().Write(0,
|
|
concat(
|
|
clockNsSub(uint64(20*time.Millisecond)),
|
|
fdReadSubFd(byte(fd)),
|
|
),
|
|
)
|
|
|
|
requireErrnoResult(t, wasip1.ErrnoSuccess, mod, wasip1.PollOneoffName,
|
|
uint64(0), uint64(out), uint64(2), uint64(resultNevents))
|
|
|
|
require.Equal(t, `
|
|
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=2)
|
|
<== (nevents=1,errno=ESUCCESS)
|
|
`, "\n"+log.String())
|
|
require.Equal(t, []int32{20}, poller.timeouts)
|
|
|
|
outMem, ok := mod.Memory().Read(out, 64)
|
|
require.True(t, ok)
|
|
require.Equal(t, byte(wasip1.ErrnoSuccess), outMem[8])
|
|
require.Equal(t, byte(wasip1.EventTypeClock), outMem[10])
|
|
require.Equal(t, make([]byte, 32), outMem[32:64])
|
|
|
|
nevents, ok := mod.Memory().ReadUint32Le(resultNevents)
|
|
require.True(t, ok)
|
|
require.Equal(t, uint32(1), nevents)
|
|
}
|
|
|
|
func Test_pollOneoff_NonStdinPollableTimeoutBudget(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
require.NoError(t, os.WriteFile(tmpDir+"/one.txt", []byte("one"), 0o600))
|
|
require.NoError(t, os.WriteFile(tmpDir+"/two.txt", []byte("two"), 0o600))
|
|
|
|
cfg := wazero.NewModuleConfig().WithFSConfig(
|
|
wazero.NewFSConfig().WithDirMount(tmpDir, "/"),
|
|
)
|
|
mod, r, log := requireProxyModule(t, cfg)
|
|
defer r.Close(testCtx)
|
|
defer log.Reset()
|
|
|
|
firstFD := requirePollOpenFile(t, mod, "one.txt")
|
|
secondFD := requirePollOpenFile(t, mod, "two.txt")
|
|
|
|
firstPoller := &pollableFile{sleep: 20 * time.Millisecond}
|
|
secondPoller := &pollableFile{}
|
|
|
|
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
|
|
firstEntry, ok := fsc.LookupFile(firstFD)
|
|
require.True(t, ok)
|
|
firstEntry.File = firstPoller
|
|
secondEntry, ok := fsc.LookupFile(secondFD)
|
|
require.True(t, ok)
|
|
secondEntry.File = secondPoller
|
|
|
|
maskMemory(t, mod, 1024)
|
|
|
|
out := uint32(128)
|
|
resultNevents := uint32(512)
|
|
mod.Memory().Write(0,
|
|
concat(
|
|
clockNsSub(uint64(10*time.Millisecond)),
|
|
fdReadSubFd(byte(firstFD)),
|
|
fdReadSubFd(byte(secondFD)),
|
|
),
|
|
)
|
|
|
|
requireErrnoResult(t, wasip1.ErrnoSuccess, mod, wasip1.PollOneoffName,
|
|
uint64(0), uint64(out), uint64(3), uint64(resultNevents))
|
|
|
|
require.Equal(t, `
|
|
==> wasi_snapshot_preview1.poll_oneoff(in=0,out=128,nsubscriptions=3)
|
|
<== (nevents=1,errno=ESUCCESS)
|
|
`, "\n"+log.String())
|
|
require.Equal(t, []int32{10}, firstPoller.timeouts)
|
|
require.Equal(t, []int32{0}, secondPoller.timeouts)
|
|
|
|
outMem, ok := mod.Memory().Read(out, 96)
|
|
require.True(t, ok)
|
|
require.Equal(t, byte(wasip1.ErrnoSuccess), outMem[8])
|
|
require.Equal(t, byte(wasip1.EventTypeClock), outMem[10])
|
|
require.Equal(t, make([]byte, 64), outMem[32:96])
|
|
|
|
nevents, ok := mod.Memory().ReadUint32Le(resultNevents)
|
|
require.True(t, ok)
|
|
require.Equal(t, uint32(1), nevents)
|
|
}
|
|
|
|
func requirePollOpenFile(t *testing.T, mod api.Module, name string) int32 {
|
|
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
|
|
preopen, ok := fsc.LookupFile(sys.FdPreopen)
|
|
require.True(t, ok)
|
|
fd, errno := fsc.OpenFile(preopen.FS, name, experimentalsys.O_RDONLY, 0)
|
|
require.EqualErrno(t, 0, errno)
|
|
return fd
|
|
}
|
|
|
|
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
|
|
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, // pad to 32 bytes
|
|
}
|
|
}
|
|
|
|
func fdReadSubFdWithUserData(fd byte, userdata []byte) []byte {
|
|
return concat(
|
|
userdata,
|
|
[]byte{
|
|
wasip1.EventTypeFdRead, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
|
fd, 0x0, 0x0, 0x0, // valid readable FD
|
|
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, // pad to 32 bytes
|
|
})
|
|
}
|
|
|
|
// subscription for an EventTypeFdRead on stdin
|
|
var fdReadSub = fdReadSubFd(byte(sys.FdStdin))
|
|
|
|
// ttyStat returns fs.ModeCharDevice | fs.ModeCharDevice as an approximation
|
|
// for isatty.
|
|
//
|
|
// See go-isatty for a more specific approach:
|
|
// https://github.com/mattn/go-isatty/blob/v0.0.18/isatty_tcgets.go#LL11C1-L12C1
|
|
type ttyStat struct{}
|
|
|
|
// Stat implements the same method as documented on sys.File
|
|
func (ttyStat) Stat() (sysapi.Stat_t, experimentalsys.Errno) {
|
|
return sysapi.Stat_t{
|
|
Mode: fs.ModeDevice | fs.ModeCharDevice,
|
|
Nlink: 1,
|
|
}, 0
|
|
}
|
|
|
|
type ttyStdinFile struct {
|
|
sys.StdinFile
|
|
ttyStat
|
|
}
|
|
|
|
type neverReadyTtyStdinFile struct {
|
|
sys.StdinFile
|
|
ttyStat
|
|
}
|
|
|
|
// Poll implements the same method as documented on sys.File
|
|
func (neverReadyTtyStdinFile) Poll(flag experimentalsys.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
|
|
if flag != experimentalsys.POLLIN {
|
|
return false, experimentalsys.ENOTSUP
|
|
}
|
|
switch {
|
|
case timeoutMillis <= 0:
|
|
return
|
|
}
|
|
time.Sleep(time.Duration(timeoutMillis) * time.Millisecond)
|
|
return false, 0
|
|
}
|
|
|
|
type pollStdinFile struct {
|
|
sys.StdinFile
|
|
ttyStat
|
|
ready bool
|
|
}
|
|
|
|
// Poll implements the same method as documented on sys.File
|
|
func (p *pollStdinFile) Poll(flag experimentalsys.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
|
|
if flag != experimentalsys.POLLIN {
|
|
return false, experimentalsys.ENOTSUP
|
|
}
|
|
return p.ready, 0
|
|
}
|
|
|
|
type pollableFile struct {
|
|
experimentalsys.UnimplementedFile
|
|
|
|
ready bool
|
|
errno experimentalsys.Errno
|
|
sleep time.Duration
|
|
timeouts []int32
|
|
}
|
|
|
|
// Poll implements the same method as documented on experimentalsys.Pollable
|
|
func (p *pollableFile) Poll(flag experimentalsys.Pflag, timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
|
|
if flag != experimentalsys.POLLIN {
|
|
return false, experimentalsys.ENOTSUP
|
|
}
|
|
p.timeouts = append(p.timeouts, timeoutMillis)
|
|
if p.sleep > 0 {
|
|
time.Sleep(p.sleep)
|
|
}
|
|
return p.ready, p.errno
|
|
}
|