mirror of https://github.com/maxcnunes/gaper.git
Initial commit
This commit is contained in:
commit
8e02bc3348
|
@ -0,0 +1,20 @@
|
||||||
|
; top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
|
@ -0,0 +1,6 @@
|
||||||
|
# binaries
|
||||||
|
*.exe
|
||||||
|
gaper
|
||||||
|
srv
|
||||||
|
vendor
|
||||||
|
coverage.out
|
|
@ -0,0 +1,20 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
# - 1.7.x
|
||||||
|
# - 1.8.x
|
||||||
|
# - 1.9.x
|
||||||
|
- 1.10.x
|
||||||
|
# - master
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go version
|
||||||
|
- make setup
|
||||||
|
- make lint
|
||||||
|
- make test
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
email: false
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Contributing to httpfake
|
||||||
|
|
||||||
|
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
|
||||||
|
|
||||||
|
There are few ways of contributing to gaper
|
||||||
|
|
||||||
|
* Report an issue.
|
||||||
|
* Contribute to the code base.
|
||||||
|
|
||||||
|
## Report an issue
|
||||||
|
|
||||||
|
* Before opening the issue make sure there isn't an issue opened for the same problem
|
||||||
|
* Include the Go and Gaper version you are using
|
||||||
|
* If it is a bug, please include all info to reproduce the problem
|
||||||
|
|
||||||
|
## Contribute to the code base
|
||||||
|
|
||||||
|
### Pull Request
|
||||||
|
|
||||||
|
* Please discuss the suggested changes on a issue before working on it. Just to make sure the change makes sense before you spending any time on it.
|
||||||
|
|
||||||
|
### Setupping development
|
||||||
|
|
||||||
|
```
|
||||||
|
make setup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running gaper in development
|
||||||
|
|
||||||
|
```
|
||||||
|
make build && ./gaper --verbose --bin-name srv --build-path ./testdata/server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running lint
|
||||||
|
|
||||||
|
```
|
||||||
|
make lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running tests
|
||||||
|
|
||||||
|
All tests:
|
||||||
|
```
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
A single test:
|
||||||
|
```
|
||||||
|
go test -run TestSimplePost ./...
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/fatih/color"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
|
||||||
|
version = "v1.7.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-colorable"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||||
|
version = "v0.0.9"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-isatty"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||||
|
version = "v0.0.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-shellwords"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "02e3cf038dcea8290e44424da473dd12be796a8a"
|
||||||
|
version = "v1.0.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/urfave/cli"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
|
||||||
|
version = "v1.20.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "golang.org/x/sys"
|
||||||
|
packages = ["unix"]
|
||||||
|
revision = "6c888cc515d3ed83fc103cf1d84468aad274b0a7"
|
||||||
|
|
||||||
|
[solve-meta]
|
||||||
|
analyzer-name = "dep"
|
||||||
|
analyzer-version = 1
|
||||||
|
inputs-digest = "bd09f9274a4112aeb869a026bcf3ab61443289a245d3d353da6623f3327e7b4e"
|
||||||
|
solver-name = "gps-cdcl"
|
||||||
|
solver-version = 1
|
|
@ -0,0 +1,15 @@
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/fatih/color"
|
||||||
|
version = "1.7.0"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/mattn/go-shellwords"
|
||||||
|
version = "1.0.3"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/urfave/cli"
|
||||||
|
version = "1.20.0"
|
||||||
|
|
||||||
|
[prune]
|
||||||
|
go-tests = true
|
||||||
|
unused-packages = true
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Max Claus Nunes
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,41 @@
|
||||||
|
OS := $(shell uname -s)
|
||||||
|
TEST_PACKAGES := $(shell go list ./...)
|
||||||
|
COVER_PACKAGES := $(shell go list ./... | paste -sd "," -)
|
||||||
|
LINTER := $(shell command -v gometalinter 2> /dev/null)
|
||||||
|
|
||||||
|
.PHONY: setup
|
||||||
|
|
||||||
|
setup:
|
||||||
|
ifeq ($(OS), Darwin)
|
||||||
|
brew install dep
|
||||||
|
else
|
||||||
|
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||||
|
endif
|
||||||
|
dep ensure -vendor-only
|
||||||
|
ifndef LINTER
|
||||||
|
@echo "Installing linter"
|
||||||
|
@go get -u github.com/alecthomas/gometalinter
|
||||||
|
@gometalinter --install
|
||||||
|
endif
|
||||||
|
|
||||||
|
build:
|
||||||
|
@go build .
|
||||||
|
|
||||||
|
## lint: Validate golang code
|
||||||
|
lint:
|
||||||
|
@gometalinter \
|
||||||
|
--deadline=120s \
|
||||||
|
--line-length=120 \
|
||||||
|
--enable-all \
|
||||||
|
--vendor ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
@go test -v -coverpkg $(COVER_PACKAGES) \
|
||||||
|
-covermode=atomic -coverprofile=coverage.out $(TEST_PACKAGES)
|
||||||
|
|
||||||
|
cover: test
|
||||||
|
@go tool cover -html=coverage.out
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
@find . -name '*.go' -not -wholename './vendor/*' | \
|
||||||
|
while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done
|
|
@ -0,0 +1,72 @@
|
||||||
|
gaper
|
||||||
|
=====
|
||||||
|
|
||||||
|
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md)
|
||||||
|
[![Build Status](https://travis-ci.org/maxcnunes/gaper.svg?branch=master)](https://travis-ci.org/maxcnunes/gaper)
|
||||||
|
[![Coverage Status](https://codecov.io/gh/maxcnunes/gaper/branch/master/graph/badge.svg)](https://codecov.io/gh/maxcnunes/gaper)
|
||||||
|
[![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/maxcnunes/gaper)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/maxcnunes/gaper)](https://goreportcard.com/report/github.com/maxcnunes/gaper)
|
||||||
|
|
||||||
|
Restarts programs when they crash or a watched file changes.
|
||||||
|
|
||||||
|
**NOT STABLE YET, STILL IN DEVELOPMENT**: Please, check out [this ticket](https://github.com/maxcnunes/gaper/issues/1) to follow its progress.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
go get -u github.com/maxcnunes/gaper
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See [Releases](https://github.com/maxcnunes/gaper/releases) for detailed history changes.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
NAME:
|
||||||
|
gaper - Used to restart programs when they crash or a watched file changes
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
gaper [global options] command [command options] [arguments...]
|
||||||
|
|
||||||
|
VERSION:
|
||||||
|
0.0.0
|
||||||
|
|
||||||
|
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 a comma-delimited list of folders or files to watch for changes
|
||||||
|
--ignore value, -i value a comma-delimited 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See the [Contributing guide](/CONTRIBUTING.md) for steps on how to contribute to this project.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
This package was heavily inspired by [gin](https://github.com/codegangsta/gin) and [node-supervisor](https://github.com/petruisfan/node-supervisor).
|
||||||
|
|
||||||
|
Basically, Gaper is a mixing of those projects above. It started from **gin** code base and I rewrote it aiming to get
|
||||||
|
something similar to **node-supervisor** (but simpler). A big thanks for those projects and for the people behind it!
|
||||||
|
:clap::clap:
|
||||||
|
|
||||||
|
### How is Gaper different of Gin
|
||||||
|
|
||||||
|
The main difference is that Gaper removes a layer of complexity from Gin which has a proxy running on top of
|
||||||
|
the executed server. It allows to postpone a build and reload the server when the first call hits it. With Gaper
|
||||||
|
we don't care about that feature, it just restarts your server whenever a change is made.
|
|
@ -0,0 +1,78 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Builder ...
|
||||||
|
type Builder interface {
|
||||||
|
Build() error
|
||||||
|
Binary() string
|
||||||
|
Errors() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type builder struct {
|
||||||
|
dir string
|
||||||
|
binary string
|
||||||
|
errors string
|
||||||
|
wd string
|
||||||
|
buildArgs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuilder ...
|
||||||
|
func NewBuilder(dir string, bin string, wd string, buildArgs []string) Builder {
|
||||||
|
if len(bin) == 0 {
|
||||||
|
bin = "bin"
|
||||||
|
}
|
||||||
|
|
||||||
|
// does not work on Windows without the ".exe" extension
|
||||||
|
if runtime.GOOS == OSWindows {
|
||||||
|
// check if it already has the .exe extension
|
||||||
|
if !strings.HasSuffix(bin, ".exe") {
|
||||||
|
bin += ".exe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &builder{dir: dir, binary: bin, wd: wd, buildArgs: buildArgs}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary ...
|
||||||
|
func (b *builder) Binary() string {
|
||||||
|
return b.binary
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors ...
|
||||||
|
func (b *builder) Errors() string {
|
||||||
|
return b.errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build ...
|
||||||
|
func (b *builder) Build() error {
|
||||||
|
logger.Info("Building program")
|
||||||
|
args := append([]string{"go", "build", "-o", filepath.Join(b.wd, b.binary)}, b.buildArgs...)
|
||||||
|
logger.Debug("Build command", args)
|
||||||
|
|
||||||
|
command := exec.Command(args[0], args[1:]...) // nolint gas
|
||||||
|
command.Dir = b.dir
|
||||||
|
|
||||||
|
output, err := command.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if command.ProcessState.Success() {
|
||||||
|
b.errors = ""
|
||||||
|
} else {
|
||||||
|
b.errors = string(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.errors) > 0 {
|
||||||
|
return fmt.Errorf(b.errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger ..
|
||||||
|
type Logger struct {
|
||||||
|
verbose bool
|
||||||
|
logDebug *log.Logger
|
||||||
|
logWarn *log.Logger
|
||||||
|
logInfo *log.Logger
|
||||||
|
logError *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogger ...
|
||||||
|
func NewLogger(prefix string) *Logger {
|
||||||
|
prefix = "[" + prefix + "] "
|
||||||
|
return &Logger{
|
||||||
|
verbose: false,
|
||||||
|
logDebug: log.New(os.Stdout, prefix, 0),
|
||||||
|
logWarn: log.New(os.Stdout, color.YellowString(prefix), 0),
|
||||||
|
logInfo: log.New(os.Stdout, color.CyanString(prefix), 0),
|
||||||
|
logError: log.New(os.Stdout, color.RedString(prefix), 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verbose ...
|
||||||
|
func (l *Logger) Verbose(verbose bool) {
|
||||||
|
l.verbose = verbose
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug ...
|
||||||
|
func (l *Logger) Debug(v ...interface{}) {
|
||||||
|
if l.verbose {
|
||||||
|
l.logDebug.Println(v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn ...
|
||||||
|
func (l *Logger) Warn(v ...interface{}) {
|
||||||
|
l.logWarn.Println(v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info ...
|
||||||
|
func (l *Logger) Info(v ...interface{}) {
|
||||||
|
l.logInfo.Println(v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error ...
|
||||||
|
func (l *Logger) Error(v ...interface{}) {
|
||||||
|
l.logError.Println(v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf ...
|
||||||
|
func (l *Logger) Errorf(format string, v ...interface{}) {
|
||||||
|
l.logError.Printf(format, v...)
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
shellwords "github.com/mattn/go-shellwords"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = NewLogger("gaper")
|
||||||
|
|
||||||
|
var defaultExtensions = cli.StringSlice{"go"}
|
||||||
|
var defaultPoolInterval = 500
|
||||||
|
|
||||||
|
// Config ...
|
||||||
|
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",
|
||||||
|
Usage: "a comma-delimited list of folders or files to watch for changes",
|
||||||
|
},
|
||||||
|
cli.StringSliceFlag{
|
||||||
|
Name: "ignore, i",
|
||||||
|
Usage: "a comma-delimited list of folders or files to ignore for changes",
|
||||||
|
},
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "poll-interval, p",
|
||||||
|
Value: defaultPoolInterval,
|
||||||
|
Usage: "how often in milliseconds to poll watched files for changes",
|
||||||
|
},
|
||||||
|
cli.StringSliceFlag{
|
||||||
|
Name: "extensions, e",
|
||||||
|
Value: &defaultExtensions,
|
||||||
|
Usage: "a comma-delimited list of file extensions to watch for changes",
|
||||||
|
},
|
||||||
|
cli.StringSliceFlag{
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve bin name by current folder name
|
||||||
|
if cfg.BinName == "" {
|
||||||
|
cfg.BinName = filepath.Base(wd)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(" | working directory: ", wd)
|
||||||
|
|
||||||
|
builder := NewBuilder(cfg.BuildPath, cfg.BinName, wd, cfg.BuildArgs)
|
||||||
|
runner := NewRunner(os.Stdout, 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 := NewWatcher(cfg.WatchItems, cfg.IgnoreItems, cfg.Extensions)
|
||||||
|
|
||||||
|
go watcher.Watch()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-watcher.Events:
|
||||||
|
logger.Debug("Detected new changed file: ", event)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
case err := <-watcher.Errors:
|
||||||
|
return fmt.Errorf("error on watching files: %v", err)
|
||||||
|
default:
|
||||||
|
time.Sleep(time.Duration(cfg.PollInterval) * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}()
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGaper(t *testing.T) {
|
||||||
|
fmt.Println("Sample test")
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OSWindows ...
|
||||||
|
const OSWindows = "windows"
|
||||||
|
|
||||||
|
// Runner ...
|
||||||
|
type Runner interface {
|
||||||
|
Run() (*exec.Cmd, error)
|
||||||
|
Kill() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type runner struct {
|
||||||
|
bin string
|
||||||
|
args []string
|
||||||
|
writer io.Writer
|
||||||
|
command *exec.Cmd
|
||||||
|
starttime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRunner ...
|
||||||
|
func NewRunner(writer io.Writer, bin string, args []string) Runner {
|
||||||
|
return &runner{
|
||||||
|
bin: bin,
|
||||||
|
args: args,
|
||||||
|
writer: writer,
|
||||||
|
starttime: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run ...
|
||||||
|
func (r *runner) Run() (*exec.Cmd, error) {
|
||||||
|
logger.Info("Starting program")
|
||||||
|
|
||||||
|
if r.command == nil || r.Exited() {
|
||||||
|
if err := r.runBin(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error running: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
return r.command, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.command, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill ...
|
||||||
|
func (r *runner) Kill() error {
|
||||||
|
if r.command == nil || r.command.Process == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan error)
|
||||||
|
go func() {
|
||||||
|
r.command.Wait() // nolint errcheck
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Trying a "soft" kill first
|
||||||
|
if runtime.GOOS == OSWindows {
|
||||||
|
if err := r.command.Process.Kill(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err := r.command.Process.Signal(os.Interrupt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for our process to die before we return or hard kill after 3 sec
|
||||||
|
select {
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
if err := r.command.Process.Kill(); err != nil {
|
||||||
|
return fmt.Errorf("failed to kill: %v", err)
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
|
||||||
|
r.command = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exited ...
|
||||||
|
func (r *runner) Exited() bool {
|
||||||
|
return r.command != nil && r.command.ProcessState != nil && r.command.ProcessState.Exited()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *runner) runBin() error {
|
||||||
|
r.command = exec.Command(r.bin, r.args...) // nolint gas
|
||||||
|
stdout, err := r.command.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr, err := r.command.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.command.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.starttime = time.Now()
|
||||||
|
|
||||||
|
// TODO: handle or log errors
|
||||||
|
go io.Copy(r.writer, stdout) // nolint errcheck
|
||||||
|
go io.Copy(r.writer, stderr) // nolint errcheck
|
||||||
|
go r.command.Wait() // nolint errcheck
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) // nolint gas
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Println("Starting server")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watcher ...
|
||||||
|
type Watcher struct {
|
||||||
|
WatchItems []string
|
||||||
|
IgnoreItems []string
|
||||||
|
AllowedExtensions map[string]bool
|
||||||
|
Events chan string
|
||||||
|
Errors chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWatcher ...
|
||||||
|
func NewWatcher(watchItems []string, ignoreItems []string, extensions []string) *Watcher {
|
||||||
|
allowedExts := make(map[string]bool)
|
||||||
|
for _, ext := range extensions {
|
||||||
|
allowedExts["."+ext] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Watcher{
|
||||||
|
Events: make(chan string),
|
||||||
|
Errors: make(chan error),
|
||||||
|
WatchItems: watchItems,
|
||||||
|
IgnoreItems: ignoreItems,
|
||||||
|
AllowedExtensions: allowedExts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var startTime = time.Now()
|
||||||
|
var errDetectedChange = errors.New("done")
|
||||||
|
|
||||||
|
// Watch ...
|
||||||
|
func (w *Watcher) Watch() { // nolint: gocyclo
|
||||||
|
for {
|
||||||
|
err := filepath.Walk(w.WatchItems[0], func(path string, info os.FileInfo, err error) error {
|
||||||
|
if path == ".git" && info.IsDir() {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, x := range w.IgnoreItems {
|
||||||
|
if x == path {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore hidden files
|
||||||
|
if filepath.Base(path)[0] == '.' {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
if _, ok := w.AllowedExtensions[ext]; ok && info.ModTime().After(startTime) {
|
||||||
|
w.Events <- path
|
||||||
|
startTime = time.Now()
|
||||||
|
return errDetectedChange
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil && err != errDetectedChange {
|
||||||
|
w.Errors <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue