From 996234d8ab5eb16496bf94c417372ddfe51b0d00 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Sun, 3 May 2020 11:36:31 +0300 Subject: [PATCH 1/5] kill whole process group Signed-off-by: Jari Kolehmainen --- main.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 2a990ee..71f8ec3 100644 --- a/main.go +++ b/main.go @@ -67,7 +67,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) @@ -109,7 +109,7 @@ func main() { } type processSignal struct { - signal os.Signal + signal syscall.Signal } type NightWatch struct { @@ -150,7 +150,7 @@ func (n *NightWatch) Run() { n.runCommand() } -func (n *NightWatch) Stop(exitSignal os.Signal) int { +func (n *NightWatch) Stop(exitSignal syscall.Signal) int { n.stopped = true if n.cmd == nil { return 0 @@ -293,7 +293,7 @@ func (n *NightWatch) runCommand() { changeDetected = true logrus.Debugf("got signal %+v", signal) if n.cmd != nil { - n.cmd.Process.Signal(signal.signal) + syscall.Kill(-1*n.cmd.Process.Pid, signal.signal) n.cmd.Wait() } } @@ -301,6 +301,9 @@ func (n *NightWatch) runCommand() { 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() From 4d81e31163e437ad8ae2b5fcf670f25118bdc5d6 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Sun, 3 May 2020 11:52:08 +0300 Subject: [PATCH 2/5] fix windows Signed-off-by: Jari Kolehmainen --- main.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 71f8ec3..ec6b918 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "os/exec" "os/signal" "path/filepath" + "runtime" "strings" "syscall" "time" @@ -293,7 +294,11 @@ func (n *NightWatch) runCommand() { changeDetected = true logrus.Debugf("got signal %+v", signal) if n.cmd != nil { - syscall.Kill(-1*n.cmd.Process.Pid, signal.signal) + if runtime.GOOS == "windows" { + n.cmd.Process.Signal(signal.signal) + } else { + syscall.Kill(-1*n.cmd.Process.Pid, signal.signal) + } n.cmd.Wait() } } From 3761cab17a28040208830f2c7e4a4fa72465a760 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Sun, 3 May 2020 11:55:41 +0300 Subject: [PATCH 3/5] fix windows Signed-off-by: Jari Kolehmainen --- main.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index ec6b918..c8d5650 100644 --- a/main.go +++ b/main.go @@ -306,8 +306,10 @@ func (n *NightWatch) runCommand() { for { changeDetected = false n.cmd = exec.Command(n.args.First(), n.args.Slice()[1:]...) - n.cmd.SysProcAttr = &syscall.SysProcAttr{ - Setsid: true, + if runtime.GOOS != "windows" { + n.cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, + } } n.cmd.Env = os.Environ() stdoutPipe, _ := n.cmd.StdoutPipe() From ec9bbec11960276c7a9ecbc899302a4415990b00 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Sun, 3 May 2020 12:13:07 +0300 Subject: [PATCH 4/5] refactor Signed-off-by: Jari Kolehmainen --- main.go | 253 ------------------------------------------ nightwatch.go | 188 +++++++++++++++++++++++++++++++ nightwatch_unix.go | 87 +++++++++++++++ nightwatch_windows.go | 83 ++++++++++++++ 4 files changed, 358 insertions(+), 253 deletions(-) create mode 100644 nightwatch.go create mode 100644 nightwatch_unix.go create mode 100644 nightwatch_windows.go diff --git a/main.go b/main.go index c8d5650..b4f56c2 100644 --- a/main.go +++ b/main.go @@ -1,19 +1,13 @@ package main import ( - "bufio" - "io" "log" "os" - "os/exec" "os/signal" - "path/filepath" - "runtime" "strings" "syscall" "time" - "github.com/fsnotify/fsnotify" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -108,250 +102,3 @@ func main() { log.Fatal(err) } } - -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) - } - } - } - } -} - -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 { - if runtime.GOOS == "windows" { - n.cmd.Process.Signal(signal.signal) - } else { - 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:]...) - if runtime.GOOS != "windows" { - 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.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..dd19541 --- /dev/null +++ b/nightwatch_unix.go @@ -0,0 +1,87 @@ +// +build linux darwin + +package main + +import ( + "io" + "os" + "os/exec" + "runtime" + "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 { + if runtime.GOOS == "windows" { + n.cmd.Process.Signal(signal.signal) + } else { + 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:]...) + if runtime.GOOS != "windows" { + 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..6847e6b --- /dev/null +++ b/nightwatch_windows.go @@ -0,0 +1,83 @@ +// +build windows + +package main + +import ( + "io" + "os" + "os/exec" + "runtime" + "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 { + n.cmd.Process.Signal(signal.signal) + n.cmd.Wait() + } + } + }() + for { + changeDetected = false + n.cmd = exec.Command(n.args.First(), n.args.Slice()[1:]...) + if runtime.GOOS != "windows" { + 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) + } +} From 419fbebf867a57b2ad8e197f76678bc89a6f7c69 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Sun, 3 May 2020 12:14:26 +0300 Subject: [PATCH 5/5] refactor Signed-off-by: Jari Kolehmainen --- nightwatch_unix.go | 13 +++---------- nightwatch_windows.go | 7 ------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/nightwatch_unix.go b/nightwatch_unix.go index dd19541..b9dd3b0 100644 --- a/nightwatch_unix.go +++ b/nightwatch_unix.go @@ -6,7 +6,6 @@ import ( "io" "os" "os/exec" - "runtime" "syscall" "time" @@ -24,11 +23,7 @@ func (n *NightWatch) runCommand() { changeDetected = true logrus.Debugf("got signal %+v", signal) if n.cmd != nil { - if runtime.GOOS == "windows" { - n.cmd.Process.Signal(signal.signal) - } else { - syscall.Kill(-1*n.cmd.Process.Pid, signal.signal) - } + syscall.Kill(-1*n.cmd.Process.Pid, signal.signal) n.cmd.Wait() } } @@ -36,10 +31,8 @@ func (n *NightWatch) runCommand() { for { changeDetected = false n.cmd = exec.Command(n.args.First(), n.args.Slice()[1:]...) - if runtime.GOOS != "windows" { - n.cmd.SysProcAttr = &syscall.SysProcAttr{ - Setsid: true, - } + n.cmd.SysProcAttr = &syscall.SysProcAttr{ + Setsid: true, } n.cmd.Env = os.Environ() stdoutPipe, _ := n.cmd.StdoutPipe() diff --git a/nightwatch_windows.go b/nightwatch_windows.go index 6847e6b..9dbaf27 100644 --- a/nightwatch_windows.go +++ b/nightwatch_windows.go @@ -6,8 +6,6 @@ import ( "io" "os" "os/exec" - "runtime" - "syscall" "time" "github.com/sirupsen/logrus" @@ -32,11 +30,6 @@ func (n *NightWatch) runCommand() { for { changeDetected = false n.cmd = exec.Command(n.args.First(), n.args.Slice()[1:]...) - if runtime.GOOS != "windows" { - n.cmd.SysProcAttr = &syscall.SysProcAttr{ - Setsid: true, - } - } n.cmd.Env = os.Environ() stdoutPipe, _ := n.cmd.StdoutPipe() stderrPipe, _ := n.cmd.StderrPipe()