├── .gitignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── README.md ├── appveyor.yml ├── debug.go ├── debug_debug.go ├── debug_nodebug.go ├── doc.go ├── event.go ├── event_fen.go ├── event_fsevents.go ├── event_inotify.go ├── event_kqueue.go ├── event_readdcw.go ├── event_stub.go ├── event_test.go ├── event_trigger.go ├── example_fsevents_test.go ├── example_inotify_test.go ├── example_readdcw_test.go ├── example_test.go ├── go.mod ├── go.sum ├── node.go ├── notify.go ├── notify_inotify_test.go ├── notify_readdcw_test.go ├── notify_test.go ├── sync_readdcw_test.go ├── sync_unix_test.go ├── testdata └── vfs.txt ├── testing_test.go ├── tree.go ├── tree_nonrecursive.go ├── tree_nonrecursive_test.go ├── tree_recursive.go ├── tree_recursive_test.go ├── util.go ├── util_darwin_test.go ├── util_test.go ├── util_unix_test.go ├── watcher.go ├── watcher_fen.go ├── watcher_fen_cgo.go ├── watcher_fen_test.go ├── watcher_fsevents.go ├── watcher_fsevents_cgo.go ├── watcher_fsevents_test.go ├── watcher_inotify.go ├── watcher_inotify_test.go ├── watcher_kqueue.go ├── watcher_kqueue_test.go ├── watcher_notimplemented.go ├── watcher_readdcw.go ├── watcher_readdcw_test.go ├── watcher_recursive_test.go ├── watcher_stub.go ├── watcher_test.go ├── watcher_trigger.go ├── watcher_trigger_test.go ├── watchpoint.go ├── watchpoint_other.go ├── watchpoint_readdcw.go └── watchpoint_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### OSX ### 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear on external disk 16 | .Spotlight-V100 17 | .Trashes 18 | 19 | # Directories potentially created on remote AFP share 20 | .AppleDB 21 | .AppleDesktop 22 | Network Trash Folder 23 | Temporary Items 24 | .apdisk 25 | 26 | 27 | ### Windows ### 28 | # Windows image file caches 29 | Thumbs.db 30 | ehthumbs.db 31 | 32 | # Folder config file 33 | Desktop.ini 34 | 35 | # Recycle Bin used on file shares 36 | $RECYCLE.BIN/ 37 | 38 | # Windows Installer files 39 | *.cab 40 | *.msi 41 | *.msm 42 | *.msp 43 | 44 | # Windows shortcuts 45 | *.lnk 46 | 47 | 48 | ### Linux ### 49 | *~ 50 | 51 | # KDE directory preferences 52 | .directory 53 | 54 | 55 | ### Go ### 56 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 57 | *.o 58 | *.a 59 | *.so 60 | 61 | # Folders 62 | _obj 63 | _test 64 | 65 | # Architecture specific extensions/prefixes 66 | *.[568vq] 67 | [568vq].out 68 | 69 | *.cgo1.go 70 | *.cgo2.c 71 | _cgo_defun.c 72 | _cgo_gotypes.go 73 | _cgo_export.* 74 | 75 | _testmain.go 76 | 77 | *.exe 78 | *.test 79 | *.prof 80 | 81 | 82 | ### vim ### 83 | [._]*.s[a-w][a-z] 84 | [._]s[a-w][a-z] 85 | *.un~ 86 | Session.vim 87 | .netrwhist 88 | *~ 89 | 90 | ### JetBrains files ### 91 | .idea/ 92 | *.iml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.10.x 5 | - 1.11.x 6 | - master 7 | 8 | os: 9 | - linux 10 | - osx 11 | 12 | matrix: 13 | include: 14 | - os: osx 15 | go: 1.7.5 16 | env: 17 | - GOFLAGS="-tags kqueue" 18 | allow_failures: 19 | - go: tip 20 | 21 | env: 22 | global: 23 | - GOBIN=$HOME/bin 24 | - PATH=$HOME/bin:$PATH 25 | 26 | install: 27 | - go get -t -v ./... 28 | 29 | script: 30 | - "(go version | grep -q 1.4) || go tool vet -all ." 31 | - go install $GOFLAGS ./... 32 | - go test -v -timeout 60s -race $GOFLAGS ./... 33 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # List of individuals who contributed to the Notify package. 2 | # 3 | # The up-to-date list of the authors one may obtain with: 4 | # 5 | # ~ $ git shortlog -es | cut -f2 | rev | uniq -f1 | rev 6 | # 7 | 8 | Pawel Blaszczyk 9 | Pawel Knap 10 | Rafal Jeczalik 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 The Notify Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | notify [![GoDoc](https://godoc.org/github.com/rjeczalik/notify?status.svg)](https://godoc.org/github.com/rjeczalik/notify) [![Build Status](https://img.shields.io/travis/rjeczalik/notify/master.svg)](https://travis-ci.org/rjeczalik/notify "inotify + FSEvents + kqueue") [![Build status](https://img.shields.io/appveyor/ci/rjeczalik/notify-246.svg)](https://ci.appveyor.com/project/rjeczalik/notify-246 "ReadDirectoryChangesW") [![Coverage Status](https://img.shields.io/coveralls/rjeczalik/notify/master.svg)](https://coveralls.io/r/rjeczalik/notify?branch=master) 2 | ====== 3 | 4 | Filesystem event notification library on steroids. 5 | 6 | *Documentation* 7 | 8 | [godoc.org/github.com/rjeczalik/notify](https://godoc.org/github.com/rjeczalik/notify) 9 | 10 | *Installation* 11 | 12 | ``` 13 | ~ $ go get -u github.com/rjeczalik/notify 14 | ``` 15 | 16 | *Projects using notify* 17 | 18 | - [github.com/rjeczalik/cmd/notify](https://godoc.org/github.com/rjeczalik/cmd/notify) 19 | - [github.com/cortesi/devd](https://github.com/cortesi/devd) 20 | - [github.com/cortesi/modd](https://github.com/cortesi/modd) 21 | - [github.com/syncthing/syncthing](https://github.com/syncthing/syncthing) 22 | - [github.com/OrlovEvgeny/TinyJPG](https://github.com/OrlovEvgeny/TinyJPG) 23 | - [github.com/mitranim/gow](https://github.com/mitranim/gow) 24 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | os: Windows Server 2012 R2 4 | 5 | clone_folder: c:\projects\src\github.com\rjeczalik\notify 6 | 7 | environment: 8 | PATH: c:\projects\bin;%PATH% 9 | GOPATH: c:\projects 10 | NOTIFY_TIMEOUT: 10s 11 | GOVERSION: 1.10.3 12 | 13 | install: 14 | - rmdir c:\go /s /q 15 | - appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.zip 16 | - 7z x go%GOVERSION%.windows-amd64.zip -y -oC:\ > NUL 17 | 18 | - cd %APPVEYOR_BUILD_FOLDER% 19 | - go version 20 | 21 | build_script: 22 | - go build ./... 23 | - go test -v -timeout 120s -race ./... 24 | 25 | test: off 26 | 27 | deploy: off 28 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import ( 8 | "log" 9 | "os" 10 | "runtime" 11 | "strings" 12 | ) 13 | 14 | var dbgprint func(...interface{}) 15 | 16 | var dbgprintf func(string, ...interface{}) 17 | 18 | var dbgcallstack func(max int) []string 19 | 20 | func init() { 21 | if _, ok := os.LookupEnv("NOTIFY_DEBUG"); ok || debugTag { 22 | log.SetOutput(os.Stdout) 23 | log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds) 24 | dbgprint = func(v ...interface{}) { 25 | v = append([]interface{}{"[D] "}, v...) 26 | log.Println(v...) 27 | } 28 | dbgprintf = func(format string, v ...interface{}) { 29 | format = "[D] " + format 30 | log.Printf(format, v...) 31 | } 32 | dbgcallstack = func(max int) []string { 33 | pc, stack := make([]uintptr, max), make([]string, 0, max) 34 | runtime.Callers(2, pc) 35 | for _, pc := range pc { 36 | if f := runtime.FuncForPC(pc); f != nil { 37 | fname := f.Name() 38 | idx := strings.LastIndex(fname, string(os.PathSeparator)) 39 | if idx != -1 { 40 | stack = append(stack, fname[idx+1:]) 41 | } else { 42 | stack = append(stack, fname) 43 | } 44 | } 45 | } 46 | return stack 47 | } 48 | return 49 | } 50 | dbgprint = func(v ...interface{}) {} 51 | dbgprintf = func(format string, v ...interface{}) {} 52 | dbgcallstack = func(max int) []string { return nil } 53 | } 54 | -------------------------------------------------------------------------------- /debug_debug.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2018 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build debug 6 | // +build debug 7 | 8 | package notify 9 | 10 | var debugTag = true 11 | -------------------------------------------------------------------------------- /debug_nodebug.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2018 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build !debug 6 | // +build !debug 7 | 8 | package notify 9 | 10 | var debugTag = false 11 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | // Package notify implements access to filesystem events. 6 | // 7 | // Notify is a high-level abstraction over filesystem watchers like inotify, 8 | // kqueue, FSEvents, FEN or ReadDirectoryChangesW. Watcher implementations are 9 | // split into two groups: ones that natively support recursive notifications 10 | // (FSEvents and ReadDirectoryChangesW) and ones that do not (inotify, kqueue, FEN). 11 | // For more details see watcher and recursiveWatcher interfaces in watcher.go 12 | // source file. 13 | // 14 | // On top of filesystem watchers notify maintains a watchpoint tree, which provides 15 | // a strategy for creating and closing filesystem watches and dispatching filesystem 16 | // events to user channels. 17 | // 18 | // An event set is just an event list joint using bitwise OR operator 19 | // into a single event value. 20 | // Both the platform-independent (see Constants) and specific events can be used. 21 | // Refer to the event_*.go source files for information about the available 22 | // events. 23 | // 24 | // A filesystem watch or just a watch is platform-specific entity which represents 25 | // a single path registered for notifications for specific event set. Setting a watch 26 | // means using platform-specific API calls for creating / initializing said watch. 27 | // For each watcher the API call is: 28 | // 29 | // - FSEvents: FSEventStreamCreate 30 | // - inotify: notify_add_watch 31 | // - kqueue: kevent 32 | // - ReadDirectoryChangesW: CreateFile+ReadDirectoryChangesW 33 | // - FEN: port_get 34 | // 35 | // To rewatch means to either shrink or expand an event set that was previously 36 | // registered during watch operation for particular filesystem watch. 37 | // 38 | // A watchpoint is a list of user channel and event set pairs for particular 39 | // path (watchpoint tree's node). A single watchpoint can contain multiple 40 | // different user channels registered to listen for one or more events. A single 41 | // user channel can be registered in one or more watchpoints, recursive and 42 | // non-recursive ones as well. 43 | package notify 44 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // Event represents the type of filesystem action. 13 | // 14 | // Number of available event values is dependent on the target system or the 15 | // watcher implmenetation used (e.g. it's possible to use either kqueue or 16 | // FSEvents on Darwin). 17 | // 18 | // Please consult documentation for your target platform to see list of all 19 | // available events. 20 | type Event uint32 21 | 22 | // Create, Remove, Write and Rename are the only event values guaranteed to be 23 | // present on all platforms. 24 | const ( 25 | Create = osSpecificCreate 26 | Remove = osSpecificRemove 27 | Write = osSpecificWrite 28 | Rename = osSpecificRename 29 | 30 | // All is handful alias for all platform-independent event values. 31 | All = Create | Remove | Write | Rename 32 | ) 33 | 34 | const internal = recursive | omit 35 | 36 | // String implements fmt.Stringer interface. 37 | func (e Event) String() string { 38 | var s []string 39 | for _, strmap := range []map[Event]string{estr, osestr} { 40 | for ev, str := range strmap { 41 | if e&ev == ev { 42 | s = append(s, str) 43 | } 44 | } 45 | } 46 | return strings.Join(s, "|") 47 | } 48 | 49 | // EventInfo describes an event reported by the underlying filesystem notification 50 | // subsystem. 51 | // 52 | // It always describes single event, even if the OS reported a coalesced action. 53 | // Reported path is absolute and clean. 54 | // 55 | // For non-recursive watchpoints its base is always equal to the path passed 56 | // to corresponding Watch call. 57 | // 58 | // The value of Sys if system-dependent and can be nil. 59 | // 60 | // # Sys 61 | // 62 | // Under Darwin (FSEvents) Sys() always returns a non-nil *notify.FSEvent value, 63 | // which is defined as: 64 | // 65 | // type FSEvent struct { 66 | // Path string // real path of the file or directory 67 | // ID uint64 // ID of the event (FSEventStreamEventId) 68 | // Flags uint32 // joint FSEvents* flags (FSEventStreamEventFlags) 69 | // } 70 | // 71 | // For possible values of Flags see Darwin godoc for notify or FSEvents 72 | // documentation for FSEventStreamEventFlags constants: 73 | // 74 | // https://developer.apple.com/library/mac/documentation/Darwin/Reference/FSEvents_Ref/index.html#//apple_ref/doc/constant_group/FSEventStreamEventFlags 75 | // 76 | // Under Linux (inotify) Sys() always returns a non-nil *unix.InotifyEvent 77 | // value, defined as: 78 | // 79 | // type InotifyEvent struct { 80 | // Wd int32 // Watch descriptor 81 | // Mask uint32 // Mask describing event 82 | // Cookie uint32 // Unique cookie associating related events (for rename(2)) 83 | // Len uint32 // Size of name field 84 | // Name [0]uint8 // Optional null-terminated name 85 | // } 86 | // 87 | // More information about inotify masks and the usage of inotify_event structure 88 | // can be found at: 89 | // 90 | // http://man7.org/linux/man-pages/man7/inotify.7.html 91 | // 92 | // Under Darwin, DragonFlyBSD, FreeBSD, NetBSD, OpenBSD (kqueue) Sys() always 93 | // returns a non-nil *notify.Kevent value, which is defined as: 94 | // 95 | // type Kevent struct { 96 | // Kevent *syscall.Kevent_t // Kevent is a kqueue specific structure 97 | // FI os.FileInfo // FI describes file/dir 98 | // } 99 | // 100 | // More information about syscall.Kevent_t can be found at: 101 | // 102 | // https://www.freebsd.org/cgi/man.cgi?query=kqueue 103 | // 104 | // Under Windows (ReadDirectoryChangesW) Sys() always returns nil. The documentation 105 | // of watcher's WinAPI function can be found at: 106 | // 107 | // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365465%28v=vs.85%29.aspx 108 | type EventInfo interface { 109 | Event() Event // event value for the filesystem action 110 | Path() string // real path of the file or directory 111 | Sys() interface{} // underlying data source (can return nil) 112 | } 113 | 114 | type isDirer interface { 115 | isDir() (bool, error) 116 | } 117 | 118 | var _ fmt.Stringer = (*event)(nil) 119 | var _ isDirer = (*event)(nil) 120 | 121 | // String implements fmt.Stringer interface. 122 | func (e *event) String() string { 123 | return e.Event().String() + `: "` + e.Path() + `"` 124 | } 125 | 126 | var estr = map[Event]string{ 127 | Create: "notify.Create", 128 | Remove: "notify.Remove", 129 | Write: "notify.Write", 130 | Rename: "notify.Rename", 131 | // Display name for recursive event is added only for debugging 132 | // purposes. It's an internal event after all and won't be exposed to the 133 | // user. Having Recursive event printable is helpful, e.g. for reading 134 | // testing failure messages: 135 | // 136 | // --- FAIL: TestWatchpoint (0.00 seconds) 137 | // watchpoint_test.go:64: want diff=[notify.Remove notify.Create|notify.Remove]; 138 | // got [notify.Remove notify.Remove|notify.Create] (i=1) 139 | // 140 | // Yup, here the diff have Recursive event inside. Go figure. 141 | recursive: "recursive", 142 | omit: "omit", 143 | } 144 | -------------------------------------------------------------------------------- /event_fen.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build solaris || illumos 6 | // +build solaris illumos 7 | 8 | package notify 9 | 10 | const ( 11 | osSpecificCreate Event = 0x00000100 << iota 12 | osSpecificRemove 13 | osSpecificWrite 14 | osSpecificRename 15 | // internal 16 | // recursive is used to distinguish recursive eventsets from non-recursive ones 17 | recursive 18 | // omit is used for dispatching internal events; only those events are sent 19 | // for which both the event and the watchpoint has omit in theirs event sets. 20 | omit 21 | ) 22 | 23 | const ( 24 | // FileAccess is an event reported when monitored file/directory was accessed. 25 | FileAccess = fileAccess 26 | // FileModified is an event reported when monitored file/directory was modified. 27 | FileModified = fileModified 28 | // FileAttrib is an event reported when monitored file/directory's ATTRIB 29 | // was changed. 30 | FileAttrib = fileAttrib 31 | // FileDelete is an event reported when monitored file/directory was deleted. 32 | FileDelete = fileDelete 33 | // FileRenameTo to is an event reported when monitored file/directory was renamed. 34 | FileRenameTo = fileRenameTo 35 | // FileRenameFrom is an event reported when monitored file/directory was renamed. 36 | FileRenameFrom = fileRenameFrom 37 | // FileTrunc is an event reported when monitored file/directory was truncated. 38 | FileTrunc = fileTrunc 39 | // FileNoFollow is an flag to indicate not to follow symbolic links. 40 | FileNoFollow = fileNoFollow 41 | // Unmounted is an event reported when monitored filesystem was unmounted. 42 | Unmounted = unmounted 43 | // MountedOver is an event reported when monitored file/directory was mounted on. 44 | MountedOver = mountedOver 45 | ) 46 | 47 | var osestr = map[Event]string{ 48 | FileAccess: "notify.FileAccess", 49 | FileModified: "notify.FileModified", 50 | FileAttrib: "notify.FileAttrib", 51 | FileDelete: "notify.FileDelete", 52 | FileRenameTo: "notify.FileRenameTo", 53 | FileRenameFrom: "notify.FileRenameFrom", 54 | FileTrunc: "notify.FileTrunc", 55 | FileNoFollow: "notify.FileNoFollow", 56 | Unmounted: "notify.Unmounted", 57 | MountedOver: "notify.MountedOver", 58 | } 59 | -------------------------------------------------------------------------------- /event_fsevents.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build darwin && !kqueue && cgo 6 | // +build darwin,!kqueue,cgo 7 | 8 | package notify 9 | 10 | const ( 11 | osSpecificCreate = Event(FSEventsCreated) 12 | osSpecificRemove = Event(FSEventsRemoved) 13 | osSpecificWrite = Event(FSEventsModified) 14 | osSpecificRename = Event(FSEventsRenamed) 15 | // internal = Event(0x100000) 16 | // recursive is used to distinguish recursive eventsets from non-recursive ones 17 | recursive = Event(0x200000) 18 | // omit is used for dispatching internal events; only those events are sent 19 | // for which both the event and the watchpoint has omit in theirs event sets. 20 | omit = Event(0x400000) 21 | ) 22 | 23 | // FSEvents specific event values. 24 | const ( 25 | FSEventsMustScanSubDirs Event = 0x00001 26 | FSEventsUserDropped = 0x00002 27 | FSEventsKernelDropped = 0x00004 28 | FSEventsEventIdsWrapped = 0x00008 29 | FSEventsHistoryDone = 0x00010 30 | FSEventsRootChanged = 0x00020 31 | FSEventsMount = 0x00040 32 | FSEventsUnmount = 0x00080 33 | FSEventsCreated = 0x00100 34 | FSEventsRemoved = 0x00200 35 | FSEventsInodeMetaMod = 0x00400 36 | FSEventsRenamed = 0x00800 37 | FSEventsModified = 0x01000 38 | FSEventsFinderInfoMod = 0x02000 39 | FSEventsChangeOwner = 0x04000 40 | FSEventsXattrMod = 0x08000 41 | FSEventsIsFile = 0x10000 42 | FSEventsIsDir = 0x20000 43 | FSEventsIsSymlink = 0x40000 44 | ) 45 | 46 | var osestr = map[Event]string{ 47 | FSEventsMustScanSubDirs: "notify.FSEventsMustScanSubDirs", 48 | FSEventsUserDropped: "notify.FSEventsUserDropped", 49 | FSEventsKernelDropped: "notify.FSEventsKernelDropped", 50 | FSEventsEventIdsWrapped: "notify.FSEventsEventIdsWrapped", 51 | FSEventsHistoryDone: "notify.FSEventsHistoryDone", 52 | FSEventsRootChanged: "notify.FSEventsRootChanged", 53 | FSEventsMount: "notify.FSEventsMount", 54 | FSEventsUnmount: "notify.FSEventsUnmount", 55 | FSEventsInodeMetaMod: "notify.FSEventsInodeMetaMod", 56 | FSEventsFinderInfoMod: "notify.FSEventsFinderInfoMod", 57 | FSEventsChangeOwner: "notify.FSEventsChangeOwner", 58 | FSEventsXattrMod: "notify.FSEventsXattrMod", 59 | FSEventsIsFile: "notify.FSEventsIsFile", 60 | FSEventsIsDir: "notify.FSEventsIsDir", 61 | FSEventsIsSymlink: "notify.FSEventsIsSymlink", 62 | } 63 | 64 | type event struct { 65 | fse FSEvent 66 | event Event 67 | } 68 | 69 | func (ei *event) Event() Event { return ei.event } 70 | func (ei *event) Path() string { return ei.fse.Path } 71 | func (ei *event) Sys() interface{} { return &ei.fse } 72 | func (ei *event) isDir() (bool, error) { return ei.fse.Flags&FSEventsIsDir != 0, nil } 73 | -------------------------------------------------------------------------------- /event_inotify.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build linux 6 | // +build linux 7 | 8 | package notify 9 | 10 | import "golang.org/x/sys/unix" 11 | 12 | // Platform independent event values. 13 | const ( 14 | osSpecificCreate Event = 0x100000 << iota 15 | osSpecificRemove 16 | osSpecificWrite 17 | osSpecificRename 18 | // internal 19 | // recursive is used to distinguish recursive eventsets from non-recursive ones 20 | recursive 21 | // omit is used for dispatching internal events; only those events are sent 22 | // for which both the event and the watchpoint has omit in theirs event sets. 23 | omit 24 | ) 25 | 26 | // Inotify specific masks are legal, implemented events that are guaranteed to 27 | // work with notify package on linux-based systems. 28 | const ( 29 | InAccess = Event(unix.IN_ACCESS) // File was accessed 30 | InModify = Event(unix.IN_MODIFY) // File was modified 31 | InAttrib = Event(unix.IN_ATTRIB) // Metadata changed 32 | InCloseWrite = Event(unix.IN_CLOSE_WRITE) // Writtable file was closed 33 | InCloseNowrite = Event(unix.IN_CLOSE_NOWRITE) // Unwrittable file closed 34 | InOpen = Event(unix.IN_OPEN) // File was opened 35 | InMovedFrom = Event(unix.IN_MOVED_FROM) // File was moved from X 36 | InMovedTo = Event(unix.IN_MOVED_TO) // File was moved to Y 37 | InCreate = Event(unix.IN_CREATE) // Subfile was created 38 | InDelete = Event(unix.IN_DELETE) // Subfile was deleted 39 | InDeleteSelf = Event(unix.IN_DELETE_SELF) // Self was deleted 40 | InMoveSelf = Event(unix.IN_MOVE_SELF) // Self was moved 41 | ) 42 | 43 | var osestr = map[Event]string{ 44 | InAccess: "notify.InAccess", 45 | InModify: "notify.InModify", 46 | InAttrib: "notify.InAttrib", 47 | InCloseWrite: "notify.InCloseWrite", 48 | InCloseNowrite: "notify.InCloseNowrite", 49 | InOpen: "notify.InOpen", 50 | InMovedFrom: "notify.InMovedFrom", 51 | InMovedTo: "notify.InMovedTo", 52 | InCreate: "notify.InCreate", 53 | InDelete: "notify.InDelete", 54 | InDeleteSelf: "notify.InDeleteSelf", 55 | InMoveSelf: "notify.InMoveSelf", 56 | } 57 | 58 | // Inotify behavior events are not **currently** supported by notify package. 59 | const ( 60 | inDontFollow = Event(unix.IN_DONT_FOLLOW) 61 | inExclUnlink = Event(unix.IN_EXCL_UNLINK) 62 | inMaskAdd = Event(unix.IN_MASK_ADD) 63 | inOneshot = Event(unix.IN_ONESHOT) 64 | inOnlydir = Event(unix.IN_ONLYDIR) 65 | ) 66 | 67 | type event struct { 68 | sys unix.InotifyEvent 69 | path string 70 | event Event 71 | } 72 | 73 | func (e *event) Event() Event { return e.event } 74 | func (e *event) Path() string { return e.path } 75 | func (e *event) Sys() interface{} { return &e.sys } 76 | func (e *event) isDir() (bool, error) { return e.sys.Mask&unix.IN_ISDIR != 0, nil } 77 | -------------------------------------------------------------------------------- /event_kqueue.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build (darwin && kqueue) || (darwin && !cgo) || dragonfly || freebsd || netbsd || openbsd 6 | // +build darwin,kqueue darwin,!cgo dragonfly freebsd netbsd openbsd 7 | 8 | package notify 9 | 10 | import "syscall" 11 | 12 | // TODO(pblaszczyk): ensure in runtime notify built-in event values do not 13 | // overlap with platform-defined ones. 14 | 15 | // Platform independent event values. 16 | const ( 17 | osSpecificCreate Event = 0x0100 << iota 18 | osSpecificRemove 19 | osSpecificWrite 20 | osSpecificRename 21 | // internal 22 | // recursive is used to distinguish recursive eventsets from non-recursive ones 23 | recursive 24 | // omit is used for dispatching internal events; only those events are sent 25 | // for which both the event and the watchpoint has omit in theirs event sets. 26 | omit 27 | ) 28 | 29 | const ( 30 | // NoteDelete is an event reported when the unlink() system call was called 31 | // on the file referenced by the descriptor. 32 | NoteDelete = Event(syscall.NOTE_DELETE) 33 | // NoteWrite is an event reported when a write occurred on the file 34 | // referenced by the descriptor. 35 | NoteWrite = Event(syscall.NOTE_WRITE) 36 | // NoteExtend is an event reported when the file referenced by the 37 | // descriptor was extended. 38 | NoteExtend = Event(syscall.NOTE_EXTEND) 39 | // NoteAttrib is an event reported when the file referenced 40 | // by the descriptor had its attributes changed. 41 | NoteAttrib = Event(syscall.NOTE_ATTRIB) 42 | // NoteLink is an event reported when the link count on the file changed. 43 | NoteLink = Event(syscall.NOTE_LINK) 44 | // NoteRename is an event reported when the file referenced 45 | // by the descriptor was renamed. 46 | NoteRename = Event(syscall.NOTE_RENAME) 47 | // NoteRevoke is an event reported when access to the file was revoked via 48 | // revoke(2) or the underlying file system was unmounted. 49 | NoteRevoke = Event(syscall.NOTE_REVOKE) 50 | ) 51 | 52 | var osestr = map[Event]string{ 53 | NoteDelete: "notify.NoteDelete", 54 | NoteWrite: "notify.NoteWrite", 55 | NoteExtend: "notify.NoteExtend", 56 | NoteAttrib: "notify.NoteAttrib", 57 | NoteLink: "notify.NoteLink", 58 | NoteRename: "notify.NoteRename", 59 | NoteRevoke: "notify.NoteRevoke", 60 | } 61 | -------------------------------------------------------------------------------- /event_readdcw.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build windows 6 | // +build windows 7 | 8 | package notify 9 | 10 | import ( 11 | "os" 12 | "path/filepath" 13 | "syscall" 14 | ) 15 | 16 | // Platform independent event values. 17 | const ( 18 | osSpecificCreate Event = 1 << (20 + iota) 19 | osSpecificRemove 20 | osSpecificWrite 21 | osSpecificRename 22 | // recursive is used to distinguish recursive eventsets from non-recursive ones 23 | recursive 24 | // omit is used for dispatching internal events; only those events are sent 25 | // for which both the event and the watchpoint has omit in theirs event sets. 26 | omit 27 | // dirmarker TODO(pknap) 28 | dirmarker 29 | ) 30 | 31 | // ReadDirectoryChangesW filters 32 | // On Windows the following events can be passed to Watch. A different set of 33 | // events (see actions below) are received on the channel passed to Watch. 34 | // For more information refer to 35 | // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx 36 | const ( 37 | FileNotifyChangeFileName = Event(syscall.FILE_NOTIFY_CHANGE_FILE_NAME) 38 | FileNotifyChangeDirName = Event(syscall.FILE_NOTIFY_CHANGE_DIR_NAME) 39 | FileNotifyChangeAttributes = Event(syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES) 40 | FileNotifyChangeSize = Event(syscall.FILE_NOTIFY_CHANGE_SIZE) 41 | FileNotifyChangeLastWrite = Event(syscall.FILE_NOTIFY_CHANGE_LAST_WRITE) 42 | FileNotifyChangeLastAccess = Event(syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS) 43 | FileNotifyChangeCreation = Event(syscall.FILE_NOTIFY_CHANGE_CREATION) 44 | FileNotifyChangeSecurity = Event(syscallFileNotifyChangeSecurity) 45 | ) 46 | 47 | const ( 48 | fileNotifyChangeAll = 0x17f // logical sum of all FileNotifyChange* events. 49 | fileNotifyChangeModified = fileNotifyChangeAll &^ (FileNotifyChangeFileName | FileNotifyChangeDirName) 50 | ) 51 | 52 | // according to: http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx 53 | // this flag should be declared in: http://golang.org/src/pkg/syscall/ztypes_windows.go 54 | const syscallFileNotifyChangeSecurity = 0x00000100 55 | 56 | // ReadDirectoryChangesW actions 57 | // The following events are returned on the channel passed to Watch, but cannot 58 | // be passed to Watch itself (see filters above). You can find a table showing 59 | // the relation between actions and filteres at 60 | // https://github.com/rjeczalik/notify/issues/10#issuecomment-66179535 61 | // The msdn documentation on actions is part of 62 | // https://msdn.microsoft.com/en-us/library/windows/desktop/aa364391(v=vs.85).aspx 63 | const ( 64 | FileActionAdded = Event(syscall.FILE_ACTION_ADDED) << 12 65 | FileActionRemoved = Event(syscall.FILE_ACTION_REMOVED) << 12 66 | FileActionModified = Event(syscall.FILE_ACTION_MODIFIED) << 14 67 | FileActionRenamedOldName = Event(syscall.FILE_ACTION_RENAMED_OLD_NAME) << 15 68 | FileActionRenamedNewName = Event(syscall.FILE_ACTION_RENAMED_NEW_NAME) << 16 69 | ) 70 | 71 | const fileActionAll = 0x7f000 // logical sum of all FileAction* events. 72 | 73 | var osestr = map[Event]string{ 74 | FileNotifyChangeFileName: "notify.FileNotifyChangeFileName", 75 | FileNotifyChangeDirName: "notify.FileNotifyChangeDirName", 76 | FileNotifyChangeAttributes: "notify.FileNotifyChangeAttributes", 77 | FileNotifyChangeSize: "notify.FileNotifyChangeSize", 78 | FileNotifyChangeLastWrite: "notify.FileNotifyChangeLastWrite", 79 | FileNotifyChangeLastAccess: "notify.FileNotifyChangeLastAccess", 80 | FileNotifyChangeCreation: "notify.FileNotifyChangeCreation", 81 | FileNotifyChangeSecurity: "notify.FileNotifyChangeSecurity", 82 | 83 | FileActionAdded: "notify.FileActionAdded", 84 | FileActionRemoved: "notify.FileActionRemoved", 85 | FileActionModified: "notify.FileActionModified", 86 | FileActionRenamedOldName: "notify.FileActionRenamedOldName", 87 | FileActionRenamedNewName: "notify.FileActionRenamedNewName", 88 | } 89 | 90 | const ( 91 | fTypeUnknown uint8 = iota 92 | fTypeFile 93 | fTypeDirectory 94 | ) 95 | 96 | // TODO(ppknap) : doc. 97 | type event struct { 98 | pathw []uint16 99 | name string 100 | ftype uint8 101 | action uint32 102 | filter uint32 103 | e Event 104 | } 105 | 106 | func (e *event) Event() Event { return e.e } 107 | func (e *event) Path() string { return filepath.Join(syscall.UTF16ToString(e.pathw), e.name) } 108 | func (e *event) Sys() interface{} { return e.ftype } 109 | 110 | func (e *event) isDir() (bool, error) { 111 | if e.ftype != fTypeUnknown { 112 | return e.ftype == fTypeDirectory, nil 113 | } 114 | fi, err := os.Stat(e.Path()) 115 | if err != nil { 116 | return false, err 117 | } 118 | return fi.IsDir(), nil 119 | } 120 | -------------------------------------------------------------------------------- /event_stub.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build !darwin && !linux && !freebsd && !dragonfly && !netbsd && !openbsd && !windows && !kqueue && !solaris && !illumos 6 | // +build !darwin,!linux,!freebsd,!dragonfly,!netbsd,!openbsd,!windows,!kqueue,!solaris,!illumos 7 | 8 | package notify 9 | 10 | // Platform independent event values. 11 | const ( 12 | osSpecificCreate Event = 1 << iota 13 | osSpecificRemove 14 | osSpecificWrite 15 | osSpecificRename 16 | // internal 17 | // recursive is used to distinguish recursive eventsets from non-recursive ones 18 | recursive 19 | // omit is used for dispatching internal events; only those events are sent 20 | // for which both the event and the watchpoint has omit in theirs event sets. 21 | omit 22 | ) 23 | 24 | var osestr = map[Event]string{} 25 | 26 | type event struct{} 27 | 28 | func (e *event) Event() (_ Event) { return } 29 | func (e *event) Path() (_ string) { return } 30 | func (e *event) Sys() (_ interface{}) { return } 31 | func (e *event) isDir() (_ bool, _ error) { return } 32 | -------------------------------------------------------------------------------- /event_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import ( 8 | "sort" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | // S is a workaround for random event strings concatenation order. 14 | func s(s string) string { 15 | z := strings.Split(s, "|") 16 | sort.StringSlice(z).Sort() 17 | return strings.Join(z, "|") 18 | } 19 | 20 | // This test is not safe to run in parallel with others. 21 | func TestEventString(t *testing.T) { 22 | cases := map[Event]string{ 23 | Create: "notify.Create", 24 | Create | Remove: "notify.Create|notify.Remove", 25 | Create | Remove | Write: "notify.Create|notify.Remove|notify.Write", 26 | Create | Write | Rename: "notify.Create|notify.Rename|notify.Write", 27 | } 28 | for e, str := range cases { 29 | if s := s(e.String()); s != str { 30 | t.Errorf("want s=%s; got %s (e=%#x)", str, s, e) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /event_trigger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build (darwin && kqueue) || (darwin && !cgo) || dragonfly || freebsd || netbsd || openbsd || solaris || illumos 6 | // +build darwin,kqueue darwin,!cgo dragonfly freebsd netbsd openbsd solaris illumos 7 | 8 | package notify 9 | 10 | type event struct { 11 | p string 12 | e Event 13 | d bool 14 | pe interface{} 15 | } 16 | 17 | func (e *event) Event() Event { return e.e } 18 | 19 | func (e *event) Path() string { return e.p } 20 | 21 | func (e *event) Sys() interface{} { return e.pe } 22 | 23 | func (e *event) isDir() (bool, error) { return e.d, nil } 24 | -------------------------------------------------------------------------------- /example_fsevents_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build darwin && !kqueue && cgo 6 | // +build darwin,!kqueue,cgo 7 | 8 | package notify_test 9 | 10 | import ( 11 | "log" 12 | 13 | "github.com/rjeczalik/notify" 14 | ) 15 | 16 | // This example shows how to use FSEvents-specifc event values. 17 | func ExampleWatch_darwin() { 18 | // Make the channel buffered to ensure no event is dropped. Notify will drop 19 | // an event if the receiver is not able to keep up the sending pace. 20 | c := make(chan notify.EventInfo, 1) 21 | 22 | // Set up a watchpoint listening for FSEvents-specific events within a 23 | // current working directory. Dispatch each FSEventsChangeOwner and FSEventsMount 24 | // events separately to c. 25 | if err := notify.Watch(".", c, notify.FSEventsChangeOwner, notify.FSEventsMount); err != nil { 26 | log.Fatal(err) 27 | } 28 | defer notify.Stop(c) 29 | 30 | // Block until an event is received. 31 | switch ei := <-c; ei.Event() { 32 | case notify.FSEventsChangeOwner: 33 | log.Println("The owner of", ei.Path(), "has changed.") 34 | case notify.FSEventsMount: 35 | log.Println("The path", ei.Path(), "has been mounted.") 36 | } 37 | } 38 | 39 | // This example shows how to work with EventInfo's underlying FSEvent struct. 40 | // Investigating notify.(*FSEvent).Flags field we are able to say whether 41 | // the event's path is a file or a directory and many more. 42 | func ExampleWatch_darwinDirFileSymlink() { 43 | var must = func(err error) { 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | } 48 | var stop = func(c ...chan<- notify.EventInfo) { 49 | for _, c := range c { 50 | notify.Stop(c) 51 | } 52 | } 53 | 54 | // Make the channels buffered to ensure no event is dropped. Notify will drop 55 | // an event if the receiver is not able to keep up the sending pace. 56 | dir := make(chan notify.EventInfo, 1) 57 | file := make(chan notify.EventInfo, 1) 58 | symlink := make(chan notify.EventInfo, 1) 59 | all := make(chan notify.EventInfo, 1) 60 | 61 | // Set up a single watchpoint listening for FSEvents-specific events on 62 | // multiple user-provided channels. 63 | must(notify.Watch(".", dir, notify.FSEventsIsDir)) 64 | must(notify.Watch(".", file, notify.FSEventsIsFile)) 65 | must(notify.Watch(".", symlink, notify.FSEventsIsSymlink)) 66 | must(notify.Watch(".", all, notify.All)) 67 | defer stop(dir, file, symlink, all) 68 | 69 | // Block until an event is received. 70 | select { 71 | case ei := <-dir: 72 | log.Println("The directory", ei.Path(), "has changed") 73 | case ei := <-file: 74 | log.Println("The file", ei.Path(), "has changed") 75 | case ei := <-symlink: 76 | log.Println("The symlink", ei.Path(), "has changed") 77 | case ei := <-all: 78 | var kind string 79 | 80 | // Investigate underlying *notify.FSEvent struct to access more 81 | // information about the event. 82 | switch flags := ei.Sys().(*notify.FSEvent).Flags; { 83 | case flags¬ify.FSEventsIsFile != 0: 84 | kind = "file" 85 | case flags¬ify.FSEventsIsDir != 0: 86 | kind = "dir" 87 | case flags¬ify.FSEventsIsSymlink != 0: 88 | kind = "symlink" 89 | } 90 | 91 | log.Printf("The %s under path %s has been %sd\n", kind, ei.Path(), ei.Event()) 92 | } 93 | } 94 | 95 | // FSEvents may report multiple filesystem actions with one, coalesced event. 96 | // Notify unscoalesces such event and dispatches series of single events 97 | // back to the user. 98 | // 99 | // This example shows how to coalesce events by investigating notify.(*FSEvent).ID 100 | // field, for the science. 101 | func ExampleWatch_darwinCoalesce() { 102 | // Make the channels buffered to ensure no event is dropped. Notify will drop 103 | // an event if the receiver is not able to keep up the sending pace. 104 | c := make(chan notify.EventInfo, 4) 105 | 106 | // Set up a watchpoint listetning for events within current working directory. 107 | // Dispatch all platform-independent separately to c. 108 | if err := notify.Watch(".", c, notify.All); err != nil { 109 | log.Fatal(err) 110 | } 111 | defer notify.Stop(c) 112 | 113 | var id uint64 114 | var coalesced []notify.EventInfo 115 | 116 | for ei := range c { 117 | switch n := ei.Sys().(*notify.FSEvent).ID; { 118 | case id == 0: 119 | id = n 120 | coalesced = []notify.EventInfo{ei} 121 | case id == n: 122 | coalesced = append(coalesced, ei) 123 | default: 124 | log.Printf("FSEvents reported a filesystem action with the following"+ 125 | " coalesced events %v groupped by %d ID\n", coalesced, id) 126 | return 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /example_inotify_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build linux 6 | // +build linux 7 | 8 | package notify_test 9 | 10 | import ( 11 | "log" 12 | 13 | "golang.org/x/sys/unix" 14 | 15 | "github.com/rjeczalik/notify" 16 | ) 17 | 18 | // This example shows how to watch changes made on file-system by text editor 19 | // when saving a file. Usually, either InCloseWrite or InMovedTo (when swapping 20 | // with a temporary file) event is created. 21 | func ExampleWatch_linux() { 22 | // Make the channel buffered to ensure no event is dropped. Notify will drop 23 | // an event if the receiver is not able to keep up the sending pace. 24 | c := make(chan notify.EventInfo, 1) 25 | 26 | // Set up a watchpoint listening for inotify-specific events within a 27 | // current working directory. Dispatch each InCloseWrite and InMovedTo 28 | // events separately to c. 29 | if err := notify.Watch(".", c, notify.InCloseWrite, notify.InMovedTo); err != nil { 30 | log.Fatal(err) 31 | } 32 | defer notify.Stop(c) 33 | 34 | // Block until an event is received. 35 | switch ei := <-c; ei.Event() { 36 | case notify.InCloseWrite: 37 | log.Println("Editing of", ei.Path(), "file is done.") 38 | case notify.InMovedTo: 39 | log.Println("File", ei.Path(), "was swapped/moved into the watched directory.") 40 | } 41 | } 42 | 43 | // This example shows how to use Sys() method from EventInfo interface to tie 44 | // two separate events generated by rename(2) function. 45 | func ExampleWatch_linuxMove() { 46 | // Make the channel buffered to ensure no event is dropped. Notify will drop 47 | // an event if the receiver is not able to keep up the sending pace. 48 | c := make(chan notify.EventInfo, 2) 49 | 50 | // Set up a watchpoint listening for inotify-specific events within a 51 | // current working directory. Dispatch each InMovedFrom and InMovedTo 52 | // events separately to c. 53 | if err := notify.Watch(".", c, notify.InMovedFrom, notify.InMovedTo); err != nil { 54 | log.Fatal(err) 55 | } 56 | defer notify.Stop(c) 57 | 58 | // Inotify reports move filesystem action by sending two events tied with 59 | // unique cookie value (uint32): one of the events is of InMovedFrom type 60 | // carrying move source path, while the second one is of InMoveTo type 61 | // carrying move destination path. 62 | moves := make(map[uint32]struct { 63 | From string 64 | To string 65 | }) 66 | 67 | // Wait for moves. 68 | for ei := range c { 69 | cookie := ei.Sys().(*unix.InotifyEvent).Cookie 70 | 71 | info := moves[cookie] 72 | switch ei.Event() { 73 | case notify.InMovedFrom: 74 | info.From = ei.Path() 75 | case notify.InMovedTo: 76 | info.To = ei.Path() 77 | } 78 | moves[cookie] = info 79 | 80 | if cookie != 0 && info.From != "" && info.To != "" { 81 | log.Println("File:", info.From, "was renamed to", info.To) 82 | delete(moves, cookie) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /example_readdcw_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build windows 6 | // +build windows 7 | 8 | package notify_test 9 | 10 | import ( 11 | "log" 12 | 13 | "github.com/rjeczalik/notify" 14 | ) 15 | 16 | // This example shows how to watch directory-name changes in the working directory subtree. 17 | func ExampleWatch_windows() { 18 | // Make the channel buffered to ensure no event is dropped. Notify will drop 19 | // an event if the receiver is not able to keep up the sending pace. 20 | c := make(chan notify.EventInfo, 4) 21 | 22 | // Since notify package behaves exactly like ReadDirectoryChangesW function, 23 | // we must register notify.FileNotifyChangeDirName filter and wait for one 24 | // of FileAction* events. 25 | if err := notify.Watch("./...", c, notify.FileNotifyChangeDirName); err != nil { 26 | log.Fatal(err) 27 | } 28 | defer notify.Stop(c) 29 | 30 | // Wait for actions. 31 | for ei := range c { 32 | switch ei.Event() { 33 | case notify.FileActionAdded, notify.FileActionRenamedNewName: 34 | log.Println("Created:", ei.Path()) 35 | case notify.FileActionRemoved, notify.FileActionRenamedOldName: 36 | log.Println("Removed:", ei.Path()) 37 | case notify.FileActionModified: 38 | panic("notify: unexpected action") 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify_test 6 | 7 | import ( 8 | "log" 9 | "path/filepath" 10 | "time" 11 | 12 | "github.com/rjeczalik/notify" 13 | ) 14 | 15 | // This is a basic example showing how to work with notify.Watch function. 16 | func ExampleWatch() { 17 | // Make the channel buffered to ensure no event is dropped. Notify will drop 18 | // an event if the receiver is not able to keep up the sending pace. 19 | c := make(chan notify.EventInfo, 1) 20 | 21 | // Set up a watchpoint listening on events within current working directory. 22 | // Dispatch each create and remove events separately to c. 23 | if err := notify.Watch(".", c, notify.Create, notify.Remove); err != nil { 24 | log.Fatal(err) 25 | } 26 | defer notify.Stop(c) 27 | 28 | // Block until an event is received. 29 | ei := <-c 30 | log.Println("Got event:", ei) 31 | } 32 | 33 | // This example shows how to set up a recursive watchpoint. 34 | func ExampleWatch_recursive() { 35 | // Make the channel buffered to ensure no event is dropped. Notify will drop 36 | // an event if the receiver is not able to keep up the sending pace. 37 | c := make(chan notify.EventInfo, 1) 38 | 39 | // Set up a watchpoint listening for events within a directory tree rooted 40 | // at current working directory. Dispatch remove events to c. 41 | if err := notify.Watch("./...", c, notify.Remove); err != nil { 42 | log.Fatal(err) 43 | } 44 | defer notify.Stop(c) 45 | 46 | // Block until an event is received. 47 | ei := <-c 48 | log.Println("Got event:", ei) 49 | } 50 | 51 | // This example shows why it is important to not create leaks by stoping 52 | // a channel when it's no longer being used. 53 | func ExampleStop() { 54 | waitfor := func(path string, e notify.Event, timeout time.Duration) bool { 55 | dir, file := filepath.Split(path) 56 | c := make(chan notify.EventInfo, 1) 57 | 58 | if err := notify.Watch(dir, c, e); err != nil { 59 | log.Fatal(err) 60 | } 61 | // Clean up watchpoint associated with c. If Stop was not called upon 62 | // return the channel would be leaked as notify holds the only reference 63 | // to it and does not release it on its own. 64 | defer notify.Stop(c) 65 | 66 | t := time.After(timeout) 67 | 68 | for { 69 | select { 70 | case ei := <-c: 71 | if filepath.Base(ei.Path()) == file { 72 | return true 73 | } 74 | case <-t: 75 | return false 76 | } 77 | } 78 | } 79 | 80 | if waitfor("index.lock", notify.Create, 5*time.Second) { 81 | log.Println("The git repository was locked") 82 | } 83 | 84 | if waitfor("index.lock", notify.Remove, 5*time.Second) { 85 | log.Println("The git repository was unlocked") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rjeczalik/notify 2 | 3 | go 1.11 4 | 5 | require golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7 h1:bit1t3mgdR35yN0cX0G8orgLtOuyL9Wqxa1mccLB0ig= 2 | golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 3 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import ( 8 | "errors" 9 | "io/fs" 10 | "os" 11 | "path/filepath" 12 | ) 13 | 14 | var errSkip = errors.New("notify: skip") 15 | 16 | type walkPathFunc func(nd node, isbase bool) error 17 | 18 | type walkFunc func(node) error 19 | 20 | func errnotexist(name string) error { 21 | return &os.PathError{ 22 | Op: "Node", 23 | Path: name, 24 | Err: os.ErrNotExist, 25 | } 26 | } 27 | 28 | type node struct { 29 | Name string 30 | Watch watchpoint 31 | Child map[string]node 32 | } 33 | 34 | func newnode(name string) node { 35 | return node{ 36 | Name: name, 37 | Watch: make(watchpoint), 38 | Child: make(map[string]node), 39 | } 40 | } 41 | 42 | func (nd node) addchild(name, base string) node { 43 | child, ok := nd.Child[base] 44 | if !ok { 45 | child = newnode(name) 46 | nd.Child[base] = child 47 | } 48 | return child 49 | } 50 | 51 | func (nd node) Add(name string) node { 52 | i := indexrel(nd.Name, name) 53 | if i == -1 { 54 | return node{} 55 | } 56 | for j := indexSep(name[i:]); j != -1; j = indexSep(name[i:]) { 57 | nd = nd.addchild(name[:i+j], name[i:i+j]) 58 | i += j + 1 59 | } 60 | return nd.addchild(name, name[i:]) 61 | } 62 | 63 | func (nd node) AddDir(fn walkFunc) error { 64 | stack := []node{nd} 65 | Traverse: 66 | for n := len(stack); n != 0; n = len(stack) { 67 | nd, stack = stack[n-1], stack[:n-1] 68 | switch err := fn(nd); err { 69 | case nil: 70 | case errSkip: 71 | continue Traverse 72 | default: 73 | return &os.PathError{ 74 | Op: "error while traversing", 75 | Path: nd.Name, 76 | Err: err, 77 | } 78 | } 79 | // TODO(rjeczalik): tolerate open failures - add failed names to 80 | // AddDirError and notify users which names are not added to the tree. 81 | fi, err := os.ReadDir(nd.Name) 82 | if err != nil { 83 | return err 84 | } 85 | for _, fi := range fi { 86 | if fi.Type()&(fs.ModeSymlink|fs.ModeDir) == fs.ModeDir { 87 | name := filepath.Join(nd.Name, fi.Name()) 88 | stack = append(stack, nd.addchild(name, name[len(nd.Name)+1:])) 89 | } 90 | } 91 | } 92 | return nil 93 | } 94 | 95 | func (nd node) Get(name string) (node, error) { 96 | i := indexrel(nd.Name, name) 97 | if i == -1 { 98 | return node{}, errnotexist(name) 99 | } 100 | ok := false 101 | for j := indexSep(name[i:]); j != -1; j = indexSep(name[i:]) { 102 | if nd, ok = nd.Child[name[i:i+j]]; !ok { 103 | return node{}, errnotexist(name) 104 | } 105 | i += j + 1 106 | } 107 | if nd, ok = nd.Child[name[i:]]; !ok { 108 | return node{}, errnotexist(name) 109 | } 110 | return nd, nil 111 | } 112 | 113 | func (nd node) Del(name string) error { 114 | i := indexrel(nd.Name, name) 115 | if i == -1 { 116 | return errnotexist(name) 117 | } 118 | stack := []node{nd} 119 | ok := false 120 | for j := indexSep(name[i:]); j != -1; j = indexSep(name[i:]) { 121 | if nd, ok = nd.Child[name[i:i+j]]; !ok { 122 | return errnotexist(name[:i+j]) 123 | } 124 | stack = append(stack, nd) 125 | i += j + 1 126 | } 127 | if _, ok = nd.Child[name[i:]]; !ok { 128 | return errnotexist(name) 129 | } 130 | delete(nd.Child, name[i:]) 131 | for name, i = name[i:], len(stack); i != 0; name, i = base(nd.Name), i-1 { 132 | nd = stack[i-1] 133 | if nd := nd.Child[name]; len(nd.Watch) > 1 || len(nd.Child) != 0 { 134 | break 135 | } else { 136 | nd.Child = nil 137 | nd.Watch = nil 138 | } 139 | delete(nd.Child, name) 140 | } 141 | return nil 142 | } 143 | 144 | func (nd node) Walk(fn walkFunc) error { 145 | stack := []node{nd} 146 | Traverse: 147 | for n := len(stack); n != 0; n = len(stack) { 148 | nd, stack = stack[n-1], stack[:n-1] 149 | switch err := fn(nd); err { 150 | case nil: 151 | case errSkip: 152 | continue Traverse 153 | default: 154 | return err 155 | } 156 | for name, nd := range nd.Child { 157 | if name == "" { 158 | // Node storing inactive watchpoints has empty name, skip it 159 | // form traversing. Root node has also an empty name, but it 160 | // never has a parent node. 161 | continue 162 | } 163 | stack = append(stack, nd) 164 | } 165 | } 166 | return nil 167 | } 168 | 169 | func (nd node) WalkPath(name string, fn walkPathFunc) error { 170 | i := indexrel(nd.Name, name) 171 | if i == -1 { 172 | return errnotexist(name) 173 | } 174 | ok := false 175 | for j := indexSep(name[i:]); j != -1; j = indexSep(name[i:]) { 176 | switch err := fn(nd, false); err { 177 | case nil: 178 | case errSkip: 179 | return nil 180 | default: 181 | return err 182 | } 183 | if nd, ok = nd.Child[name[i:i+j]]; !ok { 184 | return errnotexist(name[:i+j]) 185 | } 186 | i += j + 1 187 | } 188 | switch err := fn(nd, false); err { 189 | case nil: 190 | case errSkip: 191 | return nil 192 | default: 193 | return err 194 | } 195 | if nd, ok = nd.Child[name[i:]]; !ok { 196 | return errnotexist(name) 197 | } 198 | switch err := fn(nd, true); err { 199 | case nil, errSkip: 200 | return nil 201 | default: 202 | return err 203 | } 204 | } 205 | 206 | type root struct { 207 | nd node 208 | } 209 | 210 | func (r root) addroot(name string) node { 211 | if vol := filepath.VolumeName(name); vol != "" { 212 | root, ok := r.nd.Child[vol] 213 | if !ok { 214 | root = r.nd.addchild(vol, vol) 215 | } 216 | return root 217 | } 218 | return r.nd 219 | } 220 | 221 | func (r root) root(name string) (node, error) { 222 | if vol := filepath.VolumeName(name); vol != "" { 223 | nd, ok := r.nd.Child[vol] 224 | if !ok { 225 | return node{}, errnotexist(name) 226 | } 227 | return nd, nil 228 | } 229 | return r.nd, nil 230 | } 231 | 232 | func (r root) Add(name string) node { 233 | return r.addroot(name).Add(name) 234 | } 235 | 236 | func (r root) AddDir(dir string, fn walkFunc) error { 237 | return r.Add(dir).AddDir(fn) 238 | } 239 | 240 | func (r root) Del(name string) error { 241 | nd, err := r.root(name) 242 | if err != nil { 243 | return err 244 | } 245 | return nd.Del(name) 246 | } 247 | 248 | func (r root) Get(name string) (node, error) { 249 | nd, err := r.root(name) 250 | if err != nil { 251 | return node{}, err 252 | } 253 | if nd.Name != name { 254 | if nd, err = nd.Get(name); err != nil { 255 | return node{}, err 256 | } 257 | } 258 | return nd, nil 259 | } 260 | 261 | func (r root) Walk(name string, fn walkFunc) error { 262 | nd, err := r.Get(name) 263 | if err != nil { 264 | return err 265 | } 266 | return nd.Walk(fn) 267 | } 268 | 269 | func (r root) WalkPath(name string, fn walkPathFunc) error { 270 | nd, err := r.root(name) 271 | if err != nil { 272 | return err 273 | } 274 | return nd.WalkPath(name, fn) 275 | } 276 | -------------------------------------------------------------------------------- /notify.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | // BUG(rjeczalik): Notify does not collect watchpoints, when underlying watches 6 | // were removed by their os-specific watcher implementations. Instead users are 7 | // advised to listen on persistent paths to have guarantee they receive events 8 | // for the whole lifetime of their applications (to discuss see #69). 9 | 10 | // BUG(ppknap): Linux (inotify) does not support watcher behavior masks like 11 | // InOneshot, InOnlydir etc. Instead users are advised to perform the filtering 12 | // themselves (to discuss see #71). 13 | 14 | // BUG(ppknap): Notify was not tested for short path name support under Windows 15 | // (ReadDirectoryChangesW). 16 | 17 | // BUG(ppknap): Windows (ReadDirectoryChangesW) cannot recognize which notification 18 | // triggers FileActionModified event. (to discuss see #75). 19 | 20 | package notify 21 | 22 | var defaultTree = newTree() 23 | 24 | // Watch sets up a watchpoint on path listening for events given by the events 25 | // argument. 26 | // 27 | // File or directory given by the path must exist, otherwise Watch will fail 28 | // with non-nil error. Notify resolves, for its internal purpose, any symlinks 29 | // the provided path may contain, so it may fail if the symlinks form a cycle. 30 | // It does so, since not all watcher implementations treat passed paths as-is. 31 | // E.g. FSEvents reports a real path for every event, setting a watchpoint 32 | // on /tmp will report events with paths rooted at /private/tmp etc. 33 | // 34 | // The c almost always is a buffered channel. Watch will not block sending to c 35 | // - the caller must ensure that c has sufficient buffer space to keep up with 36 | // the expected event rate. 37 | // 38 | // It is allowed to pass the same channel multiple times with different event 39 | // list or different paths. Calling Watch with different event lists for a single 40 | // watchpoint expands its event set. The only way to shrink it, is to call 41 | // Stop on its channel. 42 | // 43 | // Calling Watch with empty event list does not expand nor shrink watchpoint's 44 | // event set. If c is the first channel to listen for events on the given path, 45 | // Watch will seamlessly create a watch on the filesystem. 46 | // 47 | // Notify dispatches copies of single filesystem event to all channels registered 48 | // for each path. If a single filesystem event contains multiple coalesced events, 49 | // each of them is dispatched separately. E.g. the following filesystem change: 50 | // 51 | // ~ $ echo Hello > Notify.txt 52 | // 53 | // dispatches two events - notify.Create and notify.Write. However, it may depend 54 | // on the underlying watcher implementation whether OS reports both of them. 55 | // 56 | // # Windows and recursive watches 57 | // 58 | // If a directory which path was used to create recursive watch under Windows 59 | // gets deleted, the OS will not report such event. It is advised to keep in 60 | // mind this limitation while setting recursive watchpoints for your application, 61 | // e.g. use persistent paths like %userprofile% or watch additionally parent 62 | // directory of a recursive watchpoint in order to receive delete events for it. 63 | func Watch(path string, c chan<- EventInfo, events ...Event) error { 64 | return defaultTree.Watch(path, c, events...) 65 | } 66 | 67 | // Stop removes all watchpoints registered for c. All underlying watches are 68 | // also removed, for which c was the last channel listening for events. 69 | // 70 | // Stop does not close c. When Stop returns, it is guaranteed that c will 71 | // receive no more signals. 72 | func Stop(c chan<- EventInfo) { 73 | defaultTree.Stop(c) 74 | } 75 | -------------------------------------------------------------------------------- /notify_inotify_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build linux 6 | // +build linux 7 | 8 | package notify 9 | 10 | import "testing" 11 | 12 | func TestNotifySystemAndGlobalMix(t *testing.T) { 13 | n := NewNotifyTest(t, "testdata/vfs.txt") 14 | 15 | ch := NewChans(2) 16 | 17 | n.Watch("src/github.com/rjeczalik/fs", ch[0], Create) 18 | n.Watch("src/github.com/rjeczalik/fs", ch[1], InCreate) 19 | 20 | cases := []NCase{ 21 | { 22 | Event: icreate(n.W(), "src/github.com/rjeczalik/fs/.main.cc.swr"), 23 | Receiver: Chans{ch[0], ch[1]}, 24 | }, 25 | } 26 | 27 | n.ExpectNotifyEvents(cases, ch) 28 | } 29 | 30 | func TestUnknownEvent(t *testing.T) { 31 | n := NewNotifyTest(t, "testdata/vfs.txt") 32 | 33 | ch := NewChans(1) 34 | 35 | n.WatchErr("src/github.com/rjeczalik/fs", ch[0], nil, inExclUnlink) 36 | } 37 | -------------------------------------------------------------------------------- /notify_readdcw_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build windows 6 | // +build windows 7 | 8 | package notify 9 | 10 | import "testing" 11 | 12 | func TestNotifySystemSpecificEvent(t *testing.T) { 13 | n := NewNotifyTest(t, "testdata/vfs.txt") 14 | 15 | ch := NewChans(1) 16 | 17 | n.Watch("src/github.com/rjeczalik/fs", ch[0], FileNotifyChangeFileName, FileNotifyChangeSize) 18 | 19 | cases := []NCase{ 20 | { 21 | Event: rremove(n.W(), "src/github.com/rjeczalik/fs/fs.go"), 22 | Receiver: Chans{ch[0]}, 23 | }, 24 | { 25 | Event: rwrite(n.W(), "src/github.com/rjeczalik/fs/README.md", []byte("XD")), 26 | Receiver: Chans{ch[0]}, 27 | }, 28 | } 29 | 30 | n.ExpectNotifyEvents(cases, ch) 31 | } 32 | 33 | func TestUnknownEvent(t *testing.T) { 34 | n := NewNotifyTest(t, "testdata/vfs.txt") 35 | 36 | ch := NewChans(1) 37 | 38 | n.WatchErr("src/github.com/rjeczalik/fs", ch[0], nil, FileActionAdded) 39 | } 40 | 41 | func TestNotifySystemAndGlobalMix(t *testing.T) { 42 | n := NewNotifyTest(t, "testdata/vfs.txt") 43 | 44 | ch := NewChans(3) 45 | 46 | n.Watch("src/github.com/rjeczalik/fs", ch[0], Create) 47 | n.Watch("src/github.com/rjeczalik/fs", ch[1], FileNotifyChangeFileName) 48 | n.Watch("src/github.com/rjeczalik/fs", ch[2], FileNotifyChangeDirName) 49 | 50 | cases := []NCase{ 51 | { 52 | Event: rcreate(n.W(), "src/github.com/rjeczalik/fs/.main.cc.swr"), 53 | Receiver: Chans{ch[0], ch[1]}, 54 | }, 55 | } 56 | 57 | n.ExpectNotifyEvents(cases, ch) 58 | } 59 | -------------------------------------------------------------------------------- /notify_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build darwin || linux || freebsd || dragonfly || netbsd || openbsd || windows || solaris 6 | // +build darwin linux freebsd dragonfly netbsd openbsd windows solaris 7 | 8 | package notify 9 | 10 | import ( 11 | "errors" 12 | "os" 13 | "path/filepath" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | func TestNotifyExample(t *testing.T) { 19 | n := NewNotifyTest(t, "testdata/vfs.txt") 20 | 21 | ch := NewChans(3) 22 | 23 | // Watch-points can be set explicitly via Watch/Stop calls... 24 | n.Watch("src/github.com/rjeczalik/fs", ch[0], Write) 25 | n.Watch("src/github.com/pblaszczyk/qttu", ch[0], Write) 26 | n.Watch("src/github.com/pblaszczyk/qttu/...", ch[1], Create) 27 | n.Watch("src/github.com/rjeczalik/fs/cmd/...", ch[2], Remove) 28 | 29 | cases := []NCase{ 30 | // i=0 31 | { 32 | Event: write(n.W(), "src/github.com/rjeczalik/fs/fs.go", []byte("XD")), 33 | Receiver: Chans{ch[0]}, 34 | }, 35 | // TODO(rjeczalik): #62 36 | // i=1 37 | // { 38 | // Event: write(n.W(), "src/github.com/pblaszczyk/qttu/README.md", []byte("XD")), 39 | // Receiver: Chans{ch[0]}, 40 | // }, 41 | // i=2 42 | { 43 | Event: write(n.W(), "src/github.com/rjeczalik/fs/cmd/gotree/go.go", []byte("XD")), 44 | Receiver: nil, 45 | }, 46 | // i=3 47 | { 48 | Event: create(n.W(), "src/github.com/pblaszczyk/qttu/src/.main.cc.swp"), 49 | Receiver: Chans{ch[1]}, 50 | }, 51 | // i=4 52 | { 53 | Event: create(n.W(), "src/github.com/pblaszczyk/qttu/src/.main.cc.swo"), 54 | Receiver: Chans{ch[1]}, 55 | }, 56 | // i=5 57 | { 58 | Event: remove(n.W(), "src/github.com/rjeczalik/fs/cmd/gotree/go.go"), 59 | Receiver: Chans{ch[2]}, 60 | }, 61 | } 62 | 63 | n.ExpectNotifyEvents(cases, ch) 64 | 65 | // ...or using Call structures. 66 | stops := [...]Call{ 67 | // i=0 68 | { 69 | F: FuncStop, 70 | C: ch[0], 71 | }, 72 | // i=1 73 | { 74 | F: FuncStop, 75 | C: ch[1], 76 | }, 77 | } 78 | 79 | n.Call(stops[:]...) 80 | 81 | cases = []NCase{ 82 | // i=0 83 | { 84 | Event: write(n.W(), "src/github.com/rjeczalik/fs/fs.go", []byte("XD")), 85 | Receiver: nil, 86 | }, 87 | // i=1 88 | { 89 | Event: write(n.W(), "src/github.com/pblaszczyk/qttu/README.md", []byte("XD")), 90 | Receiver: nil, 91 | }, 92 | // i=2 93 | { 94 | Event: create(n.W(), "src/github.com/pblaszczyk/qttu/src/.main.cc.swr"), 95 | Receiver: nil, 96 | }, 97 | // i=3 98 | { 99 | Event: remove(n.W(), "src/github.com/rjeczalik/fs/cmd/gotree/main.go"), 100 | Receiver: Chans{ch[2]}, 101 | }, 102 | } 103 | 104 | n.ExpectNotifyEvents(cases, ch) 105 | } 106 | 107 | func TestStop(t *testing.T) { 108 | t.Skip("TODO(rjeczalik)") 109 | } 110 | 111 | func TestRenameInRoot(t *testing.T) { 112 | tmpDir := t.TempDir() 113 | 114 | c := make(chan EventInfo, 100) 115 | first := filepath.Join(tmpDir, "foo") 116 | second := filepath.Join(tmpDir, "bar") 117 | file := filepath.Join(second, "file") 118 | 119 | mustT(t, os.Mkdir(first, 0777)) 120 | 121 | if err := Watch(tmpDir+"/...", c, All); err != nil { 122 | t.Fatal(err) 123 | } 124 | defer Stop(c) 125 | 126 | mustT(t, os.Rename(first, second)) 127 | time.Sleep(50 * time.Millisecond) // Need some time to process rename. 128 | fd, err := os.Create(file) 129 | mustT(t, err) 130 | fd.Close() 131 | 132 | timeout := time.After(time.Second) 133 | for { 134 | select { 135 | case ev := <-c: 136 | if samefile(t, ev.Path(), file) { 137 | return 138 | } 139 | t.Log(ev.Path()) 140 | case <-timeout: 141 | t.Fatal("timed out before receiving event") 142 | } 143 | } 144 | } 145 | 146 | func TestRecreated(t *testing.T) { 147 | tmpDir := t.TempDir() 148 | 149 | dir := filepath.Join(tmpDir, "folder") 150 | file := filepath.Join(dir, "file") 151 | 152 | // Start watching 153 | eventChan := make(chan EventInfo, 1000) 154 | mustT(t, Watch(tmpDir+"/...", eventChan, All)) 155 | defer Stop(eventChan) 156 | 157 | recreateFolder := func() { 158 | // Give the sync some time to process events 159 | mustT(t, os.RemoveAll(dir)) 160 | mustT(t, os.Mkdir(dir, 0777)) 161 | time.Sleep(100 * time.Millisecond) 162 | 163 | // Create a file 164 | mustT(t, os.WriteFile(file, []byte("abc"), 0666)) 165 | } 166 | timeout := time.After(5 * time.Second) 167 | checkCreated := func() { 168 | for { 169 | select { 170 | case ev := <-eventChan: 171 | t.Log(ev.Path(), ev.Event()) 172 | if samefile(t, ev.Path(), file) && ev.Event() == Create { 173 | return 174 | } 175 | case <-timeout: 176 | t.Fatal("timed out before receiving event") 177 | } 178 | } 179 | } 180 | 181 | // 1. Create a folder and a file within it 182 | // This will create a node in the internal tree for the subfolder test/folder 183 | // Will create a new inotify watch for the folder 184 | t.Log("######## First ########") 185 | recreateFolder() 186 | checkCreated() 187 | 188 | // 2. Create a folder and a file within it again 189 | // This will set the events for the subfolder test/folder in the internal tree 190 | // Will create a new inotify watch for the folder because events differ 191 | t.Log("######## Second ########") 192 | recreateFolder() 193 | checkCreated() 194 | 195 | // 3. Create a folder and a file within it yet again 196 | // This time no new inotify watch will be created, because the events 197 | // and node already exist in the internal tree and all subsequent events 198 | // are lost, hence there is no event for the created file here anymore 199 | t.Log("######## Third ########") 200 | recreateFolder() 201 | checkCreated() 202 | } 203 | 204 | func mustT(t testing.TB, err error) { 205 | t.Helper() 206 | if err != nil { 207 | t.Fatal(err) 208 | } 209 | } 210 | 211 | func samefile(t *testing.T, p1, p2 string) bool { 212 | // The tests sometimes delete files shortly after creating them. 213 | // That's expected; ignore stat failures. 214 | fi1, err := os.Stat(p1) 215 | if errors.Is(err, os.ErrNotExist) { 216 | return false 217 | } 218 | mustT(t, err) 219 | fi2, err := os.Stat(p2) 220 | if errors.Is(err, os.ErrNotExist) { 221 | return false 222 | } 223 | mustT(t, err) 224 | return os.SameFile(fi1, fi2) 225 | } 226 | -------------------------------------------------------------------------------- /sync_readdcw_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build windows 6 | // +build windows 7 | 8 | package notify 9 | 10 | import ( 11 | "syscall" 12 | "time" 13 | "unsafe" 14 | ) 15 | 16 | var modkernel32 = syscall.NewLazyDLL("kernel32.dll") 17 | var procSetSystemFileCacheSize = modkernel32.NewProc("SetSystemFileCacheSize") 18 | var zero = uintptr(1<<(unsafe.Sizeof(uintptr(0))*8) - 1) 19 | 20 | func Sync() { 21 | // TODO(pknap): does not work without admin privileges, but I'm going 22 | // to hack it. 23 | // r, _, err := procSetSystemFileCacheSize.Call(none, none, 0) 24 | // if r == 0 { 25 | // dbgprint("SetSystemFileCacheSize error:", err) 26 | // } 27 | } 28 | 29 | // UpdateWait pauses the program for some minimal amount of time. This function 30 | // is required only by implementations which work asynchronously. It gives 31 | // watcher structure time to update its internal state. 32 | func UpdateWait() { 33 | time.Sleep(50 * time.Millisecond) 34 | } 35 | -------------------------------------------------------------------------------- /sync_unix_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build !windows 6 | // +build !windows 7 | 8 | package notify 9 | 10 | import "golang.org/x/sys/unix" 11 | 12 | func Sync() { 13 | unix.Sync() 14 | } 15 | 16 | // UpdateWait is required only by windows watcher implementation. On other 17 | // platforms this function is no-op. 18 | func UpdateWait() { 19 | } 20 | -------------------------------------------------------------------------------- /testdata/vfs.txt: -------------------------------------------------------------------------------- 1 | src/github.com/pblaszczyk/qttu/.travis.yml 2 | src/github.com/pblaszczyk/qttu/include/qttu/detail/registrator.hh 3 | src/github.com/pblaszczyk/qttu/include/qttu/detail/registry.hh 4 | src/github.com/pblaszczyk/qttu/include/qttu/runner.hh 5 | src/github.com/pblaszczyk/qttu/LICENSE 6 | src/github.com/pblaszczyk/qttu/qttu.pri 7 | src/github.com/pblaszczyk/qttu/qttu.pro 8 | src/github.com/pblaszczyk/qttu/README.md 9 | src/github.com/pblaszczyk/qttu/src/main.cc 10 | src/github.com/pblaszczyk/qttu/src/reg.cc 11 | src/github.com/ppknap/link/.travis.yml 12 | src/github.com/ppknap/link/include/coost/link/definitions.hpp 13 | src/github.com/ppknap/link/include/coost/link/detail/bundle.hpp 14 | src/github.com/ppknap/link/include/coost/link/detail/container_invoker.hpp 15 | src/github.com/ppknap/link/include/coost/link/detail/container_value_trait.hpp 16 | src/github.com/ppknap/link/include/coost/link/detail/dummy_type.hpp 17 | src/github.com/ppknap/link/include/coost/link/detail/function_trait.hpp 18 | src/github.com/ppknap/link/include/coost/link/detail/immediate_invoker.hpp 19 | src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/always_same.hpp 20 | src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/make_unique.hpp 21 | src/github.com/ppknap/link/include/coost/link/detail/vertex.hpp 22 | src/github.com/ppknap/link/include/coost/link/detail/wire.hpp 23 | src/github.com/ppknap/link/include/coost/link/link.hpp 24 | src/github.com/ppknap/link/include/coost/link.hpp 25 | src/github.com/ppknap/link/Jamroot.jam 26 | src/github.com/ppknap/link/LICENSE.md 27 | src/github.com/ppknap/link/README.md 28 | src/github.com/ppknap/link/test/counter_helper.hpp 29 | src/github.com/ppknap/link/test/Jamfile.jam 30 | src/github.com/ppknap/link/test/test_circular_calls.cpp 31 | src/github.com/ppknap/link/test/test_container.cpp 32 | src/github.com/ppknap/link/test/test_copy.cpp 33 | src/github.com/ppknap/link/test/test_destructor.cpp 34 | src/github.com/ppknap/link/test/test_immediate.cpp 35 | src/github.com/ppknap/link/test/test_initialize.cpp 36 | src/github.com/rjeczalik/fs/.travis.yml 37 | src/github.com/rjeczalik/fs/appveyor.yml 38 | src/github.com/rjeczalik/fs/cmd/gotree/go.go 39 | src/github.com/rjeczalik/fs/cmd/gotree/main.go 40 | src/github.com/rjeczalik/fs/cmd/mktree/main.go 41 | src/github.com/rjeczalik/fs/fs.go 42 | src/github.com/rjeczalik/fs/fsutil/fixture_test.go 43 | src/github.com/rjeczalik/fs/fsutil/fsutil.go 44 | src/github.com/rjeczalik/fs/fsutil/fsutil_test.go 45 | src/github.com/rjeczalik/fs/fsutil/rel.go 46 | src/github.com/rjeczalik/fs/fsutil/rel_test.go 47 | src/github.com/rjeczalik/fs/fsutil/tee.go 48 | src/github.com/rjeczalik/fs/fsutil/tee_test.go 49 | src/github.com/rjeczalik/fs/LICENSE 50 | src/github.com/rjeczalik/fs/memfs/memfs.go 51 | src/github.com/rjeczalik/fs/memfs/memfs_test.go 52 | src/github.com/rjeczalik/fs/memfs/tree.go 53 | src/github.com/rjeczalik/fs/memfs/tree_test.go 54 | src/github.com/rjeczalik/fs/memfs/util.go 55 | src/github.com/rjeczalik/fs/memfs/util_test.go 56 | src/github.com/rjeczalik/fs/README.md 57 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | const buffer = 128 8 | 9 | type tree interface { 10 | Watch(string, chan<- EventInfo, ...Event) error 11 | Stop(chan<- EventInfo) 12 | Close() error 13 | } 14 | 15 | func newTree() tree { 16 | c := make(chan EventInfo, buffer) 17 | w := newWatcher(c) 18 | if rw, ok := w.(recursiveWatcher); ok { 19 | return newRecursiveTree(rw, c) 20 | } 21 | return newNonrecursiveTree(w, c, make(chan EventInfo, buffer)) 22 | } 23 | -------------------------------------------------------------------------------- /tree_nonrecursive.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import "sync" 8 | 9 | // nonrecursiveTree TODO(rjeczalik) 10 | type nonrecursiveTree struct { 11 | rw sync.RWMutex // protects root 12 | root root 13 | w watcher 14 | c chan EventInfo 15 | rec chan EventInfo 16 | } 17 | 18 | // newNonrecursiveTree TODO(rjeczalik) 19 | func newNonrecursiveTree(w watcher, c, rec chan EventInfo) *nonrecursiveTree { 20 | if rec == nil { 21 | rec = make(chan EventInfo, buffer) 22 | } 23 | t := &nonrecursiveTree{ 24 | root: root{nd: newnode("")}, 25 | w: w, 26 | c: c, 27 | rec: rec, 28 | } 29 | go t.dispatch(c) 30 | go t.internal(rec) 31 | return t 32 | } 33 | 34 | // dispatch TODO(rjeczalik) 35 | func (t *nonrecursiveTree) dispatch(c <-chan EventInfo) { 36 | for ei := range c { 37 | dbgprintf("dispatching %v on %q", ei.Event(), ei.Path()) 38 | go func(ei EventInfo) { 39 | var nd node 40 | var isrec bool 41 | dir, base := split(ei.Path()) 42 | fn := func(it node, isbase bool) error { 43 | isrec = isrec || it.Watch.IsRecursive() 44 | if isbase { 45 | nd = it 46 | } else { 47 | it.Watch.Dispatch(ei, recursive) 48 | } 49 | return nil 50 | } 51 | t.rw.RLock() 52 | // Notify recursive watchpoints found on the path. 53 | if err := t.root.WalkPath(dir, fn); err != nil { 54 | dbgprint("dispatch did not reach leaf:", err) 55 | t.rw.RUnlock() 56 | return 57 | } 58 | // Notify parent watchpoint. 59 | nd.Watch.Dispatch(ei, 0) 60 | isrec = isrec || nd.Watch.IsRecursive() 61 | // If leaf watchpoint exists, notify it. 62 | if nd, ok := nd.Child[base]; ok { 63 | isrec = isrec || nd.Watch.IsRecursive() 64 | nd.Watch.Dispatch(ei, 0) 65 | } 66 | t.rw.RUnlock() 67 | // If the event describes newly leaf directory created within 68 | if !isrec || ei.Event()&(Create|Remove) == 0 { 69 | return 70 | } 71 | if ok, err := ei.(isDirer).isDir(); !ok || err != nil { 72 | return 73 | } 74 | t.rec <- ei 75 | }(ei) 76 | } 77 | } 78 | 79 | // internal TODO(rjeczalik) 80 | func (t *nonrecursiveTree) internal(rec <-chan EventInfo) { 81 | for ei := range rec { 82 | t.rw.Lock() 83 | if ei.Event() == Remove { 84 | nd, err := t.root.Get(ei.Path()) 85 | if err != nil { 86 | t.rw.Unlock() 87 | continue 88 | } 89 | t.walkWatchpoint(nd, func(_ Event, nd node) error { 90 | t.w.Unwatch(nd.Name) 91 | return nil 92 | }) 93 | t.root.Del(ei.Path()) 94 | t.rw.Unlock() 95 | continue 96 | } 97 | var nd node 98 | var eset = internal 99 | t.root.WalkPath(ei.Path(), func(it node, _ bool) error { 100 | if e := it.Watch[t.rec]; e != 0 && e > eset { 101 | eset = e 102 | } 103 | nd = it 104 | return nil 105 | }) 106 | if eset == internal { 107 | t.rw.Unlock() 108 | continue 109 | } 110 | if ei.Path() != nd.Name { 111 | nd = nd.Add(ei.Path()) 112 | } 113 | err := nd.AddDir(t.recFunc(eset)) 114 | t.rw.Unlock() 115 | if err != nil { 116 | dbgprintf("internal(%p) error: %v", rec, err) 117 | } 118 | } 119 | } 120 | 121 | // watchAdd TODO(rjeczalik) 122 | func (t *nonrecursiveTree) watchAdd(nd node, c chan<- EventInfo, e Event) eventDiff { 123 | if e&recursive != 0 { 124 | diff := nd.Watch.Add(t.rec, e|Create|omit) 125 | nd.Watch.Add(c, e) 126 | return diff 127 | } 128 | return nd.Watch.Add(c, e) 129 | } 130 | 131 | // watchDelMin TODO(rjeczalik) 132 | func (t *nonrecursiveTree) watchDelMin(min Event, nd node, c chan<- EventInfo, e Event) eventDiff { 133 | old, ok := nd.Watch[t.rec] 134 | if ok { 135 | nd.Watch[t.rec] = min 136 | } 137 | diff := nd.Watch.Del(c, e) 138 | if ok { 139 | switch old &^= diff[0] &^ diff[1]; { 140 | case old|internal == internal: 141 | delete(nd.Watch, t.rec) 142 | if set, ok := nd.Watch[nil]; ok && len(nd.Watch) == 1 && set == 0 { 143 | delete(nd.Watch, nil) 144 | } 145 | default: 146 | nd.Watch.Add(t.rec, old|Create) 147 | switch { 148 | case diff == none: 149 | case diff[1]|Create == diff[0]: 150 | diff = none 151 | default: 152 | diff[1] |= Create 153 | } 154 | } 155 | } 156 | return diff 157 | } 158 | 159 | // watchDel TODO(rjeczalik) 160 | func (t *nonrecursiveTree) watchDel(nd node, c chan<- EventInfo, e Event) eventDiff { 161 | return t.watchDelMin(0, nd, c, e) 162 | } 163 | 164 | // Watch TODO(rjeczalik) 165 | func (t *nonrecursiveTree) Watch(path string, c chan<- EventInfo, events ...Event) error { 166 | if c == nil { 167 | panic("notify: Watch using nil channel") 168 | } 169 | // Expanding with empty event set is a nop. 170 | if len(events) == 0 { 171 | return nil 172 | } 173 | path, isrec, err := cleanpath(path) 174 | if err != nil { 175 | return err 176 | } 177 | eset := joinevents(events) 178 | t.rw.Lock() 179 | defer t.rw.Unlock() 180 | nd := t.root.Add(path) 181 | if isrec { 182 | return t.watchrec(nd, c, eset|recursive) 183 | } 184 | return t.watch(nd, c, eset) 185 | } 186 | 187 | func (t *nonrecursiveTree) watch(nd node, c chan<- EventInfo, e Event) (err error) { 188 | diff := nd.Watch.Add(c, e) 189 | switch { 190 | case diff == none: 191 | return nil 192 | case diff[1] == 0: 193 | // TODO(rjeczalik): cleanup this panic after implementation is stable 194 | panic("eset is empty: " + nd.Name) 195 | case diff[0] == 0: 196 | err = t.w.Watch(nd.Name, diff[1]) 197 | default: 198 | err = t.w.Rewatch(nd.Name, diff[0], diff[1]) 199 | } 200 | if err != nil { 201 | nd.Watch.Del(c, diff.Event()) 202 | return err 203 | } 204 | return nil 205 | } 206 | 207 | func (t *nonrecursiveTree) recFunc(e Event) walkFunc { 208 | return func(nd node) error { 209 | switch diff := nd.Watch.Add(t.rec, e|omit|Create); { 210 | case diff == none: 211 | case diff[1] == 0: 212 | // TODO(rjeczalik): cleanup this panic after implementation is stable 213 | panic("eset is empty: " + nd.Name) 214 | case diff[0] == 0: 215 | t.w.Watch(nd.Name, diff[1]) 216 | default: 217 | t.w.Rewatch(nd.Name, diff[0], diff[1]) 218 | } 219 | return nil 220 | } 221 | } 222 | 223 | func (t *nonrecursiveTree) watchrec(nd node, c chan<- EventInfo, e Event) error { 224 | var traverse func(walkFunc) error 225 | // Non-recursive tree listens on Create event for every recursive 226 | // watchpoint in order to automagically set a watch for every 227 | // created directory. 228 | switch diff := nd.Watch.dryAdd(t.rec, e|Create); { 229 | case diff == none: 230 | t.watchAdd(nd, c, e) 231 | nd.Watch.Add(t.rec, e|omit|Create) 232 | return nil 233 | case diff[1] == 0: 234 | // TODO(rjeczalik): cleanup this panic after implementation is stable 235 | panic("eset is empty: " + nd.Name) 236 | case diff[0] == 0: 237 | // TODO(rjeczalik): BFS into directories and skip subtree as soon as first 238 | // recursive watchpoint is encountered. 239 | traverse = nd.AddDir 240 | default: 241 | traverse = nd.Walk 242 | } 243 | // TODO(rjeczalik): account every path that failed to be (re)watched 244 | // and retry. 245 | if err := traverse(t.recFunc(e)); err != nil { 246 | return err 247 | } 248 | t.watchAdd(nd, c, e) 249 | return nil 250 | } 251 | 252 | type walkWatchpointFunc func(Event, node) error 253 | 254 | func (t *nonrecursiveTree) walkWatchpoint(nd node, fn walkWatchpointFunc) error { 255 | type minode struct { 256 | min Event 257 | nd node 258 | } 259 | mnd := minode{nd: nd} 260 | stack := []minode{mnd} 261 | Traverse: 262 | for n := len(stack); n != 0; n = len(stack) { 263 | mnd, stack = stack[n-1], stack[:n-1] 264 | // There must be no recursive watchpoints if the node has no watchpoints 265 | // itself (every node in subtree rooted at recursive watchpoints must 266 | // have at least nil (total) and t.rec watchpoints). 267 | if len(mnd.nd.Watch) != 0 { 268 | switch err := fn(mnd.min, mnd.nd); err { 269 | case nil: 270 | case errSkip: 271 | continue Traverse 272 | default: 273 | return err 274 | } 275 | } 276 | for _, nd := range mnd.nd.Child { 277 | stack = append(stack, minode{mnd.nd.Watch[t.rec], nd}) 278 | } 279 | } 280 | return nil 281 | } 282 | 283 | // Stop TODO(rjeczalik) 284 | func (t *nonrecursiveTree) Stop(c chan<- EventInfo) { 285 | fn := func(min Event, nd node) error { 286 | // TODO(rjeczalik): aggregate watcher errors and retry; in worst case 287 | // forward to the user. 288 | switch diff := t.watchDelMin(min, nd, c, all); { 289 | case diff == none: 290 | return nil 291 | case diff[1] == 0: 292 | t.w.Unwatch(nd.Name) 293 | default: 294 | t.w.Rewatch(nd.Name, diff[0], diff[1]) 295 | } 296 | return nil 297 | } 298 | t.rw.Lock() 299 | err := t.walkWatchpoint(t.root.nd, fn) // TODO(rjeczalik): store max root per c 300 | t.rw.Unlock() 301 | dbgprintf("Stop(%p) error: %v\n", c, err) 302 | } 303 | 304 | // Close TODO(rjeczalik) 305 | func (t *nonrecursiveTree) Close() error { 306 | err := t.w.Close() 307 | close(t.c) 308 | return err 309 | } 310 | -------------------------------------------------------------------------------- /tree_nonrecursive_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | ) 11 | 12 | func TestNonrecursiveTree(t *testing.T) { 13 | n := NewNonrecursiveTreeTest(t, "testdata/vfs.txt") 14 | 15 | ch := NewChans(5) 16 | 17 | watches := [...]RCase{ 18 | // i=0 19 | { 20 | Call: Call{ 21 | F: FuncWatch, 22 | P: "src/github.com/rjeczalik/fs/fs.go", 23 | C: ch[0], 24 | E: Rename, 25 | }, 26 | Record: []Call{ 27 | { 28 | F: FuncWatch, 29 | P: "src/github.com/rjeczalik/fs/fs.go", 30 | E: Rename, 31 | }, 32 | }, 33 | }, 34 | // i=1 35 | { 36 | Call: Call{ 37 | F: FuncWatch, 38 | P: "src/github.com/rjeczalik/fs/cmd/...", 39 | C: ch[1], 40 | E: Remove, 41 | }, 42 | Record: []Call{ 43 | { 44 | F: FuncWatch, 45 | P: "src/github.com/rjeczalik/fs/cmd", 46 | E: Create | Remove, 47 | }, 48 | { 49 | F: FuncWatch, 50 | P: "src/github.com/rjeczalik/fs/cmd/gotree", 51 | E: Create | Remove, 52 | }, 53 | { 54 | F: FuncWatch, 55 | P: "src/github.com/rjeczalik/fs/cmd/mktree", 56 | E: Create | Remove, 57 | }, 58 | }, 59 | }, 60 | // i=2 61 | { 62 | Call: Call{ 63 | F: FuncWatch, 64 | P: "src/github.com/rjeczalik/fs/cmd/...", 65 | C: ch[2], 66 | E: Rename, 67 | }, 68 | Record: []Call{ 69 | { 70 | F: FuncRewatch, 71 | P: "src/github.com/rjeczalik/fs/cmd", 72 | E: Create | Remove, 73 | NE: Create | Remove | Rename, 74 | }, 75 | { 76 | F: FuncRewatch, 77 | P: "src/github.com/rjeczalik/fs/cmd/gotree", 78 | E: Create | Remove, 79 | NE: Create | Remove | Rename, 80 | }, 81 | { 82 | F: FuncRewatch, 83 | P: "src/github.com/rjeczalik/fs/cmd/mktree", 84 | E: Create | Remove, 85 | NE: Create | Remove | Rename, 86 | }, 87 | }, 88 | }, 89 | // i=3 90 | { 91 | Call: Call{ 92 | F: FuncWatch, 93 | P: "src/github.com/rjeczalik/fs/cmd/mktree/...", 94 | C: ch[2], 95 | E: Write, 96 | }, 97 | Record: []Call{ 98 | { 99 | F: FuncRewatch, 100 | P: "src/github.com/rjeczalik/fs/cmd/mktree", 101 | E: Create | Remove | Rename, 102 | NE: Create | Remove | Rename | Write, 103 | }, 104 | }, 105 | }, 106 | // i=4 107 | { 108 | Call: Call{ 109 | F: FuncWatch, 110 | P: "src/github.com/pblaszczyk/qttu/include", 111 | C: ch[3], 112 | E: Create, 113 | }, 114 | Record: []Call{ 115 | { 116 | F: FuncWatch, 117 | P: "src/github.com/pblaszczyk/qttu/include", 118 | E: Create, 119 | }, 120 | }, 121 | }, 122 | // i=5 123 | { 124 | Call: Call{ 125 | F: FuncWatch, 126 | P: "src/github.com/pblaszczyk/qttu/include/qttu/detail/...", 127 | C: ch[3], 128 | E: Write, 129 | }, 130 | Record: []Call{ 131 | { 132 | F: FuncWatch, 133 | P: "src/github.com/pblaszczyk/qttu/include/qttu/detail", 134 | E: Create | Write, 135 | }, 136 | }, 137 | }, 138 | // i=6 139 | { 140 | Call: Call{ 141 | F: FuncWatch, 142 | P: "src/github.com/pblaszczyk/qttu/include/...", 143 | C: ch[0], 144 | E: Rename, 145 | }, 146 | Record: []Call{ 147 | { 148 | F: FuncRewatch, 149 | P: "src/github.com/pblaszczyk/qttu/include", 150 | E: Create, 151 | NE: Create | Rename, 152 | }, 153 | { 154 | F: FuncWatch, 155 | P: "src/github.com/pblaszczyk/qttu/include/qttu", 156 | E: Create | Rename, 157 | }, 158 | { 159 | F: FuncRewatch, 160 | P: "src/github.com/pblaszczyk/qttu/include/qttu/detail", 161 | E: Create | Write, 162 | NE: Create | Write | Rename, 163 | }, 164 | }, 165 | }, 166 | // i=7 167 | { 168 | Call: Call{ 169 | F: FuncWatch, 170 | P: "src/github.com/pblaszczyk/...", 171 | C: ch[1], 172 | E: Write, 173 | }, 174 | Record: []Call{ 175 | { 176 | F: FuncWatch, 177 | P: "src/github.com/pblaszczyk", 178 | E: Create | Write, 179 | }, 180 | { 181 | F: FuncWatch, 182 | P: "src/github.com/pblaszczyk/qttu", 183 | E: Create | Write, 184 | }, 185 | { 186 | F: FuncRewatch, 187 | P: "src/github.com/pblaszczyk/qttu/include", 188 | E: Create | Rename, 189 | NE: Create | Rename | Write, 190 | }, 191 | { 192 | F: FuncRewatch, 193 | P: "src/github.com/pblaszczyk/qttu/include/qttu", 194 | E: Create | Rename, 195 | NE: Create | Rename | Write, 196 | }, 197 | { 198 | F: FuncWatch, 199 | P: "src/github.com/pblaszczyk/qttu/src", 200 | E: Create | Write, 201 | }, 202 | }, 203 | }, 204 | // i=8 205 | { 206 | Call: Call{ 207 | F: FuncWatch, 208 | P: "src/github.com/pblaszczyk/qttu/include/...", 209 | C: ch[4], 210 | E: Write, 211 | }, 212 | Record: nil, 213 | }, 214 | // i=9 215 | { 216 | Call: Call{ 217 | F: FuncWatch, 218 | P: "src/github.com/pblaszczyk/qttu", 219 | C: ch[3], 220 | E: Remove, 221 | }, 222 | Record: []Call{ 223 | { 224 | F: FuncRewatch, 225 | P: "src/github.com/pblaszczyk/qttu", 226 | E: Create | Write, 227 | NE: Create | Write | Remove, 228 | }, 229 | }, 230 | }, 231 | } 232 | 233 | n.ExpectRecordedCalls(watches[:]) 234 | 235 | events := [...]TCase{ 236 | // i=0 237 | { 238 | Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Rename}, 239 | Receiver: Chans{ch[0]}, 240 | }, 241 | // i=1 242 | { 243 | Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Create}, 244 | Receiver: nil, 245 | }, 246 | // i=2 247 | { 248 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/cmd.go", E: Remove}, 249 | Receiver: Chans{ch[1]}, 250 | }, 251 | // i=3 252 | { 253 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/doc.go", E: Write}, 254 | Receiver: nil, 255 | }, 256 | // i=4 257 | { 258 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree/main.go", E: Write}, 259 | Receiver: Chans{ch[2]}, 260 | }, 261 | // i=5 262 | { 263 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree/tree.go", E: Create}, 264 | Receiver: nil, 265 | }, 266 | // i=6 267 | { 268 | Event: Call{P: "src/github.com/pblaszczyk/qttu/include/.lock", E: Create}, 269 | Receiver: Chans{ch[3]}, 270 | }, 271 | // i=7 272 | { 273 | Event: Call{P: "src/github.com/pblaszczyk/qttu/include/qttu/detail/registry.hh", E: Write}, 274 | Receiver: Chans{ch[3], ch[1], ch[4]}, 275 | }, 276 | // i=8 277 | { 278 | Event: Call{P: "src/github.com/pblaszczyk/qttu/include/qttu", E: Remove}, 279 | Receiver: nil, 280 | }, 281 | // i=9 282 | { 283 | Event: Call{P: "src/github.com/pblaszczyk/qttu/include", E: Remove}, 284 | Receiver: Chans{ch[3]}, 285 | }, 286 | } 287 | 288 | n.ExpectTreeEvents(events[:], ch) 289 | 290 | stops := [...]RCase{ 291 | // i=0 292 | { 293 | Call: Call{ 294 | F: FuncStop, 295 | C: ch[4], 296 | }, 297 | Record: nil, 298 | }, 299 | // i=1 300 | { 301 | Call: Call{ 302 | F: FuncStop, 303 | C: ch[3], 304 | }, 305 | Record: []Call{ 306 | { 307 | F: FuncRewatch, 308 | P: "src/github.com/pblaszczyk/qttu", 309 | E: Create | Write | Remove, 310 | NE: Create | Write, 311 | }, 312 | }, 313 | }, 314 | // i=2 315 | { 316 | Call: Call{ 317 | F: FuncStop, 318 | C: ch[2], 319 | }, 320 | Record: []Call{ 321 | { 322 | F: FuncRewatch, 323 | P: "src/github.com/rjeczalik/fs/cmd", 324 | E: Create | Remove | Rename, 325 | NE: Create | Remove, 326 | }, 327 | { 328 | F: FuncRewatch, 329 | P: "src/github.com/rjeczalik/fs/cmd/gotree", 330 | E: Create | Remove | Rename, 331 | NE: Create | Remove, 332 | }, 333 | { 334 | F: FuncRewatch, 335 | P: "src/github.com/rjeczalik/fs/cmd/mktree", 336 | E: Create | Remove | Rename | Write, 337 | NE: Create | Remove, 338 | }, 339 | }, 340 | }, 341 | // i=3 342 | { 343 | Call: Call{ 344 | F: FuncStop, 345 | C: ch[1], 346 | }, 347 | Record: []Call{ 348 | { 349 | F: FuncUnwatch, 350 | P: "src/github.com/pblaszczyk", 351 | }, 352 | { 353 | F: FuncUnwatch, 354 | P: "src/github.com/pblaszczyk/qttu", 355 | }, 356 | { 357 | F: FuncRewatch, 358 | P: "src/github.com/pblaszczyk/qttu/include", 359 | E: Create | Rename | Write, 360 | NE: Create | Rename, 361 | }, 362 | { 363 | F: FuncRewatch, 364 | P: "src/github.com/pblaszczyk/qttu/include/qttu", 365 | E: Create | Rename | Write, 366 | NE: Create | Rename, 367 | }, 368 | { 369 | F: FuncRewatch, 370 | P: "src/github.com/pblaszczyk/qttu/include/qttu/detail", 371 | E: Create | Rename | Write, 372 | NE: Create | Rename, 373 | }, 374 | { 375 | F: FuncUnwatch, 376 | P: "src/github.com/pblaszczyk/qttu/src", 377 | }, 378 | { 379 | F: FuncUnwatch, 380 | P: "src/github.com/rjeczalik/fs/cmd", 381 | }, 382 | { 383 | F: FuncUnwatch, 384 | P: "src/github.com/rjeczalik/fs/cmd/gotree", 385 | }, 386 | { 387 | F: FuncUnwatch, 388 | P: "src/github.com/rjeczalik/fs/cmd/mktree", 389 | }, 390 | }, 391 | }, 392 | // i=4 393 | { 394 | Call: Call{ 395 | F: FuncStop, 396 | C: ch[0], 397 | }, 398 | Record: []Call{ 399 | { 400 | F: FuncUnwatch, 401 | P: "src/github.com/pblaszczyk/qttu/include", 402 | }, 403 | { 404 | F: FuncUnwatch, 405 | P: "src/github.com/pblaszczyk/qttu/include/qttu", 406 | }, 407 | { 408 | F: FuncUnwatch, 409 | P: "src/github.com/pblaszczyk/qttu/include/qttu/detail", 410 | }, 411 | { 412 | F: FuncUnwatch, 413 | P: "src/github.com/rjeczalik/fs/fs.go", 414 | }, 415 | }, 416 | }, 417 | } 418 | 419 | n.ExpectRecordedCalls(stops[:]) 420 | 421 | n.Walk(func(nd node) error { 422 | if len(nd.Watch) != 0 { 423 | return fmt.Errorf("unexpected watchpoint: name=%s, eventset=%v (len=%d)", 424 | nd.Name, nd.Watch.Total(), len(nd.Watch)) 425 | } 426 | return nil 427 | }) 428 | } 429 | 430 | func TestNonrecursiveTreeInternal(t *testing.T) { 431 | n, c := NewNonrecursiveTreeTestC(t, "testdata/vfs.txt") 432 | 433 | ch := NewChans(5) 434 | 435 | watches := [...]RCase{ 436 | // i=0 437 | { 438 | Call: Call{ 439 | F: FuncWatch, 440 | P: "src/github.com/rjeczalik/fs/cmd/...", 441 | C: ch[0], 442 | E: Remove, 443 | }, 444 | Record: []Call{ 445 | { 446 | F: FuncWatch, 447 | P: "src/github.com/rjeczalik/fs/cmd", 448 | E: Create | Remove, 449 | }, 450 | { 451 | F: FuncWatch, 452 | P: "src/github.com/rjeczalik/fs/cmd/gotree", 453 | E: Create | Remove, 454 | }, 455 | { 456 | F: FuncWatch, 457 | P: "src/github.com/rjeczalik/fs/cmd/mktree", 458 | E: Create | Remove, 459 | }, 460 | }, 461 | }, 462 | // i=1 463 | { 464 | Call: Call{ 465 | F: FuncWatch, 466 | P: "src/github.com/ppknap/link/include/coost/...", 467 | C: ch[1], 468 | E: Create, 469 | }, 470 | Record: []Call{ 471 | { 472 | F: FuncWatch, 473 | P: "src/github.com/ppknap/link/include/coost", 474 | E: Create, 475 | }, 476 | { 477 | F: FuncWatch, 478 | P: "src/github.com/ppknap/link/include/coost/link", 479 | E: Create, 480 | }, 481 | { 482 | F: FuncWatch, 483 | P: "src/github.com/ppknap/link/include/coost/link/detail", 484 | E: Create, 485 | }, 486 | { 487 | F: FuncWatch, 488 | P: "src/github.com/ppknap/link/include/coost/link/detail/stdhelpers", 489 | E: Create, 490 | }, 491 | }, 492 | }, 493 | } 494 | 495 | n.ExpectRecordedCalls(watches[:]) 496 | 497 | events := [...]TCase{ 498 | // i=0 499 | { 500 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/dir", E: Create, Dir: true}, 501 | Receiver: Chans{c}, 502 | }, 503 | // i=1 504 | { 505 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/dir/another", E: Create, Dir: true}, 506 | Receiver: Chans{c}, 507 | }, 508 | // i=2 509 | { 510 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/file", E: Create, Dir: false}, 511 | Receiver: nil, 512 | }, 513 | // i=3 514 | { 515 | Event: Call{P: "src/github.com/ppknap/link/include/coost/dir", E: Create, Dir: true}, 516 | Receiver: Chans{ch[1], c}, 517 | }, 518 | // i=4 519 | { 520 | Event: Call{P: "src/github.com/ppknap/link/include/coost/dir/another", E: Create, Dir: true}, 521 | Receiver: Chans{ch[1], c}, 522 | }, 523 | // i=5 524 | { 525 | Event: Call{P: "src/github.com/ppknap/link/include/coost/file", E: Create, Dir: false}, 526 | Receiver: Chans{ch[1]}, 527 | }, 528 | // i=6 529 | { 530 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree", E: Remove}, 531 | Receiver: Chans{ch[0]}, 532 | }, 533 | // i=7 534 | { 535 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/rmtree", E: Create, Dir: true}, 536 | Receiver: Chans{c}, 537 | }, 538 | } 539 | 540 | n.ExpectTreeEvents(events[:], ch) 541 | } 542 | -------------------------------------------------------------------------------- /tree_recursive.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import "sync" 8 | 9 | // watchAdd TODO(rjeczalik) 10 | func watchAdd(nd node, c chan<- EventInfo, e Event) eventDiff { 11 | diff := nd.Watch.Add(c, e) 12 | if wp := nd.Child[""].Watch; len(wp) != 0 { 13 | e = wp.Total() 14 | diff[0] |= e 15 | diff[1] |= e 16 | if diff[0] == diff[1] { 17 | return none 18 | } 19 | } 20 | return diff 21 | } 22 | 23 | // watchAddInactive TODO(rjeczalik) 24 | func watchAddInactive(nd node, c chan<- EventInfo, e Event) eventDiff { 25 | wp := nd.Child[""].Watch 26 | if wp == nil { 27 | wp = make(watchpoint) 28 | nd.Child[""] = node{Watch: wp} 29 | } 30 | diff := wp.Add(c, e) 31 | e = nd.Watch.Total() 32 | diff[0] |= e 33 | diff[1] |= e 34 | if diff[0] == diff[1] { 35 | return none 36 | } 37 | return diff 38 | } 39 | 40 | // watchCopy TODO(rjeczalik) 41 | func watchCopy(src, dst node) { 42 | for c, e := range src.Watch { 43 | if c == nil { 44 | continue 45 | } 46 | watchAddInactive(dst, c, e) 47 | } 48 | if wpsrc := src.Child[""].Watch; len(wpsrc) != 0 { 49 | wpdst := dst.Child[""].Watch 50 | for c, e := range wpsrc { 51 | if c == nil { 52 | continue 53 | } 54 | wpdst.Add(c, e) 55 | } 56 | } 57 | } 58 | 59 | // watchDel TODO(rjeczalik) 60 | func watchDel(nd node, c chan<- EventInfo, e Event) eventDiff { 61 | diff := nd.Watch.Del(c, e) 62 | if wp := nd.Child[""].Watch; len(wp) != 0 { 63 | diffInactive := wp.Del(c, e) 64 | e = wp.Total() 65 | // TODO(rjeczalik): add e if e != all? 66 | diff[0] |= diffInactive[0] | e 67 | diff[1] |= diffInactive[1] | e 68 | if diff[0] == diff[1] { 69 | return none 70 | } 71 | } 72 | return diff 73 | } 74 | 75 | // watchTotal TODO(rjeczalik) 76 | func watchTotal(nd node) Event { 77 | e := nd.Watch.Total() 78 | if wp := nd.Child[""].Watch; len(wp) != 0 { 79 | e |= wp.Total() 80 | } 81 | return e 82 | } 83 | 84 | // watchIsRecursive TODO(rjeczalik) 85 | func watchIsRecursive(nd node) bool { 86 | ok := nd.Watch.IsRecursive() 87 | // TODO(rjeczalik): add a test for len(wp) != 0 change the condition. 88 | if wp := nd.Child[""].Watch; len(wp) != 0 { 89 | // If a watchpoint holds inactive watchpoints, it means it's a parent 90 | // one, which is recursive by nature even though it may be not recursive 91 | // itself. 92 | ok = true 93 | } 94 | return ok 95 | } 96 | 97 | // recursiveTree TODO(rjeczalik) 98 | type recursiveTree struct { 99 | rw sync.RWMutex // protects root 100 | root root 101 | // TODO(rjeczalik): merge watcher + recursiveWatcher after #5 and #6 102 | w interface { 103 | watcher 104 | recursiveWatcher 105 | } 106 | c chan EventInfo 107 | } 108 | 109 | // newRecursiveTree TODO(rjeczalik) 110 | func newRecursiveTree(w recursiveWatcher, c chan EventInfo) *recursiveTree { 111 | t := &recursiveTree{ 112 | root: root{nd: newnode("")}, 113 | w: struct { 114 | watcher 115 | recursiveWatcher 116 | }{w.(watcher), w}, 117 | c: c, 118 | } 119 | go t.dispatch() 120 | return t 121 | } 122 | 123 | // dispatch TODO(rjeczalik) 124 | func (t *recursiveTree) dispatch() { 125 | for ei := range t.c { 126 | dbgprintf("dispatching %v on %q", ei.Event(), ei.Path()) 127 | go func(ei EventInfo) { 128 | nd, ok := node{}, false 129 | dir, base := split(ei.Path()) 130 | fn := func(it node, isbase bool) error { 131 | if isbase { 132 | nd = it 133 | } else { 134 | it.Watch.Dispatch(ei, recursive) 135 | } 136 | return nil 137 | } 138 | t.rw.RLock() 139 | defer t.rw.RUnlock() 140 | // Notify recursive watchpoints found on the path. 141 | if err := t.root.WalkPath(dir, fn); err != nil { 142 | dbgprint("dispatch did not reach leaf:", err) 143 | return 144 | } 145 | // Notify parent watchpoint. 146 | nd.Watch.Dispatch(ei, 0) 147 | // If leaf watchpoint exists, notify it. 148 | if nd, ok = nd.Child[base]; ok { 149 | nd.Watch.Dispatch(ei, 0) 150 | } 151 | }(ei) 152 | } 153 | } 154 | 155 | // Watch TODO(rjeczalik) 156 | func (t *recursiveTree) Watch(path string, c chan<- EventInfo, events ...Event) error { 157 | if c == nil { 158 | panic("notify: Watch using nil channel") 159 | } 160 | // Expanding with empty event set is a nop. 161 | if len(events) == 0 { 162 | return nil 163 | } 164 | path, isrec, err := cleanpath(path) 165 | if err != nil { 166 | return err 167 | } 168 | eventset := joinevents(events) 169 | if isrec { 170 | eventset |= recursive 171 | } 172 | t.rw.Lock() 173 | defer t.rw.Unlock() 174 | // case 1: cur is a child 175 | // 176 | // Look for parent watch which already covers the given path. 177 | parent := node{} 178 | self := false 179 | err = t.root.WalkPath(path, func(nd node, isbase bool) error { 180 | if watchTotal(nd) != 0 { 181 | parent = nd 182 | self = isbase 183 | return errSkip 184 | } 185 | return nil 186 | }) 187 | cur := t.root.Add(path) // add after the walk, so it's less to traverse 188 | if err == nil && parent.Watch != nil { 189 | // Parent watch found. Register inactive watchpoint, so we have enough 190 | // information to shrink the eventset on eventual Stop. 191 | // return t.resetwatchpoint(parent, parent, c, eventset|inactive) 192 | var diff eventDiff 193 | if self { 194 | diff = watchAdd(cur, c, eventset) 195 | } else { 196 | diff = watchAddInactive(parent, c, eventset) 197 | } 198 | switch { 199 | case diff == none: 200 | // the parent watchpoint already covers requested subtree with its 201 | // eventset 202 | case diff[0] == 0: 203 | // TODO(rjeczalik): cleanup this panic after implementation is stable 204 | panic("dangling watchpoint: " + parent.Name) 205 | default: 206 | if isrec || watchIsRecursive(parent) { 207 | err = t.w.RecursiveRewatch(parent.Name, parent.Name, diff[0], diff[1]) 208 | } else { 209 | err = t.w.Rewatch(parent.Name, diff[0], diff[1]) 210 | } 211 | if err != nil { 212 | watchDel(parent, c, diff.Event()) 213 | return err 214 | } 215 | watchAdd(cur, c, eventset) 216 | // TODO(rjeczalik): account top-most path for c 217 | return nil 218 | } 219 | if !self { 220 | watchAdd(cur, c, eventset) 221 | } 222 | return nil 223 | } 224 | // case 2: cur is new parent 225 | // 226 | // Look for children nodes, unwatch n-1 of them and rewatch the last one. 227 | var children []node 228 | fn := func(nd node) error { 229 | if len(nd.Watch) == 0 { 230 | return nil 231 | } 232 | children = append(children, nd) 233 | return errSkip 234 | } 235 | switch must(cur.Walk(fn)); len(children) { 236 | case 0: 237 | // no child watches, cur holds a new watch 238 | case 1: 239 | watchAdd(cur, c, eventset) // TODO(rjeczalik): update cache c subtree root? 240 | watchCopy(children[0], cur) 241 | err = t.w.RecursiveRewatch(children[0].Name, cur.Name, watchTotal(children[0]), 242 | watchTotal(cur)) 243 | if err != nil { 244 | // Clean inactive watchpoint. The c chan did not exist before. 245 | cur.Child[""] = node{} 246 | delete(cur.Watch, c) 247 | return err 248 | } 249 | return nil 250 | default: 251 | watchAdd(cur, c, eventset) 252 | // Copy children inactive watchpoints to the new parent. 253 | for _, nd := range children { 254 | watchCopy(nd, cur) 255 | } 256 | // Watch parent subtree. 257 | if err = t.w.RecursiveWatch(cur.Name, watchTotal(cur)); err != nil { 258 | // Clean inactive watchpoint. The c chan did not exist before. 259 | cur.Child[""] = node{} 260 | delete(cur.Watch, c) 261 | return err 262 | } 263 | // Unwatch children subtrees. 264 | var e error 265 | for _, nd := range children { 266 | if watchIsRecursive(nd) { 267 | e = t.w.RecursiveUnwatch(nd.Name) 268 | } else { 269 | e = t.w.Unwatch(nd.Name) 270 | } 271 | if e != nil { 272 | err = nonil(err, e) 273 | // TODO(rjeczalik): child is still watched, warn all its watchpoints 274 | // about possible duplicate events via Error event 275 | } 276 | } 277 | return err 278 | } 279 | // case 3: cur is new, alone node 280 | switch diff := watchAdd(cur, c, eventset); { 281 | case diff == none: 282 | // TODO(rjeczalik): cleanup this panic after implementation is stable 283 | panic("watch requested but no parent watchpoint found: " + cur.Name) 284 | case diff[0] == 0: 285 | if isrec { 286 | err = t.w.RecursiveWatch(cur.Name, diff[1]) 287 | } else { 288 | err = t.w.Watch(cur.Name, diff[1]) 289 | } 290 | if err != nil { 291 | watchDel(cur, c, diff.Event()) 292 | return err 293 | } 294 | default: 295 | // TODO(rjeczalik): cleanup this panic after implementation is stable 296 | panic("watch requested but no parent watchpoint found: " + cur.Name) 297 | } 298 | return nil 299 | } 300 | 301 | // Stop TODO(rjeczalik) 302 | // 303 | // TODO(rjeczalik): Split parent watchpoint - transfer watches to children 304 | // if parent is no longer needed. This carries a risk that underlying 305 | // watcher calls could fail - reconsider if it's worth the effort. 306 | func (t *recursiveTree) Stop(c chan<- EventInfo) { 307 | var err error 308 | fn := func(nd node) (e error) { 309 | diff := watchDel(nd, c, all) 310 | switch { 311 | case diff == none && watchTotal(nd) == 0: 312 | // TODO(rjeczalik): There's no watchpoints deeper in the tree, 313 | // probably we should remove the nodes as well. 314 | return nil 315 | case diff == none: 316 | // Removing c from nd does not require shrinking its eventset. 317 | case diff[1] == 0: 318 | if watchIsRecursive(nd) { 319 | e = t.w.RecursiveUnwatch(nd.Name) 320 | } else { 321 | e = t.w.Unwatch(nd.Name) 322 | } 323 | default: 324 | if watchIsRecursive(nd) { 325 | e = t.w.RecursiveRewatch(nd.Name, nd.Name, diff[0], diff[1]) 326 | } else { 327 | e = t.w.Rewatch(nd.Name, diff[0], diff[1]) 328 | } 329 | } 330 | fn := func(nd node) error { 331 | watchDel(nd, c, all) 332 | return nil 333 | } 334 | err = nonil(err, e, nd.Walk(fn)) 335 | // TODO(rjeczalik): if e != nil store dummy chan in nd.Watch just to 336 | // retry un/rewatching next time and/or let the user handle the failure 337 | // vie Error event? 338 | return errSkip 339 | } 340 | t.rw.Lock() 341 | e := t.root.Walk("", fn) // TODO(rjeczalik): use max root per c 342 | t.rw.Unlock() 343 | if e != nil { 344 | err = nonil(err, e) 345 | } 346 | dbgprintf("Stop(%p) error: %v\n", c, err) 347 | } 348 | 349 | // Close TODO(rjeczalik) 350 | func (t *recursiveTree) Close() error { 351 | err := t.w.Close() 352 | close(t.c) 353 | return err 354 | } 355 | -------------------------------------------------------------------------------- /tree_recursive_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import "testing" 8 | 9 | func TestRecursiveTree(t *testing.T) { 10 | n := NewRecursiveTreeTest(t, "testdata/vfs.txt") 11 | 12 | ch := NewChans(5) 13 | 14 | watches := [...]RCase{ 15 | // i=0 16 | { 17 | Call: Call{ 18 | F: FuncWatch, 19 | P: "src/github.com/rjeczalik/fs/fs.go", 20 | C: ch[0], 21 | E: Create, 22 | }, 23 | Record: []Call{ 24 | { 25 | F: FuncWatch, 26 | P: "src/github.com/rjeczalik/fs/fs.go", 27 | E: Create, 28 | }, 29 | }, 30 | }, 31 | // i=1 32 | { 33 | Call: Call{ 34 | F: FuncWatch, 35 | P: "src/github.com/rjeczalik/fs/cmd/...", 36 | C: ch[1], 37 | E: Remove, 38 | }, 39 | Record: []Call{ 40 | { 41 | F: FuncRecursiveWatch, 42 | P: "src/github.com/rjeczalik/fs/cmd", 43 | E: Remove, 44 | }, 45 | }, 46 | }, 47 | // i=2 48 | { 49 | Call: Call{ 50 | F: FuncWatch, 51 | P: "src/github.com/rjeczalik/fs", 52 | C: ch[2], 53 | E: Rename, 54 | }, 55 | Record: []Call{ 56 | { 57 | F: FuncRecursiveWatch, 58 | P: "src/github.com/rjeczalik/fs", 59 | E: Create | Remove | Rename, 60 | }, 61 | { 62 | F: FuncRecursiveUnwatch, 63 | P: "src/github.com/rjeczalik/fs/cmd", 64 | }, 65 | { 66 | F: FuncUnwatch, 67 | P: "src/github.com/rjeczalik/fs/fs.go", 68 | }, 69 | }, 70 | }, 71 | // i=3 72 | { 73 | Call: Call{ 74 | F: FuncWatch, 75 | P: "src/github.com/ppknap/link/README.md", 76 | C: ch[0], 77 | E: Create, 78 | }, 79 | Record: []Call{ 80 | { 81 | F: FuncWatch, 82 | P: "src/github.com/ppknap/link/README.md", 83 | E: Create, 84 | }, 85 | }, 86 | }, 87 | // i=4 88 | { 89 | Call: Call{ 90 | F: FuncWatch, 91 | P: "src/github.com/ppknap/link/include/...", 92 | C: ch[3], 93 | E: Remove, 94 | }, 95 | Record: []Call{ 96 | { 97 | F: FuncRecursiveWatch, 98 | P: "src/github.com/ppknap/link/include", 99 | E: Remove, 100 | }, 101 | }, 102 | }, 103 | // i=5 104 | { 105 | Call: Call{ 106 | F: FuncWatch, 107 | P: "src/github.com/ppknap/link/include", 108 | C: ch[0], 109 | E: Write, 110 | }, 111 | Record: []Call{ 112 | { 113 | F: FuncRecursiveRewatch, 114 | P: "src/github.com/ppknap/link/include", 115 | NP: "src/github.com/ppknap/link/include", 116 | E: Remove, 117 | NE: Remove | Write, 118 | }, 119 | }, 120 | }, 121 | // i=6 122 | { 123 | Call: Call{ 124 | F: FuncWatch, 125 | P: "src/github.com/ppknap/link/test/Jamfile.jam", 126 | C: ch[0], 127 | E: Rename, 128 | }, 129 | Record: []Call{ 130 | { 131 | F: FuncWatch, 132 | P: "src/github.com/ppknap/link/test/Jamfile.jam", 133 | E: Rename, 134 | }, 135 | }, 136 | }, 137 | // i=7 138 | { 139 | Call: Call{ 140 | F: FuncWatch, 141 | P: "src/github.com/ppknap/link/test/Jamfile.jam", 142 | C: ch[0], 143 | E: Create, 144 | }, 145 | Record: []Call{ 146 | { 147 | F: FuncRewatch, 148 | P: "src/github.com/ppknap/link/test/Jamfile.jam", 149 | E: Rename, 150 | NE: Rename | Create, 151 | }, 152 | }, 153 | }, 154 | // i=8 155 | { 156 | Call: Call{ 157 | F: FuncWatch, 158 | P: "src/github.com/ppknap/...", 159 | C: ch[0], 160 | E: Create, 161 | }, 162 | Record: []Call{ 163 | { 164 | F: FuncRecursiveWatch, 165 | P: "src/github.com/ppknap", 166 | E: Create | Remove | Write | Rename, 167 | }, 168 | { 169 | F: FuncUnwatch, 170 | P: "src/github.com/ppknap/link/README.md", 171 | }, 172 | { 173 | F: FuncRecursiveUnwatch, 174 | P: "src/github.com/ppknap/link/include", 175 | }, 176 | { 177 | F: FuncUnwatch, 178 | P: "src/github.com/ppknap/link/test/Jamfile.jam", 179 | }, 180 | }, 181 | }, 182 | // i=9 183 | { 184 | Call: Call{ 185 | F: FuncWatch, 186 | P: "src/github.com/rjeczalik/fs/README.md", 187 | C: ch[0], 188 | E: Rename, 189 | }, 190 | Record: nil, 191 | }, 192 | // i=10 193 | { 194 | Call: Call{ 195 | F: FuncWatch, 196 | P: "src/github.com/rjeczalik/fs/cmd/gotree", 197 | C: ch[2], 198 | E: Create | Remove, 199 | }, 200 | Record: nil, 201 | }, 202 | // i=11 203 | { 204 | Call: Call{ 205 | F: FuncWatch, 206 | P: "src/github.com/pblaszczyk/qttu/src/main.cc", 207 | C: ch[0], 208 | E: Create, 209 | }, 210 | Record: []Call{ 211 | { 212 | F: FuncWatch, 213 | P: "src/github.com/pblaszczyk/qttu/src/main.cc", 214 | E: Create, 215 | }, 216 | }, 217 | }, 218 | // i=12 219 | { 220 | Call: Call{ 221 | F: FuncWatch, 222 | P: "src/github.com/pblaszczyk/qttu/src/main.cc", 223 | C: ch[0], 224 | E: Remove, 225 | }, 226 | Record: []Call{ 227 | { 228 | F: FuncRewatch, 229 | P: "src/github.com/pblaszczyk/qttu/src/main.cc", 230 | E: Create, 231 | NE: Create | Remove, 232 | }, 233 | }, 234 | }, 235 | // i=13 236 | { 237 | Call: Call{ 238 | F: FuncWatch, 239 | P: "src/github.com/pblaszczyk/qttu/src/main.cc", 240 | C: ch[0], 241 | E: Create | Remove, 242 | }, 243 | Record: nil, 244 | }, 245 | // i=14 246 | { 247 | Call: Call{ 248 | F: FuncWatch, 249 | P: "src/github.com/pblaszczyk/qttu/src", 250 | C: ch[0], 251 | E: Create, 252 | }, 253 | Record: []Call{ 254 | { 255 | F: FuncRecursiveRewatch, 256 | P: "src/github.com/pblaszczyk/qttu/src/main.cc", 257 | NP: "src/github.com/pblaszczyk/qttu/src", 258 | E: Create | Remove, 259 | NE: Create | Remove, 260 | }, 261 | }, 262 | }, 263 | // i=15 264 | { 265 | Call: Call{ 266 | F: FuncWatch, 267 | P: "src/github.com/pblaszczyk/qttu", 268 | C: ch[4], 269 | E: Write, 270 | }, 271 | Record: []Call{ 272 | { 273 | F: FuncRecursiveRewatch, 274 | P: "src/github.com/pblaszczyk/qttu/src", 275 | NP: "src/github.com/pblaszczyk/qttu", 276 | E: Create | Remove, 277 | NE: Create | Remove | Write, 278 | }, 279 | }, 280 | }, 281 | // i=16 282 | { 283 | Call: Call{ 284 | F: FuncWatch, 285 | P: "src/github.com/rjeczalik/fs/fs.go", 286 | C: ch[3], 287 | E: Rename, 288 | }, 289 | Record: nil, 290 | }, 291 | } 292 | 293 | n.ExpectRecordedCalls(watches[:]) 294 | 295 | events := [...]TCase{ 296 | // i=0 297 | { 298 | Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Rename}, 299 | Receiver: Chans{ch[2], ch[3]}, 300 | }, 301 | // i=1 302 | { 303 | Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Create}, 304 | Receiver: Chans{ch[0]}, 305 | }, 306 | // i=2 307 | { 308 | Event: Call{P: "src/github.com/rjeczalik/fs/fs.go/file", E: Create}, 309 | Receiver: Chans{ch[0]}, 310 | }, 311 | // i=3 312 | { 313 | Event: Call{P: "src/github.com/rjeczalik/fs", E: Rename}, 314 | Receiver: Chans{ch[2]}, 315 | }, 316 | // i=4 317 | { 318 | Event: Call{P: "src/github.com/rjeczalik/fs/fs_test.go", E: Rename}, 319 | Receiver: Chans{ch[2]}, 320 | }, 321 | // i=5 322 | { 323 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree/main.go", E: Remove}, 324 | Receiver: Chans{ch[1]}, 325 | }, 326 | // i=6 327 | { 328 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/gotree", E: Remove}, 329 | Receiver: Chans{ch[1], ch[2]}, 330 | }, 331 | // i=7 332 | { 333 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd", E: Remove}, 334 | Receiver: Chans{ch[1]}, 335 | }, 336 | // i=8 337 | { 338 | Event: Call{P: "src/github.com/rjeczalik/fs/fs.go/file", E: Write}, 339 | Receiver: nil, 340 | }, 341 | // i=9 342 | { 343 | Event: Call{P: "src/github.com/rjeczalik/fs/fs.go/file", E: Write}, 344 | Receiver: nil, 345 | }, 346 | // i=10 347 | { 348 | Event: Call{P: "src/github.com/rjeczalik/fs", E: Remove}, 349 | Receiver: nil, 350 | }, 351 | // i=11 352 | { 353 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd", E: Rename}, 354 | Receiver: Chans{ch[2]}, 355 | }, 356 | // i=12 357 | { 358 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree/main.go", E: Write}, 359 | Receiver: nil, 360 | }, 361 | // i=13 362 | { 363 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/gotree", E: Rename}, 364 | Receiver: nil, 365 | }, 366 | // i=14 367 | { 368 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/file", E: Rename}, 369 | Receiver: nil, 370 | }, 371 | // i=15 372 | { 373 | Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Rename}, 374 | Receiver: Chans{ch[2], ch[3]}, 375 | }, 376 | } 377 | 378 | n.ExpectTreeEvents(events[:], ch) 379 | 380 | stops := [...]RCase{ 381 | // i=0 382 | { 383 | Call: Call{ 384 | F: FuncStop, 385 | C: ch[1], 386 | }, 387 | Record: nil, 388 | }, 389 | { 390 | Call: Call{ 391 | F: FuncStop, 392 | C: ch[4], 393 | }, 394 | Record: []Call{ 395 | { 396 | F: FuncRecursiveRewatch, 397 | P: "src/github.com/pblaszczyk/qttu", 398 | NP: "src/github.com/pblaszczyk/qttu", 399 | E: Create | Remove | Write, 400 | NE: Create | Remove, 401 | }, 402 | }, 403 | }, 404 | } 405 | 406 | n.ExpectRecordedCalls(stops[:]) 407 | } 408 | 409 | func TestRecursiveTreeWatchInactiveMerge(t *testing.T) { 410 | n := NewRecursiveTreeTest(t, "testdata/vfs.txt") 411 | 412 | ch := NewChans(1) 413 | 414 | watches := [...]RCase{ 415 | // i=0 416 | { 417 | Call: Call{ 418 | F: FuncWatch, 419 | P: "src/github.com/rjeczalik/fs", 420 | C: ch[0], 421 | E: Create, 422 | }, 423 | Record: []Call{ 424 | { 425 | F: FuncWatch, 426 | P: "src/github.com/rjeczalik/fs", 427 | E: Create, 428 | }, 429 | }, 430 | }, 431 | // i=1 432 | { 433 | Call: Call{ 434 | F: FuncWatch, 435 | P: "src/github.com/rjeczalik/fs/cmd/gotree/...", 436 | C: ch[0], 437 | E: Remove, 438 | }, 439 | Record: []Call{ 440 | { 441 | F: FuncRecursiveRewatch, 442 | P: "src/github.com/rjeczalik/fs", 443 | NP: "src/github.com/rjeczalik/fs", 444 | E: Create, 445 | NE: Create | Remove, 446 | }, 447 | }, 448 | }, 449 | } 450 | 451 | n.ExpectRecordedCalls(watches[:]) 452 | 453 | events := [...]TCase{ 454 | // i=0 455 | { 456 | Event: Call{P: "src/github.com/rjeczalik/fs/.fs.go.swp", E: Create}, 457 | Receiver: Chans{ch[0]}, 458 | }, 459 | // i=1 460 | { 461 | Event: Call{P: "src/github.com/rjeczalik/fs/.fs.go.swp", E: Remove}, 462 | Receiver: nil, 463 | }, 464 | // i=2 465 | { 466 | Event: Call{P: "src/github.com/rjeczalik/fs", E: Remove}, 467 | Receiver: nil, 468 | }, 469 | // i=3 470 | { 471 | Event: Call{P: "src/github.com/rjeczalik/fs/cmd/gotree/main.go", E: Remove}, 472 | Receiver: Chans{ch[0]}, 473 | }, 474 | } 475 | 476 | n.ExpectTreeEvents(events[:], ch) 477 | } 478 | 479 | func TestRecursiveTree_Windows(t *testing.T) { 480 | n := NewRecursiveTreeTest(t, "testdata/vfs.txt") 481 | 482 | const ChangeFileName = Event(0x1) 483 | 484 | ch := NewChans(1) 485 | 486 | watches := [...]RCase{ 487 | // i=0 488 | { 489 | Call: Call{ 490 | F: FuncWatch, 491 | P: "src/github.com/rjeczalik/fs", 492 | C: ch[0], 493 | E: ChangeFileName, 494 | }, 495 | Record: []Call{ 496 | { 497 | F: FuncWatch, 498 | P: "src/github.com/rjeczalik/fs", 499 | E: ChangeFileName, 500 | }, 501 | }, 502 | }, 503 | } 504 | 505 | n.ExpectRecordedCalls(watches[:]) 506 | 507 | events := [...]TCase{ 508 | // i=0 509 | { 510 | Event: Call{P: "src/github.com/rjeczalik/fs", E: ChangeFileName}, 511 | Receiver: Chans{ch[0]}, 512 | }, 513 | // i=1 514 | { 515 | Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: ChangeFileName}, 516 | Receiver: Chans{ch[0]}, 517 | }, 518 | } 519 | 520 | n.ExpectTreeEvents(events[:], ch) 521 | } 522 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import ( 8 | "errors" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | const all = ^Event(0) 15 | const sep = string(os.PathSeparator) 16 | 17 | var errDepth = errors.New("exceeded allowed iteration count (circular symlink?)") 18 | 19 | func min(i, j int) int { 20 | if i > j { 21 | return j 22 | } 23 | return i 24 | } 25 | 26 | func max(i, j int) int { 27 | if i < j { 28 | return j 29 | } 30 | return i 31 | } 32 | 33 | // must panics if err is non-nil. 34 | func must(err error) { 35 | if err != nil { 36 | panic(err) 37 | } 38 | } 39 | 40 | // nonil gives first non-nil error from the given arguments. 41 | func nonil(err ...error) error { 42 | for _, err := range err { 43 | if err != nil { 44 | return err 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | func cleanpath(path string) (realpath string, isrec bool, err error) { 51 | if strings.HasSuffix(path, "...") { 52 | isrec = true 53 | path = path[:len(path)-3] 54 | } 55 | if path, err = filepath.Abs(path); err != nil { 56 | return "", false, err 57 | } 58 | if path, err = canonical(path); err != nil { 59 | return "", false, err 60 | } 61 | return path, isrec, nil 62 | } 63 | 64 | // canonical resolves any symlink in the given path and returns it in a clean form. 65 | // It expects the path to be absolute. It fails to resolve circular symlinks by 66 | // maintaining a simple iteration limit. 67 | func canonical(p string) (string, error) { 68 | p, err := filepath.Abs(p) 69 | if err != nil { 70 | return "", err 71 | } 72 | for i, j, depth := 1, 0, 1; i < len(p); i, depth = i+1, depth+1 { 73 | if depth > 128 { 74 | return "", &os.PathError{Op: "canonical", Path: p, Err: errDepth} 75 | } 76 | if j = strings.IndexRune(p[i:], '/'); j == -1 { 77 | j, i = i, len(p) 78 | } else { 79 | j, i = i, i+j 80 | } 81 | fi, err := os.Lstat(p[:i]) 82 | if err != nil { 83 | return "", err 84 | } 85 | if fi.Mode()&os.ModeSymlink == os.ModeSymlink { 86 | s, err := os.Readlink(p[:i]) 87 | if err != nil { 88 | return "", err 89 | } 90 | if filepath.IsAbs(s) { 91 | p = "/" + s + p[i:] 92 | } else { 93 | p = p[:j] + s + p[i:] 94 | } 95 | i = 1 // no guarantee s is canonical, start all over 96 | } 97 | } 98 | return filepath.Clean(p), nil 99 | } 100 | 101 | func joinevents(events []Event) (e Event) { 102 | if len(events) == 0 { 103 | e = All 104 | } else { 105 | for _, event := range events { 106 | e |= event 107 | } 108 | } 109 | return 110 | } 111 | 112 | func split(s string) (string, string) { 113 | if i := lastIndexSep(s); i != -1 { 114 | return s[:i], s[i+1:] 115 | } 116 | return "", s 117 | } 118 | 119 | func base(s string) string { 120 | if i := lastIndexSep(s); i != -1 { 121 | return s[i+1:] 122 | } 123 | return s 124 | } 125 | 126 | // indexrel returns the index of the first char of name that is 127 | // below/relative to root. It returns -1 if name is not a child of root. 128 | func indexrel(root, name string) int { 129 | if n, m := len(root), len(name); m > n && name[:n] == root && 130 | name[n] == os.PathSeparator { 131 | return n + 1 132 | } 133 | return -1 134 | } 135 | 136 | func indexSep(s string) int { 137 | for i := 0; i < len(s); i++ { 138 | if s[i] == os.PathSeparator { 139 | return i 140 | } 141 | } 142 | return -1 143 | } 144 | 145 | func lastIndexSep(s string) int { 146 | for i := len(s) - 1; i >= 0; i-- { 147 | if s[i] == os.PathSeparator { 148 | return i 149 | } 150 | } 151 | return -1 152 | } 153 | -------------------------------------------------------------------------------- /util_darwin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build darwin 6 | // +build darwin 7 | 8 | package notify 9 | 10 | import ( 11 | "os" 12 | "testing" 13 | ) 14 | 15 | func TestCanonicalDarwin(t *testing.T) { 16 | cases := [...]caseCanonical{ 17 | {"/etc", "/private/etc"}, 18 | {"/etc/defaults", "/private/etc/defaults"}, 19 | {"/etc/hosts", "/private/etc/hosts"}, 20 | {"/tmp", "/private/tmp"}, 21 | {"/var", "/private/var"}, 22 | } 23 | testCanonical(t, cases[:]) 24 | } 25 | 26 | func TestCanonicalDarwinMultiple(t *testing.T) { 27 | etcsym, err := symlink("/etc", "") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | tmpsym, err := symlink("/tmp", "") 32 | if err != nil { 33 | t.Fatal(nonil(err, os.Remove(etcsym))) 34 | } 35 | defer removeall(etcsym, tmpsym) 36 | cases := [...]caseCanonical{ 37 | {etcsym, "/private/etc"}, 38 | {etcsym + "/hosts", "/private/etc/hosts"}, 39 | {tmpsym, "/private/tmp"}, 40 | } 41 | testCanonical(t, cases[:]) 42 | } 43 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import ( 8 | "path/filepath" 9 | "testing" 10 | ) 11 | 12 | type caseCanonical struct { 13 | path string 14 | full string 15 | } 16 | 17 | func testCanonical(t *testing.T, cases []caseCanonical) { 18 | for i, cas := range cases { 19 | full, err := canonical(cas.path) 20 | if err != nil { 21 | t.Errorf("want err=nil; got %v (i=%d)", err, i) 22 | continue 23 | } 24 | if full != cas.full { 25 | t.Errorf("want full=%q; got %q (i=%d)", cas.full, full, i) 26 | continue 27 | } 28 | } 29 | } 30 | 31 | func TestCanonicalNoSymlink(t *testing.T) { 32 | td := filepath.Join(wd, "testdata") 33 | cases := [...]caseCanonical{ 34 | {".", wd}, 35 | {"testdata", td}, 36 | {filepath.Join("testdata", ".."), wd}, 37 | } 38 | testCanonical(t, cases[:]) 39 | } 40 | 41 | func TestJoinevents(t *testing.T) { 42 | cases := [...]struct { 43 | evs []Event 44 | ev Event 45 | }{ 46 | 0: {nil, All}, 47 | 1: {[]Event{}, All}, 48 | 2: {[]Event{Create}, Create}, 49 | 3: {[]Event{Rename}, Rename}, 50 | 4: {[]Event{Create, Write, Remove}, Create | Write | Remove}, 51 | } 52 | for i, cas := range cases { 53 | if ev := joinevents(cas.evs); ev != cas.ev { 54 | t.Errorf("want event=%v; got %v (i=%d)", cas.ev, ev, i) 55 | } 56 | } 57 | } 58 | 59 | func TestTreeSplit(t *testing.T) { 60 | cases := [...]struct { 61 | path string 62 | dir string 63 | base string 64 | }{ 65 | {"/github.com/rjeczalik/fakerpc", "/github.com/rjeczalik", "fakerpc"}, 66 | {"/home/rjeczalik/src", "/home/rjeczalik", "src"}, 67 | {"/Users/pknap/porn/gopher.avi", "/Users/pknap/porn", "gopher.avi"}, 68 | {"C:/Documents and Users", "C:", "Documents and Users"}, 69 | {"C:/Documents and Users/pblaszczyk/wiertarka.exe", "C:/Documents and Users/pblaszczyk", "wiertarka.exe"}, 70 | {"/home/(╯°□°)╯︵ ┻━┻", "/home", "(╯°□°)╯︵ ┻━┻"}, 71 | } 72 | for i, cas := range cases { 73 | dir, base := split(filepath.FromSlash(cas.path)) 74 | if want := filepath.FromSlash(cas.dir); dir != want { 75 | t.Errorf("want dir=%s; got %s (i=%d)", want, dir, i) 76 | } 77 | if want := filepath.FromSlash(cas.base); base != want { 78 | t.Errorf("want base=%s; got %s (i=%d)", want, base, i) 79 | } 80 | } 81 | } 82 | 83 | func TestTreeBase(t *testing.T) { 84 | cases := [...]struct { 85 | path string 86 | base string 87 | }{ 88 | {"/github.com/rjeczalik/fakerpc", "fakerpc"}, 89 | {"/home/rjeczalik/src", "src"}, 90 | {"/Users/pknap/porn/gopher.avi", "gopher.avi"}, 91 | {"C:/Documents and Users", "Documents and Users"}, 92 | {"C:/Documents and Users/pblaszczyk/wiertarka.exe", "wiertarka.exe"}, 93 | {"/home/(╯°□°)╯︵ ┻━┻", "(╯°□°)╯︵ ┻━┻"}, 94 | } 95 | for i, cas := range cases { 96 | if base := base(filepath.FromSlash(cas.path)); base != cas.base { 97 | t.Errorf("want base=%s; got %s (i=%d)", cas.base, base, i) 98 | } 99 | } 100 | } 101 | 102 | func TestTreeIndexSep(t *testing.T) { 103 | cases := [...]struct { 104 | path string 105 | n int 106 | }{ 107 | {"github.com/rjeczalik/fakerpc", 10}, 108 | {"home/rjeczalik/src", 4}, 109 | {"Users/pknap/porn/gopher.avi", 5}, 110 | {"C:/Documents and Users", 2}, 111 | {"Documents and Users/pblaszczyk/wiertarka.exe", 19}, 112 | {"(╯°□°)╯︵ ┻━┻/Downloads", 30}, 113 | } 114 | for i, cas := range cases { 115 | if n := indexSep(filepath.FromSlash(cas.path)); n != cas.n { 116 | t.Errorf("want n=%d; got %d (i=%d)", cas.n, n, i) 117 | } 118 | } 119 | } 120 | 121 | func TestTreeLastIndexSep(t *testing.T) { 122 | cases := [...]struct { 123 | path string 124 | n int 125 | }{ 126 | {"github.com/rjeczalik/fakerpc", 20}, 127 | {"home/rjeczalik/src", 14}, 128 | {"Users/pknap/porn/gopher.avi", 16}, 129 | {"C:/Documents and Users", 2}, 130 | {"Documents and Users/pblaszczyk/wiertarka.exe", 30}, 131 | {"/home/(╯°□°)╯︵ ┻━┻", 5}, 132 | } 133 | for i, cas := range cases { 134 | if n := lastIndexSep(filepath.FromSlash(cas.path)); n != cas.n { 135 | t.Errorf("want n=%d; got %d (i=%d)", cas.n, n, i) 136 | } 137 | } 138 | } 139 | 140 | func TestCleanpath(t *testing.T) { 141 | t.Skip("TODO(rjeczalik)") 142 | } 143 | -------------------------------------------------------------------------------- /util_unix_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build !windows 6 | // +build !windows 7 | 8 | package notify 9 | 10 | import ( 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | ) 15 | 16 | func tmpfile(s string) (string, error) { 17 | f, err := os.CreateTemp(filepath.Split(s)) 18 | if err != nil { 19 | return "", err 20 | } 21 | if err = nonil(f.Sync(), f.Close()); err != nil { 22 | return "", err 23 | } 24 | return f.Name(), nil 25 | } 26 | 27 | func symlink(src, dst string) (string, error) { 28 | name, err := tmpfile(dst) 29 | if err != nil { 30 | return "", err 31 | } 32 | if err = nonil(os.Remove(name), os.Symlink(src, name)); err != nil { 33 | return "", err 34 | } 35 | return name, nil 36 | } 37 | 38 | func removeall(s ...string) { 39 | for _, s := range s { 40 | os.Remove(s) 41 | } 42 | } 43 | 44 | func TestCanonical(t *testing.T) { 45 | wd, err := os.Getwd() 46 | if err != nil { 47 | t.Fatalf("os.Getwd()=%v", err) 48 | } 49 | wdsym, err := symlink(wd, "") 50 | if err != nil { 51 | t.Fatalf(`symlink(%q, "")=%v`, wd, err) 52 | } 53 | td := filepath.Join(wd, "testdata") 54 | tdsym, err := symlink(td, td) 55 | if err != nil { 56 | t.Errorf("symlink(%q, %q)=%v", td, td, nonil(err, os.Remove(wdsym))) 57 | } 58 | defer removeall(wdsym, tdsym) 59 | vfstxt := filepath.Join(td, "vfs.txt") 60 | cases := [...]caseCanonical{ 61 | {wdsym, wd}, 62 | {tdsym, td}, 63 | {filepath.Join(wdsym, "notify.go"), filepath.Join(wd, "notify.go")}, 64 | {filepath.Join(tdsym, "vfs.txt"), vfstxt}, 65 | {filepath.Join(wdsym, filepath.Base(tdsym), "vfs.txt"), vfstxt}, 66 | } 67 | testCanonical(t, cases[:]) 68 | } 69 | 70 | func TestCanonicalCircular(t *testing.T) { 71 | tmp1, err := tmpfile("circular") 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | tmp2, err := tmpfile("circular") 76 | if err != nil { 77 | t.Fatal(nonil(err, os.Remove(tmp1))) 78 | } 79 | defer removeall(tmp1, tmp2) 80 | // Symlink tmp1 -> tmp2. 81 | if err = nonil(os.Remove(tmp1), os.Symlink(tmp2, tmp1)); err != nil { 82 | t.Fatal(err) 83 | } 84 | // Symlnik tmp2 -> tmp1. 85 | if err = nonil(os.Remove(tmp2), os.Symlink(tmp1, tmp2)); err != nil { 86 | t.Fatal(err) 87 | } 88 | if _, err = canonical(tmp1); err == nil { 89 | t.Fatalf("want canonical(%q)!=nil", tmp1) 90 | } 91 | if _, ok := err.(*os.PathError); !ok { 92 | t.Fatalf("want canonical(%q)=os.PathError; got %T", tmp1, err) 93 | } 94 | } 95 | 96 | // issue #83 97 | func TestCanonical_RelativeSymlink(t *testing.T) { 98 | dir, err := os.MkdirTemp(wd, "") 99 | if err != nil { 100 | t.Fatalf("TempDir()=%v", err) 101 | } 102 | var ( 103 | path = filepath.Join(dir, filepath.FromSlash("a/b/c/d/e/f")) 104 | realpath = filepath.Join(dir, filepath.FromSlash("a/b/x/y/z/d/e/f")) 105 | rel = filepath.FromSlash("x/y/z/../z/../z") 106 | chdir = filepath.Join(dir, filepath.FromSlash("a/b")) 107 | ) 108 | defer os.RemoveAll(dir) 109 | if err = os.MkdirAll(realpath, 0755); err != nil { 110 | t.Fatalf("MkdirAll()=%v", err) 111 | } 112 | if err := os.Chdir(chdir); err != nil { 113 | t.Fatalf("Chdir()=%v", err) 114 | } 115 | if err := nonil(os.Symlink(rel, "c"), os.Chdir(wd)); err != nil { 116 | t.Fatalf("Symlink()=%v", err) 117 | } 118 | got, err := canonical(path) 119 | if err != nil { 120 | t.Fatalf("canonical(%s)=%v", path, err) 121 | } 122 | if got != realpath { 123 | t.Fatalf("want canonical()=%s; got %s", realpath, got) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /watcher.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import "errors" 8 | 9 | var ( 10 | errAlreadyWatched = errors.New("path is already watched") 11 | errNotWatched = errors.New("path is not being watched") 12 | errInvalidEventSet = errors.New("invalid event set provided") 13 | ) 14 | 15 | // Watcher is a intermediate interface for wrapping inotify, ReadDirChangesW, 16 | // FSEvents, kqueue and poller implementations. 17 | // 18 | // The watcher implementation is expected to do its own mapping between paths and 19 | // create watchers if underlying event notification does not support it. For 20 | // the ease of implementation it is guaranteed that paths provided via Watch and 21 | // Unwatch methods are absolute and clean. 22 | type watcher interface { 23 | // Watch requests a watcher creation for the given path and given event set. 24 | Watch(path string, event Event) error 25 | 26 | // Unwatch requests a watcher deletion for the given path and given event set. 27 | Unwatch(path string) error 28 | 29 | // Rewatch provides a functionality for modifying existing watch-points, like 30 | // expanding its event set. 31 | // 32 | // Rewatch modifies existing watch-point under for the given path. It passes 33 | // the existing event set currently registered for the given path, and the 34 | // new, requested event set. 35 | // 36 | // It is guaranteed that Tree will not pass to Rewatch zero value for any 37 | // of its arguments. If old == new and watcher can be upgraded to 38 | // recursiveWatcher interface, a watch for the corresponding path is expected 39 | // to be changed from recursive to the non-recursive one. 40 | Rewatch(path string, old, new Event) error 41 | 42 | // Close unwatches all paths that are registered. When Close returns, it 43 | // is expected it will report no more events. 44 | Close() error 45 | } 46 | 47 | // RecursiveWatcher is an interface for a Watcher for those OS, which do support 48 | // recursive watching over directories. 49 | type recursiveWatcher interface { 50 | RecursiveWatch(path string, event Event) error 51 | 52 | // RecursiveUnwatch removes a recursive watch-point given by the path. For 53 | // native recursive implementation there is no difference in functionality 54 | // between Unwatch and RecursiveUnwatch, however for those platforms, that 55 | // requires emulation for recursive watch-points, the implementation differs. 56 | RecursiveUnwatch(path string) error 57 | 58 | // RecursiveRewatcher provides a functionality for modifying and/or relocating 59 | // existing recursive watch-points. 60 | // 61 | // To relocate a watch-point means to unwatch oldpath and set a watch-point on 62 | // newpath. 63 | // 64 | // To modify a watch-point means either to expand or shrink its event set. 65 | // 66 | // Tree can want to either relocate, modify or relocate and modify a watch-point 67 | // via single RecursiveRewatch call. 68 | // 69 | // If oldpath == newpath, the watch-point is expected to change its event set value 70 | // from oldevent to newevent. 71 | // 72 | // If oldevent == newevent, the watch-point is expected to relocate from oldpath 73 | // to the newpath. 74 | // 75 | // If oldpath != newpath and oldevent != newevent, the watch-point is expected 76 | // to relocate from oldpath to the newpath first and then change its event set 77 | // value from oldevent to the newevent. In other words the end result must be 78 | // a watch-point set on newpath with newevent value of its event set. 79 | // 80 | // It is guaranteed that Tree will not pass to RecurisveRewatcha zero value 81 | // for any of its arguments. If oldpath == newpath and oldevent == newevent, 82 | // a watch for the corresponding path is expected to be changed for 83 | // non-recursive to the recursive one. 84 | RecursiveRewatch(oldpath, newpath string, oldevent, newevent Event) error 85 | } 86 | -------------------------------------------------------------------------------- /watcher_fen.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build solaris || illumos 6 | // +build solaris illumos 7 | 8 | package notify 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "syscall" 14 | ) 15 | 16 | // newTrigger returns implementation of trigger. 17 | func newTrigger(pthLkp map[string]*watched) trigger { 18 | return &fen{ 19 | pthLkp: pthLkp, 20 | cf: newCfen(), 21 | } 22 | } 23 | 24 | // fen is a structure implementing trigger for FEN. 25 | type fen struct { 26 | // p is a FEN port identifier 27 | p int 28 | // pthLkp is a structure mapping monitored files/dir with data about them, 29 | // shared with parent trg structure 30 | pthLkp map[string]*watched 31 | // cf wraps C operations for FEN 32 | cf cfen 33 | } 34 | 35 | // watched is a data structure representing watched file/directory. 36 | type watched struct { 37 | trgWatched 38 | } 39 | 40 | // Stop implements trigger. 41 | func (f *fen) Stop() error { 42 | return f.cf.portAlert(f.p) 43 | } 44 | 45 | // Close implements trigger. 46 | func (f *fen) Close() (err error) { 47 | return syscall.Close(f.p) 48 | } 49 | 50 | // NewWatched implements trigger. 51 | func (*fen) NewWatched(p string, fi os.FileInfo) (*watched, error) { 52 | return &watched{trgWatched{p: p, fi: fi}}, nil 53 | } 54 | 55 | // Record implements trigger. 56 | func (f *fen) Record(w *watched) { 57 | f.pthLkp[w.p] = w 58 | } 59 | 60 | // Del implements trigger. 61 | func (f *fen) Del(w *watched) { 62 | delete(f.pthLkp, w.p) 63 | } 64 | 65 | func inter2pe(n interface{}) PortEvent { 66 | pe, ok := n.(PortEvent) 67 | if !ok { 68 | panic(fmt.Sprintf("fen: type should be PortEvent, %T instead", n)) 69 | } 70 | return pe 71 | } 72 | 73 | // Watched implements trigger. 74 | func (f *fen) Watched(n interface{}) (*watched, int64, error) { 75 | pe := inter2pe(n) 76 | fo, ok := pe.PortevObject.(*FileObj) 77 | if !ok || fo == nil { 78 | panic(fmt.Sprintf("fen: type should be *FileObj, %T instead", fo)) 79 | } 80 | w, ok := f.pthLkp[fo.Name] 81 | if !ok { 82 | return nil, 0, errNotWatched 83 | } 84 | return w, int64(pe.PortevEvents), nil 85 | } 86 | 87 | // init initializes FEN. 88 | func (f *fen) Init() (err error) { 89 | f.p, err = f.cf.portCreate() 90 | return 91 | } 92 | 93 | func fi2fo(fi os.FileInfo, p string) FileObj { 94 | st, ok := fi.Sys().(*syscall.Stat_t) 95 | if !ok { 96 | panic(fmt.Sprintf("fen: type should be *syscall.Stat_t, %T instead", st)) 97 | } 98 | return FileObj{Name: p, Atim: st.Atim, Mtim: st.Mtim, Ctim: st.Ctim} 99 | } 100 | 101 | // Unwatch implements trigger. 102 | func (f *fen) Unwatch(w *watched) error { 103 | return f.cf.portDissociate(f.p, FileObj{Name: w.p}) 104 | } 105 | 106 | // Watch implements trigger. 107 | func (f *fen) Watch(fi os.FileInfo, w *watched, e int64) error { 108 | return f.cf.portAssociate(f.p, fi2fo(fi, w.p), int(e)) 109 | } 110 | 111 | // Wait implements trigger. 112 | func (f *fen) Wait() (interface{}, error) { 113 | var ( 114 | pe PortEvent 115 | err error 116 | ) 117 | err = f.cf.portGet(f.p, &pe) 118 | return pe, err 119 | } 120 | 121 | // IsStop implements trigger. 122 | func (f *fen) IsStop(n interface{}, err error) bool { 123 | return err == syscall.EBADF || inter2pe(n).PortevSource == srcAlert 124 | } 125 | 126 | func init() { 127 | encode = func(e Event, dir bool) (o int64) { 128 | // Create event is not supported by FEN. Instead FileModified event will 129 | // be registered. If this event will be reported on dir which is to be 130 | // monitored for Create, dir will be rescanned and Create events will 131 | // be generated and returned for new files. In case of files, 132 | // if not requested FileModified event is reported, it will be ignored. 133 | o = int64(e &^ Create) 134 | if (e&Create != 0 && dir) || e&Write != 0 { 135 | o = (o &^ int64(Write)) | int64(FileModified) 136 | } 137 | // Following events are 'exception events' and as such cannot be requested 138 | // explicitly for monitoring or filtered out. If the will be reported 139 | // by FEN and not subscribed with by user, they will be filtered out by 140 | // watcher's logic. 141 | o &= int64(^Rename & ^Remove &^ FileDelete &^ FileRenameTo &^ 142 | FileRenameFrom &^ Unmounted &^ MountedOver) 143 | return 144 | } 145 | nat2not = map[Event]Event{ 146 | FileModified: Write, 147 | FileRenameFrom: Rename, 148 | FileDelete: Remove, 149 | FileAccess: Event(0), 150 | FileAttrib: Event(0), 151 | FileRenameTo: Event(0), 152 | FileTrunc: Event(0), 153 | FileNoFollow: Event(0), 154 | Unmounted: Event(0), 155 | MountedOver: Event(0), 156 | } 157 | not2nat = map[Event]Event{ 158 | Write: FileModified, 159 | Rename: FileRenameFrom, 160 | Remove: FileDelete, 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /watcher_fen_cgo.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build solaris || illumos 6 | // +build solaris illumos 7 | 8 | package notify 9 | 10 | // #include 11 | // #include 12 | // #include 13 | // struct file_obj* newFo() { return (struct file_obj*) malloc(sizeof(struct file_obj)); } 14 | // port_event_t* newPe() { return (port_event_t*) malloc(sizeof(port_event_t)); } 15 | // uintptr_t conv(struct file_obj* fo) { return (uintptr_t) fo; } 16 | // struct file_obj* dconv(uintptr_t fo) { return (struct file_obj*) fo; } 17 | import "C" 18 | 19 | import ( 20 | "syscall" 21 | "unsafe" 22 | ) 23 | 24 | const ( 25 | fileAccess = Event(C.FILE_ACCESS) 26 | fileModified = Event(C.FILE_MODIFIED) 27 | fileAttrib = Event(C.FILE_ATTRIB) 28 | fileDelete = Event(C.FILE_DELETE) 29 | fileRenameTo = Event(C.FILE_RENAME_TO) 30 | fileRenameFrom = Event(C.FILE_RENAME_FROM) 31 | fileTrunc = Event(C.FILE_TRUNC) 32 | fileNoFollow = Event(C.FILE_NOFOLLOW) 33 | unmounted = Event(C.UNMOUNTED) 34 | mountedOver = Event(C.MOUNTEDOVER) 35 | ) 36 | 37 | // PortEvent is a notify's equivalent of port_event_t. 38 | type PortEvent struct { 39 | PortevEvents int // PortevEvents is an equivalent of portev_events. 40 | PortevSource uint8 // PortevSource is an equivalent of portev_source. 41 | PortevPad uint8 // Portevpad is an equivalent of portev_pad. 42 | PortevObject interface{} // PortevObject is an equivalent of portev_object. 43 | PortevUser uintptr // PortevUser is an equivalent of portev_user. 44 | } 45 | 46 | // FileObj is a notify's equivalent of file_obj. 47 | type FileObj struct { 48 | Atim syscall.Timespec // Atim is an equivalent of fo_atime. 49 | Mtim syscall.Timespec // Mtim is an equivalent of fo_mtime. 50 | Ctim syscall.Timespec // Ctim is an equivalent of fo_ctime. 51 | Pad [3]uintptr // Pad is an equivalent of fo_pad. 52 | Name string // Name is an equivalent of fo_name. 53 | } 54 | 55 | type cfen struct { 56 | p2pe map[string]*C.port_event_t 57 | p2fo map[string]*C.struct_file_obj 58 | } 59 | 60 | func newCfen() cfen { 61 | return cfen{ 62 | p2pe: make(map[string]*C.port_event_t), 63 | p2fo: make(map[string]*C.struct_file_obj), 64 | } 65 | } 66 | 67 | func unix2C(sec int64, nsec int64) (C.time_t, C.long) { 68 | return C.time_t(sec), C.long(nsec) 69 | } 70 | 71 | func (c *cfen) portAssociate(p int, fo FileObj, e int) (err error) { 72 | cfo := C.newFo() 73 | cfo.fo_atime.tv_sec, cfo.fo_atime.tv_nsec = unix2C(fo.Atim.Unix()) 74 | cfo.fo_mtime.tv_sec, cfo.fo_mtime.tv_nsec = unix2C(fo.Mtim.Unix()) 75 | cfo.fo_ctime.tv_sec, cfo.fo_ctime.tv_nsec = unix2C(fo.Ctim.Unix()) 76 | cfo.fo_name = C.CString(fo.Name) 77 | c.p2fo[fo.Name] = cfo 78 | _, err = C.port_associate(C.int(p), srcFile, C.conv(cfo), C.int(e), nil) 79 | return 80 | } 81 | 82 | func (c *cfen) portDissociate(port int, fo FileObj) (err error) { 83 | cfo, ok := c.p2fo[fo.Name] 84 | if !ok { 85 | return errNotWatched 86 | } 87 | _, err = C.port_dissociate(C.int(port), srcFile, C.conv(cfo)) 88 | C.free(unsafe.Pointer(cfo.fo_name)) 89 | C.free(unsafe.Pointer(cfo)) 90 | delete(c.p2fo, fo.Name) 91 | return 92 | } 93 | 94 | const srcAlert = C.PORT_SOURCE_ALERT 95 | const srcFile = C.PORT_SOURCE_FILE 96 | const alertSet = C.PORT_ALERT_SET 97 | 98 | func cfo2fo(cfo *C.struct_file_obj) *FileObj { 99 | // Currently remaining attributes are not used. 100 | if cfo == nil { 101 | return nil 102 | } 103 | var fo FileObj 104 | fo.Name = C.GoString(cfo.fo_name) 105 | return &fo 106 | } 107 | 108 | func (c *cfen) portGet(port int, pe *PortEvent) (err error) { 109 | cpe := C.newPe() 110 | if _, err = C.port_get(C.int(port), cpe, nil); err != nil { 111 | C.free(unsafe.Pointer(cpe)) 112 | return 113 | } 114 | pe.PortevEvents, pe.PortevSource, pe.PortevPad = 115 | int(cpe.portev_events), uint8(cpe.portev_source), uint8(cpe.portev_pad) 116 | pe.PortevObject = cfo2fo(C.dconv(cpe.portev_object)) 117 | pe.PortevUser = uintptr(cpe.portev_user) 118 | C.free(unsafe.Pointer(cpe)) 119 | return 120 | } 121 | 122 | func (c *cfen) portCreate() (int, error) { 123 | p, err := C.port_create() 124 | return int(p), err 125 | } 126 | 127 | func (c *cfen) portAlert(p int) (err error) { 128 | _, err = C.port_alert(C.int(p), alertSet, C.int(666), nil) 129 | return 130 | } 131 | 132 | func (c *cfen) free() { 133 | for i := range c.p2fo { 134 | C.free(unsafe.Pointer(c.p2fo[i].fo_name)) 135 | C.free(unsafe.Pointer(c.p2fo[i])) 136 | } 137 | for i := range c.p2pe { 138 | C.free(unsafe.Pointer(c.p2pe[i])) 139 | } 140 | c.p2fo = make(map[string]*C.struct_file_obj) 141 | c.p2pe = make(map[string]*C.port_event_t) 142 | } 143 | -------------------------------------------------------------------------------- /watcher_fen_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build solaris || illumos 6 | // +build solaris illumos 7 | 8 | package notify 9 | 10 | import ( 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | ) 15 | 16 | func fremove(w *W, path string, files []string) WCase { 17 | cas := remove(w, path) 18 | cas.Events[0] = &Call{P: path, E: FileDelete} 19 | for _, f := range files { 20 | cas.Events = append(cas.Events, &Call{P: f, E: FileDelete}) 21 | } 22 | return cas 23 | } 24 | 25 | func fwrite(w *W, path string, p []byte) WCase { 26 | cas := write(w, path, p) 27 | path = cas.Events[0].Path() 28 | cas.Events[0] = &Call{P: path, E: FileModified} 29 | return cas 30 | } 31 | 32 | func frename(w *W, path string, files []string) WCase { 33 | const ext = ".notify" 34 | cas := WCase{ 35 | Action: func() { 36 | file := filepath.Join(w.root, path) 37 | if err := os.Rename(file, file+ext); err != nil { 38 | w.Fatalf("Rename(%q, %q)=%v", path, path+ext, err) 39 | } 40 | }, 41 | Events: []EventInfo{ 42 | &Call{P: path + ext, E: osSpecificCreate}, 43 | &Call{P: path, E: FileRenameFrom}, 44 | }, 45 | } 46 | for _, f := range files { 47 | cas.Events = append(cas.Events, &Call{P: f, E: FileRenameFrom}) 48 | } 49 | return cas 50 | } 51 | 52 | var events = []Event{ 53 | FileModified, 54 | FileAttrib, 55 | FileRenameFrom, 56 | osSpecificCreate, 57 | FileDelete, 58 | } 59 | 60 | func TestWatcherFen(t *testing.T) { 61 | w := NewWatcherTest(t, "testdata/vfs.txt", events...) 62 | defer w.Close() 63 | 64 | cases := [...]WCase{ 65 | fremove(w, "src/github.com/ppknap/link/include/coost/link", []string{ 66 | "src/github.com/ppknap/link/include/coost/link/definitions.hpp", 67 | "src/github.com/ppknap/link/include/coost/link/detail/bundle.hpp", 68 | "src/github.com/ppknap/link/include/coost/link/detail/container_invoker.hpp", 69 | "src/github.com/ppknap/link/include/coost/link/detail/container_value_trait.hpp", 70 | "src/github.com/ppknap/link/include/coost/link/detail/dummy_type.hpp", 71 | "src/github.com/ppknap/link/include/coost/link/detail/function_trait.hpp", 72 | "src/github.com/ppknap/link/include/coost/link/detail/immediate_invoker.hpp", 73 | "src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/always_same.hpp", 74 | "src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/make_unique.hpp", 75 | "src/github.com/ppknap/link/include/coost/link/detail/stdhelpers", 76 | "src/github.com/ppknap/link/include/coost/link/detail/vertex.hpp", 77 | "src/github.com/ppknap/link/include/coost/link/detail/wire.hpp", 78 | "src/github.com/ppknap/link/include/coost/link/detail", 79 | "src/github.com/ppknap/link/include/coost/link/link.hpp", 80 | }, 81 | ), 82 | fwrite(w, "src/github.com/rjeczalik/fs/fs.go", []byte("XD")), 83 | fremove(w, "src/github.com/ppknap/link/README.md", nil), 84 | frename(w, "src/github.com/rjeczalik/fs/fs.go", nil), 85 | frename(w, "src/github.com/rjeczalik/fs/cmd/gotree", []string{ 86 | "src/github.com/rjeczalik/fs/cmd/gotree/go.go", 87 | "src/github.com/rjeczalik/fs/cmd/gotree/main.go", 88 | }, 89 | ), 90 | } 91 | 92 | w.ExpectAll(cases[:]) 93 | } 94 | -------------------------------------------------------------------------------- /watcher_fsevents.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build darwin && !kqueue && cgo 6 | // +build darwin,!kqueue,cgo 7 | 8 | package notify 9 | 10 | import ( 11 | "errors" 12 | "strings" 13 | "sync/atomic" 14 | ) 15 | 16 | const ( 17 | failure = uint32(FSEventsMustScanSubDirs | FSEventsUserDropped | FSEventsKernelDropped) 18 | filter = uint32(FSEventsCreated | FSEventsRemoved | FSEventsRenamed | 19 | FSEventsModified | FSEventsInodeMetaMod) 20 | ) 21 | 22 | // FSEvent represents single file event. It is created out of values passed by 23 | // FSEvents to FSEventStreamCallback function. 24 | type FSEvent struct { 25 | Path string // real path of the file or directory 26 | ID uint64 // ID of the event (FSEventStreamEventId) 27 | Flags uint32 // joint FSEvents* flags (FSEventStreamEventFlags) 28 | } 29 | 30 | // splitflags separates event flags from single set into slice of flags. 31 | func splitflags(set uint32) (e []uint32) { 32 | for i := uint32(1); set != 0; i, set = i<<1, set>>1 { 33 | if (set & 1) != 0 { 34 | e = append(e, i) 35 | } 36 | } 37 | return 38 | } 39 | 40 | // watch represents a filesystem watchpoint. It is a higher level abstraction 41 | // over FSEvents' stream, which implements filtering of file events based 42 | // on path and event set. It emulates non-recursive watch-point by filtering out 43 | // events which paths are more than 1 level deeper than the watched path. 44 | type watch struct { 45 | // prev stores last event set per path in order to filter out old flags 46 | // for new events, which appratenly FSEvents likes to retain. It's a disgusting 47 | // hack, it should be researched how to get rid of it. 48 | prev map[string]uint32 49 | c chan<- EventInfo 50 | stream *stream 51 | path string 52 | events uint32 53 | isrec int32 54 | flushed bool 55 | } 56 | 57 | // Example format: 58 | // 59 | // ~ $ (trigger command) # (event set) -> (effective event set) 60 | // 61 | // Heuristics: 62 | // 63 | // 1. Create event is removed when it was present in previous event set. 64 | // Example: 65 | // 66 | // ~ $ echo > file # Create|Write -> Create|Write 67 | // ~ $ echo > file # Create|Write|InodeMetaMod -> Write|InodeMetaMod 68 | // 69 | // 2. Remove event is removed if it was present in previouse event set. 70 | // Example: 71 | // 72 | // ~ $ touch file # Create -> Create 73 | // ~ $ rm file # Create|Remove -> Remove 74 | // ~ $ touch file # Create|Remove -> Create 75 | // 76 | // 3. Write event is removed if not followed by InodeMetaMod on existing 77 | // file. Example: 78 | // 79 | // ~ $ echo > file # Create|Write -> Create|Write 80 | // ~ $ chmod +x file # Create|Write|ChangeOwner -> ChangeOwner 81 | // 82 | // 4. Write&InodeMetaMod is removed when effective event set contain Remove event. 83 | // Example: 84 | // 85 | // ~ $ echo > file # Write|InodeMetaMod -> Write|InodeMetaMod 86 | // ~ $ rm file # Remove|Write|InodeMetaMod -> Remove 87 | func (w *watch) strip(base string, set uint32) uint32 { 88 | const ( 89 | write = FSEventsModified | FSEventsInodeMetaMod 90 | both = FSEventsCreated | FSEventsRemoved 91 | ) 92 | switch w.prev[base] { 93 | case FSEventsCreated: 94 | set &^= FSEventsCreated 95 | if set&FSEventsRemoved != 0 { 96 | w.prev[base] = FSEventsRemoved 97 | set &^= write 98 | } 99 | case FSEventsRemoved: 100 | set &^= FSEventsRemoved 101 | if set&FSEventsCreated != 0 { 102 | w.prev[base] = FSEventsCreated 103 | } 104 | default: 105 | switch set & both { 106 | case FSEventsCreated: 107 | w.prev[base] = FSEventsCreated 108 | case FSEventsRemoved: 109 | w.prev[base] = FSEventsRemoved 110 | set &^= write 111 | } 112 | } 113 | dbgprintf("split()=%v\n", Event(set)) 114 | return set 115 | } 116 | 117 | // Dispatch is a stream function which forwards given file events for the watched 118 | // path to underlying FileInfo channel. 119 | func (w *watch) Dispatch(ev []FSEvent) { 120 | events := atomic.LoadUint32(&w.events) 121 | isrec := (atomic.LoadInt32(&w.isrec) == 1) 122 | for i := range ev { 123 | if ev[i].Flags&FSEventsHistoryDone != 0 { 124 | w.flushed = true 125 | continue 126 | } 127 | if !w.flushed { 128 | continue 129 | } 130 | dbgprintf("%v (0x%x) (%s, i=%d, ID=%d, len=%d)\n", Event(ev[i].Flags), 131 | ev[i].Flags, ev[i].Path, i, ev[i].ID, len(ev)) 132 | if ev[i].Flags&failure != 0 && failure&events == 0 { 133 | // TODO(rjeczalik): missing error handling 134 | continue 135 | } 136 | if !strings.HasPrefix(ev[i].Path, w.path) { 137 | continue 138 | } 139 | n := len(w.path) 140 | base := "" 141 | if len(ev[i].Path) > n { 142 | if ev[i].Path[n] != '/' { 143 | continue 144 | } 145 | base = ev[i].Path[n+1:] 146 | if !isrec && strings.IndexByte(base, '/') != -1 { 147 | continue 148 | } 149 | } 150 | // TODO(rjeczalik): get diff only from filtered events? 151 | e := w.strip(string(base), ev[i].Flags) & events 152 | if e == 0 { 153 | continue 154 | } 155 | for _, e := range splitflags(e) { 156 | dbgprintf("%d: single event: %v", ev[i].ID, Event(e)) 157 | w.c <- &event{ 158 | fse: ev[i], 159 | event: Event(e), 160 | } 161 | } 162 | } 163 | } 164 | 165 | // Stop closes underlying FSEvents stream and stops dispatching events. 166 | func (w *watch) Stop() { 167 | w.stream.Stop() 168 | // TODO(rjeczalik): make (*stream).Stop flush synchronously undelivered events, 169 | // so the following hack can be removed. It should flush all the streams 170 | // concurrently as we care not to block too much here. 171 | atomic.StoreUint32(&w.events, 0) 172 | atomic.StoreInt32(&w.isrec, 0) 173 | } 174 | 175 | // fsevents implements Watcher and RecursiveWatcher interfaces backed by FSEvents 176 | // framework. 177 | type fsevents struct { 178 | watches map[string]*watch 179 | c chan<- EventInfo 180 | } 181 | 182 | func newWatcher(c chan<- EventInfo) watcher { 183 | return &fsevents{ 184 | watches: make(map[string]*watch), 185 | c: c, 186 | } 187 | } 188 | 189 | func (fse *fsevents) watch(path string, event Event, isrec int32) (err error) { 190 | if _, ok := fse.watches[path]; ok { 191 | return errAlreadyWatched 192 | } 193 | w := &watch{ 194 | prev: make(map[string]uint32), 195 | c: fse.c, 196 | path: path, 197 | events: uint32(event), 198 | isrec: isrec, 199 | } 200 | w.stream = newStream(path, w.Dispatch) 201 | if err = w.stream.Start(); err != nil { 202 | return err 203 | } 204 | fse.watches[path] = w 205 | return nil 206 | } 207 | 208 | func (fse *fsevents) unwatch(path string) (err error) { 209 | w, ok := fse.watches[path] 210 | if !ok { 211 | return errNotWatched 212 | } 213 | w.stream.Stop() 214 | delete(fse.watches, path) 215 | return nil 216 | } 217 | 218 | // Watch implements Watcher interface. It fails with non-nil error when setting 219 | // the watch-point by FSEvents fails or with errAlreadyWatched error when 220 | // the given path is already watched. 221 | func (fse *fsevents) Watch(path string, event Event) error { 222 | return fse.watch(path, event, 0) 223 | } 224 | 225 | // Unwatch implements Watcher interface. It fails with errNotWatched when 226 | // the given path is not being watched. 227 | func (fse *fsevents) Unwatch(path string) error { 228 | return fse.unwatch(path) 229 | } 230 | 231 | // Rewatch implements Watcher interface. It fails with errNotWatched when 232 | // the given path is not being watched or with errInvalidEventSet when oldevent 233 | // does not match event set the watch-point currently holds. 234 | func (fse *fsevents) Rewatch(path string, oldevent, newevent Event) error { 235 | w, ok := fse.watches[path] 236 | if !ok { 237 | return errNotWatched 238 | } 239 | if !atomic.CompareAndSwapUint32(&w.events, uint32(oldevent), uint32(newevent)) { 240 | return errInvalidEventSet 241 | } 242 | atomic.StoreInt32(&w.isrec, 0) 243 | return nil 244 | } 245 | 246 | // RecursiveWatch implements RecursiveWatcher interface. It fails with non-nil 247 | // error when setting the watch-point by FSEvents fails or with errAlreadyWatched 248 | // error when the given path is already watched. 249 | func (fse *fsevents) RecursiveWatch(path string, event Event) error { 250 | return fse.watch(path, event, 1) 251 | } 252 | 253 | // RecursiveUnwatch implements RecursiveWatcher interface. It fails with 254 | // errNotWatched when the given path is not being watched. 255 | // 256 | // TODO(rjeczalik): fail if w.isrec == 0? 257 | func (fse *fsevents) RecursiveUnwatch(path string) error { 258 | return fse.unwatch(path) 259 | } 260 | 261 | // RecursiveRewatch implements RecursiveWatcher interface. It fails: 262 | // 263 | // - with errNotWatched when the given path is not being watched 264 | // - with errInvalidEventSet when oldevent does not match the current event set 265 | // - with errAlreadyWatched when watch-point given by the oldpath was meant to 266 | // be relocated to newpath, but the newpath is already watched 267 | // - a non-nil error when setting the watch-point with FSEvents fails 268 | // 269 | // TODO(rjeczalik): Improve handling of watch-point relocation? See two TODOs 270 | // that follows. 271 | func (fse *fsevents) RecursiveRewatch(oldpath, newpath string, oldevent, newevent Event) error { 272 | switch [2]bool{oldpath == newpath, oldevent == newevent} { 273 | case [2]bool{true, true}: 274 | w, ok := fse.watches[oldpath] 275 | if !ok { 276 | return errNotWatched 277 | } 278 | atomic.StoreInt32(&w.isrec, 1) 279 | return nil 280 | case [2]bool{true, false}: 281 | w, ok := fse.watches[oldpath] 282 | if !ok { 283 | return errNotWatched 284 | } 285 | if !atomic.CompareAndSwapUint32(&w.events, uint32(oldevent), uint32(newevent)) { 286 | return errors.New("invalid event state diff") 287 | } 288 | atomic.StoreInt32(&w.isrec, 1) 289 | return nil 290 | default: 291 | // TODO(rjeczalik): rewatch newpath only if exists? 292 | // TODO(rjeczalik): migrate w.prev to new watch? 293 | if _, ok := fse.watches[newpath]; ok { 294 | return errAlreadyWatched 295 | } 296 | if err := fse.Unwatch(oldpath); err != nil { 297 | return err 298 | } 299 | // TODO(rjeczalik): revert unwatch if watch fails? 300 | return fse.watch(newpath, newevent, 1) 301 | } 302 | } 303 | 304 | // Close unwatches all watch-points. 305 | func (fse *fsevents) Close() error { 306 | for _, w := range fse.watches { 307 | w.Stop() 308 | } 309 | fse.watches = nil 310 | return nil 311 | } 312 | -------------------------------------------------------------------------------- /watcher_fsevents_cgo.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build darwin && !kqueue && cgo 6 | // +build darwin,!kqueue,cgo 7 | 8 | package notify 9 | 10 | /* 11 | #include 12 | #include 13 | 14 | void gostream(uintptr_t, uintptr_t, size_t, uintptr_t, uintptr_t, uintptr_t); 15 | 16 | static FSEventStreamRef EventStreamCreate(FSEventStreamContext * context, uintptr_t info, CFArrayRef paths, FSEventStreamEventId since, CFTimeInterval latency, FSEventStreamCreateFlags flags) { 17 | context->info = (void*) info; 18 | return FSEventStreamCreate(NULL, (FSEventStreamCallback) gostream, context, paths, since, latency, flags); 19 | } 20 | 21 | #cgo LDFLAGS: -framework CoreServices 22 | */ 23 | import "C" 24 | 25 | import ( 26 | "errors" 27 | "os" 28 | "sync" 29 | "sync/atomic" 30 | "unsafe" 31 | ) 32 | 33 | var nilstream C.FSEventStreamRef 34 | 35 | // Default arguments for FSEventStreamCreate function. 36 | var ( 37 | latency C.CFTimeInterval 38 | flags = C.FSEventStreamCreateFlags(C.kFSEventStreamCreateFlagFileEvents | C.kFSEventStreamCreateFlagNoDefer) 39 | since = uint64(C.FSEventsGetCurrentEventId()) 40 | ) 41 | 42 | // global dispatch queue which all streams are registered with 43 | var q C.dispatch_queue_t = C.dispatch_queue_create( 44 | C.CString("com.github.rjeczalik.notify"), 45 | (C.dispatch_queue_attr_t)(C.DISPATCH_QUEUE_SERIAL), 46 | ) 47 | 48 | // Errors returned when FSEvents functions fail. 49 | var ( 50 | errCreate = os.NewSyscallError("FSEventStreamCreate", errors.New("NULL")) 51 | errStart = os.NewSyscallError("FSEventStreamStart", errors.New("false")) 52 | ) 53 | 54 | //export gostream 55 | func gostream(_, info uintptr, n C.size_t, paths, flags, ids uintptr) { 56 | const ( 57 | offchar = unsafe.Sizeof((*C.char)(nil)) 58 | offflag = unsafe.Sizeof(C.FSEventStreamEventFlags(0)) 59 | offid = unsafe.Sizeof(C.FSEventStreamEventId(0)) 60 | ) 61 | if n == 0 { 62 | return 63 | } 64 | fn := streamFuncs.get(info) 65 | if fn == nil { 66 | return 67 | } 68 | ev := make([]FSEvent, 0, int(n)) 69 | for i := uintptr(0); i < uintptr(n); i++ { 70 | switch flags := *(*uint32)(unsafe.Pointer((flags + i*offflag))); { 71 | case flags&uint32(FSEventsEventIdsWrapped) != 0: 72 | atomic.StoreUint64(&since, uint64(C.FSEventsGetCurrentEventId())) 73 | default: 74 | ev = append(ev, FSEvent{ 75 | Path: C.GoString(*(**C.char)(unsafe.Pointer(paths + i*offchar))), 76 | Flags: flags, 77 | ID: *(*uint64)(unsafe.Pointer(ids + i*offid)), 78 | }) 79 | } 80 | 81 | } 82 | fn(ev) 83 | } 84 | 85 | // StreamFunc is a callback called when stream receives file events. 86 | type streamFunc func([]FSEvent) 87 | 88 | var streamFuncs = streamFuncRegistry{m: map[uintptr]streamFunc{}} 89 | 90 | type streamFuncRegistry struct { 91 | mu sync.Mutex 92 | m map[uintptr]streamFunc 93 | i uintptr 94 | } 95 | 96 | func (r *streamFuncRegistry) get(id uintptr) streamFunc { 97 | r.mu.Lock() 98 | defer r.mu.Unlock() 99 | return r.m[id] 100 | } 101 | 102 | func (r *streamFuncRegistry) add(fn streamFunc) uintptr { 103 | r.mu.Lock() 104 | defer r.mu.Unlock() 105 | r.i++ 106 | r.m[r.i] = fn 107 | return r.i 108 | } 109 | 110 | func (r *streamFuncRegistry) delete(id uintptr) { 111 | r.mu.Lock() 112 | defer r.mu.Unlock() 113 | delete(r.m, id) 114 | } 115 | 116 | // Stream represents a single watch-point which listens for events scheduled on the global dispatch queue. 117 | type stream struct { 118 | path string 119 | ref C.FSEventStreamRef 120 | info uintptr 121 | } 122 | 123 | // NewStream creates a stream for given path, listening for file events and 124 | // calling fn upon receiving any. 125 | func newStream(path string, fn streamFunc) *stream { 126 | return &stream{ 127 | path: path, 128 | info: streamFuncs.add(fn), 129 | } 130 | } 131 | 132 | // Start creates a FSEventStream for the given path and schedules on the global dispatch queue. 133 | // It's a nop if the stream was already started. 134 | func (s *stream) Start() error { 135 | if s.ref != nilstream { 136 | return nil 137 | } 138 | p := C.CFStringCreateWithCStringNoCopy(C.kCFAllocatorDefault, C.CString(s.path), C.kCFStringEncodingUTF8, C.kCFAllocatorDefault) 139 | path := C.CFArrayCreate(C.kCFAllocatorDefault, (*unsafe.Pointer)(unsafe.Pointer(&p)), 1, nil) 140 | ctx := C.FSEventStreamContext{} 141 | ref := C.EventStreamCreate(&ctx, C.uintptr_t(s.info), path, C.FSEventStreamEventId(atomic.LoadUint64(&since)), latency, flags) 142 | if ref == nilstream { 143 | return errCreate 144 | } 145 | C.FSEventStreamSetDispatchQueue(ref, q) 146 | if C.FSEventStreamStart(ref) == C.Boolean(0) { 147 | C.FSEventStreamInvalidate(ref) 148 | return errStart 149 | } 150 | s.ref = ref 151 | return nil 152 | } 153 | 154 | // Stop stops underlying FSEventStream and unregisters it from the global dispatch queue. 155 | func (s *stream) Stop() { 156 | if s.ref == nilstream { 157 | return 158 | } 159 | C.FSEventStreamStop(s.ref) 160 | C.FSEventStreamInvalidate(s.ref) 161 | s.ref = nilstream 162 | streamFuncs.delete(s.info) 163 | } 164 | -------------------------------------------------------------------------------- /watcher_fsevents_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build darwin && !kqueue && cgo 6 | // +build darwin,!kqueue,cgo 7 | 8 | package notify 9 | 10 | import ( 11 | "reflect" 12 | "testing" 13 | ) 14 | 15 | func TestSplitflags(t *testing.T) { 16 | cases := [...]struct { 17 | set uint32 18 | flags []uint32 19 | }{ 20 | {0, nil}, 21 | {0xD, []uint32{0x1, 0x4, 0x8}}, 22 | {0x0010 | 0x0040 | 0x0080 | 0x01000, []uint32{0x0010, 0x0040, 0x0080, 0x01000}}, 23 | {0x40000 | 0x00100 | 0x00200, []uint32{0x00100, 0x00200, 0x40000}}, 24 | } 25 | for i, cas := range cases { 26 | if flags := splitflags(cas.set); !reflect.DeepEqual(flags, cas.flags) { 27 | t.Errorf("want flags=%v; got %v (i=%d)", cas.flags, flags, i) 28 | } 29 | } 30 | } 31 | 32 | func TestWatchStrip(t *testing.T) { 33 | const ( 34 | create = uint32(FSEventsCreated) 35 | remove = uint32(FSEventsRemoved) 36 | rename = uint32(FSEventsRenamed) 37 | write = uint32(FSEventsModified) 38 | inode = uint32(FSEventsInodeMetaMod) 39 | owner = uint32(FSEventsChangeOwner) 40 | ) 41 | cases := [...][]struct { 42 | path string 43 | flag uint32 44 | diff uint32 45 | }{ 46 | // 1. 47 | { 48 | {"file", create | write, create | write}, 49 | {"file", create | write | inode, write | inode}, 50 | }, 51 | // 2. 52 | { 53 | {"file", create, create}, 54 | {"file", create | remove, remove}, 55 | {"file", create | remove, create}, 56 | }, 57 | // 3. 58 | { 59 | {"file", create | write, create | write}, 60 | {"file", create | write | owner, write | owner}, 61 | }, 62 | // 4. 63 | { 64 | {"file", create | write, create | write}, 65 | {"file", write | inode, write | inode}, 66 | {"file", remove | write | inode, remove}, 67 | }, 68 | { 69 | {"file", remove | write | inode, remove}, 70 | }, 71 | } 72 | Test: 73 | for i, cas := range cases { 74 | if len(cas) == 0 { 75 | t.Log("skipped") 76 | continue 77 | } 78 | w := &watch{prev: make(map[string]uint32)} 79 | for j, cas := range cas { 80 | if diff := w.strip(cas.path, cas.flag); diff != cas.diff { 81 | t.Errorf("want diff=%v; got %v (i=%d, j=%d)", Event(cas.diff), 82 | Event(diff), i, j) 83 | continue Test 84 | } 85 | } 86 | } 87 | } 88 | 89 | // Test for cases 3) and 5) with shadowed write&create events. 90 | // 91 | // See comment for (flagdiff).diff method. 92 | func TestWatcherShadowedWriteCreate(t *testing.T) { 93 | w := NewWatcherTest(t, "testdata/vfs.txt") 94 | defer w.Close() 95 | 96 | cases := [...]WCase{ 97 | // i=0 98 | create(w, "src/github.com/rjeczalik/fs/.fs.go.swp"), 99 | // i=1 100 | write(w, "src/github.com/rjeczalik/fs/.fs.go.swp", []byte("XD")), 101 | // i=2 102 | write(w, "src/github.com/rjeczalik/fs/.fs.go.swp", []byte("XD")), 103 | // i=3 104 | remove(w, "src/github.com/rjeczalik/fs/.fs.go.swp"), 105 | // i=4 106 | create(w, "src/github.com/rjeczalik/fs/.fs.go.swp"), 107 | // i=5 108 | write(w, "src/github.com/rjeczalik/fs/.fs.go.swp", []byte("XD")), 109 | } 110 | 111 | w.ExpectAny(cases[:5]) // BUG(rjeczalik): #62 112 | } 113 | -------------------------------------------------------------------------------- /watcher_inotify.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build linux 6 | // +build linux 7 | 8 | package notify 9 | 10 | import ( 11 | "bytes" 12 | "errors" 13 | "path/filepath" 14 | "runtime" 15 | "sync" 16 | "sync/atomic" 17 | "unsafe" 18 | 19 | "golang.org/x/sys/unix" 20 | ) 21 | 22 | // eventBufferSize defines the size of the buffer given to read(2) function. One 23 | // should not depend on this value, since it was arbitrary chosen and may be 24 | // changed in the future. 25 | const eventBufferSize = 64 * (unix.SizeofInotifyEvent + unix.PathMax + 1) 26 | 27 | // consumersCount defines the number of consumers in producer-consumer based 28 | // implementation. Each consumer is run in a separate goroutine and has read 29 | // access to watched files map. 30 | const consumersCount = 2 31 | 32 | const invalidDescriptor = -1 33 | 34 | // watched is a pair of file path and inotify mask used as a value in 35 | // watched files map. 36 | type watched struct { 37 | path string 38 | mask uint32 39 | } 40 | 41 | // inotify implements Watcher interface. 42 | type inotify struct { 43 | sync.RWMutex // protects inotify.m map 44 | m map[int32]*watched // watch descriptor to watched object 45 | fd int32 // inotify file descriptor 46 | pipefd []int // pipe's read and write descriptors 47 | epfd int // epoll descriptor 48 | epes []unix.EpollEvent // epoll events 49 | buffer [eventBufferSize]byte // inotify event buffer 50 | wg sync.WaitGroup // wait group used to close main loop 51 | c chan<- EventInfo // event dispatcher channel 52 | } 53 | 54 | // NewWatcher creates new non-recursive inotify backed by inotify. 55 | func newWatcher(c chan<- EventInfo) watcher { 56 | i := &inotify{ 57 | m: make(map[int32]*watched), 58 | fd: invalidDescriptor, 59 | pipefd: []int{invalidDescriptor, invalidDescriptor}, 60 | epfd: invalidDescriptor, 61 | epes: make([]unix.EpollEvent, 0), 62 | c: c, 63 | } 64 | runtime.SetFinalizer(i, func(i *inotify) { 65 | i.epollclose() 66 | if i.fd != invalidDescriptor { 67 | unix.Close(int(i.fd)) 68 | } 69 | }) 70 | return i 71 | } 72 | 73 | // Watch implements notify.watcher interface. 74 | func (i *inotify) Watch(path string, e Event) error { 75 | return i.watch(path, e) 76 | } 77 | 78 | // Rewatch implements notify.watcher interface. 79 | func (i *inotify) Rewatch(path string, _, newevent Event) error { 80 | return i.watch(path, newevent) 81 | } 82 | 83 | // watch adds a new watcher to the set of watched objects or modifies the existing 84 | // one. If called for the first time, this function initializes inotify filesystem 85 | // monitor and starts producer-consumers goroutines. 86 | func (i *inotify) watch(path string, e Event) (err error) { 87 | if e&^(All|Event(unix.IN_ALL_EVENTS)) != 0 { 88 | return errors.New("notify: unknown event") 89 | } 90 | if err = i.lazyinit(); err != nil { 91 | return 92 | } 93 | iwd, err := unix.InotifyAddWatch(int(i.fd), path, encode(e)) 94 | if err != nil { 95 | return 96 | } 97 | i.Lock() 98 | if wd, ok := i.m[int32(iwd)]; !ok { 99 | i.m[int32(iwd)] = &watched{path: path, mask: uint32(e)} 100 | } else { 101 | wd.path = path 102 | wd.mask = uint32(e) 103 | } 104 | i.Unlock() 105 | return nil 106 | } 107 | 108 | // lazyinit sets up all required file descriptors and starts 1+consumersCount 109 | // goroutines. The producer goroutine blocks until file-system notifications 110 | // occur. Then, all events are read from system buffer and sent to consumer 111 | // goroutines which construct valid notify events. This method uses 112 | // Double-Checked Locking optimization. 113 | func (i *inotify) lazyinit() error { 114 | if atomic.LoadInt32(&i.fd) == invalidDescriptor { 115 | i.Lock() 116 | defer i.Unlock() 117 | if atomic.LoadInt32(&i.fd) == invalidDescriptor { 118 | fd, err := unix.InotifyInit1(unix.IN_CLOEXEC) 119 | if err != nil { 120 | return err 121 | } 122 | i.fd = int32(fd) 123 | if err = i.epollinit(); err != nil { 124 | _, _ = i.epollclose(), unix.Close(int(fd)) // Ignore errors. 125 | i.fd = invalidDescriptor 126 | return err 127 | } 128 | esch := make(chan []*event) 129 | go i.loop(esch) 130 | i.wg.Add(consumersCount) 131 | for n := 0; n < consumersCount; n++ { 132 | go i.send(esch) 133 | } 134 | } 135 | } 136 | return nil 137 | } 138 | 139 | // epollinit opens an epoll file descriptor and creates a pipe which will be 140 | // used to wake up the epoll_wait(2) function. Then, file descriptor associated 141 | // with inotify event queue and the read end of the pipe are added to epoll set. 142 | // Note that `fd` member must be set before this function is called. 143 | func (i *inotify) epollinit() (err error) { 144 | if i.epfd, err = unix.EpollCreate1(0); err != nil { 145 | return 146 | } 147 | if err = unix.Pipe(i.pipefd); err != nil { 148 | return 149 | } 150 | i.epes = []unix.EpollEvent{ 151 | {Events: unix.EPOLLIN, Fd: i.fd}, 152 | {Events: unix.EPOLLIN, Fd: int32(i.pipefd[0])}, 153 | } 154 | if err = unix.EpollCtl(i.epfd, unix.EPOLL_CTL_ADD, int(i.fd), &i.epes[0]); err != nil { 155 | return 156 | } 157 | return unix.EpollCtl(i.epfd, unix.EPOLL_CTL_ADD, i.pipefd[0], &i.epes[1]) 158 | } 159 | 160 | // epollclose closes the file descriptor created by the call to epoll_create(2) 161 | // and two file descriptors opened by pipe(2) function. 162 | func (i *inotify) epollclose() (err error) { 163 | if i.epfd != invalidDescriptor { 164 | if err = unix.Close(i.epfd); err == nil { 165 | i.epfd = invalidDescriptor 166 | } 167 | } 168 | for n, fd := range i.pipefd { 169 | if fd != invalidDescriptor { 170 | switch e := unix.Close(fd); { 171 | case e != nil && err == nil: 172 | err = e 173 | case e == nil: 174 | i.pipefd[n] = invalidDescriptor 175 | } 176 | } 177 | } 178 | return 179 | } 180 | 181 | // loop blocks until either inotify or pipe file descriptor is ready for I/O. 182 | // All read operations triggered by filesystem notifications are forwarded to 183 | // one of the event's consumers. If pipe fd became ready, loop function closes 184 | // all file descriptors opened by lazyinit method and returns afterwards. 185 | func (i *inotify) loop(esch chan<- []*event) { 186 | epes := make([]unix.EpollEvent, 1) 187 | fd := atomic.LoadInt32(&i.fd) 188 | for { 189 | switch _, err := unix.EpollWait(i.epfd, epes, -1); err { 190 | case nil: 191 | switch epes[0].Fd { 192 | case fd: 193 | esch <- i.read() 194 | epes[0].Fd = 0 195 | case int32(i.pipefd[0]): 196 | i.Lock() 197 | defer i.Unlock() 198 | if err = unix.Close(int(fd)); err != nil && err != unix.EINTR { 199 | panic("notify: close(2) error " + err.Error()) 200 | } 201 | atomic.StoreInt32(&i.fd, invalidDescriptor) 202 | if err = i.epollclose(); err != nil && err != unix.EINTR { 203 | panic("notify: epollclose error " + err.Error()) 204 | } 205 | close(esch) 206 | return 207 | } 208 | case unix.EINTR: 209 | continue 210 | default: // We should never reach this line. 211 | panic("notify: epoll_wait(2) error " + err.Error()) 212 | } 213 | } 214 | } 215 | 216 | // read reads events from an inotify file descriptor. It does not handle errors 217 | // returned from read(2) function since they are not critical to watcher logic. 218 | func (i *inotify) read() (es []*event) { 219 | n, err := unix.Read(int(i.fd), i.buffer[:]) 220 | if err != nil || n < unix.SizeofInotifyEvent { 221 | return 222 | } 223 | var sys *unix.InotifyEvent 224 | nmin := n - unix.SizeofInotifyEvent 225 | for pos, path := 0, ""; pos <= nmin; { 226 | sys = (*unix.InotifyEvent)(unsafe.Pointer(&i.buffer[pos])) 227 | pos += unix.SizeofInotifyEvent 228 | if path = ""; sys.Len > 0 { 229 | endpos := pos + int(sys.Len) 230 | path = string(bytes.TrimRight(i.buffer[pos:endpos], "\x00")) 231 | pos = endpos 232 | } 233 | es = append(es, &event{ 234 | sys: unix.InotifyEvent{ 235 | Wd: sys.Wd, 236 | Mask: sys.Mask, 237 | Cookie: sys.Cookie, 238 | }, 239 | path: path, 240 | }) 241 | } 242 | return 243 | } 244 | 245 | // send is a consumer function which sends events to event dispatcher channel. 246 | // It is run in a separate goroutine in order to not block loop method when 247 | // possibly expensive write operations are performed on inotify map. 248 | func (i *inotify) send(esch <-chan []*event) { 249 | for es := range esch { 250 | for _, e := range i.transform(es) { 251 | if e != nil { 252 | i.c <- e 253 | } 254 | } 255 | } 256 | i.wg.Done() 257 | } 258 | 259 | // transform prepares events read from inotify file descriptor for sending to 260 | // user. It removes invalid events and these which are no longer present in 261 | // inotify map. This method may also split one raw event into two different ones 262 | // when system-dependent result is required. 263 | func (i *inotify) transform(es []*event) []*event { 264 | var multi []*event 265 | i.RLock() 266 | for idx, e := range es { 267 | if e.sys.Mask&(unix.IN_IGNORED|unix.IN_Q_OVERFLOW) != 0 { 268 | es[idx] = nil 269 | continue 270 | } 271 | wd, ok := i.m[e.sys.Wd] 272 | if !ok || e.sys.Mask&encode(Event(wd.mask)) == 0 { 273 | es[idx] = nil 274 | continue 275 | } 276 | if e.path == "" { 277 | e.path = wd.path 278 | } else { 279 | e.path = filepath.Join(wd.path, e.path) 280 | } 281 | multi = append(multi, decode(Event(wd.mask), e)) 282 | if e.event == 0 { 283 | es[idx] = nil 284 | } 285 | } 286 | i.RUnlock() 287 | es = append(es, multi...) 288 | return es 289 | } 290 | 291 | // encode converts notify system-independent events to valid inotify mask 292 | // which can be passed to inotify_add_watch(2) function. 293 | func encode(e Event) uint32 { 294 | if e&Create != 0 { 295 | e = (e ^ Create) | InCreate | InMovedTo 296 | } 297 | if e&Remove != 0 { 298 | e = (e ^ Remove) | InDelete | InDeleteSelf 299 | } 300 | if e&Write != 0 { 301 | e = (e ^ Write) | InModify 302 | } 303 | if e&Rename != 0 { 304 | e = (e ^ Rename) | InMovedFrom | InMoveSelf 305 | } 306 | return uint32(e) 307 | } 308 | 309 | // decode uses internally stored mask to distinguish whether system-independent 310 | // or system-dependent event is requested. The first one is created by modifying 311 | // `e` argument. decode method sets e.event value to 0 when an event should be 312 | // skipped. System-dependent event is set as the function's return value which 313 | // can be nil when the event should not be passed on. 314 | func decode(mask Event, e *event) (syse *event) { 315 | if sysmask := uint32(mask) & e.sys.Mask; sysmask != 0 { 316 | syse = &event{sys: unix.InotifyEvent{ 317 | Wd: e.sys.Wd, 318 | Mask: e.sys.Mask, 319 | Cookie: e.sys.Cookie, 320 | }, event: Event(sysmask), path: e.path} 321 | } 322 | imask := encode(mask) 323 | switch { 324 | case mask&Create != 0 && imask&uint32(InCreate|InMovedTo)&e.sys.Mask != 0: 325 | e.event = Create 326 | case mask&Remove != 0 && imask&uint32(InDelete|InDeleteSelf)&e.sys.Mask != 0: 327 | e.event = Remove 328 | case mask&Write != 0 && imask&uint32(InModify)&e.sys.Mask != 0: 329 | e.event = Write 330 | case mask&Rename != 0 && imask&uint32(InMovedFrom|InMoveSelf)&e.sys.Mask != 0: 331 | e.event = Rename 332 | default: 333 | e.event = 0 334 | } 335 | return 336 | } 337 | 338 | // Unwatch implements notify.watcher interface. It looks for watch descriptor 339 | // related to registered path and if found, calls inotify_rm_watch(2) function. 340 | // This method is allowed to return EINVAL error when concurrently requested to 341 | // delete identical path. 342 | func (i *inotify) Unwatch(path string) (err error) { 343 | iwd := int32(invalidDescriptor) 344 | i.RLock() 345 | for iwdkey, wd := range i.m { 346 | if wd.path == path { 347 | iwd = iwdkey 348 | break 349 | } 350 | } 351 | i.RUnlock() 352 | if iwd == invalidDescriptor { 353 | return errors.New("notify: path " + path + " is already watched") 354 | } 355 | fd := atomic.LoadInt32(&i.fd) 356 | if err = removeInotifyWatch(fd, iwd); err != nil { 357 | return 358 | } 359 | i.Lock() 360 | delete(i.m, iwd) 361 | i.Unlock() 362 | return nil 363 | } 364 | 365 | // Close implements notify.watcher interface. It removes all existing watch 366 | // descriptors and wakes up producer goroutine by sending data to the write end 367 | // of the pipe. The function waits for a signal from producer which means that 368 | // all operations on current monitoring instance are done. 369 | func (i *inotify) Close() (err error) { 370 | i.Lock() 371 | if fd := atomic.LoadInt32(&i.fd); fd == invalidDescriptor { 372 | i.Unlock() 373 | return nil 374 | } 375 | for iwd := range i.m { 376 | if e := removeInotifyWatch(i.fd, iwd); e != nil && err == nil { 377 | err = e 378 | } 379 | delete(i.m, iwd) 380 | } 381 | switch _, errwrite := unix.Write(i.pipefd[1], []byte{0x00}); { 382 | case errwrite != nil && err == nil: 383 | err = errwrite 384 | fallthrough 385 | case errwrite != nil: 386 | i.Unlock() 387 | default: 388 | i.Unlock() 389 | i.wg.Wait() 390 | } 391 | return 392 | } 393 | 394 | // if path was removed, notify already removed the watch and returns EINVAL error 395 | func removeInotifyWatch(fd int32, iwd int32) (err error) { 396 | if _, err = unix.InotifyRmWatch(int(fd), uint32(iwd)); err != nil && err != unix.EINVAL { 397 | return 398 | } 399 | return nil 400 | } 401 | -------------------------------------------------------------------------------- /watcher_inotify_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build linux 6 | // +build linux 7 | 8 | package notify 9 | 10 | import ( 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | ) 15 | 16 | func icreate(w *W, path string) WCase { 17 | cas := create(w, path) 18 | cas.Events = append(cas.Events, 19 | &Call{P: path, E: InCreate}, 20 | ) 21 | return cas 22 | } 23 | 24 | func iremove(w *W, path string) WCase { 25 | cas := remove(w, path) 26 | cas.Events = append(cas.Events, 27 | &Call{P: path, E: InDelete}, 28 | ) 29 | return cas 30 | } 31 | 32 | func iopen(w *W, path string) WCase { 33 | return WCase{ 34 | Action: func() { 35 | f, err := os.OpenFile(filepath.Join(w.root, path), os.O_RDWR, 0644) 36 | if err != nil { 37 | w.Fatalf("OpenFile(%q)=%v", path, err) 38 | } 39 | if err := f.Close(); err != nil { 40 | w.Fatalf("Close(%q)=%v", path, err) 41 | } 42 | }, 43 | Events: []EventInfo{ 44 | &Call{P: path, E: InAccess}, 45 | &Call{P: path, E: InOpen}, 46 | &Call{P: path, E: InCloseNowrite}, 47 | }, 48 | } 49 | } 50 | 51 | func iread(w *W, path string, p []byte) WCase { 52 | return WCase{ 53 | Action: func() { 54 | f, err := os.OpenFile(filepath.Join(w.root, path), os.O_RDWR, 0644) 55 | if err != nil { 56 | w.Fatalf("OpenFile(%q)=%v", path, err) 57 | } 58 | if _, err := f.Read(p); err != nil { 59 | w.Fatalf("Read(%q)=%v", path, err) 60 | } 61 | if err := f.Close(); err != nil { 62 | w.Fatalf("Close(%q)=%v", path, err) 63 | } 64 | }, 65 | Events: []EventInfo{ 66 | &Call{P: path, E: InAccess}, 67 | &Call{P: path, E: InOpen}, 68 | &Call{P: path, E: InModify}, 69 | &Call{P: path, E: InCloseNowrite}, 70 | }, 71 | } 72 | } 73 | 74 | func iwrite(w *W, path string, p []byte) WCase { 75 | cas := write(w, path, p) 76 | path = cas.Events[0].Path() 77 | cas.Events = append(cas.Events, 78 | &Call{P: path, E: InAccess}, 79 | &Call{P: path, E: InOpen}, 80 | &Call{P: path, E: InModify}, 81 | &Call{P: path, E: InCloseWrite}, 82 | ) 83 | return cas 84 | } 85 | 86 | func irename(w *W, path string) WCase { 87 | const ext = ".notify" 88 | return WCase{ 89 | Action: func() { 90 | file := filepath.Join(w.root, path) 91 | if err := os.Rename(file, file+ext); err != nil { 92 | w.Fatalf("Rename(%q, %q)=%v", path, path+ext, err) 93 | } 94 | }, 95 | Events: []EventInfo{ 96 | &Call{P: path, E: InMovedFrom}, 97 | &Call{P: path + ext, E: InMovedTo}, 98 | &Call{P: path, E: InOpen}, 99 | &Call{P: path, E: InAccess}, 100 | &Call{P: path, E: InCreate}, 101 | }, 102 | } 103 | } 104 | 105 | var events = []Event{ 106 | InAccess, 107 | InModify, 108 | InAttrib, 109 | InCloseWrite, 110 | InCloseNowrite, 111 | InOpen, 112 | InMovedFrom, 113 | InMovedTo, 114 | InCreate, 115 | InDelete, 116 | InDeleteSelf, 117 | InMoveSelf, 118 | } 119 | 120 | func TestWatcherInotify(t *testing.T) { 121 | w := NewWatcherTest(t, "testdata/vfs.txt", events...) 122 | defer w.Close() 123 | 124 | cases := [...]WCase{ 125 | iopen(w, "src/github.com/rjeczalik/fs/fs.go"), 126 | iwrite(w, "src/github.com/rjeczalik/fs/fs.go", []byte("XD")), 127 | iread(w, "src/github.com/rjeczalik/fs/fs.go", []byte("XD")), 128 | iremove(w, "src/github.com/ppknap/link/README.md"), 129 | irename(w, "src/github.com/rjeczalik/fs/LICENSE"), 130 | } 131 | 132 | w.ExpectAny(cases[:]) 133 | } 134 | -------------------------------------------------------------------------------- /watcher_kqueue.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build (darwin && kqueue) || (darwin && !cgo) || dragonfly || freebsd || netbsd || openbsd 6 | // +build darwin,kqueue darwin,!cgo dragonfly freebsd netbsd openbsd 7 | 8 | package notify 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | "os" 14 | "syscall" 15 | ) 16 | 17 | // newTrigger returns implementation of trigger. 18 | func newTrigger(pthLkp map[string]*watched) trigger { 19 | return &kq{ 20 | pthLkp: pthLkp, 21 | idLkp: make(map[int]*watched), 22 | } 23 | } 24 | 25 | // kq is a structure implementing trigger for kqueue. 26 | type kq struct { 27 | // fd is a kqueue file descriptor 28 | fd int 29 | // pipefds are file descriptors used to stop `Kevent` call. 30 | pipefds [2]int 31 | // idLkp is a data structure mapping file descriptors with data about watching 32 | // represented by them files/directories. 33 | idLkp map[int]*watched 34 | // pthLkp is a structure mapping monitored files/dir with data about them, 35 | // shared with parent trg structure 36 | pthLkp map[string]*watched 37 | } 38 | 39 | // watched is a data structure representing watched file/directory. 40 | type watched struct { 41 | trgWatched 42 | // fd is a file descriptor for watched file/directory. 43 | fd int 44 | } 45 | 46 | // Stop implements trigger. 47 | func (k *kq) Stop() (err error) { 48 | // trigger event used to interrupt Kevent call. 49 | _, err = syscall.Write(k.pipefds[1], []byte{0x00}) 50 | return 51 | } 52 | 53 | // Close implements trigger. 54 | func (k *kq) Close() error { 55 | return syscall.Close(k.fd) 56 | } 57 | 58 | // NewWatched implements trigger. 59 | func (*kq) NewWatched(p string, fi os.FileInfo) (*watched, error) { 60 | fd, err := syscall.Open(p, syscall.O_NONBLOCK|syscall.O_RDONLY, 0) 61 | if err != nil { 62 | // BSDs can't open symlinks and return an error if the symlink 63 | // cannot be followed - ignore it instead of failing. See e.g. 64 | // https://github.com/libinotify-kqueue/libinotify-kqueue/blob/a822c8f1d75404fe3132f695a898dcd42fe8afbc/patches/freebsd11-O_SYMLINK.patch 65 | if os.IsNotExist(err) && fi.Mode()&os.ModeSymlink == os.ModeSymlink { 66 | return nil, errSkip 67 | } 68 | // FreeBSD can't open unix domain sockets and returns "operation not supported" error. 69 | // Ignore it instead of failing. 70 | if errors.Is(err, syscall.ENOTSUP) && fi.Mode()&os.ModeSocket == os.ModeSocket { 71 | return nil, errSkip 72 | } 73 | return nil, err 74 | } 75 | return &watched{ 76 | trgWatched: trgWatched{p: p, fi: fi}, 77 | fd: fd, 78 | }, nil 79 | } 80 | 81 | // Record implements trigger. 82 | func (k *kq) Record(w *watched) { 83 | k.idLkp[w.fd], k.pthLkp[w.p] = w, w 84 | } 85 | 86 | // Del implements trigger. 87 | func (k *kq) Del(w *watched) { 88 | syscall.Close(w.fd) 89 | delete(k.idLkp, w.fd) 90 | delete(k.pthLkp, w.p) 91 | } 92 | 93 | func inter2kq(n interface{}) syscall.Kevent_t { 94 | kq, ok := n.(syscall.Kevent_t) 95 | if !ok { 96 | panic(fmt.Sprintf("kqueue: type should be Kevent_t, %T instead", n)) 97 | } 98 | return kq 99 | } 100 | 101 | // Init implements trigger. 102 | func (k *kq) Init() (err error) { 103 | if k.fd, err = syscall.Kqueue(); err != nil { 104 | return 105 | } 106 | // Creates pipe used to stop `Kevent` call by registering it, 107 | // watching read end and writing to other end of it. 108 | if err = syscall.Pipe(k.pipefds[:]); err != nil { 109 | return nonil(err, k.Close()) 110 | } 111 | var kevn [1]syscall.Kevent_t 112 | syscall.SetKevent(&kevn[0], k.pipefds[0], syscall.EVFILT_READ, syscall.EV_ADD) 113 | if _, err = syscall.Kevent(k.fd, kevn[:], nil, nil); err != nil { 114 | return nonil(err, k.Close()) 115 | } 116 | return 117 | } 118 | 119 | // Unwatch implements trigger. 120 | func (k *kq) Unwatch(w *watched) (err error) { 121 | var kevn [1]syscall.Kevent_t 122 | syscall.SetKevent(&kevn[0], w.fd, syscall.EVFILT_VNODE, syscall.EV_DELETE) 123 | 124 | _, err = syscall.Kevent(k.fd, kevn[:], nil, nil) 125 | return 126 | } 127 | 128 | // Watch implements trigger. 129 | func (k *kq) Watch(fi os.FileInfo, w *watched, e int64) (err error) { 130 | var kevn [1]syscall.Kevent_t 131 | syscall.SetKevent(&kevn[0], w.fd, syscall.EVFILT_VNODE, 132 | syscall.EV_ADD|syscall.EV_CLEAR) 133 | kevn[0].Fflags = uint32(e) 134 | 135 | _, err = syscall.Kevent(k.fd, kevn[:], nil, nil) 136 | return 137 | } 138 | 139 | // Wait implements trigger. 140 | func (k *kq) Wait() (interface{}, error) { 141 | var ( 142 | kevn [1]syscall.Kevent_t 143 | err error 144 | ) 145 | kevn[0] = syscall.Kevent_t{} 146 | _, err = syscall.Kevent(k.fd, nil, kevn[:], nil) 147 | 148 | return kevn[0], err 149 | } 150 | 151 | // Watched implements trigger. 152 | func (k *kq) Watched(n interface{}) (*watched, int64, error) { 153 | kevn, ok := n.(syscall.Kevent_t) 154 | if !ok { 155 | panic(fmt.Sprintf("kq: type should be syscall.Kevent_t, %T instead", kevn)) 156 | } 157 | if _, ok = k.idLkp[int(kevn.Ident)]; !ok { 158 | return nil, 0, errNotWatched 159 | } 160 | return k.idLkp[int(kevn.Ident)], int64(kevn.Fflags), nil 161 | } 162 | 163 | // IsStop implements trigger. 164 | func (k *kq) IsStop(n interface{}, err error) bool { 165 | return int(inter2kq(n).Ident) == k.pipefds[0] 166 | } 167 | 168 | func init() { 169 | encode = func(e Event, dir bool) (o int64) { 170 | // Create event is not supported by kqueue. Instead NoteWrite event will 171 | // be registered for a directory. If this event will be reported on dir 172 | // which is to be monitored for Create, dir will be rescanned 173 | // and Create events will be generated and returned for new files. 174 | // In case of files, if not requested NoteRename event is reported, 175 | // it will be ignored. 176 | o = int64(e &^ Create) 177 | if (e&Create != 0 && dir) || e&Write != 0 { 178 | o = (o &^ int64(Write)) | int64(NoteWrite) 179 | } 180 | if e&Rename != 0 { 181 | o = (o &^ int64(Rename)) | int64(NoteRename) 182 | } 183 | if e&Remove != 0 { 184 | o = (o &^ int64(Remove)) | int64(NoteDelete) 185 | } 186 | return 187 | } 188 | nat2not = map[Event]Event{ 189 | NoteWrite: Write, 190 | NoteRename: Rename, 191 | NoteDelete: Remove, 192 | NoteExtend: Event(0), 193 | NoteAttrib: Event(0), 194 | NoteRevoke: Event(0), 195 | NoteLink: Event(0), 196 | } 197 | not2nat = map[Event]Event{ 198 | Write: NoteWrite, 199 | Rename: NoteRename, 200 | Remove: NoteDelete, 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /watcher_kqueue_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build (darwin && kqueue) || (darwin && !cgo) || dragonfly || freebsd || netbsd || openbsd 6 | // +build darwin,kqueue darwin,!cgo dragonfly freebsd netbsd openbsd 7 | 8 | package notify 9 | 10 | import ( 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | ) 15 | 16 | func kqremove(w *W, path string, files []string) WCase { 17 | cas := remove(w, path) 18 | cas.Events[0] = &Call{P: path, E: NoteDelete} 19 | for _, f := range files { 20 | cas.Events = append(cas.Events, &Call{P: f, E: NoteDelete}) 21 | } 22 | return cas 23 | } 24 | 25 | func kqwrite(w *W, path string, p []byte) WCase { 26 | cas := write(w, path, p) 27 | path = cas.Events[0].Path() 28 | cas.Events[0] = &Call{P: path, E: NoteExtend | NoteWrite} 29 | return cas 30 | } 31 | 32 | func kqrename(w *W, path string, files []string) WCase { 33 | const ext = ".notify" 34 | cas := WCase{ 35 | Action: func() { 36 | file := filepath.Join(w.root, path) 37 | if err := os.Rename(file, file+ext); err != nil { 38 | w.Fatalf("Rename(%q, %q)=%v", path, path+ext, err) 39 | } 40 | }, 41 | Events: []EventInfo{ 42 | &Call{P: path + ext, E: osSpecificCreate}, 43 | &Call{P: path, E: NoteRename}, 44 | }, 45 | } 46 | for _, f := range files { 47 | cas.Events = append(cas.Events, &Call{P: f, E: NoteRename}) 48 | } 49 | return cas 50 | } 51 | 52 | func kqlink(w *W, path string) WCase { 53 | const ext = ".notify" 54 | return WCase{ 55 | Action: func() { 56 | file := filepath.Join(w.root, path) 57 | if err := os.Link(file, file+ext); err != nil { 58 | w.Fatalf("Link(%q, %q)=%v", path, path+ext, err) 59 | } 60 | }, 61 | Events: []EventInfo{ 62 | &Call{P: path, E: NoteLink}, 63 | &Call{P: path + ext, E: osSpecificCreate}, 64 | }, 65 | } 66 | } 67 | 68 | var events = []Event{ 69 | NoteWrite, 70 | NoteAttrib, 71 | NoteRename, 72 | osSpecificCreate, 73 | NoteDelete, 74 | NoteExtend, 75 | NoteLink, 76 | } 77 | 78 | func TestWatcherKqueue(t *testing.T) { 79 | w := NewWatcherTest(t, "testdata/vfs.txt", events...) 80 | defer w.Close() 81 | 82 | cases := [...]WCase{ 83 | kqremove(w, "src/github.com/ppknap/link/include/coost/link", []string{ 84 | "src/github.com/ppknap/link/include/coost/link/definitions.hpp", 85 | "src/github.com/ppknap/link/include/coost/link/detail/bundle.hpp", 86 | "src/github.com/ppknap/link/include/coost/link/detail/container_invoker.hpp", 87 | "src/github.com/ppknap/link/include/coost/link/detail/container_value_trait.hpp", 88 | "src/github.com/ppknap/link/include/coost/link/detail/dummy_type.hpp", 89 | "src/github.com/ppknap/link/include/coost/link/detail/function_trait.hpp", 90 | "src/github.com/ppknap/link/include/coost/link/detail/immediate_invoker.hpp", 91 | "src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/always_same.hpp", 92 | "src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/make_unique.hpp", 93 | "src/github.com/ppknap/link/include/coost/link/detail/stdhelpers", 94 | "src/github.com/ppknap/link/include/coost/link/detail/vertex.hpp", 95 | "src/github.com/ppknap/link/include/coost/link/detail/wire.hpp", 96 | "src/github.com/ppknap/link/include/coost/link/detail", 97 | "src/github.com/ppknap/link/include/coost/link/link.hpp", 98 | }, 99 | ), 100 | kqwrite(w, "src/github.com/rjeczalik/fs/fs.go", []byte("XD")), 101 | kqremove(w, "src/github.com/ppknap/link/README.md", nil), 102 | kqlink(w, "src/github.com/rjeczalik/fs/LICENSE"), 103 | kqrename(w, "src/github.com/rjeczalik/fs/fs.go", nil), 104 | kqrename(w, "src/github.com/rjeczalik/fs/cmd/gotree", []string{ 105 | "src/github.com/rjeczalik/fs/cmd/gotree/go.go", 106 | "src/github.com/rjeczalik/fs/cmd/gotree/main.go", 107 | }, 108 | ), 109 | } 110 | 111 | w.ExpectAll(cases[:]) 112 | } 113 | -------------------------------------------------------------------------------- /watcher_notimplemented.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2018 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build !darwin && !linux && !freebsd && !dragonfly && !netbsd && !openbsd && !windows && !kqueue && !solaris && !illumos 6 | // +build !darwin,!linux,!freebsd,!dragonfly,!netbsd,!openbsd,!windows,!kqueue,!solaris,!illumos 7 | 8 | package notify 9 | 10 | import "errors" 11 | 12 | // newWatcher stub. 13 | func newWatcher(chan<- EventInfo) watcher { 14 | return watcherStub{errors.New("notify: not implemented")} 15 | } 16 | -------------------------------------------------------------------------------- /watcher_readdcw_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build windows 6 | // +build windows 7 | 8 | package notify 9 | 10 | import "testing" 11 | 12 | // TODO(ppknap) : remove notify.Create event. 13 | func rcreate(w *W, path string) WCase { 14 | cas := create(w, path) 15 | cas.Events = append(cas.Events, 16 | &Call{P: path, E: FileActionAdded}, 17 | ) 18 | return cas 19 | } 20 | 21 | // TODO(ppknap) : remove notify.Remove event. 22 | func rremove(w *W, path string) WCase { 23 | cas := remove(w, path) 24 | cas.Events = append(cas.Events, 25 | &Call{P: path, E: FileActionRemoved}, 26 | ) 27 | return cas 28 | } 29 | 30 | // TODO(ppknap) : remove notify.Rename event. 31 | func rrename(w *W, oldpath, newpath string) WCase { 32 | cas := rename(w, oldpath, newpath) 33 | cas.Events = append(cas.Events, 34 | &Call{P: oldpath, E: FileActionRenamedOldName}, 35 | &Call{P: newpath, E: FileActionRenamedNewName}, 36 | ) 37 | return cas 38 | } 39 | 40 | // TODO(ppknap) : remove notify.Write event. 41 | func rwrite(w *W, path string, p []byte) WCase { 42 | cas := write(w, path, p) 43 | cas.Events = append(cas.Events, 44 | &Call{P: path, E: FileActionModified}, 45 | ) 46 | return cas 47 | } 48 | 49 | var events = []Event{ 50 | FileNotifyChangeFileName, 51 | FileNotifyChangeDirName, 52 | FileNotifyChangeSize, 53 | } 54 | 55 | func TestWatcherReadDirectoryChangesW(t *testing.T) { 56 | w := NewWatcherTest(t, "testdata/vfs.txt", events...) 57 | defer w.Close() 58 | 59 | cases := [...]WCase{ 60 | rcreate(w, "src/github.com/rjeczalik/fs/fs_windows.go"), 61 | rcreate(w, "src/github.com/rjeczalik/fs/subdir/"), 62 | rremove(w, "src/github.com/rjeczalik/fs/fs.go"), 63 | rrename(w, "src/github.com/rjeczalik/fs/LICENSE", "src/github.com/rjeczalik/fs/COPYLEFT"), 64 | rwrite(w, "src/github.com/rjeczalik/fs/cmd/gotree/go.go", []byte("XD")), 65 | } 66 | 67 | w.ExpectAny(cases[:]) 68 | } 69 | -------------------------------------------------------------------------------- /watcher_recursive_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build (darwin && !kqueue && cgo) || windows 6 | // +build darwin,!kqueue,cgo windows 7 | 8 | package notify 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | ) 14 | 15 | // noevent stripts test-case from expected event list, used when action is not 16 | // expected to trigger any events. 17 | func noevent(cas WCase) WCase { 18 | return WCase{Action: cas.Action} 19 | } 20 | 21 | func TestWatcherRecursiveRewatch(t *testing.T) { 22 | w := newWatcherTest(t, "testdata/vfs.txt") 23 | defer w.Close() 24 | 25 | cases := []WCase{ 26 | create(w, "src/github.com/rjeczalik/file"), 27 | create(w, "src/github.com/rjeczalik/dir/"), 28 | noevent(create(w, "src/github.com/rjeczalik/fs/dir/")), 29 | noevent(create(w, "src/github.com/dir/")), 30 | noevent(write(w, "src/github.com/rjeczalik/file", []byte("XD"))), 31 | noevent(rename(w, "src/github.com/rjeczalik/fs/LICENSE", "src/LICENSE")), 32 | } 33 | 34 | w.Watch("src/github.com/rjeczalik", Create) 35 | w.ExpectAny(cases) 36 | 37 | cases = []WCase{ 38 | create(w, "src/github.com/rjeczalik/fs/file"), 39 | create(w, "src/github.com/rjeczalik/fs/cmd/gotree/file"), 40 | create(w, "src/github.com/rjeczalik/fs/cmd/dir/"), 41 | create(w, "src/github.com/rjeczalik/fs/cmd/gotree/dir/"), 42 | noevent(write(w, "src/github.com/rjeczalik/fs/file", []byte("XD"))), 43 | noevent(create(w, "src/github.com/anotherdir/")), 44 | } 45 | 46 | w.RecursiveRewatch("src/github.com/rjeczalik", "src/github.com/rjeczalik", Create, Create) 47 | w.ExpectAny(cases) 48 | 49 | cases = []WCase{ 50 | create(w, "src/github.com/rjeczalik/1"), 51 | create(w, "src/github.com/rjeczalik/2/"), 52 | noevent(create(w, "src/github.com/rjeczalik/fs/cmd/1")), 53 | noevent(create(w, "src/github.com/rjeczalik/fs/1/")), 54 | noevent(write(w, "src/github.com/rjeczalik/fs/file", []byte("XD"))), 55 | } 56 | 57 | w.Rewatch("src/github.com/rjeczalik", Create, Create) 58 | w.ExpectAny(cases) 59 | } 60 | 61 | // TODO(rjeczalik): move to watcher_test.go after #5 62 | func TestIsDirCreateEvent(t *testing.T) { 63 | w := NewWatcherTest(t, "testdata/vfs.txt") 64 | defer w.Close() 65 | 66 | cases := [...]WCase{ 67 | // i=0 68 | create(w, "src/github.com/jszwec/"), 69 | // i=1 70 | create(w, "src/github.com/jszwec/gojunitxml/"), 71 | // i=2 72 | create(w, "src/github.com/jszwec/gojunitxml/README.md"), 73 | // i=3 74 | create(w, "src/github.com/jszwec/gojunitxml/LICENSE"), 75 | // i=4 76 | create(w, "src/github.com/jszwec/gojunitxml/cmd/"), 77 | } 78 | 79 | dirs := [...]bool{ 80 | true, // i=0 81 | true, // i=1 82 | false, // i=2 83 | false, // i=3 84 | true, // i=4 85 | } 86 | 87 | fn := func(i int, _ WCase, ei EventInfo) error { 88 | d, ok := ei.(isDirer) 89 | if !ok { 90 | return fmt.Errorf("received EventInfo does not implement isDirer") 91 | } 92 | switch ok, err := d.isDir(); { 93 | case err != nil: 94 | return err 95 | case ok != dirs[i]: 96 | return fmt.Errorf("want ok=%v; got %v", dirs[i], ok) 97 | default: 98 | return nil 99 | } 100 | } 101 | 102 | w.ExpectAnyFunc(cases[:], fn) 103 | } 104 | -------------------------------------------------------------------------------- /watcher_stub.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2018 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | type watcherStub struct{ error } 8 | 9 | // Following methods implement notify.watcher interface. 10 | func (s watcherStub) Watch(string, Event) error { return s } 11 | func (s watcherStub) Rewatch(string, Event, Event) error { return s } 12 | func (s watcherStub) Unwatch(string) (err error) { return s } 13 | func (s watcherStub) Close() error { return s } 14 | -------------------------------------------------------------------------------- /watcher_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build darwin || linux || freebsd || dragonfly || netbsd || openbsd || windows || solaris 6 | // +build darwin linux freebsd dragonfly netbsd openbsd windows solaris 7 | 8 | package notify 9 | 10 | import ( 11 | "os" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | // NOTE Set NOTIFY_DEBUG env var or debug build tag for extra debugging info. 17 | 18 | func TestWatcher(t *testing.T) { 19 | w := NewWatcherTest(t, "testdata/vfs.txt") 20 | defer w.Close() 21 | 22 | cases := [...]WCase{ 23 | create(w, "src/github.com/ppknap/link/include/coost/.link.hpp.swp"), 24 | create(w, "src/github.com/rjeczalik/fs/fs_test.go"), 25 | create(w, "src/github.com/rjeczalik/fs/binfs/"), 26 | create(w, "src/github.com/rjeczalik/fs/binfs.go"), 27 | create(w, "src/github.com/rjeczalik/fs/binfs_test.go"), 28 | remove(w, "src/github.com/rjeczalik/fs/binfs/"), 29 | create(w, "src/github.com/rjeczalik/fs/binfs/"), 30 | create(w, "src/github.com/rjeczalik/fs/virfs"), 31 | remove(w, "src/github.com/rjeczalik/fs/virfs"), 32 | create(w, "file"), 33 | create(w, "dir/"), 34 | } 35 | 36 | w.ExpectAny(cases[:]) 37 | } 38 | 39 | // Simulates the scenario, where outside of the programs control the base dir 40 | // is removed. This is detected and the watch removed. Then the directory is 41 | // restored and a new watch set up. 42 | func TestStopPathNotExists(t *testing.T) { 43 | w := NewWatcherTest(t, "testdata/vfs.txt") 44 | defer w.Close() 45 | 46 | if err := os.RemoveAll(w.root); err != nil { 47 | panic(err) 48 | } 49 | Sync() 50 | 51 | // Don't check the returned error, as the public function (notify.Stop) 52 | // does not return a potential error. As long as everything later on 53 | // works as inteded, that's fine 54 | time.Sleep(time.Duration(100) * time.Millisecond) 55 | w.Watcher.Unwatch(w.root) 56 | time.Sleep(time.Duration(100) * time.Millisecond) 57 | 58 | if err := os.Mkdir(w.root, 0777); err != nil { 59 | panic(err) 60 | } 61 | Sync() 62 | w.Watch("", All) 63 | 64 | drainall(w.C) 65 | cases := [...]WCase{ 66 | create(w, "file"), 67 | create(w, "dir/"), 68 | } 69 | w.ExpectAny(cases[:]) 70 | } 71 | 72 | func TestWatcherUnwatch(t *testing.T) { 73 | w := NewWatcherTest(t, "testdata/vfs.txt") 74 | defer w.Close() 75 | 76 | remove(w, "src/github.com/ppknap/link/test/test_circular_calls.cpp").Action() 77 | w.Unwatch("") 78 | 79 | w.Watch("", All) 80 | 81 | drainall(w.C) 82 | cases := [...]WCase{ 83 | create(w, "file"), 84 | create(w, "dir/"), 85 | } 86 | w.ExpectAny(cases[:]) 87 | } 88 | -------------------------------------------------------------------------------- /watcher_trigger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build (darwin && kqueue) || (darwin && !cgo) || dragonfly || freebsd || netbsd || openbsd || solaris || illumos 6 | // +build darwin,kqueue darwin,!cgo dragonfly freebsd netbsd openbsd solaris illumos 7 | 8 | // watcher_trigger is used for FEN and kqueue which behave similarly: 9 | // only files and dirs can be watched directly, but not files inside dirs. 10 | // As a result Create events have to be generated by implementation when 11 | // after Write event is returned for watched dir, it is rescanned and Create 12 | // event is returned for new files and these are automatically added 13 | // to watchlist. In case of removal of watched directory, native system returns 14 | // events for all files, but for Rename, they also need to be generated. 15 | // As a result native system works as something like trigger for rescan, 16 | // but contains additional data about dir in which changes occurred. For files 17 | // detailed data is returned. 18 | // Usage of watcher_trigger requires: 19 | // - trigger implementation, 20 | // - encode func, 21 | // - not2nat, nat2not maps. 22 | // Required manual operations on filesystem can lead to loss of precision. 23 | 24 | package notify 25 | 26 | import ( 27 | "fmt" 28 | "os" 29 | "path/filepath" 30 | "strings" 31 | "sync" 32 | "syscall" 33 | ) 34 | 35 | // trigger is to be implemented by platform implementation like FEN or kqueue. 36 | type trigger interface { 37 | // Close closes watcher's main native file descriptor. 38 | Close() error 39 | // Stop waiting for new events. 40 | Stop() error 41 | // Create new instance of watched. 42 | NewWatched(string, os.FileInfo) (*watched, error) 43 | // Record internally new *watched instance. 44 | Record(*watched) 45 | // Del removes internal copy of *watched instance. 46 | Del(*watched) 47 | // Watched returns *watched instance and native events for native type. 48 | Watched(interface{}) (*watched, int64, error) 49 | // Init initializes native watcher call. 50 | Init() error 51 | // Watch starts watching provided file/dir. 52 | Watch(os.FileInfo, *watched, int64) error 53 | // Unwatch stops watching provided file/dir. 54 | Unwatch(*watched) error 55 | // Wait for new events. 56 | Wait() (interface{}, error) 57 | // IsStop checks if Wait finished because of request watcher's stop. 58 | IsStop(n interface{}, err error) bool 59 | } 60 | 61 | // trgWatched is a the base data structure representing watched file/directory. 62 | // The platform specific full data structure (watched) must embed this type. 63 | type trgWatched struct { 64 | // p is a path to watched file/directory. 65 | p string 66 | // fi provides information about watched file/dir. 67 | fi os.FileInfo 68 | // eDir represents events watched directly. 69 | eDir Event 70 | // eNonDir represents events watched indirectly. 71 | eNonDir Event 72 | } 73 | 74 | // encode Event to native representation. Implementation is to be provided by 75 | // platform specific implementation. 76 | var encode func(Event, bool) int64 77 | 78 | var ( 79 | // nat2not matches native events to notify's ones. To be initialized by 80 | // platform dependent implementation. 81 | nat2not map[Event]Event 82 | // not2nat matches notify's events to native ones. To be initialized by 83 | // platform dependent implementation. 84 | not2nat map[Event]Event 85 | ) 86 | 87 | // trg is a main structure implementing watcher. 88 | type trg struct { 89 | sync.Mutex 90 | // s is a channel used to stop monitoring. 91 | s chan struct{} 92 | // c is a channel used to pass events further. 93 | c chan<- EventInfo 94 | // pthLkp is a data structure mapping file names with data about watching 95 | // represented by them files/directories. 96 | pthLkp map[string]*watched 97 | // t is a platform dependent implementation of trigger. 98 | t trigger 99 | } 100 | 101 | // newWatcher returns new watcher's implementation. 102 | func newWatcher(c chan<- EventInfo) watcher { 103 | t := &trg{ 104 | s: make(chan struct{}, 1), 105 | pthLkp: make(map[string]*watched, 0), 106 | c: c, 107 | } 108 | t.t = newTrigger(t.pthLkp) 109 | if err := t.t.Init(); err != nil { 110 | t.Close() 111 | return watcherStub{fmt.Errorf("failed setting up watcher: %v", err)} 112 | } 113 | go t.monitor() 114 | return t 115 | } 116 | 117 | // Close implements watcher. 118 | func (t *trg) Close() (err error) { 119 | t.Lock() 120 | if err = t.t.Stop(); err != nil { 121 | t.Unlock() 122 | return 123 | } 124 | <-t.s 125 | var e error 126 | for _, w := range t.pthLkp { 127 | if e = t.unwatch(w.p, w.fi); e != nil { 128 | dbgprintf("trg: unwatch %q failed: %q\n", w.p, e) 129 | err = nonil(err, e) 130 | } 131 | } 132 | if e = t.t.Close(); e != nil { 133 | dbgprintf("trg: closing native watch failed: %q\n", e) 134 | err = nonil(err, e) 135 | } 136 | if remaining := len(t.pthLkp); remaining != 0 { 137 | err = nonil(err, fmt.Errorf("Not all watches were removed: len(t.pthLkp) == %v", len(t.pthLkp))) 138 | } 139 | t.Unlock() 140 | return 141 | } 142 | 143 | // send reported events one by one through chan. 144 | func (t *trg) send(evn []event) { 145 | for i := range evn { 146 | t.c <- &evn[i] 147 | } 148 | } 149 | 150 | // singlewatch starts to watch given p file/directory. 151 | func (t *trg) singlewatch(p string, e Event, direct mode, fi os.FileInfo) (err error) { 152 | w, ok := t.pthLkp[p] 153 | if !ok { 154 | if w, err = t.t.NewWatched(p, fi); err != nil { 155 | if err == errSkip { 156 | err = nil 157 | } 158 | return 159 | } 160 | } 161 | switch direct { 162 | case dir: 163 | w.eDir |= e 164 | case ndir: 165 | w.eNonDir |= e 166 | case both: 167 | w.eDir |= e 168 | w.eNonDir |= e 169 | } 170 | if err = t.t.Watch(fi, w, encode(w.eDir|w.eNonDir, fi.IsDir())); err != nil { 171 | return 172 | } 173 | if !ok { 174 | t.t.Record(w) 175 | return nil 176 | } 177 | return errAlreadyWatched 178 | } 179 | 180 | // decode converts event received from native to notify.Event 181 | // representation taking into account requested events (w). 182 | func decode(o int64, w Event) (e Event) { 183 | for f, n := range nat2not { 184 | if o&int64(f) != 0 { 185 | if w&f != 0 { 186 | e |= f 187 | } 188 | if w&n != 0 { 189 | e |= n 190 | } 191 | } 192 | } 193 | 194 | return 195 | } 196 | 197 | func (t *trg) watch(p string, e Event, fi os.FileInfo) error { 198 | if err := t.singlewatch(p, e, dir, fi); err != nil { 199 | if err != errAlreadyWatched { 200 | return err 201 | } 202 | } 203 | if fi.IsDir() { 204 | err := t.walk(p, func(fi os.FileInfo) (err error) { 205 | if err = t.singlewatch(filepath.Join(p, fi.Name()), e, ndir, 206 | fi); err != nil { 207 | if err != errAlreadyWatched { 208 | return 209 | } 210 | } 211 | return nil 212 | }) 213 | if err != nil { 214 | return err 215 | } 216 | } 217 | return nil 218 | } 219 | 220 | // walk runs f func on each file/dir from p directory. 221 | func (t *trg) walk(p string, fn func(os.FileInfo) error) error { 222 | fp, err := os.Open(p) 223 | if err != nil { 224 | return err 225 | } 226 | ls, err := fp.Readdir(0) 227 | fp.Close() 228 | if err != nil { 229 | return err 230 | } 231 | for i := range ls { 232 | if err := fn(ls[i]); err != nil { 233 | return err 234 | } 235 | } 236 | return nil 237 | } 238 | 239 | func (t *trg) unwatch(p string, fi os.FileInfo) error { 240 | if fi.IsDir() { 241 | err := t.walk(p, func(fi os.FileInfo) error { 242 | err := t.singleunwatch(filepath.Join(p, fi.Name()), ndir) 243 | if err != errNotWatched { 244 | return err 245 | } 246 | return nil 247 | }) 248 | if err != nil { 249 | return err 250 | } 251 | } 252 | return t.singleunwatch(p, dir) 253 | } 254 | 255 | // Watch implements Watcher interface. 256 | func (t *trg) Watch(p string, e Event) error { 257 | fi, err := os.Stat(p) 258 | if err != nil { 259 | return err 260 | } 261 | t.Lock() 262 | err = t.watch(p, e, fi) 263 | t.Unlock() 264 | return err 265 | } 266 | 267 | // Unwatch implements Watcher interface. 268 | func (t *trg) Unwatch(p string) error { 269 | fi, err := os.Stat(p) 270 | if err != nil { 271 | return err 272 | } 273 | t.Lock() 274 | err = t.unwatch(p, fi) 275 | t.Unlock() 276 | return err 277 | } 278 | 279 | // Rewatch implements Watcher interface. 280 | // 281 | // TODO(rjeczalik): This is a naive hack. Rewrite might help. 282 | func (t *trg) Rewatch(p string, _, e Event) error { 283 | fi, err := os.Stat(p) 284 | if err != nil { 285 | return err 286 | } 287 | t.Lock() 288 | if err = t.unwatch(p, fi); err == nil { 289 | // TODO(rjeczalik): If watch fails then we leave trigger in inconsistent 290 | // state. Handle? Panic? Native version of rewatch? 291 | err = t.watch(p, e, fi) 292 | } 293 | t.Unlock() 294 | return nil 295 | } 296 | 297 | func (*trg) file(w *watched, n interface{}, e Event) (evn []event) { 298 | evn = append(evn, event{w.p, e, w.fi.IsDir(), n}) 299 | return 300 | } 301 | 302 | func (t *trg) dir(w *watched, n interface{}, e, ge Event) (evn []event) { 303 | // If it's dir and delete we have to send it and continue, because 304 | // other processing relies on opening (in this case not existing) dir. 305 | // Events for contents of this dir are reported by native impl. 306 | // However events for rename must be generated for all monitored files 307 | // inside of moved directory, because native impl does not report it independently 308 | // for each file descriptor being moved in result of move action on 309 | // parent directory. 310 | if (ge & (not2nat[Rename] | not2nat[Remove])) != 0 { 311 | // Write is reported also for Remove on directory. Because of that 312 | // we have to filter it out explicitly. 313 | evn = append(evn, event{w.p, e & ^Write & ^not2nat[Write], true, n}) 314 | if ge¬2nat[Rename] != 0 { 315 | for p := range t.pthLkp { 316 | if strings.HasPrefix(p, w.p+string(os.PathSeparator)) { 317 | if err := t.singleunwatch(p, both); err != nil && err != errNotWatched && 318 | !os.IsNotExist(err) { 319 | dbgprintf("trg: failed stop watching moved file (%q): %q\n", 320 | p, err) 321 | } 322 | if (w.eDir|w.eNonDir)&(not2nat[Rename]|Rename) != 0 { 323 | evn = append(evn, event{ 324 | p, (w.eDir | w.eNonDir) & e &^ Write &^ not2nat[Write], 325 | w.fi.IsDir(), nil, 326 | }) 327 | } 328 | } 329 | } 330 | } 331 | t.t.Del(w) 332 | return 333 | } 334 | if (ge & not2nat[Write]) != 0 { 335 | switch err := t.walk(w.p, func(fi os.FileInfo) error { 336 | p := filepath.Join(w.p, fi.Name()) 337 | switch err := t.singlewatch(p, w.eDir, ndir, fi); { 338 | case os.IsNotExist(err) && ((w.eDir & Remove) != 0): 339 | evn = append(evn, event{p, Remove, fi.IsDir(), n}) 340 | case err == errAlreadyWatched: 341 | case err != nil: 342 | dbgprintf("trg: watching %q failed: %q", p, err) 343 | case (w.eDir & Create) != 0: 344 | evn = append(evn, event{p, Create, fi.IsDir(), n}) 345 | default: 346 | } 347 | return nil 348 | }); { 349 | case os.IsNotExist(err): 350 | return 351 | case err != nil: 352 | dbgprintf("trg: dir processing failed: %q", err) 353 | default: 354 | } 355 | } 356 | return 357 | } 358 | 359 | type mode uint 360 | 361 | const ( 362 | dir mode = iota 363 | ndir 364 | both 365 | ) 366 | 367 | // unwatch stops watching p file/directory. 368 | func (t *trg) singleunwatch(p string, direct mode) error { 369 | w, ok := t.pthLkp[p] 370 | if !ok { 371 | return errNotWatched 372 | } 373 | switch direct { 374 | case dir: 375 | w.eDir = 0 376 | case ndir: 377 | w.eNonDir = 0 378 | case both: 379 | w.eDir, w.eNonDir = 0, 0 380 | } 381 | if err := t.t.Unwatch(w); err != nil { 382 | return err 383 | } 384 | if w.eNonDir|w.eDir != 0 { 385 | mod := dir 386 | if w.eNonDir != 0 { 387 | mod = ndir 388 | } 389 | if err := t.singlewatch(p, w.eNonDir|w.eDir, mod, 390 | w.fi); err != nil && err != errAlreadyWatched { 391 | return err 392 | } 393 | } else { 394 | t.t.Del(w) 395 | } 396 | return nil 397 | } 398 | 399 | func (t *trg) monitor() { 400 | var ( 401 | n interface{} 402 | err error 403 | ) 404 | for { 405 | switch n, err = t.t.Wait(); { 406 | case err == syscall.EINTR: 407 | case t.t.IsStop(n, err): 408 | t.s <- struct{}{} 409 | return 410 | case err != nil: 411 | dbgprintf("trg: failed to read events: %q\n", err) 412 | default: 413 | t.send(t.process(n)) 414 | } 415 | } 416 | } 417 | 418 | // process event returned by native call. 419 | func (t *trg) process(n interface{}) (evn []event) { 420 | t.Lock() 421 | w, ge, err := t.t.Watched(n) 422 | if err != nil { 423 | t.Unlock() 424 | dbgprintf("trg: %v event lookup failed: %q", Event(ge), err) 425 | return 426 | } 427 | 428 | e := decode(ge, w.eDir|w.eNonDir) 429 | if ge&int64(not2nat[Remove]|not2nat[Rename]) == 0 { 430 | switch fi, err := os.Stat(w.p); { 431 | case err != nil: 432 | default: 433 | if err = t.t.Watch(fi, w, encode(w.eDir|w.eNonDir, fi.IsDir())); err != nil { 434 | dbgprintf("trg: %q is no longer watched: %q", w.p, err) 435 | t.t.Del(w) 436 | } 437 | } 438 | } 439 | if e == Event(0) && (!w.fi.IsDir() || (ge&int64(not2nat[Write])) == 0) { 440 | t.Unlock() 441 | return 442 | } 443 | 444 | if w.fi.IsDir() { 445 | evn = append(evn, t.dir(w, n, e, Event(ge))...) 446 | } else { 447 | evn = append(evn, t.file(w, n, e)...) 448 | } 449 | if Event(ge)&(not2nat[Remove]|not2nat[Rename]) != 0 { 450 | t.t.Del(w) 451 | } 452 | t.Unlock() 453 | return 454 | } 455 | -------------------------------------------------------------------------------- /watcher_trigger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2017 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build (darwin && kqueue) || (darwin && !cgo) || dragonfly || freebsd || netbsd || openbsd || solaris || illumos 6 | // +build darwin,kqueue darwin,!cgo dragonfly freebsd netbsd openbsd solaris illumos 7 | 8 | package notify 9 | 10 | import "testing" 11 | 12 | func TestWatcherCreateOnly(t *testing.T) { 13 | w := NewWatcherTest(t, "testdata/vfs.txt", Create) 14 | defer w.Close() 15 | 16 | cases := [...]WCase{ 17 | create(w, "dir/"), 18 | create(w, "dir2/"), 19 | } 20 | 21 | w.ExpectAny(cases[:]) 22 | } 23 | -------------------------------------------------------------------------------- /watchpoint.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | // EventDiff describes a change to an event set - EventDiff[0] is an old state, 8 | // while EventDiff[1] is a new state. If event set has not changed (old == new), 9 | // functions typically return the None value. 10 | type eventDiff [2]Event 11 | 12 | func (diff eventDiff) Event() Event { 13 | return diff[1] &^ diff[0] 14 | } 15 | 16 | // Watchpoint 17 | // 18 | // The nil key holds total event set - logical sum for all registered events. 19 | // It speeds up computing EventDiff for Add method. 20 | // 21 | // The rec key holds an event set for a watchpoints created by RecursiveWatch 22 | // for a Watcher implementation which is not natively recursive. 23 | type watchpoint map[chan<- EventInfo]Event 24 | 25 | // None is an empty event diff, think null object. 26 | var none eventDiff 27 | 28 | // rec is just a placeholder 29 | var rec = func() (ch chan<- EventInfo) { 30 | ch = make(chan<- EventInfo) 31 | close(ch) 32 | return 33 | }() 34 | 35 | func (wp watchpoint) dryAdd(ch chan<- EventInfo, e Event) eventDiff { 36 | if e &^= internal; wp[ch]&e == e { 37 | return none 38 | } 39 | total := wp[ch] &^ internal 40 | return eventDiff{total, total | e} 41 | } 42 | 43 | // Add assumes neither c nor e are nil or zero values. 44 | func (wp watchpoint) Add(c chan<- EventInfo, e Event) (diff eventDiff) { 45 | wp[c] |= e 46 | diff[0] = wp[nil] 47 | diff[1] = diff[0] | e 48 | wp[nil] = diff[1] &^ omit 49 | // Strip diff from internal events. 50 | diff[0] &^= internal 51 | diff[1] &^= internal 52 | if diff[0] == diff[1] { 53 | return none 54 | } 55 | return 56 | } 57 | 58 | func (wp watchpoint) Del(c chan<- EventInfo, e Event) (diff eventDiff) { 59 | wp[c] &^= e 60 | if wp[c] == 0 { 61 | delete(wp, c) 62 | } 63 | diff[0] = wp[nil] 64 | delete(wp, nil) 65 | if len(wp) != 0 { 66 | // Recalculate total event set. 67 | for _, e := range wp { 68 | diff[1] |= e 69 | } 70 | wp[nil] = diff[1] &^ omit 71 | } 72 | // Strip diff from internal events. 73 | diff[0] &^= internal 74 | diff[1] &^= internal 75 | if diff[0] == diff[1] { 76 | return none 77 | } 78 | return 79 | } 80 | 81 | func (wp watchpoint) Dispatch(ei EventInfo, extra Event) { 82 | e := eventmask(ei, extra) 83 | if !matches(wp[nil], e) { 84 | return 85 | } 86 | for ch, eset := range wp { 87 | if ch != nil && matches(eset, e) { 88 | select { 89 | case ch <- ei: 90 | default: // Drop event if receiver is too slow 91 | dbgprintf("dropped %s on %q: receiver too slow", ei.Event(), ei.Path()) 92 | } 93 | } 94 | } 95 | } 96 | 97 | func (wp watchpoint) Total() Event { 98 | return wp[nil] &^ internal 99 | } 100 | 101 | func (wp watchpoint) IsRecursive() bool { 102 | return wp[nil]&recursive != 0 103 | } 104 | -------------------------------------------------------------------------------- /watchpoint_other.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build !windows 6 | // +build !windows 7 | 8 | package notify 9 | 10 | // eventmask uses ei to create a new event which contains internal flags used by 11 | // notify package logic. 12 | func eventmask(ei EventInfo, extra Event) Event { 13 | return ei.Event() | extra 14 | } 15 | 16 | // matches reports a match only when: 17 | // 18 | // - for user events, when event is present in the given set 19 | // - for internal events, when additionally both event and set have omit bit set 20 | // 21 | // Internal events must not be sent to user channels and vice versa. 22 | func matches(set, event Event) bool { 23 | return (set&omit)^(event&omit) == 0 && set&event == event 24 | } 25 | -------------------------------------------------------------------------------- /watchpoint_readdcw.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | //go:build windows 6 | // +build windows 7 | 8 | package notify 9 | 10 | // eventmask uses ei to create a new event which contains internal flags used by 11 | // notify package logic. If one of FileAction* masks is detected, this function 12 | // adds corresponding FileNotifyChange* values. This allows non registered 13 | // FileAction* events to be passed on. 14 | func eventmask(ei EventInfo, extra Event) (e Event) { 15 | if e = ei.Event() | extra; e&fileActionAll != 0 { 16 | if ev, ok := ei.(*event); ok { 17 | switch ev.ftype { 18 | case fTypeFile: 19 | e |= FileNotifyChangeFileName 20 | case fTypeDirectory: 21 | e |= FileNotifyChangeDirName 22 | case fTypeUnknown: 23 | e |= fileNotifyChangeModified 24 | } 25 | return e &^ fileActionAll 26 | } 27 | } 28 | return 29 | } 30 | 31 | // matches reports a match only when: 32 | // 33 | // - for user events, when event is present in the given set 34 | // - for internal events, when additionally both event and set have omit bit set 35 | // 36 | // Internal events must not be sent to user channels and vice versa. 37 | func matches(set, event Event) bool { 38 | return (set&omit)^(event&omit) == 0 && (set&event == event || set&fileNotifyChangeModified&event != 0) 39 | } 40 | -------------------------------------------------------------------------------- /watchpoint_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2015 The Notify Authors. All rights reserved. 2 | // Use of this source code is governed by the MIT license that can be 3 | // found in the LICENSE file. 4 | 5 | package notify 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | func call(wp watchpoint, fn interface{}, args []interface{}) eventDiff { 14 | vals := []reflect.Value{reflect.ValueOf(wp)} 15 | for _, arg := range args { 16 | vals = append(vals, reflect.ValueOf(arg)) 17 | } 18 | res := reflect.ValueOf(fn).Call(vals) 19 | if n := len(res); n != 1 { 20 | panic(fmt.Sprintf("unexpected len(res)=%d", n)) 21 | } 22 | diff, ok := res[0].Interface().(eventDiff) 23 | if !ok { 24 | panic(fmt.Sprintf("want typeof(diff)=EventDiff; got %T", res[0].Interface())) 25 | } 26 | return diff 27 | } 28 | 29 | func TestWatchpoint(t *testing.T) { 30 | ch := NewChans(5) 31 | all := All | recursive 32 | cases := [...]struct { 33 | fn interface{} 34 | args []interface{} 35 | diff eventDiff 36 | total Event 37 | }{ 38 | // i=0 39 | { 40 | watchpoint.Add, 41 | []interface{}{ch[0], Remove}, 42 | eventDiff{0, Remove}, 43 | Remove, 44 | }, 45 | // i=1 46 | { 47 | watchpoint.Add, 48 | []interface{}{ch[1], Create | Remove | recursive}, 49 | eventDiff{Remove, Remove | Create}, 50 | Create | Remove | recursive, 51 | }, 52 | // i=2 53 | { 54 | watchpoint.Add, 55 | []interface{}{ch[2], Create | Rename}, 56 | eventDiff{Create | Remove, Create | Remove | Rename}, 57 | Create | Remove | Rename | recursive, 58 | }, 59 | // i=3 60 | { 61 | watchpoint.Add, 62 | []interface{}{ch[0], Write | recursive}, 63 | eventDiff{Create | Remove | Rename, Create | Remove | Rename | Write}, 64 | Create | Remove | Rename | Write | recursive, 65 | }, 66 | // i=4 67 | { 68 | watchpoint.Add, 69 | []interface{}{ch[2], Remove | recursive}, 70 | none, 71 | Create | Remove | Rename | Write | recursive, 72 | }, 73 | // i=5 74 | { 75 | watchpoint.Del, 76 | []interface{}{ch[0], all}, 77 | eventDiff{Create | Remove | Rename | Write, Create | Remove | Rename}, 78 | Create | Remove | Rename | recursive, 79 | }, 80 | // i=6 81 | { 82 | watchpoint.Del, 83 | []interface{}{ch[2], all}, 84 | eventDiff{Create | Remove | Rename, Create | Remove}, 85 | Create | Remove | recursive, 86 | }, 87 | // i=7 88 | { 89 | watchpoint.Add, 90 | []interface{}{ch[3], Create | Remove}, 91 | none, 92 | Create | Remove | recursive, 93 | }, 94 | // i=8 95 | { 96 | watchpoint.Del, 97 | []interface{}{ch[1], all}, 98 | none, 99 | Create | Remove, 100 | }, 101 | // i=9 102 | { 103 | watchpoint.Add, 104 | []interface{}{ch[3], recursive | Write}, 105 | eventDiff{Create | Remove, Create | Remove | Write}, 106 | Create | Remove | Write | recursive, 107 | }, 108 | // i=10 109 | { 110 | watchpoint.Del, 111 | []interface{}{ch[3], Create}, 112 | eventDiff{Create | Remove | Write, Remove | Write}, 113 | Remove | Write | recursive, 114 | }, 115 | // i=11 116 | { 117 | watchpoint.Add, 118 | []interface{}{ch[3], Create | Rename}, 119 | eventDiff{Remove | Write, Create | Remove | Rename | Write}, 120 | Create | Remove | Rename | Write | recursive, 121 | }, 122 | // i=12 123 | { 124 | watchpoint.Add, 125 | []interface{}{ch[2], Remove | Write}, 126 | none, 127 | Create | Remove | Rename | Write | recursive, 128 | }, 129 | // i=13 130 | { 131 | watchpoint.Del, 132 | []interface{}{ch[3], Create | Remove | Write}, 133 | eventDiff{Create | Remove | Rename | Write, Remove | Rename | Write}, 134 | Remove | Rename | Write | recursive, 135 | }, 136 | // i=14 137 | { 138 | watchpoint.Del, 139 | []interface{}{ch[2], Remove}, 140 | eventDiff{Remove | Rename | Write, Rename | Write}, 141 | Rename | Write | recursive, 142 | }, 143 | // i=15 144 | { 145 | watchpoint.Del, 146 | []interface{}{ch[3], Rename | recursive}, 147 | eventDiff{Rename | Write, Write}, 148 | Write, 149 | }, 150 | } 151 | wp := watchpoint{} 152 | for i, cas := range cases { 153 | if diff := call(wp, cas.fn, cas.args); diff != cas.diff { 154 | t.Errorf("want diff=%v; got %v (i=%d)", cas.diff, diff, i) 155 | continue 156 | } 157 | if total := wp[nil]; total != cas.total { 158 | t.Errorf("want total=%v; got %v (i=%d)", cas.total, total, i) 159 | continue 160 | } 161 | } 162 | } 163 | --------------------------------------------------------------------------------