1//go:build windows
2
3// Windows backend based on ReadDirectoryChangesW()
4//
5// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
6
7package fsnotify
8
9import (
10 "errors"
11 "fmt"
12 "os"
13 "path/filepath"
14 "reflect"
15 "runtime"
16 "strings"
17 "sync"
18 "time"
19 "unsafe"
20
21 "github.com/fsnotify/fsnotify/internal"
22 "golang.org/x/sys/windows"
23)
24
25type readDirChangesW struct {
26 Events chan Event
27 Errors chan error
28
29 port windows.Handle // Handle to completion port
30 input chan *input // Inputs to the reader are sent on this channel
31 quit chan chan<- error
32
33 mu sync.Mutex // Protects access to watches, closed
34 watches watchMap // Map of watches (key: i-number)
35 closed bool // Set to true when Close() is first called
36}
37
38func newBackend(ev chan Event, errs chan error) (backend, error) {
39 return newBufferedBackend(50, ev, errs)
40}
41
42func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) {
43 port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
44 if err != nil {
45 return nil, os.NewSyscallError("CreateIoCompletionPort", err)
46 }
47 w := &readDirChangesW{
48 Events: ev,
49 Errors: errs,
50 port: port,
51 watches: make(watchMap),
52 input: make(chan *input, 1),
53 quit: make(chan chan<- error, 1),
54 }
55 go w.readEvents()
56 return w, nil
57}
58
59func (w *readDirChangesW) isClosed() bool {
60 w.mu.Lock()
61 defer w.mu.Unlock()
62 return w.closed
63}
64
65func (w *readDirChangesW) sendEvent(name, renamedFrom string, mask uint64) bool {
66 if mask == 0 {
67 return false
68 }
69
70 event := w.newEvent(name, uint32(mask))
71 event.renamedFrom = renamedFrom
72 select {
73 case ch := <-w.quit:
74 w.quit <- ch
75 case w.Events <- event:
76 }
77 return true
78}
79
80// Returns true if the error was sent, or false if watcher is closed.
81func (w *readDirChangesW) sendError(err error) bool {
82 if err == nil {
83 return true
84 }
85 select {
86 case w.Errors <- err:
87 return true
88 case <-w.quit:
89 return false
90 }
91}
92
93func (w *readDirChangesW) Close() error {
94 if w.isClosed() {
95 return nil
96 }
97
98 w.mu.Lock()
99 w.closed = true
100 w.mu.Unlock()
101
102 // Send "quit" message to the reader goroutine
103 ch := make(chan error)
104 w.quit <- ch
105 if err := w.wakeupReader(); err != nil {
106 return err
107 }
108 return <-ch
109}
110
111func (w *readDirChangesW) Add(name string) error { return w.AddWith(name) }
112
113func (w *readDirChangesW) AddWith(name string, opts ...addOpt) error {
114 if w.isClosed() {
115 return ErrClosed
116 }
117 if debug {
118 fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n",
119 time.Now().Format("15:04:05.000000000"), filepath.ToSlash(name))
120 }
121
122 with := getOptions(opts...)
123 if !w.xSupports(with.op) {
124 return fmt.Errorf("%w: %s", xErrUnsupported, with.op)
125 }
126 if with.bufsize < 4096 {
127 return fmt.Errorf("fsnotify.WithBufferSize: buffer size cannot be smaller than 4096 bytes")
128 }
129
130 in := &input{
131 op: opAddWatch,
132 path: filepath.Clean(name),
133 flags: sysFSALLEVENTS,
134 reply: make(chan error),
135 bufsize: with.bufsize,
136 }
137 w.input <- in
138 if err := w.wakeupReader(); err != nil {
139 return err
140 }
141 return <-in.reply
142}
143
144func (w *readDirChangesW) Remove(name string) error {
145 if w.isClosed() {
146 return nil
147 }
148 if debug {
149 fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n",
150 time.Now().Format("15:04:05.000000000"), filepath.ToSlash(name))
151 }
152
153 in := &input{
154 op: opRemoveWatch,
155 path: filepath.Clean(name),
156 reply: make(chan error),
157 }
158 w.input <- in
159 if err := w.wakeupReader(); err != nil {
160 return err
161 }
162 return <-in.reply
163}
164
165func (w *readDirChangesW) WatchList() []string {
166 if w.isClosed() {
167 return nil
168 }
169
170 w.mu.Lock()
171 defer w.mu.Unlock()
172
173 entries := make([]string, 0, len(w.watches))
174 for _, entry := range w.watches {
175 for _, watchEntry := range entry {
176 for name := range watchEntry.names {
177 entries = append(entries, filepath.Join(watchEntry.path, name))
178 }
179 // the directory itself is being watched
180 if watchEntry.mask != 0 {
181 entries = append(entries, watchEntry.path)
182 }
183 }
184 }
185
186 return entries
187}
188
189// These options are from the old golang.org/x/exp/winfsnotify, where you could
190// add various options to the watch. This has long since been removed.
191//
192// The "sys" in the name is misleading as they're not part of any "system".
193//
194// This should all be removed at some point, and just use windows.FILE_NOTIFY_*
195const (
196 sysFSALLEVENTS = 0xfff
197 sysFSCREATE = 0x100
198 sysFSDELETE = 0x200
199 sysFSDELETESELF = 0x400
200 sysFSMODIFY = 0x2
201 sysFSMOVE = 0xc0
202 sysFSMOVEDFROM = 0x40
203 sysFSMOVEDTO = 0x80
204 sysFSMOVESELF = 0x800
205 sysFSIGNORED = 0x8000
206)
207
208func (w *readDirChangesW) newEvent(name string, mask uint32) Event {
209 e := Event{Name: name}
210 if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO {
211 e.Op |= Create
212 }
213 if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF {
214 e.Op |= Remove
215 }
216 if mask&sysFSMODIFY == sysFSMODIFY {
217 e.Op |= Write
218 }
219 if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM {
220 e.Op |= Rename
221 }
222 return e
223}
224
225const (
226 opAddWatch = iota
227 opRemoveWatch
228)
229
230const (
231 provisional uint64 = 1 << (32 + iota)
232)
233
234type input struct {
235 op int
236 path string
237 flags uint32
238 bufsize int
239 reply chan error
240}
241
242type inode struct {
243 handle windows.Handle
244 volume uint32
245 index uint64
246}
247
248type watch struct {
249 ov windows.Overlapped
250 ino *inode // i-number
251 recurse bool // Recursive watch?
252 path string // Directory path
253 mask uint64 // Directory itself is being watched with these notify flags
254 names map[string]uint64 // Map of names being watched and their notify flags
255 rename string // Remembers the old name while renaming a file
256 buf []byte // buffer, allocated later
257}
258
259type (
260 indexMap map[uint64]*watch
261 watchMap map[uint32]indexMap
262)
263
264func (w *readDirChangesW) wakeupReader() error {
265 err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil)
266 if err != nil {
267 return os.NewSyscallError("PostQueuedCompletionStatus", err)
268 }
269 return nil
270}
271
272func (w *readDirChangesW) getDir(pathname string) (dir string, err error) {
273 attr, err := windows.GetFileAttributes(windows.StringToUTF16Ptr(pathname))
274 if err != nil {
275 return "", os.NewSyscallError("GetFileAttributes", err)
276 }
277 if attr&windows.FILE_ATTRIBUTE_DIRECTORY != 0 {
278 dir = pathname
279 } else {
280 dir, _ = filepath.Split(pathname)
281 dir = filepath.Clean(dir)
282 }
283 return
284}
285
286func (w *readDirChangesW) getIno(path string) (ino *inode, err error) {
287 h, err := windows.CreateFile(windows.StringToUTF16Ptr(path),
288 windows.FILE_LIST_DIRECTORY,
289 windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
290 nil, windows.OPEN_EXISTING,
291 windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED, 0)
292 if err != nil {
293 return nil, os.NewSyscallError("CreateFile", err)
294 }
295
296 var fi windows.ByHandleFileInformation
297 err = windows.GetFileInformationByHandle(h, &fi)
298 if err != nil {
299 windows.CloseHandle(h)
300 return nil, os.NewSyscallError("GetFileInformationByHandle", err)
301 }
302 ino = &inode{
303 handle: h,
304 volume: fi.VolumeSerialNumber,
305 index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow),
306 }
307 return ino, nil
308}
309
310// Must run within the I/O thread.
311func (m watchMap) get(ino *inode) *watch {
312 if i := m[ino.volume]; i != nil {
313 return i[ino.index]
314 }
315 return nil
316}
317
318// Must run within the I/O thread.
319func (m watchMap) set(ino *inode, watch *watch) {
320 i := m[ino.volume]
321 if i == nil {
322 i = make(indexMap)
323 m[ino.volume] = i
324 }
325 i[ino.index] = watch
326}
327
328// Must run within the I/O thread.
329func (w *readDirChangesW) addWatch(pathname string, flags uint64, bufsize int) error {
330 pathname, recurse := recursivePath(pathname)
331
332 dir, err := w.getDir(pathname)
333 if err != nil {
334 return err
335 }
336
337 ino, err := w.getIno(dir)
338 if err != nil {
339 return err
340 }
341 w.mu.Lock()
342 watchEntry := w.watches.get(ino)
343 w.mu.Unlock()
344 if watchEntry == nil {
345 _, err := windows.CreateIoCompletionPort(ino.handle, w.port, 0, 0)
346 if err != nil {
347 windows.CloseHandle(ino.handle)
348 return os.NewSyscallError("CreateIoCompletionPort", err)
349 }
350 watchEntry = &watch{
351 ino: ino,
352 path: dir,
353 names: make(map[string]uint64),
354 recurse: recurse,
355 buf: make([]byte, bufsize),
356 }
357 w.mu.Lock()
358 w.watches.set(ino, watchEntry)
359 w.mu.Unlock()
360 flags |= provisional
361 } else {
362 windows.CloseHandle(ino.handle)
363 }
364 if pathname == dir {
365 watchEntry.mask |= flags
366 } else {
367 watchEntry.names[filepath.Base(pathname)] |= flags
368 }
369
370 err = w.startRead(watchEntry)
371 if err != nil {
372 return err
373 }
374
375 if pathname == dir {
376 watchEntry.mask &= ^provisional
377 } else {
378 watchEntry.names[filepath.Base(pathname)] &= ^provisional
379 }
380 return nil
381}
382
383// Must run within the I/O thread.
384func (w *readDirChangesW) remWatch(pathname string) error {
385 pathname, recurse := recursivePath(pathname)
386
387 dir, err := w.getDir(pathname)
388 if err != nil {
389 return err
390 }
391 ino, err := w.getIno(dir)
392 if err != nil {
393 return err
394 }
395
396 w.mu.Lock()
397 watch := w.watches.get(ino)
398 w.mu.Unlock()
399
400 if recurse && !watch.recurse {
401 return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname)
402 }
403
404 err = windows.CloseHandle(ino.handle)
405 if err != nil {
406 w.sendError(os.NewSyscallError("CloseHandle", err))
407 }
408 if watch == nil {
409 return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname)
410 }
411 if pathname == dir {
412 w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED)
413 watch.mask = 0
414 } else {
415 name := filepath.Base(pathname)
416 w.sendEvent(filepath.Join(watch.path, name), "", watch.names[name]&sysFSIGNORED)
417 delete(watch.names, name)
418 }
419
420 return w.startRead(watch)
421}
422
423// Must run within the I/O thread.
424func (w *readDirChangesW) deleteWatch(watch *watch) {
425 for name, mask := range watch.names {
426 if mask&provisional == 0 {
427 w.sendEvent(filepath.Join(watch.path, name), "", mask&sysFSIGNORED)
428 }
429 delete(watch.names, name)
430 }
431 if watch.mask != 0 {
432 if watch.mask&provisional == 0 {
433 w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED)
434 }
435 watch.mask = 0
436 }
437}
438
439// Must run within the I/O thread.
440func (w *readDirChangesW) startRead(watch *watch) error {
441 err := windows.CancelIo(watch.ino.handle)
442 if err != nil {
443 w.sendError(os.NewSyscallError("CancelIo", err))
444 w.deleteWatch(watch)
445 }
446 mask := w.toWindowsFlags(watch.mask)
447 for _, m := range watch.names {
448 mask |= w.toWindowsFlags(m)
449 }
450 if mask == 0 {
451 err := windows.CloseHandle(watch.ino.handle)
452 if err != nil {
453 w.sendError(os.NewSyscallError("CloseHandle", err))
454 }
455 w.mu.Lock()
456 delete(w.watches[watch.ino.volume], watch.ino.index)
457 w.mu.Unlock()
458 return nil
459 }
460
461 // We need to pass the array, rather than the slice.
462 hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf))
463 rdErr := windows.ReadDirectoryChanges(watch.ino.handle,
464 (*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len),
465 watch.recurse, mask, nil, &watch.ov, 0)
466 if rdErr != nil {
467 err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
468 if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
469 // Watched directory was probably removed
470 w.sendEvent(watch.path, "", watch.mask&sysFSDELETESELF)
471 err = nil
472 }
473 w.deleteWatch(watch)
474 w.startRead(watch)
475 return err
476 }
477 return nil
478}
479
480// readEvents reads from the I/O completion port, converts the
481// received events into Event objects and sends them via the Events channel.
482// Entry point to the I/O thread.
483func (w *readDirChangesW) readEvents() {
484 var (
485 n uint32
486 key uintptr
487 ov *windows.Overlapped
488 )
489 runtime.LockOSThread()
490
491 for {
492 // This error is handled after the watch == nil check below.
493 qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE)
494
495 watch := (*watch)(unsafe.Pointer(ov))
496 if watch == nil {
497 select {
498 case ch := <-w.quit:
499 w.mu.Lock()
500 var indexes []indexMap
501 for _, index := range w.watches {
502 indexes = append(indexes, index)
503 }
504 w.mu.Unlock()
505 for _, index := range indexes {
506 for _, watch := range index {
507 w.deleteWatch(watch)
508 w.startRead(watch)
509 }
510 }
511
512 err := windows.CloseHandle(w.port)
513 if err != nil {
514 err = os.NewSyscallError("CloseHandle", err)
515 }
516 close(w.Events)
517 close(w.Errors)
518 ch <- err
519 return
520 case in := <-w.input:
521 switch in.op {
522 case opAddWatch:
523 in.reply <- w.addWatch(in.path, uint64(in.flags), in.bufsize)
524 case opRemoveWatch:
525 in.reply <- w.remWatch(in.path)
526 }
527 default:
528 }
529 continue
530 }
531
532 switch qErr {
533 case nil:
534 // No error
535 case windows.ERROR_MORE_DATA:
536 if watch == nil {
537 w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer"))
538 } else {
539 // The i/o succeeded but the buffer is full.
540 // In theory we should be building up a full packet.
541 // In practice we can get away with just carrying on.
542 n = uint32(unsafe.Sizeof(watch.buf))
543 }
544 case windows.ERROR_ACCESS_DENIED:
545 // Watched directory was probably removed
546 w.sendEvent(watch.path, "", watch.mask&sysFSDELETESELF)
547 w.deleteWatch(watch)
548 w.startRead(watch)
549 continue
550 case windows.ERROR_OPERATION_ABORTED:
551 // CancelIo was called on this handle
552 continue
553 default:
554 w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr))
555 continue
556 }
557
558 var offset uint32
559 for {
560 if n == 0 {
561 w.sendError(ErrEventOverflow)
562 break
563 }
564
565 // Point "raw" to the event in the buffer
566 raw := (*windows.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset]))
567
568 // Create a buf that is the size of the path name
569 size := int(raw.FileNameLength / 2)
570 var buf []uint16
571 // TODO: Use unsafe.Slice in Go 1.17; https://stackoverflow.com/questions/51187973
572 sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
573 sh.Data = uintptr(unsafe.Pointer(&raw.FileName))
574 sh.Len = size
575 sh.Cap = size
576 name := windows.UTF16ToString(buf)
577 fullname := filepath.Join(watch.path, name)
578
579 if debug {
580 internal.Debug(fullname, raw.Action)
581 }
582
583 var mask uint64
584 switch raw.Action {
585 case windows.FILE_ACTION_REMOVED:
586 mask = sysFSDELETESELF
587 case windows.FILE_ACTION_MODIFIED:
588 mask = sysFSMODIFY
589 case windows.FILE_ACTION_RENAMED_OLD_NAME:
590 watch.rename = name
591 case windows.FILE_ACTION_RENAMED_NEW_NAME:
592 // Update saved path of all sub-watches.
593 old := filepath.Join(watch.path, watch.rename)
594 w.mu.Lock()
595 for _, watchMap := range w.watches {
596 for _, ww := range watchMap {
597 if strings.HasPrefix(ww.path, old) {
598 ww.path = filepath.Join(fullname, strings.TrimPrefix(ww.path, old))
599 }
600 }
601 }
602 w.mu.Unlock()
603
604 if watch.names[watch.rename] != 0 {
605 watch.names[name] |= watch.names[watch.rename]
606 delete(watch.names, watch.rename)
607 mask = sysFSMOVESELF
608 }
609 }
610
611 if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME {
612 w.sendEvent(fullname, "", watch.names[name]&mask)
613 }
614 if raw.Action == windows.FILE_ACTION_REMOVED {
615 w.sendEvent(fullname, "", watch.names[name]&sysFSIGNORED)
616 delete(watch.names, name)
617 }
618
619 if watch.rename != "" && raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME {
620 w.sendEvent(fullname, filepath.Join(watch.path, watch.rename), watch.mask&w.toFSnotifyFlags(raw.Action))
621 } else {
622 w.sendEvent(fullname, "", watch.mask&w.toFSnotifyFlags(raw.Action))
623 }
624
625 if raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME {
626 w.sendEvent(filepath.Join(watch.path, watch.rename), "", watch.names[name]&mask)
627 }
628
629 // Move to the next event in the buffer
630 if raw.NextEntryOffset == 0 {
631 break
632 }
633 offset += raw.NextEntryOffset
634
635 // Error!
636 if offset >= n {
637 //lint:ignore ST1005 Windows should be capitalized
638 w.sendError(errors.New("Windows system assumed buffer larger than it is, events have likely been missed"))
639 break
640 }
641 }
642
643 if err := w.startRead(watch); err != nil {
644 w.sendError(err)
645 }
646 }
647}
648
649func (w *readDirChangesW) toWindowsFlags(mask uint64) uint32 {
650 var m uint32
651 if mask&sysFSMODIFY != 0 {
652 m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE
653 }
654 if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 {
655 m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME
656 }
657 return m
658}
659
660func (w *readDirChangesW) toFSnotifyFlags(action uint32) uint64 {
661 switch action {
662 case windows.FILE_ACTION_ADDED:
663 return sysFSCREATE
664 case windows.FILE_ACTION_REMOVED:
665 return sysFSDELETE
666 case windows.FILE_ACTION_MODIFIED:
667 return sysFSMODIFY
668 case windows.FILE_ACTION_RENAMED_OLD_NAME:
669 return sysFSMOVEDFROM
670 case windows.FILE_ACTION_RENAMED_NEW_NAME:
671 return sysFSMOVEDTO
672 }
673 return 0
674}
675
676func (w *readDirChangesW) xSupports(op Op) bool {
677 if op.Has(xUnportableOpen) || op.Has(xUnportableRead) ||
678 op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) {
679 return false
680 }
681 return true
682}