diff --git a/cmd/modd/main.go b/cmd/modd/main.go index 5137079..bec8c58 100644 --- a/cmd/modd/main.go +++ b/cmd/modd/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "runtime" "github.com/cortesi/modd" "github.com/cortesi/modd/notify" @@ -45,9 +46,21 @@ var debug = kingpin.Flag("debug", "Debugging for modd development"). Default("false"). Bool() +var pipesignals = new(bool) + func main() { kingpin.CommandLine.HelpFlag.Short('h') kingpin.Version(modd.Version) + + if runtime.GOOS == "windows" { + pipesignals = kingpin.Flag( + "pipesignals", + "For signals that don't exist on Windows, write their name to the stdin of daemons instead"). + // TODO(DH): Should it be enabled by default? + Default("false"). + Bool() + } + kingpin.Parse() if *ignores { @@ -75,7 +88,7 @@ func main() { notifiers = append(notifiers, ¬ify.BeepNotifier{}) } - mr, err := modd.NewModRunner(*file, log, notifiers, !(*noconf)) + mr, err := modd.NewModRunner(*file, log, notifiers, !(*noconf), *pipesignals) if err != nil { log.Shout("%s", err) return diff --git a/conf/block_windows.go b/conf/block_windows.go index 08fb8eb..ef0b33a 100644 --- a/conf/block_windows.go +++ b/conf/block_windows.go @@ -12,8 +12,9 @@ func (b *Block) addDaemon(command string, options []string) error { b.Daemons = []Daemon{} } d := Daemon{ - Command: command, - RestartSignal: syscall.SIGHUP, + Command: command, + RestartSignal: syscall.SIGHUP, + PipeRestartSignal: true, } for _, v := range options { switch v { @@ -25,6 +26,10 @@ func (b *Block) addDaemon(command string, options []string) error { d.RestartSignal = syscall.SIGINT case "+sigkill": d.RestartSignal = syscall.SIGKILL + // Although Windows doesn't have signals, Go does recognise the + // intention of SIGKILL and uses a native API to terminate the + // target process. + d.PipeRestartSignal = false case "+sigquit": d.RestartSignal = syscall.SIGQUIT default: diff --git a/conf/conf.go b/conf/conf.go index ffbd2e2..7eeae72 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -9,8 +9,9 @@ import ( // A Daemon is a persistent process that is kept running type Daemon struct { - Command string - RestartSignal os.Signal + Command string + RestartSignal os.Signal + PipeRestartSignal bool } // A Prep runs and terminates diff --git a/conf/parse_posix_test.go b/conf/parse_posix_test.go index 4b8bf62..52051e0 100644 --- a/conf/parse_posix_test.go +++ b/conf/parse_posix_test.go @@ -12,15 +12,15 @@ var parsePosixTests = []struct { }{ { "{\ndaemon +sigusr1: c\n}", - &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGUSR1}}}}}, + &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGUSR1, false}}}}}, }, { "{\ndaemon +sigusr2: c\n}", - &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGUSR2}}}}}, + &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGUSR2, false}}}}}, }, { "{\ndaemon +sigwinch: c\n}", - &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGWINCH}}}}}, + &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGWINCH, false}}}}}, }, } diff --git a/conf/parse_test.go b/conf/parse_test.go index 1569df4..f2477c3 100644 --- a/conf/parse_test.go +++ b/conf/parse_test.go @@ -1,6 +1,7 @@ package conf import ( + "runtime" "syscall" "testing" ) @@ -96,7 +97,7 @@ var parseTests = []struct { Blocks: []Block{ { Include: []string{"foo"}, - Daemons: []Daemon{{"command", syscall.SIGHUP}}, + Daemons: []Daemon{{"command", syscall.SIGHUP, windows}}, }, }, }, @@ -105,25 +106,25 @@ var parseTests = []struct { "{\ndaemon +sighup: c\n}", &Config{ Blocks: []Block{ - {Daemons: []Daemon{{"c", syscall.SIGHUP}}}, + {Daemons: []Daemon{{"c", syscall.SIGHUP, windows}}}, }, }, }, { "{\ndaemon +sigterm: c\n}", - &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGTERM}}}}}, + &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGTERM, windows}}}}}, }, { "{\ndaemon +sigint: c\n}", - &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGINT}}}}}, + &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGINT, windows}}}}}, }, { "{\ndaemon +sigkill: c\n}", - &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGKILL}}}}}, + &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGKILL, false}}}}}, }, { "{\ndaemon +sigquit: c\n}", - &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGQUIT}}}}}, + &Config{Blocks: []Block{{Daemons: []Daemon{{"c", syscall.SIGQUIT, windows}}}}}, }, { "foo {\nprep: command\n}", @@ -243,6 +244,8 @@ var parseTests = []struct { }, } +var windows = runtime.GOOS == "windows" + func TestParse(t *testing.T) { for i, tt := range parseTests { ret, err := Parse("test", tt.input) diff --git a/daemon.go b/daemon.go index 855544c..aa00752 100644 --- a/daemon.go +++ b/daemon.go @@ -1,6 +1,8 @@ package modd import ( + "fmt" + "io" "os" "os/exec" "sync" @@ -28,6 +30,7 @@ type daemon struct { log termlog.Stream cmd *exec.Cmd + stdin io.Writer shell string stop bool started bool @@ -51,6 +54,13 @@ func (d *daemon) Run() { return } c.Dir = d.indir + if d.conf.PipeRestartSignal { + d.stdin, err = c.StdinPipe() + if err != nil { + d.log.Shout("%s", err) + continue + } + } stdo, err := c.StdoutPipe() if err != nil { d.log.Shout("%s", err) @@ -97,6 +107,16 @@ func (d *daemon) Run() { } } +// Go's standard library uses a mix of word tenses for the string +// representation of signals. For our 'pipe signals' feature, the strings we +// write on the pipe should be present-tense because they are conceptually +// requests. +var pipeSignalTenseCorrections = map[string]string{ + "aborted": "abort", + "killed": "kill", + "terminated": "terminate", +} + // Restart the daemon, or start it if it's not yet running func (d *daemon) Restart() { d.Lock() @@ -106,8 +126,17 @@ func (d *daemon) Restart() { d.started = true } else { if d.cmd != nil { - d.log.Notice(">> sending signal %s", d.conf.RestartSignal) - d.cmd.Process.Signal(d.conf.RestartSignal) + if d.conf.PipeRestartSignal { + sigStr := d.conf.RestartSignal.String() + if s, ok := pipeSignalTenseCorrections[sigStr]; ok { + sigStr = s + } + d.log.Notice(">> piping signal \"%s\"", sigStr) + fmt.Fprintln(d.stdin, sigStr) + } else { + d.log.Notice(">> sending signal %s", d.conf.RestartSignal) + d.cmd.Process.Signal(d.conf.RestartSignal) + } } } } diff --git a/modd.go b/modd.go index bcdec2a..4180b8d 100644 --- a/modd.go +++ b/modd.go @@ -61,7 +61,7 @@ type ModRunner struct { } // NewModRunner constructs a new ModRunner -func NewModRunner(confPath string, log termlog.TermLog, notifiers []notify.Notifier, confreload bool) (*ModRunner, error) { +func NewModRunner(confPath string, log termlog.TermLog, notifiers []notify.Notifier, confreload, pipesignals bool) (*ModRunner, error) { mr := &ModRunner{ Log: log, ConfPath: confPath, @@ -72,6 +72,9 @@ func NewModRunner(confPath string, log termlog.TermLog, notifiers []notify.Notif if err != nil { return nil, err } + if !pipesignals { + mr.disablePipeSignals() + } return mr, nil } @@ -96,6 +99,20 @@ func (mr *ModRunner) ReadConfig() error { return nil } +// disablePipeSignals disables the PipeRestartSignal flag in all daemon config +// structs. When those structs were constructed, that flag was enabled in cases +// where the combination of the running host OS and the specific signal chosen +// would result in that signal silently failing to do anything. If the entire +// 'pipe signals' feature is to be disabled, then those smart defaults need to +// be cleared by calling this method. +func (mr *ModRunner) disablePipeSignals() { + for i := 0; i < len(mr.Config.Blocks); i++ { + for j := 0; j < len(mr.Config.Blocks[i].Daemons); j++ { + mr.Config.Blocks[i].Daemons[j].PipeRestartSignal = false + } + } +} + // PrepOnly runs all prep functions and exits func (mr *ModRunner) PrepOnly(initial bool) error { for _, b := range mr.Config.Blocks {