diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go index 98353d31..a96e9dfc 100644 --- a/imports/wasi_snapshot_preview1/fs.go +++ b/imports/wasi_snapshot_preview1/fs.go @@ -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 diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go index 02b49ad2..91b681f5 100644 --- a/imports/wasi_snapshot_preview1/fs_test.go +++ b/imports/wasi_snapshot_preview1/fs_test.go @@ -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)) }) } diff --git a/internal/platform/nonblock_unix.go b/internal/platform/nonblock_unix.go new file mode 100644 index 00000000..d022f025 --- /dev/null +++ b/internal/platform/nonblock_unix.go @@ -0,0 +1,9 @@ +//go:build !windows + +package platform + +import "syscall" + +func SetNonblock(fd uintptr, enable bool) error { + return syscall.SetNonblock(int(fd), enable) +} diff --git a/internal/platform/nonblock_windows.go b/internal/platform/nonblock_windows.go new file mode 100644 index 00000000..8d0b669b --- /dev/null +++ b/internal/platform/nonblock_windows.go @@ -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) +} diff --git a/internal/sys/fs.go b/internal/sys/fs.go index 66122c89..5a61d7ea 100644 --- a/internal/sys/fs.go +++ b/internal/sys/fs.go @@ -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 } diff --git a/internal/sys/fs_test.go b/internal/sys/fs_test.go index fd944013..98df0fe6 100644 --- a/internal/sys/fs_test.go +++ b/internal/sys/fs_test.go @@ -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