From 18ea15ac415bc30b08a87c4ad893af53490848d5 Mon Sep 17 00:00:00 2001 From: Max Claus Nunes Date: Fri, 22 Jun 2018 09:01:36 -0300 Subject: [PATCH] Move cli to a different folder so Godoc works for the rest --- .gitignore | 4 +- .goreleaser.yml | 2 +- .travis.yml | 4 +- Makefile | 4 +- README.md | 39 ++--- builder.go | 2 +- builder_test.go | 2 +- cmd/gaper/main.go | 107 ++++++++++++++ gaper.go | 201 ++++++++++++++++++++++++++ main_test.go => gaper_test.go | 4 +- loggger.go | 2 +- main.go | 264 ---------------------------------- runner.go | 2 +- runner_test.go | 3 +- testdata/print-gaper.bat | 2 +- watcher.go | 10 +- watcher_test.go | 2 +- 17 files changed, 350 insertions(+), 304 deletions(-) create mode 100644 cmd/gaper/main.go create mode 100644 gaper.go rename main_test.go => gaper_test.go (50%) delete mode 100644 main.go diff --git a/.gitignore b/.gitignore index 81ea26a..db83d95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ # binaries *.exe -gaper +/gaper srv vendor coverage.out .DS_Store +testdata/server/server +dist diff --git a/.goreleaser.yml b/.goreleaser.yml index 647bf8c..5ce78e8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,5 +1,5 @@ builds: - - main: main.go + - main: ./cmd/gaper/main.go binary: gaper goos: - windows diff --git a/.travis.yml b/.travis.yml index 4be5593..84f3531 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,12 @@ go: - 1.10.x # - master -script: +before_script: - go version - make setup - make lint + +script: - make test after_success: diff --git a/Makefile b/Makefile index e7bc5e8..a9933e9 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ ifndef LINTER endif build: - @go build . + @go build -o ./gaper cmd/gaper/main.go ## lint: Validate golang code lint: @@ -30,7 +30,7 @@ lint: --vendor ./... test: - @go test -v -coverpkg $(COVER_PACKAGES) \ + @go test -p=1 -coverpkg $(COVER_PACKAGES) \ -covermode=atomic -coverprofile=coverage.out $(TEST_PACKAGES) cover: test diff --git a/README.md b/README.md index 448fbfc..a1b227e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@

gaper

- Restarts programs when they crash or a watched file changes.
+ Used to build and restart a Go project when it crashes or some watched file changes +
Aimed to be used in development only.

@@ -20,7 +21,7 @@ ## Installation ``` -go get -u github.com/maxcnunes/gaper +go get -u github.com/maxcnunes/gaper/cmd/gaper ``` ## Changelog @@ -31,32 +32,34 @@ See [Releases](https://github.com/maxcnunes/gaper/releases) for detailed history ``` NAME: - gaper - Used to restart programs when they crash or a watched file changes + gaper - Used to build and restart a Go project when it crashes or some watched file changes USAGE: gaper [global options] command [command options] [arguments...] VERSION: - 0.0.0 + version COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: - --bin-name value name for the binary built by Gaper for the executed program - --build-path value path to the program source code - --build-args value build arguments passed to the program - --verbose turns on the verbose messages from Gaper - --watch value, -w value list of folders or files to watch for changes - --ignore value, -i value list of folders or files to ignore for changes - --poll-interval value, -p value how often in milliseconds to poll watched files for changes (default: 500) - --extensions value, -e value a comma-delimited list of file extensions to watch for changes (default: "go") - --no-restart-on value, -n value don't automatically restart the executed program if it ends: - if "error", an exit code of 0 will still restart. - if "exit", no restart regardless of exit code. - if "success", no restart only if exit code is 0. - --help, -h show help - --version, -v print the version + --bin-name value name for the binary built by gaper for the executed program (default current directory name) + --build-path value path to the program source code (default: ".") + --build-args value arguments used on building the program + --program-args value arguments used on executing the program + --verbose turns on the verbose messages from gaper + --watch value, -w value list of folders or files to watch for changes + --ignore value, -i value list of folders or files to ignore for changes + (always ignores all hidden files and directories) + --poll-interval value, -p value how often in milliseconds to poll watched files for changes (default: 500) + --extensions value, -e value a comma-delimited list of file extensions to watch for changes (default: "go") + --no-restart-on value, -n value don't automatically restart the supervised program if it ends: + if "error", an exit code of 0 will still restart. + if "exit", no restart regardless of exit code. + if "success", no restart only if exit code is 0. + --help, -h show help + --version, -v print the version ``` ### Examples diff --git a/builder.go b/builder.go index 28e1bbf..d46b27a 100644 --- a/builder.go +++ b/builder.go @@ -1,4 +1,4 @@ -package main +package gaper import ( "fmt" diff --git a/builder_test.go b/builder_test.go index a51e202..c7e6c5a 100644 --- a/builder_test.go +++ b/builder_test.go @@ -1,4 +1,4 @@ -package main +package gaper import ( "os" diff --git a/cmd/gaper/main.go b/cmd/gaper/main.go new file mode 100644 index 0000000..141f26e --- /dev/null +++ b/cmd/gaper/main.go @@ -0,0 +1,107 @@ +package main + +import ( + "os" + + "github.com/maxcnunes/gaper" + "github.com/urfave/cli" +) + +// build info +var ( + version = "dev" +) + +var logger = gaper.NewLogger("gaper") + +func main() { + parseArgs := func(c *cli.Context) *gaper.Config { + return &gaper.Config{ + BinName: c.String("bin-name"), + BuildPath: c.String("build-path"), + BuildArgsMerged: c.String("build-args"), + ProgramArgsMerged: c.String("program-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"), + ExitOnSIGINT: true, + } + } + + app := cli.NewApp() + app.Name = "gaper" + app.Usage = "Used to build and restart a Go project when it crashes or some watched file changes" + app.Version = version + + app.Action = func(c *cli.Context) { + args := parseArgs(c) + if err := gaper.Run(args); err != nil { + logger.Error(err) + os.Exit(1) + } + } + + exts := make(cli.StringSlice, len(gaper.DefaultExtensions)) + for i := range gaper.DefaultExtensions { + exts[i] = gaper.DefaultExtensions[i] + } + + // supported arguments + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "bin-name", + Usage: "name for the binary built by gaper for the executed program (default current directory name)", + }, + cli.StringFlag{ + Name: "build-path", + Value: gaper.DefaultBuildPath, + Usage: "path to the program source code", + }, + cli.StringFlag{ + Name: "build-args", + Usage: "arguments used on building the program", + }, + cli.StringFlag{ + Name: "program-args", + Usage: "arguments used on executing the program", + }, + cli.BoolFlag{ + Name: "verbose", + Usage: "turns on the verbose messages from gaper", + }, + cli.StringSliceFlag{ + Name: "watch, w", + Usage: "list of folders or files to watch for changes", + }, + cli.StringSliceFlag{ + Name: "ignore, i", + Usage: "list of folders or files to ignore for changes\n" + + "\t\t(always ignores all hidden files and directories)", + }, + cli.IntFlag{ + Name: "poll-interval, p", + Value: gaper.DefaultPoolInterval, + Usage: "how often in milliseconds to poll watched files for changes", + }, + cli.StringSliceFlag{ + Name: "extensions, e", + Value: &exts, + Usage: "a comma-delimited list of file extensions to watch for changes", + }, + 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.", + }, + } + + if err := app.Run(os.Args); err != nil { + logger.Errorf("Error running gaper: %v", err) + os.Exit(1) + } +} diff --git a/gaper.go b/gaper.go new file mode 100644 index 0000000..e3b0a79 --- /dev/null +++ b/gaper.go @@ -0,0 +1,201 @@ +// Package gaper implements a supervisor restarts a go project +// when it crashes or a watched file changes +package gaper + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "path/filepath" + "syscall" + "time" + + shellwords "github.com/mattn/go-shellwords" +) + +// DefaultBuildPath is the default build and watched path +var DefaultBuildPath = "." + +// DefaultExtensions is the default watched extension +var DefaultExtensions = []string{"go"} + +// DefaultPoolInterval is the time in ms used by the watcher to wait between scans +var DefaultPoolInterval = 500 + +var logger = NewLogger("gaper") + +// exit statuses +var exitStatusSuccess = 0 +var exitStatusError = 1 + +// Config contains all settings supported by gaper +type Config struct { + BinName string + BuildPath string + BuildArgs []string + BuildArgsMerged string + ProgramArgs []string + ProgramArgsMerged string + WatchItems []string + IgnoreItems []string + PollInterval int + Extensions []string + NoRestartOn string + Verbose bool + ExitOnSIGINT bool +} + +// Run in the gaper high level API +// It starts the whole gaper process watching for file changes or exit codes +// and restarting the program +func Run(cfg *Config) error { // nolint: gocyclo + var err error + logger.Verbose(cfg.Verbose) + logger.Debug("Starting gaper") + + if len(cfg.BuildPath) == 0 { + cfg.BuildPath = DefaultBuildPath + } + + cfg.BuildArgs, err = parseInnerArgs(cfg.BuildArgs, cfg.BuildArgsMerged) + if err != nil { + return err + } + + cfg.ProgramArgs, err = parseInnerArgs(cfg.ProgramArgs, cfg.ProgramArgsMerged) + 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) + } + + builder := NewBuilder(cfg.BuildPath, cfg.BinName, wd, cfg.BuildArgs) + runner := NewRunner(os.Stdout, os.Stderr, filepath.Join(wd, builder.Binary()), cfg.ProgramArgs) + + if err = builder.Build(); err != nil { + return fmt.Errorf("build error: %v", err) + } + + shutdown(runner, cfg.ExitOnSIGINT) + + if _, err = runner.Run(); err != nil { + return fmt.Errorf("run error: %v", err) + } + + watcher, err := NewWatcher(cfg.PollInterval, cfg.WatchItems, cfg.IgnoreItems, cfg.Extensions) + if err != nil { + return fmt.Errorf("watcher error: %v", err) + } + + var changeRestart bool + + go watcher.Watch() + for { + select { + case event := <-watcher.Events: + logger.Debug("Detected new changed file: ", event) + changeRestart = true + if err := restart(builder, runner); err != nil { + return err + } + case err := <-watcher.Errors: + return fmt.Errorf("error on watching files: %v", err) + case err := <-runner.Errors(): + if changeRestart { + changeRestart = false + } else { + logger.Debug("Detected program exit: ", err) + if err = handleProgramExit(builder, runner, err, cfg.NoRestartOn); err != nil { + return err + } + } + default: + time.Sleep(time.Duration(cfg.PollInterval) * time.Millisecond) + } + } +} + +func restart(builder Builder, runner Runner) error { + 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) + } + } + + 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 +} + +func handleProgramExit(builder Builder, runner Runner, err error, noRestartOn string) error { + exiterr, ok := err.(*exec.ExitError) + if !ok { + return fmt.Errorf("couldn't handle program crash restart: %v", err) + } + + 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 + } + + return restart(builder, runner) +} + +func shutdown(runner Runner, exitOnSIGINT bool) { + c := make(chan os.Signal, 2) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + s := <-c + logger.Debug("Got signal: ", s) + + if err := runner.Kill(); err != nil { + logger.Error("Error killing: ", err) + } + + if exitOnSIGINT { + os.Exit(0) + } + }() +} + +func parseInnerArgs(args []string, argsm string) ([]string, error) { + if len(args) > 0 || len(argsm) == 0 { + return args, nil + } + + return shellwords.Parse(argsm) +} diff --git a/main_test.go b/gaper_test.go similarity index 50% rename from main_test.go rename to gaper_test.go index f0c3559..d30ae48 100644 --- a/main_test.go +++ b/gaper_test.go @@ -1,9 +1,9 @@ -package main +package gaper import ( "testing" ) func TestGaper(t *testing.T) { - // TODO: add test to main file + // TODO: add test to gaper high level API } diff --git a/loggger.go b/loggger.go index 7029a88..4a8385a 100644 --- a/loggger.go +++ b/loggger.go @@ -1,4 +1,4 @@ -package main +package gaper import ( "log" diff --git a/main.go b/main.go deleted file mode 100644 index c21e8ec..0000000 --- a/main.go +++ /dev/null @@ -1,264 +0,0 @@ -// Package gaper implements a supervisor restarts a go project -// when it crashes or a watched file changes -package main - -import ( - "fmt" - "os" - "os/exec" - "os/signal" - "path/filepath" - "syscall" - "time" - - shellwords "github.com/mattn/go-shellwords" - "github.com/urfave/cli" -) - -// build info -var version = "dev" - -var logger = NewLogger("gaper") - -// exit statuses -var exitStatusSuccess = 0 -var exitStatusError = 1 - -// Config contains all settings supported by gaper -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.Version = version - - 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", - Usage: "list of folders or files to watch for changes", - }, - cli.StringSliceFlag{ - Name: "ignore, i", - Usage: "list of folders or files to ignore for changes", - }, - 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", - }, - 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.", - }, - } - - 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) - logger.Debug(" | poll interval: ", cfg.PollInterval) - logger.Debug(" | extensions: ", cfg.Extensions) - logger.Debug(" | no restart on: ", cfg.NoRestartOn) - logger.Debug(" | working directory: ", wd) - - builder := NewBuilder(cfg.BuildPath, cfg.BinName, wd, cfg.BuildArgs) - runner := NewRunner(os.Stdout, os.Stderr, filepath.Join(wd, builder.Binary()), cfg.ProgramArgs) - - 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) - } - - watcher, err := NewWatcher(cfg.PollInterval, cfg.WatchItems, cfg.IgnoreItems, cfg.Extensions) - if err != nil { - return fmt.Errorf("watcher error: %v", err) - } - - var changeRestart bool - - go watcher.Watch() - for { - select { - case event := <-watcher.Events: - logger.Debug("Detected new changed file: ", event) - changeRestart = true - if err := restart(builder, runner); err != nil { - return err - } - case err := <-watcher.Errors: - return fmt.Errorf("error on watching files: %v", err) - case err := <-runner.Errors(): - if changeRestart { - changeRestart = false - } else { - logger.Debug("Detected program exit: ", err) - if err = handleProgramExit(builder, runner, err, cfg.NoRestartOn); err != nil { - return err - } - } - default: - time.Sleep(time.Duration(cfg.PollInterval) * time.Millisecond) - } - } -} - -func restart(builder Builder, runner Runner) error { - 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) - } - } - - 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 -} - -func handleProgramExit(builder Builder, runner Runner, err error, noRestartOn string) error { - exiterr, ok := err.(*exec.ExitError) - if !ok { - return fmt.Errorf("couldn't handle program crash restart: %v", err) - } - - 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 - } - - return restart(builder, runner) -} - -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) - }() -} diff --git a/runner.go b/runner.go index fe2f3c7..e5fc1e3 100644 --- a/runner.go +++ b/runner.go @@ -1,4 +1,4 @@ -package main +package gaper import ( "errors" diff --git a/runner_test.go b/runner_test.go index 2bca40c..05814d8 100644 --- a/runner_test.go +++ b/runner_test.go @@ -1,4 +1,4 @@ -package main +package gaper import ( "bytes" @@ -28,6 +28,7 @@ func TestRunnerSuccessRun(t *testing.T) { errCmd := <-runner.Errors() assert.Nil(t, errCmd, "async error running binary") assert.Contains(t, stdout.String(), "Gaper Test Message") + assert.Equal(t, stderr.String(), "") } func TestRunnerSuccessKill(t *testing.T) { diff --git a/testdata/print-gaper.bat b/testdata/print-gaper.bat index b067a80..b38561e 100644 --- a/testdata/print-gaper.bat +++ b/testdata/print-gaper.bat @@ -1,2 +1,2 @@ -timeout 2 > nul +timeout 2 2>NUL @echo Gaper Test Message diff --git a/watcher.go b/watcher.go index d125250..d3bde0e 100644 --- a/watcher.go +++ b/watcher.go @@ -1,4 +1,4 @@ -package main +package gaper import ( "errors" @@ -10,12 +10,6 @@ import ( zglob "github.com/mattn/go-zglob" ) -// DefaultExtensions used by the watcher -var DefaultExtensions = []string{"go"} - -// DefaultPoolInterval used by the watcher -var DefaultPoolInterval = 500 - // Watcher is a interface for the watch process type Watcher struct { PollInterval int @@ -123,7 +117,7 @@ func resolveGlobMatches(paths []string) ([]string, error) { for _, path := range paths { matches, err := zglob.Glob(path) if err != nil { - return nil, fmt.Errorf("couldn't resolve glob path %s: %v", path, err) + return nil, fmt.Errorf("couldn't resolve glob path \"%s\": %v", path, err) } logger.Debugf("Resolved glob path %s: %v", path, matches) diff --git a/watcher_test.go b/watcher_test.go index b0201bd..90c176a 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -1,4 +1,4 @@ -package main +package gaper import ( "os"