├── .gitignore ├── LICENSE ├── README.md ├── README_EN.md ├── go.mod ├── go.sum ├── internal ├── runner │ └── runner.go └── watcher │ └── watcher.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 go-component 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 简体中文 | [English](README_EN.md) 2 | 3 | `go-watcher` 是一款实现 Golang 源码热重启的工具,可替换 `go run` 命令执行任意 `main` 入口程序,包括参数 4 | 5 | # 限制 6 | ```shell 7 | Golang 版本 >= 1.13 8 | ``` 9 | 10 | # 安装 11 | ```html 12 | go get -u github.com/go-component/go-watcher 13 | ``` 14 | 15 | # 特点 16 | * 开箱即用,完美替代 `go run` 命令 17 | * 即时重启,底层基于原子操作避免频繁重启 18 | * 稳定性强,底层实现多信号量安全关闭 19 | 20 | # go-watcher 命令行参数 21 | 22 | ```shell 23 | -w string 24 | path of watch, default exec path 25 | ``` 26 | 27 | - -w:需要监控的目录,默认为执行目录 28 | 29 | # 使用 30 | 31 | 与 `go run` 命令使用一致 32 | 33 | ```html 34 | go-watcher {main.go} 35 | ``` 36 | 37 | 例如携带命令行参数 38 | 39 | ```html 40 | go-watcher {main.go} -f etc/config.yaml 41 | ``` 42 | 43 | 路过的小伙伴们,可以的话,还烦请点个 star 哦,谢谢 ^ ^ -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](README.md) 2 | 3 | The `go-watcher` is a tool of the Go programing language to restart automatically. 4 | # Requirement 5 | ```shell 6 | Golang version >= 1.13 7 | ``` 8 | 9 | # Installation 10 | ```html 11 | go get -u github.com/go-component/go-watcher 12 | ``` 13 | 14 | # Feature 15 | * Out of the box, it can replace the `go run` command well. 16 | * Using atomic operation make it to avoid to restart frequently. 17 | * Stability, using multiple semaphore make it to stop safely. 18 | 19 | # go-watcher commandline args 20 | 21 | ```shell 22 | -w string 23 | path of watch, default exec path 24 | ``` 25 | 26 | 27 | # Tutorial 28 | 29 | Use the `go run` command to start. 30 | 31 | ```html 32 | go-watcher {main.go} 33 | ``` 34 | 35 | For example 36 | 37 | ```html 38 | go-watcher {main.go} -f etc/config.yaml 39 | ``` 40 | 41 | Please give a star if it's alright, thanks! 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-component/go-watcher 2 | 3 | go 1.13 4 | 5 | require github.com/fsnotify/fsnotify v1.4.9 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 2 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 3 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= 4 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 5 | -------------------------------------------------------------------------------- /internal/runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | "sync/atomic" 13 | "syscall" 14 | "time" 15 | ) 16 | 17 | var ErrorRepeat = errors.New("repeat exec") 18 | 19 | type Runner struct { 20 | goExecFilePath string 21 | WatchPath string 22 | 23 | execFilePath string 24 | execArgs []string 25 | 26 | sourceFilePath string 27 | WorkPath string 28 | 29 | buildArgs []string 30 | 31 | running int32 32 | procAttr *os.ProcAttr 33 | 34 | process atomic.Value 35 | } 36 | 37 | func execFilePathFormat(sourcePath string) string { 38 | 39 | rand.Seed(time.Now().UTC().UnixNano()) 40 | 41 | return fmt.Sprintf("%s_%d", strings.TrimRight(filepath.Base(sourcePath), ".go"), rand.Int()) 42 | } 43 | 44 | func NewRunner(args []string, watchPath string) (*Runner, error) { 45 | 46 | if len(args) < 1 { 47 | return nil, errors.New("command args at least one") 48 | } 49 | 50 | sourcePath, err := filepath.Abs(args[0]) 51 | 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | goExecFilePath, err := exec.LookPath("go") 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | workPath := filepath.Dir(sourcePath) 62 | 63 | execFilePath := execFilePathFormat(sourcePath) 64 | 65 | if watchPath == "" { 66 | watchPath = workPath 67 | }else{ 68 | watchPath, err = filepath.Abs(watchPath) 69 | if err != nil{ 70 | return nil ,err 71 | } 72 | } 73 | 74 | return &Runner{ 75 | goExecFilePath: goExecFilePath, 76 | 77 | execFilePath: execFilePath, 78 | execArgs: append([]string{ 79 | execFilePath, 80 | }, args[1:]...), 81 | 82 | sourceFilePath: sourcePath, 83 | WorkPath: workPath, 84 | WatchPath: watchPath, 85 | 86 | buildArgs: []string{ 87 | "", 88 | "build", 89 | "-o", 90 | execFilePath, 91 | sourcePath, 92 | }, 93 | 94 | procAttr: &os.ProcAttr{ 95 | Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, 96 | }, 97 | }, nil 98 | 99 | } 100 | 101 | func (r *Runner) build() error { 102 | 103 | process, err := os.StartProcess(r.goExecFilePath, r.buildArgs, r.procAttr) 104 | 105 | if err != nil { 106 | return err 107 | } 108 | 109 | _, err = process.Wait() 110 | if err != nil { 111 | return err 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func (r *Runner) start() (*os.Process, error) { 118 | 119 | if !atomic.CompareAndSwapInt32(&r.running, 0, 1) { 120 | return nil, ErrorRepeat 121 | } 122 | defer atomic.CompareAndSwapInt32(&r.running, 1, 0) 123 | 124 | r.Shutdown() 125 | 126 | log.Println("starting server...") 127 | 128 | err := r.build() 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | process, err := os.StartProcess(r.execFilePath, r.execArgs, r.procAttr) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | r.process.Store(process) 139 | 140 | return process, nil 141 | } 142 | 143 | func (r *Runner) Exec() error { 144 | 145 | process, err := r.start() 146 | 147 | if errors.Is(err, ErrorRepeat) { 148 | return nil 149 | } 150 | if err != nil { 151 | return err 152 | } 153 | 154 | _, err = process.Wait() 155 | if err != nil { 156 | return err 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func (r *Runner) Cleanup() { 163 | 164 | r.Shutdown() 165 | 166 | _ = os.Remove(r.execFilePath) 167 | } 168 | 169 | func (r *Runner) Shutdown() { 170 | 171 | process, ok := r.process.Load().(*os.Process) 172 | 173 | if ok { 174 | err := process.Signal(syscall.SIGTERM) 175 | if err == nil { 176 | log.Println("Closed server...") 177 | } 178 | } 179 | } 180 | 181 | func (r *Runner) Restart() error { 182 | 183 | return r.Exec() 184 | } 185 | -------------------------------------------------------------------------------- /internal/watcher/watcher.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "github.com/fsnotify/fsnotify" 5 | "github.com/go-component/go-watcher/internal/runner" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | type Watcher struct { 13 | runner *runner.Runner 14 | } 15 | 16 | func NewWatcher(runner *runner.Runner) *Watcher { 17 | 18 | return &Watcher{runner: runner} 19 | } 20 | 21 | func registerWatchPath(dirPath string, callback func(path string) error) (err error) { 22 | 23 | dir, err := ioutil.ReadDir(dirPath) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | PathSep := string(os.PathSeparator) 29 | 30 | for _, fi := range dir { 31 | 32 | path := dirPath + PathSep + fi.Name() 33 | 34 | if fi.IsDir() && !strings.HasPrefix(fi.Name(), ".") { 35 | err = callback(path) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | err = registerWatchPath(path, callback) 41 | if err != nil { 42 | return err 43 | } 44 | } 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (w *Watcher) Start() error { 51 | 52 | watcher, err := fsnotify.NewWatcher() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | defer func(watcher *fsnotify.Watcher) { 58 | err := watcher.Close() 59 | if err != nil { 60 | log.Println(err.Error()) 61 | } 62 | }(watcher) 63 | 64 | done := make(chan interface{}) 65 | 66 | go func() { 67 | 68 | defer func() { 69 | done <- recover() 70 | }() 71 | 72 | for { 73 | 74 | select { 75 | 76 | case event, ok := <-watcher.Events: 77 | 78 | if !ok || !strings.Contains(event.Name, ".go") { 79 | continue 80 | } 81 | 82 | if event.Op&fsnotify.Write == fsnotify.Write { 83 | go func() { 84 | err = w.runner.Restart() 85 | if err != nil { 86 | log.Println(err) 87 | } 88 | }() 89 | } 90 | 91 | case err, ok := <-watcher.Errors: 92 | 93 | if !ok { 94 | return 95 | } 96 | panic(err) 97 | } 98 | 99 | } 100 | }() 101 | 102 | err = watcher.Add(w.runner.WatchPath) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | err = registerWatchPath(w.runner.WatchPath, func(path string) error { 108 | err = watcher.Add(path) 109 | if err != nil { 110 | return err 111 | } 112 | return nil 113 | }) 114 | 115 | if err != nil { 116 | return err 117 | } 118 | 119 | state := <-done 120 | 121 | if err, ok := state.(error); ok { 122 | return err 123 | } 124 | 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/go-component/go-watcher/internal/runner" 6 | "github.com/go-component/go-watcher/internal/watcher" 7 | "log" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | ) 12 | 13 | var watchPath string 14 | 15 | func init(){ 16 | flag.StringVar(&watchPath,"w", "", "path of watch, default exec path") 17 | } 18 | 19 | func main() { 20 | flag.Parse() 21 | 22 | r, err := runner.NewRunner(flag.Args(), watchPath) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | defer r.Cleanup() 27 | 28 | w := watcher.NewWatcher(r) 29 | 30 | go func() { 31 | err := r.Exec() 32 | if err != nil { 33 | r.Cleanup() 34 | log.Fatal(err) 35 | } 36 | }() 37 | 38 | go func() { 39 | err := w.Start() 40 | if err != nil { 41 | r.Cleanup() 42 | log.Fatal(err) 43 | } 44 | }() 45 | 46 | sigChan := make(chan os.Signal, 1) 47 | 48 | signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) 49 | 50 | <-sigChan 51 | } 52 | --------------------------------------------------------------------------------