├── .gitignore ├── .goreleaser.yml ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE.txt ├── README.md ├── cmd └── root.go ├── main.go └── tychus ├── configuration.go ├── logger.go ├── orchestrator.go ├── proxy.go ├── runner.go └── watcher.go /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | tmp/ 3 | vendor/ 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - binary: tychus 3 | goos: 4 | - darwin 5 | - linux 6 | goarch: 7 | - amd64 8 | ldflags: -s -w 9 | 10 | brew: 11 | github: 12 | owner: devlocker 13 | name: homebrew-tap 14 | commit_author: 15 | name: Patrick Koperwas 16 | email: patrick@devlocker.io 17 | description: "Command line utility to live-reload your application." 18 | folder: Formula 19 | homepage: https://github.com/devlocker/tychus 20 | test: | 21 | system "#{bin}/tychus version" 22 | install: | 23 | bin.install "tychus" 24 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/MichaelTJones/walk" 7 | packages = ["."] 8 | revision = "4748e29d5718c2df4028a6543edf86fd8cc0f881" 9 | 10 | [[projects]] 11 | name = "github.com/inconshreveable/mousetrap" 12 | packages = ["."] 13 | revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" 14 | version = "v1.0" 15 | 16 | [[projects]] 17 | branch = "master" 18 | name = "github.com/spf13/cobra" 19 | packages = ["."] 20 | revision = "f91529fc609202eededff4de2dc0ba2f662240a3" 21 | 22 | [[projects]] 23 | name = "github.com/spf13/pflag" 24 | packages = ["."] 25 | revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" 26 | version = "v1.0.0" 27 | 28 | [solve-meta] 29 | analyzer-name = "dep" 30 | analyzer-version = 1 31 | inputs-digest = "ee9e382c2cc9c6b6b03e86f5fd2aebdc801c952fa50fbc0c1d2097716d8afa42" 32 | solver-name = "gps-cdcl" 33 | solver-version = 1 34 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | [[constraint]] 2 | name = "github.com/spf13/cobra" 3 | branch = "master" 4 | 5 | [prune] 6 | go-tests = true 7 | unused-packages = true 8 | 9 | [[constraint]] 10 | branch = "master" 11 | name = "github.com/MichaelTJones/walk" 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Patrick Koperwas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | tychus 2 | ======== 3 | 4 | Tychus is a command line utility for live reloading applications. Tychus serves 5 | your application through a proxy. Anytime the proxy receives an HTTP request, it 6 | will automatically rerun your command if the filesystem has changed. 7 | 8 | `tychus` is language agnostic - it can be configured to work with anything: Go, 9 | Rust, Ruby, Python, scripts, and arbitrary commands. 10 | 11 | 12 | ## Installation 13 | 14 | ### Homebrew on macOS 15 | 16 | ``` 17 | brew tap devlocker/tap 18 | brew install tychus 19 | ``` 20 | 21 | ### With Go 22 | Assuming you have a working Go environment and `GOPATH/bin` is in your `PATH` 23 | 24 | ``` 25 | go get github.com/devlocker/tychus 26 | ``` 27 | 28 | ### Windows 29 | Currently isn't supported :( 30 | 31 | ## Usage 32 | 33 | Usage is simple, `tychus [command]` A proxy will be started on port `4000`. When 34 | an HTTP request comes in and the filesystem has changed, your command will be 35 | rerun. 36 | 37 | ``` 38 | tychus go run main.go 39 | ``` 40 | 41 | ## Options 42 | Tychus has a few options. In most cases the defaults should be sufficient. 43 | 44 | ```yaml 45 | -a, --app-port int port your application runs on, overwritten by ENV['PORT'] (default 3000) 46 | -p, --proxy-port int proxy port (default 4000) 47 | -x, --ignore string comma separated list of directories to ignore file changes in. (default node_modules,log,tmp,vendor) 48 | -w, --wait Wait for command to finish before proxying a request. 49 | -t, --timeout int timeout for proxied requests (default 10) 50 | 51 | -h, --help help for tychus 52 | --debug print debug output 53 | --version version for tychus 54 | ``` 55 | 56 | Note: Tychus will not look for file system changes in any hidden directories 57 | (those beginning with `.`). 58 | 59 | ## Examples 60 | 61 | **Example: Web Servers** 62 | 63 | ``` 64 | // Go - Hello World Server 65 | $ tychus go run main.go 66 | [tychus] Proxing requests on port 4000 to 3000 67 | [Go App] App Starting on Port 3000 68 | 69 | // Make a request 70 | $ curl localhost:4000 71 | Hello World 72 | $ curl localhost:4000 73 | Hello World 74 | 75 | // Save a file, next request will restart your webapp 76 | $ curl localhost:4000 77 | [Go App] App Starting on Port 3000 78 | Hello World 79 | ``` 80 | 81 | This can work with any webserver: 82 | 83 | ``` 84 | // Rust 85 | tychus cargo run 86 | 87 | // Ruby 88 | tychus ruby myapp.rb 89 | ``` 90 | 91 | Need to pass flags? Stick the command in quotes 92 | 93 | ``` 94 | tychus "ruby myapp.rb -e development" 95 | ``` 96 | 97 | Complicated command? Stick it in quotes 98 | 99 | ``` 100 | tychus "go build -o my-bin && echo 'Built Binary' && ./my-bin" 101 | ``` 102 | 103 | **Example: Scripts + Commands** 104 | 105 | Scenario: You have a webserver running on port `3005`, and it serves static 106 | files from the `/public` directory. In the `/docs` folder are some markdown 107 | files. Should they change, you want them rebuilt and placed into the `public` 108 | directory so the server can pick them up. 109 | 110 | ``` 111 | tychus "multimarkdown docs/index.md > public/index.html" --wait --app-port=3005 112 | ``` 113 | 114 | Now, when you make a request to the proxy on `localhost:4000`, `tychus` will 115 | pause the request (that's what the `--wait` flag is for) until `multimarkdown` 116 | finishes. Then request will be forwarded to the server on port `3005`. 117 | `multimarkdown` will only be run if the filesystem has changed. 118 | 119 | **Other Proxy Goodies** 120 | 121 | **Error messages** 122 | 123 | If you make a syntax error, or your program won't build for some reason, the 124 | stderr output will be returned by the proxy. Handy for the times you can't see 125 | you server (its in another pane / tab / tmux split). 126 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "strconv" 8 | "strings" 9 | "syscall" 10 | 11 | "github.com/devlocker/tychus/tychus" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var version = "0.6.3" 16 | 17 | var appPort int 18 | var debug bool 19 | var ignored []string 20 | var proxyPort int 21 | var timeout int 22 | var wait bool 23 | 24 | var rootCmd = &cobra.Command{ 25 | Use: "tychus", 26 | Short: "Live reload utility + proxy", 27 | Long: `Tychus is a command line utility for live reloading applications. 28 | Tychus serves your application through a proxy. Anytime the proxy receives 29 | an HTTP request will automatically rerun your command if the filesystem has 30 | changed. 31 | `, 32 | Args: cobra.MinimumNArgs(1), 33 | Run: func(cmd *cobra.Command, args []string) { 34 | start(args) 35 | }, 36 | Version: version, 37 | } 38 | 39 | func Execute() { 40 | if err := rootCmd.Execute(); err != nil { 41 | fmt.Println(err) 42 | os.Exit(1) 43 | } 44 | } 45 | 46 | func init() { 47 | rootCmd.Flags().IntVarP(&appPort, "app-port", "a", 3000, "port your application runs on, overwritten by ENV['PORT']") 48 | rootCmd.Flags().BoolVar(&debug, "debug", false, "print debug output") 49 | rootCmd.Flags().StringSliceVarP(&ignored, "ignore", "x", []string{"node_modules", "log", "tmp", "vendor"}, "comma separated list of directories to ignore file changes in.") 50 | rootCmd.Flags().IntVarP(&proxyPort, "proxy-port", "p", 4000, "proxy port") 51 | rootCmd.Flags().IntVarP(&timeout, "timeout", "t", 10, "timeout for proxied requests") 52 | rootCmd.Flags().BoolVarP(&wait, "wait", "w", false, "Wait for command to finish before proxying a request") 53 | } 54 | 55 | func start(args []string) { 56 | // Catch signals, need to do make sure to stop any executing commands. 57 | // Otherwise they become orphan proccesses. 58 | stop := make(chan os.Signal, 1) 59 | signal.Notify( 60 | stop, 61 | os.Interrupt, 62 | syscall.SIGHUP, 63 | syscall.SIGINT, 64 | syscall.SIGTERM, 65 | syscall.SIGQUIT, 66 | ) 67 | 68 | // If PORT is set, use that instead of AppPort. For things like foreman 69 | // where ports are automatically assigned. 70 | envPort, ok := os.LookupEnv("PORT") 71 | if ok { 72 | if envPort, err := strconv.Atoi(envPort); err == nil { 73 | appPort = envPort 74 | } 75 | } 76 | 77 | // Clean up ignored dirs. 78 | for i, dir := range ignored { 79 | ignored[i] = strings.TrimRight(strings.TrimSpace(dir), "/") 80 | } 81 | 82 | // Create a configuration 83 | c := &tychus.Configuration{ 84 | Ignore: ignored, 85 | ProxyPort: proxyPort, 86 | AppPort: appPort, 87 | Timeout: timeout, 88 | Logger: tychus.NewLogger(debug), 89 | Wait: wait, 90 | } 91 | 92 | // Run tychus 93 | t := tychus.New(args, c) 94 | go func() { 95 | err := t.Start() 96 | if err != nil { 97 | t.Stop() 98 | c.Logger.Fatal(err.Error()) 99 | } 100 | }() 101 | 102 | <-stop 103 | t.Stop() 104 | } 105 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/devlocker/tychus/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /tychus/configuration.go: -------------------------------------------------------------------------------- 1 | package tychus 2 | 3 | type Configuration struct { 4 | AppPort int 5 | Ignore []string 6 | Logger Logger 7 | ProxyPort int 8 | Timeout int 9 | Wait bool 10 | } 11 | -------------------------------------------------------------------------------- /tychus/logger.go: -------------------------------------------------------------------------------- 1 | package tychus 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | type Logger interface { 9 | Debug(interface{}) 10 | Debugf(string, ...interface{}) 11 | Fatal(...interface{}) 12 | Printf(string, ...interface{}) 13 | Print(...interface{}) 14 | } 15 | 16 | func NewLogger(debug bool) Logger { 17 | l := &logger{ 18 | Logger: log.New(os.Stdout, "[tychus] ", 0), 19 | debug: debug, 20 | } 21 | 22 | return l 23 | } 24 | 25 | type logger struct { 26 | *log.Logger 27 | debug bool 28 | } 29 | 30 | func (l *logger) Debug(msg interface{}) { 31 | if l.debug { 32 | l.Printf("DEBUG: %v", msg) 33 | } 34 | } 35 | 36 | func (l *logger) Debugf(format string, args ...interface{}) { 37 | if l.debug { 38 | l.Printf("DEBUG: "+format, args...) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tychus/orchestrator.go: -------------------------------------------------------------------------------- 1 | // Package tychus is a command line application that will watch your files and 2 | // on change, trigger a rerun of a command. It's designed to work best with web 3 | // applications, but certainly not lmited to. 4 | // 5 | // Unlike other application reloaders written in Go, Tychus is language 6 | // agnostic. It can be used with Go, Rust, Python, Ruby, scripts, etc. 7 | // 8 | // If enabled, Tychus will serve an application through a proxy. This can help 9 | // mitigate annoyances like reloading your web page before the app server 10 | // finishes booting. Or attempting to make a request after the server starts, 11 | // but before it is ready to accept requests. 12 | package tychus 13 | 14 | import "time" 15 | 16 | type Orchestrator struct { 17 | config *Configuration 18 | watcher *watcher 19 | runner *runner 20 | proxy *proxy 21 | } 22 | 23 | func New(args []string, c *Configuration) *Orchestrator { 24 | return &Orchestrator{ 25 | config: c, 26 | watcher: newWatcher(c), 27 | runner: newRunner(c, args), 28 | proxy: newProxy(c), 29 | } 30 | } 31 | 32 | func (o *Orchestrator) Start() error { 33 | stop := make(chan error, 1) 34 | 35 | go func() { 36 | if err := o.proxy.start(); err != nil { 37 | stop <- err 38 | } 39 | }() 40 | 41 | if err := o.runner.run(); err != nil { 42 | o.proxy.setError(err) 43 | } 44 | 45 | for { 46 | select { 47 | case <-o.proxy.requests: 48 | modified := o.watcher.scan() 49 | if modified { 50 | o.config.Logger.Debug("Runner: FS modified, rerunning") 51 | 52 | if err := o.runner.run(); err != nil { 53 | o.proxy.setError(err) 54 | o.proxy.unpause <- true 55 | continue 56 | } 57 | 58 | o.proxy.clearError() 59 | } 60 | 61 | o.watcher.lastRun = time.Now() 62 | o.proxy.unpause <- true 63 | 64 | case err := <-o.runner.errors: 65 | o.config.Logger.Debug("Runner: Error") 66 | o.proxy.setError(err) 67 | 68 | case err := <-stop: 69 | o.Stop() 70 | return err 71 | } 72 | } 73 | } 74 | 75 | // Stops Tychus and forces any processes started by it that may be running. 76 | func (o *Orchestrator) Stop() { 77 | o.runner.kill() 78 | } 79 | -------------------------------------------------------------------------------- /tychus/proxy.go: -------------------------------------------------------------------------------- 1 | package tychus 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net" 8 | "net/http" 9 | "net/http/httputil" 10 | "net/url" 11 | "time" 12 | ) 13 | 14 | type proxy struct { 15 | config *Configuration 16 | errorStr string 17 | requests chan bool 18 | revproxy *httputil.ReverseProxy 19 | unpause chan bool 20 | } 21 | 22 | // Returns a newly configured proxy 23 | func newProxy(c *Configuration) *proxy { 24 | url, err := url.Parse(fmt.Sprintf("%s:%v", "http://localhost", c.AppPort)) 25 | if err != nil { 26 | c.Logger.Fatal(err) 27 | } 28 | 29 | revproxy := httputil.NewSingleHostReverseProxy(url) 30 | revproxy.ErrorLog = log.New(ioutil.Discard, "", 0) 31 | 32 | p := &proxy{ 33 | config: c, 34 | requests: make(chan bool), 35 | revproxy: revproxy, 36 | unpause: make(chan bool), 37 | } 38 | 39 | return p 40 | } 41 | 42 | func (p *proxy) start() error { 43 | server := &http.Server{Handler: p} 44 | 45 | listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", "localhost", p.config.ProxyPort)) 46 | if err != nil { 47 | return err 48 | } 49 | defer listener.Close() 50 | 51 | p.config.Logger.Printf("Proxing requests on port %v to %v", p.config.ProxyPort, p.config.AppPort) 52 | 53 | err = server.Serve(listener) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | return nil 59 | } 60 | 61 | // Proxy the request to the application server. 62 | func (p *proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { 63 | p.requests <- true 64 | 65 | <-p.unpause 66 | 67 | if ok := p.forward(w, r); ok { 68 | return 69 | } 70 | 71 | timeout := time.After(time.Second * time.Duration(p.config.Timeout)) 72 | tick := time.Tick(50 * time.Millisecond) 73 | 74 | ctx := r.Context() 75 | 76 | for { 77 | select { 78 | case <-tick: 79 | if ok := p.forward(w, r); ok { 80 | return 81 | } 82 | 83 | case <-timeout: 84 | p.config.Logger.Print("Timeout reached") 85 | w.WriteHeader(http.StatusBadGateway) 86 | w.Write([]byte("Connection Refused")) 87 | 88 | return 89 | 90 | case <-ctx.Done(): 91 | return 92 | } 93 | } 94 | } 95 | 96 | func (p *proxy) forward(w http.ResponseWriter, r *http.Request) bool { 97 | if len(p.errorStr) > 0 { 98 | w.WriteHeader(http.StatusInternalServerError) 99 | w.Write([]byte(p.errorStr)) 100 | return true 101 | } 102 | 103 | writer := &proxyWriter{res: w} 104 | p.revproxy.ServeHTTP(writer, r) 105 | 106 | // If the request is "successful" - as in the server responded in 107 | // some way, return the response to the client. 108 | return writer.status != http.StatusBadGateway 109 | } 110 | 111 | func (p *proxy) setError(err error) { 112 | p.config.Logger.Debug("Proxy: Error Mode") 113 | p.errorStr = err.Error() 114 | } 115 | 116 | func (p *proxy) clearError() { 117 | p.errorStr = "" 118 | } 119 | 120 | // Wrapper around http.ResponseWriter. Since the proxy works rather naively - 121 | // it just retries requests over and over until it gets a response from the app 122 | // server - we can't use the ResponseWriter that is passed to the handler 123 | // because you cannot call WriteHeader multiple times. 124 | type proxyWriter struct { 125 | res http.ResponseWriter 126 | status int 127 | } 128 | 129 | func (w *proxyWriter) WriteHeader(status int) { 130 | if status == 502 { 131 | w.status = status 132 | return 133 | } 134 | 135 | w.res.WriteHeader(status) 136 | } 137 | 138 | func (w *proxyWriter) Write(body []byte) (int, error) { 139 | return w.res.Write(body) 140 | } 141 | 142 | func (w *proxyWriter) Header() http.Header { 143 | return w.res.Header() 144 | } 145 | -------------------------------------------------------------------------------- /tychus/runner.go: -------------------------------------------------------------------------------- 1 | package tychus 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | "syscall" 11 | ) 12 | 13 | type runner struct { 14 | args []string 15 | cmd *exec.Cmd 16 | errors chan error 17 | stderr *bytes.Buffer 18 | config *Configuration 19 | } 20 | 21 | func newRunner(c *Configuration, args []string) *runner { 22 | return &runner{ 23 | args: args, 24 | config: c, 25 | errors: make(chan error), 26 | } 27 | } 28 | 29 | func (r *runner) run() error { 30 | r.kill() 31 | 32 | if err := r.execute(); err != nil { 33 | return err 34 | } 35 | 36 | if r.config.Wait { 37 | return r.wait() 38 | } else { 39 | go r.wait() 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (r *runner) execute() error { 46 | if r.cmd != nil && r.cmd.ProcessState != nil && r.cmd.ProcessState.Exited() { 47 | return nil 48 | } 49 | 50 | r.stderr = &bytes.Buffer{} 51 | mw := io.MultiWriter(r.stderr, os.Stderr) 52 | 53 | r.cmd = exec.Command("/bin/sh", "-c", strings.Join(r.args, " ")) 54 | r.cmd.Stdout = os.Stdout 55 | r.cmd.Stderr = mw 56 | 57 | // Setup a process group so when this process gets stopped, so do any child 58 | // process that it may spawn. 59 | r.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 60 | 61 | if err := r.cmd.Start(); err != nil { 62 | return errors.New(r.stderr.String()) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // Wait for the command to finish. If the process exits with an error, only log 69 | // it if it exit status is postive, as status code -1 is returned when the 70 | // process was killed by runner#kill. 71 | func (r *runner) wait() error { 72 | err := r.cmd.Wait() 73 | 74 | if err != nil { 75 | if exiterr, ok := err.(*exec.ExitError); ok { 76 | ws := exiterr.Sys().(syscall.WaitStatus) 77 | if ws.ExitStatus() > 0 { 78 | err = errors.New(r.stderr.String()) 79 | 80 | if r.config.Wait { 81 | return err 82 | } else { 83 | r.errors <- err 84 | } 85 | } 86 | } 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // Kill the existing process & process group 93 | func (r *runner) kill() { 94 | if r.cmd != nil && r.cmd.Process != nil { 95 | if pgid, err := syscall.Getpgid(r.cmd.Process.Pid); err == nil { 96 | syscall.Kill(-pgid, syscall.SIGKILL) 97 | } 98 | 99 | syscall.Kill(-r.cmd.Process.Pid, syscall.SIGKILL) 100 | 101 | r.cmd = nil 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tychus/watcher.go: -------------------------------------------------------------------------------- 1 | package tychus 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | 10 | "github.com/MichaelTJones/walk" 11 | ) 12 | 13 | type watcher struct { 14 | config *Configuration 15 | lastRun time.Time 16 | } 17 | 18 | func newWatcher(c *Configuration) *watcher { 19 | return &watcher{ 20 | config: c, 21 | lastRun: time.Now(), 22 | } 23 | } 24 | 25 | func (w *watcher) scan() bool { 26 | w.config.Logger.Debug("Watcher: Start") 27 | start := time.Now() 28 | 29 | modified := walk.Walk(".", func(path string, info os.FileInfo, err error) error { 30 | if info.IsDir() && w.shouldSkipDir(path) { 31 | return walk.SkipDir 32 | } 33 | 34 | if info.ModTime().After(w.lastRun) { 35 | w.config.Logger.Debugf("Watcher: Found modified file: %v", path) 36 | return errors.New(path) 37 | } 38 | 39 | return nil 40 | }) 41 | 42 | w.config.Logger.Debugf("Watcher: Scan finished: %v", time.Since(start)) 43 | 44 | return modified != nil 45 | } 46 | 47 | // Checks to see if this directory should be watched. Don't want to watch 48 | // hidden directories (like .git) or ignored directories. 49 | func (w *watcher) shouldSkipDir(path string) bool { 50 | if len(path) > 1 && strings.HasPrefix(filepath.Base(path), ".") { 51 | return true 52 | } 53 | 54 | for _, dir := range w.config.Ignore { 55 | if dir == path { 56 | return true 57 | } 58 | } 59 | 60 | return false 61 | } 62 | --------------------------------------------------------------------------------