From 85f50543fbd3a1303a5c0db9332399caf5d690fa Mon Sep 17 00:00:00 2001 From: Sergio Date: Sun, 8 Mar 2026 13:31:58 -0700 Subject: [PATCH 1/3] fix(watch): restart watcher on SIGHUP --- watch.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch.go b/watch.go index 8e7f7ccf7d..b67d004fa9 100644 --- a/watch.go +++ b/watch.go @@ -154,7 +154,7 @@ func isContextError(err error) bool { func closeOnInterrupt(w *fsnotify.Watcher) { ch := make(chan os.Signal, 1) - signal.Notify(ch, os.Interrupt, syscall.SIGTERM) + signal.Notify(ch, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) go func() { <-ch w.Close() From 21afafb8e5c7ab09c35e2e6c93c69128584e2d86 Mon Sep 17 00:00:00 2001 From: sergiochan Date: Thu, 19 Mar 2026 09:07:58 -0700 Subject: [PATCH 2/3] fix(watch): stop signal delivery before closing watcher --- watch.go | 1 + 1 file changed, 1 insertion(+) diff --git a/watch.go b/watch.go index b67d004fa9..6dec18b97f 100644 --- a/watch.go +++ b/watch.go @@ -157,6 +157,7 @@ func closeOnInterrupt(w *fsnotify.Watcher) { signal.Notify(ch, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) go func() { <-ch + signal.Stop(ch) w.Close() os.Exit(0) }() From 59970d9d711fc7c15478b18f2a9cdad8fb8636fd Mon Sep 17 00:00:00 2001 From: sergiochan Date: Sun, 22 Mar 2026 11:09:34 -0700 Subject: [PATCH 3/3] test(watch): add subprocess SIGHUP watcher regression test --- watch_interrupt_test.go | 95 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 watch_interrupt_test.go diff --git a/watch_interrupt_test.go b/watch_interrupt_test.go new file mode 100644 index 0000000000..2a62fb70fe --- /dev/null +++ b/watch_interrupt_test.go @@ -0,0 +1,95 @@ +//go:build watch && !windows +// +build watch,!windows + +package task_test + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "testing" + "time" +) + +func TestWatchProcessExitsOnSIGHUP(t *testing.T) { + taskPath, err := findTaskBinaryForWatchTest() + if err != nil { + t.Skipf("skipping watcher signal test: %v", err) + } + + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "src"), 0o755); err != nil { + t.Fatalf("mkdir src: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "src", "a.txt"), []byte("x"), 0o644); err != nil { + t.Fatalf("write seed source: %v", err) + } + + taskfile := `version: '3' + +tasks: + default: + watch: true + sources: + - src/**/* + cmds: + - echo "watch run"` + if err := os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte(taskfile), 0o644); err != nil { + t.Fatalf("write taskfile: %v", err) + } + + var out bytes.Buffer + sut := exec.Command(taskPath, "--watch", "default") + sut.Stdout = &out + sut.Stderr = &out + sut.Dir = dir + if err := sut.Start(); err != nil { + t.Fatalf("start task watcher process: %v", err) + } + + if err := waitForOutputContains(&out, "Started watching for tasks: default", 5*time.Second); err != nil { + _ = sut.Process.Kill() + _, _ = sut.Process.Wait() + t.Fatalf("watch process did not reach ready state: %v\noutput:\n%s", err, out.String()) + } + + if err := sut.Process.Signal(syscall.SIGHUP); err != nil { + _ = sut.Process.Kill() + _, _ = sut.Process.Wait() + t.Fatalf("send SIGHUP to watch process: %v", err) + } + + done := make(chan error, 1) + go func() { done <- sut.Wait() }() + + select { + case err := <-done: + if err != nil { + t.Fatalf("watch process exit after SIGHUP: %v\noutput:\n%s", err, out.String()) + } + case <-time.After(5 * time.Second): + _ = sut.Process.Kill() + t.Fatalf("watch process did not exit after SIGHUP\noutput:\n%s", out.String()) + } +} + +func findTaskBinaryForWatchTest() (string, error) { + if info, err := os.Stat("./bin/task"); err == nil { + return info.Name(), nil + } + return exec.LookPath("task") +} + +func waitForOutputContains(out *bytes.Buffer, needle string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if strings.Contains(out.String(), needle) { + return nil + } + time.Sleep(25 * time.Millisecond) + } + return os.ErrDeadlineExceeded +}