dsh-autorestart 0.7.0 → 0.8.0
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/README.md +137 -6
- package/client.js +387 -0
- package/cordis.patch.yml +7 -0
- package/docs/README.zh.md +83 -5
- package/docs/platform-matrix.md +92 -0
- package/docs/rescue-drill.md +606 -0
- package/index.js +598 -3
- package/lib/diagnostics.js +8 -0
- package/lib/rescue-paths.js +253 -0
- package/lib/rescue.js +596 -0
- package/lib/scheduler-capability.js +300 -0
- package/lib/watchdog-arbiter.js +117 -0
- package/lib/watchdog-schedule.js +310 -0
- package/lib/watchdog-tier.js +182 -0
- package/lib/watchdog.js +479 -0
- package/locale/en.json +28 -1
- package/locale/zh.json +28 -1
- package/package.json +1 -1
- package/scripts/README.md +65 -0
- package/scripts/drill-rescue.ps1 +508 -0
- package/scripts/emergency-toggle.ps1 +136 -65
- package/scripts/restore-profile.cmd +44 -0
- package/scripts/restore-profile.ps1 +608 -0
- package/scripts/run-tests.ps1 +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,13 @@ Chinese version: [README.zh.md](docs/README.zh.md)
|
|
|
13
13
|
|
|
14
14
|
`dsh-autorestart` turns a DeepSeek Harness restart into a resumable event instead of a dead end.
|
|
15
15
|
|
|
16
|
+
**New in 0.7.0** — it now also knows *how it was started*: the desktop client, somebody else's
|
|
17
|
+
supervisor (a shell script, systemd, a container, pm2, a Windows scheduled task), or a bare process
|
|
18
|
+
nobody manages. It never starts a second DSH next to a running one, and when it cannot tell it does
|
|
19
|
+
nothing instead of guessing a command. The settings page shows what is really inside the
|
|
20
|
+
pre-restart backups (count, local time, size, path), and carries a **Report a problem** entry that
|
|
21
|
+
copies the diagnostics and opens a pre-filled GitHub issue.
|
|
22
|
+
|
|
16
23
|
An agent can request a graceful restart. The plugin waits until every conversation is idle and the
|
|
17
24
|
host has been quiet for the settle window, then it restarts the host. Once the host is back, the
|
|
18
25
|
interrupted sessions resume automatically: a session that owns a durable goal gets that goal
|
|
@@ -98,8 +105,35 @@ writes that sentence and the matching command into the settings page.
|
|
|
98
105
|
|
|
99
106
|
### If DSH does not come up
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
**First work out which situation you are in**: if the whole environment is broken (a changed
|
|
109
|
+
configuration, another plugin, a bad upgrade) → go back to the last good state; if **only this
|
|
110
|
+
plugin** is suspect → take this plugin out instead.
|
|
111
|
+
|
|
112
|
+
**And use the scripts of YOUR platform** — a Linux machine has no `.cmd` file, and a Windows
|
|
113
|
+
machine has no `.sh`:
|
|
114
|
+
|
|
115
|
+
| Your machine | back to the last good state | take this plugin out |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| **Windows** (the desktop app, `dsh web` on Windows) | double-click `scripts/restore-profile.cmd` | double-click `scripts/emergency-disable.cmd` |
|
|
118
|
+
| **Linux / WSL / macOS** (`dsh web`) | `sh scripts/restore-profile.sh` | `sh scripts/emergency-disable.sh` |
|
|
119
|
+
|
|
120
|
+
Two scripts per platform, and they do different jobs:
|
|
121
|
+
|
|
122
|
+
- **`restore-profile.cmd` / `restore-profile.sh` — back to the last good state.** It puts the
|
|
123
|
+
newest snapshot that carries a manifest back over the profile, and leaves the broken one behind
|
|
124
|
+
as `profile.broken-<timestamp>` (moved aside, not deleted). It does not restart DSH for you.
|
|
125
|
+
- **`emergency-disable.cmd` / `emergency-disable.sh` — take this plugin out and change nothing
|
|
126
|
+
else.** It removes only `dsh-autorestart` from the profile's bundle list and keeps a backup of
|
|
127
|
+
the file.
|
|
128
|
+
|
|
129
|
+
**Which one:** "back to the last good state" → `restore-profile`; "I only suspect this plugin" →
|
|
130
|
+
`emergency-disable`. The boundaries (the profile directory has to exist, a snapshot with a
|
|
131
|
+
manifest is required, DSH is never restarted for you, the `broken-` folder stays) are written down
|
|
132
|
+
in [`scripts/README.md`](scripts/README.md).
|
|
133
|
+
|
|
134
|
+
**What each platform can do** (and what it cannot) is spelled out in
|
|
135
|
+
[`docs/platform-matrix.md`](docs/platform-matrix.md) — the settings page shows the paths of the
|
|
136
|
+
scripts that really exist on the machine you are on.
|
|
103
137
|
|
|
104
138
|
### What has really been tested
|
|
105
139
|
|
|
@@ -136,9 +170,8 @@ The settings page has a "**Report a problem**" group at the bottom:
|
|
|
136
170
|
- When the browser refuses the clipboard (an insecure context, or no permission), the plugin shows
|
|
137
171
|
the diagnostics in a **selectable** box and says so — a report never dies because the browser said
|
|
138
172
|
no.
|
|
139
|
-
- No GitHub account? The same group
|
|
140
|
-
|
|
141
|
-
paste).
|
|
173
|
+
- No GitHub account? The same group shows the mailbox (252170229@qq.com) as plain text, plus a
|
|
174
|
+
"Copy the diagnostics" button.
|
|
142
175
|
|
|
143
176
|
## What it occupies in DSH
|
|
144
177
|
|
|
@@ -165,7 +198,7 @@ what it adds before you install it.
|
|
|
165
198
|
|
|
166
199
|
## Install
|
|
167
200
|
|
|
168
|
-
Current version: **0.
|
|
201
|
+
Current version: **0.8.0**.
|
|
169
202
|
|
|
170
203
|
From npm — the package is published under the name `dsh-autorestart`:
|
|
171
204
|
|
|
@@ -485,6 +518,75 @@ own health route. A slow start inside `rollbackTimeoutMs` is never treated as a
|
|
|
485
518
|
healthy host is never touched. The restore itself is bounded as well: every copy step gives up after
|
|
486
519
|
`rollbackCopyTimeoutMs` (default `120000`) and says so in the report.
|
|
487
520
|
|
|
521
|
+
### When the machine refuses a scheduled task (the automatic rollback is gone)
|
|
522
|
+
|
|
523
|
+
The safety net above hangs on a **one-off scheduled task**: the helper that brings the host back
|
|
524
|
+
after a restart is a task that fires a minute later. On a machine where such a task cannot be
|
|
525
|
+
created (a corporate policy, a locked-down account, a machine without the rights) the net is
|
|
526
|
+
silently missing.
|
|
527
|
+
|
|
528
|
+
So the plugin **checks the machine once** — the first time it runs there — and tells the truth in
|
|
529
|
+
the settings page:
|
|
530
|
+
|
|
531
|
+
- `Automatic rollback: available` — the net is there.
|
|
532
|
+
- `Automatic rollback: **not available**` — and exactly what is lost: *DSH will not recover by
|
|
533
|
+
itself when it does not come up after a restart*. The line points at the restore script in
|
|
534
|
+
"If DSH will not start", which is the way out.
|
|
535
|
+
|
|
536
|
+
**What this deliberately does NOT do:**
|
|
537
|
+
|
|
538
|
+
- **No fallback chain.** There is no "use the startup folder instead", no "use a systemd timer
|
|
539
|
+
instead", no second mechanism that is tried when the first one is refused.
|
|
540
|
+
- **No bypassing the policy.** The plugin does not look for a way around a machine that says no.
|
|
541
|
+
If the machine refuses, the answer is "not available" — and only that is reported.
|
|
542
|
+
|
|
543
|
+
A refused check is **never** a failed installation: the plugin works exactly as before, only the
|
|
544
|
+
safety net is missing. The check itself is harmless by construction — it registers a task that
|
|
545
|
+
fires in two minutes and does nothing (`cmd /c exit 0`), reads it back, deletes it, and reads it
|
|
546
|
+
back once more to prove that it is gone. It runs **once**, not on every visit of the settings page,
|
|
547
|
+
and it never leaves a task behind (not even when a step of it fails).
|
|
548
|
+
|
|
549
|
+
### The watchdog: the three ways it can be triggered
|
|
550
|
+
|
|
551
|
+
The rollback above needs somebody to *wait*: the host is restarted, and a
|
|
552
|
+
one-off thing has to notice that the new host never came up. That thing is the
|
|
553
|
+
**watchdog** (`scripts/watchdog.mjs`), and how it is triggered is the part
|
|
554
|
+
that differs between the machines — so the plugin picks the best way the
|
|
555
|
+
machine has and says which one it picked:
|
|
556
|
+
|
|
557
|
+
| Tier | Mechanism | Where |
|
|
558
|
+
|---|---|---|
|
|
559
|
+
| `scheduled-task` | a one-off Windows scheduled task (read back after it was registered, deleted completely afterwards) | the desktop app and `dsh web` on Windows |
|
|
560
|
+
| `systemd-run` | a systemd user timer (`systemd-run --user --on-active=…`), reset when it is over | `dsh web` on Linux, WSL with systemd |
|
|
561
|
+
| `detached-process` | a background process of our own (`setsid nohup sh -c 'sleep …; …'`), its pid in the state directory | containers, WSL without systemd |
|
|
562
|
+
| `none` | nothing on this machine | then there is **no automatic rollback**, and the settings page says so |
|
|
563
|
+
|
|
564
|
+
**One function decides it** (`lib/watchdog-tier.js`), and the settings page, the
|
|
565
|
+
diagnostics and the watchdog itself all ask that one function — so the warning
|
|
566
|
+
("this machine has no automatic rollback") can never drift away from what the
|
|
567
|
+
plugin actually does.
|
|
568
|
+
|
|
569
|
+
The flow is the same for every tier (`lib/watchdog.js`): wait for the new host
|
|
570
|
+
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) → take the shared lock
|
|
572
|
+
(`rollback.lock`, so exactly one of us restores) → restore the snapshot → start
|
|
573
|
+
the host again → wait for the ready marker once more. At most **one** restore
|
|
574
|
+
and **one** start; then the watchdog stops and writes what happened into
|
|
575
|
+
`<stateDir>/last-watchdog-report.json` (which tier it used, and why).
|
|
576
|
+
|
|
577
|
+
**Only one watchdog at a time.** There is an older watcher in this plugin as
|
|
578
|
+
well (it restores the snapshot but does **not** start the host again). Both take
|
|
579
|
+
the same lock, so running them together is worse than running one: the one that
|
|
580
|
+
loses the lock gives up — and if the new one lost, the profile would be restored
|
|
581
|
+
and **nothing would be running**. So the plugin decides once, in one place
|
|
582
|
+
(`lib/watchdog-arbiter.js`): the new watchdog takes over whenever it can be
|
|
583
|
+
armed at all, and the old one only runs when the new one cannot (no tier on this
|
|
584
|
+
machine, the watchdog is switched off, or the registration failed). The report
|
|
585
|
+
names the arbiter (`arbiter: "v2"` or `"v1"`), so it is visible at a glance who
|
|
586
|
+
was watching.
|
|
587
|
+
|
|
588
|
+
`watchdogEnabled: false` switches it off entirely.
|
|
589
|
+
|
|
488
590
|
### The access link after a restart
|
|
489
591
|
|
|
490
592
|
The boot token of `dsh web` is created **per process**, so the link the browser holds answers `401`
|
|
@@ -711,6 +813,35 @@ be cancelled — the card has a button, and the moon button itself becomes a tog
|
|
|
711
813
|
is armed. If a conversation starts working again during the countdown, the countdown goes back to
|
|
712
814
|
"waiting" and nothing is switched off.
|
|
713
815
|
|
|
816
|
+
### Which machine gets switched off?
|
|
817
|
+
|
|
818
|
+
The plugin works out where it runs and switches off **that machine** — and the label in the
|
|
819
|
+
settings says which one, so there is no guessing:
|
|
820
|
+
|
|
821
|
+
| Where DSH runs | What the shutdown switches off | The label you see |
|
|
822
|
+
|---|---|---|
|
|
823
|
+
| The Windows desktop app | that Windows computer | "Switch off this Windows computer" |
|
|
824
|
+
| `dsh web` on Windows | that Windows computer | "Switch off this Windows computer" |
|
|
825
|
+
| `dsh web` on a Linux host | that Linux host | "Shut down this Linux host" |
|
|
826
|
+
| **`dsh web` inside WSL** | **the whole Windows machine** (not the little WSL box!) | "The DSH inside WSL → switches off the whole Windows" |
|
|
827
|
+
| A container | nothing at all (there is no command) | "This machine has no reliable way to switch off" |
|
|
828
|
+
|
|
829
|
+
> ⚠️ **A shutdown countdown inside WSL means your whole machine goes down** — not just that
|
|
830
|
+
> web service. WSL is a small Linux environment *inside* Windows; shutting that environment
|
|
831
|
+
> down would only stop the distribution. To switch off the **host**, the plugin goes through
|
|
832
|
+
> the mounted drive: `shutdown.exe` (`/mnt/c/Windows/System32/shutdown.exe /s /f /t 0`), with
|
|
833
|
+
> `powershell.exe -Command "Stop-Computer -Force"` and `cmd.exe /c shutdown.exe /s /f /t 0`
|
|
834
|
+
> as the fallbacks. In other words: **the host goes off.**
|
|
835
|
+
|
|
836
|
+
**How far this is verified.** The *plan* is verified for every platform
|
|
837
|
+
(`test/sleep-plan-posix.test.mjs`: inside WSL the plan must take the "switch off the whole
|
|
838
|
+
Windows" branch, a plain Linux plan must not contain a single Windows command, and a machine
|
|
839
|
+
without a way to switch off has an empty command and **executes nothing**). The real power-off
|
|
840
|
+
is verified on **Windows**; on POSIX it is deliberately **not** switched off for real — a
|
|
841
|
+
common-sense fact is not worth ending every session on the machine for.
|
|
842
|
+
|
|
843
|
+
The full picture per platform is in [`docs/platform-matrix.md`](docs/platform-matrix.md).
|
|
844
|
+
|
|
714
845
|
### Shutdown and restart: who goes first
|
|
715
846
|
|
|
716
847
|
**The short version.** Pressing the moon button means "switch the machine off when the work is
|
package/client.js
CHANGED
|
@@ -377,6 +377,31 @@
|
|
|
377
377
|
'settings.backup.withPlugins': '含已装插件',
|
|
378
378
|
'settings.backup.withoutPlugins': '只有配置',
|
|
379
379
|
'settings.groups.report': '报告问题',
|
|
380
|
+
'settings.groups.rescue': '出事了怎么办',
|
|
381
|
+
'settings.rescue.hint': 'DSH 打不开的时候,用下面这两个脚本救自己(都不用 DSH 跑起来)。',
|
|
382
|
+
'settings.rescue.restoreProfile': '回到上一次好状态',
|
|
383
|
+
'settings.rescue.emergencyDisable': '只把这个插件摘掉',
|
|
384
|
+
'settings.rescue.path': '路径',
|
|
385
|
+
'settings.rescue.copyPath': '复制路径',
|
|
386
|
+
'settings.rescue.copied': '路径已复制',
|
|
387
|
+
'settings.rescue.copyFailed': '复制失败,请手动选中上面的路径',
|
|
388
|
+
'settings.rescue.reveal': '打开所在文件夹',
|
|
389
|
+
'settings.rescue.which': '怎么选:整个环境坏了(改了配置、装了别的插件、升级崩了)→ 用「回到上一次好状态」;只怀疑这个插件 → 用「只把这个插件摘掉」。',
|
|
390
|
+
'settings.rescue.warning': '重要:这两个脚本是在 DSH 打不开的时候用的。DSH 正开着的时候用「回到上一次好状态」,会把你现在正在用的配置覆盖掉。',
|
|
391
|
+
'settings.rescue.rollbackOk': '自动回滚:可用(重启后起不来会自动回到上一个好状态)',
|
|
392
|
+
'settings.rescue.rollbackBlocked': '自动回滚:**不可用** —— 这台电脑不允许创建计划任务。**会丢失:重启后 DSH 起不来时不会自动恢复**;出问题时请双击「出事了怎么办」里的还原脚本。',
|
|
393
|
+
'settings.rescue.rollbackUnknown': '自动回滚:还不知道(这台机器的能力还没检查过)',
|
|
394
|
+
'settings.rescue.watchdogTier': '看门狗的触发方式:{tier}',
|
|
395
|
+
'settings.rescue.watchdogLast': '上一次看门狗的结果:{result}',
|
|
396
|
+
'settings.rescue.tierScheduledTask': 'Windows 计划任务',
|
|
397
|
+
'settings.rescue.tierSystemdRun': 'systemd 定时器',
|
|
398
|
+
'settings.rescue.tierDetachedProcess': '自建后台进程',
|
|
399
|
+
'settings.rescue.tierNone': '(这台机器没有可用机制)',
|
|
400
|
+
'settings.rescue.shortcut': '在桌面放一个快捷方式',
|
|
401
|
+
'settings.rescue.shortcutDone': '已在桌面',
|
|
402
|
+
'settings.rescue.noDesktop': '这台机器上没有桌面目录,所以放不了快捷方式(脚本的路径在上面,可以手动复制过去)。',
|
|
403
|
+
'settings.rescue.shortcutFailed': '快捷方式没放成:{message}',
|
|
404
|
+
'settings.rescue.revealFailed': '文件夹没打开:{message}',
|
|
380
405
|
'settings.report.label': '遇到问题了?',
|
|
381
406
|
'settings.report.github': '复制诊断信息并去 GitHub 提问题',
|
|
382
407
|
'settings.report.hint': '诊断信息里没有你的访问令牌,也没有任何密钥;把它贴进 issue,我们就能看出问题出在哪。',
|
|
@@ -675,6 +700,31 @@
|
|
|
675
700
|
'settings.backup.withPlugins': 'includes the installed plugins',
|
|
676
701
|
'settings.backup.withoutPlugins': 'configuration only',
|
|
677
702
|
'settings.groups.report': 'Report a problem',
|
|
703
|
+
'settings.groups.rescue': 'If DSH will not start',
|
|
704
|
+
'settings.rescue.hint': 'When DSH does not come up, these two scripts rescue it for you (neither of them needs DSH running).',
|
|
705
|
+
'settings.rescue.restoreProfile': 'Back to the last good state',
|
|
706
|
+
'settings.rescue.emergencyDisable': 'Take this plugin out only',
|
|
707
|
+
'settings.rescue.path': 'Path',
|
|
708
|
+
'settings.rescue.copyPath': 'Copy the path',
|
|
709
|
+
'settings.rescue.copied': 'The path is on the clipboard',
|
|
710
|
+
'settings.rescue.copyFailed': 'The copy failed — please select the path above by hand',
|
|
711
|
+
'settings.rescue.reveal': 'Open the folder',
|
|
712
|
+
'settings.rescue.which': 'Which one: the whole environment is broken (a changed configuration, another plugin, a bad upgrade) → "Back to the last good state"; only this plugin is suspect → "Take this plugin out only".',
|
|
713
|
+
'settings.rescue.warning': 'Important: these two scripts are for when DSH does not come up. Using "Back to the last good state" while DSH is running overwrites the configuration you are working with right now.',
|
|
714
|
+
'settings.rescue.rollbackOk': 'Automatic rollback: available (a host that does not come up after a restart goes back to the last good state)',
|
|
715
|
+
'settings.rescue.rollbackBlocked': 'Automatic rollback: **not available** — this computer does not allow creating scheduled tasks. **What you lose: DSH will NOT recover by itself when it does not come up after a restart**; when it breaks, double-click the restore script in "If DSH will not start".',
|
|
716
|
+
'settings.rescue.rollbackUnknown': 'Automatic rollback: not known yet (this machine has not been checked)',
|
|
717
|
+
'settings.rescue.watchdogTier': 'How the watchdog is triggered: {tier}',
|
|
718
|
+
'settings.rescue.watchdogLast': 'The last watchdog run: {result}',
|
|
719
|
+
'settings.rescue.tierScheduledTask': 'a Windows scheduled task',
|
|
720
|
+
'settings.rescue.tierSystemdRun': 'a systemd timer',
|
|
721
|
+
'settings.rescue.tierDetachedProcess': 'a detached background process',
|
|
722
|
+
'settings.rescue.tierNone': '(this machine has no way to run it)',
|
|
723
|
+
'settings.rescue.shortcut': 'Put a shortcut on the desktop',
|
|
724
|
+
'settings.rescue.shortcutDone': 'It is on the desktop',
|
|
725
|
+
'settings.rescue.noDesktop': 'This machine has no desktop directory, so no shortcut can be placed (the paths are above — copy them by hand).',
|
|
726
|
+
'settings.rescue.shortcutFailed': 'The shortcut was not placed: {message}',
|
|
727
|
+
'settings.rescue.revealFailed': 'The folder did not open: {message}',
|
|
678
728
|
'settings.report.label': 'Something wrong?',
|
|
679
729
|
'settings.report.github': 'Copy the diagnostics and open a GitHub issue',
|
|
680
730
|
'settings.report.hint': 'The diagnostics carry no access token and no secrets — paste them into the issue and we can see what went wrong.',
|
|
@@ -1818,6 +1868,73 @@
|
|
|
1818
1868
|
return { ok: false, fallback: true, text: source };
|
|
1819
1869
|
}
|
|
1820
1870
|
|
|
1871
|
+
/**
|
|
1872
|
+
* Must the warning about the missing automatic rollback be shown now?
|
|
1873
|
+
*
|
|
1874
|
+
* The warning belongs to ONE moment: the machine has NO way to run the
|
|
1875
|
+
* watchdog at all (tier 'none'), and the user has not seen it yet (the Host
|
|
1876
|
+
* recorded `warnedAt`). A machine that is blocked and stays blocked does
|
|
1877
|
+
* not nag.
|
|
1878
|
+
*
|
|
1879
|
+
* @param {object|null} capabilities
|
|
1880
|
+
* @returns {boolean}
|
|
1881
|
+
*/
|
|
1882
|
+
function watchdogNoticeFor(capabilities) {
|
|
1883
|
+
if (!capabilities || typeof capabilities !== 'object') return false
|
|
1884
|
+
if (capabilities.tier !== 'none') return false
|
|
1885
|
+
return typeof capabilities.warnedAt !== 'number'
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
/**
|
|
1889
|
+
* The name of the tier, in the language of the user.
|
|
1890
|
+
*
|
|
1891
|
+
* @param {Function} t the translator
|
|
1892
|
+
* @param {string} tier
|
|
1893
|
+
* @returns {string}
|
|
1894
|
+
*/
|
|
1895
|
+
function watchdogTierLabel(t, tier) {
|
|
1896
|
+
if (tier === 'scheduled-task') return t('settings.rescue.tierScheduledTask')
|
|
1897
|
+
if (tier === 'systemd-run') return t('settings.rescue.tierSystemdRun')
|
|
1898
|
+
if (tier === 'detached-process') return t('settings.rescue.tierDetachedProcess')
|
|
1899
|
+
if (tier === 'none') return t('settings.rescue.tierNone')
|
|
1900
|
+
return ''
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* The two rescue scripts, as the Host reported them.
|
|
1905
|
+
*
|
|
1906
|
+
* The paths come from the Host (it resolved the plugin directory for
|
|
1907
|
+
* itself) — the browser never guesses them. A path that is not there is not
|
|
1908
|
+
* shown: an empty path would be a dead end for the user.
|
|
1909
|
+
*
|
|
1910
|
+
* @param {object} info the Host's state payload
|
|
1911
|
+
* @returns {Array<{ key: string, path: string }>}
|
|
1912
|
+
*/
|
|
1913
|
+
function rescuePathsFor(info) {
|
|
1914
|
+
const rescue = info && typeof info === 'object' && info.rescue && typeof info.rescue === 'object'
|
|
1915
|
+
? info.rescue
|
|
1916
|
+
: null
|
|
1917
|
+
if (!rescue) return []
|
|
1918
|
+
const out = []
|
|
1919
|
+
const restore = typeof rescue.restoreProfile === 'string' ? rescue.restoreProfile.trim() : ''
|
|
1920
|
+
if (restore !== '') out.push({ key: 'restoreProfile', path: restore })
|
|
1921
|
+
const disable = typeof rescue.emergencyDisable === 'string' ? rescue.emergencyDisable.trim() : ''
|
|
1922
|
+
if (disable !== '') out.push({ key: 'emergencyDisable', path: disable })
|
|
1923
|
+
return out
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/**
|
|
1927
|
+
* The route that writes the desktop shortcut and opens the folder.
|
|
1928
|
+
*
|
|
1929
|
+
* @param {object} info the Host's state payload
|
|
1930
|
+
* @returns {string}
|
|
1931
|
+
*/
|
|
1932
|
+
function rescueUrlFor(info) {
|
|
1933
|
+
const sent = info && typeof info === 'object' && typeof info.rescueUrl === 'string' ? info.rescueUrl.trim() : ''
|
|
1934
|
+
if (sent.startsWith('/')) return sent
|
|
1935
|
+
return '/__dsh_autorestart/rescue'
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1821
1938
|
/**
|
|
1822
1939
|
* Whether the "how was this host started" block may be shown at all.
|
|
1823
1940
|
*
|
|
@@ -1929,6 +2046,14 @@
|
|
|
1929
2046
|
// "Report a problem": the URL builders and the clipboard with its fallback.
|
|
1930
2047
|
buildIssueUrl,
|
|
1931
2048
|
copyReportText,
|
|
2049
|
+
// The rescue kit: the two script paths as the Host resolved them, and the
|
|
2050
|
+
// route that writes the desktop shortcut and opens the folder.
|
|
2051
|
+
rescuePathsFor,
|
|
2052
|
+
rescueUrlFor,
|
|
2053
|
+
// The automatic rollback: what this machine can do, and whether the
|
|
2054
|
+
// one-time warning about it is still waiting to be shown.
|
|
2055
|
+
watchdogNoticeFor,
|
|
2056
|
+
watchdogTierLabel,
|
|
1932
2057
|
// The startup block: whether it may be shown, the one line that says what
|
|
1933
2058
|
// the plugin remembers, and the formatters of the backup list.
|
|
1934
2059
|
shouldShowStartupBlock,
|
|
@@ -4393,6 +4518,8 @@
|
|
|
4393
4518
|
const [reportFallback, setReportFallback] = useState('')
|
|
4394
4519
|
// The note under the backup path: copied, or "copy it by hand".
|
|
4395
4520
|
const [pathNote, setPathNote] = useState('')
|
|
4521
|
+
// The note under the rescue paths: copied, or "copy it by hand".
|
|
4522
|
+
const [rescueNote, setRescueNote] = useState('')
|
|
4396
4523
|
// Whether the "technical details" fold is open. It is NOT persisted:
|
|
4397
4524
|
// a fresh page starts closed, so nobody leaves technical text open by
|
|
4398
4525
|
// accident.
|
|
@@ -4479,6 +4606,49 @@
|
|
|
4479
4606
|
? info.backups
|
|
4480
4607
|
: { dir: '', max: 0, count: 0, items: [] }
|
|
4481
4608
|
const backupItems = Array.isArray(backupsInfo.items) ? backupsInfo.items : []
|
|
4609
|
+
// THE RESCUE KIT: the two scripts and where they are, as the Host
|
|
4610
|
+
// resolved them. The browser never guesses a path.
|
|
4611
|
+
const rescuePaths = rescuePathsFor(info)
|
|
4612
|
+
const rescueKit = info && typeof info === 'object' && info.rescue && typeof info.rescue === 'object'
|
|
4613
|
+
? info.rescue
|
|
4614
|
+
: {}
|
|
4615
|
+
// Where a desktop shortcut could go ('' = there is no desktop here).
|
|
4616
|
+
const rescueDesktop = typeof rescueKit.desktopDir === 'string' ? rescueKit.desktopDir.trim() : ''
|
|
4617
|
+
// Whether it is already there (then the button says so).
|
|
4618
|
+
const rescueShortcutExists = rescueKit.shortcutExists === true
|
|
4619
|
+
// WHAT THIS MACHINE CAN DO: the automatic rollback needs a watchdog,
|
|
4620
|
+
// and the watchdog needs a way to be triggered later. The tier (the
|
|
4621
|
+
// ONE function that decides it on the host) says which one that is.
|
|
4622
|
+
const capabilities = info && typeof info === 'object' && info.capabilities && typeof info.capabilities === 'object'
|
|
4623
|
+
? info.capabilities
|
|
4624
|
+
: {}
|
|
4625
|
+
const watchdog = info && typeof info === 'object' && info.watchdog && typeof info.watchdog === 'object'
|
|
4626
|
+
? info.watchdog
|
|
4627
|
+
: {}
|
|
4628
|
+
const watchdogTier = typeof watchdog.tier === 'string' && watchdog.tier !== '' ? watchdog.tier : ''
|
|
4629
|
+
const schedulerState = watchdogTier === '' ? 'unknown' : (watchdogTier === 'none' ? 'blocked' : 'ok')
|
|
4630
|
+
// The notice is for the TRANSITION: a user who has seen it is not
|
|
4631
|
+
// asked again.
|
|
4632
|
+
const schedulerNotice = watchdogNoticeFor(capabilities)
|
|
4633
|
+
const schedulerText = schedulerState === 'ok'
|
|
4634
|
+
? t('settings.rescue.rollbackOk')
|
|
4635
|
+
: schedulerState === 'blocked'
|
|
4636
|
+
? t('settings.rescue.rollbackBlocked')
|
|
4637
|
+
: t('settings.rescue.rollbackUnknown')
|
|
4638
|
+
const watchdogLast = watchdog.report && typeof watchdog.report === 'object' && typeof watchdog.report.result === 'string'
|
|
4639
|
+
? watchdog.report.result
|
|
4640
|
+
: ''
|
|
4641
|
+
useEffect(() => {
|
|
4642
|
+
if (!schedulerNotice) return
|
|
4643
|
+
// The user has read it: the Host remembers that and stops asking.
|
|
4644
|
+
void fetch(rescueUrlFor(info), {
|
|
4645
|
+
method: 'POST',
|
|
4646
|
+
headers: { 'content-type': 'application/json' },
|
|
4647
|
+
body: JSON.stringify({ action: 'ack-scheduler' }),
|
|
4648
|
+
}).catch(() => {
|
|
4649
|
+
// The Host will ask again next time — no harm.
|
|
4650
|
+
})
|
|
4651
|
+
}, [schedulerNotice])
|
|
4482
4652
|
const supervisorPending = supervisorStatus !== null
|
|
4483
4653
|
&& supervisorStatus.detail !== null
|
|
4484
4654
|
&& typeof supervisorStatus.detail === 'object'
|
|
@@ -4643,6 +4813,73 @@
|
|
|
4643
4813
|
setPathNote(copied.fallback ? t('settings.backup.copyFailed') : t('settings.backup.copied'))
|
|
4644
4814
|
}
|
|
4645
4815
|
|
|
4816
|
+
/**
|
|
4817
|
+
* Copy the path of a rescue script.
|
|
4818
|
+
*
|
|
4819
|
+
* The path is on screen anyway (selectable), so a refused clipboard
|
|
4820
|
+
* only costs a hint — the same fallback the diagnostics use.
|
|
4821
|
+
*/
|
|
4822
|
+
const copyRescuePath = async (target) => {
|
|
4823
|
+
const copied = await copyReportText(target)
|
|
4824
|
+
setRescueNote(copied.fallback ? t('settings.rescue.copyFailed') : t('settings.rescue.copied'))
|
|
4825
|
+
}
|
|
4826
|
+
|
|
4827
|
+
/**
|
|
4828
|
+
* Put the desktop shortcut there.
|
|
4829
|
+
*
|
|
4830
|
+
* ONLY because the user pressed the button: the settings page never
|
|
4831
|
+
* writes anything to the desktop by itself.
|
|
4832
|
+
*/
|
|
4833
|
+
const createRescueShortcut = async () => {
|
|
4834
|
+
if (busy) return
|
|
4835
|
+
setBusy(true)
|
|
4836
|
+
setRescueNote('')
|
|
4837
|
+
try {
|
|
4838
|
+
const response = await fetch(rescueUrlFor(info), {
|
|
4839
|
+
method: 'POST',
|
|
4840
|
+
headers: { 'content-type': 'application/json' },
|
|
4841
|
+
body: JSON.stringify({ action: 'shortcut' }),
|
|
4842
|
+
})
|
|
4843
|
+
const body = await response.json().catch(() => null)
|
|
4844
|
+
if (!response.ok) {
|
|
4845
|
+
const message = body && typeof body === 'object' && body.error ? body.error : String(response.status)
|
|
4846
|
+
setRescueNote(fill(t('settings.rescue.shortcutFailed'), { message }))
|
|
4847
|
+
return
|
|
4848
|
+
}
|
|
4849
|
+
setRescueNote(t('settings.rescue.shortcutDone'))
|
|
4850
|
+
} catch (failure) {
|
|
4851
|
+
setRescueNote(fill(t('settings.rescue.shortcutFailed'), { message: describeError(failure) }))
|
|
4852
|
+
} finally {
|
|
4853
|
+
setBusy(false)
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
|
|
4857
|
+
/**
|
|
4858
|
+
* Open the folder of the scripts in the file manager.
|
|
4859
|
+
*/
|
|
4860
|
+
const revealRescueFolder = async () => {
|
|
4861
|
+
if (busy) return
|
|
4862
|
+
setBusy(true)
|
|
4863
|
+
setRescueNote('')
|
|
4864
|
+
try {
|
|
4865
|
+
const response = await fetch(rescueUrlFor(info), {
|
|
4866
|
+
method: 'POST',
|
|
4867
|
+
headers: { 'content-type': 'application/json' },
|
|
4868
|
+
body: JSON.stringify({ action: 'reveal' }),
|
|
4869
|
+
})
|
|
4870
|
+
const body = await response.json().catch(() => null)
|
|
4871
|
+
if (!response.ok) {
|
|
4872
|
+
const message = body && typeof body === 'object' && body.error ? body.error : String(response.status)
|
|
4873
|
+
setRescueNote(fill(t('settings.rescue.revealFailed'), { message }))
|
|
4874
|
+
return
|
|
4875
|
+
}
|
|
4876
|
+
} catch (failure) {
|
|
4877
|
+
setRescueNote(fill(t('settings.rescue.revealFailed'), { message: describeError(failure) }))
|
|
4878
|
+
} finally {
|
|
4879
|
+
setBusy(false)
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4646
4883
|
return h(
|
|
4647
4884
|
'div',
|
|
4648
4885
|
{ className: 'dsh-autorestart-settings' },
|
|
@@ -5154,6 +5391,156 @@
|
|
|
5154
5391
|
)
|
|
5155
5392
|
: null,
|
|
5156
5393
|
),
|
|
5394
|
+
// "IF DSH WILL NOT START": the two rescue scripts, where they are, and
|
|
5395
|
+
// the way to get them onto the desktop. A user whose host is down
|
|
5396
|
+
// cannot open a README — they need the path, and they need to be
|
|
5397
|
+
// able to copy it.
|
|
5398
|
+
h(
|
|
5399
|
+
'div',
|
|
5400
|
+
{
|
|
5401
|
+
className: 'dsh-autorestart-settings__group',
|
|
5402
|
+
'data-dsh-autorestart-group': 'rescue',
|
|
5403
|
+
},
|
|
5404
|
+
h(
|
|
5405
|
+
'div',
|
|
5406
|
+
{ className: 'dsh-autorestart-settings__group-title' },
|
|
5407
|
+
t('settings.groups.rescue'),
|
|
5408
|
+
),
|
|
5409
|
+
h(
|
|
5410
|
+
'div',
|
|
5411
|
+
{ className: 'dsh-autorestart-settings__field' },
|
|
5412
|
+
h(
|
|
5413
|
+
'span',
|
|
5414
|
+
{ className: 'dsh-autorestart-settings__hint' },
|
|
5415
|
+
t('settings.rescue.hint'),
|
|
5416
|
+
),
|
|
5417
|
+
),
|
|
5418
|
+
rescuePaths.map((entry) => h(
|
|
5419
|
+
'div',
|
|
5420
|
+
{
|
|
5421
|
+
className: 'dsh-autorestart-settings__field',
|
|
5422
|
+
'data-dsh-autorestart-rescue': entry.key,
|
|
5423
|
+
},
|
|
5424
|
+
h(
|
|
5425
|
+
'span',
|
|
5426
|
+
{ className: 'dsh-autorestart-settings__label' },
|
|
5427
|
+
t(entry.key === 'restoreProfile' ? 'settings.rescue.restoreProfile' : 'settings.rescue.emergencyDisable'),
|
|
5428
|
+
),
|
|
5429
|
+
h(
|
|
5430
|
+
'code',
|
|
5431
|
+
{ className: 'dsh-autorestart-settings__value', 'data-dsh-autorestart-rescue': 'path' },
|
|
5432
|
+
entry.path,
|
|
5433
|
+
),
|
|
5434
|
+
h(
|
|
5435
|
+
'button',
|
|
5436
|
+
{
|
|
5437
|
+
type: 'button',
|
|
5438
|
+
className: 'dsh-autorestart-settings__button',
|
|
5439
|
+
disabled: busy,
|
|
5440
|
+
'data-dsh-autorestart-rescue': 'copy-path',
|
|
5441
|
+
onClick: () => void copyRescuePath(entry.path),
|
|
5442
|
+
},
|
|
5443
|
+
t('settings.rescue.copyPath'),
|
|
5444
|
+
),
|
|
5445
|
+
h(
|
|
5446
|
+
'button',
|
|
5447
|
+
{
|
|
5448
|
+
type: 'button',
|
|
5449
|
+
className: 'dsh-autorestart-settings__button',
|
|
5450
|
+
disabled: busy,
|
|
5451
|
+
'data-dsh-autorestart-rescue': 'reveal',
|
|
5452
|
+
onClick: () => void revealRescueFolder(),
|
|
5453
|
+
},
|
|
5454
|
+
t('settings.rescue.reveal'),
|
|
5455
|
+
),
|
|
5456
|
+
)),
|
|
5457
|
+
h(
|
|
5458
|
+
'div',
|
|
5459
|
+
{ className: 'dsh-autorestart-settings__field' },
|
|
5460
|
+
h(
|
|
5461
|
+
'span',
|
|
5462
|
+
{ className: 'dsh-autorestart-settings__hint', 'data-dsh-autorestart-rescue': 'which' },
|
|
5463
|
+
t('settings.rescue.which'),
|
|
5464
|
+
),
|
|
5465
|
+
),
|
|
5466
|
+
h(
|
|
5467
|
+
'div',
|
|
5468
|
+
{ className: 'dsh-autorestart-settings__field' },
|
|
5469
|
+
h(
|
|
5470
|
+
'span',
|
|
5471
|
+
{ className: 'dsh-autorestart-settings__hint', 'data-dsh-autorestart-rescue': 'warning' },
|
|
5472
|
+
t('settings.rescue.warning'),
|
|
5473
|
+
),
|
|
5474
|
+
),
|
|
5475
|
+
// WHAT THIS MACHINE CAN DO: the automatic rollback (and what is
|
|
5476
|
+
// lost when the machine refuses a watchdog).
|
|
5477
|
+
h(
|
|
5478
|
+
'div',
|
|
5479
|
+
{ className: 'dsh-autorestart-settings__field', 'data-dsh-autorestart-rescue': 'rollback' },
|
|
5480
|
+
h(
|
|
5481
|
+
'span',
|
|
5482
|
+
{ className: 'dsh-autorestart-settings__hint', 'data-dsh-autorestart-rescue': 'rollback-state' },
|
|
5483
|
+
schedulerText,
|
|
5484
|
+
),
|
|
5485
|
+
watchdogTier === '' ? null : h(
|
|
5486
|
+
'span',
|
|
5487
|
+
{ className: 'dsh-autorestart-settings__hint', 'data-dsh-autorestart-rescue': 'watchdog-tier' },
|
|
5488
|
+
t('settings.rescue.watchdogTier', { tier: watchdogTierLabel(t, watchdogTier) }),
|
|
5489
|
+
),
|
|
5490
|
+
watchdogLast === '' ? null : h(
|
|
5491
|
+
'span',
|
|
5492
|
+
{ className: 'dsh-autorestart-settings__hint', 'data-dsh-autorestart-rescue': 'watchdog-last' },
|
|
5493
|
+
t('settings.rescue.watchdogLast', { result: watchdogLast }),
|
|
5494
|
+
),
|
|
5495
|
+
),
|
|
5496
|
+
// The notice is shown ONCE: the Host remembers that the user has
|
|
5497
|
+
// read it.
|
|
5498
|
+
schedulerNotice
|
|
5499
|
+
? h(
|
|
5500
|
+
'div',
|
|
5501
|
+
{ className: 'dsh-autorestart-settings__field' },
|
|
5502
|
+
h(
|
|
5503
|
+
'span',
|
|
5504
|
+
{ className: 'dsh-autorestart-settings__label', 'data-dsh-autorestart-rescue': 'rollback-notice' },
|
|
5505
|
+
t('settings.rescue.rollbackBlocked'),
|
|
5506
|
+
),
|
|
5507
|
+
)
|
|
5508
|
+
: null,
|
|
5509
|
+
// The desktop shortcut is a BUTTON, never a side effect: loading
|
|
5510
|
+
// this page writes nothing to the desktop.
|
|
5511
|
+
rescueDesktop === ''
|
|
5512
|
+
? h(
|
|
5513
|
+
'div',
|
|
5514
|
+
{ className: 'dsh-autorestart-settings__field' },
|
|
5515
|
+
h(
|
|
5516
|
+
'span',
|
|
5517
|
+
{ className: 'dsh-autorestart-settings__hint', 'data-dsh-autorestart-rescue': 'no-desktop' },
|
|
5518
|
+
t('settings.rescue.noDesktop'),
|
|
5519
|
+
),
|
|
5520
|
+
)
|
|
5521
|
+
: h(
|
|
5522
|
+
'div',
|
|
5523
|
+
{ className: 'dsh-autorestart-settings__field' },
|
|
5524
|
+
h(
|
|
5525
|
+
'button',
|
|
5526
|
+
{
|
|
5527
|
+
type: 'button',
|
|
5528
|
+
className: 'dsh-autorestart-settings__button',
|
|
5529
|
+
disabled: busy,
|
|
5530
|
+
'data-dsh-autorestart-rescue': 'shortcut',
|
|
5531
|
+
onClick: () => void createRescueShortcut(),
|
|
5532
|
+
},
|
|
5533
|
+
rescueShortcutExists ? t('settings.rescue.shortcutDone') : t('settings.rescue.shortcut'),
|
|
5534
|
+
),
|
|
5535
|
+
rescueNote === ''
|
|
5536
|
+
? null
|
|
5537
|
+
: h(
|
|
5538
|
+
'span',
|
|
5539
|
+
{ className: 'dsh-autorestart-settings__hint', 'data-dsh-autorestart-rescue': 'note' },
|
|
5540
|
+
rescueNote,
|
|
5541
|
+
),
|
|
5542
|
+
),
|
|
5543
|
+
),
|
|
5157
5544
|
// "REPORT A PROBLEM": copy the diagnostics and open a pre-filled
|
|
5158
5545
|
// issue page. The email is the way out when there is no GitHub
|
|
5159
5546
|
// account. When the clipboard refuses, the text is shown in a box
|
package/cordis.patch.yml
CHANGED
|
@@ -60,6 +60,13 @@
|
|
|
60
60
|
backupIncludePlugins: true
|
|
61
61
|
# Keep at most this many snapshots; older ones are deleted automatically.
|
|
62
62
|
maxBackups: 2
|
|
63
|
+
# The WATCHDOG: the process that waits for the relaunched host and
|
|
64
|
+
# puts the snapshot back when it never answers. It has to be triggered
|
|
65
|
+
# LATER, and the machine decides how (a one-off Windows scheduled task,
|
|
66
|
+
# a systemd user timer, or a detached process of our own). Set this to
|
|
67
|
+
# false to switch the automatic rollback off entirely; the settings
|
|
68
|
+
# page then says that there is no automatic rollback.
|
|
69
|
+
watchdogEnabled: true
|
|
63
70
|
# How long the detached watcher waits for the relaunched host (ms) before
|
|
64
71
|
# it restores the snapshot. The watcher NEVER launches a second copy of
|
|
65
72
|
# the host: it restores the files and writes the report.
|