wasip1: support non-blocking mode on stdio (#1443)
Signed-off-by: Achille Roussel <achille.roussel@gmail.com>
This commit is contained in:
@@ -202,8 +202,21 @@ func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) syscall.E
|
||||
return syscall.EBADF
|
||||
} else if st, errno = f.Stat(); errno != 0 {
|
||||
return errno
|
||||
} else if f.File.AccessMode() != syscall.O_RDONLY {
|
||||
fdflags = wasip1.FD_APPEND
|
||||
} else {
|
||||
var nonblock bool
|
||||
switch file := f.File.File().(type) {
|
||||
case *sys.StdioFileReader:
|
||||
nonblock = file.IsNonblock()
|
||||
case *sys.StdioFileWriter:
|
||||
nonblock = file.IsNonblock()
|
||||
default:
|
||||
if f.File.AccessMode() != syscall.O_RDONLY {
|
||||
fdflags |= wasip1.FD_APPEND
|
||||
}
|
||||
}
|
||||
if nonblock {
|
||||
fdflags |= wasip1.FD_NONBLOCK
|
||||
}
|
||||
}
|
||||
|
||||
var fsRightsBase uint32
|
||||
@@ -276,11 +289,25 @@ func fdFdstatSetFlagsFn(_ context.Context, mod api.Module, params []uint64) sysc
|
||||
fd, wasiFlag := int32(params[0]), uint16(params[1])
|
||||
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
|
||||
|
||||
// We can only support APPEND flag.
|
||||
if wasip1.FD_DSYNC&wasiFlag != 0 || wasip1.FD_NONBLOCK&wasiFlag != 0 || wasip1.FD_RSYNC&wasiFlag != 0 || wasip1.FD_SYNC&wasiFlag != 0 {
|
||||
// Currently we only support APPEND and NONBLOCK.
|
||||
if wasip1.FD_DSYNC&wasiFlag != 0 || wasip1.FD_RSYNC&wasiFlag != 0 || wasip1.FD_SYNC&wasiFlag != 0 {
|
||||
return syscall.EINVAL
|
||||
}
|
||||
|
||||
// Currently we only support non-blocking mode for standard I/O streams.
|
||||
// Non-blocking mode is rarely supported for regular files, and we don't
|
||||
// yet have support for sockets so we make a special case.
|
||||
f, ok := fsc.LookupFile(fd)
|
||||
if ok {
|
||||
nonblock := wasip1.FD_NONBLOCK&wasiFlag != 0
|
||||
switch file := f.File.File().(type) {
|
||||
case *sys.StdioFileReader:
|
||||
return platform.UnwrapOSError(file.SetNonblock(nonblock))
|
||||
case *sys.StdioFileWriter:
|
||||
return platform.UnwrapOSError(file.SetNonblock(nonblock))
|
||||
}
|
||||
}
|
||||
|
||||
var flag int
|
||||
if wasip1.FD_APPEND&wasiFlag != 0 {
|
||||
flag = syscall.O_APPEND
|
||||
|
||||
@@ -218,6 +218,17 @@ func Test_fdDatasync(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func openPipe(t *testing.T) (*os.File, *os.File) {
|
||||
r, w, err := os.Pipe()
|
||||
require.NoError(t, err)
|
||||
return r, w
|
||||
}
|
||||
|
||||
func closePipe(r, w *os.File) {
|
||||
r.Close()
|
||||
w.Close()
|
||||
}
|
||||
|
||||
func Test_fdFdstatGet(t *testing.T) {
|
||||
file, dir := "animals.txt", "sub"
|
||||
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFS(fstest.FS))
|
||||
@@ -261,13 +272,13 @@ func Test_fdFdstatGet(t *testing.T) {
|
||||
fd: sys.FdStdout,
|
||||
expectedMemory: []byte{
|
||||
1, 0, // fs_filetype
|
||||
1, 0, 0, 0, 0, 0, // fs_flags
|
||||
0, 0, 0, 0, 0, 0, // fs_flags
|
||||
0xff, 0x1, 0xe0, 0x8, 0x0, 0x0, 0x0, 0x0, // fs_rights_base
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
|
||||
},
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.fd_fdstat_get(fd=1)
|
||||
<== (stat={filetype=BLOCK_DEVICE,fdflags=APPEND,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
|
||||
<== (stat={filetype=BLOCK_DEVICE,fdflags=,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -275,13 +286,13 @@ func Test_fdFdstatGet(t *testing.T) {
|
||||
fd: sys.FdStderr,
|
||||
expectedMemory: []byte{
|
||||
1, 0, // fs_filetype
|
||||
1, 0, 0, 0, 0, 0, // fs_flags
|
||||
0, 0, 0, 0, 0, 0, // fs_flags
|
||||
0xff, 0x1, 0xe0, 0x8, 0x0, 0x0, 0x0, 0x0, // fs_rights_base
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
|
||||
},
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.fd_fdstat_get(fd=2)
|
||||
<== (stat={filetype=BLOCK_DEVICE,fdflags=APPEND,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
|
||||
<== (stat={filetype=BLOCK_DEVICE,fdflags=,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -365,6 +376,98 @@ func Test_fdFdstatGet(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_fdFdstatGet_StdioNonblock(t *testing.T) {
|
||||
stdinR, stdinW := openPipe(t)
|
||||
defer closePipe(stdinR, stdinW)
|
||||
|
||||
stdoutR, stdoutW := openPipe(t)
|
||||
defer closePipe(stdoutR, stdoutW)
|
||||
|
||||
stderrR, stderrW := openPipe(t)
|
||||
defer closePipe(stderrR, stderrW)
|
||||
|
||||
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().
|
||||
WithStdin(stdinR).
|
||||
WithStdout(stdoutW).
|
||||
WithStderr(stderrW))
|
||||
defer r.Close(testCtx)
|
||||
|
||||
stdin, stdout, stderr := uint64(0), uint64(1), uint64(2)
|
||||
requireErrnoResult(t, 0, mod, wasip1.FdFdstatSetFlagsName, stdin, uint64(wasip1.FD_NONBLOCK))
|
||||
requireErrnoResult(t, 0, mod, wasip1.FdFdstatSetFlagsName, stdout, uint64(wasip1.FD_NONBLOCK))
|
||||
requireErrnoResult(t, 0, mod, wasip1.FdFdstatSetFlagsName, stderr, uint64(wasip1.FD_NONBLOCK))
|
||||
log.Reset()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fd int32
|
||||
resultFdstat uint32
|
||||
expectedMemory []byte
|
||||
expectedErrno wasip1.Errno
|
||||
expectedLog string
|
||||
}{
|
||||
{
|
||||
name: "stdin",
|
||||
fd: sys.FdStdin,
|
||||
expectedMemory: []byte{
|
||||
0, 0, // fs_filetype
|
||||
4, 0, 0, 0, 0, 0, // fs_flags
|
||||
0xff, 0x1, 0xe0, 0x8, 0x0, 0x0, 0x0, 0x0, // fs_rights_base
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
|
||||
},
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.fd_fdstat_get(fd=0)
|
||||
<== (stat={filetype=UNKNOWN,fdflags=NONBLOCK,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "stdout",
|
||||
fd: sys.FdStdout,
|
||||
expectedMemory: []byte{
|
||||
0, 0, // fs_filetype
|
||||
4, 0, 0, 0, 0, 0, // fs_flags
|
||||
0xff, 0x1, 0xe0, 0x8, 0x0, 0x0, 0x0, 0x0, // fs_rights_base
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
|
||||
},
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.fd_fdstat_get(fd=1)
|
||||
<== (stat={filetype=UNKNOWN,fdflags=NONBLOCK,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "stderr",
|
||||
fd: sys.FdStderr,
|
||||
expectedMemory: []byte{
|
||||
0, 0, // fs_filetype
|
||||
4, 0, 0, 0, 0, 0, // fs_flags
|
||||
0xff, 0x1, 0xe0, 0x8, 0x0, 0x0, 0x0, 0x0, // fs_rights_base
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
|
||||
},
|
||||
expectedLog: `
|
||||
==> wasi_snapshot_preview1.fd_fdstat_get(fd=2)
|
||||
<== (stat={filetype=UNKNOWN,fdflags=NONBLOCK,fs_rights_base=FD_DATASYNC|FD_READ|FD_SEEK|FDSTAT_SET_FLAGS|FD_SYNC|FD_TELL|FD_WRITE|FD_ADVISE|FD_ALLOCATE,fs_rights_inheriting=},errno=ESUCCESS)
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer log.Reset()
|
||||
|
||||
maskMemory(t, mod, len(tc.expectedMemory))
|
||||
|
||||
requireErrnoResult(t, tc.expectedErrno, mod, wasip1.FdFdstatGetName, uint64(tc.fd), uint64(tc.resultFdstat))
|
||||
require.Equal(t, tc.expectedLog, "\n"+log.String())
|
||||
|
||||
actual, ok := mod.Memory().Read(0, uint32(len(tc.expectedMemory)))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tc.expectedMemory, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_fdFdstatSetFlags(t *testing.T) {
|
||||
tmpDir := t.TempDir() // open before loop to ensure no locking problems.
|
||||
const fileName = "file.txt"
|
||||
@@ -373,8 +476,20 @@ func Test_fdFdstatSetFlags(t *testing.T) {
|
||||
realPath := joinPath(tmpDir, fileName)
|
||||
require.NoError(t, os.WriteFile(realPath, []byte("0123456789"), 0o600))
|
||||
|
||||
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().WithFSConfig(wazero.NewFSConfig().
|
||||
WithDirMount(tmpDir, "/")))
|
||||
stdinR, stdinW := openPipe(t)
|
||||
defer closePipe(stdinR, stdinW)
|
||||
|
||||
stdoutR, stdoutW := openPipe(t)
|
||||
defer closePipe(stdoutR, stdoutW)
|
||||
|
||||
stderrR, stderrW := openPipe(t)
|
||||
defer closePipe(stderrR, stderrW)
|
||||
|
||||
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().
|
||||
WithStdin(stdinR).
|
||||
WithStdout(stdoutW).
|
||||
WithStderr(stderrW).
|
||||
WithFSConfig(wazero.NewFSConfig().WithDirMount(tmpDir, "/")))
|
||||
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
|
||||
preopen := fsc.RootFS()
|
||||
defer r.Close(testCtx)
|
||||
@@ -445,13 +560,20 @@ func Test_fdFdstatSetFlags(t *testing.T) {
|
||||
writeWazero()
|
||||
requireFileContent("wazero6789" + "wazero" + "wazero")
|
||||
|
||||
t.Run("nonblock", func(t *testing.T) {
|
||||
stdin, stdout, stderr := uint64(0), uint64(1), uint64(2)
|
||||
requireErrnoResult(t, 0, mod, wasip1.FdFdstatSetFlagsName, stdin, uint64(wasip1.FD_NONBLOCK))
|
||||
requireErrnoResult(t, 0, mod, wasip1.FdFdstatSetFlagsName, stdout, uint64(wasip1.FD_NONBLOCK))
|
||||
requireErrnoResult(t, 0, mod, wasip1.FdFdstatSetFlagsName, stderr, uint64(wasip1.FD_NONBLOCK))
|
||||
})
|
||||
|
||||
t.Run("errors", func(t *testing.T) {
|
||||
requireErrnoResult(t, wasip1.ErrnoInval, mod, wasip1.FdFdstatSetFlagsName, uint64(fd), uint64(wasip1.FD_DSYNC))
|
||||
requireErrnoResult(t, wasip1.ErrnoInval, mod, wasip1.FdFdstatSetFlagsName, uint64(fd), uint64(wasip1.FD_NONBLOCK))
|
||||
requireErrnoResult(t, wasip1.ErrnoInval, mod, wasip1.FdFdstatSetFlagsName, uint64(fd), uint64(wasip1.FD_RSYNC))
|
||||
requireErrnoResult(t, wasip1.ErrnoInval, mod, wasip1.FdFdstatSetFlagsName, uint64(fd), uint64(wasip1.FD_SYNC))
|
||||
requireErrnoResult(t, wasip1.ErrnoBadf, mod, wasip1.FdFdstatSetFlagsName, uint64(12345), uint64(wasip1.FD_APPEND))
|
||||
requireErrnoResult(t, wasip1.ErrnoIsdir, mod, wasip1.FdFdstatSetFlagsName, uint64(3) /* preopen */, uint64(wasip1.FD_APPEND))
|
||||
requireErrnoResult(t, wasip1.ErrnoIsdir, mod, wasip1.FdFdstatSetFlagsName, uint64(3), uint64(wasip1.FD_NONBLOCK))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
9
internal/platform/nonblock_unix.go
Normal file
9
internal/platform/nonblock_unix.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package platform
|
||||
|
||||
import "syscall"
|
||||
|
||||
func SetNonblock(fd uintptr, enable bool) error {
|
||||
return syscall.SetNonblock(int(fd), enable)
|
||||
}
|
||||
9
internal/platform/nonblock_windows.go
Normal file
9
internal/platform/nonblock_windows.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build windows
|
||||
|
||||
package platform
|
||||
|
||||
import "syscall"
|
||||
|
||||
func SetNonblock(fd uintptr, enable bool) error {
|
||||
return syscall.SetNonblock(syscall.Handle(fd), enable)
|
||||
}
|
||||
@@ -33,30 +33,53 @@ const (
|
||||
|
||||
const modeDevice = uint32(fs.ModeDevice | 0o640)
|
||||
|
||||
type stdioFileWriter struct {
|
||||
type StdioFileWriter struct {
|
||||
w io.Writer
|
||||
s fs.FileInfo
|
||||
// Known state of the non-blocking mode. Defaults to false which may not
|
||||
// match the underlying state of the file descriptor if it was opened in
|
||||
// non-blocking mode whe instantiating the module.
|
||||
nonblock bool
|
||||
}
|
||||
|
||||
// Stat implements fs.File
|
||||
func (w *stdioFileWriter) Stat() (fs.FileInfo, error) { return w.s, nil }
|
||||
func (w *StdioFileWriter) Stat() (fs.FileInfo, error) { return w.s, nil }
|
||||
|
||||
// Read implements fs.File
|
||||
func (w *stdioFileWriter) Read([]byte) (n int, err error) {
|
||||
func (w *StdioFileWriter) Read([]byte) (n int, err error) {
|
||||
return // emulate os.Stdout which returns zero
|
||||
}
|
||||
|
||||
// Write implements io.Writer
|
||||
func (w *stdioFileWriter) Write(p []byte) (n int, err error) {
|
||||
func (w *StdioFileWriter) Write(p []byte) (n int, err error) {
|
||||
return w.w.Write(p)
|
||||
}
|
||||
|
||||
// Close implements fs.File
|
||||
func (w *stdioFileWriter) Close() error {
|
||||
func (w *StdioFileWriter) Close() error {
|
||||
// Don't actually close the underlying file, as we didn't open it!
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsNonblock returns the current known state of non-blocking mode on the
|
||||
// underlying file.
|
||||
func (w *StdioFileWriter) IsNonblock() bool {
|
||||
return w.nonblock
|
||||
}
|
||||
|
||||
// SetNonblock sets the file to non-blocking mode if the reader has access to
|
||||
// the underlying file descriptor.
|
||||
func (w *StdioFileWriter) SetNonblock(enable bool) error {
|
||||
if f, ok := w.w.(*os.File); ok {
|
||||
if err := platform.SetNonblock(f.Fd(), enable); err != nil {
|
||||
return err
|
||||
}
|
||||
w.nonblock = enable
|
||||
return nil
|
||||
}
|
||||
return syscall.ENOSYS
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -97,6 +120,8 @@ type StdioFileReader struct {
|
||||
r io.Reader
|
||||
s fs.FileInfo
|
||||
poll StdioFilePoller
|
||||
// See StdioFileWriter.
|
||||
nonblock bool
|
||||
}
|
||||
|
||||
// NewStdioFileReader is a constructor for StdioFileReader.
|
||||
@@ -127,6 +152,25 @@ func (r *StdioFileReader) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsNonblock returns the current known state of non-blocking mode on the
|
||||
// underlying file.
|
||||
func (r *StdioFileReader) IsNonblock() bool {
|
||||
return r.nonblock
|
||||
}
|
||||
|
||||
// SetNonblock sets the file to non-blocking mode if the reader has access to
|
||||
// the underlying file descriptor.
|
||||
func (r *StdioFileReader) SetNonblock(enable bool) error {
|
||||
if f, ok := r.r.(*os.File); ok {
|
||||
if err := platform.SetNonblock(f.Fd(), enable); err != nil {
|
||||
return err
|
||||
}
|
||||
r.nonblock = enable
|
||||
return nil
|
||||
}
|
||||
return syscall.ENOSYS
|
||||
}
|
||||
|
||||
var (
|
||||
noopStdinStat = stdioFileInfo{0, modeDevice}
|
||||
noopStdoutStat = stdioFileInfo{1, modeDevice}
|
||||
@@ -455,7 +499,7 @@ func stdioWriter(w io.Writer, defaultStat stdioFileInfo) (*FileEntry, error) {
|
||||
return nil, err
|
||||
}
|
||||
return &FileEntry{
|
||||
Name: s.Name(), File: platform.NewFsFile("", syscall.O_WRONLY, &stdioFileWriter{w: w, s: s}),
|
||||
Name: s.Name(), File: platform.NewFsFile("", syscall.O_WRONLY, &StdioFileWriter{w: w, s: s}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
|
||||
var (
|
||||
noopStdin = &FileEntry{Name: "stdin", File: platform.NewFsFile("", syscall.O_RDONLY, NewStdioFileReader(eofReader{}, noopStdinStat, PollerDefaultStdin))}
|
||||
noopStdout = &FileEntry{Name: "stdout", File: platform.NewFsFile("", syscall.O_WRONLY, &stdioFileWriter{w: io.Discard, s: noopStdoutStat})}
|
||||
noopStderr = &FileEntry{Name: "stderr", File: platform.NewFsFile("", syscall.O_WRONLY, &stdioFileWriter{w: io.Discard, s: noopStderrStat})}
|
||||
noopStdout = &FileEntry{Name: "stdout", File: platform.NewFsFile("", syscall.O_WRONLY, &StdioFileWriter{w: io.Discard, s: noopStdoutStat})}
|
||||
noopStderr = &FileEntry{Name: "stderr", File: platform.NewFsFile("", syscall.O_WRONLY, &StdioFileWriter{w: io.Discard, s: noopStderrStat})}
|
||||
)
|
||||
|
||||
//go:embed testdata
|
||||
|
||||
Reference in New Issue
Block a user