feat(watch): 增加 PID 信号重启控制(by AI)

This commit is contained in:
Star 2026-08-24 07:48:10 +08:00
parent cb30e4b320
commit 64f3ebec31
3 changed files with 78 additions and 4 deletions

View File

@ -45,3 +45,7 @@
- 从 `@ssgo/tool/watcher` 迁移并重构。 - 从 `@ssgo/tool/watcher` 迁移并重构。
- **基础设施对齐**: 使用 `apigo.cc/go/file` 替代标准库。 - **基础设施对齐**: 使用 `apigo.cc/go/file` 替代标准库。
- **API 优化**: 引入 `Event` 结构体,提供 `EasyStart` 极简入口。 - **API 优化**: 引入 `Event` 结构体,提供 `EasyStart` 极简入口。
# [v1.5.5] - 2026-08-24
- **CLI**:
- Add `watch.pid` lifecycle management and `SIGUSR1` manual reload support.
- Add `-r/--reload` PID-file control and ensure `-h/--help` takes precedence over `.watch` tasks.

View File

@ -57,6 +57,12 @@ build = -c -e go -i vendor/** go build -o app
**交互式重启** **交互式重启**
当程序运行在后台时,在终端内敲击 `rs` 并回车,即可强行手动重启。 当程序运行在后台时,在终端内敲击 `rs` 并回车,即可强行手动重启。
watch 启动时会在当前目录写入 `watch.pid`,也可通过信号手动重启:
```bash
kill -USR1 "$(cat watch.pid)"
watch -r # 读取当前目录 watch.pid
watch --reload path/to/watch.pid # 指定 PID 文件
```
**参数说明** **参数说明**
@ -67,6 +73,7 @@ build = -c -e go -i vendor/** go build -o app
| `-E, --exclude-ext` | 排除文件类型,逗号分隔 | | `-E, --exclude-ext` | 排除文件类型,逗号分隔 |
| `-i, --ignore` | 排除路径模式可重复gitignore 语义) | | `-i, --ignore` | 排除路径模式可重复gitignore 语义) |
| `-c, --clear` | 重启前自动清屏 | | `-c, --clear` | 重启前自动清屏 |
| `-r, --reload [pidfile]` | 向 PID 文件中的 watch 进程发送重启信号 |
| `--go` | Go 预设 (自动补齐 `go run .` 与忽略路径) | | `--go` | Go 预设 (自动补齐 `go run .` 与忽略路径) |
| `--gotest` | Go 测试预设 (自动补齐 `go test -v -count=1 ./...`) | | `--gotest` | Go 测试预设 (自动补齐 `go test -v -count=1 ./...`) |
| `--web` | Web 预设 (自动补齐 `npm run dev`) | | `--web` | Web 预设 (自动补齐 `npm run dev`) |

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"strings" "strings"
"syscall" "syscall"
"time" "time"
@ -73,6 +74,15 @@ var presets = map[string]preset{
func main() { func main() {
args := os.Args[1:] args := os.Args[1:]
// Built-in control options must be handled before loading .watch; otherwise
// a task definition can consume -h as task arguments.
if hasHelpFlag(args) {
printHelp()
return
}
if handleReload(args) {
return
}
args = tryLoadWatchFile(args) args = tryLoadWatchFile(args)
if len(args) == 0 { if len(args) == 0 {
@ -166,6 +176,12 @@ func main() {
Excludes: excludePaths, Excludes: excludePaths,
Debounce: debounceTime, Debounce: debounceTime,
} }
pidPath := filepath.Join(".", "watch.pid")
if err := os.WriteFile(pidPath, []byte(fmt.Sprintf("%d\n", os.Getpid())), 0644); err != nil {
logError("failed to write watch.pid: " + err.Error())
} else {
defer os.Remove(pidPath)
}
printBanner(config, command, clearScreen) printBanner(config, command, clearScreen)
@ -176,6 +192,47 @@ func main() {
} }
} }
func hasHelpFlag(args []string) bool {
for _, arg := range args {
if arg == "-h" || arg == "--help" {
return true
}
}
return false
}
// handleReload sends SIGUSR1 to the process recorded in a PID file.
func handleReload(args []string) bool {
for i, arg := range args {
if arg != "-r" && arg != "--reload" {
continue
}
pidPath := "watch.pid"
if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
pidPath = args[i+1]
}
data, err := os.ReadFile(pidPath)
if err != nil {
if !os.IsNotExist(err) {
logError("failed to read PID file: " + err.Error())
}
return true
}
var pid int
if _, err = fmt.Sscanf(strings.TrimSpace(string(data)), "%d", &pid); err != nil || pid <= 0 {
logError("invalid PID in " + pidPath)
return true
}
if err = syscall.Kill(pid, syscall.SIGUSR1); err != nil {
logError(fmt.Sprintf("failed to reload process %d: %v", pid, err))
} else {
fmt.Printf("sent reload signal to process %d (%s)\n", pid, pidPath)
}
return true
}
return false
}
// tryLoadWatchFile 尝试从 .watch 读取配置并处理 --task // tryLoadWatchFile 尝试从 .watch 读取配置并处理 --task
func tryLoadWatchFile(args []string) []string { func tryLoadWatchFile(args []string) []string {
contentBytes, err := os.ReadFile(".watch") contentBytes, err := os.ReadFile(".watch")
@ -183,7 +240,7 @@ func tryLoadWatchFile(args []string) []string {
return args return args
} }
content := string(contentBytes) content := string(contentBytes)
tasks := make(map[string]string) tasks := make(map[string]string)
var firstTask string var firstTask string
@ -273,7 +330,7 @@ func runCommand(config watch.Config, command []string, clearScreen bool) {
var proc *shell.Process 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) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1)
restart := func(e *watch.Event) { restart := func(e *watch.Event) {
if clearScreen { if clearScreen {
@ -312,12 +369,17 @@ func runCommand(config watch.Config, command []string, clearScreen bool) {
printRestart(command) printRestart(command)
proc, _ = shell.Start(command[0], command[1:], &shell.Options{Dir: execDir, CatchSignal: true}) proc, _ = shell.Start(command[0], command[1:], &shell.Options{Dir: execDir, CatchSignal: true})
<-sigCh for sig := range sigCh {
if sig == syscall.SIGUSR1 {
restart(&watch.Event{Path: "manual reload", Type: watch.Change})
continue
}
break
}
if proc != nil { if proc != nil {
proc.Kill() proc.Kill()
} }
fmt.Printf("\n%s%s Stopped.%s\n", dim, gray, reset) fmt.Printf("\n%s%s Stopped.%s\n", dim, gray, reset)
os.Exit(0)
} }
// ── 输出函数 ─────────────────────────────────────────────────────────────────── // ── 输出函数 ───────────────────────────────────────────────────────────────────
@ -427,6 +489,7 @@ func printHelp() {
` + cyan + `-E, --exclude-ext <exts>` + reset + ` Exclude extensions (comma-separated) ` + cyan + `-E, --exclude-ext <exts>` + reset + ` Exclude extensions (comma-separated)
` + cyan + `-i, --ignore <pattern>` + reset + ` Ignore glob patterns (repeatable), e.g., ` + yellow + `node_modules/**` + reset + ` ` + cyan + `-i, --ignore <pattern>` + reset + ` Ignore glob patterns (repeatable), e.g., ` + yellow + `node_modules/**` + reset + `
` + cyan + `-c, --clear` + reset + ` Clear screen before restarting command ` + cyan + `-c, --clear` + reset + ` Clear screen before restarting command
` + cyan + `-r, --reload [pidfile]` + reset + ` Send SIGUSR1 to the PID in watch.pid (or a specified file)
` + cyan + `-h, --help` + reset + ` Show this help ` + cyan + `-h, --help` + reset + ` Show this help
` + bold + `Presets` + reset + ` (Auto-injects default commands if no command provided) ` + bold + `Presets` + reset + ` (Auto-injects default commands if no command provided)