497 lines
14 KiB
Go
497 lines
14 KiB
Go
// Copyright 2010 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// +build freebsd openbsd netbsd dragonfly darwin
|
|
|
|
package fsnotify
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"syscall"
|
|
)
|
|
|
|
const (
|
|
// Flags (from <sys/event.h>)
|
|
sys_NOTE_DELETE = 0x0001 /* vnode was removed */
|
|
sys_NOTE_WRITE = 0x0002 /* data contents changed */
|
|
sys_NOTE_EXTEND = 0x0004 /* size increased */
|
|
sys_NOTE_ATTRIB = 0x0008 /* attributes changed */
|
|
sys_NOTE_LINK = 0x0010 /* link count changed */
|
|
sys_NOTE_RENAME = 0x0020 /* vnode was renamed */
|
|
sys_NOTE_REVOKE = 0x0040 /* vnode access was revoked */
|
|
|
|
// Watch all events
|
|
sys_NOTE_ALLEVENTS = sys_NOTE_DELETE | sys_NOTE_WRITE | sys_NOTE_ATTRIB | sys_NOTE_RENAME
|
|
|
|
// Block for 100 ms on each call to kevent
|
|
keventWaitTime = 100e6
|
|
)
|
|
|
|
type FileEvent struct {
|
|
mask uint32 // Mask of events
|
|
Name string // File name (optional)
|
|
create bool // set by fsnotify package if found new file
|
|
}
|
|
|
|
// IsCreate reports whether the FileEvent was triggered by a creation
|
|
func (e *FileEvent) IsCreate() bool { return e.create }
|
|
|
|
// IsDelete reports whether the FileEvent was triggered by a delete
|
|
func (e *FileEvent) IsDelete() bool { return (e.mask & sys_NOTE_DELETE) == sys_NOTE_DELETE }
|
|
|
|
// IsModify reports whether the FileEvent was triggered by a file modification
|
|
func (e *FileEvent) IsModify() bool {
|
|
return ((e.mask&sys_NOTE_WRITE) == sys_NOTE_WRITE || (e.mask&sys_NOTE_ATTRIB) == sys_NOTE_ATTRIB)
|
|
}
|
|
|
|
// IsRename reports whether the FileEvent was triggered by a change name
|
|
func (e *FileEvent) IsRename() bool { return (e.mask & sys_NOTE_RENAME) == sys_NOTE_RENAME }
|
|
|
|
// IsAttrib reports whether the FileEvent was triggered by a change in the file metadata.
|
|
func (e *FileEvent) IsAttrib() bool {
|
|
return (e.mask & sys_NOTE_ATTRIB) == sys_NOTE_ATTRIB
|
|
}
|
|
|
|
type Watcher struct {
|
|
mu sync.Mutex // Mutex for the Watcher itself.
|
|
kq int // File descriptor (as returned by the kqueue() syscall)
|
|
watches map[string]int // Map of watched file descriptors (key: path)
|
|
wmut sync.Mutex // Protects access to watches.
|
|
fsnFlags map[string]uint32 // Map of watched files to flags used for filter
|
|
fsnmut sync.Mutex // Protects access to fsnFlags.
|
|
enFlags map[string]uint32 // Map of watched files to evfilt note flags used in kqueue
|
|
enmut sync.Mutex // Protects access to enFlags.
|
|
paths map[int]string // Map of watched paths (key: watch descriptor)
|
|
finfo map[int]os.FileInfo // Map of file information (isDir, isReg; key: watch descriptor)
|
|
pmut sync.Mutex // Protects access to paths and finfo.
|
|
fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events)
|
|
femut sync.Mutex // Protects access to fileExists.
|
|
externalWatches map[string]bool // Map of watches added by user of the library.
|
|
ewmut sync.Mutex // Protects access to externalWatches.
|
|
Error chan error // Errors are sent on this channel
|
|
internalEvent chan *FileEvent // Events are queued on this channel
|
|
Event chan *FileEvent // Events are returned on this channel
|
|
done chan bool // Channel for sending a "quit message" to the reader goroutine
|
|
isClosed bool // Set to true when Close() is first called
|
|
}
|
|
|
|
// NewWatcher creates and returns a new kevent instance using kqueue(2)
|
|
func NewWatcher() (*Watcher, error) {
|
|
fd, errno := syscall.Kqueue()
|
|
if fd == -1 {
|
|
return nil, os.NewSyscallError("kqueue", errno)
|
|
}
|
|
w := &Watcher{
|
|
kq: fd,
|
|
watches: make(map[string]int),
|
|
fsnFlags: make(map[string]uint32),
|
|
enFlags: make(map[string]uint32),
|
|
paths: make(map[int]string),
|
|
finfo: make(map[int]os.FileInfo),
|
|
fileExists: make(map[string]bool),
|
|
externalWatches: make(map[string]bool),
|
|
internalEvent: make(chan *FileEvent),
|
|
Event: make(chan *FileEvent),
|
|
Error: make(chan error),
|
|
done: make(chan bool, 1),
|
|
}
|
|
|
|
go w.readEvents()
|
|
go w.purgeEvents()
|
|
return w, nil
|
|
}
|
|
|
|
// Close closes a kevent watcher instance
|
|
// It sends a message to the reader goroutine to quit and removes all watches
|
|
// associated with the kevent instance
|
|
func (w *Watcher) Close() error {
|
|
w.mu.Lock()
|
|
if w.isClosed {
|
|
w.mu.Unlock()
|
|
return nil
|
|
}
|
|
w.isClosed = true
|
|
w.mu.Unlock()
|
|
|
|
// Send "quit" message to the reader goroutine
|
|
w.done <- true
|
|
w.wmut.Lock()
|
|
ws := w.watches
|
|
w.wmut.Unlock()
|
|
for path := range ws {
|
|
w.removeWatch(path)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddWatch adds path to the watched file set.
|
|
// The flags are interpreted as described in kevent(2).
|
|
func (w *Watcher) addWatch(path string, flags uint32) error {
|
|
w.mu.Lock()
|
|
if w.isClosed {
|
|
w.mu.Unlock()
|
|
return errors.New("kevent instance already closed")
|
|
}
|
|
w.mu.Unlock()
|
|
|
|
watchDir := false
|
|
|
|
w.wmut.Lock()
|
|
watchfd, found := w.watches[path]
|
|
w.wmut.Unlock()
|
|
if !found {
|
|
fi, errstat := os.Lstat(path)
|
|
if errstat != nil {
|
|
return errstat
|
|
}
|
|
|
|
// don't watch socket
|
|
if fi.Mode()&os.ModeSocket == os.ModeSocket {
|
|
return nil
|
|
}
|
|
|
|
// Follow Symlinks
|
|
// Unfortunately, Linux can add bogus symlinks to watch list without
|
|
// issue, and Windows can't do symlinks period (AFAIK). To maintain
|
|
// consistency, we will act like everything is fine. There will simply
|
|
// be no file events for broken symlinks.
|
|
// Hence the returns of nil on errors.
|
|
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
path, err := filepath.EvalSymlinks(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
fi, errstat = os.Lstat(path)
|
|
if errstat != nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
fd, errno := syscall.Open(path, open_FLAGS, 0700)
|
|
if fd == -1 {
|
|
return errno
|
|
}
|
|
watchfd = fd
|
|
|
|
w.wmut.Lock()
|
|
w.watches[path] = watchfd
|
|
w.wmut.Unlock()
|
|
|
|
w.pmut.Lock()
|
|
w.paths[watchfd] = path
|
|
w.finfo[watchfd] = fi
|
|
w.pmut.Unlock()
|
|
}
|
|
// Watch the directory if it has not been watched before.
|
|
w.pmut.Lock()
|
|
w.enmut.Lock()
|
|
if w.finfo[watchfd].IsDir() &&
|
|
(flags&sys_NOTE_WRITE) == sys_NOTE_WRITE &&
|
|
(!found || (w.enFlags[path]&sys_NOTE_WRITE) != sys_NOTE_WRITE) {
|
|
watchDir = true
|
|
}
|
|
w.enmut.Unlock()
|
|
w.pmut.Unlock()
|
|
|
|
w.enmut.Lock()
|
|
w.enFlags[path] = flags
|
|
w.enmut.Unlock()
|
|
|
|
var kbuf [1]syscall.Kevent_t
|
|
watchEntry := &kbuf[0]
|
|
watchEntry.Fflags = flags
|
|
syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_ADD|syscall.EV_CLEAR)
|
|
entryFlags := watchEntry.Flags
|
|
success, errno := syscall.Kevent(w.kq, kbuf[:], nil, nil)
|
|
if success == -1 {
|
|
return errno
|
|
} else if (entryFlags & syscall.EV_ERROR) == syscall.EV_ERROR {
|
|
return errors.New("kevent add error")
|
|
}
|
|
|
|
if watchDir {
|
|
errdir := w.watchDirectoryFiles(path)
|
|
if errdir != nil {
|
|
return errdir
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Watch adds path to the watched file set, watching all events.
|
|
func (w *Watcher) watch(path string) error {
|
|
w.ewmut.Lock()
|
|
w.externalWatches[path] = true
|
|
w.ewmut.Unlock()
|
|
return w.addWatch(path, sys_NOTE_ALLEVENTS)
|
|
}
|
|
|
|
// RemoveWatch removes path from the watched file set.
|
|
func (w *Watcher) removeWatch(path string) error {
|
|
w.wmut.Lock()
|
|
watchfd, ok := w.watches[path]
|
|
w.wmut.Unlock()
|
|
if !ok {
|
|
return errors.New(fmt.Sprintf("can't remove non-existent kevent watch for: %s", path))
|
|
}
|
|
var kbuf [1]syscall.Kevent_t
|
|
watchEntry := &kbuf[0]
|
|
syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_DELETE)
|
|
entryFlags := watchEntry.Flags
|
|
success, errno := syscall.Kevent(w.kq, kbuf[:], nil, nil)
|
|
if success == -1 {
|
|
return os.NewSyscallError("kevent_rm_watch", errno)
|
|
} else if (entryFlags & syscall.EV_ERROR) == syscall.EV_ERROR {
|
|
return errors.New("kevent rm error")
|
|
}
|
|
syscall.Close(watchfd)
|
|
w.wmut.Lock()
|
|
delete(w.watches, path)
|
|
w.wmut.Unlock()
|
|
w.enmut.Lock()
|
|
delete(w.enFlags, path)
|
|
w.enmut.Unlock()
|
|
w.pmut.Lock()
|
|
delete(w.paths, watchfd)
|
|
fInfo := w.finfo[watchfd]
|
|
delete(w.finfo, watchfd)
|
|
w.pmut.Unlock()
|
|
|
|
// Find all watched paths that are in this directory that are not external.
|
|
if fInfo.IsDir() {
|
|
var pathsToRemove []string
|
|
w.pmut.Lock()
|
|
for _, wpath := range w.paths {
|
|
wdir, _ := filepath.Split(wpath)
|
|
if filepath.Clean(wdir) == filepath.Clean(path) {
|
|
w.ewmut.Lock()
|
|
if !w.externalWatches[wpath] {
|
|
pathsToRemove = append(pathsToRemove, wpath)
|
|
}
|
|
w.ewmut.Unlock()
|
|
}
|
|
}
|
|
w.pmut.Unlock()
|
|
for _, p := range pathsToRemove {
|
|
// Since these are internal, not much sense in propagating error
|
|
// to the user, as that will just confuse them with an error about
|
|
// a path they did not explicitly watch themselves.
|
|
w.removeWatch(p)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// readEvents reads from the kqueue file descriptor, converts the
|
|
// received events into Event objects and sends them via the Event channel
|
|
func (w *Watcher) readEvents() {
|
|
var (
|
|
eventbuf [10]syscall.Kevent_t // Event buffer
|
|
events []syscall.Kevent_t // Received events
|
|
twait *syscall.Timespec // Time to block waiting for events
|
|
n int // Number of events returned from kevent
|
|
errno error // Syscall errno
|
|
)
|
|
events = eventbuf[0:0]
|
|
twait = new(syscall.Timespec)
|
|
*twait = syscall.NsecToTimespec(keventWaitTime)
|
|
|
|
for {
|
|
// See if there is a message on the "done" channel
|
|
var done bool
|
|
select {
|
|
case done = <-w.done:
|
|
default:
|
|
}
|
|
|
|
// If "done" message is received
|
|
if done {
|
|
errno := syscall.Close(w.kq)
|
|
if errno != nil {
|
|
w.Error <- os.NewSyscallError("close", errno)
|
|
}
|
|
close(w.internalEvent)
|
|
close(w.Error)
|
|
return
|
|
}
|
|
|
|
// Get new events
|
|
if len(events) == 0 {
|
|
n, errno = syscall.Kevent(w.kq, nil, eventbuf[:], twait)
|
|
|
|
// EINTR is okay, basically the syscall was interrupted before
|
|
// timeout expired.
|
|
if errno != nil && errno != syscall.EINTR {
|
|
w.Error <- os.NewSyscallError("kevent", errno)
|
|
continue
|
|
}
|
|
|
|
// Received some events
|
|
if n > 0 {
|
|
events = eventbuf[0:n]
|
|
}
|
|
}
|
|
|
|
// Flush the events we received to the events channel
|
|
for len(events) > 0 {
|
|
fileEvent := new(FileEvent)
|
|
watchEvent := &events[0]
|
|
fileEvent.mask = uint32(watchEvent.Fflags)
|
|
w.pmut.Lock()
|
|
fileEvent.Name = w.paths[int(watchEvent.Ident)]
|
|
fileInfo := w.finfo[int(watchEvent.Ident)]
|
|
w.pmut.Unlock()
|
|
if fileInfo != nil && fileInfo.IsDir() && !fileEvent.IsDelete() {
|
|
// Double check to make sure the directory exist. This can happen when
|
|
// we do a rm -fr on a recursively watched folders and we receive a
|
|
// modification event first but the folder has been deleted and later
|
|
// receive the delete event
|
|
if _, err := os.Lstat(fileEvent.Name); os.IsNotExist(err) {
|
|
// mark is as delete event
|
|
fileEvent.mask |= sys_NOTE_DELETE
|
|
}
|
|
}
|
|
|
|
if fileInfo != nil && fileInfo.IsDir() && fileEvent.IsModify() && !fileEvent.IsDelete() {
|
|
w.sendDirectoryChangeEvents(fileEvent.Name)
|
|
} else {
|
|
// Send the event on the events channel
|
|
w.internalEvent <- fileEvent
|
|
}
|
|
|
|
// Move to next event
|
|
events = events[1:]
|
|
|
|
if fileEvent.IsRename() {
|
|
w.removeWatch(fileEvent.Name)
|
|
w.femut.Lock()
|
|
delete(w.fileExists, fileEvent.Name)
|
|
w.femut.Unlock()
|
|
}
|
|
if fileEvent.IsDelete() {
|
|
w.removeWatch(fileEvent.Name)
|
|
w.femut.Lock()
|
|
delete(w.fileExists, fileEvent.Name)
|
|
w.femut.Unlock()
|
|
|
|
// Look for a file that may have overwritten this
|
|
// (ie mv f1 f2 will delete f2 then create f2)
|
|
fileDir, _ := filepath.Split(fileEvent.Name)
|
|
fileDir = filepath.Clean(fileDir)
|
|
w.wmut.Lock()
|
|
_, found := w.watches[fileDir]
|
|
w.wmut.Unlock()
|
|
if found {
|
|
// make sure the directory exist before we watch for changes. When we
|
|
// do a recursive watch and perform rm -fr, the parent directory might
|
|
// have gone missing, ignore the missing directory and let the
|
|
// upcoming delete event remove the watch form the parent folder
|
|
if _, err := os.Lstat(fileDir); !os.IsNotExist(err) {
|
|
w.sendDirectoryChangeEvents(fileDir)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *Watcher) watchDirectoryFiles(dirPath string) error {
|
|
// Get all files
|
|
files, err := ioutil.ReadDir(dirPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Search for new files
|
|
for _, fileInfo := range files {
|
|
filePath := filepath.Join(dirPath, fileInfo.Name())
|
|
|
|
// Inherit fsnFlags from parent directory
|
|
w.fsnmut.Lock()
|
|
if flags, found := w.fsnFlags[dirPath]; found {
|
|
w.fsnFlags[filePath] = flags
|
|
} else {
|
|
w.fsnFlags[filePath] = FSN_ALL
|
|
}
|
|
w.fsnmut.Unlock()
|
|
|
|
if fileInfo.IsDir() == false {
|
|
// Watch file to mimic linux fsnotify
|
|
e := w.addWatch(filePath, sys_NOTE_ALLEVENTS)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
} else {
|
|
// If the user is currently watching directory
|
|
// we want to preserve the flags used
|
|
w.enmut.Lock()
|
|
currFlags, found := w.enFlags[filePath]
|
|
w.enmut.Unlock()
|
|
var newFlags uint32 = sys_NOTE_DELETE
|
|
if found {
|
|
newFlags |= currFlags
|
|
}
|
|
|
|
// Linux gives deletes if not explicitly watching
|
|
e := w.addWatch(filePath, newFlags)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
}
|
|
w.femut.Lock()
|
|
w.fileExists[filePath] = true
|
|
w.femut.Unlock()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// sendDirectoryEvents searches the directory for newly created files
|
|
// and sends them over the event channel. This functionality is to have
|
|
// the BSD version of fsnotify match linux fsnotify which provides a
|
|
// create event for files created in a watched directory.
|
|
func (w *Watcher) sendDirectoryChangeEvents(dirPath string) {
|
|
// Get all files
|
|
files, err := ioutil.ReadDir(dirPath)
|
|
if err != nil {
|
|
w.Error <- err
|
|
}
|
|
|
|
// Search for new files
|
|
for _, fileInfo := range files {
|
|
filePath := filepath.Join(dirPath, fileInfo.Name())
|
|
w.femut.Lock()
|
|
_, doesExist := w.fileExists[filePath]
|
|
w.femut.Unlock()
|
|
if !doesExist {
|
|
// Inherit fsnFlags from parent directory
|
|
w.fsnmut.Lock()
|
|
if flags, found := w.fsnFlags[dirPath]; found {
|
|
w.fsnFlags[filePath] = flags
|
|
} else {
|
|
w.fsnFlags[filePath] = FSN_ALL
|
|
}
|
|
w.fsnmut.Unlock()
|
|
|
|
// Send create event
|
|
fileEvent := new(FileEvent)
|
|
fileEvent.Name = filePath
|
|
fileEvent.create = true
|
|
w.internalEvent <- fileEvent
|
|
}
|
|
w.femut.Lock()
|
|
w.fileExists[filePath] = true
|
|
w.femut.Unlock()
|
|
}
|
|
w.watchDirectoryFiles(dirPath)
|
|
}
|