Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signal equivalent on Windows #45

Closed
wants to merge 9 commits into from
15 changes: 14 additions & 1 deletion cmd/modd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"runtime"

"github.com/cortesi/modd"
"github.com/cortesi/modd/notify"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -75,7 +88,7 @@ func main() {
notifiers = append(notifiers, &notify.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
Expand Down
9 changes: 7 additions & 2 deletions conf/block_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions conf/parse_posix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}}}}},
},
}

Expand Down
15 changes: 9 additions & 6 deletions conf/parse_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package conf

import (
"runtime"
"syscall"
"testing"
)
Expand Down Expand Up @@ -96,7 +97,7 @@ var parseTests = []struct {
Blocks: []Block{
{
Include: []string{"foo"},
Daemons: []Daemon{{"command", syscall.SIGHUP}},
Daemons: []Daemon{{"command", syscall.SIGHUP, windows}},
},
},
},
Expand All @@ -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}",
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 31 additions & 2 deletions daemon.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package modd

import (
"fmt"
"io"
"os"
"os/exec"
"sync"
Expand Down Expand Up @@ -28,6 +30,7 @@ type daemon struct {

log termlog.Stream
cmd *exec.Cmd
stdin io.Writer
shell string
stop bool
started bool
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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)
}
}
}
}
Expand Down
19 changes: 18 additions & 1 deletion modd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}

Expand All @@ -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 {
Expand Down