okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
04a6149
upstream
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 ) { 20f ,ok := reader .(File ) 21if ! ok || f .Fd ()!= os .Stdin .Fd () { 22return 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. 27coninPath ,err := windows .UTF16PtrFromString ("CONIN$" ) 28if err != nil { 29return nil ,fmt .Errorf ("convert CONIN$ to UTF16: %w" ,err ) 30 } 31 32conin ,err := windows .CreateFile ( 33coninPath , 34windows .GENERIC_READ | windows .GENERIC_WRITE , 35fileShareValidFlags , 36nil , 37windows .OPEN_EXISTING , 38windows .FILE_FLAG_OVERLAPPED , 390 ) 40if err != nil { 41return nil ,fmt .Errorf ("open CONIN$ in overlapping mode: %w" ,err ) 42 } 43 44resetConsole ,err := preparePollConsole (conin ) 45if err != nil { 46_ = windows .CloseHandle (conin ) 47return 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 53err = windows .FlushConsoleInputBuffer (conin ) 54if err != nil { 55_ = windows .CloseHandle (conin ) 56return nil ,fmt .Errorf ("flush console input buffer: %w" ,err ) 57 } 58 59cancelEvent ,err := windows .CreateEvent (nil ,0 ,0 ,nil ) 60if err != nil { 61_ = windows .CloseHandle (conin ) 62return nil ,fmt .Errorf ("create cancel event: %w" ,err ) 63 } 64 65return & conReader { 66reader :reader , 67conin :conin , 68cancelEvent :cancelEvent , 69resetConsole :resetConsole , 70blockingReadSignal :make (chan struct {},1 ), 71 },nil 72} 73 74// conReader implements pollReader using Windows I/O and Console APIs. 75type conReader struct { 76reader io.Reader 77conin windows.Handle 78cancelEvent windows.Handle 79resetConsole func ()error 80blockingReadSignal chan struct {} 81mu sync.Mutex 82canceled bool 83} 84 85// Read reads data from the underlying reader. 86func (r * conReader )Read (p []byte ) (int ,error ) { 87r .mu .Lock () 88if r .canceled { 89r .mu .Unlock () 90return 0 ,ErrCanceled 91 } 92r .mu .Unlock () 93 94return 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 ) { 99r .mu .Lock () 100if r .canceled { 101r .mu .Unlock () 102return false ,ErrCanceled 103 } 104r .mu .Unlock () 105 106timeoutMs := uint32 (windows .INFINITE ) 107if timeout >= 0 { 108timeoutMs = uint32 (timeout .Milliseconds ()) 109 } 110 111event ,err := windows .WaitForMultipleObjects ([]windows.Handle {r .conin ,r .cancelEvent },false ,timeoutMs ) 112switch { 113case windows .WAIT_OBJECT_0 <= event && event < windows .WAIT_OBJECT_0 + 2 : 114if event == windows .WAIT_OBJECT_0 + 1 { 115return false ,ErrCanceled 116 } 117 118if event == windows .WAIT_OBJECT_0 { 119return true ,nil 120 } 121 122return false ,fmt .Errorf ("unexpected wait object is ready: %d" ,event - windows .WAIT_OBJECT_0 ) 123case windows .WAIT_ABANDONED <= event && event < windows .WAIT_ABANDONED + 2 : 124return false ,fmt .Errorf ("abandoned" ) 125case event == uint32 (windows .WAIT_TIMEOUT ): 126return false ,nil 127case event == windows .WAIT_FAILED : 128return false ,fmt .Errorf ("failed" ) 129default : 130return 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 { 139r .mu .Lock () 140r .canceled = true 141r .mu .Unlock () 142 143select { 144case r .blockingReadSignal <- struct {}{}: 145err := windows .SetEvent (r .cancelEvent ) 146if err != nil { 147return false 148 } 149<- r .blockingReadSignal 150case <- 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. 154return false 155 } 156 157return true 158} 159 160// Close closes the reader and releases any resources. 161func (r * conReader )Close ()error { 162err := windows .CloseHandle (r .cancelEvent ) 163if err != nil { 164return fmt .Errorf ("closing cancel event handle: %w" ,err ) 165 } 166 167err = r .resetConsole () 168if err != nil { 169return err 170 } 171 172err = windows .CloseHandle (r .conin ) 173if err != nil { 174return fmt .Errorf ("closing CONIN$: %w" ,err ) 175 } 176 177return nil 178} 179 180func preparePollConsole (input windows.Handle ) (reset func ()error ,err error ) { 181var originalMode uint32 182 183err = windows .GetConsoleMode (input ,& originalMode ) 184if err != nil { 185return nil ,fmt .Errorf ("get console mode: %w" ,err ) 186 } 187 188var newMode uint32 189newMode &^=windows .ENABLE_ECHO_INPUT 190newMode &^=windows .ENABLE_LINE_INPUT 191newMode &^=windows .ENABLE_MOUSE_INPUT 192newMode &^=windows .ENABLE_PROCESSED_INPUT 193 194newMode |= windows .ENABLE_EXTENDED_FLAGS 195newMode |= windows .ENABLE_INSERT_MODE 196newMode |= windows .ENABLE_QUICK_EDIT_MODE 197newMode |= 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. 202newMode |= windows .ENABLE_VIRTUAL_TERMINAL_INPUT 203 204err = windows .SetConsoleMode (input ,newMode ) 205if err != nil { 206return nil ,fmt .Errorf ("set console mode: %w" ,err ) 207 } 208 209return func ()error { 210err := windows .SetConsoleMode (input ,originalMode ) 211if err != nil { 212return fmt .Errorf ("reset console mode: %w" ,err ) 213 } 214 215return nil 216 },nil 217}