dsh-update-status 0.2.0 → 0.3.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 +20 -0
- package/README.md +49 -11
- package/README.zh.md +49 -12
- package/assets/update-panel.png +0 -0
- package/assets/update-panel.zh.png +0 -0
- package/bin/dsh-update-status-service.mjs +5 -0
- package/docs/service-supervision.md +129 -0
- package/lib/client.js +591 -43
- package/lib/client.js.map +1 -1
- package/lib/index.js +302 -9
- package/lib/service-cli.js +634 -0
- package/package.json +10 -3
package/lib/index.js
CHANGED
|
@@ -28,6 +28,12 @@ const UPDATE_ENDPOINTS = {
|
|
|
28
28
|
getStatus: "dsh-update-status.get-status",
|
|
29
29
|
checkUpdate: "dsh-update-status.check-update"
|
|
30
30
|
};
|
|
31
|
+
/** Restart RPC remains on the same authenticated Connection `/api` channel. */
|
|
32
|
+
const RESTART_ENDPOINTS = {
|
|
33
|
+
status: "dsh-update-status.restart-status",
|
|
34
|
+
check: "dsh-update-status.restart-check",
|
|
35
|
+
request: "dsh-update-status.restart"
|
|
36
|
+
};
|
|
31
37
|
//#endregion
|
|
32
38
|
//#region src/host/installation.ts
|
|
33
39
|
/**
|
|
@@ -176,6 +182,16 @@ function requestOf(value) {
|
|
|
176
182
|
...record.cacheTtlMinutes === void 0 ? {} : { cacheTtlMinutes: record.cacheTtlMinutes }
|
|
177
183
|
};
|
|
178
184
|
}
|
|
185
|
+
function restartRequestOf(value) {
|
|
186
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
187
|
+
const record = value;
|
|
188
|
+
if (record.force !== void 0 && typeof record.force !== "boolean") return void 0;
|
|
189
|
+
if (typeof record.expectedInstanceId !== "string" || record.expectedInstanceId === "") return void 0;
|
|
190
|
+
return {
|
|
191
|
+
force: record.force === true,
|
|
192
|
+
expectedInstanceId: record.expectedInstanceId
|
|
193
|
+
};
|
|
194
|
+
}
|
|
179
195
|
function jsonResponse(rpcId, result) {
|
|
180
196
|
return Response.json({
|
|
181
197
|
type: "server-response",
|
|
@@ -203,7 +219,7 @@ async function dispatchUpdateStatusFetch(endpoint, handler, request) {
|
|
|
203
219
|
}
|
|
204
220
|
}
|
|
205
221
|
/** Endpoint dispatcher, exported to allow exact JSON-shape tests without a Host. */
|
|
206
|
-
function createUpdateStatusRpcHandler(service) {
|
|
222
|
+
function createUpdateStatusRpcHandler(service, restart, restartContext) {
|
|
207
223
|
return async (endpoint, payload) => {
|
|
208
224
|
try {
|
|
209
225
|
if (endpoint === UPDATE_ENDPOINTS.getStatus) {
|
|
@@ -222,6 +238,29 @@ function createUpdateStatusRpcHandler(service) {
|
|
|
222
238
|
value: await service.check(request.force === true, request.cacheTtlMinutes)
|
|
223
239
|
};
|
|
224
240
|
}
|
|
241
|
+
if (endpoint === RESTART_ENDPOINTS.status) {
|
|
242
|
+
if (restart === void 0) return failure("dsh-update-status/restart-unavailable", "restart support is not configured");
|
|
243
|
+
return {
|
|
244
|
+
ok: true,
|
|
245
|
+
value: restart.status()
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (endpoint === RESTART_ENDPOINTS.check) {
|
|
249
|
+
if (restart === void 0 || restartContext === void 0) return failure("dsh-update-status/restart-unavailable", "restart support is not configured");
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
value: restart.check(restartContext)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (endpoint === RESTART_ENDPOINTS.request) {
|
|
256
|
+
const request = restartRequestOf(payload);
|
|
257
|
+
if (request === void 0) return failure("dsh-update-status/bad-request", "`force` must be boolean");
|
|
258
|
+
if (restart === void 0 || restartContext === void 0) return failure("dsh-update-status/restart-unavailable", "restart support is not configured");
|
|
259
|
+
return {
|
|
260
|
+
ok: true,
|
|
261
|
+
value: restart.request(restartContext, request.force, request.expectedInstanceId)
|
|
262
|
+
};
|
|
263
|
+
}
|
|
225
264
|
return failure("dsh-update-status/unknown-endpoint", `unknown endpoint: ${endpoint}`);
|
|
226
265
|
} catch (error) {
|
|
227
266
|
return failure("dsh-update-status/internal", (error instanceof Error ? error.message : String(error)).slice(0, 220));
|
|
@@ -230,19 +269,18 @@ function createUpdateStatusRpcHandler(service) {
|
|
|
230
269
|
}
|
|
231
270
|
/**
|
|
232
271
|
* Static packages do not receive dynamic Cordis's `harness.handle` closure.
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* Connection before the Typert interceptor and do not need a new HTTP prefix.
|
|
272
|
+
* Exact Fetch routes on `/api/<endpoint>` are dispatched by Connection after
|
|
273
|
+
* its Host/Origin and cookie admission. Every destructive restart request is
|
|
274
|
+
* therefore authenticated before it reaches this plugin.
|
|
237
275
|
*/
|
|
238
|
-
function installUpdateStatusRpc(ctx, service) {
|
|
239
|
-
const handler = createUpdateStatusRpcHandler(service);
|
|
276
|
+
function installUpdateStatusRpc(ctx, service, restart) {
|
|
277
|
+
const handler = createUpdateStatusRpcHandler(service, restart, ctx);
|
|
240
278
|
ctx.inject(["connection"], (owned) => {
|
|
241
279
|
const connection = owned.connection;
|
|
242
280
|
const register = typeof connection.fetch?.register === "function" ? connection.fetch.register.bind(connection.fetch) : void 0;
|
|
243
281
|
if (register === void 0) return;
|
|
244
282
|
try {
|
|
245
|
-
for (const endpoint of Object.values(UPDATE_ENDPOINTS)) register({
|
|
283
|
+
for (const endpoint of [...Object.values(UPDATE_ENDPOINTS), ...Object.values(RESTART_ENDPOINTS)]) register({
|
|
246
284
|
path: `${UPDATE_STATUS_CHANNEL}/${endpoint}`,
|
|
247
285
|
methods: ["POST"],
|
|
248
286
|
requestBody: "buffered",
|
|
@@ -253,6 +291,261 @@ function installUpdateStatusRpc(ctx, service) {
|
|
|
253
291
|
}
|
|
254
292
|
});
|
|
255
293
|
}
|
|
294
|
+
const INSTANCE_KEY = Symbol.for("dsh-update-status/process-instance-id");
|
|
295
|
+
/**
|
|
296
|
+
* This must outlive plugin HMR/recomposition inside one Node process. A module
|
|
297
|
+
* local random value would make a Host reload look like a successful restart.
|
|
298
|
+
*/
|
|
299
|
+
function processInstanceId() {
|
|
300
|
+
const carrier = globalThis;
|
|
301
|
+
const existing = carrier[INSTANCE_KEY];
|
|
302
|
+
if (existing !== void 0) return existing;
|
|
303
|
+
const created = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
304
|
+
carrier[INSTANCE_KEY] = created;
|
|
305
|
+
return created;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Strict supervisor recognition: the marker is written only by this package's
|
|
309
|
+
* user-service installer. Platform-specific signals defend against a user
|
|
310
|
+
* copying that environment variable into an arbitrary terminal by accident.
|
|
311
|
+
*/
|
|
312
|
+
function restartStatusOf(options = {}) {
|
|
313
|
+
const env = options.env ?? process.env;
|
|
314
|
+
const platform = options.platform ?? process.platform;
|
|
315
|
+
const versions = options.versions ?? process.versions;
|
|
316
|
+
const instanceId = options.instanceId ?? processInstanceId();
|
|
317
|
+
if (versions.electron !== void 0 && versions.electron !== "") return {
|
|
318
|
+
instanceId,
|
|
319
|
+
available: false,
|
|
320
|
+
supervisor: null,
|
|
321
|
+
unavailableReason: "desktop"
|
|
322
|
+
};
|
|
323
|
+
if (env.DSH_WEB_SUPERVISOR !== "dsh-update-status") return {
|
|
324
|
+
instanceId,
|
|
325
|
+
available: false,
|
|
326
|
+
supervisor: null,
|
|
327
|
+
unavailableReason: "not-supervised"
|
|
328
|
+
};
|
|
329
|
+
if (platform === "darwin") {
|
|
330
|
+
if (env.XPC_SERVICE_NAME !== "com.idoall.dsh-update-status.web") return {
|
|
331
|
+
instanceId,
|
|
332
|
+
available: false,
|
|
333
|
+
supervisor: "launchd",
|
|
334
|
+
unavailableReason: "supervisor-mismatch"
|
|
335
|
+
};
|
|
336
|
+
return {
|
|
337
|
+
instanceId,
|
|
338
|
+
available: true,
|
|
339
|
+
supervisor: "launchd",
|
|
340
|
+
unavailableReason: null
|
|
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
|
+
}
|
|
357
|
+
if (platform === "win32") return {
|
|
358
|
+
instanceId,
|
|
359
|
+
available: true,
|
|
360
|
+
supervisor: "task-scheduler",
|
|
361
|
+
unavailableReason: null
|
|
362
|
+
};
|
|
363
|
+
return {
|
|
364
|
+
instanceId,
|
|
365
|
+
available: false,
|
|
366
|
+
supervisor: "unknown",
|
|
367
|
+
unavailableReason: "supervisor-mismatch"
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/** Same facts dsh-service lists before allowing its force-restart control. */
|
|
371
|
+
function collectActiveWork(ctx) {
|
|
372
|
+
const unavailable = () => ({
|
|
373
|
+
available: false,
|
|
374
|
+
hasActive: false,
|
|
375
|
+
items: []
|
|
376
|
+
});
|
|
377
|
+
const getter = typeof ctx.get === "function" ? ctx.get.bind(ctx) : void 0;
|
|
378
|
+
if (getter === void 0) return unavailable();
|
|
379
|
+
let agentsFace;
|
|
380
|
+
let jobsFace;
|
|
381
|
+
try {
|
|
382
|
+
agentsFace = getter("agents");
|
|
383
|
+
jobsFace = getter("jobs");
|
|
384
|
+
} catch {
|
|
385
|
+
return unavailable();
|
|
386
|
+
}
|
|
387
|
+
if (typeof agentsFace?.list !== "function") return unavailable();
|
|
388
|
+
let agents;
|
|
389
|
+
try {
|
|
390
|
+
agents = agentsFace.list();
|
|
391
|
+
} catch {
|
|
392
|
+
return unavailable();
|
|
393
|
+
}
|
|
394
|
+
const items = [];
|
|
395
|
+
for (const agent of agents) {
|
|
396
|
+
if (agent.status !== "running") continue;
|
|
397
|
+
const id = String(agent.id);
|
|
398
|
+
items.push({
|
|
399
|
+
type: "agent",
|
|
400
|
+
id,
|
|
401
|
+
label: id,
|
|
402
|
+
status: "running"
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (jobsFace !== void 0) {
|
|
406
|
+
if (typeof jobsFace.list !== "function") return unavailable();
|
|
407
|
+
const byId = /* @__PURE__ */ new Map();
|
|
408
|
+
for (const caller of [void 0, ...agents.map((agent) => agent.id)]) try {
|
|
409
|
+
for (const job of jobsFace.list(caller)) {
|
|
410
|
+
if (job.status !== "running" && job.status !== "stopping") continue;
|
|
411
|
+
byId.set(String(job.id), job);
|
|
412
|
+
}
|
|
413
|
+
} catch {
|
|
414
|
+
return unavailable();
|
|
415
|
+
}
|
|
416
|
+
for (const job of byId.values()) items.push({
|
|
417
|
+
type: "job",
|
|
418
|
+
id: String(job.id),
|
|
419
|
+
label: typeof job.label === "string" && job.label !== "" ? job.label : String(job.id),
|
|
420
|
+
status: typeof job.status === "string" ? job.status : "running",
|
|
421
|
+
...job.owner === void 0 ? {} : { ownerSession: String(job.owner) }
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
const terminals = /* @__PURE__ */ new Map();
|
|
425
|
+
for (const owner of agents) {
|
|
426
|
+
const ownerCtx = owner.ctx;
|
|
427
|
+
if (ownerCtx === void 0 || typeof ownerCtx.get !== "function") return unavailable();
|
|
428
|
+
let face;
|
|
429
|
+
try {
|
|
430
|
+
face = ownerCtx.get("terminals");
|
|
431
|
+
} catch {
|
|
432
|
+
return unavailable();
|
|
433
|
+
}
|
|
434
|
+
if (face === void 0) continue;
|
|
435
|
+
if (typeof face.list !== "function") return unavailable();
|
|
436
|
+
try {
|
|
437
|
+
for (const terminal of face.list(owner)) {
|
|
438
|
+
if (terminal.status?.kind !== "running") continue;
|
|
439
|
+
terminals.set(String(terminal.sessionId), terminal);
|
|
440
|
+
}
|
|
441
|
+
} catch {
|
|
442
|
+
return unavailable();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const terminal of terminals.values()) {
|
|
446
|
+
const id = String(terminal.sessionId);
|
|
447
|
+
items.push({
|
|
448
|
+
type: "terminal",
|
|
449
|
+
id,
|
|
450
|
+
label: typeof terminal.name === "string" && terminal.name !== "" ? terminal.name : `${typeof terminal.type === "string" ? terminal.type : "terminal"} terminal`,
|
|
451
|
+
status: "running"
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
available: true,
|
|
456
|
+
hasActive: items.length > 0,
|
|
457
|
+
items
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
var RestartController = class {
|
|
461
|
+
instanceId;
|
|
462
|
+
options;
|
|
463
|
+
scheduled = false;
|
|
464
|
+
constructor(options = {}) {
|
|
465
|
+
this.options = options;
|
|
466
|
+
this.instanceId = options.instanceId ?? processInstanceId();
|
|
467
|
+
}
|
|
468
|
+
status() {
|
|
469
|
+
return restartStatusOf({
|
|
470
|
+
...this.options,
|
|
471
|
+
instanceId: this.instanceId
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
check(ctx) {
|
|
475
|
+
const status = this.status();
|
|
476
|
+
if (!status.available) return {
|
|
477
|
+
kind: "unavailable",
|
|
478
|
+
status
|
|
479
|
+
};
|
|
480
|
+
const activity = collectActiveWork(ctx);
|
|
481
|
+
if (!activity.available) return {
|
|
482
|
+
kind: "unavailable",
|
|
483
|
+
status: {
|
|
484
|
+
...status,
|
|
485
|
+
available: false,
|
|
486
|
+
unavailableReason: "activity-unavailable"
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
if (activity.hasActive) return {
|
|
490
|
+
kind: "active-work",
|
|
491
|
+
status,
|
|
492
|
+
activity
|
|
493
|
+
};
|
|
494
|
+
return {
|
|
495
|
+
kind: "ready",
|
|
496
|
+
status
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
request(ctx, force, expectedInstanceId) {
|
|
500
|
+
if (expectedInstanceId !== this.instanceId) return {
|
|
501
|
+
kind: "unavailable",
|
|
502
|
+
status: {
|
|
503
|
+
...this.status(),
|
|
504
|
+
available: false,
|
|
505
|
+
unavailableReason: "stale-instance"
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const checked = this.check(ctx);
|
|
509
|
+
if (checked.kind === "unavailable") return checked;
|
|
510
|
+
if (checked.kind === "active-work" && !force) return checked;
|
|
511
|
+
if (this.scheduled) return {
|
|
512
|
+
kind: "in-progress",
|
|
513
|
+
instanceId: this.instanceId
|
|
514
|
+
};
|
|
515
|
+
let timer;
|
|
516
|
+
try {
|
|
517
|
+
timer = ctx.get?.("timer");
|
|
518
|
+
} catch {
|
|
519
|
+
timer = void 0;
|
|
520
|
+
}
|
|
521
|
+
const schedule = typeof timer?.timeout === "function" ? (callback) => {
|
|
522
|
+
timer.timeout(callback, 500);
|
|
523
|
+
} : this.options.schedule === void 0 ? void 0 : (callback) => {
|
|
524
|
+
this.options.schedule(callback, 500);
|
|
525
|
+
};
|
|
526
|
+
if (schedule === void 0) return {
|
|
527
|
+
kind: "unavailable",
|
|
528
|
+
status: {
|
|
529
|
+
...this.status(),
|
|
530
|
+
available: false,
|
|
531
|
+
unavailableReason: "timer-unavailable"
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
this.scheduled = true;
|
|
535
|
+
const exit = this.options.exit ?? ((code) => process.exit(code));
|
|
536
|
+
schedule(() => {
|
|
537
|
+
exit(42);
|
|
538
|
+
});
|
|
539
|
+
return {
|
|
540
|
+
kind: "scheduled",
|
|
541
|
+
instanceId: this.instanceId
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
/** Creates the process-scoped controller once per DSH Host generation. */
|
|
546
|
+
function createRestartController() {
|
|
547
|
+
return new RestartController();
|
|
548
|
+
}
|
|
256
549
|
//#endregion
|
|
257
550
|
//#region src/host/schemastery.ts
|
|
258
551
|
/**
|
|
@@ -885,7 +1178,7 @@ function apply(ctx, config = {}) {
|
|
|
885
1178
|
runtimeWarnings: schema.warning === null ? [] : [schema.warning]
|
|
886
1179
|
});
|
|
887
1180
|
installSettings(ctx);
|
|
888
|
-
installUpdateStatusRpc(ctx, service);
|
|
1181
|
+
installUpdateStatusRpc(ctx, service, createRestartController());
|
|
889
1182
|
if (config.autoCheckOnMount !== false) service.getStatus();
|
|
890
1183
|
}
|
|
891
1184
|
//#endregion
|