mirror of https://github.com/maxcnunes/gaper.git
Move cli to a different folder so Godoc works for the rest
This commit is contained in:
parent
a2fa6af32b
commit
18ea15ac41
|
@ -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
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
builds:
|
builds:
|
||||||
- main: main.go
|
- main: ./cmd/gaper/main.go
|
||||||
binary: gaper
|
binary: gaper
|
||||||
goos:
|
goos:
|
||||||
- windows
|
- windows
|
||||||
|
|
|
@ -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:
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -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
|
||||||
|
|
39
README.md
39
README.md
|
@ -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
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package gaper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package gaper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package gaper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
264
main.go
264
main.go
|
@ -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)
|
|
||||||
}()
|
|
||||||
}
|
|
|
@ -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) {
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
timeout 2 > nul
|
timeout 2 2>NUL
|
||||||
@echo Gaper Test Message
|
@echo Gaper Test Message
|
||||||
|
|
10
watcher.go
10
watcher.go
|
@ -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)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package gaper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
Loading…
Reference in New Issue