Tab 生命周期与看门狗¶
类型:wiki(知识沉淀) 描述:抓取 Tab 的创建/销毁完整流程 + v0.10.15 看门狗的兜底机制 最后更新:2026-05-26(v0.10.15) 相关源码:
src/utils/scrape-window.ts、src/utils/scrape-watchdog.ts、src/entrypoints/background/batch-controller.ts相关 wiki:shared-queue-architecture.md
是什么¶
抓取过程会开大量浏览器 Tab:
- 地图实开 → openHarvestTab(url) 在共享窗口里开(active: false)
- 网站抓取 → openScrapeTab(url) 同一个共享窗口
- 人机验证 → openVerifyTab() 新建可见窗口(独立窗口,不在共享里)
每个 Tab 的正常生命周期 < 60 秒。但长跑时会有各种漏的场景,需要看门狗兜底。
Tab 生命周期(正常)¶
[ openTabForTask ]
↓
activeTabs.set(tabId, ActiveTab{ createdAt, timer 60s }) + tabToTaskIdMap.set
↓
[ 加载 → content script 发 batch-tab-ready ]
↓ onTabReady → state: 'running'
[ 抓数据 → 发 batch-done ]
↓ onTabDone → finalizeTab(tabId, 'done')
├─ clearTimeout(timer)
├─ activeTabs.delete
├─ closeHarvestTab(tabId) → browser.tabs.remove
└─ maybeCloseSharedWindow(窗口 0 tab 时关)
异常路径 + 兜底层级¶
正常完成 ─→ onTabDone
60s 超时 ─→ finalizeTab('timeout')
用户手动关 ─→ tabs.onRemoved → finalizeTab('tab-removed')
异常崩溃 ─→ try/finally 里 closeScrapeTab(tabId)
─────────以上是 v0.10.0─v0.10.14 的兜底─────────
SW 重启 ─→ restoreBatch 恢复 + forceCloseSharedWindow(旧窗口杀掉)
─────────以上不够,长跑仍会卡死────────────────
v0.10.15+ 看门狗 ─→ 5 分钟周期巡检 + 3 次累积自动重启
v0.10.15 看门狗设计¶
解决的真实问题¶
用户反馈:「长时间运行后,电脑卡死,页面没正常关闭,浏览器打开一堆 Tab。」
根因分析(按概率排序):
1. SW 被 Chromium 强杀重启 → 内存 activeTabs Map 清零 → 浏览器里的真 Tab 还在 → 没人会关它
2. maybeCloseSharedWindow 只在「窗口 0 tab」才关 → engine_heartbeat 每分钟唤起新 worker → 永远不会 0 → 累积
3. 60s TAB_TIMEOUT 失效:Chromium 后台节流可能让 setTimeout 推迟甚至跳过
4. engine_heartbeat 只跑业务,没人巡检孤儿
设计¶
| 维度 | 设计 |
|---|---|
| 调度方式 | chrome.alarms API(SW 友好),不用 setInterval |
| Alarm 名 | scrape_watchdog |
| 默认周期 | 5 分钟(保守,可调 2-30) |
| 默认僵尸阈值 | 5 分钟(远超 60s 正常完成) |
| 默认重启阈值 | 3 次(= 15 分钟连续异常) |
| 关闭路径 | 引擎 OFF / 抓取失败 15 次自动停止 → 一并清 alarm |
| 重启后行为 | v0.10.23 起默认自动恢复(60s 冷却后自动 pumpTasks);连续 3 次自动恢复都失败才停下要求人工 |
巡检步骤¶
1. 读 settings.watchdogEnabled —— 关掉就直接 return
2. 读 is_engine_running —— 引擎没跑就 return
3. 列共享窗口里所有 tab
4. 对比 activeTabs Map:
├─ 窗口里 + 不在 Map + 是 http(s) URL → 【孤儿】browser.tabs.remove
└─ 排除 about:blank / chrome:// 占位 tab
5. 调 getZombieTabIds(staleMs):
└─ activeTabs 里 createdAt > 阈值 → forceFinalizeTab('watchdog-zombie')
6. 收尾:窗口里 0 http tab + 调度器空 → forceCloseSharedWindow
7. 累积计数(持久化到 local:watchdogConsecutive,跨 SW 重启):
├─ 本次清掉 ≥1 个 → consecutive++
├─ 本次干净 → consecutive = 0
└─ consecutive ≥ limit → 核弹重启:
├─ forceCloseSharedWindow
├─ nukeAllSchedulerState(清 activeTabs / paging / tabToTaskIdMap,
│ 把 running 任务降级为 paused)
├─ forceResetLocks
├─ browser.notifications 系统通知
└─ consecutive = 0
8. 写 page-log:type=watchdog,error 字段携带统计
关键代码位置¶
| 文件 | 位置 | 作用 |
|---|---|---|
scrape-watchdog.ts |
runWatchdog |
巡检入口 |
scrape-watchdog.ts |
syncWatchdogAlarm |
创建/重建 alarm |
batch-controller.ts |
getActiveTabIds |
给 watchdog 拿快照 |
batch-controller.ts |
getZombieTabIds(staleMs) |
算僵尸列表 |
batch-controller.ts |
forceFinalizeTab |
watchdog 用的强制结束 |
batch-controller.ts |
nukeAllSchedulerState |
核弹重置 |
background/index.ts |
onAlarm 监听器 |
触发 runWatchdog |
background/index.ts |
start/stop-engine | 联动 syncWatchdogAlarm |
engine-manager.ts |
handleScrapeFailure |
15 次失败时一并清 watchdog alarm |
配置项(settings)¶
| 字段 | 默认 | 范围 | 含义 |
|---|---|---|---|
watchdogEnabled |
true | bool | 总开关 |
watchdogInterval |
5 | 2-30 | 分钟,巡检周期 |
watchdogStaleThreshold |
5 | 2-30 | 分钟,单 tab 寿命阈值 |
watchdogConsecutiveLimit |
3 | 1-10 | 次,连续异常触发自动重启 |
watchdogAutoResume |
true | bool | v0.10.23 — 重启后是否自动 pumpTasks 继续抓取 |
watchdogAutoResumeCooldownSec |
10 | 0-600 | v0.10.24 — 重启后冷却秒数(让 SW / 网络喘口气) |
watchdogAutoResumeMaxCycles |
9999 | 1-99999 | v0.10.24 — 默认无限次(除非用户点停止);保守用户可调小 |
自动恢复设计(v0.10.23 SPEC-002)¶
核弹重启后默认不要求手动:
连续 N 次累积异常 → 触发核弹
↓
读 settings.watchdogAutoResume + storage.watchdogAutoResumeCycles
↓
shouldAutoResume = autoResume && cycles < maxCycles
↓
shouldAutoResume = true:
├─ nukeAllSchedulerState({ keepTasksRunning: true }) ← 不降级 task
├─ forceCloseSharedWindow + forceResetLocks
├─ cycles++ 持久化到 storage
├─ 通知用户「自动恢复中(第 N/M 次)」(priority 1,温和)
└─ setTimeout(cooldownMs) → pumpTasks() + manageQueue()
shouldAutoResume = false(达上限 / 用户关闭):
├─ nukeAllSchedulerState({ keepTasksRunning: false }) ← 降级 paused(老行为)
├─ cycles = 0
└─ 通知用户「自动恢复达上限,请人工」(priority 2,强烈)
每次巡检干净(orphans=0, zombies=0)→ cycles = 0 ← 避免长期累积误触发
local:watchdogAutoResumeCycles 持久化跨 SW 重启(同 watchdogConsecutive)。
易踩坑¶
⚠️ 坑 1:不要在共享窗口外的 tab 上动手¶
watchdog 必须用 browser.tabs.query({ windowId: sharedWinId }) 严格限定窗口。如果换成 browser.tabs.query({}) 扫全部 tab → 可能误关用户的私人 tab,灾难。
⚠️ 坑 2:累积计数要持久化¶
local:watchdogConsecutive 必须存 storage,不能放模块内存。否则 SW 重启 → 内存清零 → 永远到不了 limit → 自动重启失效。
⚠️ 坑 3:核弹重启不要清 tasks Map¶
nukeAllSchedulerState 把 running 任务降级为 paused,不删除。这样用户能看到「哦它停了」、点恢复继续 —— 而不是「咦我的任务呢」。
⚠️ 坑 4:watchdog 不能挂¶
runWatchdog 整体 try/catch 吞所有异常 + 写日志。它如果自己挂了 = 兜底层失效。
⚠️ 坑 5:openVerifyTab 不在巡检范围¶
人机验证 tab 是独立窗口的可见 tab,不在 sharedWinId 内 → watchdog 不会动它。这是对的(用户可能正在做验证)。但如果用户忘了关 → 累积。当前不处理,看后续是否需要单独机制识别"验证完成但没关"。
修改这块时要 / 不要做什么¶
- ✅ 加新巡检项 → 在
runWatchdog中加步骤 + 单独统计 + 写日志 - ✅ 调整默认值 → 改
storage-data.ts的DefaultSettings即可 - ❌ 不要把 watchdog 周期降到 < 2 分钟(CPU 浪费)
- ❌ 不要直接
browser.tabs.query({})扫全部(误伤用户 tab) - ❌ 不要让累积计数仅存内存(SW 重启会丢)
版本里程碑¶
| 版本 | 事件 |
|---|---|
| v0.9.x | 仅靠 60s TAB_TIMEOUT + try/finally 兜底,长跑会卡 |
| v0.10.0 | 共享窗口 + maybeCloseSharedWindow,但仍有累积场景 |
| v0.10.15 | 首次引入看门狗 — 5 分钟巡检 + 3 次累积自动重启 + UI 设置 |
| v0.10.20 | mutex 防并发 / nuke 真关 tab / 清 consecutive — ISSUE-0015 |
| v0.10.23 | 核弹后自动恢复(默认 ON) — 用户不需要手动点开始 — SPEC-002 |
| v0.10.24 | 自动恢复默认调激进:cooldown 60→10s / maxCycles 3→9999(用户:「无限次除非人工停止」) |