@1e0zj/dsh-plugin-mall 0.3.1 → 0.3.3
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/README.md +65 -8
- package/package.json +1 -1
- package/src/cli.js +48 -0
- package/src/client.js +120 -26
- package/src/guard.js +629 -7
- package/src/index.js +124 -4
- package/src/installer.js +218 -22
package/README.md
CHANGED
|
@@ -73,9 +73,11 @@ node <profile>/node_modules/@1e0zj/dsh-plugin-mall/src/cli.js guard launch --pro
|
|
|
73
73
|
|
|
74
74
|
**A plain `dsh web` already resolves pending installs.** The marketplace plugin runs
|
|
75
75
|
recovery when it loads: reaching that point proves dsh booted far enough to compose
|
|
76
|
-
the profile, so the pending marker is committed
|
|
77
|
-
fails validation
|
|
78
|
-
|
|
76
|
+
the profile, so the pending marker is committed — or rolled back, either because the
|
|
77
|
+
profile fails validation or because the install was left paused at the build-script
|
|
78
|
+
approval gate (an unapproved install is never committed, however healthy it looks).
|
|
79
|
+
Without this a single install would wedge the profile — every later install and
|
|
80
|
+
uninstall refuses while a marker is outstanding.
|
|
79
81
|
|
|
80
82
|
**`guard launch` is still strictly better**, because it also covers what a plain
|
|
81
83
|
start cannot: a plugin that boots fine and then crashes seconds later. It checks the
|
|
@@ -83,13 +85,42 @@ profile's pending-install marker before starting the command after `--`:
|
|
|
83
85
|
|
|
84
86
|
- **No pending install** — the command runs as-is, inheriting the terminal, and its exit code is preserved.
|
|
85
87
|
- **Clearly broken on disk** — the profile is rolled back to its pre-install snapshot *before* launch, then the command starts on the restored state.
|
|
86
|
-
- **Alive through the grace period** (default **10 seconds**; `--grace-ms <ms>` to change) — the pending snapshot is committed and the wrapper keeps waiting on the process.
|
|
88
|
+
- **Alive through the grace period** (default **10 seconds**; `--grace-ms <ms>` to change) — the pending snapshot is committed and the wrapper keeps waiting on the process. An install still paused at the approval gate is rolled back instead: surviving probation only proves the JS loads, not that you approved its build scripts.
|
|
87
89
|
- **Exits 0 inside the grace period** (one-shot command) — the pending snapshot is committed.
|
|
88
90
|
- **Crashes or exits nonzero inside the grace period** — the profile is rolled back and the *exact same command* is restarted once with the restored state (never in a loop); the restarted process's exit code is preserved. SIGINT/SIGTERM are forwarded to the child where the platform supports it; on Windows `.cmd`/`.bat` shims go through `%ComSpec%` with strict per-argument quoting.
|
|
89
91
|
|
|
90
92
|
Limitations: the grace window is the probation period — a failure that only surfaces **after** it (a plugin that crashes minutes in, or on a specific interaction) cannot be rolled back automatically, because committing deletes the active snapshot and `guard recover` then has nothing to restore. `guard validate` still diagnoses the on-disk state, but a post-commit failure needs manual repair — uninstall and reinstall the plugin, or restore a backup you kept separately. Both commands do only **static on-disk validation**; neither proves the plugin actually loads. A corrupt pending marker fails closed: the command is not launched and no unvalidated path is deleted. Preserve the snapshot and repair or restore a trustworthy marker, then run `guard recover`; quarantine the marker only after you have independently verified the profile, or decided to abandon automatic recovery.
|
|
91
93
|
|
|
92
94
|
|
|
95
|
+
## dsh won't start after an update? (affects 0.2.0 – 0.3.2, fixed in 0.3.3)
|
|
96
|
+
|
|
97
|
+
Symptom: `dsh web` exits with `cannot resolve profile bundle "<package>"`.
|
|
98
|
+
|
|
99
|
+
Cause: **updating an already-installed plugin** into a rollback (most often the
|
|
100
|
+
target carries build scripts and the flow paused at the approval card) could lose
|
|
101
|
+
the package — the rollback's reinstall of the old version was fooled by pnpm's
|
|
102
|
+
"Already up to date" short-circuit, so the plugin left node_modules while its
|
|
103
|
+
bundles declaration stayed. Fresh installs and removals are unaffected.
|
|
104
|
+
|
|
105
|
+
Recovery: add the package back exactly as the profile's `package.json` declares
|
|
106
|
+
it, then run `dsh web`.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# <profile> = %USERPROFILE%\.dsh\profiles\web or ~/.dsh/profiles/web
|
|
110
|
+
# npm package — package.json says "dsh-better-sidebar": "^0.13.1"
|
|
111
|
+
pnpm --dir <profile> add "dsh-better-sidebar@^0.13.1" --ignore-scripts
|
|
112
|
+
# GitHub source — package.json says "dsh-at-file": "github:omdsh-dev/dsh-at-file"
|
|
113
|
+
pnpm --dir <profile> add "github:omdsh-dev/dsh-at-file" --ignore-scripts
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> Do not reach for 0.3.2's `dsh-plugin-guard recover` here. Its per-package
|
|
117
|
+
> fallback refuses `^` (a cmd metacharacter) and skips `github:` entirely, and
|
|
118
|
+
> pnpm writes nearly every dependency with one or the other — so the fallback
|
|
119
|
+
> never fires and recovery fails closed. Fixed on main: the fallback now pins the
|
|
120
|
+
> version (or commit) the lockfile resolved. Fixed in 0.3.3 — on that version
|
|
121
|
+
> `dsh-plugin-guard recover` does repair this.
|
|
122
|
+
|
|
123
|
+
|
|
93
124
|
## Agent tools
|
|
94
125
|
|
|
95
126
|
| Tool | What it does |
|
|
@@ -191,22 +222,48 @@ node <profile>/node_modules/@1e0zj/dsh-plugin-mall/src/cli.js guard launch --pro
|
|
|
191
222
|
```
|
|
192
223
|
|
|
193
224
|
**普通的 `dsh web` 就会了结 pending 安装。** 本插件加载时即执行恢复——能加载
|
|
194
|
-
本身就证明 dsh 已经组装好 profile、启动到了这一步,于是提交 pending
|
|
195
|
-
|
|
196
|
-
|
|
225
|
+
本身就证明 dsh 已经组装好 profile、启动到了这一步,于是提交 pending 标记;两种
|
|
226
|
+
情况改为回滚:profile 校验不过,或者那次安装停在构建脚本批准闸而未获批准
|
|
227
|
+
(没批准的安装绝不提交,哪怕它看起来一切正常)。没有这一步的话,装完一个插件
|
|
228
|
+
就会把 profile 卡住:只要标记还在,之后所有安装和卸载都会被拒绝。
|
|
197
229
|
|
|
198
230
|
**`guard launch` 仍然更强**,因为它覆盖普通启动覆盖不了的情况:插件启动正常、
|
|
199
231
|
几秒后才崩。它在启动 `--` 之后的命令前检查该 profile 的 pending 安装标记:
|
|
200
232
|
|
|
201
233
|
- **无 pending 安装** —— 命令原样运行(继承终端),透传退出码;
|
|
202
234
|
- **静态校验明显过不了** —— 启动*之前*先把 profile 回滚到安装前快照,再在恢复后的状态上启动;
|
|
203
|
-
- **活过缓刑期**(默认 **10 秒**,`--grace-ms <ms>` 可调)—— 提交 pending
|
|
235
|
+
- **活过缓刑期**(默认 **10 秒**,`--grace-ms <ms>` 可调)—— 提交 pending 快照,包装器继续守候该进程;但仍停在批准闸的安装改为回滚:活过缓刑期只证明 JS 能加载,不证明你批准了它的构建脚本;
|
|
204
236
|
- **缓刑期内以 0 退出**(一次性命令)—— 同样提交 pending 快照;
|
|
205
237
|
- **缓刑期内崩溃或非零退出** —— 回滚 profile,并用恢复后的状态**原样重启同一命令一次**(绝不循环),透传重启进程的退出码。支持的平台会把 SIGINT/SIGTERM 转发给子进程;Windows 上 `.cmd`/`.bat` 经 `%ComSpec%` 启动,逐参数严格加引号。
|
|
206
238
|
|
|
207
239
|
限制:缓刑期就是观察期——**之后**才暴露的故障(跑了几分钟才崩、或某个特定操作才触发)无法自动回滚:提交会删掉当前快照,此时 `guard recover` 已无可恢复的东西。`guard validate` 仍能诊断落盘状态,但提交之后的故障只能手工修复——卸载并重装插件(或恢复你另行保留的备份)。两条命令都只做**静态落盘校验**,都不证明插件真的能加载。pending 标记损坏时关闭式失败:不启动命令、不删除任何未校验路径。要**保留快照**、修复或恢复一个可信的标记后再跑 `guard recover`;只有在你已经独立核实过 profile、或决定放弃自动恢复之后,才去隔离(删除/移走)标记。
|
|
208
240
|
|
|
209
241
|
|
|
242
|
+
## 升级后 dsh 起不来?(0.2.0 – 0.3.2 受影响,0.3.3 已修复)
|
|
243
|
+
|
|
244
|
+
症状:`dsh web` 报 `cannot resolve profile bundle "<包名>"` 直接退出。
|
|
245
|
+
|
|
246
|
+
原因:**更新已装插件**时若走到回滚(最常见:目标插件带构建脚本、停在批准卡),
|
|
247
|
+
回滚里「装回旧版本」的一步会被 pnpm 的 "Already up to date" 空转骗过——包从
|
|
248
|
+
node_modules 消失而 bundles 声明还在。新装、卸载不受影响。
|
|
249
|
+
|
|
250
|
+
恢复:照 profile `package.json` 里原本的写法把包装回去,然后 `dsh web`。
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
# <profile> = %USERPROFILE%\.dsh\profiles\web 或 ~/.dsh/profiles/web
|
|
254
|
+
# npm 包 —— package.json 里是 "dsh-better-sidebar": "^0.13.1"
|
|
255
|
+
pnpm --dir <profile> add "dsh-better-sidebar@^0.13.1" --ignore-scripts
|
|
256
|
+
# GitHub 源 —— package.json 里是 "dsh-at-file": "github:omdsh-dev/dsh-at-file"
|
|
257
|
+
pnpm --dir <profile> add "github:omdsh-dev/dsh-at-file" --ignore-scripts
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
> 别指望 0.3.2 的 `dsh-plugin-guard recover` 修这个:它的 per-package 兜底会
|
|
261
|
+
> 拒掉 `^`(cmd 转义符),`github:` 更是整个跳过,而 pnpm 存依赖几乎不是前者
|
|
262
|
+
> 就是后者——兜底一次也不会触发,恢复只会 fail-closed。main 上已修:兜底改钉
|
|
263
|
+
> lockfile 解析出的版本(或 commit)。0.3.3 已修复——那个版本的
|
|
264
|
+
> `dsh-plugin-guard recover` 确实能修这个故障。
|
|
265
|
+
|
|
266
|
+
|
|
210
267
|
## 工作原理
|
|
211
268
|
|
|
212
269
|
- 双面包(dual-face)插件:`dsh.bundle` 半边挂在 **host 平面**(profile bundle 层),
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -40,13 +40,18 @@ import { tmpdir } from "node:os";
|
|
|
40
40
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
41
41
|
import { fileURLToPath } from "node:url";
|
|
42
42
|
import {
|
|
43
|
+
clearPendingApprovalPause,
|
|
43
44
|
commitPendingSnapshot,
|
|
44
45
|
createProfileSnapshot,
|
|
46
|
+
describeRollbackRebuild,
|
|
45
47
|
listPendingSnapshots,
|
|
48
|
+
markPendingApprovalPause,
|
|
46
49
|
markPendingSnapshot,
|
|
50
|
+
pendingApprovalPaused,
|
|
47
51
|
pnpmGuardEnv,
|
|
48
52
|
preflightInstall,
|
|
49
53
|
readPendingSnapshot,
|
|
54
|
+
readValidatedPendingSnapshot,
|
|
50
55
|
recoverAll,
|
|
51
56
|
recoverProfile,
|
|
52
57
|
resolveDshHome,
|
|
@@ -380,6 +385,8 @@ function cmdRecover({ home, profileDir }) {
|
|
|
380
385
|
} else if (entry.action === "rolled-back") {
|
|
381
386
|
console.log(`ROLLED BACK ${scope}: ${(entry.issues ?? []).map((issueEntry) => issueEntry.title).join("; ") || "profile would not load"}`);
|
|
382
387
|
if (entry.removed?.length) console.log(` removed from node_modules: ${entry.removed.join(", ")}`);
|
|
388
|
+
const rebuild = describeRollbackRebuild(entry.rebuild);
|
|
389
|
+
if (rebuild !== undefined) console.log(` node_modules rebuild: ${rebuild}`);
|
|
383
390
|
} else if (entry.action === "none") {
|
|
384
391
|
console.log(`no pending ${scope}`);
|
|
385
392
|
} else {
|
|
@@ -693,9 +700,25 @@ async function runPlain(command, args) {
|
|
|
693
700
|
* Commit the pending snapshot once startup probation passes. A commit failure
|
|
694
701
|
* is a warning, not a launch failure — the process is already running and
|
|
695
702
|
* healthy, and the marker simply stays pending for the next launch.
|
|
703
|
+
*
|
|
704
|
+
* An approval-paused marker must never commit: the new version sits there with
|
|
705
|
+
* its build scripts never approved, so staying alive only proves the JS loads.
|
|
706
|
+
* Roll it back to the pre-install snapshot instead (a rollback failure keeps
|
|
707
|
+
* the marker for the next attempt, same as recoverProfile).
|
|
696
708
|
*/
|
|
697
709
|
function commitLaunchSnapshot(profileDir) {
|
|
698
710
|
try {
|
|
711
|
+
let pending;
|
|
712
|
+
try {
|
|
713
|
+
pending = readValidatedPendingSnapshot(profileDir);
|
|
714
|
+
} catch {
|
|
715
|
+
pending = undefined; // unreadable marker: leave it to guard recover
|
|
716
|
+
}
|
|
717
|
+
if (pending !== undefined && pendingApprovalPaused(pending) !== undefined) {
|
|
718
|
+
rollbackPendingSnapshot(profileDir);
|
|
719
|
+
console.log(`[guard] startup probation passed, but the install was abandoned at the approval gate — profile rolled back for ${profileDir}`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
699
722
|
commitPendingSnapshot(profileDir);
|
|
700
723
|
console.log(`[guard] startup probation passed — pending snapshot committed for ${profileDir}`);
|
|
701
724
|
} catch (error) {
|
|
@@ -1156,6 +1179,31 @@ async function selfTest() {
|
|
|
1156
1179
|
const validated = validateInstalledProfile(profileDir);
|
|
1157
1180
|
if (validated.ok !== true) throw new Error("healthy profile should validate clean (cli)");
|
|
1158
1181
|
|
|
1182
|
+
// commitLaunchSnapshot: a marker paused at the approval gate must roll back
|
|
1183
|
+
// instead of committing even after a healthy probation — the candidate sits
|
|
1184
|
+
// there with its build scripts never approved, and committing would delete
|
|
1185
|
+
// the only rollback snapshot. Layout keeps the candidate a NEW dependency
|
|
1186
|
+
// so the rollback prunes node_modules without spawning pnpm.
|
|
1187
|
+
{
|
|
1188
|
+
const pauseProfile = join(root, "pause-home", "profiles", "web");
|
|
1189
|
+
mkdirSync(join(pauseProfile, "node_modules"), { recursive: true });
|
|
1190
|
+
writeFileSync(join(pauseProfile, "package.json"), JSON.stringify({ dependencies: {} }));
|
|
1191
|
+
writeFileSync(join(pauseProfile, "cordis.patch.yml"), "[]\n");
|
|
1192
|
+
const snap = createProfileSnapshot(pauseProfile, { fixture: true });
|
|
1193
|
+
markPendingSnapshot(snap, { spec: "good@2.0.0", preflight: { candidate: { name: "good", version: "2.0.0", kind: "bundle" } } });
|
|
1194
|
+
// 暂停现场:候选已装、声明已写,静态校验过得去——正是不许提交的原因。
|
|
1195
|
+
writeFileSync(join(pauseProfile, "package.json"), JSON.stringify({ dependencies: { good: "^2.0.0" } }));
|
|
1196
|
+
mkdirSync(join(pauseProfile, "node_modules", "good"), { recursive: true });
|
|
1197
|
+
writeFileSync(join(pauseProfile, "node_modules", "good", "package.json"), JSON.stringify({ name: "good", version: "2.0.0" }));
|
|
1198
|
+
markPendingApprovalPause(pauseProfile);
|
|
1199
|
+
commitLaunchSnapshot(pauseProfile);
|
|
1200
|
+
if (readPendingSnapshot(pauseProfile) !== undefined) throw new Error("commitLaunchSnapshot must consume (roll back) an approval-paused marker");
|
|
1201
|
+
if (existsSync(snap.dir)) throw new Error("the paused rollback must delete the snapshot dir");
|
|
1202
|
+
if (JSON.parse(readFileSync(join(pauseProfile, "package.json"), "utf8")).dependencies?.good !== undefined) {
|
|
1203
|
+
throw new Error("the paused rollback must restore the pre-install manifest");
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1159
1207
|
// guarded remove: exact official argv + shell:false runner seam, snapshot
|
|
1160
1208
|
// before mutation, immediate commit after a statically safe removal.
|
|
1161
1209
|
{
|
package/src/client.js
CHANGED
|
@@ -133,10 +133,29 @@ window.__ModuleLoader__.load({
|
|
|
133
133
|
// onPreflightSettled:预检 job(kind=dsh-plugin-preflight)落定时回调,
|
|
134
134
|
// 携带 (spec, report)。safe 由调用方直接续装;有风险由调用方出内联卡片。
|
|
135
135
|
function useJobPolling(call, onSettled, onApprovalToken, onPreflightSettled) {
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
// 重挂载垫底:安装完成的收尾会写 cordis.patch.yml,dsh 随之重放装配树、
|
|
137
|
+
// 整个市场 UI 重挂载,React state 归零——面板先空一拍再被异步的
|
|
138
|
+
// call("jobs") 恢复,用户看到「任务清掉又回来」的割裂(实测反馈)。
|
|
139
|
+
// sessionStorage 镜像让重挂载的第一帧直接渲染上一帧的面板,随后的
|
|
140
|
+
// RPC 恢复用后端权威数据覆盖。tab 级存储,随 tab 关闭而清,
|
|
141
|
+
// 与内存里的任务数据同生命周期。
|
|
142
|
+
var JOBS_MIRROR_KEY = "@1e0zj/dsh-plugin-mall:jobs";
|
|
143
|
+
var readJobsMirror = function () {
|
|
144
|
+
try {
|
|
145
|
+
var parsed = JSON.parse(window.sessionStorage.getItem(JOBS_MIRROR_KEY) || "null");
|
|
146
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
147
|
+
} catch (e) { return {}; }
|
|
148
|
+
};
|
|
149
|
+
var jobsRef = useRef(null);
|
|
150
|
+
if (jobsRef.current === null) jobsRef.current = readJobsMirror();
|
|
151
|
+
var _jobs = useState(Object.assign({}, jobsRef.current));
|
|
138
152
|
var jobs = _jobs[0];
|
|
139
153
|
var setJobs = _jobs[1];
|
|
154
|
+
var commit = useCallback(function (next) {
|
|
155
|
+
jobsRef.current = next;
|
|
156
|
+
setJobs(Object.assign({}, next));
|
|
157
|
+
try { window.sessionStorage.setItem(JOBS_MIRROR_KEY, JSON.stringify(next)); } catch (e) { /* 存储被禁/写满不致命 */ }
|
|
158
|
+
}, []);
|
|
140
159
|
useEffect(function () {
|
|
141
160
|
var timer = setInterval(function () {
|
|
142
161
|
var current = jobsRef.current;
|
|
@@ -165,15 +184,16 @@ window.__ModuleLoader__.load({
|
|
|
165
184
|
}
|
|
166
185
|
}
|
|
167
186
|
var output = (old.output || "") + (value.output || "");
|
|
168
|
-
|
|
187
|
+
commit(Object.assign({}, jobsRef.current, { [id]: Object.assign({}, old, {
|
|
169
188
|
status: snapshot.status,
|
|
170
189
|
detail: snapshot.detail,
|
|
171
190
|
needsApproval: snapshot.needsApproval,
|
|
191
|
+
staleOnRestart: snapshot.staleOnRestart,
|
|
172
192
|
approvalToken: snapshot.approvalToken,
|
|
173
193
|
kind: snapshot.kind,
|
|
174
194
|
output: output,
|
|
175
|
-
|
|
176
|
-
|
|
195
|
+
finishedAt: snapshot.finishedAt,
|
|
196
|
+
}) }));
|
|
177
197
|
}).catch(function () { /* keep polling */ });
|
|
178
198
|
});
|
|
179
199
|
}, 1200);
|
|
@@ -198,20 +218,73 @@ window.__ModuleLoader__.load({
|
|
|
198
218
|
}
|
|
199
219
|
}
|
|
200
220
|
next[id] = { status: "running", spec: spec, output: carried };
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}, []);
|
|
221
|
+
commit(next);
|
|
222
|
+
}, [commit]);
|
|
204
223
|
var clear = useCallback(function () {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}, []);
|
|
224
|
+
commit({});
|
|
225
|
+
}, [commit]);
|
|
208
226
|
var drop = useCallback(function (id) {
|
|
209
227
|
var next = Object.assign({}, jobsRef.current);
|
|
210
228
|
delete next[id];
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
229
|
+
commit(next);
|
|
230
|
+
}, [commit]);
|
|
231
|
+
// 恢复后端任务记录(tracker.list 的形状):安装事务改写
|
|
232
|
+
// cordis.patch.yml 会让 dsh 重放装配树、整个市场 UI 重挂载,React
|
|
233
|
+
// state 全丢——任务面板、完成提醒、重启按钮一起消失(真实事故:
|
|
234
|
+
// 更新其实成功了,用户靠手动重启+查版本才确认)。记录在后端活着,
|
|
235
|
+
// 挂载时拉回来。两个细节:恢复出的**已落定**预检任务标
|
|
236
|
+
// preflightHandled,否则轮询的第一拍会重放 onPreflightSettled——
|
|
237
|
+
// 那等于页面一刷新就自动续装一次;running 的不标,交回轮询线。
|
|
238
|
+
var restore = useCallback(function (entries) {
|
|
239
|
+
var next = Object.assign({}, jobsRef.current);
|
|
240
|
+
var serverIds = {};
|
|
241
|
+
for (var index = 0; index < (entries || []).length; index++) {
|
|
242
|
+
var entry = entries[index] || {};
|
|
243
|
+
var snap = entry.snapshot || {};
|
|
244
|
+
if (!entry.id) continue;
|
|
245
|
+
serverIds[entry.id] = true;
|
|
246
|
+
// 服务器记录是权威(覆盖垫底镜像);本地独有的 id 保留——后端
|
|
247
|
+
// 修剪掉的旧条目不至于从面板上闪没。
|
|
248
|
+
next[entry.id] = {
|
|
249
|
+
status: snap.status,
|
|
250
|
+
spec: snap.spec,
|
|
251
|
+
detail: snap.detail,
|
|
252
|
+
needsApproval: snap.needsApproval,
|
|
253
|
+
staleOnRestart: snap.staleOnRestart,
|
|
254
|
+
approvalToken: snap.approvalToken,
|
|
255
|
+
kind: snap.kind,
|
|
256
|
+
output: entry.output || "",
|
|
257
|
+
finishedAt: snap.finishedAt,
|
|
258
|
+
preflightHandled: snap.kind === "dsh-plugin-preflight" && snap.status !== "running",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// 不在本次服务器列表里的条目属于上一次宿主会话(进程重启后
|
|
262
|
+
// tracker 清空)。已兑现的直接翻篇撤掉:completed 的重启已经
|
|
263
|
+
// 发生;needsApproval 暂停的批准卡片已随进程失效(事务由启动
|
|
264
|
+
// 恢复处置),留着只会让人点一个必然失败的按钮;staleOnRestart
|
|
265
|
+
// 的失败是「被另一个未了结事务挡住」,而那个事务必然已被启动恢复
|
|
266
|
+
// 处置——它的报错是现在时写的(「还没做完」「现在无法安装」),
|
|
267
|
+
// 留到重启之后会被当成当前状态读,而它描述的情形已经不存在。
|
|
268
|
+
// running 的标中断,别让轮询对着不存在的 id 空转。其余 failed
|
|
269
|
+
// 保留——网络、预检阻断这类原因重启后可能仍然成立,日志有排障
|
|
270
|
+
// 价值。这个判据不依赖 finishedAt(旧镜像里没有该字段)。
|
|
271
|
+
for (var key in next) {
|
|
272
|
+
if (serverIds[key]) continue;
|
|
273
|
+
var stale = next[key];
|
|
274
|
+
if (stale.status === "running") {
|
|
275
|
+
next[key] = Object.assign({}, stale, {
|
|
276
|
+
status: "killed",
|
|
277
|
+
detail: "宿主进程已重启,该任务的记录随之丢失",
|
|
278
|
+
});
|
|
279
|
+
} else if (stale.status === "completed"
|
|
280
|
+
|| stale.staleOnRestart === true
|
|
281
|
+
|| (Array.isArray(stale.needsApproval) && stale.needsApproval.length > 0)) {
|
|
282
|
+
delete next[key];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
commit(next);
|
|
286
|
+
}, [commit]);
|
|
287
|
+
return { jobs: jobs, track: track, clear: clear, drop: drop, restore: restore };
|
|
215
288
|
}
|
|
216
289
|
|
|
217
290
|
// ── plugin verification badge ───────────────────────────────────────────
|
|
@@ -425,10 +498,12 @@ window.__ModuleLoader__.load({
|
|
|
425
498
|
needsApproval: job.needsApproval,
|
|
426
499
|
busy: props.approving === job.spec,
|
|
427
500
|
onApprove: function (names) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
501
|
+
// 不先 drop:旧条目由重试任务的 track(carryFromId) 原子接管
|
|
502
|
+
// (撤条目 + 日志接续一拍完成)。先删的话,call("install")
|
|
503
|
+
// 要走数秒(重试还会重跑一次隔离预检),面板会空白一段,
|
|
504
|
+
// 「批准后任务消失、开始安装才冒出来」的割裂就是这么来的。
|
|
505
|
+
// 等待期间按钮由 approving 态显示「继续中…」。
|
|
506
|
+
props.onApprove(job.spec, names, job.approvalToken, id);
|
|
432
507
|
},
|
|
433
508
|
onDismiss: function () { props.onDismiss(id); },
|
|
434
509
|
})
|
|
@@ -437,11 +512,16 @@ window.__ModuleLoader__.load({
|
|
|
437
512
|
// 也不再重复一个绿色「完成」:状态行已经写了「· 完成」。
|
|
438
513
|
done && job.status === "completed" && job.kind !== "dsh-plugin-preflight"
|
|
439
514
|
? h("div", { className: "mkt_jobDone" },
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
515
|
+
// 完成时间早于本次宿主启动 = 重启已经发生过了(防御分支:
|
|
516
|
+
// 已兑现的条目通常在恢复时就被撤掉了):换成说明文字,
|
|
517
|
+
// 不再催一次没必要的重启。
|
|
518
|
+
props.hostStartedAt && job.finishedAt && job.finishedAt < props.hostStartedAt
|
|
519
|
+
? h("span", { className: "mkt_meta" }, "重启已生效")
|
|
520
|
+
: h("button", {
|
|
521
|
+
className: "mkt_btn mkt_btnPrimary mkt_btnSm",
|
|
522
|
+
disabled: props.restarting === true,
|
|
523
|
+
onClick: props.onRestart,
|
|
524
|
+
}, props.restarting ? "重启中…" : "重启 dsh 生效"))
|
|
445
525
|
: job.status === "failed" && !(job.needsApproval && job.needsApproval.length > 0)
|
|
446
526
|
? h("div", { className: "mkt_error" }, "失败,见下方输出")
|
|
447
527
|
: null,
|
|
@@ -567,6 +647,11 @@ window.__ModuleLoader__.load({
|
|
|
567
647
|
var _restarting = useState(false);
|
|
568
648
|
var restarting = _restarting[0];
|
|
569
649
|
var setRestarting = _restarting[1];
|
|
650
|
+
// 本次宿主进程的启动时间(jobs 端点带回):完成时间早于它的任务,
|
|
651
|
+
// 其「重启 dsh 生效」按钮已经兑现,改显示「重启已生效」。
|
|
652
|
+
var _hostStartedAt = useState(0);
|
|
653
|
+
var hostStartedAt = _hostStartedAt[0];
|
|
654
|
+
var setHostStartedAt = _hostStartedAt[1];
|
|
570
655
|
var _preflight = useState(null);
|
|
571
656
|
var preflight = _preflight[0];
|
|
572
657
|
var setPreflight = _preflight[1];
|
|
@@ -724,6 +809,14 @@ window.__ModuleLoader__.load({
|
|
|
724
809
|
|
|
725
810
|
useEffect(function () {
|
|
726
811
|
doSearch();
|
|
812
|
+
// 挂载即恢复任务面板(见 useJobPolling.restore 的注释):重挂载丢掉
|
|
813
|
+
// 的完成提醒、重启按钮、暂停中的批准卡片都从后端拉回来。
|
|
814
|
+
// hostStartedAt:本次宿主进程的启动时间——早于它的完成任务说明
|
|
815
|
+
// 重启已经发生,按钮要换成「重启已生效」而不是再催一次。
|
|
816
|
+
call("jobs", {}).then(function (value) {
|
|
817
|
+
if (value.hostStartedAt) setHostStartedAt(value.hostStartedAt);
|
|
818
|
+
polling.restore(value.jobs);
|
|
819
|
+
}).catch(function () { /* 恢复失败不阻塞面板 */ });
|
|
727
820
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
728
821
|
}, []);
|
|
729
822
|
|
|
@@ -748,14 +841,14 @@ window.__ModuleLoader__.load({
|
|
|
748
841
|
});
|
|
749
842
|
}, [call, track]);
|
|
750
843
|
|
|
751
|
-
var doApprove = useCallback(function (spec, names, token) {
|
|
844
|
+
var doApprove = useCallback(function (spec, names, token, carryFromId) {
|
|
752
845
|
var extra = { allowBuildScripts: names };
|
|
753
846
|
var apprToken = token || approvalTokensRef.current[spec];
|
|
754
847
|
if (apprToken) {
|
|
755
848
|
extra.approvalToken = apprToken;
|
|
756
849
|
delete approvalTokensRef.current[spec];
|
|
757
850
|
}
|
|
758
|
-
doRawInstall(spec, extra);
|
|
851
|
+
doRawInstall(spec, extra, carryFromId);
|
|
759
852
|
}, [doRawInstall]);
|
|
760
853
|
|
|
761
854
|
// 预检落定后的去向:safe 直接续装,其余出内联风险卡片。
|
|
@@ -912,6 +1005,7 @@ window.__ModuleLoader__.load({
|
|
|
912
1005
|
onDrop: dropJob,
|
|
913
1006
|
onRestart: doRestart,
|
|
914
1007
|
restarting: restarting,
|
|
1008
|
+
hostStartedAt: hostStartedAt,
|
|
915
1009
|
approving: Object.keys(installing).filter(function (s) { return installing[s]; })[0],
|
|
916
1010
|
}),
|
|
917
1011
|
preflight ? h(PreflightCard, {
|