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/sleep.js
CHANGED
|
@@ -32,6 +32,7 @@ import fs from 'node:fs';
|
|
|
32
32
|
|
|
33
33
|
import { blockerTaskKeys, countBlockerTasks } from './blockers.js';
|
|
34
34
|
import { detectHostKind, detectWslEvidence, HOST_KINDS, WSL_PROC_VERSION, WSL_SHUTDOWN_EXE } from './host-kind.js';
|
|
35
|
+
import { detectContainer } from './supervisor-config.js';
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* The Windows shutdown binary as it is reachable from WSL.
|
|
@@ -201,6 +202,30 @@ export function detectShutdownPlan({
|
|
|
201
202
|
platform: plat,
|
|
202
203
|
};
|
|
203
204
|
}
|
|
205
|
+
// A CONTAINER IS NOT A LINUX MACHINE (2026-11-02). `systemctl poweroff`
|
|
206
|
+
// inside a container either fails or takes the container's own PID 1 down
|
|
207
|
+
// with it, and `shutdown -h now` is no better. The old code fell through to
|
|
208
|
+
// the plain Linux plan and the page then promised "关闭这台 Linux 主机" —
|
|
209
|
+
// a promise this machine cannot keep. The honest answer is the same one the
|
|
210
|
+
// unsupported platforms get: there is no reliable way to switch it off.
|
|
211
|
+
const container = detectContainer({
|
|
212
|
+
env: env && typeof env === 'object' ? env : process.env,
|
|
213
|
+
existsSyncFn: existsSafe,
|
|
214
|
+
});
|
|
215
|
+
if (container.container === true) {
|
|
216
|
+
return {
|
|
217
|
+
kind: 'container',
|
|
218
|
+
label: '这台机器没有可靠的关机方式',
|
|
219
|
+
source: 'container: ' + container.evidence,
|
|
220
|
+
command: null,
|
|
221
|
+
args: [],
|
|
222
|
+
fallbacks: [],
|
|
223
|
+
platform: plat,
|
|
224
|
+
reason: 'this host runs in a container (evidence: '
|
|
225
|
+
+ container.evidence
|
|
226
|
+
+ '): the orchestrator owns the process, and there is no reliable way to switch this machine off',
|
|
227
|
+
};
|
|
228
|
+
}
|
|
204
229
|
return {
|
|
205
230
|
kind: 'linux',
|
|
206
231
|
label: '关闭这台 Linux 主机',
|
|
@@ -273,11 +298,32 @@ export function planInvocation(plan, { sleepCommand = '', platform = process.pla
|
|
|
273
298
|
export const SPAWN_TIMEOUT_MS = 5000;
|
|
274
299
|
|
|
275
300
|
/**
|
|
276
|
-
*
|
|
301
|
+
* How long the shutdown command may run before "it started" is the only answer.
|
|
302
|
+
*
|
|
303
|
+
* A real shutdown binary answers in well under a second: `shutdown.exe /s /f
|
|
304
|
+
* /t 0`, `systemctl poweroff`, `osascript -e …` all return immediately (the
|
|
305
|
+
* machine goes off in the background). A CUSTOM `sleepCommand` may instead keep
|
|
306
|
+
* running in the foreground (`a script that starts something`), and the plugin
|
|
307
|
+
* must not wait for it forever — so the wait is bounded and the answer then says
|
|
308
|
+
* exactly what is known: it started, and it had not finished yet.
|
|
309
|
+
*/
|
|
310
|
+
export const EXIT_TIMEOUT_MS = 1500;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Start ONE command, wait for the real answer, AND read its exit code.
|
|
314
|
+
*
|
|
315
|
+
* Three outcomes, all of them distinguishable:
|
|
316
|
+
*
|
|
317
|
+
* - `spawn-failed` the command never started (missing binary, permission);
|
|
318
|
+
* - `exited` it ran and exited: `exitCode === 0` is success, anything
|
|
319
|
+
* else is a REFUSAL ("the system said no") and carries the
|
|
320
|
+
* code in the error text;
|
|
321
|
+
* - `stillRunning` it started and was still running when the bounded wait
|
|
322
|
+
* ran out (a custom foreground command).
|
|
277
323
|
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
324
|
+
* A spawner without events (a test double) cannot report any of that: it counts
|
|
325
|
+
* as started, `exited` stays `null`, and `observed: false` says that nothing was
|
|
326
|
+
* actually proven — the caller must not claim more.
|
|
281
327
|
*
|
|
282
328
|
* @param {string} command
|
|
283
329
|
* @param {string[]} args
|
|
@@ -285,14 +331,25 @@ export const SPAWN_TIMEOUT_MS = 5000;
|
|
|
285
331
|
* spawn?: Function,
|
|
286
332
|
* log?: { warn?: Function },
|
|
287
333
|
* timeoutMs?: number,
|
|
334
|
+
* exitTimeoutMs?: number,
|
|
288
335
|
* setTimeout?: Function,
|
|
289
336
|
* clearTimeout?: Function,
|
|
290
337
|
* }} [options]
|
|
291
|
-
* @returns {Promise<{
|
|
338
|
+
* @returns {Promise<{
|
|
339
|
+
* ok: boolean,
|
|
340
|
+
* pid: number|null,
|
|
341
|
+
* error: string|null,
|
|
342
|
+
* exitCode: number|null,
|
|
343
|
+
* exited: boolean|null,
|
|
344
|
+
* started: boolean,
|
|
345
|
+
* stillRunning?: boolean,
|
|
346
|
+
* observed?: boolean,
|
|
347
|
+
* }>}
|
|
292
348
|
*/
|
|
293
349
|
function startCommand(command, args, {
|
|
294
350
|
spawn = spawnProcess,
|
|
295
351
|
timeoutMs = SPAWN_TIMEOUT_MS,
|
|
352
|
+
exitTimeoutMs = EXIT_TIMEOUT_MS,
|
|
296
353
|
setTimeout: setTimeoutFn = setTimeout,
|
|
297
354
|
clearTimeout: clearTimeoutFn = clearTimeout,
|
|
298
355
|
} = {}) {
|
|
@@ -301,45 +358,101 @@ function startCommand(command, args, {
|
|
|
301
358
|
try {
|
|
302
359
|
child = spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true });
|
|
303
360
|
} catch (error) {
|
|
304
|
-
resolve({
|
|
361
|
+
resolve({
|
|
362
|
+
ok: false,
|
|
363
|
+
pid: null,
|
|
364
|
+
error: `spawn-failed: ${errorText(error)}`,
|
|
365
|
+
exitCode: null,
|
|
366
|
+
exited: null,
|
|
367
|
+
started: false,
|
|
368
|
+
});
|
|
305
369
|
return;
|
|
306
370
|
}
|
|
307
371
|
const pid = child && Number.isFinite(child.pid) ? child.pid : null;
|
|
308
372
|
if (!child || typeof child.on !== 'function') {
|
|
309
|
-
// A spawner without events (a test double) counts as started
|
|
373
|
+
// A spawner without events (a test double) counts as started — but the
|
|
374
|
+
// exit code CANNOT be observed, and `observed: false` says so.
|
|
310
375
|
try {
|
|
311
376
|
if (child && typeof child.unref === 'function') child.unref();
|
|
312
377
|
} catch { /* nothing else to do */ }
|
|
313
|
-
resolve({
|
|
378
|
+
resolve({
|
|
379
|
+
ok: true,
|
|
380
|
+
pid,
|
|
381
|
+
error: null,
|
|
382
|
+
exitCode: null,
|
|
383
|
+
exited: null,
|
|
384
|
+
started: true,
|
|
385
|
+
observed: false,
|
|
386
|
+
});
|
|
314
387
|
return;
|
|
315
388
|
}
|
|
316
389
|
let settled = false;
|
|
317
|
-
let
|
|
390
|
+
let spawnTimer = null;
|
|
391
|
+
let exitTimer = null;
|
|
392
|
+
// WHAT THE ANSWER SAYS BESIDES ok/error (2026-11-02): the exit code, whether
|
|
393
|
+
// it was observed, and whether the command is still running. Kept in a
|
|
394
|
+
// small record so `finish` keeps its two-argument shape.
|
|
395
|
+
let outcome = { exitCode: null, exited: null, started: true };
|
|
396
|
+
const stopTimer = (handle) => {
|
|
397
|
+
if (handle === null) return;
|
|
398
|
+
try {
|
|
399
|
+
clearTimeoutFn(handle);
|
|
400
|
+
} catch { /* nothing else to do */ }
|
|
401
|
+
};
|
|
318
402
|
const finish = (ok, error) => {
|
|
319
403
|
if (settled) return;
|
|
320
404
|
settled = true;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
405
|
+
stopTimer(spawnTimer);
|
|
406
|
+
stopTimer(exitTimer);
|
|
407
|
+
resolve({ ok, pid, error, ...outcome });
|
|
408
|
+
};
|
|
409
|
+
// THE EXIT CODE IS THE ANSWER (2026-11-02). "The command started" is not
|
|
410
|
+
// "the machine is going off": a refused shutdown exits non-zero and used to
|
|
411
|
+
// be reported as success, so the page said "正在关机" while the machine
|
|
412
|
+
// stayed on. Both `exit` and `close` are listened to, because a spawner may
|
|
413
|
+
// emit only one of them; `finish` makes the second one a no-op.
|
|
414
|
+
const onExit = (code, signal) => {
|
|
415
|
+
const numeric = Number.isFinite(code) ? Number(code) : null;
|
|
416
|
+
if (numeric === 0) {
|
|
417
|
+
outcome = { exitCode: 0, exited: true, started: true };
|
|
418
|
+
finish(true, null);
|
|
419
|
+
return;
|
|
326
420
|
}
|
|
327
|
-
|
|
421
|
+
const why = numeric === null
|
|
422
|
+
? `the command was killed by ${signal || 'a signal'}`
|
|
423
|
+
: `the command exited with code ${numeric}`;
|
|
424
|
+
outcome = { exitCode: numeric, exited: true, started: true };
|
|
425
|
+
finish(false, `shutdown-refused: ${why}`);
|
|
328
426
|
};
|
|
329
427
|
child.on('spawn', () => {
|
|
330
428
|
try {
|
|
331
429
|
if (child && typeof child.unref === 'function') child.unref();
|
|
332
430
|
} catch { /* nothing else to do */ }
|
|
333
|
-
|
|
431
|
+
stopTimer(spawnTimer);
|
|
432
|
+
// The command started; now give it a bounded window to answer with its
|
|
433
|
+
// exit code.
|
|
434
|
+
try {
|
|
435
|
+
exitTimer = setTimeoutFn(() => {
|
|
436
|
+
outcome = { exitCode: null, exited: false, started: true, stillRunning: true };
|
|
437
|
+
finish(true, null);
|
|
438
|
+
}, exitTimeoutMs);
|
|
439
|
+
if (exitTimer && typeof exitTimer.unref === 'function') exitTimer.unref();
|
|
440
|
+
} catch { /* the wait is only a safety net the other way round */ }
|
|
334
441
|
});
|
|
335
442
|
child.on('error', (error) => {
|
|
443
|
+
outcome = { exitCode: null, exited: null, started: false };
|
|
336
444
|
finish(false, `spawn-failed: ${errorText(error)}`);
|
|
337
445
|
});
|
|
446
|
+
if (typeof child.once === 'function' || typeof child.on === 'function') {
|
|
447
|
+
child.on('exit', onExit);
|
|
448
|
+
child.on('close', onExit);
|
|
449
|
+
}
|
|
338
450
|
try {
|
|
339
|
-
|
|
451
|
+
spawnTimer = setTimeoutFn(() => {
|
|
452
|
+
outcome = { exitCode: null, exited: null, started: false };
|
|
340
453
|
finish(false, `spawn-timeout: ${command} did not start within ${timeoutMs}ms`);
|
|
341
454
|
}, timeoutMs);
|
|
342
|
-
if (
|
|
455
|
+
if (spawnTimer && typeof spawnTimer.unref === 'function') spawnTimer.unref();
|
|
343
456
|
} catch { /* the timeout is only a safety net */ }
|
|
344
457
|
});
|
|
345
458
|
}
|
|
@@ -350,11 +463,15 @@ function startCommand(command, args, {
|
|
|
350
463
|
* The gate is checked ONE more time right before the command runs: a session
|
|
351
464
|
* that started working in the meantime must be able to stop the shutdown.
|
|
352
465
|
*
|
|
353
|
-
* The command is really started
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
* reaches Windows through two of them), and the first one that
|
|
466
|
+
* The command is really started AND its exit code is read: a command that does
|
|
467
|
+
* not start, and a command the system REFUSED (non-zero exit code), are both a
|
|
468
|
+
* FAILURE (`executed: false`) with the reason in `error`. When the main command
|
|
469
|
+
* fails that way, the plan's fallbacks are tried one after another (the WSL
|
|
470
|
+
* plan reaches Windows through two of them), and the first one that works wins.
|
|
471
|
+
*
|
|
472
|
+
* `executed: true` therefore means: the command ran and the system did not
|
|
473
|
+
* refuse it. `exitCode` / `exited` / `stillRunning` travel with the answer so a
|
|
474
|
+
* caller can tell "confirmed" from "started, not confirmed".
|
|
358
475
|
*
|
|
359
476
|
* With `dryRun` nothing is executed at all: the command that WOULD run is
|
|
360
477
|
* reported instead.
|
|
@@ -366,6 +483,7 @@ function startCommand(command, args, {
|
|
|
366
483
|
* log?: { warn?: Function, info?: Function },
|
|
367
484
|
* gate?: () => { allow?: boolean } | null,
|
|
368
485
|
* timeoutMs?: number,
|
|
486
|
+
* exitTimeoutMs?: number,
|
|
369
487
|
* }} [options]
|
|
370
488
|
* @returns {Promise<{
|
|
371
489
|
* ok: boolean,
|
|
@@ -375,6 +493,10 @@ function startCommand(command, args, {
|
|
|
375
493
|
* executed: boolean,
|
|
376
494
|
* pid: number|null,
|
|
377
495
|
* error: string|null,
|
|
496
|
+
* exitCode?: number|null,
|
|
497
|
+
* exited?: boolean|null,
|
|
498
|
+
* stillRunning?: boolean,
|
|
499
|
+
* observed?: boolean,
|
|
378
500
|
* dryRun?: boolean,
|
|
379
501
|
* }>}
|
|
380
502
|
*/
|
|
@@ -384,6 +506,7 @@ export function runShutdown(plan, {
|
|
|
384
506
|
log = console,
|
|
385
507
|
gate = null,
|
|
386
508
|
timeoutMs = SPAWN_TIMEOUT_MS,
|
|
509
|
+
exitTimeoutMs = EXIT_TIMEOUT_MS,
|
|
387
510
|
} = {}) {
|
|
388
511
|
const at = new Date().toISOString();
|
|
389
512
|
const source = plan && typeof plan === 'object' ? plan : {};
|
|
@@ -425,8 +548,9 @@ export function runShutdown(plan, {
|
|
|
425
548
|
|
|
426
549
|
return (async () => {
|
|
427
550
|
let lastError = 'no-shutdown-command';
|
|
551
|
+
let lastExitCode = null;
|
|
428
552
|
for (const attempt of attempts) {
|
|
429
|
-
const outcome = await startCommand(attempt.command, attempt.args, { spawn, timeoutMs });
|
|
553
|
+
const outcome = await startCommand(attempt.command, attempt.args, { spawn, timeoutMs, exitTimeoutMs });
|
|
430
554
|
if (outcome.ok) {
|
|
431
555
|
return {
|
|
432
556
|
ok: true,
|
|
@@ -436,14 +560,29 @@ export function runShutdown(plan, {
|
|
|
436
560
|
executed: true,
|
|
437
561
|
pid: outcome.pid,
|
|
438
562
|
error: null,
|
|
563
|
+
exitCode: outcome.exitCode === undefined ? null : outcome.exitCode,
|
|
564
|
+
exited: outcome.exited === undefined ? null : outcome.exited,
|
|
565
|
+
...(outcome.stillRunning === true ? { stillRunning: true } : {}),
|
|
566
|
+
...(outcome.observed === false ? { observed: false } : {}),
|
|
439
567
|
};
|
|
440
568
|
}
|
|
441
569
|
lastError = outcome.error;
|
|
570
|
+
lastExitCode = outcome.exitCode === undefined ? null : outcome.exitCode;
|
|
442
571
|
if (log && typeof log.warn === 'function') {
|
|
443
|
-
log.warn(`the sleep command ${attempt.command} did not
|
|
572
|
+
log.warn(`the sleep command ${attempt.command} did not work:`, outcome.error);
|
|
444
573
|
}
|
|
445
574
|
}
|
|
446
|
-
return {
|
|
575
|
+
return {
|
|
576
|
+
ok: false,
|
|
577
|
+
at,
|
|
578
|
+
command,
|
|
579
|
+
args,
|
|
580
|
+
executed: false,
|
|
581
|
+
pid: null,
|
|
582
|
+
error: lastError,
|
|
583
|
+
exitCode: lastExitCode,
|
|
584
|
+
exited: true,
|
|
585
|
+
};
|
|
447
586
|
})();
|
|
448
587
|
}
|
|
449
588
|
|
|
@@ -728,6 +867,13 @@ export function createSleepController({
|
|
|
728
867
|
pid: Number.isFinite(run.pid) ? run.pid : null,
|
|
729
868
|
error: run.error == null ? null : String(run.error),
|
|
730
869
|
};
|
|
870
|
+
// THE EXIT CODE TRAVELS WITH THE ANSWER (2026-11-02): "the command exited
|
|
871
|
+
// with code 1" and "the command is still running" are different facts, and
|
|
872
|
+
// the interface must be able to say which one happened.
|
|
873
|
+
if (Number.isFinite(run.exitCode)) normalized.exitCode = Number(run.exitCode);
|
|
874
|
+
if (typeof run.exited === 'boolean') normalized.exited = run.exited;
|
|
875
|
+
if (run.stillRunning === true) normalized.stillRunning = true;
|
|
876
|
+
if (run.observed === false) normalized.observed = false;
|
|
731
877
|
// A dry run is reported as such, because the UI has to know that the
|
|
732
878
|
// command was NOT executed. A real run keeps its shape untouched.
|
|
733
879
|
if (run.dryRun === true) normalized.dryRun = true;
|
|
@@ -879,9 +1025,15 @@ export function createSleepController({
|
|
|
879
1025
|
stopTimer();
|
|
880
1026
|
// A countdown of zero seconds says so in the message too: the user
|
|
881
1027
|
// must see that the machine went off without any countdown.
|
|
1028
|
+
//
|
|
1029
|
+
// THE WORDING IS "REQUESTED", NOT "SHUTTING DOWN" (2026-11-02): at this
|
|
1030
|
+
// moment the command has only been ISSUED. Whether the machine really
|
|
1031
|
+
// goes off is decided by the system, and the exit code arrives a moment
|
|
1032
|
+
// later — the run's answer either keeps this stage or turns it back into
|
|
1033
|
+
// "idle, the machine is still on".
|
|
882
1034
|
message = countdownMs > 0
|
|
883
|
-
?
|
|
884
|
-
: '0
|
|
1035
|
+
? `已发出${target}命令,正在等系统的回答……`
|
|
1036
|
+
: '0 秒:立即关机,已发出关机命令,正在等系统的回答……';
|
|
885
1037
|
let result = null;
|
|
886
1038
|
try {
|
|
887
1039
|
result = shutdown();
|
|
@@ -941,7 +1093,16 @@ export function createSleepController({
|
|
|
941
1093
|
armed = false;
|
|
942
1094
|
blockers = [];
|
|
943
1095
|
remainingMs = 0;
|
|
944
|
-
|
|
1096
|
+
// A REFUSAL IS NOT A CRASH (2026-11-02): "the system said no" (a
|
|
1097
|
+
// non-zero exit code) and "the command could not be started" are
|
|
1098
|
+
// different sentences, and the user has to read the right one.
|
|
1099
|
+
const failure = run.error || 'unknown error';
|
|
1100
|
+
if (String(failure).startsWith('shutdown-refused')) {
|
|
1101
|
+
const detail = String(failure).replace(/^shutdown-refused:\s*/, '');
|
|
1102
|
+
message = `关机被系统拒绝(${detail}),电脑还开着,可以再点一次月亮按钮。`;
|
|
1103
|
+
} else {
|
|
1104
|
+
message = `关机命令没跑起来(${failure}),电脑还开着,可以再点一次月亮按钮。`;
|
|
1105
|
+
}
|
|
945
1106
|
disarm('shutdown-failed');
|
|
946
1107
|
};
|
|
947
1108
|
if (result && typeof result.then === 'function') {
|
package/lib/watchdog-disarm.js
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
readReady,
|
|
42
42
|
writeReady,
|
|
43
43
|
} from './watchdog.js';
|
|
44
|
-
import { removeScheduleItem } from './watchdog-schedule.js';
|
|
44
|
+
import { removeLeftoverProbeTasks, removeScheduleItem } from './watchdog-schedule.js';
|
|
45
45
|
import { TIERS } from './watchdog-tier.js';
|
|
46
46
|
|
|
47
47
|
/** The file that records how the last disarm went. */
|
|
@@ -270,7 +270,9 @@ export function disarmOnce({
|
|
|
270
270
|
clearPendingFn = clearPending,
|
|
271
271
|
removeItem = removeScheduleItem,
|
|
272
272
|
taskExists = defaultTaskExists,
|
|
273
|
+
removeProbes = removeLeftoverProbeTasks,
|
|
273
274
|
pendingHint = null,
|
|
275
|
+
platform = process.platform,
|
|
274
276
|
} = {}) {
|
|
275
277
|
const dir = typeof stateDir === 'string' ? stateDir : '';
|
|
276
278
|
const errors = [];
|
|
@@ -377,6 +379,40 @@ export function disarmOnce({
|
|
|
377
379
|
}
|
|
378
380
|
}
|
|
379
381
|
|
|
382
|
+
// (4) THE LEFTOVER CAPABILITY PROBES (2026-11-01).
|
|
383
|
+
//
|
|
384
|
+
// The capability probe (lib/scheduler-capability.js) registers a one-off task
|
|
385
|
+
// and deletes it as its LAST step, so a crash, a kill or a refused delete
|
|
386
|
+
// leaves it behind: two were found on the real machine, both `Ready`, neither
|
|
387
|
+
// with a next run time. The old disarm only removed the watchdog item named by
|
|
388
|
+
// the pending marker and never looked for them, so they accumulated one per
|
|
389
|
+
// boot.
|
|
390
|
+
//
|
|
391
|
+
// A refusal here is recorded and NOT pushed into `errors`: those decide whether
|
|
392
|
+
// the disarm itself counts as done, and the host being up does not depend on
|
|
393
|
+
// cleaning a stale probe task. The caller logs it and the record carries it, so
|
|
394
|
+
// "the cleanup did not work" is still visible.
|
|
395
|
+
let probes = null;
|
|
396
|
+
if (typeof removeProbes === 'function') {
|
|
397
|
+
try {
|
|
398
|
+
// THE PLATFORM TRAVELS WITH THE REQUEST (2026-11-02): off Windows there
|
|
399
|
+
// are no scheduled tasks to clean, so the cleanup says `skipped` instead
|
|
400
|
+
// of reporting a `schtasks` failure on every single boot.
|
|
401
|
+
probes = removeProbes({ platform: typeof platform === 'string' && platform !== '' ? platform : process.platform });
|
|
402
|
+
} catch (error) {
|
|
403
|
+
probes = {
|
|
404
|
+
ok: false,
|
|
405
|
+
prefix: '',
|
|
406
|
+
matched: [],
|
|
407
|
+
removed: [],
|
|
408
|
+
failed: [],
|
|
409
|
+
skipped: false,
|
|
410
|
+
reason: null,
|
|
411
|
+
error: error && error.message ? String(error.message) : String(error),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
380
416
|
const ready = (() => {
|
|
381
417
|
try {
|
|
382
418
|
const marker = read(dir);
|
|
@@ -389,7 +425,7 @@ export function disarmOnce({
|
|
|
389
425
|
const pendingLeft = pendingAfter !== null && pendingAfter !== undefined;
|
|
390
426
|
const ok = errors.length === 0 && ready === true && pendingLeft === false
|
|
391
427
|
&& (removed === null || removed.ok === true);
|
|
392
|
-
return { ok, ready, pending: pendingLeft, removed, errors };
|
|
428
|
+
return { ok, ready, pending: pendingLeft, removed, errors, probes };
|
|
393
429
|
}
|
|
394
430
|
|
|
395
431
|
/**
|
|
@@ -437,8 +473,10 @@ export async function disarmWatchdog({
|
|
|
437
473
|
clearPendingFn = clearPending,
|
|
438
474
|
removeItem = removeScheduleItem,
|
|
439
475
|
taskExists = defaultTaskExists,
|
|
476
|
+
removeProbes = removeLeftoverProbeTasks,
|
|
440
477
|
writeRecord = true,
|
|
441
478
|
log = null,
|
|
479
|
+
platform = process.platform,
|
|
442
480
|
} = {}) {
|
|
443
481
|
const dir = typeof stateDir === 'string' ? stateDir : '';
|
|
444
482
|
const wanted = Number.isFinite(Number(attempts)) && Number(attempts) > 0
|
|
@@ -469,7 +507,9 @@ export async function disarmWatchdog({
|
|
|
469
507
|
clearPendingFn,
|
|
470
508
|
removeItem,
|
|
471
509
|
taskExists,
|
|
510
|
+
removeProbes,
|
|
472
511
|
pendingHint: hint,
|
|
512
|
+
platform,
|
|
473
513
|
});
|
|
474
514
|
errors.push(...last.errors);
|
|
475
515
|
if (last.ok) break;
|
|
@@ -493,6 +533,10 @@ export async function disarmWatchdog({
|
|
|
493
533
|
scheduleError: last !== null && last.removed !== null && last.removed.error
|
|
494
534
|
? String(last.removed.error)
|
|
495
535
|
: null,
|
|
536
|
+
// THE LEFTOVER CAPABILITY PROBES: what the cleanup found and what it did.
|
|
537
|
+
// Kept OUT of `errors` on purpose — a refused cleanup of a stale probe task
|
|
538
|
+
// is not a failed disarm, but it must not be invisible either.
|
|
539
|
+
probes: last !== null && last.probes !== null && last.probes !== undefined ? last.probes : null,
|
|
496
540
|
errors,
|
|
497
541
|
};
|
|
498
542
|
if (writeRecord) writeDisarmRecord(dir, record);
|