github.com/tetratelabs/wazero@v1.2.1/internal/sysfs/select_windows.go (about)

     1  package sysfs
     2  
     3  import (
     4  	"context"
     5  	"syscall"
     6  	"time"
     7  	"unsafe"
     8  
     9  	"github.com/tetratelabs/wazero/internal/platform"
    10  )
    11  
    12  // wasiFdStdin is the constant value for stdin on Wasi.
    13  // We need this constant because on Windows os.Stdin.Fd() != 0.
    14  const wasiFdStdin = 0
    15  
    16  // pollInterval is the interval between each calls to peekNamedPipe in pollNamedPipe
    17  const pollInterval = 100 * time.Millisecond
    18  
    19  var kernel32 = syscall.NewLazyDLL("kernel32.dll")
    20  
    21  // procPeekNamedPipe is the syscall.LazyProc in kernel32 for PeekNamedPipe
    22  var procPeekNamedPipe = kernel32.NewProc("PeekNamedPipe")
    23  
    24  // syscall_select emulates the select syscall on Windows for two, well-known cases, returns syscall.ENOSYS for all others.
    25  // If r contains fd 0, and it is a regular file, then it immediately returns 1 (data ready on stdin)
    26  // and r will have the fd 0 bit set.
    27  // If r contains fd 0, and it is a FILE_TYPE_CHAR, then it invokes PeekNamedPipe to check the buffer for input;
    28  // if there is data ready, then it returns 1 and r will have fd 0 bit set.
    29  // If n==0 it will wait for the given timeout duration, but it will return syscall.ENOSYS if timeout is nil,
    30  // i.e. it won't block indefinitely.
    31  //
    32  // Note: idea taken from https://stackoverflow.com/questions/6839508/test-if-stdin-has-input-for-c-windows-and-or-linux
    33  // PeekNamedPipe: https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
    34  // "GetFileType can assist in determining what device type the handle refers to. A console handle presents as FILE_TYPE_CHAR."
    35  // https://learn.microsoft.com/en-us/windows/console/console-handles
    36  func syscall_select(n int, r, w, e *platform.FdSet, timeout *time.Duration) (int, error) {
    37  	if n == 0 {
    38  		// Don't block indefinitely.
    39  		if timeout == nil {
    40  			return -1, syscall.ENOSYS
    41  		}
    42  		time.Sleep(*timeout)
    43  		return 0, nil
    44  	}
    45  	if r.IsSet(wasiFdStdin) {
    46  		fileType, err := syscall.GetFileType(syscall.Stdin)
    47  		if err != nil {
    48  			return 0, err
    49  		}
    50  		if fileType&syscall.FILE_TYPE_CHAR != 0 {
    51  			res, err := pollNamedPipe(context.TODO(), syscall.Stdin, timeout)
    52  			if err != nil {
    53  				return -1, err
    54  			}
    55  			if !res {
    56  				r.Zero()
    57  				return 0, nil
    58  			}
    59  		}
    60  		r.Zero()
    61  		r.Set(wasiFdStdin)
    62  		return 1, nil
    63  	}
    64  	return -1, syscall.ENOSYS
    65  }
    66  
    67  // pollNamedPipe polls the given named pipe handle for the given duration.
    68  //
    69  // The implementation actually polls every 100 milliseconds until it reaches the given duration.
    70  // The duration may be nil, in which case it will wait undefinely. The given ctx is
    71  // used to allow for cancellation. Currently used only in tests.
    72  func pollNamedPipe(ctx context.Context, pipeHandle syscall.Handle, duration *time.Duration) (bool, error) {
    73  	// Short circuit when the duration is zero.
    74  	if duration != nil && *duration == time.Duration(0) {
    75  		return peekNamedPipe(pipeHandle)
    76  	}
    77  
    78  	// Ticker that emits at every pollInterval.
    79  	tick := time.NewTicker(pollInterval)
    80  	tichCh := tick.C
    81  	defer tick.Stop()
    82  
    83  	// Timer that expires after the given duration.
    84  	// Initialize afterCh as nil: the select below will wait forever.
    85  	var afterCh <-chan time.Time
    86  	if duration != nil {
    87  		// If duration is not nil, instantiate the timer.
    88  		after := time.NewTimer(*duration)
    89  		defer after.Stop()
    90  		afterCh = after.C
    91  	}
    92  
    93  	for {
    94  		select {
    95  		case <-ctx.Done():
    96  			return false, nil
    97  		case <-afterCh:
    98  			return false, nil
    99  		case <-tichCh:
   100  			res, err := peekNamedPipe(pipeHandle)
   101  			if err != nil {
   102  				return false, err
   103  			}
   104  			if res {
   105  				return true, nil
   106  			}
   107  		}
   108  	}
   109  }
   110  
   111  // peekNamedPipe partially exposes PeekNamedPipe from the Win32 API
   112  // see https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
   113  func peekNamedPipe(handle syscall.Handle) (bool, error) {
   114  	var totalBytesAvail uint32
   115  	totalBytesPtr := unsafe.Pointer(&totalBytesAvail)
   116  	_, _, err := procPeekNamedPipe.Call(
   117  		uintptr(handle),        // [in]            HANDLE  hNamedPipe,
   118  		0,                      // [out, optional] LPVOID  lpBuffer,
   119  		0,                      // [in]            DWORD   nBufferSize,
   120  		0,                      // [out, optional] LPDWORD lpBytesRead
   121  		uintptr(totalBytesPtr), // [out, optional] LPDWORD lpTotalBytesAvail,
   122  		0)                      // [out, optional] LPDWORD lpBytesLeftThisMessage
   123  	if err == syscall.Errno(0) {
   124  		return totalBytesAvail > 0, nil
   125  	}
   126  	return totalBytesAvail > 0, err
   127  }