git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/tools/watchgod/main.go (about) 1 /* 2 watchgod is a very simple compile daemon for Go. 3 watchgod watches your .go files in a directory and invokes `go build` 4 if a file changes. 5 Examples 6 In its simplest form, the defaults will do. With the current working directory set 7 to the source directory you can simply… 8 9 $ watchgod 10 11 … and it will recompile your code whenever you save a source file. 12 If you want it to also run your program each time it builds you might add… 13 14 $ watchgod -command="./MyProgram -my-options" 15 16 … and it will also keep a copy of your program running. Killing the old one and 17 starting a new one each time you build. For advanced usage you can also supply 18 the changed file to the command by doing… 19 20 $ watchgod -command="./MyProgram -my-options %[1]s" 21 22 …but note that this will not be set on the first start. 23 You may find that you need to exclude some directories and files from 24 monitoring, such as a .git repository or emacs temporary files… 25 26 $ watchgod -exclude-dir=.git -exclude=".#*" 27 28 If you want to monitor files other than .go and .c files you might… 29 30 $ watchgod -include=Makefile -include="*.less" -include="*.tmpl" 31 32 Options 33 There are command line options. 34 35 FILE SELECTION 36 -directory=XXX – Which directory to monitor for changes 37 -recursive=XXX – Look into subdirectories 38 -exclude-dir=XXX – Exclude directories matching glob pattern XXX 39 -exlude=XXX – Exclude files whose basename matches glob pattern XXX 40 -include=XXX – Include files whose basename matches glob pattern XXX 41 -pattern=XXX – Include files whose path matches regexp XXX 42 MISC 43 -log-prefix - Enable/disable stdout/stderr labelling for the child process 44 -graceful-kill - On supported platforms, send the child process a SIGTERM to 45 allow it to exit gracefully if possible. 46 -graceful-timeout - Duration (in seconds) to wait for graceful kill to complete 47 -verbose - Print information about watched directories. 48 ACTIONS 49 -build=CCC – Execute CCC to rebuild when a file changes 50 -command=CCC – Run command CCC after a successful build, stops previous command first 51 */ 52 package main 53 54 import ( 55 "bufio" 56 "flag" 57 "fmt" 58 "io" 59 "log" 60 "os" 61 "os/exec" 62 "os/signal" 63 "path/filepath" 64 "regexp" 65 "strings" 66 "syscall" 67 "time" 68 69 "github.com/fsnotify/fsnotify" 70 ) 71 72 // Milliseconds to wait for the next job to begin after a file change 73 const WorkDelay = 900 74 75 // Default pattern to match files which trigger a build 76 const FilePattern = `(.+\.go|.+\.c)$` 77 78 type globList []string 79 80 func (g *globList) String() string { 81 return fmt.Sprint(*g) 82 } 83 func (g *globList) Set(value string) error { 84 *g = append(*g, filepath.Clean(value)) 85 return nil 86 } 87 func (g *globList) Matches(value string) bool { 88 for _, v := range *g { 89 if match, err := filepath.Match(v, value); err != nil { 90 log.Fatalf("Bad pattern \"%s\": %s", v, err.Error()) 91 } else if match { 92 return true 93 } 94 } 95 return false 96 } 97 98 var ( 99 flag_directory = flag.String("directory", ".", "Directory to watch for changes") 100 flag_pattern = flag.String("pattern", FilePattern, "Pattern of watched files") 101 flag_command = flag.String("command", "", "Command to run and restart after build") 102 flag_command_stop = flag.Bool("command-stop", false, "Stop command before building") 103 flag_build = flag.String("build", "", "Command to rebuild after changes") 104 flag_build_dir = flag.String("build-dir", "", "Directory to run build command in. Defaults to directory") 105 flag_run_dir = flag.String("run-dir", "", "Directory to run command in. Defaults to directory") 106 flag_logprefix = flag.Bool("log-prefix", true, "Print log timestamps and subprocess stderr/stdout output") 107 flag_gracefulkill = flag.Bool("graceful-kill", false, "Gracefully attempt to kill the child process by sending a SIGTERM first") 108 flag_gracefultimeout = flag.Uint("graceful-timeout", 3, "Duration (in seconds) to wait for graceful kill to complete") 109 flag_verbose = flag.Bool("verbose", false, "Be verbose about which directories are watched.") 110 111 // initialized in main() due to custom type. 112 flag_include_dirs globList 113 flag_excludedDirs globList 114 flag_excludedFiles globList 115 flag_includedFiles globList 116 ) 117 118 func okColor(format string, args ...interface{}) string { 119 return fmt.Sprintf(format, args...) 120 } 121 122 func failColor(format string, args ...interface{}) string { 123 return fmt.Sprintf(format, args...) 124 } 125 126 // Run `go build` and print the output if something's gone wrong. 127 func build() bool { 128 log.Println(okColor("Running build command!")) 129 130 args := strings.Split(*flag_build, " ") 131 if len(args) == 0 || *flag_build == "" { 132 // If the user has specified and empty then we are done. 133 return true 134 } 135 136 cmd := exec.Command(args[0], args[1:]...) 137 138 if *flag_build_dir != "" { 139 cmd.Dir = *flag_build_dir 140 } else { 141 cmd.Dir = *flag_directory 142 } 143 144 output, err := cmd.CombinedOutput() 145 146 if err == nil { 147 log.Println(okColor("Build ok.")) 148 } else { 149 log.Println(failColor("Error while building:\n"), failColor(string(output))) 150 } 151 152 return err == nil 153 } 154 155 func matchesPattern(pattern *regexp.Regexp, file string) bool { 156 return pattern.MatchString(file) 157 } 158 159 // Accept build jobs and start building when there are no jobs rushing in. 160 // The inrush protection is WorkDelay milliseconds long, in this period 161 // every incoming job will reset the timer. 162 func builder(jobs <-chan string, buildStarted chan<- string, buildDone chan<- bool) { 163 createThreshold := func() <-chan time.Time { 164 return time.After(time.Duration(WorkDelay * time.Millisecond)) 165 } 166 167 threshold := createThreshold() 168 eventPath := "" 169 170 for { 171 select { 172 case eventPath = <-jobs: 173 threshold = createThreshold() 174 case <-threshold: 175 buildStarted <- eventPath 176 buildDone <- build() 177 } 178 } 179 } 180 181 func logger(pipeChan <-chan io.ReadCloser) { 182 dumper := func(pipe io.ReadCloser, prefix string) { 183 reader := bufio.NewReader(pipe) 184 185 readloop: 186 for { 187 line, err := reader.ReadString('\n') 188 189 if err != nil { 190 break readloop 191 } 192 193 if *flag_logprefix { 194 log.Print(prefix, " ", line) 195 } else { 196 log.Print(line) 197 } 198 } 199 } 200 201 for { 202 pipe := <-pipeChan 203 go dumper(pipe, "stdout:") 204 205 pipe = <-pipeChan 206 go dumper(pipe, "stderr:") 207 } 208 } 209 210 // Start the supplied command and return stdout and stderr pipes for logging. 211 func startCommand(command string) (cmd *exec.Cmd, stdout io.ReadCloser, stderr io.ReadCloser, err error) { 212 args := strings.Split(command, " ") 213 cmd = exec.Command(args[0], args[1:]...) 214 215 if *flag_run_dir != "" { 216 cmd.Dir = *flag_run_dir 217 } 218 219 if stdout, err = cmd.StdoutPipe(); err != nil { 220 err = fmt.Errorf("can't get stdout pipe for command: %s", err) 221 return 222 } 223 224 if stderr, err = cmd.StderrPipe(); err != nil { 225 err = fmt.Errorf("can't get stderr pipe for command: %s", err) 226 return 227 } 228 229 if err = cmd.Start(); err != nil { 230 err = fmt.Errorf("can't start command: %s", err) 231 return 232 } 233 234 return 235 } 236 237 // Run the command in the given string and restart it after 238 // a message was received on the buildDone channel. 239 func runner(commandTemplate string, buildStarted <-chan string, buildSuccess <-chan bool) { 240 var currentProcess *os.Process 241 pipeChan := make(chan io.ReadCloser) 242 243 go logger(pipeChan) 244 245 go func() { 246 sigChan := make(chan os.Signal, 1) 247 signal.Notify(sigChan, fatalSignals...) 248 <-sigChan 249 log.Println(okColor("Received signal, terminating cleanly.")) 250 if currentProcess != nil { 251 killProcess(currentProcess) 252 } 253 os.Exit(0) 254 }() 255 256 for { 257 eventPath := <-buildStarted 258 259 // append %0.s to use format specifier even if not supplied by user 260 // to suppress warning in returned string. 261 command := fmt.Sprintf("%0.s"+commandTemplate, eventPath) 262 263 if !*flag_command_stop { 264 if !<-buildSuccess { 265 continue 266 } 267 } 268 269 if currentProcess != nil { 270 killProcess(currentProcess) 271 } 272 if *flag_command_stop { 273 log.Println(okColor("Command stopped. Waiting for build to complete.")) 274 if !<-buildSuccess { 275 continue 276 } 277 } 278 279 log.Println(okColor("Restarting the given command.")) 280 cmd, stdoutPipe, stderrPipe, err := startCommand(command) 281 282 if err != nil { 283 log.Fatal(failColor("Could not start command: %s", err)) 284 } 285 286 pipeChan <- stdoutPipe 287 pipeChan <- stderrPipe 288 289 currentProcess = cmd.Process 290 } 291 } 292 293 func killProcess(process *os.Process) { 294 if *flag_gracefulkill { 295 killProcessGracefully(process) 296 } else { 297 killProcessHard(process) 298 } 299 } 300 301 func killProcessHard(process *os.Process) { 302 log.Println(okColor("Hard stopping the current process..")) 303 304 if err := process.Kill(); err != nil { 305 log.Println(failColor("Warning: could not kill child process. It may have already exited.")) 306 } 307 308 if _, err := process.Wait(); err != nil { 309 log.Fatal(failColor("Could not wait for child process. Aborting due to danger of infinite forks.")) 310 } 311 } 312 313 func killProcessGracefully(process *os.Process) { 314 done := make(chan error, 1) 315 go func() { 316 log.Println(okColor("Gracefully stopping the current process..")) 317 if err := terminateGracefully(process); err != nil { 318 done <- err 319 return 320 } 321 _, err := process.Wait() 322 done <- err 323 }() 324 325 select { 326 case <-time.After(time.Duration(*flag_gracefultimeout) * time.Second): 327 log.Println(failColor("Could not gracefully stop the current process, proceeding to hard stop.")) 328 killProcessHard(process) 329 <-done 330 case err := <-done: 331 if err != nil { 332 log.Fatal(failColor("Could not kill child process. Aborting due to danger of infinite forks.")) 333 } 334 } 335 } 336 337 func flusher(buildStarted <-chan string, buildSuccess <-chan bool) { 338 for { 339 <-buildStarted 340 <-buildSuccess 341 } 342 } 343 344 func main() { 345 flag.Var(&flag_excludedDirs, "exclude-dir", " Don't watch directories matching this name") 346 flag.Var(&flag_excludedFiles, "exclude", " Don't watch files matching this name") 347 flag.Var(&flag_includedFiles, "include", " Watch files matching this name") 348 flag.Var(&flag_include_dirs, "include-dir", "More directories to watch") 349 350 flag.Parse() 351 352 if !*flag_logprefix { 353 log.SetFlags(0) 354 } 355 356 if *flag_directory == "" { 357 fmt.Fprintf(os.Stderr, "-directory=... is required.\n") 358 os.Exit(1) 359 } 360 361 if *flag_gracefulkill && !gracefulTerminationPossible() { 362 log.Fatal("Graceful termination is not supported on your platform.") 363 } 364 365 watcher, err := fsnotify.NewWatcher() 366 367 if err != nil { 368 log.Fatal(err) 369 } 370 371 defer watcher.Close() 372 373 if flag_include_dirs == nil { 374 flag_include_dirs = make(globList, 0) 375 } 376 dirs := append(flag_include_dirs, *flag_directory) 377 378 for _, dir := range dirs { 379 err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 380 if err == nil && info.IsDir() { 381 if flag_excludedDirs.Matches(path) { 382 return filepath.SkipDir 383 } else { 384 if *flag_verbose { 385 log.Printf("Watching directory '%s' for changes.\n", path) 386 } 387 return watcher.Add(path) 388 } 389 } 390 return err 391 }) 392 393 if err != nil { 394 log.Fatal("filepath.Walk():", err) 395 } 396 397 if err := watcher.Add(dir); err != nil { 398 log.Fatal("watcher.Add():", err) 399 } 400 } 401 402 pattern := regexp.MustCompile(*flag_pattern) 403 jobs := make(chan string) 404 buildSuccess := make(chan bool) 405 buildStarted := make(chan string) 406 407 go builder(jobs, buildStarted, buildSuccess) 408 409 if *flag_command != "" { 410 go runner(*flag_command, buildStarted, buildSuccess) 411 } else { 412 go flusher(buildStarted, buildSuccess) 413 } 414 415 for { 416 select { 417 case ev := <-watcher.Events: 418 if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create { 419 base := filepath.Base(ev.Name) 420 421 // Assume it is a directory and track it. 422 if !flag_excludedDirs.Matches(ev.Name) { 423 watcher.Add(ev.Name) 424 } 425 426 if flag_includedFiles.Matches(base) || matchesPattern(pattern, ev.Name) { 427 if !flag_excludedFiles.Matches(base) { 428 jobs <- ev.Name 429 } 430 } 431 } 432 433 case err := <-watcher.Errors: 434 if v, ok := err.(*os.SyscallError); ok { 435 if v.Err == syscall.EINTR { 436 continue 437 } 438 log.Fatal("watcher.Error: SyscallError:", v) 439 } 440 log.Fatal("watcher.Error:", err) 441 } 442 } 443 }