diff --git a/daemon/container.go b/daemon/container.go index 9e3cd4186a..6976dbff75 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -81,6 +81,7 @@ type Container struct { MountLabel, ProcessLabel string AppArmorProfile string RestartCount int + UpdateDns bool // Maps container paths to volume paths. The key in this is the path to which // the volume is being mounted inside the container. Value is the path of the @@ -942,6 +943,29 @@ func (container *Container) DisableLink(name string) { func (container *Container) setupContainerDns() error { if container.ResolvConfPath != "" { + // check if this is an existing container that needs DNS update: + if container.UpdateDns { + // read the host's resolv.conf, get the hash and call updateResolvConf + log.Debugf("Check container (%s) for update to resolv.conf - UpdateDns flag was set", container.ID) + latestResolvConf, latestHash := resolvconf.GetLastModified() + + // because the new host resolv.conf might have localhost nameservers.. + updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(latestResolvConf) + if modified { + // changes have occurred during resolv.conf localhost cleanup: generate an updated hash + newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf)) + if err != nil { + return err + } + latestHash = newHash + } + + if err := container.updateResolvConf(updatedResolvConf, latestHash); err != nil { + return err + } + // successful update of the restarting container; set the flag off + container.UpdateDns = false + } return nil } @@ -980,17 +1004,86 @@ func (container *Container) setupContainerDns() error { } // replace any localhost/127.* nameservers - resolvConf = utils.RemoveLocalDns(resolvConf) - // if the resulting resolvConf is empty, use DefaultDns - if !bytes.Contains(resolvConf, []byte("nameserver")) { - log.Infof("No non localhost DNS resolver found in resolv.conf and containers can't use it. Using default external servers : %v", DefaultDns) - // prefix the default dns options with nameserver - resolvConf = append(resolvConf, []byte("\nnameserver "+strings.Join(DefaultDns, "\nnameserver "))...) - } + resolvConf, _ = resolvconf.RemoveReplaceLocalDns(resolvConf) + } + //get a sha256 hash of the resolv conf at this point so we can check + //for changes when the host resolv.conf changes (e.g. network update) + resolvHash, err := utils.HashData(bytes.NewReader(resolvConf)) + if err != nil { + return err + } + resolvHashFile := container.ResolvConfPath + ".hash" + if err = ioutil.WriteFile(resolvHashFile, []byte(resolvHash), 0644); err != nil { + return err } return ioutil.WriteFile(container.ResolvConfPath, resolvConf, 0644) } +// called when the host's resolv.conf changes to check whether container's resolv.conf +// is unchanged by the container "user" since container start: if unchanged, the +// container's resolv.conf will be updated to match the host's new resolv.conf +func (container *Container) updateResolvConf(updatedResolvConf []byte, newResolvHash string) error { + + if container.ResolvConfPath == "" { + return nil + } + if container.Running { + //set a marker in the hostConfig to update on next start/restart + container.UpdateDns = true + return nil + } + + resolvHashFile := container.ResolvConfPath + ".hash" + + //read the container's current resolv.conf and compute the hash + resolvBytes, err := ioutil.ReadFile(container.ResolvConfPath) + if err != nil { + return err + } + curHash, err := utils.HashData(bytes.NewReader(resolvBytes)) + if err != nil { + return err + } + + //read the hash from the last time we wrote resolv.conf in the container + hashBytes, err := ioutil.ReadFile(resolvHashFile) + if err != nil { + return err + } + + //if the user has not modified the resolv.conf of the container since we wrote it last + //we will replace it with the updated resolv.conf from the host + if string(hashBytes) == curHash { + log.Debugf("replacing %q with updated host resolv.conf", container.ResolvConfPath) + + // for atomic updates to these files, use temporary files with os.Rename: + dir := path.Dir(container.ResolvConfPath) + tmpHashFile, err := ioutil.TempFile(dir, "hash") + if err != nil { + return err + } + tmpResolvFile, err := ioutil.TempFile(dir, "resolv") + if err != nil { + return err + } + + // write the updates to the temp files + if err = ioutil.WriteFile(tmpHashFile.Name(), []byte(newResolvHash), 0644); err != nil { + return err + } + if err = ioutil.WriteFile(tmpResolvFile.Name(), updatedResolvConf, 0644); err != nil { + return err + } + + // rename the temp files for atomic replace + if err = os.Rename(tmpHashFile.Name(), resolvHashFile); err != nil { + return err + } + return os.Rename(tmpResolvFile.Name(), container.ResolvConfPath) + } + return nil +} + func (container *Container) updateParentsHosts() error { parents, err := container.daemon.Parents(container.Name) if err != nil { diff --git a/daemon/daemon.go b/daemon/daemon.go index 8ad677bed0..7cc9d1e2cf 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -1,6 +1,7 @@ package daemon import ( + "bytes" "fmt" "io" "io/ioutil" @@ -32,6 +33,7 @@ import ( "github.com/docker/docker/pkg/graphdb" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/namesgenerator" + "github.com/docker/docker/pkg/networkfs/resolvconf" "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/parsers/kernel" "github.com/docker/docker/pkg/sysinfo" @@ -40,10 +42,11 @@ import ( "github.com/docker/docker/trust" "github.com/docker/docker/utils" "github.com/docker/docker/volumes" + + "github.com/go-fsnotify/fsnotify" ) var ( - DefaultDns = []string{"8.8.8.8", "8.8.4.4"} validContainerNameChars = `[a-zA-Z0-9][a-zA-Z0-9_.-]` validContainerNamePattern = regexp.MustCompile(`^/?` + validContainerNameChars + `+$`) ) @@ -408,6 +411,60 @@ func (daemon *Daemon) restore() error { return nil } +// set up the watch on the host's /etc/resolv.conf so that we can update container's +// live resolv.conf when the network changes on the host +func (daemon *Daemon) setupResolvconfWatcher() error { + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + + //this goroutine listens for the events on the watch we add + //on the resolv.conf file on the host + go func() { + for { + select { + case event := <-watcher.Events: + if event.Op&fsnotify.Write == fsnotify.Write { + // verify a real change happened before we go further--a file write may have happened + // without an actual change to the file + updatedResolvConf, newResolvConfHash, err := resolvconf.GetIfChanged() + if err != nil { + log.Debugf("Error retrieving updated host resolv.conf: %v", err) + } else if updatedResolvConf != nil { + // because the new host resolv.conf might have localhost nameservers.. + updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(updatedResolvConf) + if modified { + // changes have occurred during localhost cleanup: generate an updated hash + newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf)) + if err != nil { + log.Debugf("Error generating hash of new resolv.conf: %v", err) + } else { + newResolvConfHash = newHash + } + } + log.Debugf("host network resolv.conf changed--walking container list for updates") + contList := daemon.containers.List() + for _, container := range contList { + if err := container.updateResolvConf(updatedResolvConf, newResolvConfHash); err != nil { + log.Debugf("Error on resolv.conf update check for container ID: %s: %v", container.ID, err) + } + } + } + } + case err := <-watcher.Errors: + log.Debugf("host resolv.conf notify error: %v", err) + } + } + }() + + if err := watcher.Add("/etc/resolv.conf"); err != nil { + return err + } + return nil +} + func (daemon *Daemon) checkDeprecatedExpose(config *runconfig.Config) bool { if config != nil { if config.PortSpecs != nil { @@ -930,6 +987,12 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) if err := daemon.restore(); err != nil { return nil, err } + + // set up filesystem watch on resolv.conf for network changes + if err := daemon.setupResolvconfWatcher(); err != nil { + return nil, err + } + // Setup shutdown handlers // FIXME: can these shutdown handlers be registered closer to their source? eng.OnShutdown(func() { diff --git a/daemon/utils_test.go b/daemon/utils_test.go index 28a15c64e1..ff5b082ba5 100644 --- a/daemon/utils_test.go +++ b/daemon/utils_test.go @@ -24,34 +24,3 @@ func TestMergeLxcConfig(t *testing.T) { t.Fatalf("expected %s got %s", expected, cpuset) } } - -func TestRemoveLocalDns(t *testing.T) { - ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n" - - if result := utils.RemoveLocalDns([]byte(ns0)); result != nil { - if ns0 != string(result) { - t.Fatalf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) - } - } - - ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n" - if result := utils.RemoveLocalDns([]byte(ns1)); result != nil { - if ns0 != string(result) { - t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) - } - } - - ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n" - if result := utils.RemoveLocalDns([]byte(ns1)); result != nil { - if ns0 != string(result) { - t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) - } - } - - ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n" - if result := utils.RemoveLocalDns([]byte(ns1)); result != nil { - if ns0 != string(result) { - t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) - } - } -} diff --git a/docs/sources/articles/networking.md b/docs/sources/articles/networking.md index 4bfbcfdade..05e59816b9 100644 --- a/docs/sources/articles/networking.md +++ b/docs/sources/articles/networking.md @@ -130,7 +130,7 @@ information. You can see this by running `mount` inside a container: ... /dev/disk/by-uuid/1fec...ebdf on /etc/hostname type ext4 ... /dev/disk/by-uuid/1fec...ebdf on /etc/hosts type ext4 ... - tmpfs on /etc/resolv.conf type tmpfs ... + /dev/disk/by-uuid/1fec...ebdf on /etc/resolv.conf type ext4 ... ... This arrangement allows Docker to do clever things like keep @@ -178,7 +178,20 @@ Four different options affect container domain name services. Note that Docker, in the absence of either of the last two options above, will make `/etc/resolv.conf` inside of each container look like the `/etc/resolv.conf` of the host machine where the `docker` daemon is -running. The options then modify this default configuration. +running. You might wonder what happens when the host machine's +`/etc/resolv.conf` file changes. The `docker` daemon has a file change +notifier active which will watch for changes to the host DNS configuration. +When the host file changes, all stopped containers which have a matching +`resolv.conf` to the host will be updated immediately to this newest host +configuration. Containers which are running when the host configuration +changes will need to stop and start to pick up the host changes due to lack +of a facility to ensure atomic writes of the `resolv.conf` file while the +container is running. If the container's `resolv.conf` has been edited since +it was started with the default configuration, no replacement will be +attempted as it would overwrite the changes performed by the container. +If the options (`--dns` or `--dns-search`) have been used to modify the +default host configuration, then the replacement with an updated host's +`/etc/resolv.conf` will not happen as well. ## Communication between containers and the wider world diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 748c4d6815..18495be410 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -1403,6 +1403,157 @@ func TestRunDnsOptionsBasedOnHostResolvConf(t *testing.T) { logDone("run - dns options based on host resolv.conf") } +// Test the file watch notifier on docker host's /etc/resolv.conf +// A go-routine is responsible for auto-updating containers which are +// stopped and have an unmodified copy of resolv.conf, as well as +// marking running containers as requiring an update on next restart +func TestRunResolvconfUpdater(t *testing.T) { + + tmpResolvConf := []byte("search pommesfrites.fr\nnameserver 12.34.56.78") + tmpLocalhostResolvConf := []byte("nameserver 127.0.0.1") + + //take a copy of resolv.conf for restoring after test completes + resolvConfSystem, err := ioutil.ReadFile("/etc/resolv.conf") + if err != nil { + t.Fatal(err) + } + + //cleanup + defer func() { + deleteAllContainers() + if err := ioutil.WriteFile("/etc/resolv.conf", resolvConfSystem, 0644); err != nil { + t.Fatal(err) + } + }() + + //1. test that a non-running container gets an updated resolv.conf + cmd := exec.Command(dockerBinary, "run", "--name='first'", "busybox", "true") + if _, err := runCommand(cmd); err != nil { + t.Fatal(err) + } + containerID1, err := getIDByName("first") + if err != nil { + t.Fatal(err) + } + + // replace resolv.conf with our temporary copy + bytesResolvConf := []byte(tmpResolvConf) + if err := ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil { + t.Fatal(err) + } + + time.Sleep(time.Second / 2) + // check for update in container + containerResolv, err := readContainerFile(containerID1, "resolv.conf") + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(containerResolv, bytesResolvConf) { + t.Fatalf("Stopped container does not have updated resolv.conf; expected %q, got %q", tmpResolvConf, string(containerResolv)) + } + + //2. test that a non-running container does not receive resolv.conf updates + // if it modified the container copy of the starting point resolv.conf + cmd = exec.Command(dockerBinary, "run", "--name='second'", "busybox", "sh", "-c", "echo 'search mylittlepony.com' >>/etc/resolv.conf") + if _, err = runCommand(cmd); err != nil { + t.Fatal(err) + } + containerID2, err := getIDByName("second") + if err != nil { + t.Fatal(err) + } + containerResolvHashBefore, err := readContainerFile(containerID2, "resolv.conf.hash") + if err != nil { + t.Fatal(err) + } + + //make a change to resolv.conf (in this case replacing our tmp copy with orig copy) + if err := ioutil.WriteFile("/etc/resolv.conf", resolvConfSystem, 0644); err != nil { + t.Fatal(err) + } + + time.Sleep(time.Second / 2) + containerResolvHashAfter, err := readContainerFile(containerID2, "resolv.conf.hash") + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(containerResolvHashBefore, containerResolvHashAfter) { + t.Fatalf("Stopped container with modified resolv.conf should not have been updated; expected hash: %v, new hash: %v", containerResolvHashBefore, containerResolvHashAfter) + } + + //3. test that a running container's resolv.conf is not modified while running + cmd = exec.Command(dockerBinary, "run", "-d", "busybox", "top") + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatal(err) + } + runningContainerID := strings.TrimSpace(out) + + containerResolvHashBefore, err = readContainerFile(runningContainerID, "resolv.conf.hash") + if err != nil { + t.Fatal(err) + } + + // replace resolv.conf + if err := ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil { + t.Fatal(err) + } + + // make sure the updater has time to run to validate we really aren't + // getting updated + time.Sleep(time.Second / 2) + containerResolvHashAfter, err = readContainerFile(runningContainerID, "resolv.conf.hash") + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(containerResolvHashBefore, containerResolvHashAfter) { + t.Fatalf("Running container's resolv.conf should not be updated; expected hash: %v, new hash: %v", containerResolvHashBefore, containerResolvHashAfter) + } + + //4. test that a running container's resolv.conf is updated upon restart + // (the above container is still running..) + cmd = exec.Command(dockerBinary, "restart", runningContainerID) + if _, err = runCommand(cmd); err != nil { + t.Fatal(err) + } + + // check for update in container + containerResolv, err = readContainerFile(runningContainerID, "resolv.conf") + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(containerResolv, bytesResolvConf) { + t.Fatalf("Restarted container should have updated resolv.conf; expected %q, got %q", tmpResolvConf, string(containerResolv)) + } + + //5. test that additions of a localhost resolver are cleaned from + // host resolv.conf before updating container's resolv.conf copies + + // replace resolv.conf with a localhost-only nameserver copy + bytesResolvConf = []byte(tmpLocalhostResolvConf) + if err = ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil { + t.Fatal(err) + } + + time.Sleep(time.Second / 2) + // our first exited container ID should have been updated, but with default DNS + // after the cleanup of resolv.conf found only a localhost nameserver: + containerResolv, err = readContainerFile(containerID1, "resolv.conf") + if err != nil { + t.Fatal(err) + } + + expected := "\nnameserver 8.8.8.8\nnameserver 8.8.4.4" + if !bytes.Equal(containerResolv, []byte(expected)) { + t.Fatalf("Container does not have cleaned/replaced DNS in resolv.conf; expected %q, got %q", expected, string(containerResolv)) + } + + //cleanup, restore original resolv.conf happens in defer func() + logDone("run - resolv.conf updater") +} + func TestRunAddHost(t *testing.T) { defer deleteAllContainers() cmd := exec.Command(dockerBinary, "run", "--add-host=extra:86.75.30.9", "busybox", "grep", "extra", "/etc/hosts") diff --git a/pkg/networkfs/resolvconf/resolvconf.go b/pkg/networkfs/resolvconf/resolvconf.go index 9165caeaaa..a43daa527c 100644 --- a/pkg/networkfs/resolvconf/resolvconf.go +++ b/pkg/networkfs/resolvconf/resolvconf.go @@ -5,13 +5,25 @@ import ( "io/ioutil" "regexp" "strings" + "sync" + + log "github.com/Sirupsen/logrus" + "github.com/docker/docker/utils" ) var ( - nsRegexp = regexp.MustCompile(`^\s*nameserver\s*(([0-9]+\.){3}([0-9]+))\s*$`) - searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`) + defaultDns = []string{"8.8.8.8", "8.8.4.4"} + localHostRegexp = regexp.MustCompile(`(?m)^nameserver 127[^\n]+\n*`) + nsRegexp = regexp.MustCompile(`^\s*nameserver\s*(([0-9]+\.){3}([0-9]+))\s*$`) + searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`) ) +var lastModified struct { + sync.Mutex + sha256 string + contents []byte +} + func Get() ([]byte, error) { resolv, err := ioutil.ReadFile("/etc/resolv.conf") if err != nil { @@ -20,6 +32,57 @@ func Get() ([]byte, error) { return resolv, nil } +// Retrieves the host /etc/resolv.conf file, checks against the last hash +// and, if modified since last check, returns the bytes and new hash. +// This feature is used by the resolv.conf updater for containers +func GetIfChanged() ([]byte, string, error) { + lastModified.Lock() + defer lastModified.Unlock() + + resolv, err := ioutil.ReadFile("/etc/resolv.conf") + if err != nil { + return nil, "", err + } + newHash, err := utils.HashData(bytes.NewReader(resolv)) + if err != nil { + return nil, "", err + } + if lastModified.sha256 != newHash { + lastModified.sha256 = newHash + lastModified.contents = resolv + return resolv, newHash, nil + } + // nothing changed, so return no data + return nil, "", nil +} + +// retrieve the last used contents and hash of the host resolv.conf +// Used by containers updating on restart +func GetLastModified() ([]byte, string) { + lastModified.Lock() + defer lastModified.Unlock() + + return lastModified.contents, lastModified.sha256 +} + +// RemoveReplaceLocalDns looks for localhost (127.*) entries in the provided +// resolv.conf, removing local nameserver entries, and, if the resulting +// cleaned config has no defined nameservers left, adds default DNS entries +// It also returns a boolean to notify the caller if changes were made at all +func RemoveReplaceLocalDns(resolvConf []byte) ([]byte, bool) { + changed := false + cleanedResolvConf := localHostRegexp.ReplaceAll(resolvConf, []byte{}) + // if the resulting resolvConf is empty, use defaultDns + if !bytes.Contains(cleanedResolvConf, []byte("nameserver")) { + log.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : %v", defaultDns) + cleanedResolvConf = append(cleanedResolvConf, []byte("\nnameserver "+strings.Join(defaultDns, "\nnameserver "))...) + } + if !bytes.Equal(resolvConf, cleanedResolvConf) { + changed = true + } + return cleanedResolvConf, changed +} + // getLines parses input into lines and strips away comments. func getLines(input []byte, commentMarker []byte) [][]byte { lines := bytes.Split(input, []byte("\n")) diff --git a/pkg/networkfs/resolvconf/resolvconf_test.go b/pkg/networkfs/resolvconf/resolvconf_test.go index 6187acbae7..2432ea53c0 100644 --- a/pkg/networkfs/resolvconf/resolvconf_test.go +++ b/pkg/networkfs/resolvconf/resolvconf_test.go @@ -156,3 +156,34 @@ func TestBuildWithZeroLengthDomainSearch(t *testing.T) { t.Fatalf("Expected to not find '%s' got '%s'", notExpected, content) } } + +func TestRemoveReplaceLocalDns(t *testing.T) { + ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n" + + if result, _ := RemoveReplaceLocalDns([]byte(ns0)); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } + + ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n" + if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } + + ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n" + if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } + + ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n" + if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil { + if ns0 != string(result) { + t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result)) + } + } +} diff --git a/project/vendor.sh b/project/vendor.sh index 91196d703b..94ad5e3371 100755 --- a/project/vendor.sh +++ b/project/vendor.sh @@ -55,6 +55,8 @@ clone git github.com/docker/libtrust 230dfd18c232 clone git github.com/Sirupsen/logrus v0.6.0 +clone git github.com/go-fsnotify/fsnotify v1.0.4 + # get Go tip's archive/tar, for xattr support and improved performance # TODO after Go 1.4 drops, bump our minimum supported version and drop this vendored dep if [ "$1" = '--go' ]; then diff --git a/utils/utils.go b/utils/utils.go index 4e36b4061e..ccb3ec00cb 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -291,14 +291,6 @@ func NewHTTPRequestError(msg string, res *http.Response) error { } } -var localHostRx = regexp.MustCompile(`(?m)^nameserver 127[^\n]+\n*`) - -// RemoveLocalDns looks into the /etc/resolv.conf, -// and removes any local nameserver entries. -func RemoveLocalDns(resolvConf []byte) []byte { - return localHostRx.ReplaceAll(resolvConf, []byte{}) -} - // An StatusError reports an unsuccessful exit by a command. type StatusError struct { Status string diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/.gitignore b/vendor/src/github.com/go-fsnotify/fsnotify/.gitignore new file mode 100644 index 0000000000..4cd0cbaf43 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/.gitignore @@ -0,0 +1,6 @@ +# Setup a Global .gitignore for OS and editor generated files: +# https://help.github.com/articles/ignoring-files +# git config --global core.excludesfile ~/.gitignore_global + +.vagrant +*.sublime-project diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/.travis.yml b/vendor/src/github.com/go-fsnotify/fsnotify/.travis.yml new file mode 100644 index 0000000000..f8e76fc660 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/.travis.yml @@ -0,0 +1,13 @@ +language: go + +go: + - 1.2 + - tip + +# not yet https://github.com/travis-ci/travis-ci/issues/2318 +os: + - linux + - osx + +notifications: + email: false diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/AUTHORS b/vendor/src/github.com/go-fsnotify/fsnotify/AUTHORS new file mode 100644 index 0000000000..306091eda6 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/AUTHORS @@ -0,0 +1,32 @@ +# Names should be added to this file as +# Name or Organization +# The email address is not required for organizations. + +# You can update this list using the following command: +# +# $ git shortlog -se | awk '{print $2 " " $3 " " $4}' + +# Please keep the list sorted. + +Adrien Bustany +Caleb Spare +Case Nelson +Chris Howey +Christoffer Buchholz +Dave Cheney +Francisco Souza +Hari haran +John C Barstow +Kelvin Fo +Nathan Youngman +Paul Hammond +Pursuit92 +Rob Figueiredo +Soge Zhang +Tilak Sharma +Travis Cline +Tudor Golubenco +Yukang +bronze1man +debrando +henrikedwards diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/CHANGELOG.md b/vendor/src/github.com/go-fsnotify/fsnotify/CHANGELOG.md new file mode 100644 index 0000000000..79f4ddbaa1 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/CHANGELOG.md @@ -0,0 +1,237 @@ +# Changelog + +## v1.0.4 / 2014-09-07 + +* kqueue: add dragonfly to the build tags. +* Rename source code files, rearrange code so exported APIs are at the top. +* Add done channel to example code. [#37](https://github.com/go-fsnotify/fsnotify/pull/37) (thanks @chenyukang) + +## v1.0.3 / 2014-08-19 + +* [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/go-fsnotify/fsnotify/issues/36) + +## v1.0.2 / 2014-08-17 + +* [Fix] Missing create events on OS X. [#14](https://github.com/go-fsnotify/fsnotify/issues/14) (thanks @zhsso) +* [Fix] Make ./path and path equivalent. (thanks @zhsso) + +## v1.0.0 / 2014-08-15 + +* [API] Remove AddWatch on Windows, use Add. +* Improve documentation for exported identifiers. [#30](https://github.com/go-fsnotify/fsnotify/issues/30) +* Minor updates based on feedback from golint. + +## dev / 2014-07-09 + +* Moved to [github.com/go-fsnotify/fsnotify](https://github.com/go-fsnotify/fsnotify). +* Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno) + +## dev / 2014-07-04 + +* kqueue: fix incorrect mutex used in Close() +* Update example to demonstrate usage of Op. + +## dev / 2014-06-28 + +* [API] Don't set the Write Op for attribute notifications [#4](https://github.com/go-fsnotify/fsnotify/issues/4) +* Fix for String() method on Event (thanks Alex Brainman) +* Don't build on Plan 9 or Solaris (thanks @4ad) + +## dev / 2014-06-21 + +* Events channel of type Event rather than *Event. +* [internal] use syscall constants directly for inotify and kqueue. +* [internal] kqueue: rename events to kevents and fileEvent to event. + +## dev / 2014-06-19 + +* Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally). +* [internal] remove cookie from Event struct (unused). +* [internal] Event struct has the same definition across every OS. +* [internal] remove internal watch and removeWatch methods. + +## dev / 2014-06-12 + +* [API] Renamed Watch() to Add() and RemoveWatch() to Remove(). +* [API] Pluralized channel names: Events and Errors. +* [API] Renamed FileEvent struct to Event. +* [API] Op constants replace methods like IsCreate(). + +## dev / 2014-06-12 + +* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) + +## dev / 2014-05-23 + +* [API] Remove current implementation of WatchFlags. + * current implementation doesn't take advantage of OS for efficiency + * provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes + * no tests for the current implementation + * not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195) + +## v0.9.2 / 2014-08-17 + +* [Backport] Fix missing create events on OS X. [#14](https://github.com/go-fsnotify/fsnotify/issues/14) (thanks @zhsso) + +## v0.9.1 / 2014-06-12 + +* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) + +## v0.9.0 / 2014-01-17 + +* IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany) +* [Fix] kqueue: fix deadlock [#77][] (thanks @cespare) +* [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library. + +## v0.8.12 / 2013-11-13 + +* [API] Remove FD_SET and friends from Linux adapter + +## v0.8.11 / 2013-11-02 + +* [Doc] Add Changelog [#72][] (thanks @nathany) +* [Doc] Spotlight and double modify events on OS X [#62][] (reported by @paulhammond) + +## v0.8.10 / 2013-10-19 + +* [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott) +* [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer) +* [Doc] specify OS-specific limits in README (thanks @debrando) + +## v0.8.9 / 2013-09-08 + +* [Doc] Contributing (thanks @nathany) +* [Doc] update package path in example code [#63][] (thanks @paulhammond) +* [Doc] GoCI badge in README (Linux only) [#60][] +* [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany) + +## v0.8.8 / 2013-06-17 + +* [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie) + +## v0.8.7 / 2013-06-03 + +* [API] Make syscall flags internal +* [Fix] inotify: ignore event changes +* [Fix] race in symlink test [#45][] (reported by @srid) +* [Fix] tests on Windows +* lower case error messages + +## v0.8.6 / 2013-05-23 + +* kqueue: Use EVT_ONLY flag on Darwin +* [Doc] Update README with full example + +## v0.8.5 / 2013-05-09 + +* [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg) + +## v0.8.4 / 2013-04-07 + +* [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz) + +## v0.8.3 / 2013-03-13 + +* [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin) +* [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin) + +## v0.8.2 / 2013-02-07 + +* [Doc] add Authors +* [Fix] fix data races for map access [#29][] (thanks @fsouza) + +## v0.8.1 / 2013-01-09 + +* [Fix] Windows path separators +* [Doc] BSD License + +## v0.8.0 / 2012-11-09 + +* kqueue: directory watching improvements (thanks @vmirage) +* inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto) +* [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr) + +## v0.7.4 / 2012-10-09 + +* [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji) +* [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig) +* [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig) +* [Fix] kqueue: modify after recreation of file + +## v0.7.3 / 2012-09-27 + +* [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage) +* [Fix] kqueue: no longer get duplicate CREATE events + +## v0.7.2 / 2012-09-01 + +* kqueue: events for created directories + +## v0.7.1 / 2012-07-14 + +* [Fix] for renaming files + +## v0.7.0 / 2012-07-02 + +* [Feature] FSNotify flags +* [Fix] inotify: Added file name back to event path + +## v0.6.0 / 2012-06-06 + +* kqueue: watch files after directory created (thanks @tmc) + +## v0.5.1 / 2012-05-22 + +* [Fix] inotify: remove all watches before Close() + +## v0.5.0 / 2012-05-03 + +* [API] kqueue: return errors during watch instead of sending over channel +* kqueue: match symlink behavior on Linux +* inotify: add `DELETE_SELF` (requested by @taralx) +* [Fix] kqueue: handle EINTR (reported by @robfig) +* [Doc] Godoc example [#1][] (thanks @davecheney) + +## v0.4.0 / 2012-03-30 + +* Go 1 released: build with go tool +* [Feature] Windows support using winfsnotify +* Windows does not have attribute change notifications +* Roll attribute notifications into IsModify + +## v0.3.0 / 2012-02-19 + +* kqueue: add files when watch directory + +## v0.2.0 / 2011-12-30 + +* update to latest Go weekly code + +## v0.1.0 / 2011-10-19 + +* kqueue: add watch on file creation to match inotify +* kqueue: create file event +* inotify: ignore `IN_IGNORED` events +* event String() +* linux: common FileEvent functions +* initial commit + +[#79]: https://github.com/howeyc/fsnotify/pull/79 +[#77]: https://github.com/howeyc/fsnotify/pull/77 +[#72]: https://github.com/howeyc/fsnotify/issues/72 +[#71]: https://github.com/howeyc/fsnotify/issues/71 +[#70]: https://github.com/howeyc/fsnotify/issues/70 +[#63]: https://github.com/howeyc/fsnotify/issues/63 +[#62]: https://github.com/howeyc/fsnotify/issues/62 +[#60]: https://github.com/howeyc/fsnotify/issues/60 +[#59]: https://github.com/howeyc/fsnotify/issues/59 +[#49]: https://github.com/howeyc/fsnotify/issues/49 +[#45]: https://github.com/howeyc/fsnotify/issues/45 +[#40]: https://github.com/howeyc/fsnotify/issues/40 +[#36]: https://github.com/howeyc/fsnotify/issues/36 +[#33]: https://github.com/howeyc/fsnotify/issues/33 +[#29]: https://github.com/howeyc/fsnotify/issues/29 +[#25]: https://github.com/howeyc/fsnotify/issues/25 +[#24]: https://github.com/howeyc/fsnotify/issues/24 +[#21]: https://github.com/howeyc/fsnotify/issues/21 + diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/CONTRIBUTING.md b/vendor/src/github.com/go-fsnotify/fsnotify/CONTRIBUTING.md new file mode 100644 index 0000000000..2fd0423cca --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/CONTRIBUTING.md @@ -0,0 +1,56 @@ +# Contributing + +* Send questions to [golang-dev@googlegroups.com](mailto:golang-dev@googlegroups.com). + +### Issues + +* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/go-fsnotify/fsnotify/issues). +* Please indicate the platform you are running on. + +### Pull Requests + +A future version of Go will have [fsnotify in the standard library](https://code.google.com/p/go/issues/detail?id=4068), therefore fsnotify carries the same [LICENSE](https://github.com/go-fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so we need you to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual). + +Please indicate that you have signed the CLA in your pull request. + +To hack on fsnotify: + +1. Install as usual (`go get -u github.com/go-fsnotify/fsnotify`) +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Ensure everything works and the tests pass (see below) +4. Commit your changes (`git commit -am 'Add some feature'`) + +Contribute upstream: + +1. Fork fsnotify on GitHub +2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`) +3. Push to the branch (`git push fork my-new-feature`) +4. Create a new Pull Request on GitHub + +If other team members need your patch before I merge it: + +1. Install as usual (`go get -u github.com/go-fsnotify/fsnotify`) +2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`) +3. Pull your revisions (`git fetch fork; git checkout -b my-new-feature fork/my-new-feature`) + +Notice: For smooth sailing, always use the original import path. Installing with `go get` makes this easy. + +Note: The maintainers will update the CHANGELOG on your behalf. Please don't modify it in your pull request. + +### Testing + +fsnotify uses build tags to compile different code on Linux, BSD, OS X, and Windows. + +Before doing a pull request, please do your best to test your changes on multiple platforms, and list which platforms you were able/unable to test on. + +To make cross-platform testing easier, I've created a Vagrantfile for Linux and BSD. + +* Install [Vagrant](http://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) +* Setup [Vagrant Gopher](https://github.com/nathany/vagrant-gopher) in your `src` folder. +* Run `vagrant up` from the project folder. You can also setup just one box with `vagrant up linux` or `vagrant up bsd` (note: the BSD box doesn't support Windows hosts at this time, and NFS may prompt for your host OS password) +* Once setup, you can run the test suite on a given OS with a single command `vagrant ssh linux -c 'cd go-fsnotify/fsnotify; go test'`. +* When you're done, you will want to halt or destroy the Vagrant boxes. + +Notice: fsnotify file system events don't work on shared folders. The tests get around this limitation by using a tmp directory, but it is something to be aware of. + +Right now I don't have an equivalent solution for Windows and OS X, but there are Windows VMs [freely available from Microsoft](http://www.modern.ie/en-us/virtualization-tools#downloads). diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/LICENSE b/vendor/src/github.com/go-fsnotify/fsnotify/LICENSE new file mode 100644 index 0000000000..f21e540800 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2012 The Go Authors. All rights reserved. +Copyright (c) 2012 fsnotify Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/README.md b/vendor/src/github.com/go-fsnotify/fsnotify/README.md new file mode 100644 index 0000000000..0759284269 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/README.md @@ -0,0 +1,53 @@ +# File system notifications for Go + +[![Coverage](http://gocover.io/_badge/github.com/go-fsnotify/fsnotify)](http://gocover.io/github.com/go-fsnotify/fsnotify) [![GoDoc](https://godoc.org/gopkg.in/fsnotify.v1?status.svg)](https://godoc.org/gopkg.in/fsnotify.v1) + +Cross platform: Windows, Linux, BSD and OS X. + +|Adapter |OS |Status | +|----------|----------|----------| +|inotify |Linux, Android\*|Supported| +|kqueue |BSD, OS X, iOS\*|Supported| +|ReadDirectoryChangesW|Windows|Supported| +|FSEvents |OS X |[Planned](https://github.com/go-fsnotify/fsnotify/issues/11)| +|FEN |Solaris 11 |[Planned](https://github.com/go-fsnotify/fsnotify/issues/12)| +|fanotify |Linux 2.6.37+ | | +|Polling |*All* |[Maybe](https://github.com/go-fsnotify/fsnotify/issues/9)| +| |Plan 9 | | + +\* Android and iOS are untested. + +Please see [the documentation](https://godoc.org/gopkg.in/fsnotify.v1) for usage. Consult the [Wiki](https://github.com/go-fsnotify/fsnotify/wiki) for the FAQ and further information. + +## API stability + +Two major versions of fsnotify exist. + +**[fsnotify.v1](https://gopkg.in/fsnotify.v1)** provides [a new API](https://godoc.org/gopkg.in/fsnotify.v1) based on [this design document](http://goo.gl/MrYxyA). You can import v1 with: + +```go +import "gopkg.in/fsnotify.v1" +``` + +\* Refer to the package as fsnotify (without the .v1 suffix). + +**[fsnotify.v0](https://gopkg.in/fsnotify.v0)** is API-compatible with [howeyc/fsnotify](https://godoc.org/github.com/howeyc/fsnotify). Bugfixes *may* be backported, but I recommend upgrading to v1. + +```go +import "gopkg.in/fsnotify.v0" +``` + +Further API changes are [planned](https://github.com/go-fsnotify/fsnotify/milestones), but a new major revision will be tagged, so you can depend on the v1 API. + +## Contributing + +* Send questions to [golang-dev@googlegroups.com](mailto:golang-dev@googlegroups.com). +* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/go-fsnotify/fsnotify/issues). + +A future version of Go will have [fsnotify in the standard library](https://code.google.com/p/go/issues/detail?id=4068), therefore fsnotify carries the same [LICENSE](https://github.com/go-fsnotify/fsnotify/blob/master/LICENSE) as Go. Contributors retain their copyright, so we need you to fill out a short form before we can accept your contribution: [Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual). + +Please read [CONTRIBUTING](https://github.com/go-fsnotify/fsnotify/blob/master/CONTRIBUTING.md) before opening a pull request. + +## Example + +See [example_test.go](https://github.com/go-fsnotify/fsnotify/blob/master/example_test.go). diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/example_test.go b/vendor/src/github.com/go-fsnotify/fsnotify/example_test.go new file mode 100644 index 0000000000..9f2c63f475 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/example_test.go @@ -0,0 +1,42 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !plan9,!solaris + +package fsnotify_test + +import ( + "log" + + "gopkg.in/fsnotify.v1" +) + +func ExampleNewWatcher() { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + defer watcher.Close() + + done := make(chan bool) + go func() { + for { + select { + case event := <-watcher.Events: + log.Println("event:", event) + if event.Op&fsnotify.Write == fsnotify.Write { + log.Println("modified file:", event.Name) + } + case err := <-watcher.Errors: + log.Println("error:", err) + } + } + }() + + err = watcher.Add("/tmp/foo") + if err != nil { + log.Fatal(err) + } + <-done +} diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/fsnotify.go b/vendor/src/github.com/go-fsnotify/fsnotify/fsnotify.go new file mode 100644 index 0000000000..7b5233f4bb --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/fsnotify.go @@ -0,0 +1,56 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !plan9,!solaris + +// Package fsnotify provides a platform-independent interface for file system notifications. +package fsnotify + +import "fmt" + +// Event represents a single file system notification. +type Event struct { + Name string // Relative path to the file or directory. + Op Op // File operation that triggered the event. +} + +// Op describes a set of file operations. +type Op uint32 + +// These are the generalized file operations that can trigger a notification. +const ( + Create Op = 1 << iota + Write + Remove + Rename + Chmod +) + +// String returns a string representation of the event in the form +// "file: REMOVE|WRITE|..." +func (e Event) String() string { + events := "" + + if e.Op&Create == Create { + events += "|CREATE" + } + if e.Op&Remove == Remove { + events += "|REMOVE" + } + if e.Op&Write == Write { + events += "|WRITE" + } + if e.Op&Rename == Rename { + events += "|RENAME" + } + if e.Op&Chmod == Chmod { + events += "|CHMOD" + } + + if len(events) > 0 { + events = events[1:] + } + + return fmt.Sprintf("%q: %s", e.Name, events) +} diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/inotify.go b/vendor/src/github.com/go-fsnotify/fsnotify/inotify.go new file mode 100644 index 0000000000..f5c0aaef04 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/inotify.go @@ -0,0 +1,239 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build linux + +package fsnotify + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "syscall" + "unsafe" +) + +// Watcher watches a set of files, delivering events to a channel. +type Watcher struct { + Events chan Event + Errors chan error + mu sync.Mutex // Map access + fd int // File descriptor (as returned by the inotify_init() syscall) + watches map[string]*watch // Map of inotify watches (key: path) + paths map[int]string // Map of watched paths (key: watch descriptor) + done chan bool // Channel for sending a "quit message" to the reader goroutine + isClosed bool // Set to true when Close() is first called +} + +// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. +func NewWatcher() (*Watcher, error) { + fd, errno := syscall.InotifyInit() + if fd == -1 { + return nil, os.NewSyscallError("inotify_init", errno) + } + w := &Watcher{ + fd: fd, + watches: make(map[string]*watch), + paths: make(map[int]string), + Events: make(chan Event), + Errors: make(chan error), + done: make(chan bool, 1), + } + + go w.readEvents() + return w, nil +} + +// Close removes all watches and closes the events channel. +func (w *Watcher) Close() error { + if w.isClosed { + return nil + } + w.isClosed = true + + // Remove all watches + for name := range w.watches { + w.Remove(name) + } + + // Send "quit" message to the reader goroutine + w.done <- true + + return nil +} + +// Add starts watching the named file or directory (non-recursively). +func (w *Watcher) Add(name string) error { + name = filepath.Clean(name) + if w.isClosed { + return errors.New("inotify instance already closed") + } + + const agnosticEvents = syscall.IN_MOVED_TO | syscall.IN_MOVED_FROM | + syscall.IN_CREATE | syscall.IN_ATTRIB | syscall.IN_MODIFY | + syscall.IN_MOVE_SELF | syscall.IN_DELETE | syscall.IN_DELETE_SELF + + var flags uint32 = agnosticEvents + + w.mu.Lock() + watchEntry, found := w.watches[name] + w.mu.Unlock() + if found { + watchEntry.flags |= flags + flags |= syscall.IN_MASK_ADD + } + wd, errno := syscall.InotifyAddWatch(w.fd, name, flags) + if wd == -1 { + return os.NewSyscallError("inotify_add_watch", errno) + } + + w.mu.Lock() + w.watches[name] = &watch{wd: uint32(wd), flags: flags} + w.paths[wd] = name + w.mu.Unlock() + + return nil +} + +// Remove stops watching the the named file or directory (non-recursively). +func (w *Watcher) Remove(name string) error { + name = filepath.Clean(name) + w.mu.Lock() + defer w.mu.Unlock() + watch, ok := w.watches[name] + if !ok { + return fmt.Errorf("can't remove non-existent inotify watch for: %s", name) + } + success, errno := syscall.InotifyRmWatch(w.fd, watch.wd) + if success == -1 { + return os.NewSyscallError("inotify_rm_watch", errno) + } + delete(w.watches, name) + return nil +} + +type watch struct { + wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) + flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) +} + +// readEvents reads from the inotify file descriptor, converts the +// received events into Event objects and sends them via the Events channel +func (w *Watcher) readEvents() { + var ( + buf [syscall.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events + n int // Number of bytes read with read() + errno error // Syscall errno + ) + + for { + // See if there is a message on the "done" channel + select { + case <-w.done: + syscall.Close(w.fd) + close(w.Events) + close(w.Errors) + return + default: + } + + n, errno = syscall.Read(w.fd, buf[:]) + + // If EOF is received + if n == 0 { + syscall.Close(w.fd) + close(w.Events) + close(w.Errors) + return + } + + if n < 0 { + w.Errors <- os.NewSyscallError("read", errno) + continue + } + if n < syscall.SizeofInotifyEvent { + w.Errors <- errors.New("inotify: short read in readEvents()") + continue + } + + var offset uint32 + // We don't know how many events we just read into the buffer + // While the offset points to at least one whole event... + for offset <= uint32(n-syscall.SizeofInotifyEvent) { + // Point "raw" to the event in the buffer + raw := (*syscall.InotifyEvent)(unsafe.Pointer(&buf[offset])) + + mask := uint32(raw.Mask) + nameLen := uint32(raw.Len) + // If the event happened to the watched directory or the watched file, the kernel + // doesn't append the filename to the event, but we would like to always fill the + // the "Name" field with a valid filename. We retrieve the path of the watch from + // the "paths" map. + w.mu.Lock() + name := w.paths[int(raw.Wd)] + w.mu.Unlock() + if nameLen > 0 { + // Point "bytes" at the first byte of the filename + bytes := (*[syscall.PathMax]byte)(unsafe.Pointer(&buf[offset+syscall.SizeofInotifyEvent])) + // The filename is padded with NULL bytes. TrimRight() gets rid of those. + name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") + } + + event := newEvent(name, mask) + + // Send the events that are not ignored on the events channel + if !event.ignoreLinux(mask) { + w.Events <- event + } + + // Move to the next event in the buffer + offset += syscall.SizeofInotifyEvent + nameLen + } + } +} + +// Certain types of events can be "ignored" and not sent over the Events +// channel. Such as events marked ignore by the kernel, or MODIFY events +// against files that do not exist. +func (e *Event) ignoreLinux(mask uint32) bool { + // Ignore anything the inotify API says to ignore + if mask&syscall.IN_IGNORED == syscall.IN_IGNORED { + return true + } + + // If the event is not a DELETE or RENAME, the file must exist. + // Otherwise the event is ignored. + // *Note*: this was put in place because it was seen that a MODIFY + // event was sent after the DELETE. This ignores that MODIFY and + // assumes a DELETE will come or has come if the file doesn't exist. + if !(e.Op&Remove == Remove || e.Op&Rename == Rename) { + _, statErr := os.Lstat(e.Name) + return os.IsNotExist(statErr) + } + return false +} + +// newEvent returns an platform-independent Event based on an inotify mask. +func newEvent(name string, mask uint32) Event { + e := Event{Name: name} + if mask&syscall.IN_CREATE == syscall.IN_CREATE || mask&syscall.IN_MOVED_TO == syscall.IN_MOVED_TO { + e.Op |= Create + } + if mask&syscall.IN_DELETE_SELF == syscall.IN_DELETE_SELF || mask&syscall.IN_DELETE == syscall.IN_DELETE { + e.Op |= Remove + } + if mask&syscall.IN_MODIFY == syscall.IN_MODIFY { + e.Op |= Write + } + if mask&syscall.IN_MOVE_SELF == syscall.IN_MOVE_SELF || mask&syscall.IN_MOVED_FROM == syscall.IN_MOVED_FROM { + e.Op |= Rename + } + if mask&syscall.IN_ATTRIB == syscall.IN_ATTRIB { + e.Op |= Chmod + } + return e +} diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/integration_test.go b/vendor/src/github.com/go-fsnotify/fsnotify/integration_test.go new file mode 100644 index 0000000000..ad51ab60b2 --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/integration_test.go @@ -0,0 +1,1120 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !plan9,!solaris + +package fsnotify + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync/atomic" + "testing" + "time" +) + +// An atomic counter +type counter struct { + val int32 +} + +func (c *counter) increment() { + atomic.AddInt32(&c.val, 1) +} + +func (c *counter) value() int32 { + return atomic.LoadInt32(&c.val) +} + +func (c *counter) reset() { + atomic.StoreInt32(&c.val, 0) +} + +// tempMkdir makes a temporary directory +func tempMkdir(t *testing.T) string { + dir, err := ioutil.TempDir("", "fsnotify") + if err != nil { + t.Fatalf("failed to create test directory: %s", err) + } + return dir +} + +// newWatcher initializes an fsnotify Watcher instance. +func newWatcher(t *testing.T) *Watcher { + watcher, err := NewWatcher() + if err != nil { + t.Fatalf("NewWatcher() failed: %s", err) + } + return watcher +} + +// addWatch adds a watch for a directory +func addWatch(t *testing.T, watcher *Watcher, dir string) { + if err := watcher.Add(dir); err != nil { + t.Fatalf("watcher.Add(%q) failed: %s", dir, err) + } +} + +func TestFsnotifyMultipleOperations(t *testing.T) { + watcher := newWatcher(t) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create directory that's not watched + testDirToMoveFiles := tempMkdir(t) + defer os.RemoveAll(testDirToMoveFiles) + + testFile := filepath.Join(testDir, "TestFsnotifySeq.testfile") + testFileRenamed := filepath.Join(testDirToMoveFiles, "TestFsnotifySeqRename.testfile") + + addWatch(t, watcher, testDir) + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var createReceived, modifyReceived, deleteReceived, renameReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) { + t.Logf("event received: %s", event) + if event.Op&Remove == Remove { + deleteReceived.increment() + } + if event.Op&Write == Write { + modifyReceived.increment() + } + if event.Op&Create == Create { + createReceived.increment() + } + if event.Op&Rename == Rename { + renameReceived.increment() + } + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + time.Sleep(time.Millisecond) + f.WriteString("data") + f.Sync() + f.Close() + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + if err := testRename(testFile, testFileRenamed); err != nil { + t.Fatalf("rename failed: %s", err) + } + + // Modify the file outside of the watched dir + f, err = os.Open(testFileRenamed) + if err != nil { + t.Fatalf("open test renamed file failed: %s", err) + } + f.WriteString("data") + f.Sync() + f.Close() + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + // Recreate the file that was moved + f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Close() + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + cReceived := createReceived.value() + if cReceived != 2 { + t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 2) + } + mReceived := modifyReceived.value() + if mReceived != 1 { + t.Fatalf("incorrect number of modify events received after 500 ms (%d vs %d)", mReceived, 1) + } + dReceived := deleteReceived.value() + rReceived := renameReceived.value() + if dReceived+rReceived != 1 { + t.Fatalf("incorrect number of rename+delete events received after 500 ms (%d vs %d)", rReceived+dReceived, 1) + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } +} + +func TestFsnotifyMultipleCreates(t *testing.T) { + watcher := newWatcher(t) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + testFile := filepath.Join(testDir, "TestFsnotifySeq.testfile") + + addWatch(t, watcher, testDir) + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var createReceived, modifyReceived, deleteReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) { + t.Logf("event received: %s", event) + if event.Op&Remove == Remove { + deleteReceived.increment() + } + if event.Op&Create == Create { + createReceived.increment() + } + if event.Op&Write == Write { + modifyReceived.increment() + } + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + time.Sleep(time.Millisecond) + f.WriteString("data") + f.Sync() + f.Close() + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + os.Remove(testFile) + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + // Recreate the file + f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Close() + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + // Modify + f, err = os.OpenFile(testFile, os.O_WRONLY, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + time.Sleep(time.Millisecond) + f.WriteString("data") + f.Sync() + f.Close() + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + // Modify + f, err = os.OpenFile(testFile, os.O_WRONLY, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + time.Sleep(time.Millisecond) + f.WriteString("data") + f.Sync() + f.Close() + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + cReceived := createReceived.value() + if cReceived != 2 { + t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 2) + } + mReceived := modifyReceived.value() + if mReceived < 3 { + t.Fatalf("incorrect number of modify events received after 500 ms (%d vs atleast %d)", mReceived, 3) + } + dReceived := deleteReceived.value() + if dReceived != 1 { + t.Fatalf("incorrect number of rename+delete events received after 500 ms (%d vs %d)", dReceived, 1) + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } +} + +func TestFsnotifyDirOnly(t *testing.T) { + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create a file before watching directory + // This should NOT add any events to the fsnotify event queue + testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") + { + var f *os.File + f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + } + + addWatch(t, watcher, testDir) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + testFile := filepath.Join(testDir, "TestFsnotifyDirOnly.testfile") + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var createReceived, modifyReceived, deleteReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) || event.Name == filepath.Clean(testFileAlreadyExists) { + t.Logf("event received: %s", event) + if event.Op&Remove == Remove { + deleteReceived.increment() + } + if event.Op&Write == Write { + modifyReceived.increment() + } + if event.Op&Create == Create { + createReceived.increment() + } + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + time.Sleep(time.Millisecond) + f.WriteString("data") + f.Sync() + f.Close() + + time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete + + os.Remove(testFile) + os.Remove(testFileAlreadyExists) + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + cReceived := createReceived.value() + if cReceived != 1 { + t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 1) + } + mReceived := modifyReceived.value() + if mReceived != 1 { + t.Fatalf("incorrect number of modify events received after 500 ms (%d vs %d)", mReceived, 1) + } + dReceived := deleteReceived.value() + if dReceived != 2 { + t.Fatalf("incorrect number of delete events received after 500 ms (%d vs %d)", dReceived, 2) + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } +} + +func TestFsnotifyDeleteWatchedDir(t *testing.T) { + watcher := newWatcher(t) + defer watcher.Close() + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create a file before watching directory + testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") + { + var f *os.File + f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + } + + addWatch(t, watcher, testDir) + + // Add a watch for testFile + addWatch(t, watcher, testFileAlreadyExists) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var deleteReceived counter + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFileAlreadyExists) { + t.Logf("event received: %s", event) + if event.Op&Remove == Remove { + deleteReceived.increment() + } + } else { + t.Logf("unexpected event received: %s", event) + } + } + }() + + os.RemoveAll(testDir) + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + dReceived := deleteReceived.value() + if dReceived < 2 { + t.Fatalf("did not receive at least %d delete events, received %d after 500 ms", 2, dReceived) + } +} + +func TestFsnotifySubDir(t *testing.T) { + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + testFile1 := filepath.Join(testDir, "TestFsnotifyFile1.testfile") + testSubDir := filepath.Join(testDir, "sub") + testSubDirFile := filepath.Join(testDir, "sub/TestFsnotifyFile1.testfile") + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var createReceived, deleteReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testSubDir) || event.Name == filepath.Clean(testFile1) { + t.Logf("event received: %s", event) + if event.Op&Create == Create { + createReceived.increment() + } + if event.Op&Remove == Remove { + deleteReceived.increment() + } + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + addWatch(t, watcher, testDir) + + // Create sub-directory + if err := os.Mkdir(testSubDir, 0777); err != nil { + t.Fatalf("failed to create test sub-directory: %s", err) + } + + // Create a file + var f *os.File + f, err := os.OpenFile(testFile1, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + + // Create a file (Should not see this! we are not watching subdir) + var fs *os.File + fs, err = os.OpenFile(testSubDirFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + fs.Sync() + fs.Close() + + time.Sleep(200 * time.Millisecond) + + // Make sure receive deletes for both file and sub-directory + os.RemoveAll(testSubDir) + os.Remove(testFile1) + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + cReceived := createReceived.value() + if cReceived != 2 { + t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 2) + } + dReceived := deleteReceived.value() + if dReceived != 2 { + t.Fatalf("incorrect number of delete events received after 500 ms (%d vs %d)", dReceived, 2) + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } +} + +func TestFsnotifyRename(t *testing.T) { + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + addWatch(t, watcher, testDir) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + testFile := filepath.Join(testDir, "TestFsnotifyEvents.testfile") + testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var renameReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) || event.Name == filepath.Clean(testFileRenamed) { + if event.Op&Rename == Rename { + renameReceived.increment() + } + t.Logf("event received: %s", event) + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + f.WriteString("data") + f.Sync() + f.Close() + + // Add a watch for testFile + addWatch(t, watcher, testFile) + + if err := testRename(testFile, testFileRenamed); err != nil { + t.Fatalf("rename failed: %s", err) + } + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + if renameReceived.value() == 0 { + t.Fatal("fsnotify rename events have not been received after 500 ms") + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } + + os.Remove(testFileRenamed) +} + +func TestFsnotifyRenameToCreate(t *testing.T) { + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create directory to get file + testDirFrom := tempMkdir(t) + defer os.RemoveAll(testDirFrom) + + addWatch(t, watcher, testDir) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + testFile := filepath.Join(testDirFrom, "TestFsnotifyEvents.testfile") + testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var createReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) || event.Name == filepath.Clean(testFileRenamed) { + if event.Op&Create == Create { + createReceived.increment() + } + t.Logf("event received: %s", event) + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + + if err := testRename(testFile, testFileRenamed); err != nil { + t.Fatalf("rename failed: %s", err) + } + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + if createReceived.value() == 0 { + t.Fatal("fsnotify create events have not been received after 500 ms") + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } + + os.Remove(testFileRenamed) +} + +func TestFsnotifyRenameToOverwrite(t *testing.T) { + switch runtime.GOOS { + case "plan9", "windows": + t.Skipf("skipping test on %q (os.Rename over existing file does not create event).", runtime.GOOS) + } + + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create directory to get file + testDirFrom := tempMkdir(t) + defer os.RemoveAll(testDirFrom) + + testFile := filepath.Join(testDirFrom, "TestFsnotifyEvents.testfile") + testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") + + // Create a file + var fr *os.File + fr, err := os.OpenFile(testFileRenamed, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + fr.Sync() + fr.Close() + + addWatch(t, watcher, testDir) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + var eventReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testFileRenamed) { + eventReceived.increment() + t.Logf("event received: %s", event) + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + + if err := testRename(testFile, testFileRenamed); err != nil { + t.Fatalf("rename failed: %s", err) + } + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + if eventReceived.value() == 0 { + t.Fatal("fsnotify events have not been received after 500 ms") + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(2 * time.Second): + t.Fatal("event stream was not closed after 2 seconds") + } + + os.Remove(testFileRenamed) +} + +func TestRemovalOfWatch(t *testing.T) { + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create a file before watching directory + testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") + { + var f *os.File + f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + } + + watcher := newWatcher(t) + defer watcher.Close() + + addWatch(t, watcher, testDir) + if err := watcher.Remove(testDir); err != nil { + t.Fatalf("Could not remove the watch: %v\n", err) + } + + go func() { + select { + case ev := <-watcher.Events: + t.Fatalf("We received event: %v\n", ev) + case <-time.After(500 * time.Millisecond): + t.Log("No event received, as expected.") + } + }() + + time.Sleep(200 * time.Millisecond) + // Modify the file outside of the watched dir + f, err := os.Open(testFileAlreadyExists) + if err != nil { + t.Fatalf("Open test file failed: %s", err) + } + f.WriteString("data") + f.Sync() + f.Close() + if err := os.Chmod(testFileAlreadyExists, 0700); err != nil { + t.Fatalf("chmod failed: %s", err) + } + time.Sleep(400 * time.Millisecond) +} + +func TestFsnotifyAttrib(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("attributes don't work on Windows.") + } + + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Errors { + t.Fatalf("error received: %s", err) + } + }() + + testFile := filepath.Join(testDir, "TestFsnotifyAttrib.testfile") + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Events + // The modifyReceived counter counts IsModify events that are not IsAttrib, + // and the attribReceived counts IsAttrib events (which are also IsModify as + // a consequence). + var modifyReceived counter + var attribReceived counter + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) { + if event.Op&Write == Write { + modifyReceived.increment() + } + if event.Op&Chmod == Chmod { + attribReceived.increment() + } + t.Logf("event received: %s", event) + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + f.WriteString("data") + f.Sync() + f.Close() + + // Add a watch for testFile + addWatch(t, watcher, testFile) + + if err := os.Chmod(testFile, 0700); err != nil { + t.Fatalf("chmod failed: %s", err) + } + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + // Creating/writing a file changes also the mtime, so IsAttrib should be set to true here + time.Sleep(500 * time.Millisecond) + if modifyReceived.value() != 0 { + t.Fatal("received an unexpected modify event when creating a test file") + } + if attribReceived.value() == 0 { + t.Fatal("fsnotify attribute events have not received after 500 ms") + } + + // Modifying the contents of the file does not set the attrib flag (although eg. the mtime + // might have been modified). + modifyReceived.reset() + attribReceived.reset() + + f, err = os.OpenFile(testFile, os.O_WRONLY, 0) + if err != nil { + t.Fatalf("reopening test file failed: %s", err) + } + + f.WriteString("more data") + f.Sync() + f.Close() + + time.Sleep(500 * time.Millisecond) + + if modifyReceived.value() != 1 { + t.Fatal("didn't receive a modify event after changing test file contents") + } + + if attribReceived.value() != 0 { + t.Fatal("did receive an unexpected attrib event after changing test file contents") + } + + modifyReceived.reset() + attribReceived.reset() + + // Doing a chmod on the file should trigger an event with the "attrib" flag set (the contents + // of the file are not changed though) + if err := os.Chmod(testFile, 0600); err != nil { + t.Fatalf("chmod failed: %s", err) + } + + time.Sleep(500 * time.Millisecond) + + if attribReceived.value() != 1 { + t.Fatal("didn't receive an attribute change after 500ms") + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(1e9): + t.Fatal("event stream was not closed after 1 second") + } + + os.Remove(testFile) +} + +func TestFsnotifyClose(t *testing.T) { + watcher := newWatcher(t) + watcher.Close() + + var done int32 + go func() { + watcher.Close() + atomic.StoreInt32(&done, 1) + }() + + time.Sleep(50e6) // 50 ms + if atomic.LoadInt32(&done) == 0 { + t.Fatal("double Close() test failed: second Close() call didn't return") + } + + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + if err := watcher.Add(testDir); err == nil { + t.Fatal("expected error on Watch() after Close(), got nil") + } +} + +func TestFsnotifyFakeSymlink(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlinks don't work on Windows.") + } + + watcher := newWatcher(t) + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + var errorsReceived counter + // Receive errors on the error channel on a separate goroutine + go func() { + for errors := range watcher.Errors { + t.Logf("Received error: %s", errors) + errorsReceived.increment() + } + }() + + // Count the CREATE events received + var createEventsReceived, otherEventsReceived counter + go func() { + for ev := range watcher.Events { + t.Logf("event received: %s", ev) + if ev.Op&Create == Create { + createEventsReceived.increment() + } else { + otherEventsReceived.increment() + } + } + }() + + addWatch(t, watcher, testDir) + + if err := os.Symlink(filepath.Join(testDir, "zzz"), filepath.Join(testDir, "zzznew")); err != nil { + t.Fatalf("Failed to create bogus symlink: %s", err) + } + t.Logf("Created bogus symlink") + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500 * time.Millisecond) + + // Should not be error, just no events for broken links (watching nothing) + if errorsReceived.value() > 0 { + t.Fatal("fsnotify errors have been received.") + } + if otherEventsReceived.value() > 0 { + t.Fatal("fsnotify other events received on the broken link") + } + + // Except for 1 create event (for the link itself) + if createEventsReceived.value() == 0 { + t.Fatal("fsnotify create events were not received after 500 ms") + } + if createEventsReceived.value() > 1 { + t.Fatal("fsnotify more create events received than expected") + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() +} + +// TestConcurrentRemovalOfWatch tests that concurrent calls to RemoveWatch do not race. +// See https://codereview.appspot.com/103300045/ +// go test -test.run=TestConcurrentRemovalOfWatch -test.cpu=1,1,1,1,1 -race +func TestConcurrentRemovalOfWatch(t *testing.T) { + if runtime.GOOS != "darwin" { + t.Skip("regression test for race only present on darwin") + } + + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create a file before watching directory + testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") + { + var f *os.File + f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + } + + watcher := newWatcher(t) + defer watcher.Close() + + addWatch(t, watcher, testDir) + + // Test that RemoveWatch can be invoked concurrently, with no data races. + removed1 := make(chan struct{}) + go func() { + defer close(removed1) + watcher.Remove(testDir) + }() + removed2 := make(chan struct{}) + go func() { + close(removed2) + watcher.Remove(testDir) + }() + <-removed1 + <-removed2 +} + +func testRename(file1, file2 string) error { + switch runtime.GOOS { + case "windows", "plan9": + return os.Rename(file1, file2) + default: + cmd := exec.Command("mv", file1, file2) + return cmd.Run() + } +} diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/kqueue.go b/vendor/src/github.com/go-fsnotify/fsnotify/kqueue.go new file mode 100644 index 0000000000..5ef1346c0d --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/kqueue.go @@ -0,0 +1,479 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build freebsd openbsd netbsd dragonfly darwin + +package fsnotify + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sync" + "syscall" +) + +// Watcher watches a set of files, delivering events to a channel. +type Watcher struct { + Events chan Event + Errors chan error + mu sync.Mutex // Mutex for the Watcher itself. + kq int // File descriptor (as returned by the kqueue() syscall). + watches map[string]int // Map of watched file descriptors (key: path). + wmut sync.Mutex // Protects access to watches. + enFlags map[string]uint32 // Map of watched files to evfilt note flags used in kqueue. + enmut sync.Mutex // Protects access to enFlags. + paths map[int]string // Map of watched paths (key: watch descriptor). + finfo map[int]os.FileInfo // Map of file information (isDir, isReg; key: watch descriptor). + pmut sync.Mutex // Protects access to paths and finfo. + fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events). + femut sync.Mutex // Protects access to fileExists. + externalWatches map[string]bool // Map of watches added by user of the library. + ewmut sync.Mutex // Protects access to externalWatches. + done chan bool // Channel for sending a "quit message" to the reader goroutine + isClosed bool // Set to true when Close() is first called +} + +// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. +func NewWatcher() (*Watcher, error) { + fd, errno := syscall.Kqueue() + if fd == -1 { + return nil, os.NewSyscallError("kqueue", errno) + } + w := &Watcher{ + kq: fd, + watches: make(map[string]int), + enFlags: make(map[string]uint32), + paths: make(map[int]string), + finfo: make(map[int]os.FileInfo), + fileExists: make(map[string]bool), + externalWatches: make(map[string]bool), + Events: make(chan Event), + Errors: make(chan error), + done: make(chan bool, 1), + } + + go w.readEvents() + return w, nil +} + +// Close removes all watches and closes the events channel. +func (w *Watcher) Close() error { + w.mu.Lock() + if w.isClosed { + w.mu.Unlock() + return nil + } + w.isClosed = true + w.mu.Unlock() + + // Send "quit" message to the reader goroutine: + w.done <- true + w.wmut.Lock() + ws := w.watches + w.wmut.Unlock() + for name := range ws { + w.Remove(name) + } + + return nil +} + +// Add starts watching the named file or directory (non-recursively). +func (w *Watcher) Add(name string) error { + w.ewmut.Lock() + w.externalWatches[name] = true + w.ewmut.Unlock() + return w.addWatch(name, noteAllEvents) +} + +// Remove stops watching the the named file or directory (non-recursively). +func (w *Watcher) Remove(name string) error { + name = filepath.Clean(name) + w.wmut.Lock() + watchfd, ok := w.watches[name] + w.wmut.Unlock() + if !ok { + return fmt.Errorf("can't remove non-existent kevent watch for: %s", name) + } + var kbuf [1]syscall.Kevent_t + watchEntry := &kbuf[0] + syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_DELETE) + entryFlags := watchEntry.Flags + success, errno := syscall.Kevent(w.kq, kbuf[:], nil, nil) + if success == -1 { + return os.NewSyscallError("kevent_rm_watch", errno) + } else if (entryFlags & syscall.EV_ERROR) == syscall.EV_ERROR { + return errors.New("kevent rm error") + } + syscall.Close(watchfd) + w.wmut.Lock() + delete(w.watches, name) + w.wmut.Unlock() + w.enmut.Lock() + delete(w.enFlags, name) + w.enmut.Unlock() + w.pmut.Lock() + delete(w.paths, watchfd) + fInfo := w.finfo[watchfd] + delete(w.finfo, watchfd) + w.pmut.Unlock() + + // Find all watched paths that are in this directory that are not external. + if fInfo.IsDir() { + var pathsToRemove []string + w.pmut.Lock() + for _, wpath := range w.paths { + wdir, _ := filepath.Split(wpath) + if filepath.Clean(wdir) == filepath.Clean(name) { + w.ewmut.Lock() + if !w.externalWatches[wpath] { + pathsToRemove = append(pathsToRemove, wpath) + } + w.ewmut.Unlock() + } + } + w.pmut.Unlock() + for _, name := range pathsToRemove { + // Since these are internal, not much sense in propagating error + // to the user, as that will just confuse them with an error about + // a path they did not explicitly watch themselves. + w.Remove(name) + } + } + + return nil +} + +const ( + // Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE) + noteAllEvents = syscall.NOTE_DELETE | syscall.NOTE_WRITE | syscall.NOTE_ATTRIB | syscall.NOTE_RENAME + + // Block for 100 ms on each call to kevent + keventWaitTime = 100e6 +) + +// addWatch adds path to the watched file set. +// The flags are interpreted as described in kevent(2). +func (w *Watcher) addWatch(path string, flags uint32) error { + path = filepath.Clean(path) + w.mu.Lock() + if w.isClosed { + w.mu.Unlock() + return errors.New("kevent instance already closed") + } + w.mu.Unlock() + + watchDir := false + + w.wmut.Lock() + watchfd, found := w.watches[path] + w.wmut.Unlock() + if !found { + fi, errstat := os.Lstat(path) + if errstat != nil { + return errstat + } + + // don't watch socket + if fi.Mode()&os.ModeSocket == os.ModeSocket { + return nil + } + + // Follow Symlinks + // Unfortunately, Linux can add bogus symlinks to watch list without + // issue, and Windows can't do symlinks period (AFAIK). To maintain + // consistency, we will act like everything is fine. There will simply + // be no file events for broken symlinks. + // Hence the returns of nil on errors. + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + path, err := filepath.EvalSymlinks(path) + if err != nil { + return nil + } + + fi, errstat = os.Lstat(path) + if errstat != nil { + return nil + } + } + + fd, errno := syscall.Open(path, openMode, 0700) + if fd == -1 { + return os.NewSyscallError("Open", errno) + } + watchfd = fd + + w.wmut.Lock() + w.watches[path] = watchfd + w.wmut.Unlock() + + w.pmut.Lock() + w.paths[watchfd] = path + w.finfo[watchfd] = fi + w.pmut.Unlock() + } + // Watch the directory if it has not been watched before. + w.pmut.Lock() + w.enmut.Lock() + if w.finfo[watchfd].IsDir() && + (flags&syscall.NOTE_WRITE) == syscall.NOTE_WRITE && + (!found || (w.enFlags[path]&syscall.NOTE_WRITE) != syscall.NOTE_WRITE) { + watchDir = true + } + w.enmut.Unlock() + w.pmut.Unlock() + + w.enmut.Lock() + w.enFlags[path] = flags + w.enmut.Unlock() + + var kbuf [1]syscall.Kevent_t + watchEntry := &kbuf[0] + watchEntry.Fflags = flags + syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_ADD|syscall.EV_CLEAR) + entryFlags := watchEntry.Flags + success, errno := syscall.Kevent(w.kq, kbuf[:], nil, nil) + if success == -1 { + return errno + } else if (entryFlags & syscall.EV_ERROR) == syscall.EV_ERROR { + return errors.New("kevent add error") + } + + if watchDir { + errdir := w.watchDirectoryFiles(path) + if errdir != nil { + return errdir + } + } + return nil +} + +// readEvents reads from the kqueue file descriptor, converts the +// received events into Event objects and sends them via the Events channel +func (w *Watcher) readEvents() { + var ( + keventbuf [10]syscall.Kevent_t // Event buffer + kevents []syscall.Kevent_t // Received events + twait *syscall.Timespec // Time to block waiting for events + n int // Number of events returned from kevent + errno error // Syscall errno + ) + kevents = keventbuf[0:0] + twait = new(syscall.Timespec) + *twait = syscall.NsecToTimespec(keventWaitTime) + + for { + // See if there is a message on the "done" channel + var done bool + select { + case done = <-w.done: + default: + } + + // If "done" message is received + if done { + errno := syscall.Close(w.kq) + if errno != nil { + w.Errors <- os.NewSyscallError("close", errno) + } + close(w.Events) + close(w.Errors) + return + } + + // Get new events + if len(kevents) == 0 { + n, errno = syscall.Kevent(w.kq, nil, keventbuf[:], twait) + + // EINTR is okay, basically the syscall was interrupted before + // timeout expired. + if errno != nil && errno != syscall.EINTR { + w.Errors <- os.NewSyscallError("kevent", errno) + continue + } + + // Received some events + if n > 0 { + kevents = keventbuf[0:n] + } + } + + // Flush the events we received to the Events channel + for len(kevents) > 0 { + watchEvent := &kevents[0] + mask := uint32(watchEvent.Fflags) + w.pmut.Lock() + name := w.paths[int(watchEvent.Ident)] + fileInfo := w.finfo[int(watchEvent.Ident)] + w.pmut.Unlock() + + event := newEvent(name, mask, false) + + if fileInfo != nil && fileInfo.IsDir() && !(event.Op&Remove == Remove) { + // Double check to make sure the directory exist. This can happen when + // we do a rm -fr on a recursively watched folders and we receive a + // modification event first but the folder has been deleted and later + // receive the delete event + if _, err := os.Lstat(event.Name); os.IsNotExist(err) { + // mark is as delete event + event.Op |= Remove + } + } + + if fileInfo != nil && fileInfo.IsDir() && event.Op&Write == Write && !(event.Op&Remove == Remove) { + w.sendDirectoryChangeEvents(event.Name) + } else { + // Send the event on the Events channel + w.Events <- event + } + + // Move to next event + kevents = kevents[1:] + + if event.Op&Rename == Rename { + w.Remove(event.Name) + w.femut.Lock() + delete(w.fileExists, event.Name) + w.femut.Unlock() + } + if event.Op&Remove == Remove { + w.Remove(event.Name) + w.femut.Lock() + delete(w.fileExists, event.Name) + w.femut.Unlock() + + // Look for a file that may have overwritten this + // (ie mv f1 f2 will delete f2 then create f2) + fileDir, _ := filepath.Split(event.Name) + fileDir = filepath.Clean(fileDir) + w.wmut.Lock() + _, found := w.watches[fileDir] + w.wmut.Unlock() + if found { + // make sure the directory exist before we watch for changes. When we + // do a recursive watch and perform rm -fr, the parent directory might + // have gone missing, ignore the missing directory and let the + // upcoming delete event remove the watch form the parent folder + if _, err := os.Lstat(fileDir); !os.IsNotExist(err) { + w.sendDirectoryChangeEvents(fileDir) + } + } + } + } + } +} + +// newEvent returns an platform-independent Event based on kqueue Fflags. +func newEvent(name string, mask uint32, create bool) Event { + e := Event{Name: name} + if create { + e.Op |= Create + } + if mask&syscall.NOTE_DELETE == syscall.NOTE_DELETE { + e.Op |= Remove + } + if mask&syscall.NOTE_WRITE == syscall.NOTE_WRITE { + e.Op |= Write + } + if mask&syscall.NOTE_RENAME == syscall.NOTE_RENAME { + e.Op |= Rename + } + if mask&syscall.NOTE_ATTRIB == syscall.NOTE_ATTRIB { + e.Op |= Chmod + } + return e +} + +func (w *Watcher) watchDirectoryFiles(dirPath string) error { + // Get all files + files, err := ioutil.ReadDir(dirPath) + if err != nil { + return err + } + + // Search for new files + for _, fileInfo := range files { + filePath := filepath.Join(dirPath, fileInfo.Name()) + + if fileInfo.IsDir() == false { + // Watch file to mimic linux fsnotify + e := w.addWatch(filePath, noteAllEvents) + if e != nil { + return e + } + } else { + // If the user is currently watching directory + // we want to preserve the flags used + w.enmut.Lock() + currFlags, found := w.enFlags[filePath] + w.enmut.Unlock() + var newFlags uint32 = syscall.NOTE_DELETE + if found { + newFlags |= currFlags + } + + // Linux gives deletes if not explicitly watching + e := w.addWatch(filePath, newFlags) + if e != nil { + return e + } + } + w.femut.Lock() + w.fileExists[filePath] = true + w.femut.Unlock() + } + + return nil +} + +// sendDirectoryEvents searches the directory for newly created files +// and sends them over the event channel. This functionality is to have +// the BSD version of fsnotify match linux fsnotify which provides a +// create event for files created in a watched directory. +func (w *Watcher) sendDirectoryChangeEvents(dirPath string) { + // Get all files + files, err := ioutil.ReadDir(dirPath) + if err != nil { + w.Errors <- err + } + + // Search for new files + for _, fileInfo := range files { + filePath := filepath.Join(dirPath, fileInfo.Name()) + w.femut.Lock() + _, doesExist := w.fileExists[filePath] + w.femut.Unlock() + if !doesExist { + // Send create event (mask=0) + event := newEvent(filePath, 0, true) + w.Events <- event + } + + // watchDirectoryFiles (but without doing another ReadDir) + if fileInfo.IsDir() == false { + // Watch file to mimic linux fsnotify + w.addWatch(filePath, noteAllEvents) + } else { + // If the user is currently watching directory + // we want to preserve the flags used + w.enmut.Lock() + currFlags, found := w.enFlags[filePath] + w.enmut.Unlock() + var newFlags uint32 = syscall.NOTE_DELETE + if found { + newFlags |= currFlags + } + + // Linux gives deletes if not explicitly watching + w.addWatch(filePath, newFlags) + } + + w.femut.Lock() + w.fileExists[filePath] = true + w.femut.Unlock() + } +} diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/open_mode_bsd.go b/vendor/src/github.com/go-fsnotify/fsnotify/open_mode_bsd.go new file mode 100644 index 0000000000..c57ccb427b --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/open_mode_bsd.go @@ -0,0 +1,11 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build freebsd openbsd netbsd dragonfly + +package fsnotify + +import "syscall" + +const openMode = syscall.O_NONBLOCK | syscall.O_RDONLY diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/open_mode_darwin.go b/vendor/src/github.com/go-fsnotify/fsnotify/open_mode_darwin.go new file mode 100644 index 0000000000..174b2c331f --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/open_mode_darwin.go @@ -0,0 +1,12 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin + +package fsnotify + +import "syscall" + +// note: this constant is not defined on BSD +const openMode = syscall.O_EVTONLY diff --git a/vendor/src/github.com/go-fsnotify/fsnotify/windows.go b/vendor/src/github.com/go-fsnotify/fsnotify/windows.go new file mode 100644 index 0000000000..811585227d --- /dev/null +++ b/vendor/src/github.com/go-fsnotify/fsnotify/windows.go @@ -0,0 +1,561 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package fsnotify + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "sync" + "syscall" + "unsafe" +) + +// Watcher watches a set of files, delivering events to a channel. +type Watcher struct { + Events chan Event + Errors chan error + isClosed bool // Set to true when Close() is first called + mu sync.Mutex // Map access + port syscall.Handle // Handle to completion port + watches watchMap // Map of watches (key: i-number) + input chan *input // Inputs to the reader are sent on this channel + quit chan chan<- error +} + +// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. +func NewWatcher() (*Watcher, error) { + port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0) + if e != nil { + return nil, os.NewSyscallError("CreateIoCompletionPort", e) + } + w := &Watcher{ + port: port, + watches: make(watchMap), + input: make(chan *input, 1), + Events: make(chan Event, 50), + Errors: make(chan error), + quit: make(chan chan<- error, 1), + } + go w.readEvents() + return w, nil +} + +// Close removes all watches and closes the events channel. +func (w *Watcher) Close() error { + if w.isClosed { + return nil + } + w.isClosed = true + + // Send "quit" message to the reader goroutine + ch := make(chan error) + w.quit <- ch + if err := w.wakeupReader(); err != nil { + return err + } + return <-ch +} + +// Add starts watching the named file or directory (non-recursively). +func (w *Watcher) Add(name string) error { + if w.isClosed { + return errors.New("watcher already closed") + } + in := &input{ + op: opAddWatch, + path: filepath.Clean(name), + flags: sys_FS_ALL_EVENTS, + reply: make(chan error), + } + w.input <- in + if err := w.wakeupReader(); err != nil { + return err + } + return <-in.reply +} + +// Remove stops watching the the named file or directory (non-recursively). +func (w *Watcher) Remove(name string) error { + in := &input{ + op: opRemoveWatch, + path: filepath.Clean(name), + reply: make(chan error), + } + w.input <- in + if err := w.wakeupReader(); err != nil { + return err + } + return <-in.reply +} + +const ( + // Options for AddWatch + sys_FS_ONESHOT = 0x80000000 + sys_FS_ONLYDIR = 0x1000000 + + // Events + sys_FS_ACCESS = 0x1 + sys_FS_ALL_EVENTS = 0xfff + sys_FS_ATTRIB = 0x4 + sys_FS_CLOSE = 0x18 + sys_FS_CREATE = 0x100 + sys_FS_DELETE = 0x200 + sys_FS_DELETE_SELF = 0x400 + sys_FS_MODIFY = 0x2 + sys_FS_MOVE = 0xc0 + sys_FS_MOVED_FROM = 0x40 + sys_FS_MOVED_TO = 0x80 + sys_FS_MOVE_SELF = 0x800 + + // Special events + sys_FS_IGNORED = 0x8000 + sys_FS_Q_OVERFLOW = 0x4000 +) + +func newEvent(name string, mask uint32) Event { + e := Event{Name: name} + if mask&sys_FS_CREATE == sys_FS_CREATE || mask&sys_FS_MOVED_TO == sys_FS_MOVED_TO { + e.Op |= Create + } + if mask&sys_FS_DELETE == sys_FS_DELETE || mask&sys_FS_DELETE_SELF == sys_FS_DELETE_SELF { + e.Op |= Remove + } + if mask&sys_FS_MODIFY == sys_FS_MODIFY { + e.Op |= Write + } + if mask&sys_FS_MOVE == sys_FS_MOVE || mask&sys_FS_MOVE_SELF == sys_FS_MOVE_SELF || mask&sys_FS_MOVED_FROM == sys_FS_MOVED_FROM { + e.Op |= Rename + } + if mask&sys_FS_ATTRIB == sys_FS_ATTRIB { + e.Op |= Chmod + } + return e +} + +const ( + opAddWatch = iota + opRemoveWatch +) + +const ( + provisional uint64 = 1 << (32 + iota) +) + +type input struct { + op int + path string + flags uint32 + reply chan error +} + +type inode struct { + handle syscall.Handle + volume uint32 + index uint64 +} + +type watch struct { + ov syscall.Overlapped + ino *inode // i-number + path string // Directory path + mask uint64 // Directory itself is being watched with these notify flags + names map[string]uint64 // Map of names being watched and their notify flags + rename string // Remembers the old name while renaming a file + buf [4096]byte +} + +type indexMap map[uint64]*watch +type watchMap map[uint32]indexMap + +func (w *Watcher) wakeupReader() error { + e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil) + if e != nil { + return os.NewSyscallError("PostQueuedCompletionStatus", e) + } + return nil +} + +func getDir(pathname string) (dir string, err error) { + attr, e := syscall.GetFileAttributes(syscall.StringToUTF16Ptr(pathname)) + if e != nil { + return "", os.NewSyscallError("GetFileAttributes", e) + } + if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { + dir = pathname + } else { + dir, _ = filepath.Split(pathname) + dir = filepath.Clean(dir) + } + return +} + +func getIno(path string) (ino *inode, err error) { + h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path), + syscall.FILE_LIST_DIRECTORY, + syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, + nil, syscall.OPEN_EXISTING, + syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0) + if e != nil { + return nil, os.NewSyscallError("CreateFile", e) + } + var fi syscall.ByHandleFileInformation + if e = syscall.GetFileInformationByHandle(h, &fi); e != nil { + syscall.CloseHandle(h) + return nil, os.NewSyscallError("GetFileInformationByHandle", e) + } + ino = &inode{ + handle: h, + volume: fi.VolumeSerialNumber, + index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), + } + return ino, nil +} + +// Must run within the I/O thread. +func (m watchMap) get(ino *inode) *watch { + if i := m[ino.volume]; i != nil { + return i[ino.index] + } + return nil +} + +// Must run within the I/O thread. +func (m watchMap) set(ino *inode, watch *watch) { + i := m[ino.volume] + if i == nil { + i = make(indexMap) + m[ino.volume] = i + } + i[ino.index] = watch +} + +// Must run within the I/O thread. +func (w *Watcher) addWatch(pathname string, flags uint64) error { + dir, err := getDir(pathname) + if err != nil { + return err + } + if flags&sys_FS_ONLYDIR != 0 && pathname != dir { + return nil + } + ino, err := getIno(dir) + if err != nil { + return err + } + w.mu.Lock() + watchEntry := w.watches.get(ino) + w.mu.Unlock() + if watchEntry == nil { + if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil { + syscall.CloseHandle(ino.handle) + return os.NewSyscallError("CreateIoCompletionPort", e) + } + watchEntry = &watch{ + ino: ino, + path: dir, + names: make(map[string]uint64), + } + w.mu.Lock() + w.watches.set(ino, watchEntry) + w.mu.Unlock() + flags |= provisional + } else { + syscall.CloseHandle(ino.handle) + } + if pathname == dir { + watchEntry.mask |= flags + } else { + watchEntry.names[filepath.Base(pathname)] |= flags + } + if err = w.startRead(watchEntry); err != nil { + return err + } + if pathname == dir { + watchEntry.mask &= ^provisional + } else { + watchEntry.names[filepath.Base(pathname)] &= ^provisional + } + return nil +} + +// Must run within the I/O thread. +func (w *Watcher) remWatch(pathname string) error { + dir, err := getDir(pathname) + if err != nil { + return err + } + ino, err := getIno(dir) + if err != nil { + return err + } + w.mu.Lock() + watch := w.watches.get(ino) + w.mu.Unlock() + if watch == nil { + return fmt.Errorf("can't remove non-existent watch for: %s", pathname) + } + if pathname == dir { + w.sendEvent(watch.path, watch.mask&sys_FS_IGNORED) + watch.mask = 0 + } else { + name := filepath.Base(pathname) + w.sendEvent(watch.path+"\\"+name, watch.names[name]&sys_FS_IGNORED) + delete(watch.names, name) + } + return w.startRead(watch) +} + +// Must run within the I/O thread. +func (w *Watcher) deleteWatch(watch *watch) { + for name, mask := range watch.names { + if mask&provisional == 0 { + w.sendEvent(watch.path+"\\"+name, mask&sys_FS_IGNORED) + } + delete(watch.names, name) + } + if watch.mask != 0 { + if watch.mask&provisional == 0 { + w.sendEvent(watch.path, watch.mask&sys_FS_IGNORED) + } + watch.mask = 0 + } +} + +// Must run within the I/O thread. +func (w *Watcher) startRead(watch *watch) error { + if e := syscall.CancelIo(watch.ino.handle); e != nil { + w.Errors <- os.NewSyscallError("CancelIo", e) + w.deleteWatch(watch) + } + mask := toWindowsFlags(watch.mask) + for _, m := range watch.names { + mask |= toWindowsFlags(m) + } + if mask == 0 { + if e := syscall.CloseHandle(watch.ino.handle); e != nil { + w.Errors <- os.NewSyscallError("CloseHandle", e) + } + w.mu.Lock() + delete(w.watches[watch.ino.volume], watch.ino.index) + w.mu.Unlock() + return nil + } + e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], + uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) + if e != nil { + err := os.NewSyscallError("ReadDirectoryChanges", e) + if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { + // Watched directory was probably removed + if w.sendEvent(watch.path, watch.mask&sys_FS_DELETE_SELF) { + if watch.mask&sys_FS_ONESHOT != 0 { + watch.mask = 0 + } + } + err = nil + } + w.deleteWatch(watch) + w.startRead(watch) + return err + } + return nil +} + +// readEvents reads from the I/O completion port, converts the +// received events into Event objects and sends them via the Events channel. +// Entry point to the I/O thread. +func (w *Watcher) readEvents() { + var ( + n, key uint32 + ov *syscall.Overlapped + ) + runtime.LockOSThread() + + for { + e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE) + watch := (*watch)(unsafe.Pointer(ov)) + + if watch == nil { + select { + case ch := <-w.quit: + w.mu.Lock() + var indexes []indexMap + for _, index := range w.watches { + indexes = append(indexes, index) + } + w.mu.Unlock() + for _, index := range indexes { + for _, watch := range index { + w.deleteWatch(watch) + w.startRead(watch) + } + } + var err error + if e := syscall.CloseHandle(w.port); e != nil { + err = os.NewSyscallError("CloseHandle", e) + } + close(w.Events) + close(w.Errors) + ch <- err + return + case in := <-w.input: + switch in.op { + case opAddWatch: + in.reply <- w.addWatch(in.path, uint64(in.flags)) + case opRemoveWatch: + in.reply <- w.remWatch(in.path) + } + default: + } + continue + } + + switch e { + case syscall.ERROR_MORE_DATA: + if watch == nil { + w.Errors <- errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer") + } else { + // The i/o succeeded but the buffer is full. + // In theory we should be building up a full packet. + // In practice we can get away with just carrying on. + n = uint32(unsafe.Sizeof(watch.buf)) + } + case syscall.ERROR_ACCESS_DENIED: + // Watched directory was probably removed + w.sendEvent(watch.path, watch.mask&sys_FS_DELETE_SELF) + w.deleteWatch(watch) + w.startRead(watch) + continue + case syscall.ERROR_OPERATION_ABORTED: + // CancelIo was called on this handle + continue + default: + w.Errors <- os.NewSyscallError("GetQueuedCompletionPort", e) + continue + case nil: + } + + var offset uint32 + for { + if n == 0 { + w.Events <- newEvent("", sys_FS_Q_OVERFLOW) + w.Errors <- errors.New("short read in readEvents()") + break + } + + // Point "raw" to the event in the buffer + raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) + buf := (*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName)) + name := syscall.UTF16ToString(buf[:raw.FileNameLength/2]) + fullname := watch.path + "\\" + name + + var mask uint64 + switch raw.Action { + case syscall.FILE_ACTION_REMOVED: + mask = sys_FS_DELETE_SELF + case syscall.FILE_ACTION_MODIFIED: + mask = sys_FS_MODIFY + case syscall.FILE_ACTION_RENAMED_OLD_NAME: + watch.rename = name + case syscall.FILE_ACTION_RENAMED_NEW_NAME: + if watch.names[watch.rename] != 0 { + watch.names[name] |= watch.names[watch.rename] + delete(watch.names, watch.rename) + mask = sys_FS_MOVE_SELF + } + } + + sendNameEvent := func() { + if w.sendEvent(fullname, watch.names[name]&mask) { + if watch.names[name]&sys_FS_ONESHOT != 0 { + delete(watch.names, name) + } + } + } + if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME { + sendNameEvent() + } + if raw.Action == syscall.FILE_ACTION_REMOVED { + w.sendEvent(fullname, watch.names[name]&sys_FS_IGNORED) + delete(watch.names, name) + } + if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) { + if watch.mask&sys_FS_ONESHOT != 0 { + watch.mask = 0 + } + } + if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME { + fullname = watch.path + "\\" + watch.rename + sendNameEvent() + } + + // Move to the next event in the buffer + if raw.NextEntryOffset == 0 { + break + } + offset += raw.NextEntryOffset + + // Error! + if offset >= n { + w.Errors <- errors.New("Windows system assumed buffer larger than it is, events have likely been missed.") + break + } + } + + if err := w.startRead(watch); err != nil { + w.Errors <- err + } + } +} + +func (w *Watcher) sendEvent(name string, mask uint64) bool { + if mask == 0 { + return false + } + event := newEvent(name, uint32(mask)) + select { + case ch := <-w.quit: + w.quit <- ch + case w.Events <- event: + } + return true +} + +func toWindowsFlags(mask uint64) uint32 { + var m uint32 + if mask&sys_FS_ACCESS != 0 { + m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS + } + if mask&sys_FS_MODIFY != 0 { + m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE + } + if mask&sys_FS_ATTRIB != 0 { + m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES + } + if mask&(sys_FS_MOVE|sys_FS_CREATE|sys_FS_DELETE) != 0 { + m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME + } + return m +} + +func toFSnotifyFlags(action uint32) uint64 { + switch action { + case syscall.FILE_ACTION_ADDED: + return sys_FS_CREATE + case syscall.FILE_ACTION_REMOVED: + return sys_FS_DELETE + case syscall.FILE_ACTION_MODIFIED: + return sys_FS_MODIFY + case syscall.FILE_ACTION_RENAMED_OLD_NAME: + return sys_FS_MOVED_FROM + case syscall.FILE_ACTION_RENAMED_NEW_NAME: + return sys_FS_MOVED_TO + } + return 0 +}