├── .codecov.yaml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example └── example.go ├── go.mod ├── go.sum ├── reload.go └── reload_test.go /.codecov.yaml: -------------------------------------------------------------------------------- 1 | comment: off 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /coverage.txt 3 | /example/example 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: ['1.16.x', '1.17.x', '1.18.x', '1.19.x'] 3 | go_import_path: github.com/teamwork/reload 4 | jobs: 5 | include: 6 | - {'os': 'linux', 'arch': 'amd64'} 7 | - {'os': 'linux', 'arch': 'amd64'} 8 | - {'os': 'linux', 'arch': 'arm64'} 9 | - {'os': 'linux', 'arch': 'ppc64le'} 10 | - {'os': 'linux', 'arch': 's390x'} 11 | - {'os': 'osx', 'arch': 'amd64'} 12 | - {'os': 'windows', 'arch': 'amd64'} 13 | 14 | notifications: 15 | email: false 16 | install: 17 | script: | 18 | export GO111MODULE=on 19 | go test -race \ 20 | -coverprofile=coverage.txt \ 21 | -coverpkg=./... \ 22 | ./... 23 | bash <(curl -s https://codecov.io/bash) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016-2017 © Teamwork.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/teamwork/reload?status.svg)](https://godoc.org/github.com/teamwork/reload) 2 | [![Build Status](https://travis-ci.com/Teamwork/reload.svg?branch=master)](https://travis-ci.com/Teamwork/reload) 3 | [![codecov](https://codecov.io/gh/Teamwork/reload/branch/master/graph/badge.svg?token=n0k8YjbQOL)](https://codecov.io/gh/Teamwork/reload) 4 | 5 | Lightweight automatic reloading of Go processes. 6 | 7 | After initialisation with `reload.Do()` any changes to the binary (and *only* 8 | the binary) will restart the process. For example: 9 | 10 | ```go 11 | func main() { 12 | go func() { 13 | err := reload.Do(log.Printf) 14 | if err != nil { 15 | panic(err) // Only returns initialisation errors. 16 | } 17 | }() 18 | 19 | fmt.Println(os.Args) 20 | fmt.Println(os.Environ()) 21 | ch := make(chan bool) 22 | <-ch 23 | } 24 | ``` 25 | 26 | Now use `go install` or `go build` to restart the process. 27 | 28 | Additional directories can be watched using `reload.Dir()`; this is useful for 29 | reloading templates: 30 | 31 | ```go 32 | func main() { 33 | go func() { 34 | err := reload.Do(log.Printf, reload.Dir("tpl", reloadTpl)) 35 | if err != nil { 36 | panic(err) 37 | } 38 | }() 39 | } 40 | ``` 41 | 42 | You can also use `reload.Exec()` to manually restart your process without 43 | calling `reload.Do()`. 44 | 45 | --- 46 | 47 | This is an alternative to the "restart binary after any `*.go` file 48 | changed"-strategy that some other projects – such as 49 | [gin](https://github.com/codegangsta/gin) or 50 | [go-watcher](https://github.com/canthefason/go-watcher) – take. 51 | The advantage of `reload`'s approach is that you have a more control over when 52 | the process restarts, and it only watches a single directory for changes which 53 | has some performance benefits, especially when used over NFS or Docker with a 54 | large number of files. 55 | 56 | It also means you won't start a whole bunch of builds if you update 20 files in 57 | a quick succession. On a desktop this probably isn't a huge deal, but on a 58 | laptop it'll save some battery power. 59 | 60 | Because it's in-process you can also do things like reloading just templates 61 | instead of recompiling/restarting everything. 62 | 63 | Caveat: the old process will continue running happily if `go install` has a 64 | compile error, so if you missed any compile errors due to switching the window 65 | too soon you may get confused. 66 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/teamwork/reload" 9 | ) 10 | 11 | func main() { 12 | go func() { 13 | err := reload.Do(log.Printf, 14 | reload.Dir("/tmp", func() { log.Printf("/tmp changed") }), 15 | reload.Dir(".", reload.Exec)) 16 | if err != nil { 17 | panic(err) 18 | } 19 | }() 20 | 21 | fmt.Println(os.Args) 22 | fmt.Println(os.Environ()) 23 | ch := make(chan bool) 24 | <-ch 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/teamwork/reload 2 | 3 | go 1.16 4 | 5 | require github.com/fsnotify/fsnotify v1.6.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 2 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 3 | golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY= 4 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 5 | -------------------------------------------------------------------------------- /reload.go: -------------------------------------------------------------------------------- 1 | // Package reload offers lightweight automatic reloading of running processes. 2 | // 3 | // After initialisation with reload.Do() any changes to the binary will restart 4 | // the process. 5 | // 6 | // Example: 7 | // 8 | // go func() { 9 | // err := reload.Do(log.Printf) 10 | // if err != nil { 11 | // panic(err) 12 | // } 13 | // }() 14 | // 15 | // A list of additional directories to watch can be added: 16 | // 17 | // go func() { 18 | // err := reload.Do(log.Printf, reload.Dir("tpl", reloadTpl) 19 | // if err != nil { 20 | // panic(err) 21 | // } 22 | // }() 23 | // 24 | // Note that this package won't prevent race conditions (e.g. when assigning to 25 | // a global templates variable). You'll need to use sync.RWMutex yourself. 26 | package reload // import "github.com/teamwork/reload" 27 | 28 | import ( 29 | "fmt" 30 | "math" 31 | "os" 32 | "path/filepath" 33 | "strings" 34 | "syscall" 35 | "time" 36 | 37 | "github.com/fsnotify/fsnotify" 38 | ) 39 | 40 | var ( 41 | binSelf string 42 | 43 | // The watcher won't be closed automatically, and the file descriptor will be 44 | // leaked if we don't close it in Exec(); see #9. 45 | closeWatcher func() error 46 | ) 47 | 48 | type dir struct { 49 | path string 50 | cb func() 51 | } 52 | 53 | // Dir is an additional directory to watch for changes. Directories are watched 54 | // non-recursively. 55 | // 56 | // The second argument is the callback that to run when the directory changes. 57 | // Use reload.Exec() to restart the process. 58 | func Dir(path string, cb func()) dir { return dir{path, cb} } 59 | 60 | // Do reload the current process when its binary changes. 61 | // 62 | // The log function is used to display an informational startup message and 63 | // errors. It works well with e.g. the standard log package or Logrus. 64 | // 65 | // The error return will only return initialisation errors. Once initialized it 66 | // will use the log function to print errors, rather than return. 67 | func Do(log func(string, ...interface{}), additional ...dir) error { 68 | watcher, err := fsnotify.NewWatcher() 69 | if err != nil { 70 | return fmt.Errorf("reload.Do: cannot setup watcher: %w", err) 71 | } 72 | closeWatcher = watcher.Close 73 | 74 | timers := make(map[string]*time.Timer) 75 | 76 | binSelf, err = self() 77 | if err != nil { 78 | return err 79 | } 80 | timers[binSelf] = stoppedTimer(Exec) 81 | 82 | // Watch the directory, because a recompile renames the existing 83 | // file (rather than rewriting it), so we won't get events for that. 84 | dirs := make([]string, len(additional)+1) 85 | dirs[0] = filepath.Dir(binSelf) 86 | 87 | for i, a := range additional { 88 | path, err := filepath.Abs(a.path) 89 | if err != nil { 90 | return fmt.Errorf("reload.Do: cannot get absolute path to %q: %w", 91 | a.path, err) 92 | } 93 | 94 | s, err := os.Stat(path) 95 | if err != nil { 96 | return fmt.Errorf("reload.Do: %w", err) 97 | } 98 | if !s.IsDir() { 99 | return fmt.Errorf("reload.Do: not a directory: %q; can only watch directories", 100 | a.path) 101 | } 102 | 103 | additional[i].path = path 104 | dirs[i+1] = path 105 | timers[path] = stoppedTimer(a.cb) 106 | } 107 | 108 | done := make(chan bool) 109 | go func() { 110 | for { 111 | select { 112 | case err := <-watcher.Errors: 113 | if err != nil { 114 | log("reload error: %v", err) 115 | } 116 | case event := <-watcher.Events: 117 | trigger := (event.Op&fsnotify.Write == fsnotify.Write) || (event.Op&fsnotify.Create == fsnotify.Create) 118 | if !trigger { 119 | continue 120 | } 121 | 122 | if event.Name == binSelf { 123 | timers[binSelf].Reset(100 * time.Millisecond) 124 | } 125 | 126 | for _, a := range additional { 127 | if strings.HasPrefix(event.Name, a.path) { 128 | timers[a.path].Reset(100 * time.Millisecond) 129 | } 130 | } 131 | } 132 | } 133 | }() 134 | 135 | for _, d := range dirs { 136 | if err := watcher.Add(d); err != nil { 137 | return fmt.Errorf("reload.Do: cannot add %q to watcher: %w", d, err) 138 | } 139 | } 140 | 141 | add := "" 142 | if len(additional) > 0 { 143 | reldirs := make([]string, len(dirs)-1) 144 | for i := range dirs[1:] { 145 | reldirs[i] = relpath(dirs[i+1]) 146 | } 147 | add = fmt.Sprintf(" (additional dirs: %s)", strings.Join(reldirs, ", ")) 148 | } 149 | log("restarting %q when it changes%s", relpath(binSelf), add) 150 | <-done 151 | return nil 152 | } 153 | 154 | // Exec replaces the current process with a new copy of itself. 155 | func Exec() { 156 | execName := binSelf 157 | if execName == "" { 158 | selfName, err := self() 159 | if err != nil { 160 | panic(fmt.Sprintf("cannot restart: cannot find self: %v", err)) 161 | } 162 | execName = selfName 163 | } 164 | 165 | if closeWatcher != nil { 166 | closeWatcher() 167 | } 168 | 169 | err := syscall.Exec(execName, append([]string{execName}, os.Args[1:]...), os.Environ()) 170 | if err != nil { 171 | panic(fmt.Sprintf("cannot restart: %v", err)) 172 | } 173 | } 174 | 175 | func stoppedTimer(cb func()) *time.Timer { 176 | t := time.AfterFunc(math.MaxInt64, cb) 177 | t.Stop() 178 | return t 179 | } 180 | 181 | // Get location to executable. 182 | func self() (string, error) { 183 | bin := os.Args[0] 184 | if !filepath.IsAbs(bin) { 185 | var err error 186 | bin, err = os.Executable() 187 | if err != nil { 188 | return "", fmt.Errorf( 189 | "cannot get path to binary %q (launch with absolute path): %w", 190 | os.Args[0], err) 191 | } 192 | } 193 | return bin, nil 194 | } 195 | 196 | // Get path relative to cwd. 197 | func relpath(p string) string { 198 | cwd, err := os.Getwd() 199 | if err != nil { 200 | return p 201 | } 202 | 203 | if strings.HasPrefix(p, cwd) { 204 | return "./" + strings.TrimLeft(p[len(cwd):], "/") 205 | } 206 | 207 | return p 208 | } 209 | -------------------------------------------------------------------------------- /reload_test.go: -------------------------------------------------------------------------------- 1 | package reload 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestLog(t *testing.T) { 10 | go func() { 11 | err := Do(log.Printf) 12 | if err != nil { 13 | panic(err) 14 | } 15 | }() 16 | 17 | time.Sleep(1 * time.Second) 18 | 19 | // TODO: maybe write some meaningful tests? 20 | } 21 | --------------------------------------------------------------------------------