backend_windows.go

  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}