dsh-autorestart 0.8.4 → 0.8.6
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 +313 -0
- package/README.md +465 -40
- package/assets/icon-9c245e5b.ico +0 -0
- package/assets/icon-rescue-1902bb15.ico +0 -0
- package/assets/icon-rescue-1902bb15.png +0 -0
- package/client.js +3002 -170
- package/cordis.patch.yml +12 -0
- package/docs/README.zh.md +231 -23
- package/docs/platform-matrix.md +171 -55
- package/docs/real-machine-verification-0.8.6.md +202 -0
- package/index.js +13272 -8513
- package/lib/autostart-advice.js +31 -2
- package/lib/backup.js +2768 -21
- package/lib/checkpoint.js +214 -7
- package/lib/cookie-clear.js +260 -0
- package/lib/lifecycle.js +23 -40
- package/lib/plugin-compare.js +499 -0
- package/lib/plugin-failure.js +382 -0
- package/lib/plugin-recover.js +1557 -0
- package/lib/plugin-watch.js +213 -0
- package/lib/rescue-paths.js +982 -56
- package/lib/restart-phase.js +5 -0
- package/lib/restart-verdict.js +379 -0
- package/lib/restart.js +244 -7
- package/lib/scheduler-capability.js +19 -3
- package/lib/sleep.js +190 -29
- package/lib/watchdog-disarm.js +46 -2
- package/lib/watchdog-schedule.js +834 -17
- package/lib/watchdog-tier.js +8 -1
- package/lib/watchdog.js +1222 -47
- package/locale/en.json +175 -68
- package/locale/zh.json +175 -68
- package/package.json +3 -2
- package/screenshots.json +6 -6
- package/scripts/README.md +6 -3
- package/scripts/build-icon.mjs +998 -0
- package/scripts/clear-dsh-auth-cookies.mjs +257 -0
- package/scripts/emergency-disable.sh +69 -2
- package/scripts/emergency-toggle.ps1 +85 -69
- package/scripts/rescue-strings.zh.json +36 -18
- package/scripts/restore-profile.ps1 +182 -1
- package/scripts/watchdog.mjs +432 -79
package/lib/restart.js
CHANGED
|
@@ -15,9 +15,125 @@
|
|
|
15
15
|
* ESM only, Node builtins only. No DSH / npm imports.
|
|
16
16
|
*/
|
|
17
17
|
import { execFileSync, spawn } from 'node:child_process';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
mkdirSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
readdirSync,
|
|
22
|
+
readlinkSync,
|
|
23
|
+
renameSync,
|
|
24
|
+
statSync,
|
|
25
|
+
unlinkSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
} from 'node:fs';
|
|
19
28
|
import path from 'node:path';
|
|
20
29
|
|
|
30
|
+
/**
|
|
31
|
+
* THE V1 ROLLBACK WATCHER IS A POWERSHELL SCRIPT (2026-11-02).
|
|
32
|
+
*
|
|
33
|
+
* `buildWatchScript` writes PowerShell, so the whole v1 path is a Windows path.
|
|
34
|
+
* Older builds did not say so: they wrote `restart-<n>-watch.ps1` on EVERY
|
|
35
|
+
* platform and then tried to launch it, which on Linux/WSL could only fail —
|
|
36
|
+
* and left the file behind. A real WSL machine had SEVEN of them
|
|
37
|
+
* (restart-4/5/6/7/8/9/16), which is why the release checklist item
|
|
38
|
+
* "leftover files = 0" was failing.
|
|
39
|
+
*
|
|
40
|
+
* This pattern is the only name the plugin ever writes, so it is also the only
|
|
41
|
+
* name the sweep below is allowed to touch.
|
|
42
|
+
*/
|
|
43
|
+
export const LEGACY_WATCH_SCRIPT_PATTERN = /^restart-\d+-watch\.ps1$/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Does this text look like OUR rollback watcher (and not like a user's file that
|
|
47
|
+
* happens to have the same name)?
|
|
48
|
+
*
|
|
49
|
+
* The check is deliberately narrow: the three markers below all come from
|
|
50
|
+
* `buildWatchScript`, and a file that misses any of them is left alone. A sweep
|
|
51
|
+
* that deletes the wrong file in a rescue path is worse than a leftover.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} text
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
export function looksLikeWatchScript(text) {
|
|
57
|
+
if (typeof text !== 'string' || text === '') return false;
|
|
58
|
+
return /param\s*\(/.test(text)
|
|
59
|
+
&& /Start-Sleep|Wait-Job/.test(text)
|
|
60
|
+
&& /_dsh_autorestart|dsh-autorestart/.test(text);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Remove the watcher scripts the old builds left on a NON-Windows machine.
|
|
65
|
+
*
|
|
66
|
+
* Rules (all of them are "be conservative"):
|
|
67
|
+
* - Windows keeps everything: there those files really are used, and the one
|
|
68
|
+
* that is running right now must not be pulled from under it.
|
|
69
|
+
* - Only `restart-<digits>-watch.ps1` is considered.
|
|
70
|
+
* - The CONTENT must look like our watcher (`looksLikeWatchScript`).
|
|
71
|
+
* - A file modified in the last `minAgeMs` is skipped: a process that just
|
|
72
|
+
* wrote it may still be starting.
|
|
73
|
+
* - Nothing here throws: a failure is returned.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} dir the plugin's state directory
|
|
76
|
+
* @param {{ platform?: string, minAgeMs?: number, now?: number }} [options]
|
|
77
|
+
* @returns {{ skipped: boolean, reason: string|null, removed: string[], kept: string[], scanned: number, error: string|null }}
|
|
78
|
+
*/
|
|
79
|
+
export function sweepLegacyWatcherScripts(
|
|
80
|
+
dir,
|
|
81
|
+
{ platform = process.platform, minAgeMs = 60_000, now = Date.now() } = {},
|
|
82
|
+
) {
|
|
83
|
+
const result = { skipped: false, reason: null, removed: [], kept: [], scanned: 0, error: null };
|
|
84
|
+
if (platform === 'win32') {
|
|
85
|
+
result.skipped = true;
|
|
86
|
+
result.reason = 'not-applicable: on Windows these watcher scripts are really used, so none is removed';
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
if (typeof dir !== 'string' || dir === '') {
|
|
90
|
+
result.skipped = true;
|
|
91
|
+
result.reason = 'no state directory to sweep';
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
let names = [];
|
|
95
|
+
try {
|
|
96
|
+
names = readdirSync(dir);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
// A directory that does not exist yet is not a failure: nothing was left.
|
|
99
|
+
const code = error && error.code;
|
|
100
|
+
if (code === 'ENOENT') {
|
|
101
|
+
result.skipped = true;
|
|
102
|
+
result.reason = 'no state directory yet';
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
result.error = error && error.message ? String(error.message) : String(error);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
for (const name of names) {
|
|
109
|
+
if (!LEGACY_WATCH_SCRIPT_PATTERN.test(name)) continue;
|
|
110
|
+
result.scanned += 1;
|
|
111
|
+
const file = path.join(dir, name);
|
|
112
|
+
try {
|
|
113
|
+
const stat = statSync(file);
|
|
114
|
+
if (!stat.isFile()) {
|
|
115
|
+
result.kept.push(name);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Never touch a file that was just written: the writer may still be running.
|
|
119
|
+
if (minAgeMs > 0 && now - stat.mtimeMs < minAgeMs) {
|
|
120
|
+
result.kept.push(name);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!looksLikeWatchScript(readFileSync(file, 'utf8'))) {
|
|
124
|
+
result.kept.push(name);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
unlinkSync(file);
|
|
128
|
+
result.removed.push(name);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
result.kept.push(name);
|
|
131
|
+
result.error = error && error.message ? String(error.message) : String(error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
21
137
|
/** Default name of the one-shot scheduled-task fallback. */
|
|
22
138
|
export const DEFAULT_TASK_NAME = 'dsh-autorestart-restart';
|
|
23
139
|
|
|
@@ -497,6 +613,7 @@ export function relaunchCommandLine(exe) {
|
|
|
497
613
|
* appExePath?: string|null,
|
|
498
614
|
* relaunchDelaySeconds?: number,
|
|
499
615
|
* taskkillImageName?: string,
|
|
616
|
+
* cookieClearCommand?: string,
|
|
500
617
|
* }} [options]
|
|
501
618
|
* @returns {string}
|
|
502
619
|
*/
|
|
@@ -504,6 +621,7 @@ export function buildRestartCommand({
|
|
|
504
621
|
appExePath = null,
|
|
505
622
|
relaunchDelaySeconds = DEFAULT_RELAUNCH_DELAY_SECONDS,
|
|
506
623
|
taskkillImageName = DEFAULT_IMAGE_NAME,
|
|
624
|
+
cookieClearCommand = '',
|
|
507
625
|
} = {}) {
|
|
508
626
|
const seconds =
|
|
509
627
|
typeof relaunchDelaySeconds === 'number' && Number.isFinite(relaunchDelaySeconds) && relaunchDelaySeconds >= 0
|
|
@@ -511,9 +629,18 @@ export function buildRestartCommand({
|
|
|
511
629
|
: DEFAULT_RELAUNCH_DELAY_SECONDS;
|
|
512
630
|
const image = nonEmptyString(taskkillImageName) ?? DEFAULT_IMAGE_NAME;
|
|
513
631
|
const exe = typeof appExePath === 'string' ? appExePath : '';
|
|
632
|
+
const clear = nonEmptyString(cookieClearCommand);
|
|
514
633
|
// `ping -n N` where N = seconds + 1: one packet is sent immediately, so a
|
|
515
634
|
// 4-second delay needs 5 pings. `>nul` keeps the detached console silent.
|
|
516
|
-
|
|
635
|
+
const steps = [
|
|
636
|
+
`cmd.exe /c ping -n ${seconds + 1} 127.0.0.1 >nul`,
|
|
637
|
+
`taskkill /IM "${image}" /T /F`,
|
|
638
|
+
];
|
|
639
|
+
// The cleanup runs AFTER the kill and BEFORE the relaunch: only then has
|
|
640
|
+
// Chromium left, and only then is the cookie jar unlocked.
|
|
641
|
+
if (clear !== null) steps.push(clear);
|
|
642
|
+
steps.push(relaunchCommandLine(exe));
|
|
643
|
+
return steps.join(' & ');
|
|
517
644
|
}
|
|
518
645
|
|
|
519
646
|
/**
|
|
@@ -550,6 +677,7 @@ export function buildWmiRestartCommand(command) {
|
|
|
550
677
|
* relaunchDelaySeconds?: number,
|
|
551
678
|
* gateUrl?: string,
|
|
552
679
|
* taskkillImageName?: string,
|
|
680
|
+
* cookieClearCommand?: string,
|
|
553
681
|
* }} [options]
|
|
554
682
|
* @returns {string} `.cmd` script text (CRLF line endings, no BOM)
|
|
555
683
|
*/
|
|
@@ -559,6 +687,7 @@ export function buildGateScript({
|
|
|
559
687
|
gateUrl = '',
|
|
560
688
|
taskkillImageName = DEFAULT_IMAGE_NAME,
|
|
561
689
|
watchScriptPath = null,
|
|
690
|
+
cookieClearCommand = '',
|
|
562
691
|
} = {}) {
|
|
563
692
|
const seconds =
|
|
564
693
|
typeof relaunchDelaySeconds === 'number' && Number.isFinite(relaunchDelaySeconds) && relaunchDelaySeconds >= 0
|
|
@@ -573,6 +702,7 @@ export function buildGateScript({
|
|
|
573
702
|
+ gateArgument
|
|
574
703
|
+ ' -TimeoutSec 5; if ($r.allowRestart -ne $true) { exit 3 } } catch { exit 4 }"';
|
|
575
704
|
const watch = nonEmptyString(watchScriptPath);
|
|
705
|
+
const clear = nonEmptyString(cookieClearCommand);
|
|
576
706
|
const lines = [
|
|
577
707
|
'@echo off',
|
|
578
708
|
'rem dsh-autorestart: wait, re-check the idle gate, then kill + relaunch DeepSeek Harness.',
|
|
@@ -581,8 +711,11 @@ export function buildGateScript({
|
|
|
581
711
|
'if errorlevel 1 exit /b 0',
|
|
582
712
|
'taskkill /IM "' + image + '" /T /F',
|
|
583
713
|
'ping -n 3 127.0.0.1 >nul',
|
|
584
|
-
relaunchCommandLine(exe),
|
|
585
714
|
];
|
|
715
|
+
// The cleanup runs AFTER the kill and BEFORE the relaunch: only then has
|
|
716
|
+
// Chromium left, and only then is the cookie jar unlocked.
|
|
717
|
+
if (clear !== null) lines.push(clear);
|
|
718
|
+
lines.push(relaunchCommandLine(exe));
|
|
586
719
|
if (watch !== null) {
|
|
587
720
|
lines.push('rem dsh-autorestart: wait for the host to come back; roll the backup back when it does not.');
|
|
588
721
|
// Launched as its own hidden process so the .cmd can end immediately: the
|
|
@@ -820,6 +953,77 @@ export function writeTextFileAtomic(file, text) {
|
|
|
820
953
|
}
|
|
821
954
|
}
|
|
822
955
|
|
|
956
|
+
/**
|
|
957
|
+
* Is this pid still alive?
|
|
958
|
+
*
|
|
959
|
+
* Best effort, never throws: `process.kill(pid, 0)` sends no signal, it only
|
|
960
|
+
* asks the operating system whether the process exists. `EPERM` means "it
|
|
961
|
+
* exists, but it is not mine" — still alive.
|
|
962
|
+
*
|
|
963
|
+
* @param {number|null} pid
|
|
964
|
+
* @param {{ kill?: Function }} [options]
|
|
965
|
+
* @returns {boolean}
|
|
966
|
+
*/
|
|
967
|
+
export function isProcessAlive(pid, { kill = process.kill } = {}) {
|
|
968
|
+
const value = Number(pid);
|
|
969
|
+
if (!Number.isFinite(value) || value <= 0) return false;
|
|
970
|
+
try {
|
|
971
|
+
kill(value, 0);
|
|
972
|
+
return true;
|
|
973
|
+
} catch (error) {
|
|
974
|
+
return Boolean(error && error.code === 'EPERM');
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Did the rollback watcher really survive its own start?
|
|
980
|
+
*
|
|
981
|
+
* The POSIX launch only proves that `spawn` returned a pid: the detached child
|
|
982
|
+
* may still die a moment later (a missing `node`, a script that cannot be read).
|
|
983
|
+
* Waiting a moment and asking again is the difference between "armed" and
|
|
984
|
+
* "probably armed" — and the caller is told which one it got.
|
|
985
|
+
*
|
|
986
|
+
* @param {{
|
|
987
|
+
* pid?: number|null,
|
|
988
|
+
* waitMs?: number,
|
|
989
|
+
* sleep?: (ms: number) => Promise<void>,
|
|
990
|
+
* isAlive?: (pid: number) => boolean,
|
|
991
|
+
* }} [options]
|
|
992
|
+
* @returns {Promise<{ confirmed: boolean, alive: boolean, pid: number|null, reason: string }>}
|
|
993
|
+
*/
|
|
994
|
+
export async function confirmWatcherAlive({
|
|
995
|
+
pid = null,
|
|
996
|
+
waitMs = 250,
|
|
997
|
+
sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }),
|
|
998
|
+
isAlive = isProcessAlive,
|
|
999
|
+
} = {}) {
|
|
1000
|
+
const value = Number(pid);
|
|
1001
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1002
|
+
return { confirmed: false, alive: false, pid: null, reason: 'no pid was reported, so nothing could be confirmed' };
|
|
1003
|
+
}
|
|
1004
|
+
const wait = Number.isFinite(Number(waitMs)) && Number(waitMs) > 0 ? Math.trunc(Number(waitMs)) : 0;
|
|
1005
|
+
if (wait > 0) {
|
|
1006
|
+
try {
|
|
1007
|
+
await sleep(wait);
|
|
1008
|
+
} catch { /* a clock that fails must not turn into "confirmed" */ }
|
|
1009
|
+
}
|
|
1010
|
+
const alive = (() => {
|
|
1011
|
+
try {
|
|
1012
|
+
return isAlive(value) === true;
|
|
1013
|
+
} catch {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
})();
|
|
1017
|
+
return {
|
|
1018
|
+
confirmed: alive,
|
|
1019
|
+
alive,
|
|
1020
|
+
pid: value,
|
|
1021
|
+
reason: alive
|
|
1022
|
+
? `the watcher process ${value} is alive`
|
|
1023
|
+
: `the watcher process ${value} is gone right after its start`,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
|
|
823
1027
|
/**
|
|
824
1028
|
* Start the rollback watcher as a process that survives the death of the host.
|
|
825
1029
|
*
|
|
@@ -833,6 +1037,11 @@ export function writeTextFileAtomic(file, text) {
|
|
|
833
1037
|
* its own session and therefore immune to a kill of the host's process
|
|
834
1038
|
* group.
|
|
835
1039
|
*
|
|
1040
|
+
* WHAT `ok: true` MEANS (2026-11-02): the launch mechanism accepted the command.
|
|
1041
|
+
* It does NOT mean "somebody is definitely waiting" — on POSIX the child may
|
|
1042
|
+
* still die a moment later, so the caller must confirm the pid
|
|
1043
|
+
* ({@link confirmWatcherAlive}) before it reports "the rollback is armed".
|
|
1044
|
+
*
|
|
836
1045
|
* @param {{
|
|
837
1046
|
* watchScriptPath?: string|null,
|
|
838
1047
|
* watchCommand?: string|null,
|
|
@@ -849,6 +1058,8 @@ export function writeTextFileAtomic(file, text) {
|
|
|
849
1058
|
* command: string,
|
|
850
1059
|
* error: string|null,
|
|
851
1060
|
* dryRun: boolean,
|
|
1061
|
+
* pid: number|null,
|
|
1062
|
+
* spawnError: string|null,
|
|
852
1063
|
* }}
|
|
853
1064
|
*/
|
|
854
1065
|
export function launchDetachedWatcher({
|
|
@@ -869,6 +1080,10 @@ export function launchDetachedWatcher({
|
|
|
869
1080
|
command,
|
|
870
1081
|
error: null,
|
|
871
1082
|
dryRun: dryRun === true,
|
|
1083
|
+
// The pid is what the caller needs to confirm that somebody is really
|
|
1084
|
+
// waiting: `ok: true` alone only says "the launcher accepted it".
|
|
1085
|
+
pid: /** @type {number|null} */ (null),
|
|
1086
|
+
spawnError: /** @type {string|null} */ (null),
|
|
872
1087
|
};
|
|
873
1088
|
if (command === '') {
|
|
874
1089
|
return { ...base, error: 'launchDetachedWatcher requires a watcher command' };
|
|
@@ -926,18 +1141,31 @@ export function launchDetachedWatcher({
|
|
|
926
1141
|
|
|
927
1142
|
// POSIX: a detached child is its own session leader (setsid), so it survives
|
|
928
1143
|
// the death of the host — even when the host is killed as a process group.
|
|
1144
|
+
//
|
|
1145
|
+
// THE SPAWN ERROR IS NO LONGER SWALLOWED (2026-11-02): the old code attached
|
|
1146
|
+
// an empty `error` handler and returned `ok: true` as soon as a pid existed.
|
|
1147
|
+
// With `shell: true` a pid exists even when the real command cannot be
|
|
1148
|
+
// executed at all (a missing `node`, an unreadable script), so "the rollback
|
|
1149
|
+
// is armed" was reported for a watcher that never ran. The launch now returns
|
|
1150
|
+
// the pid and the caller CONFIRMS it (`confirmWatcherAlive`); the error
|
|
1151
|
+
// listener keeps the reason so a late failure is at least recorded.
|
|
929
1152
|
try {
|
|
930
1153
|
const child = spawnFn(command, { shell: true, detached: true, stdio: 'ignore' });
|
|
931
|
-
|
|
1154
|
+
const pid = child && typeof child.pid === 'number' ? child.pid : null;
|
|
1155
|
+
if (typeof child.on === 'function') {
|
|
1156
|
+
child.on('error', (error) => {
|
|
1157
|
+
base.spawnError = errorText(error);
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
932
1160
|
try {
|
|
933
1161
|
child.unref();
|
|
934
1162
|
} catch {
|
|
935
1163
|
/* nothing else to do */
|
|
936
1164
|
}
|
|
937
|
-
if (
|
|
1165
|
+
if (pid === null) {
|
|
938
1166
|
return { ...base, method: 'spawn', error: 'the watcher could not be started' };
|
|
939
1167
|
}
|
|
940
|
-
return { ...base, ok: true, method: 'spawn' };
|
|
1168
|
+
return { ...base, ok: true, method: 'spawn', pid };
|
|
941
1169
|
} catch (error) {
|
|
942
1170
|
return { ...base, method: 'spawn', error: errorText(error) };
|
|
943
1171
|
}
|
|
@@ -959,6 +1187,7 @@ export function launchDetachedWatcher({
|
|
|
959
1187
|
* dryRun?: boolean,
|
|
960
1188
|
* gateUrl?: string|null,
|
|
961
1189
|
* scriptPath?: string|null,
|
|
1190
|
+
* cookieClearCommand?: string,
|
|
962
1191
|
* }} [options]
|
|
963
1192
|
* @returns {{
|
|
964
1193
|
* ok: boolean,
|
|
@@ -986,12 +1215,14 @@ export function scheduleDetachedRestart({
|
|
|
986
1215
|
scriptPath = null,
|
|
987
1216
|
watchScriptPath = null,
|
|
988
1217
|
watchScript = null,
|
|
1218
|
+
cookieClearCommand = '',
|
|
989
1219
|
} = {}) {
|
|
990
1220
|
const task = nonEmptyString(taskName) ?? DEFAULT_TASK_NAME;
|
|
991
1221
|
const gate = nonEmptyString(gateUrl);
|
|
992
1222
|
const scriptFile = nonEmptyString(scriptPath);
|
|
993
1223
|
const watchFile = nonEmptyString(watchScriptPath);
|
|
994
1224
|
const watchText = typeof watchScript === 'string' ? watchScript : null;
|
|
1225
|
+
const clear = nonEmptyString(cookieClearCommand);
|
|
995
1226
|
const gated = gate !== null;
|
|
996
1227
|
let script = null;
|
|
997
1228
|
let command;
|
|
@@ -1002,10 +1233,16 @@ export function scheduleDetachedRestart({
|
|
|
1002
1233
|
gateUrl: gate,
|
|
1003
1234
|
taskkillImageName,
|
|
1004
1235
|
watchScriptPath: watchFile,
|
|
1236
|
+
cookieClearCommand: clear,
|
|
1005
1237
|
});
|
|
1006
1238
|
command = scriptFile === null ? '' : 'cmd.exe /c "' + scriptFile + '"';
|
|
1007
1239
|
} else {
|
|
1008
|
-
command = buildRestartCommand({
|
|
1240
|
+
command = buildRestartCommand({
|
|
1241
|
+
appExePath,
|
|
1242
|
+
relaunchDelaySeconds,
|
|
1243
|
+
taskkillImageName,
|
|
1244
|
+
cookieClearCommand: clear,
|
|
1245
|
+
});
|
|
1009
1246
|
}
|
|
1010
1247
|
const wmiCommand = buildWmiRestartCommand(command);
|
|
1011
1248
|
const schtasksCreate = ['/Create', '/SC', 'ONCE', '/ST', '23:59', '/TN', task, '/TR', command, '/F'];
|
|
@@ -41,6 +41,16 @@ export const SCHEDULER_OK = 'ok';
|
|
|
41
41
|
export const SCHEDULER_BLOCKED = 'blocked';
|
|
42
42
|
/** Nobody checked yet. */
|
|
43
43
|
export const SCHEDULER_UNKNOWN = 'unknown';
|
|
44
|
+
/**
|
|
45
|
+
* THE QUESTION DOES NOT APPLY HERE (2026-11-02).
|
|
46
|
+
*
|
|
47
|
+
* Only Windows has the one-off scheduled task this probe is about. The answer
|
|
48
|
+
* used to be `ok` — a green tick for a check that never ran, which made the
|
|
49
|
+
* page and the report claim something nobody had measured. `not-applicable` is
|
|
50
|
+
* a DIFFERENT answer from `ok`: it never counts as "passed", and the caller
|
|
51
|
+
* must say "this check does not apply on this machine" instead.
|
|
52
|
+
*/
|
|
53
|
+
export const SCHEDULER_NOT_APPLICABLE = 'not-applicable';
|
|
44
54
|
|
|
45
55
|
/** The name of the probe task. It is unique per run, so two probes cannot collide. */
|
|
46
56
|
export const PROBE_TASK_PREFIX = 'dsh-autorestart-capability-probe';
|
|
@@ -173,11 +183,14 @@ export function probeSchedulerCapability({
|
|
|
173
183
|
const evidence = { platform, taskName, steps: [] };
|
|
174
184
|
|
|
175
185
|
// Only Windows uses a one-off scheduled task. Everywhere else the question
|
|
176
|
-
// does not apply
|
|
186
|
+
// does not apply — and the answer SAYS SO (`not-applicable`), because `ok`
|
|
187
|
+
// was a green tick for a check that never ran (2026-11-02).
|
|
177
188
|
if (platform !== 'win32') {
|
|
178
189
|
return {
|
|
179
|
-
scheduler:
|
|
190
|
+
scheduler: SCHEDULER_NOT_APPLICABLE,
|
|
180
191
|
checkedAt,
|
|
192
|
+
applicable: false,
|
|
193
|
+
reason: 'not applicable: this platform does not use a one-off scheduled task',
|
|
181
194
|
evidence: {
|
|
182
195
|
...evidence,
|
|
183
196
|
applicable: false,
|
|
@@ -296,5 +309,8 @@ export function emptyCapabilities() {
|
|
|
296
309
|
export function isCapabilities(raw) {
|
|
297
310
|
if (!raw || typeof raw !== 'object') return false;
|
|
298
311
|
const scheduler = raw.scheduler;
|
|
299
|
-
return scheduler === SCHEDULER_OK
|
|
312
|
+
return scheduler === SCHEDULER_OK
|
|
313
|
+
|| scheduler === SCHEDULER_BLOCKED
|
|
314
|
+
|| scheduler === SCHEDULER_UNKNOWN
|
|
315
|
+
|| scheduler === SCHEDULER_NOT_APPLICABLE;
|
|
300
316
|
}
|