1
0
mirror of https://github.com/go-task/task.git synced 2025-04-13 11:50:50 +02:00
task/watch.go
2025-03-22 20:06:16 -03:00

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
}