okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Ayman Bagabasfeat: add new poll reader for cross-platform I/O polling (#71)04a6149

main
5.6 KiB217 linesraw
1//go:build windows
2// +build windows
3
4package uv
5
6import (
7	"fmt"
8	"io"
9	"os"
10	"sync"
11	"time"
12
13	"golang.org/x/sys/windows"
14)
15
16var fileShareValidFlags uint32 = windows.FILE_SHARE_DELETE | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_READ
17
18// newPollReader creates a new pollReader for the given io.Reader.
19func newPollReader(reader io.Reader) (pollReader, error) {
20	f, ok := reader.(File)
21	if !ok || f.Fd() != os.Stdin.Fd() {
22		return newFallbackReader(reader)
23	}
24
25	// it is necessary to open CONIN$ (NOT windows.STD_INPUT_HANDLE) in
26	// overlapped mode to be able to use it with WaitForMultipleObjects.
27	coninPath, err := windows.UTF16PtrFromString("CONIN$")
28	if err != nil {
29		return nil, fmt.Errorf("convert CONIN$ to UTF16: %w", err)
30	}
31
32	conin, err := windows.CreateFile(
33		coninPath,
34		windows.GENERIC_READ|windows.GENERIC_WRITE,
35		fileShareValidFlags,
36		nil,
37		windows.OPEN_EXISTING,
38		windows.FILE_FLAG_OVERLAPPED,
39		0)
40	if err != nil {
41		return nil, fmt.Errorf("open CONIN$ in overlapping mode: %w", err)
42	}
43
44	resetConsole, err := preparePollConsole(conin)
45	if err != nil {
46		_ = windows.CloseHandle(conin)
47		return nil, fmt.Errorf("prepare console: %w", err)
48	}
49
50	// flush input, otherwise it can contain events which trigger
51	// WaitForMultipleObjects but which ReadFile cannot read, resulting in an
52	// un-cancelable read
53	err = windows.FlushConsoleInputBuffer(conin)
54	if err != nil {
55		_ = windows.CloseHandle(conin)
56		return nil, fmt.Errorf("flush console input buffer: %w", err)
57	}
58
59	cancelEvent, err := windows.CreateEvent(nil, 0, 0, nil)
60	if err != nil {
61		_ = windows.CloseHandle(conin)
62		return nil, fmt.Errorf("create cancel event: %w", err)
63	}
64
65	return &conReader{
66		reader:             reader,
67		conin:              conin,
68		cancelEvent:        cancelEvent,
69		resetConsole:       resetConsole,
70		blockingReadSignal: make(chan struct{}, 1),
71	}, nil
72}
73
74// conReader implements pollReader using Windows I/O and Console APIs.
75type conReader struct {
76	reader             io.Reader
77	conin              windows.Handle
78	cancelEvent        windows.Handle
79	resetConsole       func() error
80	blockingReadSignal chan struct{}
81	mu                 sync.Mutex
82	canceled           bool
83}
84
85// Read reads data from the underlying reader.
86func (r *conReader) Read(p []byte) (int, error) {
87	r.mu.Lock()
88	if r.canceled {
89		r.mu.Unlock()
90		return 0, ErrCanceled
91	}
92	r.mu.Unlock()
93
94	return r.reader.Read(p)
95}
96
97// Poll waits for data to be available to read with the given timeout.
98func (r *conReader) Poll(timeout time.Duration) (bool, error) {
99	r.mu.Lock()
100	if r.canceled {
101		r.mu.Unlock()
102		return false, ErrCanceled
103	}
104	r.mu.Unlock()
105
106	timeoutMs := uint32(windows.INFINITE)
107	if timeout >= 0 {
108		timeoutMs = uint32(timeout.Milliseconds())
109	}
110
111	event, err := windows.WaitForMultipleObjects([]windows.Handle{r.conin, r.cancelEvent}, false, timeoutMs)
112	switch {
113	case windows.WAIT_OBJECT_0 <= event && event < windows.WAIT_OBJECT_0+2:
114		if event == windows.WAIT_OBJECT_0+1 {
115			return false, ErrCanceled
116		}
117
118		if event == windows.WAIT_OBJECT_0 {
119			return true, nil
120		}
121
122		return false, fmt.Errorf("unexpected wait object is ready: %d", event-windows.WAIT_OBJECT_0)
123	case windows.WAIT_ABANDONED <= event && event < windows.WAIT_ABANDONED+2:
124		return false, fmt.Errorf("abandoned")
125	case event == uint32(windows.WAIT_TIMEOUT):
126		return false, nil
127	case event == windows.WAIT_FAILED:
128		return false, fmt.Errorf("failed")
129	default:
130		return false, fmt.Errorf("unexpected error: %w", error(err))
131	}
132}
133
134// Cancel cancels any ongoing poll or read operations.
135// On Windows Terminal, WaitForMultipleObjects sometimes immediately returns
136// without input being available. In this case, graceful cancelation is not
137// possible and Cancel() returns false.
138func (r *conReader) Cancel() bool {
139	r.mu.Lock()
140	r.canceled = true
141	r.mu.Unlock()
142
143	select {
144	case r.blockingReadSignal <- struct{}{}:
145		err := windows.SetEvent(r.cancelEvent)
146		if err != nil {
147			return false
148		}
149		<-r.blockingReadSignal
150	case <-time.After(100 * time.Millisecond):
151		// Read() hangs in a GetOverlappedResult which is likely due to
152		// WaitForMultipleObjects returning without input being available
153		// so we cannot cancel this ongoing read.
154		return false
155	}
156
157	return true
158}
159
160// Close closes the reader and releases any resources.
161func (r *conReader) Close() error {
162	err := windows.CloseHandle(r.cancelEvent)
163	if err != nil {
164		return fmt.Errorf("closing cancel event handle: %w", err)
165	}
166
167	err = r.resetConsole()
168	if err != nil {
169		return err
170	}
171
172	err = windows.CloseHandle(r.conin)
173	if err != nil {
174		return fmt.Errorf("closing CONIN$: %w", err)
175	}
176
177	return nil
178}
179
180func preparePollConsole(input windows.Handle) (reset func() error, err error) {
181	var originalMode uint32
182
183	err = windows.GetConsoleMode(input, &originalMode)
184	if err != nil {
185		return nil, fmt.Errorf("get console mode: %w", err)
186	}
187
188	var newMode uint32
189	newMode &^= windows.ENABLE_ECHO_INPUT
190	newMode &^= windows.ENABLE_LINE_INPUT
191	newMode &^= windows.ENABLE_MOUSE_INPUT
192	newMode &^= windows.ENABLE_PROCESSED_INPUT
193
194	newMode |= windows.ENABLE_EXTENDED_FLAGS
195	newMode |= windows.ENABLE_INSERT_MODE
196	newMode |= windows.ENABLE_QUICK_EDIT_MODE
197	newMode |= windows.ENABLE_WINDOW_INPUT // Enable window input events
198
199	// Enabling virtual terminal input is necessary for processing certain
200	// types of input like X10 mouse events and arrows keys with the current
201	// bytes-based input reader.
202	newMode |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT
203
204	err = windows.SetConsoleMode(input, newMode)
205	if err != nil {
206		return nil, fmt.Errorf("set console mode: %w", err)
207	}
208
209	return func() error {
210		err := windows.SetConsoleMode(input, originalMode)
211		if err != nil {
212			return fmt.Errorf("reset console mode: %w", err)
213		}
214
215		return nil
216	}, nil
217}