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,32 +32,34 @@ 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
--watch value, -w value list of folders or files to watch for changes --verbose turns on the verbose messages from gaper
--ignore value, -i value list of folders or files to ignore for changes --watch value, -w value list of folders or files to watch for changes
--poll-interval value, -p value how often in milliseconds to poll watched files for changes (default: 500) --ignore value, -i value list of folders or files to ignore for changes
--extensions value, -e value a comma-delimited list of file extensions to watch for changes (default: "go") (always ignores all hidden files and directories)
--no-restart-on value, -n value don't automatically restart the executed program if it ends: --poll-interval value, -p value how often in milliseconds to poll watched files for changes (default: 500)
if "error", an exit code of 0 will still restart. --extensions value, -e value a comma-delimited list of file extensions to watch for changes (default: "go")
if "exit", no restart regardless of exit code. --no-restart-on value, -n value don't automatically restart the supervised program if it ends:
if "success", no restart only if exit code is 0. if "error", an exit code of 0 will still restart.
--help, -h show help if "exit", no restart regardless of exit code.
--version, -v print the version if "success", no restart only if exit code is 0.
--help, -h show help
--version, -v print the version
``` ```
### Examples ### Examples

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"