Move cli to a different folder so Godoc works for the rest

This commit is contained in:
Max Claus Nunes 2018-06-22 09:01:36 -03:00
parent a2fa6af32b
commit 18ea15ac41
17 changed files with 350 additions and 304 deletions

4
.gitignore vendored
View File

@ -1,7 +1,9 @@
# binaries # binaries
*.exe *.exe
gaper /gaper
srv srv
vendor vendor
coverage.out coverage.out
.DS_Store .DS_Store
testdata/server/server
dist

View File

@ -1,5 +1,5 @@
builds: builds:
- main: main.go - main: ./cmd/gaper/main.go
binary: gaper binary: gaper
goos: goos:
- windows - windows

View File

@ -7,10 +7,12 @@ go:
- 1.10.x - 1.10.x
# - master # - master
script: before_script:
- go version - go version
- make setup - make setup
- make lint - make lint
script:
- make test - make test
after_success: after_success:

View File

@ -19,7 +19,7 @@ ifndef LINTER
endif endif
build: build:
@go build . @go build -o ./gaper cmd/gaper/main.go
## lint: Validate golang code ## lint: Validate golang code
lint: lint:
@ -30,7 +30,7 @@ lint:
--vendor ./... --vendor ./...
test: test:
@go test -v -coverpkg $(COVER_PACKAGES) \ @go test -p=1 -coverpkg $(COVER_PACKAGES) \
-covermode=atomic -coverprofile=coverage.out $(TEST_PACKAGES) -covermode=atomic -coverprofile=coverage.out $(TEST_PACKAGES)
cover: test cover: test

View File

@ -2,7 +2,8 @@
<img width="200px" src="https://raw.githubusercontent.com/maxcnunes/gaper/master/gopher-gaper.png"> <img width="200px" src="https://raw.githubusercontent.com/maxcnunes/gaper/master/gopher-gaper.png">
<h3 align="center">gaper</h3> <h3 align="center">gaper</h3>
<p align="center"> <p align="center">
Restarts programs when they crash or a watched file changes.<br /> Used to build and restart a Go project when it crashes or some watched file changes
<br />
<b>Aimed to be used in development only.</b> <b>Aimed to be used in development only.</b>
</p> </p>
</p> </p>
@ -20,7 +21,7 @@
## Installation ## Installation
``` ```
go get -u github.com/maxcnunes/gaper go get -u github.com/maxcnunes/gaper/cmd/gaper
``` ```
## Changelog ## Changelog
@ -31,27 +32,29 @@ See [Releases](https://github.com/maxcnunes/gaper/releases) for detailed history
``` ```
NAME: 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: USAGE:
gaper [global options] command [command options] [arguments...] gaper [global options] command [command options] [arguments...]
VERSION: VERSION:
0.0.0 version
COMMANDS: COMMANDS:
help, h Shows a list of commands or help for one command help, h Shows a list of commands or help for one command
GLOBAL OPTIONS: GLOBAL OPTIONS:
--bin-name value name for the binary built by Gaper for the executed program --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 --build-path value path to the program source code (default: ".")
--build-args value build arguments passed to the program --build-args value arguments used on building the program
--verbose turns on the verbose messages from Gaper --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 --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 --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) --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") --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: --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 "error", an exit code of 0 will still restart.
if "exit", no restart regardless of exit code. if "exit", no restart regardless of exit code.
if "success", no restart only if exit code is 0. if "success", no restart only if exit code is 0.

View File

@ -1,4 +1,4 @@
package main package gaper
import ( import (
"fmt" "fmt"

View File

@ -1,4 +1,4 @@
package main package gaper
import ( import (
"os" "os"

107
cmd/gaper/main.go Normal file
View File

@ -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)
}
}

201
gaper.go Normal file
View File

@ -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)
}

View File

@ -1,9 +1,9 @@
package main package gaper
import ( import (
"testing" "testing"
) )
func TestGaper(t *testing.T) { func TestGaper(t *testing.T) {
// TODO: add test to main file // TODO: add test to gaper high level API
} }

View File

@ -1,4 +1,4 @@
package main package gaper
import ( import (
"log" "log"

264
main.go
View File

@ -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)
}()
}

View File

@ -1,4 +1,4 @@
package main package gaper
import ( import (
"errors" "errors"

View File

@ -1,4 +1,4 @@
package main package gaper
import ( import (
"bytes" "bytes"
@ -28,6 +28,7 @@ func TestRunnerSuccessRun(t *testing.T) {
errCmd := <-runner.Errors() errCmd := <-runner.Errors()
assert.Nil(t, errCmd, "async error running binary") assert.Nil(t, errCmd, "async error running binary")
assert.Contains(t, stdout.String(), "Gaper Test Message") assert.Contains(t, stdout.String(), "Gaper Test Message")
assert.Equal(t, stderr.String(), "")
} }
func TestRunnerSuccessKill(t *testing.T) { func TestRunnerSuccessKill(t *testing.T) {

View File

@ -1,2 +1,2 @@
timeout 2 > nul timeout 2 2>NUL
@echo Gaper Test Message @echo Gaper Test Message

View File

@ -1,4 +1,4 @@
package main package gaper
import ( import (
"errors" "errors"
@ -10,12 +10,6 @@ import (
zglob "github.com/mattn/go-zglob" 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 // Watcher is a interface for the watch process
type Watcher struct { type Watcher struct {
PollInterval int PollInterval int
@ -123,7 +117,7 @@ func resolveGlobMatches(paths []string) ([]string, error) {
for _, path := range paths { for _, path := range paths {
matches, err := zglob.Glob(path) matches, err := zglob.Glob(path)
if err != nil { 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) logger.Debugf("Resolved glob path %s: %v", path, matches)

View File

@ -1,4 +1,4 @@
package main package gaper
import ( import (
"os" "os"