CI runner 修"双看门狗抢 .runner.lock":-StartNow 先清残留进程再启动

实测发现(比文档问题更要紧):runner 网页 online、-Status 说 daemon 在跑,
但 runner-watchdog.log 每 10 秒刷 "starting daemon → daemon exited, exit code 1",
daemon 日志写 "another gitea-runner process is already using .runner" /
"runner file is already locked by another process"。

根因:Unregister-ScheduledTask 不会带走旧实例启动的进程。13:22 重跑 bootstrap 时,
12:53 起的旧看门狗成为孤儿并继续养着旧 daemon,旧 daemon 占住 .runner.lock,
新注册任务启动的看门狗永远抢不到锁。online 是旧 daemon 在硬撑——任务实际谁也没管,
日志还在以约 20 MB/天 增长。

- ci_runner_bootstrap.ps1:-StartNow 新增 Stop-StaleRunnerInstances,先停孤儿看门狗
  再停残留 daemon(顺序不能反:只杀 daemon 会被活着的看门狗 10 秒内重拉),启动后
  等 12 秒回显单实例确认;-Status 新增重复实例计数 + daemon 日志锁冲突检测
- 指南 §3.4 补注销不停进程的说明与"纯注释改动不需重启任务";§7 排障表加
  "online 却每 10 秒重启"一行(含手工应急命令)
- §3.2 / README:deploy_runner_task.ps1 与 run_runner.cmd 状态由"仍在磁盘、建议删除"
  改为"已删除";README 补重跑部署前的锁冲突警示

机器侧同步(不在版本库):config.yaml 头部"常驻改用 sc.exe / New-Service"是 §3.0
已证伪的错误指引,改为看门狗口径并指向本文——改后 YAML 可解析且所有配置值逐项与
改前一致(只动注释);删除两个废弃脚本前核实计划任务动作目标是
run_runner_watchdog.ps1,删除不影响在跑 runner;check_env.ps1 指向废弃脚本的提示
改指 bootstrap(带 BOM 文件,改后 BOM 完好、语法 0 错)。

验证:pre-commit run --all-files 17 项全 Passed;bootstrap parse 0 错、纯 ASCII、
-DryRun 正常。当前 runner 仍 online(旧 daemon 硬撑),彻底归一需管理员跑一次
-StartNow 清残留。
This commit is contained in:
2026-10-12 17:37:19 +08:00
parent 8b35769a5c
commit 49f3e2dac5
3 changed files with 92 additions and 13 deletions
+11 -4
View File
@@ -56,10 +56,16 @@
> 变量;返回码 `0x41301` 是"任务正在运行"、`next run` 为空是开机触发器的正常表现,
> 都不是故障(详见指南 §3.4)。
> 🗑 **机器侧遗留,别误用**:`D:\Docker\act_runner\` 下还留着旧方案的 `deploy_runner_task.ps1`
> 与 `run_runner.cmd`。它们跑的是**单次** daemon——首次 Declare 失败就永久停摆(机理见指南
> §3.2),而且**执行它会把看门狗任务覆盖回单次任务**。现行部署只有 `ci_runner_bootstrap.ps1`
> 这一条路;那两个文件建议改名加 `.deprecated` 后缀或直接删除。
> ✅ **机器侧遗留已清理(2026-10-12)**:旧方案的 `deploy_runner_task.ps1` 与 `run_runner.cmd`
> 已从 `D:\Docker\act_runner\` **删除**(它们跑单次 daemon,首次 Declare 失败就永久停摆,
> 机理见指南 §3.2;`check_env.ps1` 里指向它们的提示也已改指 bootstrap)。
> 现行部署只有 `ci_runner_bootstrap.ps1` 这一条路。
> 🔴 **重跑部署前先知道这条**(2026-10-12 实测):`Unregister-ScheduledTask` **不会**带走旧实例
> 启动的进程。旧看门狗会作为孤儿继续养着它的 daemon,而那个 daemon 占着 `.runner.lock`,
> 于是新看门狗每 10 秒失败一次(`runner file is already locked by another process`)——
> **而 runner 在网页上照样显示 online**,CI 看着正常,任务其实谁也没管。
> 现在 `-StartNow` 会先清掉这些残留进程再启动(约离线几秒),`-Status` 也会报出重复实例。
## 相关文档
@@ -73,5 +79,6 @@
| 日期 | 变更 |
|---|---|
| 2026-10-12 | 🔴 **修复"双看门狗抢 `.runner.lock`"缺陷**:`Unregister-ScheduledTask` 不停旧实例进程 → 孤儿看门狗养着旧 daemon 占锁,新看门狗每 10 秒失败一次,而 runner 网页照样 online、`-Status` 也说"在跑"。现 `-StartNow` 先清残留进程再启动、`-Status` 报重复实例与锁冲突。同日清理机器侧:`config.yaml` 头部错误指引("改用 sc.exe / New-Service")改为看门狗口径,删除已废弃的 `deploy_runner_task.ps1` 与 `run_runner.cmd`,`check_env.ps1` 提示同步改指 bootstrap |
| 2026-10-12 | **单一口径清理**:合并稿初版对 4 处分歧"两说并存",现统一为本机实测现状——注册口径(`gitea-runner v4.0.1` / `D:\Docker\act_runner` / `127.0.0.1:3001`,旧 `act_runner v0.2.11` + `C:\act_runner` 段删去可复制命令只留警示)、`config.yaml`(`fetch_timeout: 1m0s`、`timeout: 30m0s`)、常驻方式(只认看门狗,`deploy_runner_task.ps1` 标废弃)、验收数字(L0.5 `288/288`,历史数字标注为快照)。并补"机器侧遗留脚本勿误用"警示 |
| 2026-10-12 | **新建本目录**:远程 CI 内容原先散在 `docs/环境与部署/`(两篇互相重叠的指南)、`docs/测试说明/测试开发标准流程.md` §9、`AGENTS.md` 提交门禁节共 4 处,约 2000 行;现合并为单一指南并把 `AGENTS.md` 压成一行指针。动因:远程 CI 只涉及本机一台 runner,与日常开发规范混在一起每次会话都被读,造成困惑 |
+12 -5
View File
@@ -533,10 +533,10 @@ level=info msg="runner: win-stm32-ci, with version: v4.0.1, with labels: [window
→ `Register-ScheduledTask`(开机+登录双触发器、SYSTEM、最高权限、崩溃重启)→ 启动 → 校验存活。
它的"崩溃重启"参数看着齐全,但**对"正常退出"完全无效**——这正是它失效的那一点。
> ⚠️ **这两个文件目前仍留在磁盘上**(`D:\Docker\act_runner\deploy_runner_task.ps1` 与
> `run_runner.cmd`,2026-10-12 核实)。它们不在版本库里,属机器侧遗留;建议改名加
> `.deprecated` 后缀或直接删除——**否则下次手滑执行,就会把看门狗任务覆盖回单次任务**,
> 等于把 §2.4 的修复整个抹掉。现行部署一律用仓库里的 `scripts/ci_runner_bootstrap.ps1`。
> ✅ **已于 2026-10-12 删除** `D:\Docker\act_runner\deploy_runner_task.ps1` 与 `run_runner.cmd`
> (删除前核实:只有 `check_env.ps1` 的一句提示引用它,已同步改指 `ci_runner_bootstrap.ps1`;
> 计划任务的动作目标是 `run_runner_watchdog.ps1`,与这两个文件无关,删除不影响在跑的 runner)。
> 现行部署一律用仓库里的 `scripts/ci_runner_bootstrap.ps1`。
### 3.3 手动等价命令(等价于 §2.4 的 bootstrap,用于理解任务对象)
@@ -592,7 +592,12 @@ Enable-ScheduledTask -TaskName GiteaActionsRunner # 恢复自启
Unregister-ScheduledTask -TaskName GiteaActionsRunner -Confirm:$false # 完全卸载
```
改完 `config.yaml` 后需 **Stop 再 Start** 任务才生效。
改完 `config.yaml` 后需 **Stop 再 Start** 任务才生效(**纯注释改动不需要**——注释不参与解析)。
> 🔴 **重跑 `ci_runner_bootstrap.ps1` 会先注销再注册任务,而注销不会带走旧实例的进程**:
> 旧看门狗变成孤儿、继续养着旧 daemon,旧 daemon 占住 `.runner.lock`,新看门狗就永远起不来
> (症状见 §7"online 却每 10 秒重启"那行)。2026-10-12 起 `-StartNow` 会**先停掉残留的看门狗与
> daemon 再启动**并回显单实例确认;`-Status` 也会把重复实例与锁冲突报出来。
> 💡 **`LastTaskResult` 数值解读**(别被非 0 吓到):
>
@@ -1074,6 +1079,7 @@ date +%s # 对比 last_online
| 🔴 `Start-Service gitea-runner` 报 **无法启动服务**(错误 1053) | **`daemon` 不是服务型程序**,不向 SCM 回报 `SERVICE_RUNNING` | **删除该服务,改用任务计划程序**(§3.0 / §3.5) |
| `Register-ScheduledTask` 报 **拒绝访问** | Shell 未提权(`IsAdmin=False`) | 右键 PowerShell → **以管理员身份运行**(§3.6) |
| 任务 `LastTaskResult` 非 0 | 启动器路径/权限问题,或任务被手工终止 | 看 `log\runner-watchdog.log` 与 `log\runner-daemon.log`;确认 `run_runner_watchdog.ps1` 存在且为纯 ASCII;`0x41301` 是"正在运行"不是错误 |
| 🔴 **runner 网页显示 online,但 `runner-watchdog.log` 每 10 秒刷 `starting daemon` → `daemon exited, exit code 1`** | **两个看门狗抢 `.runner.lock`**:`Unregister-ScheduledTask` 不会带走旧实例启动的进程,孤儿看门狗养着的旧 daemon 仍占着锁,新看门狗起不来(daemon 日志写 `another gitea-runner process is already using ".runner"` / `runner file is already locked by another process`)。此时 online 是**旧 daemon 在硬撑**,任务实际谁也没管 | `scripts\ci_runner_bootstrap.ps1 -Status` 会报出重复实例;**管理员**跑 `-StartNow`(现已会先清残留进程再启动,runner 约离线几秒)。应急可手工:`Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" \| ? CommandLine -match 'run_runner_watchdog'` 找出多个 pid,杀掉**旧的那个**,再 `taskkill /PID <旧 daemon>` 让在册看门狗接管 |
| `l0-gates` 报 `Cannot find: pwsh in PATH` | `shell: pwsh` = PS7,本机只有 WinPS 5.1 | 改 `shell: powershell` |
| 🔴 `doc-links` 钩子本地过、CI 挂 | 引用了 **gitignore 的运行时文件**(如 `.dsh/skills/serial-assistant/scripts/state/*.json`),本地存在、干净克隆里没有 | 该行加状态词「未创建/不存在」等(§4.1.2) |
| 任务跑满 72 小时被杀 | 计划任务默认 `ExecutionTimeLimit` | 设为 `0`(不限时长),见 §3.3 警示 |
@@ -1313,6 +1319,7 @@ powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -DryRun
| 日期 | 变更 |
|---|---|
| 2026-10-12 | 🔴 **实测揪出"双看门狗抢锁"缺陷并修复** + 机器侧清理。① 现象:runner 网页 online、`-Status` 说 daemon 在跑,但 `runner-watchdog.log` 每 10 秒刷 `starting daemon → daemon exited, exit code 1`,daemon 日志 `runner file is already locked by another process`。② 根因:`Unregister-ScheduledTask` **不停旧实例启动的进程**——13:22 重跑 bootstrap 时旧看门狗(12:53 起)成孤儿并继续养着旧 daemon、占住 `.runner.lock`,新看门狗永远抢不到锁,任务实际谁也没管,日志以约 20 MB/天在刷。③ 修复:`ci_runner_bootstrap.ps1` 的 `-StartNow` 现在**先停孤儿看门狗与残留 daemon 再启动**并回显单实例确认;`-Status` 新增重复实例计数与锁冲突检测。④ 机器侧清理:`config.yaml` 头部那句**错误指引**("常驻改用 Windows sc.exe / New-Service",正是 §3.0 证伪的路)改为看门狗口径并指向本文;删除已废弃的 `deploy_runner_task.ps1` 与 `run_runner.cmd`(§3.2),`check_env.ps1` 指向它们的提示同步改指 bootstrap。⑤ 核实:`config.yaml` 改后 YAML 可解析且**所有配置值与改前逐项一致**(只动注释);`check_env.ps1` 带 BOM 且语法 0 错;删除前确认计划任务动作目标是 `run_runner_watchdog.ps1`,删除不影响在跑 runner |
| 2026-10-12 | 🧹 **单一口径清理(以本机实测现状为准)**:合并稿初版对 4 处分歧采取"两说并存 + 衔接注",本次统一。① 注册口径——`act_runner v0.2.11` + `C:\act_runner` + 公网域名 instance 一段**删除可复制命令**,只留"勿照抄"警示(核实 `C:\act_runner` 至今不存在,本机是 `gitea-runner v4.0.1` + `D:\Docker\act_runner` + `127.0.0.1:3001`);② `config.yaml` 示例 `fetch_timeout` 由 `5m0s` 改为磁盘实际值 **`1m0s`**、`timeout` 统一 `30m0s`,并补"不得超过 RPC timeout(1m0s)";③ 常驻口径统一为看门狗——§3.2 `deploy_runner_task.ps1` 标为**已废弃**并说明失效机理(单次 daemon = 正常退出,失败重启永不触发),§3.3 手动等价命令**重写为看门狗版任务对象**(旧版会教人手搓出坏配置),§10 TL;DR 第 4 步改指 `ci_runner_bootstrap.ps1`;④ 数字口径——L0.5 期望值更新为 **288/288**(run#8 的 39/39 与 278/278 标注为历史快照,判定只看 `fail=0` 与 A==B),钩子数按现行 18 定义 / 17 报告。另同步:健康三查与排障表改用现行日志名(`runner-daemon.log` / `runner-watchdog.log`,`runner-stdout.log` 标为遗留且说明其混编码)、BOM 示例对象换成 `check_env.ps1` 并注明看门狗是纯 ASCII 无需 BOM、配套脚本表补 `run_runner_watchdog.ps1` 并标出磁盘上仍存的废弃脚本建议删除、补 `Next Run Time` 为空属正常的说明 |
| 2026-10-12 | **文档合并**:本次把 `CI配置指南.md` 与 `Gitea_Runner部署指南.md` 合并为本篇(`docs/CI/远程CI完整指南.md`)并迁入 `docs/CI/`,去重(runner 注册 / config.yaml / 装软件 / 排障表两篇均有)、补入 runner 看门狗与机器级变量实测教训;相对链接按新位置改写,原两篇互引改为本文内部引用 |
| 2026-10-11 | **bootstrap 自身修复**:`StartWhenAvailable` 误设到**触发器**上(该属性属**任务设置**,触发器没有)→ 脚本在"已注销旧任务、尚未注册新任务"之间抛 `SetValueInvocationException`,把 runner 任务删没了。现改为①先构建 action/trigger/settings、**后**注销旧任务,②`Register-ScheduledTask` 失败时显式告警"旧任务已删、CI 裸奔",③新增 **`-DryRun`**(免管理员即可全量校验任务对象),④`-Status` 在非管理员下额外提示"看不见任务 ≠ 任务不存在"。同时更正根因:旧任务其实存在,但跑的是**单次** `daemon`——daemon 属"正常退出"而非"失败",计划任务的失败重启**救不到**它,必须用看门狗循环重拉 |
+69 -4
View File
@@ -218,8 +218,31 @@ if ($Status) {
}
}
$proc = Get-Process | Where-Object { $_.ProcessName -match 'gitea-runner|act_runner' }
if ($proc) { Ok ("daemon running: pid(s) " + ($proc.Id -join ',')) } else { Warn 'daemon not running' }
$proc = @(Get-Process | Where-Object { $_.ProcessName -match 'gitea-runner|act_runner' })
if ($proc.Count -eq 1) {
Ok ("daemon running: pid {0}" -f $proc[0].Id)
} elseif ($proc.Count -gt 1) {
Warn (" {0} daemon processes -- only ONE can hold .runner.lock, the rest are in a restart loop" -f $proc.Count)
Warn ' fix: re-run this script elevated with -StartNow (it clears stale instances first)'
} else { Warn 'daemon not running' }
# A watchdog that cannot get the lock fails every 10s while the runner still
# reports ONLINE, so the loop is invisible from the web UI. Count watchdogs and
# grep the daemon log for the lock error (measured 2026-10-12).
$wd = @(Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.CommandLine -match 'run_runner_watchdog\.ps1' })
if ($wd.Count -gt 1) {
Warn (" {0} watchdog processes (pids {1}) -- an old one survived a task re-registration" -f `
$wd.Count, (($wd | ForEach-Object { $_.ProcessId }) -join ','))
Warn ' and is racing the new one for .runner.lock; re-run elevated with -StartNow'
}
$dlog = Join-Path $RunnerDir 'log\runner-daemon.log'
if (Test-Path -LiteralPath $dlog) {
$tail = Get-Content -LiteralPath $dlog -Tail 40 -Encoding UTF8 -ErrorAction SilentlyContinue
if ($tail -match 'already locked by another process') {
Warn ' daemon log says ".runner is already locked by another process" -- stale instance present'
}
}
if (Test-Gitea $GiteaUrl) { Ok "Gitea reachable at $GiteaUrl" }
else { Fail "Gitea NOT reachable at $GiteaUrl" }
@@ -417,14 +440,56 @@ catch {
Ok "scheduled task registered: $TaskName (at boot +1m, as SYSTEM, unlimited runtime, restart x999/1m)"
# ---------------------------------------------------------------------------
# 4. start it
# 4. start it -- but clear stale instances first, or the new watchdog never gets
# the .runner lock.
#
# MEASURED TRAP (2026-10-12): Unregister-ScheduledTask does NOT stop the process
# the old instance had started. That orphaned watchdog keeps its daemon alive and
# the daemon holds .runner.lock, so the freshly started watchdog fails every 10s:
# level=error msg="another gitea-runner process is already using \".runner\"..."
# Error: runner file is already locked by another process
# The runner still shows ONLINE (the stale daemon serves fine), so CI looks healthy
# while the log fills with a restart loop and the task supervises nothing at all.
# ---------------------------------------------------------------------------
function Stop-StaleRunnerInstances {
# Supervisors first: killing the daemon alone lets a live watchdog respawn it
# within 10s and the race starts over.
$killed = 0
Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.CommandLine -match 'run_runner_watchdog\.ps1' } |
ForEach-Object {
Info (" stopping stale watchdog pid {0}" -f $_.ProcessId)
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
$killed++
}
Get-Process -Name 'gitea-runner' -ErrorAction SilentlyContinue | ForEach-Object {
Info (" stopping stale daemon pid {0} (releases .runner.lock)" -f $_.Id)
Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
$killed++
}
return $killed
}
if ($StartNow) {
if (-not (Test-Gitea $GiteaUrl)) {
Warn "Gitea is not answering at $GiteaUrl right now -- the watchdog will keep waiting"
}
$stale = Stop-StaleRunnerInstances
if ($stale -gt 0) {
Warn "$stale stale process(es) stopped; the runner is offline for a few seconds"
Start-Sleep -Seconds 2
}
Start-ScheduledTask -TaskName $TaskName
Ok "task started; daemon log: Get-Content '$(Join-Path $RunnerDir 'log\runner-daemon.log')' -Tail 20"
Start-Sleep -Seconds 12 # the watchdog probes Gitea, then execs the daemon
$d = @(Get-Process -Name 'gitea-runner' -ErrorAction SilentlyContinue)
if ($d.Count -eq 1) {
Ok ("daemon up: pid {0} (single instance, holds .runner.lock)" -f $d[0].Id)
} elseif ($d.Count -eq 0) {
Warn 'no daemon yet -- the watchdog may still be waiting for Gitea; check the watchdog log'
} else {
Warn ("{0} daemons running -- only one can hold the lock; inspect and kill the extras" -f $d.Count)
}
Ok "daemon log: Get-Content '$(Join-Path $RunnerDir 'log\runner-daemon.log')' -Tail 20"
Ok "watchdog log: Get-Content '$(Join-Path $RunnerDir 'log\runner-watchdog.log')' -Tail 20"
Info 'verify on the web UI: repo -> Settings -> Actions -> Runners -> win-stm32-ci should turn Idle'
}