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