github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/sysfs/poll_windows.go (about)

     1  package sysfs
     2  
     3  import (
     4  	"syscall"
     5  	"time"
     6  	"unsafe"
     7  
     8  	"github.com/wasilibs/wazerox/experimental/sys"
     9  )
    10  
    11  var (
    12  	procWSAPoll          = modws2_32.NewProc("WSAPoll")
    13  	procGetNamedPipeInfo = kernel32.NewProc("GetNamedPipeInfo")
    14  )
    15  
    16  const (
    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.
    26  type 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.
    36  func 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
    41  const 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.
    58  func _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  
   110  func 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  
   131  func 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
   147  func 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.
   164  type ftype uint8
   165  
   166  const (
   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.
   174  func 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.
   193  func 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.
   215  func 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  }