poll_windows.go

  1package sysfs
  2
  3import (
  4	"syscall"
  5	"time"
  6	"unsafe"
  7
  8	"github.com/tetratelabs/wazero/experimental/sys"
  9)
 10
 11var (
 12	procWSAPoll          = modws2_32.NewProc("WSAPoll")
 13	procGetNamedPipeInfo = kernel32.NewProc("GetNamedPipeInfo")
 14)
 15
 16const (
 17	// _POLLRDNORM subscribes to normal data for read.
 18	_POLLRDNORM = 0x0100
 19	// _POLLRDBAND subscribes to priority band (out-of-band) data for read.
 20	_POLLRDBAND = 0x0200
 21	// _POLLIN subscribes a notification when any readable data is available.
 22	_POLLIN = (_POLLRDNORM | _POLLRDBAND)
 23)
 24
 25// pollFd is the struct to query for file descriptor events using poll.
 26type pollFd struct {
 27	// fd is the file descriptor.
 28	fd uintptr
 29	// events is a bitmap containing the requested events.
 30	events int16
 31	// revents is a bitmap containing the returned events.
 32	revents int16
 33}
 34
 35// newPollFd is a constructor for pollFd that abstracts the platform-specific type of file descriptors.
 36func newPollFd(fd uintptr, events, revents int16) pollFd {
 37	return pollFd{fd: fd, events: events, revents: revents}
 38}
 39
 40// pollInterval is the interval between each calls to peekNamedPipe in selectAllHandles
 41const pollInterval = 100 * time.Millisecond
 42
 43// _poll implements poll on Windows, for a subset of cases.
 44//
 45// fds may contain any number of file handles, but regular files and pipes are only processed for _POLLIN.
 46// Stdin is a pipe, thus it is checked for readiness when present. Pipes are checked using PeekNamedPipe.
 47// Regular files always immediately reported as ready, regardless their actual state and timeouts.
 48//
 49// If n==0 it will wait for the given timeout duration, but it will return sys.ENOSYS if timeout is nil,
 50// i.e. it won't block indefinitely. The given ctx is used to allow for cancellation,
 51// and it is currently used only in tests.
 52//
 53// The implementation actually polls every 100 milliseconds (pollInterval) until it reaches the
 54// given timeout (in millis).
 55//
 56// The duration may be negative, in which case it will wait indefinitely. The given ctx is
 57// used to allow for cancellation, and it is currently used only in tests.
 58func _poll(fds []pollFd, timeoutMillis int32) (n int, errno sys.Errno) {
 59	if fds == nil {
 60		return -1, sys.ENOSYS
 61	}
 62
 63	regular, pipes, sockets, errno := partionByFtype(fds)
 64	nregular := len(regular)
 65	if errno != 0 {
 66		return -1, errno
 67	}
 68
 69	// Ticker that emits at every pollInterval.
 70	tick := time.NewTicker(pollInterval)
 71	tickCh := tick.C
 72	defer tick.Stop()
 73
 74	// Timer that expires after the given duration.
 75	// Initialize afterCh as nil: the select below will wait forever.
 76	var afterCh <-chan time.Time
 77	if timeoutMillis >= 0 {
 78		// If duration is not nil, instantiate the timer.
 79		after := time.NewTimer(time.Duration(timeoutMillis) * time.Millisecond)
 80		defer after.Stop()
 81		afterCh = after.C
 82	}
 83
 84	npipes, nsockets, errno := peekAll(pipes, sockets)
 85	if errno != 0 {
 86		return -1, errno
 87	}
 88	count := nregular + npipes + nsockets
 89	if count > 0 {
 90		return count, 0
 91	}
 92
 93	for {
 94		select {
 95		case <-afterCh:
 96			return 0, 0
 97		case <-tickCh:
 98			npipes, nsockets, errno := peekAll(pipes, sockets)
 99			if errno != 0 {
100				return -1, errno
101			}
102			count = nregular + npipes + nsockets
103			if count > 0 {
104				return count, 0
105			}
106		}
107	}
108}
109
110func peekAll(pipes, sockets []pollFd) (npipes, nsockets int, errno sys.Errno) {
111	npipes, errno = peekPipes(pipes)
112	if errno != 0 {
113		return
114	}
115
116	// Invoke wsaPoll with a 0-timeout to avoid blocking.
117	// Timeouts are handled in pollWithContext instead.
118	nsockets, errno = wsaPoll(sockets, 0)
119	if errno != 0 {
120		return
121	}
122
123	count := npipes + nsockets
124	if count > 0 {
125		return
126	}
127
128	return
129}
130
131func peekPipes(fds []pollFd) (n int, errno sys.Errno) {
132	for _, fd := range fds {
133		bytes, errno := peekNamedPipe(syscall.Handle(fd.fd))
134		if errno != 0 {
135			return -1, sys.UnwrapOSError(errno)
136		}
137		if bytes > 0 {
138			n++
139		}
140	}
141	return
142}
143
144// wsaPoll is the WSAPoll function from winsock2.
145//
146// See https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsapoll
147func wsaPoll(fds []pollFd, timeout int) (n int, errno sys.Errno) {
148	if len(fds) > 0 {
149		sockptr := &fds[0]
150		ns, _, e := syscall.SyscallN(
151			procWSAPoll.Addr(),
152			uintptr(unsafe.Pointer(sockptr)),
153			uintptr(len(fds)),
154			uintptr(timeout))
155		if e != 0 {
156			return -1, sys.UnwrapOSError(e)
157		}
158		n = int(ns)
159	}
160	return
161}
162
163// ftype is a type of file that can be handled by poll.
164type ftype uint8
165
166const (
167	ftype_regular ftype = iota
168	ftype_pipe
169	ftype_socket
170)
171
172// partionByFtype checks the type of each fd in fds and returns 3 distinct partitions
173// for regular files, named pipes and sockets.
174func partionByFtype(fds []pollFd) (regular, pipe, socket []pollFd, errno sys.Errno) {
175	for _, pfd := range fds {
176		t, errno := ftypeOf(pfd.fd)
177		if errno != 0 {
178			return nil, nil, nil, errno
179		}
180		switch t {
181		case ftype_regular:
182			regular = append(regular, pfd)
183		case ftype_pipe:
184			pipe = append(pipe, pfd)
185		case ftype_socket:
186			socket = append(socket, pfd)
187		}
188	}
189	return
190}
191
192// ftypeOf checks the type of fd and return the corresponding ftype.
193func ftypeOf(fd uintptr) (ftype, sys.Errno) {
194	h := syscall.Handle(fd)
195	t, err := syscall.GetFileType(h)
196	if err != nil {
197		return 0, sys.UnwrapOSError(err)
198	}
199	switch t {
200	case syscall.FILE_TYPE_CHAR, syscall.FILE_TYPE_DISK:
201		return ftype_regular, 0
202	case syscall.FILE_TYPE_PIPE:
203		if isSocket(h) {
204			return ftype_socket, 0
205		} else {
206			return ftype_pipe, 0
207		}
208	default:
209		return ftype_regular, 0
210	}
211}
212
213// isSocket returns true if the given file handle
214// is a pipe.
215func isSocket(fd syscall.Handle) bool {
216	r, _, errno := syscall.SyscallN(
217		procGetNamedPipeInfo.Addr(),
218		uintptr(fd),
219		uintptr(unsafe.Pointer(nil)),
220		uintptr(unsafe.Pointer(nil)),
221		uintptr(unsafe.Pointer(nil)),
222		uintptr(unsafe.Pointer(nil)))
223	return r == 0 || errno != 0
224}