mirror of
https://github.com/go-task/task.git
synced 2025-04-13 11:50:50 +02:00
230 lines
5.0 KiB
Go
230 lines
5.0 KiB
Go
package task
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/puzpuzpuz/xsync/v3"
|
|
|
|
"github.com/go-task/task/v3/errors"
|
|
"github.com/go-task/task/v3/internal/filepathext"
|
|
"github.com/go-task/task/v3/internal/fingerprint"
|
|
"github.com/go-task/task/v3/internal/fsnotifyext"
|
|
"github.com/go-task/task/v3/internal/logger"
|
|
)
|
|
|
|
const defaultWaitTime = 100 * time.Millisecond
|
|
|
|
// watchTasks start watching the given tasks
|
|
func (e *Executor) watchTasks(calls ...*Call) error {
|
|
tasks := make([]string, len(calls))
|
|
for i, c := range calls {
|
|
tasks[i] = c.Task
|
|
}
|
|
|
|
e.Logger.Errf(logger.Green, "task: Started watching for tasks: %s\n", strings.Join(tasks, ", "))
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
for _, c := range calls {
|
|
c := c
|
|
go func() {
|
|
err := e.RunTask(ctx, c)
|
|
if err == nil {
|
|
e.Logger.Errf(logger.Green, "task: task \"%s\" finished running\n", c.Task)
|
|
} else if !isContextError(err) {
|
|
e.Logger.Errf(logger.Red, "%v\n", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
var waitTime time.Duration
|
|
switch {
|
|
case e.Interval != 0:
|
|
waitTime = e.Interval
|
|
case e.Taskfile.Interval != 0:
|
|
waitTime = e.Taskfile.Interval
|
|
default:
|
|
waitTime = defaultWaitTime
|
|
}
|
|
|
|
w, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
cancel()
|
|
return err
|
|
}
|
|
defer w.Close()
|
|
|
|
deduper := fsnotifyext.NewDeduper(w, waitTime)
|
|
eventsChan := deduper.GetChan()
|
|
|
|
closeOnInterrupt(w)
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case event, ok := <-eventsChan:
|
|
switch {
|
|
case !ok:
|
|
cancel()
|
|
return
|
|
case event.Op == fsnotify.Chmod:
|
|
continue
|
|
}
|
|
e.Logger.VerboseErrf(logger.Magenta, "task: received watch event: %v\n", event)
|
|
|
|
cancel()
|
|
ctx, cancel = context.WithCancel(context.Background())
|
|
|
|
e.Compiler.ResetCache()
|
|
|
|
for _, c := range calls {
|
|
c := c
|
|
go func() {
|
|
t, err := e.GetTask(c)
|
|
if err != nil {
|
|
e.Logger.Errf(logger.Red, "%v\n", err)
|
|
return
|
|
}
|
|
baseDir := filepathext.SmartJoin(e.Dir, t.Dir)
|
|
files, err := fingerprint.Globs(baseDir, t.Sources)
|
|
if err != nil {
|
|
e.Logger.Errf(logger.Red, "%v\n", err)
|
|
return
|
|
}
|
|
if !event.Has(fsnotify.Remove) && !slices.Contains(files, event.Name) {
|
|
relPath, _ := filepath.Rel(baseDir, event.Name)
|
|
e.Logger.VerboseErrf(logger.Magenta, "task: skipped for file not in sources: %s\n", relPath)
|
|
return
|
|
}
|
|
err = e.RunTask(ctx, c)
|
|
if err == nil {
|
|
e.Logger.Errf(logger.Green, "task: task \"%s\" finished running\n", c.Task)
|
|
} else if !isContextError(err) {
|
|
e.Logger.Errf(logger.Red, "%v\n", err)
|
|
}
|
|
}()
|
|
}
|
|
case err, ok := <-w.Errors:
|
|
switch {
|
|
case !ok:
|
|
cancel()
|
|
return
|
|
default:
|
|
e.Logger.Errf(logger.Red, "%v\n", err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
e.watchedDirs = xsync.NewMapOf[string, bool]()
|
|
|
|
go func() {
|
|
// NOTE(@andreynering): New files can be created in directories
|
|
// that were previously empty, so we need to check for new dirs
|
|
// from time to time.
|
|
for {
|
|
if err := e.registerWatchedDirs(w, calls...); err != nil {
|
|
e.Logger.Errf(logger.Red, "%v\n", err)
|
|
}
|
|
time.Sleep(5 * time.Second)
|
|
}
|
|
}()
|
|
|
|
<-make(chan struct{})
|
|
return nil
|
|
}
|
|
|
|
func isContextError(err error) bool {
|
|
if taskRunErr, ok := err.(*errors.TaskRunError); ok {
|
|
err = taskRunErr.Err
|
|
}
|
|
|
|
return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
|
|
}
|
|
|
|
func closeOnInterrupt(w *fsnotify.Watcher) {
|
|
ch := make(chan os.Signal, 1)
|
|
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
|
|
go func() {
|
|
<-ch
|
|
w.Close()
|
|
os.Exit(0)
|
|
}()
|
|
}
|
|
|
|
func (e *Executor) registerWatchedDirs(w *fsnotify.Watcher, calls ...*Call) error {
|
|
var registerTaskDirs func(*Call) error
|
|
registerTaskDirs = func(c *Call) error {
|
|
task, err := e.CompiledTask(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, d := range task.Deps {
|
|
if err := registerTaskDirs(&Call{Task: d.Task, Vars: d.Vars}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, c := range task.Cmds {
|
|
if c.Task != "" {
|
|
if err := registerTaskDirs(&Call{Task: c.Task, Vars: c.Vars}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
files, err := fingerprint.Globs(task.Dir, task.Sources)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, f := range files {
|
|
d := filepath.Dir(f)
|
|
if isSet, ok := e.watchedDirs.Load(d); ok && isSet {
|
|
continue
|
|
}
|
|
if ShouldIgnoreFile(d) {
|
|
continue
|
|
}
|
|
if err := w.Add(d); err != nil {
|
|
return err
|
|
}
|
|
e.watchedDirs.Store(d, true)
|
|
relPath, _ := filepath.Rel(e.Dir, d)
|
|
w.Events <- fsnotify.Event{Name: f, Op: fsnotify.Create}
|
|
e.Logger.VerboseOutf(logger.Green, "task: watching new dir: %v\n", relPath)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for _, c := range calls {
|
|
if err := registerTaskDirs(c); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ShouldIgnoreFile(path string) bool {
|
|
ignorePaths := []string{
|
|
"/.task",
|
|
"/.git",
|
|
"/.hg",
|
|
"/node_modules",
|
|
}
|
|
for _, p := range ignorePaths {
|
|
if strings.Contains(path, fmt.Sprintf("%s/", p)) || strings.HasSuffix(path, p) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|