From 6480be9802ac26c603dc55f6bb2f1a447e1a8ca9 Mon Sep 17 00:00:00 2001 From: Shulhan Date: Sun, 6 Mar 2022 01:33:06 +0700 Subject: all: move the DirWatcher and Watcher types from io to memfs There are two reasons why we move them. First, DirWatcher and Watcher code internally depends on the memfs package, especially on Node type. Second, we want to add new Watch method to MemFS which depends on package io. If we do that, there will be circular imports. --- lib/io/dirwatcher.go | 397 ------------------------------------------- lib/io/dirwatcher_test.go | 288 ------------------------------- lib/io/filestate.go | 32 ---- lib/io/io.go | 3 +- lib/io/nodestate.go | 19 --- lib/io/watchcallback.go | 12 -- lib/io/watcher.go | 154 ----------------- lib/io/watcher_test.go | 88 ---------- lib/memfs/dirwatcher.go | 396 ++++++++++++++++++++++++++++++++++++++++++ lib/memfs/dirwatcher_test.go | 287 +++++++++++++++++++++++++++++++ lib/memfs/filestate.go | 32 ++++ lib/memfs/nodestate.go | 15 ++ lib/memfs/watchcallback.go | 12 ++ lib/memfs/watcher.go | 153 +++++++++++++++++ lib/memfs/watcher_test.go | 88 ++++++++++ 15 files changed, 984 insertions(+), 992 deletions(-) delete mode 100644 lib/io/dirwatcher.go delete mode 100644 lib/io/dirwatcher_test.go delete mode 100644 lib/io/filestate.go delete mode 100644 lib/io/nodestate.go delete mode 100644 lib/io/watchcallback.go delete mode 100644 lib/io/watcher.go delete mode 100644 lib/io/watcher_test.go create mode 100644 lib/memfs/dirwatcher.go create mode 100644 lib/memfs/dirwatcher_test.go create mode 100644 lib/memfs/filestate.go create mode 100644 lib/memfs/nodestate.go create mode 100644 lib/memfs/watchcallback.go create mode 100644 lib/memfs/watcher.go create mode 100644 lib/memfs/watcher_test.go diff --git a/lib/io/dirwatcher.go b/lib/io/dirwatcher.go deleted file mode 100644 index 9ba59061..00000000 --- a/lib/io/dirwatcher.go +++ /dev/null @@ -1,397 +0,0 @@ -// Copyright 2019, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package io - -import ( - "fmt" - "log" - "os" - "sort" - "time" - - "github.com/shuLhan/share/lib/debug" - "github.com/shuLhan/share/lib/memfs" -) - -// -// DirWatcher is a naive implementation of directory change notification. -// -type DirWatcher struct { - root *memfs.Node - fs *memfs.MemFS - ticker *time.Ticker - - // Callback define a function that will be called when change detected - // on directory. - Callback WatchCallback - - // dirs contains list of directory and their sub-directories that is - // being watched for changes. - // The map key is relative path to directory and its value is a node - // information. - dirs map[string]*memfs.Node - - // This struct embed memfs.Options to map the directory to be watched - // into memory. - // - // The Root field define the directory that we want to watch. - // - // Includes contains list of regex to filter file names that we want - // to be notified. - // - // Excludes contains list of regex to filter file names that we did - // not want to be notified. - memfs.Options - - // Delay define a duration when the new changes will be fetched from - // system. - // This field is optional, minimum is 100 milli second and default is - // 5 seconds. - Delay time.Duration -} - -// -// Start watching changes in directory and its content. -// -func (dw *DirWatcher) Start() (err error) { - logp := "DirWatcher.Start" - - if dw.Delay < 100*time.Millisecond { - dw.Delay = time.Second * 5 - } - if dw.Callback == nil { - return fmt.Errorf("%s: callback is not defined", logp) - } - - fi, err := os.Stat(dw.Root) - if err != nil { - return fmt.Errorf("%s: %w", logp, err) - } - if !fi.IsDir() { - return fmt.Errorf("%s: %q is not a directory", logp, dw.Root) - } - - dw.Options.MaxFileSize = -1 - - dw.fs, err = memfs.New(&dw.Options) - if err != nil { - return fmt.Errorf("%s: %w", logp, err) - } - - dw.root = dw.fs.Root - - dw.dirs = make(map[string]*memfs.Node) - dw.mapSubdirs(dw.root) - go dw.start() - - return nil -} - -// Stop watching changes on directory. -func (dw *DirWatcher) Stop() { - dw.ticker.Stop() -} - -// dirsKeys return all the key in field dirs sorted in ascending order. -func (dw *DirWatcher) dirsKeys() (keys []string) { - var ( - key string - ) - for key = range dw.dirs { - keys = append(keys, key) - } - sort.Strings(keys) - return keys -} - -// -// mapSubdirs iterate each child node and check if its a directory or regular -// file. -// If its a directory add it to map of node and recursively iterate -// the childs. -// If its a regular file, start a NewWatcher. -// -func (dw *DirWatcher) mapSubdirs(node *memfs.Node) { - var ( - logp = "DirWatcher.mapSubdirs" - err error - ) - - for _, child := range node.Childs { - if child.IsDir() { - dw.dirs[child.Path] = child - dw.mapSubdirs(child) - continue - } - _, err = newWatcher(node, child, dw.Delay, dw.Callback) - if err != nil { - log.Printf("%s: %q: %s", logp, child.SysPath, err) - } - } -} - -// -// unmapSubdirs find sub directories in node's childrens, recursively and -// remove it from map of node. -// -func (dw *DirWatcher) unmapSubdirs(node *memfs.Node) { - for _, child := range node.Childs { - if child.IsDir() { - delete(dw.dirs, child.Path) - dw.unmapSubdirs(child) - } - dw.fs.RemoveChild(node, child) - } - if node.IsDir() { - delete(dw.dirs, node.Path) - } - dw.fs.RemoveChild(node.Parent, node) -} - -// -// onContentChange handle changes on the content of directory. -// -// It will re-read the list of files in node directory and compare them with -// old content to detect deletion and addition of files. -// -func (dw *DirWatcher) onContentChange(node *memfs.Node) { - var ( - logp = "onContentChange" - ) - - if debug.Value >= 2 { - fmt.Printf("%s: %+v\n", logp, node) - } - - f, err := os.Open(node.SysPath) - if err != nil { - log.Printf("%s: %s", logp, err) - return - } - - fis, err := f.Readdir(0) - if err != nil { - log.Printf("%s: %s", logp, err) - return - } - - err = f.Close() - if err != nil { - log.Printf("%s: %s", logp, err) - } - - // Find deleted files in directory. - for _, child := range node.Childs { - found := false - for _, newInfo := range fis { - if child.Name() == newInfo.Name() { - found = true - break - } - } - if found { - continue - } - if debug.Value >= 2 { - fmt.Printf("%s: %q deleted\n", logp, child.Path) - } - dw.unmapSubdirs(child) - } - - // Find new files in directory. - for _, newInfo := range fis { - found := false - for _, child := range node.Childs { - if newInfo.Name() == child.Name() { - found = true - break - } - } - if found { - continue - } - - newChild, err := dw.fs.AddChild(node, newInfo) - if err != nil { - log.Printf("%s: %s", logp, err) - continue - } - if newChild == nil { - // a node is excluded. - continue - } - - if debug.Value >= 2 { - fmt.Printf("%s: new child %s\n", logp, newChild.Path) - } - - ns := &NodeState{ - Node: newChild, - State: FileStateCreated, - } - - dw.Callback(ns) - - if newChild.IsDir() { - dw.dirs[newChild.Path] = newChild - dw.mapSubdirs(newChild) - dw.onContentChange(newChild) - continue - } - - // Start watching the file for modification. - _, err = newWatcher(node, newInfo, dw.Delay, dw.Callback) - if err != nil { - log.Printf("%s: %s", logp, err) - } - } -} - -// -// onRootCreated handle changes when the root directory that we watch get -// created again, after being deleted. -// It will send created event, and re-mount the root directory back to memory, -// recursively. -// -func (dw *DirWatcher) onRootCreated() { - var ( - logp = "DirWatcher.onRootCreated" - err error - ) - - dw.fs, err = memfs.New(&dw.Options) - if err != nil { - log.Printf("%s: %s", logp, err) - return - } - - dw.root, err = dw.fs.Get("/") - if err != nil { - log.Printf("%s: %s", logp, err) - return - } - - dw.dirs = make(map[string]*memfs.Node) - dw.mapSubdirs(dw.root) - - ns := &NodeState{ - Node: dw.root, - State: FileStateCreated, - } - - if debug.Value >= 2 { - fmt.Printf("%s: %s", logp, dw.root.Path) - } - - dw.Callback(ns) -} - -// -// onRootDeleted handle change when the root directory that we watch get -// deleted. It will send deleted event and unmount the root directory from -// memory. -// -func (dw *DirWatcher) onRootDeleted() { - ns := &NodeState{ - Node: dw.root, - State: FileStateDeleted, - } - - dw.fs = nil - dw.root = nil - dw.dirs = nil - - if debug.Value >= 2 { - fmt.Println("DirWatcher.onRootDeleted: root directory deleted") - } - - dw.Callback(ns) -} - -// -// onModified handle change when permission or attribute on node directory -// changed. -// -func (dw *DirWatcher) onModified(node *memfs.Node, newDirInfo os.FileInfo) { - dw.fs.Update(node, newDirInfo) - - ns := &NodeState{ - Node: node, - State: FileStateUpdateMode, - } - - dw.Callback(ns) - - if debug.Value >= 2 { - fmt.Printf("DirWatcher.onModified: %s\n", node.Path) - } -} - -func (dw *DirWatcher) start() { - logp := "DirWatcher" - - dw.ticker = time.NewTicker(dw.Delay) - - for range dw.ticker.C { - newDirInfo, err := os.Stat(dw.Root) - if err != nil { - if !os.IsNotExist(err) { - log.Printf("%s: %s", logp, err) - continue - } - if dw.fs != nil { - dw.onRootDeleted() - } - continue - } - if dw.fs == nil { - dw.onRootCreated() - dw.onContentChange(dw.root) - continue - } - if dw.root.Mode() != newDirInfo.Mode() { - dw.onModified(dw.root, newDirInfo) - continue - } - if dw.root.ModTime().Equal(newDirInfo.ModTime()) { - dw.processSubdirs() - continue - } - - dw.fs.Update(dw.root, newDirInfo) - dw.onContentChange(dw.root) - dw.processSubdirs() - } -} - -func (dw *DirWatcher) processSubdirs() { - logp := "processSubdirs" - - for _, node := range dw.dirs { - if debug.Value >= 3 { - fmt.Printf("%s: %q\n", logp, node.SysPath) - } - - newDirInfo, err := os.Stat(node.SysPath) - if err != nil { - if os.IsNotExist(err) { - dw.unmapSubdirs(node) - } else { - log.Printf("%s: %q: %s", logp, node.SysPath, err) - } - continue - } - if node.Mode() != newDirInfo.Mode() { - dw.onModified(node, newDirInfo) - continue - } - if node.ModTime().Equal(newDirInfo.ModTime()) { - continue - } - - dw.fs.Update(node, newDirInfo) - dw.onContentChange(node) - } -} diff --git a/lib/io/dirwatcher_test.go b/lib/io/dirwatcher_test.go deleted file mode 100644 index f6c40fc6..00000000 --- a/lib/io/dirwatcher_test.go +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright 2019, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package io - -import ( - "fmt" - "io/ioutil" - "log" - "os" - "path/filepath" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/shuLhan/share/lib/memfs" - "github.com/shuLhan/share/lib/test" -) - -func TestDirWatcher(t *testing.T) { - var wg sync.WaitGroup - - dir, err := ioutil.TempDir("", "libio") - if err != nil { - t.Fatal(err) - } - - t.Cleanup(func() { - _ = os.RemoveAll(dir) - }) - fmt.Printf(">>> Watching directory %q for changes ...\n", dir) - - exps := []struct { - path string - state FileState - }{{ - state: FileStateDeleted, - path: "/", - }, { - state: FileStateCreated, - path: "/", - }, { - state: FileStateCreated, - path: "/assets", - }, { - state: FileStateUpdateMode, - path: "/", - }, { - state: FileStateCreated, - path: "/new.adoc", - }, { - state: FileStateDeleted, - path: "/new.adoc", - }, { - state: FileStateCreated, - path: "/sub", - }, { - state: FileStateCreated, - path: "/sub/new.adoc", - }, { - state: FileStateDeleted, - path: "/sub/new.adoc", - }, { - state: FileStateCreated, - path: "/assets/new", - }, { - state: FileStateDeleted, - path: "/assets/new", - }} - - var x int32 - - dw := &DirWatcher{ - Options: memfs.Options{ - Root: dir, - Includes: []string{ - `assets/.*`, - `.*\.adoc$`, - }, - Excludes: []string{ - `.*\.html$`, - }, - }, - Delay: 150 * time.Millisecond, - Callback: func(ns *NodeState) { - localx := atomic.LoadInt32(&x) - if exps[localx].path != ns.Node.Path { - log.Fatalf("TestDirWatcher got node path %q, want %q\n", ns.Node.Path, exps[x].path) - } - if exps[localx].state != ns.State { - log.Fatalf("TestDirWatcher got state %d, want %d\n", ns.State, exps[x].state) - } - atomic.AddInt32(&x, 1) - wg.Done() - }, - } - - err = dw.Start() - if err != nil { - t.Fatal(err) - } - - // Delete the directory being watched. - t.Logf("Deleting root directory %q ...\n", dir) - wg.Add(1) - err = os.Remove(dir) - if err != nil { - t.Fatal(err) - } - wg.Wait() - - // Create the watched directory back with sub directory - // This will trigger two FileStateCreated events, one for "/" and one - // for "/assets". - dirAssets := filepath.Join(dir, "assets") - t.Logf("Re-create root directory %q ...\n", dirAssets) - wg.Add(2) - err = os.MkdirAll(dirAssets, 0770) - if err != nil { - t.Fatal(err) - } - wg.Wait() - - // Modify the permission on root directory - wg.Add(1) - t.Logf("Modify root directory %q ...\n", dir) - err = os.Chmod(dir, 0700) - if err != nil { - t.Fatal(err) - } - wg.Wait() - - // Add new file to watched directory. - newFile := filepath.Join(dir, "new.adoc") - t.Logf("Create new file on root directory: %q ...\n", newFile) - wg.Add(1) - err = ioutil.WriteFile(newFile, nil, 0600) - if err != nil { - t.Fatal(err) - } - wg.Wait() - - // Remove file. - t.Logf("Remove file on root directory: %q ...\n", newFile) - wg.Add(1) - err = os.Remove(newFile) - if err != nil { - t.Fatal(err) - } - wg.Wait() - - // Create sub-directory. - subDir := filepath.Join(dir, "sub") - t.Logf("Create new sub-directory: %q ...\n", subDir) - wg.Add(1) - err = os.Mkdir(subDir, 0770) - if err != nil { - t.Fatal(err) - } - wg.Wait() - - // Add new file in sub directory. - newFile = filepath.Join(subDir, "new.adoc") - t.Logf("Create new file in sub directory: %q ...\n", newFile) - wg.Add(1) - err = ioutil.WriteFile(newFile, nil, 0600) - if err != nil { - t.Fatal(err) - } - wg.Wait() - - // Remove file in sub directory. - t.Logf("Remove file in sub directory: %q ...\n", newFile) - wg.Add(1) - err = os.Remove(newFile) - if err != nil { - t.Fatal(err) - } - wg.Wait() - - // Create exclude file, should not trigger event. - newFile = filepath.Join(subDir, "new.html") - t.Logf("Create excluded file in sub directory: %q ...\n", newFile) - err = ioutil.WriteFile(newFile, nil, 0600) - if err != nil { - t.Fatal(err) - } - - // Create file without extension in white list directory "assets", - // should trigger event. - newFile = filepath.Join(dirAssets, "new") - t.Logf("Create new file on assets: %q ...\n", newFile) - wg.Add(1) - err = ioutil.WriteFile(newFile, nil, 0600) - if err != nil { - t.Fatal(err) - } - wg.Wait() - - wg.Add(1) - dw.Stop() -} - -// -// Test renaming sub-directory being watched. -// -func TestDirWatcher_renameDirectory(t *testing.T) { - var ( - logp = "TestDirWatcher_renameDirectory" - nsq = make(chan *NodeState) - - dw DirWatcher - err error - - rootDir string - subDir string - subDirFile string - newSubDir string - ) - - // - // Create a directory with its content to be watched. - // - // rootDir - // |_ subDir - // |_ subDirFile - // - - rootDir, err = os.MkdirTemp("", "") - if err != nil { - t.Fatal(err) - } - - t.Cleanup(func() { - err = os.RemoveAll(rootDir) - if err != nil { - t.Logf("%s: on cleanup: %s", logp, err) - } - }) - - subDir = filepath.Join(rootDir, "subdir") - err = os.Mkdir(subDir, 0700) - if err != nil { - t.Fatal(err) - } - - subDirFile = filepath.Join(subDir, "testfile") - err = os.WriteFile(subDirFile, []byte(`content of testfile`), 0600) - if err != nil { - t.Fatal(err) - } - - dw = DirWatcher{ - Callback: func(ns *NodeState) { - nsq <- ns - }, - Options: memfs.Options{ - Root: rootDir, - }, - Delay: 200 * time.Millisecond, - } - - err = dw.Start() - if err != nil { - t.Fatal(err) - } - - // Wait for all watcher started. - time.Sleep(400 * time.Millisecond) - - newSubDir = filepath.Join(rootDir, "newsubdir") - err = os.Rename(subDir, newSubDir) - if err != nil { - t.Fatal(err) - } - - <-nsq - <-nsq - <-nsq - - var expDirs = []string{ - "/newsubdir", - } - - test.Assert(t, "dirs", expDirs, dw.dirsKeys()) -} diff --git a/lib/io/filestate.go b/lib/io/filestate.go deleted file mode 100644 index 3532068d..00000000 --- a/lib/io/filestate.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2019, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package io - -// FileState define the state of file. -// There are four states of file: created, updated on mode, updated on content -// or deleted. -type FileState byte - -const ( - FileStateCreated FileState = iota // New file is created. - FileStateUpdateContent // The content of file is modified. - FileStateUpdateMode // The mode of file is modified. - FileStateDeleted // The file has been deleted. -) - -// String return the string representation of FileState. -func (fs FileState) String() (s string) { - switch fs { - case FileStateCreated: - s = "FileStateCreated" - case FileStateUpdateContent: - s = "FileStateUpdateContent" - case FileStateUpdateMode: - s = "FileStateUpdateMode" - case FileStateDeleted: - s = "FileStateDeleted" - } - return s -} diff --git a/lib/io/io.go b/lib/io/io.go index 22e30f0a..1312b023 100644 --- a/lib/io/io.go +++ b/lib/io/io.go @@ -3,8 +3,7 @@ // license that can be found in the LICENSE file. // -// Package io provide a library for reading and watching file, and reading -// from standard input. +// Package io extends the standard io library. // package io diff --git a/lib/io/nodestate.go b/lib/io/nodestate.go deleted file mode 100644 index 400c66bc..00000000 --- a/lib/io/nodestate.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2019, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package io - -import ( - "github.com/shuLhan/share/lib/memfs" -) - -// -// NodeState contains the information about the file and its state. -// -type NodeState struct { - // Node represent the file information. - Node *memfs.Node - // State of file, its either created, modified, or deleted. - State FileState -} diff --git a/lib/io/watchcallback.go b/lib/io/watchcallback.go deleted file mode 100644 index 475028bf..00000000 --- a/lib/io/watchcallback.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2019, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package io - -// -// WatchCallback is a function that will be called when Watcher or DirWatcher -// detect any changes on its file or directory. -// The watcher will pass the file information and its state. -// -type WatchCallback func(*NodeState) diff --git a/lib/io/watcher.go b/lib/io/watcher.go deleted file mode 100644 index f4c2e2e7..00000000 --- a/lib/io/watcher.go +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2018, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package io - -import ( - "fmt" - "log" - "os" - "path/filepath" - "time" - - "github.com/shuLhan/share/lib/debug" - "github.com/shuLhan/share/lib/memfs" -) - -// -// Watcher is a naive implementation of file event change notification. -// -type Watcher struct { - node *memfs.Node - ticker *time.Ticker - - // cb define a function that will be called when file modified or - // deleted. - cb WatchCallback - - // Delay define a duration when the new changes will be fetched from - // system. - // This field is optional, minimum is 100 millisecond and default is - // 5 seconds. - delay time.Duration -} - -// -// NewWatcher return a new file watcher that will inspect the file for changes -// with period specified by duration `d` argument. -// -// If duration is less or equal to 100 millisecond, it will be set to default -// duration (5 seconds). -// -func NewWatcher(path string, d time.Duration, cb WatchCallback) (w *Watcher, err error) { - logp := "NewWatcher" - - if len(path) == 0 { - return nil, fmt.Errorf("%s: path is empty", logp) - } - if cb == nil { - return nil, fmt.Errorf("%s: callback is not defined", logp) - } - - fi, err := os.Stat(path) - if err != nil { - return nil, fmt.Errorf("%s: %w", logp, err) - } - if fi.IsDir() { - return nil, fmt.Errorf("%s: path is directory", logp) - } - - dummyParent := &memfs.Node{ - SysPath: filepath.Dir(path), - } - dummyParent.Path = dummyParent.SysPath - - return newWatcher(dummyParent, fi, d, cb) -} - -// newWatcher create and initialize new Watcher like NewWatcher but using -// parent node. -func newWatcher(parent *memfs.Node, fi os.FileInfo, d time.Duration, cb WatchCallback) ( - w *Watcher, err error, -) { - logp := "newWatcher" - - // Create new node based on FileInfo without caching the content. - node, err := memfs.NewNode(parent, fi, -1) - if err != nil { - return nil, fmt.Errorf("%s: %w", logp, err) - } - - if d < 100*time.Millisecond { - d = time.Second * 5 - } - - w = &Watcher{ - delay: d, - cb: cb, - ticker: time.NewTicker(d), - node: node, - } - - go w.start() - - return w, nil -} - -// start fetching new file information every tick. -// This method run as goroutine and will finish when the file is deleted. -func (w *Watcher) start() { - logp := "Watcher" - if debug.Value >= 2 { - fmt.Printf("%s: %s: watching for changes\n", logp, w.node.SysPath) - } - for range w.ticker.C { - ns := &NodeState{ - Node: w.node, - } - - newInfo, err := os.Stat(w.node.SysPath) - if err != nil { - if debug.Value >= 2 { - fmt.Printf("%s: %s: deleted\n", logp, w.node.SysPath) - } - if !os.IsNotExist(err) { - log.Printf("%s: %s: %s", logp, w.node.SysPath, err) - continue - } - ns.State = FileStateDeleted - w.cb(ns) - w.node = nil - return - } - - if w.node.Mode() != newInfo.Mode() { - if debug.Value >= 2 { - fmt.Printf("%s: %s: mode updated\n", logp, w.node.SysPath) - } - ns.State = FileStateUpdateMode - w.node.SetMode(newInfo.Mode()) - w.cb(ns) - continue - } - if w.node.ModTime().Equal(newInfo.ModTime()) { - continue - } - if debug.Value >= 2 { - fmt.Printf("%s: %s: content updated\n", logp, w.node.SysPath) - } - - w.node.SetModTime(newInfo.ModTime()) - w.node.SetSize(newInfo.Size()) - - ns.State = FileStateUpdateContent - w.cb(ns) - } -} - -// -// Stop watching the file. -// -func (w *Watcher) Stop() { - w.ticker.Stop() -} diff --git a/lib/io/watcher_test.go b/lib/io/watcher_test.go deleted file mode 100644 index 55b131df..00000000 --- a/lib/io/watcher_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2018, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package io - -import ( - "io/ioutil" - "log" - "os" - "sync" - "testing" - "time" -) - -func TestWatcher(t *testing.T) { - var ( - wg sync.WaitGroup - content = "Write changes" - ) - - f, err := ioutil.TempFile("", "watcher") - if err != nil { - t.Fatal(err) - } - - exps := []struct { - state FileState - mode os.FileMode - size int64 - }{{ - state: FileStateUpdateMode, - mode: 0700, - }, { - state: FileStateUpdateContent, - mode: 0700, - size: int64(len(content)), - }, { - state: FileStateDeleted, - mode: 0700, - size: int64(len(content)), - }} - - x := 0 - _, err = NewWatcher(f.Name(), 150*time.Millisecond, func(ns *NodeState) { - if exps[x].state != ns.State { - log.Fatalf("Got state %s, want %s", ns.State, exps[x].state) - } - if exps[x].mode != ns.Node.Mode() { - log.Fatalf("Got mode %d, want %d", ns.Node.Mode(), exps[x].mode) - } - if exps[x].size != ns.Node.Size() { - log.Fatalf("Got size %d, want %d", ns.Node.Size(), exps[x].size) - } - x++ - wg.Done() - }) - if err != nil { - t.Fatal(err) - } - - // Update file mode - wg.Add(1) - err = f.Chmod(0700) - if err != nil { - t.Fatal(err) - } - wg.Wait() - - wg.Add(1) - _, err = f.WriteString(content) - if err != nil { - t.Fatal(err) - } - wg.Wait() - - err = f.Close() - if err != nil { - t.Fatal(err) - } - - wg.Add(1) - err = os.Remove(f.Name()) - if err != nil { - t.Fatal(err) - } - wg.Wait() -} diff --git a/lib/memfs/dirwatcher.go b/lib/memfs/dirwatcher.go new file mode 100644 index 00000000..6ad31867 --- /dev/null +++ b/lib/memfs/dirwatcher.go @@ -0,0 +1,396 @@ +// Copyright 2019, Shulhan . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package memfs + +import ( + "fmt" + "log" + "os" + "sort" + "time" + + "github.com/shuLhan/share/lib/debug" +) + +// +// DirWatcher is a naive implementation of directory change notification. +// +type DirWatcher struct { + root *Node + fs *MemFS + ticker *time.Ticker + + // Callback define a function that will be called when change detected + // on directory. + Callback WatchCallback + + // dirs contains list of directory and their sub-directories that is + // being watched for changes. + // The map key is relative path to directory and its value is a node + // information. + dirs map[string]*Node + + // This struct embed Options to map the directory to be watched + // into memory. + // + // The Root field define the directory that we want to watch. + // + // Includes contains list of regex to filter file names that we want + // to be notified. + // + // Excludes contains list of regex to filter file names that we did + // not want to be notified. + Options + + // Delay define a duration when the new changes will be fetched from + // system. + // This field is optional, minimum is 100 milli second and default is + // 5 seconds. + Delay time.Duration +} + +// +// Start watching changes in directory and its content. +// +func (dw *DirWatcher) Start() (err error) { + logp := "DirWatcher.Start" + + if dw.Delay < 100*time.Millisecond { + dw.Delay = time.Second * 5 + } + if dw.Callback == nil { + return fmt.Errorf("%s: callback is not defined", logp) + } + + fi, err := os.Stat(dw.Root) + if err != nil { + return fmt.Errorf("%s: %w", logp, err) + } + if !fi.IsDir() { + return fmt.Errorf("%s: %q is not a directory", logp, dw.Root) + } + + dw.Options.MaxFileSize = -1 + + dw.fs, err = New(&dw.Options) + if err != nil { + return fmt.Errorf("%s: %w", logp, err) + } + + dw.root = dw.fs.Root + + dw.dirs = make(map[string]*Node) + dw.mapSubdirs(dw.root) + go dw.start() + + return nil +} + +// Stop watching changes on directory. +func (dw *DirWatcher) Stop() { + dw.ticker.Stop() +} + +// dirsKeys return all the key in field dirs sorted in ascending order. +func (dw *DirWatcher) dirsKeys() (keys []string) { + var ( + key string + ) + for key = range dw.dirs { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +// +// mapSubdirs iterate each child node and check if its a directory or regular +// file. +// If its a directory add it to map of node and recursively iterate +// the childs. +// If its a regular file, start a NewWatcher. +// +func (dw *DirWatcher) mapSubdirs(node *Node) { + var ( + logp = "DirWatcher.mapSubdirs" + err error + ) + + for _, child := range node.Childs { + if child.IsDir() { + dw.dirs[child.Path] = child + dw.mapSubdirs(child) + continue + } + _, err = newWatcher(node, child, dw.Delay, dw.Callback) + if err != nil { + log.Printf("%s: %q: %s", logp, child.SysPath, err) + } + } +} + +// +// unmapSubdirs find sub directories in node's childrens, recursively and +// remove it from map of node. +// +func (dw *DirWatcher) unmapSubdirs(node *Node) { + for _, child := range node.Childs { + if child.IsDir() { + delete(dw.dirs, child.Path) + dw.unmapSubdirs(child) + } + dw.fs.RemoveChild(node, child) + } + if node.IsDir() { + delete(dw.dirs, node.Path) + } + dw.fs.RemoveChild(node.Parent, node) +} + +// +// onContentChange handle changes on the content of directory. +// +// It will re-read the list of files in node directory and compare them with +// old content to detect deletion and addition of files. +// +func (dw *DirWatcher) onContentChange(node *Node) { + var ( + logp = "onContentChange" + ) + + if debug.Value >= 2 { + fmt.Printf("%s: %+v\n", logp, node) + } + + f, err := os.Open(node.SysPath) + if err != nil { + log.Printf("%s: %s", logp, err) + return + } + + fis, err := f.Readdir(0) + if err != nil { + log.Printf("%s: %s", logp, err) + return + } + + err = f.Close() + if err != nil { + log.Printf("%s: %s", logp, err) + } + + // Find deleted files in directory. + for _, child := range node.Childs { + found := false + for _, newInfo := range fis { + if child.Name() == newInfo.Name() { + found = true + break + } + } + if found { + continue + } + if debug.Value >= 2 { + fmt.Printf("%s: %q deleted\n", logp, child.Path) + } + dw.unmapSubdirs(child) + } + + // Find new files in directory. + for _, newInfo := range fis { + found := false + for _, child := range node.Childs { + if newInfo.Name() == child.Name() { + found = true + break + } + } + if found { + continue + } + + newChild, err := dw.fs.AddChild(node, newInfo) + if err != nil { + log.Printf("%s: %s", logp, err) + continue + } + if newChild == nil { + // a node is excluded. + continue + } + + if debug.Value >= 2 { + fmt.Printf("%s: new child %s\n", logp, newChild.Path) + } + + ns := &NodeState{ + Node: newChild, + State: FileStateCreated, + } + + dw.Callback(ns) + + if newChild.IsDir() { + dw.dirs[newChild.Path] = newChild + dw.mapSubdirs(newChild) + dw.onContentChange(newChild) + continue + } + + // Start watching the file for modification. + _, err = newWatcher(node, newInfo, dw.Delay, dw.Callback) + if err != nil { + log.Printf("%s: %s", logp, err) + } + } +} + +// +// onRootCreated handle changes when the root directory that we watch get +// created again, after being deleted. +// It will send created event, and re-mount the root directory back to memory, +// recursively. +// +func (dw *DirWatcher) onRootCreated() { + var ( + logp = "DirWatcher.onRootCreated" + err error + ) + + dw.fs, err = New(&dw.Options) + if err != nil { + log.Printf("%s: %s", logp, err) + return + } + + dw.root, err = dw.fs.Get("/") + if err != nil { + log.Printf("%s: %s", logp, err) + return + } + + dw.dirs = make(map[string]*Node) + dw.mapSubdirs(dw.root) + + ns := &NodeState{ + Node: dw.root, + State: FileStateCreated, + } + + if debug.Value >= 2 { + fmt.Printf("%s: %s", logp, dw.root.Path) + } + + dw.Callback(ns) +} + +// +// onRootDeleted handle change when the root directory that we watch get +// deleted. It will send deleted event and unmount the root directory from +// memory. +// +func (dw *DirWatcher) onRootDeleted() { + ns := &NodeState{ + Node: dw.root, + State: FileStateDeleted, + } + + dw.fs = nil + dw.root = nil + dw.dirs = nil + + if debug.Value >= 2 { + fmt.Println("DirWatcher.onRootDeleted: root directory deleted") + } + + dw.Callback(ns) +} + +// +// onModified handle change when permission or attribute on node directory +// changed. +// +func (dw *DirWatcher) onModified(node *Node, newDirInfo os.FileInfo) { + dw.fs.Update(node, newDirInfo) + + ns := &NodeState{ + Node: node, + State: FileStateUpdateMode, + } + + dw.Callback(ns) + + if debug.Value >= 2 { + fmt.Printf("DirWatcher.onModified: %s\n", node.Path) + } +} + +func (dw *DirWatcher) start() { + logp := "DirWatcher" + + dw.ticker = time.NewTicker(dw.Delay) + + for range dw.ticker.C { + newDirInfo, err := os.Stat(dw.Root) + if err != nil { + if !os.IsNotExist(err) { + log.Printf("%s: %s", logp, err) + continue + } + if dw.fs != nil { + dw.onRootDeleted() + } + continue + } + if dw.fs == nil { + dw.onRootCreated() + dw.onContentChange(dw.root) + continue + } + if dw.root.Mode() != newDirInfo.Mode() { + dw.onModified(dw.root, newDirInfo) + continue + } + if dw.root.ModTime().Equal(newDirInfo.ModTime()) { + dw.processSubdirs() + continue + } + + dw.fs.Update(dw.root, newDirInfo) + dw.onContentChange(dw.root) + dw.processSubdirs() + } +} + +func (dw *DirWatcher) processSubdirs() { + logp := "processSubdirs" + + for _, node := range dw.dirs { + if debug.Value >= 3 { + fmt.Printf("%s: %q\n", logp, node.SysPath) + } + + newDirInfo, err := os.Stat(node.SysPath) + if err != nil { + if os.IsNotExist(err) { + dw.unmapSubdirs(node) + } else { + log.Printf("%s: %q: %s", logp, node.SysPath, err) + } + continue + } + if node.Mode() != newDirInfo.Mode() { + dw.onModified(node, newDirInfo) + continue + } + if node.ModTime().Equal(newDirInfo.ModTime()) { + continue + } + + dw.fs.Update(node, newDirInfo) + dw.onContentChange(node) + } +} diff --git a/lib/memfs/dirwatcher_test.go b/lib/memfs/dirwatcher_test.go new file mode 100644 index 00000000..28767e8e --- /dev/null +++ b/lib/memfs/dirwatcher_test.go @@ -0,0 +1,287 @@ +// Copyright 2019, Shulhan . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package memfs + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/shuLhan/share/lib/test" +) + +func TestDirWatcher(t *testing.T) { + var wg sync.WaitGroup + + dir, err := ioutil.TempDir("", "libio") + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + _ = os.RemoveAll(dir) + }) + fmt.Printf(">>> Watching directory %q for changes ...\n", dir) + + exps := []struct { + path string + state FileState + }{{ + state: FileStateDeleted, + path: "/", + }, { + state: FileStateCreated, + path: "/", + }, { + state: FileStateCreated, + path: "/assets", + }, { + state: FileStateUpdateMode, + path: "/", + }, { + state: FileStateCreated, + path: "/new.adoc", + }, { + state: FileStateDeleted, + path: "/new.adoc", + }, { + state: FileStateCreated, + path: "/sub", + }, { + state: FileStateCreated, + path: "/sub/new.adoc", + }, { + state: FileStateDeleted, + path: "/sub/new.adoc", + }, { + state: FileStateCreated, + path: "/assets/new", + }, { + state: FileStateDeleted, + path: "/assets/new", + }} + + var x int32 + + dw := &DirWatcher{ + Options: Options{ + Root: dir, + Includes: []string{ + `assets/.*`, + `.*\.adoc$`, + }, + Excludes: []string{ + `.*\.html$`, + }, + }, + Delay: 150 * time.Millisecond, + Callback: func(ns *NodeState) { + localx := atomic.LoadInt32(&x) + if exps[localx].path != ns.Node.Path { + log.Fatalf("TestDirWatcher got node path %q, want %q\n", ns.Node.Path, exps[x].path) + } + if exps[localx].state != ns.State { + log.Fatalf("TestDirWatcher got state %d, want %d\n", ns.State, exps[x].state) + } + atomic.AddInt32(&x, 1) + wg.Done() + }, + } + + err = dw.Start() + if err != nil { + t.Fatal(err) + } + + // Delete the directory being watched. + t.Logf("Deleting root directory %q ...\n", dir) + wg.Add(1) + err = os.Remove(dir) + if err != nil { + t.Fatal(err) + } + wg.Wait() + + // Create the watched directory back with sub directory + // This will trigger two FileStateCreated events, one for "/" and one + // for "/assets". + dirAssets := filepath.Join(dir, "assets") + t.Logf("Re-create root directory %q ...\n", dirAssets) + wg.Add(2) + err = os.MkdirAll(dirAssets, 0770) + if err != nil { + t.Fatal(err) + } + wg.Wait() + + // Modify the permission on root directory + wg.Add(1) + t.Logf("Modify root directory %q ...\n", dir) + err = os.Chmod(dir, 0700) + if err != nil { + t.Fatal(err) + } + wg.Wait() + + // Add new file to watched directory. + newFile := filepath.Join(dir, "new.adoc") + t.Logf("Create new file on root directory: %q ...\n", newFile) + wg.Add(1) + err = ioutil.WriteFile(newFile, nil, 0600) + if err != nil { + t.Fatal(err) + } + wg.Wait() + + // Remove file. + t.Logf("Remove file on root directory: %q ...\n", newFile) + wg.Add(1) + err = os.Remove(newFile) + if err != nil { + t.Fatal(err) + } + wg.Wait() + + // Create sub-directory. + subDir := filepath.Join(dir, "sub") + t.Logf("Create new sub-directory: %q ...\n", subDir) + wg.Add(1) + err = os.Mkdir(subDir, 0770) + if err != nil { + t.Fatal(err) + } + wg.Wait() + + // Add new file in sub directory. + newFile = filepath.Join(subDir, "new.adoc") + t.Logf("Create new file in sub directory: %q ...\n", newFile) + wg.Add(1) + err = ioutil.WriteFile(newFile, nil, 0600) + if err != nil { + t.Fatal(err) + } + wg.Wait() + + // Remove file in sub directory. + t.Logf("Remove file in sub directory: %q ...\n", newFile) + wg.Add(1) + err = os.Remove(newFile) + if err != nil { + t.Fatal(err) + } + wg.Wait() + + // Create exclude file, should not trigger event. + newFile = filepath.Join(subDir, "new.html") + t.Logf("Create excluded file in sub directory: %q ...\n", newFile) + err = ioutil.WriteFile(newFile, nil, 0600) + if err != nil { + t.Fatal(err) + } + + // Create file without extension in white list directory "assets", + // should trigger event. + newFile = filepath.Join(dirAssets, "new") + t.Logf("Create new file on assets: %q ...\n", newFile) + wg.Add(1) + err = ioutil.WriteFile(newFile, nil, 0600) + if err != nil { + t.Fatal(err) + } + wg.Wait() + + wg.Add(1) + dw.Stop() +} + +// +// Test renaming sub-directory being watched. +// +func TestDirWatcher_renameDirectory(t *testing.T) { + var ( + logp = "TestDirWatcher_renameDirectory" + nsq = make(chan *NodeState) + + dw DirWatcher + err error + + rootDir string + subDir string + subDirFile string + newSubDir string + ) + + // + // Create a directory with its content to be watched. + // + // rootDir + // |_ subDir + // |_ subDirFile + // + + rootDir, err = os.MkdirTemp("", "") + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + err = os.RemoveAll(rootDir) + if err != nil { + t.Logf("%s: on cleanup: %s", logp, err) + } + }) + + subDir = filepath.Join(rootDir, "subdir") + err = os.Mkdir(subDir, 0700) + if err != nil { + t.Fatal(err) + } + + subDirFile = filepath.Join(subDir, "testfile") + err = os.WriteFile(subDirFile, []byte(`content of testfile`), 0600) + if err != nil { + t.Fatal(err) + } + + dw = DirWatcher{ + Callback: func(ns *NodeState) { + nsq <- ns + }, + Options: Options{ + Root: rootDir, + }, + Delay: 200 * time.Millisecond, + } + + err = dw.Start() + if err != nil { + t.Fatal(err) + } + + // Wait for all watcher started. + time.Sleep(400 * time.Millisecond) + + newSubDir = filepath.Join(rootDir, "newsubdir") + err = os.Rename(subDir, newSubDir) + if err != nil { + t.Fatal(err) + } + + <-nsq + <-nsq + <-nsq + + var expDirs = []string{ + "/newsubdir", + } + + test.Assert(t, "dirs", expDirs, dw.dirsKeys()) +} diff --git a/lib/memfs/filestate.go b/lib/memfs/filestate.go new file mode 100644 index 00000000..6dc1bbcc --- /dev/null +++ b/lib/memfs/filestate.go @@ -0,0 +1,32 @@ +// Copyright 2019, Shulhan . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package memfs + +// FileState define the state of file. +// There are four states of file: created, updated on mode, updated on content +// or deleted. +type FileState byte + +const ( + FileStateCreated FileState = iota // New file is created. + FileStateUpdateContent // The content of file is modified. + FileStateUpdateMode // The mode of file is modified. + FileStateDeleted // The file has been deleted. +) + +// String return the string representation of FileState. +func (fs FileState) String() (s string) { + switch fs { + case FileStateCreated: + s = "FileStateCreated" + case FileStateUpdateContent: + s = "FileStateUpdateContent" + case FileStateUpdateMode: + s = "FileStateUpdateMode" + case FileStateDeleted: + s = "FileStateDeleted" + } + return s +} diff --git a/lib/memfs/nodestate.go b/lib/memfs/nodestate.go new file mode 100644 index 00000000..ba6001c7 --- /dev/null +++ b/lib/memfs/nodestate.go @@ -0,0 +1,15 @@ +// Copyright 2019, Shulhan . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package memfs + +// +// NodeState contains the information about the file and its state. +// +type NodeState struct { + // Node represent the file information. + Node *Node + // State of file, its either created, modified, or deleted. + State FileState +} diff --git a/lib/memfs/watchcallback.go b/lib/memfs/watchcallback.go new file mode 100644 index 00000000..b965c988 --- /dev/null +++ b/lib/memfs/watchcallback.go @@ -0,0 +1,12 @@ +// Copyright 2019, Shulhan . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package memfs + +// +// WatchCallback is a function that will be called when Watcher or DirWatcher +// detect any changes on its file or directory. +// The watcher will pass the file information and its state. +// +type WatchCallback func(*NodeState) diff --git a/lib/memfs/watcher.go b/lib/memfs/watcher.go new file mode 100644 index 00000000..b021e0d4 --- /dev/null +++ b/lib/memfs/watcher.go @@ -0,0 +1,153 @@ +// Copyright 2018, Shulhan . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package memfs + +import ( + "fmt" + "log" + "os" + "path/filepath" + "time" + + "github.com/shuLhan/share/lib/debug" +) + +// +// Watcher is a naive implementation of file event change notification. +// +type Watcher struct { + node *Node + ticker *time.Ticker + + // cb define a function that will be called when file modified or + // deleted. + cb WatchCallback + + // Delay define a duration when the new changes will be fetched from + // system. + // This field is optional, minimum is 100 millisecond and default is + // 5 seconds. + delay time.Duration +} + +// +// NewWatcher return a new file watcher that will inspect the file for changes +// with period specified by duration `d` argument. +// +// If duration is less or equal to 100 millisecond, it will be set to default +// duration (5 seconds). +// +func NewWatcher(path string, d time.Duration, cb WatchCallback) (w *Watcher, err error) { + logp := "NewWatcher" + + if len(path) == 0 { + return nil, fmt.Errorf("%s: path is empty", logp) + } + if cb == nil { + return nil, fmt.Errorf("%s: callback is not defined", logp) + } + + fi, err := os.Stat(path) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + if fi.IsDir() { + return nil, fmt.Errorf("%s: path is directory", logp) + } + + dummyParent := &Node{ + SysPath: filepath.Dir(path), + } + dummyParent.Path = dummyParent.SysPath + + return newWatcher(dummyParent, fi, d, cb) +} + +// newWatcher create and initialize new Watcher like NewWatcher but using +// parent node. +func newWatcher(parent *Node, fi os.FileInfo, d time.Duration, cb WatchCallback) ( + w *Watcher, err error, +) { + logp := "newWatcher" + + // Create new node based on FileInfo without caching the content. + node, err := NewNode(parent, fi, -1) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + + if d < 100*time.Millisecond { + d = time.Second * 5 + } + + w = &Watcher{ + delay: d, + cb: cb, + ticker: time.NewTicker(d), + node: node, + } + + go w.start() + + return w, nil +} + +// start fetching new file information every tick. +// This method run as goroutine and will finish when the file is deleted. +func (w *Watcher) start() { + logp := "Watcher" + if debug.Value >= 2 { + fmt.Printf("%s: %s: watching for changes\n", logp, w.node.SysPath) + } + for range w.ticker.C { + ns := &NodeState{ + Node: w.node, + } + + newInfo, err := os.Stat(w.node.SysPath) + if err != nil { + if debug.Value >= 2 { + fmt.Printf("%s: %s: deleted\n", logp, w.node.SysPath) + } + if !os.IsNotExist(err) { + log.Printf("%s: %s: %s", logp, w.node.SysPath, err) + continue + } + ns.State = FileStateDeleted + w.cb(ns) + w.node = nil + return + } + + if w.node.Mode() != newInfo.Mode() { + if debug.Value >= 2 { + fmt.Printf("%s: %s: mode updated\n", logp, w.node.SysPath) + } + ns.State = FileStateUpdateMode + w.node.SetMode(newInfo.Mode()) + w.cb(ns) + continue + } + if w.node.ModTime().Equal(newInfo.ModTime()) { + continue + } + if debug.Value >= 2 { + fmt.Printf("%s: %s: content updated\n", logp, w.node.SysPath) + } + + w.node.SetModTime(newInfo.ModTime()) + w.node.SetSize(newInfo.Size()) + + ns.State = FileStateUpdateContent + w.cb(ns) + } +} + +// +// Stop watching the file. +// +func (w *Watcher) Stop() { + w.ticker.Stop() +} diff --git a/lib/memfs/watcher_test.go b/lib/memfs/watcher_test.go new file mode 100644 index 00000000..246bc56b --- /dev/null +++ b/lib/memfs/watcher_test.go @@ -0,0 +1,88 @@ +// Copyright 2018, Shulhan . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package memfs + +import ( + "io/ioutil" + "log" + "os" + "sync" + "testing" + "time" +) + +func TestWatcher(t *testing.T) { + var ( + wg sync.WaitGroup + content = "Write changes" + ) + + f, err := ioutil.TempFile("", "watcher") + if err != nil { + t.Fatal(err) + } + + exps := []struct { + state FileState + mode os.FileMode + size int64 + }{{ + state: FileStateUpdateMode, + mode: 0700, + }, { + state: FileStateUpdateContent, + mode: 0700, + size: int64(len(content)), + }, { + state: FileStateDeleted, + mode: 0700, + size: int64(len(content)), + }} + + x := 0 + _, err = NewWatcher(f.Name(), 150*time.Millisecond, func(ns *NodeState) { + if exps[x].state != ns.State { + log.Fatalf("Got state %s, want %s", ns.State, exps[x].state) + } + if exps[x].mode != ns.Node.Mode() { + log.Fatalf("Got mode %d, want %d", ns.Node.Mode(), exps[x].mode) + } + if exps[x].size != ns.Node.Size() { + log.Fatalf("Got size %d, want %d", ns.Node.Size(), exps[x].size) + } + x++ + wg.Done() + }) + if err != nil { + t.Fatal(err) + } + + // Update file mode + wg.Add(1) + err = f.Chmod(0700) + if err != nil { + t.Fatal(err) + } + wg.Wait() + + wg.Add(1) + _, err = f.WriteString(content) + if err != nil { + t.Fatal(err) + } + wg.Wait() + + err = f.Close() + if err != nil { + t.Fatal(err) + } + + wg.Add(1) + err = os.Remove(f.Name()) + if err != nil { + t.Fatal(err) + } + wg.Wait() +} -- cgit v1.3