wasip1: support non-blocking mode on stdio (#1443)

Signed-off-by: Achille Roussel <achille.roussel@gmail.com>
This commit is contained in:
Achille
2023-05-08 15:45:04 -07:00
committed by GitHub
parent 2bf8abe1fe
commit 99d45623c0
6 changed files with 230 additions and 19 deletions

View File

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

View File

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

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

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

View File

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

View File

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