diff --git a/README.md b/README.md index 11a91c3..c56183c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -notify [![GoDoc](https://godoc.org/github.com/rjeczalik/notify?status.svg)](https://godoc.org/github.com/rjeczalik/notify) [![Build Status](https://img.shields.io/travis/rjeczalik/notify/master.svg)](https://travis-ci.org/rjeczalik/notify "inotify") [![Build Status](https://img.shields.io/travis/rjeczalik/notify/fsevents.svg)](https://travis-ci.org/rjeczalik/notify "FSEvents") [![Build Status](https://img.shields.io/travis/rjeczalik/notify/kqueue.svg)](https://travis-ci.org/rjeczalik/notify "kqueue") [![Build status](https://img.shields.io/appveyor/ci/rjeczalik/notify-246.svg)](https://ci.appveyor.com/project/rjeczalik/notify-246 "ReadDirectoryChangesW") [![Coverage Status](https://img.shields.io/coveralls/rjeczalik/notify/master.svg)](https://coveralls.io/r/rjeczalik/notify?branch=master) +[![Build Status](https://img.shields.io/travis/rjeczalik/notify/master.svg)](https://travis-ci.org/rjeczalik/notify "inotify") [![Build Status](https://img.shields.io/travis/rjeczalik/notify/fsevents.svg)](https://travis-ci.org/rjeczalik/notify "FSEvents") [![Build Status](https://img.shields.io/travis/rjeczalik/notify/kqueue.svg)](https://travis-ci.org/rjeczalik/notify "kqueue") [![Build status](https://img.shields.io/appveyor/ci/rjeczalik/notify-246.svg)](https://ci.appveyor.com/project/rjeczalik/notify-246 "ReadDirectoryChangesW") [![Coverage Status](https://img.shields.io/coveralls/rjeczalik/notify/master.svg)](https://coveralls.io/r/rjeczalik/notify?branch=master) ====== +### notify [![GoDoc](https://godoc.org/github.com/rjeczalik/notify?status.svg)](https://godoc.org/github.com/rjeczalik/notify) + Filesystem event notification library on steroids. (under active development) *Installation* @@ -9,6 +11,40 @@ Filesystem event notification library on steroids. (under active development) ~ $ go get -u github.com/rjeczalik/notify ``` -*Documentation* +*Documentation* [godoc.org/github.com/rjeczalik/notify](https://godoc.org/github.com/rjeczalik/notify) + +### cmd/notify [![GoDoc](https://godoc.org/github.com/rjeczalik/notify?status.svg)](https://godoc.org/github.com/rjeczalik/notify) + +Listens on filesystem changes and forwards received events to user-defined handlers. + +*Installation* + +``` +~ $ go get -u github.com/rjeczalik/notify/cmd/notify +``` + +*Documentation* + +[godoc.org/github.com/rjeczalik/notify/cmd/notify](https://godoc.org/github.com/rjeczalik/notify/cmd/notify) + +*Usage* + +```bash +~ $ notify -c 'echo "Hello from handler! (event={{.Event}}, path={{.Path}})"' +2015/02/17 01:17:40 received notify.Create: "/Users/rjeczalik/notify.tmp" +Hello from handler! (event=create, path=/Users/rjeczalik/notify.tmp) +2015/02/17 01:18:13 received notify.Write: "/Users/rjeczalik/notify.tmp" +Hello from handler! (event=write, path=/Users/rjeczalik/notify.tmp) +``` +```bash +~ $ cat > handler < echo "Hello from handler! (event={{.Event}}, path={{.Path}})" +> EOF +~ $ notify -f handler +2015/02/17 01:22:26 received notify.Create: "/Users/rjeczalik/notify.tmp" +Hello from handler! (event=create, path=/Users/rjeczalik/notify.tmp) +2015/02/17 01:22:26 received notify.Remove: "/Users/rjeczalik/notify.tmp" +Hello from handler! (event=remove, path=/Users/rjeczalik/notify.tmp) +``` diff --git a/cmd/notify/main.go b/cmd/notify/main.go index 4f55839..25cdad6 100644 --- a/cmd/notify/main.go +++ b/cmd/notify/main.go @@ -2,41 +2,194 @@ // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. +// Command notify listens on filesystem changes and forwards received mapping to +// user-defined handlers. +// +// Usage +// +// usage: notify [-c command] [-f script file] [path]... +// +// The -c flag registers a command handler, which uses the syntax +// of package template. Notify passes struct to the template, +// splits produced string into command and args, and runs it using +// exec.Command(). Additionaly the path and event type values are +// accesible to the process via NOTIFY_PATH and NOTIFY_EVENT +// environment variables. +// +// The struct being passed to the template is: +// +// type Event struct { +// Path string +// Event string +// } +// +// Values for the Event field are: +// +// - create +// - remove +// - rename +// - write +// +// The -t flag registers a file handler, which works similary +// to the -c handler. The only difference the template is read from +// the given file instead of the command line. +// +// The path argument tells notify which director or directories to +// listen on. By default notify listens recursively in current working +// directory. +// +// If no handler is specified notify prints each event to os.Stdout. package main import ( + "bytes" + "flag" "fmt" + "io/ioutil" + "log" "os" + "os/exec" + "runtime" "strings" - "time" + "text/template" "github.com/rjeczalik/notify" ) -const usage = "usage: notify path [EVENT...]" +const usage = `usage: notify [-c command] [-f script file] [path]... -const tformat = "2006-01-02 15:04:05.0000" +Listens on filesystem changes and forwards received mapping to +user-defined handlers. -var event = map[string]notify.Event{ - "all": notify.All, - "create": notify.Create, - "delete": notify.Remove, - "write": notify.Write, - "move": notify.Rename, +The -c flag registers a command handler, which uses the syntax +of package template. Notify passes struct to the template, +splits produced string into command and args, and runs it using +exec.Command(). Additionaly the path and event type values are +accesible to the process via NOTIFY_PATH and NOTIFY_EVENT +environment variables. + +The struct being passed to the template is: + + type Event struct { + Path string + Event string + } + +Values for the Event field are: + + - create + - remove + - rename + - write + +The -t flag registers a file handler, which works similary +to the -c handler. The only difference the template is read from +the given file instead of the command line. + +The path argument tells notify which director or directories to +listen on. By default notify listens recursively in current working +directory. + +If no handler is specified notify prints each event to os.Stdout.` + +var ( + file string + command string + paths = []string{"." + string(os.PathSeparator) + "..."} + env = newenv() +) + +var mapping = map[notify.Event]string{ + notify.Create: "create", + notify.Remove: "remove", + notify.Rename: "rename", + notify.Write: "write", } -func parse(s []string) (e []notify.Event) { - if len(s) == 0 { - return []notify.Event{notify.All} - } - for _, s := range s { - event, ok := event[strings.ToLower(s)] - if !ok { - die("invalid event: " + s) +func newenv() func(Event) []string { + env := os.Environ() + for i, s := range env { + s = strings.ToLower(s) + if strings.Contains(s, "NOTIFY_PATH=") || strings.Contains(s, "NOTIFY_EVENT=") { + env[i], env = env[len(env)-1], env[:len(env)-1] } - e = append(e, event) } - return + env = append(env, "", "") + return func(e Event) []string { + s := make([]string, len(env)) + copy(s, env) + s[len(s)-1] = "NOTIFY_EVENT=" + e.Event + s[len(s)-2] = "NOTIFY_PATH=" + e.Path + return s + } +} + +// Handler TODO(rjeczalik) +type Handler struct { + tmpl *template.Template + env []string +} + +// NewHandler TODO(rjeczalik) +func NewHandler(text string) (*Handler, error) { + tmpl, err := template.New("main.Handler").Parse(text) + if err != nil { + return nil, err + } + h := &Handler{ + tmpl: tmpl, + env: env(Event{}), + } + return h, nil +} + +// Run TODO(rjeczalik) +func (h *Handler) Run(e Event) error { + var buf bytes.Buffer + if err := h.tmpl.Execute(&buf, e); err != nil { + return err + } + s := buf.String() + var cmd *exec.Cmd + switch runtime.GOOS { + case "windows": + cmd = exec.Command("cmd", "/c", s) + default: + cmd = exec.Command("/bin/sh", "-c", s) + } + h.env[len(h.env)-1] = "NOTIFY_EVENT=" + e.Event + h.env[len(h.env)-2] = "NOTIFY_PATH=" + e.Path + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = h.env + return cmd.Run() +} + +// Daemon TODO(rjeczalik) +func (h *Handler) Daemon() chan<- Event { + c := make(chan Event) + go func() { + for e := range c { + if err := h.Run(e); err != nil { + log.Println("handler error:", err) + } + } + }() + return c +} + +// Event TODO(rjeczalik) +type Event struct { + Path string + Event string +} + +// NewEvent TODO(rjeczalik) +func NewEvent(ei notify.EventInfo) Event { + return Event{ + Path: ei.Path(), + Event: mapping[ei.Event()], + } } func die(v interface{}) { @@ -44,20 +197,57 @@ func die(v interface{}) { os.Exit(1) } -func main() { - if len(os.Args) == 1 { - die(usage) +func init() { + flag.CommandLine.Usage = func() { + fmt.Fprintln(os.Stderr, usage) } - for _, path := range strings.Split(os.Args[1], string(os.PathListSeparator)) { - ch := make(chan notify.EventInfo, 10) - if err := notify.Watch(path, ch, parse(os.Args[2:])...); err != nil { + flag.StringVar(&file, "f", "", "script file to execute on received event") + flag.StringVar(&command, "c", "", "command to run on received event") + flag.Parse() + if flag.NArg() != 0 { + paths = flag.Args() + } +} + +func main() { + var handlers []*Handler + if command != "" { + h, err := NewHandler(command) + if err != nil { die(err) } - go func(path string) { - for ei := range ch { - fmt.Printf("[%v] [%s] Event: %v\n", time.Now().Format(tformat), path, ei) + handlers = append(handlers, h) + } + if file != "" { + p, err := ioutil.ReadFile(file) + if err != nil { + die(err) + } + h, err := NewHandler(string(p)) + if err != nil { + die(err) + } + handlers = append(handlers, h) + } + var run []chan<- Event + for _, h := range handlers { + run = append(run, h.Daemon()) + } + c := make(chan notify.EventInfo, 1) + for _, path := range paths { + if err := notify.Watch(path, c, notify.All); err != nil { + die(err) + } + } + for ei := range c { + log.Println("received", ei) + e := NewEvent(ei) + for _, run := range run { + select { + case run <- e: + default: + log.Println("event dropped due to slow handler") } - }(path) + } } - select {} } diff --git a/notify.go b/notify.go index 13df163..8675281 100644 --- a/notify.go +++ b/notify.go @@ -2,10 +2,10 @@ // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. -// BUG(rjeczalik): Currently notify does not automagically set a watch for newly +// BUG(rjeczalik): Notify does not automagically set a watch for newly // created directory within recursively watched path for inotify and kqueue. (#5) -// BUG(rjeczalik): Currently notify does not gracefully handle rewatching directories, +// BUG(rjeczalik): Notify does not gracefully handle rewatching directories, // that were deleted but their watchpoints were not cleaned by the user. (#69) // BUG(ppknap): Linux(inotify) does not currently support watcher behavior masks