2018-06-20 21:04:16 -05:00
|
|
|
// Package gaper implements a supervisor restarts a go project
|
|
|
|
// when it crashes or a watched file changes
|
2018-06-16 19:22:21 -05:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"os"
|
2018-06-18 22:50:05 -05:00
|
|
|
"os/exec"
|
2018-06-16 19:22:21 -05:00
|
|
|
"os/signal"
|
|
|
|
"path/filepath"
|
|
|
|
"syscall"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
shellwords "github.com/mattn/go-shellwords"
|
|
|
|
"github.com/urfave/cli"
|
|
|
|
)
|
|
|
|
|
|
|
|
var logger = NewLogger("gaper")
|
|
|
|
|
2018-06-18 23:19:04 -05:00
|
|
|
// exit statuses
|
|
|
|
var exitStatusSuccess = 0
|
|
|
|
var exitStatusError = 1
|
|
|
|
|
2018-06-20 21:04:16 -05:00
|
|
|
// Config contains all settings supported by gaper
|
2018-06-16 19:22:21 -05:00
|
|
|
type Config struct {
|
|
|
|
BinName string
|
|
|
|
BuildPath string
|
|
|
|
BuildArgs []string
|
|
|
|
BuildArgsMerged string
|
|
|
|
ProgramArgs []string
|
|
|
|
Verbose bool
|
|
|
|
WatchItems []string
|
|
|
|
IgnoreItems []string
|
|
|
|
PollInterval int
|
|
|
|
Extensions []string
|
|
|
|
NoRestartOn string
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
parseArgs := func(c *cli.Context) *Config {
|
|
|
|
return &Config{
|
|
|
|
BinName: c.String("bin-name"),
|
|
|
|
BuildPath: c.String("build-path"),
|
|
|
|
BuildArgsMerged: c.String("build-args"),
|
|
|
|
ProgramArgs: c.Args(),
|
|
|
|
Verbose: c.Bool("verbose"),
|
|
|
|
WatchItems: c.StringSlice("watch"),
|
|
|
|
IgnoreItems: c.StringSlice("ignore"),
|
|
|
|
PollInterval: c.Int("poll-interval"),
|
|
|
|
Extensions: c.StringSlice("extensions"),
|
|
|
|
NoRestartOn: c.String("no-restart-on"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
app := cli.NewApp()
|
|
|
|
app.Name = "gaper"
|
|
|
|
app.Usage = "Used to restart programs when they crash or a watched file changes"
|
|
|
|
|
|
|
|
app.Action = func(c *cli.Context) {
|
|
|
|
args := parseArgs(c)
|
|
|
|
if err := runGaper(args); err != nil {
|
|
|
|
logger.Error(err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// supported arguments
|
|
|
|
app.Flags = []cli.Flag{
|
|
|
|
cli.StringFlag{
|
|
|
|
Name: "bin-name",
|
|
|
|
Usage: "name for the binary built by Gaper for the executed program",
|
|
|
|
},
|
|
|
|
cli.StringFlag{
|
|
|
|
Name: "build-path",
|
|
|
|
Usage: "path to the program source code",
|
|
|
|
},
|
|
|
|
cli.StringFlag{
|
|
|
|
Name: "build-args",
|
|
|
|
Usage: "build arguments passed to the program",
|
|
|
|
},
|
|
|
|
cli.BoolFlag{
|
|
|
|
Name: "verbose",
|
|
|
|
Usage: "turns on the verbose messages from Gaper",
|
|
|
|
},
|
|
|
|
cli.StringSliceFlag{
|
|
|
|
Name: "watch, w",
|
2018-06-18 21:22:18 -05:00
|
|
|
Usage: "list of folders or files to watch for changes",
|
2018-06-16 19:22:21 -05:00
|
|
|
},
|
|
|
|
cli.StringSliceFlag{
|
|
|
|
Name: "ignore, i",
|
2018-06-18 21:22:18 -05:00
|
|
|
Usage: "list of folders or files to ignore for changes",
|
2018-06-16 19:22:21 -05:00
|
|
|
},
|
|
|
|
cli.IntFlag{
|
|
|
|
Name: "poll-interval, p",
|
|
|
|
Usage: "how often in milliseconds to poll watched files for changes",
|
|
|
|
},
|
|
|
|
cli.StringSliceFlag{
|
|
|
|
Name: "extensions, e",
|
|
|
|
Usage: "a comma-delimited list of file extensions to watch for changes",
|
|
|
|
},
|
2018-06-18 23:19:04 -05:00
|
|
|
cli.StringFlag{
|
|
|
|
Name: "no-restart-on, n",
|
|
|
|
Usage: "don't automatically restart the supervised program if it ends:\n" +
|
|
|
|
"\t\tif \"error\", an exit code of 0 will still restart.\n" +
|
|
|
|
"\t\tif \"exit\", no restart regardless of exit code.\n" +
|
|
|
|
"\t\tif \"success\", no restart only if exit code is 0.",
|
2018-06-16 19:22:21 -05:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := app.Run(os.Args); err != nil {
|
|
|
|
logger.Errorf("Error running gaper: %v", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// nolint: gocyclo
|
|
|
|
func runGaper(cfg *Config) error {
|
|
|
|
var err error
|
|
|
|
logger.Verbose(cfg.Verbose)
|
|
|
|
logger.Debug("Starting gaper")
|
|
|
|
|
|
|
|
if len(cfg.BuildArgs) == 0 && len(cfg.BuildArgsMerged) > 0 {
|
|
|
|
cfg.BuildArgs, err = shellwords.Parse(cfg.BuildArgsMerged)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
wd, err := os.Getwd()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(cfg.WatchItems) == 0 {
|
|
|
|
cfg.WatchItems = append(cfg.WatchItems, cfg.BuildPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.Debug("Settings: ")
|
|
|
|
logger.Debug(" | bin: ", cfg.BinName)
|
|
|
|
logger.Debug(" | build path: ", cfg.BuildPath)
|
|
|
|
logger.Debug(" | build args: ", cfg.BuildArgs)
|
|
|
|
logger.Debug(" | verbose: ", cfg.Verbose)
|
|
|
|
logger.Debug(" | watch: ", cfg.WatchItems)
|
|
|
|
logger.Debug(" | ignore: ", cfg.IgnoreItems)
|
2018-06-18 23:19:04 -05:00
|
|
|
logger.Debug(" | poll interval: ", cfg.PollInterval)
|
2018-06-16 19:22:21 -05:00
|
|
|
logger.Debug(" | extensions: ", cfg.Extensions)
|
2018-06-18 23:19:04 -05:00
|
|
|
logger.Debug(" | no restart on: ", cfg.NoRestartOn)
|
2018-06-16 19:22:21 -05:00
|
|
|
logger.Debug(" | working directory: ", wd)
|
|
|
|
|
|
|
|
builder := NewBuilder(cfg.BuildPath, cfg.BinName, wd, cfg.BuildArgs)
|
2018-06-20 09:54:10 -05:00
|
|
|
runner := NewRunner(os.Stdout, os.Stderr, filepath.Join(wd, builder.Binary()), cfg.ProgramArgs)
|
2018-06-16 19:22:21 -05:00
|
|
|
|
|
|
|
if err = builder.Build(); err != nil {
|
|
|
|
return fmt.Errorf("build error: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
shutdown(runner)
|
|
|
|
|
|
|
|
if _, err = runner.Run(); err != nil {
|
|
|
|
return fmt.Errorf("run error: %v", err)
|
|
|
|
}
|
|
|
|
|
2018-06-18 21:47:20 -05:00
|
|
|
watcher, err := NewWatcher(cfg.PollInterval, cfg.WatchItems, cfg.IgnoreItems, cfg.Extensions)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("watcher error: %v", err)
|
|
|
|
}
|
2018-06-16 19:22:21 -05:00
|
|
|
|
2018-06-18 22:50:05 -05:00
|
|
|
var changeRestart bool
|
|
|
|
|
2018-06-16 19:22:21 -05:00
|
|
|
go watcher.Watch()
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case event := <-watcher.Events:
|
|
|
|
logger.Debug("Detected new changed file: ", event)
|
2018-06-18 22:50:05 -05:00
|
|
|
changeRestart = true
|
2018-06-20 20:40:09 -05:00
|
|
|
if err := restart(builder, runner); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-06-16 19:22:21 -05:00
|
|
|
case err := <-watcher.Errors:
|
|
|
|
return fmt.Errorf("error on watching files: %v", err)
|
2018-06-18 22:50:05 -05:00
|
|
|
case err := <-runner.Errors():
|
|
|
|
if changeRestart {
|
|
|
|
changeRestart = false
|
|
|
|
} else {
|
|
|
|
logger.Debug("Detected program exit: ", err)
|
2018-06-18 23:19:04 -05:00
|
|
|
if err = handleProgramExit(builder, runner, err, cfg.NoRestartOn); err != nil {
|
2018-06-18 22:50:05 -05:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2018-06-16 19:22:21 -05:00
|
|
|
default:
|
|
|
|
time.Sleep(time.Duration(cfg.PollInterval) * time.Millisecond)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-18 21:53:49 -05:00
|
|
|
func restart(builder Builder, runner Runner) error {
|
2018-06-18 22:50:05 -05:00
|
|
|
logger.Debug("Restarting program")
|
|
|
|
|
|
|
|
// kill process if it is running
|
|
|
|
if !runner.Exited() {
|
|
|
|
if err := runner.Kill(); err != nil {
|
|
|
|
return fmt.Errorf("kill error: %v", err)
|
|
|
|
}
|
2018-06-18 21:53:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := builder.Build(); err != nil {
|
|
|
|
return fmt.Errorf("build error: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := runner.Run(); err != nil {
|
|
|
|
return fmt.Errorf("run error: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-18 23:19:04 -05:00
|
|
|
func handleProgramExit(builder Builder, runner Runner, err error, noRestartOn string) error {
|
|
|
|
exiterr, ok := err.(*exec.ExitError)
|
2018-06-18 22:50:05 -05:00
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("couldn't handle program crash restart: %v", err)
|
|
|
|
}
|
|
|
|
|
2018-06-18 23:19:04 -05:00
|
|
|
status, oks := exiterr.Sys().(syscall.WaitStatus)
|
|
|
|
if !oks {
|
|
|
|
return fmt.Errorf("couldn't resolve exit status: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
exitStatus := status.ExitStatus()
|
|
|
|
|
|
|
|
// if "error", an exit code of 0 will still restart.
|
|
|
|
if noRestartOn == "error" && exitStatus == exitStatusError {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// if "success", no restart only if exit code is 0.
|
|
|
|
if noRestartOn == "success" && exitStatus == exitStatusSuccess {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// if "exit", no restart regardless of exit code.
|
|
|
|
if noRestartOn == "exit" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-20 20:40:09 -05:00
|
|
|
return restart(builder, runner)
|
2018-06-18 22:50:05 -05:00
|
|
|
}
|
|
|
|
|
2018-06-16 19:22:21 -05:00
|
|
|
func shutdown(runner Runner) {
|
|
|
|
c := make(chan os.Signal, 2)
|
|
|
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
|
|
go func() {
|
|
|
|
s := <-c
|
|
|
|
logger.Debug("Got signal: ", s)
|
|
|
|
err := runner.Kill()
|
|
|
|
if err != nil {
|
|
|
|
logger.Error("Error killing: ", err)
|
|
|
|
}
|
|
|
|
os.Exit(1)
|
|
|
|
}()
|
|
|
|
}
|