fix(watch): 串行化命令重启流程(by AI)
This commit is contained in:
parent
64f3ebec31
commit
a22ebe96cd
@ -1,5 +1,10 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## Unreleased (2026-08-24)
|
||||||
|
- **重启可靠性**:
|
||||||
|
- 文件事件、终端 `rs` 和 `SIGUSR1` 重启请求统一串行处理,防止并行启动多个子进程。
|
||||||
|
- 等待旧进程完整退出后再启动新进程,并输出停止或启动失败信息。
|
||||||
|
|
||||||
## v1.5.4 (2026-06-27)
|
## v1.5.4 (2026-06-27)
|
||||||
- **修复 Bug**: 修复了配置 `Types`(文件类型白名单)时,目录本身的创建/删除等事件未能被正确过滤的问题。
|
- **修复 Bug**: 修复了配置 `Types`(文件类型白名单)时,目录本身的创建/删除等事件未能被正确过滤的问题。
|
||||||
- **CLI 体验大幅升级**:
|
- **CLI 体验大幅升级**:
|
||||||
|
|||||||
@ -63,6 +63,7 @@ kill -USR1 "$(cat watch.pid)"
|
|||||||
watch -r # 读取当前目录 watch.pid
|
watch -r # 读取当前目录 watch.pid
|
||||||
watch --reload path/to/watch.pid # 指定 PID 文件
|
watch --reload path/to/watch.pid # 指定 PID 文件
|
||||||
```
|
```
|
||||||
|
所有文件事件、`rs` 和信号重启请求均串行执行;旧命令退出完成后才会启动新命令。
|
||||||
|
|
||||||
**参数说明**:
|
**参数说明**:
|
||||||
|
|
||||||
|
|||||||
1
TEST.md
1
TEST.md
@ -36,3 +36,4 @@ BenchmarkKeyGenerationWithFmt-16 7360394 167.6 ns/op
|
|||||||
5. **极简接口**: 验证 `EasyStart` 的可用性。
|
5. **极简接口**: 验证 `EasyStart` 的可用性。
|
||||||
6. **性能优化验证**: 验证在高频匹配场景下的 $O(1)$ 查找能力。
|
6. **性能优化验证**: 验证在高频匹配场景下的 $O(1)$ 查找能力。
|
||||||
7. **类型白名单与目录过滤**: 验证开启 Types 白名单时,正常过滤目录变更事件,不误触发回调 (`TestDirEventWithTypeFilter`)。
|
7. **类型白名单与目录过滤**: 验证开启 Types 白名单时,正常过滤目录变更事件,不误触发回调 (`TestDirEventWithTypeFilter`)。
|
||||||
|
8. **重启并发控制**: 连续发送多个 `SIGUSR1` 时重启串行执行,旧监听端口释放后才启动新进程。
|
||||||
|
|||||||
@ -328,23 +328,21 @@ func runMonitor(config watch.Config) {
|
|||||||
func runCommand(config watch.Config, command []string, clearScreen bool) {
|
func runCommand(config watch.Config, command []string, clearScreen bool) {
|
||||||
execDir, _ := os.Getwd()
|
execDir, _ := os.Getwd()
|
||||||
|
|
||||||
var proc *shell.Process
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1)
|
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) {
|
requestRestart := func(e *watch.Event) {
|
||||||
if clearScreen {
|
select {
|
||||||
fmt.Print("\033[2J\033[H")
|
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 {
|
if err != nil {
|
||||||
logError("failed to start watcher: " + err.Error())
|
logError("failed to start watcher: " + err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -357,28 +355,62 @@ func runCommand(config watch.Config, command []string, clearScreen bool) {
|
|||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
text := strings.TrimSpace(scanner.Text())
|
text := strings.TrimSpace(scanner.Text())
|
||||||
if text == "rs" {
|
if text == "rs" {
|
||||||
restart(&watch.Event{Path: "manual restart", Type: watch.Change})
|
requestRestart(&watch.Event{Path: "manual restart", Type: watch.Change})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// 启动时先执行一次
|
// 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 {
|
if clearScreen {
|
||||||
fmt.Print("\033[2J\033[H")
|
fmt.Print("\033[2J\033[H")
|
||||||
}
|
}
|
||||||
printRestart(command)
|
start()
|
||||||
proc, _ = shell.Start(command[0], command[1:], &shell.Options{Dir: execDir, CatchSignal: true})
|
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 {
|
for sig := range sigCh {
|
||||||
if sig == syscall.SIGUSR1 {
|
if sig == syscall.SIGUSR1 {
|
||||||
restart(&watch.Event{Path: "manual reload", Type: watch.Change})
|
requestRestart(&watch.Event{Path: "manual reload", Type: watch.Change})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if proc != nil {
|
close(stopCh)
|
||||||
proc.Kill()
|
<-doneCh
|
||||||
}
|
|
||||||
fmt.Printf("\n%s%s Stopped.%s\n", dim, gray, reset)
|
fmt.Printf("\n%s%s Stopped.%s\n", dim, gray, reset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user