dsh-update-status 0.3.1 → 0.4.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/CHANGELOG.md +22 -0
- package/README.md +30 -26
- package/README.zh.md +30 -26
- package/docs/platform-acceptance.md +3 -1
- package/docs/service-supervision.md +36 -6
- package/lib/client.js +83 -13
- package/lib/client.js.map +1 -1
- package/lib/index.js +347 -43
- package/lib/plan-BornHBEZ.js +587 -0
- package/lib/service-cli.d.ts +58 -0
- package/lib/service-cli.js +98 -346
- package/package.json +2 -6
package/lib/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { c as parseSystemdMainPid, d as bootStampFile, f as countRecentRestarts, h as serviceLogDir, i as SYSTEMD_UNIT, m as recordBootStamp, p as dshHomeOf, r as LAUNCHD_LABEL, s as parseLaunchdTrackedPid, t as planFor, u as RESTART_STORM_WINDOW_MS } from "./plan-BornHBEZ.js";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
|
-
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
4
|
import { basename, dirname, join, normalize, sep } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { homedir } from "node:os";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
6
8
|
//#region src/shared/types.ts
|
|
7
9
|
const PACKAGE_NAME = "@deepseek-ai/dsh";
|
|
8
10
|
/** Shared Connection RPC channel. Custom prefixes 405 on the SPA fallback. */
|
|
@@ -242,14 +244,14 @@ function createUpdateStatusRpcHandler(service, restart, restartContext) {
|
|
|
242
244
|
if (restart === void 0) return failure("dsh-update-status/restart-unavailable", "restart support is not configured");
|
|
243
245
|
return {
|
|
244
246
|
ok: true,
|
|
245
|
-
value: restart.
|
|
247
|
+
value: await restart.assess()
|
|
246
248
|
};
|
|
247
249
|
}
|
|
248
250
|
if (endpoint === RESTART_ENDPOINTS.check) {
|
|
249
251
|
if (restart === void 0 || restartContext === void 0) return failure("dsh-update-status/restart-unavailable", "restart support is not configured");
|
|
250
252
|
return {
|
|
251
253
|
ok: true,
|
|
252
|
-
value: restart.check(restartContext)
|
|
254
|
+
value: await restart.check(restartContext)
|
|
253
255
|
};
|
|
254
256
|
}
|
|
255
257
|
if (endpoint === RESTART_ENDPOINTS.request) {
|
|
@@ -258,7 +260,7 @@ function createUpdateStatusRpcHandler(service, restart, restartContext) {
|
|
|
258
260
|
if (restart === void 0 || restartContext === void 0) return failure("dsh-update-status/restart-unavailable", "restart support is not configured");
|
|
259
261
|
return {
|
|
260
262
|
ok: true,
|
|
261
|
-
value: restart.request(restartContext, request.force, request.expectedInstanceId)
|
|
263
|
+
value: await restart.request(restartContext, request.force, request.expectedInstanceId)
|
|
262
264
|
};
|
|
263
265
|
}
|
|
264
266
|
return failure("dsh-update-status/unknown-endpoint", `unknown endpoint: ${endpoint}`);
|
|
@@ -291,7 +293,228 @@ function installUpdateStatusRpc(ctx, service, restart) {
|
|
|
291
293
|
}
|
|
292
294
|
});
|
|
293
295
|
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/host/service-record.ts
|
|
298
|
+
/**
|
|
299
|
+
* What the *service* is, read back from our own installer receipt.
|
|
300
|
+
*
|
|
301
|
+
* The panel must be able to tell an operator how to get supervision back after
|
|
302
|
+
* it was lost — a restart storm tripped the wrapper's ceiling, or another tool's
|
|
303
|
+
* instance took the port. Those steps are platform commands, and the plugin
|
|
304
|
+
* never guesses them: the installer wrote the receipt, so the receipt is where
|
|
305
|
+
* the port and the matching recover command come from.
|
|
306
|
+
*
|
|
307
|
+
* Read-only and best effort. A missing, foreign or unreadable receipt simply
|
|
308
|
+
* means "no recovery hint", never an error on a status route.
|
|
309
|
+
*/
|
|
310
|
+
const SERVICE_RECEIPT_FILE = "dsh-update-status-service.json";
|
|
311
|
+
/** Whoever is listening on the port, without touching it — per platform. */
|
|
312
|
+
function portListCommand(platform, port) {
|
|
313
|
+
if (platform === "win32") return `netstat -ano | findstr :${String(port)}`;
|
|
314
|
+
return `lsof -nP -iTCP:${String(port)} -sTCP:LISTEN`;
|
|
315
|
+
}
|
|
316
|
+
function specOf(value) {
|
|
317
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
318
|
+
const spec = value;
|
|
319
|
+
if (spec.platform !== "darwin" && spec.platform !== "linux" && spec.platform !== "win32") return void 0;
|
|
320
|
+
if (typeof spec.port !== "number" || !Number.isInteger(spec.port) || spec.port < 1 || spec.port > 65535) return void 0;
|
|
321
|
+
if (typeof spec.label !== "string" || spec.label === "") return void 0;
|
|
322
|
+
for (const field of [
|
|
323
|
+
spec.nodePath,
|
|
324
|
+
spec.dshPath,
|
|
325
|
+
spec.profile,
|
|
326
|
+
spec.workspace,
|
|
327
|
+
spec.host,
|
|
328
|
+
spec.home,
|
|
329
|
+
spec.logDir
|
|
330
|
+
]) if (typeof field !== "string" || field === "") return void 0;
|
|
331
|
+
if (spec.supervisorMarker !== "dsh-update-status") return void 0;
|
|
332
|
+
return spec;
|
|
333
|
+
}
|
|
334
|
+
const CACHE_TTL_MS = 3e4;
|
|
335
|
+
/**
|
|
336
|
+
* Cached for a short window: the receipt changes only when the operator re-installs,
|
|
337
|
+
* and this runs on every restart-status poll while a restart is unavailable.
|
|
338
|
+
*/
|
|
339
|
+
function createServiceRecovery(options = {}) {
|
|
340
|
+
const now = options.now ?? Date.now;
|
|
341
|
+
const read = options.readFile ?? ((path) => {
|
|
342
|
+
try {
|
|
343
|
+
if (!existsSync(path) || !statSync(path).isFile()) return void 0;
|
|
344
|
+
return readFileSync(path, "utf8");
|
|
345
|
+
} catch {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
let cached;
|
|
350
|
+
return () => {
|
|
351
|
+
const at = now();
|
|
352
|
+
if (cached !== void 0 && at - cached.at < CACHE_TTL_MS) return cached.value;
|
|
353
|
+
const value = readRecovery(options, read);
|
|
354
|
+
cached = {
|
|
355
|
+
at,
|
|
356
|
+
value
|
|
357
|
+
};
|
|
358
|
+
return value;
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function readRecovery(options, read) {
|
|
362
|
+
const text = read(join(dshHomeOf(options.env ?? process.env), SERVICE_RECEIPT_FILE));
|
|
363
|
+
if (text === void 0) return void 0;
|
|
364
|
+
try {
|
|
365
|
+
const receipt = JSON.parse(text);
|
|
366
|
+
if (receipt.version !== 1 || receipt.managedBy !== "dsh-update-status") return void 0;
|
|
367
|
+
const spec = specOf(receipt.spec);
|
|
368
|
+
if (spec === void 0) return void 0;
|
|
369
|
+
const plan = planFor(spec, options.uid ?? (typeof process.getuid === "function" ? process.getuid() : 0));
|
|
370
|
+
return { commands: `${portListCommand(spec.platform, spec.port)}\n${plan.recoverCommand}` };
|
|
371
|
+
} catch {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/host/supervision.ts
|
|
377
|
+
/**
|
|
378
|
+
* Platform ownership of the running DSH Web process, plus restart-storm evidence.
|
|
379
|
+
*
|
|
380
|
+
* The environment marker (`DSH_WEB_SUPERVISOR`) is necessary but *not* sufficient:
|
|
381
|
+
* every descendant of a supervised process inherits it. A foreign launcher — a
|
|
382
|
+
* second plugin's restart helper, a `nohup` script — therefore looks supervised
|
|
383
|
+
* while the real job is stuck respawning a process that can never bind the port.
|
|
384
|
+
* The panel would offer a destructive restart and promise a recovery the platform
|
|
385
|
+
* cannot deliver, so ownership is asked of the platform itself:
|
|
386
|
+
*
|
|
387
|
+
* - launchd: the tracked pid of our fixed job label must be this process or its
|
|
388
|
+
* wrapper parent (asked by label, never from the environment);
|
|
389
|
+
* - systemd: the unit's `MainPID` must be this process or its wrapper parent;
|
|
390
|
+
* - Task Scheduler: the parent process must be the wrapper this plugin installed.
|
|
391
|
+
*
|
|
392
|
+
* Anything unverifiable fails closed: restart is destructive, and a false "yes"
|
|
393
|
+
* strands the page, while a false "no" only asks the user to restart DSH by hand.
|
|
394
|
+
*/
|
|
395
|
+
function defaultRun(file, args) {
|
|
396
|
+
return new Promise((resolve) => {
|
|
397
|
+
execFile(file, [...args], {
|
|
398
|
+
encoding: "utf8",
|
|
399
|
+
timeout: 5e3,
|
|
400
|
+
windowsHide: true
|
|
401
|
+
}, (error, stdout, stderr) => {
|
|
402
|
+
const code = error === null ? 0 : error.code;
|
|
403
|
+
resolve({
|
|
404
|
+
status: typeof code === "number" ? code : error === null ? 0 : null,
|
|
405
|
+
stdout: stdout ?? "",
|
|
406
|
+
stderr: stderr ?? "",
|
|
407
|
+
...error === null ? {} : { error: error.message }
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
async function probeSupervisorOwnership(options = {}) {
|
|
413
|
+
const platform = options.platform ?? process.platform;
|
|
414
|
+
const pid = options.pid ?? process.pid;
|
|
415
|
+
const ppid = options.ppid ?? process.ppid;
|
|
416
|
+
const run = options.run ?? defaultRun;
|
|
417
|
+
try {
|
|
418
|
+
if (platform === "darwin") {
|
|
419
|
+
const uid = options.uid ?? (typeof process.getuid === "function" ? process.getuid() : void 0);
|
|
420
|
+
if (uid === void 0) return "unverifiable";
|
|
421
|
+
const result = await run("/bin/launchctl", ["print", `gui/${String(uid)}/${LAUNCHD_LABEL}`]);
|
|
422
|
+
if (result.status === null && result.error !== void 0) return "unverifiable";
|
|
423
|
+
const tracked = parseLaunchdTrackedPid(result.stdout);
|
|
424
|
+
if (tracked === void 0) return "not-owner";
|
|
425
|
+
return tracked === pid || tracked === ppid ? "owned" : "not-owner";
|
|
426
|
+
}
|
|
427
|
+
if (platform === "linux") {
|
|
428
|
+
const result = await run("systemctl", [
|
|
429
|
+
"--user",
|
|
430
|
+
"show",
|
|
431
|
+
SYSTEMD_UNIT,
|
|
432
|
+
"-p",
|
|
433
|
+
"MainPID",
|
|
434
|
+
"--value"
|
|
435
|
+
]);
|
|
436
|
+
if (result.status === null && result.error !== void 0) return "unverifiable";
|
|
437
|
+
const tracked = parseSystemdMainPid(result.stdout);
|
|
438
|
+
if (tracked === void 0) return "not-owner";
|
|
439
|
+
return tracked === pid || tracked === ppid ? "owned" : "not-owner";
|
|
440
|
+
}
|
|
441
|
+
if (platform === "win32") {
|
|
442
|
+
const result = await run("powershell.exe", [
|
|
443
|
+
"-NoProfile",
|
|
444
|
+
"-NonInteractive",
|
|
445
|
+
"-Command",
|
|
446
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId=${String(ppid)}").CommandLine`
|
|
447
|
+
]);
|
|
448
|
+
if (result.status === null && result.error !== void 0) return "unverifiable";
|
|
449
|
+
return result.stdout.includes("dsh-web-supervisor.ps1") ? "owned" : "not-owner";
|
|
450
|
+
}
|
|
451
|
+
return "unverifiable";
|
|
452
|
+
} catch {
|
|
453
|
+
return "unverifiable";
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Ownership is cheap to cache: the answer cannot change while a restart is not in
|
|
458
|
+
* flight, and the status route is polled by every open panel.
|
|
459
|
+
*/
|
|
460
|
+
function cacheOwnershipProbe(probe, ttlMs = 1e4, now = Date.now) {
|
|
461
|
+
let cached;
|
|
462
|
+
let inflight;
|
|
463
|
+
return async () => {
|
|
464
|
+
if (cached !== void 0 && now() - cached.at < ttlMs) return cached.verdict;
|
|
465
|
+
if (inflight !== void 0) return inflight;
|
|
466
|
+
inflight = probe().catch(() => "unverifiable").then((verdict) => {
|
|
467
|
+
cached = {
|
|
468
|
+
at: now(),
|
|
469
|
+
verdict
|
|
470
|
+
};
|
|
471
|
+
return verdict;
|
|
472
|
+
}).finally(() => {
|
|
473
|
+
inflight = void 0;
|
|
474
|
+
});
|
|
475
|
+
return inflight;
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function classifySupervisorHealth(ownership, recentRestarts, windowMs = RESTART_STORM_WINDOW_MS, threshold = 4) {
|
|
479
|
+
if (ownership === "unverifiable") return { kind: "unverifiable" };
|
|
480
|
+
if (ownership === "not-owner") return { kind: "not-owner" };
|
|
481
|
+
if (recentRestarts >= threshold) return {
|
|
482
|
+
kind: "thrashing",
|
|
483
|
+
recentRestarts,
|
|
484
|
+
windowMs
|
|
485
|
+
};
|
|
486
|
+
return { kind: "owned" };
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Ownership first, storm second: "not the supervisor's own process" is the more
|
|
490
|
+
* precise explanation whenever both are true.
|
|
491
|
+
*/
|
|
492
|
+
function createSupervisorHealth(options) {
|
|
493
|
+
const now = options.now ?? Date.now;
|
|
494
|
+
const windowMs = options.windowMs ?? 18e4;
|
|
495
|
+
const threshold = options.threshold ?? 4;
|
|
496
|
+
const probe = options.probe ?? cacheOwnershipProbe(() => probeSupervisorOwnership(), 1e4, now);
|
|
497
|
+
return async () => {
|
|
498
|
+
const ownership = await probe();
|
|
499
|
+
if (ownership !== "owned") return classifySupervisorHealth(ownership, 0, windowMs, threshold);
|
|
500
|
+
return classifySupervisorHealth(ownership, countRecentRestarts(options.logDir, options.bootLog, now(), windowMs), windowMs, threshold);
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const BOOT_STAMPED = Symbol.for("dsh-update-status/boot-stamped");
|
|
504
|
+
/**
|
|
505
|
+
* One stamp per Node process, not per plugin composition: DSH can recompose this
|
|
506
|
+
* plugin (HMR) inside a generation that already stamped itself, and counting
|
|
507
|
+
* those would fake a storm.
|
|
508
|
+
*/
|
|
509
|
+
function recordBootOnce(stamp, path = bootStampFile()) {
|
|
510
|
+
const carrier = globalThis;
|
|
511
|
+
if (carrier[BOOT_STAMPED] === true) return false;
|
|
512
|
+
carrier[BOOT_STAMPED] = true;
|
|
513
|
+
recordBootStamp(path, stamp);
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
294
516
|
const INSTANCE_KEY = Symbol.for("dsh-update-status/process-instance-id");
|
|
517
|
+
const STARTED_AT_KEY = Symbol.for("dsh-update-status/process-started-at");
|
|
295
518
|
/**
|
|
296
519
|
* This must outlive plugin HMR/recomposition inside one Node process. A module
|
|
297
520
|
* local random value would make a Host reload look like a successful restart.
|
|
@@ -305,63 +528,76 @@ function processInstanceId() {
|
|
|
305
528
|
return created;
|
|
306
529
|
}
|
|
307
530
|
/**
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
531
|
+
* Wall-clock start of this process, derived once from `process.uptime()` — the
|
|
532
|
+
* one clock the Host owns without extra bookkeeping. It is cached on globalThis
|
|
533
|
+
* for the same reason as the instance id: a recomposed plugin must keep reporting
|
|
534
|
+
* the start of the process it is running in, not the moment it was recomposed.
|
|
535
|
+
*/
|
|
536
|
+
function processStartedAt() {
|
|
537
|
+
const carrier = globalThis;
|
|
538
|
+
const existing = carrier[STARTED_AT_KEY];
|
|
539
|
+
if (existing !== void 0) return existing;
|
|
540
|
+
const created = new Date(Date.now() - Math.round(process.uptime() * 1e3)).toISOString();
|
|
541
|
+
carrier[STARTED_AT_KEY] = created;
|
|
542
|
+
return created;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* The environment/platform half of the verdict: which supervisor this process
|
|
546
|
+
* *claims* to be running under.
|
|
547
|
+
*
|
|
548
|
+
* The explicit marker is never inherited by accident, but every descendant of a
|
|
549
|
+
* supervised process does inherit it, so this function alone can only report a
|
|
550
|
+
* claim. What turns a claim into an answer is `assess()` below, which asks the
|
|
551
|
+
* platform which process the service actually owns. The platform identity is
|
|
552
|
+
* deliberately NOT read from the environment: launchd configures
|
|
553
|
+
* `XPC_SERVICE_NAME=<label>` for the job, yet a DSH process behind the staged
|
|
554
|
+
* `/bin/sh` wrapper reads `XPC_SERVICE_NAME=0`, so an environment check would
|
|
555
|
+
* refuse the very setup this plugin installs.
|
|
311
556
|
*/
|
|
312
557
|
function restartStatusOf(options = {}) {
|
|
313
558
|
const env = options.env ?? process.env;
|
|
314
559
|
const platform = options.platform ?? process.platform;
|
|
315
560
|
const versions = options.versions ?? process.versions;
|
|
316
561
|
const instanceId = options.instanceId ?? processInstanceId();
|
|
562
|
+
const startedAt = options.startedAt ?? processStartedAt();
|
|
317
563
|
if (versions.electron !== void 0 && versions.electron !== "") return {
|
|
318
564
|
instanceId,
|
|
565
|
+
startedAt,
|
|
319
566
|
available: false,
|
|
320
567
|
supervisor: null,
|
|
321
568
|
unavailableReason: "desktop"
|
|
322
569
|
};
|
|
323
570
|
if (env.DSH_WEB_SUPERVISOR !== "dsh-update-status") return {
|
|
324
571
|
instanceId,
|
|
572
|
+
startedAt,
|
|
325
573
|
available: false,
|
|
326
574
|
supervisor: null,
|
|
327
575
|
unavailableReason: "not-supervised"
|
|
328
576
|
};
|
|
329
|
-
if (platform === "darwin") {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
if (platform === "linux") {
|
|
344
|
-
if (typeof env.INVOCATION_ID !== "string" || env.INVOCATION_ID === "") return {
|
|
345
|
-
instanceId,
|
|
346
|
-
available: false,
|
|
347
|
-
supervisor: "systemd",
|
|
348
|
-
unavailableReason: "supervisor-mismatch"
|
|
349
|
-
};
|
|
350
|
-
return {
|
|
351
|
-
instanceId,
|
|
352
|
-
available: true,
|
|
353
|
-
supervisor: "systemd",
|
|
354
|
-
unavailableReason: null
|
|
355
|
-
};
|
|
356
|
-
}
|
|
577
|
+
if (platform === "darwin") return {
|
|
578
|
+
instanceId,
|
|
579
|
+
startedAt,
|
|
580
|
+
available: true,
|
|
581
|
+
supervisor: "launchd",
|
|
582
|
+
unavailableReason: null
|
|
583
|
+
};
|
|
584
|
+
if (platform === "linux") return {
|
|
585
|
+
instanceId,
|
|
586
|
+
startedAt,
|
|
587
|
+
available: true,
|
|
588
|
+
supervisor: "systemd",
|
|
589
|
+
unavailableReason: null
|
|
590
|
+
};
|
|
357
591
|
if (platform === "win32") return {
|
|
358
592
|
instanceId,
|
|
593
|
+
startedAt,
|
|
359
594
|
available: true,
|
|
360
595
|
supervisor: "task-scheduler",
|
|
361
596
|
unavailableReason: null
|
|
362
597
|
};
|
|
363
598
|
return {
|
|
364
599
|
instanceId,
|
|
600
|
+
startedAt,
|
|
365
601
|
available: false,
|
|
366
602
|
supervisor: "unknown",
|
|
367
603
|
unavailableReason: "supervisor-mismatch"
|
|
@@ -460,10 +696,14 @@ function collectActiveWork(ctx) {
|
|
|
460
696
|
var RestartController = class {
|
|
461
697
|
instanceId;
|
|
462
698
|
options;
|
|
699
|
+
health;
|
|
700
|
+
recovery;
|
|
463
701
|
scheduled = false;
|
|
464
702
|
constructor(options = {}) {
|
|
465
703
|
this.options = options;
|
|
466
704
|
this.instanceId = options.instanceId ?? processInstanceId();
|
|
705
|
+
this.health = options.health;
|
|
706
|
+
this.recovery = options.recovery;
|
|
467
707
|
}
|
|
468
708
|
status() {
|
|
469
709
|
return restartStatusOf({
|
|
@@ -471,8 +711,50 @@ var RestartController = class {
|
|
|
471
711
|
instanceId: this.instanceId
|
|
472
712
|
});
|
|
473
713
|
}
|
|
474
|
-
|
|
475
|
-
|
|
714
|
+
/**
|
|
715
|
+
* The environment verdict plus what only the platform knows: whether this
|
|
716
|
+
* process is the one its supervisor is actually tracking, and whether a restart
|
|
717
|
+
* storm says the supervisor cannot complete a start at all. A probe that throws
|
|
718
|
+
* is treated as unverifiable, because restart is destructive.
|
|
719
|
+
*/
|
|
720
|
+
async assess() {
|
|
721
|
+
const base = this.status();
|
|
722
|
+
if (!base.available) return this.withRecovery(base);
|
|
723
|
+
let health;
|
|
724
|
+
if (this.health === void 0) health = { kind: "unverifiable" };
|
|
725
|
+
else try {
|
|
726
|
+
health = await this.health();
|
|
727
|
+
} catch {
|
|
728
|
+
health = { kind: "unverifiable" };
|
|
729
|
+
}
|
|
730
|
+
if (health.kind === "thrashing") return this.withRecovery({
|
|
731
|
+
...base,
|
|
732
|
+
available: false,
|
|
733
|
+
unavailableReason: "supervisor-thrashing"
|
|
734
|
+
});
|
|
735
|
+
if (health.kind === "not-owner" || health.kind === "unverifiable") return this.withRecovery({
|
|
736
|
+
...base,
|
|
737
|
+
available: false,
|
|
738
|
+
unavailableReason: "supervisor-mismatch"
|
|
739
|
+
});
|
|
740
|
+
return base;
|
|
741
|
+
}
|
|
742
|
+
/** A refused restart carries the steps back; an offerable one does not need them. */
|
|
743
|
+
withRecovery(status) {
|
|
744
|
+
if (this.recovery === void 0) return status;
|
|
745
|
+
let recovery;
|
|
746
|
+
try {
|
|
747
|
+
recovery = this.recovery();
|
|
748
|
+
} catch {
|
|
749
|
+
recovery = void 0;
|
|
750
|
+
}
|
|
751
|
+
return recovery === void 0 ? status : {
|
|
752
|
+
...status,
|
|
753
|
+
recovery
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
async check(ctx) {
|
|
757
|
+
const status = await this.assess();
|
|
476
758
|
if (!status.available) return {
|
|
477
759
|
kind: "unavailable",
|
|
478
760
|
status
|
|
@@ -496,7 +778,7 @@ var RestartController = class {
|
|
|
496
778
|
status
|
|
497
779
|
};
|
|
498
780
|
}
|
|
499
|
-
request(ctx, force, expectedInstanceId) {
|
|
781
|
+
async request(ctx, force, expectedInstanceId) {
|
|
500
782
|
if (expectedInstanceId !== this.instanceId) return {
|
|
501
783
|
kind: "unavailable",
|
|
502
784
|
status: {
|
|
@@ -505,7 +787,7 @@ var RestartController = class {
|
|
|
505
787
|
unavailableReason: "stale-instance"
|
|
506
788
|
}
|
|
507
789
|
};
|
|
508
|
-
const checked = this.check(ctx);
|
|
790
|
+
const checked = await this.check(ctx);
|
|
509
791
|
if (checked.kind === "unavailable") return checked;
|
|
510
792
|
if (checked.kind === "active-work" && !force) return checked;
|
|
511
793
|
if (this.scheduled) return {
|
|
@@ -542,9 +824,24 @@ var RestartController = class {
|
|
|
542
824
|
};
|
|
543
825
|
}
|
|
544
826
|
};
|
|
545
|
-
/**
|
|
546
|
-
|
|
547
|
-
|
|
827
|
+
/**
|
|
828
|
+
* Creates the process-scoped controller once per DSH Host generation.
|
|
829
|
+
*
|
|
830
|
+
* The health probe is wired here rather than inside the CLI's plan so the Host
|
|
831
|
+
* and the installer agree on where the evidence lives: `$DSH_HOME/logs`.
|
|
832
|
+
*/
|
|
833
|
+
function createRestartController(options = {}) {
|
|
834
|
+
const env = options.env ?? process.env;
|
|
835
|
+
const health = options.health ?? createSupervisorHealth({
|
|
836
|
+
logDir: serviceLogDir(env),
|
|
837
|
+
bootLog: bootStampFile(env)
|
|
838
|
+
});
|
|
839
|
+
const recovery = options.recovery ?? createServiceRecovery({ env });
|
|
840
|
+
return new RestartController({
|
|
841
|
+
...options,
|
|
842
|
+
health,
|
|
843
|
+
recovery
|
|
844
|
+
});
|
|
548
845
|
}
|
|
549
846
|
//#endregion
|
|
550
847
|
//#region src/host/schemastery.ts
|
|
@@ -1178,7 +1475,14 @@ function apply(ctx, config = {}) {
|
|
|
1178
1475
|
runtimeWarnings: schema.warning === null ? [] : [schema.warning]
|
|
1179
1476
|
});
|
|
1180
1477
|
installSettings(ctx);
|
|
1181
|
-
|
|
1478
|
+
const restart = createRestartController();
|
|
1479
|
+
const restartStatus = restart.status();
|
|
1480
|
+
recordBootOnce({
|
|
1481
|
+
pid: process.pid,
|
|
1482
|
+
instanceId: restartStatus.instanceId,
|
|
1483
|
+
supervisor: restartStatus.supervisor
|
|
1484
|
+
});
|
|
1485
|
+
installUpdateStatusRpc(ctx, service, restart);
|
|
1182
1486
|
if (config.autoCheckOnMount !== false) service.getStatus();
|
|
1183
1487
|
}
|
|
1184
1488
|
//#endregion
|