Update container resolv.conf when host network changes /etc/resolv.conf
| ... | ... |
@@ -81,6 +81,7 @@ type Container struct {
|
| 81 | 81 |
MountLabel, ProcessLabel string |
| 82 | 82 |
AppArmorProfile string |
| 83 | 83 |
RestartCount int |
| 84 |
+ UpdateDns bool |
|
| 84 | 85 |
|
| 85 | 86 |
// Maps container paths to volume paths. The key in this is the path to which |
| 86 | 87 |
// the volume is being mounted inside the container. Value is the path of the |
| ... | ... |
@@ -942,6 +943,29 @@ func (container *Container) DisableLink(name string) {
|
| 942 | 942 |
|
| 943 | 943 |
func (container *Container) setupContainerDns() error {
|
| 944 | 944 |
if container.ResolvConfPath != "" {
|
| 945 |
+ // check if this is an existing container that needs DNS update: |
|
| 946 |
+ if container.UpdateDns {
|
|
| 947 |
+ // read the host's resolv.conf, get the hash and call updateResolvConf |
|
| 948 |
+ log.Debugf("Check container (%s) for update to resolv.conf - UpdateDns flag was set", container.ID)
|
|
| 949 |
+ latestResolvConf, latestHash := resolvconf.GetLastModified() |
|
| 950 |
+ |
|
| 951 |
+ // because the new host resolv.conf might have localhost nameservers.. |
|
| 952 |
+ updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(latestResolvConf) |
|
| 953 |
+ if modified {
|
|
| 954 |
+ // changes have occurred during resolv.conf localhost cleanup: generate an updated hash |
|
| 955 |
+ newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf)) |
|
| 956 |
+ if err != nil {
|
|
| 957 |
+ return err |
|
| 958 |
+ } |
|
| 959 |
+ latestHash = newHash |
|
| 960 |
+ } |
|
| 961 |
+ |
|
| 962 |
+ if err := container.updateResolvConf(updatedResolvConf, latestHash); err != nil {
|
|
| 963 |
+ return err |
|
| 964 |
+ } |
|
| 965 |
+ // successful update of the restarting container; set the flag off |
|
| 966 |
+ container.UpdateDns = false |
|
| 967 |
+ } |
|
| 945 | 968 |
return nil |
| 946 | 969 |
} |
| 947 | 970 |
|
| ... | ... |
@@ -980,17 +1004,86 @@ func (container *Container) setupContainerDns() error {
|
| 980 | 980 |
} |
| 981 | 981 |
|
| 982 | 982 |
// replace any localhost/127.* nameservers |
| 983 |
- resolvConf = utils.RemoveLocalDns(resolvConf) |
|
| 984 |
- // if the resulting resolvConf is empty, use DefaultDns |
|
| 985 |
- if !bytes.Contains(resolvConf, []byte("nameserver")) {
|
|
| 986 |
- log.Infof("No non localhost DNS resolver found in resolv.conf and containers can't use it. Using default external servers : %v", DefaultDns)
|
|
| 987 |
- // prefix the default dns options with nameserver |
|
| 988 |
- resolvConf = append(resolvConf, []byte("\nnameserver "+strings.Join(DefaultDns, "\nnameserver "))...)
|
|
| 989 |
- } |
|
| 983 |
+ resolvConf, _ = resolvconf.RemoveReplaceLocalDns(resolvConf) |
|
| 984 |
+ } |
|
| 985 |
+ //get a sha256 hash of the resolv conf at this point so we can check |
|
| 986 |
+ //for changes when the host resolv.conf changes (e.g. network update) |
|
| 987 |
+ resolvHash, err := utils.HashData(bytes.NewReader(resolvConf)) |
|
| 988 |
+ if err != nil {
|
|
| 989 |
+ return err |
|
| 990 |
+ } |
|
| 991 |
+ resolvHashFile := container.ResolvConfPath + ".hash" |
|
| 992 |
+ if err = ioutil.WriteFile(resolvHashFile, []byte(resolvHash), 0644); err != nil {
|
|
| 993 |
+ return err |
|
| 990 | 994 |
} |
| 991 | 995 |
return ioutil.WriteFile(container.ResolvConfPath, resolvConf, 0644) |
| 992 | 996 |
} |
| 993 | 997 |
|
| 998 |
+// called when the host's resolv.conf changes to check whether container's resolv.conf |
|
| 999 |
+// is unchanged by the container "user" since container start: if unchanged, the |
|
| 1000 |
+// container's resolv.conf will be updated to match the host's new resolv.conf |
|
| 1001 |
+func (container *Container) updateResolvConf(updatedResolvConf []byte, newResolvHash string) error {
|
|
| 1002 |
+ |
|
| 1003 |
+ if container.ResolvConfPath == "" {
|
|
| 1004 |
+ return nil |
|
| 1005 |
+ } |
|
| 1006 |
+ if container.Running {
|
|
| 1007 |
+ //set a marker in the hostConfig to update on next start/restart |
|
| 1008 |
+ container.UpdateDns = true |
|
| 1009 |
+ return nil |
|
| 1010 |
+ } |
|
| 1011 |
+ |
|
| 1012 |
+ resolvHashFile := container.ResolvConfPath + ".hash" |
|
| 1013 |
+ |
|
| 1014 |
+ //read the container's current resolv.conf and compute the hash |
|
| 1015 |
+ resolvBytes, err := ioutil.ReadFile(container.ResolvConfPath) |
|
| 1016 |
+ if err != nil {
|
|
| 1017 |
+ return err |
|
| 1018 |
+ } |
|
| 1019 |
+ curHash, err := utils.HashData(bytes.NewReader(resolvBytes)) |
|
| 1020 |
+ if err != nil {
|
|
| 1021 |
+ return err |
|
| 1022 |
+ } |
|
| 1023 |
+ |
|
| 1024 |
+ //read the hash from the last time we wrote resolv.conf in the container |
|
| 1025 |
+ hashBytes, err := ioutil.ReadFile(resolvHashFile) |
|
| 1026 |
+ if err != nil {
|
|
| 1027 |
+ return err |
|
| 1028 |
+ } |
|
| 1029 |
+ |
|
| 1030 |
+ //if the user has not modified the resolv.conf of the container since we wrote it last |
|
| 1031 |
+ //we will replace it with the updated resolv.conf from the host |
|
| 1032 |
+ if string(hashBytes) == curHash {
|
|
| 1033 |
+ log.Debugf("replacing %q with updated host resolv.conf", container.ResolvConfPath)
|
|
| 1034 |
+ |
|
| 1035 |
+ // for atomic updates to these files, use temporary files with os.Rename: |
|
| 1036 |
+ dir := path.Dir(container.ResolvConfPath) |
|
| 1037 |
+ tmpHashFile, err := ioutil.TempFile(dir, "hash") |
|
| 1038 |
+ if err != nil {
|
|
| 1039 |
+ return err |
|
| 1040 |
+ } |
|
| 1041 |
+ tmpResolvFile, err := ioutil.TempFile(dir, "resolv") |
|
| 1042 |
+ if err != nil {
|
|
| 1043 |
+ return err |
|
| 1044 |
+ } |
|
| 1045 |
+ |
|
| 1046 |
+ // write the updates to the temp files |
|
| 1047 |
+ if err = ioutil.WriteFile(tmpHashFile.Name(), []byte(newResolvHash), 0644); err != nil {
|
|
| 1048 |
+ return err |
|
| 1049 |
+ } |
|
| 1050 |
+ if err = ioutil.WriteFile(tmpResolvFile.Name(), updatedResolvConf, 0644); err != nil {
|
|
| 1051 |
+ return err |
|
| 1052 |
+ } |
|
| 1053 |
+ |
|
| 1054 |
+ // rename the temp files for atomic replace |
|
| 1055 |
+ if err = os.Rename(tmpHashFile.Name(), resolvHashFile); err != nil {
|
|
| 1056 |
+ return err |
|
| 1057 |
+ } |
|
| 1058 |
+ return os.Rename(tmpResolvFile.Name(), container.ResolvConfPath) |
|
| 1059 |
+ } |
|
| 1060 |
+ return nil |
|
| 1061 |
+} |
|
| 1062 |
+ |
|
| 994 | 1063 |
func (container *Container) updateParentsHosts() error {
|
| 995 | 1064 |
parents, err := container.daemon.Parents(container.Name) |
| 996 | 1065 |
if err != nil {
|
| ... | ... |
@@ -1,6 +1,7 @@ |
| 1 | 1 |
package daemon |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "bytes" |
|
| 4 | 5 |
"fmt" |
| 5 | 6 |
"io" |
| 6 | 7 |
"io/ioutil" |
| ... | ... |
@@ -32,6 +33,7 @@ import ( |
| 32 | 32 |
"github.com/docker/docker/pkg/graphdb" |
| 33 | 33 |
"github.com/docker/docker/pkg/ioutils" |
| 34 | 34 |
"github.com/docker/docker/pkg/namesgenerator" |
| 35 |
+ "github.com/docker/docker/pkg/networkfs/resolvconf" |
|
| 35 | 36 |
"github.com/docker/docker/pkg/parsers" |
| 36 | 37 |
"github.com/docker/docker/pkg/parsers/kernel" |
| 37 | 38 |
"github.com/docker/docker/pkg/sysinfo" |
| ... | ... |
@@ -40,10 +42,11 @@ import ( |
| 40 | 40 |
"github.com/docker/docker/trust" |
| 41 | 41 |
"github.com/docker/docker/utils" |
| 42 | 42 |
"github.com/docker/docker/volumes" |
| 43 |
+ |
|
| 44 |
+ "github.com/go-fsnotify/fsnotify" |
|
| 43 | 45 |
) |
| 44 | 46 |
|
| 45 | 47 |
var ( |
| 46 |
- DefaultDns = []string{"8.8.8.8", "8.8.4.4"}
|
|
| 47 | 48 |
validContainerNameChars = `[a-zA-Z0-9][a-zA-Z0-9_.-]` |
| 48 | 49 |
validContainerNamePattern = regexp.MustCompile(`^/?` + validContainerNameChars + `+$`) |
| 49 | 50 |
) |
| ... | ... |
@@ -408,6 +411,60 @@ func (daemon *Daemon) restore() error {
|
| 408 | 408 |
return nil |
| 409 | 409 |
} |
| 410 | 410 |
|
| 411 |
+// set up the watch on the host's /etc/resolv.conf so that we can update container's |
|
| 412 |
+// live resolv.conf when the network changes on the host |
|
| 413 |
+func (daemon *Daemon) setupResolvconfWatcher() error {
|
|
| 414 |
+ |
|
| 415 |
+ watcher, err := fsnotify.NewWatcher() |
|
| 416 |
+ if err != nil {
|
|
| 417 |
+ return err |
|
| 418 |
+ } |
|
| 419 |
+ |
|
| 420 |
+ //this goroutine listens for the events on the watch we add |
|
| 421 |
+ //on the resolv.conf file on the host |
|
| 422 |
+ go func() {
|
|
| 423 |
+ for {
|
|
| 424 |
+ select {
|
|
| 425 |
+ case event := <-watcher.Events: |
|
| 426 |
+ if event.Op&fsnotify.Write == fsnotify.Write {
|
|
| 427 |
+ // verify a real change happened before we go further--a file write may have happened |
|
| 428 |
+ // without an actual change to the file |
|
| 429 |
+ updatedResolvConf, newResolvConfHash, err := resolvconf.GetIfChanged() |
|
| 430 |
+ if err != nil {
|
|
| 431 |
+ log.Debugf("Error retrieving updated host resolv.conf: %v", err)
|
|
| 432 |
+ } else if updatedResolvConf != nil {
|
|
| 433 |
+ // because the new host resolv.conf might have localhost nameservers.. |
|
| 434 |
+ updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(updatedResolvConf) |
|
| 435 |
+ if modified {
|
|
| 436 |
+ // changes have occurred during localhost cleanup: generate an updated hash |
|
| 437 |
+ newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf)) |
|
| 438 |
+ if err != nil {
|
|
| 439 |
+ log.Debugf("Error generating hash of new resolv.conf: %v", err)
|
|
| 440 |
+ } else {
|
|
| 441 |
+ newResolvConfHash = newHash |
|
| 442 |
+ } |
|
| 443 |
+ } |
|
| 444 |
+ log.Debugf("host network resolv.conf changed--walking container list for updates")
|
|
| 445 |
+ contList := daemon.containers.List() |
|
| 446 |
+ for _, container := range contList {
|
|
| 447 |
+ if err := container.updateResolvConf(updatedResolvConf, newResolvConfHash); err != nil {
|
|
| 448 |
+ log.Debugf("Error on resolv.conf update check for container ID: %s: %v", container.ID, err)
|
|
| 449 |
+ } |
|
| 450 |
+ } |
|
| 451 |
+ } |
|
| 452 |
+ } |
|
| 453 |
+ case err := <-watcher.Errors: |
|
| 454 |
+ log.Debugf("host resolv.conf notify error: %v", err)
|
|
| 455 |
+ } |
|
| 456 |
+ } |
|
| 457 |
+ }() |
|
| 458 |
+ |
|
| 459 |
+ if err := watcher.Add("/etc/resolv.conf"); err != nil {
|
|
| 460 |
+ return err |
|
| 461 |
+ } |
|
| 462 |
+ return nil |
|
| 463 |
+} |
|
| 464 |
+ |
|
| 411 | 465 |
func (daemon *Daemon) checkDeprecatedExpose(config *runconfig.Config) bool {
|
| 412 | 466 |
if config != nil {
|
| 413 | 467 |
if config.PortSpecs != nil {
|
| ... | ... |
@@ -930,6 +987,12 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) |
| 930 | 930 |
if err := daemon.restore(); err != nil {
|
| 931 | 931 |
return nil, err |
| 932 | 932 |
} |
| 933 |
+ |
|
| 934 |
+ // set up filesystem watch on resolv.conf for network changes |
|
| 935 |
+ if err := daemon.setupResolvconfWatcher(); err != nil {
|
|
| 936 |
+ return nil, err |
|
| 937 |
+ } |
|
| 938 |
+ |
|
| 933 | 939 |
// Setup shutdown handlers |
| 934 | 940 |
// FIXME: can these shutdown handlers be registered closer to their source? |
| 935 | 941 |
eng.OnShutdown(func() {
|
| ... | ... |
@@ -24,34 +24,3 @@ func TestMergeLxcConfig(t *testing.T) {
|
| 24 | 24 |
t.Fatalf("expected %s got %s", expected, cpuset)
|
| 25 | 25 |
} |
| 26 | 26 |
} |
| 27 |
- |
|
| 28 |
-func TestRemoveLocalDns(t *testing.T) {
|
|
| 29 |
- ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n" |
|
| 30 |
- |
|
| 31 |
- if result := utils.RemoveLocalDns([]byte(ns0)); result != nil {
|
|
| 32 |
- if ns0 != string(result) {
|
|
| 33 |
- t.Fatalf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
|
|
| 34 |
- } |
|
| 35 |
- } |
|
| 36 |
- |
|
| 37 |
- ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n" |
|
| 38 |
- if result := utils.RemoveLocalDns([]byte(ns1)); result != nil {
|
|
| 39 |
- if ns0 != string(result) {
|
|
| 40 |
- t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
|
|
| 41 |
- } |
|
| 42 |
- } |
|
| 43 |
- |
|
| 44 |
- ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n" |
|
| 45 |
- if result := utils.RemoveLocalDns([]byte(ns1)); result != nil {
|
|
| 46 |
- if ns0 != string(result) {
|
|
| 47 |
- t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
|
|
| 48 |
- } |
|
| 49 |
- } |
|
| 50 |
- |
|
| 51 |
- ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n" |
|
| 52 |
- if result := utils.RemoveLocalDns([]byte(ns1)); result != nil {
|
|
| 53 |
- if ns0 != string(result) {
|
|
| 54 |
- t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
|
|
| 55 |
- } |
|
| 56 |
- } |
|
| 57 |
-} |
| ... | ... |
@@ -130,7 +130,7 @@ information. You can see this by running `mount` inside a container: |
| 130 | 130 |
... |
| 131 | 131 |
/dev/disk/by-uuid/1fec...ebdf on /etc/hostname type ext4 ... |
| 132 | 132 |
/dev/disk/by-uuid/1fec...ebdf on /etc/hosts type ext4 ... |
| 133 |
- tmpfs on /etc/resolv.conf type tmpfs ... |
|
| 133 |
+ /dev/disk/by-uuid/1fec...ebdf on /etc/resolv.conf type ext4 ... |
|
| 134 | 134 |
... |
| 135 | 135 |
|
| 136 | 136 |
This arrangement allows Docker to do clever things like keep |
| ... | ... |
@@ -178,7 +178,20 @@ Four different options affect container domain name services. |
| 178 | 178 |
Note that Docker, in the absence of either of the last two options |
| 179 | 179 |
above, will make `/etc/resolv.conf` inside of each container look like |
| 180 | 180 |
the `/etc/resolv.conf` of the host machine where the `docker` daemon is |
| 181 |
-running. The options then modify this default configuration. |
|
| 181 |
+running. You might wonder what happens when the host machine's |
|
| 182 |
+`/etc/resolv.conf` file changes. The `docker` daemon has a file change |
|
| 183 |
+notifier active which will watch for changes to the host DNS configuration. |
|
| 184 |
+When the host file changes, all stopped containers which have a matching |
|
| 185 |
+`resolv.conf` to the host will be updated immediately to this newest host |
|
| 186 |
+configuration. Containers which are running when the host configuration |
|
| 187 |
+changes will need to stop and start to pick up the host changes due to lack |
|
| 188 |
+of a facility to ensure atomic writes of the `resolv.conf` file while the |
|
| 189 |
+container is running. If the container's `resolv.conf` has been edited since |
|
| 190 |
+it was started with the default configuration, no replacement will be |
|
| 191 |
+attempted as it would overwrite the changes performed by the container. |
|
| 192 |
+If the options (`--dns` or `--dns-search`) have been used to modify the |
|
| 193 |
+default host configuration, then the replacement with an updated host's |
|
| 194 |
+`/etc/resolv.conf` will not happen as well. |
|
| 182 | 195 |
|
| 183 | 196 |
## Communication between containers and the wider world |
| 184 | 197 |
|
| ... | ... |
@@ -1403,6 +1403,157 @@ func TestRunDnsOptionsBasedOnHostResolvConf(t *testing.T) {
|
| 1403 | 1403 |
logDone("run - dns options based on host resolv.conf")
|
| 1404 | 1404 |
} |
| 1405 | 1405 |
|
| 1406 |
+// Test the file watch notifier on docker host's /etc/resolv.conf |
|
| 1407 |
+// A go-routine is responsible for auto-updating containers which are |
|
| 1408 |
+// stopped and have an unmodified copy of resolv.conf, as well as |
|
| 1409 |
+// marking running containers as requiring an update on next restart |
|
| 1410 |
+func TestRunResolvconfUpdater(t *testing.T) {
|
|
| 1411 |
+ |
|
| 1412 |
+ tmpResolvConf := []byte("search pommesfrites.fr\nnameserver 12.34.56.78")
|
|
| 1413 |
+ tmpLocalhostResolvConf := []byte("nameserver 127.0.0.1")
|
|
| 1414 |
+ |
|
| 1415 |
+ //take a copy of resolv.conf for restoring after test completes |
|
| 1416 |
+ resolvConfSystem, err := ioutil.ReadFile("/etc/resolv.conf")
|
|
| 1417 |
+ if err != nil {
|
|
| 1418 |
+ t.Fatal(err) |
|
| 1419 |
+ } |
|
| 1420 |
+ |
|
| 1421 |
+ //cleanup |
|
| 1422 |
+ defer func() {
|
|
| 1423 |
+ deleteAllContainers() |
|
| 1424 |
+ if err := ioutil.WriteFile("/etc/resolv.conf", resolvConfSystem, 0644); err != nil {
|
|
| 1425 |
+ t.Fatal(err) |
|
| 1426 |
+ } |
|
| 1427 |
+ }() |
|
| 1428 |
+ |
|
| 1429 |
+ //1. test that a non-running container gets an updated resolv.conf |
|
| 1430 |
+ cmd := exec.Command(dockerBinary, "run", "--name='first'", "busybox", "true") |
|
| 1431 |
+ if _, err := runCommand(cmd); err != nil {
|
|
| 1432 |
+ t.Fatal(err) |
|
| 1433 |
+ } |
|
| 1434 |
+ containerID1, err := getIDByName("first")
|
|
| 1435 |
+ if err != nil {
|
|
| 1436 |
+ t.Fatal(err) |
|
| 1437 |
+ } |
|
| 1438 |
+ |
|
| 1439 |
+ // replace resolv.conf with our temporary copy |
|
| 1440 |
+ bytesResolvConf := []byte(tmpResolvConf) |
|
| 1441 |
+ if err := ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil {
|
|
| 1442 |
+ t.Fatal(err) |
|
| 1443 |
+ } |
|
| 1444 |
+ |
|
| 1445 |
+ time.Sleep(time.Second / 2) |
|
| 1446 |
+ // check for update in container |
|
| 1447 |
+ containerResolv, err := readContainerFile(containerID1, "resolv.conf") |
|
| 1448 |
+ if err != nil {
|
|
| 1449 |
+ t.Fatal(err) |
|
| 1450 |
+ } |
|
| 1451 |
+ if !bytes.Equal(containerResolv, bytesResolvConf) {
|
|
| 1452 |
+ t.Fatalf("Stopped container does not have updated resolv.conf; expected %q, got %q", tmpResolvConf, string(containerResolv))
|
|
| 1453 |
+ } |
|
| 1454 |
+ |
|
| 1455 |
+ //2. test that a non-running container does not receive resolv.conf updates |
|
| 1456 |
+ // if it modified the container copy of the starting point resolv.conf |
|
| 1457 |
+ cmd = exec.Command(dockerBinary, "run", "--name='second'", "busybox", "sh", "-c", "echo 'search mylittlepony.com' >>/etc/resolv.conf") |
|
| 1458 |
+ if _, err = runCommand(cmd); err != nil {
|
|
| 1459 |
+ t.Fatal(err) |
|
| 1460 |
+ } |
|
| 1461 |
+ containerID2, err := getIDByName("second")
|
|
| 1462 |
+ if err != nil {
|
|
| 1463 |
+ t.Fatal(err) |
|
| 1464 |
+ } |
|
| 1465 |
+ containerResolvHashBefore, err := readContainerFile(containerID2, "resolv.conf.hash") |
|
| 1466 |
+ if err != nil {
|
|
| 1467 |
+ t.Fatal(err) |
|
| 1468 |
+ } |
|
| 1469 |
+ |
|
| 1470 |
+ //make a change to resolv.conf (in this case replacing our tmp copy with orig copy) |
|
| 1471 |
+ if err := ioutil.WriteFile("/etc/resolv.conf", resolvConfSystem, 0644); err != nil {
|
|
| 1472 |
+ t.Fatal(err) |
|
| 1473 |
+ } |
|
| 1474 |
+ |
|
| 1475 |
+ time.Sleep(time.Second / 2) |
|
| 1476 |
+ containerResolvHashAfter, err := readContainerFile(containerID2, "resolv.conf.hash") |
|
| 1477 |
+ if err != nil {
|
|
| 1478 |
+ t.Fatal(err) |
|
| 1479 |
+ } |
|
| 1480 |
+ |
|
| 1481 |
+ if !bytes.Equal(containerResolvHashBefore, containerResolvHashAfter) {
|
|
| 1482 |
+ t.Fatalf("Stopped container with modified resolv.conf should not have been updated; expected hash: %v, new hash: %v", containerResolvHashBefore, containerResolvHashAfter)
|
|
| 1483 |
+ } |
|
| 1484 |
+ |
|
| 1485 |
+ //3. test that a running container's resolv.conf is not modified while running |
|
| 1486 |
+ cmd = exec.Command(dockerBinary, "run", "-d", "busybox", "top") |
|
| 1487 |
+ out, _, err := runCommandWithOutput(cmd) |
|
| 1488 |
+ if err != nil {
|
|
| 1489 |
+ t.Fatal(err) |
|
| 1490 |
+ } |
|
| 1491 |
+ runningContainerID := strings.TrimSpace(out) |
|
| 1492 |
+ |
|
| 1493 |
+ containerResolvHashBefore, err = readContainerFile(runningContainerID, "resolv.conf.hash") |
|
| 1494 |
+ if err != nil {
|
|
| 1495 |
+ t.Fatal(err) |
|
| 1496 |
+ } |
|
| 1497 |
+ |
|
| 1498 |
+ // replace resolv.conf |
|
| 1499 |
+ if err := ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil {
|
|
| 1500 |
+ t.Fatal(err) |
|
| 1501 |
+ } |
|
| 1502 |
+ |
|
| 1503 |
+ // make sure the updater has time to run to validate we really aren't |
|
| 1504 |
+ // getting updated |
|
| 1505 |
+ time.Sleep(time.Second / 2) |
|
| 1506 |
+ containerResolvHashAfter, err = readContainerFile(runningContainerID, "resolv.conf.hash") |
|
| 1507 |
+ if err != nil {
|
|
| 1508 |
+ t.Fatal(err) |
|
| 1509 |
+ } |
|
| 1510 |
+ |
|
| 1511 |
+ if !bytes.Equal(containerResolvHashBefore, containerResolvHashAfter) {
|
|
| 1512 |
+ t.Fatalf("Running container's resolv.conf should not be updated; expected hash: %v, new hash: %v", containerResolvHashBefore, containerResolvHashAfter)
|
|
| 1513 |
+ } |
|
| 1514 |
+ |
|
| 1515 |
+ //4. test that a running container's resolv.conf is updated upon restart |
|
| 1516 |
+ // (the above container is still running..) |
|
| 1517 |
+ cmd = exec.Command(dockerBinary, "restart", runningContainerID) |
|
| 1518 |
+ if _, err = runCommand(cmd); err != nil {
|
|
| 1519 |
+ t.Fatal(err) |
|
| 1520 |
+ } |
|
| 1521 |
+ |
|
| 1522 |
+ // check for update in container |
|
| 1523 |
+ containerResolv, err = readContainerFile(runningContainerID, "resolv.conf") |
|
| 1524 |
+ if err != nil {
|
|
| 1525 |
+ t.Fatal(err) |
|
| 1526 |
+ } |
|
| 1527 |
+ if !bytes.Equal(containerResolv, bytesResolvConf) {
|
|
| 1528 |
+ t.Fatalf("Restarted container should have updated resolv.conf; expected %q, got %q", tmpResolvConf, string(containerResolv))
|
|
| 1529 |
+ } |
|
| 1530 |
+ |
|
| 1531 |
+ //5. test that additions of a localhost resolver are cleaned from |
|
| 1532 |
+ // host resolv.conf before updating container's resolv.conf copies |
|
| 1533 |
+ |
|
| 1534 |
+ // replace resolv.conf with a localhost-only nameserver copy |
|
| 1535 |
+ bytesResolvConf = []byte(tmpLocalhostResolvConf) |
|
| 1536 |
+ if err = ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil {
|
|
| 1537 |
+ t.Fatal(err) |
|
| 1538 |
+ } |
|
| 1539 |
+ |
|
| 1540 |
+ time.Sleep(time.Second / 2) |
|
| 1541 |
+ // our first exited container ID should have been updated, but with default DNS |
|
| 1542 |
+ // after the cleanup of resolv.conf found only a localhost nameserver: |
|
| 1543 |
+ containerResolv, err = readContainerFile(containerID1, "resolv.conf") |
|
| 1544 |
+ if err != nil {
|
|
| 1545 |
+ t.Fatal(err) |
|
| 1546 |
+ } |
|
| 1547 |
+ |
|
| 1548 |
+ expected := "\nnameserver 8.8.8.8\nnameserver 8.8.4.4" |
|
| 1549 |
+ if !bytes.Equal(containerResolv, []byte(expected)) {
|
|
| 1550 |
+ t.Fatalf("Container does not have cleaned/replaced DNS in resolv.conf; expected %q, got %q", expected, string(containerResolv))
|
|
| 1551 |
+ } |
|
| 1552 |
+ |
|
| 1553 |
+ //cleanup, restore original resolv.conf happens in defer func() |
|
| 1554 |
+ logDone("run - resolv.conf updater")
|
|
| 1555 |
+} |
|
| 1556 |
+ |
|
| 1406 | 1557 |
func TestRunAddHost(t *testing.T) {
|
| 1407 | 1558 |
defer deleteAllContainers() |
| 1408 | 1559 |
cmd := exec.Command(dockerBinary, "run", "--add-host=extra:86.75.30.9", "busybox", "grep", "extra", "/etc/hosts") |
| ... | ... |
@@ -5,13 +5,25 @@ import ( |
| 5 | 5 |
"io/ioutil" |
| 6 | 6 |
"regexp" |
| 7 | 7 |
"strings" |
| 8 |
+ "sync" |
|
| 9 |
+ |
|
| 10 |
+ log "github.com/Sirupsen/logrus" |
|
| 11 |
+ "github.com/docker/docker/utils" |
|
| 8 | 12 |
) |
| 9 | 13 |
|
| 10 | 14 |
var ( |
| 11 |
- nsRegexp = regexp.MustCompile(`^\s*nameserver\s*(([0-9]+\.){3}([0-9]+))\s*$`)
|
|
| 12 |
- searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`) |
|
| 15 |
+ defaultDns = []string{"8.8.8.8", "8.8.4.4"}
|
|
| 16 |
+ localHostRegexp = regexp.MustCompile(`(?m)^nameserver 127[^\n]+\n*`) |
|
| 17 |
+ nsRegexp = regexp.MustCompile(`^\s*nameserver\s*(([0-9]+\.){3}([0-9]+))\s*$`)
|
|
| 18 |
+ searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`) |
|
| 13 | 19 |
) |
| 14 | 20 |
|
| 21 |
+var lastModified struct {
|
|
| 22 |
+ sync.Mutex |
|
| 23 |
+ sha256 string |
|
| 24 |
+ contents []byte |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 15 | 27 |
func Get() ([]byte, error) {
|
| 16 | 28 |
resolv, err := ioutil.ReadFile("/etc/resolv.conf")
|
| 17 | 29 |
if err != nil {
|
| ... | ... |
@@ -20,6 +32,57 @@ func Get() ([]byte, error) {
|
| 20 | 20 |
return resolv, nil |
| 21 | 21 |
} |
| 22 | 22 |
|
| 23 |
+// Retrieves the host /etc/resolv.conf file, checks against the last hash |
|
| 24 |
+// and, if modified since last check, returns the bytes and new hash. |
|
| 25 |
+// This feature is used by the resolv.conf updater for containers |
|
| 26 |
+func GetIfChanged() ([]byte, string, error) {
|
|
| 27 |
+ lastModified.Lock() |
|
| 28 |
+ defer lastModified.Unlock() |
|
| 29 |
+ |
|
| 30 |
+ resolv, err := ioutil.ReadFile("/etc/resolv.conf")
|
|
| 31 |
+ if err != nil {
|
|
| 32 |
+ return nil, "", err |
|
| 33 |
+ } |
|
| 34 |
+ newHash, err := utils.HashData(bytes.NewReader(resolv)) |
|
| 35 |
+ if err != nil {
|
|
| 36 |
+ return nil, "", err |
|
| 37 |
+ } |
|
| 38 |
+ if lastModified.sha256 != newHash {
|
|
| 39 |
+ lastModified.sha256 = newHash |
|
| 40 |
+ lastModified.contents = resolv |
|
| 41 |
+ return resolv, newHash, nil |
|
| 42 |
+ } |
|
| 43 |
+ // nothing changed, so return no data |
|
| 44 |
+ return nil, "", nil |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+// retrieve the last used contents and hash of the host resolv.conf |
|
| 48 |
+// Used by containers updating on restart |
|
| 49 |
+func GetLastModified() ([]byte, string) {
|
|
| 50 |
+ lastModified.Lock() |
|
| 51 |
+ defer lastModified.Unlock() |
|
| 52 |
+ |
|
| 53 |
+ return lastModified.contents, lastModified.sha256 |
|
| 54 |
+} |
|
| 55 |
+ |
|
| 56 |
+// RemoveReplaceLocalDns looks for localhost (127.*) entries in the provided |
|
| 57 |
+// resolv.conf, removing local nameserver entries, and, if the resulting |
|
| 58 |
+// cleaned config has no defined nameservers left, adds default DNS entries |
|
| 59 |
+// It also returns a boolean to notify the caller if changes were made at all |
|
| 60 |
+func RemoveReplaceLocalDns(resolvConf []byte) ([]byte, bool) {
|
|
| 61 |
+ changed := false |
|
| 62 |
+ cleanedResolvConf := localHostRegexp.ReplaceAll(resolvConf, []byte{})
|
|
| 63 |
+ // if the resulting resolvConf is empty, use defaultDns |
|
| 64 |
+ if !bytes.Contains(cleanedResolvConf, []byte("nameserver")) {
|
|
| 65 |
+ log.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : %v", defaultDns)
|
|
| 66 |
+ cleanedResolvConf = append(cleanedResolvConf, []byte("\nnameserver "+strings.Join(defaultDns, "\nnameserver "))...)
|
|
| 67 |
+ } |
|
| 68 |
+ if !bytes.Equal(resolvConf, cleanedResolvConf) {
|
|
| 69 |
+ changed = true |
|
| 70 |
+ } |
|
| 71 |
+ return cleanedResolvConf, changed |
|
| 72 |
+} |
|
| 73 |
+ |
|
| 23 | 74 |
// getLines parses input into lines and strips away comments. |
| 24 | 75 |
func getLines(input []byte, commentMarker []byte) [][]byte {
|
| 25 | 76 |
lines := bytes.Split(input, []byte("\n"))
|
| ... | ... |
@@ -156,3 +156,34 @@ func TestBuildWithZeroLengthDomainSearch(t *testing.T) {
|
| 156 | 156 |
t.Fatalf("Expected to not find '%s' got '%s'", notExpected, content)
|
| 157 | 157 |
} |
| 158 | 158 |
} |
| 159 |
+ |
|
| 160 |
+func TestRemoveReplaceLocalDns(t *testing.T) {
|
|
| 161 |
+ ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n" |
|
| 162 |
+ |
|
| 163 |
+ if result, _ := RemoveReplaceLocalDns([]byte(ns0)); result != nil {
|
|
| 164 |
+ if ns0 != string(result) {
|
|
| 165 |
+ t.Fatalf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
|
|
| 166 |
+ } |
|
| 167 |
+ } |
|
| 168 |
+ |
|
| 169 |
+ ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n" |
|
| 170 |
+ if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil {
|
|
| 171 |
+ if ns0 != string(result) {
|
|
| 172 |
+ t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
|
|
| 173 |
+ } |
|
| 174 |
+ } |
|
| 175 |
+ |
|
| 176 |
+ ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n" |
|
| 177 |
+ if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil {
|
|
| 178 |
+ if ns0 != string(result) {
|
|
| 179 |
+ t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
|
|
| 180 |
+ } |
|
| 181 |
+ } |
|
| 182 |
+ |
|
| 183 |
+ ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n" |
|
| 184 |
+ if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil {
|
|
| 185 |
+ if ns0 != string(result) {
|
|
| 186 |
+ t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
|
|
| 187 |
+ } |
|
| 188 |
+ } |
|
| 189 |
+} |
| ... | ... |
@@ -55,6 +55,8 @@ clone git github.com/docker/libtrust 230dfd18c232 |
| 55 | 55 |
|
| 56 | 56 |
clone git github.com/Sirupsen/logrus v0.6.0 |
| 57 | 57 |
|
| 58 |
+clone git github.com/go-fsnotify/fsnotify v1.0.4 |
|
| 59 |
+ |
|
| 58 | 60 |
# get Go tip's archive/tar, for xattr support and improved performance |
| 59 | 61 |
# TODO after Go 1.4 drops, bump our minimum supported version and drop this vendored dep |
| 60 | 62 |
if [ "$1" = '--go' ]; then |
| ... | ... |
@@ -291,14 +291,6 @@ func NewHTTPRequestError(msg string, res *http.Response) error {
|
| 291 | 291 |
} |
| 292 | 292 |
} |
| 293 | 293 |
|
| 294 |
-var localHostRx = regexp.MustCompile(`(?m)^nameserver 127[^\n]+\n*`) |
|
| 295 |
- |
|
| 296 |
-// RemoveLocalDns looks into the /etc/resolv.conf, |
|
| 297 |
-// and removes any local nameserver entries. |
|
| 298 |
-func RemoveLocalDns(resolvConf []byte) []byte {
|
|
| 299 |
- return localHostRx.ReplaceAll(resolvConf, []byte{})
|
|
| 300 |
-} |
|
| 301 |
- |
|
| 302 | 294 |
// An StatusError reports an unsuccessful exit by a command. |
| 303 | 295 |
type StatusError struct {
|
| 304 | 296 |
Status string |
| 0 | 13 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,32 @@ |
| 0 |
+# Names should be added to this file as |
|
| 1 |
+# Name or Organization <email address> |
|
| 2 |
+# The email address is not required for organizations. |
|
| 3 |
+ |
|
| 4 |
+# You can update this list using the following command: |
|
| 5 |
+# |
|
| 6 |
+# $ git shortlog -se | awk '{print $2 " " $3 " " $4}'
|
|
| 7 |
+ |
|
| 8 |
+# Please keep the list sorted. |
|
| 9 |
+ |
|
| 10 |
+Adrien Bustany <adrien@bustany.org> |
|
| 11 |
+Caleb Spare <cespare@gmail.com> |
|
| 12 |
+Case Nelson <case@teammating.com> |
|
| 13 |
+Chris Howey <howeyc@gmail.com> <chris@howey.me> |
|
| 14 |
+Christoffer Buchholz <christoffer.buchholz@gmail.com> |
|
| 15 |
+Dave Cheney <dave@cheney.net> |
|
| 16 |
+Francisco Souza <f@souza.cc> |
|
| 17 |
+Hari haran <hariharan.uno@gmail.com> |
|
| 18 |
+John C Barstow |
|
| 19 |
+Kelvin Fo <vmirage@gmail.com> |
|
| 20 |
+Nathan Youngman <git@nathany.com> |
|
| 21 |
+Paul Hammond <paul@paulhammond.org> |
|
| 22 |
+Pursuit92 <JoshChase@techpursuit.net> |
|
| 23 |
+Rob Figueiredo <robfig@gmail.com> |
|
| 24 |
+Soge Zhang <zhssoge@gmail.com> |
|
| 25 |
+Tilak Sharma <tilaks@google.com> |
|
| 26 |
+Travis Cline <travis.cline@gmail.com> |
|
| 27 |
+Tudor Golubenco <tudor.g@gmail.com> |
|
| 28 |
+Yukang <moorekang@gmail.com> |
|
| 29 |
+bronze1man <bronze1man@gmail.com> |
|
| 30 |
+debrando <denis.brandolini@gmail.com> |
|
| 31 |
+henrikedwards <henrik.edwards@gmail.com> |
| 0 | 32 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,237 @@ |
| 0 |
+# Changelog |
|
| 1 |
+ |
|
| 2 |
+## v1.0.4 / 2014-09-07 |
|
| 3 |
+ |
|
| 4 |
+* kqueue: add dragonfly to the build tags. |
|
| 5 |
+* Rename source code files, rearrange code so exported APIs are at the top. |
|
| 6 |
+* Add done channel to example code. [#37](https://github.com/go-fsnotify/fsnotify/pull/37) (thanks @chenyukang) |
|
| 7 |
+ |
|
| 8 |
+## v1.0.3 / 2014-08-19 |
|
| 9 |
+ |
|
| 10 |
+* [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/go-fsnotify/fsnotify/issues/36) |
|
| 11 |
+ |
|
| 12 |
+## v1.0.2 / 2014-08-17 |
|
| 13 |
+ |
|
| 14 |
+* [Fix] Missing create events on OS X. [#14](https://github.com/go-fsnotify/fsnotify/issues/14) (thanks @zhsso) |
|
| 15 |
+* [Fix] Make ./path and path equivalent. (thanks @zhsso) |
|
| 16 |
+ |
|
| 17 |
+## v1.0.0 / 2014-08-15 |
|
| 18 |
+ |
|
| 19 |
+* [API] Remove AddWatch on Windows, use Add. |
|
| 20 |
+* Improve documentation for exported identifiers. [#30](https://github.com/go-fsnotify/fsnotify/issues/30) |
|
| 21 |
+* Minor updates based on feedback from golint. |
|
| 22 |
+ |
|
| 23 |
+## dev / 2014-07-09 |
|
| 24 |
+ |
|
| 25 |
+* Moved to [github.com/go-fsnotify/fsnotify](https://github.com/go-fsnotify/fsnotify). |
|
| 26 |
+* Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno) |
|
| 27 |
+ |
|
| 28 |
+## dev / 2014-07-04 |
|
| 29 |
+ |
|
| 30 |
+* kqueue: fix incorrect mutex used in Close() |
|
| 31 |
+* Update example to demonstrate usage of Op. |
|
| 32 |
+ |
|
| 33 |
+## dev / 2014-06-28 |
|
| 34 |
+ |
|
| 35 |
+* [API] Don't set the Write Op for attribute notifications [#4](https://github.com/go-fsnotify/fsnotify/issues/4) |
|
| 36 |
+* Fix for String() method on Event (thanks Alex Brainman) |
|
| 37 |
+* Don't build on Plan 9 or Solaris (thanks @4ad) |
|
| 38 |
+ |
|
| 39 |
+## dev / 2014-06-21 |
|
| 40 |
+ |
|
| 41 |
+* Events channel of type Event rather than *Event. |
|
| 42 |
+* [internal] use syscall constants directly for inotify and kqueue. |
|
| 43 |
+* [internal] kqueue: rename events to kevents and fileEvent to event. |
|
| 44 |
+ |
|
| 45 |
+## dev / 2014-06-19 |
|
| 46 |
+ |
|
| 47 |
+* Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally). |
|
| 48 |
+* [internal] remove cookie from Event struct (unused). |
|
| 49 |
+* [internal] Event struct has the same definition across every OS. |
|
| 50 |
+* [internal] remove internal watch and removeWatch methods. |
|
| 51 |
+ |
|
| 52 |
+## dev / 2014-06-12 |
|
| 53 |
+ |
|
| 54 |
+* [API] Renamed Watch() to Add() and RemoveWatch() to Remove(). |
|
| 55 |
+* [API] Pluralized channel names: Events and Errors. |
|
| 56 |
+* [API] Renamed FileEvent struct to Event. |
|
| 57 |
+* [API] Op constants replace methods like IsCreate(). |
|
| 58 |
+ |
|
| 59 |
+## dev / 2014-06-12 |
|
| 60 |
+ |
|
| 61 |
+* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) |
|
| 62 |
+ |
|
| 63 |
+## dev / 2014-05-23 |
|
| 64 |
+ |
|
| 65 |
+* [API] Remove current implementation of WatchFlags. |
|
| 66 |
+ * current implementation doesn't take advantage of OS for efficiency |
|
| 67 |
+ * provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes |
|
| 68 |
+ * no tests for the current implementation |
|
| 69 |
+ * not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195) |
|
| 70 |
+ |
|
| 71 |
+## v0.9.2 / 2014-08-17 |
|
| 72 |
+ |
|
| 73 |
+* [Backport] Fix missing create events on OS X. [#14](https://github.com/go-fsnotify/fsnotify/issues/14) (thanks @zhsso) |
|
| 74 |
+ |
|
| 75 |
+## v0.9.1 / 2014-06-12 |
|
| 76 |
+ |
|
| 77 |
+* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98) |
|
| 78 |
+ |
|
| 79 |
+## v0.9.0 / 2014-01-17 |
|
| 80 |
+ |
|
| 81 |
+* IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany) |
|
| 82 |
+* [Fix] kqueue: fix deadlock [#77][] (thanks @cespare) |
|
| 83 |
+* [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library. |
|
| 84 |
+ |
|
| 85 |
+## v0.8.12 / 2013-11-13 |
|
| 86 |
+ |
|
| 87 |
+* [API] Remove FD_SET and friends from Linux adapter |
|
| 88 |
+ |
|
| 89 |
+## v0.8.11 / 2013-11-02 |
|
| 90 |
+ |
|
| 91 |
+* [Doc] Add Changelog [#72][] (thanks @nathany) |
|
| 92 |
+* [Doc] Spotlight and double modify events on OS X [#62][] (reported by @paulhammond) |
|
| 93 |
+ |
|
| 94 |
+## v0.8.10 / 2013-10-19 |
|
| 95 |
+ |
|
| 96 |
+* [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott) |
|
| 97 |
+* [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer) |
|
| 98 |
+* [Doc] specify OS-specific limits in README (thanks @debrando) |
|
| 99 |
+ |
|
| 100 |
+## v0.8.9 / 2013-09-08 |
|
| 101 |
+ |
|
| 102 |
+* [Doc] Contributing (thanks @nathany) |
|
| 103 |
+* [Doc] update package path in example code [#63][] (thanks @paulhammond) |
|
| 104 |
+* [Doc] GoCI badge in README (Linux only) [#60][] |
|
| 105 |
+* [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany) |
|
| 106 |
+ |
|
| 107 |
+## v0.8.8 / 2013-06-17 |
|
| 108 |
+ |
|
| 109 |
+* [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie) |
|
| 110 |
+ |
|
| 111 |
+## v0.8.7 / 2013-06-03 |
|
| 112 |
+ |
|
| 113 |
+* [API] Make syscall flags internal |
|
| 114 |
+* [Fix] inotify: ignore event changes |
|
| 115 |
+* [Fix] race in symlink test [#45][] (reported by @srid) |
|
| 116 |
+* [Fix] tests on Windows |
|
| 117 |
+* lower case error messages |
|
| 118 |
+ |
|
| 119 |
+## v0.8.6 / 2013-05-23 |
|
| 120 |
+ |
|
| 121 |
+* kqueue: Use EVT_ONLY flag on Darwin |
|
| 122 |
+* [Doc] Update README with full example |
|
| 123 |
+ |
|
| 124 |
+## v0.8.5 / 2013-05-09 |
|
| 125 |
+ |
|
| 126 |
+* [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg) |
|
| 127 |
+ |
|
| 128 |
+## v0.8.4 / 2013-04-07 |
|
| 129 |
+ |
|
| 130 |
+* [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz) |
|
| 131 |
+ |
|
| 132 |
+## v0.8.3 / 2013-03-13 |
|
| 133 |
+ |
|
| 134 |
+* [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin) |
|
| 135 |
+* [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin) |
|
| 136 |
+ |
|
| 137 |
+## v0.8.2 / 2013-02-07 |
|
| 138 |
+ |
|
| 139 |
+* [Doc] add Authors |
|
| 140 |
+* [Fix] fix data races for map access [#29][] (thanks @fsouza) |
|
| 141 |
+ |
|
| 142 |
+## v0.8.1 / 2013-01-09 |
|
| 143 |
+ |
|
| 144 |
+* [Fix] Windows path separators |
|
| 145 |
+* [Doc] BSD License |
|
| 146 |
+ |
|
| 147 |
+## v0.8.0 / 2012-11-09 |
|
| 148 |
+ |
|
| 149 |
+* kqueue: directory watching improvements (thanks @vmirage) |
|
| 150 |
+* inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto) |
|
| 151 |
+* [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr) |
|
| 152 |
+ |
|
| 153 |
+## v0.7.4 / 2012-10-09 |
|
| 154 |
+ |
|
| 155 |
+* [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji) |
|
| 156 |
+* [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig) |
|
| 157 |
+* [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig) |
|
| 158 |
+* [Fix] kqueue: modify after recreation of file |
|
| 159 |
+ |
|
| 160 |
+## v0.7.3 / 2012-09-27 |
|
| 161 |
+ |
|
| 162 |
+* [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage) |
|
| 163 |
+* [Fix] kqueue: no longer get duplicate CREATE events |
|
| 164 |
+ |
|
| 165 |
+## v0.7.2 / 2012-09-01 |
|
| 166 |
+ |
|
| 167 |
+* kqueue: events for created directories |
|
| 168 |
+ |
|
| 169 |
+## v0.7.1 / 2012-07-14 |
|
| 170 |
+ |
|
| 171 |
+* [Fix] for renaming files |
|
| 172 |
+ |
|
| 173 |
+## v0.7.0 / 2012-07-02 |
|
| 174 |
+ |
|
| 175 |
+* [Feature] FSNotify flags |
|
| 176 |
+* [Fix] inotify: Added file name back to event path |
|
| 177 |
+ |
|
| 178 |
+## v0.6.0 / 2012-06-06 |
|
| 179 |
+ |
|
| 180 |
+* kqueue: watch files after directory created (thanks @tmc) |
|
| 181 |
+ |
|
| 182 |
+## v0.5.1 / 2012-05-22 |
|
| 183 |
+ |
|
| 184 |
+* [Fix] inotify: remove all watches before Close() |
|
| 185 |
+ |
|
| 186 |
+## v0.5.0 / 2012-05-03 |
|
| 187 |
+ |
|
| 188 |
+* [API] kqueue: return errors during watch instead of sending over channel |
|
| 189 |
+* kqueue: match symlink behavior on Linux |
|
| 190 |
+* inotify: add `DELETE_SELF` (requested by @taralx) |
|
| 191 |
+* [Fix] kqueue: handle EINTR (reported by @robfig) |
|
| 192 |
+* [Doc] Godoc example [#1][] (thanks @davecheney) |
|
| 193 |
+ |
|
| 194 |
+## v0.4.0 / 2012-03-30 |
|
| 195 |
+ |
|
| 196 |
+* Go 1 released: build with go tool |
|
| 197 |
+* [Feature] Windows support using winfsnotify |
|
| 198 |
+* Windows does not have attribute change notifications |
|
| 199 |
+* Roll attribute notifications into IsModify |
|
| 200 |
+ |
|
| 201 |
+## v0.3.0 / 2012-02-19 |
|
| 202 |
+ |
|
| 203 |
+* kqueue: add files when watch directory |
|
| 204 |
+ |
|
| 205 |
+## v0.2.0 / 2011-12-30 |
|
| 206 |
+ |
|
| 207 |
+* update to latest Go weekly code |
|
| 208 |
+ |
|
| 209 |
+## v0.1.0 / 2011-10-19 |
|
| 210 |
+ |
|
| 211 |
+* kqueue: add watch on file creation to match inotify |
|
| 212 |
+* kqueue: create file event |
|
| 213 |
+* inotify: ignore `IN_IGNORED` events |
|
| 214 |
+* event String() |
|
| 215 |
+* linux: common FileEvent functions |
|
| 216 |
+* initial commit |
|
| 217 |
+ |
|
| 218 |
+[#79]: https://github.com/howeyc/fsnotify/pull/79 |
|
| 219 |
+[#77]: https://github.com/howeyc/fsnotify/pull/77 |
|
| 220 |
+[#72]: https://github.com/howeyc/fsnotify/issues/72 |
|
| 221 |
+[#71]: https://github.com/howeyc/fsnotify/issues/71 |
|
| 222 |
+[#70]: https://github.com/howeyc/fsnotify/issues/70 |
|
| 223 |
+[#63]: https://github.com/howeyc/fsnotify/issues/63 |
|
| 224 |
+[#62]: https://github.com/howeyc/fsnotify/issues/62 |
|
| 225 |
+[#60]: https://github.com/howeyc/fsnotify/issues/60 |
|
| 226 |
+[#59]: https://github.com/howeyc/fsnotify/issues/59 |
|
| 227 |
+[#49]: https://github.com/howeyc/fsnotify/issues/49 |
|
| 228 |
+[#45]: https://github.com/howeyc/fsnotify/issues/45 |
|
| 229 |
+[#40]: https://github.com/howeyc/fsnotify/issues/40 |
|
| 230 |
+[#36]: https://github.com/howeyc/fsnotify/issues/36 |
|
| 231 |
+[#33]: https://github.com/howeyc/fsnotify/issues/33 |
|
| 232 |
+[#29]: https://github.com/howeyc/fsnotify/issues/29 |
|
| 233 |
+[#25]: https://github.com/howeyc/fsnotify/issues/25 |
|
| 234 |
+[#24]: https://github.com/howeyc/fsnotify/issues/24 |
|
| 235 |
+[#21]: https://github.com/howeyc/fsnotify/issues/21 |
|
| 236 |
+ |
| 0 | 237 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,56 @@ |
| 0 |
+# Contributing |
|
| 1 |
+ |
|
| 2 |
+* Send questions to [golang-dev@googlegroups.com](mailto:golang-dev@googlegroups.com). |
|
| 3 |
+ |
|
| 4 |
+### Issues |
|
| 5 |
+ |
|
| 6 |
+* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/go-fsnotify/fsnotify/issues). |
|
| 7 |
+* Please indicate the platform you are running on. |
|
| 8 |
+ |
|
| 9 |
+### Pull Requests |
|
| 10 |
+ |
|
| 11 |
+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). |
|
| 12 |
+ |
|
| 13 |
+Please indicate that you have signed the CLA in your pull request. |
|
| 14 |
+ |
|
| 15 |
+To hack on fsnotify: |
|
| 16 |
+ |
|
| 17 |
+1. Install as usual (`go get -u github.com/go-fsnotify/fsnotify`) |
|
| 18 |
+2. Create your feature branch (`git checkout -b my-new-feature`) |
|
| 19 |
+3. Ensure everything works and the tests pass (see below) |
|
| 20 |
+4. Commit your changes (`git commit -am 'Add some feature'`) |
|
| 21 |
+ |
|
| 22 |
+Contribute upstream: |
|
| 23 |
+ |
|
| 24 |
+1. Fork fsnotify on GitHub |
|
| 25 |
+2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`) |
|
| 26 |
+3. Push to the branch (`git push fork my-new-feature`) |
|
| 27 |
+4. Create a new Pull Request on GitHub |
|
| 28 |
+ |
|
| 29 |
+If other team members need your patch before I merge it: |
|
| 30 |
+ |
|
| 31 |
+1. Install as usual (`go get -u github.com/go-fsnotify/fsnotify`) |
|
| 32 |
+2. Add your remote (`git remote add fork git@github.com:mycompany/repo.git`) |
|
| 33 |
+3. Pull your revisions (`git fetch fork; git checkout -b my-new-feature fork/my-new-feature`) |
|
| 34 |
+ |
|
| 35 |
+Notice: For smooth sailing, always use the original import path. Installing with `go get` makes this easy. |
|
| 36 |
+ |
|
| 37 |
+Note: The maintainers will update the CHANGELOG on your behalf. Please don't modify it in your pull request. |
|
| 38 |
+ |
|
| 39 |
+### Testing |
|
| 40 |
+ |
|
| 41 |
+fsnotify uses build tags to compile different code on Linux, BSD, OS X, and Windows. |
|
| 42 |
+ |
|
| 43 |
+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. |
|
| 44 |
+ |
|
| 45 |
+To make cross-platform testing easier, I've created a Vagrantfile for Linux and BSD. |
|
| 46 |
+ |
|
| 47 |
+* Install [Vagrant](http://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) |
|
| 48 |
+* Setup [Vagrant Gopher](https://github.com/nathany/vagrant-gopher) in your `src` folder. |
|
| 49 |
+* 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) |
|
| 50 |
+* 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'`. |
|
| 51 |
+* When you're done, you will want to halt or destroy the Vagrant boxes. |
|
| 52 |
+ |
|
| 53 |
+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. |
|
| 54 |
+ |
|
| 55 |
+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). |
| 0 | 56 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,28 @@ |
| 0 |
+Copyright (c) 2012 The Go Authors. All rights reserved. |
|
| 1 |
+Copyright (c) 2012 fsnotify Authors. All rights reserved. |
|
| 2 |
+ |
|
| 3 |
+Redistribution and use in source and binary forms, with or without |
|
| 4 |
+modification, are permitted provided that the following conditions are |
|
| 5 |
+met: |
|
| 6 |
+ |
|
| 7 |
+ * Redistributions of source code must retain the above copyright |
|
| 8 |
+notice, this list of conditions and the following disclaimer. |
|
| 9 |
+ * Redistributions in binary form must reproduce the above |
|
| 10 |
+copyright notice, this list of conditions and the following disclaimer |
|
| 11 |
+in the documentation and/or other materials provided with the |
|
| 12 |
+distribution. |
|
| 13 |
+ * Neither the name of Google Inc. nor the names of its |
|
| 14 |
+contributors may be used to endorse or promote products derived from |
|
| 15 |
+this software without specific prior written permission. |
|
| 16 |
+ |
|
| 17 |
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
| 18 |
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
| 19 |
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|
| 20 |
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|
| 21 |
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
| 22 |
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
| 23 |
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
|
| 24 |
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
|
| 25 |
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
| 26 |
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|
| 27 |
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 0 | 28 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,53 @@ |
| 0 |
+# File system notifications for Go |
|
| 1 |
+ |
|
| 2 |
+[](http://gocover.io/github.com/go-fsnotify/fsnotify) [](https://godoc.org/gopkg.in/fsnotify.v1) |
|
| 3 |
+ |
|
| 4 |
+Cross platform: Windows, Linux, BSD and OS X. |
|
| 5 |
+ |
|
| 6 |
+|Adapter |OS |Status | |
|
| 7 |
+|----------|----------|----------| |
|
| 8 |
+|inotify |Linux, Android\*|Supported| |
|
| 9 |
+|kqueue |BSD, OS X, iOS\*|Supported| |
|
| 10 |
+|ReadDirectoryChangesW|Windows|Supported| |
|
| 11 |
+|FSEvents |OS X |[Planned](https://github.com/go-fsnotify/fsnotify/issues/11)| |
|
| 12 |
+|FEN |Solaris 11 |[Planned](https://github.com/go-fsnotify/fsnotify/issues/12)| |
|
| 13 |
+|fanotify |Linux 2.6.37+ | | |
|
| 14 |
+|Polling |*All* |[Maybe](https://github.com/go-fsnotify/fsnotify/issues/9)| |
|
| 15 |
+| |Plan 9 | | |
|
| 16 |
+ |
|
| 17 |
+\* Android and iOS are untested. |
|
| 18 |
+ |
|
| 19 |
+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. |
|
| 20 |
+ |
|
| 21 |
+## API stability |
|
| 22 |
+ |
|
| 23 |
+Two major versions of fsnotify exist. |
|
| 24 |
+ |
|
| 25 |
+**[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: |
|
| 26 |
+ |
|
| 27 |
+```go |
|
| 28 |
+import "gopkg.in/fsnotify.v1" |
|
| 29 |
+``` |
|
| 30 |
+ |
|
| 31 |
+\* Refer to the package as fsnotify (without the .v1 suffix). |
|
| 32 |
+ |
|
| 33 |
+**[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. |
|
| 34 |
+ |
|
| 35 |
+```go |
|
| 36 |
+import "gopkg.in/fsnotify.v0" |
|
| 37 |
+``` |
|
| 38 |
+ |
|
| 39 |
+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. |
|
| 40 |
+ |
|
| 41 |
+## Contributing |
|
| 42 |
+ |
|
| 43 |
+* Send questions to [golang-dev@googlegroups.com](mailto:golang-dev@googlegroups.com). |
|
| 44 |
+* Request features and report bugs using the [GitHub Issue Tracker](https://github.com/go-fsnotify/fsnotify/issues). |
|
| 45 |
+ |
|
| 46 |
+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). |
|
| 47 |
+ |
|
| 48 |
+Please read [CONTRIBUTING](https://github.com/go-fsnotify/fsnotify/blob/master/CONTRIBUTING.md) before opening a pull request. |
|
| 49 |
+ |
|
| 50 |
+## Example |
|
| 51 |
+ |
|
| 52 |
+See [example_test.go](https://github.com/go-fsnotify/fsnotify/blob/master/example_test.go). |
| 0 | 53 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,42 @@ |
| 0 |
+// Copyright 2012 The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE file. |
|
| 3 |
+ |
|
| 4 |
+// +build !plan9,!solaris |
|
| 5 |
+ |
|
| 6 |
+package fsnotify_test |
|
| 7 |
+ |
|
| 8 |
+import ( |
|
| 9 |
+ "log" |
|
| 10 |
+ |
|
| 11 |
+ "gopkg.in/fsnotify.v1" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+func ExampleNewWatcher() {
|
|
| 15 |
+ watcher, err := fsnotify.NewWatcher() |
|
| 16 |
+ if err != nil {
|
|
| 17 |
+ log.Fatal(err) |
|
| 18 |
+ } |
|
| 19 |
+ defer watcher.Close() |
|
| 20 |
+ |
|
| 21 |
+ done := make(chan bool) |
|
| 22 |
+ go func() {
|
|
| 23 |
+ for {
|
|
| 24 |
+ select {
|
|
| 25 |
+ case event := <-watcher.Events: |
|
| 26 |
+ log.Println("event:", event)
|
|
| 27 |
+ if event.Op&fsnotify.Write == fsnotify.Write {
|
|
| 28 |
+ log.Println("modified file:", event.Name)
|
|
| 29 |
+ } |
|
| 30 |
+ case err := <-watcher.Errors: |
|
| 31 |
+ log.Println("error:", err)
|
|
| 32 |
+ } |
|
| 33 |
+ } |
|
| 34 |
+ }() |
|
| 35 |
+ |
|
| 36 |
+ err = watcher.Add("/tmp/foo")
|
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ log.Fatal(err) |
|
| 39 |
+ } |
|
| 40 |
+ <-done |
|
| 41 |
+} |
| 0 | 42 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,56 @@ |
| 0 |
+// Copyright 2012 The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE file. |
|
| 3 |
+ |
|
| 4 |
+// +build !plan9,!solaris |
|
| 5 |
+ |
|
| 6 |
+// Package fsnotify provides a platform-independent interface for file system notifications. |
|
| 7 |
+package fsnotify |
|
| 8 |
+ |
|
| 9 |
+import "fmt" |
|
| 10 |
+ |
|
| 11 |
+// Event represents a single file system notification. |
|
| 12 |
+type Event struct {
|
|
| 13 |
+ Name string // Relative path to the file or directory. |
|
| 14 |
+ Op Op // File operation that triggered the event. |
|
| 15 |
+} |
|
| 16 |
+ |
|
| 17 |
+// Op describes a set of file operations. |
|
| 18 |
+type Op uint32 |
|
| 19 |
+ |
|
| 20 |
+// These are the generalized file operations that can trigger a notification. |
|
| 21 |
+const ( |
|
| 22 |
+ Create Op = 1 << iota |
|
| 23 |
+ Write |
|
| 24 |
+ Remove |
|
| 25 |
+ Rename |
|
| 26 |
+ Chmod |
|
| 27 |
+) |
|
| 28 |
+ |
|
| 29 |
+// String returns a string representation of the event in the form |
|
| 30 |
+// "file: REMOVE|WRITE|..." |
|
| 31 |
+func (e Event) String() string {
|
|
| 32 |
+ events := "" |
|
| 33 |
+ |
|
| 34 |
+ if e.Op&Create == Create {
|
|
| 35 |
+ events += "|CREATE" |
|
| 36 |
+ } |
|
| 37 |
+ if e.Op&Remove == Remove {
|
|
| 38 |
+ events += "|REMOVE" |
|
| 39 |
+ } |
|
| 40 |
+ if e.Op&Write == Write {
|
|
| 41 |
+ events += "|WRITE" |
|
| 42 |
+ } |
|
| 43 |
+ if e.Op&Rename == Rename {
|
|
| 44 |
+ events += "|RENAME" |
|
| 45 |
+ } |
|
| 46 |
+ if e.Op&Chmod == Chmod {
|
|
| 47 |
+ events += "|CHMOD" |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 50 |
+ if len(events) > 0 {
|
|
| 51 |
+ events = events[1:] |
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ return fmt.Sprintf("%q: %s", e.Name, events)
|
|
| 55 |
+} |
| 0 | 56 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,239 @@ |
| 0 |
+// Copyright 2010 The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE file. |
|
| 3 |
+ |
|
| 4 |
+// +build linux |
|
| 5 |
+ |
|
| 6 |
+package fsnotify |
|
| 7 |
+ |
|
| 8 |
+import ( |
|
| 9 |
+ "errors" |
|
| 10 |
+ "fmt" |
|
| 11 |
+ "os" |
|
| 12 |
+ "path/filepath" |
|
| 13 |
+ "strings" |
|
| 14 |
+ "sync" |
|
| 15 |
+ "syscall" |
|
| 16 |
+ "unsafe" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+// Watcher watches a set of files, delivering events to a channel. |
|
| 20 |
+type Watcher struct {
|
|
| 21 |
+ Events chan Event |
|
| 22 |
+ Errors chan error |
|
| 23 |
+ mu sync.Mutex // Map access |
|
| 24 |
+ fd int // File descriptor (as returned by the inotify_init() syscall) |
|
| 25 |
+ watches map[string]*watch // Map of inotify watches (key: path) |
|
| 26 |
+ paths map[int]string // Map of watched paths (key: watch descriptor) |
|
| 27 |
+ done chan bool // Channel for sending a "quit message" to the reader goroutine |
|
| 28 |
+ isClosed bool // Set to true when Close() is first called |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. |
|
| 32 |
+func NewWatcher() (*Watcher, error) {
|
|
| 33 |
+ fd, errno := syscall.InotifyInit() |
|
| 34 |
+ if fd == -1 {
|
|
| 35 |
+ return nil, os.NewSyscallError("inotify_init", errno)
|
|
| 36 |
+ } |
|
| 37 |
+ w := &Watcher{
|
|
| 38 |
+ fd: fd, |
|
| 39 |
+ watches: make(map[string]*watch), |
|
| 40 |
+ paths: make(map[int]string), |
|
| 41 |
+ Events: make(chan Event), |
|
| 42 |
+ Errors: make(chan error), |
|
| 43 |
+ done: make(chan bool, 1), |
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ go w.readEvents() |
|
| 47 |
+ return w, nil |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+// Close removes all watches and closes the events channel. |
|
| 51 |
+func (w *Watcher) Close() error {
|
|
| 52 |
+ if w.isClosed {
|
|
| 53 |
+ return nil |
|
| 54 |
+ } |
|
| 55 |
+ w.isClosed = true |
|
| 56 |
+ |
|
| 57 |
+ // Remove all watches |
|
| 58 |
+ for name := range w.watches {
|
|
| 59 |
+ w.Remove(name) |
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 62 |
+ // Send "quit" message to the reader goroutine |
|
| 63 |
+ w.done <- true |
|
| 64 |
+ |
|
| 65 |
+ return nil |
|
| 66 |
+} |
|
| 67 |
+ |
|
| 68 |
+// Add starts watching the named file or directory (non-recursively). |
|
| 69 |
+func (w *Watcher) Add(name string) error {
|
|
| 70 |
+ name = filepath.Clean(name) |
|
| 71 |
+ if w.isClosed {
|
|
| 72 |
+ return errors.New("inotify instance already closed")
|
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ const agnosticEvents = syscall.IN_MOVED_TO | syscall.IN_MOVED_FROM | |
|
| 76 |
+ syscall.IN_CREATE | syscall.IN_ATTRIB | syscall.IN_MODIFY | |
|
| 77 |
+ syscall.IN_MOVE_SELF | syscall.IN_DELETE | syscall.IN_DELETE_SELF |
|
| 78 |
+ |
|
| 79 |
+ var flags uint32 = agnosticEvents |
|
| 80 |
+ |
|
| 81 |
+ w.mu.Lock() |
|
| 82 |
+ watchEntry, found := w.watches[name] |
|
| 83 |
+ w.mu.Unlock() |
|
| 84 |
+ if found {
|
|
| 85 |
+ watchEntry.flags |= flags |
|
| 86 |
+ flags |= syscall.IN_MASK_ADD |
|
| 87 |
+ } |
|
| 88 |
+ wd, errno := syscall.InotifyAddWatch(w.fd, name, flags) |
|
| 89 |
+ if wd == -1 {
|
|
| 90 |
+ return os.NewSyscallError("inotify_add_watch", errno)
|
|
| 91 |
+ } |
|
| 92 |
+ |
|
| 93 |
+ w.mu.Lock() |
|
| 94 |
+ w.watches[name] = &watch{wd: uint32(wd), flags: flags}
|
|
| 95 |
+ w.paths[wd] = name |
|
| 96 |
+ w.mu.Unlock() |
|
| 97 |
+ |
|
| 98 |
+ return nil |
|
| 99 |
+} |
|
| 100 |
+ |
|
| 101 |
+// Remove stops watching the the named file or directory (non-recursively). |
|
| 102 |
+func (w *Watcher) Remove(name string) error {
|
|
| 103 |
+ name = filepath.Clean(name) |
|
| 104 |
+ w.mu.Lock() |
|
| 105 |
+ defer w.mu.Unlock() |
|
| 106 |
+ watch, ok := w.watches[name] |
|
| 107 |
+ if !ok {
|
|
| 108 |
+ return fmt.Errorf("can't remove non-existent inotify watch for: %s", name)
|
|
| 109 |
+ } |
|
| 110 |
+ success, errno := syscall.InotifyRmWatch(w.fd, watch.wd) |
|
| 111 |
+ if success == -1 {
|
|
| 112 |
+ return os.NewSyscallError("inotify_rm_watch", errno)
|
|
| 113 |
+ } |
|
| 114 |
+ delete(w.watches, name) |
|
| 115 |
+ return nil |
|
| 116 |
+} |
|
| 117 |
+ |
|
| 118 |
+type watch struct {
|
|
| 119 |
+ wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) |
|
| 120 |
+ flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) |
|
| 121 |
+} |
|
| 122 |
+ |
|
| 123 |
+// readEvents reads from the inotify file descriptor, converts the |
|
| 124 |
+// received events into Event objects and sends them via the Events channel |
|
| 125 |
+func (w *Watcher) readEvents() {
|
|
| 126 |
+ var ( |
|
| 127 |
+ buf [syscall.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events |
|
| 128 |
+ n int // Number of bytes read with read() |
|
| 129 |
+ errno error // Syscall errno |
|
| 130 |
+ ) |
|
| 131 |
+ |
|
| 132 |
+ for {
|
|
| 133 |
+ // See if there is a message on the "done" channel |
|
| 134 |
+ select {
|
|
| 135 |
+ case <-w.done: |
|
| 136 |
+ syscall.Close(w.fd) |
|
| 137 |
+ close(w.Events) |
|
| 138 |
+ close(w.Errors) |
|
| 139 |
+ return |
|
| 140 |
+ default: |
|
| 141 |
+ } |
|
| 142 |
+ |
|
| 143 |
+ n, errno = syscall.Read(w.fd, buf[:]) |
|
| 144 |
+ |
|
| 145 |
+ // If EOF is received |
|
| 146 |
+ if n == 0 {
|
|
| 147 |
+ syscall.Close(w.fd) |
|
| 148 |
+ close(w.Events) |
|
| 149 |
+ close(w.Errors) |
|
| 150 |
+ return |
|
| 151 |
+ } |
|
| 152 |
+ |
|
| 153 |
+ if n < 0 {
|
|
| 154 |
+ w.Errors <- os.NewSyscallError("read", errno)
|
|
| 155 |
+ continue |
|
| 156 |
+ } |
|
| 157 |
+ if n < syscall.SizeofInotifyEvent {
|
|
| 158 |
+ w.Errors <- errors.New("inotify: short read in readEvents()")
|
|
| 159 |
+ continue |
|
| 160 |
+ } |
|
| 161 |
+ |
|
| 162 |
+ var offset uint32 |
|
| 163 |
+ // We don't know how many events we just read into the buffer |
|
| 164 |
+ // While the offset points to at least one whole event... |
|
| 165 |
+ for offset <= uint32(n-syscall.SizeofInotifyEvent) {
|
|
| 166 |
+ // Point "raw" to the event in the buffer |
|
| 167 |
+ raw := (*syscall.InotifyEvent)(unsafe.Pointer(&buf[offset])) |
|
| 168 |
+ |
|
| 169 |
+ mask := uint32(raw.Mask) |
|
| 170 |
+ nameLen := uint32(raw.Len) |
|
| 171 |
+ // If the event happened to the watched directory or the watched file, the kernel |
|
| 172 |
+ // doesn't append the filename to the event, but we would like to always fill the |
|
| 173 |
+ // the "Name" field with a valid filename. We retrieve the path of the watch from |
|
| 174 |
+ // the "paths" map. |
|
| 175 |
+ w.mu.Lock() |
|
| 176 |
+ name := w.paths[int(raw.Wd)] |
|
| 177 |
+ w.mu.Unlock() |
|
| 178 |
+ if nameLen > 0 {
|
|
| 179 |
+ // Point "bytes" at the first byte of the filename |
|
| 180 |
+ bytes := (*[syscall.PathMax]byte)(unsafe.Pointer(&buf[offset+syscall.SizeofInotifyEvent])) |
|
| 181 |
+ // The filename is padded with NULL bytes. TrimRight() gets rid of those. |
|
| 182 |
+ name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") |
|
| 183 |
+ } |
|
| 184 |
+ |
|
| 185 |
+ event := newEvent(name, mask) |
|
| 186 |
+ |
|
| 187 |
+ // Send the events that are not ignored on the events channel |
|
| 188 |
+ if !event.ignoreLinux(mask) {
|
|
| 189 |
+ w.Events <- event |
|
| 190 |
+ } |
|
| 191 |
+ |
|
| 192 |
+ // Move to the next event in the buffer |
|
| 193 |
+ offset += syscall.SizeofInotifyEvent + nameLen |
|
| 194 |
+ } |
|
| 195 |
+ } |
|
| 196 |
+} |
|
| 197 |
+ |
|
| 198 |
+// Certain types of events can be "ignored" and not sent over the Events |
|
| 199 |
+// channel. Such as events marked ignore by the kernel, or MODIFY events |
|
| 200 |
+// against files that do not exist. |
|
| 201 |
+func (e *Event) ignoreLinux(mask uint32) bool {
|
|
| 202 |
+ // Ignore anything the inotify API says to ignore |
|
| 203 |
+ if mask&syscall.IN_IGNORED == syscall.IN_IGNORED {
|
|
| 204 |
+ return true |
|
| 205 |
+ } |
|
| 206 |
+ |
|
| 207 |
+ // If the event is not a DELETE or RENAME, the file must exist. |
|
| 208 |
+ // Otherwise the event is ignored. |
|
| 209 |
+ // *Note*: this was put in place because it was seen that a MODIFY |
|
| 210 |
+ // event was sent after the DELETE. This ignores that MODIFY and |
|
| 211 |
+ // assumes a DELETE will come or has come if the file doesn't exist. |
|
| 212 |
+ if !(e.Op&Remove == Remove || e.Op&Rename == Rename) {
|
|
| 213 |
+ _, statErr := os.Lstat(e.Name) |
|
| 214 |
+ return os.IsNotExist(statErr) |
|
| 215 |
+ } |
|
| 216 |
+ return false |
|
| 217 |
+} |
|
| 218 |
+ |
|
| 219 |
+// newEvent returns an platform-independent Event based on an inotify mask. |
|
| 220 |
+func newEvent(name string, mask uint32) Event {
|
|
| 221 |
+ e := Event{Name: name}
|
|
| 222 |
+ if mask&syscall.IN_CREATE == syscall.IN_CREATE || mask&syscall.IN_MOVED_TO == syscall.IN_MOVED_TO {
|
|
| 223 |
+ e.Op |= Create |
|
| 224 |
+ } |
|
| 225 |
+ if mask&syscall.IN_DELETE_SELF == syscall.IN_DELETE_SELF || mask&syscall.IN_DELETE == syscall.IN_DELETE {
|
|
| 226 |
+ e.Op |= Remove |
|
| 227 |
+ } |
|
| 228 |
+ if mask&syscall.IN_MODIFY == syscall.IN_MODIFY {
|
|
| 229 |
+ e.Op |= Write |
|
| 230 |
+ } |
|
| 231 |
+ if mask&syscall.IN_MOVE_SELF == syscall.IN_MOVE_SELF || mask&syscall.IN_MOVED_FROM == syscall.IN_MOVED_FROM {
|
|
| 232 |
+ e.Op |= Rename |
|
| 233 |
+ } |
|
| 234 |
+ if mask&syscall.IN_ATTRIB == syscall.IN_ATTRIB {
|
|
| 235 |
+ e.Op |= Chmod |
|
| 236 |
+ } |
|
| 237 |
+ return e |
|
| 238 |
+} |
| 0 | 239 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,1120 @@ |
| 0 |
+// Copyright 2010 The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE file. |
|
| 3 |
+ |
|
| 4 |
+// +build !plan9,!solaris |
|
| 5 |
+ |
|
| 6 |
+package fsnotify |
|
| 7 |
+ |
|
| 8 |
+import ( |
|
| 9 |
+ "io/ioutil" |
|
| 10 |
+ "os" |
|
| 11 |
+ "os/exec" |
|
| 12 |
+ "path/filepath" |
|
| 13 |
+ "runtime" |
|
| 14 |
+ "sync/atomic" |
|
| 15 |
+ "testing" |
|
| 16 |
+ "time" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+// An atomic counter |
|
| 20 |
+type counter struct {
|
|
| 21 |
+ val int32 |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func (c *counter) increment() {
|
|
| 25 |
+ atomic.AddInt32(&c.val, 1) |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func (c *counter) value() int32 {
|
|
| 29 |
+ return atomic.LoadInt32(&c.val) |
|
| 30 |
+} |
|
| 31 |
+ |
|
| 32 |
+func (c *counter) reset() {
|
|
| 33 |
+ atomic.StoreInt32(&c.val, 0) |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 36 |
+// tempMkdir makes a temporary directory |
|
| 37 |
+func tempMkdir(t *testing.T) string {
|
|
| 38 |
+ dir, err := ioutil.TempDir("", "fsnotify")
|
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ t.Fatalf("failed to create test directory: %s", err)
|
|
| 41 |
+ } |
|
| 42 |
+ return dir |
|
| 43 |
+} |
|
| 44 |
+ |
|
| 45 |
+// newWatcher initializes an fsnotify Watcher instance. |
|
| 46 |
+func newWatcher(t *testing.T) *Watcher {
|
|
| 47 |
+ watcher, err := NewWatcher() |
|
| 48 |
+ if err != nil {
|
|
| 49 |
+ t.Fatalf("NewWatcher() failed: %s", err)
|
|
| 50 |
+ } |
|
| 51 |
+ return watcher |
|
| 52 |
+} |
|
| 53 |
+ |
|
| 54 |
+// addWatch adds a watch for a directory |
|
| 55 |
+func addWatch(t *testing.T, watcher *Watcher, dir string) {
|
|
| 56 |
+ if err := watcher.Add(dir); err != nil {
|
|
| 57 |
+ t.Fatalf("watcher.Add(%q) failed: %s", dir, err)
|
|
| 58 |
+ } |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+func TestFsnotifyMultipleOperations(t *testing.T) {
|
|
| 62 |
+ watcher := newWatcher(t) |
|
| 63 |
+ |
|
| 64 |
+ // Receive errors on the error channel on a separate goroutine |
|
| 65 |
+ go func() {
|
|
| 66 |
+ for err := range watcher.Errors {
|
|
| 67 |
+ t.Fatalf("error received: %s", err)
|
|
| 68 |
+ } |
|
| 69 |
+ }() |
|
| 70 |
+ |
|
| 71 |
+ // Create directory to watch |
|
| 72 |
+ testDir := tempMkdir(t) |
|
| 73 |
+ defer os.RemoveAll(testDir) |
|
| 74 |
+ |
|
| 75 |
+ // Create directory that's not watched |
|
| 76 |
+ testDirToMoveFiles := tempMkdir(t) |
|
| 77 |
+ defer os.RemoveAll(testDirToMoveFiles) |
|
| 78 |
+ |
|
| 79 |
+ testFile := filepath.Join(testDir, "TestFsnotifySeq.testfile") |
|
| 80 |
+ testFileRenamed := filepath.Join(testDirToMoveFiles, "TestFsnotifySeqRename.testfile") |
|
| 81 |
+ |
|
| 82 |
+ addWatch(t, watcher, testDir) |
|
| 83 |
+ |
|
| 84 |
+ // Receive events on the event channel on a separate goroutine |
|
| 85 |
+ eventstream := watcher.Events |
|
| 86 |
+ var createReceived, modifyReceived, deleteReceived, renameReceived counter |
|
| 87 |
+ done := make(chan bool) |
|
| 88 |
+ go func() {
|
|
| 89 |
+ for event := range eventstream {
|
|
| 90 |
+ // Only count relevant events |
|
| 91 |
+ if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) {
|
|
| 92 |
+ t.Logf("event received: %s", event)
|
|
| 93 |
+ if event.Op&Remove == Remove {
|
|
| 94 |
+ deleteReceived.increment() |
|
| 95 |
+ } |
|
| 96 |
+ if event.Op&Write == Write {
|
|
| 97 |
+ modifyReceived.increment() |
|
| 98 |
+ } |
|
| 99 |
+ if event.Op&Create == Create {
|
|
| 100 |
+ createReceived.increment() |
|
| 101 |
+ } |
|
| 102 |
+ if event.Op&Rename == Rename {
|
|
| 103 |
+ renameReceived.increment() |
|
| 104 |
+ } |
|
| 105 |
+ } else {
|
|
| 106 |
+ t.Logf("unexpected event received: %s", event)
|
|
| 107 |
+ } |
|
| 108 |
+ } |
|
| 109 |
+ done <- true |
|
| 110 |
+ }() |
|
| 111 |
+ |
|
| 112 |
+ // Create a file |
|
| 113 |
+ // This should add at least one event to the fsnotify event queue |
|
| 114 |
+ var f *os.File |
|
| 115 |
+ f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 116 |
+ if err != nil {
|
|
| 117 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 118 |
+ } |
|
| 119 |
+ f.Sync() |
|
| 120 |
+ |
|
| 121 |
+ time.Sleep(time.Millisecond) |
|
| 122 |
+ f.WriteString("data")
|
|
| 123 |
+ f.Sync() |
|
| 124 |
+ f.Close() |
|
| 125 |
+ |
|
| 126 |
+ time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete |
|
| 127 |
+ |
|
| 128 |
+ if err := testRename(testFile, testFileRenamed); err != nil {
|
|
| 129 |
+ t.Fatalf("rename failed: %s", err)
|
|
| 130 |
+ } |
|
| 131 |
+ |
|
| 132 |
+ // Modify the file outside of the watched dir |
|
| 133 |
+ f, err = os.Open(testFileRenamed) |
|
| 134 |
+ if err != nil {
|
|
| 135 |
+ t.Fatalf("open test renamed file failed: %s", err)
|
|
| 136 |
+ } |
|
| 137 |
+ f.WriteString("data")
|
|
| 138 |
+ f.Sync() |
|
| 139 |
+ f.Close() |
|
| 140 |
+ |
|
| 141 |
+ time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete |
|
| 142 |
+ |
|
| 143 |
+ // Recreate the file that was moved |
|
| 144 |
+ f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 145 |
+ if err != nil {
|
|
| 146 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 147 |
+ } |
|
| 148 |
+ f.Close() |
|
| 149 |
+ time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete |
|
| 150 |
+ |
|
| 151 |
+ // We expect this event to be received almost immediately, but let's wait 500 ms to be sure |
|
| 152 |
+ time.Sleep(500 * time.Millisecond) |
|
| 153 |
+ cReceived := createReceived.value() |
|
| 154 |
+ if cReceived != 2 {
|
|
| 155 |
+ t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 2)
|
|
| 156 |
+ } |
|
| 157 |
+ mReceived := modifyReceived.value() |
|
| 158 |
+ if mReceived != 1 {
|
|
| 159 |
+ t.Fatalf("incorrect number of modify events received after 500 ms (%d vs %d)", mReceived, 1)
|
|
| 160 |
+ } |
|
| 161 |
+ dReceived := deleteReceived.value() |
|
| 162 |
+ rReceived := renameReceived.value() |
|
| 163 |
+ if dReceived+rReceived != 1 {
|
|
| 164 |
+ t.Fatalf("incorrect number of rename+delete events received after 500 ms (%d vs %d)", rReceived+dReceived, 1)
|
|
| 165 |
+ } |
|
| 166 |
+ |
|
| 167 |
+ // Try closing the fsnotify instance |
|
| 168 |
+ t.Log("calling Close()")
|
|
| 169 |
+ watcher.Close() |
|
| 170 |
+ t.Log("waiting for the event channel to become closed...")
|
|
| 171 |
+ select {
|
|
| 172 |
+ case <-done: |
|
| 173 |
+ t.Log("event channel closed")
|
|
| 174 |
+ case <-time.After(2 * time.Second): |
|
| 175 |
+ t.Fatal("event stream was not closed after 2 seconds")
|
|
| 176 |
+ } |
|
| 177 |
+} |
|
| 178 |
+ |
|
| 179 |
+func TestFsnotifyMultipleCreates(t *testing.T) {
|
|
| 180 |
+ watcher := newWatcher(t) |
|
| 181 |
+ |
|
| 182 |
+ // Receive errors on the error channel on a separate goroutine |
|
| 183 |
+ go func() {
|
|
| 184 |
+ for err := range watcher.Errors {
|
|
| 185 |
+ t.Fatalf("error received: %s", err)
|
|
| 186 |
+ } |
|
| 187 |
+ }() |
|
| 188 |
+ |
|
| 189 |
+ // Create directory to watch |
|
| 190 |
+ testDir := tempMkdir(t) |
|
| 191 |
+ defer os.RemoveAll(testDir) |
|
| 192 |
+ |
|
| 193 |
+ testFile := filepath.Join(testDir, "TestFsnotifySeq.testfile") |
|
| 194 |
+ |
|
| 195 |
+ addWatch(t, watcher, testDir) |
|
| 196 |
+ |
|
| 197 |
+ // Receive events on the event channel on a separate goroutine |
|
| 198 |
+ eventstream := watcher.Events |
|
| 199 |
+ var createReceived, modifyReceived, deleteReceived counter |
|
| 200 |
+ done := make(chan bool) |
|
| 201 |
+ go func() {
|
|
| 202 |
+ for event := range eventstream {
|
|
| 203 |
+ // Only count relevant events |
|
| 204 |
+ if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) {
|
|
| 205 |
+ t.Logf("event received: %s", event)
|
|
| 206 |
+ if event.Op&Remove == Remove {
|
|
| 207 |
+ deleteReceived.increment() |
|
| 208 |
+ } |
|
| 209 |
+ if event.Op&Create == Create {
|
|
| 210 |
+ createReceived.increment() |
|
| 211 |
+ } |
|
| 212 |
+ if event.Op&Write == Write {
|
|
| 213 |
+ modifyReceived.increment() |
|
| 214 |
+ } |
|
| 215 |
+ } else {
|
|
| 216 |
+ t.Logf("unexpected event received: %s", event)
|
|
| 217 |
+ } |
|
| 218 |
+ } |
|
| 219 |
+ done <- true |
|
| 220 |
+ }() |
|
| 221 |
+ |
|
| 222 |
+ // Create a file |
|
| 223 |
+ // This should add at least one event to the fsnotify event queue |
|
| 224 |
+ var f *os.File |
|
| 225 |
+ f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 226 |
+ if err != nil {
|
|
| 227 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 228 |
+ } |
|
| 229 |
+ f.Sync() |
|
| 230 |
+ |
|
| 231 |
+ time.Sleep(time.Millisecond) |
|
| 232 |
+ f.WriteString("data")
|
|
| 233 |
+ f.Sync() |
|
| 234 |
+ f.Close() |
|
| 235 |
+ |
|
| 236 |
+ time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete |
|
| 237 |
+ |
|
| 238 |
+ os.Remove(testFile) |
|
| 239 |
+ |
|
| 240 |
+ time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete |
|
| 241 |
+ |
|
| 242 |
+ // Recreate the file |
|
| 243 |
+ f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 244 |
+ if err != nil {
|
|
| 245 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 246 |
+ } |
|
| 247 |
+ f.Close() |
|
| 248 |
+ time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete |
|
| 249 |
+ |
|
| 250 |
+ // Modify |
|
| 251 |
+ f, err = os.OpenFile(testFile, os.O_WRONLY, 0666) |
|
| 252 |
+ if err != nil {
|
|
| 253 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 254 |
+ } |
|
| 255 |
+ f.Sync() |
|
| 256 |
+ |
|
| 257 |
+ time.Sleep(time.Millisecond) |
|
| 258 |
+ f.WriteString("data")
|
|
| 259 |
+ f.Sync() |
|
| 260 |
+ f.Close() |
|
| 261 |
+ |
|
| 262 |
+ time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete |
|
| 263 |
+ |
|
| 264 |
+ // Modify |
|
| 265 |
+ f, err = os.OpenFile(testFile, os.O_WRONLY, 0666) |
|
| 266 |
+ if err != nil {
|
|
| 267 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 268 |
+ } |
|
| 269 |
+ f.Sync() |
|
| 270 |
+ |
|
| 271 |
+ time.Sleep(time.Millisecond) |
|
| 272 |
+ f.WriteString("data")
|
|
| 273 |
+ f.Sync() |
|
| 274 |
+ f.Close() |
|
| 275 |
+ |
|
| 276 |
+ time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete |
|
| 277 |
+ |
|
| 278 |
+ // We expect this event to be received almost immediately, but let's wait 500 ms to be sure |
|
| 279 |
+ time.Sleep(500 * time.Millisecond) |
|
| 280 |
+ cReceived := createReceived.value() |
|
| 281 |
+ if cReceived != 2 {
|
|
| 282 |
+ t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 2)
|
|
| 283 |
+ } |
|
| 284 |
+ mReceived := modifyReceived.value() |
|
| 285 |
+ if mReceived < 3 {
|
|
| 286 |
+ t.Fatalf("incorrect number of modify events received after 500 ms (%d vs atleast %d)", mReceived, 3)
|
|
| 287 |
+ } |
|
| 288 |
+ dReceived := deleteReceived.value() |
|
| 289 |
+ if dReceived != 1 {
|
|
| 290 |
+ t.Fatalf("incorrect number of rename+delete events received after 500 ms (%d vs %d)", dReceived, 1)
|
|
| 291 |
+ } |
|
| 292 |
+ |
|
| 293 |
+ // Try closing the fsnotify instance |
|
| 294 |
+ t.Log("calling Close()")
|
|
| 295 |
+ watcher.Close() |
|
| 296 |
+ t.Log("waiting for the event channel to become closed...")
|
|
| 297 |
+ select {
|
|
| 298 |
+ case <-done: |
|
| 299 |
+ t.Log("event channel closed")
|
|
| 300 |
+ case <-time.After(2 * time.Second): |
|
| 301 |
+ t.Fatal("event stream was not closed after 2 seconds")
|
|
| 302 |
+ } |
|
| 303 |
+} |
|
| 304 |
+ |
|
| 305 |
+func TestFsnotifyDirOnly(t *testing.T) {
|
|
| 306 |
+ watcher := newWatcher(t) |
|
| 307 |
+ |
|
| 308 |
+ // Create directory to watch |
|
| 309 |
+ testDir := tempMkdir(t) |
|
| 310 |
+ defer os.RemoveAll(testDir) |
|
| 311 |
+ |
|
| 312 |
+ // Create a file before watching directory |
|
| 313 |
+ // This should NOT add any events to the fsnotify event queue |
|
| 314 |
+ testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") |
|
| 315 |
+ {
|
|
| 316 |
+ var f *os.File |
|
| 317 |
+ f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 318 |
+ if err != nil {
|
|
| 319 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 320 |
+ } |
|
| 321 |
+ f.Sync() |
|
| 322 |
+ f.Close() |
|
| 323 |
+ } |
|
| 324 |
+ |
|
| 325 |
+ addWatch(t, watcher, testDir) |
|
| 326 |
+ |
|
| 327 |
+ // Receive errors on the error channel on a separate goroutine |
|
| 328 |
+ go func() {
|
|
| 329 |
+ for err := range watcher.Errors {
|
|
| 330 |
+ t.Fatalf("error received: %s", err)
|
|
| 331 |
+ } |
|
| 332 |
+ }() |
|
| 333 |
+ |
|
| 334 |
+ testFile := filepath.Join(testDir, "TestFsnotifyDirOnly.testfile") |
|
| 335 |
+ |
|
| 336 |
+ // Receive events on the event channel on a separate goroutine |
|
| 337 |
+ eventstream := watcher.Events |
|
| 338 |
+ var createReceived, modifyReceived, deleteReceived counter |
|
| 339 |
+ done := make(chan bool) |
|
| 340 |
+ go func() {
|
|
| 341 |
+ for event := range eventstream {
|
|
| 342 |
+ // Only count relevant events |
|
| 343 |
+ if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) || event.Name == filepath.Clean(testFileAlreadyExists) {
|
|
| 344 |
+ t.Logf("event received: %s", event)
|
|
| 345 |
+ if event.Op&Remove == Remove {
|
|
| 346 |
+ deleteReceived.increment() |
|
| 347 |
+ } |
|
| 348 |
+ if event.Op&Write == Write {
|
|
| 349 |
+ modifyReceived.increment() |
|
| 350 |
+ } |
|
| 351 |
+ if event.Op&Create == Create {
|
|
| 352 |
+ createReceived.increment() |
|
| 353 |
+ } |
|
| 354 |
+ } else {
|
|
| 355 |
+ t.Logf("unexpected event received: %s", event)
|
|
| 356 |
+ } |
|
| 357 |
+ } |
|
| 358 |
+ done <- true |
|
| 359 |
+ }() |
|
| 360 |
+ |
|
| 361 |
+ // Create a file |
|
| 362 |
+ // This should add at least one event to the fsnotify event queue |
|
| 363 |
+ var f *os.File |
|
| 364 |
+ f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 365 |
+ if err != nil {
|
|
| 366 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 367 |
+ } |
|
| 368 |
+ f.Sync() |
|
| 369 |
+ |
|
| 370 |
+ time.Sleep(time.Millisecond) |
|
| 371 |
+ f.WriteString("data")
|
|
| 372 |
+ f.Sync() |
|
| 373 |
+ f.Close() |
|
| 374 |
+ |
|
| 375 |
+ time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete |
|
| 376 |
+ |
|
| 377 |
+ os.Remove(testFile) |
|
| 378 |
+ os.Remove(testFileAlreadyExists) |
|
| 379 |
+ |
|
| 380 |
+ // We expect this event to be received almost immediately, but let's wait 500 ms to be sure |
|
| 381 |
+ time.Sleep(500 * time.Millisecond) |
|
| 382 |
+ cReceived := createReceived.value() |
|
| 383 |
+ if cReceived != 1 {
|
|
| 384 |
+ t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 1)
|
|
| 385 |
+ } |
|
| 386 |
+ mReceived := modifyReceived.value() |
|
| 387 |
+ if mReceived != 1 {
|
|
| 388 |
+ t.Fatalf("incorrect number of modify events received after 500 ms (%d vs %d)", mReceived, 1)
|
|
| 389 |
+ } |
|
| 390 |
+ dReceived := deleteReceived.value() |
|
| 391 |
+ if dReceived != 2 {
|
|
| 392 |
+ t.Fatalf("incorrect number of delete events received after 500 ms (%d vs %d)", dReceived, 2)
|
|
| 393 |
+ } |
|
| 394 |
+ |
|
| 395 |
+ // Try closing the fsnotify instance |
|
| 396 |
+ t.Log("calling Close()")
|
|
| 397 |
+ watcher.Close() |
|
| 398 |
+ t.Log("waiting for the event channel to become closed...")
|
|
| 399 |
+ select {
|
|
| 400 |
+ case <-done: |
|
| 401 |
+ t.Log("event channel closed")
|
|
| 402 |
+ case <-time.After(2 * time.Second): |
|
| 403 |
+ t.Fatal("event stream was not closed after 2 seconds")
|
|
| 404 |
+ } |
|
| 405 |
+} |
|
| 406 |
+ |
|
| 407 |
+func TestFsnotifyDeleteWatchedDir(t *testing.T) {
|
|
| 408 |
+ watcher := newWatcher(t) |
|
| 409 |
+ defer watcher.Close() |
|
| 410 |
+ |
|
| 411 |
+ // Create directory to watch |
|
| 412 |
+ testDir := tempMkdir(t) |
|
| 413 |
+ defer os.RemoveAll(testDir) |
|
| 414 |
+ |
|
| 415 |
+ // Create a file before watching directory |
|
| 416 |
+ testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") |
|
| 417 |
+ {
|
|
| 418 |
+ var f *os.File |
|
| 419 |
+ f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 420 |
+ if err != nil {
|
|
| 421 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 422 |
+ } |
|
| 423 |
+ f.Sync() |
|
| 424 |
+ f.Close() |
|
| 425 |
+ } |
|
| 426 |
+ |
|
| 427 |
+ addWatch(t, watcher, testDir) |
|
| 428 |
+ |
|
| 429 |
+ // Add a watch for testFile |
|
| 430 |
+ addWatch(t, watcher, testFileAlreadyExists) |
|
| 431 |
+ |
|
| 432 |
+ // Receive errors on the error channel on a separate goroutine |
|
| 433 |
+ go func() {
|
|
| 434 |
+ for err := range watcher.Errors {
|
|
| 435 |
+ t.Fatalf("error received: %s", err)
|
|
| 436 |
+ } |
|
| 437 |
+ }() |
|
| 438 |
+ |
|
| 439 |
+ // Receive events on the event channel on a separate goroutine |
|
| 440 |
+ eventstream := watcher.Events |
|
| 441 |
+ var deleteReceived counter |
|
| 442 |
+ go func() {
|
|
| 443 |
+ for event := range eventstream {
|
|
| 444 |
+ // Only count relevant events |
|
| 445 |
+ if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFileAlreadyExists) {
|
|
| 446 |
+ t.Logf("event received: %s", event)
|
|
| 447 |
+ if event.Op&Remove == Remove {
|
|
| 448 |
+ deleteReceived.increment() |
|
| 449 |
+ } |
|
| 450 |
+ } else {
|
|
| 451 |
+ t.Logf("unexpected event received: %s", event)
|
|
| 452 |
+ } |
|
| 453 |
+ } |
|
| 454 |
+ }() |
|
| 455 |
+ |
|
| 456 |
+ os.RemoveAll(testDir) |
|
| 457 |
+ |
|
| 458 |
+ // We expect this event to be received almost immediately, but let's wait 500 ms to be sure |
|
| 459 |
+ time.Sleep(500 * time.Millisecond) |
|
| 460 |
+ dReceived := deleteReceived.value() |
|
| 461 |
+ if dReceived < 2 {
|
|
| 462 |
+ t.Fatalf("did not receive at least %d delete events, received %d after 500 ms", 2, dReceived)
|
|
| 463 |
+ } |
|
| 464 |
+} |
|
| 465 |
+ |
|
| 466 |
+func TestFsnotifySubDir(t *testing.T) {
|
|
| 467 |
+ watcher := newWatcher(t) |
|
| 468 |
+ |
|
| 469 |
+ // Create directory to watch |
|
| 470 |
+ testDir := tempMkdir(t) |
|
| 471 |
+ defer os.RemoveAll(testDir) |
|
| 472 |
+ |
|
| 473 |
+ testFile1 := filepath.Join(testDir, "TestFsnotifyFile1.testfile") |
|
| 474 |
+ testSubDir := filepath.Join(testDir, "sub") |
|
| 475 |
+ testSubDirFile := filepath.Join(testDir, "sub/TestFsnotifyFile1.testfile") |
|
| 476 |
+ |
|
| 477 |
+ // Receive errors on the error channel on a separate goroutine |
|
| 478 |
+ go func() {
|
|
| 479 |
+ for err := range watcher.Errors {
|
|
| 480 |
+ t.Fatalf("error received: %s", err)
|
|
| 481 |
+ } |
|
| 482 |
+ }() |
|
| 483 |
+ |
|
| 484 |
+ // Receive events on the event channel on a separate goroutine |
|
| 485 |
+ eventstream := watcher.Events |
|
| 486 |
+ var createReceived, deleteReceived counter |
|
| 487 |
+ done := make(chan bool) |
|
| 488 |
+ go func() {
|
|
| 489 |
+ for event := range eventstream {
|
|
| 490 |
+ // Only count relevant events |
|
| 491 |
+ if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testSubDir) || event.Name == filepath.Clean(testFile1) {
|
|
| 492 |
+ t.Logf("event received: %s", event)
|
|
| 493 |
+ if event.Op&Create == Create {
|
|
| 494 |
+ createReceived.increment() |
|
| 495 |
+ } |
|
| 496 |
+ if event.Op&Remove == Remove {
|
|
| 497 |
+ deleteReceived.increment() |
|
| 498 |
+ } |
|
| 499 |
+ } else {
|
|
| 500 |
+ t.Logf("unexpected event received: %s", event)
|
|
| 501 |
+ } |
|
| 502 |
+ } |
|
| 503 |
+ done <- true |
|
| 504 |
+ }() |
|
| 505 |
+ |
|
| 506 |
+ addWatch(t, watcher, testDir) |
|
| 507 |
+ |
|
| 508 |
+ // Create sub-directory |
|
| 509 |
+ if err := os.Mkdir(testSubDir, 0777); err != nil {
|
|
| 510 |
+ t.Fatalf("failed to create test sub-directory: %s", err)
|
|
| 511 |
+ } |
|
| 512 |
+ |
|
| 513 |
+ // Create a file |
|
| 514 |
+ var f *os.File |
|
| 515 |
+ f, err := os.OpenFile(testFile1, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 516 |
+ if err != nil {
|
|
| 517 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 518 |
+ } |
|
| 519 |
+ f.Sync() |
|
| 520 |
+ f.Close() |
|
| 521 |
+ |
|
| 522 |
+ // Create a file (Should not see this! we are not watching subdir) |
|
| 523 |
+ var fs *os.File |
|
| 524 |
+ fs, err = os.OpenFile(testSubDirFile, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 525 |
+ if err != nil {
|
|
| 526 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 527 |
+ } |
|
| 528 |
+ fs.Sync() |
|
| 529 |
+ fs.Close() |
|
| 530 |
+ |
|
| 531 |
+ time.Sleep(200 * time.Millisecond) |
|
| 532 |
+ |
|
| 533 |
+ // Make sure receive deletes for both file and sub-directory |
|
| 534 |
+ os.RemoveAll(testSubDir) |
|
| 535 |
+ os.Remove(testFile1) |
|
| 536 |
+ |
|
| 537 |
+ // We expect this event to be received almost immediately, but let's wait 500 ms to be sure |
|
| 538 |
+ time.Sleep(500 * time.Millisecond) |
|
| 539 |
+ cReceived := createReceived.value() |
|
| 540 |
+ if cReceived != 2 {
|
|
| 541 |
+ t.Fatalf("incorrect number of create events received after 500 ms (%d vs %d)", cReceived, 2)
|
|
| 542 |
+ } |
|
| 543 |
+ dReceived := deleteReceived.value() |
|
| 544 |
+ if dReceived != 2 {
|
|
| 545 |
+ t.Fatalf("incorrect number of delete events received after 500 ms (%d vs %d)", dReceived, 2)
|
|
| 546 |
+ } |
|
| 547 |
+ |
|
| 548 |
+ // Try closing the fsnotify instance |
|
| 549 |
+ t.Log("calling Close()")
|
|
| 550 |
+ watcher.Close() |
|
| 551 |
+ t.Log("waiting for the event channel to become closed...")
|
|
| 552 |
+ select {
|
|
| 553 |
+ case <-done: |
|
| 554 |
+ t.Log("event channel closed")
|
|
| 555 |
+ case <-time.After(2 * time.Second): |
|
| 556 |
+ t.Fatal("event stream was not closed after 2 seconds")
|
|
| 557 |
+ } |
|
| 558 |
+} |
|
| 559 |
+ |
|
| 560 |
+func TestFsnotifyRename(t *testing.T) {
|
|
| 561 |
+ watcher := newWatcher(t) |
|
| 562 |
+ |
|
| 563 |
+ // Create directory to watch |
|
| 564 |
+ testDir := tempMkdir(t) |
|
| 565 |
+ defer os.RemoveAll(testDir) |
|
| 566 |
+ |
|
| 567 |
+ addWatch(t, watcher, testDir) |
|
| 568 |
+ |
|
| 569 |
+ // Receive errors on the error channel on a separate goroutine |
|
| 570 |
+ go func() {
|
|
| 571 |
+ for err := range watcher.Errors {
|
|
| 572 |
+ t.Fatalf("error received: %s", err)
|
|
| 573 |
+ } |
|
| 574 |
+ }() |
|
| 575 |
+ |
|
| 576 |
+ testFile := filepath.Join(testDir, "TestFsnotifyEvents.testfile") |
|
| 577 |
+ testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") |
|
| 578 |
+ |
|
| 579 |
+ // Receive events on the event channel on a separate goroutine |
|
| 580 |
+ eventstream := watcher.Events |
|
| 581 |
+ var renameReceived counter |
|
| 582 |
+ done := make(chan bool) |
|
| 583 |
+ go func() {
|
|
| 584 |
+ for event := range eventstream {
|
|
| 585 |
+ // Only count relevant events |
|
| 586 |
+ if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) || event.Name == filepath.Clean(testFileRenamed) {
|
|
| 587 |
+ if event.Op&Rename == Rename {
|
|
| 588 |
+ renameReceived.increment() |
|
| 589 |
+ } |
|
| 590 |
+ t.Logf("event received: %s", event)
|
|
| 591 |
+ } else {
|
|
| 592 |
+ t.Logf("unexpected event received: %s", event)
|
|
| 593 |
+ } |
|
| 594 |
+ } |
|
| 595 |
+ done <- true |
|
| 596 |
+ }() |
|
| 597 |
+ |
|
| 598 |
+ // Create a file |
|
| 599 |
+ // This should add at least one event to the fsnotify event queue |
|
| 600 |
+ var f *os.File |
|
| 601 |
+ f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 602 |
+ if err != nil {
|
|
| 603 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 604 |
+ } |
|
| 605 |
+ f.Sync() |
|
| 606 |
+ |
|
| 607 |
+ f.WriteString("data")
|
|
| 608 |
+ f.Sync() |
|
| 609 |
+ f.Close() |
|
| 610 |
+ |
|
| 611 |
+ // Add a watch for testFile |
|
| 612 |
+ addWatch(t, watcher, testFile) |
|
| 613 |
+ |
|
| 614 |
+ if err := testRename(testFile, testFileRenamed); err != nil {
|
|
| 615 |
+ t.Fatalf("rename failed: %s", err)
|
|
| 616 |
+ } |
|
| 617 |
+ |
|
| 618 |
+ // We expect this event to be received almost immediately, but let's wait 500 ms to be sure |
|
| 619 |
+ time.Sleep(500 * time.Millisecond) |
|
| 620 |
+ if renameReceived.value() == 0 {
|
|
| 621 |
+ t.Fatal("fsnotify rename events have not been received after 500 ms")
|
|
| 622 |
+ } |
|
| 623 |
+ |
|
| 624 |
+ // Try closing the fsnotify instance |
|
| 625 |
+ t.Log("calling Close()")
|
|
| 626 |
+ watcher.Close() |
|
| 627 |
+ t.Log("waiting for the event channel to become closed...")
|
|
| 628 |
+ select {
|
|
| 629 |
+ case <-done: |
|
| 630 |
+ t.Log("event channel closed")
|
|
| 631 |
+ case <-time.After(2 * time.Second): |
|
| 632 |
+ t.Fatal("event stream was not closed after 2 seconds")
|
|
| 633 |
+ } |
|
| 634 |
+ |
|
| 635 |
+ os.Remove(testFileRenamed) |
|
| 636 |
+} |
|
| 637 |
+ |
|
| 638 |
+func TestFsnotifyRenameToCreate(t *testing.T) {
|
|
| 639 |
+ watcher := newWatcher(t) |
|
| 640 |
+ |
|
| 641 |
+ // Create directory to watch |
|
| 642 |
+ testDir := tempMkdir(t) |
|
| 643 |
+ defer os.RemoveAll(testDir) |
|
| 644 |
+ |
|
| 645 |
+ // Create directory to get file |
|
| 646 |
+ testDirFrom := tempMkdir(t) |
|
| 647 |
+ defer os.RemoveAll(testDirFrom) |
|
| 648 |
+ |
|
| 649 |
+ addWatch(t, watcher, testDir) |
|
| 650 |
+ |
|
| 651 |
+ // Receive errors on the error channel on a separate goroutine |
|
| 652 |
+ go func() {
|
|
| 653 |
+ for err := range watcher.Errors {
|
|
| 654 |
+ t.Fatalf("error received: %s", err)
|
|
| 655 |
+ } |
|
| 656 |
+ }() |
|
| 657 |
+ |
|
| 658 |
+ testFile := filepath.Join(testDirFrom, "TestFsnotifyEvents.testfile") |
|
| 659 |
+ testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") |
|
| 660 |
+ |
|
| 661 |
+ // Receive events on the event channel on a separate goroutine |
|
| 662 |
+ eventstream := watcher.Events |
|
| 663 |
+ var createReceived counter |
|
| 664 |
+ done := make(chan bool) |
|
| 665 |
+ go func() {
|
|
| 666 |
+ for event := range eventstream {
|
|
| 667 |
+ // Only count relevant events |
|
| 668 |
+ if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) || event.Name == filepath.Clean(testFileRenamed) {
|
|
| 669 |
+ if event.Op&Create == Create {
|
|
| 670 |
+ createReceived.increment() |
|
| 671 |
+ } |
|
| 672 |
+ t.Logf("event received: %s", event)
|
|
| 673 |
+ } else {
|
|
| 674 |
+ t.Logf("unexpected event received: %s", event)
|
|
| 675 |
+ } |
|
| 676 |
+ } |
|
| 677 |
+ done <- true |
|
| 678 |
+ }() |
|
| 679 |
+ |
|
| 680 |
+ // Create a file |
|
| 681 |
+ // This should add at least one event to the fsnotify event queue |
|
| 682 |
+ var f *os.File |
|
| 683 |
+ f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 684 |
+ if err != nil {
|
|
| 685 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 686 |
+ } |
|
| 687 |
+ f.Sync() |
|
| 688 |
+ f.Close() |
|
| 689 |
+ |
|
| 690 |
+ if err := testRename(testFile, testFileRenamed); err != nil {
|
|
| 691 |
+ t.Fatalf("rename failed: %s", err)
|
|
| 692 |
+ } |
|
| 693 |
+ |
|
| 694 |
+ // We expect this event to be received almost immediately, but let's wait 500 ms to be sure |
|
| 695 |
+ time.Sleep(500 * time.Millisecond) |
|
| 696 |
+ if createReceived.value() == 0 {
|
|
| 697 |
+ t.Fatal("fsnotify create events have not been received after 500 ms")
|
|
| 698 |
+ } |
|
| 699 |
+ |
|
| 700 |
+ // Try closing the fsnotify instance |
|
| 701 |
+ t.Log("calling Close()")
|
|
| 702 |
+ watcher.Close() |
|
| 703 |
+ t.Log("waiting for the event channel to become closed...")
|
|
| 704 |
+ select {
|
|
| 705 |
+ case <-done: |
|
| 706 |
+ t.Log("event channel closed")
|
|
| 707 |
+ case <-time.After(2 * time.Second): |
|
| 708 |
+ t.Fatal("event stream was not closed after 2 seconds")
|
|
| 709 |
+ } |
|
| 710 |
+ |
|
| 711 |
+ os.Remove(testFileRenamed) |
|
| 712 |
+} |
|
| 713 |
+ |
|
| 714 |
+func TestFsnotifyRenameToOverwrite(t *testing.T) {
|
|
| 715 |
+ switch runtime.GOOS {
|
|
| 716 |
+ case "plan9", "windows": |
|
| 717 |
+ t.Skipf("skipping test on %q (os.Rename over existing file does not create event).", runtime.GOOS)
|
|
| 718 |
+ } |
|
| 719 |
+ |
|
| 720 |
+ watcher := newWatcher(t) |
|
| 721 |
+ |
|
| 722 |
+ // Create directory to watch |
|
| 723 |
+ testDir := tempMkdir(t) |
|
| 724 |
+ defer os.RemoveAll(testDir) |
|
| 725 |
+ |
|
| 726 |
+ // Create directory to get file |
|
| 727 |
+ testDirFrom := tempMkdir(t) |
|
| 728 |
+ defer os.RemoveAll(testDirFrom) |
|
| 729 |
+ |
|
| 730 |
+ testFile := filepath.Join(testDirFrom, "TestFsnotifyEvents.testfile") |
|
| 731 |
+ testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") |
|
| 732 |
+ |
|
| 733 |
+ // Create a file |
|
| 734 |
+ var fr *os.File |
|
| 735 |
+ fr, err := os.OpenFile(testFileRenamed, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 736 |
+ if err != nil {
|
|
| 737 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 738 |
+ } |
|
| 739 |
+ fr.Sync() |
|
| 740 |
+ fr.Close() |
|
| 741 |
+ |
|
| 742 |
+ addWatch(t, watcher, testDir) |
|
| 743 |
+ |
|
| 744 |
+ // Receive errors on the error channel on a separate goroutine |
|
| 745 |
+ go func() {
|
|
| 746 |
+ for err := range watcher.Errors {
|
|
| 747 |
+ t.Fatalf("error received: %s", err)
|
|
| 748 |
+ } |
|
| 749 |
+ }() |
|
| 750 |
+ |
|
| 751 |
+ // Receive events on the event channel on a separate goroutine |
|
| 752 |
+ eventstream := watcher.Events |
|
| 753 |
+ var eventReceived counter |
|
| 754 |
+ done := make(chan bool) |
|
| 755 |
+ go func() {
|
|
| 756 |
+ for event := range eventstream {
|
|
| 757 |
+ // Only count relevant events |
|
| 758 |
+ if event.Name == filepath.Clean(testFileRenamed) {
|
|
| 759 |
+ eventReceived.increment() |
|
| 760 |
+ t.Logf("event received: %s", event)
|
|
| 761 |
+ } else {
|
|
| 762 |
+ t.Logf("unexpected event received: %s", event)
|
|
| 763 |
+ } |
|
| 764 |
+ } |
|
| 765 |
+ done <- true |
|
| 766 |
+ }() |
|
| 767 |
+ |
|
| 768 |
+ // Create a file |
|
| 769 |
+ // This should add at least one event to the fsnotify event queue |
|
| 770 |
+ var f *os.File |
|
| 771 |
+ f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 772 |
+ if err != nil {
|
|
| 773 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 774 |
+ } |
|
| 775 |
+ f.Sync() |
|
| 776 |
+ f.Close() |
|
| 777 |
+ |
|
| 778 |
+ if err := testRename(testFile, testFileRenamed); err != nil {
|
|
| 779 |
+ t.Fatalf("rename failed: %s", err)
|
|
| 780 |
+ } |
|
| 781 |
+ |
|
| 782 |
+ // We expect this event to be received almost immediately, but let's wait 500 ms to be sure |
|
| 783 |
+ time.Sleep(500 * time.Millisecond) |
|
| 784 |
+ if eventReceived.value() == 0 {
|
|
| 785 |
+ t.Fatal("fsnotify events have not been received after 500 ms")
|
|
| 786 |
+ } |
|
| 787 |
+ |
|
| 788 |
+ // Try closing the fsnotify instance |
|
| 789 |
+ t.Log("calling Close()")
|
|
| 790 |
+ watcher.Close() |
|
| 791 |
+ t.Log("waiting for the event channel to become closed...")
|
|
| 792 |
+ select {
|
|
| 793 |
+ case <-done: |
|
| 794 |
+ t.Log("event channel closed")
|
|
| 795 |
+ case <-time.After(2 * time.Second): |
|
| 796 |
+ t.Fatal("event stream was not closed after 2 seconds")
|
|
| 797 |
+ } |
|
| 798 |
+ |
|
| 799 |
+ os.Remove(testFileRenamed) |
|
| 800 |
+} |
|
| 801 |
+ |
|
| 802 |
+func TestRemovalOfWatch(t *testing.T) {
|
|
| 803 |
+ // Create directory to watch |
|
| 804 |
+ testDir := tempMkdir(t) |
|
| 805 |
+ defer os.RemoveAll(testDir) |
|
| 806 |
+ |
|
| 807 |
+ // Create a file before watching directory |
|
| 808 |
+ testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") |
|
| 809 |
+ {
|
|
| 810 |
+ var f *os.File |
|
| 811 |
+ f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 812 |
+ if err != nil {
|
|
| 813 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 814 |
+ } |
|
| 815 |
+ f.Sync() |
|
| 816 |
+ f.Close() |
|
| 817 |
+ } |
|
| 818 |
+ |
|
| 819 |
+ watcher := newWatcher(t) |
|
| 820 |
+ defer watcher.Close() |
|
| 821 |
+ |
|
| 822 |
+ addWatch(t, watcher, testDir) |
|
| 823 |
+ if err := watcher.Remove(testDir); err != nil {
|
|
| 824 |
+ t.Fatalf("Could not remove the watch: %v\n", err)
|
|
| 825 |
+ } |
|
| 826 |
+ |
|
| 827 |
+ go func() {
|
|
| 828 |
+ select {
|
|
| 829 |
+ case ev := <-watcher.Events: |
|
| 830 |
+ t.Fatalf("We received event: %v\n", ev)
|
|
| 831 |
+ case <-time.After(500 * time.Millisecond): |
|
| 832 |
+ t.Log("No event received, as expected.")
|
|
| 833 |
+ } |
|
| 834 |
+ }() |
|
| 835 |
+ |
|
| 836 |
+ time.Sleep(200 * time.Millisecond) |
|
| 837 |
+ // Modify the file outside of the watched dir |
|
| 838 |
+ f, err := os.Open(testFileAlreadyExists) |
|
| 839 |
+ if err != nil {
|
|
| 840 |
+ t.Fatalf("Open test file failed: %s", err)
|
|
| 841 |
+ } |
|
| 842 |
+ f.WriteString("data")
|
|
| 843 |
+ f.Sync() |
|
| 844 |
+ f.Close() |
|
| 845 |
+ if err := os.Chmod(testFileAlreadyExists, 0700); err != nil {
|
|
| 846 |
+ t.Fatalf("chmod failed: %s", err)
|
|
| 847 |
+ } |
|
| 848 |
+ time.Sleep(400 * time.Millisecond) |
|
| 849 |
+} |
|
| 850 |
+ |
|
| 851 |
+func TestFsnotifyAttrib(t *testing.T) {
|
|
| 852 |
+ if runtime.GOOS == "windows" {
|
|
| 853 |
+ t.Skip("attributes don't work on Windows.")
|
|
| 854 |
+ } |
|
| 855 |
+ |
|
| 856 |
+ watcher := newWatcher(t) |
|
| 857 |
+ |
|
| 858 |
+ // Create directory to watch |
|
| 859 |
+ testDir := tempMkdir(t) |
|
| 860 |
+ defer os.RemoveAll(testDir) |
|
| 861 |
+ |
|
| 862 |
+ // Receive errors on the error channel on a separate goroutine |
|
| 863 |
+ go func() {
|
|
| 864 |
+ for err := range watcher.Errors {
|
|
| 865 |
+ t.Fatalf("error received: %s", err)
|
|
| 866 |
+ } |
|
| 867 |
+ }() |
|
| 868 |
+ |
|
| 869 |
+ testFile := filepath.Join(testDir, "TestFsnotifyAttrib.testfile") |
|
| 870 |
+ |
|
| 871 |
+ // Receive events on the event channel on a separate goroutine |
|
| 872 |
+ eventstream := watcher.Events |
|
| 873 |
+ // The modifyReceived counter counts IsModify events that are not IsAttrib, |
|
| 874 |
+ // and the attribReceived counts IsAttrib events (which are also IsModify as |
|
| 875 |
+ // a consequence). |
|
| 876 |
+ var modifyReceived counter |
|
| 877 |
+ var attribReceived counter |
|
| 878 |
+ done := make(chan bool) |
|
| 879 |
+ go func() {
|
|
| 880 |
+ for event := range eventstream {
|
|
| 881 |
+ // Only count relevant events |
|
| 882 |
+ if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) {
|
|
| 883 |
+ if event.Op&Write == Write {
|
|
| 884 |
+ modifyReceived.increment() |
|
| 885 |
+ } |
|
| 886 |
+ if event.Op&Chmod == Chmod {
|
|
| 887 |
+ attribReceived.increment() |
|
| 888 |
+ } |
|
| 889 |
+ t.Logf("event received: %s", event)
|
|
| 890 |
+ } else {
|
|
| 891 |
+ t.Logf("unexpected event received: %s", event)
|
|
| 892 |
+ } |
|
| 893 |
+ } |
|
| 894 |
+ done <- true |
|
| 895 |
+ }() |
|
| 896 |
+ |
|
| 897 |
+ // Create a file |
|
| 898 |
+ // This should add at least one event to the fsnotify event queue |
|
| 899 |
+ var f *os.File |
|
| 900 |
+ f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 901 |
+ if err != nil {
|
|
| 902 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 903 |
+ } |
|
| 904 |
+ f.Sync() |
|
| 905 |
+ |
|
| 906 |
+ f.WriteString("data")
|
|
| 907 |
+ f.Sync() |
|
| 908 |
+ f.Close() |
|
| 909 |
+ |
|
| 910 |
+ // Add a watch for testFile |
|
| 911 |
+ addWatch(t, watcher, testFile) |
|
| 912 |
+ |
|
| 913 |
+ if err := os.Chmod(testFile, 0700); err != nil {
|
|
| 914 |
+ t.Fatalf("chmod failed: %s", err)
|
|
| 915 |
+ } |
|
| 916 |
+ |
|
| 917 |
+ // We expect this event to be received almost immediately, but let's wait 500 ms to be sure |
|
| 918 |
+ // Creating/writing a file changes also the mtime, so IsAttrib should be set to true here |
|
| 919 |
+ time.Sleep(500 * time.Millisecond) |
|
| 920 |
+ if modifyReceived.value() != 0 {
|
|
| 921 |
+ t.Fatal("received an unexpected modify event when creating a test file")
|
|
| 922 |
+ } |
|
| 923 |
+ if attribReceived.value() == 0 {
|
|
| 924 |
+ t.Fatal("fsnotify attribute events have not received after 500 ms")
|
|
| 925 |
+ } |
|
| 926 |
+ |
|
| 927 |
+ // Modifying the contents of the file does not set the attrib flag (although eg. the mtime |
|
| 928 |
+ // might have been modified). |
|
| 929 |
+ modifyReceived.reset() |
|
| 930 |
+ attribReceived.reset() |
|
| 931 |
+ |
|
| 932 |
+ f, err = os.OpenFile(testFile, os.O_WRONLY, 0) |
|
| 933 |
+ if err != nil {
|
|
| 934 |
+ t.Fatalf("reopening test file failed: %s", err)
|
|
| 935 |
+ } |
|
| 936 |
+ |
|
| 937 |
+ f.WriteString("more data")
|
|
| 938 |
+ f.Sync() |
|
| 939 |
+ f.Close() |
|
| 940 |
+ |
|
| 941 |
+ time.Sleep(500 * time.Millisecond) |
|
| 942 |
+ |
|
| 943 |
+ if modifyReceived.value() != 1 {
|
|
| 944 |
+ t.Fatal("didn't receive a modify event after changing test file contents")
|
|
| 945 |
+ } |
|
| 946 |
+ |
|
| 947 |
+ if attribReceived.value() != 0 {
|
|
| 948 |
+ t.Fatal("did receive an unexpected attrib event after changing test file contents")
|
|
| 949 |
+ } |
|
| 950 |
+ |
|
| 951 |
+ modifyReceived.reset() |
|
| 952 |
+ attribReceived.reset() |
|
| 953 |
+ |
|
| 954 |
+ // Doing a chmod on the file should trigger an event with the "attrib" flag set (the contents |
|
| 955 |
+ // of the file are not changed though) |
|
| 956 |
+ if err := os.Chmod(testFile, 0600); err != nil {
|
|
| 957 |
+ t.Fatalf("chmod failed: %s", err)
|
|
| 958 |
+ } |
|
| 959 |
+ |
|
| 960 |
+ time.Sleep(500 * time.Millisecond) |
|
| 961 |
+ |
|
| 962 |
+ if attribReceived.value() != 1 {
|
|
| 963 |
+ t.Fatal("didn't receive an attribute change after 500ms")
|
|
| 964 |
+ } |
|
| 965 |
+ |
|
| 966 |
+ // Try closing the fsnotify instance |
|
| 967 |
+ t.Log("calling Close()")
|
|
| 968 |
+ watcher.Close() |
|
| 969 |
+ t.Log("waiting for the event channel to become closed...")
|
|
| 970 |
+ select {
|
|
| 971 |
+ case <-done: |
|
| 972 |
+ t.Log("event channel closed")
|
|
| 973 |
+ case <-time.After(1e9): |
|
| 974 |
+ t.Fatal("event stream was not closed after 1 second")
|
|
| 975 |
+ } |
|
| 976 |
+ |
|
| 977 |
+ os.Remove(testFile) |
|
| 978 |
+} |
|
| 979 |
+ |
|
| 980 |
+func TestFsnotifyClose(t *testing.T) {
|
|
| 981 |
+ watcher := newWatcher(t) |
|
| 982 |
+ watcher.Close() |
|
| 983 |
+ |
|
| 984 |
+ var done int32 |
|
| 985 |
+ go func() {
|
|
| 986 |
+ watcher.Close() |
|
| 987 |
+ atomic.StoreInt32(&done, 1) |
|
| 988 |
+ }() |
|
| 989 |
+ |
|
| 990 |
+ time.Sleep(50e6) // 50 ms |
|
| 991 |
+ if atomic.LoadInt32(&done) == 0 {
|
|
| 992 |
+ t.Fatal("double Close() test failed: second Close() call didn't return")
|
|
| 993 |
+ } |
|
| 994 |
+ |
|
| 995 |
+ testDir := tempMkdir(t) |
|
| 996 |
+ defer os.RemoveAll(testDir) |
|
| 997 |
+ |
|
| 998 |
+ if err := watcher.Add(testDir); err == nil {
|
|
| 999 |
+ t.Fatal("expected error on Watch() after Close(), got nil")
|
|
| 1000 |
+ } |
|
| 1001 |
+} |
|
| 1002 |
+ |
|
| 1003 |
+func TestFsnotifyFakeSymlink(t *testing.T) {
|
|
| 1004 |
+ if runtime.GOOS == "windows" {
|
|
| 1005 |
+ t.Skip("symlinks don't work on Windows.")
|
|
| 1006 |
+ } |
|
| 1007 |
+ |
|
| 1008 |
+ watcher := newWatcher(t) |
|
| 1009 |
+ |
|
| 1010 |
+ // Create directory to watch |
|
| 1011 |
+ testDir := tempMkdir(t) |
|
| 1012 |
+ defer os.RemoveAll(testDir) |
|
| 1013 |
+ |
|
| 1014 |
+ var errorsReceived counter |
|
| 1015 |
+ // Receive errors on the error channel on a separate goroutine |
|
| 1016 |
+ go func() {
|
|
| 1017 |
+ for errors := range watcher.Errors {
|
|
| 1018 |
+ t.Logf("Received error: %s", errors)
|
|
| 1019 |
+ errorsReceived.increment() |
|
| 1020 |
+ } |
|
| 1021 |
+ }() |
|
| 1022 |
+ |
|
| 1023 |
+ // Count the CREATE events received |
|
| 1024 |
+ var createEventsReceived, otherEventsReceived counter |
|
| 1025 |
+ go func() {
|
|
| 1026 |
+ for ev := range watcher.Events {
|
|
| 1027 |
+ t.Logf("event received: %s", ev)
|
|
| 1028 |
+ if ev.Op&Create == Create {
|
|
| 1029 |
+ createEventsReceived.increment() |
|
| 1030 |
+ } else {
|
|
| 1031 |
+ otherEventsReceived.increment() |
|
| 1032 |
+ } |
|
| 1033 |
+ } |
|
| 1034 |
+ }() |
|
| 1035 |
+ |
|
| 1036 |
+ addWatch(t, watcher, testDir) |
|
| 1037 |
+ |
|
| 1038 |
+ if err := os.Symlink(filepath.Join(testDir, "zzz"), filepath.Join(testDir, "zzznew")); err != nil {
|
|
| 1039 |
+ t.Fatalf("Failed to create bogus symlink: %s", err)
|
|
| 1040 |
+ } |
|
| 1041 |
+ t.Logf("Created bogus symlink")
|
|
| 1042 |
+ |
|
| 1043 |
+ // We expect this event to be received almost immediately, but let's wait 500 ms to be sure |
|
| 1044 |
+ time.Sleep(500 * time.Millisecond) |
|
| 1045 |
+ |
|
| 1046 |
+ // Should not be error, just no events for broken links (watching nothing) |
|
| 1047 |
+ if errorsReceived.value() > 0 {
|
|
| 1048 |
+ t.Fatal("fsnotify errors have been received.")
|
|
| 1049 |
+ } |
|
| 1050 |
+ if otherEventsReceived.value() > 0 {
|
|
| 1051 |
+ t.Fatal("fsnotify other events received on the broken link")
|
|
| 1052 |
+ } |
|
| 1053 |
+ |
|
| 1054 |
+ // Except for 1 create event (for the link itself) |
|
| 1055 |
+ if createEventsReceived.value() == 0 {
|
|
| 1056 |
+ t.Fatal("fsnotify create events were not received after 500 ms")
|
|
| 1057 |
+ } |
|
| 1058 |
+ if createEventsReceived.value() > 1 {
|
|
| 1059 |
+ t.Fatal("fsnotify more create events received than expected")
|
|
| 1060 |
+ } |
|
| 1061 |
+ |
|
| 1062 |
+ // Try closing the fsnotify instance |
|
| 1063 |
+ t.Log("calling Close()")
|
|
| 1064 |
+ watcher.Close() |
|
| 1065 |
+} |
|
| 1066 |
+ |
|
| 1067 |
+// TestConcurrentRemovalOfWatch tests that concurrent calls to RemoveWatch do not race. |
|
| 1068 |
+// See https://codereview.appspot.com/103300045/ |
|
| 1069 |
+// go test -test.run=TestConcurrentRemovalOfWatch -test.cpu=1,1,1,1,1 -race |
|
| 1070 |
+func TestConcurrentRemovalOfWatch(t *testing.T) {
|
|
| 1071 |
+ if runtime.GOOS != "darwin" {
|
|
| 1072 |
+ t.Skip("regression test for race only present on darwin")
|
|
| 1073 |
+ } |
|
| 1074 |
+ |
|
| 1075 |
+ // Create directory to watch |
|
| 1076 |
+ testDir := tempMkdir(t) |
|
| 1077 |
+ defer os.RemoveAll(testDir) |
|
| 1078 |
+ |
|
| 1079 |
+ // Create a file before watching directory |
|
| 1080 |
+ testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") |
|
| 1081 |
+ {
|
|
| 1082 |
+ var f *os.File |
|
| 1083 |
+ f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) |
|
| 1084 |
+ if err != nil {
|
|
| 1085 |
+ t.Fatalf("creating test file failed: %s", err)
|
|
| 1086 |
+ } |
|
| 1087 |
+ f.Sync() |
|
| 1088 |
+ f.Close() |
|
| 1089 |
+ } |
|
| 1090 |
+ |
|
| 1091 |
+ watcher := newWatcher(t) |
|
| 1092 |
+ defer watcher.Close() |
|
| 1093 |
+ |
|
| 1094 |
+ addWatch(t, watcher, testDir) |
|
| 1095 |
+ |
|
| 1096 |
+ // Test that RemoveWatch can be invoked concurrently, with no data races. |
|
| 1097 |
+ removed1 := make(chan struct{})
|
|
| 1098 |
+ go func() {
|
|
| 1099 |
+ defer close(removed1) |
|
| 1100 |
+ watcher.Remove(testDir) |
|
| 1101 |
+ }() |
|
| 1102 |
+ removed2 := make(chan struct{})
|
|
| 1103 |
+ go func() {
|
|
| 1104 |
+ close(removed2) |
|
| 1105 |
+ watcher.Remove(testDir) |
|
| 1106 |
+ }() |
|
| 1107 |
+ <-removed1 |
|
| 1108 |
+ <-removed2 |
|
| 1109 |
+} |
|
| 1110 |
+ |
|
| 1111 |
+func testRename(file1, file2 string) error {
|
|
| 1112 |
+ switch runtime.GOOS {
|
|
| 1113 |
+ case "windows", "plan9": |
|
| 1114 |
+ return os.Rename(file1, file2) |
|
| 1115 |
+ default: |
|
| 1116 |
+ cmd := exec.Command("mv", file1, file2)
|
|
| 1117 |
+ return cmd.Run() |
|
| 1118 |
+ } |
|
| 1119 |
+} |
| 0 | 1120 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,479 @@ |
| 0 |
+// Copyright 2010 The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE file. |
|
| 3 |
+ |
|
| 4 |
+// +build freebsd openbsd netbsd dragonfly darwin |
|
| 5 |
+ |
|
| 6 |
+package fsnotify |
|
| 7 |
+ |
|
| 8 |
+import ( |
|
| 9 |
+ "errors" |
|
| 10 |
+ "fmt" |
|
| 11 |
+ "io/ioutil" |
|
| 12 |
+ "os" |
|
| 13 |
+ "path/filepath" |
|
| 14 |
+ "sync" |
|
| 15 |
+ "syscall" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+// Watcher watches a set of files, delivering events to a channel. |
|
| 19 |
+type Watcher struct {
|
|
| 20 |
+ Events chan Event |
|
| 21 |
+ Errors chan error |
|
| 22 |
+ mu sync.Mutex // Mutex for the Watcher itself. |
|
| 23 |
+ kq int // File descriptor (as returned by the kqueue() syscall). |
|
| 24 |
+ watches map[string]int // Map of watched file descriptors (key: path). |
|
| 25 |
+ wmut sync.Mutex // Protects access to watches. |
|
| 26 |
+ enFlags map[string]uint32 // Map of watched files to evfilt note flags used in kqueue. |
|
| 27 |
+ enmut sync.Mutex // Protects access to enFlags. |
|
| 28 |
+ paths map[int]string // Map of watched paths (key: watch descriptor). |
|
| 29 |
+ finfo map[int]os.FileInfo // Map of file information (isDir, isReg; key: watch descriptor). |
|
| 30 |
+ pmut sync.Mutex // Protects access to paths and finfo. |
|
| 31 |
+ fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events). |
|
| 32 |
+ femut sync.Mutex // Protects access to fileExists. |
|
| 33 |
+ externalWatches map[string]bool // Map of watches added by user of the library. |
|
| 34 |
+ ewmut sync.Mutex // Protects access to externalWatches. |
|
| 35 |
+ done chan bool // Channel for sending a "quit message" to the reader goroutine |
|
| 36 |
+ isClosed bool // Set to true when Close() is first called |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. |
|
| 40 |
+func NewWatcher() (*Watcher, error) {
|
|
| 41 |
+ fd, errno := syscall.Kqueue() |
|
| 42 |
+ if fd == -1 {
|
|
| 43 |
+ return nil, os.NewSyscallError("kqueue", errno)
|
|
| 44 |
+ } |
|
| 45 |
+ w := &Watcher{
|
|
| 46 |
+ kq: fd, |
|
| 47 |
+ watches: make(map[string]int), |
|
| 48 |
+ enFlags: make(map[string]uint32), |
|
| 49 |
+ paths: make(map[int]string), |
|
| 50 |
+ finfo: make(map[int]os.FileInfo), |
|
| 51 |
+ fileExists: make(map[string]bool), |
|
| 52 |
+ externalWatches: make(map[string]bool), |
|
| 53 |
+ Events: make(chan Event), |
|
| 54 |
+ Errors: make(chan error), |
|
| 55 |
+ done: make(chan bool, 1), |
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ go w.readEvents() |
|
| 59 |
+ return w, nil |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+// Close removes all watches and closes the events channel. |
|
| 63 |
+func (w *Watcher) Close() error {
|
|
| 64 |
+ w.mu.Lock() |
|
| 65 |
+ if w.isClosed {
|
|
| 66 |
+ w.mu.Unlock() |
|
| 67 |
+ return nil |
|
| 68 |
+ } |
|
| 69 |
+ w.isClosed = true |
|
| 70 |
+ w.mu.Unlock() |
|
| 71 |
+ |
|
| 72 |
+ // Send "quit" message to the reader goroutine: |
|
| 73 |
+ w.done <- true |
|
| 74 |
+ w.wmut.Lock() |
|
| 75 |
+ ws := w.watches |
|
| 76 |
+ w.wmut.Unlock() |
|
| 77 |
+ for name := range ws {
|
|
| 78 |
+ w.Remove(name) |
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ return nil |
|
| 82 |
+} |
|
| 83 |
+ |
|
| 84 |
+// Add starts watching the named file or directory (non-recursively). |
|
| 85 |
+func (w *Watcher) Add(name string) error {
|
|
| 86 |
+ w.ewmut.Lock() |
|
| 87 |
+ w.externalWatches[name] = true |
|
| 88 |
+ w.ewmut.Unlock() |
|
| 89 |
+ return w.addWatch(name, noteAllEvents) |
|
| 90 |
+} |
|
| 91 |
+ |
|
| 92 |
+// Remove stops watching the the named file or directory (non-recursively). |
|
| 93 |
+func (w *Watcher) Remove(name string) error {
|
|
| 94 |
+ name = filepath.Clean(name) |
|
| 95 |
+ w.wmut.Lock() |
|
| 96 |
+ watchfd, ok := w.watches[name] |
|
| 97 |
+ w.wmut.Unlock() |
|
| 98 |
+ if !ok {
|
|
| 99 |
+ return fmt.Errorf("can't remove non-existent kevent watch for: %s", name)
|
|
| 100 |
+ } |
|
| 101 |
+ var kbuf [1]syscall.Kevent_t |
|
| 102 |
+ watchEntry := &kbuf[0] |
|
| 103 |
+ syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_DELETE) |
|
| 104 |
+ entryFlags := watchEntry.Flags |
|
| 105 |
+ success, errno := syscall.Kevent(w.kq, kbuf[:], nil, nil) |
|
| 106 |
+ if success == -1 {
|
|
| 107 |
+ return os.NewSyscallError("kevent_rm_watch", errno)
|
|
| 108 |
+ } else if (entryFlags & syscall.EV_ERROR) == syscall.EV_ERROR {
|
|
| 109 |
+ return errors.New("kevent rm error")
|
|
| 110 |
+ } |
|
| 111 |
+ syscall.Close(watchfd) |
|
| 112 |
+ w.wmut.Lock() |
|
| 113 |
+ delete(w.watches, name) |
|
| 114 |
+ w.wmut.Unlock() |
|
| 115 |
+ w.enmut.Lock() |
|
| 116 |
+ delete(w.enFlags, name) |
|
| 117 |
+ w.enmut.Unlock() |
|
| 118 |
+ w.pmut.Lock() |
|
| 119 |
+ delete(w.paths, watchfd) |
|
| 120 |
+ fInfo := w.finfo[watchfd] |
|
| 121 |
+ delete(w.finfo, watchfd) |
|
| 122 |
+ w.pmut.Unlock() |
|
| 123 |
+ |
|
| 124 |
+ // Find all watched paths that are in this directory that are not external. |
|
| 125 |
+ if fInfo.IsDir() {
|
|
| 126 |
+ var pathsToRemove []string |
|
| 127 |
+ w.pmut.Lock() |
|
| 128 |
+ for _, wpath := range w.paths {
|
|
| 129 |
+ wdir, _ := filepath.Split(wpath) |
|
| 130 |
+ if filepath.Clean(wdir) == filepath.Clean(name) {
|
|
| 131 |
+ w.ewmut.Lock() |
|
| 132 |
+ if !w.externalWatches[wpath] {
|
|
| 133 |
+ pathsToRemove = append(pathsToRemove, wpath) |
|
| 134 |
+ } |
|
| 135 |
+ w.ewmut.Unlock() |
|
| 136 |
+ } |
|
| 137 |
+ } |
|
| 138 |
+ w.pmut.Unlock() |
|
| 139 |
+ for _, name := range pathsToRemove {
|
|
| 140 |
+ // Since these are internal, not much sense in propagating error |
|
| 141 |
+ // to the user, as that will just confuse them with an error about |
|
| 142 |
+ // a path they did not explicitly watch themselves. |
|
| 143 |
+ w.Remove(name) |
|
| 144 |
+ } |
|
| 145 |
+ } |
|
| 146 |
+ |
|
| 147 |
+ return nil |
|
| 148 |
+} |
|
| 149 |
+ |
|
| 150 |
+const ( |
|
| 151 |
+ // Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE) |
|
| 152 |
+ noteAllEvents = syscall.NOTE_DELETE | syscall.NOTE_WRITE | syscall.NOTE_ATTRIB | syscall.NOTE_RENAME |
|
| 153 |
+ |
|
| 154 |
+ // Block for 100 ms on each call to kevent |
|
| 155 |
+ keventWaitTime = 100e6 |
|
| 156 |
+) |
|
| 157 |
+ |
|
| 158 |
+// addWatch adds path to the watched file set. |
|
| 159 |
+// The flags are interpreted as described in kevent(2). |
|
| 160 |
+func (w *Watcher) addWatch(path string, flags uint32) error {
|
|
| 161 |
+ path = filepath.Clean(path) |
|
| 162 |
+ w.mu.Lock() |
|
| 163 |
+ if w.isClosed {
|
|
| 164 |
+ w.mu.Unlock() |
|
| 165 |
+ return errors.New("kevent instance already closed")
|
|
| 166 |
+ } |
|
| 167 |
+ w.mu.Unlock() |
|
| 168 |
+ |
|
| 169 |
+ watchDir := false |
|
| 170 |
+ |
|
| 171 |
+ w.wmut.Lock() |
|
| 172 |
+ watchfd, found := w.watches[path] |
|
| 173 |
+ w.wmut.Unlock() |
|
| 174 |
+ if !found {
|
|
| 175 |
+ fi, errstat := os.Lstat(path) |
|
| 176 |
+ if errstat != nil {
|
|
| 177 |
+ return errstat |
|
| 178 |
+ } |
|
| 179 |
+ |
|
| 180 |
+ // don't watch socket |
|
| 181 |
+ if fi.Mode()&os.ModeSocket == os.ModeSocket {
|
|
| 182 |
+ return nil |
|
| 183 |
+ } |
|
| 184 |
+ |
|
| 185 |
+ // Follow Symlinks |
|
| 186 |
+ // Unfortunately, Linux can add bogus symlinks to watch list without |
|
| 187 |
+ // issue, and Windows can't do symlinks period (AFAIK). To maintain |
|
| 188 |
+ // consistency, we will act like everything is fine. There will simply |
|
| 189 |
+ // be no file events for broken symlinks. |
|
| 190 |
+ // Hence the returns of nil on errors. |
|
| 191 |
+ if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
| 192 |
+ path, err := filepath.EvalSymlinks(path) |
|
| 193 |
+ if err != nil {
|
|
| 194 |
+ return nil |
|
| 195 |
+ } |
|
| 196 |
+ |
|
| 197 |
+ fi, errstat = os.Lstat(path) |
|
| 198 |
+ if errstat != nil {
|
|
| 199 |
+ return nil |
|
| 200 |
+ } |
|
| 201 |
+ } |
|
| 202 |
+ |
|
| 203 |
+ fd, errno := syscall.Open(path, openMode, 0700) |
|
| 204 |
+ if fd == -1 {
|
|
| 205 |
+ return os.NewSyscallError("Open", errno)
|
|
| 206 |
+ } |
|
| 207 |
+ watchfd = fd |
|
| 208 |
+ |
|
| 209 |
+ w.wmut.Lock() |
|
| 210 |
+ w.watches[path] = watchfd |
|
| 211 |
+ w.wmut.Unlock() |
|
| 212 |
+ |
|
| 213 |
+ w.pmut.Lock() |
|
| 214 |
+ w.paths[watchfd] = path |
|
| 215 |
+ w.finfo[watchfd] = fi |
|
| 216 |
+ w.pmut.Unlock() |
|
| 217 |
+ } |
|
| 218 |
+ // Watch the directory if it has not been watched before. |
|
| 219 |
+ w.pmut.Lock() |
|
| 220 |
+ w.enmut.Lock() |
|
| 221 |
+ if w.finfo[watchfd].IsDir() && |
|
| 222 |
+ (flags&syscall.NOTE_WRITE) == syscall.NOTE_WRITE && |
|
| 223 |
+ (!found || (w.enFlags[path]&syscall.NOTE_WRITE) != syscall.NOTE_WRITE) {
|
|
| 224 |
+ watchDir = true |
|
| 225 |
+ } |
|
| 226 |
+ w.enmut.Unlock() |
|
| 227 |
+ w.pmut.Unlock() |
|
| 228 |
+ |
|
| 229 |
+ w.enmut.Lock() |
|
| 230 |
+ w.enFlags[path] = flags |
|
| 231 |
+ w.enmut.Unlock() |
|
| 232 |
+ |
|
| 233 |
+ var kbuf [1]syscall.Kevent_t |
|
| 234 |
+ watchEntry := &kbuf[0] |
|
| 235 |
+ watchEntry.Fflags = flags |
|
| 236 |
+ syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_ADD|syscall.EV_CLEAR) |
|
| 237 |
+ entryFlags := watchEntry.Flags |
|
| 238 |
+ success, errno := syscall.Kevent(w.kq, kbuf[:], nil, nil) |
|
| 239 |
+ if success == -1 {
|
|
| 240 |
+ return errno |
|
| 241 |
+ } else if (entryFlags & syscall.EV_ERROR) == syscall.EV_ERROR {
|
|
| 242 |
+ return errors.New("kevent add error")
|
|
| 243 |
+ } |
|
| 244 |
+ |
|
| 245 |
+ if watchDir {
|
|
| 246 |
+ errdir := w.watchDirectoryFiles(path) |
|
| 247 |
+ if errdir != nil {
|
|
| 248 |
+ return errdir |
|
| 249 |
+ } |
|
| 250 |
+ } |
|
| 251 |
+ return nil |
|
| 252 |
+} |
|
| 253 |
+ |
|
| 254 |
+// readEvents reads from the kqueue file descriptor, converts the |
|
| 255 |
+// received events into Event objects and sends them via the Events channel |
|
| 256 |
+func (w *Watcher) readEvents() {
|
|
| 257 |
+ var ( |
|
| 258 |
+ keventbuf [10]syscall.Kevent_t // Event buffer |
|
| 259 |
+ kevents []syscall.Kevent_t // Received events |
|
| 260 |
+ twait *syscall.Timespec // Time to block waiting for events |
|
| 261 |
+ n int // Number of events returned from kevent |
|
| 262 |
+ errno error // Syscall errno |
|
| 263 |
+ ) |
|
| 264 |
+ kevents = keventbuf[0:0] |
|
| 265 |
+ twait = new(syscall.Timespec) |
|
| 266 |
+ *twait = syscall.NsecToTimespec(keventWaitTime) |
|
| 267 |
+ |
|
| 268 |
+ for {
|
|
| 269 |
+ // See if there is a message on the "done" channel |
|
| 270 |
+ var done bool |
|
| 271 |
+ select {
|
|
| 272 |
+ case done = <-w.done: |
|
| 273 |
+ default: |
|
| 274 |
+ } |
|
| 275 |
+ |
|
| 276 |
+ // If "done" message is received |
|
| 277 |
+ if done {
|
|
| 278 |
+ errno := syscall.Close(w.kq) |
|
| 279 |
+ if errno != nil {
|
|
| 280 |
+ w.Errors <- os.NewSyscallError("close", errno)
|
|
| 281 |
+ } |
|
| 282 |
+ close(w.Events) |
|
| 283 |
+ close(w.Errors) |
|
| 284 |
+ return |
|
| 285 |
+ } |
|
| 286 |
+ |
|
| 287 |
+ // Get new events |
|
| 288 |
+ if len(kevents) == 0 {
|
|
| 289 |
+ n, errno = syscall.Kevent(w.kq, nil, keventbuf[:], twait) |
|
| 290 |
+ |
|
| 291 |
+ // EINTR is okay, basically the syscall was interrupted before |
|
| 292 |
+ // timeout expired. |
|
| 293 |
+ if errno != nil && errno != syscall.EINTR {
|
|
| 294 |
+ w.Errors <- os.NewSyscallError("kevent", errno)
|
|
| 295 |
+ continue |
|
| 296 |
+ } |
|
| 297 |
+ |
|
| 298 |
+ // Received some events |
|
| 299 |
+ if n > 0 {
|
|
| 300 |
+ kevents = keventbuf[0:n] |
|
| 301 |
+ } |
|
| 302 |
+ } |
|
| 303 |
+ |
|
| 304 |
+ // Flush the events we received to the Events channel |
|
| 305 |
+ for len(kevents) > 0 {
|
|
| 306 |
+ watchEvent := &kevents[0] |
|
| 307 |
+ mask := uint32(watchEvent.Fflags) |
|
| 308 |
+ w.pmut.Lock() |
|
| 309 |
+ name := w.paths[int(watchEvent.Ident)] |
|
| 310 |
+ fileInfo := w.finfo[int(watchEvent.Ident)] |
|
| 311 |
+ w.pmut.Unlock() |
|
| 312 |
+ |
|
| 313 |
+ event := newEvent(name, mask, false) |
|
| 314 |
+ |
|
| 315 |
+ if fileInfo != nil && fileInfo.IsDir() && !(event.Op&Remove == Remove) {
|
|
| 316 |
+ // Double check to make sure the directory exist. This can happen when |
|
| 317 |
+ // we do a rm -fr on a recursively watched folders and we receive a |
|
| 318 |
+ // modification event first but the folder has been deleted and later |
|
| 319 |
+ // receive the delete event |
|
| 320 |
+ if _, err := os.Lstat(event.Name); os.IsNotExist(err) {
|
|
| 321 |
+ // mark is as delete event |
|
| 322 |
+ event.Op |= Remove |
|
| 323 |
+ } |
|
| 324 |
+ } |
|
| 325 |
+ |
|
| 326 |
+ if fileInfo != nil && fileInfo.IsDir() && event.Op&Write == Write && !(event.Op&Remove == Remove) {
|
|
| 327 |
+ w.sendDirectoryChangeEvents(event.Name) |
|
| 328 |
+ } else {
|
|
| 329 |
+ // Send the event on the Events channel |
|
| 330 |
+ w.Events <- event |
|
| 331 |
+ } |
|
| 332 |
+ |
|
| 333 |
+ // Move to next event |
|
| 334 |
+ kevents = kevents[1:] |
|
| 335 |
+ |
|
| 336 |
+ if event.Op&Rename == Rename {
|
|
| 337 |
+ w.Remove(event.Name) |
|
| 338 |
+ w.femut.Lock() |
|
| 339 |
+ delete(w.fileExists, event.Name) |
|
| 340 |
+ w.femut.Unlock() |
|
| 341 |
+ } |
|
| 342 |
+ if event.Op&Remove == Remove {
|
|
| 343 |
+ w.Remove(event.Name) |
|
| 344 |
+ w.femut.Lock() |
|
| 345 |
+ delete(w.fileExists, event.Name) |
|
| 346 |
+ w.femut.Unlock() |
|
| 347 |
+ |
|
| 348 |
+ // Look for a file that may have overwritten this |
|
| 349 |
+ // (ie mv f1 f2 will delete f2 then create f2) |
|
| 350 |
+ fileDir, _ := filepath.Split(event.Name) |
|
| 351 |
+ fileDir = filepath.Clean(fileDir) |
|
| 352 |
+ w.wmut.Lock() |
|
| 353 |
+ _, found := w.watches[fileDir] |
|
| 354 |
+ w.wmut.Unlock() |
|
| 355 |
+ if found {
|
|
| 356 |
+ // make sure the directory exist before we watch for changes. When we |
|
| 357 |
+ // do a recursive watch and perform rm -fr, the parent directory might |
|
| 358 |
+ // have gone missing, ignore the missing directory and let the |
|
| 359 |
+ // upcoming delete event remove the watch form the parent folder |
|
| 360 |
+ if _, err := os.Lstat(fileDir); !os.IsNotExist(err) {
|
|
| 361 |
+ w.sendDirectoryChangeEvents(fileDir) |
|
| 362 |
+ } |
|
| 363 |
+ } |
|
| 364 |
+ } |
|
| 365 |
+ } |
|
| 366 |
+ } |
|
| 367 |
+} |
|
| 368 |
+ |
|
| 369 |
+// newEvent returns an platform-independent Event based on kqueue Fflags. |
|
| 370 |
+func newEvent(name string, mask uint32, create bool) Event {
|
|
| 371 |
+ e := Event{Name: name}
|
|
| 372 |
+ if create {
|
|
| 373 |
+ e.Op |= Create |
|
| 374 |
+ } |
|
| 375 |
+ if mask&syscall.NOTE_DELETE == syscall.NOTE_DELETE {
|
|
| 376 |
+ e.Op |= Remove |
|
| 377 |
+ } |
|
| 378 |
+ if mask&syscall.NOTE_WRITE == syscall.NOTE_WRITE {
|
|
| 379 |
+ e.Op |= Write |
|
| 380 |
+ } |
|
| 381 |
+ if mask&syscall.NOTE_RENAME == syscall.NOTE_RENAME {
|
|
| 382 |
+ e.Op |= Rename |
|
| 383 |
+ } |
|
| 384 |
+ if mask&syscall.NOTE_ATTRIB == syscall.NOTE_ATTRIB {
|
|
| 385 |
+ e.Op |= Chmod |
|
| 386 |
+ } |
|
| 387 |
+ return e |
|
| 388 |
+} |
|
| 389 |
+ |
|
| 390 |
+func (w *Watcher) watchDirectoryFiles(dirPath string) error {
|
|
| 391 |
+ // Get all files |
|
| 392 |
+ files, err := ioutil.ReadDir(dirPath) |
|
| 393 |
+ if err != nil {
|
|
| 394 |
+ return err |
|
| 395 |
+ } |
|
| 396 |
+ |
|
| 397 |
+ // Search for new files |
|
| 398 |
+ for _, fileInfo := range files {
|
|
| 399 |
+ filePath := filepath.Join(dirPath, fileInfo.Name()) |
|
| 400 |
+ |
|
| 401 |
+ if fileInfo.IsDir() == false {
|
|
| 402 |
+ // Watch file to mimic linux fsnotify |
|
| 403 |
+ e := w.addWatch(filePath, noteAllEvents) |
|
| 404 |
+ if e != nil {
|
|
| 405 |
+ return e |
|
| 406 |
+ } |
|
| 407 |
+ } else {
|
|
| 408 |
+ // If the user is currently watching directory |
|
| 409 |
+ // we want to preserve the flags used |
|
| 410 |
+ w.enmut.Lock() |
|
| 411 |
+ currFlags, found := w.enFlags[filePath] |
|
| 412 |
+ w.enmut.Unlock() |
|
| 413 |
+ var newFlags uint32 = syscall.NOTE_DELETE |
|
| 414 |
+ if found {
|
|
| 415 |
+ newFlags |= currFlags |
|
| 416 |
+ } |
|
| 417 |
+ |
|
| 418 |
+ // Linux gives deletes if not explicitly watching |
|
| 419 |
+ e := w.addWatch(filePath, newFlags) |
|
| 420 |
+ if e != nil {
|
|
| 421 |
+ return e |
|
| 422 |
+ } |
|
| 423 |
+ } |
|
| 424 |
+ w.femut.Lock() |
|
| 425 |
+ w.fileExists[filePath] = true |
|
| 426 |
+ w.femut.Unlock() |
|
| 427 |
+ } |
|
| 428 |
+ |
|
| 429 |
+ return nil |
|
| 430 |
+} |
|
| 431 |
+ |
|
| 432 |
+// sendDirectoryEvents searches the directory for newly created files |
|
| 433 |
+// and sends them over the event channel. This functionality is to have |
|
| 434 |
+// the BSD version of fsnotify match linux fsnotify which provides a |
|
| 435 |
+// create event for files created in a watched directory. |
|
| 436 |
+func (w *Watcher) sendDirectoryChangeEvents(dirPath string) {
|
|
| 437 |
+ // Get all files |
|
| 438 |
+ files, err := ioutil.ReadDir(dirPath) |
|
| 439 |
+ if err != nil {
|
|
| 440 |
+ w.Errors <- err |
|
| 441 |
+ } |
|
| 442 |
+ |
|
| 443 |
+ // Search for new files |
|
| 444 |
+ for _, fileInfo := range files {
|
|
| 445 |
+ filePath := filepath.Join(dirPath, fileInfo.Name()) |
|
| 446 |
+ w.femut.Lock() |
|
| 447 |
+ _, doesExist := w.fileExists[filePath] |
|
| 448 |
+ w.femut.Unlock() |
|
| 449 |
+ if !doesExist {
|
|
| 450 |
+ // Send create event (mask=0) |
|
| 451 |
+ event := newEvent(filePath, 0, true) |
|
| 452 |
+ w.Events <- event |
|
| 453 |
+ } |
|
| 454 |
+ |
|
| 455 |
+ // watchDirectoryFiles (but without doing another ReadDir) |
|
| 456 |
+ if fileInfo.IsDir() == false {
|
|
| 457 |
+ // Watch file to mimic linux fsnotify |
|
| 458 |
+ w.addWatch(filePath, noteAllEvents) |
|
| 459 |
+ } else {
|
|
| 460 |
+ // If the user is currently watching directory |
|
| 461 |
+ // we want to preserve the flags used |
|
| 462 |
+ w.enmut.Lock() |
|
| 463 |
+ currFlags, found := w.enFlags[filePath] |
|
| 464 |
+ w.enmut.Unlock() |
|
| 465 |
+ var newFlags uint32 = syscall.NOTE_DELETE |
|
| 466 |
+ if found {
|
|
| 467 |
+ newFlags |= currFlags |
|
| 468 |
+ } |
|
| 469 |
+ |
|
| 470 |
+ // Linux gives deletes if not explicitly watching |
|
| 471 |
+ w.addWatch(filePath, newFlags) |
|
| 472 |
+ } |
|
| 473 |
+ |
|
| 474 |
+ w.femut.Lock() |
|
| 475 |
+ w.fileExists[filePath] = true |
|
| 476 |
+ w.femut.Unlock() |
|
| 477 |
+ } |
|
| 478 |
+} |
| 0 | 479 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,11 @@ |
| 0 |
+// Copyright 2013 The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE file. |
|
| 3 |
+ |
|
| 4 |
+// +build freebsd openbsd netbsd dragonfly |
|
| 5 |
+ |
|
| 6 |
+package fsnotify |
|
| 7 |
+ |
|
| 8 |
+import "syscall" |
|
| 9 |
+ |
|
| 10 |
+const openMode = syscall.O_NONBLOCK | syscall.O_RDONLY |
| 0 | 11 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,12 @@ |
| 0 |
+// Copyright 2013 The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE file. |
|
| 3 |
+ |
|
| 4 |
+// +build darwin |
|
| 5 |
+ |
|
| 6 |
+package fsnotify |
|
| 7 |
+ |
|
| 8 |
+import "syscall" |
|
| 9 |
+ |
|
| 10 |
+// note: this constant is not defined on BSD |
|
| 11 |
+const openMode = syscall.O_EVTONLY |
| 0 | 12 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,561 @@ |
| 0 |
+// Copyright 2011 The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE file. |
|
| 3 |
+ |
|
| 4 |
+// +build windows |
|
| 5 |
+ |
|
| 6 |
+package fsnotify |
|
| 7 |
+ |
|
| 8 |
+import ( |
|
| 9 |
+ "errors" |
|
| 10 |
+ "fmt" |
|
| 11 |
+ "os" |
|
| 12 |
+ "path/filepath" |
|
| 13 |
+ "runtime" |
|
| 14 |
+ "sync" |
|
| 15 |
+ "syscall" |
|
| 16 |
+ "unsafe" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+// Watcher watches a set of files, delivering events to a channel. |
|
| 20 |
+type Watcher struct {
|
|
| 21 |
+ Events chan Event |
|
| 22 |
+ Errors chan error |
|
| 23 |
+ isClosed bool // Set to true when Close() is first called |
|
| 24 |
+ mu sync.Mutex // Map access |
|
| 25 |
+ port syscall.Handle // Handle to completion port |
|
| 26 |
+ watches watchMap // Map of watches (key: i-number) |
|
| 27 |
+ input chan *input // Inputs to the reader are sent on this channel |
|
| 28 |
+ quit chan chan<- error |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. |
|
| 32 |
+func NewWatcher() (*Watcher, error) {
|
|
| 33 |
+ port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0) |
|
| 34 |
+ if e != nil {
|
|
| 35 |
+ return nil, os.NewSyscallError("CreateIoCompletionPort", e)
|
|
| 36 |
+ } |
|
| 37 |
+ w := &Watcher{
|
|
| 38 |
+ port: port, |
|
| 39 |
+ watches: make(watchMap), |
|
| 40 |
+ input: make(chan *input, 1), |
|
| 41 |
+ Events: make(chan Event, 50), |
|
| 42 |
+ Errors: make(chan error), |
|
| 43 |
+ quit: make(chan chan<- error, 1), |
|
| 44 |
+ } |
|
| 45 |
+ go w.readEvents() |
|
| 46 |
+ return w, nil |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 49 |
+// Close removes all watches and closes the events channel. |
|
| 50 |
+func (w *Watcher) Close() error {
|
|
| 51 |
+ if w.isClosed {
|
|
| 52 |
+ return nil |
|
| 53 |
+ } |
|
| 54 |
+ w.isClosed = true |
|
| 55 |
+ |
|
| 56 |
+ // Send "quit" message to the reader goroutine |
|
| 57 |
+ ch := make(chan error) |
|
| 58 |
+ w.quit <- ch |
|
| 59 |
+ if err := w.wakeupReader(); err != nil {
|
|
| 60 |
+ return err |
|
| 61 |
+ } |
|
| 62 |
+ return <-ch |
|
| 63 |
+} |
|
| 64 |
+ |
|
| 65 |
+// Add starts watching the named file or directory (non-recursively). |
|
| 66 |
+func (w *Watcher) Add(name string) error {
|
|
| 67 |
+ if w.isClosed {
|
|
| 68 |
+ return errors.New("watcher already closed")
|
|
| 69 |
+ } |
|
| 70 |
+ in := &input{
|
|
| 71 |
+ op: opAddWatch, |
|
| 72 |
+ path: filepath.Clean(name), |
|
| 73 |
+ flags: sys_FS_ALL_EVENTS, |
|
| 74 |
+ reply: make(chan error), |
|
| 75 |
+ } |
|
| 76 |
+ w.input <- in |
|
| 77 |
+ if err := w.wakeupReader(); err != nil {
|
|
| 78 |
+ return err |
|
| 79 |
+ } |
|
| 80 |
+ return <-in.reply |
|
| 81 |
+} |
|
| 82 |
+ |
|
| 83 |
+// Remove stops watching the the named file or directory (non-recursively). |
|
| 84 |
+func (w *Watcher) Remove(name string) error {
|
|
| 85 |
+ in := &input{
|
|
| 86 |
+ op: opRemoveWatch, |
|
| 87 |
+ path: filepath.Clean(name), |
|
| 88 |
+ reply: make(chan error), |
|
| 89 |
+ } |
|
| 90 |
+ w.input <- in |
|
| 91 |
+ if err := w.wakeupReader(); err != nil {
|
|
| 92 |
+ return err |
|
| 93 |
+ } |
|
| 94 |
+ return <-in.reply |
|
| 95 |
+} |
|
| 96 |
+ |
|
| 97 |
+const ( |
|
| 98 |
+ // Options for AddWatch |
|
| 99 |
+ sys_FS_ONESHOT = 0x80000000 |
|
| 100 |
+ sys_FS_ONLYDIR = 0x1000000 |
|
| 101 |
+ |
|
| 102 |
+ // Events |
|
| 103 |
+ sys_FS_ACCESS = 0x1 |
|
| 104 |
+ sys_FS_ALL_EVENTS = 0xfff |
|
| 105 |
+ sys_FS_ATTRIB = 0x4 |
|
| 106 |
+ sys_FS_CLOSE = 0x18 |
|
| 107 |
+ sys_FS_CREATE = 0x100 |
|
| 108 |
+ sys_FS_DELETE = 0x200 |
|
| 109 |
+ sys_FS_DELETE_SELF = 0x400 |
|
| 110 |
+ sys_FS_MODIFY = 0x2 |
|
| 111 |
+ sys_FS_MOVE = 0xc0 |
|
| 112 |
+ sys_FS_MOVED_FROM = 0x40 |
|
| 113 |
+ sys_FS_MOVED_TO = 0x80 |
|
| 114 |
+ sys_FS_MOVE_SELF = 0x800 |
|
| 115 |
+ |
|
| 116 |
+ // Special events |
|
| 117 |
+ sys_FS_IGNORED = 0x8000 |
|
| 118 |
+ sys_FS_Q_OVERFLOW = 0x4000 |
|
| 119 |
+) |
|
| 120 |
+ |
|
| 121 |
+func newEvent(name string, mask uint32) Event {
|
|
| 122 |
+ e := Event{Name: name}
|
|
| 123 |
+ if mask&sys_FS_CREATE == sys_FS_CREATE || mask&sys_FS_MOVED_TO == sys_FS_MOVED_TO {
|
|
| 124 |
+ e.Op |= Create |
|
| 125 |
+ } |
|
| 126 |
+ if mask&sys_FS_DELETE == sys_FS_DELETE || mask&sys_FS_DELETE_SELF == sys_FS_DELETE_SELF {
|
|
| 127 |
+ e.Op |= Remove |
|
| 128 |
+ } |
|
| 129 |
+ if mask&sys_FS_MODIFY == sys_FS_MODIFY {
|
|
| 130 |
+ e.Op |= Write |
|
| 131 |
+ } |
|
| 132 |
+ 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 {
|
|
| 133 |
+ e.Op |= Rename |
|
| 134 |
+ } |
|
| 135 |
+ if mask&sys_FS_ATTRIB == sys_FS_ATTRIB {
|
|
| 136 |
+ e.Op |= Chmod |
|
| 137 |
+ } |
|
| 138 |
+ return e |
|
| 139 |
+} |
|
| 140 |
+ |
|
| 141 |
+const ( |
|
| 142 |
+ opAddWatch = iota |
|
| 143 |
+ opRemoveWatch |
|
| 144 |
+) |
|
| 145 |
+ |
|
| 146 |
+const ( |
|
| 147 |
+ provisional uint64 = 1 << (32 + iota) |
|
| 148 |
+) |
|
| 149 |
+ |
|
| 150 |
+type input struct {
|
|
| 151 |
+ op int |
|
| 152 |
+ path string |
|
| 153 |
+ flags uint32 |
|
| 154 |
+ reply chan error |
|
| 155 |
+} |
|
| 156 |
+ |
|
| 157 |
+type inode struct {
|
|
| 158 |
+ handle syscall.Handle |
|
| 159 |
+ volume uint32 |
|
| 160 |
+ index uint64 |
|
| 161 |
+} |
|
| 162 |
+ |
|
| 163 |
+type watch struct {
|
|
| 164 |
+ ov syscall.Overlapped |
|
| 165 |
+ ino *inode // i-number |
|
| 166 |
+ path string // Directory path |
|
| 167 |
+ mask uint64 // Directory itself is being watched with these notify flags |
|
| 168 |
+ names map[string]uint64 // Map of names being watched and their notify flags |
|
| 169 |
+ rename string // Remembers the old name while renaming a file |
|
| 170 |
+ buf [4096]byte |
|
| 171 |
+} |
|
| 172 |
+ |
|
| 173 |
+type indexMap map[uint64]*watch |
|
| 174 |
+type watchMap map[uint32]indexMap |
|
| 175 |
+ |
|
| 176 |
+func (w *Watcher) wakeupReader() error {
|
|
| 177 |
+ e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil) |
|
| 178 |
+ if e != nil {
|
|
| 179 |
+ return os.NewSyscallError("PostQueuedCompletionStatus", e)
|
|
| 180 |
+ } |
|
| 181 |
+ return nil |
|
| 182 |
+} |
|
| 183 |
+ |
|
| 184 |
+func getDir(pathname string) (dir string, err error) {
|
|
| 185 |
+ attr, e := syscall.GetFileAttributes(syscall.StringToUTF16Ptr(pathname)) |
|
| 186 |
+ if e != nil {
|
|
| 187 |
+ return "", os.NewSyscallError("GetFileAttributes", e)
|
|
| 188 |
+ } |
|
| 189 |
+ if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
|
|
| 190 |
+ dir = pathname |
|
| 191 |
+ } else {
|
|
| 192 |
+ dir, _ = filepath.Split(pathname) |
|
| 193 |
+ dir = filepath.Clean(dir) |
|
| 194 |
+ } |
|
| 195 |
+ return |
|
| 196 |
+} |
|
| 197 |
+ |
|
| 198 |
+func getIno(path string) (ino *inode, err error) {
|
|
| 199 |
+ h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path), |
|
| 200 |
+ syscall.FILE_LIST_DIRECTORY, |
|
| 201 |
+ syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, |
|
| 202 |
+ nil, syscall.OPEN_EXISTING, |
|
| 203 |
+ syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0) |
|
| 204 |
+ if e != nil {
|
|
| 205 |
+ return nil, os.NewSyscallError("CreateFile", e)
|
|
| 206 |
+ } |
|
| 207 |
+ var fi syscall.ByHandleFileInformation |
|
| 208 |
+ if e = syscall.GetFileInformationByHandle(h, &fi); e != nil {
|
|
| 209 |
+ syscall.CloseHandle(h) |
|
| 210 |
+ return nil, os.NewSyscallError("GetFileInformationByHandle", e)
|
|
| 211 |
+ } |
|
| 212 |
+ ino = &inode{
|
|
| 213 |
+ handle: h, |
|
| 214 |
+ volume: fi.VolumeSerialNumber, |
|
| 215 |
+ index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), |
|
| 216 |
+ } |
|
| 217 |
+ return ino, nil |
|
| 218 |
+} |
|
| 219 |
+ |
|
| 220 |
+// Must run within the I/O thread. |
|
| 221 |
+func (m watchMap) get(ino *inode) *watch {
|
|
| 222 |
+ if i := m[ino.volume]; i != nil {
|
|
| 223 |
+ return i[ino.index] |
|
| 224 |
+ } |
|
| 225 |
+ return nil |
|
| 226 |
+} |
|
| 227 |
+ |
|
| 228 |
+// Must run within the I/O thread. |
|
| 229 |
+func (m watchMap) set(ino *inode, watch *watch) {
|
|
| 230 |
+ i := m[ino.volume] |
|
| 231 |
+ if i == nil {
|
|
| 232 |
+ i = make(indexMap) |
|
| 233 |
+ m[ino.volume] = i |
|
| 234 |
+ } |
|
| 235 |
+ i[ino.index] = watch |
|
| 236 |
+} |
|
| 237 |
+ |
|
| 238 |
+// Must run within the I/O thread. |
|
| 239 |
+func (w *Watcher) addWatch(pathname string, flags uint64) error {
|
|
| 240 |
+ dir, err := getDir(pathname) |
|
| 241 |
+ if err != nil {
|
|
| 242 |
+ return err |
|
| 243 |
+ } |
|
| 244 |
+ if flags&sys_FS_ONLYDIR != 0 && pathname != dir {
|
|
| 245 |
+ return nil |
|
| 246 |
+ } |
|
| 247 |
+ ino, err := getIno(dir) |
|
| 248 |
+ if err != nil {
|
|
| 249 |
+ return err |
|
| 250 |
+ } |
|
| 251 |
+ w.mu.Lock() |
|
| 252 |
+ watchEntry := w.watches.get(ino) |
|
| 253 |
+ w.mu.Unlock() |
|
| 254 |
+ if watchEntry == nil {
|
|
| 255 |
+ if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil {
|
|
| 256 |
+ syscall.CloseHandle(ino.handle) |
|
| 257 |
+ return os.NewSyscallError("CreateIoCompletionPort", e)
|
|
| 258 |
+ } |
|
| 259 |
+ watchEntry = &watch{
|
|
| 260 |
+ ino: ino, |
|
| 261 |
+ path: dir, |
|
| 262 |
+ names: make(map[string]uint64), |
|
| 263 |
+ } |
|
| 264 |
+ w.mu.Lock() |
|
| 265 |
+ w.watches.set(ino, watchEntry) |
|
| 266 |
+ w.mu.Unlock() |
|
| 267 |
+ flags |= provisional |
|
| 268 |
+ } else {
|
|
| 269 |
+ syscall.CloseHandle(ino.handle) |
|
| 270 |
+ } |
|
| 271 |
+ if pathname == dir {
|
|
| 272 |
+ watchEntry.mask |= flags |
|
| 273 |
+ } else {
|
|
| 274 |
+ watchEntry.names[filepath.Base(pathname)] |= flags |
|
| 275 |
+ } |
|
| 276 |
+ if err = w.startRead(watchEntry); err != nil {
|
|
| 277 |
+ return err |
|
| 278 |
+ } |
|
| 279 |
+ if pathname == dir {
|
|
| 280 |
+ watchEntry.mask &= ^provisional |
|
| 281 |
+ } else {
|
|
| 282 |
+ watchEntry.names[filepath.Base(pathname)] &= ^provisional |
|
| 283 |
+ } |
|
| 284 |
+ return nil |
|
| 285 |
+} |
|
| 286 |
+ |
|
| 287 |
+// Must run within the I/O thread. |
|
| 288 |
+func (w *Watcher) remWatch(pathname string) error {
|
|
| 289 |
+ dir, err := getDir(pathname) |
|
| 290 |
+ if err != nil {
|
|
| 291 |
+ return err |
|
| 292 |
+ } |
|
| 293 |
+ ino, err := getIno(dir) |
|
| 294 |
+ if err != nil {
|
|
| 295 |
+ return err |
|
| 296 |
+ } |
|
| 297 |
+ w.mu.Lock() |
|
| 298 |
+ watch := w.watches.get(ino) |
|
| 299 |
+ w.mu.Unlock() |
|
| 300 |
+ if watch == nil {
|
|
| 301 |
+ return fmt.Errorf("can't remove non-existent watch for: %s", pathname)
|
|
| 302 |
+ } |
|
| 303 |
+ if pathname == dir {
|
|
| 304 |
+ w.sendEvent(watch.path, watch.mask&sys_FS_IGNORED) |
|
| 305 |
+ watch.mask = 0 |
|
| 306 |
+ } else {
|
|
| 307 |
+ name := filepath.Base(pathname) |
|
| 308 |
+ w.sendEvent(watch.path+"\\"+name, watch.names[name]&sys_FS_IGNORED) |
|
| 309 |
+ delete(watch.names, name) |
|
| 310 |
+ } |
|
| 311 |
+ return w.startRead(watch) |
|
| 312 |
+} |
|
| 313 |
+ |
|
| 314 |
+// Must run within the I/O thread. |
|
| 315 |
+func (w *Watcher) deleteWatch(watch *watch) {
|
|
| 316 |
+ for name, mask := range watch.names {
|
|
| 317 |
+ if mask&provisional == 0 {
|
|
| 318 |
+ w.sendEvent(watch.path+"\\"+name, mask&sys_FS_IGNORED) |
|
| 319 |
+ } |
|
| 320 |
+ delete(watch.names, name) |
|
| 321 |
+ } |
|
| 322 |
+ if watch.mask != 0 {
|
|
| 323 |
+ if watch.mask&provisional == 0 {
|
|
| 324 |
+ w.sendEvent(watch.path, watch.mask&sys_FS_IGNORED) |
|
| 325 |
+ } |
|
| 326 |
+ watch.mask = 0 |
|
| 327 |
+ } |
|
| 328 |
+} |
|
| 329 |
+ |
|
| 330 |
+// Must run within the I/O thread. |
|
| 331 |
+func (w *Watcher) startRead(watch *watch) error {
|
|
| 332 |
+ if e := syscall.CancelIo(watch.ino.handle); e != nil {
|
|
| 333 |
+ w.Errors <- os.NewSyscallError("CancelIo", e)
|
|
| 334 |
+ w.deleteWatch(watch) |
|
| 335 |
+ } |
|
| 336 |
+ mask := toWindowsFlags(watch.mask) |
|
| 337 |
+ for _, m := range watch.names {
|
|
| 338 |
+ mask |= toWindowsFlags(m) |
|
| 339 |
+ } |
|
| 340 |
+ if mask == 0 {
|
|
| 341 |
+ if e := syscall.CloseHandle(watch.ino.handle); e != nil {
|
|
| 342 |
+ w.Errors <- os.NewSyscallError("CloseHandle", e)
|
|
| 343 |
+ } |
|
| 344 |
+ w.mu.Lock() |
|
| 345 |
+ delete(w.watches[watch.ino.volume], watch.ino.index) |
|
| 346 |
+ w.mu.Unlock() |
|
| 347 |
+ return nil |
|
| 348 |
+ } |
|
| 349 |
+ e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], |
|
| 350 |
+ uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) |
|
| 351 |
+ if e != nil {
|
|
| 352 |
+ err := os.NewSyscallError("ReadDirectoryChanges", e)
|
|
| 353 |
+ if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
|
|
| 354 |
+ // Watched directory was probably removed |
|
| 355 |
+ if w.sendEvent(watch.path, watch.mask&sys_FS_DELETE_SELF) {
|
|
| 356 |
+ if watch.mask&sys_FS_ONESHOT != 0 {
|
|
| 357 |
+ watch.mask = 0 |
|
| 358 |
+ } |
|
| 359 |
+ } |
|
| 360 |
+ err = nil |
|
| 361 |
+ } |
|
| 362 |
+ w.deleteWatch(watch) |
|
| 363 |
+ w.startRead(watch) |
|
| 364 |
+ return err |
|
| 365 |
+ } |
|
| 366 |
+ return nil |
|
| 367 |
+} |
|
| 368 |
+ |
|
| 369 |
+// readEvents reads from the I/O completion port, converts the |
|
| 370 |
+// received events into Event objects and sends them via the Events channel. |
|
| 371 |
+// Entry point to the I/O thread. |
|
| 372 |
+func (w *Watcher) readEvents() {
|
|
| 373 |
+ var ( |
|
| 374 |
+ n, key uint32 |
|
| 375 |
+ ov *syscall.Overlapped |
|
| 376 |
+ ) |
|
| 377 |
+ runtime.LockOSThread() |
|
| 378 |
+ |
|
| 379 |
+ for {
|
|
| 380 |
+ e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE) |
|
| 381 |
+ watch := (*watch)(unsafe.Pointer(ov)) |
|
| 382 |
+ |
|
| 383 |
+ if watch == nil {
|
|
| 384 |
+ select {
|
|
| 385 |
+ case ch := <-w.quit: |
|
| 386 |
+ w.mu.Lock() |
|
| 387 |
+ var indexes []indexMap |
|
| 388 |
+ for _, index := range w.watches {
|
|
| 389 |
+ indexes = append(indexes, index) |
|
| 390 |
+ } |
|
| 391 |
+ w.mu.Unlock() |
|
| 392 |
+ for _, index := range indexes {
|
|
| 393 |
+ for _, watch := range index {
|
|
| 394 |
+ w.deleteWatch(watch) |
|
| 395 |
+ w.startRead(watch) |
|
| 396 |
+ } |
|
| 397 |
+ } |
|
| 398 |
+ var err error |
|
| 399 |
+ if e := syscall.CloseHandle(w.port); e != nil {
|
|
| 400 |
+ err = os.NewSyscallError("CloseHandle", e)
|
|
| 401 |
+ } |
|
| 402 |
+ close(w.Events) |
|
| 403 |
+ close(w.Errors) |
|
| 404 |
+ ch <- err |
|
| 405 |
+ return |
|
| 406 |
+ case in := <-w.input: |
|
| 407 |
+ switch in.op {
|
|
| 408 |
+ case opAddWatch: |
|
| 409 |
+ in.reply <- w.addWatch(in.path, uint64(in.flags)) |
|
| 410 |
+ case opRemoveWatch: |
|
| 411 |
+ in.reply <- w.remWatch(in.path) |
|
| 412 |
+ } |
|
| 413 |
+ default: |
|
| 414 |
+ } |
|
| 415 |
+ continue |
|
| 416 |
+ } |
|
| 417 |
+ |
|
| 418 |
+ switch e {
|
|
| 419 |
+ case syscall.ERROR_MORE_DATA: |
|
| 420 |
+ if watch == nil {
|
|
| 421 |
+ w.Errors <- errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer")
|
|
| 422 |
+ } else {
|
|
| 423 |
+ // The i/o succeeded but the buffer is full. |
|
| 424 |
+ // In theory we should be building up a full packet. |
|
| 425 |
+ // In practice we can get away with just carrying on. |
|
| 426 |
+ n = uint32(unsafe.Sizeof(watch.buf)) |
|
| 427 |
+ } |
|
| 428 |
+ case syscall.ERROR_ACCESS_DENIED: |
|
| 429 |
+ // Watched directory was probably removed |
|
| 430 |
+ w.sendEvent(watch.path, watch.mask&sys_FS_DELETE_SELF) |
|
| 431 |
+ w.deleteWatch(watch) |
|
| 432 |
+ w.startRead(watch) |
|
| 433 |
+ continue |
|
| 434 |
+ case syscall.ERROR_OPERATION_ABORTED: |
|
| 435 |
+ // CancelIo was called on this handle |
|
| 436 |
+ continue |
|
| 437 |
+ default: |
|
| 438 |
+ w.Errors <- os.NewSyscallError("GetQueuedCompletionPort", e)
|
|
| 439 |
+ continue |
|
| 440 |
+ case nil: |
|
| 441 |
+ } |
|
| 442 |
+ |
|
| 443 |
+ var offset uint32 |
|
| 444 |
+ for {
|
|
| 445 |
+ if n == 0 {
|
|
| 446 |
+ w.Events <- newEvent("", sys_FS_Q_OVERFLOW)
|
|
| 447 |
+ w.Errors <- errors.New("short read in readEvents()")
|
|
| 448 |
+ break |
|
| 449 |
+ } |
|
| 450 |
+ |
|
| 451 |
+ // Point "raw" to the event in the buffer |
|
| 452 |
+ raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) |
|
| 453 |
+ buf := (*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName)) |
|
| 454 |
+ name := syscall.UTF16ToString(buf[:raw.FileNameLength/2]) |
|
| 455 |
+ fullname := watch.path + "\\" + name |
|
| 456 |
+ |
|
| 457 |
+ var mask uint64 |
|
| 458 |
+ switch raw.Action {
|
|
| 459 |
+ case syscall.FILE_ACTION_REMOVED: |
|
| 460 |
+ mask = sys_FS_DELETE_SELF |
|
| 461 |
+ case syscall.FILE_ACTION_MODIFIED: |
|
| 462 |
+ mask = sys_FS_MODIFY |
|
| 463 |
+ case syscall.FILE_ACTION_RENAMED_OLD_NAME: |
|
| 464 |
+ watch.rename = name |
|
| 465 |
+ case syscall.FILE_ACTION_RENAMED_NEW_NAME: |
|
| 466 |
+ if watch.names[watch.rename] != 0 {
|
|
| 467 |
+ watch.names[name] |= watch.names[watch.rename] |
|
| 468 |
+ delete(watch.names, watch.rename) |
|
| 469 |
+ mask = sys_FS_MOVE_SELF |
|
| 470 |
+ } |
|
| 471 |
+ } |
|
| 472 |
+ |
|
| 473 |
+ sendNameEvent := func() {
|
|
| 474 |
+ if w.sendEvent(fullname, watch.names[name]&mask) {
|
|
| 475 |
+ if watch.names[name]&sys_FS_ONESHOT != 0 {
|
|
| 476 |
+ delete(watch.names, name) |
|
| 477 |
+ } |
|
| 478 |
+ } |
|
| 479 |
+ } |
|
| 480 |
+ if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME {
|
|
| 481 |
+ sendNameEvent() |
|
| 482 |
+ } |
|
| 483 |
+ if raw.Action == syscall.FILE_ACTION_REMOVED {
|
|
| 484 |
+ w.sendEvent(fullname, watch.names[name]&sys_FS_IGNORED) |
|
| 485 |
+ delete(watch.names, name) |
|
| 486 |
+ } |
|
| 487 |
+ if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) {
|
|
| 488 |
+ if watch.mask&sys_FS_ONESHOT != 0 {
|
|
| 489 |
+ watch.mask = 0 |
|
| 490 |
+ } |
|
| 491 |
+ } |
|
| 492 |
+ if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME {
|
|
| 493 |
+ fullname = watch.path + "\\" + watch.rename |
|
| 494 |
+ sendNameEvent() |
|
| 495 |
+ } |
|
| 496 |
+ |
|
| 497 |
+ // Move to the next event in the buffer |
|
| 498 |
+ if raw.NextEntryOffset == 0 {
|
|
| 499 |
+ break |
|
| 500 |
+ } |
|
| 501 |
+ offset += raw.NextEntryOffset |
|
| 502 |
+ |
|
| 503 |
+ // Error! |
|
| 504 |
+ if offset >= n {
|
|
| 505 |
+ w.Errors <- errors.New("Windows system assumed buffer larger than it is, events have likely been missed.")
|
|
| 506 |
+ break |
|
| 507 |
+ } |
|
| 508 |
+ } |
|
| 509 |
+ |
|
| 510 |
+ if err := w.startRead(watch); err != nil {
|
|
| 511 |
+ w.Errors <- err |
|
| 512 |
+ } |
|
| 513 |
+ } |
|
| 514 |
+} |
|
| 515 |
+ |
|
| 516 |
+func (w *Watcher) sendEvent(name string, mask uint64) bool {
|
|
| 517 |
+ if mask == 0 {
|
|
| 518 |
+ return false |
|
| 519 |
+ } |
|
| 520 |
+ event := newEvent(name, uint32(mask)) |
|
| 521 |
+ select {
|
|
| 522 |
+ case ch := <-w.quit: |
|
| 523 |
+ w.quit <- ch |
|
| 524 |
+ case w.Events <- event: |
|
| 525 |
+ } |
|
| 526 |
+ return true |
|
| 527 |
+} |
|
| 528 |
+ |
|
| 529 |
+func toWindowsFlags(mask uint64) uint32 {
|
|
| 530 |
+ var m uint32 |
|
| 531 |
+ if mask&sys_FS_ACCESS != 0 {
|
|
| 532 |
+ m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS |
|
| 533 |
+ } |
|
| 534 |
+ if mask&sys_FS_MODIFY != 0 {
|
|
| 535 |
+ m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE |
|
| 536 |
+ } |
|
| 537 |
+ if mask&sys_FS_ATTRIB != 0 {
|
|
| 538 |
+ m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES |
|
| 539 |
+ } |
|
| 540 |
+ if mask&(sys_FS_MOVE|sys_FS_CREATE|sys_FS_DELETE) != 0 {
|
|
| 541 |
+ m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME |
|
| 542 |
+ } |
|
| 543 |
+ return m |
|
| 544 |
+} |
|
| 545 |
+ |
|
| 546 |
+func toFSnotifyFlags(action uint32) uint64 {
|
|
| 547 |
+ switch action {
|
|
| 548 |
+ case syscall.FILE_ACTION_ADDED: |
|
| 549 |
+ return sys_FS_CREATE |
|
| 550 |
+ case syscall.FILE_ACTION_REMOVED: |
|
| 551 |
+ return sys_FS_DELETE |
|
| 552 |
+ case syscall.FILE_ACTION_MODIFIED: |
|
| 553 |
+ return sys_FS_MODIFY |
|
| 554 |
+ case syscall.FILE_ACTION_RENAMED_OLD_NAME: |
|
| 555 |
+ return sys_FS_MOVED_FROM |
|
| 556 |
+ case syscall.FILE_ACTION_RENAMED_NEW_NAME: |
|
| 557 |
+ return sys_FS_MOVED_TO |
|
| 558 |
+ } |
|
| 559 |
+ return 0 |
|
| 560 |
+} |