From e2bbdb94fcda47d087c4196749e8edd78bf4b984 Mon Sep 17 00:00:00 2001 From: Chris Howey Date: Fri, 14 Oct 2011 14:52:29 -0700 Subject: [PATCH] Initial commit --- Makefile | 19 +++ fsnotify_bsd.go | 270 +++++++++++++++++++++++++++++++++++++++++++ fsnotify_linux.go | 288 ++++++++++++++++++++++++++++++++++++++++++++++ fsnotify_test.go | 112 ++++++++++++++++++ 4 files changed, 689 insertions(+) create mode 100644 Makefile create mode 100644 fsnotify_bsd.go create mode 100644 fsnotify_linux.go create mode 100644 fsnotify_test.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c6e89e8 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +include $(GOROOT)/src/Make.inc + +TARG=exp/fsnotify + +GOFILES_linux=\ + fsnotify_linux.go\ + +GOFILES_freebsd=\ + fsnotify_bsd.go\ + +GOFILES_openbsd=\ + fsnotify_bsd.go\ + +GOFILES_darwin=\ + fsnotify_bsd.go\ + +GOFILES+=$(GOFILES_$(GOOS)) + +include $(GOROOT)/src/Make.pkg diff --git a/fsnotify_bsd.go b/fsnotify_bsd.go new file mode 100644 index 0000000..54e7fa9 --- /dev/null +++ b/fsnotify_bsd.go @@ -0,0 +1,270 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package fsnotify implements filesystem notification. + +Example: + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + err = watcher.Watch("/tmp") + if err != nil { + log.Fatal(err) + } + for { + select { + case ev := <-watcher.Event: + log.Println("event:", ev) + case err := <-watcher.Error: + log.Println("error:", err) + } + } + +*/ +package fsnotify + +import ( + "fmt" + "os" + "syscall" +) + +type FileEvent struct { + mask uint32 // Mask of events + Name string // File name (optional) +} + +// IsDelete reports whether the FileEvent was triggerd by a delete +func (e *FileEvent) IsDelete() bool { return (e.mask & NOTE_DELETE) == NOTE_DELETE } + +// IsModify reports whether the FileEvent was triggerd by a file modification +func (e *FileEvent) IsModify() bool { return (e.mask & NOTE_WRITE) == NOTE_WRITE } + +// IsAttribute reports whether the FileEvent was triggerd by a change of attributes +func (e *FileEvent) IsAttribute() bool { return (e.mask & NOTE_ATTRIB) == NOTE_ATTRIB } + +// IsRename reports whether the FileEvent was triggerd by a change name +func (e *FileEvent) IsRename() bool { return (e.mask & NOTE_RENAME) == NOTE_RENAME } + +type Watcher struct { + kq int // File descriptor (as returned by the kqueue() syscall) + watches map[string]int // Map of watched file diescriptors (key: path) + paths map[int]string // Map of watched paths (key: watch descriptor) + Error chan os.Error // Errors are sent on this channel + Event chan *FileEvent // Events are returned on this channel + done chan bool // Channel for sending a "quit message" to the reader goroutine + isClosed bool // Set to true when Close() is first called + kbuf [1]syscall.Kevent_t // An event buffer for Add/Remove watch +} + +// NewWatcher creates and returns a new kevent instance using kqueue(2) +func NewWatcher() (*Watcher, os.Error) { + fd, errno := syscall.Kqueue() + if fd == -1 { + return nil, os.NewSyscallError("kqueue", errno) + } + w := &Watcher{ + kq: fd, + watches: make(map[string]int), + paths: make(map[int]string), + Event: make(chan *FileEvent), + Error: make(chan os.Error), + done: make(chan bool, 1), + } + + go w.readEvents() + return w, nil +} + +// Close closes a kevent watcher instance +// It sends a message to the reader goroutine to quit and removes all watches +// associated with the kevent instance +func (w *Watcher) Close() os.Error { + if w.isClosed { + return nil + } + w.isClosed = true + + // Send "quit" message to the reader goroutine + w.done <- true + for path := range w.watches { + w.RemoveWatch(path) + } + + return nil +} + +// AddWatch adds path to the watched file set. +// The flags are interpreted as described in kevent(2). +func (w *Watcher) addWatch(path string, flags uint32) os.Error { + if w.isClosed { + return os.NewError("kevent instance already closed") + } + + watchEntry := &w.kbuf[0] + watchEntry.Fflags = flags + + watchfd, found := w.watches[path] + if !found { + fd, errno := syscall.Open(path, syscall.O_NONBLOCK|syscall.O_RDONLY, 0700) + if fd == -1 { + return &os.PathError{"kevent_add_watch", path, os.Errno(errno)} + } + watchfd = fd + + w.watches[path] = watchfd + w.paths[watchfd] = path + } + syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_ADD|syscall.EV_CLEAR) + + wd, errno := syscall.Kevent(w.kq, w.kbuf[:], nil, nil) + if wd == -1 { + return &os.PathError{"kevent_add_watch", path, os.Errno(errno)} + } else if (watchEntry.Flags & syscall.EV_ERROR) == syscall.EV_ERROR { + return &os.PathError{"kevent_add_watch", path, os.Errno(int(watchEntry.Data))} + } + + return nil +} + +// Watch adds path to the watched file set, watching all events. +func (w *Watcher) Watch(path string) os.Error { + return w.addWatch(path, NOTE_ALLEVENTS) +} + +// RemoveWatch removes path from the watched file set. +func (w *Watcher) RemoveWatch(path string) os.Error { + watchfd, ok := w.watches[path] + if !ok { + return os.NewError(fmt.Sprintf("can't remove non-existent kevent watch for: %s", path)) + } + syscall.Close(watchfd) + watchEntry := &w.kbuf[0] + syscall.SetKevent(watchEntry, w.watches[path], syscall.EVFILT_VNODE, syscall.EV_DELETE) + success, errno := syscall.Kevent(w.kq, w.kbuf[:], nil, nil) + if success == -1 { + return os.NewSyscallError("kevent_rm_watch", errno) + } else if (watchEntry.Flags & syscall.EV_ERROR) == syscall.EV_ERROR { + return os.NewSyscallError("kevent_rm_watch", int(watchEntry.Data)) + } + w.watches[path] = 0, false + return nil +} + +// readEvents reads from the kqueue file descriptor, converts the +// received events into Event objects and sends them via the Event channel +func (w *Watcher) readEvents() { + var ( + eventbuf [10]syscall.Kevent_t // Event buffer + events []syscall.Kevent_t // Received events + twait *syscall.Timespec // Time to block waiting for events + n int // Number of events returned from kevent + errno int // Syscall errno + ) + events = eventbuf[0:0] + twait = new(syscall.Timespec) + *twait = syscall.NsecToTimespec(keventWaitTime) + + for { + if len(events) == 0 { + n, errno = syscall.Kevent(w.kq, nil, eventbuf[:], twait) + events = eventbuf[0:n] + } + // See if there is a message on the "done" channel + var done bool + select { + case done = <-w.done: + default: + } + + // If "done" message is received + if done { + errno := syscall.Close(w.kq) + if errno == -1 { + w.Error <- os.NewSyscallError("close", errno) + } + close(w.Event) + close(w.Error) + return + } + if n < 0 { + w.Error <- os.NewSyscallError("kevent", errno) + continue + } + + // Timeout, no big deal + if n == 0 { + continue + } + + // Flush the events we recieved to the events channel + for len(events) > 0 { + fileEvent := new(FileEvent) + watchEvent := &events[0] + fileEvent.mask = uint32(watchEvent.Fflags) + fileEvent.Name = w.paths[int(watchEvent.Ident)] + + // Send the event on the events channel + w.Event <- fileEvent + + // Move to next event + events = events[1:] + } + } +} + +// String formats the event e in the form +// "filename: 0xEventMask = NOTE_EXTEND|NOTE_ATTRIB|..." +func (e *FileEvent) String() string { + var events string = "" + + m := e.mask + for _, b := range eventBits { + if m&b.Value != 0 { + m &^= b.Value + events += "|" + b.Name + } + } + + if m != 0 { + events += fmt.Sprintf("|%#x", m) + } + if len(events) > 0 { + events = " == " + events[1:] + } + + return fmt.Sprintf("%q: %#x%s", e.Name, e.mask, events) +} + +const ( + // Flags (from ) + NOTE_DELETE = 0x0001 /* vnode was removed */ + NOTE_WRITE = 0x0002 /* data contents changed */ + NOTE_EXTEND = 0x0004 /* size increased */ + NOTE_ATTRIB = 0x0008 /* attributes changed */ + NOTE_LINK = 0x0010 /* link count changed */ + NOTE_RENAME = 0x0020 /* vnode was renamed */ + NOTE_REVOKE = 0x0040 /* vnode access was revoked */ + + // Watch all events + NOTE_ALLEVENTS = NOTE_DELETE | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME + + // Block for 100 ms on each call to kevent + keventWaitTime = 100e6 +) + +var eventBits = []struct { + Value uint32 + Name string +}{ + {NOTE_DELETE, "NOTE_DELETE"}, + {NOTE_WRITE, "NOTE_WRITE"}, + {NOTE_EXTEND, "NOTE_EXTEND"}, + {NOTE_ATTRIB, "NOTE_ATTRIB"}, + {NOTE_LINK, "NOTE_LINK"}, + {NOTE_RENAME, "NOTE_RENAME"}, + {NOTE_REVOKE, "NOTE_REVOKE"}, +} diff --git a/fsnotify_linux.go b/fsnotify_linux.go new file mode 100644 index 0000000..99fa516 --- /dev/null +++ b/fsnotify_linux.go @@ -0,0 +1,288 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package inotify implements a wrapper for the Linux inotify system. + +Example: + watcher, err := inotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + err = watcher.Watch("/tmp") + if err != nil { + log.Fatal(err) + } + for { + select { + case ev := <-watcher.Event: + log.Println("event:", ev) + case err := <-watcher.Error: + log.Println("error:", err) + } + } + +*/ +package inotify + +import ( + "fmt" + "os" + "strings" + "syscall" + "unsafe" +) + +type Event struct { + Mask uint32 // Mask of events + Cookie uint32 // Unique cookie associating related events (for rename(2)) + Name string // File name (optional) +} + +type watch struct { + wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) + flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) +} + +type Watcher struct { + fd int // File descriptor (as returned by the inotify_init() syscall) + watches map[string]*watch // Map of inotify watches (key: path) + paths map[int]string // Map of watched paths (key: watch descriptor) + Error chan os.Error // Errors are sent on this channel + Event chan *Event // Events are returned on this channel + done chan bool // Channel for sending a "quit message" to the reader goroutine + isClosed bool // Set to true when Close() is first called +} + +// NewWatcher creates and returns a new inotify instance using inotify_init(2) +func NewWatcher() (*Watcher, os.Error) { + fd, errno := syscall.InotifyInit() + if fd == -1 { + return nil, os.NewSyscallError("inotify_init", errno) + } + w := &Watcher{ + fd: fd, + watches: make(map[string]*watch), + paths: make(map[int]string), + Event: make(chan *Event), + Error: make(chan os.Error), + done: make(chan bool, 1), + } + + go w.readEvents() + return w, nil +} + +// Close closes an inotify watcher instance +// It sends a message to the reader goroutine to quit and removes all watches +// associated with the inotify instance +func (w *Watcher) Close() os.Error { + if w.isClosed { + return nil + } + w.isClosed = true + + // Send "quit" message to the reader goroutine + w.done <- true + for path := range w.watches { + w.RemoveWatch(path) + } + + return nil +} + +// AddWatch adds path to the watched file set. +// The flags are interpreted as described in inotify_add_watch(2). +func (w *Watcher) AddWatch(path string, flags uint32) os.Error { + if w.isClosed { + return os.NewError("inotify instance already closed") + } + + watchEntry, found := w.watches[path] + if found { + watchEntry.flags |= flags + flags |= syscall.IN_MASK_ADD + } + wd, errno := syscall.InotifyAddWatch(w.fd, path, flags) + if wd == -1 { + return &os.PathError{"inotify_add_watch", path, os.Errno(errno)} + } + + if !found { + w.watches[path] = &watch{wd: uint32(wd), flags: flags} + w.paths[wd] = path + } + return nil +} + +// Watch adds path to the watched file set, watching all events. +func (w *Watcher) Watch(path string) os.Error { + return w.AddWatch(path, IN_ALL_EVENTS) +} + +// RemoveWatch removes path from the watched file set. +func (w *Watcher) RemoveWatch(path string) os.Error { + watch, ok := w.watches[path] + if !ok { + return os.NewError(fmt.Sprintf("can't remove non-existent inotify watch for: %s", path)) + } + success, errno := syscall.InotifyRmWatch(w.fd, watch.wd) + if success == -1 { + return os.NewSyscallError("inotify_rm_watch", errno) + } + w.watches[path] = nil, false + return nil +} + +// readEvents reads from the inotify file descriptor, converts the +// received events into Event objects and sends them via the Event channel +func (w *Watcher) readEvents() { + var ( + buf [syscall.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events + n int // Number of bytes read with read() + errno int // Syscall errno + ) + + for { + n, errno = syscall.Read(w.fd, buf[0:]) + // See if there is a message on the "done" channel + var done bool + select { + case done = <-w.done: + default: + } + + // If EOF or a "done" message is received + if n == 0 || done { + errno := syscall.Close(w.fd) + if errno == -1 { + w.Error <- os.NewSyscallError("close", errno) + } + close(w.Event) + close(w.Error) + return + } + if n < 0 { + w.Error <- os.NewSyscallError("read", errno) + continue + } + if n < syscall.SizeofInotifyEvent { + w.Error <- os.NewError("inotify: short read in readEvents()") + continue + } + + var offset uint32 = 0 + // We don't know how many events we just read into the buffer + // While the offset points to at least one whole event... + for offset <= uint32(n-syscall.SizeofInotifyEvent) { + // Point "raw" to the event in the buffer + raw := (*syscall.InotifyEvent)(unsafe.Pointer(&buf[offset])) + event := new(Event) + event.Mask = uint32(raw.Mask) + event.Cookie = uint32(raw.Cookie) + nameLen := uint32(raw.Len) + // If the event happened to the watched directory or the watched file, the kernel + // doesn't append the filename to the event, but we would like to always fill the + // the "Name" field with a valid filename. We retrieve the path of the watch from + // the "paths" map. + event.Name = w.paths[int(raw.Wd)] + if nameLen > 0 { + // Point "bytes" at the first byte of the filename + bytes := (*[syscall.PathMax]byte)(unsafe.Pointer(&buf[offset+syscall.SizeofInotifyEvent])) + // The filename is padded with NUL bytes. TrimRight() gets rid of those. + event.Name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") + } + // Send the event on the events channel + w.Event <- event + + // Move to the next event in the buffer + offset += syscall.SizeofInotifyEvent + nameLen + } + } +} + +// String formats the event e in the form +// "filename: 0xEventMask = IN_ACCESS|IN_ATTRIB_|..." +func (e *Event) String() string { + var events string = "" + + m := e.Mask + for _, b := range eventBits { + if m&b.Value != 0 { + m &^= b.Value + events += "|" + b.Name + } + } + + if m != 0 { + events += fmt.Sprintf("|%#x", m) + } + if len(events) > 0 { + events = " == " + events[1:] + } + + return fmt.Sprintf("%q: %#x%s", e.Name, e.Mask, events) +} + +const ( + // Options for inotify_init() are not exported + // IN_CLOEXEC uint32 = syscall.IN_CLOEXEC + // IN_NONBLOCK uint32 = syscall.IN_NONBLOCK + + // Options for AddWatch + IN_DONT_FOLLOW uint32 = syscall.IN_DONT_FOLLOW + IN_ONESHOT uint32 = syscall.IN_ONESHOT + IN_ONLYDIR uint32 = syscall.IN_ONLYDIR + + // The "IN_MASK_ADD" option is not exported, as AddWatch + // adds it automatically, if there is already a watch for the given path + // IN_MASK_ADD uint32 = syscall.IN_MASK_ADD + + // Events + IN_ACCESS uint32 = syscall.IN_ACCESS + IN_ALL_EVENTS uint32 = syscall.IN_ALL_EVENTS + IN_ATTRIB uint32 = syscall.IN_ATTRIB + IN_CLOSE uint32 = syscall.IN_CLOSE + IN_CLOSE_NOWRITE uint32 = syscall.IN_CLOSE_NOWRITE + IN_CLOSE_WRITE uint32 = syscall.IN_CLOSE_WRITE + IN_CREATE uint32 = syscall.IN_CREATE + IN_DELETE uint32 = syscall.IN_DELETE + IN_DELETE_SELF uint32 = syscall.IN_DELETE_SELF + IN_MODIFY uint32 = syscall.IN_MODIFY + IN_MOVE uint32 = syscall.IN_MOVE + IN_MOVED_FROM uint32 = syscall.IN_MOVED_FROM + IN_MOVED_TO uint32 = syscall.IN_MOVED_TO + IN_MOVE_SELF uint32 = syscall.IN_MOVE_SELF + IN_OPEN uint32 = syscall.IN_OPEN + + // Special events + IN_ISDIR uint32 = syscall.IN_ISDIR + IN_IGNORED uint32 = syscall.IN_IGNORED + IN_Q_OVERFLOW uint32 = syscall.IN_Q_OVERFLOW + IN_UNMOUNT uint32 = syscall.IN_UNMOUNT +) + +var eventBits = []struct { + Value uint32 + Name string +}{ + {IN_ACCESS, "IN_ACCESS"}, + {IN_ATTRIB, "IN_ATTRIB"}, + {IN_CLOSE, "IN_CLOSE"}, + {IN_CLOSE_NOWRITE, "IN_CLOSE_NOWRITE"}, + {IN_CLOSE_WRITE, "IN_CLOSE_WRITE"}, + {IN_CREATE, "IN_CREATE"}, + {IN_DELETE, "IN_DELETE"}, + {IN_DELETE_SELF, "IN_DELETE_SELF"}, + {IN_MODIFY, "IN_MODIFY"}, + {IN_MOVE, "IN_MOVE"}, + {IN_MOVED_FROM, "IN_MOVED_FROM"}, + {IN_MOVED_TO, "IN_MOVED_TO"}, + {IN_MOVE_SELF, "IN_MOVE_SELF"}, + {IN_OPEN, "IN_OPEN"}, + {IN_ISDIR, "IN_ISDIR"}, + {IN_IGNORED, "IN_IGNORED"}, + {IN_Q_OVERFLOW, "IN_Q_OVERFLOW"}, + {IN_UNMOUNT, "IN_UNMOUNT"}, +} diff --git a/fsnotify_test.go b/fsnotify_test.go new file mode 100644 index 0000000..bc6ffe4 --- /dev/null +++ b/fsnotify_test.go @@ -0,0 +1,112 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fsnotify + +import ( + "os" + "time" + "testing" +) + +func TestFsnotifyEvents(t *testing.T) { + // Create an fsnotify watcher instance and initialize it + watcher, err := NewWatcher() + if err != nil { + t.Fatalf("NewWatcher() failed: %s", err) + } + + const testDir string = "_test" + + // Add a watch for testDir + err = watcher.Watch(testDir) + if err != nil { + t.Fatalf("Watcher.Watch() failed: %s", err) + } + + // Receive errors on the error channel on a separate goroutine + go func() { + for err := range watcher.Error { + t.Fatalf("error received: %s", err) + } + }() + + const testFile string = "_test/TestFsnotifyEvents.testfile" + + // Receive events on the event channel on a separate goroutine + eventstream := watcher.Event + var eventsReceived = 0 + done := make(chan bool) + go func() { + for event := range eventstream { + // Only count relevant events + if event.Name == testDir || event.Name == testFile { + eventsReceived++ + t.Logf("event received: %s", event) + } else { + t.Logf("unexpected event received: %s", event) + } + } + done <- true + }() + + // Create a file + // This should add at least one event to the fsnotify event queue + var f *os.File + f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + + // Add a watch for testFile + err = watcher.Watch(testFile) + if err != nil { + t.Fatalf("Watcher.Watch() failed: %s", err) + } + + f.WriteString("data") + f.Sync() + f.Close() + + os.Remove(testFile) + + // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + time.Sleep(500e6) // 500 ms + if eventsReceived == 0 { + t.Fatal("fsnotify event hasn't been received after 500 ms") + } + + // Try closing the fsnotify instance + t.Log("calling Close()") + watcher.Close() + t.Log("waiting for the event channel to become closed...") + select { + case <-done: + t.Log("event channel closed") + case <-time.After(1e9): + t.Fatal("event stream was not closed after 1 second") + } +} + +func TestFsnotifyClose(t *testing.T) { + watcher, _ := NewWatcher() + watcher.Close() + + done := false + go func() { + watcher.Close() + done = true + }() + + time.Sleep(50e6) // 50 ms + if !done { + t.Fatal("double Close() test failed: second Close() call didn't return") + } + + err := watcher.Watch("_test") + if err == nil { + t.Fatal("expected error on Watch() after Close(), got nil") + } +} -- 2.50.1