Skip to content

Commit

Permalink
Added fsnotify abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
bjconlan committed Jan 3, 2012
1 parent 00d3fa8 commit 1f58d42
Show file tree
Hide file tree
Showing 6 changed files with 1,211 additions and 0 deletions.
22 changes: 22 additions & 0 deletions src/exp/fsnotify/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
include $(GOROOT)/src/Make.inc

TARG=exp/fsnotify

GOFILES=\
fsnotify.go

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
35 changes: 35 additions & 0 deletions src/exp/fsnotify/fsnotify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package fsnotify

import "fmt"

// String formats the event e in the form
// "filename: DELETE|MODIFY|..."
func (e *FileEvent) String() string {
var events string = ""

if e.IsCreate() {
events += "|" + "CREATE"
}

if e.IsDelete() {
events += "|" + "DELETE"
}

if e.IsModify() {
events += "|" + "MODIFY"
}

if e.IsAttribute() {
events += "|" + "ATTRIB"
}

if e.IsRename() {
events += "|" + "RENAME"
}

if len(events) > 0 {
events = events[1:]
}

return fmt.Sprintf("%q: %s", e.Name, events)
}
284 changes: 284 additions & 0 deletions src/exp/fsnotify/fsnotify_bsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
// 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 (
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"syscall"
)

type FileEvent struct {
mask uint32 // Mask of events
Name string // File name (optional)
create bool // set by fsnotify package if found new file
}

// IsCreate reports whether the FileEvent was triggerd by a creation
func (e *FileEvent) IsCreate() bool { return e.create }

// 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)
finfo map[int]*os.FileInfo // Map of file information (isDir, isReg; key: watch descriptor)
Error chan 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, 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),
finfo: make(map[int]*os.FileInfo),
Event: make(chan *FileEvent),
Error: make(chan 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() 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) error {
if w.isClosed {
return errors.New("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, errno}
}
watchfd = fd

w.watches[path] = watchfd
w.paths[watchfd] = path

fi, _ := os.Stat(path)
w.finfo[watchfd] = &fi
}
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, errno}
} else if (watchEntry.Flags & syscall.EV_ERROR) == syscall.EV_ERROR {
return &os.PathError{"kevent_add_watch", path, errors.New(fmt.Sprintf("%v", watchEntry.Data))}
}

return nil
}

// Watch adds path to the watched file set, watching all events.
func (w *Watcher) Watch(path string) error {
return w.addWatch(path, NOTE_ALLEVENTS)
}

// RemoveWatch removes path from the watched file set.
func (w *Watcher) RemoveWatch(path string) error {
watchfd, ok := w.watches[path]
if !ok {
return errors.New(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", errors.New(fmt.Sprintf("%v", watchEntry.Data)))
}
delete(w.watches, path)
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 error // 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 != nil {
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)]

fileInfo := *w.finfo[int(watchEvent.Ident)]
if fileInfo.IsDir() && fileEvent.IsModify() {
w.sendDirectoryChangeEvents(fileEvent.Name)
} else {
// Send the event on the events channel
w.Event <- fileEvent
}

// Move to next event
events = events[1:]
}
}
}

// sendDirectoryEvents searches the directory for newly created files
// and sends them over the event channel. This functionality is to have
// the BSD version of fsnotify mach linux fsnotify which provides a
// create event for files created in a watched directory.
func (w *Watcher) sendDirectoryChangeEvents(dirPath string) {
// Get all files
files, err := ioutil.ReadDir(dirPath)
if err != nil {
w.Error <- err
}

// Search for new files
for _, fileInfo := range files {
if (fileInfo.Mode() & os.ModeType) == 0 { // isRegular()
filePath := path.Join(dirPath, fileInfo.Name())
if w.watches[filePath] == 0 {
// Watch file to mimic linux fsnotify
e := w.addWatch(filePath, NOTE_DELETE|NOTE_WRITE|NOTE_RENAME)
if e != nil {
w.Error <- e
}

// Send create event
fileEvent := new(FileEvent)
fileEvent.Name = filePath
fileEvent.create = true
w.Event <- fileEvent
}
}
}
}

const (
// Flags (from <sys/event.h>)
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
)
Loading

0 comments on commit 1f58d42

Please sign in to comment.