| // Copyright 2011 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // +build windows |
| |
| // Package winfsnotify allows the user to receive |
| // file system event notifications on Windows. |
| package winfsnotify // import "golang.org/x/exp/winfsnotify" |
| |
| import ( |
| "errors" |
| "fmt" |
| "os" |
| "path/filepath" |
| "runtime" |
| "syscall" |
| "unsafe" |
| ) |
| |
| // Event is the type of the notification messages |
| // received on the watcher's Event channel. |
| type Event struct { |
| Mask uint32 // Mask of events |
| Cookie uint32 // Unique cookie associating related events (for rename) |
| Name string // File name (optional) |
| } |
| |
| const ( |
| opAddWatch = iota |
| opRemoveWatch |
| ) |
| |
| const ( |
| provisional uint64 = 1 << (32 + iota) |
| ) |
| |
| type input struct { |
| op int |
| path string |
| flags uint32 |
| reply chan error |
| } |
| |
| type inode struct { |
| handle syscall.Handle |
| volume uint32 |
| index uint64 |
| } |
| |
| type watch struct { |
| ov syscall.Overlapped |
| ino *inode // i-number |
| path string // Directory path |
| mask uint64 // Directory itself is being watched with these notify flags |
| names map[string]uint64 // Map of names being watched and their notify flags |
| rename string // Remembers the old name while renaming a file |
| buf [4096]byte |
| } |
| |
| type indexMap map[uint64]*watch |
| type watchMap map[uint32]indexMap |
| |
| // A Watcher waits for and receives event notifications |
| // for a specific set of files and directories. |
| type Watcher struct { |
| port syscall.Handle // Handle to completion port |
| watches watchMap // Map of watches (key: i-number) |
| input chan *input // Inputs to the reader are sent on this channel |
| Event chan *Event // Events are returned on this channel |
| Error chan error // Errors are sent on this channel |
| isClosed bool // Set to true when Close() is first called |
| quit chan chan<- error |
| cookie uint32 |
| } |
| |
| // NewWatcher creates and returns a Watcher. |
| func NewWatcher() (*Watcher, error) { |
| port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0) |
| if e != nil { |
| return nil, os.NewSyscallError("CreateIoCompletionPort", e) |
| } |
| w := &Watcher{ |
| port: port, |
| watches: make(watchMap), |
| input: make(chan *input, 1), |
| Event: make(chan *Event, 50), |
| Error: make(chan error), |
| quit: make(chan chan<- error, 1), |
| } |
| go w.readEvents() |
| return w, nil |
| } |
| |
| // Close closes a Watcher. |
| // It sends a message to the reader goroutine to quit and removes all watches |
| // associated with the watcher. |
| func (w *Watcher) Close() error { |
| if w.isClosed { |
| return nil |
| } |
| w.isClosed = true |
| |
| // Send "quit" message to the reader goroutine |
| ch := make(chan error) |
| w.quit <- ch |
| if err := w.wakeupReader(); err != nil { |
| return err |
| } |
| return <-ch |
| } |
| |
| // AddWatch adds path to the watched file set. |
| func (w *Watcher) AddWatch(path string, flags uint32) error { |
| if w.isClosed { |
| return errors.New("watcher already closed") |
| } |
| in := &input{ |
| op: opAddWatch, |
| path: filepath.Clean(path), |
| flags: flags, |
| reply: make(chan error), |
| } |
| w.input <- in |
| if err := w.wakeupReader(); err != nil { |
| return err |
| } |
| return <-in.reply |
| } |
| |
| // Watch adds path to the watched file set, watching all events. |
| func (w *Watcher) Watch(path string) error { |
| return w.AddWatch(path, FS_ALL_EVENTS) |
| } |
| |
| // RemoveWatch removes path from the watched file set. |
| func (w *Watcher) RemoveWatch(path string) error { |
| in := &input{ |
| op: opRemoveWatch, |
| path: filepath.Clean(path), |
| reply: make(chan error), |
| } |
| w.input <- in |
| if err := w.wakeupReader(); err != nil { |
| return err |
| } |
| return <-in.reply |
| } |
| |
| func (w *Watcher) wakeupReader() error { |
| e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil) |
| if e != nil { |
| return os.NewSyscallError("PostQueuedCompletionStatus", e) |
| } |
| return nil |
| } |
| |
| func getDir(pathname string) (dir string, err error) { |
| pathnamep, e := syscall.UTF16PtrFromString(pathname) |
| if e != nil { |
| return "", e |
| } |
| attr, e := syscall.GetFileAttributes(pathnamep) |
| if e != nil { |
| return "", os.NewSyscallError("GetFileAttributes", e) |
| } |
| if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { |
| dir = pathname |
| } else { |
| dir, _ = filepath.Split(pathname) |
| dir = filepath.Clean(dir) |
| } |
| return |
| } |
| |
| func getIno(path string) (ino *inode, err error) { |
| pathp, e := syscall.UTF16PtrFromString(path) |
| if e != nil { |
| return nil, e |
| } |
| h, e := syscall.CreateFile(pathp, |
| syscall.FILE_LIST_DIRECTORY, |
| syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, |
| nil, syscall.OPEN_EXISTING, |
| syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0) |
| if e != nil { |
| return nil, os.NewSyscallError("CreateFile", e) |
| } |
| var fi syscall.ByHandleFileInformation |
| if e = syscall.GetFileInformationByHandle(h, &fi); e != nil { |
| syscall.CloseHandle(h) |
| return nil, os.NewSyscallError("GetFileInformationByHandle", e) |
| } |
| ino = &inode{ |
| handle: h, |
| volume: fi.VolumeSerialNumber, |
| index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), |
| } |
| return ino, nil |
| } |
| |
| // Must run within the I/O thread. |
| func (m watchMap) get(ino *inode) *watch { |
| if i := m[ino.volume]; i != nil { |
| return i[ino.index] |
| } |
| return nil |
| } |
| |
| // Must run within the I/O thread. |
| func (m watchMap) set(ino *inode, watch *watch) { |
| i := m[ino.volume] |
| if i == nil { |
| i = make(indexMap) |
| m[ino.volume] = i |
| } |
| i[ino.index] = watch |
| } |
| |
| // Must run within the I/O thread. |
| func (w *Watcher) addWatch(pathname string, flags uint64) error { |
| dir, err := getDir(pathname) |
| if err != nil { |
| return err |
| } |
| if flags&FS_ONLYDIR != 0 && pathname != dir { |
| return nil |
| } |
| ino, err := getIno(dir) |
| if err != nil { |
| return err |
| } |
| watchEntry := w.watches.get(ino) |
| if watchEntry == nil { |
| if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil { |
| syscall.CloseHandle(ino.handle) |
| return os.NewSyscallError("CreateIoCompletionPort", e) |
| } |
| watchEntry = &watch{ |
| ino: ino, |
| path: dir, |
| names: make(map[string]uint64), |
| } |
| w.watches.set(ino, watchEntry) |
| flags |= provisional |
| } else { |
| syscall.CloseHandle(ino.handle) |
| } |
| if pathname == dir { |
| watchEntry.mask |= flags |
| } else { |
| watchEntry.names[filepath.Base(pathname)] |= flags |
| } |
| if err = w.startRead(watchEntry); err != nil { |
| return err |
| } |
| if pathname == dir { |
| watchEntry.mask &= ^provisional |
| } else { |
| watchEntry.names[filepath.Base(pathname)] &= ^provisional |
| } |
| return nil |
| } |
| |
| // Must run within the I/O thread. |
| func (w *Watcher) removeWatch(pathname string) error { |
| dir, err := getDir(pathname) |
| if err != nil { |
| return err |
| } |
| ino, err := getIno(dir) |
| if err != nil { |
| return err |
| } |
| watch := w.watches.get(ino) |
| if watch == nil { |
| return fmt.Errorf("can't remove non-existent watch for: %s", pathname) |
| } |
| if pathname == dir { |
| w.sendEvent(watch.path, watch.mask&FS_IGNORED) |
| watch.mask = 0 |
| } else { |
| name := filepath.Base(pathname) |
| w.sendEvent(watch.path+"/"+name, watch.names[name]&FS_IGNORED) |
| delete(watch.names, name) |
| } |
| return w.startRead(watch) |
| } |
| |
| // Must run within the I/O thread. |
| func (w *Watcher) deleteWatch(watch *watch) { |
| for name, mask := range watch.names { |
| if mask&provisional == 0 { |
| w.sendEvent(watch.path+"/"+name, mask&FS_IGNORED) |
| } |
| delete(watch.names, name) |
| } |
| if watch.mask != 0 { |
| if watch.mask&provisional == 0 { |
| w.sendEvent(watch.path, watch.mask&FS_IGNORED) |
| } |
| watch.mask = 0 |
| } |
| } |
| |
| // Must run within the I/O thread. |
| func (w *Watcher) startRead(watch *watch) error { |
| if e := syscall.CancelIo(watch.ino.handle); e != nil { |
| w.Error <- os.NewSyscallError("CancelIo", e) |
| w.deleteWatch(watch) |
| } |
| mask := toWindowsFlags(watch.mask) |
| for _, m := range watch.names { |
| mask |= toWindowsFlags(m) |
| } |
| if mask == 0 { |
| if e := syscall.CloseHandle(watch.ino.handle); e != nil { |
| w.Error <- os.NewSyscallError("CloseHandle", e) |
| } |
| delete(w.watches[watch.ino.volume], watch.ino.index) |
| return nil |
| } |
| e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], |
| uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) |
| if e != nil { |
| err := os.NewSyscallError("ReadDirectoryChanges", e) |
| if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { |
| // Watched directory was probably removed |
| if w.sendEvent(watch.path, watch.mask&FS_DELETE_SELF) { |
| if watch.mask&FS_ONESHOT != 0 { |
| watch.mask = 0 |
| } |
| } |
| err = nil |
| } |
| w.deleteWatch(watch) |
| w.startRead(watch) |
| return err |
| } |
| return nil |
| } |
| |
| // readEvents reads from the I/O completion port, converts the |
| // received events into Event objects and sends them via the Event channel. |
| // Entry point to the I/O thread. |
| func (w *Watcher) readEvents() { |
| var ( |
| n, key uint32 |
| ov *syscall.Overlapped |
| ) |
| runtime.LockOSThread() |
| |
| for { |
| e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE) |
| watch := (*watch)(unsafe.Pointer(ov)) |
| |
| if watch == nil { |
| select { |
| case ch := <-w.quit: |
| for _, index := range w.watches { |
| for _, watch := range index { |
| w.deleteWatch(watch) |
| w.startRead(watch) |
| } |
| } |
| var err error |
| if e := syscall.CloseHandle(w.port); e != nil { |
| err = os.NewSyscallError("CloseHandle", e) |
| } |
| close(w.Event) |
| close(w.Error) |
| ch <- err |
| return |
| case in := <-w.input: |
| switch in.op { |
| case opAddWatch: |
| in.reply <- w.addWatch(in.path, uint64(in.flags)) |
| case opRemoveWatch: |
| in.reply <- w.removeWatch(in.path) |
| } |
| default: |
| } |
| continue |
| } |
| |
| switch e { |
| case syscall.ERROR_ACCESS_DENIED: |
| // Watched directory was probably removed |
| w.sendEvent(watch.path, watch.mask&FS_DELETE_SELF) |
| w.deleteWatch(watch) |
| w.startRead(watch) |
| continue |
| case syscall.ERROR_OPERATION_ABORTED: |
| // CancelIo was called on this handle |
| continue |
| default: |
| w.Error <- os.NewSyscallError("GetQueuedCompletionPort", e) |
| continue |
| case nil: |
| } |
| |
| var offset uint32 |
| for { |
| if n == 0 { |
| w.Event <- &Event{Mask: FS_Q_OVERFLOW} |
| w.Error <- errors.New("short read in readEvents()") |
| break |
| } |
| |
| // Point "raw" to the event in the buffer |
| raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) |
| buf := (*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName)) |
| name := syscall.UTF16ToString(buf[:raw.FileNameLength/2]) |
| fullname := watch.path + "/" + name |
| |
| var mask uint64 |
| switch raw.Action { |
| case syscall.FILE_ACTION_REMOVED: |
| mask = FS_DELETE_SELF |
| case syscall.FILE_ACTION_MODIFIED: |
| mask = FS_MODIFY |
| case syscall.FILE_ACTION_RENAMED_OLD_NAME: |
| watch.rename = name |
| case syscall.FILE_ACTION_RENAMED_NEW_NAME: |
| if watch.names[watch.rename] != 0 { |
| watch.names[name] |= watch.names[watch.rename] |
| delete(watch.names, watch.rename) |
| mask = FS_MOVE_SELF |
| } |
| } |
| |
| sendNameEvent := func() { |
| if w.sendEvent(fullname, watch.names[name]&mask) { |
| if watch.names[name]&FS_ONESHOT != 0 { |
| delete(watch.names, name) |
| } |
| } |
| } |
| if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME { |
| sendNameEvent() |
| } |
| if raw.Action == syscall.FILE_ACTION_REMOVED { |
| w.sendEvent(fullname, watch.names[name]&FS_IGNORED) |
| delete(watch.names, name) |
| } |
| if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) { |
| if watch.mask&FS_ONESHOT != 0 { |
| watch.mask = 0 |
| } |
| } |
| if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME { |
| fullname = watch.path + "/" + watch.rename |
| sendNameEvent() |
| } |
| |
| // Move to the next event in the buffer |
| if raw.NextEntryOffset == 0 { |
| break |
| } |
| offset += raw.NextEntryOffset |
| } |
| |
| if err := w.startRead(watch); err != nil { |
| w.Error <- err |
| } |
| } |
| } |
| |
| func (w *Watcher) sendEvent(name string, mask uint64) bool { |
| if mask == 0 { |
| return false |
| } |
| event := &Event{Mask: uint32(mask), Name: name} |
| if mask&FS_MOVE != 0 { |
| if mask&FS_MOVED_FROM != 0 { |
| w.cookie++ |
| } |
| event.Cookie = w.cookie |
| } |
| select { |
| case ch := <-w.quit: |
| w.quit <- ch |
| case w.Event <- event: |
| } |
| return true |
| } |
| |
| // String formats the event e in the form |
| // "filename: 0xEventMask = FS_ACCESS|FS_ATTRIB_|..." |
| func (e *Event) String() string { |
| var events string |
| m := e.Mask |
| for _, b := range eventBits { |
| if m&b.Value != 0 { |
| m &^= b.Value |
| events += "|" + b.Name |
| } |
| } |
| if m != 0 { |
| events += fmt.Sprintf("|%#x", m) |
| } |
| if len(events) > 0 { |
| events = " == " + events[1:] |
| } |
| return fmt.Sprintf("%q: %#x%s", e.Name, e.Mask, events) |
| } |
| |
| func toWindowsFlags(mask uint64) uint32 { |
| var m uint32 |
| if mask&FS_ACCESS != 0 { |
| m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS |
| } |
| if mask&FS_MODIFY != 0 { |
| m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE |
| } |
| if mask&FS_ATTRIB != 0 { |
| m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES |
| } |
| if mask&(FS_MOVE|FS_CREATE|FS_DELETE) != 0 { |
| m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME |
| } |
| return m |
| } |
| |
| func toFSnotifyFlags(action uint32) uint64 { |
| switch action { |
| case syscall.FILE_ACTION_ADDED: |
| return FS_CREATE |
| case syscall.FILE_ACTION_REMOVED: |
| return FS_DELETE |
| case syscall.FILE_ACTION_MODIFIED: |
| return FS_MODIFY |
| case syscall.FILE_ACTION_RENAMED_OLD_NAME: |
| return FS_MOVED_FROM |
| case syscall.FILE_ACTION_RENAMED_NEW_NAME: |
| return FS_MOVED_TO |
| } |
| return 0 |
| } |
| |
| const ( |
| // Options for AddWatch |
| FS_ONESHOT = 0x80000000 |
| FS_ONLYDIR = 0x1000000 |
| |
| // Events |
| FS_ACCESS = 0x1 |
| FS_ALL_EVENTS = 0xfff |
| FS_ATTRIB = 0x4 |
| FS_CLOSE = 0x18 |
| FS_CREATE = 0x100 |
| FS_DELETE = 0x200 |
| FS_DELETE_SELF = 0x400 |
| FS_MODIFY = 0x2 |
| FS_MOVE = 0xc0 |
| FS_MOVED_FROM = 0x40 |
| FS_MOVED_TO = 0x80 |
| FS_MOVE_SELF = 0x800 |
| |
| // Special events |
| FS_IGNORED = 0x8000 |
| FS_Q_OVERFLOW = 0x4000 |
| ) |
| |
| var eventBits = []struct { |
| Value uint32 |
| Name string |
| }{ |
| {FS_ACCESS, "FS_ACCESS"}, |
| {FS_ATTRIB, "FS_ATTRIB"}, |
| {FS_CREATE, "FS_CREATE"}, |
| {FS_DELETE, "FS_DELETE"}, |
| {FS_DELETE_SELF, "FS_DELETE_SELF"}, |
| {FS_MODIFY, "FS_MODIFY"}, |
| {FS_MOVED_FROM, "FS_MOVED_FROM"}, |
| {FS_MOVED_TO, "FS_MOVED_TO"}, |
| {FS_MOVE_SELF, "FS_MOVE_SELF"}, |
| {FS_IGNORED, "FS_IGNORED"}, |
| {FS_Q_OVERFLOW, "FS_Q_OVERFLOW"}, |
| } |