diff --git a/README.md b/README.md index 5e4865f..970dfaf 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,6 @@ Flag `--build` makes rerun execute `go build` in the local folder, creating a ex Flag `--no-run` omits actually running the program. This is useful if you only wish to test and/or build. -Flag `--race` will test/build/run the program with race detection enabled. \ No newline at end of file +Flag `--race` will test/build/run the program with race detection enabled. + +Flag `--connect host:port` will connect to a remote socket that sends file system events. See https://github.com/guard/listen#forwarding-file-events-over-tcp for more details. \ No newline at end of file diff --git a/rerun.go b/rerun.go index a82ecb3..4ac8efe 100644 --- a/rerun.go +++ b/rerun.go @@ -6,16 +6,23 @@ package main import ( "bytes" + "encoding/binary" + "encoding/json" "errors" "flag" "fmt" - "github.com/howeyc/fsnotify" "go/build" + "io" "log" + "net" "os" "os/exec" "path" "path/filepath" + "strings" + "time" + + "github.com/howeyc/fsnotify" ) var ( @@ -23,6 +30,8 @@ var ( do_build = flag.Bool("build", false, "Build program") never_run = flag.Bool("no-run", false, "Do not run") race_detector = flag.Bool("race", false, "Run program and tests with the race detector") + tcp_connect = flag.String("connect", "", "Connect to an event tcp socket (rubygem listen)") + interval = flag.Duration("interval", time.Millisecond*100, "Duration to collect events before rebuild") ) func install(buildpath, lastError string) (installed bool, errorOutput string, err error) { @@ -111,7 +120,6 @@ func gobuild(buildpath string) (passed bool, err error) { func run(binName, binPath string, args []string) (runch chan bool) { runch = make(chan bool) go func() { - cmdline := append([]string{binName}, args...) var proc *os.Process for relaunch := range runch { if proc != nil { @@ -128,7 +136,7 @@ func run(binName, binPath string, args []string) (runch chan bool) { cmd := exec.Command(binPath, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - log.Print(cmdline) + log.Printf("running %s [%s]", binPath, strings.Join(args, " ")) err := cmd.Start() if err != nil { log.Printf("error on starting process: '%s'\n", err) @@ -153,6 +161,7 @@ func addToWatcher(watcher *fsnotify.Watcher, importpath string, watching map[str if pkg.Goroot { return } + log.Printf("watching %s", pkg.Dir) watcher.Watch(pkg.Dir) watching[importpath] = true for _, imp := range pkg.Imports { @@ -162,6 +171,23 @@ func addToWatcher(watcher *fsnotify.Watcher, importpath string, watching map[str } } +func debounce(changes chan string, f func(file string)) { + var changed = "" + for { + select { + case file := <-changes: + if filepath.Ext(file) == ".go" { + changed = file + } + case <-time.After(*interval): + if changed != "" { + f(changed) + changed = "" + } + } + } +} + func rerun(buildpath string, args []string) (err error) { log.Printf("setting up %s %v", buildpath, args) @@ -200,62 +226,35 @@ func rerun(buildpath string, args []string) (err error) { gobuild(buildpath) } - var errorOutput string - _, errorOutput, ierr := install(buildpath, errorOutput) + _, errorOutput, ierr := install(buildpath, "") if !no_run && !(*never_run) && ierr == nil { runch <- true } - var watcher *fsnotify.Watcher - watcher, err = getWatcher(buildpath) - if err != nil { - return - } - - for { - // read event from the watcher - we, _ := <-watcher.Event - // other files in the directory don't count - we watch the whole thing in case new .go files appear. - if filepath.Ext(we.Name) != ".go" { - continue - } - - log.Print(we.Name) - - // close the watcher - watcher.Close() - // to clean things up: read events from the watcher until events chan is closed. - go func(events chan *fsnotify.FileEvent) { - for _ = range events { - + changes := make(chan string, 10) + go func() { + if *tcp_connect != "" { + if err := connect(*tcp_connect, changes); err != nil { + log.Fatal(err) } - }(watcher.Event) - // create a new watcher - log.Println("rescanning") - watcher, err = getWatcher(buildpath) - if err != nil { - return - } - - // we don't need the errors from the new watcher. - // we continiously discard them from the channel to avoid a deadlock. - go func(errors chan error) { - for _ = range errors { - + } else { + if err = watch(buildpath, changes); err != nil { + log.Fatal(err) } - }(watcher.Error) + } + close(changes) + }() - var installed bool - // rebuild - installed, errorOutput, _ = install(buildpath, errorOutput) - if !installed { - continue + debounce(changes, func(file string) { + log.Printf("%s changed, rebuilding", file) + if installed, _, _ := install(buildpath, errorOutput); !installed { + return } if *do_tests { passed, _ := test(buildpath) if !passed { - continue + return } } @@ -263,19 +262,71 @@ func rerun(buildpath string, args []string) (err error) { gobuild(buildpath) } - // rerun. if we're only testing, sending if !(*never_run) { runch <- true } + }) + + return nil +} + +func watch(buildpath string, buildCh chan string) error { + watcher, err := getWatcher(buildpath) + if err != nil { + return err } - return + defer watcher.Close() + + // read event from the watcher + for { + select { + case we := <-watcher.Event: + buildCh <- we.Name + case err := <-watcher.Error: + return err + } + } + + return nil +} + +func connect(address string, buildCh chan string) error { + conn, err := net.Dial("tcp", address) + if err != nil { + return err + } + + log.Printf("connected to %s for remote file events", address) + + for { + // https://github.com/guard/listen/blob/master/lib/listen/tcp/message.rb + var length uint32 + err := binary.Read(conn, binary.BigEndian, &length) + if err != nil { + return err + } + + var buf = make([]byte, length) + if _, err := io.ReadFull(conn, buf); err != nil { + return err + } + + var msg []interface{} + if err := json.Unmarshal(buf, &msg); err != nil { + return err + } + + buildCh <- msg[3].(string) + } + + return nil } func main() { flag.Parse() if len(flag.Args()) < 1 { - log.Fatal("Usage: rerun [--test] [--no-run] [--build] [--race] [arg]*") + log.Fatal("Usage: rerun [--test] [--no-run] [--build] [--race] [--connect ip:port] [arg]*") } buildpath := flag.Args()[0]