diff --git a/Gopkg.lock b/Gopkg.lock index f337040..6d50701 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,42 +1,12 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. -[[projects]] - name = "github.com/devlocker/devproxy" - packages = ["devproxy"] - revision = "c5444b29b6e6db1308ed9ad4b175b0303ffd5737" - version = "0.2.2" - -[[projects]] - name = "github.com/fatih/color" - packages = ["."] - revision = "570b54cabe6b8eb0bc2dfce68d964677d63b5260" - version = "v1.5.0" - -[[projects]] - name = "github.com/fsnotify/fsnotify" - packages = ["."] - revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" - version = "v1.4.7" - [[projects]] name = "github.com/inconshreveable/mousetrap" packages = ["."] revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" version = "v1.0" -[[projects]] - name = "github.com/mattn/go-colorable" - packages = ["."] - revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" - version = "v0.0.9" - -[[projects]] - name = "github.com/mattn/go-isatty" - packages = ["."] - revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" - version = "v0.0.3" - [[projects]] branch = "master" name = "github.com/spf13/cobra" @@ -49,15 +19,9 @@ revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" version = "v1.0.0" -[[projects]] - branch = "master" - name = "golang.org/x/sys" - packages = ["unix"] - revision = "ff2a66f350cefa5c93a634eadb5d25bb60c85a9c" - [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "c5a9fb7c0f7c220499ac2425585aaa1b366e40e74c38e14eb5d9211db31dc61f" + inputs-digest = "fd56cc1810ff527dc29fa5ac153938af6d8df6a405c953638f1275f24d6073d9" solver-name = "gps-cdcl" solver-version = 1 diff --git a/README.md b/README.md index e9c3edc..5e8dc47 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,12 @@ tychus ======== -`tychus` is a command line utility to live-reload your application. `tychus` -will watch your filesystem for changes and automatically recompile and restart -code on change. +Tychus is a command line utility for live reloading applications. Tychus serves +your application through a proxy. Anytime the proxy receives an HTTP request, it +will automatically rerun your command if the filesystem has changed. `tychus` is language agnostic - it can be configured to work with just about -anything: Go, Rust, Ruby, Python, etc. - -Should you desire you can use `tychus` as a proxy to your application. It will -pause requests while your application rebuilds & restarts. +anything: Go, Rust, Ruby, Python, scripts & arbitrary commands. ## Installation @@ -33,36 +30,14 @@ Currently isn't supported :( ## Usage -Usage is simple, `tychus` and then your command. On a filesystem change that +Usage is simple, `tychus` and then your command. That will start a proxy on port +`4000`. When an HTTP request comes in and the filesystem has changed, your command will be rerun. ``` -// Go tychus go run main.go - -// Rust -tychus cargo run - -// Ruby -tychus ruby myapp.rb - -// Shell Commands -tychus ls -``` - -Need to pass flags? Stick the command in quotes - -``` -tychus "ruby myapp.rb -e development" ``` -Complicated command? Stick it in quotes - -``` -tychus "go build -o my-bin && echo 'Built Binary' && ./my-bin" -``` - - ## Options Tychus has a few options. In most cases the defaults should be sufficient. See below for a few examples. @@ -70,100 +45,121 @@ below for a few examples. ```yaml -a, --app-port int port your application runs on, overwritten by ENV['PORT'] (default 3000) -p, --proxy-port int proxy port (default 4000) - -w, --watch stringSlice comma separated list of extensions that will trigger a reload. If not set, will reload on any file change. -x, --ignore stringSlice comma separated list of directories to ignore file changes in. (default [node_modules,log,tmp,vendor]) + --wait Wait for command to finish before proxying a request. -t, --timeout int timeout for proxied requests (default 10) + -h, --help help for tychus --debug print debug output - --no-proxy will not start proxy if set --version version for tychus ``` -Note: If you do not specify any file extensions in `--watch`, Tychus will -trigger a reload on any file change, except for files inside directories listed -in `--ignore` - -Note: Tychus will not watch any hidden directories (those beginning with `.`). +Note: Tychus will not look for file system changes any hidden directories +(those beginning with `.`). ## Examples -### Sinatra -By default, Sinatra runs on port `4567`. Only want to watch `ruby` and -`erb` files. Default ignore list is sufficient. The following are equivalent. +**Example: Web Servers** ``` -tychus ruby myapp.rb -w .rb,.erb -a 4567 -tychus ruby myapp.rb --watch=.rb,.erb --app-port=4567 +// Go - Hello World Server +$ tychus go run main.go +[tychus] Proxing requests on port 4000 to 3000 +[Go App] App Starting + +// Make a request +$ curl localhost:4000 +Hello World +$ curl localhost:4000 +Hello World + +// Save a file, next request will restart your webapp +$ curl localhost:4000 +[Go App] App Starting +Hello World ``` -Visit http://localhost:4000 (4000 is the default proxy host) and to view your -app. +This can work with any webserver: +``` +// Rust +tychus cargo run -### Foreman / Procfile -Similar to the previous example, except this time running inside of -[foreman](https://github.com/ddollar/foreman) (or someother Procfile runner). +// Ruby +tychus ruby myapp.rb +``` + +Need to pass flags? Stick the command in quotes ``` -# Procfile -web: tychus "rackup -p $PORT -s puma" -w rb,erb +tychus "ruby myapp.rb -e development" ``` -Note: If you need to pass flags to your command (like `-p` & `-s` in this case), -wrap your entire command in quotes. +Complicated command? Stick it in quotes -We don't need to explicitly add a `-a $PORT` flag, because `tychus` will -automatically pick up the $PORT and automatically set `app-port` for you. +``` +tychus "go build -o my-bin && echo 'Built Binary' && ./my-bin" +``` +**Example: Scripts + Commands** -### Kitchen Sink Example -Running a Go program, separate build and run steps, with some logging thrown in, -only watching `.go` files, running a server on port `5000`, running proxy on -`8080`, ignoring just `tmp` and `vendor`, with a timeout of 5 seconds. +Scenario: You have a webserver running on port `3005`, and it serves static +files from the `/public` directory. In the `/docs` folder are some markdown +files. Should they change, you want them rebuilt and placed into the `public` +directory so the server can pick them up. ``` -tychus "echo 'Building...' && go build -o tmp/my-bin && echo 'Built' && ./tmp/my-bin some args -e development" --app-port=5000 --proxy-port=8080 --watch=.go --ignore=tmp,vendor --timeout=5 +tychus "multimarkdown docs/index.md > public/index.html" --wait --app-port=3005 +``` -# Or, using short flags +Now, when you make a request to the proxy on `localhost:4000`, `tychus` will +pause the request (that's what the `--wait` flag is for) until `multimarkdown` +finishes. Then it will forward the request to the server on port `3005`. +`multimarkdown` will only be run if the filesystem has changed. -tychus "echo 'Building...' && go build -o tmp/my-bin && echo 'Built' && ./tmp/my-bin some args -e development" -a 5000 -p 8080 -w .go -x tmp,vendor -t 5 -``` -## Whats the point of the proxy? -Consider the following situations: +**Advanced Example: Reload Scripts and Webserver** -**1. There is a gap between program starting and server accepting requests.** +Like the scenario above, but you also want your server to autoreload as files +change. You can chain `tychus` together, by setting the `app-port` equal to the +`proxy-port` of the previous `tychus`. An example: -```ruby -# myapp.rb -sleep 5 -require "sinatra" +The first instance of `tychus` will run a Go webserver that serves assets out of +`public/`. We only want it to restart when the `app` folder changes, so ignore +`docs` and `public` directories. -get "/" - "Hello World" -end ``` +$ tychus go run main.go --app-port=3000 --proxy-port=4000 --ignore=docs,public -After your application restarts, any requests that get sent to it within 5 -seconds will return an error / show you the "Site can't be reached page". +[tychus] Proxing requests on port 4000 to 3000 +... +... +``` -Really puts a damper on the save, alt+tab, refresh workflow. +In order to serve upto date docs, `multimarkdown` needs to be invoked to +transform markdown into servable html. So we start another `tychus` process to +and point its app-port to server's proxy port. -By going through the proxy, when you hit refresh, your request will wait until -the server is actually ready to accept and send you back a response. So save, -alt+tab to browser hit refresh. Page will wait the 5 seconds until the server is -ready. Then it will forward the request. +``` +$ tychus "multimarkdown docs/index.md > public/index.html" --wait --app-port=4000 --proxy-port=4001 +``` -**2. Your code has a compile step.** +Now, there is a proxy running on `4001` pointing at a proxy on `4000` pointing +at a webserver on `3000`. If you save `docs/index.html`, and then make a request +to `localhost:4001`, that will pause the request while `multimarkdown` runs. +Once it is finished, the requests gets forwarded to `localhost:4000`, which in +turn forwards it our websever on `3000`. The request gets sent all the way back, +with the correctly updated html! -While your code is still compiling you alt+tab to the browser and hit refresh... -and you are potentially served old code. Avoid that by going through a proxy. +Had our server code been modified in the `app/` folder, then after +`multimarkdown` finished, and the request got passed on to `4000`, that would +have also triggered a restart of our websever. **Other Proxy Goodies** **Error messages** -If you make a syntax error, or your program won't build for some reason, the -output will be displayed in the webpage. Handy for the times you can't see you -server (its in another pane / tab / tmux split). +If you make a syntax error, or your program won't build for some reason, stderr +will be returned by the proxy. Handy for the times you can't see you server (its +in another pane / tab / tmux split). diff --git a/cmd/root.go b/cmd/root.go index 86def9c..99b5e10 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,27 +20,15 @@ var ignored []string var noProxy bool var proxyPort int var timeout int -var watch []string +var wait bool var rootCmd = &cobra.Command{ Use: "tychus", - Short: "Starts and reloads your application as you make changes to source files.", - Long: `tychus is a command line utility to live-reload your application. tychus will -watch your filesystem for changes and automatically recompile and restart code -on change. - -Example: - tychus go run main.go -w .go - tychus ruby myapp.rb --app-port=4567 --proxy-port=4000 --watch .rb,.erb --ignore node_modules - -Example: No Proxy - tychus ls --no-proxy - -Example: Flags - use quotes - tychus "ruby myapp.rb -p 5000 -e development" -a 5000 -p 4000 -w .rb,.erb - -Example: Multiple Commands - use quotes - tychus "go build -o my-bin && echo 'Done Building' && ./my-bin" + Short: "Live reload utility + proxy", + Long: `Tychus is a command line utility for live reloading applications. +Tychus serves your application through a proxy. Anytime the proxy receives +an HTTP request will automatically rerun your command if the filesystem has +changed. `, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { @@ -63,7 +51,7 @@ func init() { rootCmd.Flags().BoolVar(&noProxy, "no-proxy", false, "will not start proxy if set") rootCmd.Flags().IntVarP(&proxyPort, "proxy-port", "p", 4000, "proxy port") rootCmd.Flags().IntVarP(&timeout, "timeout", "t", 10, "timeout for proxied requests") - rootCmd.Flags().StringSliceVarP(&watch, "watch", "w", []string{}, "comma separated list of extensions that will trigger a reload. If not set, will reload on any file change.") + rootCmd.Flags().BoolVar(&wait, "wait", false, "Wait for command to finish before proxying a request") } func start(args []string) { @@ -79,16 +67,6 @@ func start(args []string) { syscall.SIGQUIT, ) - // Clean up watched file extensions - for i, ext := range watch { - ext = strings.TrimSpace(ext) - if !strings.HasPrefix(ext, ".") { - ext = "." + ext - } - - watch[i] = ext - } - // If PORT is set, use that instead of AppPort. For things like foreman // where ports are automatically assigned. envPort, ok := os.LookupEnv("PORT") @@ -98,15 +76,20 @@ func start(args []string) { } } + // Clean up ignored dirs. + for i, dir := range ignored { + ignored[i] = strings.TrimRight(strings.TrimSpace(dir), "/") + } + // Create a configuration c := &tychus.Configuration{ - Extensions: watch, Ignore: ignored, ProxyEnabled: !noProxy, ProxyPort: proxyPort, AppPort: appPort, Timeout: timeout, Logger: tychus.NewLogger(debug), + Wait: wait, } // Run tychus diff --git a/tychus/configuration.go b/tychus/configuration.go index 421d545..61efb05 100644 --- a/tychus/configuration.go +++ b/tychus/configuration.go @@ -1,11 +1,11 @@ package tychus type Configuration struct { - Extensions []string `yaml:"extensions"` - Ignore []string `yaml:"ignore"` - ProxyEnabled bool `yaml:"proxy_enabled"` - ProxyPort int `yaml:"proxy_port"` - AppPort int `yaml:"app_port"` - Timeout int `yaml:"timeout"` - Logger Logger `yaml:"-"` + AppPort int + Ignore []string + Logger Logger + ProxyEnabled bool + ProxyPort int + Timeout int + Wait bool } diff --git a/tychus/event.go b/tychus/event.go index 4a626a1..977b0eb 100644 --- a/tychus/event.go +++ b/tychus/event.go @@ -10,6 +10,8 @@ const ( rebuilt changed errored + requested + unchanged ) type event struct { diff --git a/tychus/orchestrator.go b/tychus/orchestrator.go index e281b12..983b948 100644 --- a/tychus/orchestrator.go +++ b/tychus/orchestrator.go @@ -12,16 +12,14 @@ package tychus import ( - "strings" - - "github.com/devlocker/devproxy/devproxy" + "time" ) type Orchestrator struct { config *Configuration watcher *watcher runner *runner - proxy *devproxy.Proxy + proxy *proxy } func New(args []string, c *Configuration) *Orchestrator { @@ -29,62 +27,74 @@ func New(args []string, c *Configuration) *Orchestrator { config: c, watcher: newWatcher(), runner: newRunner(args), - proxy: devproxy.New(&devproxy.Configuration{ - AppPort: c.AppPort, - ProxyPort: c.ProxyPort, - Timeout: c.Timeout, - Logger: c.Logger, - }), + proxy: newProxy(c), } } -// Starts Tychus. Any filesystem changes will cause the command passed in to be -// rerun. To avoid orphaning processes, make sure to call Stop before exiting. func (o *Orchestrator) Start() error { - o.printStartMessage() - stop := make(chan error, 1) + go func() { + err := o.proxy.start() + if err != nil { + stop <- err + } + }() + go o.watcher.start(o.config) go o.runner.start(o.config) - if o.config.ProxyEnabled { - go func() { - err := o.proxy.Start() - if err != nil { - stop <- err - } - }() - } - o.runner.restart <- true for { select { + + // Proxy events: If a request comes in, pause the websever and start + // scanning for changes. Unless the last time a command ran it errored. + // In which case just rerun the command regardless of whether or not + // the FS has been changed. + case event := <-o.proxy.events: + o.config.Logger.Debug(event) + + switch event.op { + case requested: + if o.proxy.mode == mode_errored { + o.runner.restart <- true + } else { + o.watcher.scan <- true + } + + o.proxy.pause() + } + + // Watcher events: If FS has changed since the last time the watcher + // checked, go ahead and trigger a restart. Otherwise, unpause the + // proxy. case event := <-o.watcher.events: o.config.Logger.Debug(event) + switch event.op { case changed: - o.proxy.Command <- devproxy.Command{ - Cmd: devproxy.Pause, - } o.runner.restart <- true + case unchanged: + o.proxy.serve() } + // Runner events. If restart successful, go ahead an unpause the proxy. + // If the command exited with an error code, have the proxy display the + // error message. case event := <-o.runner.events: o.config.Logger.Debug(event) + switch event.op { case restarted: - o.proxy.Command <- devproxy.Command{ - Cmd: devproxy.Serve, - } + o.watcher.lastRun = time.Now() + o.proxy.serve() case errored: - o.proxy.Command <- devproxy.Command{ - Cmd: devproxy.Error, - Data: event.info, - } + o.proxy.error(event.info) } + // Stop Tychus case err := <-stop: o.Stop() return err @@ -93,19 +103,6 @@ func (o *Orchestrator) Start() error { } // Stops Tychus and forces any processes started by it that may be running. -func (o *Orchestrator) Stop() error { - return o.runner.kill() -} - -func (o *Orchestrator) printStartMessage() { - exts := o.config.Extensions - if len(exts) == 0 { - exts = []string{"all"} - } - - o.config.Logger.Printf( - "Starting: watching extensions: [%v], ignoring dirs: [%v]", - strings.Join(exts, ", "), - strings.Join(o.config.Ignore, ", "), - ) +func (o *Orchestrator) Stop() { + o.runner.kill() } diff --git a/tychus/proxy.go b/tychus/proxy.go new file mode 100644 index 0000000..e090a16 --- /dev/null +++ b/tychus/proxy.go @@ -0,0 +1,168 @@ +package tychus + +import ( + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "net/http/httputil" + "net/url" + "strconv" + "time" +) + +type mode uint32 + +const ( + mode_errored mode = 1 << iota + mode_paused + mode_serving +) + +type proxy struct { + config *Configuration + errorStr string + mode mode + events chan event + revproxy *httputil.ReverseProxy +} + +// Returns a newly configured proxy +func newProxy(c *Configuration) *proxy { + url, err := url.Parse(fmt.Sprintf("%s:%v", "http://localhost", c.AppPort)) + if err != nil { + c.Logger.Fatal(err) + } + + revproxy := httputil.NewSingleHostReverseProxy(url) + revproxy.ErrorLog = log.New(ioutil.Discard, "", 0) + + p := &proxy{ + config: c, + revproxy: revproxy, + mode: mode_paused, + events: make(chan event), + } + + return p +} + +func (p *proxy) start() error { + server := &http.Server{Handler: p} + + listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", "localhost", p.config.ProxyPort)) + if err != nil { + return err + } + defer listener.Close() + + p.config.Logger.Printf( + "Proxing requests on port %v to %v", + strconv.Itoa(p.config.ProxyPort), + strconv.Itoa(p.config.AppPort), + ) + + p.serve() + + err = server.Serve(listener) + if err != nil { + return err + } + + return nil +} + +// Proxy the request to the application server. +// +// The behavior of this function depends on the mode of the proxy. While +// serving, should the proxied request return a 502, the request will be +// retried until a non 502 status code is returned, or the timeout specified in +// the configuration is reached. Paused has the request wait until the server +// either moves into serving or erroed mode, or a timeout is reached. While in +// the errored mode, all requests will return a 500 along with some specified +// error body. +func (p *proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + p.events <- event{op: requested, info: "Incoming Request"} + + timeout := time.After(time.Second * time.Duration(p.config.Timeout)) + tick := time.Tick(100 * time.Millisecond) + + ctx := r.Context() + + for { + select { + case <-tick: + if p.mode == mode_paused { + continue + } + + if p.mode == mode_errored { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(p.errorStr)) + return + } + + writer := &proxyWriter{res: w} + p.revproxy.ServeHTTP(writer, r) + + // If the request is "successful" - as in the server responded in + // some way, return the response to the client. + if writer.status != http.StatusBadGateway { + return + } + + case <-timeout: + p.config.Logger.Print("Timeout reached") + w.WriteHeader(http.StatusBadGateway) + w.Write([]byte("Connection Refused")) + + return + + case <-ctx.Done(): + return + } + } +} + +func (p *proxy) serve() { + p.config.Logger.Debug("Proxy: Serving") + p.mode = mode_serving +} + +func (p *proxy) pause() { + p.config.Logger.Debug("Proxy: Paused") + p.mode = mode_paused +} + +func (p *proxy) error(e string) { + p.config.Logger.Debug("Proxy: Error Mode") + p.mode = mode_errored + p.errorStr = e +} + +// Wrapper around http.ResponseWriter. Since the proxy works rather naively - +// it just retries requests over and over until it gets a response from the app +// server - we can't use the ResponseWriter that is passed to the handler +// because you cannot call WriteHeader multiple times. +type proxyWriter struct { + res http.ResponseWriter + status int +} + +func (w *proxyWriter) WriteHeader(status int) { + if status == 502 { + w.status = status + return + } + + w.res.WriteHeader(status) +} + +func (w *proxyWriter) Write(body []byte) (int, error) { + return w.res.Write(body) +} + +func (w *proxyWriter) Header() http.Header { + return w.res.Header() +} diff --git a/tychus/runner.go b/tychus/runner.go index 38c1d5b..963b061 100644 --- a/tychus/runner.go +++ b/tychus/runner.go @@ -15,6 +15,7 @@ type runner struct { cmd *exec.Cmd events chan event restart chan bool + stderr *bytes.Buffer } func newRunner(args []string) *runner { @@ -29,20 +30,28 @@ func (r *runner) start(c *Configuration) { for { <-r.restart - if err := r.kill(); err != nil { + // Kill previous running process + r.kill() + + // Start command + if err := r.rerun(); err != nil { r.events <- event{op: errored, info: err.Error()} + continue } - go func() { - c.Logger.Debugf("Running: %v", strings.Join(r.args, " ")) - if err := r.rerun(); err != nil { - r.events <- event{op: errored, info: err.Error()} - } - }() + // If configured to wait, block until command finishes. Otherwise, wait + // in the background. + if c.Wait { + r.wait() + } else { + go r.wait() + } + + // Let Orchestrator know process has been restarted. + r.events <- event{info: "Restarted", op: restarted} } } -// Rerun the command. func (r *runner) rerun() error { if r.cmd != nil && r.cmd.ProcessState != nil && r.cmd.ProcessState.Exited() { return nil @@ -50,6 +59,7 @@ func (r *runner) rerun() error { var stderr bytes.Buffer mw := io.MultiWriter(&stderr, os.Stderr) + r.stderr = &stderr r.cmd = exec.Command("/bin/sh", "-c", strings.Join(r.args, " ")) r.cmd.Stdout = os.Stdout @@ -64,24 +74,27 @@ func (r *runner) rerun() error { return errors.New(stderr.String()) } - r.events <- event{info: "Restarted", op: restarted} + return nil +} + +// Wait for the command to finish. If the process exits with an error, only log +// it if it exit status is postive, as status code -1 is returned when the +// process was killed by kill(). +func (r *runner) wait() { + err := r.cmd.Wait() - if err := r.cmd.Wait(); err != nil { - // Program errored. Only log it if it exit status is postive, as status - // code -1 is returned when the process was killed by kill(). + if err != nil { if exiterr, ok := err.(*exec.ExitError); ok { ws := exiterr.Sys().(syscall.WaitStatus) if ws.ExitStatus() > 0 { - r.events <- event{info: stderr.String(), op: errored} + r.events <- event{op: errored, info: r.stderr.String()} } } } - - return nil } // Kill the existing process & process group -func (r *runner) kill() error { +func (r *runner) kill() { if r.cmd != nil && r.cmd.Process != nil { if pgid, err := syscall.Getpgid(r.cmd.Process.Pid); err == nil { syscall.Kill(-pgid, syscall.SIGKILL) @@ -91,6 +104,4 @@ func (r *runner) kill() error { r.cmd = nil } - - return nil } diff --git a/tychus/watcher.go b/tychus/watcher.go index c6cc886..872d12e 100644 --- a/tychus/watcher.go +++ b/tychus/watcher.go @@ -2,94 +2,56 @@ package tychus import ( "errors" - "log" + "fmt" "os" "path/filepath" "strings" "time" - - "github.com/fsnotify/fsnotify" ) type watcher struct { - *fsnotify.Watcher - events chan event + events chan event + lastRun time.Time + scan chan bool } func newWatcher() *watcher { - w, err := fsnotify.NewWatcher() - if err != nil { - log.Fatal("Could not start watcher") - } - return &watcher{ - Watcher: w, events: make(chan event), + lastRun: time.Now(), + scan: make(chan bool), } } func (w *watcher) start(c *Configuration) { - go w.watchForChanges(c) - - // With the way most editors work, they will generate multiple events on - // save. To avoid excessive restarts, batch all messages together within - // some interval and notify the orchestrator once per interval. - tick := time.Tick(200 * time.Millisecond) - events := make([]fsnotify.Event, 0) - for { - select { - case event := <-w.Events: - if event.Op == fsnotify.Chmod { - continue - } - - if w.isWatchedFile(event.Name, c) { - events = append(events, event) - } + <-w.scan - case <-tick: - if len(events) == 0 { - continue - } - - c.Logger.Debugf("FS Changes: %v", events) - w.events <- event{info: "File System Change", op: changed} - - events = make([]fsnotify.Event, 0) - } - } -} + c.Logger.Debug("Scan: Start") + start := time.Now() -// Setup file watchers for all valid extensions and directories. -func (w *watcher) watchForChanges(c *Configuration) error { - for { - err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { - if info == nil { - return errors.New("nil directory") + modified := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + if info.IsDir() && w.shouldSkipDir(path, c) { + return filepath.SkipDir } - if info.IsDir() { - if w.shouldSkipDir(path, c) { - return filepath.SkipDir - } - - c.Logger.Debugf("Watching: %v", path) - w.Add(path) + if info.ModTime().After(w.lastRun) { + return errors.New(path) } return nil }) - if err != nil { - break - } + c.Logger.Debugf("Scan: took: %v", time.Since(start)) - // Walk once a second. - time.Sleep(1 * time.Second) - } + w.lastRun = time.Now() - return errors.New("Watcher died") + if modified != nil { + w.events <- event{op: changed, info: fmt.Sprintf("FS Change: %v", modified)} + } else { + w.events <- event{op: unchanged, info: "FS Unchanged"} + } + } } // Checks to see if this directory should be watched. Don't want to watch @@ -99,27 +61,8 @@ func (w *watcher) shouldSkipDir(path string, c *Configuration) bool { return true } - for _, f := range c.Ignore { - f = strings.TrimSpace(f) - f = strings.TrimRight(f, "/") - - if f == path { - return true - } - } - - return false -} - -// Checks to see if path matches a configured extension. -func (w *watcher) isWatchedFile(path string, c *Configuration) bool { - if len(c.Extensions) == 0 { - return true - } - - ext := filepath.Ext(path) - for _, e := range c.Extensions { - if e == ext { + for _, dir := range c.Ignore { + if dir == path { return true } }