Files
wazero/internal/sysfs/select_windows.go
Crypt Keeper 180ff682d9 sysfs: changes PollRead to accept int32 timeoutMillis (#1597)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
Co-authored-by: Takeshi Yoneda <takeshi@tetrate.io>
2023-07-28 10:01:00 +08:00

179 lines
5.3 KiB
Go

package sysfs
import (
"context"
"syscall"
"time"
"unsafe"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/platform"
)
// pollInterval is the interval between each calls to peekNamedPipe in selectAllHandles
const pollInterval = 100 * time.Millisecond
// zeroDuration is the zero value for time.Duration. It is used in selectAllHandles.
var zeroDuration = time.Duration(0)
// syscall_select implements _select on Windows, for a subset of cases.
//
// r, w, e may contain any number of file handles, but regular files and pipes are only processed for r (Read).
// Stdin is a pipe, thus it is checked for readiness when present. Pipes are checked using PeekNamedPipe.
// Regular files always immediately report as ready, regardless their actual state and timeouts.
//
// If n==0 it will wait for the given timeout duration, but it will return sys.ENOSYS if timeout is nil,
// i.e. it won't block indefinitely.
//
// Note: ideas taken from https://stackoverflow.com/questions/6839508/test-if-stdin-has-input-for-c-windows-and-or-linux
// PeekNamedPipe: https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
func syscall_select(n int, r, w, e *platform.FdSet, timeoutNanos int32) (ready bool, errno sys.Errno) {
var timeout *time.Duration
if timeoutNanos >= 0 {
duration := time.Duration(timeoutNanos)
timeout = &duration
}
// TODO: This impl was left alone because it will change soon.
// See https://github.com/tetratelabs/wazero/pull/1596
if n == 0 {
// Don't block indefinitely.
if timeout == nil {
return false, sys.ENOSYS
}
time.Sleep(*timeout)
return false, 0
}
n, errno = selectAllHandles(context.TODO(), r, w, e, timeout)
return n > 0, errno
}
// selectAllHandles emulates a general-purpose POSIX select on Windows.
//
// The implementation actually polls every 100 milliseconds until it reaches the given duration.
// The duration may be nil, in which case it will wait undefinely. The given ctx is
// used to allow for cancellation, and it is currently used only in tests.
//
// As indicated in the man page for select [1], r, w, e are modified upon completion:
//
// "Upon successful completion, the pselect() or select() function shall modify the objects pointed to by the readfds,
// writefds, and errorfds arguments to indicate which file descriptors are ready for reading, ready for writing,
// or have an error condition pending, respectively, and shall return the total number of ready descriptors in all the output sets"
//
// However, for our purposes, this may be pedantic because currently we do not check the values of r, w, e
// after the invocation of select; thus, this behavior may be subject to change in the future for the sake of simplicity.
//
// [1]: https://linux.die.net/man/3/select
func selectAllHandles(ctx context.Context, r, w, e *platform.FdSet, duration *time.Duration) (n int, errno sys.Errno) {
r2, w2, e2 := r.Copy(), w.Copy(), e.Copy()
n, errno = peekAllHandles(r2, w2, e2)
// Short circuit when there is an error, there is data or the duration is zero.
if errno != 0 || n > 0 || (duration != nil && *duration == time.Duration(0)) {
r.SetAll(r2)
w.SetAll(w2)
e.SetAll(e2)
return
}
// Ticker that emits at every pollInterval.
tick := time.NewTicker(pollInterval)
tickCh := tick.C
defer tick.Stop()
// Timer that expires after the given duration.
// Initialize afterCh as nil: the select below will wait forever.
var afterCh <-chan time.Time
if duration != nil {
// If duration is not nil, instantiate the timer.
after := time.NewTimer(*duration)
defer after.Stop()
afterCh = after.C
}
for {
select {
case <-ctx.Done():
r.Zero()
w.Zero()
e.Zero()
return
case <-afterCh:
r.Zero()
w.Zero()
e.Zero()
return
case <-tickCh:
r2, w2, e2 = r.Copy(), w.Copy(), e.Copy()
n, errno = peekAllHandles(r2, w2, e2)
if errno != 0 || n > 0 {
r.SetAll(r2)
w.SetAll(w2)
e.SetAll(e2)
return
}
}
}
}
func peekAllHandles(r, w, e *platform.FdSet) (int, sys.Errno) {
// pipes are not checked on w, e
w.Pipes().Zero()
e.Pipes().Zero()
// peek pipes only for reading
errno := peekAllPipes(r.Pipes())
if errno != 0 {
return 0, errno
}
_, errno = winsock_select(r.Sockets(), w.Sockets(), e.Sockets(), &zeroDuration)
if errno != 0 {
return 0, errno
}
return r.Count() + w.Count() + e.Count(), 0
}
func peekAllPipes(pipeHandles *platform.WinSockFdSet) sys.Errno {
ready := &platform.WinSockFdSet{}
for i := 0; i < pipeHandles.Count(); i++ {
h := pipeHandles.Get(i)
bytes, errno := peekNamedPipe(h)
if bytes > 0 {
ready.Set(int(h))
}
if errno != 0 {
return sys.UnwrapOSError(errno)
}
}
*pipeHandles = *ready
return 0
}
func winsock_select(r, w, e *platform.WinSockFdSet, timeout *time.Duration) (int, sys.Errno) {
if r.Count() == 0 && w.Count() == 0 && e.Count() == 0 {
return 0, 0
}
var t *syscall.Timeval
if timeout != nil {
tv := syscall.NsecToTimeval(timeout.Nanoseconds())
t = &tv
}
rp := unsafe.Pointer(r)
wp := unsafe.Pointer(w)
ep := unsafe.Pointer(e)
tp := unsafe.Pointer(t)
r0, _, err := syscall.SyscallN(
procselect.Addr(),
uintptr(0), // the first argument is ignored and exists only for compat with BSD sockets.
uintptr(rp),
uintptr(wp),
uintptr(ep),
uintptr(tp))
return int(r0), sys.UnwrapOSError(err)
}