Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wasi: nonblocking I/O for sockets and pipes on Windows #1579

Merged
merged 11 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions imports/wasi_snapshot_preview1/wasi_stdlib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"os"
"os/exec"
"path"
"runtime"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -478,13 +477,11 @@ func testSock(t *testing.T, bin []byte) {
console := <-ch
require.NotEqual(t, 0, n)
require.NoError(t, err)
require.Equal(t, "wazero\n", console)
// Nonblocking connections may contain error logging, we ignore those.
require.Equal(t, "wazero\n", console[len(console)-7:])
}

func Test_HTTP(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fsapi.Nonblocking() is not supported on wasip1+windows.")
}
toolchains := map[string][]byte{}
if wasmGotip != nil {
toolchains["gotip"] = wasmGotip
Expand Down
2 changes: 2 additions & 0 deletions internal/platform/fdset.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

package platform

// Set adds the given fd to the set.
Expand Down
7 changes: 2 additions & 5 deletions internal/platform/fdset_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
//go:build !windows

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

Expand Down
2 changes: 1 addition & 1 deletion internal/platform/fdset_unsupported.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !darwin && !linux
//go:build !darwin && !linux && !windows

package platform

Expand Down
209 changes: 209 additions & 0 deletions internal/platform/fdset_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package platform

import (
"syscall"
"unsafe"
)

var procGetNamedPipeInfo = kernel32.NewProc("GetNamedPipeInfo")

// Maximum number of fds in a WinSockFdSet.
const _FD_SETSIZE = 64

// WinSockFdSet implements the FdSet representation that is used internally by WinSock.
//
// Note: this representation is quite different from the one used in most POSIX implementations
// where a bitfield is usually implemented; instead on Windows we have a simpler array+count pair.
// Notice that because it keeps a count of the inserted handles, the first argument of select
// in WinSock is actually ignored.
//
// The implementation of the Set, Clear, IsSet, Zero, methods follows exactly
// the real implementation found in WinSock2.h, e.g. see:
// https://github.com/microsoft/win32metadata/blob/ef7725c75c6b39adfdc13ba26fb1d89ac954449a/generation/WinSDK/RecompiledIdlHeaders/um/WinSock2.h#L124-L175
type WinSockFdSet struct {
// count is the number of used slots used in the handles slice.
count uint64
// handles is the array of handles. This is called "array" in the WinSock implementation
// and it has a fixed length of _FD_SETSIZE.
handles [_FD_SETSIZE]syscall.Handle
}

// FdSet implements the same methods provided on other plaforms.
//
// Note: the implementation is very different from POSIX; Windows provides
// POSIX select only for sockets. We emulate a select for other APIs in the sysfs
// package, but we still want to use the "real" select in the case of sockets.
// So, we keep a separate FdSet of sockets, so that we can pass it directly
// to the winsock select implementation
type FdSet struct {
sockets WinSockFdSet
pipes WinSockFdSet
regular WinSockFdSet
}

// Sockets returns a WinSockFdSet containing the handles in this FdSet that are sockets.
func (f *FdSet) Sockets() *WinSockFdSet {
if f == nil {
return nil
}
return &f.sockets
}

func (f *FdSet) SetSockets(s WinSockFdSet) {
f.sockets = s
}

// Regular returns a WinSockFdSet containing the handles in this FdSet that are regular files.
func (f *FdSet) Regular() *WinSockFdSet {
if f == nil {
return nil
}
return &f.regular
}

func (f *FdSet) SetRegular(r WinSockFdSet) {
f.regular = r
}

// Regular returns a WinSockFdSet containing the handles in this FdSet that are pipes.
func (f *FdSet) Pipes() *WinSockFdSet {
if f == nil {
return nil
}
return &f.pipes
}

func (f *FdSet) SetPipes(p WinSockFdSet) {
f.pipes = p
}

func (f *FdSet) getFdSetFor(fd int) *WinSockFdSet {
h := syscall.Handle(fd)
t, err := syscall.GetFileType(h)
if err != nil {
return nil
}
switch t {
case syscall.FILE_TYPE_CHAR, syscall.FILE_TYPE_DISK:
return &f.regular
case syscall.FILE_TYPE_PIPE:
if isSocket(h) {
return &f.sockets
} else {
return &f.pipes
}
default:
return nil
}
}

// Set adds the given fd to the set.
func (f *FdSet) Set(fd int) {
if s := f.getFdSetFor(fd); s != nil {
s.Set(fd)
}
}

// Clear removes the given fd from the set.
func (f *FdSet) Clear(fd int) {
if s := f.getFdSetFor(fd); s != nil {
s.Clear(fd)
}
}

// IsSet returns true when fd is in the set.
func (f *FdSet) IsSet(fd int) bool {
if s := f.getFdSetFor(fd); s != nil {
return s.IsSet(fd)
}
return false
}

// Zero clears the set.
func (f *FdSet) Zero() {
f.sockets.Zero()
f.regular.Zero()
f.pipes.Zero()
}

// Set adds the given fd to the set.
func (f *WinSockFdSet) Set(fd int) {
if f.count < _FD_SETSIZE {
f.handles[f.count] = syscall.Handle(fd)
f.count++
}
}

// Clear removes the given fd from the set.
func (f *WinSockFdSet) Clear(fd int) {
h := syscall.Handle(fd)
if !isSocket(h) {
return
}

for i := uint64(0); i < f.count; i++ {
if f.handles[i] == h {
for ; i < f.count-1; i++ {
f.handles[i] = f.handles[i+1]
}
f.count--
break
}
}
}

// IsSet returns true when fd is in the set.
func (f *WinSockFdSet) IsSet(fd int) bool {
h := syscall.Handle(fd)
for i := uint64(0); i < f.count; i++ {
if f.handles[i] == h {
return true
}
}
return false
}

// Zero clears the set.
func (f *WinSockFdSet) Zero() {
f.count = 0
}

func (f *WinSockFdSet) Count() int {
if f == nil {
return 0
}
return int(f.count)
}

func (f *WinSockFdSet) Copy() *WinSockFdSet {
if f == nil {
return nil
}
copy := *f
return &copy
}

func (f *WinSockFdSet) Get(index int) syscall.Handle {
return f.handles[index]
}

// isSocket returns true if the given file handle
// is a pipe.
func isSocket(fd syscall.Handle) bool {
// n, err := syscall.GetFileType(fd)
// if err != nil {
// return false
// }
// if n != syscall.FILE_TYPE_PIPE {
// return false
// }
// If the call to GetNamedPipeInfo succeeds then
// the handle is a pipe handle, otherwise it is a socket.
r, _, errno := syscall.SyscallN(
procGetNamedPipeInfo.Addr(),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)))
return r != 0 && errno == 0
}
8 changes: 0 additions & 8 deletions internal/sysfs/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ func TestStdioFileSetNonblock(t *testing.T) {
}

func TestRegularFileSetNonblock(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Nonblock on regular files is not supported on Windows")
}

// Test using os.Pipe as it is known to support non-blocking reads.
r, w, err := os.Pipe()
require.NoError(t, err)
Expand Down Expand Up @@ -339,10 +335,6 @@ func TestFilePollRead(t *testing.T) {

// When there's nothing in the pipe, it isn't ready.
ready, errno := rF.PollRead(&timeout)
if runtime.GOOS == "windows" {
require.EqualErrno(t, experimentalsys.ENOSYS, errno)
t.Skip("TODO: windows File.PollRead")
}
require.EqualErrno(t, 0, errno)
require.False(t, ready)

Expand Down
44 changes: 31 additions & 13 deletions internal/sysfs/file_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ var procPeekNamedPipe = kernel32.NewProc("PeekNamedPipe")
// https://learn.microsoft.com/en-us/windows/console/console-handles
func readFd(fd uintptr, buf []byte) (int, sys.Errno) {
handle := syscall.Handle(fd)
fileType, err := syscall.GetFileType(syscall.Stdin)
fileType, err := syscall.GetFileType(handle)
if err != nil {
return 0, sys.UnwrapOSError(err)
}
if fileType&syscall.FILE_TYPE_CHAR == 0 {
return -1, sys.ENOSYS
}
n, err := peekNamedPipe(handle)
if err == syscall.ERROR_BROKEN_PIPE {
n, errno := peekNamedPipe(handle)
if errno == syscall.ERROR_BROKEN_PIPE {
return 0, 0
}
if n == 0 {
Expand All @@ -42,24 +42,42 @@ func readFd(fd uintptr, buf []byte) (int, sys.Errno) {
return un, sys.UnwrapOSError(err)
}

func writeFd(fd uintptr, buf []byte) (int, sys.Errno) {
return -1, sys.ENOSYS
}

func readSocket(h syscall.Handle, buf []byte) (int, sys.Errno) {
var overlapped syscall.Overlapped
var done uint32
errno := syscall.ReadFile(h, buf, &done, &overlapped)
if errno == syscall.ERROR_IO_PENDING {
errno = sys.EAGAIN
}
return int(done), sys.UnwrapOSError(errno)
}

func writeSocket(fd uintptr, buf []byte) (int, sys.Errno) {
var done uint32
var overlapped syscall.Overlapped
errno := syscall.WriteFile(syscall.Handle(fd), buf, &done, &overlapped)
if errno == syscall.ERROR_IO_PENDING {
errno = syscall.EAGAIN
}
return int(done), sys.UnwrapOSError(errno)
}

// peekNamedPipe partially exposes PeekNamedPipe from the Win32 API
// see https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
func peekNamedPipe(handle syscall.Handle) (uint32, error) {
func peekNamedPipe(handle syscall.Handle) (uint32, syscall.Errno) {
var totalBytesAvail uint32
totalBytesPtr := unsafe.Pointer(&totalBytesAvail)
_, _, err := procPeekNamedPipe.Call(
_, _, errno := syscall.SyscallN(
procPeekNamedPipe.Addr(),
uintptr(handle), // [in] HANDLE hNamedPipe,
0, // [out, optional] LPVOID lpBuffer,
0, // [in] DWORD nBufferSize,
0, // [out, optional] LPDWORD lpBytesRead
uintptr(totalBytesPtr), // [out, optional] LPDWORD lpTotalBytesAvail,
0) // [out, optional] LPDWORD lpBytesLeftThisMessage
if err == syscall.Errno(0) {
return totalBytesAvail, nil
}
return totalBytesAvail, err
}

func writeFd(fd uintptr, buf []byte) (int, sys.Errno) {
return -1, sys.ENOSYS
return totalBytesAvail, errno
}
6 changes: 0 additions & 6 deletions internal/sysfs/select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,6 @@ func TestSelect(t *testing.T) {

for {
n, err := _select(fd+1, rFdSet, nil, nil, nil)
if runtime.GOOS == "windows" {
// Not implemented for fds != wasiFdStdin
require.ErrorIs(t, err, sys.ENOSYS)
require.Equal(t, -1, n)
break
}
if err == sys.EINTR {
t.Log("Select interrupted")
continue
Expand Down
Loading