feat(watch): 增加 PID 信号重启控制(by AI)
This commit is contained in:
parent
cb30e4b320
commit
64f3ebec31
@ -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.
|
||||||
|
|||||||
@ -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`) |
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user