dsh-autorestart 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +55 -6
- package/client.js +102 -9
- package/docs/README.zh.md +4 -2
- package/index.js +514 -39
- package/lib/backup.js +279 -9
- package/lib/checkpoint.js +134 -0
- package/lib/diagnostics.js +18 -0
- package/lib/restart-phase.js +263 -0
- package/lib/watchdog-disarm.js +506 -0
- package/lib/watchdog.js +86 -0
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -0
- package/package.json +1 -1
- package/scripts/drill-watchdog.mjs +135 -2
- package/scripts/watchdog.mjs +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.2 — 2026-10-29
|
|
4
|
+
|
|
5
|
+
**线上缺陷修复:宿主起来之后没有撤销看门狗,会把好环境回滚掉。**
|
|
6
|
+
|
|
7
|
+
### 事故现场(真机,19:19–19:21)
|
|
8
|
+
|
|
9
|
+
触发重启 → `watchdog-pending.json` 写入、`dsh-autorestart-watchdog` 计划任务注册成功、报告写 `armed`;新宿主 19:20:20 起来,health 连打 3 次都是 200;**但 `watchdog-ready.json` 从头到尾没出现**,`watchdog-pending.json` 还在、计划任务还在 `Ready`。当时是人工 `Unregister-ScheduledTask` 拆掉的——没人管的话,90 秒后它会还原快照并再拉起一个实例,**把一个健康的环境回滚掉**。
|
|
10
|
+
|
|
11
|
+
### 根因(已复现)
|
|
12
|
+
|
|
13
|
+
health 路由里那段"写 marker + 撤看门狗"的代码读的是 `stateDirOf()`——那是 runtime 工厂内部的闭包变量,**模块级函数根本看不见它**。所以每次请求都在第一行就抛 `ReferenceError: stateDirOf is not defined`,外面那个 `catch {}` 把异常吞了:路由照样回 200,marker 没写、pending 没清、计划任务没删,任何地方都没有痕迹。
|
|
14
|
+
|
|
15
|
+
- 复现方式:用测试上下文调真实的 health 路由,断言 marker 出现——旧代码失败(`ready written: false`)。
|
|
16
|
+
- 同源的第二个坑:`clearPending` / `readPending` 当时也没被 import,`armWatchdog` 注册失败的回滚分支同样会抛 `ReferenceError`。
|
|
17
|
+
|
|
18
|
+
### 修
|
|
19
|
+
|
|
20
|
+
- **health 路由不再靠闭包**:状态目录改由 `runtime.stateDir` 提供(`runtime` 上新增 `stateDir` / `pluginVersion`)。这类"路由看不见工厂内部变量"的错误从此没机会重演。
|
|
21
|
+
- **撤销拆成独立模块**(`lib/watchdog-disarm.js`),三件事分开做、都留痕:
|
|
22
|
+
1. **先写 ready marker**(同步、read-back 校验)——它是唯一能阻止回滚的东西,绝不再依赖计划任务是否删成功;
|
|
23
|
+
2. 清 `watchdog-pending.json`;
|
|
24
|
+
3. 按 pending 自己写的 tier/taskName 删调度项(不再硬编码名字),删完**回查**(`schtasks /Query`)确认真的没了。
|
|
25
|
+
- **失败必须重试并留痕**:默认 3 次、退避 120ms/240ms;每次失败都带 `error.name` + `message`,写进 `watchdog-disarm.json`、health 响应、诊断文本("报告一个问题"),不再静默。
|
|
26
|
+
- **开机就撤销,不等任何人来问**:`apply()` 里注册完路由立刻跑一次 disarm。即使一次 health 都没人打,宿主起来就会把看门狗撤掉;health 路由仍是第二道保险(后台清理,不拖慢响应)。
|
|
27
|
+
- **兜底(最要紧的一条):看门狗回滚前必须自己问一次 health**。`runWatchdogFlow` 新增 3b 步:直接 HTTP 问 `127.0.0.1:<port>`(端口依次取实例记录 `access.json`、pending 里的 port、旧 `healthUrl`),**只要有一台在应答就绝不回滚**(`result: not-needed`);一个端口都问不到时,额外再等一轮、复查 ready marker 与 pending,仍然没动静才允许回滚。
|
|
28
|
+
- **顺手修掉的版本号 bug**:runtime 里那份 `pluginVersion()` 读的是 `../package.json`(插件目录的上一层),永远返回 `unknown`;health 于是把 `unknown` / 硬编码常量当版本报出来。现在统一走模块级读取,`health.version` 是真正加载的版本。
|
|
29
|
+
|
|
30
|
+
### 演练
|
|
31
|
+
|
|
32
|
+
`node scripts/drill-watchdog.mjs` 从三个场景扩到**四个**,新增:
|
|
33
|
+
|
|
34
|
+
- **场景 ④「宿主起来了但没写 ready marker」**(就是这次事故):假宿主只应答 health、绝不写 marker,并且**真实注册一个 Windows 计划任务**;断言 watchdog `result: not-needed`、`healthCheck: healthy`、**一次还原都没有**、pending 已消失、**计划任务真的被删掉**(`schtasks /Query` 查不到)。
|
|
35
|
+
|
|
36
|
+
### 重启阶段化 UI(同版发布:点下去 1 秒内就有反馈,备份有真实进度)
|
|
37
|
+
|
|
38
|
+
用户反馈:"点『0 秒后重启』之后十几秒什么都不动,其实在备份 156MB,我会以为按钮没反应 / 0 秒是假的 / 有 bug。"
|
|
39
|
+
|
|
40
|
+
- **四个阶段立刻可见**:`waiting`「正在等待所有会话空闲…」→ `snapshotting`「正在备份配置和插件(预计 X 秒,已完成 Y MB)…」→ `restarting`「正在重启,稍后自动继续…」→ `back`「已回来,接着干」。阶段写在 checkpoint(`restartPhase`)里,点击的**同一次调用**就把 `waiting` 落盘并 persist,界面下一次轮询(2 秒一次,且点击后立即再拉一次)就能看到。
|
|
41
|
+
- **备份进度是真的**:`createBackup` 改成**文件级拷贝循环**(原来是 `fs.cpSync` 一次性拷完,既没进度也不能取消),边拷边报 `{ files, bytes, total }`,`total` 是拷之前量出来的真实总量;拷贝过程**每 120ms / 每 40 个文件让出一次事件循环**,所以取消请求能在拷贝途中被服务,进度也能在拷贝途中到界面。
|
|
42
|
+
- **备份阶段可取消**:取消后拷贝在下一个文件处停下,**半成品快照目录被删掉**(不留一个看着能还原的假备份),请求被撤回、宿主不重启;**进入 `restarting` 后取消被拒绝**(HTTP 409 + `reason: not-cancellable` + 原因),界面按钮置灰并说明"正在重启,已经不能取消了"。
|
|
43
|
+
- **两处都能读到**:`/__dsh_autorestart/health?state=1` 与 `/health` 都带 `restartPhase`(阶段、双语文字、可取消性、`slow`)和 `progress`(files/bytes/total/percent/ageMs)。
|
|
44
|
+
- **慢就明说**:备份超过 300 秒,或字节数 60 秒没动 → 显示「备份比预期慢(可能磁盘很忙),还在继续…」,绝不假装正常进度。
|
|
45
|
+
- **界面**:composer 那颗 pill 多一行阶段文字(含进度),设置页「重启设置」组标题下常驻一行同样的状态(pill 只在有待办重启时出现,设置页那行一直都在)。
|
|
46
|
+
|
|
47
|
+
### 验证
|
|
48
|
+
|
|
49
|
+
- 单元测试 **740 → 753 项全绿**(新增 `test/watchdog-disarm.test.mjs`:事故复现、开机自动撤销、marker 读写与 read-back、失败/重试/留痕、按 pending 选调度项、诊断文本、流程里"健康就不回滚"、问不到端口多等一轮)。
|
|
50
|
+
- 阶段化 UI 再加 **`test/restart-phase.test.mjs`(6 项,累计 760 项全绿)**:①点击后 1 秒内可读到 `phase='waiting'`(含 state 文件与 state 路由两处)②备份中 `progress.bytes` 单调增长且等于最终写入量 ③备份中取消 → 半成品目录消失、宿主没重启、请求被撤回 ④`restarting` 阶段取消被 409 拒绝并给出原因 ⑤慢拷贝两种文案 ⑥里程碑:健康拷贝报真实 MB 与预计秒数、没有测量前不报任何数字。
|
|
51
|
+
- 演练四场景 + 三档调度真机触发全部 exit 0。
|
|
52
|
+
|
|
3
53
|
## 0.8.1 — 2026-10-29
|
|
4
54
|
|
|
5
55
|
**修复打包遗漏**:0.8.0 的 npm 包按 `package.json` 的 `files` 白名单打包,而那份白名单只收了 `scripts/*.ps1` 与 `scripts/*.cmd`——**三个 POSIX 自救脚本(`restore-profile.sh` / `emergency-disable.sh` / `drill-rescue.sh`)、`scripts/README.md`、`scripts/rescue-strings.zh.json` 全都没进包**。Linux / WSL 用户装完 0.8.0 后,文档让他去双击/运行 `scripts/restore-profile.sh`,那个文件却不存在。
|
package/README.md
CHANGED
|
@@ -198,7 +198,7 @@ what it adds before you install it.
|
|
|
198
198
|
|
|
199
199
|
## Install
|
|
200
200
|
|
|
201
|
-
Current version: **0.8.
|
|
201
|
+
Current version: **0.8.2**.
|
|
202
202
|
|
|
203
203
|
From npm — the package is published under the name `dsh-autorestart`:
|
|
204
204
|
|
|
@@ -407,6 +407,42 @@ hold the restart back:
|
|
|
407
407
|
- **Where to look:** `GET /health?state=1`, `GET /health?gate=1` and `continuum_status` all report
|
|
408
408
|
`queuedGraceMs`.
|
|
409
409
|
|
|
410
|
+
### The four stages of a restart (what you see between the click and the comeback)
|
|
411
|
+
|
|
412
|
+
A restart is not instant, and for a while it used to look like nothing at all: the click was
|
|
413
|
+
accepted, then the host spent ten-odd seconds writing a pre-restart snapshot (a desktop profile
|
|
414
|
+
with its plugins is ~150 MB). The button looked dead. It is not — and now it says so:
|
|
415
|
+
|
|
416
|
+
| Stage | What the interface shows | Where the data comes from |
|
|
417
|
+
|---|---|---|
|
|
418
|
+
| `waiting` | "Waiting for every conversation to be idle…" (+ how many are busy) | the idle gate's blockers |
|
|
419
|
+
| `snapshotting` | "Backing up the configuration and the plugins (about 12s left, 78 MB done)…" | the REAL copy loop: files and bytes written, measured total |
|
|
420
|
+
| `restarting` | "Restarting; the work continues by itself in a moment…" | the host is on its way out |
|
|
421
|
+
| `back` | "Back — carrying on where we left off" | the new host, once it boots |
|
|
422
|
+
|
|
423
|
+
Three things make this honest rather than decorative:
|
|
424
|
+
|
|
425
|
+
1. **The first stage is written by the click itself** and persisted at once, so the pill (which
|
|
426
|
+
polls the `state=1` route every 2 s, and immediately after a click) has something true to show
|
|
427
|
+
well inside the first second.
|
|
428
|
+
2. **The progress is measured, never animated.** The copy reports the files and bytes that really
|
|
429
|
+
landed, against a total measured before it started; the estimate only appears once there is a
|
|
430
|
+
real speed to divide. When there is no measurement yet, the text claims no number at all.
|
|
431
|
+
3. **A slow copy is announced** — after five minutes, or when the byte counter has not moved for a
|
|
432
|
+
minute, the line becomes "The backup is slower than expected (the disk may be busy); it is still
|
|
433
|
+
running…". That is the opposite of a progress bar that pretends.
|
|
434
|
+
|
|
435
|
+
**Cancelling.** While the host is still waiting or copying, cancelling stops the copy between two
|
|
436
|
+
files, deletes the half-written snapshot (no fake backup is left behind), withdraws the request and
|
|
437
|
+
the host keeps running. Once the stage is `restarting` a cancel is refused (HTTP 409 with the
|
|
438
|
+
reason) and the button is greyed out: the host is already on its way out, and a cancel there would
|
|
439
|
+
be a promise this plugin cannot keep.
|
|
440
|
+
|
|
441
|
+
Both `…/health` and `…/health?state=1` carry `restartPhase` (stage, bilingual text, `cancellable`,
|
|
442
|
+
`slow`) and `progress` (files, bytes, total, percent, age), and the settings page keeps one status
|
|
443
|
+
line next to the restart settings — the pill is only there while a restart is pending, the settings
|
|
444
|
+
line is always there.
|
|
445
|
+
|
|
410
446
|
### What you see while the restart waits a long time
|
|
411
447
|
|
|
412
448
|
When the hard deadline (`deferHardMs`) is over and a conversation is still running, the plugin does
|
|
@@ -568,11 +604,24 @@ plugin actually does.
|
|
|
568
604
|
|
|
569
605
|
The flow is the same for every tier (`lib/watchdog.js`): wait for the new host
|
|
570
606
|
to write the ready marker → ask again (a slow host is not a dead host) → is an
|
|
571
|
-
instance alive? (then there is nothing to restore) →
|
|
572
|
-
(
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
607
|
+
instance alive? (then there is nothing to restore) → **ask the health route of
|
|
608
|
+
the host itself** (only a host that does not answer may be rolled back; when
|
|
609
|
+
there is no port to ask, one more round is waited and the markers are looked at
|
|
610
|
+
again) → take the shared lock (`rollback.lock`, so exactly one of us restores) →
|
|
611
|
+
restore the snapshot → start the host again → wait for the ready marker once
|
|
612
|
+
more. At most **one** restore and **one** start; then the watchdog stops and
|
|
613
|
+
writes what happened into `<stateDir>/last-watchdog-report.json` (which tier it
|
|
614
|
+
used, `healthCheck`, and why).
|
|
615
|
+
|
|
616
|
+
**The disarming is the host's job, and it does it at boot.** As soon as this
|
|
617
|
+
plugin is loaded, it writes the ready marker, removes `watchdog-pending.json` and
|
|
618
|
+
deletes its own scheduling item (retried with a backoff, verified afterwards by
|
|
619
|
+
asking the task scheduler, recorded in `<stateDir>/watchdog-disarm.json`). It
|
|
620
|
+
does not wait for anybody to call the health route — the watchdog must be
|
|
621
|
+
disarmed even when nobody opens the interface, and a disarm that fails is
|
|
622
|
+
visible in `health`, in the diagnostics text and in that record instead of
|
|
623
|
+
being silent. That is the fix of 0.8.2: an error swallowed by the health route
|
|
624
|
+
left an armed watchdog behind a healthy host.
|
|
576
625
|
|
|
577
626
|
**Only one watchdog at a time.** There is an older watcher in this plugin as
|
|
578
627
|
well (it restores the snapshot but does **not** start the host again). Both take
|
package/client.js
CHANGED
|
@@ -225,6 +225,7 @@
|
|
|
225
225
|
cancelPanelAria: '待重启详情',
|
|
226
226
|
cancelButton: '取消我的这次重启',
|
|
227
227
|
cancelButtonAria: '取消我的这次重启',
|
|
228
|
+
cancelLocked: '正在重启,已经不能取消了',
|
|
228
229
|
cancelConfirmTitle: '取消你的这次重启?',
|
|
229
230
|
cancelConfirmOnlyMe: '取消后这次重启就不会发生了。',
|
|
230
231
|
cancelConfirmOthers: '还有 {n} 个会话也在等这次重启——取消只影响你自己,他们仍会重启。',
|
|
@@ -548,6 +549,7 @@
|
|
|
548
549
|
cancelPanelAria: 'Pending restart details',
|
|
549
550
|
cancelButton: 'Cancel my restart',
|
|
550
551
|
cancelButtonAria: 'Cancel my restart',
|
|
552
|
+
cancelLocked: 'Restarting — this can no longer be cancelled',
|
|
551
553
|
cancelConfirmTitle: 'Cancel your restart?',
|
|
552
554
|
cancelConfirmOnlyMe: 'After this, the restart will not happen.',
|
|
553
555
|
cancelConfirmOthers: '{n} other session(s) are waiting for this restart too — cancelling only affects you; they will still restart.',
|
|
@@ -2134,6 +2136,44 @@
|
|
|
2134
2136
|
// factory-scoped handle instead of a standard `t` prop.
|
|
2135
2137
|
let translate = null
|
|
2136
2138
|
|
|
2139
|
+
/**
|
|
2140
|
+
* THE STAGE LINE of a running restart, in the reader's language.
|
|
2141
|
+
*
|
|
2142
|
+
* The Host sends BOTH languages (`text: { zh, en }`) with every stage, so
|
|
2143
|
+
* the pill and the settings page need no locale key that an older Host
|
|
2144
|
+
* might not ship — and there is exactly ONE sentence per stage instead of
|
|
2145
|
+
* two that could drift apart.
|
|
2146
|
+
*
|
|
2147
|
+
* @param {object|null} stage the `restartPhase` of the state payload
|
|
2148
|
+
* @returns {string} the sentence, or '' when there is no restart
|
|
2149
|
+
*/
|
|
2150
|
+
function restartStageText(stage) {
|
|
2151
|
+
if (!stage || typeof stage !== 'object') return ''
|
|
2152
|
+
const text = stage.text && typeof stage.text === 'object' ? stage.text : null
|
|
2153
|
+
if (!text) return ''
|
|
2154
|
+
const lang = typeof document !== 'undefined' && document.documentElement && document.documentElement.lang
|
|
2155
|
+
? String(document.documentElement.lang)
|
|
2156
|
+
: (typeof navigator !== 'undefined' && navigator.language ? String(navigator.language) : 'en')
|
|
2157
|
+
const zh = lang.toLowerCase().indexOf('zh') === 0
|
|
2158
|
+
const picked = zh ? (text.zh || text.en) : (text.en || text.zh)
|
|
2159
|
+
return typeof picked === 'string' ? picked : ''
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
/**
|
|
2163
|
+
* May this restart still be called off?
|
|
2164
|
+
*
|
|
2165
|
+
* It is the HOST that decides (`cancellable`): once the host is on its
|
|
2166
|
+
* way out, a cancel button would be a promise the plugin cannot keep, so
|
|
2167
|
+
* it is shown disabled together with the reason.
|
|
2168
|
+
*
|
|
2169
|
+
* @param {object|null} stage
|
|
2170
|
+
* @returns {boolean}
|
|
2171
|
+
*/
|
|
2172
|
+
function restartStageLocked(stage) {
|
|
2173
|
+
if (!stage || typeof stage !== 'object') return false
|
|
2174
|
+
return stage.cancellable === false
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2137
2177
|
/**
|
|
2138
2178
|
* ONE shared poller, shared by every surface that shows the pending
|
|
2139
2179
|
* restart state (the composer pill and the sidebar session-row dot).
|
|
@@ -2613,10 +2653,14 @@
|
|
|
2613
2653
|
cursor: 'pointer',
|
|
2614
2654
|
}
|
|
2615
2655
|
|
|
2616
|
-
// The
|
|
2617
|
-
//
|
|
2618
|
-
|
|
2619
|
-
|
|
2656
|
+
// The same button, greyed out: used while the host is already restarting,
|
|
2657
|
+
// when a cancel can no longer be honoured.
|
|
2658
|
+
const PANEL_BUTTON_DISABLED_STYLE = {
|
|
2659
|
+
...PANEL_BUTTON_STYLE,
|
|
2660
|
+
cursor: 'not-allowed',
|
|
2661
|
+
opacity: 0.55,
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2620
2664
|
const PARK_BUTTON_STYLE = {
|
|
2621
2665
|
...PILL_STYLE,
|
|
2622
2666
|
height: 18,
|
|
@@ -2916,6 +2960,12 @@
|
|
|
2916
2960
|
if (!waiting && !withdrawn) return null
|
|
2917
2961
|
|
|
2918
2962
|
const blockedBy = Array.isArray(info.blockedBy) ? info.blockedBy : []
|
|
2963
|
+
// WHICH STAGE THE RESTART IS IN (waiting / backing up / restarting) and
|
|
2964
|
+
// whether a cancel is still honest. Both come from the Host: the pill
|
|
2965
|
+
// never guesses the stage and never invents progress.
|
|
2966
|
+
const stage = info && info.restartPhase && typeof info.restartPhase === 'object' ? info.restartPhase : null
|
|
2967
|
+
const stageText = restartStageText(stage)
|
|
2968
|
+
const cancelLocked = restartStageLocked(stage)
|
|
2919
2969
|
// The TASK of every blocker: a session and its own subagents are ONE
|
|
2920
2970
|
// task (the sidebar counts them once). The keys are aligned with the
|
|
2921
2971
|
// list, so the filter below can ask "is this MY task?".
|
|
@@ -3233,16 +3283,22 @@
|
|
|
3233
3283
|
'button',
|
|
3234
3284
|
{
|
|
3235
3285
|
type: 'button',
|
|
3236
|
-
style: PANEL_BUTTON_STYLE,
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3286
|
+
style: cancelLocked ? PANEL_BUTTON_DISABLED_STYLE : PANEL_BUTTON_STYLE,
|
|
3287
|
+
// A cancel once the host is restarting would be a promise the
|
|
3288
|
+
// plugin cannot keep: the button is greyed out and the reason
|
|
3289
|
+
// is in its tooltip.
|
|
3290
|
+
title: cancelLocked
|
|
3291
|
+
? t('cancelLocked') + (stage && stage.cancelReason ? ' · ' + stage.cancelReason : '')
|
|
3292
|
+
: t('cancelButtonAria'),
|
|
3293
|
+
'aria-label': cancelLocked ? t('cancelLocked') : t('cancelButtonAria'),
|
|
3294
|
+
'aria-disabled': cancelLocked ? 'true' : 'false',
|
|
3295
|
+
disabled: cancelling || cancelLocked,
|
|
3240
3296
|
onClick: (event) => {
|
|
3241
3297
|
if (event && typeof event.stopPropagation === 'function') event.stopPropagation()
|
|
3242
3298
|
setConfirmOpen(true)
|
|
3243
3299
|
},
|
|
3244
3300
|
},
|
|
3245
|
-
t('cancelButton'),
|
|
3301
|
+
cancelLocked ? t('cancelLocked') : t('cancelButton'),
|
|
3246
3302
|
),
|
|
3247
3303
|
)
|
|
3248
3304
|
: null
|
|
@@ -3271,6 +3327,21 @@
|
|
|
3271
3327
|
},
|
|
3272
3328
|
h('span', { style: dotStyle(DOT_STYLE, pulsing), 'aria-hidden': 'true' }),
|
|
3273
3329
|
h('span', { style: TEXT_STYLE }, h('b', null, t('label')), status),
|
|
3330
|
+
// ONE MORE LINE: WHICH STAGE THE RESTART IS IN, with the real
|
|
3331
|
+
// progress of the backup. It is the answer to "I clicked restart
|
|
3332
|
+
// and nothing happened" — the pill says "waiting", then "backing up
|
|
3333
|
+
// 120 MB of 156 MB", then "restarting".
|
|
3334
|
+
stageText === ''
|
|
3335
|
+
? null
|
|
3336
|
+
: h(
|
|
3337
|
+
'span',
|
|
3338
|
+
{
|
|
3339
|
+
style: LONG_WAIT_STYLE,
|
|
3340
|
+
'data-dsh-autorestart-stage': String(stage && stage.phase ? stage.phase : ''),
|
|
3341
|
+
title: stageText,
|
|
3342
|
+
},
|
|
3343
|
+
stageText,
|
|
3344
|
+
),
|
|
3274
3345
|
// One more line for the long wait: how long the restart has been
|
|
3275
3346
|
// waiting and how many tasks are running. It is only there when the
|
|
3276
3347
|
// Host says so. The line carries NO name at all — it is one
|
|
@@ -4897,6 +4968,28 @@
|
|
|
4897
4968
|
{ className: 'dsh-autorestart-settings__group-title' },
|
|
4898
4969
|
t('settings.groups.restart'),
|
|
4899
4970
|
),
|
|
4971
|
+
// THE STATUS LINE: what the restart is doing right now, next to the
|
|
4972
|
+
// settings that control it. It lives here (the pill only exists
|
|
4973
|
+
// while a restart is pending), so a slow backup, "restarting" and
|
|
4974
|
+
// "back — carrying on" are visible where the user went looking.
|
|
4975
|
+
restartStageText(info && info.restartPhase) === ''
|
|
4976
|
+
? null
|
|
4977
|
+
: h(
|
|
4978
|
+
'div',
|
|
4979
|
+
{
|
|
4980
|
+
className: 'dsh-autorestart-settings__status',
|
|
4981
|
+
'data-dsh-autorestart-stage': String(
|
|
4982
|
+
info && info.restartPhase && info.restartPhase.phase ? info.restartPhase.phase : '',
|
|
4983
|
+
),
|
|
4984
|
+
style: {
|
|
4985
|
+
margin: '2px 0 8px',
|
|
4986
|
+
fontSize: 12,
|
|
4987
|
+
lineHeight: '16px',
|
|
4988
|
+
color: 'var(--dsw-alias-label-secondary, inherit)',
|
|
4989
|
+
},
|
|
4990
|
+
},
|
|
4991
|
+
restartStageText(info && info.restartPhase),
|
|
4992
|
+
),
|
|
4900
4993
|
h(
|
|
4901
4994
|
'label',
|
|
4902
4995
|
{ className: 'dsh-autorestart-settings__field' },
|
package/docs/README.zh.md
CHANGED
|
@@ -183,7 +183,9 @@ session resume on boot, durable-goal re-arm, and an agent-callable idle-aware gr
|
|
|
183
183
|
|
|
184
184
|
**由一个函数决定**(`lib/watchdog-tier.js`),设置页、diagnostics 和看门狗自己问的都是这同一个函数 —— 所以"这台机器没有自动回滚"这句警告**不可能**和插件实际做的事对不上。
|
|
185
185
|
|
|
186
|
-
三档跑的是**同一套**流程(`lib/watchdog.js`):等新宿主写下 ready 标记 → **再问一次**(起得慢不等于死了)→ 有实例活着吗(活着就没什么好还原的)→ 抢共用的锁(`rollback.lock`,保证只有一个在还原)→ 还原快照 → **再拉起宿主** → 再等一次 ready。最多**一次**还原 + **一次**拉起;然后看门狗停下,把结果写进 `<stateDir>/last-watchdog-report.json
|
|
186
|
+
三档跑的是**同一套**流程(`lib/watchdog.js`):等新宿主写下 ready 标记 → **再问一次**(起得慢不等于死了)→ 有实例活着吗(活着就没什么好还原的)→ **直接问宿主自己的 health 路由**(只要它在应答就绝不还原;一个端口都问不到时,再等一轮、复查标记,仍然没动静才允许还原)→ 抢共用的锁(`rollback.lock`,保证只有一个在还原)→ 还原快照 → **再拉起宿主** → 再等一次 ready。最多**一次**还原 + **一次**拉起;然后看门狗停下,把结果写进 `<stateDir>/last-watchdog-report.json`(用的是哪一档、`healthCheck` 是什么、为什么)。
|
|
187
|
+
|
|
188
|
+
**撤销看门狗是宿主的责任,而且开机就做。** 插件一加载就会写下 ready 标记、删掉 `watchdog-pending.json`、并把自己注册的调度项删掉(带退避重试,删完回查计划任务,过程写进 `<stateDir>/watchdog-disarm.json`)。它**不等任何人来打 health 接口** —— 没人开界面时看门狗也必须被撤掉;撤销失败会出现在 `health`、诊断文本和那份记录里,而不是悄悄消失。这就是 0.8.2 修的那个线上缺陷:一个被 health 路由吞掉的异常,让一个健康宿主背后留着一只看门狗。
|
|
187
189
|
|
|
188
190
|
**同一时刻只有一个监护者。** 插件里还有一版旧的看门狗(只还原快照、**不拉起**宿主)。两者抢同一把锁,同时开反而更糟:抢不到锁的那个会放弃 —— 要是新版抢输了,结果就是**配置还原了,但没有任何东西在跑**。所以插件只在一个地方做这个决定(`lib/watchdog-arbiter.js`):**新版能用就是新版接管**(v2),新版用不了(这台机器没有档位、看门狗被关掉、或者注册失败)才**回落旧版**(v1)。报告里写着当时是谁在监护(`arbiter: "v2"` 或 `"v1"`),一眼就能看出来。
|
|
189
191
|
|
|
@@ -328,7 +330,7 @@ agent/status(running) ──► 写 checkpoint(<DSH_HOME>\storages\dsh-autores
|
|
|
328
330
|
|
|
329
331
|
## 安装
|
|
330
332
|
|
|
331
|
-
当前版本:**0.8.
|
|
333
|
+
当前版本:**0.8.2**。
|
|
332
334
|
|
|
333
335
|
```powershell
|
|
334
336
|
# 在 dsh 里(或对 Agent 说):把工作区里的这个包安装为 profile bundle
|