From cf0c69c5bc84cdcefe887245d4a093f41999caf2 Mon Sep 17 00:00:00 2001 From: Andrew Sutherland Date: Sat, 10 Oct 2015 15:59:08 -0500 Subject: [PATCH] add godeps --- Godeps/Godeps.json | 14 + Godeps/Readme | 5 + Godeps/_workspace/.gitignore | 2 + .../src/github.com/cenkalti/backoff/.gitignore | 22 + .../src/github.com/cenkalti/backoff/.travis.yml | 2 + .../src/github.com/cenkalti/backoff/LICENSE | 20 + .../src/github.com/cenkalti/backoff/README.md | 116 +++ .../cenkalti/backoff/adv_example_test.go | 117 +++ .../src/github.com/cenkalti/backoff/backoff.go | 59 ++ .../github.com/cenkalti/backoff/backoff_test.go | 27 + .../github.com/cenkalti/backoff/example_test.go | 51 ++ .../src/github.com/cenkalti/backoff/exponential.go | 151 ++++ .../cenkalti/backoff/exponential_test.go | 108 +++ .../src/github.com/cenkalti/backoff/retry.go | 46 ++ .../src/github.com/cenkalti/backoff/retry_test.go | 34 + .../src/github.com/cenkalti/backoff/ticker.go | 79 ++ .../src/github.com/cenkalti/backoff/ticker_test.go | 45 ++ .../src/github.com/zillode/notify/.gitignore | 88 ++ .../src/github.com/zillode/notify/.travis.yml | 35 + .../src/github.com/zillode/notify/AUTHORS | 10 + .../src/github.com/zillode/notify/LICENSE | 21 + .../src/github.com/zillode/notify/README.md | 18 + .../src/github.com/zillode/notify/appveyor.yml | 24 + .../src/github.com/zillode/notify/debug.go | 11 + .../src/github.com/zillode/notify/debug_debug.go | 43 + .../src/github.com/zillode/notify/doc.go | 39 + .../src/github.com/zillode/notify/event.go | 143 ++++ .../github.com/zillode/notify/event_fsevents.go | 71 ++ .../src/github.com/zillode/notify/event_inotify.go | 75 ++ .../src/github.com/zillode/notify/event_kqueue.go | 89 ++ .../src/github.com/zillode/notify/event_readdcw.go | 108 +++ .../src/github.com/zillode/notify/event_stub.go | 31 + .../src/github.com/zillode/notify/event_test.go | 33 + .../zillode/notify/example_fsevents_test.go | 128 +++ .../zillode/notify/example_inotify_test.go | 83 ++ .../zillode/notify/example_readdcw_test.go | 40 + .../src/github.com/zillode/notify/example_test.go | 87 ++ .../src/github.com/zillode/notify/node.go | 271 +++++++ .../src/github.com/zillode/notify/notify.go | 70 ++ .../zillode/notify/notify_inotify_test.go | 37 + .../zillode/notify/notify_readdcw_test.go | 60 ++ .../src/github.com/zillode/notify/notify_test.go | 103 +++ .../github.com/zillode/notify/sync_readdcw_test.go | 33 + .../github.com/zillode/notify/sync_unix_test.go | 18 + .../src/github.com/zillode/notify/testdata/vfs.txt | 56 ++ .../src/github.com/zillode/notify/testing_test.go | 895 +++++++++++++++++++++ .../src/github.com/zillode/notify/tree.go | 22 + .../github.com/zillode/notify/tree_nonrecursive.go | 292 +++++++ .../zillode/notify/tree_nonrecursive_test.go | 543 +++++++++++++ .../github.com/zillode/notify/tree_recursive.go | 354 ++++++++ .../zillode/notify/tree_recursive_test.go | 524 ++++++++++++ .../src/github.com/zillode/notify/util.go | 146 ++++ .../github.com/zillode/notify/util_darwin_test.go | 41 + .../src/github.com/zillode/notify/util_test.go | 147 ++++ .../github.com/zillode/notify/util_unix_test.go | 94 +++ .../src/github.com/zillode/notify/watcher.go | 85 ++ .../github.com/zillode/notify/watcher_fsevents.go | 319 ++++++++ .../zillode/notify/watcher_fsevents_cgo.go | 156 ++++ .../zillode/notify/watcher_fsevents_test.go | 111 +++ .../github.com/zillode/notify/watcher_inotify.go | 396 +++++++++ .../zillode/notify/watcher_inotify_test.go | 132 +++ .../github.com/zillode/notify/watcher_kqueue.go | 413 ++++++++++ .../zillode/notify/watcher_kqueue_test.go | 72 ++ .../github.com/zillode/notify/watcher_readdcw.go | 574 +++++++++++++ .../zillode/notify/watcher_readdcw_test.go | 67 ++ .../zillode/notify/watcher_recursive_test.go | 102 +++ .../src/github.com/zillode/notify/watcher_stub.go | 21 + .../src/github.com/zillode/notify/watcher_test.go | 32 + .../src/github.com/zillode/notify/watchpoint.go | 102 +++ .../github.com/zillode/notify/watchpoint_other.go | 23 + .../zillode/notify/watchpoint_readdcw.go | 38 + .../github.com/zillode/notify/watchpoint_test.go | 162 ++++ 72 files changed, 8586 insertions(+) create mode 100644 Godeps/Godeps.json create mode 100644 Godeps/Readme create mode 100644 Godeps/_workspace/.gitignore create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/.gitignore create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/.travis.yml create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/LICENSE create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/README.md create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/adv_example_test.go create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/backoff.go create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/backoff_test.go create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/example_test.go create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/exponential.go create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/exponential_test.go create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/retry.go create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/retry_test.go create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/ticker.go create mode 100644 Godeps/_workspace/src/github.com/cenkalti/backoff/ticker_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/.gitignore create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/.travis.yml create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/AUTHORS create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/LICENSE create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/README.md create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/appveyor.yml create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/debug.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/debug_debug.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/doc.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/event.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/event_fsevents.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/event_inotify.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/event_kqueue.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/event_readdcw.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/event_stub.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/event_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/example_fsevents_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/example_inotify_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/example_readdcw_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/example_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/node.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/notify.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/notify_inotify_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/notify_readdcw_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/notify_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/sync_readdcw_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/sync_unix_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/testdata/vfs.txt create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/testing_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/tree.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/tree_nonrecursive.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/tree_nonrecursive_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/tree_recursive.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/tree_recursive_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/util.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/util_darwin_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/util_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/util_unix_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher_fsevents.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher_fsevents_cgo.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher_fsevents_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher_inotify.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher_inotify_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher_kqueue.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher_kqueue_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher_readdcw.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher_readdcw_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher_recursive_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher_stub.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watcher_test.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watchpoint.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watchpoint_other.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watchpoint_readdcw.go create mode 100644 Godeps/_workspace/src/github.com/zillode/notify/watchpoint_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json new file mode 100644 index 0000000..79c42a5 --- /dev/null +++ b/Godeps/Godeps.json @@ -0,0 +1,14 @@ +{ + "ImportPath": "github.com/syncthing/syncthing-inotify", + "GoVersion": "go1.5.1", + "Deps": [ + { + "ImportPath": "github.com/cenkalti/backoff", + "Rev": "4dc77674aceaabba2c7e3da25d4c823edfb73f99" + }, + { + "ImportPath": "github.com/zillode/notify", + "Rev": "f06b1e3b795091f2e1414067b08e5f07332cdb05" + } + ] +} diff --git a/Godeps/Readme b/Godeps/Readme new file mode 100644 index 0000000..4cdaa53 --- /dev/null +++ b/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/Godeps/_workspace/.gitignore b/Godeps/_workspace/.gitignore new file mode 100644 index 0000000..f037d68 --- /dev/null +++ b/Godeps/_workspace/.gitignore @@ -0,0 +1,2 @@ +/pkg +/bin diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/.gitignore b/Godeps/_workspace/src/github.com/cenkalti/backoff/.gitignore new file mode 100644 index 0000000..0026861 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/.travis.yml b/Godeps/_workspace/src/github.com/cenkalti/backoff/.travis.yml new file mode 100644 index 0000000..ce9cb62 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/.travis.yml @@ -0,0 +1,2 @@ +language: go +go: 1.3.3 diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/LICENSE b/Godeps/_workspace/src/github.com/cenkalti/backoff/LICENSE new file mode 100644 index 0000000..89b8179 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Cenk Altı + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/README.md b/Godeps/_workspace/src/github.com/cenkalti/backoff/README.md new file mode 100644 index 0000000..020b8fb --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/README.md @@ -0,0 +1,116 @@ +# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Build Status][travis image]][travis] + +This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client]. + +[Exponential backoff][exponential backoff wiki] +is an algorithm that uses feedback to multiplicatively decrease the rate of some process, +in order to gradually find an acceptable rate. +The retries exponentially increase and stop increasing when a certain threshold is met. + +## How To + +We define two functions, `Retry()` and `RetryNotify()`. +They receive an `Operation` to execute, a `BackOff` algorithm, +and an optional `Notify` error handler. + +The operation will be executed, and will be retried on failure with delay +as given by the backoff algorithm. The backoff algorithm can also decide when to stop +retrying. +In addition, the notify error handler will be called after each failed attempt, +except for the last time, whose error should be handled by the caller. + +```go +// An Operation is executing by Retry() or RetryNotify(). +// The operation will be retried using a backoff policy if it returns an error. +type Operation func() error + +// Notify is a notify-on-error function. It receives an operation error and +// backoff delay if the operation failed (with an error). +// +// NOTE that if the backoff policy stated to stop retrying, +// the notify function isn't called. +type Notify func(error, time.Duration) + +func Retry(Operation, BackOff) error +func RetryNotify(Operation, BackOff, Notify) +``` + +## Examples + +See more advanced examples in the [godoc][advanced example]. + +### Retry + +Simple retry helper that uses the default exponential backoff algorithm: + +```go +operation := func() error { + // An operation that might fail. + return nil // or return errors.New("some error") +} + +err := Retry(operation, NewExponentialBackOff()) +if err != nil { + // Handle error. + return err +} + +// Operation is successful. +return nil +``` + +### Ticker + +```go +operation := func() error { + // An operation that might fail + return nil // or return errors.New("some error") +} + +b := NewExponentialBackOff() +ticker := NewTicker(b) + +var err error + +// Ticks will continue to arrive when the previous operation is still running, +// so operations that take a while to fail could run in quick succession. +for range ticker.C { + if err = operation(); err != nil { + log.Println(err, "will retry...") + continue + } + + ticker.Stop() + break +} + +if err != nil { + // Operation has failed. + return err +} + +// Operation is successful. +return nil +``` + +## Getting Started + +```bash +# install +$ go get github.com/cenkalti/backoff + +# test +$ cd $GOPATH/src/github.com/cenkalti/backoff +$ go get -t ./... +$ go test -v -cover +``` + +[godoc]: https://godoc.org/github.com/cenkalti/backoff +[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png +[travis]: https://travis-ci.org/cenkalti/backoff +[travis image]: https://travis-ci.org/cenkalti/backoff.png + +[google-http-java-client]: https://github.com/google/google-http-java-client +[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff + +[advanced example]: https://godoc.org/github.com/cenkalti/backoff#example_ diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/adv_example_test.go b/Godeps/_workspace/src/github.com/cenkalti/backoff/adv_example_test.go new file mode 100644 index 0000000..3fe6783 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/adv_example_test.go @@ -0,0 +1,117 @@ +package backoff + +import ( + "io/ioutil" + "log" + "net/http" + "time" +) + +// This is an example that demonstrates how this package could be used +// to perform various advanced operations. +// +// It executes an HTTP GET request with exponential backoff, +// while errors are logged and failed responses are closed, as required by net/http package. +// +// Note we define a condition function which is used inside the operation to +// determine whether the operation succeeded or failed. +func Example() error { + res, err := GetWithRetry( + "http://localhost:9999", + ErrorIfStatusCodeIsNot(http.StatusOK), + NewExponentialBackOff()) + + if err != nil { + // Close response body of last (failed) attempt. + // The Last attempt isn't handled by the notify-on-error function, + // which closes the body of all the previous attempts. + if e := res.Body.Close(); e != nil { + log.Printf("error closing last attempt's response body: %s", e) + } + log.Printf("too many failed request attempts: %s", err) + return err + } + defer res.Body.Close() // The response's Body must be closed. + + // Read body + _, _ = ioutil.ReadAll(res.Body) + + // Do more stuff + return nil +} + +// GetWithRetry is a helper function that performs an HTTP GET request +// to the given URL, and retries with the given backoff using the given condition function. +// +// It also uses a notify-on-error function which logs +// and closes the response body of the failed request. +func GetWithRetry(url string, condition Condition, bck BackOff) (*http.Response, error) { + var res *http.Response + err := RetryNotify( + func() error { + var err error + res, err = http.Get(url) + if err != nil { + return err + } + return condition(res) + }, + bck, + LogAndClose()) + + return res, err +} + +// Condition is a retry condition function. +// It receives a response, and returns an error +// if the response failed the condition. +type Condition func(*http.Response) error + +// ErrorIfStatusCodeIsNot returns a retry condition function. +// The condition returns an error +// if the given response's status code is not the given HTTP status code. +func ErrorIfStatusCodeIsNot(status int) Condition { + return func(res *http.Response) error { + if res.StatusCode != status { + return NewError(res) + } + return nil + } +} + +// Error is returned on ErrorIfX() condition functions throughout this package. +type Error struct { + Response *http.Response +} + +func NewError(res *http.Response) *Error { + // Sanity check + if res == nil { + panic("response object is nil") + } + return &Error{Response: res} +} +func (err *Error) Error() string { return "request failed" } + +// LogAndClose is a notify-on-error function. +// It logs the error and closes the response body. +func LogAndClose() Notify { + return func(err error, wait time.Duration) { + switch e := err.(type) { + case *Error: + defer e.Response.Body.Close() + + b, err := ioutil.ReadAll(e.Response.Body) + var body string + if err != nil { + body = "can't read body" + } else { + body = string(b) + } + + log.Printf("%s: %s", e.Response.Status, body) + default: + log.Println(err) + } + } +} diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/backoff.go b/Godeps/_workspace/src/github.com/cenkalti/backoff/backoff.go new file mode 100644 index 0000000..61bd6df --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/backoff.go @@ -0,0 +1,59 @@ +// Package backoff implements backoff algorithms for retrying operations. +// +// Also has a Retry() helper for retrying operations that may fail. +package backoff + +import "time" + +// BackOff is a backoff policy for retrying an operation. +type BackOff interface { + // NextBackOff returns the duration to wait before retrying the operation, + // or backoff.Stop to indicate that no more retries should be made. + // + // Example usage: + // + // duration := backoff.NextBackOff(); + // if (duration == backoff.Stop) { + // // Do not retry operation. + // } else { + // // Sleep for duration and retry operation. + // } + // + NextBackOff() time.Duration + + // Reset to initial state. + Reset() +} + +// Indicates that no more retries should be made for use in NextBackOff(). +const Stop time.Duration = -1 + +// ZeroBackOff is a fixed backoff policy whose backoff time is always zero, +// meaning that the operation is retried immediately without waiting, indefinitely. +type ZeroBackOff struct{} + +func (b *ZeroBackOff) Reset() {} + +func (b *ZeroBackOff) NextBackOff() time.Duration { return 0 } + +// StopBackOff is a fixed backoff policy that always returns backoff.Stop for +// NextBackOff(), meaning that the operation should never be retried. +type StopBackOff struct{} + +func (b *StopBackOff) Reset() {} + +func (b *StopBackOff) NextBackOff() time.Duration { return Stop } + +// ConstantBackOff is a backoff policy that always returns the same backoff delay. +// This is in contrast to an exponential backoff policy, +// which returns a delay that grows longer as you call NextBackOff() over and over again. +type ConstantBackOff struct { + Interval time.Duration +} + +func (b *ConstantBackOff) Reset() {} +func (b *ConstantBackOff) NextBackOff() time.Duration { return b.Interval } + +func NewConstantBackOff(d time.Duration) *ConstantBackOff { + return &ConstantBackOff{Interval: d} +} diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/backoff_test.go b/Godeps/_workspace/src/github.com/cenkalti/backoff/backoff_test.go new file mode 100644 index 0000000..91f27c4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/backoff_test.go @@ -0,0 +1,27 @@ +package backoff + +import ( + "testing" + "time" +) + +func TestNextBackOffMillis(t *testing.T) { + subtestNextBackOff(t, 0, new(ZeroBackOff)) + subtestNextBackOff(t, Stop, new(StopBackOff)) +} + +func subtestNextBackOff(t *testing.T, expectedValue time.Duration, backOffPolicy BackOff) { + for i := 0; i < 10; i++ { + next := backOffPolicy.NextBackOff() + if next != expectedValue { + t.Errorf("got: %d expected: %d", next, expectedValue) + } + } +} + +func TestConstantBackOff(t *testing.T) { + backoff := NewConstantBackOff(time.Second) + if backoff.NextBackOff() != time.Second { + t.Error("invalid interval") + } +} diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/example_test.go b/Godeps/_workspace/src/github.com/cenkalti/backoff/example_test.go new file mode 100644 index 0000000..0d1852e --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/example_test.go @@ -0,0 +1,51 @@ +package backoff + +import "log" + +func ExampleRetry() error { + operation := func() error { + // An operation that might fail. + return nil // or return errors.New("some error") + } + + err := Retry(operation, NewExponentialBackOff()) + if err != nil { + // Handle error. + return err + } + + // Operation is successful. + return nil +} + +func ExampleTicker() error { + operation := func() error { + // An operation that might fail + return nil // or return errors.New("some error") + } + + b := NewExponentialBackOff() + ticker := NewTicker(b) + + var err error + + // Ticks will continue to arrive when the previous operation is still running, + // so operations that take a while to fail could run in quick succession. + for _ = range ticker.C { + if err = operation(); err != nil { + log.Println(err, "will retry...") + continue + } + + ticker.Stop() + break + } + + if err != nil { + // Operation has failed. + return err + } + + // Operation is successful. + return nil +} diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/exponential.go b/Godeps/_workspace/src/github.com/cenkalti/backoff/exponential.go new file mode 100644 index 0000000..cc2a164 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/exponential.go @@ -0,0 +1,151 @@ +package backoff + +import ( + "math/rand" + "time" +) + +/* +ExponentialBackOff is a backoff implementation that increases the backoff +period for each retry attempt using a randomization function that grows exponentially. + +NextBackOff() is calculated using the following formula: + + randomized interval = + RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor]) + +In other words NextBackOff() will range between the randomization factor +percentage below and above the retry interval. + +For example, given the following parameters: + + RetryInterval = 2 + RandomizationFactor = 0.5 + Multiplier = 2 + +the actual backoff period used in the next retry attempt will range between 1 and 3 seconds, +multiplied by the exponential, that is, between 2 and 6 seconds. + +Note: MaxInterval caps the RetryInterval and not the randomized interval. + +If the time elapsed since an ExponentialBackOff instance is created goes past the +MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop. + +The elapsed time can be reset by calling Reset(). + +Example: Given the following default arguments, for 10 tries the sequence will be, +and assuming we go over the MaxElapsedTime on the 10th try: + + Request # RetryInterval (seconds) Randomized Interval (seconds) + + 1 0.5 [0.25, 0.75] + 2 0.75 [0.375, 1.125] + 3 1.125 [0.562, 1.687] + 4 1.687 [0.8435, 2.53] + 5 2.53 [1.265, 3.795] + 6 3.795 [1.897, 5.692] + 7 5.692 [2.846, 8.538] + 8 8.538 [4.269, 12.807] + 9 12.807 [6.403, 19.210] + 10 19.210 backoff.Stop + +Note: Implementation is not thread-safe. +*/ +type ExponentialBackOff struct { + InitialInterval time.Duration + RandomizationFactor float64 + Multiplier float64 + MaxInterval time.Duration + // After MaxElapsedTime the ExponentialBackOff stops. + // It never stops if MaxElapsedTime == 0. + MaxElapsedTime time.Duration + Clock Clock + + currentInterval time.Duration + startTime time.Time +} + +// Clock is an interface that returns current time for BackOff. +type Clock interface { + Now() time.Time +} + +// Default values for ExponentialBackOff. +const ( + DefaultInitialInterval = 500 * time.Millisecond + DefaultRandomizationFactor = 0.5 + DefaultMultiplier = 1.5 + DefaultMaxInterval = 60 * time.Second + DefaultMaxElapsedTime = 15 * time.Minute +) + +// NewExponentialBackOff creates an instance of ExponentialBackOff using default values. +func NewExponentialBackOff() *ExponentialBackOff { + b := &ExponentialBackOff{ + InitialInterval: DefaultInitialInterval, + RandomizationFactor: DefaultRandomizationFactor, + Multiplier: DefaultMultiplier, + MaxInterval: DefaultMaxInterval, + MaxElapsedTime: DefaultMaxElapsedTime, + Clock: SystemClock, + } + b.Reset() + return b +} + +type systemClock struct{} + +func (t systemClock) Now() time.Time { + return time.Now() +} + +// SystemClock implements Clock interface that uses time.Now(). +var SystemClock = systemClock{} + +// Reset the interval back to the initial retry interval and restarts the timer. +func (b *ExponentialBackOff) Reset() { + b.currentInterval = b.InitialInterval + b.startTime = b.Clock.Now() +} + +// NextBackOff calculates the next backoff interval using the formula: +// Randomized interval = RetryInterval +/- (RandomizationFactor * RetryInterval) +func (b *ExponentialBackOff) NextBackOff() time.Duration { + // Make sure we have not gone over the maximum elapsed time. + if b.MaxElapsedTime != 0 && b.GetElapsedTime() > b.MaxElapsedTime { + return Stop + } + defer b.incrementCurrentInterval() + return getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval) +} + +// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance +// is created and is reset when Reset() is called. +// +// The elapsed time is computed using time.Now().UnixNano(). +func (b *ExponentialBackOff) GetElapsedTime() time.Duration { + return b.Clock.Now().Sub(b.startTime) +} + +// Increments the current interval by multiplying it with the multiplier. +func (b *ExponentialBackOff) incrementCurrentInterval() { + // Check for overflow, if overflow is detected set the current interval to the max interval. + if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier { + b.currentInterval = b.MaxInterval + } else { + b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier) + } +} + +// Returns a random value from the following interval: +// [randomizationFactor * currentInterval, randomizationFactor * currentInterval]. +func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration { + var delta = randomizationFactor * float64(currentInterval) + var minInterval = float64(currentInterval) - delta + var maxInterval = float64(currentInterval) + delta + + // Get a random value from the range [minInterval, maxInterval]. + // The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then + // we want a 33% chance for selecting either 1, 2 or 3. + return time.Duration(minInterval + (random * (maxInterval - minInterval + 1))) +} diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/exponential_test.go b/Godeps/_workspace/src/github.com/cenkalti/backoff/exponential_test.go new file mode 100644 index 0000000..11b95e4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/exponential_test.go @@ -0,0 +1,108 @@ +package backoff + +import ( + "math" + "testing" + "time" +) + +func TestBackOff(t *testing.T) { + var ( + testInitialInterval = 500 * time.Millisecond + testRandomizationFactor = 0.1 + testMultiplier = 2.0 + testMaxInterval = 5 * time.Second + testMaxElapsedTime = 15 * time.Minute + ) + + exp := NewExponentialBackOff() + exp.InitialInterval = testInitialInterval + exp.RandomizationFactor = testRandomizationFactor + exp.Multiplier = testMultiplier + exp.MaxInterval = testMaxInterval + exp.MaxElapsedTime = testMaxElapsedTime + exp.Reset() + + var expectedResults = []time.Duration{500, 1000, 2000, 4000, 5000, 5000, 5000, 5000, 5000, 5000} + for i, d := range expectedResults { + expectedResults[i] = d * time.Millisecond + } + + for _, expected := range expectedResults { + assertEquals(t, expected, exp.currentInterval) + // Assert that the next backoff falls in the expected range. + var minInterval = expected - time.Duration(testRandomizationFactor*float64(expected)) + var maxInterval = expected + time.Duration(testRandomizationFactor*float64(expected)) + var actualInterval = exp.NextBackOff() + if !(minInterval <= actualInterval && actualInterval <= maxInterval) { + t.Error("error") + } + } +} + +func TestGetRandomizedInterval(t *testing.T) { + // 33% chance of being 1. + assertEquals(t, 1, getRandomValueFromInterval(0.5, 0, 2)) + assertEquals(t, 1, getRandomValueFromInterval(0.5, 0.33, 2)) + // 33% chance of being 2. + assertEquals(t, 2, getRandomValueFromInterval(0.5, 0.34, 2)) + assertEquals(t, 2, getRandomValueFromInterval(0.5, 0.66, 2)) + // 33% chance of being 3. + assertEquals(t, 3, getRandomValueFromInterval(0.5, 0.67, 2)) + assertEquals(t, 3, getRandomValueFromInterval(0.5, 0.99, 2)) +} + +type TestClock struct { + i time.Duration + start time.Time +} + +func (c *TestClock) Now() time.Time { + t := c.start.Add(c.i) + c.i += time.Second + return t +} + +func TestGetElapsedTime(t *testing.T) { + var exp = NewExponentialBackOff() + exp.Clock = &TestClock{} + exp.Reset() + + var elapsedTime = exp.GetElapsedTime() + if elapsedTime != time.Second { + t.Errorf("elapsedTime=%d", elapsedTime) + } +} + +func TestMaxElapsedTime(t *testing.T) { + var exp = NewExponentialBackOff() + exp.Clock = &TestClock{start: time.Time{}.Add(10000 * time.Second)} + // Change the currentElapsedTime to be 0 ensuring that the elapsed time will be greater + // than the max elapsed time. + exp.startTime = time.Time{} + assertEquals(t, Stop, exp.NextBackOff()) +} + +func TestBackOffOverflow(t *testing.T) { + var ( + testInitialInterval time.Duration = math.MaxInt64 / 2 + testMaxInterval time.Duration = math.MaxInt64 + testMultiplier = 2.1 + ) + + exp := NewExponentialBackOff() + exp.InitialInterval = testInitialInterval + exp.Multiplier = testMultiplier + exp.MaxInterval = testMaxInterval + exp.Reset() + + exp.NextBackOff() + // Assert that when an overflow is possible the current varerval time.Duration is set to the max varerval time.Duration . + assertEquals(t, testMaxInterval, exp.currentInterval) +} + +func assertEquals(t *testing.T, expected, value time.Duration) { + if expected != value { + t.Errorf("got: %d, expected: %d", value, expected) + } +} diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/retry.go b/Godeps/_workspace/src/github.com/cenkalti/backoff/retry.go new file mode 100644 index 0000000..f01f2bb --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/retry.go @@ -0,0 +1,46 @@ +package backoff + +import "time" + +// An Operation is executing by Retry() or RetryNotify(). +// The operation will be retried using a backoff policy if it returns an error. +type Operation func() error + +// Notify is a notify-on-error function. It receives an operation error and +// backoff delay if the operation failed (with an error). +// +// NOTE that if the backoff policy stated to stop retrying, +// the notify function isn't called. +type Notify func(error, time.Duration) + +// Retry the function f until it does not return error or BackOff stops. +// f is guaranteed to be run at least once. +// It is the caller's responsibility to reset b after Retry returns. +// +// Retry sleeps the goroutine for the duration returned by BackOff after a +// failed operation returns. +func Retry(o Operation, b BackOff) error { return RetryNotify(o, b, nil) } + +// RetryNotify calls notify function with the error and wait duration +// for each failed attempt before sleep. +func RetryNotify(operation Operation, b BackOff, notify Notify) error { + var err error + var next time.Duration + + b.Reset() + for { + if err = operation(); err == nil { + return nil + } + + if next = b.NextBackOff(); next == Stop { + return err + } + + if notify != nil { + notify(err, next) + } + + time.Sleep(next) + } +} diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/retry_test.go b/Godeps/_workspace/src/github.com/cenkalti/backoff/retry_test.go new file mode 100644 index 0000000..c0d25ab --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/retry_test.go @@ -0,0 +1,34 @@ +package backoff + +import ( + "errors" + "log" + "testing" +) + +func TestRetry(t *testing.T) { + const successOn = 3 + var i = 0 + + // This function is successfull on "successOn" calls. + f := func() error { + i++ + log.Printf("function is called %d. time\n", i) + + if i == successOn { + log.Println("OK") + return nil + } + + log.Println("error") + return errors.New("error") + } + + err := Retry(f, NewExponentialBackOff()) + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + if i != successOn { + t.Errorf("invalid number of retries: %d", i) + } +} diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/ticker.go b/Godeps/_workspace/src/github.com/cenkalti/backoff/ticker.go new file mode 100644 index 0000000..7a5ff4e --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/ticker.go @@ -0,0 +1,79 @@ +package backoff + +import ( + "runtime" + "sync" + "time" +) + +// Ticker holds a channel that delivers `ticks' of a clock at times reported by a BackOff. +// +// Ticks will continue to arrive when the previous operation is still running, +// so operations that take a while to fail could run in quick succession. +type Ticker struct { + C <-chan time.Time + c chan time.Time + b BackOff + stop chan struct{} + stopOnce sync.Once +} + +// NewTicker returns a new Ticker containing a channel that will send the time at times +// specified by the BackOff argument. Ticker is guaranteed to tick at least once. +// The channel is closed when Stop method is called or BackOff stops. +func NewTicker(b BackOff) *Ticker { + c := make(chan time.Time) + t := &Ticker{ + C: c, + c: c, + b: b, + stop: make(chan struct{}), + } + go t.run() + runtime.SetFinalizer(t, (*Ticker).Stop) + return t +} + +// Stop turns off a ticker. After Stop, no more ticks will be sent. +func (t *Ticker) Stop() { + t.stopOnce.Do(func() { close(t.stop) }) +} + +func (t *Ticker) run() { + c := t.c + defer close(c) + t.b.Reset() + + // Ticker is guaranteed to tick at least once. + afterC := t.send(time.Now()) + + for { + if afterC == nil { + return + } + + select { + case tick := <-afterC: + afterC = t.send(tick) + case <-t.stop: + t.c = nil // Prevent future ticks from being sent to the channel. + return + } + } +} + +func (t *Ticker) send(tick time.Time) <-chan time.Time { + select { + case t.c <- tick: + case <-t.stop: + return nil + } + + next := t.b.NextBackOff() + if next == Stop { + t.Stop() + return nil + } + + return time.After(next) +} diff --git a/Godeps/_workspace/src/github.com/cenkalti/backoff/ticker_test.go b/Godeps/_workspace/src/github.com/cenkalti/backoff/ticker_test.go new file mode 100644 index 0000000..7c392df --- /dev/null +++ b/Godeps/_workspace/src/github.com/cenkalti/backoff/ticker_test.go @@ -0,0 +1,45 @@ +package backoff + +import ( + "errors" + "log" + "testing" +) + +func TestTicker(t *testing.T) { + const successOn = 3 + var i = 0 + + // This function is successfull on "successOn" calls. + f := func() error { + i++ + log.Printf("function is called %d. time\n", i) + + if i == successOn { + log.Println("OK") + return nil + } + + log.Println("error") + return errors.New("error") + } + + b := NewExponentialBackOff() + ticker := NewTicker(b) + + var err error + for _ = range ticker.C { + if err = f(); err != nil { + t.Log(err) + continue + } + + break + } + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + if i != successOn { + t.Errorf("invalid number of retries: %d", i) + } +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/.gitignore b/Godeps/_workspace/src/github.com/zillode/notify/.gitignore new file mode 100644 index 0000000..86d4fa8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/.gitignore @@ -0,0 +1,88 @@ +# Created by https://www.gitignore.io + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +### Linux ### +*~ + +# KDE directory preferences +.directory + + +### Go ### +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + + +### vim ### +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist +*~ + diff --git a/Godeps/_workspace/src/github.com/zillode/notify/.travis.yml b/Godeps/_workspace/src/github.com/zillode/notify/.travis.yml new file mode 100644 index 0000000..f287755 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/.travis.yml @@ -0,0 +1,35 @@ +language: go + +go: + - 1.4.2 + +os: + - linux + - osx + +matrix: + include: + - os: osx + go: 1.4.2 + env: + - GOFLAGS="-tags kqueue" + +env: + global: + - secure: JfdLa3hcnvIwT13kbH2fr4c+4V9H4yO4GZsqDckODmxILlEtKcCLM5eCi09D21uTuaMUfZDIFNPN4mpDhbeEoM6Aoro4Ht+mX0vlpFUOS5bRqhP0l59PKEXqKFfh2IIlqEOmQ9XU6XvqWezTirN1OFUPMIMO9qSM/HuKhbX5wJQ= + - GOBIN=$HOME/bin + - PATH=$HOME/bin:$PATH + +install: + - if [ "$(curl -LIsS -w %{http_code} coveralls.io | tail -1)" = "200" ] && [ "$TRAVIS_OS_NAME" = "linux" ]; then export COVERALLS=1; fi + - go get golang.org/x/tools/cmd/vet + - if [ "$COVERALLS" = "1" ]; then go get golang.org/x/tools/cmd/cover github.com/mattn/goveralls github.com/modocache/gover; fi + - go get -t -v ./... + +script: + - go tool vet -all . + - go install $GOFLAGS ./... + - go test -v -race $GOFLAGS ./... + - if [ "$COVERALLS" = "1" ]; then go list -f '{{if len .TestGoFiles}}go test -coverprofile={{.Dir}}/.coverprofile {{.ImportPath}}{{end}}' ./... | xargs -i sh -c {}; fi + - if [ "$COVERALLS" = "1" ]; then gover; fi + - if [ "$COVERALLS" = "1" ]; then goveralls -coverprofile=gover.coverprofile -service=travis-ci -repotoken $COVERALLS_TOKEN; fi diff --git a/Godeps/_workspace/src/github.com/zillode/notify/AUTHORS b/Godeps/_workspace/src/github.com/zillode/notify/AUTHORS new file mode 100644 index 0000000..9262eae --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/AUTHORS @@ -0,0 +1,10 @@ +# List of individuals who contributed to the Notify package. +# +# The up-to-date list of the authors one may obtain with: +# +# ~ $ git shortlog -es | cut -f2 | rev | uniq -f1 | rev +# + +Pawel Blaszczyk +Pawel Knap +Rafal Jeczalik diff --git a/Godeps/_workspace/src/github.com/zillode/notify/LICENSE b/Godeps/_workspace/src/github.com/zillode/notify/LICENSE new file mode 100644 index 0000000..3e67881 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2015 The Notify Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/zillode/notify/README.md b/Godeps/_workspace/src/github.com/zillode/notify/README.md new file mode 100644 index 0000000..adbeb3f --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/README.md @@ -0,0 +1,18 @@ +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) +====== + +Filesystem event notification library on steroids. (under active development) + +*Documentation* + +[godoc.org/github.com/rjeczalik/notify](https://godoc.org/github.com/rjeczalik/notify) + +*Installation* + +``` +~ $ go get -u github.com/rjeczalik/notify +``` + +*Projects using notify* + +- [github.com/rjeczalik/cmd/notify](https://godoc.org/github.com/rjeczalik/cmd/notify) diff --git a/Godeps/_workspace/src/github.com/zillode/notify/appveyor.yml b/Godeps/_workspace/src/github.com/zillode/notify/appveyor.yml new file mode 100644 index 0000000..16d09ac --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/appveyor.yml @@ -0,0 +1,24 @@ +version: "{build}" + +os: Windows Server 2012 R2 + +clone_folder: c:\projects\src\github.com\rjeczalik\notify + +environment: + PATH: c:\projects\bin;%PATH% + GOPATH: c:\projects + NOTIFY_TIMEOUT: 5s + +install: + - go version + - go get golang.org/x/tools/cmd/vet + - go get -v -t ./... + +build_script: + - go tool vet -all . + - go build ./... + - go test -v -race ./... + +test: off + +deploy: off diff --git a/Godeps/_workspace/src/github.com/zillode/notify/debug.go b/Godeps/_workspace/src/github.com/zillode/notify/debug.go new file mode 100644 index 0000000..bd9bc46 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/debug.go @@ -0,0 +1,11 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build !debug + +package notify + +func dbgprint(...interface{}) {} + +func dbgprintf(string, ...interface{}) {} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/debug_debug.go b/Godeps/_workspace/src/github.com/zillode/notify/debug_debug.go new file mode 100644 index 0000000..f062291 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/debug_debug.go @@ -0,0 +1,43 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build debug + +package notify + +import ( + "fmt" + "os" + "runtime" + "strings" +) + +func dbgprint(v ...interface{}) { + fmt.Printf("[D] ") + fmt.Print(v...) + fmt.Printf("\n\n") +} + +func dbgprintf(format string, v ...interface{}) { + fmt.Printf("[D] ") + fmt.Printf(format, v...) + fmt.Printf("\n\n") +} + +func dbgcallstack(max int) []string { + pc, stack := make([]uintptr, max), make([]string, 0, max) + runtime.Callers(2, pc) + for _, pc := range pc { + if f := runtime.FuncForPC(pc); f != nil { + fname := f.Name() + idx := strings.LastIndex(fname, string(os.PathSeparator)) + if idx != -1 { + stack = append(stack, fname[idx+1:]) + } else { + stack = append(stack, fname) + } + } + } + return stack +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/doc.go b/Godeps/_workspace/src/github.com/zillode/notify/doc.go new file mode 100644 index 0000000..e26014e --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/doc.go @@ -0,0 +1,39 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// Package notify implements access to filesystem events. +// +// Notify is a high-level abstraction over filesystem watchers like inotify, +// kqueue, FSEvents or ReadDirectoryChangesW. Watcher implementations are +// split into two groups: ones that natively support recursive notifications +// (FSEvents and ReadDirectoryChangesW) and ones that do not (inotify and kqueue). +// For more details see watcher and recursiveWatcher interfaces in watcher.go +// source file. +// +// On top of filesystem watchers notify maintains a watchpoint tree, which provides +// strategy for creating and closing filesystem watches and dispatching filesystem +// events to user channels. +// +// An event set is just an event list joint using bitwise OR operator +// into a single event value. +// +// A filesystem watch or just a watch is platform-specific entity which represents +// a single path registered for notifications for specific event set. Setting a watch +// means using platform-specific API calls for creating / initializing said watch. +// For each watcher the API call is: +// +// - FSEvents: FSEventStreamCreate +// - inotify: notify_add_watch +// - kqueue: kevent +// - ReadDirectoryChangesW: CreateFile+ReadDirectoryChangesW +// +// To rewatch means to either shrink or expand an event set that was previously +// registered during watch operation for particular filesystem watch. +// +// A watchpoint is a list of user channel and event set pairs for particular +// path (watchpoint tree's node). A single watchpoint can contain multiple +// different user channels registered to listen for one or more events. A single +// user channel can be registered in one or more watchpoints, recurisve and +// non-recursive ones as well. +package notify diff --git a/Godeps/_workspace/src/github.com/zillode/notify/event.go b/Godeps/_workspace/src/github.com/zillode/notify/event.go new file mode 100644 index 0000000..e045edc --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/event.go @@ -0,0 +1,143 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +import ( + "fmt" + "strings" +) + +// Event represents the type of filesystem action. +// +// Number of available event values is dependent on the target system or the +// watcher implmenetation used (e.g. it's possible to use either kqueue or +// FSEvents on Darwin). +// +// Please consult documentation for your target platform to see list of all +// available events. +type Event uint32 + +// Create, Remove, Write and Rename are the only event values guaranteed to be +// present on all platforms. +const ( + Create = osSpecificCreate + Remove = osSpecificRemove + Write = osSpecificWrite + Rename = osSpecificRename + + // All is handful alias for all platform-independent event values. + All = Create | Remove | Write | Rename +) + +const internal = recursive | omit + +// String implements fmt.Stringer interface. +func (e Event) String() string { + var s []string + for _, strmap := range []map[Event]string{estr, osestr} { + for ev, str := range strmap { + if e&ev == ev { + s = append(s, str) + } + } + } + return strings.Join(s, "|") +} + +// EventInfo describes an event reported by the underlying filesystem notification +// subsystem. +// +// It always describes single event, even if the OS reported a coalesced action. +// Reported path is absolute and clean. +// +// For non-recursive watchpoints its base is always equal to the path passed +// to corresponding Watch call. +// +// The value of Sys if system-dependent and can be nil. +// +// Sys +// +// Under Darwin (FSEvents) Sys() always returns a non-nil *notify.FSEvent value, +// which is defined as: +// +// type FSEvent struct { +// Path string // real path of the file or directory +// ID uint64 // ID of the event (FSEventStreamEventId) +// Flags uint32 // joint FSEvents* flags (FSEventStreamEventFlags) +// } +// +// For possible values of Flags see Darwin godoc for notify or FSEvents +// documentation for FSEventStreamEventFlags constants: +// +// https://developer.apple.com/library/mac/documentation/Darwin/Reference/FSEvents_Ref/index.html#//apple_ref/doc/constant_group/FSEventStreamEventFlags +// +// Under Linux (inotify) Sys() always returns a non-nil *syscall.InotifyEvent +// value, defined as: +// +// type InotifyEvent struct { +// Wd int32 // Watch descriptor +// Mask uint32 // Mask describing event +// Cookie uint32 // Unique cookie associating related events (for rename(2)) +// Len uint32 // Size of name field +// Name [0]uint8 // Optional null-terminated name +// } +// +// More information about inotify masks and the usage of inotify_event structure +// can be found at: +// +// http://man7.org/linux/man-pages/man7/inotify.7.html +// +// Under Darwin, DragonFlyBSD, FreeBSD, NetBSD, OpenBSD (kqueue) Sys() always +// returns a non-nil *notify.Kevent value, which is defined as: +// +// type Kevent struct { +// Kevent *syscall.Kevent_t // Kevent is a kqueue specific structure +// FI os.FileInfo // FI describes file/dir +// } +// +// More information about syscall.Kevent_t can be found at: +// +// https://www.freebsd.org/cgi/man.cgi?query=kqueue +// +// Under Windows (ReadDirectoryChangesW) Sys() always returns nil. The documentation +// of watcher's WinAPI function can be found at: +// +// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365465%28v=vs.85%29.aspx +type EventInfo interface { + Event() Event // event value for the filesystem action + Path() string // real path of the file or directory + Sys() interface{} // underlying data source (can return nil) +} + +type isDirer interface { + isDir() (bool, error) +} + +var _ fmt.Stringer = (*event)(nil) +var _ isDirer = (*event)(nil) + +// String implements fmt.Stringer interface. +func (e *event) String() string { + return e.Event().String() + `: "` + e.Path() + `"` +} + +var estr = map[Event]string{ + Create: "notify.Create", + Remove: "notify.Remove", + Write: "notify.Write", + Rename: "notify.Rename", + // Display name for recursive event is added only for debugging + // purposes. It's an internal event after all and won't be exposed to the + // user. Having Recursive event printable is helpful, e.g. for reading + // testing failure messages: + // + // --- FAIL: TestWatchpoint (0.00 seconds) + // watchpoint_test.go:64: want diff=[notify.Remove notify.Create|notify.Remove]; + // got [notify.Remove notify.Remove|notify.Create] (i=1) + // + // Yup, here the diff have Recursive event inside. Go figure. + recursive: "recursive", + omit: "omit", +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/event_fsevents.go b/Godeps/_workspace/src/github.com/zillode/notify/event_fsevents.go new file mode 100644 index 0000000..6ded80b --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/event_fsevents.go @@ -0,0 +1,71 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build darwin,!kqueue + +package notify + +const ( + osSpecificCreate = Event(FSEventsCreated) + osSpecificRemove = Event(FSEventsRemoved) + osSpecificWrite = Event(FSEventsModified) + osSpecificRename = Event(FSEventsRenamed) + // internal = Event(0x100000) + // recursive is used to distinguish recursive eventsets from non-recursive ones + recursive = Event(0x200000) + // omit is used for dispatching internal events; only those events are sent + // for which both the event and the watchpoint has omit in theirs event sets. + omit = Event(0x400000) +) + +// FSEvents specific event values. +const ( + FSEventsMustScanSubDirs Event = 0x00001 + FSEventsUserDropped = 0x00002 + FSEventsKernelDropped = 0x00004 + FSEventsEventIdsWrapped = 0x00008 + FSEventsHistoryDone = 0x00010 + FSEventsRootChanged = 0x00020 + FSEventsMount = 0x00040 + FSEventsUnmount = 0x00080 + FSEventsCreated = 0x00100 + FSEventsRemoved = 0x00200 + FSEventsInodeMetaMod = 0x00400 + FSEventsRenamed = 0x00800 + FSEventsModified = 0x01000 + FSEventsFinderInfoMod = 0x02000 + FSEventsChangeOwner = 0x04000 + FSEventsXattrMod = 0x08000 + FSEventsIsFile = 0x10000 + FSEventsIsDir = 0x20000 + FSEventsIsSymlink = 0x40000 +) + +var osestr = map[Event]string{ + FSEventsMustScanSubDirs: "notify.FSEventsMustScanSubDirs", + FSEventsUserDropped: "notify.FSEventsUserDropped", + FSEventsKernelDropped: "notify.FSEventsKernelDropped", + FSEventsEventIdsWrapped: "notify.FSEventsEventIdsWrapped", + FSEventsHistoryDone: "notify.FSEventsHistoryDone", + FSEventsRootChanged: "notify.FSEventsRootChanged", + FSEventsMount: "notify.FSEventsMount", + FSEventsUnmount: "notify.FSEventsUnmount", + FSEventsInodeMetaMod: "notify.FSEventsInodeMetaMod", + FSEventsFinderInfoMod: "notify.FSEventsFinderInfoMod", + FSEventsChangeOwner: "notify.FSEventsChangeOwner", + FSEventsXattrMod: "notify.FSEventsXattrMod", + FSEventsIsFile: "notify.FSEventsIsFile", + FSEventsIsDir: "notify.FSEventsIsDir", + FSEventsIsSymlink: "notify.FSEventsIsSymlink", +} + +type event struct { + fse FSEvent + event Event +} + +func (ei *event) Event() Event { return ei.event } +func (ei *event) Path() string { return ei.fse.Path } +func (ei *event) Sys() interface{} { return &ei.fse } +func (ei *event) isDir() (bool, error) { return ei.fse.Flags&FSEventsIsDir != 0, nil } diff --git a/Godeps/_workspace/src/github.com/zillode/notify/event_inotify.go b/Godeps/_workspace/src/github.com/zillode/notify/event_inotify.go new file mode 100644 index 0000000..82954a9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/event_inotify.go @@ -0,0 +1,75 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build linux + +package notify + +import "syscall" + +// Platform independent event values. +const ( + osSpecificCreate Event = 0x100000 << iota + osSpecificRemove + osSpecificWrite + osSpecificRename + // internal + // recursive is used to distinguish recursive eventsets from non-recursive ones + recursive + // omit is used for dispatching internal events; only those events are sent + // for which both the event and the watchpoint has omit in theirs event sets. + omit +) + +// Inotify specific masks are legal, implemented events that are guaranteed to +// work with notify package on linux-based systems. +const ( + InAccess = Event(syscall.IN_ACCESS) // File was accessed + InModify = Event(syscall.IN_MODIFY) // File was modified + InAttrib = Event(syscall.IN_ATTRIB) // Metadata changed + InCloseWrite = Event(syscall.IN_CLOSE_WRITE) // Writtable file was closed + InCloseNowrite = Event(syscall.IN_CLOSE_NOWRITE) // Unwrittable file closed + InOpen = Event(syscall.IN_OPEN) // File was opened + InMovedFrom = Event(syscall.IN_MOVED_FROM) // File was moved from X + InMovedTo = Event(syscall.IN_MOVED_TO) // File was moved to Y + InCreate = Event(syscall.IN_CREATE) // Subfile was created + InDelete = Event(syscall.IN_DELETE) // Subfile was deleted + InDeleteSelf = Event(syscall.IN_DELETE_SELF) // Self was deleted + InMoveSelf = Event(syscall.IN_MOVE_SELF) // Self was moved +) + +var osestr = map[Event]string{ + InAccess: "notify.InAccess", + InModify: "notify.InModify", + InAttrib: "notify.InAttrib", + InCloseWrite: "notify.InCloseWrite", + InCloseNowrite: "notify.InCloseNowrite", + InOpen: "notify.InOpen", + InMovedFrom: "notify.InMovedFrom", + InMovedTo: "notify.InMovedTo", + InCreate: "notify.InCreate", + InDelete: "notify.InDelete", + InDeleteSelf: "notify.InDeleteSelf", + InMoveSelf: "notify.InMoveSelf", +} + +// Inotify behavior events are not **currently** supported by notify package. +const ( + inDontFollow = Event(syscall.IN_DONT_FOLLOW) + inExclUnlink = Event(syscall.IN_EXCL_UNLINK) + inMaskAdd = Event(syscall.IN_MASK_ADD) + inOneshot = Event(syscall.IN_ONESHOT) + inOnlydir = Event(syscall.IN_ONLYDIR) +) + +type event struct { + sys syscall.InotifyEvent + path string + event Event +} + +func (e *event) Event() Event { return e.event } +func (e *event) Path() string { return e.path } +func (e *event) Sys() interface{} { return &e.sys } +func (e *event) isDir() (bool, error) { return e.sys.Mask&syscall.IN_ISDIR != 0, nil } diff --git a/Godeps/_workspace/src/github.com/zillode/notify/event_kqueue.go b/Godeps/_workspace/src/github.com/zillode/notify/event_kqueue.go new file mode 100644 index 0000000..b5fee08 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/event_kqueue.go @@ -0,0 +1,89 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build darwin,kqueue dragonfly freebsd netbsd openbsd + +package notify + +import ( + "os" + "syscall" +) + +// TODO(pblaszczyk): ensure in runtime notify built-in event values do not +// overlap with platform-defined ones. + +// Platform independent event values. +const ( + osSpecificCreate Event = 0x0100 << iota + osSpecificRemove + osSpecificWrite + osSpecificRename + // internal + // recursive is used to distinguish recursive eventsets from non-recursive ones + recursive + // omit is used for dispatching internal events; only those events are sent + // for which both the event and the watchpoint has omit in theirs event sets. + omit +) + +const ( + // NoteDelete is an even reported when the unlink() system call was called + // on the file referenced by the descriptor. + NoteDelete = Event(syscall.NOTE_DELETE) + // NoteWrite is an event reported when a write occurred on the file + // referenced by the descriptor. + NoteWrite = Event(syscall.NOTE_WRITE) + // NoteExtend is an event reported when the file referenced by the + // descriptor was extended. + NoteExtend = Event(syscall.NOTE_EXTEND) + // NoteAttrib is an event reported when the file referenced + // by the descriptor had its attributes changed. + NoteAttrib = Event(syscall.NOTE_ATTRIB) + // NoteLink is an event reported when the link count on the file changed. + NoteLink = Event(syscall.NOTE_LINK) + // NoteRename is an event reported when the file referenced + // by the descriptor was renamed. + NoteRename = Event(syscall.NOTE_RENAME) + // NoteRevoke is an event reported when access to the file was revoked via + // revoke(2) or the underlying file system was unmounted. + NoteRevoke = Event(syscall.NOTE_REVOKE) +) + +var osestr = map[Event]string{ + NoteDelete: "notify.NoteDelete", + NoteWrite: "notify.NoteWrite", + NoteExtend: "notify.NoteExtend", + NoteAttrib: "notify.NoteAttrib", + NoteLink: "notify.NoteLink", + NoteRename: "notify.NoteRename", + NoteRevoke: "notify.NoteRevoke", +} + +// event is a struct storing reported event's data. +type event struct { + // p is an absolute path to file for which event is reported. + p string + // e specifies type of a reported event. + e Event + // kq specifies event's additional information. + kq Kevent +} + +// Event returns type of a reported event. +func (e *event) Event() Event { return e.e } + +// Path returns path to file/directory for which event is reported. +func (e *event) Path() string { return e.p } + +// Sys returns platform specific object describing reported event. +func (e *event) Sys() interface{} { return &e.kq } + +func (e *event) isDir() (bool, error) { return e.kq.FI.IsDir(), nil } + +// Kevent represents a single event. +type Kevent struct { + Kevent *syscall.Kevent_t // Kevent is a kqueue specific structure. + FI os.FileInfo // FI describes file/dir. +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/event_readdcw.go b/Godeps/_workspace/src/github.com/zillode/notify/event_readdcw.go new file mode 100644 index 0000000..11ead9e --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/event_readdcw.go @@ -0,0 +1,108 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build windows + +package notify + +import ( + "os" + "path/filepath" + "syscall" +) + +// Platform independent event values. +const ( + osSpecificCreate Event = 1 << (20 + iota) + osSpecificRemove + osSpecificWrite + osSpecificRename + // recursive is used to distinguish recursive eventsets from non-recursive ones + recursive + // omit is used for dispatching internal events; only those events are sent + // for which both the event and the watchpoint has omit in theirs event sets. + omit + // dirmarker TODO(pknap) + dirmarker +) + +// ReadDirectoryChangesW filters. +const ( + FileNotifyChangeFileName = Event(syscall.FILE_NOTIFY_CHANGE_FILE_NAME) + FileNotifyChangeDirName = Event(syscall.FILE_NOTIFY_CHANGE_DIR_NAME) + FileNotifyChangeAttributes = Event(syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES) + FileNotifyChangeSize = Event(syscall.FILE_NOTIFY_CHANGE_SIZE) + FileNotifyChangeLastWrite = Event(syscall.FILE_NOTIFY_CHANGE_LAST_WRITE) + FileNotifyChangeLastAccess = Event(syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS) + FileNotifyChangeCreation = Event(syscall.FILE_NOTIFY_CHANGE_CREATION) + FileNotifyChangeSecurity = Event(syscallFileNotifyChangeSecurity) +) + +const ( + fileNotifyChangeAll = 0x17f // logical sum of all FileNotifyChange* events. + fileNotifyChangeModified = fileNotifyChangeAll &^ (FileNotifyChangeFileName | FileNotifyChangeDirName) +) + +// according to: http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx +// this flag should be declared in: http://golang.org/src/pkg/syscall/ztypes_windows.go +const syscallFileNotifyChangeSecurity = 0x00000100 + +// ReadDirectoryChangesW actions. +const ( + FileActionAdded = Event(syscall.FILE_ACTION_ADDED) << 12 + FileActionRemoved = Event(syscall.FILE_ACTION_REMOVED) << 12 + FileActionModified = Event(syscall.FILE_ACTION_MODIFIED) << 14 + FileActionRenamedOldName = Event(syscall.FILE_ACTION_RENAMED_OLD_NAME) << 15 + FileActionRenamedNewName = Event(syscall.FILE_ACTION_RENAMED_NEW_NAME) << 16 +) + +const fileActionAll = 0x7f000 // logical sum of all FileAction* events. + +var osestr = map[Event]string{ + FileNotifyChangeFileName: "notify.FileNotifyChangeFileName", + FileNotifyChangeDirName: "notify.FileNotifyChangeDirName", + FileNotifyChangeAttributes: "notify.FileNotifyChangeAttributes", + FileNotifyChangeSize: "notify.FileNotifyChangeSize", + FileNotifyChangeLastWrite: "notify.FileNotifyChangeLastWrite", + FileNotifyChangeLastAccess: "notify.FileNotifyChangeLastAccess", + FileNotifyChangeCreation: "notify.FileNotifyChangeCreation", + FileNotifyChangeSecurity: "notify.FileNotifyChangeSecurity", + + FileActionAdded: "notify.FileActionAdded", + FileActionRemoved: "notify.FileActionRemoved", + FileActionModified: "notify.FileActionModified", + FileActionRenamedOldName: "notify.FileActionRenamedOldName", + FileActionRenamedNewName: "notify.FileActionRenamedNewName", +} + +const ( + fTypeUnknown uint8 = iota + fTypeFile + fTypeDirectory +) + +// TODO(ppknap) : doc. +type event struct { + pathw []uint16 + name string + ftype uint8 + action uint32 + filter uint32 + e Event +} + +func (e *event) Event() Event { return e.e } +func (e *event) Path() string { return filepath.Join(syscall.UTF16ToString(e.pathw), e.name) } +func (e *event) Sys() interface{} { return e.ftype } + +func (e *event) isDir() (bool, error) { + if e.ftype != fTypeUnknown { + return e.ftype == fTypeDirectory, nil + } + fi, err := os.Stat(e.Path()) + if err != nil { + return false, err + } + return fi.IsDir(), nil +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/event_stub.go b/Godeps/_workspace/src/github.com/zillode/notify/event_stub.go new file mode 100644 index 0000000..919bce2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/event_stub.go @@ -0,0 +1,31 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build !darwin,!linux,!freebsd,!dragonfly,!netbsd,!openbsd,!windows +// +build !kqueue + +package notify + +// Platform independent event values. +const ( + osSpecificCreate Event = 1 << iota + osSpecificRemove + osSpecificWrite + osSpecificRename + // internal + // recursive is used to distinguish recursive eventsets from non-recursive ones + recursive + // omit is used for dispatching internal events; only those events are sent + // for which both the event and the watchpoint has omit in theirs event sets. + omit +) + +var osestr = map[Event]string{} + +type event struct{} + +func (e *event) Event() (_ Event) { return } +func (e *event) Path() (_ string) { return } +func (e *event) Sys() (_ interface{}) { return } +func (e *event) isDir() (_ bool, _ error) { return } diff --git a/Godeps/_workspace/src/github.com/zillode/notify/event_test.go b/Godeps/_workspace/src/github.com/zillode/notify/event_test.go new file mode 100644 index 0000000..2803509 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/event_test.go @@ -0,0 +1,33 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +import ( + "sort" + "strings" + "testing" +) + +// S is a workaround for random event strings concatenation order. +func s(s string) string { + z := strings.Split(s, "|") + sort.StringSlice(z).Sort() + return strings.Join(z, "|") +} + +// This test is not safe to run in parallel with others. +func TestEventString(t *testing.T) { + cases := map[Event]string{ + Create: "notify.Create", + Create | Remove: "notify.Create|notify.Remove", + Create | Remove | Write: "notify.Create|notify.Remove|notify.Write", + Create | Write | Rename: "notify.Create|notify.Rename|notify.Write", + } + for e, str := range cases { + if s := s(e.String()); s != str { + t.Errorf("want s=%s; got %s (e=%#x)", str, s, e) + } + } +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/example_fsevents_test.go b/Godeps/_workspace/src/github.com/zillode/notify/example_fsevents_test.go new file mode 100644 index 0000000..53d1b34 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/example_fsevents_test.go @@ -0,0 +1,128 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build darwin,!kqueue + +package notify_test + +import ( + "log" + + "github.com/rjeczalik/notify" +) + +// This example shows how to use FSEvents-specifc event values. +func ExampleWatch_darwin() { + // Make the channel buffered to ensure no event is dropped. Notify will drop + // an event if the receiver is not able to keep up the sending pace. + c := make(chan notify.EventInfo, 1) + + // Set up a watchpoint listening for FSEvents-specific events within a + // current working directory. Dispatch each FSEventsChangeOwner and FSEventsMount + // events separately to c. + if err := notify.Watch(".", c, notify.FSEventsChangeOwner, notify.FSEventsMount); err != nil { + log.Fatal(err) + } + defer notify.Stop(c) + + // Block until an event is received. + switch ei := <-c; ei.Event() { + case notify.FSEventsChangeOwner: + log.Println("The owner of", ei.Path(), "has changed.") + case notify.FSEventsMount: + log.Println("The path", ei.Path(), "has been mounted.") + } +} + +// This example shows how to work with EventInfo's underlying FSEvent struct. +// Investigating notify.(*FSEvent).Flags field we are able to say whether +// the event's path is a file or a directory and many more. +func ExampleWatch_darwinDirFileSymlink() { + var must = func(err error) { + if err != nil { + log.Fatal(err) + } + } + var stop = func(c ...chan<- notify.EventInfo) { + for _, c := range c { + notify.Stop(c) + } + } + + // Make the channels buffered to ensure no event is dropped. Notify will drop + // an event if the receiver is not able to keep up the sending pace. + dir := make(chan notify.EventInfo, 1) + file := make(chan notify.EventInfo, 1) + symlink := make(chan notify.EventInfo, 1) + all := make(chan notify.EventInfo, 1) + + // Set up a single watchpoint listening for FSEvents-specific events on + // multiple user-provided channels. + must(notify.Watch(".", dir, notify.FSEventsIsDir)) + must(notify.Watch(".", file, notify.FSEventsIsFile)) + must(notify.Watch(".", symlink, notify.FSEventsIsSymlink)) + must(notify.Watch(".", all, notify.All)) + defer stop(dir, file, symlink, all) + + // Block until an event is received. + select { + case ei := <-dir: + log.Println("The directory", ei.Path(), "has changed") + case ei := <-file: + log.Println("The file", ei.Path(), "has changed") + case ei := <-symlink: + log.Println("The symlink", ei.Path(), "has changed") + case ei := <-all: + var kind string + + // Investigate underlying *notify.FSEvent struct to access more + // information about the event. + switch flags := ei.Sys().(*notify.FSEvent).Flags; { + case flags¬ify.FSEventsIsFile != 0: + kind = "file" + case flags¬ify.FSEventsIsDir != 0: + kind = "dir" + case flags¬ify.FSEventsIsSymlink != 0: + kind = "symlink" + } + + log.Printf("The %s under path %s has been %sd\n", kind, ei.Path(), ei.Event()) + } +} + +// FSEvents may report multiple filesystem actions with one, coalesced event. +// Notify unscoalesces such event and dispatches series of single events +// back to the user. +// +// This example shows how to coalesce events by investigating notify.(*FSEvent).ID +// field, for the science. +func ExampleWatch_darwinCoalesce() { + // Make the channels buffered to ensure no event is dropped. Notify will drop + // an event if the receiver is not able to keep up the sending pace. + c := make(chan notify.EventInfo, 4) + + // Set up a watchpoint listetning for events within current working directory. + // Dispatch all platform-independent separately to c. + if err := notify.Watch(".", c, notify.All); err != nil { + log.Fatal(err) + } + defer notify.Stop(c) + + var id uint64 + var coalesced []notify.EventInfo + + for ei := range c { + switch n := ei.Sys().(*notify.FSEvent).ID; { + case id == 0: + id = n + coalesced = []notify.EventInfo{ei} + case id == n: + coalesced = append(coalesced, ei) + default: + log.Printf("FSEvents reported a filesystem action with the following"+ + " coalesced events %v groupped by %d ID\n", coalesced, id) + return + } + } +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/example_inotify_test.go b/Godeps/_workspace/src/github.com/zillode/notify/example_inotify_test.go new file mode 100644 index 0000000..e6a68e7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/example_inotify_test.go @@ -0,0 +1,83 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build linux + +package notify_test + +import ( + "log" + "syscall" + + "github.com/rjeczalik/notify" +) + +// This example shows how to watch changes made on file-system by text editor +// when saving a file. Usually, either InCloseWrite or InMovedTo (when swapping +// with a temporary file) event is created. +func ExampleWatch_linux() { + // Make the channel buffered to ensure no event is dropped. Notify will drop + // an event if the receiver is not able to keep up the sending pace. + c := make(chan notify.EventInfo, 1) + + // Set up a watchpoint listening for inotify-specific events within a + // current working directory. Dispatch each InCloseWrite and InMovedTo + // events separately to c. + if err := notify.Watch(".", c, notify.InCloseWrite, notify.InMovedTo); err != nil { + log.Fatal(err) + } + defer notify.Stop(c) + + // Block until an event is received. + switch ei := <-c; ei.Event() { + case notify.InCloseWrite: + log.Println("Editing of", ei.Path(), "file is done.") + case notify.InMovedTo: + log.Println("File", ei.Path(), "was swapped/moved into the watched directory.") + } +} + +// This example shows how to use Sys() method from EventInfo interface to tie +// two separate events generated by rename(2) function. +func ExampleWatch_linuxMove() { + // Make the channel buffered to ensure no event is dropped. Notify will drop + // an event if the receiver is not able to keep up the sending pace. + c := make(chan notify.EventInfo, 2) + + // Set up a watchpoint listening for inotify-specific events within a + // current working directory. Dispatch each InMovedFrom and InMovedTo + // events separately to c. + if err := notify.Watch(".", c, notify.InMovedFrom, notify.InMovedTo); err != nil { + log.Fatal(err) + } + defer notify.Stop(c) + + // Inotify reports move filesystem action by sending two events tied with + // unique cookie value (uint32): one of the events is of InMovedFrom type + // carrying move source path, while the second one is of InMoveTo type + // carrying move destination path. + moves := make(map[uint32]struct { + From string + To string + }) + + // Wait for moves. + for ei := range c { + cookie := ei.Sys().(*syscall.InotifyEvent).Cookie + + info := moves[cookie] + switch ei.Event() { + case notify.InMovedFrom: + info.From = ei.Path() + case notify.InMovedTo: + info.To = ei.Path() + } + moves[cookie] = info + + if cookie != 0 && info.From != "" && info.To != "" { + log.Println("File:", info.From, "was renamed to", info.To) + delete(moves, cookie) + } + } +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/example_readdcw_test.go b/Godeps/_workspace/src/github.com/zillode/notify/example_readdcw_test.go new file mode 100644 index 0000000..e9e05be --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/example_readdcw_test.go @@ -0,0 +1,40 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build windows + +package notify_test + +import ( + "log" + + "github.com/rjeczalik/notify" +) + +// This example shows how to watch directory-name changes in the working directory subtree. +func ExampleWatch_windows() { + // Make the channel buffered to ensure no event is dropped. Notify will drop + // an event if the receiver is not able to keep up the sending pace. + c := make(chan notify.EventInfo, 4) + + // Since notify package behaves exactly like ReadDirectoryChangesW function, + // we must register notify.FileNotifyChangeDirName filter and wait for one + // of FileAction* events. + if err := notify.Watch("./...", c, notify.FileNotifyChangeDirName); err != nil { + log.Fatal(err) + } + defer notify.Stop(c) + + // Wait for actions. + for ei := range c { + switch ei.Event() { + case notify.FileActionAdded, notify.FileActionRenamedNewName: + log.Println("Created:", ei.Path()) + case notify.FileActionRemoved, notify.FileActionRenamedOldName: + log.Println("Removed:", ei.Path()) + case notify.FileActionModified: + panic("notify: unexpected action") + } + } +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/example_test.go b/Godeps/_workspace/src/github.com/zillode/notify/example_test.go new file mode 100644 index 0000000..3c8df71 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/example_test.go @@ -0,0 +1,87 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify_test + +import ( + "log" + "path/filepath" + "time" + + "github.com/rjeczalik/notify" +) + +// This is a basic example showing how to work with notify.Watch function. +func ExampleWatch() { + // Make the channel buffered to ensure no event is dropped. Notify will drop + // an event if the receiver is not able to keep up the sending pace. + c := make(chan notify.EventInfo, 1) + + // Set up a watchpoint listening on events within current working directory. + // Dispatch each create and remove events separately to c. + if err := notify.Watch(".", c, notify.Create, notify.Remove); err != nil { + log.Fatal(err) + } + defer notify.Stop(c) + + // Block until an event is received. + ei := <-c + log.Println("Got event:", ei) +} + +// This example shows how to set up a recursive watchpoint. +func ExampleWatch_recursive() { + // Make the channel buffered to ensure no event is dropped. Notify will drop + // an event if the receiver is not able to keep up the sending pace. + c := make(chan notify.EventInfo, 1) + + // Set up a watchpoint listening for events within a directory tree rooted + // at current working directory. Dispatch remove events to c. + if err := notify.Watch("./...", c, notify.Remove); err != nil { + log.Fatal(err) + } + defer notify.Stop(c) + + // Block until an event is received. + ei := <-c + log.Println("Got event:", ei) +} + +// This example shows why it is important to not create leaks by stoping +// a channel when it's no longer being used. +func ExampleStop() { + waitfor := func(path string, e notify.Event, timeout time.Duration) bool { + dir, file := filepath.Split(path) + c := make(chan notify.EventInfo, 1) + + if err := notify.Watch(dir, c, e); err != nil { + log.Fatal(err) + } + // Clean up watchpoint associated with c. If Stop was not called upon + // return the channel would be leaked as notify holds the only reference + // to it and does not release it on its own. + defer notify.Stop(c) + + t := time.After(timeout) + + for { + select { + case ei := <-c: + if filepath.Base(ei.Path()) == file { + return true + } + case <-t: + return false + } + } + } + + if waitfor("index.lock", notify.Create, 5*time.Second) { + log.Println("The git repository was locked") + } + + if waitfor("index.lock", notify.Remove, 5*time.Second) { + log.Println("The git repository was unlocked") + } +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/node.go b/Godeps/_workspace/src/github.com/zillode/notify/node.go new file mode 100644 index 0000000..4302071 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/node.go @@ -0,0 +1,271 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +import ( + "errors" + "io/ioutil" + "os" + "path/filepath" +) + +var errSkip = errors.New("notify: skip") + +type walkPathFunc func(nd node, isbase bool) error + +type walkFunc func(node) error + +func errnotexist(name string) error { + return &os.PathError{ + Op: "Node", + Path: name, + Err: os.ErrNotExist, + } +} + +type node struct { + Name string + Watch watchpoint + Child map[string]node +} + +func newnode(name string) node { + return node{ + Name: name, + Watch: make(watchpoint), + Child: make(map[string]node), + } +} + +func (nd node) addchild(name, base string) node { + child, ok := nd.Child[base] + if !ok { + child = newnode(name) + nd.Child[base] = child + } + return child +} + +func (nd node) Add(name string) node { + i := indexbase(nd.Name, name) + if i == -1 { + return node{} + } + for j := indexSep(name[i:]); j != -1; j = indexSep(name[i:]) { + nd = nd.addchild(name[:i+j], name[i:i+j]) + i += j + 1 + } + return nd.addchild(name, name[i:]) +} + +func (nd node) AddDir(fn walkFunc) error { + stack := []node{nd} +Traverse: + for n := len(stack); n != 0; n = len(stack) { + nd, stack = stack[n-1], stack[:n-1] + switch err := fn(nd); err { + case nil: + case errSkip: + continue Traverse + default: + return err + } + // TODO(rjeczalik): tolerate open failures - add failed names to + // AddDirError and notify users which names are not added to the tree. + fi, err := ioutil.ReadDir(nd.Name) + if err != nil { + return err + } + for _, fi := range fi { + if fi.Mode()&(os.ModeSymlink|os.ModeDir) == os.ModeDir { + name := filepath.Join(nd.Name, fi.Name()) + stack = append(stack, nd.addchild(name, name[len(nd.Name)+1:])) + } + } + } + return nil +} + +func (nd node) Get(name string) (node, error) { + i := indexbase(nd.Name, name) + if i == -1 { + return node{}, errnotexist(name) + } + ok := false + for j := indexSep(name[i:]); j != -1; j = indexSep(name[i:]) { + if nd, ok = nd.Child[name[i:i+j]]; !ok { + return node{}, errnotexist(name) + } + i += j + 1 + } + if nd, ok = nd.Child[name[i:]]; !ok { + return node{}, errnotexist(name) + } + return nd, nil +} + +func (nd node) Del(name string) error { + i := indexbase(nd.Name, name) + if i == -1 { + return errnotexist(name) + } + stack := []node{nd} + ok := false + for j := indexSep(name[i:]); j != -1; j = indexSep(name[i:]) { + if nd, ok = nd.Child[name[i:i+j]]; !ok { + return errnotexist(name[:i+j]) + } + stack = append(stack, nd) + } + if nd, ok = nd.Child[name[i:]]; !ok { + return errnotexist(name) + } + nd.Child = nil + nd.Watch = nil + for name, i = base(nd.Name), len(stack); i != 0; name, i = base(nd.Name), i-1 { + nd = stack[i-1] + if nd := nd.Child[name]; len(nd.Watch) > 1 || len(nd.Child) != 0 { + break + } else { + nd.Child = nil + nd.Watch = nil + } + delete(nd.Child, name) + } + return nil +} + +func (nd node) Walk(fn walkFunc) error { + stack := []node{nd} +Traverse: + for n := len(stack); n != 0; n = len(stack) { + nd, stack = stack[n-1], stack[:n-1] + switch err := fn(nd); err { + case nil: + case errSkip: + continue Traverse + default: + return err + } + for name, nd := range nd.Child { + if name == "" { + // Node storing inactive watchpoints has empty name, skip it + // form traversing. Root node has also an empty name, but it + // never has a parent node. + continue + } + stack = append(stack, nd) + } + } + return nil +} + +func (nd node) WalkPath(name string, fn walkPathFunc) error { + i := indexbase(nd.Name, name) + if i == -1 { + return errnotexist(name) + } + ok := false + for j := indexSep(name[i:]); j != -1; j = indexSep(name[i:]) { + switch err := fn(nd, false); err { + case nil: + case errSkip: + return nil + default: + return err + } + if nd, ok = nd.Child[name[i:i+j]]; !ok { + return errnotexist(name[:i+j]) + } + i += j + 1 + } + switch err := fn(nd, false); err { + case nil: + case errSkip: + return nil + default: + return err + } + if nd, ok = nd.Child[name[i:]]; !ok { + return errnotexist(name) + } + switch err := fn(nd, true); err { + case nil, errSkip: + return nil + default: + return err + } +} + +type root struct { + nd node +} + +func (r root) addroot(name string) node { + if vol := filepath.VolumeName(name); vol != "" { + root, ok := r.nd.Child[vol] + if !ok { + root = r.nd.addchild(vol, vol) + } + return root + } + return r.nd +} + +func (r root) root(name string) (node, error) { + if vol := filepath.VolumeName(name); vol != "" { + nd, ok := r.nd.Child[vol] + if !ok { + return node{}, errnotexist(name) + } + return nd, nil + } + return r.nd, nil +} + +func (r root) Add(name string) node { + return r.addroot(name).Add(name) +} + +func (r root) AddDir(dir string, fn walkFunc) error { + return r.Add(dir).AddDir(fn) +} + +func (r root) Del(name string) error { + nd, err := r.root(name) + if err != nil { + return err + } + return nd.Del(name) +} + +func (r root) Get(name string) (node, error) { + nd, err := r.root(name) + if err != nil { + return node{}, err + } + if nd.Name != name { + if nd, err = nd.Get(name); err != nil { + return node{}, err + } + } + return nd, nil +} + +func (r root) Walk(name string, fn walkFunc) error { + nd, err := r.Get(name) + if err != nil { + return err + } + return nd.Walk(fn) +} + +func (r root) WalkPath(name string, fn walkPathFunc) error { + nd, err := r.root(name) + if err != nil { + return err + } + return nd.WalkPath(name, fn) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/notify.go b/Godeps/_workspace/src/github.com/zillode/notify/notify.go new file mode 100644 index 0000000..4ffca02 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/notify.go @@ -0,0 +1,70 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// BUG(rjeczalik): Notify does not collect watchpoints, when underlying watches +// were removed by their os-specific watcher implementations. Instead users are +// advised to listen on persistant paths to have guarantee they receive events +// for the whole lifetime of their applications (to discuss see #69). + +// BUG(ppknap): Linux (inotify) does not support watcher behavior masks like +// InOneshot, InOnlydir etc. Instead users are advised to perform the filtering +// themselves (to discuss see #71). + +// BUG(ppknap): Notify was not tested for short path name support under Windows +// (ReadDirectoryChangesW). + +// BUG(ppknap): Windows (ReadDirectoryChangesW) cannot recognize which notification +// triggers FileActionModified event. (to discuss see #75). + +package notify + +var defaultTree = newTree() + +// Watch sets up a watchpoint on path listening for events given by the events +// argument. +// +// File or directory given by the path must exist, otherwise Watch will fail +// with non-nil error. Notify resolves, for its internal purpose, any symlinks +// the provided path may contain, so it may fail if the symlinks form a cycle. +// It does so, since not all watcher implementations treat passed paths as-is. +// E.g. FSEvents reports a real path for every event, setting a watchpoint +// on /tmp will report events with paths rooted at /private/tmp etc. +// +// It is allowed to pass the same channel multiple times with different event +// list or different paths. Calling Watch with different event lists for a single +// watchpoint expands its event set. The only way to shrink it, is to call +// Stop on its channel. +// +// Calling Watch with empty event list does expand nor shrink watchpoint's event +// set. If c is the first channel to listen for events on the given path, Watch +// will seamlessly create a watch on the filesystem. +// +// Notify dispatches copies of single filesystem event to all channels registered +// for each path. If a single filesystem event contains multiple coalesced events, +// each of them is dispatched separately. E.g. the following filesystem change: +// +// ~ $ echo Hello > Notify.txt +// +// dispatches two events - notify.Create and notify.Write. However, it may depend +// on the underlying watcher implementation whether OS reports both of them. +// +// Windows and recursive watches +// +// If a directory which path was used to create recursive watch under Windows +// gets deleted, the OS will not report such event. It is advised to keep in +// mind this limitation while setting recursive watchpoints for your application, +// e.g. use persistant paths like %userprofile% or watch additionally parent +// directory of a recursive watchpoint in order to receive delete events for it. +func Watch(path string, c chan<- EventInfo, events ...Event) error { + return defaultTree.Watch(path, c, events...) +} + +// Stop removes all watchpoints registered for c. All underlying watches are +// also removed, for which c was the last channel listening for events. +// +// Stop does not close c. When Stop returns, it is guranteed that c will +// receive no more signals. +func Stop(c chan<- EventInfo) { + defaultTree.Stop(c) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/notify_inotify_test.go b/Godeps/_workspace/src/github.com/zillode/notify/notify_inotify_test.go new file mode 100644 index 0000000..c5984d3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/notify_inotify_test.go @@ -0,0 +1,37 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build linux + +package notify + +import "testing" + +func TestNotifySystemAndGlobalMix(t *testing.T) { + n := NewNotifyTest(t, "testdata/vfs.txt") + defer n.Close() + + ch := NewChans(2) + + n.Watch("src/github.com/rjeczalik/fs", ch[0], Create) + n.Watch("src/github.com/rjeczalik/fs", ch[1], InCreate) + + cases := []NCase{ + { + Event: icreate(n.W(), "src/github.com/rjeczalik/fs/.main.cc.swr"), + Receiver: Chans{ch[0], ch[1]}, + }, + } + + n.ExpectNotifyEvents(cases, ch) +} + +func TestUnknownEvent(t *testing.T) { + n := NewNotifyTest(t, "testdata/vfs.txt") + defer n.Close() + + ch := NewChans(1) + + n.WatchErr("src/github.com/rjeczalik/fs", ch[0], nil, inExclUnlink) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/notify_readdcw_test.go b/Godeps/_workspace/src/github.com/zillode/notify/notify_readdcw_test.go new file mode 100644 index 0000000..944b533 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/notify_readdcw_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build windows + +package notify + +import "testing" + +func TestNotifySystemSpecificEvent(t *testing.T) { + n := NewNotifyTest(t, "testdata/vfs.txt") + defer n.Close() + + ch := NewChans(1) + + n.Watch("src/github.com/rjeczalik/fs", ch[0], FileNotifyChangeFileName, FileNotifyChangeSize) + + cases := []NCase{ + { + Event: rremove(n.W(), "src/github.com/rjeczalik/fs/fs.go"), + Receiver: Chans{ch[0]}, + }, + { + Event: rwrite(n.W(), "src/github.com/rjeczalik/fs/README.md", []byte("XD")), + Receiver: Chans{ch[0]}, + }, + } + + n.ExpectNotifyEvents(cases, ch) +} + +func TestUnknownEvent(t *testing.T) { + n := NewNotifyTest(t, "testdata/vfs.txt") + defer n.Close() + + ch := NewChans(1) + + n.WatchErr("src/github.com/rjeczalik/fs", ch[0], nil, FileActionAdded) +} + +func TestNotifySystemAndGlobalMix(t *testing.T) { + n := NewNotifyTest(t, "testdata/vfs.txt") + defer n.Close() + + ch := NewChans(3) + + n.Watch("src/github.com/rjeczalik/fs", ch[0], Create) + n.Watch("src/github.com/rjeczalik/fs", ch[1], FileNotifyChangeFileName) + n.Watch("src/github.com/rjeczalik/fs", ch[2], FileNotifyChangeDirName) + + cases := []NCase{ + { + Event: rcreate(n.W(), "src/github.com/rjeczalik/fs/.main.cc.swr"), + Receiver: Chans{ch[0], ch[1]}, + }, + } + + n.ExpectNotifyEvents(cases, ch) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/notify_test.go b/Godeps/_workspace/src/github.com/zillode/notify/notify_test.go new file mode 100644 index 0000000..c8bc9d4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/notify_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build darwin linux freebsd dragonfly netbsd openbsd windows + +package notify + +import "testing" + +func TestNotifyExample(t *testing.T) { + n := NewNotifyTest(t, "testdata/vfs.txt") + defer n.Close() + + ch := NewChans(3) + + // Watch-points can be set explicitely via Watch/Stop calls... + n.Watch("src/github.com/rjeczalik/fs", ch[0], Write) + n.Watch("src/github.com/pblaszczyk/qttu", ch[0], Write) + n.Watch("src/github.com/pblaszczyk/qttu/...", ch[1], Create) + n.Watch("src/github.com/rjeczalik/fs/cmd/...", ch[2], Remove) + + cases := []NCase{ + // i=0 + { + Event: write(n.W(), "src/github.com/rjeczalik/fs/fs.go", []byte("XD")), + Receiver: Chans{ch[0]}, + }, + // TODO(rjeczalik): #62 + // i=1 + // { + // Event: write(n.W(), "src/github.com/pblaszczyk/qttu/README.md", []byte("XD")), + // Receiver: Chans{ch[0]}, + // }, + // i=2 + { + Event: write(n.W(), "src/github.com/rjeczalik/fs/cmd/gotree/go.go", []byte("XD")), + Receiver: nil, + }, + // i=3 + { + Event: create(n.W(), "src/github.com/pblaszczyk/qttu/src/.main.cc.swp"), + Receiver: Chans{ch[1]}, + }, + // i=4 + { + Event: create(n.W(), "src/github.com/pblaszczyk/qttu/src/.main.cc.swo"), + Receiver: Chans{ch[1]}, + }, + // i=5 + { + Event: remove(n.W(), "src/github.com/rjeczalik/fs/cmd/gotree/go.go"), + Receiver: Chans{ch[2]}, + }, + } + + n.ExpectNotifyEvents(cases, ch) + + // ...or using Call structures. + stops := [...]Call{ + // i=0 + { + F: FuncStop, + C: ch[0], + }, + // i=1 + { + F: FuncStop, + C: ch[1], + }, + } + + n.Call(stops[:]...) + + cases = []NCase{ + // i=0 + { + Event: write(n.W(), "src/github.com/rjeczalik/fs/fs.go", []byte("XD")), + Receiver: nil, + }, + // i=1 + { + Event: write(n.W(), "src/github.com/pblaszczyk/qttu/README.md", []byte("XD")), + Receiver: nil, + }, + // i=2 + { + Event: create(n.W(), "src/github.com/pblaszczyk/qttu/src/.main.cc.swr"), + Receiver: nil, + }, + // i=3 + { + Event: remove(n.W(), "src/github.com/rjeczalik/fs/cmd/gotree/main.go"), + Receiver: Chans{ch[2]}, + }, + } + + n.ExpectNotifyEvents(cases, ch) +} + +func TestStop(t *testing.T) { + t.Skip("TODO(rjeczalik)") +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/sync_readdcw_test.go b/Godeps/_workspace/src/github.com/zillode/notify/sync_readdcw_test.go new file mode 100644 index 0000000..1a7f830 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/sync_readdcw_test.go @@ -0,0 +1,33 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build windows + +package notify + +import ( + "syscall" + "time" + "unsafe" +) + +var modkernel32 = syscall.NewLazyDLL("kernel32.dll") +var procSetSystemFileCacheSize = modkernel32.NewProc("SetSystemFileCacheSize") +var zero = uintptr(1<<(unsafe.Sizeof(uintptr(0))*8) - 1) + +func Sync() { + // TODO(pknap): does not work without admin privilages, but I'm going + // to hack it. + // r, _, err := procSetSystemFileCacheSize.Call(none, none, 0) + // if r == 0 { + // dbgprint("SetSystemFileCacheSize error:", err) + // } +} + +// UpdateWait pauses the program for some minimal amount of time. This function +// is required only by implementations which work asynchronously. It gives +// watcher structure time to update its internal state. +func UpdateWait() { + time.Sleep(50 * time.Millisecond) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/sync_unix_test.go b/Godeps/_workspace/src/github.com/zillode/notify/sync_unix_test.go new file mode 100644 index 0000000..9804c6c --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/sync_unix_test.go @@ -0,0 +1,18 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build !windows + +package notify + +import "syscall" + +func Sync() { + syscall.Sync() +} + +// UpdateWait is required only by windows watcher implementation. On other +// platforms this function is no-op. +func UpdateWait() { +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/testdata/vfs.txt b/Godeps/_workspace/src/github.com/zillode/notify/testdata/vfs.txt new file mode 100644 index 0000000..88afbc6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/testdata/vfs.txt @@ -0,0 +1,56 @@ +src/github.com/pblaszczyk/qttu/.travis.yml +src/github.com/pblaszczyk/qttu/include/qttu/detail/registrator.hh +src/github.com/pblaszczyk/qttu/include/qttu/detail/registry.hh +src/github.com/pblaszczyk/qttu/include/qttu/runner.hh +src/github.com/pblaszczyk/qttu/LICENSE +src/github.com/pblaszczyk/qttu/qttu.pri +src/github.com/pblaszczyk/qttu/qttu.pro +src/github.com/pblaszczyk/qttu/README.md +src/github.com/pblaszczyk/qttu/src/main.cc +src/github.com/pblaszczyk/qttu/src/reg.cc +src/github.com/ppknap/link/.travis.yml +src/github.com/ppknap/link/include/coost/link/definitions.hpp +src/github.com/ppknap/link/include/coost/link/detail/bundle.hpp +src/github.com/ppknap/link/include/coost/link/detail/container_invoker.hpp +src/github.com/ppknap/link/include/coost/link/detail/container_value_trait.hpp +src/github.com/ppknap/link/include/coost/link/detail/dummy_type.hpp +src/github.com/ppknap/link/include/coost/link/detail/function_trait.hpp +src/github.com/ppknap/link/include/coost/link/detail/immediate_invoker.hpp +src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/always_same.hpp +src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/make_unique.hpp +src/github.com/ppknap/link/include/coost/link/detail/vertex.hpp +src/github.com/ppknap/link/include/coost/link/detail/wire.hpp +src/github.com/ppknap/link/include/coost/link/link.hpp +src/github.com/ppknap/link/include/coost/link.hpp +src/github.com/ppknap/link/Jamroot.jam +src/github.com/ppknap/link/LICENSE.md +src/github.com/ppknap/link/README.md +src/github.com/ppknap/link/test/counter_helper.hpp +src/github.com/ppknap/link/test/Jamfile.jam +src/github.com/ppknap/link/test/test_circular_calls.cpp +src/github.com/ppknap/link/test/test_container.cpp +src/github.com/ppknap/link/test/test_copy.cpp +src/github.com/ppknap/link/test/test_destructor.cpp +src/github.com/ppknap/link/test/test_immediate.cpp +src/github.com/ppknap/link/test/test_initialize.cpp +src/github.com/rjeczalik/fs/.travis.yml +src/github.com/rjeczalik/fs/appveyor.yml +src/github.com/rjeczalik/fs/cmd/gotree/go.go +src/github.com/rjeczalik/fs/cmd/gotree/main.go +src/github.com/rjeczalik/fs/cmd/mktree/main.go +src/github.com/rjeczalik/fs/fs.go +src/github.com/rjeczalik/fs/fsutil/fixture_test.go +src/github.com/rjeczalik/fs/fsutil/fsutil.go +src/github.com/rjeczalik/fs/fsutil/fsutil_test.go +src/github.com/rjeczalik/fs/fsutil/rel.go +src/github.com/rjeczalik/fs/fsutil/rel_test.go +src/github.com/rjeczalik/fs/fsutil/tee.go +src/github.com/rjeczalik/fs/fsutil/tee_test.go +src/github.com/rjeczalik/fs/LICENSE +src/github.com/rjeczalik/fs/memfs/memfs.go +src/github.com/rjeczalik/fs/memfs/memfs_test.go +src/github.com/rjeczalik/fs/memfs/tree.go +src/github.com/rjeczalik/fs/memfs/tree_test.go +src/github.com/rjeczalik/fs/memfs/util.go +src/github.com/rjeczalik/fs/memfs/util_test.go +src/github.com/rjeczalik/fs/README.md diff --git a/Godeps/_workspace/src/github.com/zillode/notify/testing_test.go b/Godeps/_workspace/src/github.com/zillode/notify/testing_test.go new file mode 100644 index 0000000..9ef41e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/testing_test.go @@ -0,0 +1,895 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "runtime" + "sort" + "strconv" + "strings" + "testing" + "time" +) + +// NOTE(rjeczalik): some useful environment variables: +// +// - NOTIFY_DEBUG gives some extra information about generated events +// - NOTIFY_TIMEOUT allows for changing default wait time for watcher's +// events +// - NOTIFY_TMP allows for changing location of temporary directory trees +// created for test purpose + +var wd = func() string { + s, err := os.Getwd() + if err != nil { + panic(err) + } + return s +}() + +func timeout() time.Duration { + if s := os.Getenv("NOTIFY_TIMEOUT"); s != "" { + if t, err := time.ParseDuration(s); err == nil { + return t + } + } + return 2 * time.Second +} + +func vfs() (string, string) { + if s := os.Getenv("NOTIFY_TMP"); s != "" { + return filepath.Split(s) + } + return "testdata", "" +} + +func isDir(path string) bool { + r := path[len(path)-1] + return r == '\\' || r == '/' +} + +func tmpcreateall(tmp string, path string) error { + isdir := isDir(path) + path = filepath.Join(tmp, filepath.FromSlash(path)) + if isdir { + if err := os.MkdirAll(path, 0755); err != nil { + return err + } + } else { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + f, err := os.Create(path) + if err != nil { + return err + } + if err := nonil(f.Sync(), f.Close()); err != nil { + return err + } + } + return nil +} + +func tmpcreate(root, path string) (bool, error) { + isdir := isDir(path) + path = filepath.Join(root, filepath.FromSlash(path)) + if isdir { + if err := os.Mkdir(path, 0755); err != nil { + return false, err + } + } else { + f, err := os.Create(path) + if err != nil { + return false, err + } + if err := nonil(f.Sync(), f.Close()); err != nil { + return false, err + } + } + return isdir, nil +} + +func tmptree(root, list string) (string, error) { + f, err := os.Open(list) + if err != nil { + return "", err + } + defer f.Close() + if root == "" { + if root, err = ioutil.TempDir(vfs()); err != nil { + return "", err + } + } + scanner := bufio.NewScanner(f) + for scanner.Scan() { + if err := tmpcreateall(root, scanner.Text()); err != nil { + return "", err + } + } + if err := scanner.Err(); err != nil { + return "", err + } + return root, nil +} + +func callern(n int) string { + _, file, line, ok := runtime.Caller(n) + if !ok { + return "" + } + return filepath.Base(file) + ":" + strconv.Itoa(line) +} + +func caller() string { + return callern(3) +} + +type WCase struct { + Action func() + Events []EventInfo +} + +func (cas WCase) String() string { + s := make([]string, 0, len(cas.Events)) + for _, ei := range cas.Events { + s = append(s, "Event("+ei.Event().String()+")@"+filepath.FromSlash(ei.Path())) + } + return strings.Join(s, ", ") +} + +type W struct { + Watcher watcher + C chan EventInfo + Timeout time.Duration + + t *testing.T + root string +} + +func newWatcherTest(t *testing.T, tree string) *W { + root, err := tmptree("", filepath.FromSlash(tree)) + if err != nil { + t.Fatalf(`tmptree("", %q)=%v`, tree, err) + } + Sync() + return &W{ + t: t, + root: root, + } +} + +func NewWatcherTest(t *testing.T, tree string, events ...Event) *W { + w := newWatcherTest(t, tree) + if len(events) == 0 { + events = []Event{Create, Remove, Write, Rename} + } + if rw, ok := w.watcher().(recursiveWatcher); ok { + if err := rw.RecursiveWatch(w.root, joinevents(events)); err != nil { + t.Fatalf("RecursiveWatch(%q, All)=%v", w.root, err) + } + } else { + fn := func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if fi.IsDir() { + if err := w.watcher().Watch(path, joinevents(events)); err != nil { + return err + } + } + return nil + } + if err := filepath.Walk(w.root, fn); err != nil { + t.Fatalf("Walk(%q, fn)=%v", w.root, err) + } + } + drainall(w.C) + return w +} + +func (w *W) clean(path string) string { + path, isrec, err := cleanpath(filepath.Join(w.root, path)) + if err != nil { + w.Fatalf("cleanpath(%q)=%v", path, err) + } + if isrec { + path = path + "..." + } + return path +} + +func (w *W) Fatal(v interface{}) { + w.t.Fatalf("%s: %v", caller(), v) +} + +func (w *W) Fatalf(format string, v ...interface{}) { + w.t.Fatalf("%s: %s", caller(), fmt.Sprintf(format, v...)) +} + +func (w *W) Watch(path string, e Event) { + if err := w.watcher().Watch(w.clean(path), e); err != nil { + w.Fatalf("Watch(%s, %v)=%v", path, e, err) + } +} + +func (w *W) Rewatch(path string, olde, newe Event) { + if err := w.watcher().Rewatch(w.clean(path), olde, newe); err != nil { + w.Fatalf("Rewatch(%s, %v, %v)=%v", path, olde, newe, err) + } +} + +func (w *W) Unwatch(path string) { + if err := w.watcher().Unwatch(w.clean(path)); err != nil { + w.Fatalf("Unwatch(%s)=%v", path, err) + } +} + +func (w *W) RecursiveWatch(path string, e Event) { + rw, ok := w.watcher().(recursiveWatcher) + if !ok { + w.Fatal("watcher does not implement recursive watching on this platform") + } + if err := rw.RecursiveWatch(w.clean(path), e); err != nil { + w.Fatalf("RecursiveWatch(%s, %v)=%v", path, e, err) + } +} + +func (w *W) RecursiveRewatch(oldp, newp string, olde, newe Event) { + rw, ok := w.watcher().(recursiveWatcher) + if !ok { + w.Fatal("watcher does not implement recursive watching on this platform") + } + if err := rw.RecursiveRewatch(w.clean(oldp), w.clean(newp), olde, newe); err != nil { + w.Fatalf("RecursiveRewatch(%s, %s, %v, %v)=%v", oldp, newp, olde, newe, err) + } +} + +func (w *W) RecursiveUnwatch(path string) { + rw, ok := w.watcher().(recursiveWatcher) + if !ok { + w.Fatal("watcher does not implement recursive watching on this platform") + } + if err := rw.RecursiveUnwatch(w.clean(path)); err != nil { + w.Fatalf("RecursiveUnwatch(%s)=%v", path, err) + } +} + +func (w *W) initwatcher(buffer int) { + c := make(chan EventInfo, buffer) + w.Watcher = newWatcher(c) + w.C = c +} + +func (w *W) watcher() watcher { + if w.Watcher == nil { + w.initwatcher(512) + } + return w.Watcher +} + +func (w *W) c() chan EventInfo { + if w.C == nil { + w.initwatcher(512) + } + return w.C +} + +func (w *W) timeout() time.Duration { + if w.Timeout != 0 { + return w.Timeout + } + return timeout() +} + +func (w *W) Close() error { + defer os.RemoveAll(w.root) + if err := w.watcher().Close(); err != nil { + w.Fatalf("w.Watcher.Close()=%v", err) + } + return nil +} + +func EqualEventInfo(want, got EventInfo) error { + if got.Event() != want.Event() { + return fmt.Errorf("want Event()=%v; got %v (path=%s)", want.Event(), + got.Event(), want.Path()) + } + path := strings.TrimRight(filepath.FromSlash(want.Path()), `/\`) + if !strings.HasSuffix(got.Path(), path) { + return fmt.Errorf("want Path()=%s; got %s (event=%v)", path, got.Path(), + want.Event()) + } + return nil +} + +func EqualCall(want, got Call) error { + if want.F != got.F { + return fmt.Errorf("want F=%v; got %v (want.P=%q, got.P=%q)", want.F, got.F, want.P, got.P) + } + if got.E != want.E { + return fmt.Errorf("want E=%v; got %v (want.P=%q, got.P=%q)", want.E, got.E, want.P, got.P) + } + if got.NE != want.NE { + return fmt.Errorf("want NE=%v; got %v (want.P=%q, got.P=%q)", want.NE, got.NE, want.P, got.P) + } + if want.C != got.C { + return fmt.Errorf("want C=%p; got %p (want.P=%q, got.P=%q)", want.C, got.C, want.P, got.P) + } + if want := filepath.FromSlash(want.P); !strings.HasSuffix(got.P, want) { + return fmt.Errorf("want P=%s; got %s", want, got.P) + } + if want := filepath.FromSlash(want.NP); !strings.HasSuffix(got.NP, want) { + return fmt.Errorf("want NP=%s; got %s", want, got.NP) + } + return nil +} + +func create(w *W, path string) WCase { + return WCase{ + Action: func() { + isdir, err := tmpcreate(w.root, filepath.FromSlash(path)) + if err != nil { + w.Fatalf("tmpcreate(%q, %q)=%v", w.root, path, err) + } + if isdir { + dbgprintf("[FS] os.Mkdir(%q)\n", path) + } else { + dbgprintf("[FS] os.Create(%q)\n", path) + } + }, + Events: []EventInfo{ + &Call{P: path, E: Create}, + }, + } +} + +func remove(w *W, path string) WCase { + return WCase{ + Action: func() { + if err := os.RemoveAll(filepath.Join(w.root, filepath.FromSlash(path))); err != nil { + w.Fatal(err) + } + dbgprintf("[FS] os.Remove(%q)\n", path) + }, + Events: []EventInfo{ + &Call{P: path, E: Remove}, + }, + } +} + +func rename(w *W, oldpath, newpath string) WCase { + return WCase{ + Action: func() { + err := os.Rename(filepath.Join(w.root, filepath.FromSlash(oldpath)), + filepath.Join(w.root, filepath.FromSlash(newpath))) + if err != nil { + w.Fatal(err) + } + dbgprintf("[FS] os.Rename(%q, %q)\n", oldpath, newpath) + }, + Events: []EventInfo{ + &Call{P: newpath, E: Rename}, + }, + } +} + +func write(w *W, path string, p []byte) WCase { + return WCase{ + Action: func() { + f, err := os.OpenFile(filepath.Join(w.root, filepath.FromSlash(path)), + os.O_WRONLY, 0644) + if err != nil { + w.Fatalf("OpenFile(%q)=%v", path, err) + } + if _, err := f.Write(p); err != nil { + w.Fatalf("Write(%q)=%v", path, err) + } + if err := nonil(f.Sync(), f.Close()); err != nil { + w.Fatalf("Sync(%q)/Close(%q)=%v", path, path, err) + } + dbgprintf("[FS] Write(%q)\n", path) + }, + Events: []EventInfo{ + &Call{P: path, E: Write}, + }, + } +} + +func drainall(c chan EventInfo) (ei []EventInfo) { + time.Sleep(50 * time.Millisecond) + for { + select { + case e := <-c: + ei = append(ei, e) + runtime.Gosched() + default: + return + } + } +} + +type WCaseFunc func(i int, cas WCase, ei EventInfo) error + +func (w *W) ExpectAnyFunc(cases []WCase, fn WCaseFunc) { + UpdateWait() // Wait some time before starting the test. +Test: + for i, cas := range cases { + dbgprintf("ExpectAny: i=%d\n", i) + cas.Action() + Sync() + switch cas.Events { + case nil: + if ei := drainall(w.C); len(ei) != 0 { + w.Fatalf("unexpected dangling events: %v (i=%d)", ei, i) + } + default: + select { + case ei := <-w.C: + dbgprintf("received: path=%q, event=%v, sys=%v (i=%d)", ei.Path(), + ei.Event(), ei.Sys(), i) + for j, want := range cas.Events { + if err := EqualEventInfo(want, ei); err != nil { + dbgprint(err, j) + continue + } + if fn != nil { + if err := fn(i, cas, ei); err != nil { + w.Fatalf("ExpectAnyFunc(%d, %v)=%v", i, ei, err) + } + } + drainall(w.C) // TODO(rjeczalik): revisit + continue Test + } + w.Fatalf("ExpectAny received an event which does not match any of "+ + "the expected ones (i=%d): want one of %v; got %v", i, cas.Events, ei) + case <-time.After(w.timeout()): + w.Fatalf("timed out after %v waiting for one of %v (i=%d)", w.timeout(), + cas.Events, i) + } + drainall(w.C) // TODO(rjeczalik): revisit + } + } +} + +func (w *W) ExpectAny(cases []WCase) { + w.ExpectAnyFunc(cases, nil) +} + +// FuncType represents enums for Watcher interface. +type FuncType string + +const ( + FuncWatch = FuncType("Watch") + FuncUnwatch = FuncType("Unwatch") + FuncRewatch = FuncType("Rewatch") + FuncRecursiveWatch = FuncType("RecursiveWatch") + FuncRecursiveUnwatch = FuncType("RecursiveUnwatch") + FuncRecursiveRewatch = FuncType("RecursiveRewatch") + FuncStop = FuncType("Stop") +) + +type Chans []chan EventInfo + +func NewChans(n int) Chans { + ch := make([]chan EventInfo, n) + for i := range ch { + ch[i] = make(chan EventInfo, buffer) + } + return ch +} + +func (c Chans) Foreach(fn func(chan<- EventInfo, node)) { + for i, ch := range c { + fn(ch, node{Name: strconv.Itoa(i)}) + } +} + +func (c Chans) Drain() (ei []EventInfo) { + n := len(c) + stop := make(chan struct{}) + eich := make(chan EventInfo, n*buffer) + go func() { + defer close(eich) + cases := make([]reflect.SelectCase, n+1) + for i := range c { + cases[i].Chan = reflect.ValueOf(c[i]) + cases[i].Dir = reflect.SelectRecv + } + cases[n].Chan = reflect.ValueOf(stop) + cases[n].Dir = reflect.SelectRecv + for { + i, v, ok := reflect.Select(cases) + if i == n { + return + } + if !ok { + panic("(Chans).Drain(): unexpected chan close") + } + eich <- v.Interface().(EventInfo) + } + }() + <-time.After(50 * time.Duration(n) * time.Millisecond) + close(stop) + for e := range eich { + ei = append(ei, e) + } + return +} + +// Call represents single call to Watcher issued by the Tree +// and recorded by a spy Watcher mock. +type Call struct { + F FuncType // denotes type of function to call, for both watcher and notifier interface + C chan EventInfo // user channel being an argument to either Watch or Stop function + P string // regular Path argument and old path from RecursiveRewatch call + NP string // new Path argument from RecursiveRewatch call + E Event // regular Event argument and old Event from a Rewatch call + NE Event // new Event argument from Rewatch call + S interface{} // when Call is used as EventInfo, S is a value of Sys() + Dir bool // when Call is used as EventInfo, Dir is a value of isDir() +} + +// Call implements the EventInfo interface. +func (c *Call) Event() Event { return c.E } +func (c *Call) Path() string { return c.P } +func (c *Call) String() string { return fmt.Sprintf("%#v", c) } +func (c *Call) Sys() interface{} { return c.S } +func (c *Call) isDir() (bool, error) { return c.Dir, nil } + +// CallSlice is a conveniance wrapper for a slice of Call values, which allows +// to sort them in ascending order. +type CallSlice []Call + +// CallSlice implements sort.Interface inteface. +func (cs CallSlice) Len() int { return len(cs) } +func (cs CallSlice) Less(i, j int) bool { return cs[i].P < cs[j].P } +func (cs CallSlice) Swap(i, j int) { cs[i], cs[j] = cs[j], cs[i] } +func (cs CallSlice) Sort() { sort.Sort(cs) } + +// Spy is a mock for Watcher interface, which records every call. +type Spy []Call + +func (s *Spy) Close() (_ error) { return } + +func (s *Spy) Watch(p string, e Event) (_ error) { + dbgprintf("%s: (*Spy).Watch(%q, %v)", caller(), p, e) + *s = append(*s, Call{F: FuncWatch, P: p, E: e}) + return +} + +func (s *Spy) Unwatch(p string) (_ error) { + dbgprintf("%s: (*Spy).Unwatch(%q)", caller(), p) + *s = append(*s, Call{F: FuncUnwatch, P: p}) + return +} + +func (s *Spy) Rewatch(p string, olde, newe Event) (_ error) { + dbgprintf("%s: (*Spy).Rewatch(%q, %v, %v)", caller(), p, olde, newe) + *s = append(*s, Call{F: FuncRewatch, P: p, E: olde, NE: newe}) + return +} + +func (s *Spy) RecursiveWatch(p string, e Event) (_ error) { + dbgprintf("%s: (*Spy).RecursiveWatch(%q, %v)", caller(), p, e) + *s = append(*s, Call{F: FuncRecursiveWatch, P: p, E: e}) + return +} + +func (s *Spy) RecursiveUnwatch(p string) (_ error) { + dbgprintf("%s: (*Spy).RecursiveUnwatch(%q)", caller(), p) + *s = append(*s, Call{F: FuncRecursiveUnwatch, P: p}) + return +} + +func (s *Spy) RecursiveRewatch(oldp, newp string, olde, newe Event) (_ error) { + dbgprintf("%s: (*Spy).RecursiveRewatch(%q, %q, %v, %v)", caller(), oldp, newp, olde, newe) + *s = append(*s, Call{F: FuncRecursiveRewatch, P: oldp, NP: newp, E: olde, NE: newe}) + return +} + +type RCase struct { + Call Call + Record []Call +} + +type TCase struct { + Event Call + Receiver Chans +} + +type NCase struct { + Event WCase + Receiver Chans +} + +type N struct { + Timeout time.Duration + + t *testing.T + tree tree + w *W + spy *Spy + c chan EventInfo + j int // spy offset + + realroot string +} + +func newN(t *testing.T, tree string) *N { + n := &N{ + t: t, + w: newWatcherTest(t, tree), + } + realroot, err := canonical(n.w.root) + if err != nil { + t.Fatalf("%s: unexpected fixture failure: %v", caller(), err) + } + n.realroot = realroot + return n +} + +func newTreeN(t *testing.T, tree string) *N { + c := make(chan EventInfo, buffer) + n := newN(t, tree) + n.spy = &Spy{} + n.w.Watcher = n.spy + n.w.C = c + n.c = c + return n +} + +func NewNotifyTest(t *testing.T, tree string) *N { + n := newN(t, tree) + if rw, ok := n.w.watcher().(recursiveWatcher); ok { + n.tree = newRecursiveTree(rw, n.w.c()) + } else { + n.tree = newNonrecursiveTree(n.w.watcher(), n.w.c(), nil) + } + return n +} + +func NewRecursiveTreeTest(t *testing.T, tree string) *N { + n := newTreeN(t, tree) + n.tree = newRecursiveTree(n.spy, n.c) + return n +} + +func NewNonrecursiveTreeTest(t *testing.T, tree string) *N { + n := newTreeN(t, tree) + n.tree = newNonrecursiveTree(n.spy, n.c, nil) + return n +} + +func NewNonrecursiveTreeTestC(t *testing.T, tree string) (*N, chan EventInfo) { + rec := make(chan EventInfo, buffer) + recinternal := make(chan EventInfo, buffer) + recuser := make(chan EventInfo, buffer) + go func() { + for ei := range rec { + select { + case recinternal <- ei: + default: + t.Fatalf("failed to send ei to recinternal: not ready") + } + select { + case recuser <- ei: + default: + t.Fatalf("failed to send ei to recuser: not ready") + } + } + }() + n := newTreeN(t, tree) + tr := newNonrecursiveTree(n.spy, n.c, recinternal) + tr.rec = rec + n.tree = tr + return n, recuser +} + +func (n *N) timeout() time.Duration { + if n.Timeout != 0 { + return n.Timeout + } + return n.w.timeout() +} + +func (n *N) W() *W { + return n.w +} + +func (n *N) Close() error { + defer os.RemoveAll(n.w.root) + if err := n.tree.Close(); err != nil { + n.w.Fatalf("(notifier).Close()=%v", err) + } + return nil +} + +func (n *N) Watch(path string, c chan<- EventInfo, events ...Event) { + UpdateWait() // we need to wait on Windows because of its asynchronous watcher. + path = filepath.Join(n.w.root, path) + if err := n.tree.Watch(path, c, events...); err != nil { + n.t.Errorf("Watch(%s, %p, %v)=%v", path, c, events, err) + } +} + +func (n *N) WatchErr(path string, c chan<- EventInfo, err error, events ...Event) { + path = filepath.Join(n.w.root, path) + switch e := n.tree.Watch(path, c, events...); { + case err == nil && e == nil: + n.t.Errorf("Watch(%s, %p, %v)=nil", path, c, events) + case err != nil && e != err: + n.t.Errorf("Watch(%s, %p, %v)=%v != %v", path, c, events, e, err) + } +} + +func (n *N) Stop(c chan<- EventInfo) { + n.tree.Stop(c) +} + +func (n *N) Call(calls ...Call) { + for i := range calls { + switch calls[i].F { + case FuncWatch: + n.Watch(calls[i].P, calls[i].C, calls[i].E) + case FuncStop: + n.Stop(calls[i].C) + default: + panic("unsupported call type: " + string(calls[i].F)) + } + } +} + +func (n *N) expectDry(ch Chans, i int) { + if ei := ch.Drain(); len(ei) != 0 { + n.w.Fatalf("unexpected dangling events: %v (i=%d)", ei, i) + } +} + +func (n *N) ExpectRecordedCalls(cases []RCase) { + for i, cas := range cases { + dbgprintf("ExpectRecordedCalls: i=%d\n", i) + n.Call(cas.Call) + record := (*n.spy)[n.j:] + if len(cas.Record) == 0 && len(record) == 0 { + continue + } + n.j = len(*n.spy) + if len(record) != len(cas.Record) { + n.t.Fatalf("%s: want len(record)=%d; got %d [%+v] (i=%d)", caller(), + len(cas.Record), len(record), record, i) + } + CallSlice(record).Sort() + for j := range cas.Record { + if err := EqualCall(cas.Record[j], record[j]); err != nil { + n.t.Fatalf("%s: %v (i=%d, j=%d)", caller(), err, i, j) + } + } + } +} + +func (n *N) collect(ch Chans) <-chan []EventInfo { + done := make(chan []EventInfo) + go func() { + cases := make([]reflect.SelectCase, len(ch)) + unique := make(map[<-chan EventInfo]EventInfo, len(ch)) + for i := range ch { + cases[i].Chan = reflect.ValueOf(ch[i]) + cases[i].Dir = reflect.SelectRecv + } + for i := len(cases); i != 0; i = len(cases) { + j, v, ok := reflect.Select(cases) + if !ok { + n.t.Fatal("unexpected chan close") + } + ch := cases[j].Chan.Interface().(chan EventInfo) + got := v.Interface().(EventInfo) + if ei, ok := unique[ch]; ok { + n.t.Fatalf("duplicated event %v (previous=%v) received on collect", got, ei) + } + unique[ch] = got + cases[j], cases = cases[i-1], cases[:i-1] + } + collected := make([]EventInfo, 0, len(ch)) + for _, ch := range unique { + collected = append(collected, ch) + } + done <- collected + }() + return done +} + +func (n *N) abs(rel Call) *Call { + rel.P = filepath.Join(n.realroot, filepath.FromSlash(rel.P)) + if !filepath.IsAbs(rel.P) { + rel.P = filepath.Join(wd, rel.P) + } + return &rel +} + +func (n *N) ExpectTreeEvents(cases []TCase, all Chans) { + for i, cas := range cases { + dbgprintf("ExpectTreeEvents: i=%d\n", i) + // Ensure there're no dangling event left by previous test-case. + n.expectDry(all, i) + n.c <- n.abs(cas.Event) + switch cas.Receiver { + case nil: + n.expectDry(all, i) + default: + ch := n.collect(cas.Receiver) + select { + case collected := <-ch: + for _, got := range collected { + if err := EqualEventInfo(&cas.Event, got); err != nil { + n.w.Fatalf("%s: %s (i=%d)", caller(), err, i) + } + } + case <-time.After(n.timeout()): + n.w.Fatalf("ExpectTreeEvents has timed out after %v waiting for"+ + " %v on %s (i=%d)", n.timeout(), cas.Event.E, cas.Event.P, i) + } + + } + } + n.expectDry(all, -1) +} + +func (n *N) ExpectNotifyEvents(cases []NCase, all Chans) { + UpdateWait() // Wait some time before starting the test. + for i, cas := range cases { + dbgprintf("ExpectNotifyEvents: i=%d\n", i) + cas.Event.Action() + Sync() + switch cas.Receiver { + case nil: + n.expectDry(all, i) + default: + ch := n.collect(cas.Receiver) + select { + case collected := <-ch: + Compare: + for j, ei := range collected { + dbgprintf("received: path=%q, event=%v, sys=%v (i=%d, j=%d)", ei.Path(), + ei.Event(), ei.Sys(), i, j) + for _, want := range cas.Event.Events { + if err := EqualEventInfo(want, ei); err != nil { + dbgprint(err, j) + continue + } + continue Compare + } + n.w.Fatalf("ExpectNotifyEvents received an event which does not"+ + " match any of the expected ones (i=%d): want one of %v; got %v", i, + cas.Event.Events, ei) + } + case <-time.After(n.timeout()): + n.w.Fatalf("ExpectNotifyEvents did not receive any of the expected events [%v] "+ + "after %v (i=%d)", cas.Event, n.timeout(), i) + } + } + } + n.expectDry(all, -1) +} + +func (n *N) Walk(fn walkFunc) { + switch t := n.tree.(type) { + case *recursiveTree: + if err := t.root.Walk("", fn); err != nil { + n.w.Fatal(err) + } + case *nonrecursiveTree: + if err := t.root.Walk("", fn); err != nil { + n.w.Fatal(err) + } + default: + n.t.Fatal("unknown tree type") + } +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/tree.go b/Godeps/_workspace/src/github.com/zillode/notify/tree.go new file mode 100644 index 0000000..cd6afd6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/tree.go @@ -0,0 +1,22 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +const buffer = 128 + +type tree interface { + Watch(string, chan<- EventInfo, ...Event) error + Stop(chan<- EventInfo) + Close() error +} + +func newTree() tree { + c := make(chan EventInfo, buffer) + w := newWatcher(c) + if rw, ok := w.(recursiveWatcher); ok { + return newRecursiveTree(rw, c) + } + return newNonrecursiveTree(w, c, make(chan EventInfo, buffer)) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/tree_nonrecursive.go b/Godeps/_workspace/src/github.com/zillode/notify/tree_nonrecursive.go new file mode 100644 index 0000000..788a17b --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/tree_nonrecursive.go @@ -0,0 +1,292 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +import "sync" + +// nonrecursiveTree TODO(rjeczalik) +type nonrecursiveTree struct { + rw sync.RWMutex // protects root + root root + w watcher + c chan EventInfo + rec chan EventInfo +} + +// newNonrecursiveTree TODO(rjeczalik) +func newNonrecursiveTree(w watcher, c, rec chan EventInfo) *nonrecursiveTree { + if rec == nil { + rec = make(chan EventInfo, buffer) + } + t := &nonrecursiveTree{ + root: root{nd: newnode("")}, + w: w, + c: c, + rec: rec, + } + go t.dispatch(c) + go t.internal(rec) + return t +} + +// dispatch TODO(rjeczalik) +func (t *nonrecursiveTree) dispatch(c <-chan EventInfo) { + for ei := range c { + dbgprintf("dispatching %v on %q", ei.Event(), ei.Path()) + go func(ei EventInfo) { + var nd node + var isrec bool + dir, base := split(ei.Path()) + fn := func(it node, isbase bool) error { + isrec = isrec || it.Watch.IsRecursive() + if isbase { + nd = it + } else { + it.Watch.Dispatch(ei, recursive) + } + return nil + } + t.rw.RLock() + // Notify recursive watchpoints found on the path. + if err := t.root.WalkPath(dir, fn); err != nil { + dbgprint("dispatch did not reach leaf:", err) + t.rw.RUnlock() + return + } + // Notify parent watchpoint. + nd.Watch.Dispatch(ei, 0) + isrec = isrec || nd.Watch.IsRecursive() + // If leaf watchpoint exists, notify it. + if nd, ok := nd.Child[base]; ok { + isrec = isrec || nd.Watch.IsRecursive() + nd.Watch.Dispatch(ei, 0) + } + t.rw.RUnlock() + // If the event describes newly leaf directory created within + if !isrec || ei.Event() != Create { + return + } + if ok, err := ei.(isDirer).isDir(); !ok || err != nil { + return + } + t.rec <- ei + }(ei) + } +} + +// internal TODO(rjeczalik) +func (t *nonrecursiveTree) internal(rec <-chan EventInfo) { + for ei := range rec { + var nd node + var eset = internal + t.rw.Lock() + t.root.WalkPath(ei.Path(), func(it node, _ bool) error { + if e := it.Watch[t.rec]; e != 0 && e > eset { + eset = e + } + nd = it + return nil + }) + if eset == internal { + t.rw.Unlock() + continue + } + err := nd.Add(ei.Path()).AddDir(t.recFunc(eset)) + t.rw.Unlock() + if err != nil { + dbgprintf("internal(%p) error: %v", rec, err) + } + } +} + +// watchAdd TODO(rjeczalik) +func (t *nonrecursiveTree) watchAdd(nd node, c chan<- EventInfo, e Event) eventDiff { + if e&recursive != 0 { + diff := nd.Watch.Add(t.rec, e|Create|omit) + nd.Watch.Add(c, e) + return diff + } + return nd.Watch.Add(c, e) +} + +// watchDelMin TODO(rjeczalik) +func (t *nonrecursiveTree) watchDelMin(min Event, nd node, c chan<- EventInfo, e Event) eventDiff { + old, ok := nd.Watch[t.rec] + if ok { + nd.Watch[t.rec] = min + } + diff := nd.Watch.Del(c, e) + if ok { + switch old &^= diff[0] &^ diff[1]; { + case old|internal == internal: + delete(nd.Watch, t.rec) + if set, ok := nd.Watch[nil]; ok && len(nd.Watch) == 1 && set == 0 { + delete(nd.Watch, nil) + } + default: + nd.Watch.Add(t.rec, old|Create) + switch { + case diff == none: + case diff[1]|Create == diff[0]: + diff = none + default: + diff[1] |= Create + } + } + } + return diff +} + +// watchDel TODO(rjeczalik) +func (t *nonrecursiveTree) watchDel(nd node, c chan<- EventInfo, e Event) eventDiff { + return t.watchDelMin(0, nd, c, e) +} + +// Watch TODO(rjeczalik) +func (t *nonrecursiveTree) Watch(path string, c chan<- EventInfo, events ...Event) error { + if c == nil { + panic("notify: Watch using nil channel") + } + // Expanding with empty event set is a nop. + if len(events) == 0 { + return nil + } + path, isrec, err := cleanpath(path) + if err != nil { + return err + } + eset := joinevents(events) + t.rw.Lock() + defer t.rw.Unlock() + nd := t.root.Add(path) + if isrec { + return t.watchrec(nd, c, eset|recursive) + } + return t.watch(nd, c, eset) +} + +func (t *nonrecursiveTree) watch(nd node, c chan<- EventInfo, e Event) (err error) { + diff := nd.Watch.Add(c, e) + switch { + case diff == none: + return nil + case diff[1] == 0: + // TODO(rjeczalik): cleanup this panic after implementation is stable + panic("eset is empty: " + nd.Name) + case diff[0] == 0: + err = t.w.Watch(nd.Name, diff[1]) + default: + err = t.w.Rewatch(nd.Name, diff[0], diff[1]) + } + if err != nil { + nd.Watch.Del(c, diff.Event()) + return err + } + return nil +} + +func (t *nonrecursiveTree) recFunc(e Event) walkFunc { + return func(nd node) (err error) { + switch diff := nd.Watch.Add(t.rec, e|omit|Create); { + case diff == none: + case diff[1] == 0: + // TODO(rjeczalik): cleanup this panic after implementation is stable + panic("eset is empty: " + nd.Name) + case diff[0] == 0: + err = t.w.Watch(nd.Name, diff[1]) + default: + err = t.w.Rewatch(nd.Name, diff[0], diff[1]) + } + return + } +} + +func (t *nonrecursiveTree) watchrec(nd node, c chan<- EventInfo, e Event) error { + var traverse func(walkFunc) error + // Non-recursive tree listens on Create event for every recursive + // watchpoint in order to automagically set a watch for every + // created directory. + switch diff := nd.Watch.dryAdd(t.rec, e|Create); { + case diff == none: + t.watchAdd(nd, c, e) + nd.Watch.Add(t.rec, e|omit|Create) + return nil + case diff[1] == 0: + // TODO(rjeczalik): cleanup this panic after implementation is stable + panic("eset is empty: " + nd.Name) + case diff[0] == 0: + // TODO(rjeczalik): BFS into directories and skip subtree as soon as first + // recursive watchpoint is encountered. + traverse = nd.AddDir + default: + traverse = nd.Walk + } + // TODO(rjeczalik): account every path that failed to be (re)watched + // and retry. + if err := traverse(t.recFunc(e)); err != nil { + return err + } + t.watchAdd(nd, c, e) + return nil +} + +type walkWatchpointFunc func(Event, node) error + +func (t *nonrecursiveTree) walkWatchpoint(nd node, fn walkWatchpointFunc) error { + type minode struct { + min Event + nd node + } + mnd := minode{nd: nd} + stack := []minode{mnd} +Traverse: + for n := len(stack); n != 0; n = len(stack) { + mnd, stack = stack[n-1], stack[:n-1] + // There must be no recursive watchpoints if the node has no watchpoints + // itself (every node in subtree rooted at recursive watchpoints must + // have at least nil (total) and t.rec watchpoints). + if len(mnd.nd.Watch) != 0 { + switch err := fn(mnd.min, mnd.nd); err { + case nil: + case errSkip: + continue Traverse + default: + return err + } + } + for _, nd := range mnd.nd.Child { + stack = append(stack, minode{mnd.nd.Watch[t.rec], nd}) + } + } + return nil +} + +// Stop TODO(rjeczalik) +func (t *nonrecursiveTree) Stop(c chan<- EventInfo) { + fn := func(min Event, nd node) error { + // TODO(rjeczalik): aggregate watcher errors and retry; in worst case + // forward to the user. + switch diff := t.watchDelMin(min, nd, c, all); { + case diff == none: + return nil + case diff[1] == 0: + t.w.Unwatch(nd.Name) + default: + t.w.Rewatch(nd.Name, diff[0], diff[1]) + } + return nil + } + t.rw.Lock() + err := t.walkWatchpoint(t.root.nd, fn) // TODO(rjeczalik): store max root per c + t.rw.Unlock() + dbgprintf("Stop(%p) error: %v\n", c, err) +} + +// Close TODO(rjeczalik) +func (t *nonrecursiveTree) Close() error { + err := t.w.Close() + close(t.c) + return err +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/tree_nonrecursive_test.go b/Godeps/_workspace/src/github.com/zillode/notify/tree_nonrecursive_test.go new file mode 100644 index 0000000..37625bf --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/tree_nonrecursive_test.go @@ -0,0 +1,543 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +import ( + "fmt" + "testing" +) + +func TestNonrecursiveTree(t *testing.T) { + n := NewNonrecursiveTreeTest(t, "testdata/vfs.txt") + defer n.Close() + + ch := NewChans(5) + + watches := [...]RCase{ + // i=0 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/fs.go", + C: ch[0], + E: Rename, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/fs.go", + E: Rename, + }, + }, + }, + // i=1 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd/...", + C: ch[1], + E: Remove, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd", + E: Create | Remove, + }, + { + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd/gotree", + E: Create | Remove, + }, + { + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd/mktree", + E: Create | Remove, + }, + }, + }, + // i=2 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd/...", + C: ch[2], + E: Rename, + }, + Record: []Call{ + { + F: FuncRewatch, + P: "src/github.com/rjeczalik/fs/cmd", + E: Create | Remove, + NE: Create | Remove | Rename, + }, + { + F: FuncRewatch, + P: "src/github.com/rjeczalik/fs/cmd/gotree", + E: Create | Remove, + NE: Create | Remove | Rename, + }, + { + F: FuncRewatch, + P: "src/github.com/rjeczalik/fs/cmd/mktree", + E: Create | Remove, + NE: Create | Remove | Rename, + }, + }, + }, + // i=3 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd/mktree/...", + C: ch[2], + E: Write, + }, + Record: []Call{ + { + F: FuncRewatch, + P: "src/github.com/rjeczalik/fs/cmd/mktree", + E: Create | Remove | Rename, + NE: Create | Remove | Rename | Write, + }, + }, + }, + // i=4 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/include", + C: ch[3], + E: Create, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/include", + E: Create, + }, + }, + }, + // i=5 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/include/qttu/detail/...", + C: ch[3], + E: Write, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/include/qttu/detail", + E: Create | Write, + }, + }, + }, + // i=6 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/include/...", + C: ch[0], + E: Rename, + }, + Record: []Call{ + { + F: FuncRewatch, + P: "src/github.com/pblaszczyk/qttu/include", + E: Create, + NE: Create | Rename, + }, + { + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/include/qttu", + E: Create | Rename, + }, + { + F: FuncRewatch, + P: "src/github.com/pblaszczyk/qttu/include/qttu/detail", + E: Create | Write, + NE: Create | Write | Rename, + }, + }, + }, + // i=7 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/pblaszczyk/...", + C: ch[1], + E: Write, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/pblaszczyk", + E: Create | Write, + }, + { + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu", + E: Create | Write, + }, + { + F: FuncRewatch, + P: "src/github.com/pblaszczyk/qttu/include", + E: Create | Rename, + NE: Create | Rename | Write, + }, + { + F: FuncRewatch, + P: "src/github.com/pblaszczyk/qttu/include/qttu", + E: Create | Rename, + NE: Create | Rename | Write, + }, + { + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/src", + E: Create | Write, + }, + }, + }, + // i=8 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/include/...", + C: ch[4], + E: Write, + }, + Record: nil, + }, + // i=9 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu", + C: ch[3], + E: Remove, + }, + Record: []Call{ + { + F: FuncRewatch, + P: "src/github.com/pblaszczyk/qttu", + E: Create | Write, + NE: Create | Write | Remove, + }, + }, + }, + } + + n.ExpectRecordedCalls(watches[:]) + + events := [...]TCase{ + // i=0 + { + Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Rename}, + Receiver: Chans{ch[0]}, + }, + // i=1 + { + Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Create}, + Receiver: nil, + }, + // i=2 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/cmd.go", E: Remove}, + Receiver: Chans{ch[1]}, + }, + // i=3 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/doc.go", E: Write}, + Receiver: nil, + }, + // i=4 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree/main.go", E: Write}, + Receiver: Chans{ch[2]}, + }, + // i=5 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree/tree.go", E: Create}, + Receiver: nil, + }, + // i=6 + { + Event: Call{P: "src/github.com/pblaszczyk/qttu/include/.lock", E: Create}, + Receiver: Chans{ch[3]}, + }, + // i=7 + { + Event: Call{P: "src/github.com/pblaszczyk/qttu/include/qttu/detail/registry.hh", E: Write}, + Receiver: Chans{ch[3], ch[1], ch[4]}, + }, + // i=8 + { + Event: Call{P: "src/github.com/pblaszczyk/qttu/include/qttu", E: Remove}, + Receiver: nil, + }, + // i=9 + { + Event: Call{P: "src/github.com/pblaszczyk/qttu/include", E: Remove}, + Receiver: Chans{ch[3]}, + }, + } + + n.ExpectTreeEvents(events[:], ch) + + stops := [...]RCase{ + // i=0 + { + Call: Call{ + F: FuncStop, + C: ch[4], + }, + Record: nil, + }, + // i=1 + { + Call: Call{ + F: FuncStop, + C: ch[3], + }, + Record: []Call{ + { + F: FuncRewatch, + P: "src/github.com/pblaszczyk/qttu", + E: Create | Write | Remove, + NE: Create | Write, + }, + }, + }, + // i=2 + { + Call: Call{ + F: FuncStop, + C: ch[2], + }, + Record: []Call{ + { + F: FuncRewatch, + P: "src/github.com/rjeczalik/fs/cmd", + E: Create | Remove | Rename, + NE: Create | Remove, + }, + { + F: FuncRewatch, + P: "src/github.com/rjeczalik/fs/cmd/gotree", + E: Create | Remove | Rename, + NE: Create | Remove, + }, + { + F: FuncRewatch, + P: "src/github.com/rjeczalik/fs/cmd/mktree", + E: Create | Remove | Rename | Write, + NE: Create | Remove, + }, + }, + }, + // i=3 + { + Call: Call{ + F: FuncStop, + C: ch[1], + }, + Record: []Call{ + { + F: FuncUnwatch, + P: "src/github.com/pblaszczyk", + }, + { + F: FuncUnwatch, + P: "src/github.com/pblaszczyk/qttu", + }, + { + F: FuncRewatch, + P: "src/github.com/pblaszczyk/qttu/include", + E: Create | Rename | Write, + NE: Create | Rename, + }, + { + F: FuncRewatch, + P: "src/github.com/pblaszczyk/qttu/include/qttu", + E: Create | Rename | Write, + NE: Create | Rename, + }, + { + F: FuncRewatch, + P: "src/github.com/pblaszczyk/qttu/include/qttu/detail", + E: Create | Rename | Write, + NE: Create | Rename, + }, + { + F: FuncUnwatch, + P: "src/github.com/pblaszczyk/qttu/src", + }, + { + F: FuncUnwatch, + P: "src/github.com/rjeczalik/fs/cmd", + }, + { + F: FuncUnwatch, + P: "src/github.com/rjeczalik/fs/cmd/gotree", + }, + { + F: FuncUnwatch, + P: "src/github.com/rjeczalik/fs/cmd/mktree", + }, + }, + }, + // i=4 + { + Call: Call{ + F: FuncStop, + C: ch[0], + }, + Record: []Call{ + { + F: FuncUnwatch, + P: "src/github.com/pblaszczyk/qttu/include", + }, + { + F: FuncUnwatch, + P: "src/github.com/pblaszczyk/qttu/include/qttu", + }, + { + F: FuncUnwatch, + P: "src/github.com/pblaszczyk/qttu/include/qttu/detail", + }, + { + F: FuncUnwatch, + P: "src/github.com/rjeczalik/fs/fs.go", + }, + }, + }, + } + + n.ExpectRecordedCalls(stops[:]) + + n.Walk(func(nd node) error { + if len(nd.Watch) != 0 { + return fmt.Errorf("unexpected watchpoint: name=%s, eventset=%v (len=%d)", + nd.Name, nd.Watch.Total(), len(nd.Watch)) + } + return nil + }) +} + +func TestNonrecursiveTreeInternal(t *testing.T) { + n, c := NewNonrecursiveTreeTestC(t, "testdata/vfs.txt") + defer n.Close() + + ch := NewChans(5) + + watches := [...]RCase{ + // i=0 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd/...", + C: ch[0], + E: Remove, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd", + E: Create | Remove, + }, + { + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd/gotree", + E: Create | Remove, + }, + { + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd/mktree", + E: Create | Remove, + }, + }, + }, + // i=1 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/ppknap/link/include/coost/...", + C: ch[1], + E: Create, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/ppknap/link/include/coost", + E: Create, + }, + { + F: FuncWatch, + P: "src/github.com/ppknap/link/include/coost/link", + E: Create, + }, + { + F: FuncWatch, + P: "src/github.com/ppknap/link/include/coost/link/detail", + E: Create, + }, + { + F: FuncWatch, + P: "src/github.com/ppknap/link/include/coost/link/detail/stdhelpers", + E: Create, + }, + }, + }, + } + + n.ExpectRecordedCalls(watches[:]) + + events := [...]TCase{ + // i=0 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/dir", E: Create, Dir: true}, + Receiver: Chans{c}, + }, + // i=1 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/dir/another", E: Create, Dir: true}, + Receiver: Chans{c}, + }, + // i=2 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/file", E: Create, Dir: false}, + Receiver: nil, + }, + // i=3 + { + Event: Call{P: "src/github.com/ppknap/link/include/coost/dir", E: Create, Dir: true}, + Receiver: Chans{ch[1], c}, + }, + // i=4 + { + Event: Call{P: "src/github.com/ppknap/link/include/coost/dir/another", E: Create, Dir: true}, + Receiver: Chans{ch[1], c}, + }, + // i=5 + { + Event: Call{P: "src/github.com/ppknap/link/include/coost/file", E: Create, Dir: false}, + Receiver: Chans{ch[1]}, + }, + // i=6 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree", E: Remove}, + Receiver: Chans{ch[0]}, + }, + // i=7 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/rmtree", E: Create, Dir: true}, + Receiver: Chans{c}, + }, + } + + n.ExpectTreeEvents(events[:], ch) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/tree_recursive.go b/Godeps/_workspace/src/github.com/zillode/notify/tree_recursive.go new file mode 100644 index 0000000..7f00dfe --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/tree_recursive.go @@ -0,0 +1,354 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +import "sync" + +// watchAdd TODO(rjeczalik) +func watchAdd(nd node, c chan<- EventInfo, e Event) eventDiff { + diff := nd.Watch.Add(c, e) + if wp := nd.Child[""].Watch; len(wp) != 0 { + e = wp.Total() + diff[0] |= e + diff[1] |= e + if diff[0] == diff[1] { + return none + } + } + return diff +} + +// watchAddInactive TODO(rjeczalik) +func watchAddInactive(nd node, c chan<- EventInfo, e Event) eventDiff { + wp := nd.Child[""].Watch + if wp == nil { + wp = make(watchpoint) + nd.Child[""] = node{Watch: wp} + } + diff := wp.Add(c, e) + e = nd.Watch.Total() + diff[0] |= e + diff[1] |= e + if diff[0] == diff[1] { + return none + } + return diff +} + +// watchCopy TODO(rjeczalik) +func watchCopy(src, dst node) { + for c, e := range src.Watch { + if c == nil { + continue + } + watchAddInactive(dst, c, e) + } + if wpsrc := src.Child[""].Watch; len(wpsrc) != 0 { + wpdst := dst.Child[""].Watch + for c, e := range wpsrc { + if c == nil { + continue + } + wpdst.Add(c, e) + } + } +} + +// watchDel TODO(rjeczalik) +func watchDel(nd node, c chan<- EventInfo, e Event) eventDiff { + diff := nd.Watch.Del(c, e) + if wp := nd.Child[""].Watch; len(wp) != 0 { + diffInactive := wp.Del(c, e) + e = wp.Total() + // TODO(rjeczalik): add e if e != all? + diff[0] |= diffInactive[0] | e + diff[1] |= diffInactive[1] | e + if diff[0] == diff[1] { + return none + } + } + return diff +} + +// watchTotal TODO(rjeczalik) +func watchTotal(nd node) Event { + e := nd.Watch.Total() + if wp := nd.Child[""].Watch; len(wp) != 0 { + e |= wp.Total() + } + return e +} + +// watchIsRecursive TODO(rjeczalik) +func watchIsRecursive(nd node) bool { + ok := nd.Watch.IsRecursive() + // TODO(rjeczalik): add a test for len(wp) != 0 change the condition. + if wp := nd.Child[""].Watch; len(wp) != 0 { + // If a watchpoint holds inactive watchpoints, it means it's a parent + // one, which is recursive by nature even though it may be not recursive + // itself. + ok = true + } + return ok +} + +// recursiveTree TODO(rjeczalik) +type recursiveTree struct { + rw sync.RWMutex // protects root + root root + // TODO(rjeczalik): merge watcher + recursiveWatcher after #5 and #6 + w interface { + watcher + recursiveWatcher + } + c chan EventInfo +} + +// newRecursiveTree TODO(rjeczalik) +func newRecursiveTree(w recursiveWatcher, c chan EventInfo) *recursiveTree { + t := &recursiveTree{ + root: root{nd: newnode("")}, + w: struct { + watcher + recursiveWatcher + }{w.(watcher), w}, + c: c, + } + go t.dispatch() + return t +} + +// dispatch TODO(rjeczalik) +func (t *recursiveTree) dispatch() { + for ei := range t.c { + dbgprintf("dispatching %v on %q", ei.Event(), ei.Path()) + go func(ei EventInfo) { + nd, ok := node{}, false + dir, base := split(ei.Path()) + fn := func(it node, isbase bool) error { + if isbase { + nd = it + } else { + it.Watch.Dispatch(ei, recursive) + } + return nil + } + t.rw.RLock() + defer t.rw.RUnlock() + // Notify recursive watchpoints found on the path. + if err := t.root.WalkPath(dir, fn); err != nil { + dbgprint("dispatch did not reach leaf:", err) + return + } + // Notify parent watchpoint. + nd.Watch.Dispatch(ei, 0) + // If leaf watchpoint exists, notify it. + if nd, ok = nd.Child[base]; ok { + nd.Watch.Dispatch(ei, 0) + } + }(ei) + } +} + +// Watch TODO(rjeczalik) +func (t *recursiveTree) Watch(path string, c chan<- EventInfo, events ...Event) error { + if c == nil { + panic("notify: Watch using nil channel") + } + // Expanding with empty event set is a nop. + if len(events) == 0 { + return nil + } + path, isrec, err := cleanpath(path) + if err != nil { + return err + } + eventset := joinevents(events) + if isrec { + eventset |= recursive + } + t.rw.Lock() + defer t.rw.Unlock() + // case 1: cur is a child + // + // Look for parent watch which already covers the given path. + parent := node{} + self := false + err = t.root.WalkPath(path, func(nd node, isbase bool) error { + if watchTotal(nd) != 0 { + parent = nd + self = isbase + return errSkip + } + return nil + }) + cur := t.root.Add(path) // add after the walk, so it's less to traverse + if err == nil && parent.Watch != nil { + // Parent watch found. Register inactive watchpoint, so we have enough + // information to shrink the eventset on eventual Stop. + // return t.resetwatchpoint(parent, parent, c, eventset|inactive) + var diff eventDiff + if self { + diff = watchAdd(cur, c, eventset) + } else { + diff = watchAddInactive(parent, c, eventset) + } + switch { + case diff == none: + // the parent watchpoint already covers requested subtree with its + // eventset + case diff[0] == 0: + // TODO(rjeczalik): cleanup this panic after implementation is stable + panic("dangling watchpoint: " + parent.Name) + default: + if isrec || watchIsRecursive(parent) { + err = t.w.RecursiveRewatch(parent.Name, parent.Name, diff[0], diff[1]) + } else { + err = t.w.Rewatch(parent.Name, diff[0], diff[1]) + } + if err != nil { + watchDel(parent, c, diff.Event()) + return err + } + watchAdd(cur, c, eventset) + // TODO(rjeczalik): account top-most path for c + return nil + } + if !self { + watchAdd(cur, c, eventset) + } + return nil + } + // case 2: cur is new parent + // + // Look for children nodes, unwatch n-1 of them and rewatch the last one. + var children []node + fn := func(nd node) error { + if len(nd.Watch) == 0 { + return nil + } + children = append(children, nd) + return errSkip + } + switch must(cur.Walk(fn)); len(children) { + case 0: + // no child watches, cur holds a new watch + case 1: + watchAdd(cur, c, eventset) // TODO(rjeczalik): update cache c subtree root? + watchCopy(children[0], cur) + err = t.w.RecursiveRewatch(children[0].Name, cur.Name, watchTotal(children[0]), + watchTotal(cur)) + if err != nil { + // Clean inactive watchpoint. The c chan did not exist before. + cur.Child[""] = node{} + delete(cur.Watch, c) + return err + } + return nil + default: + watchAdd(cur, c, eventset) + // Copy children inactive watchpoints to the new parent. + for _, nd := range children { + watchCopy(nd, cur) + } + // Watch parent subtree. + if err = t.w.RecursiveWatch(cur.Name, watchTotal(cur)); err != nil { + // Clean inactive watchpoint. The c chan did not exist before. + cur.Child[""] = node{} + delete(cur.Watch, c) + return err + } + // Unwatch children subtrees. + var e error + for _, nd := range children { + if watchIsRecursive(nd) { + e = t.w.RecursiveUnwatch(nd.Name) + } else { + e = t.w.Unwatch(nd.Name) + } + if e != nil { + err = nonil(err, e) + // TODO(rjeczalik): child is still watched, warn all its watchpoints + // about possible duplicate events via Error event + } + } + return err + } + // case 3: cur is new, alone node + switch diff := watchAdd(cur, c, eventset); { + case diff == none: + // TODO(rjeczalik): cleanup this panic after implementation is stable + panic("watch requested but no parent watchpoint found: " + cur.Name) + case diff[0] == 0: + if isrec { + err = t.w.RecursiveWatch(cur.Name, diff[1]) + } else { + err = t.w.Watch(cur.Name, diff[1]) + } + if err != nil { + watchDel(cur, c, diff.Event()) + return err + } + default: + // TODO(rjeczalik): cleanup this panic after implementation is stable + panic("watch requested but no parent watchpoint found: " + cur.Name) + } + return nil +} + +// Stop TODO(rjeczalik) +// +// TODO(rjeczalik): Split parent watchpoint - transfer watches to children +// if parent is no longer needed. This carries a risk that underlying +// watcher calls could fail - reconsider if it's worth the effort. +func (t *recursiveTree) Stop(c chan<- EventInfo) { + var err error + fn := func(nd node) (e error) { + diff := watchDel(nd, c, all) + switch { + case diff == none && watchTotal(nd) == 0: + // TODO(rjeczalik): There's no watchpoints deeper in the tree, + // probably we should remove the nodes as well. + return nil + case diff == none: + // Removing c from nd does not require shrinking its eventset. + case diff[1] == 0: + if watchIsRecursive(nd) { + e = t.w.RecursiveUnwatch(nd.Name) + } else { + e = t.w.Unwatch(nd.Name) + } + default: + if watchIsRecursive(nd) { + e = t.w.RecursiveRewatch(nd.Name, nd.Name, diff[0], diff[1]) + } else { + e = t.w.Rewatch(nd.Name, diff[0], diff[1]) + } + } + fn := func(nd node) error { + watchDel(nd, c, all) + return nil + } + err = nonil(err, e, nd.Walk(fn)) + // TODO(rjeczalik): if e != nil store dummy chan in nd.Watch just to + // retry un/rewatching next time and/or let the user handle the failure + // vie Error event? + return errSkip + } + t.rw.Lock() + e := t.root.Walk("", fn) // TODO(rjeczalik): use max root per c + t.rw.Unlock() + if e != nil { + err = nonil(err, e) + } + dbgprintf("Stop(%p) error: %v\n", c, err) +} + +// Close TODO(rjeczalik) +func (t *recursiveTree) Close() error { + err := t.w.Close() + close(t.c) + return err +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/tree_recursive_test.go b/Godeps/_workspace/src/github.com/zillode/notify/tree_recursive_test.go new file mode 100644 index 0000000..329fca0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/tree_recursive_test.go @@ -0,0 +1,524 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +import "testing" + +func TestRecursiveTree(t *testing.T) { + n := NewRecursiveTreeTest(t, "testdata/vfs.txt") + defer n.Close() + + ch := NewChans(5) + + watches := [...]RCase{ + // i=0 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/fs.go", + C: ch[0], + E: Create, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/fs.go", + E: Create, + }, + }, + }, + // i=1 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd/...", + C: ch[1], + E: Remove, + }, + Record: []Call{ + { + F: FuncRecursiveWatch, + P: "src/github.com/rjeczalik/fs/cmd", + E: Remove, + }, + }, + }, + // i=2 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs", + C: ch[2], + E: Rename, + }, + Record: []Call{ + { + F: FuncRecursiveWatch, + P: "src/github.com/rjeczalik/fs", + E: Create | Remove | Rename, + }, + { + F: FuncRecursiveUnwatch, + P: "src/github.com/rjeczalik/fs/cmd", + }, + { + F: FuncUnwatch, + P: "src/github.com/rjeczalik/fs/fs.go", + }, + }, + }, + // i=3 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/ppknap/link/README.md", + C: ch[0], + E: Create, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/ppknap/link/README.md", + E: Create, + }, + }, + }, + // i=4 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/ppknap/link/include/...", + C: ch[3], + E: Remove, + }, + Record: []Call{ + { + F: FuncRecursiveWatch, + P: "src/github.com/ppknap/link/include", + E: Remove, + }, + }, + }, + // i=5 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/ppknap/link/include", + C: ch[0], + E: Write, + }, + Record: []Call{ + { + F: FuncRecursiveRewatch, + P: "src/github.com/ppknap/link/include", + NP: "src/github.com/ppknap/link/include", + E: Remove, + NE: Remove | Write, + }, + }, + }, + // i=6 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/ppknap/link/test/Jamfile.jam", + C: ch[0], + E: Rename, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/ppknap/link/test/Jamfile.jam", + E: Rename, + }, + }, + }, + // i=7 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/ppknap/link/test/Jamfile.jam", + C: ch[0], + E: Create, + }, + Record: []Call{ + { + F: FuncRewatch, + P: "src/github.com/ppknap/link/test/Jamfile.jam", + E: Rename, + NE: Rename | Create, + }, + }, + }, + // i=8 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/ppknap/...", + C: ch[0], + E: Create, + }, + Record: []Call{ + { + F: FuncRecursiveWatch, + P: "src/github.com/ppknap", + E: Create | Remove | Write | Rename, + }, + { + F: FuncUnwatch, + P: "src/github.com/ppknap/link/README.md", + }, + { + F: FuncRecursiveUnwatch, + P: "src/github.com/ppknap/link/include", + }, + { + F: FuncUnwatch, + P: "src/github.com/ppknap/link/test/Jamfile.jam", + }, + }, + }, + // i=9 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/README.md", + C: ch[0], + E: Rename, + }, + Record: nil, + }, + // i=10 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd/gotree", + C: ch[2], + E: Create | Remove, + }, + Record: nil, + }, + // i=11 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/src/main.cc", + C: ch[0], + E: Create, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/src/main.cc", + E: Create, + }, + }, + }, + // i=12 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/src/main.cc", + C: ch[0], + E: Remove, + }, + Record: []Call{ + { + F: FuncRewatch, + P: "src/github.com/pblaszczyk/qttu/src/main.cc", + E: Create, + NE: Create | Remove, + }, + }, + }, + // i=13 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/src/main.cc", + C: ch[0], + E: Create | Remove, + }, + Record: nil, + }, + // i=14 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu/src", + C: ch[0], + E: Create, + }, + Record: []Call{ + { + F: FuncRecursiveRewatch, + P: "src/github.com/pblaszczyk/qttu/src/main.cc", + NP: "src/github.com/pblaszczyk/qttu/src", + E: Create | Remove, + NE: Create | Remove, + }, + }, + }, + // i=15 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/pblaszczyk/qttu", + C: ch[4], + E: Write, + }, + Record: []Call{ + { + F: FuncRecursiveRewatch, + P: "src/github.com/pblaszczyk/qttu/src", + NP: "src/github.com/pblaszczyk/qttu", + E: Create | Remove, + NE: Create | Remove | Write, + }, + }, + }, + // i=16 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/fs.go", + C: ch[3], + E: Rename, + }, + Record: nil, + }, + } + + n.ExpectRecordedCalls(watches[:]) + + events := [...]TCase{ + // i=0 + { + Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Rename}, + Receiver: Chans{ch[2], ch[3]}, + }, + // i=1 + { + Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Create}, + Receiver: Chans{ch[0]}, + }, + // i=2 + { + Event: Call{P: "src/github.com/rjeczalik/fs/fs.go/file", E: Create}, + Receiver: Chans{ch[0]}, + }, + // i=3 + { + Event: Call{P: "src/github.com/rjeczalik/fs", E: Rename}, + Receiver: Chans{ch[2]}, + }, + // i=4 + { + Event: Call{P: "src/github.com/rjeczalik/fs/fs_test.go", E: Rename}, + Receiver: Chans{ch[2]}, + }, + // i=5 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree/main.go", E: Remove}, + Receiver: Chans{ch[1]}, + }, + // i=6 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/gotree", E: Remove}, + Receiver: Chans{ch[1], ch[2]}, + }, + // i=7 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd", E: Remove}, + Receiver: Chans{ch[1]}, + }, + // i=8 + { + Event: Call{P: "src/github.com/rjeczalik/fs/fs.go/file", E: Write}, + Receiver: nil, + }, + // i=9 + { + Event: Call{P: "src/github.com/rjeczalik/fs/fs.go/file", E: Write}, + Receiver: nil, + }, + // i=10 + { + Event: Call{P: "src/github.com/rjeczalik/fs", E: Remove}, + Receiver: nil, + }, + // i=11 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd", E: Rename}, + Receiver: Chans{ch[2]}, + }, + // i=12 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree/main.go", E: Write}, + Receiver: nil, + }, + // i=13 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/gotree", E: Rename}, + Receiver: nil, + }, + // i=14 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/file", E: Rename}, + Receiver: nil, + }, + // i=15 + { + Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Rename}, + Receiver: Chans{ch[2], ch[3]}, + }, + } + + n.ExpectTreeEvents(events[:], ch) + + stops := [...]RCase{ + // i=0 + { + Call: Call{ + F: FuncStop, + C: ch[1], + }, + Record: nil, + }, + { + Call: Call{ + F: FuncStop, + C: ch[4], + }, + Record: []Call{ + { + F: FuncRecursiveRewatch, + P: "src/github.com/pblaszczyk/qttu", + NP: "src/github.com/pblaszczyk/qttu", + E: Create | Remove | Write, + NE: Create | Remove, + }, + }, + }, + } + + n.ExpectRecordedCalls(stops[:]) +} + +func TestRecursiveTreeWatchInactiveMerge(t *testing.T) { + n := NewRecursiveTreeTest(t, "testdata/vfs.txt") + defer n.Close() + + ch := NewChans(1) + + watches := [...]RCase{ + // i=0 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs", + C: ch[0], + E: Create, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/rjeczalik/fs", + E: Create, + }, + }, + }, + // i=1 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs/cmd/gotree/...", + C: ch[0], + E: Remove, + }, + Record: []Call{ + { + F: FuncRecursiveRewatch, + P: "src/github.com/rjeczalik/fs", + NP: "src/github.com/rjeczalik/fs", + E: Create, + NE: Create | Remove, + }, + }, + }, + } + + n.ExpectRecordedCalls(watches[:]) + + events := [...]TCase{ + // i=0 + { + Event: Call{P: "src/github.com/rjeczalik/fs/.fs.go.swp", E: Create}, + Receiver: Chans{ch[0]}, + }, + // i=1 + { + Event: Call{P: "src/github.com/rjeczalik/fs/.fs.go.swp", E: Remove}, + Receiver: nil, + }, + // i=2 + { + Event: Call{P: "src/github.com/rjeczalik/fs", E: Remove}, + Receiver: nil, + }, + // i=3 + { + Event: Call{P: "src/github.com/rjeczalik/fs/cmd/gotree/main.go", E: Remove}, + Receiver: Chans{ch[0]}, + }, + } + + n.ExpectTreeEvents(events[:], ch) +} + +func TestRecursiveTree_Windows(t *testing.T) { + n := NewRecursiveTreeTest(t, "testdata/vfs.txt") + defer n.Close() + + const ChangeFileName = Event(0x1) + + ch := NewChans(1) + + watches := [...]RCase{ + // i=0 + { + Call: Call{ + F: FuncWatch, + P: "src/github.com/rjeczalik/fs", + C: ch[0], + E: ChangeFileName, + }, + Record: []Call{ + { + F: FuncWatch, + P: "src/github.com/rjeczalik/fs", + E: ChangeFileName, + }, + }, + }, + } + + n.ExpectRecordedCalls(watches[:]) + + events := [...]TCase{ + // i=0 + { + Event: Call{P: "src/github.com/rjeczalik/fs", E: ChangeFileName}, + Receiver: Chans{ch[0]}, + }, + // i=1 + { + Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: ChangeFileName}, + Receiver: Chans{ch[0]}, + }, + } + + n.ExpectTreeEvents(events[:], ch) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/util.go b/Godeps/_workspace/src/github.com/zillode/notify/util.go new file mode 100644 index 0000000..bbc84c3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/util.go @@ -0,0 +1,146 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +import ( + "errors" + "os" + "path/filepath" + "strings" +) + +const all = ^Event(0) +const sep = string(os.PathSeparator) + +var errDepth = errors.New("exceeded allowed iteration count (circular symlink?)") + +func min(i, j int) int { + if i > j { + return j + } + return i +} + +func max(i, j int) int { + if i < j { + return j + } + return i +} + +// must panics if err is non-nil. +func must(err error) { + if err != nil { + panic(err) + } +} + +// nonil gives first non-nil error from the given arguments. +func nonil(err ...error) error { + for _, err := range err { + if err != nil { + return err + } + } + return nil +} + +func cleanpath(path string) (realpath string, isrec bool, err error) { + if strings.HasSuffix(path, "...") { + isrec = true + path = path[:len(path)-3] + } + if path, err = filepath.Abs(path); err != nil { + return "", false, err + } + if path, err = canonical(path); err != nil { + return "", false, err + } + return path, isrec, nil +} + +// canonical resolves any symlink in the given path and returns it in a clean form. +// It expects the path to be absolute. It fails to resolve circular symlinks by +// maintaining a simple iteration limit. +func canonical(p string) (string, error) { + p, err := filepath.Abs(p) + if err != nil { + return "", err + } + for i, depth := 1, 1; i < len(p); i, depth = i+1, depth+1 { + if depth > 128 { + return "", &os.PathError{Op: "canonical", Path: p, Err: errDepth} + } + if j := strings.IndexRune(p[i:], '/'); j == -1 { + i = len(p) + } else { + i = i + j + } + fi, err := os.Lstat(p[:i]) + if err != nil { + return "", err + } + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + s, err := os.Readlink(p[:i]) + if err != nil { + return "", err + } + p = "/" + s + p[i:] + i = 1 // no guarantee s is canonical, start all over + } + } + return filepath.Clean(p), nil +} + +func joinevents(events []Event) (e Event) { + if len(events) == 0 { + e = All + } else { + for _, event := range events { + e |= event + } + } + return +} + +func split(s string) (string, string) { + if i := lastIndexSep(s); i != -1 { + return s[:i], s[i+1:] + } + return "", s +} + +func base(s string) string { + if i := lastIndexSep(s); i != -1 { + return s[i+1:] + } + return s +} + +func indexbase(root, name string) int { + if n, m := len(root), len(name); m >= n && name[:n] == root && + (n == m || name[n] == os.PathSeparator) { + return min(n+1, m) + } + return -1 +} + +func indexSep(s string) int { + for i := 0; i < len(s); i++ { + if s[i] == os.PathSeparator { + return i + } + } + return -1 +} + +func lastIndexSep(s string) int { + for i := len(s) - 1; i >= 0; i-- { + if s[i] == os.PathSeparator { + return i + } + } + return -1 +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/util_darwin_test.go b/Godeps/_workspace/src/github.com/zillode/notify/util_darwin_test.go new file mode 100644 index 0000000..8c95db3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/util_darwin_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build darwin + +package notify + +import ( + "os" + "testing" +) + +func TestCanonicalDarwin(t *testing.T) { + cases := [...]caseCanonical{ + {"/etc", "/private/etc"}, + {"/etc/defaults", "/private/etc/defaults"}, + {"/etc/hosts", "/private/etc/hosts"}, + {"/tmp", "/private/tmp"}, + {"/var", "/private/var"}, + } + testCanonical(t, cases[:]) +} + +func TestCanonicalDarwinMultiple(t *testing.T) { + etcsym, err := symlink("/etc", "") + if err != nil { + t.Fatal(err) + } + tmpsym, err := symlink("/tmp", "") + if err != nil { + t.Fatal(nonil(err, os.Remove(etcsym))) + } + defer removeall(etcsym, tmpsym) + cases := [...]caseCanonical{ + {etcsym, "/private/etc"}, + {etcsym + "/hosts", "/private/etc/hosts"}, + {tmpsym, "/private/tmp"}, + } + testCanonical(t, cases[:]) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/util_test.go b/Godeps/_workspace/src/github.com/zillode/notify/util_test.go new file mode 100644 index 0000000..43af3e7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/util_test.go @@ -0,0 +1,147 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +import ( + "os" + "path/filepath" + "testing" +) + +type caseCanonical struct { + path string + full string +} + +func testCanonical(t *testing.T, cases []caseCanonical) { + for i, cas := range cases { + full, err := canonical(cas.path) + if err != nil { + t.Errorf("want err=nil; got %v (i=%d)", err, i) + continue + } + if full != cas.full { + t.Errorf("want full=%q; got %q (i=%d)", cas.full, full, i) + continue + } + } +} + +func TestCanonicalNoSymlink(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("os.Getwd()=%v", err) + } + td := filepath.Join(wd, "testdata") + cases := [...]caseCanonical{ + {".", wd}, + {"testdata", td}, + {filepath.Join("testdata", ".."), wd}, + } + testCanonical(t, cases[:]) +} + +func TestJoinevents(t *testing.T) { + cases := [...]struct { + evs []Event + ev Event + }{ + 0: {nil, All}, + 1: {[]Event{}, All}, + 2: {[]Event{Create}, Create}, + 3: {[]Event{Rename}, Rename}, + 4: {[]Event{Create, Write, Remove}, Create | Write | Remove}, + } + for i, cas := range cases { + if ev := joinevents(cas.evs); ev != cas.ev { + t.Errorf("want event=%v; got %v (i=%d)", cas.ev, ev, i) + } + } +} + +func TestTreeSplit(t *testing.T) { + cases := [...]struct { + path string + dir string + base string + }{ + {"/github.com/rjeczalik/fakerpc", "/github.com/rjeczalik", "fakerpc"}, + {"/home/rjeczalik/src", "/home/rjeczalik", "src"}, + {"/Users/pknap/porn/gopher.avi", "/Users/pknap/porn", "gopher.avi"}, + {"C:/Documents and Users", "C:", "Documents and Users"}, + {"C:/Documents and Users/pblaszczyk/wiertarka.exe", "C:/Documents and Users/pblaszczyk", "wiertarka.exe"}, + {"/home/(╯°□°)╯︵ ┻━┻", "/home", "(╯°□°)╯︵ ┻━┻"}, + } + for i, cas := range cases { + dir, base := split(filepath.FromSlash(cas.path)) + if want := filepath.FromSlash(cas.dir); dir != want { + t.Errorf("want dir=%s; got %s (i=%d)", want, dir, i) + } + if want := filepath.FromSlash(cas.base); base != want { + t.Errorf("want base=%s; got %s (i=%d)", want, base, i) + } + } +} + +func TestTreeBase(t *testing.T) { + cases := [...]struct { + path string + base string + }{ + {"/github.com/rjeczalik/fakerpc", "fakerpc"}, + {"/home/rjeczalik/src", "src"}, + {"/Users/pknap/porn/gopher.avi", "gopher.avi"}, + {"C:/Documents and Users", "Documents and Users"}, + {"C:/Documents and Users/pblaszczyk/wiertarka.exe", "wiertarka.exe"}, + {"/home/(╯°□°)╯︵ ┻━┻", "(╯°□°)╯︵ ┻━┻"}, + } + for i, cas := range cases { + if base := base(filepath.FromSlash(cas.path)); base != cas.base { + t.Errorf("want base=%s; got %s (i=%d)", cas.base, base, i) + } + } +} + +func TestTreeIndexSep(t *testing.T) { + cases := [...]struct { + path string + n int + }{ + {"github.com/rjeczalik/fakerpc", 10}, + {"home/rjeczalik/src", 4}, + {"Users/pknap/porn/gopher.avi", 5}, + {"C:/Documents and Users", 2}, + {"Documents and Users/pblaszczyk/wiertarka.exe", 19}, + {"(╯°□°)╯︵ ┻━┻/Downloads", 30}, + } + for i, cas := range cases { + if n := indexSep(filepath.FromSlash(cas.path)); n != cas.n { + t.Errorf("want n=%d; got %d (i=%d)", cas.n, n, i) + } + } +} + +func TestTreeLastIndexSep(t *testing.T) { + cases := [...]struct { + path string + n int + }{ + {"github.com/rjeczalik/fakerpc", 20}, + {"home/rjeczalik/src", 14}, + {"Users/pknap/porn/gopher.avi", 16}, + {"C:/Documents and Users", 2}, + {"Documents and Users/pblaszczyk/wiertarka.exe", 30}, + {"/home/(╯°□°)╯︵ ┻━┻", 5}, + } + for i, cas := range cases { + if n := lastIndexSep(filepath.FromSlash(cas.path)); n != cas.n { + t.Errorf("want n=%d; got %d (i=%d)", cas.n, n, i) + } + } +} + +func TestCleanpath(t *testing.T) { + t.Skip("TODO(rjeczalik)") +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/util_unix_test.go b/Godeps/_workspace/src/github.com/zillode/notify/util_unix_test.go new file mode 100644 index 0000000..0011506 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/util_unix_test.go @@ -0,0 +1,94 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build !windows + +package notify + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func tmpfile(s string) (string, error) { + f, err := ioutil.TempFile(filepath.Split(s)) + if err != nil { + return "", err + } + if err = nonil(f.Sync(), f.Close()); err != nil { + return "", err + } + return f.Name(), nil +} + +func symlink(src, dst string) (string, error) { + name, err := tmpfile(dst) + if err != nil { + return "", err + } + if err = nonil(os.Remove(name), os.Symlink(src, name)); err != nil { + return "", err + } + return name, nil +} + +func removeall(s ...string) { + for _, s := range s { + os.Remove(s) + } +} + +func TestCanonical(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("os.Getwd()=%v", err) + } + wdsym, err := symlink(wd, "") + if err != nil { + t.Fatalf(`symlink(%q, "")=%v`, wd, err) + } + td := filepath.Join(wd, "testdata") + tdsym, err := symlink(td, td) + if err != nil { + t.Errorf("symlink(%q, %q)=%v", td, td, nonil(err, os.Remove(wdsym))) + } + defer removeall(wdsym, tdsym) + vfstxt := filepath.Join(td, "vfs.txt") + cases := [...]caseCanonical{ + {wdsym, wd}, + {tdsym, td}, + {filepath.Join(wdsym, "notify.go"), filepath.Join(wd, "notify.go")}, + {filepath.Join(tdsym, "vfs.txt"), vfstxt}, + {filepath.Join(wdsym, filepath.Base(tdsym), "vfs.txt"), vfstxt}, + } + testCanonical(t, cases[:]) +} + +func TestCanonicalCircular(t *testing.T) { + tmp1, err := tmpfile("circular") + if err != nil { + t.Fatal(err) + } + tmp2, err := tmpfile("circular") + if err != nil { + t.Fatal(nonil(err, os.Remove(tmp1))) + } + defer removeall(tmp1, tmp2) + // Symlink tmp1 -> tmp2. + if err = nonil(os.Remove(tmp1), os.Symlink(tmp2, tmp1)); err != nil { + t.Fatal(err) + } + // Symlnik tmp2 -> tmp1. + if err = nonil(os.Remove(tmp2), os.Symlink(tmp1, tmp2)); err != nil { + t.Fatal(err) + } + if _, err = canonical(tmp1); err == nil { + t.Fatalf("want canonical(%q)!=nil", tmp1) + } + if _, ok := err.(*os.PathError); !ok { + t.Fatalf("want canonical(%q)=os.PathError; got %T", tmp1, err) + } +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher.go new file mode 100644 index 0000000..34148ef --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher.go @@ -0,0 +1,85 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +import "errors" + +var ( + errAlreadyWatched = errors.New("path is already watched") + errNotWatched = errors.New("path is not being watched") + errInvalidEventSet = errors.New("invalid event set provided") +) + +// Watcher is a intermediate interface for wrapping inotify, ReadDirChangesW, +// FSEvents, kqueue and poller implementations. +// +// The watcher implementation is expected to do its own mapping between paths and +// create watchers if underlying event notification does not support it. For +// the ease of implementation it is guaranteed that paths provided via Watch and +// Unwatch methods are absolute and clean. +type watcher interface { + // Watch requests a watcher creation for the given path and given event set. + Watch(path string, event Event) error + + // Unwatch requests a watcher deletion for the given path and given event set. + Unwatch(path string) error + + // Rewatch provides a functionality for modifying existing watch-points, like + // expanding its event set. + // + // Rewatch modifies existing watch-point under for the given path. It passes + // the existing event set currently registered for the given path, and the + // new, requested event set. + // + // It is guaranteed that Tree will not pass to Rewatch zero value for any + // of its arguments. If old == new and watcher can be upgraded to + // recursiveWatcher interface, a watch for the corresponding path is expected + // to be changed from recursive to the non-recursive one. + Rewatch(path string, old, new Event) error + + // Close unwatches all paths that are registered. When Close returns, it + // is expected it will report no more events. + Close() error +} + +// RecursiveWatcher is an interface for a Watcher for those OS, which do support +// recursive watching over directories. +type recursiveWatcher interface { + RecursiveWatch(path string, event Event) error + + // RecursiveUnwatch removes a recursive watch-point given by the path. For + // native recursive implementation there is no difference in functionality + // between Unwatch and RecursiveUnwatch, however for those platforms, that + // requires emulation for recursive watch-points, the implementation differs. + RecursiveUnwatch(path string) error + + // RecursiveRewatcher provides a functionality for modifying and/or relocating + // existing recursive watch-points. + // + // To relocate a watch-point means to unwatch oldpath and set a watch-point on + // newpath. + // + // To modify a watch-point means either to expand or shrink its event set. + // + // Tree can want to either relocate, modify or relocate and modify a watch-point + // via single RecursiveRewatch call. + // + // If oldpath == newpath, the watch-point is expected to change its event set value + // from oldevent to newevent. + // + // If oldevent == newevent, the watch-point is expected to relocate from oldpath + // to the newpath. + // + // If oldpath != newpath and oldevent != newevent, the watch-point is expected + // to relocate from oldpath to the newpath first and then change its event set + // value from oldevent to the newevent. In other words the end result must be + // a watch-point set on newpath with newevent value of its event set. + // + // It is guaranteed that Tree will not pass to RecurisveRewatcha zero value + // for any of its arguments. If oldpath == newpath and oldevent == newevent, + // a watch for the corresponding path is expected to be changed for + // non-recursive to the recursive one. + RecursiveRewatch(oldpath, newpath string, oldevent, newevent Event) error +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher_fsevents.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher_fsevents.go new file mode 100644 index 0000000..5433491 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher_fsevents.go @@ -0,0 +1,319 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build darwin,!kqueue + +package notify + +import ( + "errors" + "strings" + "sync/atomic" +) + +// TODO(rjeczalik): get rid of calls to canonical, it's tree responsibility + +const ( + failure = uint32(FSEventsMustScanSubDirs | FSEventsUserDropped | FSEventsKernelDropped) + filter = uint32(FSEventsCreated | FSEventsRemoved | FSEventsRenamed | + FSEventsModified | FSEventsInodeMetaMod) +) + +// FSEvent represents single file event. It is created out of values passed by +// FSEvents to FSEventStreamCallback function. +type FSEvent struct { + Path string // real path of the file or directory + ID uint64 // ID of the event (FSEventStreamEventId) + Flags uint32 // joint FSEvents* flags (FSEventStreamEventFlags) +} + +// splitflags separates event flags from single set into slice of flags. +func splitflags(set uint32) (e []uint32) { + for i := uint32(1); set != 0; i, set = i<<1, set>>1 { + if (set & 1) != 0 { + e = append(e, i) + } + } + return +} + +// watch represents a filesystem watchpoint. It is a higher level abstraction +// over FSEvents' stream, which implements filtering of file events based +// on path and event set. It emulates non-recursive watch-point by filtering out +// events which paths are more than 1 level deeper than the watched path. +type watch struct { + // prev stores last event set per path in order to filter out old flags + // for new events, which appratenly FSEvents likes to retain. It's a disgusting + // hack, it should be researched how to get rid of it. + prev map[string]uint32 + c chan<- EventInfo + stream *stream + path string + events uint32 + isrec int32 + flushed bool +} + +// Example format: +// +// ~ $ (trigger command) # (event set) -> (effective event set) +// +// Heuristics: +// +// 1. Create event is removed when it was present in previous event set. +// Example: +// +// ~ $ echo > file # Create|Write -> Create|Write +// ~ $ echo > file # Create|Write|InodeMetaMod -> Write|InodeMetaMod +// +// 2. Remove event is removed if it was present in previouse event set. +// Example: +// +// ~ $ touch file # Create -> Create +// ~ $ rm file # Create|Remove -> Remove +// ~ $ touch file # Create|Remove -> Create +// +// 3. Write event is removed if not followed by InodeMetaMod on existing +// file. Example: +// +// ~ $ echo > file # Create|Write -> Create|Write +// ~ $ chmod +x file # Create|Write|ChangeOwner -> ChangeOwner +// +// 4. Write&InodeMetaMod is removed when effective event set contain Remove event. +// Example: +// +// ~ $ echo > file # Write|InodeMetaMod -> Write|InodeMetaMod +// ~ $ rm file # Remove|Write|InodeMetaMod -> Remove +// +func (w *watch) strip(base string, set uint32) uint32 { + const ( + write = FSEventsModified | FSEventsInodeMetaMod + both = FSEventsCreated | FSEventsRemoved + ) + switch w.prev[base] { + case FSEventsCreated: + set &^= FSEventsCreated + if set&FSEventsRemoved != 0 { + w.prev[base] = FSEventsRemoved + set &^= write + } + case FSEventsRemoved: + set &^= FSEventsRemoved + if set&FSEventsCreated != 0 { + w.prev[base] = FSEventsCreated + } + default: + switch set & both { + case FSEventsCreated: + w.prev[base] = FSEventsCreated + case FSEventsRemoved: + w.prev[base] = FSEventsRemoved + set &^= write + } + } + dbgprintf("split()=%v\n", Event(set)) + return set +} + +// Dispatch is a stream function which forwards given file events for the watched +// path to underlying FileInfo channel. +func (w *watch) Dispatch(ev []FSEvent) { + events := atomic.LoadUint32(&w.events) + isrec := (atomic.LoadInt32(&w.isrec) == 1) + for i := range ev { + if ev[i].Flags&FSEventsHistoryDone != 0 { + w.flushed = true + continue + } + if !w.flushed { + continue + } + dbgprintf("%v (0x%x) (%s, i=%d, ID=%d, len=%d)\n", Event(ev[i].Flags), + ev[i].Flags, ev[i].Path, i, ev[i].ID, len(ev)) + if ev[i].Flags&failure != 0 { + // TODO(rjeczalik): missing error handling + panic("unhandled error: " + Event(ev[i].Flags).String()) + } + if !strings.HasPrefix(ev[i].Path, w.path) { + continue + } + n := len(w.path) + base := "" + if len(ev[i].Path) > n { + if ev[i].Path[n] != '/' { + continue + } + base = ev[i].Path[n+1:] + if !isrec && strings.IndexByte(base, '/') != -1 { + continue + } + } + // TODO(rjeczalik): get diff only from filtered events? + e := w.strip(string(base), ev[i].Flags) & events + if e == 0 { + continue + } + for _, e := range splitflags(e) { + dbgprintf("%d: single event: %v", ev[i].ID, Event(e)) + w.c <- &event{ + fse: ev[i], + event: Event(e), + } + } + } +} + +// Stop closes underlying FSEvents stream and stops dispatching events. +func (w *watch) Stop() { + w.stream.Stop() + // TODO(rjeczalik): make (*stream).Stop flush synchronously undelivered events, + // so the following hack can be removed. It should flush all the streams + // concurrently as we care not to block too much here. + atomic.StoreUint32(&w.events, 0) + atomic.StoreInt32(&w.isrec, 0) +} + +// fsevents implements Watcher and RecursiveWatcher interfaces backed by FSEvents +// framework. +type fsevents struct { + watches map[string]*watch + c chan<- EventInfo +} + +func newWatcher(c chan<- EventInfo) watcher { + return &fsevents{ + watches: make(map[string]*watch), + c: c, + } +} + +func (fse *fsevents) watch(path string, event Event, isrec int32) (err error) { + if path, err = canonical(path); err != nil { + return err + } + if _, ok := fse.watches[path]; ok { + return errAlreadyWatched + } + w := &watch{ + prev: make(map[string]uint32), + c: fse.c, + path: path, + events: uint32(event), + isrec: isrec, + } + w.stream = newStream(path, w.Dispatch) + if err = w.stream.Start(); err != nil { + return err + } + fse.watches[path] = w + return nil +} + +func (fse *fsevents) unwatch(path string) (err error) { + if path, err = canonical(path); err != nil { + return + } + w, ok := fse.watches[path] + if !ok { + return errNotWatched + } + w.stream.Stop() + delete(fse.watches, path) + return nil +} + +// Watch implements Watcher interface. It fails with non-nil error when setting +// the watch-point by FSEvents fails or with errAlreadyWatched error when +// the given path is already watched. +func (fse *fsevents) Watch(path string, event Event) error { + return fse.watch(path, event, 0) +} + +// Unwatch implements Watcher interface. It fails with errNotWatched when +// the given path is not being watched. +func (fse *fsevents) Unwatch(path string) error { + return fse.unwatch(path) +} + +// Rewatch implements Watcher interface. It fails with errNotWatched when +// the given path is not being watched or with errInvalidEventSet when oldevent +// does not match event set the watch-point currently holds. +func (fse *fsevents) Rewatch(path string, oldevent, newevent Event) error { + w, ok := fse.watches[path] + if !ok { + return errNotWatched + } + if !atomic.CompareAndSwapUint32(&w.events, uint32(oldevent), uint32(newevent)) { + return errInvalidEventSet + } + atomic.StoreInt32(&w.isrec, 0) + return nil +} + +// RecursiveWatch implements RecursiveWatcher interface. It fails with non-nil +// error when setting the watch-point by FSEvents fails or with errAlreadyWatched +// error when the given path is already watched. +func (fse *fsevents) RecursiveWatch(path string, event Event) error { + return fse.watch(path, event, 1) +} + +// RecursiveUnwatch implements RecursiveWatcher interface. It fails with +// errNotWatched when the given path is not being watched. +// +// TODO(rjeczalik): fail if w.isrec == 0? +func (fse *fsevents) RecursiveUnwatch(path string) error { + return fse.unwatch(path) +} + +// RecrusiveRewatch implements RecursiveWatcher interface. It fails: +// +// * with errNotWatched when the given path is not being watched +// * with errInvalidEventSet when oldevent does not match the current event set +// * with errAlreadyWatched when watch-point given by the oldpath was meant to +// be relocated to newpath, but the newpath is already watched +// * a non-nil error when setting the watch-point with FSEvents fails +// +// TODO(rjeczalik): Improve handling of watch-point relocation? See two TODOs +// that follows. +func (fse *fsevents) RecursiveRewatch(oldpath, newpath string, oldevent, newevent Event) error { + switch [2]bool{oldpath == newpath, oldevent == newevent} { + case [2]bool{true, true}: + w, ok := fse.watches[oldpath] + if !ok { + return errNotWatched + } + atomic.StoreInt32(&w.isrec, 1) + return nil + case [2]bool{true, false}: + w, ok := fse.watches[oldpath] + if !ok { + return errNotWatched + } + if !atomic.CompareAndSwapUint32(&w.events, uint32(oldevent), uint32(newevent)) { + return errors.New("invalid event state diff") + } + atomic.StoreInt32(&w.isrec, 1) + return nil + default: + // TODO(rjeczalik): rewatch newpath only if exists? + // TODO(rjeczalik): migrate w.prev to new watch? + if _, ok := fse.watches[newpath]; ok { + return errAlreadyWatched + } + if err := fse.Unwatch(oldpath); err != nil { + return err + } + // TODO(rjeczalik): revert unwatch if watch fails? + return fse.watch(newpath, newevent, 1) + } +} + +// Close unwatches all watch-points. +func (fse *fsevents) Close() error { + for _, w := range fse.watches { + w.Stop() + } + fse.watches = nil + return nil +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher_fsevents_cgo.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher_fsevents_cgo.go new file mode 100644 index 0000000..c88819e --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher_fsevents_cgo.go @@ -0,0 +1,156 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build darwin,!kqueue + +package notify + +// #include +// +// typedef void (*CFRunLoopPerformCallBack)(void*); +// +// void gosource(void *); +// void gostream(void*, void*, size_t, uintptr_t, uintptr_t, uintptr_t); +// +// #cgo LDFLAGS: -framework CoreServices +import "C" + +import ( + "errors" + "os" + "sync" + "sync/atomic" + "time" + "unsafe" +) + +var nilstream C.FSEventStreamRef + +// Default arguments for FSEventStreamCreate function. +var ( + latency C.CFTimeInterval + flags = C.FSEventStreamCreateFlags(C.kFSEventStreamCreateFlagFileEvents | C.kFSEventStreamCreateFlagNoDefer) + since = uint64(C.FSEventsGetCurrentEventId()) +) + +var runloop C.CFRunLoopRef // global runloop which all streams are registered with +var wg sync.WaitGroup // used to wait until the runloop starts + +// source is used for synchronization purposes - it signals when runloop has +// started and is ready via the wg. It also serves purpose of a dummy source, +// thanks to it the runloop does not return as it also has at least one source +// registered. +var source = C.CFRunLoopSourceCreate(nil, 0, &C.CFRunLoopSourceContext{ + perform: (C.CFRunLoopPerformCallBack)(C.gosource), +}) + +// Errors returned when FSEvents functions fail. +var ( + errCreate = os.NewSyscallError("FSEventStreamCreate", errors.New("NULL")) + errStart = os.NewSyscallError("FSEventStreamStart", errors.New("false")) +) + +// initializes the global runloop and ensures any created stream awaits its +// readiness. +func init() { + wg.Add(1) + go func() { + runloop = C.CFRunLoopGetCurrent() + C.CFRunLoopAddSource(runloop, source, C.kCFRunLoopDefaultMode) + C.CFRunLoopRun() + panic("runloop has just unexpectedly stopped") + }() + C.CFRunLoopSourceSignal(source) +} + +//export gosource +func gosource(unsafe.Pointer) { + time.Sleep(time.Second) + wg.Done() +} + +//export gostream +func gostream(_, ctx unsafe.Pointer, n C.size_t, paths, flags, ids uintptr) { + const ( + offchar = unsafe.Sizeof((*C.char)(nil)) + offflag = unsafe.Sizeof(C.FSEventStreamEventFlags(0)) + offid = unsafe.Sizeof(C.FSEventStreamEventId(0)) + ) + if n == 0 { + return + } + ev := make([]FSEvent, 0, int(n)) + for i := uintptr(0); i < uintptr(n); i++ { + switch flags := *(*uint32)(unsafe.Pointer((flags + i*offflag))); { + case flags&uint32(FSEventsEventIdsWrapped) != 0: + atomic.StoreUint64(&since, uint64(C.FSEventsGetCurrentEventId())) + default: + ev = append(ev, FSEvent{ + Path: C.GoString(*(**C.char)(unsafe.Pointer(paths + i*offchar))), + Flags: flags, + ID: *(*uint64)(unsafe.Pointer(ids + i*offid)), + }) + } + + } + (*(*streamFunc)(ctx))(ev) +} + +// StreamFunc is a callback called when stream receives file events. +type streamFunc func([]FSEvent) + +// Stream represents single watch-point which listens for events scheduled by +// the global runloop. +type stream struct { + path string + ref C.FSEventStreamRef + ctx C.FSEventStreamContext +} + +// NewStream creates a stream for given path, listening for file events and +// calling fn upon receving any. +func newStream(path string, fn streamFunc) *stream { + return &stream{ + path: path, + ctx: C.FSEventStreamContext{ + info: unsafe.Pointer(&fn), + }, + } +} + +// Start creates a FSEventStream for the given path and schedules it with +// global runloop. It's a nop if the stream was already started. +func (s *stream) Start() error { + if s.ref != nilstream { + return nil + } + wg.Wait() + p := C.CFStringCreateWithCStringNoCopy(nil, C.CString(s.path), C.kCFStringEncodingUTF8, nil) + path := C.CFArrayCreate(nil, (*unsafe.Pointer)(unsafe.Pointer(&p)), 1, nil) + ref := C.FSEventStreamCreate(nil, (C.FSEventStreamCallback)(C.gostream), + &s.ctx, path, C.FSEventStreamEventId(atomic.LoadUint64(&since)), latency, flags) + if ref == nilstream { + return errCreate + } + C.FSEventStreamScheduleWithRunLoop(ref, runloop, C.kCFRunLoopDefaultMode) + if C.FSEventStreamStart(ref) == C.Boolean(0) { + C.FSEventStreamInvalidate(ref) + return errStart + } + C.CFRunLoopWakeUp(runloop) + s.ref = ref + return nil +} + +// Stop stops underlying FSEventStream and unregisters it from global runloop. +func (s *stream) Stop() { + if s.ref == nilstream { + return + } + wg.Wait() + C.FSEventStreamStop(s.ref) + C.FSEventStreamInvalidate(s.ref) + C.CFRunLoopWakeUp(runloop) + s.ref = nilstream +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher_fsevents_test.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher_fsevents_test.go new file mode 100644 index 0000000..1d60877 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher_fsevents_test.go @@ -0,0 +1,111 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build darwin,!kqueue + +package notify + +import ( + "reflect" + "testing" +) + +func TestSplitflags(t *testing.T) { + cases := [...]struct { + set uint32 + flags []uint32 + }{ + {0, nil}, + {0xD, []uint32{0x1, 0x4, 0x8}}, + {0x0010 | 0x0040 | 0x0080 | 0x01000, []uint32{0x0010, 0x0040, 0x0080, 0x01000}}, + {0x40000 | 0x00100 | 0x00200, []uint32{0x00100, 0x00200, 0x40000}}, + } + for i, cas := range cases { + if flags := splitflags(cas.set); !reflect.DeepEqual(flags, cas.flags) { + t.Errorf("want flags=%v; got %v (i=%d)", cas.flags, flags, i) + } + } +} + +func TestWatchStrip(t *testing.T) { + const ( + create = uint32(FSEventsCreated) + remove = uint32(FSEventsRemoved) + rename = uint32(FSEventsRenamed) + write = uint32(FSEventsModified) + inode = uint32(FSEventsInodeMetaMod) + owner = uint32(FSEventsChangeOwner) + ) + cases := [...][]struct { + path string + flag uint32 + diff uint32 + }{ + // 1. + { + {"file", create | write, create | write}, + {"file", create | write | inode, write | inode}, + }, + // 2. + { + {"file", create, create}, + {"file", create | remove, remove}, + {"file", create | remove, create}, + }, + // 3. + { + {"file", create | write, create | write}, + {"file", create | write | owner, write | owner}, + }, + // 4. + { + {"file", create | write, create | write}, + {"file", write | inode, write | inode}, + {"file", remove | write | inode, remove}, + }, + { + {"file", remove | write | inode, remove}, + }, + } +Test: + for i, cas := range cases { + if len(cas) == 0 { + t.Log("skipped") + continue + } + w := &watch{prev: make(map[string]uint32)} + for j, cas := range cas { + if diff := w.strip(cas.path, cas.flag); diff != cas.diff { + t.Errorf("want diff=%v; got %v (i=%d, j=%d)", Event(cas.diff), + Event(diff), i, j) + continue Test + } + } + } +} + +// Test for cases 3) and 5) with shadowed write&create events. +// +// See comment for (flagdiff).diff method. +func TestWatcherShadowedWriteCreate(t *testing.T) { + w := NewWatcherTest(t, "testdata/vfs.txt") + defer w.Close() + + cases := [...]WCase{ + // i=0 + create(w, "src/github.com/rjeczalik/fs/.fs.go.swp"), + // i=1 + write(w, "src/github.com/rjeczalik/fs/.fs.go.swp", []byte("XD")), + // i=2 + write(w, "src/github.com/rjeczalik/fs/.fs.go.swp", []byte("XD")), + // i=3 + remove(w, "src/github.com/rjeczalik/fs/.fs.go.swp"), + // i=4 + create(w, "src/github.com/rjeczalik/fs/.fs.go.swp"), + // i=5 + write(w, "src/github.com/rjeczalik/fs/.fs.go.swp", []byte("XD")), + } + + w.ExpectAny(cases[:5]) // BUG(rjeczalik): #62 +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher_inotify.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher_inotify.go new file mode 100644 index 0000000..8c0c27b --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher_inotify.go @@ -0,0 +1,396 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build linux + +package notify + +import ( + "bytes" + "errors" + "path/filepath" + "runtime" + "sync" + "sync/atomic" + "syscall" + "unsafe" +) + +// eventBufferSize defines the size of the buffer given to read(2) function. One +// should not depend on this value, since it was arbitrary chosen and may be +// changed in the future. +const eventBufferSize = 64 * (syscall.SizeofInotifyEvent + syscall.PathMax + 1) + +// consumersCount defines the number of consumers in producer-consumer based +// implementation. Each consumer is run in a separate goroutine and has read +// access to watched files map. +const consumersCount = 2 + +const invalidDescriptor = -1 + +// watched is a pair of file path and inotify mask used as a value in +// watched files map. +type watched struct { + path string + mask uint32 +} + +// inotify implements Watcher interface. +type inotify struct { + sync.RWMutex // protects inotify.m map + m map[int32]*watched // watch descriptor to watched object + fd int32 // inotify file descriptor + pipefd []int // pipe's read and write descriptors + epfd int // epoll descriptor + epes []syscall.EpollEvent // epoll events + buffer [eventBufferSize]byte // inotify event buffer + wg sync.WaitGroup // wait group used to close main loop + c chan<- EventInfo // event dispatcher channel +} + +// NewWatcher creates new non-recursive inotify backed by inotify. +func newWatcher(c chan<- EventInfo) watcher { + i := &inotify{ + m: make(map[int32]*watched), + fd: invalidDescriptor, + pipefd: []int{invalidDescriptor, invalidDescriptor}, + epfd: invalidDescriptor, + epes: make([]syscall.EpollEvent, 0), + c: c, + } + runtime.SetFinalizer(i, func(i *inotify) { + i.epollclose() + if i.fd != invalidDescriptor { + syscall.Close(int(i.fd)) + } + }) + return i +} + +// Watch implements notify.watcher interface. +func (i *inotify) Watch(path string, e Event) error { + return i.watch(path, e) +} + +// Rewatch implements notify.watcher interface. +func (i *inotify) Rewatch(path string, _, newevent Event) error { + return i.watch(path, newevent) +} + +// watch adds a new watcher to the set of watched objects or modifies the existing +// one. If called for the first time, this function initializes inotify filesystem +// monitor and starts producer-consumers goroutines. +func (i *inotify) watch(path string, e Event) (err error) { + if e&^(All|Event(syscall.IN_ALL_EVENTS)) != 0 { + return errors.New("notify: unknown event") + } + if err = i.lazyinit(); err != nil { + return + } + iwd, err := syscall.InotifyAddWatch(int(i.fd), path, encode(e)) + if err != nil { + return + } + i.RLock() + wd := i.m[int32(iwd)] + i.RUnlock() + if wd == nil { + i.Lock() + if i.m[int32(iwd)] == nil { + i.m[int32(iwd)] = &watched{path: path, mask: uint32(e)} + } + i.Unlock() + } else { + i.Lock() + wd.mask = uint32(e) + i.Unlock() + } + return nil +} + +// lazyinit sets up all required file descriptors and starts 1+consumersCount +// goroutines. The producer goroutine blocks until file-system notifications +// occur. Then, all events are read from system buffer and sent to consumer +// goroutines which construct valid notify events. This method uses +// Double-Checked Locking optimization. +func (i *inotify) lazyinit() error { + if atomic.LoadInt32(&i.fd) == invalidDescriptor { + i.Lock() + defer i.Unlock() + if atomic.LoadInt32(&i.fd) == invalidDescriptor { + fd, err := syscall.InotifyInit() + if err != nil { + return err + } + i.fd = int32(fd) + if err = i.epollinit(); err != nil { + _, _ = i.epollclose(), syscall.Close(int(fd)) // Ignore errors. + i.fd = invalidDescriptor + return err + } + esch := make(chan []*event) + go i.loop(esch) + i.wg.Add(consumersCount) + for n := 0; n < consumersCount; n++ { + go i.send(esch) + } + } + } + return nil +} + +// epollinit opens an epoll file descriptor and creates a pipe which will be +// used to wake up the epoll_wait(2) function. Then, file descriptor associated +// with inotify event queue and the read end of the pipe are added to epoll set. +// Note that `fd` member must be set before this function is called. +func (i *inotify) epollinit() (err error) { + if i.epfd, err = syscall.EpollCreate(2); err != nil { + return + } + if err = syscall.Pipe(i.pipefd); err != nil { + return + } + i.epes = []syscall.EpollEvent{ + {Events: syscall.EPOLLIN, Fd: i.fd}, + {Events: syscall.EPOLLIN, Fd: int32(i.pipefd[0])}, + } + if err = syscall.EpollCtl(i.epfd, syscall.EPOLL_CTL_ADD, int(i.fd), &i.epes[0]); err != nil { + return + } + return syscall.EpollCtl(i.epfd, syscall.EPOLL_CTL_ADD, i.pipefd[0], &i.epes[1]) +} + +// epollclose closes the file descriptor created by the call to epoll_create(2) +// and two file descriptors opened by pipe(2) function. +func (i *inotify) epollclose() (err error) { + if i.epfd != invalidDescriptor { + if err = syscall.Close(i.epfd); err == nil { + i.epfd = invalidDescriptor + } + } + for n, fd := range i.pipefd { + if fd != invalidDescriptor { + switch e := syscall.Close(fd); { + case e != nil && err == nil: + err = e + case e == nil: + i.pipefd[n] = invalidDescriptor + } + } + } + return +} + +// loop blocks until either inotify or pipe file descriptor is ready for I/O. +// All read operations triggered by filesystem notifications are forwarded to +// one of the event's consumers. If pipe fd became ready, loop function closes +// all file descriptors opened by lazyinit method and returns afterwards. +func (i *inotify) loop(esch chan<- []*event) { + epes := make([]syscall.EpollEvent, 1) + fd := atomic.LoadInt32(&i.fd) + for { + switch _, err := syscall.EpollWait(i.epfd, epes, -1); err { + case nil: + switch epes[0].Fd { + case fd: + esch <- i.read() + epes[0].Fd = 0 + case int32(i.pipefd[0]): + i.Lock() + defer i.Unlock() + if err = syscall.Close(int(fd)); err != nil && err != syscall.EINTR { + panic("notify: close(2) error " + err.Error()) + } + atomic.StoreInt32(&i.fd, invalidDescriptor) + if err = i.epollclose(); err != nil && err != syscall.EINTR { + panic("notify: epollclose error " + err.Error()) + } + close(esch) + return + } + case syscall.EINTR: + continue + default: // We should never reach this line. + panic("notify: epoll_wait(2) error " + err.Error()) + } + } +} + +// read reads events from an inotify file descriptor. It does not handle errors +// returned from read(2) function since they are not critical to watcher logic. +func (i *inotify) read() (es []*event) { + n, err := syscall.Read(int(i.fd), i.buffer[:]) + if err != nil || n < syscall.SizeofInotifyEvent { + return + } + var sys *syscall.InotifyEvent + nmin := n - syscall.SizeofInotifyEvent + for pos, path := 0, ""; pos <= nmin; { + sys = (*syscall.InotifyEvent)(unsafe.Pointer(&i.buffer[pos])) + pos += syscall.SizeofInotifyEvent + if path = ""; sys.Len > 0 { + endpos := pos + int(sys.Len) + path = string(bytes.TrimRight(i.buffer[pos:endpos], "\x00")) + pos = endpos + } + es = append(es, &event{ + sys: syscall.InotifyEvent{ + Wd: sys.Wd, + Mask: sys.Mask, + Cookie: sys.Cookie, + }, + path: path, + }) + } + return +} + +// send is a consumer function which sends events to event dispatcher channel. +// It is run in a separate goroutine in order to not block loop method when +// possibly expensive write operations are performed on inotify map. +func (i *inotify) send(esch <-chan []*event) { + for es := range esch { + for _, e := range i.transform(es) { + if e != nil { + i.c <- e + } + } + } + i.wg.Done() +} + +// transform prepares events read from inotify file descriptor for sending to +// user. It removes invalid events and these which are no longer present in +// inotify map. This method may also split one raw event into two different ones +// when system-dependent result is required. +func (i *inotify) transform(es []*event) []*event { + var multi []*event + i.RLock() + for idx, e := range es { + if e.sys.Mask&(syscall.IN_IGNORED|syscall.IN_Q_OVERFLOW) != 0 { + es[idx] = nil + continue + } + wd, ok := i.m[e.sys.Wd] + if !ok || e.sys.Mask&encode(Event(wd.mask)) == 0 { + es[idx] = nil + continue + } + if e.path == "" { + e.path = wd.path + } else { + e.path = filepath.Join(wd.path, e.path) + } + multi = append(multi, decode(Event(wd.mask), e)) + if e.event == 0 { + es[idx] = nil + } + } + i.RUnlock() + es = append(es, multi...) + return es +} + +// encode converts notify system-independent events to valid inotify mask +// which can be passed to inotify_add_watch(2) function. +func encode(e Event) uint32 { + if e&Create != 0 { + e = (e ^ Create) | InCreate | InMovedTo + } + if e&Remove != 0 { + e = (e ^ Remove) | InDelete | InDeleteSelf + } + if e&Write != 0 { + e = (e ^ Write) | InModify + } + if e&Rename != 0 { + e = (e ^ Rename) | InMovedFrom | InMoveSelf + } + return uint32(e) +} + +// decode uses internally stored mask to distinguish whether system-independent +// or system-dependent event is requested. The first one is created by modifying +// `e` argument. decode method sets e.event value to 0 when an event should be +// skipped. System-dependent event is set as the function's return value which +// can be nil when the event should not be passed on. +func decode(mask Event, e *event) (syse *event) { + if sysmask := uint32(mask) & e.sys.Mask; sysmask != 0 { + syse = &event{sys: syscall.InotifyEvent{ + Wd: e.sys.Wd, + Mask: e.sys.Mask, + Cookie: e.sys.Cookie, + }, event: Event(sysmask), path: e.path} + } + imask := encode(mask) + switch { + case mask&Create != 0 && imask&uint32(InCreate|InMovedTo)&e.sys.Mask != 0: + e.event = Create + case mask&Remove != 0 && imask&uint32(InDelete|InDeleteSelf)&e.sys.Mask != 0: + e.event = Remove + case mask&Write != 0 && imask&uint32(InModify)&e.sys.Mask != 0: + e.event = Write + case mask&Rename != 0 && imask&uint32(InMovedFrom|InMoveSelf)&e.sys.Mask != 0: + e.event = Rename + default: + e.event = 0 + } + return +} + +// Unwatch implements notify.watcher interface. It looks for watch descriptor +// related to registered path and if found, calls inotify_rm_watch(2) function. +// This method is allowed to return EINVAL error when concurrently requested to +// delete identical path. +func (i *inotify) Unwatch(path string) (err error) { + iwd := int32(invalidDescriptor) + i.RLock() + for iwdkey, wd := range i.m { + if wd.path == path { + iwd = iwdkey + break + } + } + i.RUnlock() + if iwd == invalidDescriptor { + return errors.New("notify: path " + path + " is already watched") + } + fd := atomic.LoadInt32(&i.fd) + if _, err = syscall.InotifyRmWatch(int(fd), uint32(iwd)); err != nil { + return + } + i.Lock() + delete(i.m, iwd) + i.Unlock() + return nil +} + +// Close implements notify.watcher interface. It removes all existing watch +// descriptors and wakes up producer goroutine by sending data to the write end +// of the pipe. The function waits for a signal from producer which means that +// all operations on current monitoring instance are done. +func (i *inotify) Close() (err error) { + i.Lock() + if fd := atomic.LoadInt32(&i.fd); fd == invalidDescriptor { + i.Unlock() + return nil + } + for iwd := range i.m { + if _, e := syscall.InotifyRmWatch(int(i.fd), uint32(iwd)); e != nil && err == nil { + err = e + } + delete(i.m, iwd) + } + switch _, errwrite := syscall.Write(i.pipefd[1], []byte{0x00}); { + case errwrite != nil && err == nil: + err = errwrite + fallthrough + case errwrite != nil: + i.Unlock() + default: + i.Unlock() + i.wg.Wait() + } + return +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher_inotify_test.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher_inotify_test.go new file mode 100644 index 0000000..5feadaf --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher_inotify_test.go @@ -0,0 +1,132 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build linux + +package notify + +import ( + "os" + "path/filepath" + "testing" +) + +func icreate(w *W, path string) WCase { + cas := create(w, path) + cas.Events = append(cas.Events, + &Call{P: path, E: InCreate}, + ) + return cas +} + +func iremove(w *W, path string) WCase { + cas := remove(w, path) + cas.Events = append(cas.Events, + &Call{P: path, E: InDelete}, + ) + return cas +} + +func iopen(w *W, path string) WCase { + return WCase{ + Action: func() { + f, err := os.OpenFile(filepath.Join(w.root, path), os.O_RDWR, 0644) + if err != nil { + w.Fatalf("OpenFile(%q)=%v", path, err) + } + if err := f.Close(); err != nil { + w.Fatalf("Close(%q)=%v", path, err) + } + }, + Events: []EventInfo{ + &Call{P: path, E: InAccess}, + &Call{P: path, E: InOpen}, + &Call{P: path, E: InCloseNowrite}, + }, + } +} + +func iread(w *W, path string, p []byte) WCase { + return WCase{ + Action: func() { + f, err := os.OpenFile(filepath.Join(w.root, path), os.O_RDWR, 0644) + if err != nil { + w.Fatalf("OpenFile(%q)=%v", path, err) + } + if _, err := f.Read(p); err != nil { + w.Fatalf("Read(%q)=%v", path, err) + } + if err := f.Close(); err != nil { + w.Fatalf("Close(%q)=%v", path, err) + } + }, + Events: []EventInfo{ + &Call{P: path, E: InAccess}, + &Call{P: path, E: InOpen}, + &Call{P: path, E: InModify}, + &Call{P: path, E: InCloseNowrite}, + }, + } +} + +func iwrite(w *W, path string, p []byte) WCase { + cas := write(w, path, p) + path = cas.Events[0].Path() + cas.Events = append(cas.Events, + &Call{P: path, E: InAccess}, + &Call{P: path, E: InOpen}, + &Call{P: path, E: InModify}, + &Call{P: path, E: InCloseWrite}, + ) + return cas +} + +func irename(w *W, path string) WCase { + const ext = ".notify" + return WCase{ + Action: func() { + file := filepath.Join(w.root, path) + if err := os.Rename(file, file+ext); err != nil { + w.Fatalf("Rename(%q, %q)=%v", path, path+ext, err) + } + }, + Events: []EventInfo{ + &Call{P: path, E: InMovedFrom}, + &Call{P: path + ext, E: InMovedTo}, + &Call{P: path, E: InOpen}, + &Call{P: path, E: InAccess}, + &Call{P: path, E: InCreate}, + }, + } +} + +var events = []Event{ + InAccess, + InModify, + InAttrib, + InCloseWrite, + InCloseNowrite, + InOpen, + InMovedFrom, + InMovedTo, + InCreate, + InDelete, + InDeleteSelf, + InMoveSelf, +} + +func TestWatcherInotify(t *testing.T) { + w := NewWatcherTest(t, "testdata/vfs.txt", events...) + defer w.Close() + + cases := [...]WCase{ + iopen(w, "src/github.com/rjeczalik/fs/fs.go"), + iwrite(w, "src/github.com/rjeczalik/fs/fs.go", []byte("XD")), + iread(w, "src/github.com/rjeczalik/fs/fs.go", []byte("XD")), + iremove(w, "src/github.com/ppknap/link/README.md"), + irename(w, "src/github.com/rjeczalik/fs/LICENSE"), + } + + w.ExpectAny(cases[:]) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher_kqueue.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher_kqueue.go new file mode 100644 index 0000000..e7e2e4d --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher_kqueue.go @@ -0,0 +1,413 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build darwin,kqueue dragonfly freebsd netbsd openbsd + +package notify + +import ( + "os" + "path/filepath" + "sync" + "syscall" +) + +// newWatcher returns kqueue Watcher implementation. +func newWatcher(c chan<- EventInfo) watcher { + k := &kqueue{ + idLkp: make(map[int]*watched, 0), + pthLkp: make(map[string]*watched, 0), + c: c, + s: make(chan struct{}, 1), + } + if err := k.init(); err != nil { + panic(err) + } + go k.monitor() + return k +} + +// Close closes all still open file descriptors and kqueue. +func (k *kqueue) Close() (err error) { + // trigger event used to interrup Kevent call. + if _, err = syscall.Write(k.pipefds[1], []byte{0x00}); err != nil { + return + } + <-k.s + k.Lock() + var e error + for _, w := range k.idLkp { + if e = k.unwatch(w.p, w.fi); e != nil && err == nil { + dbgprintf("kqueue: unwatch %q failed: %q", w.p, e) + err = e + } + } + if e := error(syscall.Close(k.fd)); e != nil && err == nil { + dbgprintf("kqueue: closing kqueu fd failed: %q", e) + err = e + } + k.idLkp, k.pthLkp = nil, nil + k.Unlock() + return +} + +// sendEvents sends reported events one by one through chan. +func (k *kqueue) sendEvents(evn []event) { + for i := range evn { + k.c <- &evn[i] + } +} + +// encode converts requested events to kqueue representation. +func encode(e Event) (o uint32) { + o = uint32(e &^ Create) + if e&Write != 0 { + o = (o &^ uint32(Write)) | uint32(NoteWrite) + } + if e&Rename != 0 { + o = (o &^ uint32(Rename)) | uint32(NoteRename) + } + if e&Remove != 0 { + o = (o &^ uint32(Remove)) | uint32(NoteDelete) + } + return +} + +// decode converts event received from kqueue to notify.Event +// representation taking into account requested events (w). +func decode(o uint32, w Event) (e Event) { + // TODO(someone) : fix me + if o&uint32(NoteWrite) != 0 { + if w&NoteWrite != 0 { + e |= NoteWrite + } else { + e |= Write + } + } + if o&uint32(NoteRename) != 0 { + if w&NoteRename != 0 { + e |= NoteRename + } else { + e |= Rename + } + } + if o&uint32(NoteDelete) != 0 { + if w&NoteDelete != 0 { + e |= NoteDelete + } else { + e |= Remove + } + } + e |= Event(o) & w + return +} + +// del closes fd for watched and removes it from internal cache of monitored +// files/directories. +func (k *kqueue) del(w watched) { + syscall.Close(w.fd) + delete(k.idLkp, w.fd) + delete(k.pthLkp, w.p) +} + +// monitor reads reported kqueue events and forwards them further after +// performing additional processing. If read event concerns directory, +// it generates Create/Remove event and sent them further instead of directory +// event. This event is detected based on reading contents of analyzed +// directory. If no changes in file list are detected, no event is send further. +// Reading directory structure is less accurate than kqueue and can lead +// to lack of detection of all events. +func (k *kqueue) monitor() { + var ( + kevn [1]syscall.Kevent_t + n int + err error + ) + for { + kevn[0] = syscall.Kevent_t{} + switch n, err = syscall.Kevent(k.fd, nil, kevn[:], nil); { + case err == syscall.EINTR: + case err != nil: + dbgprintf("kqueue: failed to read events: %q\n", err) + case int(kevn[0].Ident) == k.pipefds[0]: + k.s <- struct{}{} + return + case n > 0: + k.sendEvents(k.process(kevn[0])) + } + } +} + +func (k *kqueue) dir(w watched, kevn syscall.Kevent_t, e Event) (evn []event) { + // If it's dir and delete we have to send it and continue, because + // other processing relies on opening (in this case not existing) dir. + // Events for contents of this dir are reported by kqueue. + if (Event(kevn.Fflags) & NoteDelete) != 0 { + // Write is reported also for Remove on directory. Because of that + // we have to filter it out explicitly. + evn = append(evn, event{w.p, + e & ^Write & ^NoteWrite, Kevent{&kevn, w.fi}}) + k.del(w) + return + } + if (Event(kevn.Fflags) & NoteWrite) != 0 { + switch err := k.walk(w.p, func(fi os.FileInfo) error { + p := filepath.Join(w.p, fi.Name()) + switch err := k.singlewatch(p, w.eDir, false, fi); { + case os.IsNotExist(err) && ((w.eDir & Remove) != 0): + evn = append(evn, event{p, Remove, Kevent{nil, fi}}) + case err == errAlreadyWatched: + case err != nil: + dbgprintf("kqueue: watching %q failed: %q", p, err) + case (w.eDir & Create) != 0: + evn = append(evn, event{p, Create, Kevent{nil, fi}}) + } + return nil + }); { + // If file is already watched, kqueue will return remove event. + case os.IsNotExist(err): + return + case err != nil: + dbgprintf("kqueue: dir processing failed: %q", err) + default: + } + } + return +} + +func (*kqueue) file(w watched, kevn syscall.Kevent_t, e Event) (evn []event) { + evn = append(evn, event{w.p, e, Kevent{&kevn, w.fi}}) + return +} + +// process event returned by Kevent call. +func (k *kqueue) process(kevn syscall.Kevent_t) (evn []event) { + k.Lock() + w := k.idLkp[int(kevn.Ident)] + if w == nil { + k.Unlock() + dbgprintf("kqueue: %v event for not registered fd", kevn) + return + } + e := decode(kevn.Fflags, w.eDir|w.eNonDir) + if w.fi.IsDir() { + evn = k.dir(*w, kevn, e) + } else { + evn = k.file(*w, kevn, e) + } + if (Event(kevn.Fflags) & NoteDelete) != 0 { + k.del(*w) + } + k.Unlock() + return +} + +// kqueue is a type holding data for kqueue watcher. +type kqueue struct { + sync.Mutex + // fd is a kqueue file descriptor + fd int + // pipefds are file descriptors used to stop `Kevent` call. + pipefds [2]int + // idLkp is a data structure mapping file descriptors with data about watching + // represented by them files/directories. + idLkp map[int]*watched + // pthLkp is a data structure mapping file names with data about watching + // represented by them files/directories. + pthLkp map[string]*watched + // c is a channel used to pass events further. + c chan<- EventInfo + // s is a channel used to stop monitoring. + s chan struct{} +} + +// watched is a data structure representing watched file/directory. +type watched struct { + // p is a path to watched file/directory. + p string + // fd is a file descriptor for watched file/directory. + fd int + // fi provides information about watched file/dir. + fi os.FileInfo + // eDir represents events watched directly. + eDir Event + // eNonDir represents events watched indirectly. + eNonDir Event +} + +// init initializes kqueue. +func (k *kqueue) init() (err error) { + if k.fd, err = syscall.Kqueue(); err != nil { + return + } + // Creates pipe used to stop `Kevent` call by registering it, + // watching read end and writing to other end of it. + if err = syscall.Pipe(k.pipefds[:]); err != nil { + return + } + var kevn [1]syscall.Kevent_t + syscall.SetKevent(&kevn[0], k.pipefds[0], syscall.EVFILT_READ, syscall.EV_ADD) + _, err = syscall.Kevent(k.fd, kevn[:], nil, nil) + return +} + +func (k *kqueue) watch(p string, e Event, fi os.FileInfo) error { + if err := k.singlewatch(p, e, true, fi); err != nil { + if err != errAlreadyWatched { + return nil + } + } + if fi.IsDir() { + err := k.walk(p, func(fi os.FileInfo) (err error) { + if err = k.singlewatch(filepath.Join(p, fi.Name()), e, false, + fi); err != nil { + if err != errAlreadyWatched { + return + } + } + return nil + }) + if err != nil { + return err + } + } + return nil +} + +// watch starts to watch given p file/directory. +func (k *kqueue) singlewatch(p string, e Event, direct bool, + fi os.FileInfo) error { + w, ok := k.pthLkp[p] + if !ok { + fd, err := syscall.Open(p, syscall.O_NONBLOCK|syscall.O_RDONLY, 0) + if err != nil { + return err + } + w = &watched{fd: fd, p: p, fi: fi} + } + if direct { + w.eDir |= e + } else { + w.eNonDir |= e + } + var kevn [1]syscall.Kevent_t + syscall.SetKevent(&kevn[0], w.fd, syscall.EVFILT_VNODE, + syscall.EV_ADD|syscall.EV_CLEAR) + kevn[0].Fflags = encode(w.eDir | w.eNonDir) + if _, err := syscall.Kevent(k.fd, kevn[:], nil, nil); err != nil { + return err + } + if !ok { + k.idLkp[w.fd], k.pthLkp[w.p] = w, w + return nil + } + return errAlreadyWatched +} + +// unwatch stops watching p file/directory. +func (k *kqueue) singleunwatch(p string, direct bool) error { + w := k.pthLkp[p] + if w == nil { + return errNotWatched + } + if direct { + w.eDir = 0 + } else { + w.eNonDir = 0 + } + var kevn [1]syscall.Kevent_t + syscall.SetKevent(&kevn[0], w.fd, syscall.EVFILT_VNODE, syscall.EV_DELETE) + if _, err := syscall.Kevent(k.fd, kevn[:], nil, nil); err != nil { + return err + } + if w.eNonDir&w.eDir != 0 { + if err := k.singlewatch(p, w.eNonDir|w.eDir, w.eNonDir == 0, + w.fi); err != nil { + return err + } + } else { + k.del(*w) + } + return nil +} + +// walk runs f func on each file/dir from p directory. +func (k *kqueue) walk(p string, f func(os.FileInfo) error) error { + fp, err := os.Open(p) + if err != nil { + return err + } + ls, err := fp.Readdir(0) + fp.Close() + if err != nil { + return err + } + for i := range ls { + if err := f(ls[i]); err != nil { + return err + } + } + return nil +} + +func (k *kqueue) unwatch(p string, fi os.FileInfo) error { + if fi.IsDir() { + err := k.walk(p, func(fi os.FileInfo) error { + if !fi.IsDir() { + err := k.singleunwatch(filepath.Join(p, fi.Name()), false) + if err != errNotWatched { + return err + } + return nil + } + return nil + }) + if err != nil { + return err + } + } + return k.singleunwatch(p, true) +} + +// Watch implements Watcher interface. +func (k *kqueue) Watch(p string, e Event) error { + fi, err := os.Stat(p) + if err != nil { + return err + } + k.Lock() + err = k.watch(p, e, fi) + k.Unlock() + return nil +} + +// Unwatch implements Watcher interface. +func (k *kqueue) Unwatch(p string) error { + fi, err := os.Stat(p) + if err != nil { + return err + } + k.Lock() + err = k.unwatch(p, fi) + k.Unlock() + return nil +} + +// Rewatch implements Watcher interface. +// +// TODO(rjeczalik): This is a naive hack. Rewrite might help. +func (k *kqueue) Rewatch(p string, _, e Event) error { + fi, err := os.Stat(p) + if err != nil { + return err + } + k.Lock() + if err = k.unwatch(p, fi); err == nil { + // TODO(rjeczalik): If watch fails then we leave kqueue in inconsistent + // state. Handle? Panic? Native version of rewatch? + err = k.watch(p, e, fi) + } + k.Unlock() + return nil +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher_kqueue_test.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher_kqueue_test.go new file mode 100644 index 0000000..b4a57e8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher_kqueue_test.go @@ -0,0 +1,72 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build ignore + +// TODO(rjeczalik): rewrite with test_test.go + +package notify_test + +import ( + "os" + "os/exec" + "testing" + + "github.com/rjeczalik/notify" + "github.com/rjeczalik/notify/test" +) + +var kqueueActions = test.Actions{ + notify.NoteAttrib: func(p string) (err error) { + _, err = exec.Command("touch", p).CombinedOutput() + return + }, + notify.NoteExtend: func(p string) (err error) { + //TODO: Extend and no Write + var f *os.File + if f, err = os.OpenFile(p, os.O_RDWR|os.O_APPEND, 0755); err != nil { + return + } + defer f.Close() + if _, err = f.WriteString(" "); err != nil { + return + } + return + }, + notify.NoteLink: func(p string) error { + return os.Link(p, p+"link") + }, + /*TODO: notify.NoteRevoke: func(p string) error { + if err := syscall.Revoke(p); err != nil { + fmt.Printf("this is the error %v\n", err) + return err + } + return nil + },*/ +} + +func ExpectKqueueEvents(t *testing.T, wr notify.watcher, e notify.Event, + ei map[notify.EventInfo][]notify.Event) { + w := test.W(t, kqueueActions) + defer w.Close() + if err := w.WatchAll(wr, e); err != nil { + t.Fatal(err) + } + defer w.UnwatchAll(wr) + w.ExpectEvents(wr, ei) +} + +func TestKqueue(t *testing.T) { + ei := map[notify.EventInfo][]notify.Event{ + test.EI("github.com/rjeczalik/fs/fs.go", + notify.NoteAttrib): {notify.NoteAttrib}, + test.EI("github.com/rjeczalik/fs/fs.go", + notify.NoteExtend): {notify.NoteExtend | notify.NoteWrite}, + test.EI("github.com/rjeczalik/fs/fs.go", + notify.NoteLink): {notify.NoteLink}, + } + ExpectKqueueEvents(t, notify.NewWatcher(), notify.NoteDelete| + notify.NoteWrite|notify.NoteExtend|notify.NoteAttrib|notify.NoteLink| + notify.NoteRename|notify.NoteRevoke, ei) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher_readdcw.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher_readdcw.go new file mode 100644 index 0000000..e8359bb --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher_readdcw.go @@ -0,0 +1,574 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build windows + +package notify + +import ( + "errors" + "runtime" + "sync" + "sync/atomic" + "syscall" + "unsafe" +) + +// readBufferSize defines the size of an array in which read statuses are stored. +// The buffer have to be DWORD-aligned and, if notify is used in monitoring a +// directory over the network, its size must not be greater than 64KB. Each of +// watched directories uses its own buffer for storing events. +const readBufferSize = 4096 + +// Since all operations which go through the Windows completion routine are done +// asynchronously, filter may set one of the constants belor. They were defined +// in order to distinguish whether current folder should be re-registered in +// ReadDirectoryChangesW function or some control operations need to be executed. +const ( + stateRewatch uint32 = 1 << (28 + iota) + stateUnwatch + stateCPClose +) + +// Filter used in current implementation was split into four segments: +// - bits 0-11 store ReadDirectoryChangesW filters, +// - bits 12-19 store File notify actions, +// - bits 20-27 store notify specific events and flags, +// - bits 28-31 store states which are used in loop's FSM. +// Constants below are used as masks to retrieve only specific filter parts. +const ( + onlyNotifyChanges uint32 = 0x00000FFF + onlyNGlobalEvents uint32 = 0x0FF00000 + onlyMachineStates uint32 = 0xF0000000 +) + +// grip represents a single watched directory. It stores the data required by +// ReadDirectoryChangesW function. Only the filter, recursive, and handle members +// may by modified by watcher implementation. Rest of the them have to remain +// constant since they are used by Windows completion routine. This indicates that +// grip can be removed only when all operations on the file handle are finished. +type grip struct { + handle syscall.Handle + filter uint32 + recursive bool + pathw []uint16 + buffer [readBufferSize]byte + parent *watched + ovlapped *overlappedEx +} + +// overlappedEx stores information used in asynchronous input and output. +// Additionally, overlappedEx contains a pointer to 'grip' item which is used in +// order to gather the structure in which the overlappedEx object was created. +type overlappedEx struct { + syscall.Overlapped + parent *grip +} + +// newGrip creates a new file handle that can be used in overlapped operations. +// Then, the handle is associated with I/O completion port 'cph' and its value +// is stored in newly created 'grip' object. +func newGrip(cph syscall.Handle, parent *watched, filter uint32) (*grip, error) { + g := &grip{ + handle: syscall.InvalidHandle, + filter: filter, + recursive: parent.recursive, + pathw: parent.pathw, + parent: parent, + ovlapped: &overlappedEx{}, + } + if err := g.register(cph); err != nil { + return nil, err + } + g.ovlapped.parent = g + return g, nil +} + +// NOTE : Thread safe +func (g *grip) register(cph syscall.Handle) (err error) { + if g.handle, err = syscall.CreateFile( + &g.pathw[0], + syscall.FILE_LIST_DIRECTORY, + syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, + nil, + syscall.OPEN_EXISTING, + syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, + 0, + ); err != nil { + return + } + if _, err = syscall.CreateIoCompletionPort(g.handle, cph, 0, 0); err != nil { + syscall.CloseHandle(g.handle) + return + } + return g.readDirChanges() +} + +// readDirChanges tells the system to store file change information in grip's +// buffer. Directory changes that occur between calls to this function are added +// to the buffer and then, returned with the next call. +func (g *grip) readDirChanges() error { + return syscall.ReadDirectoryChanges( + g.handle, + &g.buffer[0], + uint32(unsafe.Sizeof(g.buffer)), + g.recursive, + encode(g.filter), + nil, + (*syscall.Overlapped)(unsafe.Pointer(g.ovlapped)), + 0, + ) +} + +// encode transforms a generic filter, which contains platform independent and +// implementation specific bit fields, to value that can be used as NotifyFilter +// parameter in ReadDirectoryChangesW function. +func encode(filter uint32) uint32 { + e := Event(filter & (onlyNGlobalEvents | onlyNotifyChanges)) + if e&dirmarker != 0 { + return uint32(FileNotifyChangeDirName) + } + if e&Create != 0 { + e = (e ^ Create) | FileNotifyChangeFileName + } + if e&Remove != 0 { + e = (e ^ Remove) | FileNotifyChangeFileName + } + if e&Write != 0 { + e = (e ^ Write) | FileNotifyChangeAttributes | FileNotifyChangeSize | + FileNotifyChangeCreation | FileNotifyChangeSecurity + } + if e&Rename != 0 { + e = (e ^ Rename) | FileNotifyChangeFileName + } + return uint32(e) +} + +// watched is made in order to check whether an action comes from a directory or +// file. This approach requires two file handlers per single monitored folder. The +// second grip handles actions which include creating or deleting a directory. If +// these processes are not monitored, only the first grip is created. +type watched struct { + filter uint32 + recursive bool + count uint8 + pathw []uint16 + digrip [2]*grip +} + +// newWatched creates a new watched instance. It splits the filter variable into +// two parts. The first part is responsible for watching all events which can be +// created for a file in watched directory structure and the second one watches +// only directory Create/Remove actions. If all operations succeed, the Create +// message is sent to I/O completion port queue for further processing. +func newWatched(cph syscall.Handle, filter uint32, recursive bool, + path string) (wd *watched, err error) { + wd = &watched{ + filter: filter, + recursive: recursive, + } + if wd.pathw, err = syscall.UTF16FromString(path); err != nil { + return + } + if err = wd.recreate(cph); err != nil { + return + } + return wd, nil +} + +// TODO : doc +func (wd *watched) recreate(cph syscall.Handle) (err error) { + filefilter := wd.filter &^ uint32(FileNotifyChangeDirName) + if err = wd.updateGrip(0, cph, filefilter == 0, filefilter); err != nil { + return + } + dirfilter := wd.filter & uint32(FileNotifyChangeDirName|Create|Remove) + if err = wd.updateGrip(1, cph, dirfilter == 0, wd.filter|uint32(dirmarker)); err != nil { + return + } + wd.filter &^= onlyMachineStates + return +} + +// TODO : doc +func (wd *watched) updateGrip(idx int, cph syscall.Handle, reset bool, + newflag uint32) (err error) { + if reset { + wd.digrip[idx] = nil + } else { + if wd.digrip[idx] == nil { + if wd.digrip[idx], err = newGrip(cph, wd, newflag); err != nil { + wd.closeHandle() + return + } + } else { + wd.digrip[idx].filter = newflag + wd.digrip[idx].recursive = wd.recursive + if err = wd.digrip[idx].register(cph); err != nil { + wd.closeHandle() + return + } + } + wd.count++ + } + return +} + +// closeHandle closes handles that are stored in digrip array. Function always +// tries to close all of the handlers before it exits, even when there are errors +// returned from the operating system kernel. +func (wd *watched) closeHandle() (err error) { + for _, g := range wd.digrip { + if g != nil && g.handle != syscall.InvalidHandle { + switch suberr := syscall.CloseHandle(g.handle); { + case suberr == nil: + g.handle = syscall.InvalidHandle + case err == nil: + err = suberr + } + } + } + return +} + +// watcher implements Watcher interface. It stores a set of watched directories. +// All operations which remove watched objects from map `m` must be performed in +// loop goroutine since these structures are used internally by operating system. +type readdcw struct { + sync.Mutex + m map[string]*watched + cph syscall.Handle + start bool + wg sync.WaitGroup + c chan<- EventInfo +} + +// NewWatcher creates new non-recursive watcher backed by ReadDirectoryChangesW. +func newWatcher(c chan<- EventInfo) watcher { + r := &readdcw{ + m: make(map[string]*watched), + cph: syscall.InvalidHandle, + c: c, + } + runtime.SetFinalizer(r, func(r *readdcw) { + if r.cph != syscall.InvalidHandle { + syscall.CloseHandle(r.cph) + } + }) + return r +} + +// Watch implements notify.Watcher interface. +func (r *readdcw) Watch(path string, event Event) error { + return r.watch(path, event, false) +} + +// RecursiveWatch implements notify.RecursiveWatcher interface. +func (r *readdcw) RecursiveWatch(path string, event Event) error { + return r.watch(path, event, true) +} + +// watch inserts a directory to the group of watched folders. If watched folder +// already exists, function tries to rewatch it with new filters(NOT VALID). Moreover, +// watch starts the main event loop goroutine when called for the first time. +func (r *readdcw) watch(path string, event Event, recursive bool) (err error) { + if event&^(All|fileNotifyChangeAll) != 0 { + return errors.New("notify: unknown event") + } + r.Lock() + wd, ok := r.m[path] + r.Unlock() + if !ok { + if err = r.lazyinit(); err != nil { + return + } + r.Lock() + if wd, ok = r.m[path]; ok { + r.Unlock() + return + } + if wd, err = newWatched(r.cph, uint32(event), recursive, path); err != nil { + r.Unlock() + return + } + r.m[path] = wd + r.Unlock() + } + return nil +} + +// lazyinit creates an I/O completion port and starts the main event processing +// loop. This method uses Double-Checked Locking optimization. +func (r *readdcw) lazyinit() (err error) { + invalid := uintptr(syscall.InvalidHandle) + if atomic.LoadUintptr((*uintptr)(&r.cph)) == invalid { + r.Lock() + defer r.Unlock() + if atomic.LoadUintptr((*uintptr)(&r.cph)) == invalid { + cph := syscall.InvalidHandle + if cph, err = syscall.CreateIoCompletionPort(cph, 0, 0, 0); err != nil { + return + } + r.cph, r.start = cph, true + go r.loop() + } + } + return +} + +// TODO(pknap) : doc +func (r *readdcw) loop() { + var n, key uint32 + var overlapped *syscall.Overlapped + for { + err := syscall.GetQueuedCompletionStatus(r.cph, &n, &key, &overlapped, syscall.INFINITE) + if key == stateCPClose { + r.Lock() + handle := r.cph + r.cph = syscall.InvalidHandle + r.Unlock() + syscall.CloseHandle(handle) + r.wg.Done() + return + } + if overlapped == nil { + // TODO: check key == rewatch delete or 0(panic) + continue + } + overEx := (*overlappedEx)(unsafe.Pointer(overlapped)) + if n == 0 { + r.loopstate(overEx) + } else { + r.loopevent(n, overEx) + if err = overEx.parent.readDirChanges(); err != nil { + // TODO: error handling + } + } + } +} + +// TODO(pknap) : doc +func (r *readdcw) loopstate(overEx *overlappedEx) { + filter := atomic.LoadUint32(&overEx.parent.parent.filter) + if filter&onlyMachineStates == 0 { + return + } + if overEx.parent.parent.count--; overEx.parent.parent.count == 0 { + switch filter & onlyMachineStates { + case stateRewatch: + r.Lock() + overEx.parent.parent.recreate(r.cph) + r.Unlock() + case stateUnwatch: + r.Lock() + delete(r.m, syscall.UTF16ToString(overEx.parent.pathw)) + r.Unlock() + case stateCPClose: + default: + panic(`notify: windows loopstate logic error`) + } + } +} + +// TODO(pknap) : doc +func (r *readdcw) loopevent(n uint32, overEx *overlappedEx) { + events := []*event{} + var currOffset uint32 + for { + raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&overEx.parent.buffer[currOffset])) + name := syscall.UTF16ToString((*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName))[:raw.FileNameLength>>1]) + events = append(events, &event{ + pathw: overEx.parent.pathw, + filter: overEx.parent.filter, + action: raw.Action, + name: name, + }) + if raw.NextEntryOffset == 0 { + break + } + if currOffset += raw.NextEntryOffset; currOffset >= n { + break + } + } + r.send(events) +} + +// TODO(pknap) : doc +func (r *readdcw) send(es []*event) { + for _, e := range es { + var syse Event + if e.e, syse = decode(e.filter, e.action); e.e == 0 && syse == 0 { + continue + } + switch { + case e.action == syscall.FILE_ACTION_MODIFIED: + e.ftype = fTypeUnknown + case e.filter&uint32(dirmarker) != 0: + e.ftype = fTypeDirectory + default: + e.ftype = fTypeFile + } + switch { + case e.e == 0: + e.e = syse + case syse != 0: + r.c <- &event{ + pathw: e.pathw, + name: e.name, + ftype: e.ftype, + action: e.action, + filter: e.filter, + e: syse, + } + } + r.c <- e + } +} + +// Rewatch implements notify.Rewatcher interface. +func (r *readdcw) Rewatch(path string, oldevent, newevent Event) error { + return r.rewatch(path, uint32(oldevent), uint32(newevent), false) +} + +// RecursiveRewatch implements notify.RecursiveRewatcher interface. +func (r *readdcw) RecursiveRewatch(oldpath, newpath string, oldevent, + newevent Event) error { + if oldpath != newpath { + if err := r.unwatch(oldpath); err != nil { + return err + } + return r.watch(newpath, newevent, true) + } + return r.rewatch(newpath, uint32(oldevent), uint32(newevent), true) +} + +// TODO : (pknap) doc. +func (r *readdcw) rewatch(path string, oldevent, newevent uint32, recursive bool) (err error) { + if Event(newevent)&^(All|fileNotifyChangeAll) != 0 { + return errors.New("notify: unknown event") + } + var wd *watched + r.Lock() + if wd, err = r.nonStateWatched(path); err != nil { + r.Unlock() + return + } + if wd.filter&(onlyNotifyChanges|onlyNGlobalEvents) != oldevent { + panic(`notify: windows re-watcher logic error`) + } + wd.filter = stateRewatch | newevent + wd.recursive, recursive = recursive, wd.recursive + if err = wd.closeHandle(); err != nil { + wd.filter = oldevent + wd.recursive = recursive + r.Unlock() + return + } + r.Unlock() + return +} + +// TODO : pknap +func (r *readdcw) nonStateWatched(path string) (wd *watched, err error) { + wd, ok := r.m[path] + if !ok || wd == nil { + err = errors.New(`notify: ` + path + ` path is unwatched`) + return + } + if filter := atomic.LoadUint32(&wd.filter); filter&onlyMachineStates != 0 { + err = errors.New(`notify: another re/unwatching operation in progress`) + return + } + return +} + +// Unwatch implements notify.Watcher interface. +func (r *readdcw) Unwatch(path string) error { + return r.unwatch(path) +} + +// RecursiveUnwatch implements notify.RecursiveWatcher interface. +func (r *readdcw) RecursiveUnwatch(path string) error { + return r.unwatch(path) +} + +// TODO : pknap +func (r *readdcw) unwatch(path string) (err error) { + var wd *watched + r.Lock() + if wd, err = r.nonStateWatched(path); err != nil { + r.Unlock() + return + } + wd.filter |= stateUnwatch + if err = wd.closeHandle(); err != nil { + wd.filter &^= stateUnwatch + r.Unlock() + return + } + r.Unlock() + return +} + +// Close resets the whole watcher object, closes all existing file descriptors, +// and sends stateCPClose state as completion key to the main watcher's loop. +func (r *readdcw) Close() (err error) { + r.Lock() + if !r.start { + r.Unlock() + return nil + } + for _, wd := range r.m { + wd.filter &^= onlyMachineStates + wd.filter |= stateCPClose + if e := wd.closeHandle(); e != nil && err == nil { + err = e + } + } + r.start = false + r.Unlock() + r.wg.Add(1) + if e := syscall.PostQueuedCompletionStatus(r.cph, 0, stateCPClose, nil); e != nil && err == nil { + return e + } + r.wg.Wait() + return +} + +// decode creates a notify event from both non-raw filter and action which was +// returned from completion routine. Function may return Event(0) in case when +// filter was replaced by a new value which does not contain fields that are +// valid with passed action. +func decode(filter, action uint32) (Event, Event) { + switch action { + case syscall.FILE_ACTION_ADDED: + return gensys(filter, Create, FileActionAdded) + case syscall.FILE_ACTION_REMOVED: + return gensys(filter, Remove, FileActionRemoved) + case syscall.FILE_ACTION_MODIFIED: + return gensys(filter, Write, FileActionModified) + case syscall.FILE_ACTION_RENAMED_OLD_NAME: + return gensys(filter, Rename, FileActionRenamedOldName) + case syscall.FILE_ACTION_RENAMED_NEW_NAME: + return gensys(filter, Rename, FileActionRenamedNewName) + } + panic(`notify: cannot decode internal mask`) +} + +// gensys decides whether the Windows action, system-independent event or both +// of them should be returned. Since the grip's filter may be atomically changed +// during watcher lifetime, it is possible that neither Windows nor notify masks +// are watched by the user when this function is called. +func gensys(filter uint32, ge, se Event) (gene, syse Event) { + isdir := filter&uint32(dirmarker) != 0 + if isdir && filter&uint32(FileNotifyChangeDirName) != 0 || + !isdir && filter&uint32(FileNotifyChangeFileName) != 0 || + filter&uint32(fileNotifyChangeModified) != 0 { + syse = se + } + if filter&uint32(ge) != 0 { + gene = ge + } + return +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher_readdcw_test.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher_readdcw_test.go new file mode 100644 index 0000000..ea15b4a --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher_readdcw_test.go @@ -0,0 +1,67 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build windows + +package notify + +import "testing" + +// TODO(ppknap) : remove notify.Create event. +func rcreate(w *W, path string) WCase { + cas := create(w, path) + cas.Events = append(cas.Events, + &Call{P: path, E: FileActionAdded}, + ) + return cas +} + +// TODO(ppknap) : remove notify.Remove event. +func rremove(w *W, path string) WCase { + cas := remove(w, path) + cas.Events = append(cas.Events, + &Call{P: path, E: FileActionRemoved}, + ) + return cas +} + +// TODO(ppknap) : remove notify.Rename event. +func rrename(w *W, oldpath, newpath string) WCase { + cas := rename(w, oldpath, newpath) + cas.Events = append(cas.Events, + &Call{P: oldpath, E: FileActionRenamedOldName}, + &Call{P: newpath, E: FileActionRenamedNewName}, + ) + return cas +} + +// TODO(ppknap) : remove notify.Write event. +func rwrite(w *W, path string, p []byte) WCase { + cas := write(w, path, p) + cas.Events = append(cas.Events, + &Call{P: path, E: FileActionModified}, + ) + return cas +} + +var events = []Event{ + FileNotifyChangeFileName, + FileNotifyChangeDirName, + FileNotifyChangeSize, +} + +func TestWatcherReadDirectoryChangesW(t *testing.T) { + w := NewWatcherTest(t, "testdata/vfs.txt", events...) + defer w.Close() + + cases := [...]WCase{ + rcreate(w, "src/github.com/rjeczalik/fs/fs_windows.go"), + rcreate(w, "src/github.com/rjeczalik/fs/subdir/"), + rremove(w, "src/github.com/rjeczalik/fs/fs.go"), + rrename(w, "src/github.com/rjeczalik/fs/LICENSE", "src/github.com/rjeczalik/fs/COPYLEFT"), + rwrite(w, "src/github.com/rjeczalik/fs/cmd/gotree/go.go", []byte("XD")), + } + + w.ExpectAny(cases[:]) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher_recursive_test.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher_recursive_test.go new file mode 100644 index 0000000..3b1f148 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher_recursive_test.go @@ -0,0 +1,102 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build darwin,!kqueue windows + +package notify + +import ( + "fmt" + "testing" +) + +// noevent stripts test-case from expected event list, used when action is not +// expected to trigger any events. +func noevent(cas WCase) WCase { + return WCase{Action: cas.Action} +} + +func TestWatcherRecursiveRewatch(t *testing.T) { + w := newWatcherTest(t, "testdata/vfs.txt") + defer w.Close() + + cases := []WCase{ + create(w, "src/github.com/rjeczalik/file"), + create(w, "src/github.com/rjeczalik/dir/"), + noevent(create(w, "src/github.com/rjeczalik/fs/dir/")), + noevent(create(w, "src/github.com/dir/")), + noevent(write(w, "src/github.com/rjeczalik/file", []byte("XD"))), + noevent(rename(w, "src/github.com/rjeczalik/fs/LICENSE", "src/LICENSE")), + } + + w.Watch("src/github.com/rjeczalik", Create) + w.ExpectAny(cases) + + cases = []WCase{ + create(w, "src/github.com/rjeczalik/fs/file"), + create(w, "src/github.com/rjeczalik/fs/cmd/gotree/file"), + create(w, "src/github.com/rjeczalik/fs/cmd/dir/"), + create(w, "src/github.com/rjeczalik/fs/cmd/gotree/dir/"), + noevent(write(w, "src/github.com/rjeczalik/fs/file", []byte("XD"))), + noevent(create(w, "src/github.com/anotherdir/")), + } + + w.RecursiveRewatch("src/github.com/rjeczalik", "src/github.com/rjeczalik", Create, Create) + w.ExpectAny(cases) + + cases = []WCase{ + create(w, "src/github.com/rjeczalik/1"), + create(w, "src/github.com/rjeczalik/2/"), + noevent(create(w, "src/github.com/rjeczalik/fs/cmd/1")), + noevent(create(w, "src/github.com/rjeczalik/fs/1/")), + noevent(write(w, "src/github.com/rjeczalik/fs/file", []byte("XD"))), + } + + w.Rewatch("src/github.com/rjeczalik", Create, Create) + w.ExpectAny(cases) +} + +// TODO(rjeczalik): move to watcher_test.go after #5 +func TestIsDirCreateEvent(t *testing.T) { + w := NewWatcherTest(t, "testdata/vfs.txt") + defer w.Close() + + cases := [...]WCase{ + // i=0 + create(w, "src/github.com/jszwec/"), + // i=1 + create(w, "src/github.com/jszwec/gojunitxml/"), + // i=2 + create(w, "src/github.com/jszwec/gojunitxml/README.md"), + // i=3 + create(w, "src/github.com/jszwec/gojunitxml/LICENSE"), + // i=4 + create(w, "src/github.com/jszwec/gojunitxml/cmd/"), + } + + dirs := [...]bool{ + true, // i=0 + true, // i=1 + false, // i=2 + false, // i=3 + true, // i=4 + } + + fn := func(i int, _ WCase, ei EventInfo) error { + d, ok := ei.(isDirer) + if !ok { + return fmt.Errorf("received EventInfo does not implement isDirer") + } + switch ok, err := d.isDir(); { + case err != nil: + return err + case ok != dirs[i]: + return fmt.Errorf("want ok=%v; got %v", dirs[i], ok) + default: + return nil + } + } + + w.ExpectAnyFunc(cases[:], fn) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher_stub.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher_stub.go new file mode 100644 index 0000000..df55102 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher_stub.go @@ -0,0 +1,21 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build !darwin,!linux,!freebsd,!dragonfly,!netbsd,!openbsd,!windows +// +build !kqueue + +package notify + +type stub struct{ error } + +// NewWatcher stub. +func newWatcher(chan<- EventInfo) stub { + return stub{errors.New("notify: not implemented")} +} + +// Following methods implement notify.watcher interface. +func (s stub) Watch(string, Event) error { return s } +func (s stub) Rewatch(string, Event, Event) error { return s } +func (s stub) Unwatch(string) (err error) { return s } +func (s stub) Close() error { return s } diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watcher_test.go b/Godeps/_workspace/src/github.com/zillode/notify/watcher_test.go new file mode 100644 index 0000000..45633e1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watcher_test.go @@ -0,0 +1,32 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build darwin linux freebsd dragonfly netbsd openbsd windows + +package notify + +import "testing" + +// NOTE Set DEBUG env var for extra debugging info. + +func TestWatcher(t *testing.T) { + w := NewWatcherTest(t, "testdata/vfs.txt") + defer w.Close() + + cases := [...]WCase{ + create(w, "src/github.com/ppknap/link/include/coost/.link.hpp.swp"), + create(w, "src/github.com/rjeczalik/fs/fs_test.go"), + create(w, "src/github.com/rjeczalik/fs/binfs/"), + create(w, "src/github.com/rjeczalik/fs/binfs.go"), + create(w, "src/github.com/rjeczalik/fs/binfs_test.go"), + remove(w, "src/github.com/rjeczalik/fs/binfs/"), + create(w, "src/github.com/rjeczalik/fs/binfs/"), + create(w, "src/github.com/rjeczalik/fs/virfs"), + remove(w, "src/github.com/rjeczalik/fs/virfs"), + create(w, "file"), + create(w, "dir/"), + } + + w.ExpectAny(cases[:]) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watchpoint.go b/Godeps/_workspace/src/github.com/zillode/notify/watchpoint.go new file mode 100644 index 0000000..e034013 --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watchpoint.go @@ -0,0 +1,102 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +// EventDiff describes a change to an event set - EventDiff[0] is an old state, +// while EventDiff[1] is a new state. If event set has not changed (old == new), +// functions typically return the None value. +type eventDiff [2]Event + +func (diff eventDiff) Event() Event { + return diff[1] &^ diff[0] +} + +// Watchpoint +// +// The nil key holds total event set - logical sum for all registered events. +// It speeds up computing EventDiff for Add method. +// +// The rec key holds an event set for a watchpoints created by RecursiveWatch +// for a Watcher implementation which is not natively recursive. +type watchpoint map[chan<- EventInfo]Event + +// None is an empty event diff, think null object. +var none eventDiff + +// rec is just a placeholder +var rec = func() (ch chan<- EventInfo) { + ch = make(chan<- EventInfo) + close(ch) + return +}() + +func (wp watchpoint) dryAdd(ch chan<- EventInfo, e Event) eventDiff { + if e &^= internal; wp[ch]&e == e { + return none + } + total := wp[ch] &^ internal + return eventDiff{total, total | e} +} + +// Add assumes neither c nor e are nil or zero values. +func (wp watchpoint) Add(c chan<- EventInfo, e Event) (diff eventDiff) { + wp[c] |= e + diff[0] = wp[nil] + diff[1] = diff[0] | e + wp[nil] = diff[1] &^ omit + // Strip diff from internal events. + diff[0] &^= internal + diff[1] &^= internal + if diff[0] == diff[1] { + return none + } + return +} + +func (wp watchpoint) Del(c chan<- EventInfo, e Event) (diff eventDiff) { + wp[c] &^= e + if wp[c] == 0 { + delete(wp, c) + } + diff[0] = wp[nil] + delete(wp, nil) + if len(wp) != 0 { + // Recalculate total event set. + for _, e := range wp { + diff[1] |= e + } + wp[nil] = diff[1] &^ omit + } + // Strip diff from internal events. + diff[0] &^= internal + diff[1] &^= internal + if diff[0] == diff[1] { + return none + } + return +} + +func (wp watchpoint) Dispatch(ei EventInfo, extra Event) { + e := eventmask(ei, extra) + if !matches(wp[nil], e) { + return + } + for ch, eset := range wp { + if ch != nil && matches(eset, e) { + select { + case ch <- ei: + default: // Drop event if receiver is too slow + } + } + } +} + +func (wp watchpoint) Total() Event { + return wp[nil] &^ internal +} + +func (wp watchpoint) IsRecursive() bool { + return wp[nil]&recursive != 0 +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watchpoint_other.go b/Godeps/_workspace/src/github.com/zillode/notify/watchpoint_other.go new file mode 100644 index 0000000..881631c --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watchpoint_other.go @@ -0,0 +1,23 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build !windows + +package notify + +// eventmask uses ei to create a new event which contains internal flags used by +// notify package logic. +func eventmask(ei EventInfo, extra Event) Event { + return ei.Event() | extra +} + +// matches reports a match only when: +// +// - for user events, when event is present in the given set +// - for internal events, when additionaly both event and set have omit bit set +// +// Internal events must not be sent to user channels and vice versa. +func matches(set, event Event) bool { + return (set&omit)^(event&omit) == 0 && set&event == event +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watchpoint_readdcw.go b/Godeps/_workspace/src/github.com/zillode/notify/watchpoint_readdcw.go new file mode 100644 index 0000000..9fd1e1d --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watchpoint_readdcw.go @@ -0,0 +1,38 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +// +build windows + +package notify + +// eventmask uses ei to create a new event which contains internal flags used by +// notify package logic. If one of FileAction* masks is detected, this function +// adds corresponding FileNotifyChange* values. This allows non registered +// FileAction* events to be passed on. +func eventmask(ei EventInfo, extra Event) (e Event) { + if e = ei.Event() | extra; e&fileActionAll != 0 { + if ev, ok := ei.(*event); ok { + switch ev.ftype { + case fTypeFile: + e |= FileNotifyChangeFileName + case fTypeDirectory: + e |= FileNotifyChangeDirName + case fTypeUnknown: + e |= fileNotifyChangeModified + } + return e &^ fileActionAll + } + } + return +} + +// matches reports a match only when: +// +// - for user events, when event is present in the given set +// - for internal events, when additionally both event and set have omit bit set +// +// Internal events must not be sent to user channels and vice versa. +func matches(set, event Event) bool { + return (set&omit)^(event&omit) == 0 && (set&event == event || set&fileNotifyChangeModified&event != 0) +} diff --git a/Godeps/_workspace/src/github.com/zillode/notify/watchpoint_test.go b/Godeps/_workspace/src/github.com/zillode/notify/watchpoint_test.go new file mode 100644 index 0000000..e35483d --- /dev/null +++ b/Godeps/_workspace/src/github.com/zillode/notify/watchpoint_test.go @@ -0,0 +1,162 @@ +// Copyright (c) 2014-2015 The Notify Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +package notify + +import ( + "fmt" + "reflect" + "testing" +) + +func call(wp watchpoint, fn interface{}, args []interface{}) eventDiff { + vals := []reflect.Value{reflect.ValueOf(wp)} + for _, arg := range args { + vals = append(vals, reflect.ValueOf(arg)) + } + res := reflect.ValueOf(fn).Call(vals) + if n := len(res); n != 1 { + panic(fmt.Sprintf("unexpected len(res)=%d", n)) + } + diff, ok := res[0].Interface().(eventDiff) + if !ok { + panic(fmt.Sprintf("want typeof(diff)=EventDiff; got %T", res[0].Interface())) + } + return diff +} + +func TestWatchpoint(t *testing.T) { + ch := NewChans(5) + all := All | recursive + cases := [...]struct { + fn interface{} + args []interface{} + diff eventDiff + total Event + }{ + // i=0 + { + watchpoint.Add, + []interface{}{ch[0], Remove}, + eventDiff{0, Remove}, + Remove, + }, + // i=1 + { + watchpoint.Add, + []interface{}{ch[1], Create | Remove | recursive}, + eventDiff{Remove, Remove | Create}, + Create | Remove | recursive, + }, + // i=2 + { + watchpoint.Add, + []interface{}{ch[2], Create | Rename}, + eventDiff{Create | Remove, Create | Remove | Rename}, + Create | Remove | Rename | recursive, + }, + // i=3 + { + watchpoint.Add, + []interface{}{ch[0], Write | recursive}, + eventDiff{Create | Remove | Rename, Create | Remove | Rename | Write}, + Create | Remove | Rename | Write | recursive, + }, + // i=4 + { + watchpoint.Add, + []interface{}{ch[2], Remove | recursive}, + none, + Create | Remove | Rename | Write | recursive, + }, + // i=5 + { + watchpoint.Del, + []interface{}{ch[0], all}, + eventDiff{Create | Remove | Rename | Write, Create | Remove | Rename}, + Create | Remove | Rename | recursive, + }, + // i=6 + { + watchpoint.Del, + []interface{}{ch[2], all}, + eventDiff{Create | Remove | Rename, Create | Remove}, + Create | Remove | recursive, + }, + // i=7 + { + watchpoint.Add, + []interface{}{ch[3], Create | Remove}, + none, + Create | Remove | recursive, + }, + // i=8 + { + watchpoint.Del, + []interface{}{ch[1], all}, + none, + Create | Remove, + }, + // i=9 + { + watchpoint.Add, + []interface{}{ch[3], recursive | Write}, + eventDiff{Create | Remove, Create | Remove | Write}, + Create | Remove | Write | recursive, + }, + // i=10 + { + watchpoint.Del, + []interface{}{ch[3], Create}, + eventDiff{Create | Remove | Write, Remove | Write}, + Remove | Write | recursive, + }, + // i=11 + { + watchpoint.Add, + []interface{}{ch[3], Create | Rename}, + eventDiff{Remove | Write, Create | Remove | Rename | Write}, + Create | Remove | Rename | Write | recursive, + }, + // i=12 + { + watchpoint.Add, + []interface{}{ch[2], Remove | Write}, + none, + Create | Remove | Rename | Write | recursive, + }, + // i=13 + { + watchpoint.Del, + []interface{}{ch[3], Create | Remove | Write}, + eventDiff{Create | Remove | Rename | Write, Remove | Rename | Write}, + Remove | Rename | Write | recursive, + }, + // i=14 + { + watchpoint.Del, + []interface{}{ch[2], Remove}, + eventDiff{Remove | Rename | Write, Rename | Write}, + Rename | Write | recursive, + }, + // i=15 + { + watchpoint.Del, + []interface{}{ch[3], Rename | recursive}, + eventDiff{Rename | Write, Write}, + Write, + }, + } + wp := watchpoint{} + for i, cas := range cases { + if diff := call(wp, cas.fn, cas.args); diff != cas.diff { + t.Errorf("want diff=%v; got %v (i=%d)", cas.diff, diff, i) + continue + } + if total := wp[nil]; total != cas.total { + t.Errorf("want total=%v; got %v (i=%d)", cas.total, total, i) + continue + } + } +} -- 2.5.2