dsh-update-status 0.2.1 → 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 +11 -0
- package/README.md +46 -10
- package/README.zh.md +46 -11
- 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 +568 -37
- 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/client.js
CHANGED
|
@@ -53,6 +53,12 @@ window.__ModuleLoader__.load({
|
|
|
53
53
|
getStatus: "dsh-update-status.get-status",
|
|
54
54
|
checkUpdate: "dsh-update-status.check-update"
|
|
55
55
|
};
|
|
56
|
+
/** Restart RPC remains on the same authenticated Connection `/api` channel. */
|
|
57
|
+
const RESTART_ENDPOINTS = {
|
|
58
|
+
status: "dsh-update-status.restart-status",
|
|
59
|
+
check: "dsh-update-status.restart-check",
|
|
60
|
+
request: "dsh-update-status.restart"
|
|
61
|
+
};
|
|
56
62
|
//#endregion
|
|
57
63
|
//#region src/client/i18n.ts
|
|
58
64
|
const DICTIONARY = {
|
|
@@ -177,12 +183,88 @@ window.__ModuleLoader__.load({
|
|
|
177
183
|
en: "Clipboard unavailable; the command is selected for long-press or copy."
|
|
178
184
|
},
|
|
179
185
|
"panel.commandNote": {
|
|
180
|
-
zh: "仅复制,不会执行。请在运行 DSH
|
|
181
|
-
en: "Copy only — nothing runs here. Execute it in a terminal on the computer running DSH, then restart DSH yourself."
|
|
186
|
+
zh: "仅复制,不会执行。请在运行 DSH 的那台电脑的终端执行;完成后仅在已安装用户级监督服务时使用本面板的重启,否则由你自行重启 DSH。",
|
|
187
|
+
en: "Copy only — nothing runs here. Execute it in a terminal on the computer running DSH, then use this panel’s restart only when user-level supervision is installed; otherwise restart DSH yourself."
|
|
182
188
|
},
|
|
183
189
|
"panel.readOnly": {
|
|
184
|
-
zh: "
|
|
185
|
-
en: "
|
|
190
|
+
zh: "插件不会安装、升级、回滚或替换 DSH 文件;只有当前 DSH 已由用户显式安装的后台服务监督时,才允许安全重启。",
|
|
191
|
+
en: "The plugin never installs, upgrades, rolls back, or replaces DSH files. Safe restart is available only when the current DSH is supervised by a user-installed service."
|
|
192
|
+
},
|
|
193
|
+
"restart.title": {
|
|
194
|
+
zh: "安全重启 DSH Web",
|
|
195
|
+
en: "Safely restart DSH Web"
|
|
196
|
+
},
|
|
197
|
+
"restart.button": {
|
|
198
|
+
zh: "重启",
|
|
199
|
+
en: "Restart"
|
|
200
|
+
},
|
|
201
|
+
"restart.loading": {
|
|
202
|
+
zh: "读取重启状态…",
|
|
203
|
+
en: "Reading restart status…"
|
|
204
|
+
},
|
|
205
|
+
"restart.checking": {
|
|
206
|
+
zh: "检查活动任务…",
|
|
207
|
+
en: "Checking active work…"
|
|
208
|
+
},
|
|
209
|
+
"restart.confirm": {
|
|
210
|
+
zh: "确认重启",
|
|
211
|
+
en: "Confirm restart"
|
|
212
|
+
},
|
|
213
|
+
"restart.force": {
|
|
214
|
+
zh: "强制重启",
|
|
215
|
+
en: "Force restart"
|
|
216
|
+
},
|
|
217
|
+
"restart.cancel": {
|
|
218
|
+
zh: "取消",
|
|
219
|
+
en: "Cancel"
|
|
220
|
+
},
|
|
221
|
+
"restart.retry": {
|
|
222
|
+
zh: "重新检查",
|
|
223
|
+
en: "Check again"
|
|
224
|
+
},
|
|
225
|
+
"restart.confirmHint": {
|
|
226
|
+
zh: "再次点击“确认重启”后,页面会短暂断开;后台服务恢复新实例后将自动刷新。",
|
|
227
|
+
en: "Click Confirm restart again. The page disconnects briefly and refreshes once the supervised service starts a new instance."
|
|
228
|
+
},
|
|
229
|
+
"restart.activeWarning": {
|
|
230
|
+
zh: "检测到 {count} 个正在运行的任务。强制重启会中断它们。",
|
|
231
|
+
en: "{count} active work item(s) are running. A forced restart interrupts them."
|
|
232
|
+
},
|
|
233
|
+
"restart.waitingShort": {
|
|
234
|
+
zh: "重启中…",
|
|
235
|
+
en: "Restarting…"
|
|
236
|
+
},
|
|
237
|
+
"restart.waiting": {
|
|
238
|
+
zh: "正在等待后台服务恢复 DSH(已等待 {seconds} 秒)…",
|
|
239
|
+
en: "Waiting for the user service to restore DSH ({seconds}s elapsed)…"
|
|
240
|
+
},
|
|
241
|
+
"restart.timeout": {
|
|
242
|
+
zh: "60 秒内未检测到新 DSH 实例。请检查用户服务状态和日志;重新加载本页不会再次触发重启。",
|
|
243
|
+
en: "No new DSH instance was detected within 60 seconds. Check the user-service status and logs; reloading this page will not trigger another restart."
|
|
244
|
+
},
|
|
245
|
+
"restart.unavailable": {
|
|
246
|
+
zh: "当前 DSH 不是由已确认的用户级后台服务启动。为避免页面重启后无法恢复,按钮已禁用;请先按服务管理文档安装并接管 DSH。",
|
|
247
|
+
en: "This DSH is not running under a verified user-level service. Restart is disabled so the page cannot strand itself; install and activate supervision first."
|
|
248
|
+
},
|
|
249
|
+
"restart.unavailableDesktop": {
|
|
250
|
+
zh: "DSH Desktop 管理当前 Host;请退出并重新打开桌面应用。",
|
|
251
|
+
en: "DSH Desktop manages this Host. Quit and reopen the desktop app instead."
|
|
252
|
+
},
|
|
253
|
+
"restart.unavailableMismatch": {
|
|
254
|
+
zh: "检测到监督标记,但当前进程不符合对应平台服务的身份。为安全起见,重启已禁用。",
|
|
255
|
+
en: "A supervision marker exists, but this process does not match the platform service identity. Restart is disabled for safety."
|
|
256
|
+
},
|
|
257
|
+
"restart.unavailableActivity": {
|
|
258
|
+
zh: "无法完整读取正在运行的任务。为避免中断未能列出的工作,重启已禁用。",
|
|
259
|
+
en: "Active work could not be inspected completely. Restart is disabled to avoid interrupting work that could not be listed."
|
|
260
|
+
},
|
|
261
|
+
"restart.unavailableTimer": {
|
|
262
|
+
zh: "当前 Host 没有可用的安全退出调度器。为确保响应先送达浏览器,重启已禁用。",
|
|
263
|
+
en: "This Host has no safe exit scheduler. Restart is disabled so its response can reach the browser first."
|
|
264
|
+
},
|
|
265
|
+
"restart.unavailableStale": {
|
|
266
|
+
zh: "此页面连接的是过期 DSH 实例。请等待恢复或重新加载后再重试。",
|
|
267
|
+
en: "This page is connected to a stale DSH instance. Wait for recovery or reload before trying again."
|
|
186
268
|
},
|
|
187
269
|
"panel.error": {
|
|
188
270
|
zh: "检查提示",
|
|
@@ -412,6 +494,85 @@ window.__ModuleLoader__.load({
|
|
|
412
494
|
})]
|
|
413
495
|
});
|
|
414
496
|
}
|
|
497
|
+
function restartUnavailableText(reason) {
|
|
498
|
+
if (reason === "desktop") return t("restart.unavailableDesktop");
|
|
499
|
+
if (reason === "supervisor-mismatch") return t("restart.unavailableMismatch");
|
|
500
|
+
if (reason === "activity-unavailable") return t("restart.unavailableActivity");
|
|
501
|
+
if (reason === "timer-unavailable") return t("restart.unavailableTimer");
|
|
502
|
+
if (reason === "stale-instance") return t("restart.unavailableStale");
|
|
503
|
+
return t("restart.unavailable");
|
|
504
|
+
}
|
|
505
|
+
function RestartControl({ restart }) {
|
|
506
|
+
const snapshot = useObservable(restart);
|
|
507
|
+
React.useEffect(() => {
|
|
508
|
+
restart.refresh();
|
|
509
|
+
}, [restart]);
|
|
510
|
+
const unavailable = snapshot.status !== null && !snapshot.status.available;
|
|
511
|
+
const hasActivity = snapshot.activity.length > 0;
|
|
512
|
+
const waiting = snapshot.phase === "waiting";
|
|
513
|
+
const label = snapshot.phase === "unknown" ? t("restart.loading") : snapshot.phase === "checking" ? t("restart.checking") : snapshot.phase === "armed" ? hasActivity ? t("restart.force") : t("restart.confirm") : waiting ? t("restart.waitingShort") : snapshot.phase === "timeout" ? t("restart.retry") : snapshot.phase === "error" ? t("restart.retry") : t("restart.button");
|
|
514
|
+
const disabled = snapshot.phase === "unknown" || snapshot.phase === "checking" || waiting || unavailable;
|
|
515
|
+
const click = () => {
|
|
516
|
+
if (snapshot.phase === "armed") {
|
|
517
|
+
restart.request(hasActivity);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (snapshot.phase === "timeout" || snapshot.phase === "error") {
|
|
521
|
+
restart.check();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
restart.check();
|
|
525
|
+
};
|
|
526
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
527
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
528
|
+
className: "dus-action dus-restart",
|
|
529
|
+
type: "button",
|
|
530
|
+
disabled,
|
|
531
|
+
title: unavailable ? restartUnavailableText(snapshot.status?.unavailableReason) : t("restart.title"),
|
|
532
|
+
onClick: click,
|
|
533
|
+
children: label
|
|
534
|
+
}),
|
|
535
|
+
snapshot.phase === "armed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
536
|
+
className: "dus-action",
|
|
537
|
+
type: "button",
|
|
538
|
+
onClick: () => restart.cancel(),
|
|
539
|
+
children: t("restart.cancel")
|
|
540
|
+
}),
|
|
541
|
+
unavailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
542
|
+
className: "dus-restart-message",
|
|
543
|
+
role: "status",
|
|
544
|
+
children: restartUnavailableText(snapshot.status?.unavailableReason)
|
|
545
|
+
}),
|
|
546
|
+
snapshot.phase === "armed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
547
|
+
className: "dus-restart-message",
|
|
548
|
+
role: "status",
|
|
549
|
+
children: hasActivity ? t("restart.activeWarning", { count: String(snapshot.activity.length) }) : t("restart.confirmHint")
|
|
550
|
+
}),
|
|
551
|
+
hasActivity && snapshot.phase === "armed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
552
|
+
className: "dus-restart-work",
|
|
553
|
+
children: snapshot.activity.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
554
|
+
item.label,
|
|
555
|
+
" · ",
|
|
556
|
+
item.status
|
|
557
|
+
] }, `${item.type}:${item.id}`))
|
|
558
|
+
}),
|
|
559
|
+
waiting && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
560
|
+
className: "dus-restart-message",
|
|
561
|
+
role: "status",
|
|
562
|
+
children: t("restart.waiting", { seconds: String(Math.floor(snapshot.elapsedMs / 1e3)) })
|
|
563
|
+
}),
|
|
564
|
+
snapshot.phase === "timeout" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
565
|
+
className: "dus-restart-message",
|
|
566
|
+
role: "alert",
|
|
567
|
+
children: t("restart.timeout")
|
|
568
|
+
}),
|
|
569
|
+
snapshot.phase === "error" && snapshot.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
570
|
+
className: "dus-restart-message",
|
|
571
|
+
role: "alert",
|
|
572
|
+
children: snapshot.error
|
|
573
|
+
})
|
|
574
|
+
] });
|
|
575
|
+
}
|
|
415
576
|
function statusSummary(status, loading, error) {
|
|
416
577
|
if (loading && status === null) return {
|
|
417
578
|
kind: "warning",
|
|
@@ -655,21 +816,25 @@ window.__ModuleLoader__.load({
|
|
|
655
816
|
}),
|
|
656
817
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
657
818
|
className: "dus-actions",
|
|
658
|
-
children: [
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
819
|
+
children: [
|
|
820
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
821
|
+
className: "dus-action",
|
|
822
|
+
type: "button",
|
|
823
|
+
disabled: snapshot.loading,
|
|
824
|
+
onClick: () => {
|
|
825
|
+
ui.status.refresh(preferences.cacheTtlMinutes);
|
|
826
|
+
},
|
|
827
|
+
children: snapshot.loading ? t("panel.checking") : t("panel.check")
|
|
828
|
+
}),
|
|
829
|
+
status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
830
|
+
className: "dus-action",
|
|
831
|
+
href: status.changelogUrl,
|
|
832
|
+
target: "_blank",
|
|
833
|
+
rel: "noreferrer",
|
|
834
|
+
children: t("panel.releaseNotes")
|
|
835
|
+
}),
|
|
836
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartControl, { restart: ui.restart })
|
|
837
|
+
]
|
|
673
838
|
}),
|
|
674
839
|
hasUpdate && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandBlock, {
|
|
675
840
|
command,
|
|
@@ -762,21 +927,25 @@ window.__ModuleLoader__.load({
|
|
|
762
927
|
}),
|
|
763
928
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
764
929
|
className: "dus-actions",
|
|
765
|
-
children: [
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
930
|
+
children: [
|
|
931
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
932
|
+
className: "dus-action",
|
|
933
|
+
type: "button",
|
|
934
|
+
disabled: snapshot.loading,
|
|
935
|
+
onClick: () => {
|
|
936
|
+
ui.status.refresh(preferences.cacheTtlMinutes);
|
|
937
|
+
},
|
|
938
|
+
children: snapshot.loading ? t("panel.checking") : t("panel.check")
|
|
939
|
+
}),
|
|
940
|
+
status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
941
|
+
className: "dus-action",
|
|
942
|
+
href: status.changelogUrl,
|
|
943
|
+
target: "_blank",
|
|
944
|
+
rel: "noreferrer",
|
|
945
|
+
children: t("panel.releaseNotes")
|
|
946
|
+
}),
|
|
947
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartControl, { restart: ui.restart })
|
|
948
|
+
]
|
|
780
949
|
})
|
|
781
950
|
]
|
|
782
951
|
})
|
|
@@ -788,6 +957,71 @@ window.__ModuleLoader__.load({
|
|
|
788
957
|
function errorMessage(error) {
|
|
789
958
|
return (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim().slice(0, 220) || "unknown error";
|
|
790
959
|
}
|
|
960
|
+
function objectOf(value) {
|
|
961
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
962
|
+
}
|
|
963
|
+
function restartStatusOf(value) {
|
|
964
|
+
const record = objectOf(value);
|
|
965
|
+
if (record === void 0 || typeof record.instanceId !== "string" || record.instanceId === "" || typeof record.available !== "boolean") return void 0;
|
|
966
|
+
const supervisor = record.supervisor === null ? null : record.supervisor === "launchd" || record.supervisor === "systemd" || record.supervisor === "task-scheduler" || record.supervisor === "unknown" ? record.supervisor : void 0;
|
|
967
|
+
const unavailableReason = record.unavailableReason === null ? null : record.unavailableReason === "not-supervised" || record.unavailableReason === "desktop" || record.unavailableReason === "supervisor-mismatch" || record.unavailableReason === "activity-unavailable" || record.unavailableReason === "timer-unavailable" || record.unavailableReason === "stale-instance" ? record.unavailableReason : void 0;
|
|
968
|
+
if (supervisor === void 0 || unavailableReason === void 0) return void 0;
|
|
969
|
+
if (record.available && (supervisor === null || unavailableReason !== null)) return void 0;
|
|
970
|
+
if (!record.available && unavailableReason === null) return void 0;
|
|
971
|
+
return {
|
|
972
|
+
instanceId: record.instanceId,
|
|
973
|
+
available: record.available,
|
|
974
|
+
supervisor,
|
|
975
|
+
unavailableReason
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
function restartActivityOf(value) {
|
|
979
|
+
const record = objectOf(value);
|
|
980
|
+
if (record === void 0 || typeof record.hasActive !== "boolean" || !Array.isArray(record.items)) return void 0;
|
|
981
|
+
const items = [];
|
|
982
|
+
for (const raw of record.items) {
|
|
983
|
+
const item = objectOf(raw);
|
|
984
|
+
if (item === void 0 || item.type !== "agent" && item.type !== "job" && item.type !== "terminal" || typeof item.id !== "string" || typeof item.label !== "string" || typeof item.status !== "string") return void 0;
|
|
985
|
+
if (item.ownerSession !== void 0 && typeof item.ownerSession !== "string") return void 0;
|
|
986
|
+
items.push({
|
|
987
|
+
type: item.type,
|
|
988
|
+
id: item.id,
|
|
989
|
+
label: item.label,
|
|
990
|
+
status: item.status,
|
|
991
|
+
...item.ownerSession === void 0 ? {} : { ownerSession: item.ownerSession }
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
if (record.hasActive !== items.length > 0) return void 0;
|
|
995
|
+
return {
|
|
996
|
+
hasActive: record.hasActive,
|
|
997
|
+
items
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
function restartCheckOf(value) {
|
|
1001
|
+
const record = objectOf(value);
|
|
1002
|
+
if (record === void 0 || record.kind !== "ready" && record.kind !== "active-work" && record.kind !== "unavailable") return void 0;
|
|
1003
|
+
const status = restartStatusOf(record.status);
|
|
1004
|
+
if (status === void 0) return void 0;
|
|
1005
|
+
const activity = record.activity === void 0 ? void 0 : restartActivityOf(record.activity);
|
|
1006
|
+
if (record.activity !== void 0 && activity === void 0) return void 0;
|
|
1007
|
+
if (record.kind === "active-work" && (activity === void 0 || !activity.hasActive)) return void 0;
|
|
1008
|
+
if (record.kind !== "active-work" && activity !== void 0) return void 0;
|
|
1009
|
+
return {
|
|
1010
|
+
kind: record.kind,
|
|
1011
|
+
status,
|
|
1012
|
+
...activity === void 0 ? {} : { activity }
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function restartRequestOf(value) {
|
|
1016
|
+
const record = objectOf(value);
|
|
1017
|
+
if (record === void 0 || record.kind !== "ready" && record.kind !== "active-work" && record.kind !== "unavailable" && record.kind !== "scheduled" && record.kind !== "in-progress") return void 0;
|
|
1018
|
+
if (record.kind === "scheduled" || record.kind === "in-progress") return typeof record.instanceId === "string" && record.instanceId !== "" ? {
|
|
1019
|
+
kind: record.kind,
|
|
1020
|
+
instanceId: record.instanceId
|
|
1021
|
+
} : void 0;
|
|
1022
|
+
const check = restartCheckOf(record);
|
|
1023
|
+
return check === void 0 ? void 0 : check;
|
|
1024
|
+
}
|
|
791
1025
|
function stringOrNull(value) {
|
|
792
1026
|
return typeof value === "string" ? value : null;
|
|
793
1027
|
}
|
|
@@ -1064,6 +1298,292 @@ window.__ModuleLoader__.load({
|
|
|
1064
1298
|
for (const listener of this.listeners) listener();
|
|
1065
1299
|
}
|
|
1066
1300
|
};
|
|
1301
|
+
const INITIAL_RESTART = {
|
|
1302
|
+
phase: "unknown",
|
|
1303
|
+
status: null,
|
|
1304
|
+
activity: [],
|
|
1305
|
+
elapsedMs: 0,
|
|
1306
|
+
error: null
|
|
1307
|
+
};
|
|
1308
|
+
/**
|
|
1309
|
+
* Restart state belongs to the client entry, not a modal component, so closing
|
|
1310
|
+
* the update panel cannot strand a confirmed restart half-way through recovery.
|
|
1311
|
+
* It starts no timer until the user confirms a restart request.
|
|
1312
|
+
*/
|
|
1313
|
+
var RestartStore = class {
|
|
1314
|
+
connection;
|
|
1315
|
+
snapshot = INITIAL_RESTART;
|
|
1316
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1317
|
+
timer;
|
|
1318
|
+
generation = 0;
|
|
1319
|
+
stopped = false;
|
|
1320
|
+
schedule;
|
|
1321
|
+
cancelTimer;
|
|
1322
|
+
requestTimeout;
|
|
1323
|
+
cancelRequestTimeout;
|
|
1324
|
+
liveness;
|
|
1325
|
+
reload;
|
|
1326
|
+
pendingAbort;
|
|
1327
|
+
reloaded = false;
|
|
1328
|
+
constructor(connection, options = {}) {
|
|
1329
|
+
this.connection = connection;
|
|
1330
|
+
this.schedule = options.schedule ?? ((callback, delay) => window.setTimeout(callback, delay));
|
|
1331
|
+
this.cancelTimer = options.cancel ?? ((handle) => {
|
|
1332
|
+
window.clearTimeout(handle);
|
|
1333
|
+
});
|
|
1334
|
+
this.requestTimeout = options.requestTimeout ?? ((callback, delay) => window.setTimeout(callback, delay));
|
|
1335
|
+
this.cancelRequestTimeout = options.cancelRequestTimeout ?? ((handle) => {
|
|
1336
|
+
window.clearTimeout(handle);
|
|
1337
|
+
});
|
|
1338
|
+
this.liveness = options.liveness ?? (async () => {
|
|
1339
|
+
try {
|
|
1340
|
+
const response = await fetch(new URL(".", window.location.href), {
|
|
1341
|
+
method: "HEAD",
|
|
1342
|
+
credentials: "same-origin",
|
|
1343
|
+
cache: "no-store"
|
|
1344
|
+
});
|
|
1345
|
+
return response.status === 200 || response.status === 401;
|
|
1346
|
+
} catch {
|
|
1347
|
+
return false;
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
this.reload = options.reload ?? (() => window.location.reload());
|
|
1351
|
+
}
|
|
1352
|
+
getSnapshot = () => this.snapshot;
|
|
1353
|
+
subscribe = (listener) => {
|
|
1354
|
+
this.listeners.add(listener);
|
|
1355
|
+
return () => {
|
|
1356
|
+
this.listeners.delete(listener);
|
|
1357
|
+
};
|
|
1358
|
+
};
|
|
1359
|
+
stop() {
|
|
1360
|
+
this.stopped = true;
|
|
1361
|
+
this.generation += 1;
|
|
1362
|
+
if (this.timer !== void 0) this.cancelTimer(this.timer);
|
|
1363
|
+
this.timer = void 0;
|
|
1364
|
+
this.pendingAbort?.abort();
|
|
1365
|
+
this.pendingAbort = void 0;
|
|
1366
|
+
this.listeners.clear();
|
|
1367
|
+
}
|
|
1368
|
+
/** Read capability on a visible surface; this is not a restart and never polls. */
|
|
1369
|
+
async refresh() {
|
|
1370
|
+
if (this.snapshot.phase === "waiting") return;
|
|
1371
|
+
const raw = await this.call(RESTART_ENDPOINTS.status, {});
|
|
1372
|
+
if (raw === void 0) return;
|
|
1373
|
+
const status = restartStatusOf(raw);
|
|
1374
|
+
if (status === void 0) {
|
|
1375
|
+
this.publish({
|
|
1376
|
+
...this.snapshot,
|
|
1377
|
+
phase: "error",
|
|
1378
|
+
error: "invalid restart-status payload"
|
|
1379
|
+
});
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
this.publish({
|
|
1383
|
+
phase: status.available ? "idle" : "idle",
|
|
1384
|
+
status,
|
|
1385
|
+
activity: [],
|
|
1386
|
+
elapsedMs: 0,
|
|
1387
|
+
error: null
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
/** First click checks active work and arms the second, destructive action. */
|
|
1391
|
+
async check() {
|
|
1392
|
+
this.publish({
|
|
1393
|
+
...this.snapshot,
|
|
1394
|
+
phase: "checking",
|
|
1395
|
+
error: null
|
|
1396
|
+
});
|
|
1397
|
+
const raw = await this.call(RESTART_ENDPOINTS.check, {});
|
|
1398
|
+
if (raw === void 0) return;
|
|
1399
|
+
const checked = restartCheckOf(raw);
|
|
1400
|
+
if (checked === void 0) {
|
|
1401
|
+
this.publish({
|
|
1402
|
+
...this.snapshot,
|
|
1403
|
+
phase: "error",
|
|
1404
|
+
error: "invalid restart-check payload"
|
|
1405
|
+
});
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (checked.kind === "unavailable") {
|
|
1409
|
+
this.publish({
|
|
1410
|
+
phase: "idle",
|
|
1411
|
+
status: checked.status,
|
|
1412
|
+
activity: [],
|
|
1413
|
+
elapsedMs: 0,
|
|
1414
|
+
error: null
|
|
1415
|
+
});
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
this.publish({
|
|
1419
|
+
phase: "armed",
|
|
1420
|
+
status: checked.status,
|
|
1421
|
+
activity: checked.activity?.items ?? [],
|
|
1422
|
+
elapsedMs: 0,
|
|
1423
|
+
error: null
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
cancel() {
|
|
1427
|
+
if (this.snapshot.phase !== "armed" && this.snapshot.phase !== "error" && this.snapshot.phase !== "timeout") return;
|
|
1428
|
+
this.publish({
|
|
1429
|
+
...this.snapshot,
|
|
1430
|
+
phase: this.snapshot.status === null ? "unknown" : "idle",
|
|
1431
|
+
activity: [],
|
|
1432
|
+
elapsedMs: 0,
|
|
1433
|
+
error: null
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
/** Second click emits the Host request; `force` is only true after the activity list was shown. */
|
|
1437
|
+
async request(force) {
|
|
1438
|
+
const current = this.snapshot;
|
|
1439
|
+
if (current.phase !== "armed" || current.status === null || !current.status.available) return;
|
|
1440
|
+
this.publish({
|
|
1441
|
+
...current,
|
|
1442
|
+
phase: "checking",
|
|
1443
|
+
error: null
|
|
1444
|
+
});
|
|
1445
|
+
const raw = await this.call(RESTART_ENDPOINTS.request, {
|
|
1446
|
+
force,
|
|
1447
|
+
expectedInstanceId: current.status.instanceId
|
|
1448
|
+
});
|
|
1449
|
+
if (raw === void 0) return;
|
|
1450
|
+
const result = restartRequestOf(raw);
|
|
1451
|
+
if (result === void 0) {
|
|
1452
|
+
this.publish({
|
|
1453
|
+
...this.snapshot,
|
|
1454
|
+
phase: "error",
|
|
1455
|
+
error: "invalid restart payload"
|
|
1456
|
+
});
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (result.kind === "scheduled" || result.kind === "in-progress") {
|
|
1460
|
+
if (result.instanceId === void 0) {
|
|
1461
|
+
this.publish({
|
|
1462
|
+
...this.snapshot,
|
|
1463
|
+
phase: "error",
|
|
1464
|
+
error: "restart response omitted instance identity"
|
|
1465
|
+
});
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
this.beginRecovery(result.instanceId);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
if (result.kind === "active-work" || result.kind === "ready") {
|
|
1472
|
+
this.publish({
|
|
1473
|
+
phase: "armed",
|
|
1474
|
+
status: result.status ?? current.status,
|
|
1475
|
+
activity: result.activity?.items ?? [],
|
|
1476
|
+
elapsedMs: 0,
|
|
1477
|
+
error: null
|
|
1478
|
+
});
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
this.publish({
|
|
1482
|
+
phase: "idle",
|
|
1483
|
+
status: result.status ?? current.status,
|
|
1484
|
+
activity: [],
|
|
1485
|
+
elapsedMs: 0,
|
|
1486
|
+
error: null
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
beginRecovery(previousInstanceId) {
|
|
1490
|
+
const generation = ++this.generation;
|
|
1491
|
+
const startedAt = Date.now();
|
|
1492
|
+
this.publish({
|
|
1493
|
+
...this.snapshot,
|
|
1494
|
+
phase: "waiting",
|
|
1495
|
+
elapsedMs: 0,
|
|
1496
|
+
error: null
|
|
1497
|
+
});
|
|
1498
|
+
const attempt = async (delay) => {
|
|
1499
|
+
if (this.stopped || generation !== this.generation) return;
|
|
1500
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1501
|
+
if (elapsedMs >= 6e4) {
|
|
1502
|
+
this.publish({
|
|
1503
|
+
...this.snapshot,
|
|
1504
|
+
phase: "timeout",
|
|
1505
|
+
elapsedMs,
|
|
1506
|
+
error: null
|
|
1507
|
+
});
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
this.timer = this.schedule(() => {
|
|
1511
|
+
(async () => {
|
|
1512
|
+
if (this.stopped || generation !== this.generation) return;
|
|
1513
|
+
const raw = await this.call(RESTART_ENDPOINTS.status, {}, false, 4e3);
|
|
1514
|
+
if (this.stopped || generation !== this.generation) return;
|
|
1515
|
+
const status = raw === void 0 ? void 0 : restartStatusOf(raw);
|
|
1516
|
+
const nextElapsed = Date.now() - startedAt;
|
|
1517
|
+
if (status !== void 0 && status.instanceId !== previousInstanceId) {
|
|
1518
|
+
this.publish({
|
|
1519
|
+
phase: "waiting",
|
|
1520
|
+
status,
|
|
1521
|
+
activity: [],
|
|
1522
|
+
elapsedMs: nextElapsed,
|
|
1523
|
+
error: null
|
|
1524
|
+
});
|
|
1525
|
+
if (!this.reloaded) {
|
|
1526
|
+
this.reloaded = true;
|
|
1527
|
+
this.reload();
|
|
1528
|
+
}
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
if (raw === void 0 && await this.liveness()) {
|
|
1532
|
+
if (!this.reloaded) {
|
|
1533
|
+
this.reloaded = true;
|
|
1534
|
+
this.reload();
|
|
1535
|
+
}
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
this.publish({
|
|
1539
|
+
...this.snapshot,
|
|
1540
|
+
phase: "waiting",
|
|
1541
|
+
elapsedMs: nextElapsed
|
|
1542
|
+
});
|
|
1543
|
+
await attempt(Math.min(delay * 2, 1e4));
|
|
1544
|
+
})();
|
|
1545
|
+
}, delay);
|
|
1546
|
+
};
|
|
1547
|
+
attempt(1e3);
|
|
1548
|
+
}
|
|
1549
|
+
async call(endpoint, payload, surfaceError = true, timeoutMs) {
|
|
1550
|
+
const rpc = this.connection.rpc;
|
|
1551
|
+
if (rpc === void 0 || typeof rpc.call !== "function") {
|
|
1552
|
+
if (surfaceError) this.publish({
|
|
1553
|
+
...this.snapshot,
|
|
1554
|
+
phase: "error",
|
|
1555
|
+
error: "DSH connection RPC is unavailable"
|
|
1556
|
+
});
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
const controller = timeoutMs === void 0 ? void 0 : new AbortController();
|
|
1560
|
+
if (controller !== void 0) this.pendingAbort = controller;
|
|
1561
|
+
let timeout;
|
|
1562
|
+
if (controller !== void 0 && timeoutMs !== void 0) timeout = this.requestTimeout(() => controller.abort(), timeoutMs);
|
|
1563
|
+
try {
|
|
1564
|
+
const raw = await rpc.call(UPDATE_STATUS_CHANNEL, endpoint, payload, controller?.signal);
|
|
1565
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("invalid restart RPC response");
|
|
1566
|
+
const envelope = raw;
|
|
1567
|
+
if (envelope.ok !== true) throw new Error(typeof envelope.error?.message === "string" ? envelope.error.message : "restart RPC failed");
|
|
1568
|
+
return envelope.value;
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
if (surfaceError) this.publish({
|
|
1571
|
+
...this.snapshot,
|
|
1572
|
+
phase: "error",
|
|
1573
|
+
error: errorMessage(error)
|
|
1574
|
+
});
|
|
1575
|
+
return;
|
|
1576
|
+
} finally {
|
|
1577
|
+
if (timeout !== void 0) this.cancelRequestTimeout(timeout);
|
|
1578
|
+
if (this.pendingAbort === controller) this.pendingAbort = void 0;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
publish(next) {
|
|
1582
|
+
if (this.stopped) return;
|
|
1583
|
+
this.snapshot = next;
|
|
1584
|
+
for (const listener of this.listeners) listener();
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1067
1587
|
/**
|
|
1068
1588
|
* The one settings namespace, which on DSH 0.1.7 IS the Loader entry id in
|
|
1069
1589
|
* `cordis.patch.yml`: the Host half's volatile `Config` fields are projected as
|
|
@@ -1543,6 +2063,9 @@ window.__ModuleLoader__.load({
|
|
|
1543
2063
|
.dus-actions{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 12px}
|
|
1544
2064
|
.dus-action{background:var(--dsw-alias-button-floating-fill);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:inherit;cursor:pointer;font-size:12px;line-height:30px;min-height:32px;padding:0 10px;touch-action:manipulation}
|
|
1545
2065
|
.dus-action:disabled{cursor:wait;opacity:.65}
|
|
2066
|
+
.dus-restart{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary)}
|
|
2067
|
+
.dus-restart-message{color:var(--dsw-alias-label-secondary);flex-basis:100%;font-size:11px;line-height:1.45;margin:0}
|
|
2068
|
+
.dus-restart-work{color:var(--dsw-alias-label-secondary);flex-basis:100%;font-size:11px;line-height:1.5;margin:0;overflow-wrap:anywhere;padding-left:18px}
|
|
1546
2069
|
.dus-command-label{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;margin:0 0 6px}
|
|
1547
2070
|
.dus-command{background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;display:block;font-family:var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:11px;line-height:1.5;margin:0;overflow-wrap:anywhere;padding:9px;tab-size:2;user-select:text;white-space:pre-wrap}
|
|
1548
2071
|
.dus-command:focus{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
@@ -1586,12 +2109,16 @@ window.__ModuleLoader__.load({
|
|
|
1586
2109
|
}
|
|
1587
2110
|
}
|
|
1588
2111
|
function apply(ctx) {
|
|
1589
|
-
const
|
|
2112
|
+
const connection = connectionOf(ctx);
|
|
2113
|
+
const status = new StatusStore(connection);
|
|
1590
2114
|
const preferences = new PreferencesStore();
|
|
2115
|
+
const panel = new PanelStore();
|
|
2116
|
+
const restart = new RestartStore(connection);
|
|
1591
2117
|
const ui = {
|
|
1592
2118
|
status,
|
|
1593
2119
|
preferences,
|
|
1594
|
-
panel
|
|
2120
|
+
panel,
|
|
2121
|
+
restart
|
|
1595
2122
|
};
|
|
1596
2123
|
const disposers = [];
|
|
1597
2124
|
let stopped = false;
|
|
@@ -1644,6 +2171,7 @@ window.__ModuleLoader__.load({
|
|
|
1644
2171
|
dispose();
|
|
1645
2172
|
} catch {}
|
|
1646
2173
|
channel.dispose();
|
|
2174
|
+
restart.stop();
|
|
1647
2175
|
status.stop();
|
|
1648
2176
|
disposeStyles();
|
|
1649
2177
|
};
|
|
@@ -1668,7 +2196,10 @@ window.__ModuleLoader__.load({
|
|
|
1668
2196
|
});
|
|
1669
2197
|
} catch {}
|
|
1670
2198
|
if (typeof ctx.on === "function") try {
|
|
1671
|
-
own(ctx.on("connection/reset", () =>
|
|
2199
|
+
own(ctx.on("connection/reset", () => {
|
|
2200
|
+
channel.reload();
|
|
2201
|
+
restart.refresh();
|
|
2202
|
+
}));
|
|
1672
2203
|
} catch {}
|
|
1673
2204
|
ctx.slots.inject("sidebar.brand.name", () => {
|
|
1674
2205
|
let registration;
|