diff --git a/CHANGELOG.md b/CHANGELOG.md index fc13a5c..03e52fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## Unreleased (2026-08-24) +- **重启可靠性**: + - 文件事件、终端 `rs` 和 `SIGUSR1` 重启请求统一串行处理,防止并行启动多个子进程。 + - 等待旧进程完整退出后再启动新进程,并输出停止或启动失败信息。 + ## v1.5.4 (2026-06-27) - **修复 Bug**: 修复了配置 `Types`(文件类型白名单)时,目录本身的创建/删除等事件未能被正确过滤的问题。 - **CLI 体验大幅升级**: diff --git a/README.md b/README.md index 273b7e6..aebd1ca 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ kill -USR1 "$(cat watch.pid)" watch -r # 读取当前目录 watch.pid watch --reload path/to/watch.pid # 指定 PID 文件 ``` +所有文件事件、`rs` 和信号重启请求均串行执行;旧命令退出完成后才会启动新命令。 **参数说明**: diff --git a/TEST.md b/TEST.md index 4048a81..6fc9ab0 100644 --- a/TEST.md +++ b/TEST.md @@ -36,3 +36,4 @@ BenchmarkKeyGenerationWithFmt-16 7360394 167.6 ns/op 5. **极简接口**: 验证 `EasyStart` 的可用性。 6. **性能优化验证**: 验证在高频匹配场景下的 $O(1)$ 查找能力。 7. **类型白名单与目录过滤**: 验证开启 Types 白名单时,正常过滤目录变更事件,不误触发回调 (`TestDirEventWithTypeFilter`)。 +8. **重启并发控制**: 连续发送多个 `SIGUSR1` 时重启串行执行,旧监听端口释放后才启动新进程。 diff --git a/watch/main.go b/watch/main.go index 7bb5469..d020f1d 100644 --- a/watch/main.go +++ b/watch/main.go @@ -328,23 +328,21 @@ func runMonitor(config watch.Config) { func runCommand(config watch.Config, command []string, clearScreen bool) { execDir, _ := os.Getwd() - var proc *shell.Process sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1) + restartCh := make(chan *watch.Event, 1) + stopCh := make(chan struct{}) + doneCh := make(chan struct{}) - restart := func(e *watch.Event) { - if clearScreen { - fmt.Print("\033[2J\033[H") + requestRestart := func(e *watch.Event) { + select { + case restartCh <- e: + default: + // A restart is already pending; one restart will include all recent changes. } - printEvent(e) - printRestart(command) - if proc != nil { - proc.Kill() // shell.Kill() 已内置完整的进程树(PGID)清理逻辑 - } - proc, _ = shell.Start(command[0], command[1:], &shell.Options{Dir: execDir, CatchSignal: true}) } - w, err := watch.Start(config, restart) + w, err := watch.Start(config, requestRestart) if err != nil { logError("failed to start watcher: " + err.Error()) os.Exit(1) @@ -357,28 +355,62 @@ func runCommand(config watch.Config, command []string, clearScreen bool) { for scanner.Scan() { text := strings.TrimSpace(scanner.Text()) if text == "rs" { - restart(&watch.Event{Path: "manual restart", Type: watch.Change}) + requestRestart(&watch.Event{Path: "manual restart", Type: watch.Change}) } } }() - // 启动时先执行一次 - if clearScreen { - fmt.Print("\033[2J\033[H") - } - printRestart(command) - proc, _ = shell.Start(command[0], command[1:], &shell.Options{Dir: execDir, CatchSignal: true}) + // A single worker owns proc so file events, rs, and signals cannot restart concurrently. + go func() { + defer close(doneCh) + var proc *shell.Process + start := func() { + printRestart(command) + var err error + proc, err = shell.Start(command[0], command[1:], &shell.Options{Dir: execDir, CatchSignal: true}) + if err != nil { + proc = nil + logError("failed to start command: " + err.Error()) + } + } + if clearScreen { + fmt.Print("\033[2J\033[H") + } + start() + for { + select { + case e := <-restartCh: + if clearScreen { + fmt.Print("\033[2J\033[H") + } + printEvent(e) + if proc != nil { + if err := proc.Kill(); err != nil { + logError("failed to stop command: " + err.Error()) + continue + } + } + start() + case <-stopCh: + if proc != nil { + if err := proc.Kill(); err != nil { + logError("failed to stop command: " + err.Error()) + } + } + return + } + } + }() for sig := range sigCh { if sig == syscall.SIGUSR1 { - restart(&watch.Event{Path: "manual reload", Type: watch.Change}) + requestRestart(&watch.Event{Path: "manual reload", Type: watch.Change}) continue } break } - if proc != nil { - proc.Kill() - } + close(stopCh) + <-doneCh fmt.Printf("\n%s%s Stopped.%s\n", dim, gray, reset) }