472 lines
9.5 KiB
Go
472 lines
9.5 KiB
Go
|
package runner
|
||
|
|
||
|
import (
|
||
|
"io"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"path/filepath"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"github.com/fsnotify/fsnotify"
|
||
|
)
|
||
|
|
||
|
// Engine ...
|
||
|
type Engine struct {
|
||
|
config *config
|
||
|
logger *logger
|
||
|
watcher *fsnotify.Watcher
|
||
|
debugMode bool
|
||
|
|
||
|
eventCh chan string
|
||
|
watcherStopCh chan bool
|
||
|
buildRunCh chan bool
|
||
|
buildRunStopCh chan bool
|
||
|
binStopCh chan bool
|
||
|
exitCh chan bool
|
||
|
|
||
|
mu sync.RWMutex
|
||
|
binRunning bool
|
||
|
watchers uint
|
||
|
fileChecksums *checksumMap
|
||
|
|
||
|
ll sync.Mutex // lock for logger
|
||
|
}
|
||
|
|
||
|
// NewEngine ...
|
||
|
func NewEngine(cfgPath string, debugMode bool) (*Engine, error) {
|
||
|
var err error
|
||
|
cfg, err := initConfig(cfgPath)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
logger := newLogger(cfg)
|
||
|
watcher, err := fsnotify.NewWatcher()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
e := Engine{
|
||
|
config: cfg,
|
||
|
logger: logger,
|
||
|
watcher: watcher,
|
||
|
debugMode: debugMode,
|
||
|
eventCh: make(chan string, 1000),
|
||
|
watcherStopCh: make(chan bool, 10),
|
||
|
buildRunCh: make(chan bool, 1),
|
||
|
buildRunStopCh: make(chan bool, 1),
|
||
|
binStopCh: make(chan bool),
|
||
|
exitCh: make(chan bool),
|
||
|
binRunning: false,
|
||
|
watchers: 0,
|
||
|
}
|
||
|
|
||
|
if cfg.Build.ExcludeUnchanged {
|
||
|
e.fileChecksums = &checksumMap{m: make(map[string]string)}
|
||
|
}
|
||
|
|
||
|
return &e, nil
|
||
|
}
|
||
|
|
||
|
// Run run run
|
||
|
func (e *Engine) Run() {
|
||
|
if len(os.Args) > 1 && os.Args[1] == "init" {
|
||
|
writeDefaultConfig()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
e.mainDebug("CWD: %s", e.config.Root)
|
||
|
|
||
|
var err error
|
||
|
if err = e.checkRunEnv(); err != nil {
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
if err = e.watching(e.config.Root); err != nil {
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
|
||
|
e.start()
|
||
|
e.cleanup()
|
||
|
}
|
||
|
|
||
|
func (e *Engine) checkRunEnv() error {
|
||
|
p := e.config.tmpPath()
|
||
|
if _, err := os.Stat(p); os.IsNotExist(err) {
|
||
|
e.runnerLog("mkdir %s", p)
|
||
|
if err := os.Mkdir(p, 0755); err != nil {
|
||
|
e.runnerLog("failed to mkdir, error: %s", err.Error())
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (e *Engine) watching(root string) error {
|
||
|
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||
|
// NOTE: path is absolute
|
||
|
if info != nil && !info.IsDir() {
|
||
|
return nil
|
||
|
}
|
||
|
// exclude tmp dir
|
||
|
if e.isTmpDir(path) {
|
||
|
e.watcherLog("!exclude %s", e.config.rel(path))
|
||
|
return filepath.SkipDir
|
||
|
}
|
||
|
// exclude hidden directories like .git, .idea, etc.
|
||
|
if isHiddenDirectory(path) {
|
||
|
return filepath.SkipDir
|
||
|
}
|
||
|
// exclude user specified directories
|
||
|
if e.isExcludeDir(path) {
|
||
|
e.watcherLog("!exclude %s", e.config.rel(path))
|
||
|
return filepath.SkipDir
|
||
|
}
|
||
|
isIn, walkDir := e.checkIncludeDir(path)
|
||
|
if !walkDir {
|
||
|
e.watcherLog("!exclude %s", e.config.rel(path))
|
||
|
return filepath.SkipDir
|
||
|
}
|
||
|
if isIn {
|
||
|
return e.watchDir(path)
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// cacheFileChecksums calculates and stores checksums for each non-excluded file it finds from root.
|
||
|
func (e *Engine) cacheFileChecksums(root string) error {
|
||
|
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||
|
if err != nil {
|
||
|
if info.IsDir() {
|
||
|
return filepath.SkipDir
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if !info.Mode().IsRegular() {
|
||
|
if e.isTmpDir(path) || isHiddenDirectory(path) || e.isExcludeDir(path) {
|
||
|
e.watcherDebug("!exclude checksum %s", e.config.rel(path))
|
||
|
return filepath.SkipDir
|
||
|
}
|
||
|
|
||
|
// Follow symbolic link
|
||
|
if e.config.Build.FollowSymlink && (info.Mode()&os.ModeSymlink) > 0 {
|
||
|
link, err := filepath.EvalSymlinks(path)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
linkInfo, err := os.Stat(link)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if linkInfo.IsDir() {
|
||
|
err = e.watchDir(link)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if e.isExcludeFile(path) || !e.isIncludeExt(path) {
|
||
|
e.watcherDebug("!exclude checksum %s", e.config.rel(path))
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
excludeRegex, err := e.isExcludeRegex(path)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if excludeRegex {
|
||
|
e.watcherDebug("!exclude checksum %s", e.config.rel(path))
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// update the checksum cache for the current file
|
||
|
_ = e.isModified(path)
|
||
|
|
||
|
return nil
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (e *Engine) watchDir(path string) error {
|
||
|
if err := e.watcher.Add(path); err != nil {
|
||
|
e.watcherLog("failed to watching %s, error: %s", path, err.Error())
|
||
|
return err
|
||
|
}
|
||
|
e.watcherLog("watching %s", e.config.rel(path))
|
||
|
|
||
|
go func() {
|
||
|
e.withLock(func() {
|
||
|
e.watchers++
|
||
|
})
|
||
|
defer func() {
|
||
|
e.withLock(func() {
|
||
|
e.watchers--
|
||
|
})
|
||
|
}()
|
||
|
|
||
|
if e.config.Build.ExcludeUnchanged {
|
||
|
err := e.cacheFileChecksums(path)
|
||
|
if err != nil {
|
||
|
e.watcherLog("error building checksum cache: %v", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for {
|
||
|
select {
|
||
|
case <-e.watcherStopCh:
|
||
|
return
|
||
|
case ev := <-e.watcher.Events:
|
||
|
e.mainDebug("event: %+v", ev)
|
||
|
if !validEvent(ev) {
|
||
|
break
|
||
|
}
|
||
|
if isDir(ev.Name) {
|
||
|
e.watchNewDir(ev.Name, removeEvent(ev))
|
||
|
break
|
||
|
}
|
||
|
if e.isExcludeFile(ev.Name) {
|
||
|
break
|
||
|
}
|
||
|
excludeRegex, _ := e.isExcludeRegex(ev.Name)
|
||
|
if excludeRegex {
|
||
|
break
|
||
|
}
|
||
|
if !e.isIncludeExt(ev.Name) {
|
||
|
break
|
||
|
}
|
||
|
e.watcherDebug("%s has changed", e.config.rel(ev.Name))
|
||
|
e.eventCh <- ev.Name
|
||
|
case err := <-e.watcher.Errors:
|
||
|
e.watcherLog("error: %s", err.Error())
|
||
|
}
|
||
|
}
|
||
|
}()
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (e *Engine) watchNewDir(dir string, removeDir bool) {
|
||
|
if e.isTmpDir(dir) {
|
||
|
return
|
||
|
}
|
||
|
if isHiddenDirectory(dir) || e.isExcludeDir(dir) {
|
||
|
e.watcherLog("!exclude %s", e.config.rel(dir))
|
||
|
return
|
||
|
}
|
||
|
if removeDir {
|
||
|
if err := e.watcher.Remove(dir); err != nil {
|
||
|
e.watcherLog("failed to stop watching %s, error: %s", dir, err.Error())
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
go func(dir string) {
|
||
|
if err := e.watching(dir); err != nil {
|
||
|
e.watcherLog("failed to watching %s, error: %s", dir, err.Error())
|
||
|
}
|
||
|
}(dir)
|
||
|
}
|
||
|
|
||
|
func (e *Engine) isModified(filename string) bool {
|
||
|
newChecksum, err := fileChecksum(filename)
|
||
|
if err != nil {
|
||
|
e.watcherDebug("can't determine if file was changed: %v - assuming it did without updating cache", err)
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
if e.fileChecksums.updateFileChecksum(filename, newChecksum) {
|
||
|
e.watcherDebug("stored checksum for %s: %s", e.config.rel(filename), newChecksum)
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Endless loop and never return
|
||
|
func (e *Engine) start() {
|
||
|
firstRunCh := make(chan bool, 1)
|
||
|
firstRunCh <- true
|
||
|
|
||
|
for {
|
||
|
var filename string
|
||
|
|
||
|
select {
|
||
|
case <-e.exitCh:
|
||
|
return
|
||
|
case filename = <-e.eventCh:
|
||
|
time.Sleep(e.config.buildDelay())
|
||
|
e.flushEvents()
|
||
|
if !e.isIncludeExt(filename) {
|
||
|
continue
|
||
|
}
|
||
|
if e.config.Build.ExcludeUnchanged {
|
||
|
if !e.isModified(filename) {
|
||
|
e.mainLog("skipping %s because contents unchanged", e.config.rel(filename))
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
e.mainLog("%s has changed", e.config.rel(filename))
|
||
|
case <-firstRunCh:
|
||
|
// go down
|
||
|
break
|
||
|
}
|
||
|
|
||
|
select {
|
||
|
case <-e.buildRunCh:
|
||
|
e.buildRunStopCh <- true
|
||
|
default:
|
||
|
}
|
||
|
e.withLock(func() {
|
||
|
if e.binRunning {
|
||
|
e.binStopCh <- true
|
||
|
}
|
||
|
})
|
||
|
go e.buildRun()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (e *Engine) buildRun() {
|
||
|
e.buildRunCh <- true
|
||
|
defer func() {
|
||
|
<-e.buildRunCh
|
||
|
}()
|
||
|
|
||
|
select {
|
||
|
case <-e.buildRunStopCh:
|
||
|
return
|
||
|
default:
|
||
|
}
|
||
|
var err error
|
||
|
if err = e.building(); err != nil {
|
||
|
e.buildLog("failed to build, error: %s", err.Error())
|
||
|
_ = e.writeBuildErrorLog(err.Error())
|
||
|
if e.config.Build.StopOnError {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
select {
|
||
|
case <-e.buildRunStopCh:
|
||
|
return
|
||
|
default:
|
||
|
}
|
||
|
if err = e.runBin(); err != nil {
|
||
|
e.runnerLog("failed to run, error: %s", err.Error())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (e *Engine) flushEvents() {
|
||
|
for {
|
||
|
select {
|
||
|
case <-e.eventCh:
|
||
|
e.mainDebug("flushing events")
|
||
|
default:
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (e *Engine) building() error {
|
||
|
var err error
|
||
|
e.buildLog("building...")
|
||
|
cmd, stdout, stderr, err := e.startCmd(e.config.Build.Cmd)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer func() {
|
||
|
stdout.Close()
|
||
|
stderr.Close()
|
||
|
}()
|
||
|
_, _ = io.Copy(os.Stdout, stdout)
|
||
|
_, _ = io.Copy(os.Stderr, stderr)
|
||
|
// wait for building
|
||
|
err = cmd.Wait()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (e *Engine) runBin() error {
|
||
|
var err error
|
||
|
e.runnerLog("running...")
|
||
|
cmd, stdout, stderr, err := e.startCmd(e.config.Build.Bin)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
e.withLock(func() {
|
||
|
e.binRunning = true
|
||
|
})
|
||
|
|
||
|
go func() {
|
||
|
_, _ = io.Copy(os.Stdout, stdout)
|
||
|
_, _ = io.Copy(os.Stderr, stderr)
|
||
|
}()
|
||
|
|
||
|
go func(cmd *exec.Cmd, stdout io.ReadCloser, stderr io.ReadCloser) {
|
||
|
<-e.binStopCh
|
||
|
e.mainDebug("trying to kill cmd %+v", cmd.Args)
|
||
|
defer func() {
|
||
|
stdout.Close()
|
||
|
stderr.Close()
|
||
|
}()
|
||
|
|
||
|
var err error
|
||
|
pid, err := e.killCmd(cmd)
|
||
|
if err != nil {
|
||
|
e.mainDebug("failed to kill PID %d, error: %s", pid, err.Error())
|
||
|
if cmd.ProcessState != nil && !cmd.ProcessState.Exited() {
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
} else {
|
||
|
e.mainDebug("cmd killed, pid: %d", pid)
|
||
|
}
|
||
|
e.withLock(func() {
|
||
|
e.binRunning = false
|
||
|
})
|
||
|
cmdBinPath := cmdPath(e.config.rel(e.config.binPath()))
|
||
|
if _, err = os.Stat(cmdBinPath); os.IsNotExist(err) {
|
||
|
return
|
||
|
}
|
||
|
if err = os.Remove(cmdBinPath); err != nil {
|
||
|
e.mainLog("failed to remove %s, error: %s", e.config.rel(e.config.binPath()), err)
|
||
|
}
|
||
|
}(cmd, stdout, stderr)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (e *Engine) cleanup() {
|
||
|
e.mainLog("cleaning...")
|
||
|
defer e.mainLog("see you again~")
|
||
|
|
||
|
e.withLock(func() {
|
||
|
if e.binRunning {
|
||
|
e.binStopCh <- true
|
||
|
}
|
||
|
})
|
||
|
|
||
|
e.withLock(func() {
|
||
|
for i := 0; i < int(e.watchers); i++ {
|
||
|
e.watcherStopCh <- true
|
||
|
}
|
||
|
})
|
||
|
|
||
|
var err error
|
||
|
if err = e.watcher.Close(); err != nil {
|
||
|
e.mainLog("failed to close watcher, error: %s", err.Error())
|
||
|
}
|
||
|
|
||
|
if e.config.Misc.CleanOnExit {
|
||
|
e.mainLog("deleting %s", e.config.tmpPath())
|
||
|
if err = os.RemoveAll(e.config.tmpPath()); err != nil {
|
||
|
e.mainLog("failed to delete tmp dir, err: %+v", err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Stop the air
|
||
|
func (e *Engine) Stop() {
|
||
|
e.exitCh <- true
|
||
|
}
|