diff --git a/main.go b/main.go index 2a990ee..b4f56c2 100644 --- a/main.go +++ b/main.go @@ -1,18 +1,13 @@ package main import ( - "bufio" - "io" "log" "os" - "os/exec" "os/signal" - "path/filepath" "strings" "syscall" "time" - "github.com/fsnotify/fsnotify" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -67,7 +62,7 @@ func main() { go nightWatch.Run() exitSignal := <-done - exitCode := nightWatch.Stop(exitSignal) + exitCode := nightWatch.Stop(exitSignal.(syscall.Signal)) time.Sleep(10 * time.Second) nightWatch.Cleanup() os.Exit(exitCode) @@ -107,241 +102,3 @@ func main() { log.Fatal(err) } } - -type processSignal struct { - signal os.Signal -} - -type NightWatch struct { - cmd *exec.Cmd - watchCmd string - filesList []string - args cli.Args - cmdSignal chan *processSignal - watcher *fsnotify.Watcher - exitOnChange int - exitOnError bool - exitOnSuccess bool - stopped bool -} - -func (n *NightWatch) Run() { - n.cmdSignal = make(chan *processSignal, 1) - watcher, err := fsnotify.NewWatcher() - if err != nil { - logrus.Fatal(err) - } - n.watcher = watcher - - files := []string{} - stat, _ := os.Stdin.Stat() - if (stat.Mode() & os.ModeCharDevice) == 0 { - logrus.Debugln("reading files from stdin") - files = n.watchFromStdin() - } else if len(n.filesList) > 0 { - logrus.Debugf("Reading files from static list: %s", strings.Join(n.filesList, ", ")) - files = n.filesList - } else { - logrus.Debugf("Reading files from command: %s", n.watchCmd) - files = n.watchFromCmd() - } - n.watchFiles(files) - go n.handleWatchEvents() - n.runCommand() -} - -func (n *NightWatch) Stop(exitSignal os.Signal) int { - n.stopped = true - if n.cmd == nil { - return 0 - } - if n.cmd.ProcessState != nil && n.cmd.ProcessState.Exited() { - return n.cmd.ProcessState.ExitCode() - } - - logrus.Debugf("stop requested: %s", exitSignal) - n.cmdSignal <- &processSignal{signal: exitSignal} - n.cmd.Wait() - return n.cmd.ProcessState.ExitCode() -} - -func (n *NightWatch) Cleanup() { - if n.watcher != nil { - n.watcher.Close() - } -} - -func (n *NightWatch) handleWatchEvents() { - for { - select { - case event, ok := <-n.watcher.Events: - if !ok { - return - } - var signal *processSignal - if event.Op == fsnotify.Write || event.Op == fsnotify.Chmod { - logrus.Debugf("modified (%s): %s", event.Op.String(), event.Name) - signal = &processSignal{signal: syscall.SIGTERM} - } else if event.Op == fsnotify.Create { - logrus.Debugf("created: %s", event.Name) - signal = &processSignal{signal: syscall.SIGTERM} - } else if event.Op == fsnotify.Remove { - logrus.Debugf("removed: %s", event.Name) - n.watcher.Remove(event.Name) - signal = &processSignal{signal: syscall.SIGTERM} - } - if signal == nil { - return - } - select { - case n.cmdSignal <- signal: - default: - logrus.Debugln("restart already scheduled, ignoring change.") - } - case err, ok := <-n.watcher.Errors: - if !ok { - return - } - logrus.Warnf("error: %s", err.Error()) - } - } -} - -func (n *NightWatch) watchFromStdin() []string { - files := []string{} - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - file := scanner.Text() - files = append(files, file) - } - - return files -} - -func (n *NightWatch) watchFromCmd() []string { - shell := os.Getenv("SHELL") - if shell == "" { - shell, _ = exec.LookPath("sh") - } - cmd := exec.Command(shell, "-c", n.watchCmd) - cmd.Env = os.Environ() - stdoutPipe, err := cmd.StdoutPipe() - if err != nil { - logrus.Errorln(err) - os.Exit(1) - } - defer func() { - if stdoutPipe != nil { - stdoutPipe.Close() - } - }() - cmd.Start() - files := []string{} - scanner := bufio.NewScanner(stdoutPipe) - for scanner.Scan() { - file := scanner.Text() - files = append(files, file) - } - cmd.Wait() - if cmd.ProcessState.ExitCode() != 0 { - os.Exit(cmd.ProcessState.ExitCode()) - } - - return files -} - -func (n *NightWatch) watchFiles(files []string) { - watchedPaths := []string{} - for _, file := range files { - absFile, err := filepath.Abs(file) - if err == nil { - fileInfo, _ := os.Stat(absFile) - shouldWatch := true - for _, path := range watchedPaths { - var dirName string - if fileInfo.IsDir() { - dirName = absFile - } else { - dirName = filepath.Dir(absFile) - } - if dirName == path { - shouldWatch = false - } - } - if shouldWatch { - logrus.Debugf("watching file %s", absFile) - err = n.watcher.Add(absFile) - if err != nil { - logrus.Warningf("failed to watch file %s: %s", absFile, err.Error()) - os.Exit(1) - } else if fileInfo.IsDir() { - watchedPaths = append(watchedPaths, absFile) - } - } - } - } -} - -func (n *NightWatch) runCommand() { - changeDetected := false - go func() { - for { - signal := <-n.cmdSignal - if changeDetected { - continue - } - changeDetected = true - logrus.Debugf("got signal %+v", signal) - if n.cmd != nil { - n.cmd.Process.Signal(signal.signal) - n.cmd.Wait() - } - } - }() - for { - changeDetected = false - n.cmd = exec.Command(n.args.First(), n.args.Slice()[1:]...) - n.cmd.Env = os.Environ() - stdoutPipe, _ := n.cmd.StdoutPipe() - stderrPipe, _ := n.cmd.StderrPipe() - defer func() { - if stdoutPipe != nil { - stdoutPipe.Close() - } - if stderrPipe != nil { - stderrPipe.Close() - } - }() - - err := n.cmd.Start() - if err != nil { - logrus.Fatal(err.Error()) - } - go func() { - io.Copy(os.Stdout, stdoutPipe) - }() - go func() { - io.Copy(os.Stderr, stderrPipe) - }() - logrus.Debugf("process (pid: %d) started", n.cmd.Process.Pid) - n.cmd.Wait() - logrus.Debugf("process (pid: %d) killed", n.cmd.Process.Pid) - - exitCode := n.cmd.ProcessState.ExitCode() - if n.stopped { - os.Exit(exitCode) - } - if changeDetected { - if n.exitOnChange > -1 { - os.Exit(n.exitOnChange) - } - } else { - if n.exitOnError && exitCode > 0 { - os.Exit(exitCode) - } else if n.exitOnSuccess && exitCode == 0 { - os.Exit(exitCode) - } - } - time.Sleep(500 * time.Millisecond) - } -} diff --git a/nightwatch.go b/nightwatch.go new file mode 100644 index 0000000..d4f21e2 --- /dev/null +++ b/nightwatch.go @@ -0,0 +1,188 @@ +package main + +import ( + "bufio" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + + "github.com/fsnotify/fsnotify" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +type processSignal struct { + signal syscall.Signal +} + +type NightWatch struct { + cmd *exec.Cmd + watchCmd string + filesList []string + args cli.Args + cmdSignal chan *processSignal + watcher *fsnotify.Watcher + exitOnChange int + exitOnError bool + exitOnSuccess bool + stopped bool +} + +func (n *NightWatch) Run() { + n.cmdSignal = make(chan *processSignal, 1) + watcher, err := fsnotify.NewWatcher() + if err != nil { + logrus.Fatal(err) + } + n.watcher = watcher + + files := []string{} + stat, _ := os.Stdin.Stat() + if (stat.Mode() & os.ModeCharDevice) == 0 { + logrus.Debugln("reading files from stdin") + files = n.watchFromStdin() + } else if len(n.filesList) > 0 { + logrus.Debugf("Reading files from static list: %s", strings.Join(n.filesList, ", ")) + files = n.filesList + } else { + logrus.Debugf("Reading files from command: %s", n.watchCmd) + files = n.watchFromCmd() + } + n.watchFiles(files) + go n.handleWatchEvents() + n.runCommand() +} + +func (n *NightWatch) Stop(exitSignal syscall.Signal) int { + n.stopped = true + if n.cmd == nil { + return 0 + } + if n.cmd.ProcessState != nil && n.cmd.ProcessState.Exited() { + return n.cmd.ProcessState.ExitCode() + } + + logrus.Debugf("stop requested: %s", exitSignal) + n.cmdSignal <- &processSignal{signal: exitSignal} + n.cmd.Wait() + return n.cmd.ProcessState.ExitCode() +} + +func (n *NightWatch) Cleanup() { + if n.watcher != nil { + n.watcher.Close() + } +} + +func (n *NightWatch) handleWatchEvents() { + for { + select { + case event, ok := <-n.watcher.Events: + if !ok { + return + } + var signal *processSignal + if event.Op == fsnotify.Write || event.Op == fsnotify.Chmod { + logrus.Debugf("modified (%s): %s", event.Op.String(), event.Name) + signal = &processSignal{signal: syscall.SIGTERM} + } else if event.Op == fsnotify.Create { + logrus.Debugf("created: %s", event.Name) + signal = &processSignal{signal: syscall.SIGTERM} + } else if event.Op == fsnotify.Remove { + logrus.Debugf("removed: %s", event.Name) + n.watcher.Remove(event.Name) + signal = &processSignal{signal: syscall.SIGTERM} + } + if signal == nil { + return + } + select { + case n.cmdSignal <- signal: + default: + logrus.Debugln("restart already scheduled, ignoring change.") + } + case err, ok := <-n.watcher.Errors: + if !ok { + return + } + logrus.Warnf("error: %s", err.Error()) + } + } +} + +func (n *NightWatch) watchFromStdin() []string { + files := []string{} + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + file := scanner.Text() + files = append(files, file) + } + + return files +} + +func (n *NightWatch) watchFromCmd() []string { + shell := os.Getenv("SHELL") + if shell == "" { + shell, _ = exec.LookPath("sh") + } + cmd := exec.Command(shell, "-c", n.watchCmd) + cmd.Env = os.Environ() + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + logrus.Errorln(err) + os.Exit(1) + } + defer func() { + if stdoutPipe != nil { + stdoutPipe.Close() + } + }() + cmd.Start() + files := []string{} + scanner := bufio.NewScanner(stdoutPipe) + for scanner.Scan() { + file := scanner.Text() + files = append(files, file) + } + cmd.Wait() + if cmd.ProcessState.ExitCode() != 0 { + os.Exit(cmd.ProcessState.ExitCode()) + } + + return files +} + +func (n *NightWatch) watchFiles(files []string) { + watchedPaths := []string{} + for _, file := range files { + absFile, err := filepath.Abs(file) + if err == nil { + fileInfo, _ := os.Stat(absFile) + shouldWatch := true + for _, path := range watchedPaths { + var dirName string + if fileInfo.IsDir() { + dirName = absFile + } else { + dirName = filepath.Dir(absFile) + } + if dirName == path { + shouldWatch = false + } + } + if shouldWatch { + logrus.Debugf("watching file %s", absFile) + err = n.watcher.Add(absFile) + if err != nil { + logrus.Warningf("failed to watch file %s: %s", absFile, err.Error()) + os.Exit(1) + } else if fileInfo.IsDir() { + watchedPaths = append(watchedPaths, absFile) + } + } + } + } +} diff --git a/nightwatch_unix.go b/nightwatch_unix.go new file mode 100644 index 0000000..b9dd3b0 --- /dev/null +++ b/nightwatch_unix.go @@ -0,0 +1,80 @@ +// +build linux darwin + +package main + +import ( + "io" + "os" + "os/exec" + "syscall" + "time" + + "github.com/sirupsen/logrus" +) + +func (n *NightWatch) runCommand() { + changeDetected := false + go func() { + for { + signal := <-n.cmdSignal + if changeDetected { + continue + } + changeDetected = true + logrus.Debugf("got signal %+v", signal) + if n.cmd != nil { + syscall.Kill(-1*n.cmd.Process.Pid, signal.signal) + n.cmd.Wait() + } + } + }() + for { + changeDetected = false + n.cmd = exec.Command(n.args.First(), n.args.Slice()[1:]...) + n.cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + } + n.cmd.Env = os.Environ() + stdoutPipe, _ := n.cmd.StdoutPipe() + stderrPipe, _ := n.cmd.StderrPipe() + defer func() { + if stdoutPipe != nil { + stdoutPipe.Close() + } + if stderrPipe != nil { + stderrPipe.Close() + } + }() + + err := n.cmd.Start() + if err != nil { + logrus.Fatal(err.Error()) + } + go func() { + io.Copy(os.Stdout, stdoutPipe) + }() + go func() { + io.Copy(os.Stderr, stderrPipe) + }() + logrus.Debugf("process (pid: %d) started", n.cmd.Process.Pid) + n.cmd.Wait() + logrus.Debugf("process (pid: %d) killed", n.cmd.Process.Pid) + + exitCode := n.cmd.ProcessState.ExitCode() + if n.stopped { + os.Exit(exitCode) + } + if changeDetected { + if n.exitOnChange > -1 { + os.Exit(n.exitOnChange) + } + } else { + if n.exitOnError && exitCode > 0 { + os.Exit(exitCode) + } else if n.exitOnSuccess && exitCode == 0 { + os.Exit(exitCode) + } + } + time.Sleep(500 * time.Millisecond) + } +} diff --git a/nightwatch_windows.go b/nightwatch_windows.go new file mode 100644 index 0000000..9dbaf27 --- /dev/null +++ b/nightwatch_windows.go @@ -0,0 +1,76 @@ +// +build windows + +package main + +import ( + "io" + "os" + "os/exec" + "time" + + "github.com/sirupsen/logrus" +) + +func (n *NightWatch) runCommand() { + changeDetected := false + go func() { + for { + signal := <-n.cmdSignal + if changeDetected { + continue + } + changeDetected = true + logrus.Debugf("got signal %+v", signal) + if n.cmd != nil { + n.cmd.Process.Signal(signal.signal) + n.cmd.Wait() + } + } + }() + for { + changeDetected = false + n.cmd = exec.Command(n.args.First(), n.args.Slice()[1:]...) + n.cmd.Env = os.Environ() + stdoutPipe, _ := n.cmd.StdoutPipe() + stderrPipe, _ := n.cmd.StderrPipe() + defer func() { + if stdoutPipe != nil { + stdoutPipe.Close() + } + if stderrPipe != nil { + stderrPipe.Close() + } + }() + + err := n.cmd.Start() + if err != nil { + logrus.Fatal(err.Error()) + } + go func() { + io.Copy(os.Stdout, stdoutPipe) + }() + go func() { + io.Copy(os.Stderr, stderrPipe) + }() + logrus.Debugf("process (pid: %d) started", n.cmd.Process.Pid) + n.cmd.Wait() + logrus.Debugf("process (pid: %d) killed", n.cmd.Process.Pid) + + exitCode := n.cmd.ProcessState.ExitCode() + if n.stopped { + os.Exit(exitCode) + } + if changeDetected { + if n.exitOnChange > -1 { + os.Exit(n.exitOnChange) + } + } else { + if n.exitOnError && exitCode > 0 { + os.Exit(exitCode) + } else if n.exitOnSuccess && exitCode == 0 { + os.Exit(exitCode) + } + } + time.Sleep(500 * time.Millisecond) + } +}