Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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.
151 changes: 101 additions & 50 deletions rerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,32 @@ 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 (
do_tests = flag.Bool("test", false, "Run tests (before running program)")
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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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)

Expand Down Expand Up @@ -200,82 +226,107 @@ 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
}
}

if *do_build {
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] <import path> [arg]*")
log.Fatal("Usage: rerun [--test] [--no-run] [--build] [--race] [--connect ip:port] <import path> [arg]*")
}

buildpath := flag.Args()[0]
Expand Down