Update fsnotify to 1.2.0

Signed-off-by: Andrew "Tianon" Page <admwiggin@gmail.com>
This commit is contained in:
Tianon Gravi 2015-05-02 23:28:15 -06:00
parent 0ad6f90127
commit e1ccfabdc5
16 changed files with 1206 additions and 345 deletions

View File

@ -55,7 +55,7 @@ clone git github.com/docker/libtrust 230dfd18c232
clone git github.com/Sirupsen/logrus v0.7.2 clone git github.com/Sirupsen/logrus v0.7.2
clone git github.com/go-fsnotify/fsnotify v1.0.4 clone git github.com/go-fsnotify/fsnotify v1.2.0
clone git github.com/go-check/check 64131543e7896d5bcc6bd5a76287eb75ea96c673 clone git github.com/go-check/check 64131543e7896d5bcc6bd5a76287eb75ea96c673

View File

@ -1,10 +1,12 @@
sudo: false
language: go language: go
go: go:
- 1.2 - 1.4.1
- tip
before_script:
- FIXED=$(go fmt ./... | wc -l); if [ $FIXED -gt 0 ]; then echo "gofmt - $FIXED file(s) not formatted correctly, please run gofmt to fix this." && exit 1; fi
# not yet https://github.com/travis-ci/travis-ci/issues/2318
os: os:
- linux - linux
- osx - osx

View File

@ -18,8 +18,10 @@ Francisco Souza <f@souza.cc>
Hari haran <hariharan.uno@gmail.com> Hari haran <hariharan.uno@gmail.com>
John C Barstow John C Barstow
Kelvin Fo <vmirage@gmail.com> Kelvin Fo <vmirage@gmail.com>
Matt Layher <mdlayher@gmail.com>
Nathan Youngman <git@nathany.com> Nathan Youngman <git@nathany.com>
Paul Hammond <paul@paulhammond.org> Paul Hammond <paul@paulhammond.org>
Pieter Droogendijk <pieter@binky.org.uk>
Pursuit92 <JoshChase@techpursuit.net> Pursuit92 <JoshChase@techpursuit.net>
Rob Figueiredo <robfig@gmail.com> Rob Figueiredo <robfig@gmail.com>
Soge Zhang <zhssoge@gmail.com> Soge Zhang <zhssoge@gmail.com>

View File

@ -1,5 +1,27 @@
# Changelog # Changelog
## v1.2.0 / 2015-02-08
* inotify: use epoll to wake up readEvents [#66](https://github.com/go-fsnotify/fsnotify/pull/66) (thanks @PieterD)
* inotify: closing watcher should now always shut down goroutine [#63](https://github.com/go-fsnotify/fsnotify/pull/63) (thanks @PieterD)
* kqueue: close kqueue after removing watches, fixes [#59](https://github.com/go-fsnotify/fsnotify/issues/59)
## v1.1.1 / 2015-02-05
* inotify: Retry read on EINTR [#61](https://github.com/go-fsnotify/fsnotify/issues/61) (thanks @PieterD)
## v1.1.0 / 2014-12-12
* kqueue: rework internals [#43](https://github.com/go-fsnotify/fsnotify/pull/43)
* add low-level functions
* only need to store flags on directories
* less mutexes [#13](https://github.com/go-fsnotify/fsnotify/issues/13)
* done can be an unbuffered channel
* remove calls to os.NewSyscallError
* More efficient string concatenation for Event.String() [#52](https://github.com/go-fsnotify/fsnotify/pull/52) (thanks @mdlayher)
* kqueue: fix regression in rework causing subdirectories to be watched [#48](https://github.com/go-fsnotify/fsnotify/issues/48)
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/go-fsnotify/fsnotify/issues/51)
## v1.0.4 / 2014-09-07 ## v1.0.4 / 2014-09-07
* kqueue: add dragonfly to the build tags. * kqueue: add dragonfly to the build tags.
@ -69,6 +91,10 @@
* no tests for the current implementation * no tests for the current implementation
* not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195) * not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195)
## v0.9.3 / 2014-12-31
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/go-fsnotify/fsnotify/issues/51)
## v0.9.2 / 2014-08-17 ## v0.9.2 / 2014-08-17
* [Backport] Fix missing create events on OS X. [#14](https://github.com/go-fsnotify/fsnotify/issues/14) (thanks @zhsso) * [Backport] Fix missing create events on OS X. [#14](https://github.com/go-fsnotify/fsnotify/issues/14) (thanks @zhsso)

View File

@ -1,21 +1,34 @@
# Contributing # Contributing
* Send questions to [golang-dev@googlegroups.com](mailto:golang-dev@googlegroups.com). ## Issues
### Issues
* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/go-fsnotify/fsnotify/issues). * Request features and report bugs using the [GitHub Issue Tracker](https://github.com/go-fsnotify/fsnotify/issues).
* Please indicate the platform you are running on. * Please indicate the platform you are using fsnotify on.
* A code example to reproduce the problem is appreciated.
### Pull Requests ## Pull Requests
A future version of Go will have [fsnotify in the standard library](https://code.google.com/p/go/issues/detail?id=4068), therefore fsnotify carries the same [LICENSE](https://github.com/go-fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so we need you to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual). ### Contributor License Agreement
fsnotify is derived from code in the [golang.org/x/exp](https://godoc.org/golang.org/x/exp) package and it may be included [in the standard library](https://github.com/go-fsnotify/fsnotify/issues/1) in the future. Therefore fsnotify carries the same [LICENSE](https://github.com/go-fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so you need to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual).
Please indicate that you have signed the CLA in your pull request. Please indicate that you have signed the CLA in your pull request.
To hack on fsnotify: ### How fsnotify is Developed
1. Install as usual (`go get -u github.com/go-fsnotify/fsnotify`) * Development is done on feature branches.
* Tests are run on BSD, Linux, OS X and Windows.
* Pull requests are reviewed and [applied to master][am] using [hub][].
* Maintainers may modify or squash commits rather than asking contributors to.
* To issue a new release, the maintainers will:
* Update the CHANGELOG
* Tag a version, which will become available through gopkg.in.
### How to Fork
For smooth sailing, always use the original import path. Installing with `go get` makes this easy.
1. Install from GitHub (`go get -u github.com/go-fsnotify/fsnotify`)
2. Create your feature branch (`git checkout -b my-new-feature`) 2. Create your feature branch (`git checkout -b my-new-feature`)
3. Ensure everything works and the tests pass (see below) 3. Ensure everything works and the tests pass (see below)
4. Commit your changes (`git commit -am 'Add some feature'`) 4. Commit your changes (`git commit -am 'Add some feature'`)
@ -27,15 +40,7 @@ Contribute upstream:
3. Push to the branch (`git push fork my-new-feature`) 3. Push to the branch (`git push fork my-new-feature`)
4. Create a new Pull Request on GitHub 4. Create a new Pull Request on GitHub
If other team members need your patch before I merge it: This workflow is [thoroughly explained by Katrina Owen](https://blog.splice.com/contributing-open-source-git-repositories-go/).
1. Install as usual (`go get -u github.com/go-fsnotify/fsnotify`)
2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`)
3. Pull your revisions (`git fetch fork; git checkout -b my-new-feature fork/my-new-feature`)
Notice: For smooth sailing, always use the original import path. Installing with `go get` makes this easy.
Note: The maintainers will update the CHANGELOG on your behalf. Please don't modify it in your pull request.
### Testing ### Testing
@ -43,7 +48,7 @@ fsnotify uses build tags to compile different code on Linux, BSD, OS X, and Wind
Before doing a pull request, please do your best to test your changes on multiple platforms, and list which platforms you were able/unable to test on. Before doing a pull request, please do your best to test your changes on multiple platforms, and list which platforms you were able/unable to test on.
To make cross-platform testing easier, I've created a Vagrantfile for Linux and BSD. To aid in cross-platform testing there is a Vagrantfile for Linux and BSD.
* Install [Vagrant](http://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) * Install [Vagrant](http://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/)
* Setup [Vagrant Gopher](https://github.com/nathany/vagrant-gopher) in your `src` folder. * Setup [Vagrant Gopher](https://github.com/nathany/vagrant-gopher) in your `src` folder.
@ -51,6 +56,22 @@ To make cross-platform testing easier, I've created a Vagrantfile for Linux and
* Once setup, you can run the test suite on a given OS with a single command `vagrant ssh linux -c 'cd go-fsnotify/fsnotify; go test'`. * Once setup, you can run the test suite on a given OS with a single command `vagrant ssh linux -c 'cd go-fsnotify/fsnotify; go test'`.
* When you're done, you will want to halt or destroy the Vagrant boxes. * When you're done, you will want to halt or destroy the Vagrant boxes.
Notice: fsnotify file system events don't work on shared folders. The tests get around this limitation by using a tmp directory, but it is something to be aware of. Notice: fsnotify file system events won't trigger in shared folders. The tests get around this limitation by using the /tmp directory.
Right now I don't have an equivalent solution for Windows and OS X, but there are Windows VMs [freely available from Microsoft](http://www.modern.ie/en-us/virtualization-tools#downloads). Right now there is no equivalent solution for Windows and OS X, but there are Windows VMs [freely available from Microsoft](http://www.modern.ie/en-us/virtualization-tools#downloads).
### Maintainers
Help maintaining fsnotify is welcome. To be a maintainer:
* Submit a pull request and sign the CLA as above.
* You must be able to run the test suite on Mac, Windows, Linux and BSD.
To keep master clean, the fsnotify project uses the "apply mail" workflow outlined in Nathaniel Talbott's post ["Merge pull request" Considered Harmful][am]. This requires installing [hub][].
All code changes should be internal pull requests.
Releases are tagged using [Semantic Versioning](http://semver.org/).
[hub]: https://github.com/github/hub
[am]: http://blog.spreedly.com/2014/06/24/merge-pull-request-considered-harmful/#.VGa5yZPF_Zs

View File

@ -2,18 +2,20 @@
[![Coverage](http://gocover.io/_badge/github.com/go-fsnotify/fsnotify)](http://gocover.io/github.com/go-fsnotify/fsnotify) [![GoDoc](https://godoc.org/gopkg.in/fsnotify.v1?status.svg)](https://godoc.org/gopkg.in/fsnotify.v1) [![Coverage](http://gocover.io/_badge/github.com/go-fsnotify/fsnotify)](http://gocover.io/github.com/go-fsnotify/fsnotify) [![GoDoc](https://godoc.org/gopkg.in/fsnotify.v1?status.svg)](https://godoc.org/gopkg.in/fsnotify.v1)
Go 1.3+ required.
Cross platform: Windows, Linux, BSD and OS X. Cross platform: Windows, Linux, BSD and OS X.
|Adapter |OS |Status | |Adapter |OS |Status |
|----------|----------|----------| |----------|----------|----------|
|inotify |Linux, Android\*|Supported| |inotify |Linux, Android\*|Supported [![Build Status](https://travis-ci.org/go-fsnotify/fsnotify.svg?branch=master)](https://travis-ci.org/go-fsnotify/fsnotify)|
|kqueue |BSD, OS X, iOS\*|Supported| |kqueue |BSD, OS X, iOS\*|Supported [![Circle CI](https://circleci.com/gh/go-fsnotify/fsnotify.svg?style=svg)](https://circleci.com/gh/go-fsnotify/fsnotify)|
|ReadDirectoryChangesW|Windows|Supported| |ReadDirectoryChangesW|Windows|Supported [![Build status](https://ci.appveyor.com/api/projects/status/ivwjubaih4r0udeh/branch/master?svg=true)](https://ci.appveyor.com/project/NathanYoungman/fsnotify/branch/master)|
|FSEvents |OS X |[Planned](https://github.com/go-fsnotify/fsnotify/issues/11)| |FSEvents |OS X |[Planned](https://github.com/go-fsnotify/fsnotify/issues/11)|
|FEN |Solaris 11 |[Planned](https://github.com/go-fsnotify/fsnotify/issues/12)| |FEN |Solaris 11 |[Planned](https://github.com/go-fsnotify/fsnotify/issues/12)|
|fanotify |Linux 2.6.37+ | | |fanotify |Linux 2.6.37+ | |
|USN Journals |Windows |[Maybe](https://github.com/go-fsnotify/fsnotify/issues/53)|
|Polling |*All* |[Maybe](https://github.com/go-fsnotify/fsnotify/issues/9)| |Polling |*All* |[Maybe](https://github.com/go-fsnotify/fsnotify/issues/9)|
| |Plan 9 | |
\* Android and iOS are untested. \* Android and iOS are untested.
@ -23,31 +25,35 @@ Please see [the documentation](https://godoc.org/gopkg.in/fsnotify.v1) for usage
Two major versions of fsnotify exist. Two major versions of fsnotify exist.
**[fsnotify.v1](https://gopkg.in/fsnotify.v1)** provides [a new API](https://godoc.org/gopkg.in/fsnotify.v1) based on [this design document](http://goo.gl/MrYxyA). You can import v1 with:
```go
import "gopkg.in/fsnotify.v1"
```
\* Refer to the package as fsnotify (without the .v1 suffix).
**[fsnotify.v0](https://gopkg.in/fsnotify.v0)** is API-compatible with [howeyc/fsnotify](https://godoc.org/github.com/howeyc/fsnotify). Bugfixes *may* be backported, but I recommend upgrading to v1. **[fsnotify.v0](https://gopkg.in/fsnotify.v0)** is API-compatible with [howeyc/fsnotify](https://godoc.org/github.com/howeyc/fsnotify). Bugfixes *may* be backported, but I recommend upgrading to v1.
```go ```go
import "gopkg.in/fsnotify.v0" import "gopkg.in/fsnotify.v0"
``` ```
\* Refer to the package as fsnotify (without the .v0 suffix).
**[fsnotify.v1](https://gopkg.in/fsnotify.v1)** provides [a new API](https://godoc.org/gopkg.in/fsnotify.v1) based on [this design document](http://goo.gl/MrYxyA). You can import v1 with:
```go
import "gopkg.in/fsnotify.v1"
```
Further API changes are [planned](https://github.com/go-fsnotify/fsnotify/milestones), but a new major revision will be tagged, so you can depend on the v1 API. Further API changes are [planned](https://github.com/go-fsnotify/fsnotify/milestones), but a new major revision will be tagged, so you can depend on the v1 API.
**Master** may have unreleased changes. Use it to test the very latest code or when [contributing][], but don't expect it to remain API-compatible:
```go
import "github.com/go-fsnotify/fsnotify"
```
## Contributing ## Contributing
* Send questions to [golang-dev@googlegroups.com](mailto:golang-dev@googlegroups.com). Please refer to [CONTRIBUTING][] before opening an issue or pull request.
* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/go-fsnotify/fsnotify/issues).
A future version of Go will have [fsnotify in the standard library](https://code.google.com/p/go/issues/detail?id=4068), therefore fsnotify carries the same [LICENSE](https://github.com/go-fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so we need you to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual).
Please read [CONTRIBUTING](https://github.com/go-fsnotify/fsnotify/blob/master/CONTRIBUTING.md) before opening a pull request.
## Example ## Example
See [example_test.go](https://github.com/go-fsnotify/fsnotify/blob/master/example_test.go). See [example_test.go](https://github.com/go-fsnotify/fsnotify/blob/master/example_test.go).
[contributing]: https://github.com/go-fsnotify/fsnotify/blob/master/CONTRIBUTING.md

View File

@ -0,0 +1,26 @@
## OS X build (CircleCI iOS beta)
# Pretend like it's an Xcode project, at least to get it running.
machine:
environment:
XCODE_WORKSPACE: NotUsed.xcworkspace
XCODE_SCHEME: NotUsed
# This is where the go project is actually checked out to:
CIRCLE_BUILD_DIR: $HOME/.go_project/src/github.com/go-fsnotify/fsnotify
dependencies:
pre:
- brew upgrade go
test:
override:
- go test ./...
# Idealized future config, eventually with cross-platform build matrix :-)
# machine:
# go:
# version: 1.4
# os:
# - osx
# - linux

View File

@ -9,7 +9,7 @@ package fsnotify_test
import ( import (
"log" "log"
"gopkg.in/fsnotify.v1" "github.com/go-fsnotify/fsnotify"
) )
func ExampleNewWatcher() { func ExampleNewWatcher() {

View File

@ -7,7 +7,10 @@
// Package fsnotify provides a platform-independent interface for file system notifications. // Package fsnotify provides a platform-independent interface for file system notifications.
package fsnotify package fsnotify
import "fmt" import (
"bytes"
"fmt"
)
// Event represents a single file system notification. // Event represents a single file system notification.
type Event struct { type Event struct {
@ -30,27 +33,30 @@ const (
// String returns a string representation of the event in the form // String returns a string representation of the event in the form
// "file: REMOVE|WRITE|..." // "file: REMOVE|WRITE|..."
func (e Event) String() string { func (e Event) String() string {
events := "" // Use a buffer for efficient string concatenation
var buffer bytes.Buffer
if e.Op&Create == Create { if e.Op&Create == Create {
events += "|CREATE" buffer.WriteString("|CREATE")
} }
if e.Op&Remove == Remove { if e.Op&Remove == Remove {
events += "|REMOVE" buffer.WriteString("|REMOVE")
} }
if e.Op&Write == Write { if e.Op&Write == Write {
events += "|WRITE" buffer.WriteString("|WRITE")
} }
if e.Op&Rename == Rename { if e.Op&Rename == Rename {
events += "|RENAME" buffer.WriteString("|RENAME")
} }
if e.Op&Chmod == Chmod { if e.Op&Chmod == Chmod {
events += "|CHMOD" buffer.WriteString("|CHMOD")
} }
if len(events) > 0 { // If buffer remains empty, return no event names
events = events[1:] if buffer.Len() == 0 {
return fmt.Sprintf("%q: ", e.Name)
} }
return fmt.Sprintf("%q: %s", e.Name, events) // Return a list of event names, with leading pipe character stripped
return fmt.Sprintf("%q: %s", e.Name, buffer.String()[1:])
} }

View File

@ -9,6 +9,7 @@ package fsnotify
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -21,47 +22,66 @@ import (
type Watcher struct { type Watcher struct {
Events chan Event Events chan Event
Errors chan error Errors chan error
mu sync.Mutex // Map access mu sync.Mutex // Map access
fd int // File descriptor (as returned by the inotify_init() syscall) fd int
poller *fdPoller
watches map[string]*watch // Map of inotify watches (key: path) watches map[string]*watch // Map of inotify watches (key: path)
paths map[int]string // Map of watched paths (key: watch descriptor) paths map[int]string // Map of watched paths (key: watch descriptor)
done chan bool // Channel for sending a "quit message" to the reader goroutine done chan struct{} // Channel for sending a "quit message" to the reader goroutine
isClosed bool // Set to true when Close() is first called doneResp chan struct{} // Channel to respond to Close
} }
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. // NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
func NewWatcher() (*Watcher, error) { func NewWatcher() (*Watcher, error) {
// Create inotify fd
fd, errno := syscall.InotifyInit() fd, errno := syscall.InotifyInit()
if fd == -1 { if fd == -1 {
return nil, os.NewSyscallError("inotify_init", errno) return nil, errno
}
// Create epoll
poller, err := newFdPoller(fd)
if err != nil {
syscall.Close(fd)
return nil, err
} }
w := &Watcher{ w := &Watcher{
fd: fd, fd: fd,
watches: make(map[string]*watch), poller: poller,
paths: make(map[int]string), watches: make(map[string]*watch),
Events: make(chan Event), paths: make(map[int]string),
Errors: make(chan error), Events: make(chan Event),
done: make(chan bool, 1), Errors: make(chan error),
done: make(chan struct{}),
doneResp: make(chan struct{}),
} }
go w.readEvents() go w.readEvents()
return w, nil return w, nil
} }
func (w *Watcher) isClosed() bool {
select {
case <-w.done:
return true
default:
return false
}
}
// Close removes all watches and closes the events channel. // Close removes all watches and closes the events channel.
func (w *Watcher) Close() error { func (w *Watcher) Close() error {
if w.isClosed { if w.isClosed() {
return nil return nil
} }
w.isClosed = true
// Remove all watches // Send 'close' signal to goroutine, and set the Watcher to closed.
for name := range w.watches { close(w.done)
w.Remove(name)
}
// Send "quit" message to the reader goroutine // Wake up goroutine
w.done <- true w.poller.wake()
// Wait for goroutine to close
<-w.doneResp
return nil return nil
} }
@ -69,7 +89,7 @@ func (w *Watcher) Close() error {
// Add starts watching the named file or directory (non-recursively). // Add starts watching the named file or directory (non-recursively).
func (w *Watcher) Add(name string) error { func (w *Watcher) Add(name string) error {
name = filepath.Clean(name) name = filepath.Clean(name)
if w.isClosed { if w.isClosed() {
return errors.New("inotify instance already closed") return errors.New("inotify instance already closed")
} }
@ -88,7 +108,7 @@ func (w *Watcher) Add(name string) error {
} }
wd, errno := syscall.InotifyAddWatch(w.fd, name, flags) wd, errno := syscall.InotifyAddWatch(w.fd, name, flags)
if wd == -1 { if wd == -1 {
return os.NewSyscallError("inotify_add_watch", errno) return errno
} }
w.mu.Lock() w.mu.Lock()
@ -99,20 +119,33 @@ func (w *Watcher) Add(name string) error {
return nil return nil
} }
// Remove stops watching the the named file or directory (non-recursively). // Remove stops watching the named file or directory (non-recursively).
func (w *Watcher) Remove(name string) error { func (w *Watcher) Remove(name string) error {
name = filepath.Clean(name) name = filepath.Clean(name)
// Fetch the watch.
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
watch, ok := w.watches[name] watch, ok := w.watches[name]
// Remove it from inotify.
if !ok { if !ok {
return fmt.Errorf("can't remove non-existent inotify watch for: %s", name) return fmt.Errorf("can't remove non-existent inotify watch for: %s", name)
} }
// inotify_rm_watch will return EINVAL if the file has been deleted;
// the inotify will already have been removed.
// That means we can safely delete it from our watches, whatever inotify_rm_watch does.
delete(w.watches, name)
success, errno := syscall.InotifyRmWatch(w.fd, watch.wd) success, errno := syscall.InotifyRmWatch(w.fd, watch.wd)
if success == -1 { if success == -1 {
return os.NewSyscallError("inotify_rm_watch", errno) // TODO: Perhaps it's not helpful to return an error here in every case.
// the only two possible errors are:
// EBADF, which happens when w.fd is not a valid file descriptor of any kind.
// EINVAL, which is when fd is not an inotify descriptor or wd is not a valid watch descriptor.
// Watch descriptors are invalidated when they are removed explicitly or implicitly;
// explicitly by inotify_rm_watch, implicitly when the file they are watching is deleted.
return errno
} }
delete(w.watches, name)
return nil return nil
} }
@ -128,35 +161,65 @@ func (w *Watcher) readEvents() {
buf [syscall.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events buf [syscall.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
n int // Number of bytes read with read() n int // Number of bytes read with read()
errno error // Syscall errno errno error // Syscall errno
ok bool // For poller.wait
) )
defer close(w.doneResp)
defer close(w.Errors)
defer close(w.Events)
defer syscall.Close(w.fd)
defer w.poller.close()
for { for {
// See if there is a message on the "done" channel // See if we have been closed.
select { if w.isClosed() {
case <-w.done:
syscall.Close(w.fd)
close(w.Events)
close(w.Errors)
return return
default: }
ok, errno = w.poller.wait()
if errno != nil {
select {
case w.Errors <- errno:
case <-w.done:
return
}
continue
}
if !ok {
continue
} }
n, errno = syscall.Read(w.fd, buf[:]) n, errno = syscall.Read(w.fd, buf[:])
// If a signal interrupted execution, see if we've been asked to close, and try again.
// http://man7.org/linux/man-pages/man7/signal.7.html :
// "Before Linux 3.8, reads from an inotify(7) file descriptor were not restartable"
if errno == syscall.EINTR {
continue
}
// If EOF is received // syscall.Read might have been woken up by Close. If so, we're done.
if n == 0 { if w.isClosed() {
syscall.Close(w.fd)
close(w.Events)
close(w.Errors)
return return
} }
if n < 0 {
w.Errors <- os.NewSyscallError("read", errno)
continue
}
if n < syscall.SizeofInotifyEvent { if n < syscall.SizeofInotifyEvent {
w.Errors <- errors.New("inotify: short read in readEvents()") var err error
if n == 0 {
// If EOF is received. This should really never happen.
err = io.EOF
} else if n < 0 {
// If an error occured while reading.
err = errno
} else {
// Read was too short.
err = errors.New("notify: short read in readEvents()")
}
select {
case w.Errors <- err:
case <-w.done:
return
}
continue continue
} }
@ -187,7 +250,11 @@ func (w *Watcher) readEvents() {
// Send the events that are not ignored on the events channel // Send the events that are not ignored on the events channel
if !event.ignoreLinux(mask) { if !event.ignoreLinux(mask) {
w.Events <- event select {
case w.Events <- event:
case <-w.done:
return
}
} }
// Move to the next event in the buffer // Move to the next event in the buffer

View File

@ -0,0 +1,186 @@
// Copyright 2015 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 linux
package fsnotify
import (
"errors"
"syscall"
)
type fdPoller struct {
fd int // File descriptor (as returned by the inotify_init() syscall)
epfd int // Epoll file descriptor
pipe [2]int // Pipe for waking up
}
func emptyPoller(fd int) *fdPoller {
poller := new(fdPoller)
poller.fd = fd
poller.epfd = -1
poller.pipe[0] = -1
poller.pipe[1] = -1
return poller
}
// Create a new inotify poller.
// This creates an inotify handler, and an epoll handler.
func newFdPoller(fd int) (*fdPoller, error) {
var errno error
poller := emptyPoller(fd)
defer func() {
if errno != nil {
poller.close()
}
}()
poller.fd = fd
// Create epoll fd
poller.epfd, errno = syscall.EpollCreate(1)
if poller.epfd == -1 {
return nil, errno
}
// Create pipe; pipe[0] is the read end, pipe[1] the write end.
errno = syscall.Pipe2(poller.pipe[:], syscall.O_NONBLOCK)
if errno != nil {
return nil, errno
}
// Register inotify fd with epoll
event := syscall.EpollEvent{
Fd: int32(poller.fd),
Events: syscall.EPOLLIN,
}
errno = syscall.EpollCtl(poller.epfd, syscall.EPOLL_CTL_ADD, poller.fd, &event)
if errno != nil {
return nil, errno
}
// Register pipe fd with epoll
event = syscall.EpollEvent{
Fd: int32(poller.pipe[0]),
Events: syscall.EPOLLIN,
}
errno = syscall.EpollCtl(poller.epfd, syscall.EPOLL_CTL_ADD, poller.pipe[0], &event)
if errno != nil {
return nil, errno
}
return poller, nil
}
// Wait using epoll.
// Returns true if something is ready to be read,
// false if there is not.
func (poller *fdPoller) wait() (bool, error) {
// 3 possible events per fd, and 2 fds, makes a maximum of 6 events.
// I don't know whether epoll_wait returns the number of events returned,
// or the total number of events ready.
// I decided to catch both by making the buffer one larger than the maximum.
events := make([]syscall.EpollEvent, 7)
for {
n, errno := syscall.EpollWait(poller.epfd, events, -1)
if n == -1 {
if errno == syscall.EINTR {
continue
}
return false, errno
}
if n == 0 {
// If there are no events, try again.
continue
}
if n > 6 {
// This should never happen. More events were returned than should be possible.
return false, errors.New("epoll_wait returned more events than I know what to do with")
}
ready := events[:n]
epollhup := false
epollerr := false
epollin := false
for _, event := range ready {
if event.Fd == int32(poller.fd) {
if event.Events&syscall.EPOLLHUP != 0 {
// This should not happen, but if it does, treat it as a wakeup.
epollhup = true
}
if event.Events&syscall.EPOLLERR != 0 {
// If an error is waiting on the file descriptor, we should pretend
// something is ready to read, and let syscall.Read pick up the error.
epollerr = true
}
if event.Events&syscall.EPOLLIN != 0 {
// There is data to read.
epollin = true
}
}
if event.Fd == int32(poller.pipe[0]) {
if event.Events&syscall.EPOLLHUP != 0 {
// Write pipe descriptor was closed, by us. This means we're closing down the
// watcher, and we should wake up.
}
if event.Events&syscall.EPOLLERR != 0 {
// If an error is waiting on the pipe file descriptor.
// This is an absolute mystery, and should never ever happen.
return false, errors.New("Error on the pipe descriptor.")
}
if event.Events&syscall.EPOLLIN != 0 {
// This is a regular wakeup, so we have to clear the buffer.
err := poller.clearWake()
if err != nil {
return false, err
}
}
}
}
if epollhup || epollerr || epollin {
return true, nil
}
return false, nil
}
}
// Close the write end of the poller.
func (poller *fdPoller) wake() error {
buf := make([]byte, 1)
n, errno := syscall.Write(poller.pipe[1], buf)
if n == -1 {
if errno == syscall.EAGAIN {
// Buffer is full, poller will wake.
return nil
}
return errno
}
return nil
}
func (poller *fdPoller) clearWake() error {
// You have to be woken up a LOT in order to get to 100!
buf := make([]byte, 100)
n, errno := syscall.Read(poller.pipe[0], buf)
if n == -1 {
if errno == syscall.EAGAIN {
// Buffer is empty, someone else cleared our wake.
return nil
}
return errno
}
return nil
}
// Close all poller file descriptors, but not the one passed to it.
func (poller *fdPoller) close() {
if poller.pipe[1] != -1 {
syscall.Close(poller.pipe[1])
}
if poller.pipe[0] != -1 {
syscall.Close(poller.pipe[0])
}
if poller.epfd != -1 {
syscall.Close(poller.epfd)
}
}

View File

@ -0,0 +1,228 @@
// Copyright 2015 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 linux
package fsnotify
import (
"syscall"
"testing"
"time"
)
type testFd [2]int
func makeTestFd(t *testing.T) testFd {
var tfd testFd
errno := syscall.Pipe(tfd[:])
if errno != nil {
t.Fatalf("Failed to create pipe: %v", errno)
}
return tfd
}
func (tfd testFd) fd() int {
return tfd[0]
}
func (tfd testFd) closeWrite(t *testing.T) {
errno := syscall.Close(tfd[1])
if errno != nil {
t.Fatalf("Failed to close write end of pipe: %v", errno)
}
}
func (tfd testFd) put(t *testing.T) {
buf := make([]byte, 10)
_, errno := syscall.Write(tfd[1], buf)
if errno != nil {
t.Fatalf("Failed to write to pipe: %v", errno)
}
}
func (tfd testFd) get(t *testing.T) {
buf := make([]byte, 10)
_, errno := syscall.Read(tfd[0], buf)
if errno != nil {
t.Fatalf("Failed to read from pipe: %v", errno)
}
}
func (tfd testFd) close() {
syscall.Close(tfd[1])
syscall.Close(tfd[0])
}
func makePoller(t *testing.T) (testFd, *fdPoller) {
tfd := makeTestFd(t)
poller, err := newFdPoller(tfd.fd())
if err != nil {
t.Fatalf("Failed to create poller: %v", err)
}
return tfd, poller
}
func TestPollerWithBadFd(t *testing.T) {
_, err := newFdPoller(-1)
if err != syscall.EBADF {
t.Fatalf("Expected EBADF, got: %v", err)
}
}
func TestPollerWithData(t *testing.T) {
tfd, poller := makePoller(t)
defer tfd.close()
defer poller.close()
tfd.put(t)
ok, err := poller.wait()
if err != nil {
t.Fatalf("poller failed: %v", err)
}
if !ok {
t.Fatalf("expected poller to return true")
}
tfd.get(t)
}
func TestPollerWithWakeup(t *testing.T) {
tfd, poller := makePoller(t)
defer tfd.close()
defer poller.close()
err := poller.wake()
if err != nil {
t.Fatalf("wake failed: %v", err)
}
ok, err := poller.wait()
if err != nil {
t.Fatalf("poller failed: %v", err)
}
if ok {
t.Fatalf("expected poller to return false")
}
}
func TestPollerWithClose(t *testing.T) {
tfd, poller := makePoller(t)
defer tfd.close()
defer poller.close()
tfd.closeWrite(t)
ok, err := poller.wait()
if err != nil {
t.Fatalf("poller failed: %v", err)
}
if !ok {
t.Fatalf("expected poller to return true")
}
}
func TestPollerWithWakeupAndData(t *testing.T) {
tfd, poller := makePoller(t)
defer tfd.close()
defer poller.close()
tfd.put(t)
err := poller.wake()
if err != nil {
t.Fatalf("wake failed: %v", err)
}
// both data and wakeup
ok, err := poller.wait()
if err != nil {
t.Fatalf("poller failed: %v", err)
}
if !ok {
t.Fatalf("expected poller to return true")
}
// data is still in the buffer, wakeup is cleared
ok, err = poller.wait()
if err != nil {
t.Fatalf("poller failed: %v", err)
}
if !ok {
t.Fatalf("expected poller to return true")
}
tfd.get(t)
// data is gone, only wakeup now
err = poller.wake()
if err != nil {
t.Fatalf("wake failed: %v", err)
}
ok, err = poller.wait()
if err != nil {
t.Fatalf("poller failed: %v", err)
}
if ok {
t.Fatalf("expected poller to return false")
}
}
func TestPollerConcurrent(t *testing.T) {
tfd, poller := makePoller(t)
defer tfd.close()
defer poller.close()
oks := make(chan bool)
live := make(chan bool)
defer close(live)
go func() {
defer close(oks)
for {
ok, err := poller.wait()
if err != nil {
t.Fatalf("poller failed: %v", err)
}
oks <- ok
if !<-live {
return
}
}
}()
// Try a write
select {
case <-time.After(50 * time.Millisecond):
case <-oks:
t.Fatalf("poller did not wait")
}
tfd.put(t)
if !<-oks {
t.Fatalf("expected true")
}
tfd.get(t)
live <- true
// Try a wakeup
select {
case <-time.After(50 * time.Millisecond):
case <-oks:
t.Fatalf("poller did not wait")
}
err := poller.wake()
if err != nil {
t.Fatalf("wake failed: %v", err)
}
if <-oks {
t.Fatalf("expected false")
}
live <- true
// Try a close
select {
case <-time.After(50 * time.Millisecond):
case <-oks:
t.Fatalf("poller did not wait")
}
tfd.closeWrite(t)
if !<-oks {
t.Fatalf("expected true")
}
tfd.get(t)
}

View File

@ -0,0 +1,292 @@
// Copyright 2015 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 linux
package fsnotify
import (
"os"
"path/filepath"
"syscall"
"testing"
"time"
)
func TestInotifyCloseRightAway(t *testing.T) {
w, err := NewWatcher()
if err != nil {
t.Fatalf("Failed to create watcher")
}
// Close immediately; it won't even reach the first syscall.Read.
w.Close()
// Wait for the close to complete.
<-time.After(50 * time.Millisecond)
isWatcherReallyClosed(t, w)
}
func TestInotifyCloseSlightlyLater(t *testing.T) {
w, err := NewWatcher()
if err != nil {
t.Fatalf("Failed to create watcher")
}
// Wait until readEvents has reached syscall.Read, and Close.
<-time.After(50 * time.Millisecond)
w.Close()
// Wait for the close to complete.
<-time.After(50 * time.Millisecond)
isWatcherReallyClosed(t, w)
}
func TestInotifyCloseSlightlyLaterWithWatch(t *testing.T) {
testDir := tempMkdir(t)
defer os.RemoveAll(testDir)
w, err := NewWatcher()
if err != nil {
t.Fatalf("Failed to create watcher")
}
w.Add(testDir)
// Wait until readEvents has reached syscall.Read, and Close.
<-time.After(50 * time.Millisecond)
w.Close()
// Wait for the close to complete.
<-time.After(50 * time.Millisecond)
isWatcherReallyClosed(t, w)
}
func TestInotifyCloseAfterRead(t *testing.T) {
testDir := tempMkdir(t)
defer os.RemoveAll(testDir)
w, err := NewWatcher()
if err != nil {
t.Fatalf("Failed to create watcher")
}
err = w.Add(testDir)
if err != nil {
t.Fatalf("Failed to add .")
}
// Generate an event.
os.Create(filepath.Join(testDir, "somethingSOMETHINGsomethingSOMETHING"))
// Wait for readEvents to read the event, then close the watcher.
<-time.After(50 * time.Millisecond)
w.Close()
// Wait for the close to complete.
<-time.After(50 * time.Millisecond)
isWatcherReallyClosed(t, w)
}
func isWatcherReallyClosed(t *testing.T, w *Watcher) {
select {
case err, ok := <-w.Errors:
if ok {
t.Fatalf("w.Errors is not closed; readEvents is still alive after closing (error: %v)", err)
}
default:
t.Fatalf("w.Errors would have blocked; readEvents is still alive!")
}
select {
case _, ok := <-w.Events:
if ok {
t.Fatalf("w.Events is not closed; readEvents is still alive after closing")
}
default:
t.Fatalf("w.Events would have blocked; readEvents is still alive!")
}
}
func TestInotifyCloseCreate(t *testing.T) {
testDir := tempMkdir(t)
defer os.RemoveAll(testDir)
w, err := NewWatcher()
if err != nil {
t.Fatalf("Failed to create watcher: %v", err)
}
defer w.Close()
err = w.Add(testDir)
if err != nil {
t.Fatalf("Failed to add testDir: %v", err)
}
h, err := os.Create(filepath.Join(testDir, "testfile"))
if err != nil {
t.Fatalf("Failed to create file in testdir: %v", err)
}
h.Close()
select {
case _ = <-w.Events:
case err := <-w.Errors:
t.Fatalf("Error from watcher: %v", err)
case <-time.After(50 * time.Millisecond):
t.Fatalf("Took too long to wait for event")
}
// At this point, we've received one event, so the goroutine is ready.
// It's also blocking on syscall.Read.
// Now we try to swap the file descriptor under its nose.
w.Close()
w, err = NewWatcher()
defer w.Close()
if err != nil {
t.Fatalf("Failed to create second watcher: %v", err)
}
<-time.After(50 * time.Millisecond)
err = w.Add(testDir)
if err != nil {
t.Fatalf("Error adding testDir again: %v", err)
}
}
func TestInotifyStress(t *testing.T) {
testDir := tempMkdir(t)
defer os.RemoveAll(testDir)
testFile := filepath.Join(testDir, "testfile")
w, err := NewWatcher()
if err != nil {
t.Fatalf("Failed to create watcher: %v", err)
}
defer w.Close()
killchan := make(chan struct{})
defer close(killchan)
err = w.Add(testDir)
if err != nil {
t.Fatalf("Failed to add testDir: %v", err)
}
proc, err := os.FindProcess(os.Getpid())
if err != nil {
t.Fatalf("Error finding process: %v", err)
}
go func() {
for {
select {
case <-time.After(5 * time.Millisecond):
err := proc.Signal(syscall.SIGUSR1)
if err != nil {
t.Fatalf("Signal failed: %v", err)
}
case <-killchan:
return
}
}
}()
go func() {
for {
select {
case <-time.After(11 * time.Millisecond):
err := w.poller.wake()
if err != nil {
t.Fatalf("Wake failed: %v", err)
}
case <-killchan:
return
}
}
}()
go func() {
for {
select {
case <-killchan:
return
default:
handle, err := os.Create(testFile)
if err != nil {
t.Fatalf("Create failed: %v", err)
}
handle.Close()
time.Sleep(time.Millisecond)
err = os.Remove(testFile)
if err != nil {
t.Fatalf("Remove failed: %v", err)
}
}
}
}()
creates := 0
removes := 0
after := time.After(5 * time.Second)
for {
select {
case <-after:
if creates-removes > 1 || creates-removes < -1 {
t.Fatalf("Creates and removes should not be off by more than one: %d creates, %d removes", creates, removes)
}
if creates < 50 {
t.Fatalf("Expected at least 50 creates, got %d", creates)
}
return
case err := <-w.Errors:
t.Fatalf("Got an error from watcher: %v", err)
case evt := <-w.Events:
if evt.Name != testFile {
t.Fatalf("Got an event for an unknown file: %s", evt.Name)
}
if evt.Op == Create {
creates++
}
if evt.Op == Remove {
removes++
}
}
}
}
func TestInotifyRemoveTwice(t *testing.T) {
testDir := tempMkdir(t)
defer os.RemoveAll(testDir)
testFile := filepath.Join(testDir, "testfile")
handle, err := os.Create(testFile)
if err != nil {
t.Fatalf("Create failed: %v", err)
}
handle.Close()
w, err := NewWatcher()
if err != nil {
t.Fatalf("Failed to create watcher: %v", err)
}
defer w.Close()
err = w.Add(testFile)
if err != nil {
t.Fatalf("Failed to add testFile: %v", err)
}
err = os.Remove(testFile)
if err != nil {
t.Fatalf("Failed to remove testFile: %v", err)
}
err = w.Remove(testFile)
if err != syscall.EINVAL {
t.Fatalf("Expected EINVAL from Remove, got: %v", err)
}
err = w.Remove(testFile)
if err == syscall.EINVAL {
t.Fatalf("Got EINVAL again, watch was not removed")
}
}

View File

@ -1109,6 +1109,21 @@ func TestConcurrentRemovalOfWatch(t *testing.T) {
<-removed2 <-removed2
} }
func TestClose(t *testing.T) {
// Regression test for #59 bad file descriptor from Close
testDir := tempMkdir(t)
defer os.RemoveAll(testDir)
watcher := newWatcher(t)
if err := watcher.Add(testDir); err != nil {
t.Fatalf("Expected no error on Add, got %v", err)
}
err := watcher.Close()
if err != nil {
t.Fatalf("Expected no error on Close, got %v.", err)
}
}
func testRename(file1, file2 string) error { func testRename(file1, file2 string) error {
switch runtime.GOOS { switch runtime.GOOS {
case "windows", "plan9": case "windows", "plan9":

View File

@ -14,46 +14,48 @@ import (
"path/filepath" "path/filepath"
"sync" "sync"
"syscall" "syscall"
"time"
) )
// Watcher watches a set of files, delivering events to a channel. // Watcher watches a set of files, delivering events to a channel.
type Watcher struct { type Watcher struct {
Events chan Event Events chan Event
Errors chan error Errors chan error
mu sync.Mutex // Mutex for the Watcher itself. done chan bool // Channel for sending a "quit message" to the reader goroutine
kq int // File descriptor (as returned by the kqueue() syscall).
watches map[string]int // Map of watched file descriptors (key: path). kq int // File descriptor (as returned by the kqueue() syscall).
wmut sync.Mutex // Protects access to watches.
enFlags map[string]uint32 // Map of watched files to evfilt note flags used in kqueue. mu sync.Mutex // Protects access to watcher data
enmut sync.Mutex // Protects access to enFlags. watches map[string]int // Map of watched file descriptors (key: path).
paths map[int]string // Map of watched paths (key: watch descriptor). externalWatches map[string]bool // Map of watches added by user of the library.
finfo map[int]os.FileInfo // Map of file information (isDir, isReg; key: watch descriptor). dirFlags map[string]uint32 // Map of watched directories to fflags used in kqueue.
pmut sync.Mutex // Protects access to paths and finfo. paths map[int]pathInfo // Map file descriptors to path names for processing kqueue events.
fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events). 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. isClosed bool // Set to true when Close() is first called
externalWatches map[string]bool // Map of watches added by user of the library. }
ewmut sync.Mutex // Protects access to externalWatches.
done chan bool // Channel for sending a "quit message" to the reader goroutine type pathInfo struct {
isClosed bool // Set to true when Close() is first called name string
isDir bool
} }
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. // NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
func NewWatcher() (*Watcher, error) { func NewWatcher() (*Watcher, error) {
fd, errno := syscall.Kqueue() kq, err := kqueue()
if fd == -1 { if err != nil {
return nil, os.NewSyscallError("kqueue", errno) return nil, err
} }
w := &Watcher{ w := &Watcher{
kq: fd, kq: kq,
watches: make(map[string]int), watches: make(map[string]int),
enFlags: make(map[string]uint32), dirFlags: make(map[string]uint32),
paths: make(map[int]string), paths: make(map[int]pathInfo),
finfo: make(map[int]os.FileInfo),
fileExists: make(map[string]bool), fileExists: make(map[string]bool),
externalWatches: make(map[string]bool), externalWatches: make(map[string]bool),
Events: make(chan Event), Events: make(chan Event),
Errors: make(chan error), Errors: make(chan error),
done: make(chan bool, 1), done: make(chan bool),
} }
go w.readEvents() go w.readEvents()
@ -70,73 +72,68 @@ func (w *Watcher) Close() error {
w.isClosed = true w.isClosed = true
w.mu.Unlock() w.mu.Unlock()
w.mu.Lock()
ws := w.watches
w.mu.Unlock()
var err error
for name := range ws {
if e := w.Remove(name); e != nil && err == nil {
err = e
}
}
// Send "quit" message to the reader goroutine: // Send "quit" message to the reader goroutine:
w.done <- true w.done <- true
w.wmut.Lock()
ws := w.watches
w.wmut.Unlock()
for name := range ws {
w.Remove(name)
}
return nil return nil
} }
// Add starts watching the named file or directory (non-recursively). // Add starts watching the named file or directory (non-recursively).
func (w *Watcher) Add(name string) error { func (w *Watcher) Add(name string) error {
w.ewmut.Lock() w.mu.Lock()
w.externalWatches[name] = true w.externalWatches[name] = true
w.ewmut.Unlock() w.mu.Unlock()
return w.addWatch(name, noteAllEvents) return w.addWatch(name, noteAllEvents)
} }
// Remove stops watching the the named file or directory (non-recursively). // Remove stops watching the the named file or directory (non-recursively).
func (w *Watcher) Remove(name string) error { func (w *Watcher) Remove(name string) error {
name = filepath.Clean(name) name = filepath.Clean(name)
w.wmut.Lock() w.mu.Lock()
watchfd, ok := w.watches[name] watchfd, ok := w.watches[name]
w.wmut.Unlock() w.mu.Unlock()
if !ok { if !ok {
return fmt.Errorf("can't remove non-existent kevent watch for: %s", name) return fmt.Errorf("can't remove non-existent kevent watch for: %s", name)
} }
var kbuf [1]syscall.Kevent_t
watchEntry := &kbuf[0] const registerRemove = syscall.EV_DELETE
syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_DELETE) if err := register(w.kq, []int{watchfd}, registerRemove, 0); err != nil {
entryFlags := watchEntry.Flags return err
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) syscall.Close(watchfd)
w.wmut.Lock()
w.mu.Lock()
isDir := w.paths[watchfd].isDir
delete(w.watches, name) delete(w.watches, name)
w.wmut.Unlock()
w.enmut.Lock()
delete(w.enFlags, name)
w.enmut.Unlock()
w.pmut.Lock()
delete(w.paths, watchfd) delete(w.paths, watchfd)
fInfo := w.finfo[watchfd] delete(w.dirFlags, name)
delete(w.finfo, watchfd) w.mu.Unlock()
w.pmut.Unlock()
// Find all watched paths that are in this directory that are not external. // Find all watched paths that are in this directory that are not external.
if fInfo.IsDir() { if isDir {
var pathsToRemove []string var pathsToRemove []string
w.pmut.Lock() w.mu.Lock()
for _, wpath := range w.paths { for _, path := range w.paths {
wdir, _ := filepath.Split(wpath) wdir, _ := filepath.Split(path.name)
if filepath.Clean(wdir) == filepath.Clean(name) { if filepath.Clean(wdir) == name {
w.ewmut.Lock() if !w.externalWatches[path.name] {
if !w.externalWatches[wpath] { pathsToRemove = append(pathsToRemove, path.name)
pathsToRemove = append(pathsToRemove, wpath)
} }
w.ewmut.Unlock()
} }
} }
w.pmut.Unlock() w.mu.Unlock()
for _, name := range pathsToRemove { for _, name := range pathsToRemove {
// Since these are internal, not much sense in propagating error // Since these are internal, not much sense in propagating error
// to the user, as that will just confuse them with an error about // to the user, as that will just confuse them with an error about
@ -148,37 +145,38 @@ func (w *Watcher) Remove(name string) error {
return nil return nil
} }
const ( // Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE) const noteAllEvents = syscall.NOTE_DELETE | syscall.NOTE_WRITE | syscall.NOTE_ATTRIB | syscall.NOTE_RENAME
noteAllEvents = syscall.NOTE_DELETE | syscall.NOTE_WRITE | syscall.NOTE_ATTRIB | syscall.NOTE_RENAME
// Block for 100 ms on each call to kevent // keventWaitTime to block on each read from kevent
keventWaitTime = 100e6 var keventWaitTime = durationToTimespec(100 * time.Millisecond)
)
// addWatch adds path to the watched file set. // addWatch adds name to the watched file set.
// The flags are interpreted as described in kevent(2). // The flags are interpreted as described in kevent(2).
func (w *Watcher) addWatch(path string, flags uint32) error { func (w *Watcher) addWatch(name string, flags uint32) error {
path = filepath.Clean(path) var isDir bool
// Make ./name and name equivalent
name = filepath.Clean(name)
w.mu.Lock() w.mu.Lock()
if w.isClosed { if w.isClosed {
w.mu.Unlock() w.mu.Unlock()
return errors.New("kevent instance already closed") return errors.New("kevent instance already closed")
} }
watchfd, alreadyWatching := w.watches[name]
// We already have a watch, but we can still override flags.
if alreadyWatching {
isDir = w.paths[watchfd].isDir
}
w.mu.Unlock() w.mu.Unlock()
watchDir := false if !alreadyWatching {
fi, err := os.Lstat(name)
w.wmut.Lock() if err != nil {
watchfd, found := w.watches[path] return err
w.wmut.Unlock()
if !found {
fi, errstat := os.Lstat(path)
if errstat != nil {
return errstat
} }
// don't watch socket // Don't watch sockets.
if fi.Mode()&os.ModeSocket == os.ModeSocket { if fi.Mode()&os.ModeSocket == os.ModeSocket {
return nil return nil
} }
@ -190,131 +188,96 @@ func (w *Watcher) addWatch(path string, flags uint32) error {
// be no file events for broken symlinks. // be no file events for broken symlinks.
// Hence the returns of nil on errors. // Hence the returns of nil on errors.
if fi.Mode()&os.ModeSymlink == os.ModeSymlink { if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
path, err := filepath.EvalSymlinks(path) name, err = filepath.EvalSymlinks(name)
if err != nil { if err != nil {
return nil return nil
} }
fi, errstat = os.Lstat(path) fi, err = os.Lstat(name)
if errstat != nil { if err != nil {
return nil return nil
} }
} }
fd, errno := syscall.Open(path, openMode, 0700) watchfd, err = syscall.Open(name, openMode, 0700)
if fd == -1 { if watchfd == -1 {
return os.NewSyscallError("Open", errno) return err
} }
watchfd = fd
w.wmut.Lock() isDir = fi.IsDir()
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&syscall.NOTE_WRITE) == syscall.NOTE_WRITE &&
(!found || (w.enFlags[path]&syscall.NOTE_WRITE) != syscall.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 { const registerAdd = syscall.EV_ADD | syscall.EV_CLEAR | syscall.EV_ENABLE
errdir := w.watchDirectoryFiles(path) if err := register(w.kq, []int{watchfd}, registerAdd, flags); err != nil {
if errdir != nil { syscall.Close(watchfd)
return errdir return err
}
if !alreadyWatching {
w.mu.Lock()
w.watches[name] = watchfd
w.paths[watchfd] = pathInfo{name: name, isDir: isDir}
w.mu.Unlock()
}
if isDir {
// Watch the directory if it has not been watched before,
// or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
w.mu.Lock()
watchDir := (flags&syscall.NOTE_WRITE) == syscall.NOTE_WRITE &&
(!alreadyWatching || (w.dirFlags[name]&syscall.NOTE_WRITE) != syscall.NOTE_WRITE)
// Store flags so this watch can be updated later
w.dirFlags[name] = flags
w.mu.Unlock()
if watchDir {
if err := w.watchDirectoryFiles(name); err != nil {
return err
}
} }
} }
return nil return nil
} }
// readEvents reads from the kqueue file descriptor, converts the // readEvents reads from kqueue and converts the received kevents into
// received events into Event objects and sends them via the Events channel // Event values that it sends down the Events channel.
func (w *Watcher) readEvents() { func (w *Watcher) readEvents() {
var ( eventBuffer := make([]syscall.Kevent_t, 10)
keventbuf [10]syscall.Kevent_t // Event buffer
kevents []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
)
kevents = keventbuf[0:0]
twait = new(syscall.Timespec)
*twait = syscall.NsecToTimespec(keventWaitTime)
for { for {
// See if there is a message on the "done" channel // See if there is a message on the "done" channel
var done bool
select { select {
case done = <-w.done: case <-w.done:
default: err := syscall.Close(w.kq)
} if err != nil {
w.Errors <- err
// If "done" message is received
if done {
errno := syscall.Close(w.kq)
if errno != nil {
w.Errors <- os.NewSyscallError("close", errno)
} }
close(w.Events) close(w.Events)
close(w.Errors) close(w.Errors)
return return
default:
} }
// Get new events // Get new events
if len(kevents) == 0 { kevents, err := read(w.kq, eventBuffer, &keventWaitTime)
n, errno = syscall.Kevent(w.kq, nil, keventbuf[:], twait) // EINTR is okay, the syscall was interrupted before timeout expired.
if err != nil && err != syscall.EINTR {
// EINTR is okay, basically the syscall was interrupted before w.Errors <- err
// timeout expired. continue
if errno != nil && errno != syscall.EINTR {
w.Errors <- os.NewSyscallError("kevent", errno)
continue
}
// Received some events
if n > 0 {
kevents = keventbuf[0:n]
}
} }
// Flush the events we received to the Events channel // Flush the events we received to the Events channel
for len(kevents) > 0 { for len(kevents) > 0 {
watchEvent := &kevents[0] kevent := &kevents[0]
mask := uint32(watchEvent.Fflags) watchfd := int(kevent.Ident)
w.pmut.Lock() mask := uint32(kevent.Fflags)
name := w.paths[int(watchEvent.Ident)] w.mu.Lock()
fileInfo := w.finfo[int(watchEvent.Ident)] path := w.paths[watchfd]
w.pmut.Unlock() w.mu.Unlock()
event := newEvent(path.name, mask)
event := newEvent(name, mask, false) if path.isDir && !(event.Op&Remove == Remove) {
// Double check to make sure the directory exists. This can happen when
if fileInfo != nil && fileInfo.IsDir() && !(event.Op&Remove == Remove) {
// 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 // 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 // modification event first but the folder has been deleted and later
// receive the delete event // receive the delete event
@ -324,55 +287,49 @@ func (w *Watcher) readEvents() {
} }
} }
if fileInfo != nil && fileInfo.IsDir() && event.Op&Write == Write && !(event.Op&Remove == Remove) { if event.Op&Rename == Rename || event.Op&Remove == Remove {
w.Remove(event.Name)
w.mu.Lock()
delete(w.fileExists, event.Name)
w.mu.Unlock()
}
if path.isDir && event.Op&Write == Write && !(event.Op&Remove == Remove) {
w.sendDirectoryChangeEvents(event.Name) w.sendDirectoryChangeEvents(event.Name)
} else { } else {
// Send the event on the Events channel // Send the event on the Events channel
w.Events <- event w.Events <- event
} }
// Move to next event
kevents = kevents[1:]
if event.Op&Rename == Rename {
w.Remove(event.Name)
w.femut.Lock()
delete(w.fileExists, event.Name)
w.femut.Unlock()
}
if event.Op&Remove == Remove { if event.Op&Remove == Remove {
w.Remove(event.Name) // Look for a file that may have overwritten this.
w.femut.Lock() // For example, mv f1 f2 will delete f2, then create f2.
delete(w.fileExists, event.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(event.Name) fileDir, _ := filepath.Split(event.Name)
fileDir = filepath.Clean(fileDir) fileDir = filepath.Clean(fileDir)
w.wmut.Lock() w.mu.Lock()
_, found := w.watches[fileDir] _, found := w.watches[fileDir]
w.wmut.Unlock() w.mu.Unlock()
if found { if found {
// make sure the directory exist before we watch for changes. When we // make sure the directory exists before we watch for changes. When we
// do a recursive watch and perform rm -fr, the parent directory might // do a recursive watch and perform rm -fr, the parent directory might
// have gone missing, ignore the missing directory and let the // have gone missing, ignore the missing directory and let the
// upcoming delete event remove the watch form the parent folder // upcoming delete event remove the watch from the parent directory.
if _, err := os.Lstat(fileDir); !os.IsNotExist(err) { if _, err := os.Lstat(fileDir); os.IsExist(err) {
w.sendDirectoryChangeEvents(fileDir) w.sendDirectoryChangeEvents(fileDir)
// FIXME: should this be for events on files or just isDir?
} }
} }
} }
// Move to next event
kevents = kevents[1:]
} }
} }
} }
// newEvent returns an platform-independent Event based on kqueue Fflags. // newEvent returns an platform-independent Event based on kqueue Fflags.
func newEvent(name string, mask uint32, create bool) Event { func newEvent(name string, mask uint32) Event {
e := Event{Name: name} e := Event{Name: name}
if create {
e.Op |= Create
}
if mask&syscall.NOTE_DELETE == syscall.NOTE_DELETE { if mask&syscall.NOTE_DELETE == syscall.NOTE_DELETE {
e.Op |= Remove e.Op |= Remove
} }
@ -388,6 +345,11 @@ func newEvent(name string, mask uint32, create bool) Event {
return e return e
} }
func newCreateEvent(name string) Event {
return Event{Name: name, Op: Create}
}
// watchDirectoryFiles to mimic inotify when adding a watch on a directory
func (w *Watcher) watchDirectoryFiles(dirPath string) error { func (w *Watcher) watchDirectoryFiles(dirPath string) error {
// Get all files // Get all files
files, err := ioutil.ReadDir(dirPath) files, err := ioutil.ReadDir(dirPath)
@ -395,36 +357,15 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error {
return err return err
} }
// Search for new files
for _, fileInfo := range files { for _, fileInfo := range files {
filePath := filepath.Join(dirPath, fileInfo.Name()) filePath := filepath.Join(dirPath, fileInfo.Name())
if err := w.internalWatch(filePath, fileInfo); err != nil {
if fileInfo.IsDir() == false { return err
// Watch file to mimic linux fsnotify
e := w.addWatch(filePath, noteAllEvents)
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 = syscall.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.mu.Lock()
w.fileExists[filePath] = true w.fileExists[filePath] = true
w.femut.Unlock() w.mu.Unlock()
} }
return nil return nil
@ -432,7 +373,7 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error {
// sendDirectoryEvents searches the directory for newly created files // sendDirectoryEvents searches the directory for newly created files
// and sends them over the event channel. This functionality is to have // and sends them over the event channel. This functionality is to have
// the BSD version of fsnotify match linux fsnotify which provides a // the BSD version of fsnotify match Linux inotify which provides a
// create event for files created in a watched directory. // create event for files created in a watched directory.
func (w *Watcher) sendDirectoryChangeEvents(dirPath string) { func (w *Watcher) sendDirectoryChangeEvents(dirPath string) {
// Get all files // Get all files
@ -444,36 +385,79 @@ func (w *Watcher) sendDirectoryChangeEvents(dirPath string) {
// Search for new files // Search for new files
for _, fileInfo := range files { for _, fileInfo := range files {
filePath := filepath.Join(dirPath, fileInfo.Name()) filePath := filepath.Join(dirPath, fileInfo.Name())
w.femut.Lock() w.mu.Lock()
_, doesExist := w.fileExists[filePath] _, doesExist := w.fileExists[filePath]
w.femut.Unlock() w.mu.Unlock()
if !doesExist { if !doesExist {
// Send create event (mask=0) // Send create event
event := newEvent(filePath, 0, true) w.Events <- newCreateEvent(filePath)
w.Events <- event
} }
// watchDirectoryFiles (but without doing another ReadDir) // like watchDirectoryFiles (but without doing another ReadDir)
if fileInfo.IsDir() == false { if err := w.internalWatch(filePath, fileInfo); err != nil {
// Watch file to mimic linux fsnotify return
w.addWatch(filePath, noteAllEvents)
} 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 = syscall.NOTE_DELETE
if found {
newFlags |= currFlags
}
// Linux gives deletes if not explicitly watching
w.addWatch(filePath, newFlags)
} }
w.femut.Lock() w.mu.Lock()
w.fileExists[filePath] = true w.fileExists[filePath] = true
w.femut.Unlock() w.mu.Unlock()
} }
} }
func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) error {
if fileInfo.IsDir() {
// mimic Linux providing delete events for subdirectories
// but preserve the flags used if currently watching subdirectory
w.mu.Lock()
flags := w.dirFlags[name]
w.mu.Unlock()
flags |= syscall.NOTE_DELETE
return w.addWatch(name, flags)
}
// watch file to mimic Linux inotify
return w.addWatch(name, noteAllEvents)
}
// kqueue creates a new kernel event queue and returns a descriptor.
func kqueue() (kq int, err error) {
kq, err = syscall.Kqueue()
if kq == -1 {
return kq, err
}
return kq, nil
}
// register events with the queue
func register(kq int, fds []int, flags int, fflags uint32) error {
changes := make([]syscall.Kevent_t, len(fds))
for i, fd := range fds {
// SetKevent converts int to the platform-specific types:
syscall.SetKevent(&changes[i], fd, syscall.EVFILT_VNODE, flags)
changes[i].Fflags = fflags
}
// register the events
success, err := syscall.Kevent(kq, changes, nil, nil)
if success == -1 {
return err
}
return nil
}
// read retrieves pending events, or waits until an event occurs.
// A timeout of nil blocks indefinitely, while 0 polls the queue.
func read(kq int, events []syscall.Kevent_t, timeout *syscall.Timespec) ([]syscall.Kevent_t, error) {
n, err := syscall.Kevent(kq, nil, events, timeout)
if err != nil {
return nil, err
}
return events[0:n], nil
}
// durationToTimespec prepares a timeout value
func durationToTimespec(d time.Duration) syscall.Timespec {
return syscall.NsecToTimespec(d.Nanoseconds())
}