// Package reload offers lightweight automatic reloading of running processes. // // After initialisation with reload.Do() any changes to the binary will // restart the process. // // Example: // // go func() { // err := reload.Do(log.Printf) // if err != nil { // panic(err) // } // }() // // A list of additional directories to watch can be added: // // go func() { // err := reload.Do(log.Printf, reload.Dir("tpl", reloadTpl) // if err != nil { // panic(err) // } // }() // // Note that this package won't prevent race conditions (e.g. when assigning to // a global templates variable). You'll need to use sync.RWMutex yourself. package serv import ( "fmt" "os" "path/filepath" "runtime" "strings" "syscall" "time" "github.com/fsnotify/fsnotify" "github.com/pkg/errors" ) var binSelf string type dir struct { path string cb func() } // Dir is an additional directory to watch for changes. Directories are watched // non-recursively. // // The second argument is the callback that to run when the directory changes. // Use reload.ReExec() to restart the process. func Dir(path string, cb func()) dir { return dir{path, cb} } // nolint: golint // Do reload the current process when its binary changes. // // The log function is used to display an informational startup message and // errors. It works well with e.g. the standard log package or Logrus. // // The error return will only return initialisation errors. Once initialized it // will use the log function to print errors, rather than return. func Do(log func(string, ...interface{}), additional ...dir) error { watcher, err := fsnotify.NewWatcher() if err != nil { return errors.Wrap(err, "cannot setup watcher") } defer watcher.Close() // nolint: errcheck binSelf, err = self() if err != nil { return err } // Watch the directory, because a recompile renames the existing // file (rather than rewriting it), so we won't get events for that. dirs := make([]string, len(additional)+1) dirs[0] = filepath.Dir(binSelf) for i := range additional { path, err := filepath.Abs(additional[i].path) if err != nil { return errors.Wrapf(err, "cannot get absolute path to %q: %v", additional[i].path, err) } s, err := os.Stat(path) if err != nil { return errors.Wrap(err, "os.Stat") } if !s.IsDir() { return errors.Errorf("not a directory: %q; can only watch directories", additional[i].path) } additional[i].path = path dirs[i+1] = path } done := make(chan bool) go func() { for { select { case err := <-watcher.Errors: // Standard logger doesn't have anything other than Print, // Panic, and Fatal :-/ Printf() is probably best. log("reload error: %v", err) case event := <-watcher.Events: // Ensure that we use the correct events, as they are not uniform across // platforms. See https://github.com/fsnotify/fsnotify/issues/74 if conf.Production == false && strings.HasSuffix(event.Name, "/allow.list") { continue } logger.Info().Msgf("Reloading, file changed detected '%s'", event) var trigger bool switch runtime.GOOS { case "darwin", "freebsd", "openbsd", "netbsd", "dragonfly": trigger = event.Op&fsnotify.Create == fsnotify.Create case "linux": trigger = event.Op&fsnotify.Write == fsnotify.Write default: trigger = event.Op&fsnotify.Create == fsnotify.Create log("reload: untested GOOS %q; this package may not work correctly", runtime.GOOS) } if !trigger { continue } if event.Name == binSelf { // Wait for writes to finish. time.Sleep(100 * time.Millisecond) ReExec() } for _, a := range additional { if strings.HasPrefix(event.Name, a.path) { time.Sleep(100 * time.Millisecond) a.cb() } } } } }() for _, d := range dirs { if err := watcher.Add(d); err != nil { return errors.Wrapf(err, "cannot add %q to watcher", d) } } add := "" if len(additional) > 0 { reldirs := make([]string, len(dirs)-1) for i := range dirs[1:] { reldirs[i] = relpath(dirs[i+1]) } add = fmt.Sprintf(" (additional dirs: %s)", strings.Join(reldirs, ", ")) } log("restarting %q when it changes%s", relpath(binSelf), add) <-done return nil } // Exec replaces the current process with a new copy of itself. func ReExec() { err := syscall.Exec(binSelf, append([]string{binSelf}, os.Args[1:]...), os.Environ()) if err != nil { errlog.Fatal().Err(err).Msg("cannot restart") } } // Get location to executable. func self() (string, error) { bin := os.Args[0] if !filepath.IsAbs(bin) { var err error bin, err = os.Executable() if err != nil { return "", errors.Wrapf(err, "cannot get path to binary %q (launch with absolute path): %v", os.Args[0], err) } } return bin, nil } // Get path relative to cwd func relpath(p string) string { cwd, err := os.Getwd() if err != nil { return p } if strings.HasPrefix(p, cwd) { return "./" + strings.TrimLeft(p[len(cwd):], "/") } return p }