mirror of https://github.com/maxcnunes/gaper.git
Add FS event watch support and move to v2 module
This commit is contained in:
parent
c44dd59952
commit
20438173e0
|
@ -8,3 +8,5 @@ coverage.out
|
||||||
testdata/server/server
|
testdata/server/server
|
||||||
dist
|
dist
|
||||||
test-srv
|
test-srv
|
||||||
|
testdata/.hidden-file
|
||||||
|
testdata/.hidden-folder
|
||||||
|
|
|
@ -2,22 +2,22 @@
|
||||||
|
|
||||||
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
|
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
|
||||||
|
|
||||||
There are few ways of contributing to gaper
|
There are a few ways of contributing to gaper
|
||||||
|
|
||||||
* Report an issue.
|
- Report an issue.
|
||||||
* Contribute to the code base.
|
- Contribute to the code base.
|
||||||
|
|
||||||
## Report an issue
|
## Report an issue
|
||||||
|
|
||||||
* Before opening the issue make sure there isn't an issue opened for the same problem
|
- 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
|
- Include the Go and Gaper version you are using
|
||||||
* If it is a bug, please include all info to reproduce the problem
|
- If it is a bug, please include all info to reproduce the problem
|
||||||
|
|
||||||
## Contribute to the code base
|
## Contribute to the code base
|
||||||
|
|
||||||
### Pull Request
|
### 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.
|
- 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
|
### Setupping development
|
||||||
|
|
||||||
|
@ -46,11 +46,13 @@ make lint
|
||||||
### Running tests
|
### Running tests
|
||||||
|
|
||||||
All tests:
|
All tests:
|
||||||
|
|
||||||
```
|
```
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
A single test:
|
A single test:
|
||||||
|
|
||||||
```
|
```
|
||||||
go test -run TestSimplePost ./...
|
go test -run TestSimplePost ./...
|
||||||
```
|
```
|
||||||
|
|
7
Makefile
7
Makefile
|
@ -13,8 +13,11 @@ lint:
|
||||||
@golangci-lint run
|
@golangci-lint run
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test -p=1 -coverpkg $(COVER_PACKAGES) \
|
@go test -p=1 -v \
|
||||||
-covermode=atomic -coverprofile=coverage.out $(TEST_PACKAGES)
|
-coverpkg $(COVER_PACKAGES) \
|
||||||
|
-covermode=atomic \
|
||||||
|
-coverprofile=coverage.out \
|
||||||
|
$(TEST_PACKAGES)
|
||||||
|
|
||||||
cover: test
|
cover: test
|
||||||
@go tool cover -html=coverage.out
|
@go tool cover -html=coverage.out
|
||||||
|
|
|
@ -3,8 +3,9 @@ package main
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/maxcnunes/gaper"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
|
gaper "github.com/maxcnunes/gaper/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// build info
|
// build info
|
||||||
|
@ -30,7 +31,8 @@ func main() {
|
||||||
DisableDefaultIgnore: c.Bool("disable-default-ignore"),
|
DisableDefaultIgnore: c.Bool("disable-default-ignore"),
|
||||||
WatchItems: c.StringSlice("watch"),
|
WatchItems: c.StringSlice("watch"),
|
||||||
IgnoreItems: c.StringSlice("ignore"),
|
IgnoreItems: c.StringSlice("ignore"),
|
||||||
PollInterval: c.Int("poll-interval"),
|
Poll: c.Bool("poll"),
|
||||||
|
PollInterval: c.Duration("poll-interval"),
|
||||||
Extensions: c.StringSlice("extensions"),
|
Extensions: c.StringSlice("extensions"),
|
||||||
NoRestartOn: c.String("no-restart-on"),
|
NoRestartOn: c.String("no-restart-on"),
|
||||||
}
|
}
|
||||||
|
@ -85,10 +87,15 @@ func main() {
|
||||||
Usage: "list of folders or files to ignore for changes\n" +
|
Usage: "list of folders or files to ignore for changes\n" +
|
||||||
"\t\t(always ignores all hidden files and directories)",
|
"\t\t(always ignores all hidden files and directories)",
|
||||||
},
|
},
|
||||||
&cli.IntFlag{
|
&cli.BoolFlag{
|
||||||
Name: "poll-interval, p",
|
Name: "poll, p",
|
||||||
|
Value: false,
|
||||||
|
Usage: "uses poll instead of fs events to watch file changes",
|
||||||
|
},
|
||||||
|
&cli.DurationFlag{
|
||||||
|
Name: "poll-interval",
|
||||||
Value: gaper.DefaultPoolInterval,
|
Value: gaper.DefaultPoolInterval,
|
||||||
Usage: "how often in milliseconds to poll watched files for changes",
|
Usage: "how often in milliseconds to poll watched files for changes (e.g. 1s, 500ms)",
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "extensions, e",
|
Name: "extensions, e",
|
||||||
|
|
98
gaper.go
98
gaper.go
|
@ -1,5 +1,5 @@
|
||||||
// Package gaper implements a supervisor restarts a go project
|
// Package gaper implements a supervisor that restarts a go project
|
||||||
// when it crashes or a watched file changes
|
// either when it crashes or when any watched file has changed.
|
||||||
package gaper
|
package gaper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -12,16 +12,21 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
shellwords "github.com/mattn/go-shellwords"
|
shellwords "github.com/mattn/go-shellwords"
|
||||||
|
|
||||||
|
"github.com/maxcnunes/gaper/v2/internal/build"
|
||||||
|
"github.com/maxcnunes/gaper/v2/internal/log"
|
||||||
|
"github.com/maxcnunes/gaper/v2/internal/run"
|
||||||
|
"github.com/maxcnunes/gaper/v2/internal/watch"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultBuildPath is the default build and watched path
|
var (
|
||||||
var DefaultBuildPath = "."
|
// DefaultBuildPath is the default build and watched path
|
||||||
|
DefaultBuildPath = "."
|
||||||
// DefaultExtensions is the default watched extension
|
// DefaultExtensions is the default watched extension
|
||||||
var DefaultExtensions = []string{"go"}
|
DefaultExtensions = []string{"go"}
|
||||||
|
// DefaultPoolInterval is the time in ms used by the watcher to wait between scans
|
||||||
// DefaultPoolInterval is the time in ms used by the watcher to wait between scans
|
DefaultPoolInterval = 500 * time.Millisecond
|
||||||
var DefaultPoolInterval = 500
|
)
|
||||||
|
|
||||||
// No restart types
|
// No restart types
|
||||||
var (
|
var (
|
||||||
|
@ -31,8 +36,10 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// exit statuses
|
// exit statuses
|
||||||
var exitStatusSuccess = 0
|
var (
|
||||||
var exitStatusError = 1
|
exitStatusSuccess = 0
|
||||||
|
exitStatusError = 1
|
||||||
|
)
|
||||||
|
|
||||||
// Config contains all settings supported by gaper
|
// Config contains all settings supported by gaper
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -44,7 +51,8 @@ type Config struct {
|
||||||
ProgramArgsMerged string
|
ProgramArgsMerged string
|
||||||
WatchItems []string
|
WatchItems []string
|
||||||
IgnoreItems []string
|
IgnoreItems []string
|
||||||
PollInterval int
|
Poll bool
|
||||||
|
PollInterval time.Duration
|
||||||
Extensions []string
|
Extensions []string
|
||||||
NoRestartOn string
|
NoRestartOn string
|
||||||
DisableDefaultIgnore bool
|
DisableDefaultIgnore bool
|
||||||
|
@ -54,34 +62,46 @@ type Config struct {
|
||||||
// Run starts the whole gaper process watching for file changes or exit codes
|
// Run starts the whole gaper process watching for file changes or exit codes
|
||||||
// and restarting the program
|
// and restarting the program
|
||||||
func Run(cfg *Config, chOSSiginal chan os.Signal) error {
|
func Run(cfg *Config, chOSSiginal chan os.Signal) error {
|
||||||
logger.Debug("Starting gaper")
|
log.Logger.Debug("Starting gaper")
|
||||||
|
|
||||||
if err := setupConfig(cfg); err != nil {
|
if err := setupConfig(cfg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf("Config: %+v", cfg)
|
log.Logger.Debugf("Config: %+v", cfg)
|
||||||
|
|
||||||
wCfg := WatcherConfig{
|
wCfg := watch.WatcherConfig{
|
||||||
DefaultIgnore: !cfg.DisableDefaultIgnore,
|
DefaultIgnore: !cfg.DisableDefaultIgnore,
|
||||||
|
Poll: cfg.Poll,
|
||||||
PollInterval: cfg.PollInterval,
|
PollInterval: cfg.PollInterval,
|
||||||
WatchItems: cfg.WatchItems,
|
WatchItems: cfg.WatchItems,
|
||||||
IgnoreItems: cfg.IgnoreItems,
|
IgnoreItems: cfg.IgnoreItems,
|
||||||
Extensions: cfg.Extensions,
|
Extensions: cfg.Extensions,
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := NewBuilder(cfg.BuildPath, cfg.BinName, cfg.WorkingDirectory, cfg.BuildArgs)
|
builder := build.NewBuilder(cfg.BuildPath, cfg.BinName, cfg.WorkingDirectory, cfg.BuildArgs)
|
||||||
runner := NewRunner(os.Stdout, os.Stderr, filepath.Join(cfg.WorkingDirectory, builder.Binary()), cfg.ProgramArgs)
|
runner := run.NewRunner(
|
||||||
watcher, err := NewWatcher(wCfg)
|
os.Stdout,
|
||||||
|
os.Stderr,
|
||||||
|
filepath.Join(cfg.WorkingDirectory, builder.Binary()),
|
||||||
|
cfg.ProgramArgs,
|
||||||
|
)
|
||||||
|
watcher, err := watch.NewWatcher(wCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("watcher error: %v", err)
|
return fmt.Errorf("watcher error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return run(cfg, chOSSiginal, builder, runner, watcher)
|
return start(cfg, chOSSiginal, builder, runner, watcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint: gocyclo
|
// nolint: gocyclo
|
||||||
func run(cfg *Config, chOSSiginal chan os.Signal, builder Builder, runner Runner, watcher Watcher) error {
|
func start(
|
||||||
|
cfg *Config,
|
||||||
|
chOSSiginal chan os.Signal,
|
||||||
|
builder build.Builder,
|
||||||
|
runner run.Runner,
|
||||||
|
watcher watch.Watcher,
|
||||||
|
) error {
|
||||||
if err := builder.Build(); err != nil {
|
if err := builder.Build(); err != nil {
|
||||||
return fmt.Errorf("build error: %v", err)
|
return fmt.Errorf("build error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -99,10 +119,10 @@ func run(cfg *Config, chOSSiginal chan os.Signal, builder Builder, runner Runner
|
||||||
go watcher.Watch()
|
go watcher.Watch()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event := <-watcher.Events():
|
case events := <-watcher.Events():
|
||||||
logger.Debug("Detected new changed file:", event)
|
log.Logger.Debug("Detected new changed file:", events)
|
||||||
if changeRestart {
|
if changeRestart {
|
||||||
logger.Debug("Skip restart due to existing on going restart")
|
log.Logger.Debug("Skip restart due to existing on going restart")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +134,7 @@ func run(cfg *Config, chOSSiginal chan os.Signal, builder Builder, runner Runner
|
||||||
case err := <-watcher.Errors():
|
case err := <-watcher.Errors():
|
||||||
return fmt.Errorf("error on watching files: %v", err)
|
return fmt.Errorf("error on watching files: %v", err)
|
||||||
case err := <-runner.Errors():
|
case err := <-runner.Errors():
|
||||||
logger.Debug("Detected program exit:", err)
|
log.Logger.Debug("Detected program exit:", err)
|
||||||
|
|
||||||
// ignore exit by change
|
// ignore exit by change
|
||||||
if changeRestart {
|
if changeRestart {
|
||||||
|
@ -126,21 +146,21 @@ func run(cfg *Config, chOSSiginal chan os.Signal, builder Builder, runner Runner
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case signal := <-chOSSiginal:
|
case signal := <-chOSSiginal:
|
||||||
logger.Debug("Got signal:", signal)
|
log.Logger.Debug("Got signal:", signal)
|
||||||
|
|
||||||
if err := runner.Kill(); err != nil {
|
if err := runner.Kill(); err != nil {
|
||||||
logger.Error("Error killing:", err)
|
log.Logger.Error("Error killing:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("OS signal: %v", signal)
|
return fmt.Errorf("OS signal: %v", signal)
|
||||||
default:
|
default:
|
||||||
time.Sleep(time.Duration(cfg.PollInterval) * time.Millisecond)
|
time.Sleep(cfg.PollInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restart(builder Builder, runner Runner) error {
|
func restart(builder build.Builder, runner run.Runner) error {
|
||||||
logger.Debug("Restarting program")
|
log.Logger.Debug("Restarting program")
|
||||||
|
|
||||||
// kill process if it is running
|
// kill process if it is running
|
||||||
if !runner.Exited() {
|
if !runner.Exited() {
|
||||||
|
@ -150,19 +170,19 @@ func restart(builder Builder, runner Runner) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := builder.Build(); err != nil {
|
if err := builder.Build(); err != nil {
|
||||||
logger.Error("Error building binary during a restart:", err)
|
log.Logger.Error("Error building binary during a restart:", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := runner.Run(); err != nil {
|
if _, err := runner.Run(); err != nil {
|
||||||
logger.Error("Error starting process during a restart:", err)
|
log.Logger.Error("Error starting process during a restart:", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleProgramExit(builder Builder, runner Runner, err error, noRestartOn string) error {
|
func handleProgramExit(builder build.Builder, runner run.Runner, err error, noRestartOn string) error {
|
||||||
exitStatus := runner.ExitStatus(err)
|
exitStatus := runner.ExitStatus(err)
|
||||||
|
|
||||||
// if "error", an exit code of 0 will still restart.
|
// if "error", an exit code of 0 will still restart.
|
||||||
|
@ -205,6 +225,14 @@ func setupConfig(cfg *Config) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.Poll && cfg.PollInterval == 0 {
|
||||||
|
cfg.PollInterval = DefaultPoolInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.Extensions) == 0 {
|
||||||
|
cfg.Extensions = DefaultExtensions
|
||||||
|
}
|
||||||
|
|
||||||
if len(cfg.WatchItems) == 0 {
|
if len(cfg.WatchItems) == 0 {
|
||||||
cfg.WatchItems = append(cfg.WatchItems, cfg.BuildPath)
|
cfg.WatchItems = append(cfg.WatchItems, cfg.BuildPath)
|
||||||
}
|
}
|
||||||
|
@ -226,3 +254,7 @@ func parseInnerArgs(args []string, argsm string) ([]string, error) {
|
||||||
|
|
||||||
return shellwords.Parse(argsm)
|
return shellwords.Parse(argsm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Logger() *log.LoggerEntity {
|
||||||
|
return log.Logger
|
||||||
|
}
|
||||||
|
|
|
@ -9,8 +9,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/maxcnunes/gaper/testdata"
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/maxcnunes/gaper/testdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGaperRunStopOnSGINT(t *testing.T) {
|
func TestGaperRunStopOnSGINT(t *testing.T) {
|
||||||
|
@ -48,7 +50,7 @@ func TestGaperBuildError(t *testing.T) {
|
||||||
cfg := &Config{}
|
cfg := &Config{}
|
||||||
|
|
||||||
chOSSiginal := make(chan os.Signal, 2)
|
chOSSiginal := make(chan os.Signal, 2)
|
||||||
err := run(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
|
err := start(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
|
||||||
assert.NotNil(t, err, "build error")
|
assert.NotNil(t, err, "build error")
|
||||||
assert.Equal(t, "build error: build-error", err.Error())
|
assert.Equal(t, "build error: build-error", err.Error())
|
||||||
}
|
}
|
||||||
|
@ -63,7 +65,7 @@ func TestGaperRunError(t *testing.T) {
|
||||||
cfg := &Config{}
|
cfg := &Config{}
|
||||||
|
|
||||||
chOSSiginal := make(chan os.Signal, 2)
|
chOSSiginal := make(chan os.Signal, 2)
|
||||||
err := run(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
|
err := start(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
|
||||||
assert.NotNil(t, err, "runner error")
|
assert.NotNil(t, err, "runner error")
|
||||||
assert.Equal(t, "run error: runner-error", err.Error())
|
assert.Equal(t, "run error: runner-error", err.Error())
|
||||||
}
|
}
|
||||||
|
@ -80,7 +82,7 @@ func TestGaperWatcherError(t *testing.T) {
|
||||||
|
|
||||||
mockWatcher := new(testdata.MockWacther)
|
mockWatcher := new(testdata.MockWacther)
|
||||||
watcherErrorsChan := make(chan error)
|
watcherErrorsChan := make(chan error)
|
||||||
watcherEvetnsChan := make(chan string)
|
watcherEvetnsChan := make(chan []fsnotify.Event)
|
||||||
mockWatcher.On("Errors").Return(watcherErrorsChan)
|
mockWatcher.On("Errors").Return(watcherErrorsChan)
|
||||||
mockWatcher.On("Events").Return(watcherEvetnsChan)
|
mockWatcher.On("Events").Return(watcherEvetnsChan)
|
||||||
|
|
||||||
|
@ -96,7 +98,7 @@ func TestGaperWatcherError(t *testing.T) {
|
||||||
watcherErrorsChan <- errors.New("watcher-error")
|
watcherErrorsChan <- errors.New("watcher-error")
|
||||||
}()
|
}()
|
||||||
chOSSiginal := make(chan os.Signal, 2)
|
chOSSiginal := make(chan os.Signal, 2)
|
||||||
err := run(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
|
err := start(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
|
||||||
assert.NotNil(t, err, "build error")
|
assert.NotNil(t, err, "build error")
|
||||||
assert.Equal(t, "error on watching files: watcher-error", err.Error())
|
assert.Equal(t, "error on watching files: watcher-error", err.Error())
|
||||||
mockBuilder.AssertExpectations(t)
|
mockBuilder.AssertExpectations(t)
|
||||||
|
@ -165,7 +167,7 @@ func TestGaperProgramExit(t *testing.T) {
|
||||||
|
|
||||||
mockWatcher := new(testdata.MockWacther)
|
mockWatcher := new(testdata.MockWacther)
|
||||||
watcherErrorsChan := make(chan error)
|
watcherErrorsChan := make(chan error)
|
||||||
watcherEvetnsChan := make(chan string)
|
watcherEvetnsChan := make(chan []fsnotify.Event)
|
||||||
mockWatcher.On("Errors").Return(watcherErrorsChan)
|
mockWatcher.On("Errors").Return(watcherErrorsChan)
|
||||||
mockWatcher.On("Events").Return(watcherEvetnsChan)
|
mockWatcher.On("Events").Return(watcherEvetnsChan)
|
||||||
|
|
||||||
|
@ -184,7 +186,7 @@ func TestGaperProgramExit(t *testing.T) {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
chOSSiginal <- syscall.SIGINT
|
chOSSiginal <- syscall.SIGINT
|
||||||
}()
|
}()
|
||||||
err := run(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
|
err := start(cfg, chOSSiginal, mockBuilder, mockRunner, mockWatcher)
|
||||||
assert.NotNil(t, err, "build error")
|
assert.NotNil(t, err, "build error")
|
||||||
assert.Equal(t, "OS signal: interrupt", err.Error())
|
assert.Equal(t, "OS signal: interrupt", err.Error())
|
||||||
mockBuilder.AssertExpectations(t)
|
mockBuilder.AssertExpectations(t)
|
||||||
|
|
21
go.mod
21
go.mod
|
@ -1,16 +1,25 @@
|
||||||
module github.com/maxcnunes/gaper
|
module github.com/maxcnunes/gaper/v2
|
||||||
|
|
||||||
go 1.13
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fatih/color v1.7.0
|
github.com/fatih/color v1.7.0
|
||||||
github.com/mattn/go-colorable v0.0.9 // indirect
|
github.com/fsnotify/fsnotify v1.6.0
|
||||||
github.com/mattn/go-isatty v0.0.3 // indirect
|
|
||||||
github.com/mattn/go-shellwords v1.0.3
|
github.com/mattn/go-shellwords v1.0.3
|
||||||
github.com/mattn/go-zglob v0.0.0-20180607075734-49693fbb3fe3
|
github.com/mattn/go-zglob v0.0.0-20180607075734-49693fbb3fe3
|
||||||
github.com/stretchr/objx v0.1.1 // indirect
|
|
||||||
github.com/stretchr/testify v1.4.0
|
github.com/stretchr/testify v1.4.0
|
||||||
github.com/urfave/cli/v2 v2.11.1
|
github.com/urfave/cli/v2 v2.11.1
|
||||||
golang.org/x/sys v0.0.0-20180616030259-6c888cc515d3 // indirect
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.0.9 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.3 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.1.1 // indirect
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
|
golang.org/x/sys v0.11.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
14
go.sum
14
go.sum
|
@ -1,10 +1,12 @@
|
||||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||||
|
@ -26,13 +28,11 @@ github.com/urfave/cli/v2 v2.11.1 h1:UKK6SP7fV3eKOefbS87iT9YHefv7iB/53ih6e+GNAsE=
|
||||||
github.com/urfave/cli/v2 v2.11.1/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
|
github.com/urfave/cli/v2 v2.11.1/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
golang.org/x/sys v0.0.0-20180616030259-6c888cc515d3 h1:FCfAlbS73+IQQJktaKGHldMdL2bGDVpm+OrCEbVz1f4=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20180616030259-6c888cc515d3/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package gaper
|
package build
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -6,6 +6,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/maxcnunes/gaper/v2/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Builder is a interface for the build process
|
// Builder is a interface for the build process
|
||||||
|
@ -29,7 +31,7 @@ func NewBuilder(dir string, bin string, wd string, buildArgs []string) Builder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// does not work on Windows without the ".exe" extension
|
// does not work on Windows without the ".exe" extension
|
||||||
if runtime.GOOS == OSWindows {
|
if runtime.GOOS == "windows" {
|
||||||
// check if it already has the .exe extension
|
// check if it already has the .exe extension
|
||||||
if !strings.HasSuffix(bin, ".exe") {
|
if !strings.HasSuffix(bin, ".exe") {
|
||||||
bin += ".exe"
|
bin += ".exe"
|
||||||
|
@ -46,9 +48,9 @@ func (b *builder) Binary() string {
|
||||||
|
|
||||||
// Build the Golang project set for this builder
|
// Build the Golang project set for this builder
|
||||||
func (b *builder) Build() error {
|
func (b *builder) Build() error {
|
||||||
logger.Info("Building program")
|
log.Logger.Info("Building program")
|
||||||
args := append([]string{"go", "build", "-o", filepath.Join(b.wd, b.binary)}, b.buildArgs...)
|
args := append([]string{"go", "build", "-o", filepath.Join(b.wd, b.binary)}, b.buildArgs...)
|
||||||
logger.Debug("Build command", args)
|
log.Logger.Debug("Build command", args)
|
||||||
|
|
||||||
command := exec.Command(args[0], args[1:]...) // nolint gas
|
command := exec.Command(args[0], args[1:]...) // nolint gas
|
||||||
command.Dir = b.dir
|
command.Dir = b.dir
|
|
@ -1,4 +1,4 @@
|
||||||
package gaper
|
package build
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
@ -12,7 +12,7 @@ import (
|
||||||
func TestBuilderSuccessBuild(t *testing.T) {
|
func TestBuilderSuccessBuild(t *testing.T) {
|
||||||
bArgs := []string{}
|
bArgs := []string{}
|
||||||
bin := resolveBinNameByOS("srv")
|
bin := resolveBinNameByOS("srv")
|
||||||
dir := filepath.Join("testdata", "server")
|
dir := filepath.Join("..", "..", "testdata", "server")
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("couldn't get current working directory: %v", err)
|
t.Fatalf("couldn't get current working directory: %v", err)
|
||||||
|
@ -32,7 +32,7 @@ func TestBuilderSuccessBuild(t *testing.T) {
|
||||||
func TestBuilderFailureBuild(t *testing.T) {
|
func TestBuilderFailureBuild(t *testing.T) {
|
||||||
bArgs := []string{}
|
bArgs := []string{}
|
||||||
bin := "srv"
|
bin := "srv"
|
||||||
dir := filepath.Join("testdata", "build-failure")
|
dir := filepath.Join("..", "..", "testdata", "build-failure")
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("couldn't get current working directory: %v", err)
|
t.Fatalf("couldn't get current working directory: %v", err)
|
||||||
|
@ -41,7 +41,7 @@ func TestBuilderFailureBuild(t *testing.T) {
|
||||||
b := NewBuilder(dir, bin, wd, bArgs)
|
b := NewBuilder(dir, bin, wd, bArgs)
|
||||||
err = b.Build()
|
err = b.Build()
|
||||||
assert.NotNil(t, err, "build error")
|
assert.NotNil(t, err, "build error")
|
||||||
assert.Equal(t, err.Error(), "build failed with exit status 2\n"+
|
assert.Equal(t, err.Error(), "build failed with exit status 1\n"+
|
||||||
"# github.com/maxcnunes/gaper/testdata/build-failure\n"+
|
"# github.com/maxcnunes/gaper/testdata/build-failure\n"+
|
||||||
"./main.go:4:6: func main must have no arguments and no return values\n"+
|
"./main.go:4:6: func main must have no arguments and no return values\n"+
|
||||||
"./main.go:5:1: missing return\n")
|
"./main.go:5:1: missing return\n")
|
||||||
|
@ -49,14 +49,14 @@ func TestBuilderFailureBuild(t *testing.T) {
|
||||||
|
|
||||||
func TestBuilderDefaultBinName(t *testing.T) {
|
func TestBuilderDefaultBinName(t *testing.T) {
|
||||||
bin := ""
|
bin := ""
|
||||||
dir := filepath.Join("testdata", "server")
|
dir := filepath.Join("..", "..", "testdata", "server")
|
||||||
wd := "/src/projects/project-name"
|
wd := "/src/projects/project-name"
|
||||||
b := NewBuilder(dir, bin, wd, nil)
|
b := NewBuilder(dir, bin, wd, nil)
|
||||||
assert.Equal(t, b.Binary(), resolveBinNameByOS("project-name"))
|
assert.Equal(t, b.Binary(), resolveBinNameByOS("project-name"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveBinNameByOS(name string) string {
|
func resolveBinNameByOS(name string) string {
|
||||||
if runtime.GOOS == OSWindows {
|
if runtime.GOOS == "windows" {
|
||||||
name += ".exe"
|
name += ".exe"
|
||||||
}
|
}
|
||||||
return name
|
return name
|
|
@ -1,4 +1,4 @@
|
||||||
package gaper
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
@ -8,12 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// logger use by the whole package
|
// logger use by the whole package
|
||||||
var logger = newLogger("gaper")
|
var Logger = newLogger("gaper")
|
||||||
|
|
||||||
// Logger give access to external packages to use gaper logger
|
|
||||||
func Logger() *LoggerEntity {
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoggerEntity used by gaper
|
// LoggerEntity used by gaper
|
||||||
type LoggerEntity struct {
|
type LoggerEntity struct {
|
||||||
|
@ -27,7 +22,7 @@ type LoggerEntity struct {
|
||||||
func newLogger(prefix string) *LoggerEntity {
|
func newLogger(prefix string) *LoggerEntity {
|
||||||
prefix = "[" + prefix + "] "
|
prefix = "[" + prefix + "] "
|
||||||
return &LoggerEntity{
|
return &LoggerEntity{
|
||||||
verbose: false,
|
verbose: os.Getenv("GAPER_VERBOSE") == "true",
|
||||||
logDebug: log.New(os.Stdout, prefix, 0),
|
logDebug: log.New(os.Stdout, prefix, 0),
|
||||||
logInfo: log.New(os.Stdout, color.CyanString(prefix), 0),
|
logInfo: log.New(os.Stdout, color.CyanString(prefix), 0),
|
||||||
logError: log.New(os.Stdout, color.RedString(prefix), 0),
|
logError: log.New(os.Stdout, color.RedString(prefix), 0),
|
|
@ -1,4 +1,4 @@
|
||||||
package gaper
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
|
@ -1,4 +1,4 @@
|
||||||
package gaper
|
package run
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -9,10 +9,9 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
|
||||||
|
|
||||||
// OSWindows is used to check if current OS is a Windows
|
"github.com/maxcnunes/gaper/v2/internal/log"
|
||||||
const OSWindows = "windows"
|
)
|
||||||
|
|
||||||
// os errors
|
// os errors
|
||||||
var errFinished = errors.New("os: process already finished")
|
var errFinished = errors.New("os: process already finished")
|
||||||
|
@ -33,7 +32,6 @@ type runner struct {
|
||||||
writerStdout io.Writer
|
writerStdout io.Writer
|
||||||
writerStderr io.Writer
|
writerStderr io.Writer
|
||||||
command *exec.Cmd
|
command *exec.Cmd
|
||||||
starttime time.Time
|
|
||||||
errors chan error
|
errors chan error
|
||||||
end chan bool // used internally by Kill to wait a process die
|
end chan bool // used internally by Kill to wait a process die
|
||||||
}
|
}
|
||||||
|
@ -45,7 +43,6 @@ func NewRunner(wStdout io.Writer, wStderr io.Writer, bin string, args []string)
|
||||||
args: args,
|
args: args,
|
||||||
writerStdout: wStdout,
|
writerStdout: wStdout,
|
||||||
writerStderr: wStderr,
|
writerStderr: wStderr,
|
||||||
starttime: time.Now(),
|
|
||||||
errors: make(chan error),
|
errors: make(chan error),
|
||||||
end: make(chan bool),
|
end: make(chan bool),
|
||||||
}
|
}
|
||||||
|
@ -53,7 +50,7 @@ func NewRunner(wStdout io.Writer, wStderr io.Writer, bin string, args []string)
|
||||||
|
|
||||||
// Run executes the project binary
|
// Run executes the project binary
|
||||||
func (r *runner) Run() (*exec.Cmd, error) {
|
func (r *runner) Run() (*exec.Cmd, error) {
|
||||||
logger.Info("Starting program")
|
log.Logger.Info("Starting program")
|
||||||
|
|
||||||
if r.command != nil && !r.Exited() {
|
if r.command != nil && !r.Exited() {
|
||||||
return r.command, nil
|
return r.command, nil
|
||||||
|
@ -79,7 +76,7 @@ func (r *runner) Kill() error { // nolint gocyclo
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Trying a "soft" kill first
|
// Trying a "soft" kill first
|
||||||
if runtime.GOOS == OSWindows {
|
if runtime.GOOS == "windows" {
|
||||||
if err := r.command.Process.Kill(); err != nil {
|
if err := r.command.Process.Kill(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -133,27 +130,15 @@ func (r *runner) ExitStatus(err error) int {
|
||||||
|
|
||||||
func (r *runner) runBin() error {
|
func (r *runner) runBin() error {
|
||||||
r.command = exec.Command(r.bin, r.args...) // nolint gas
|
r.command = exec.Command(r.bin, r.args...) // nolint gas
|
||||||
stdout, err := r.command.StdoutPipe()
|
|
||||||
|
r.command.Stdout = r.writerStdout
|
||||||
|
r.command.Stderr = r.writerStderr
|
||||||
|
|
||||||
|
err := r.command.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
stderr, err := r.command.StderrPipe()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: handle or log errors
|
|
||||||
go io.Copy(r.writerStdout, stdout) // nolint errcheck
|
|
||||||
go io.Copy(r.writerStderr, stderr) // nolint errcheck
|
|
||||||
|
|
||||||
err = r.command.Start()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.starttime = time.Now()
|
|
||||||
|
|
||||||
// wait for exit errors
|
// wait for exit errors
|
||||||
go func() {
|
go func() {
|
||||||
r.errors <- r.command.Wait()
|
r.errors <- r.command.Wait()
|
|
@ -1,4 +1,4 @@
|
||||||
package gaper
|
package run
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -16,8 +16,8 @@ func TestRunnerSuccessRun(t *testing.T) {
|
||||||
stdout := bytes.NewBufferString("")
|
stdout := bytes.NewBufferString("")
|
||||||
stderr := bytes.NewBufferString("")
|
stderr := bytes.NewBufferString("")
|
||||||
pArgs := []string{}
|
pArgs := []string{}
|
||||||
bin := filepath.Join("testdata", "print-gaper")
|
bin := filepath.Join("..", "..", "testdata", "print-gaper")
|
||||||
if runtime.GOOS == OSWindows {
|
if runtime.GOOS == "windows" {
|
||||||
bin += ".bat"
|
bin += ".bat"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,12 +30,12 @@ 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(), "")
|
// assert.Equal(t, stderr.String(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunnerSuccessKill(t *testing.T) {
|
func TestRunnerSuccessKill(t *testing.T) {
|
||||||
bin := filepath.Join("testdata", "print-gaper")
|
bin := filepath.Join("..", "..", "testdata", "print-gaper")
|
||||||
if runtime.GOOS == OSWindows {
|
if runtime.GOOS == "windows" {
|
||||||
bin += ".bat"
|
bin += ".bat"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Package fsmonitor provides a mechanism for watching file(s) for changes.
|
||||||
|
// Generally leans on fsnotify, but provides a poll-based notifier which fsnotify does not support.
|
||||||
|
// These are wrapped up in a common interface so that either can be used interchangeably in your code.
|
||||||
|
//
|
||||||
|
// This package is adapted from https://github.com/gohugoio/hugo/blob/master/watcher/filenotify Apache-2.0 License.
|
||||||
|
// Hopefully this can be replaced with an external package sometime in the future, see https://github.com/fsnotify/fsnotify/issues/9
|
||||||
|
package fsmonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileWatcher is an interface for implementing file notification watchers
|
||||||
|
type FileWatcher interface {
|
||||||
|
Events() <-chan fsnotify.Event
|
||||||
|
Errors() <-chan error
|
||||||
|
Add(name string) error
|
||||||
|
Remove(name string) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventWatcherWithPollFallback tries to use an fs-event watcher, and falls back to the poller if there is an error
|
||||||
|
func NewEventWatcherWithPollFallback(interval time.Duration) (FileWatcher, error) {
|
||||||
|
if watcher, err := NewEventWatcher(); err == nil {
|
||||||
|
return watcher, nil
|
||||||
|
}
|
||||||
|
return NewPollingWatcher(interval), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPollingWatcher returns a poll-based file watcher
|
||||||
|
func NewPollingWatcher(interval time.Duration) FileWatcher {
|
||||||
|
return &filePoller{
|
||||||
|
interval: interval,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
events: make(chan fsnotify.Event),
|
||||||
|
errors: make(chan error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventWatcher returns an fs-event based file watcher
|
||||||
|
func NewEventWatcher() (FileWatcher, error) {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &fsNotifyWatcher{watcher}, nil
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package fsmonitor
|
||||||
|
|
||||||
|
import "github.com/fsnotify/fsnotify"
|
||||||
|
|
||||||
|
// fsNotifyWatcher wraps the fsnotify package to satisfy the FileNotifier interface
|
||||||
|
type fsNotifyWatcher struct {
|
||||||
|
*fsnotify.Watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events returns the fsnotify event channel receiver
|
||||||
|
func (w *fsNotifyWatcher) Events() <-chan fsnotify.Event {
|
||||||
|
return w.Watcher.Events
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors returns the fsnotify error channel receiver
|
||||||
|
func (w *fsNotifyWatcher) Errors() <-chan error {
|
||||||
|
return w.Watcher.Errors
|
||||||
|
}
|
|
@ -0,0 +1,330 @@
|
||||||
|
package fsmonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
|
||||||
|
"github.com/maxcnunes/gaper/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// errPollerClosed is returned when the poller is closed
|
||||||
|
errPollerClosed = errors.New("poller is closed")
|
||||||
|
// errNoSuchWatch is returned when trying to remove a watch that doesn't exist
|
||||||
|
errNoSuchWatch = errors.New("watch does not exist")
|
||||||
|
)
|
||||||
|
|
||||||
|
// filePoller is used to poll files for changes, especially in cases where fsnotify
|
||||||
|
// can't be run (e.g. when inotify handles are exhausted)
|
||||||
|
// filePoller satisfies the FileWatcher interface
|
||||||
|
type filePoller struct {
|
||||||
|
// the duration between polls.
|
||||||
|
interval time.Duration
|
||||||
|
// watches is the list of files currently being polled, close the associated channel to stop the watch
|
||||||
|
watches map[string]struct{}
|
||||||
|
// Will be closed when done.
|
||||||
|
done chan struct{}
|
||||||
|
// events is the channel to listen to for watch events
|
||||||
|
events chan fsnotify.Event
|
||||||
|
// errors is the channel to listen to for watch errors
|
||||||
|
errors chan error
|
||||||
|
// mu locks the poller for modification
|
||||||
|
mu sync.Mutex
|
||||||
|
// closed is used to specify when the poller has already closed
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a filename to the list of watches
|
||||||
|
// once added the file is polled for changes in a separate goroutine
|
||||||
|
func (w *filePoller) Add(name string) error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
if w.closed {
|
||||||
|
return errPollerClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := newItemToWatch(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if item.left.FileInfo == nil {
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.watches == nil {
|
||||||
|
w.watches = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
if _, exists := w.watches[name]; exists {
|
||||||
|
return fmt.Errorf("watch exists")
|
||||||
|
}
|
||||||
|
w.watches[name] = struct{}{}
|
||||||
|
|
||||||
|
go w.watch(item)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove stops and removes watch with the specified name
|
||||||
|
func (w *filePoller) Remove(name string) error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
return w.remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *filePoller) remove(name string) error {
|
||||||
|
if w.closed {
|
||||||
|
return errPollerClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
_, exists := w.watches[name]
|
||||||
|
if !exists {
|
||||||
|
return errNoSuchWatch
|
||||||
|
}
|
||||||
|
delete(w.watches, name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events returns the event channel
|
||||||
|
// This is used for notifications on events about watched files
|
||||||
|
func (w *filePoller) Events() <-chan fsnotify.Event {
|
||||||
|
return w.events
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors returns the errors channel
|
||||||
|
// This is used for notifications about errors on watched files
|
||||||
|
func (w *filePoller) Errors() <-chan error {
|
||||||
|
return w.errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the poller
|
||||||
|
// All watches are stopped, removed, and the poller cannot be added to
|
||||||
|
func (w *filePoller) Close() error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
if w.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w.closed = true
|
||||||
|
close(w.done)
|
||||||
|
for name := range w.watches {
|
||||||
|
if err := w.remove(name); err != nil {
|
||||||
|
log.Logger.Error("Error removing file ", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendEvent publishes the specified event to the events channel
|
||||||
|
func (w *filePoller) sendEvent(e fsnotify.Event) error {
|
||||||
|
select {
|
||||||
|
case w.events <- e:
|
||||||
|
case <-w.done:
|
||||||
|
return fmt.Errorf("closed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendErr publishes the specified error to the errors channel
|
||||||
|
func (w *filePoller) sendErr(e error) error {
|
||||||
|
select {
|
||||||
|
case w.errors <- e:
|
||||||
|
case <-w.done:
|
||||||
|
return fmt.Errorf("closed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// watch watches item for changes until done is closed.
|
||||||
|
func (w *filePoller) watch(item *itemToWatch) {
|
||||||
|
ticker := time.NewTicker(w.interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
case <-w.done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
evs, err := item.checkForChanges()
|
||||||
|
if err != nil {
|
||||||
|
if err := w.sendErr(err); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item.left, item.right = item.right, item.left
|
||||||
|
|
||||||
|
for _, ev := range evs {
|
||||||
|
if err := w.sendEvent(ev); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recording records the state of a file or a dir.
|
||||||
|
type recording struct {
|
||||||
|
os.FileInfo
|
||||||
|
|
||||||
|
// Set if FileInfo is a dir.
|
||||||
|
entries map[string]os.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *recording) clear() {
|
||||||
|
r.FileInfo = nil
|
||||||
|
if r.entries != nil {
|
||||||
|
for k := range r.entries {
|
||||||
|
delete(r.entries, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *recording) record(filename string) error {
|
||||||
|
r.clear()
|
||||||
|
|
||||||
|
fi, err := os.Stat(filename)
|
||||||
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.FileInfo = fi
|
||||||
|
|
||||||
|
// If fi is a dir, we watch the files inside that directory (not recursively).
|
||||||
|
// This matches the behaviour of fsnotity.
|
||||||
|
if fi.IsDir() {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
fis, err := f.Readdir(-1)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fi := range fis {
|
||||||
|
r.entries[fi.Name()] = fi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// itemToWatch may be a file or a dir.
|
||||||
|
type itemToWatch struct {
|
||||||
|
// Full path to the filename.
|
||||||
|
filename string
|
||||||
|
|
||||||
|
// Snapshots of the stat state of this file or dir.
|
||||||
|
left *recording
|
||||||
|
right *recording
|
||||||
|
}
|
||||||
|
|
||||||
|
func newItemToWatch(filename string) (*itemToWatch, error) {
|
||||||
|
r := &recording{
|
||||||
|
entries: make(map[string]os.FileInfo),
|
||||||
|
}
|
||||||
|
err := r.record(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &itemToWatch{filename: filename, left: r}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item *itemToWatch) checkForChanges() ([]fsnotify.Event, error) {
|
||||||
|
if item.right == nil {
|
||||||
|
item.right = &recording{
|
||||||
|
entries: make(map[string]os.FileInfo),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Logger.Debug("Watching", item.filename)
|
||||||
|
|
||||||
|
err := item.right.record(item.filename)
|
||||||
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dirOp := checkChange(item.left.FileInfo, item.right.FileInfo)
|
||||||
|
|
||||||
|
if dirOp != 0 {
|
||||||
|
evs := []fsnotify.Event{{Op: dirOp, Name: item.filename}}
|
||||||
|
return evs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.left.FileInfo == nil || !item.left.IsDir() {
|
||||||
|
// Done.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
leftIsIn := false
|
||||||
|
left, right := item.left.entries, item.right.entries
|
||||||
|
if len(right) > len(left) {
|
||||||
|
left, right = right, left
|
||||||
|
leftIsIn = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var evs []fsnotify.Event
|
||||||
|
|
||||||
|
for name, fi1 := range left {
|
||||||
|
fi2 := right[name]
|
||||||
|
fil, fir := fi1, fi2
|
||||||
|
if leftIsIn {
|
||||||
|
fil, fir = fir, fil
|
||||||
|
}
|
||||||
|
op := checkChange(fil, fir)
|
||||||
|
if op != 0 {
|
||||||
|
evs = append(evs, fsnotify.Event{Op: op, Name: filepath.Join(item.filename, name)})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return evs, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkChange(fi1, fi2 os.FileInfo) fsnotify.Op {
|
||||||
|
if fi1 == nil && fi2 != nil {
|
||||||
|
return fsnotify.Create
|
||||||
|
}
|
||||||
|
if fi1 != nil && fi2 == nil {
|
||||||
|
return fsnotify.Remove
|
||||||
|
}
|
||||||
|
if fi1 == nil && fi2 == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if fi1.IsDir() || fi2.IsDir() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if fi1.Mode() != fi2.Mode() {
|
||||||
|
return fsnotify.Chmod
|
||||||
|
}
|
||||||
|
if fi1.ModTime() != fi2.ModTime() || fi1.Size() != fi2.Size() {
|
||||||
|
return fsnotify.Write
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
|
@ -0,0 +1,375 @@
|
||||||
|
// Package watch provides a mechanism for watching file(s) for changes.
|
||||||
|
// This package is adapted from https://github.com/gohugoio/hugo/blob/master/watcher Apache-2.0 License.
|
||||||
|
package watch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
zglob "github.com/mattn/go-zglob"
|
||||||
|
|
||||||
|
"github.com/maxcnunes/gaper/v2/internal/log"
|
||||||
|
"github.com/maxcnunes/gaper/v2/internal/watch/fsmonitor"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Time to gather changes and handle them in batches
|
||||||
|
const intervalBatcher = 500 * time.Millisecond
|
||||||
|
|
||||||
|
// Watcher is a interface for the watch process
|
||||||
|
type Watcher interface {
|
||||||
|
Watch()
|
||||||
|
Errors() chan error
|
||||||
|
Events() chan []fsnotify.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatcherConfig defines the settings available for the watcher
|
||||||
|
type WatcherConfig struct {
|
||||||
|
DefaultIgnore bool
|
||||||
|
Poll bool
|
||||||
|
PollInterval time.Duration
|
||||||
|
WatchItems []string
|
||||||
|
IgnoreItems []string
|
||||||
|
Extensions []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvePaths(paths []string, extensions map[string]bool) (map[string]bool, error) {
|
||||||
|
result := map[string]bool{}
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
matches := []string{path}
|
||||||
|
|
||||||
|
isGlob := strings.Contains(path, "*")
|
||||||
|
if isGlob {
|
||||||
|
var err error
|
||||||
|
matches, err = zglob.Glob(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't resolve glob path \"%s\": %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
// ignore existing files that don't match the allowed extensions
|
||||||
|
if f, err := os.Stat(match); !os.IsNotExist(err) && !f.IsDir() {
|
||||||
|
if ext := filepath.Ext(match); ext != "" {
|
||||||
|
if _, ok := extensions[ext]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := result[match]; !ok {
|
||||||
|
result[match] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeOverlappedPaths(result)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove overlapped paths so it makes the scan for changes later faster and simpler
|
||||||
|
func removeOverlappedPaths(mapPaths map[string]bool) {
|
||||||
|
startDot := regexp.MustCompile(`^\./`)
|
||||||
|
|
||||||
|
for p1 := range mapPaths {
|
||||||
|
p1 = startDot.ReplaceAllString(p1, "")
|
||||||
|
|
||||||
|
// skip to next item if this path has already been checked
|
||||||
|
if v, ok := mapPaths[p1]; ok && !v {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for p2 := range mapPaths {
|
||||||
|
p2 = startDot.ReplaceAllString(p2, "")
|
||||||
|
|
||||||
|
if p1 == p2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(p2, p1) {
|
||||||
|
mapPaths[p2] = false
|
||||||
|
} else if strings.HasPrefix(p1, p2) {
|
||||||
|
mapPaths[p1] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup path list
|
||||||
|
for p := range mapPaths {
|
||||||
|
if !mapPaths[p] {
|
||||||
|
delete(mapPaths, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// watcher batches file watch events in a given interval.
|
||||||
|
type watcher struct {
|
||||||
|
fsmonitor.FileWatcher
|
||||||
|
ticker *time.Ticker
|
||||||
|
done chan struct{}
|
||||||
|
errors chan error
|
||||||
|
|
||||||
|
events chan []fsnotify.Event // Events are returned on this channel
|
||||||
|
|
||||||
|
defaultIgnore bool
|
||||||
|
watchItems map[string]bool
|
||||||
|
ignoreItems map[string]bool
|
||||||
|
allowedExtensions map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWatcher creates and starts a watcher with the given time interval.
|
||||||
|
// It will fall back to a poll based watcher if native isn's supported.
|
||||||
|
// To always use polling, set poll to true.
|
||||||
|
func NewWatcher(cfg WatcherConfig) (Watcher, error) {
|
||||||
|
allowedExts := make(map[string]bool)
|
||||||
|
for _, ext := range cfg.Extensions {
|
||||||
|
allowedExts["."+ext] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
watchPaths, err := resolvePaths(cfg.WatchItems, allowedExts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ignorePaths, err := resolvePaths(cfg.IgnoreItems, allowedExts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Logger.Debugf("Resolved watch paths: %v", watchPaths)
|
||||||
|
log.Logger.Debugf("Resolved ignore paths: %v", ignorePaths)
|
||||||
|
|
||||||
|
var fw fsmonitor.FileWatcher
|
||||||
|
|
||||||
|
if cfg.Poll {
|
||||||
|
fw = fsmonitor.NewPollingWatcher(cfg.PollInterval)
|
||||||
|
} else {
|
||||||
|
fw, err = fsmonitor.NewEventWatcherWithPollFallback(cfg.PollInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &watcher{
|
||||||
|
defaultIgnore: cfg.DefaultIgnore,
|
||||||
|
watchItems: watchPaths,
|
||||||
|
ignoreItems: ignorePaths,
|
||||||
|
allowedExtensions: allowedExts,
|
||||||
|
|
||||||
|
FileWatcher: fw,
|
||||||
|
ticker: time.NewTicker(intervalBatcher),
|
||||||
|
done: make(chan struct{}, 1),
|
||||||
|
events: make(chan []fsnotify.Event, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
for fpath := range watchPaths {
|
||||||
|
log.Logger.Debug("Add file", fpath)
|
||||||
|
if err := w.Add(fpath); err != nil {
|
||||||
|
log.Logger.Error("Error adding path ", fpath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch starts watching for file changes
|
||||||
|
func (w *watcher) Watch() {
|
||||||
|
evs := make([]fsnotify.Event, 0)
|
||||||
|
OuterLoop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case ev := <-w.FileWatcher.Events():
|
||||||
|
evs = append(evs, ev)
|
||||||
|
case <-w.ticker.C:
|
||||||
|
if len(evs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := w.filterEvents(evs)
|
||||||
|
evs = make([]fsnotify.Event, 0)
|
||||||
|
|
||||||
|
if len(filtered) > 0 {
|
||||||
|
w.events <- filtered
|
||||||
|
}
|
||||||
|
case <-w.done:
|
||||||
|
break OuterLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(w.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watcher) filterEvents(evs []fsnotify.Event) []fsnotify.Event {
|
||||||
|
filtered := []fsnotify.Event{}
|
||||||
|
|
||||||
|
for _, ev := range evs {
|
||||||
|
log.Logger.Debug("Filter event", ev.Op, ev.Name)
|
||||||
|
|
||||||
|
if w.ignoreFile(ev.Name) {
|
||||||
|
log.Logger.Debug("Ignored based on the configuration")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these
|
||||||
|
if ev.Name == "" {
|
||||||
|
log.Logger.Debug("Ignored because event name is empty")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write and rename operations are often followed by CHMOD.
|
||||||
|
// There may be valid use cases for rebuilding the site on CHMOD,
|
||||||
|
// but that will require more complex logic than this simple conditional.
|
||||||
|
// On OS X this seems to be related to Spotlight, see:
|
||||||
|
// https://github.com/go-fsnotify/fsnotify/issues/15
|
||||||
|
// A workaround is to put your site(s) on the Spotlight exception list,
|
||||||
|
// but that may be a little mysterious for most end users.
|
||||||
|
// So, for now, we skip reload on CHMOD.
|
||||||
|
// We do have to check for WRITE though. On slower laptops a Chmod
|
||||||
|
// could be aggregated with other important events, and we still want
|
||||||
|
// to rebuild on those
|
||||||
|
if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
|
||||||
|
log.Logger.Debug("Ignored because it is a CHMOD event")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
walkAdder := func(path string, f os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
// Ignore attempt to access go temporary unmask
|
||||||
|
if strings.Contains(err.Error(), "-go-tmp-umask") {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("couldn't walk to path \"%s\": %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.IsDir() {
|
||||||
|
log.Logger.Debugf("Adding created directory to watchlist %s", path)
|
||||||
|
if err := w.FileWatcher.Add(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filtered = append(filtered, fsnotify.Event{Name: path, Op: fsnotify.Create})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively add new directories to watch list
|
||||||
|
// When mkdir -p is used, only the top directory triggers an event (at least on OSX)
|
||||||
|
if ev.Op&fsnotify.Create == fsnotify.Create {
|
||||||
|
info, err := os.Stat(ev.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Logger.Errorf("Error reading created file/dir %s: %v", ev.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Mode().IsDir() {
|
||||||
|
if err = filepath.Walk(ev.Name, walkAdder); err != nil {
|
||||||
|
log.Logger.Errorf("Error walking to created file/dir %s: %v", ev.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Logger.Debug("Accepted")
|
||||||
|
filtered = append(filtered, ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Support gitignore rules https://github.com/sabhiram/go-gitignore
|
||||||
|
func (w *watcher) ignoreFile(filename string) bool {
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
baseName := filepath.Base(filename)
|
||||||
|
|
||||||
|
istemp := strings.HasSuffix(ext, "~") ||
|
||||||
|
(ext == ".swp") || // vim
|
||||||
|
(ext == ".swx") || // vim
|
||||||
|
(ext == ".tmp") || // generic temp file
|
||||||
|
(ext == ".DS_Store") || // OSX Thumbnail
|
||||||
|
baseName == "4913" || // vim
|
||||||
|
strings.HasPrefix(ext, ".goutputstream") || // gnome
|
||||||
|
strings.HasSuffix(ext, "jb_old___") || // intelliJ
|
||||||
|
strings.HasSuffix(ext, "jb_tmp___") || // intelliJ
|
||||||
|
strings.HasSuffix(ext, "jb_bak___") || // intelliJ
|
||||||
|
strings.HasPrefix(ext, ".sb-") || // byword
|
||||||
|
strings.HasPrefix(baseName, ".#") || // emacs
|
||||||
|
strings.HasPrefix(baseName, "#") || // emacs
|
||||||
|
strings.Contains(baseName, "-go-tmp-umask") // golang
|
||||||
|
|
||||||
|
if istemp {
|
||||||
|
log.Logger.Debug("Ignored file: temp")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Logger.Debugf("Ignored file: stats failure: %v", err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// if a file has been deleted after gaper was watching it
|
||||||
|
// info will be nil in the other iterations
|
||||||
|
if info == nil {
|
||||||
|
log.Logger.Debug("Ignored file: reason not info")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if preset ignore is enabled
|
||||||
|
if w.defaultIgnore {
|
||||||
|
// check for hidden files and directories
|
||||||
|
if name := info.Name(); name[0] == '.' && name != "." {
|
||||||
|
log.Logger.Debug("Ignored file: hidden")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if it is a Go testing file
|
||||||
|
if strings.HasSuffix(filename, "_test.go") {
|
||||||
|
log.Logger.Debug("Ignored file: go test file")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if it is the vendor folder
|
||||||
|
if info.IsDir() && info.Name() == "vendor" {
|
||||||
|
log.Logger.Debug("Ignored file: vendor")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ignored := w.ignoreItems[filename]; ignored {
|
||||||
|
log.Logger.Debug("Ignored file: ignored list")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ext != "" && len(w.allowedExtensions) > 0 {
|
||||||
|
if _, allowed := w.allowedExtensions[ext]; !allowed {
|
||||||
|
log.Logger.Debugf("Ignored file: extension not allowed '%s'", ext)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops the watching of the files.
|
||||||
|
func (w *watcher) Close() {
|
||||||
|
w.done <- struct{}{}
|
||||||
|
w.FileWatcher.Close()
|
||||||
|
w.ticker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events get events occurred during the watching
|
||||||
|
// these events are emitted only a file changing is detected
|
||||||
|
func (w *watcher) Events() chan []fsnotify.Event {
|
||||||
|
return w.events
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors get errors occurred during the watching
|
||||||
|
func (w *watcher) Errors() chan error {
|
||||||
|
return w.errors
|
||||||
|
}
|
|
@ -1,221 +1,221 @@
|
||||||
package gaper
|
package watch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func testdataPath(paths ...string) string {
|
||||||
|
return filepath.Join("..", "..", "testdata", filepath.Join(paths...))
|
||||||
|
}
|
||||||
|
|
||||||
func TestWatcherDefaultValues(t *testing.T) {
|
func TestWatcherDefaultValues(t *testing.T) {
|
||||||
pollInterval := 0
|
watchItems := []string{testdataPath("server")}
|
||||||
watchItems := []string{filepath.Join("testdata", "server")}
|
|
||||||
var ignoreItems []string
|
var ignoreItems []string
|
||||||
var extensions []string
|
|
||||||
|
|
||||||
wCfg := WatcherConfig{
|
wCfg := WatcherConfig{
|
||||||
DefaultIgnore: true,
|
DefaultIgnore: true,
|
||||||
PollInterval: pollInterval,
|
Poll: true,
|
||||||
|
PollInterval: 500 * time.Millisecond,
|
||||||
WatchItems: watchItems,
|
WatchItems: watchItems,
|
||||||
IgnoreItems: ignoreItems,
|
IgnoreItems: ignoreItems,
|
||||||
Extensions: extensions,
|
Extensions: []string{"go"},
|
||||||
}
|
}
|
||||||
wt, err := NewWatcher(wCfg)
|
wt, err := NewWatcher(wCfg)
|
||||||
|
|
||||||
expectedPath := "testdata/server"
|
expectedPath := testdataPath("server")
|
||||||
if runtime.GOOS == OSWindows {
|
|
||||||
expectedPath = "testdata\\server"
|
|
||||||
}
|
|
||||||
|
|
||||||
w := wt.(*watcher)
|
w := wt.(*watcher)
|
||||||
assert.Nil(t, err, "wacher error")
|
assert.Nil(t, err, "wacher error")
|
||||||
assert.Equal(t, 500, w.pollInterval)
|
|
||||||
assert.Equal(t, map[string]bool{expectedPath: true}, w.watchItems)
|
assert.Equal(t, map[string]bool{expectedPath: true}, w.watchItems)
|
||||||
assert.Len(t, w.ignoreItems, 0)
|
assert.Len(t, w.ignoreItems, 0)
|
||||||
assert.Equal(t, map[string]bool{".go": true}, w.allowedExtensions)
|
assert.Equal(t, map[string]bool{".go": true}, w.allowedExtensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWatcherGlobPath(t *testing.T) {
|
func TestWatcherGlobPath(t *testing.T) {
|
||||||
pollInterval := 0
|
watchItems := []string{testdataPath("server")}
|
||||||
watchItems := []string{filepath.Join("testdata", "server")}
|
ignoreItems := []string{"../../testdata/**/*_test.go"}
|
||||||
ignoreItems := []string{"./testdata/**/*_test.go"}
|
|
||||||
var extensions []string
|
|
||||||
|
|
||||||
wCfg := WatcherConfig{
|
wCfg := WatcherConfig{
|
||||||
DefaultIgnore: true,
|
DefaultIgnore: true,
|
||||||
PollInterval: pollInterval,
|
Poll: true,
|
||||||
|
PollInterval: 500 * time.Millisecond,
|
||||||
WatchItems: watchItems,
|
WatchItems: watchItems,
|
||||||
IgnoreItems: ignoreItems,
|
IgnoreItems: ignoreItems,
|
||||||
Extensions: extensions,
|
Extensions: []string{"go"},
|
||||||
}
|
}
|
||||||
wt, err := NewWatcher(wCfg)
|
wt, err := NewWatcher(wCfg)
|
||||||
assert.Nil(t, err, "wacher error")
|
assert.Nil(t, err, "wacher error")
|
||||||
w := wt.(*watcher)
|
w := wt.(*watcher)
|
||||||
assert.Equal(t, map[string]bool{"testdata/server/main_test.go": true}, w.ignoreItems)
|
assert.Equal(t, map[string]bool{"../../testdata/server/main_test.go": true}, w.ignoreItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWatcherRemoveOverlapdPaths(t *testing.T) {
|
func TestWatcherRemoveOverlapdPaths(t *testing.T) {
|
||||||
pollInterval := 0
|
watchItems := []string{testdataPath("server")}
|
||||||
watchItems := []string{filepath.Join("testdata", "server")}
|
ignoreItems := []string{"../../testdata/server/**/*", "../../testdata/server"}
|
||||||
ignoreItems := []string{"./testdata/server/**/*", "./testdata/server"}
|
|
||||||
var extensions []string
|
|
||||||
|
|
||||||
wCfg := WatcherConfig{
|
wCfg := WatcherConfig{
|
||||||
DefaultIgnore: true,
|
DefaultIgnore: true,
|
||||||
PollInterval: pollInterval,
|
Poll: true,
|
||||||
|
PollInterval: 500 * time.Millisecond,
|
||||||
WatchItems: watchItems,
|
WatchItems: watchItems,
|
||||||
IgnoreItems: ignoreItems,
|
IgnoreItems: ignoreItems,
|
||||||
Extensions: extensions,
|
Extensions: []string{"go"},
|
||||||
}
|
}
|
||||||
wt, err := NewWatcher(wCfg)
|
wt, err := NewWatcher(wCfg)
|
||||||
assert.Nil(t, err, "wacher error")
|
assert.Nil(t, err, "wacher error")
|
||||||
w := wt.(*watcher)
|
w := wt.(*watcher)
|
||||||
assert.Equal(t, map[string]bool{"./testdata/server": true}, w.ignoreItems)
|
assert.Equal(t, map[string]bool{"../../testdata/server": true}, w.ignoreItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWatcherWatchChange(t *testing.T) {
|
func TestWatcherWatchChange(t *testing.T) {
|
||||||
srvdir := filepath.Join("testdata", "server")
|
testCases := []struct {
|
||||||
hiddendir := filepath.Join("testdata", "hidden-test")
|
name string
|
||||||
|
poll bool
|
||||||
hiddenfile1 := filepath.Join("testdata", ".hidden-file")
|
}{
|
||||||
hiddenfile2 := filepath.Join("testdata", ".hidden-folder", ".gitkeep")
|
{
|
||||||
mainfile := filepath.Join("testdata", "server", "main.go")
|
name: "poll",
|
||||||
testfile := filepath.Join("testdata", "server", "main_test.go")
|
poll: true,
|
||||||
|
},
|
||||||
pollInterval := 0
|
{
|
||||||
watchItems := []string{srvdir, hiddendir}
|
name: "fsnotify",
|
||||||
ignoreItems := []string{testfile}
|
poll: false,
|
||||||
extensions := []string{"go"}
|
},
|
||||||
|
|
||||||
wCfg := WatcherConfig{
|
|
||||||
DefaultIgnore: true,
|
|
||||||
PollInterval: pollInterval,
|
|
||||||
WatchItems: watchItems,
|
|
||||||
IgnoreItems: ignoreItems,
|
|
||||||
Extensions: extensions,
|
|
||||||
}
|
}
|
||||||
w, err := NewWatcher(wCfg)
|
|
||||||
assert.Nil(t, err, "wacher error")
|
|
||||||
|
|
||||||
go w.Watch()
|
for _, tc := range testCases {
|
||||||
time.Sleep(time.Millisecond * 500)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
srvdir := testdataPath("server")
|
||||||
|
hiddendir := testdataPath("hidden-test")
|
||||||
|
|
||||||
// update hidden files and dirs to check builtin hidden ignore is working
|
hiddenfile1 := testdataPath(".hidden-file")
|
||||||
err = os.Chtimes(hiddenfile1, time.Now(), time.Now())
|
hiddenfile2 := testdataPath(".hidden-folder", ".deep-hidden-file")
|
||||||
assert.Nil(t, err, "chtimes error")
|
mainfile := testdataPath("server", "main.go")
|
||||||
|
testfile := testdataPath("server", "main_test.go")
|
||||||
|
|
||||||
err = os.Chtimes(hiddenfile2, time.Now(), time.Now())
|
watchItems := []string{srvdir, hiddendir}
|
||||||
assert.Nil(t, err, "chtimes error")
|
ignoreItems := []string{testfile}
|
||||||
|
|
||||||
// update testfile first to check ignore is working
|
wCfg := WatcherConfig{
|
||||||
err = os.Chtimes(testfile, time.Now(), time.Now())
|
DefaultIgnore: true,
|
||||||
assert.Nil(t, err, "chtimes error")
|
Poll: tc.poll,
|
||||||
|
PollInterval: 500 * time.Millisecond,
|
||||||
|
WatchItems: watchItems,
|
||||||
|
IgnoreItems: ignoreItems,
|
||||||
|
Extensions: []string{"go"},
|
||||||
|
}
|
||||||
|
|
||||||
time.Sleep(time.Millisecond * 500)
|
w, err := NewWatcher(wCfg)
|
||||||
err = os.Chtimes(mainfile, time.Now(), time.Now())
|
assert.Nil(t, err, "wacher error")
|
||||||
assert.Nil(t, err, "chtimes error")
|
|
||||||
|
|
||||||
select {
|
go w.Watch()
|
||||||
case event := <-w.Events():
|
time.Sleep(time.Millisecond * 500)
|
||||||
assert.Equal(t, mainfile, event)
|
|
||||||
case err := <-w.Errors():
|
// update hidden files and dirs to check builtin hidden ignore is working
|
||||||
assert.Nil(t, err, "wacher event error")
|
modifyFile(t, hiddenfile1)
|
||||||
|
modifyFile(t, hiddenfile2)
|
||||||
|
|
||||||
|
// update testfile first to check ignore is working
|
||||||
|
modifyFile(t, testfile)
|
||||||
|
modifyFile(t, mainfile)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case events := <-w.Events():
|
||||||
|
assert.Equal(t, 1, len(events))
|
||||||
|
assert.Equal(t, mainfile, events[0].Name)
|
||||||
|
case err := <-w.Errors():
|
||||||
|
assert.Nil(t, err, "wacher event error")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWatcherIgnoreFile(t *testing.T) {
|
func TestWatcherIgnoreFile(t *testing.T) {
|
||||||
|
vendorPath := filepath.Join("..", "..", "vendor")
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name, file, ignoreFile string
|
name, file, ignoreFile string
|
||||||
defaultIgnore, expectIgnore bool
|
defaultIgnore, expectIgnore bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "with default ignore enabled it ignores vendor folder",
|
name: "with default ignore enabled it ignores vendor folder",
|
||||||
file: "vendor",
|
file: vendorPath,
|
||||||
defaultIgnore: true,
|
defaultIgnore: true,
|
||||||
expectIgnore: true,
|
expectIgnore: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "without default ignore enabled it does not ignore vendor folder",
|
name: "without default ignore enabled it does not ignore vendor folder",
|
||||||
file: "vendor",
|
file: vendorPath,
|
||||||
defaultIgnore: false,
|
defaultIgnore: false,
|
||||||
expectIgnore: false,
|
expectIgnore: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with default ignore enabled it ignores test file",
|
name: "with default ignore enabled it ignores test file",
|
||||||
file: filepath.Join("testdata", "server", "main_test.go"),
|
file: testdataPath("server", "main_test.go"),
|
||||||
defaultIgnore: true,
|
defaultIgnore: true,
|
||||||
expectIgnore: true,
|
expectIgnore: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with default ignore enabled it does no ignore non test files which have test in the name",
|
name: "with default ignore enabled it does not ignore non test files which have test in the name",
|
||||||
file: filepath.Join("testdata", "ignore-test-name.txt"),
|
file: testdataPath("ignore-test-name.txt"),
|
||||||
defaultIgnore: true,
|
defaultIgnore: true,
|
||||||
expectIgnore: false,
|
expectIgnore: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "without default ignore enabled it does not ignore test file",
|
name: "without default ignore enabled it does not ignore test file",
|
||||||
file: filepath.Join("testdata", "server", "main_test.go"),
|
file: testdataPath("server", "main_test.go"),
|
||||||
defaultIgnore: false,
|
defaultIgnore: false,
|
||||||
expectIgnore: false,
|
expectIgnore: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with default ignore enabled it ignores ignored items",
|
name: "with default ignore enabled it ignores ignored items",
|
||||||
file: filepath.Join("testdata", "server", "main.go"),
|
file: testdataPath("server", "main.go"),
|
||||||
ignoreFile: filepath.Join("testdata", "server", "main.go"),
|
ignoreFile: testdataPath("server", "main.go"),
|
||||||
defaultIgnore: true,
|
defaultIgnore: true,
|
||||||
expectIgnore: true,
|
expectIgnore: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "without default ignore enabled it ignores ignored items",
|
name: "without default ignore enabled it ignores ignored items",
|
||||||
file: filepath.Join("testdata", "server", "main.go"),
|
file: testdataPath("server", "main.go"),
|
||||||
ignoreFile: filepath.Join("testdata", "server", "main.go"),
|
ignoreFile: testdataPath("server", "main.go"),
|
||||||
defaultIgnore: false,
|
defaultIgnore: false,
|
||||||
expectIgnore: true,
|
expectIgnore: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// create vendor folder for testing
|
// create vendor folder for testing
|
||||||
if err := os.MkdirAll("vendor", os.ModePerm); err != nil {
|
if err := os.MkdirAll(vendorPath, os.ModePerm); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer os.RemoveAll(vendorPath)
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
srvdir := "."
|
srvdir := filepath.Join("..", "..")
|
||||||
|
|
||||||
watchItems := []string{srvdir}
|
watchItems := []string{srvdir}
|
||||||
ignoreItems := []string{}
|
ignoreItems := []string{}
|
||||||
if len(tc.ignoreFile) > 0 {
|
if len(tc.ignoreFile) > 0 {
|
||||||
ignoreItems = append(ignoreItems, tc.ignoreFile)
|
ignoreItems = append(ignoreItems, tc.ignoreFile)
|
||||||
}
|
}
|
||||||
extensions := []string{"go"}
|
|
||||||
|
|
||||||
wCfg := WatcherConfig{
|
wCfg := WatcherConfig{
|
||||||
DefaultIgnore: tc.defaultIgnore,
|
DefaultIgnore: tc.defaultIgnore,
|
||||||
WatchItems: watchItems,
|
WatchItems: watchItems,
|
||||||
IgnoreItems: ignoreItems,
|
IgnoreItems: ignoreItems,
|
||||||
Extensions: extensions,
|
Extensions: []string{"go", "txt"},
|
||||||
}
|
}
|
||||||
w, err := NewWatcher(wCfg)
|
w, err := NewWatcher(wCfg)
|
||||||
assert.Nil(t, err, "wacher error")
|
assert.Nil(t, err, "wacher error")
|
||||||
|
|
||||||
wt := w.(*watcher)
|
wt := w.(*watcher)
|
||||||
|
|
||||||
filePath := tc.file
|
assert.Equal(t, tc.expectIgnore, wt.ignoreFile(tc.file))
|
||||||
file, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInfo, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, tc.expectIgnore, wt.ignoreFile(filePath, fileInfo))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,3 +259,12 @@ func TestWatcherResolvePaths(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func modifyFile(t *testing.T, filename string) {
|
||||||
|
f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
_, err = f.WriteString("\n")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package testdata
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -78,9 +79,9 @@ type MockWacther struct {
|
||||||
func (m *MockWacther) Watch() {}
|
func (m *MockWacther) Watch() {}
|
||||||
|
|
||||||
// Events ...
|
// Events ...
|
||||||
func (m *MockWacther) Events() chan string {
|
func (m *MockWacther) Events() chan []fsnotify.Event {
|
||||||
args := m.Called()
|
args := m.Called()
|
||||||
return args.Get(0).(chan string)
|
return args.Get(0).(chan []fsnotify.Event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errors ...
|
// Errors ...
|
||||||
|
|
|
@ -21,3 +21,19 @@ func main() {
|
||||||
log.Println("Starting server: Version", Version)
|
log.Println("Starting server: Version", Version)
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,19 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
// an empty test file just to check during tests we can ignore _test.go files
|
// an empty test file just to check during tests we can ignore _test.go files
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
257
watcher.go
257
watcher.go
|
@ -1,257 +0,0 @@
|
||||||
package gaper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
zglob "github.com/mattn/go-zglob"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Watcher is a interface for the watch process
|
|
||||||
type Watcher interface {
|
|
||||||
Watch()
|
|
||||||
Errors() chan error
|
|
||||||
Events() chan string
|
|
||||||
}
|
|
||||||
|
|
||||||
// watcher is a interface for the watch process
|
|
||||||
type watcher struct {
|
|
||||||
defaultIgnore bool
|
|
||||||
pollInterval int
|
|
||||||
watchItems map[string]bool
|
|
||||||
ignoreItems map[string]bool
|
|
||||||
allowedExtensions map[string]bool
|
|
||||||
events chan string
|
|
||||||
errors chan error
|
|
||||||
}
|
|
||||||
|
|
||||||
// WatcherConfig defines the settings available for the watcher
|
|
||||||
type WatcherConfig struct {
|
|
||||||
DefaultIgnore bool
|
|
||||||
PollInterval int
|
|
||||||
WatchItems []string
|
|
||||||
IgnoreItems []string
|
|
||||||
Extensions []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWatcher creates a new watcher
|
|
||||||
func NewWatcher(cfg WatcherConfig) (Watcher, error) {
|
|
||||||
if cfg.PollInterval == 0 {
|
|
||||||
cfg.PollInterval = DefaultPoolInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.Extensions) == 0 {
|
|
||||||
cfg.Extensions = DefaultExtensions
|
|
||||||
}
|
|
||||||
|
|
||||||
allowedExts := make(map[string]bool)
|
|
||||||
for _, ext := range cfg.Extensions {
|
|
||||||
allowedExts["."+ext] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
watchPaths, err := resolvePaths(cfg.WatchItems, allowedExts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ignorePaths, err := resolvePaths(cfg.IgnoreItems, allowedExts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("Resolved watch paths: %v", watchPaths)
|
|
||||||
logger.Debugf("Resolved ignore paths: %v", ignorePaths)
|
|
||||||
return &watcher{
|
|
||||||
events: make(chan string),
|
|
||||||
errors: make(chan error),
|
|
||||||
defaultIgnore: cfg.DefaultIgnore,
|
|
||||||
pollInterval: cfg.PollInterval,
|
|
||||||
watchItems: watchPaths,
|
|
||||||
ignoreItems: ignorePaths,
|
|
||||||
allowedExtensions: allowedExts,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var startTime = time.Now()
|
|
||||||
var errDetectedChange = errors.New("done")
|
|
||||||
|
|
||||||
// Watch starts watching for file changes
|
|
||||||
func (w *watcher) Watch() {
|
|
||||||
for {
|
|
||||||
for watchPath := range w.watchItems {
|
|
||||||
fileChanged, err := w.scanChange(watchPath)
|
|
||||||
if err != nil {
|
|
||||||
w.errors <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileChanged != "" {
|
|
||||||
w.events <- fileChanged
|
|
||||||
startTime = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Duration(w.pollInterval) * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Events get events occurred during the watching
|
|
||||||
// these events are emitted only a file changing is detected
|
|
||||||
func (w *watcher) Events() chan string {
|
|
||||||
return w.events
|
|
||||||
}
|
|
||||||
|
|
||||||
// Errors get errors occurred during the watching
|
|
||||||
func (w *watcher) Errors() chan error {
|
|
||||||
return w.errors
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *watcher) scanChange(watchPath string) (string, error) {
|
|
||||||
logger.Debug("Watching ", watchPath)
|
|
||||||
|
|
||||||
var fileChanged string
|
|
||||||
|
|
||||||
err := filepath.Walk(watchPath, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
// Ignore attempt to acess go temporary unmask
|
|
||||||
if strings.Contains(err.Error(), "-go-tmp-umask") {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("couldn't walk to path \"%s\": %v", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.ignoreFile(path, info) {
|
|
||||||
return skipFile(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
ext := filepath.Ext(path)
|
|
||||||
if _, ok := w.allowedExtensions[ext]; ok && info.ModTime().After(startTime) {
|
|
||||||
fileChanged = path
|
|
||||||
return errDetectedChange
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil && err != errDetectedChange {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileChanged, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *watcher) ignoreFile(path string, info os.FileInfo) bool {
|
|
||||||
// if a file has been deleted after gaper was watching it
|
|
||||||
// info will be nil in the other iterations
|
|
||||||
if info == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if preset ignore is enabled
|
|
||||||
if w.defaultIgnore {
|
|
||||||
// check for hidden files and directories
|
|
||||||
if name := info.Name(); name[0] == '.' && name != "." {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if it is a Go testing file
|
|
||||||
if strings.HasSuffix(path, "_test.go") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if it is the vendor folder
|
|
||||||
if info.IsDir() && info.Name() == "vendor" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ignored := w.ignoreItems[path]; ignored {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolvePaths(paths []string, extensions map[string]bool) (map[string]bool, error) {
|
|
||||||
result := map[string]bool{}
|
|
||||||
|
|
||||||
for _, path := range paths {
|
|
||||||
matches := []string{path}
|
|
||||||
|
|
||||||
isGlob := strings.Contains(path, "*")
|
|
||||||
if isGlob {
|
|
||||||
var err error
|
|
||||||
matches, err = zglob.Glob(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("couldn't resolve glob path \"%s\": %v", path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, match := range matches {
|
|
||||||
// ignore existing files that don't match the allowed extensions
|
|
||||||
if f, err := os.Stat(match); !os.IsNotExist(err) && !f.IsDir() {
|
|
||||||
if ext := filepath.Ext(match); ext != "" {
|
|
||||||
if _, ok := extensions[ext]; !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := result[match]; !ok {
|
|
||||||
result[match] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeOverlappedPaths(result)
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove overlapped paths so it makes the scan for changes later faster and simpler
|
|
||||||
func removeOverlappedPaths(mapPaths map[string]bool) {
|
|
||||||
startDot := regexp.MustCompile(`^\./`)
|
|
||||||
|
|
||||||
for p1 := range mapPaths {
|
|
||||||
p1 = startDot.ReplaceAllString(p1, "")
|
|
||||||
|
|
||||||
// skip to next item if this path has already been checked
|
|
||||||
if v, ok := mapPaths[p1]; ok && !v {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for p2 := range mapPaths {
|
|
||||||
p2 = startDot.ReplaceAllString(p2, "")
|
|
||||||
|
|
||||||
if p1 == p2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(p2, p1) {
|
|
||||||
mapPaths[p2] = false
|
|
||||||
} else if strings.HasPrefix(p1, p2) {
|
|
||||||
mapPaths[p1] = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup path list
|
|
||||||
for p := range mapPaths {
|
|
||||||
if !mapPaths[p] {
|
|
||||||
delete(mapPaths, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func skipFile(info os.FileInfo) error {
|
|
||||||
if info.IsDir() {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
Loading…
Reference in New Issue