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