github.com/Serizao/go-winio@v0.0.0-20230906082528-f02f7f4ad6e8/file.go (about) 1 //go:build windows 2 // +build windows 3 4 package winio 5 6 import ( 7 "errors" 8 "io" 9 "runtime" 10 "sync" 11 "sync/atomic" 12 "syscall" 13 "time" 14 15 "golang.org/x/sys/windows" 16 ) 17 18 //sys cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) = CancelIoEx 19 //sys createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) = CreateIoCompletionPort 20 //sys getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus 21 //sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes 22 //sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult 23 24 //todo (go1.19): switch to [atomic.Bool] 25 26 type atomicBool int32 27 28 func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 } 29 func (b *atomicBool) setFalse() { atomic.StoreInt32((*int32)(b), 0) } 30 func (b *atomicBool) setTrue() { atomic.StoreInt32((*int32)(b), 1) } 31 32 //revive:disable-next-line:predeclared Keep "new" to maintain consistency with "atomic" pkg 33 func (b *atomicBool) swap(new bool) bool { 34 var newInt int32 35 if new { 36 newInt = 1 37 } 38 return atomic.SwapInt32((*int32)(b), newInt) == 1 39 } 40 41 var ( 42 ErrFileClosed = errors.New("file has already been closed") 43 ErrTimeout = &timeoutError{} 44 ) 45 46 type timeoutError struct{} 47 48 func (*timeoutError) Error() string { return "i/o timeout" } 49 func (*timeoutError) Timeout() bool { return true } 50 func (*timeoutError) Temporary() bool { return true } 51 52 type timeoutChan chan struct{} 53 54 var ioInitOnce sync.Once 55 var ioCompletionPort windows.Handle 56 57 // ioResult contains the result of an asynchronous IO operation. 58 type ioResult struct { 59 bytes uint32 60 err error 61 } 62 63 // ioOperation represents an outstanding asynchronous Win32 IO. 64 type ioOperation struct { 65 o windows.Overlapped 66 ch chan ioResult 67 } 68 69 func initIO() { 70 h, err := createIoCompletionPort(windows.InvalidHandle, 0, 0, 0xffffffff) 71 if err != nil { 72 panic(err) 73 } 74 ioCompletionPort = h 75 go ioCompletionProcessor(h) 76 } 77 78 // win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall. 79 // It takes ownership of this handle and will close it if it is garbage collected. 80 type win32File struct { 81 handle windows.Handle 82 wg sync.WaitGroup 83 wgLock sync.RWMutex 84 closing atomicBool 85 socket bool 86 readDeadline deadlineHandler 87 writeDeadline deadlineHandler 88 } 89 90 type deadlineHandler struct { 91 setLock sync.Mutex 92 channel timeoutChan 93 channelLock sync.RWMutex 94 timer *time.Timer 95 timedout atomicBool 96 } 97 98 // makeWin32File makes a new win32File from an existing file handle. 99 func makeWin32File(h windows.Handle) (*win32File, error) { 100 f := &win32File{handle: h} 101 ioInitOnce.Do(initIO) 102 _, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff) 103 if err != nil { 104 return nil, err 105 } 106 err = setFileCompletionNotificationModes(h, windows.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS|windows.FILE_SKIP_SET_EVENT_ON_HANDLE) 107 if err != nil { 108 return nil, err 109 } 110 f.readDeadline.channel = make(timeoutChan) 111 f.writeDeadline.channel = make(timeoutChan) 112 return f, nil 113 } 114 115 // Deprecated: use NewOpenFile instead. 116 func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) { 117 return NewOpenFile(windows.Handle(h)) 118 } 119 120 func NewOpenFile(h windows.Handle) (io.ReadWriteCloser, error) { 121 // If we return the result of makeWin32File directly, it can result in an 122 // interface-wrapped nil, rather than a nil interface value. 123 f, err := makeWin32File(h) 124 if err != nil { 125 return nil, err 126 } 127 return f, nil 128 } 129 130 // closeHandle closes the resources associated with a Win32 handle. 131 func (f *win32File) closeHandle() { 132 f.wgLock.Lock() 133 // Atomically set that we are closing, releasing the resources only once. 134 if !f.closing.swap(true) { 135 f.wgLock.Unlock() 136 // cancel all IO and wait for it to complete 137 _ = cancelIoEx(f.handle, nil) 138 f.wg.Wait() 139 // at this point, no new IO can start 140 windows.Close(f.handle) 141 f.handle = 0 142 } else { 143 f.wgLock.Unlock() 144 } 145 } 146 147 // Close closes a win32File. 148 func (f *win32File) Close() error { 149 f.closeHandle() 150 return nil 151 } 152 153 // IsClosed checks if the file has been closed. 154 func (f *win32File) IsClosed() bool { 155 return f.closing.isSet() 156 } 157 158 // prepareIO prepares for a new IO operation. 159 // The caller must call f.wg.Done() when the IO is finished, prior to Close() returning. 160 func (f *win32File) prepareIO() (*ioOperation, error) { 161 f.wgLock.RLock() 162 if f.closing.isSet() { 163 f.wgLock.RUnlock() 164 return nil, ErrFileClosed 165 } 166 f.wg.Add(1) 167 f.wgLock.RUnlock() 168 c := &ioOperation{} 169 c.ch = make(chan ioResult) 170 return c, nil 171 } 172 173 // ioCompletionProcessor processes completed async IOs forever. 174 func ioCompletionProcessor(h windows.Handle) { 175 for { 176 var bytes uint32 177 var key uintptr 178 var op *ioOperation 179 err := getQueuedCompletionStatus(h, &bytes, &key, &op, windows.INFINITE) 180 if op == nil { 181 panic(err) 182 } 183 op.ch <- ioResult{bytes, err} 184 } 185 } 186 187 // todo: helsaawy - create an asyncIO version that takes a context 188 189 // asyncIO processes the return value from ReadFile or WriteFile, blocking until 190 // the operation has actually completed. 191 func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) { 192 if err != windows.ERROR_IO_PENDING { //nolint:errorlint // err is Errno 193 return int(bytes), err 194 } 195 196 if f.closing.isSet() { 197 _ = cancelIoEx(f.handle, &c.o) 198 } 199 200 var timeout timeoutChan 201 if d != nil { 202 d.channelLock.Lock() 203 timeout = d.channel 204 d.channelLock.Unlock() 205 } 206 207 var r ioResult 208 select { 209 case r = <-c.ch: 210 err = r.err 211 if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno 212 if f.closing.isSet() { 213 err = ErrFileClosed 214 } 215 } else if err != nil && f.socket { 216 // err is from Win32. Query the overlapped structure to get the winsock error. 217 var bytes, flags uint32 218 err = wsaGetOverlappedResult(f.handle, &c.o, &bytes, false, &flags) 219 } 220 case <-timeout: 221 _ = cancelIoEx(f.handle, &c.o) 222 r = <-c.ch 223 err = r.err 224 if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno 225 err = ErrTimeout 226 } 227 } 228 229 // runtime.KeepAlive is needed, as c is passed via native 230 // code to ioCompletionProcessor, c must remain alive 231 // until the channel read is complete. 232 // todo: (de)allocate *ioOperation via win32 heap functions, instead of needing to KeepAlive? 233 runtime.KeepAlive(c) 234 return int(r.bytes), err 235 } 236 237 // Read reads from a file handle. 238 func (f *win32File) Read(b []byte) (int, error) { 239 c, err := f.prepareIO() 240 if err != nil { 241 return 0, err 242 } 243 defer f.wg.Done() 244 245 if f.readDeadline.timedout.isSet() { 246 return 0, ErrTimeout 247 } 248 249 var bytes uint32 250 err = windows.ReadFile(f.handle, b, &bytes, &c.o) 251 n, err := f.asyncIO(c, &f.readDeadline, bytes, err) 252 runtime.KeepAlive(b) 253 254 // Handle EOF conditions. 255 if err == nil && n == 0 && len(b) != 0 { 256 return 0, io.EOF 257 } else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno 258 return 0, io.EOF 259 } else { 260 return n, err 261 } 262 } 263 264 // Write writes to a file handle. 265 func (f *win32File) Write(b []byte) (int, error) { 266 c, err := f.prepareIO() 267 if err != nil { 268 return 0, err 269 } 270 defer f.wg.Done() 271 272 if f.writeDeadline.timedout.isSet() { 273 return 0, ErrTimeout 274 } 275 276 var bytes uint32 277 err = windows.WriteFile(f.handle, b, &bytes, &c.o) 278 n, err := f.asyncIO(c, &f.writeDeadline, bytes, err) 279 runtime.KeepAlive(b) 280 return n, err 281 } 282 283 func (f *win32File) SetReadDeadline(deadline time.Time) error { 284 return f.readDeadline.set(deadline) 285 } 286 287 func (f *win32File) SetWriteDeadline(deadline time.Time) error { 288 return f.writeDeadline.set(deadline) 289 } 290 291 func (f *win32File) Flush() error { 292 return windows.FlushFileBuffers(f.handle) 293 } 294 295 func (f *win32File) Fd() uintptr { 296 return uintptr(f.handle) 297 } 298 299 func (d *deadlineHandler) set(deadline time.Time) error { 300 d.setLock.Lock() 301 defer d.setLock.Unlock() 302 303 if d.timer != nil { 304 if !d.timer.Stop() { 305 <-d.channel 306 } 307 d.timer = nil 308 } 309 d.timedout.setFalse() 310 311 select { 312 case <-d.channel: 313 d.channelLock.Lock() 314 d.channel = make(chan struct{}) 315 d.channelLock.Unlock() 316 default: 317 } 318 319 if deadline.IsZero() { 320 return nil 321 } 322 323 timeoutIO := func() { 324 d.timedout.setTrue() 325 close(d.channel) 326 } 327 328 now := time.Now() 329 duration := deadline.Sub(now) 330 if deadline.After(now) { 331 // Deadline is in the future, set a timer to wait 332 d.timer = time.AfterFunc(duration, timeoutIO) 333 } else { 334 // Deadline is in the past. Cancel all pending IO now. 335 timeoutIO() 336 } 337 return nil 338 }