dsh-session-manager 0.7.1 → 0.7.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/CHANGELOG.md +19 -0
- package/README.md +22 -20
- package/README.zh.md +20 -18
- package/lib/client.js +429 -95
- package/lib/compat/dsh-adapter.js +177 -7
- package/lib/index.js +946 -39
- package/lib/session-files.js +73 -6
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -22,13 +22,14 @@
|
|
|
22
22
|
* The compatibility adapter normalizes service access and list snapshots.
|
|
23
23
|
* Optional live-writer/coordinator internals are guarded before mutation.
|
|
24
24
|
*/
|
|
25
|
+
import { existsSync, readFileSync, writeFileSync, rmSync, renameSync } from "node:fs";
|
|
25
26
|
import { lstat, mkdir, readdir, realpath, rename, rm } from "node:fs/promises";
|
|
26
27
|
import { basename, dirname, join } from "node:path";
|
|
27
28
|
import { randomBytes } from "node:crypto";
|
|
28
29
|
import { createAnnotationStore } from "./annotation-store.js";
|
|
29
30
|
import { createWorkspaceArchiveStore, assertWorkspaceId } from "./workspace-archive-store.js";
|
|
30
|
-
import { createDshAdapter, normalizeSessionHeader, compareSemver } from "./compat/dsh-adapter.js";
|
|
31
|
-
import { ARTIFACT_NAMES, assertSessionId, assertSessionDirectory, encodeSessionSegment, parseSessionGenerationFromFilename, readSessionHeader, readSessionFile, writeTempFile, replaceSessionFile } from "./session-files.js";
|
|
31
|
+
import { createDshAdapter, normalizeSessionHeader, compareSemver, detectPluginSource, isHmrActive, resolveProfileDir } from "./compat/dsh-adapter.js";
|
|
32
|
+
import { ARTIFACT_NAMES, assertSessionId, assertSessionDirectory, candidateSessionDirs, encodeFsSessionSegment, decodeFsSessionSegment, encodeProjectSegment, encodeSessionSegment, parseSessionGenerationFromFilename, readSessionHeader, readSessionFile, writeTempFile, replaceSessionFile } from "./session-files.js";
|
|
32
33
|
|
|
33
34
|
export const name = "dsh-session-manager";
|
|
34
35
|
|
|
@@ -125,10 +126,16 @@ const scanSessionHeaders = async (persistence, dshHome) => {
|
|
|
125
126
|
}
|
|
126
127
|
const result = () => ({ headers, complete: listed && errors.length === 0, errors });
|
|
127
128
|
// Skip known files before fopen; retain the orphan/raw-id compatibility path.
|
|
129
|
+
// Only add canonical session directories that do not start and end with "--"
|
|
130
|
+
// to avoid colliding with legacy projectKey candidates (e.g. "--s1--").
|
|
128
131
|
const knownDirs = new Set();
|
|
129
132
|
for (const id of seen) {
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
try {
|
|
134
|
+
const canonical = encodeFsSessionSegment(id);
|
|
135
|
+
if (!canonical.startsWith("--") || !canonical.endsWith("--")) {
|
|
136
|
+
knownDirs.add(canonical);
|
|
137
|
+
}
|
|
138
|
+
} catch (_) {}
|
|
132
139
|
}
|
|
133
140
|
if (typeof dshHome !== "function") return result();
|
|
134
141
|
const root = dshHome("sessions");
|
|
@@ -150,7 +157,7 @@ const scanSessionHeaders = async (persistence, dshHome) => {
|
|
|
150
157
|
for (const filename of ARTIFACT_NAMES) {
|
|
151
158
|
try {
|
|
152
159
|
const header = await readSessionHeader(join(root, proj.name, entry.name, filename));
|
|
153
|
-
if (!
|
|
160
|
+
if (!candidateSessionDirs(header.id).includes(entry.name)) {
|
|
154
161
|
throw new Error("会话目录与 header id 不一致");
|
|
155
162
|
}
|
|
156
163
|
if (!seen.has(header.id)) { seen.add(header.id); headers.push(header); }
|
|
@@ -168,9 +175,199 @@ const scanSessionHeaders = async (persistence, dshHome) => {
|
|
|
168
175
|
|
|
169
176
|
const listSessionHeaders = async (persistence, dshHome) => (await scanSessionHeaders(persistence, dshHome)).headers;
|
|
170
177
|
|
|
178
|
+
const UPDATE_STATUS_FILE = ".dsh-session-manager-update.json";
|
|
179
|
+
const STALE_EXTERNAL_TASK_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes fallback for dead external processes
|
|
180
|
+
const HEARTBEAT_STALE_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes without heartbeat for external processes
|
|
181
|
+
|
|
182
|
+
const TRUSTED_REGISTRIES = new Set([
|
|
183
|
+
"https://registry.npmjs.org",
|
|
184
|
+
"https://registry.npmmirror.com",
|
|
185
|
+
"http://127.0.0.1:4873",
|
|
186
|
+
"http://localhost:4873"
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
function normalizeRegistryUrl(url) {
|
|
190
|
+
if (!url || typeof url !== "string") return "";
|
|
191
|
+
const trimmed = url.trim();
|
|
192
|
+
if (!trimmed) return "";
|
|
193
|
+
return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolveTrustedRegistry(requested, envRegistry) {
|
|
197
|
+
const normEnv = normalizeRegistryUrl(envRegistry);
|
|
198
|
+
const normReq = normalizeRegistryUrl(requested);
|
|
199
|
+
|
|
200
|
+
if (normReq) {
|
|
201
|
+
if (TRUSTED_REGISTRIES.has(normReq) || (normEnv && normReq === normEnv)) {
|
|
202
|
+
return normReq;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (normEnv && (TRUSTED_REGISTRIES.has(normEnv) || normEnv.length > 0)) {
|
|
208
|
+
return normEnv;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return "https://registry.npmjs.org";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const ACTIVE_UPDATE_TASK_KEY = Symbol.for("dsh-session-manager.activeUpdateTask");
|
|
215
|
+
|
|
216
|
+
function getActiveTask() {
|
|
217
|
+
return globalThis[ACTIVE_UPDATE_TASK_KEY] || null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function setActiveTask(task) {
|
|
221
|
+
if (task) {
|
|
222
|
+
globalThis[ACTIVE_UPDATE_TASK_KEY] = task;
|
|
223
|
+
} else {
|
|
224
|
+
delete globalThis[ACTIVE_UPDATE_TASK_KEY];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function releaseActiveTask(taskId) {
|
|
229
|
+
const cur = getActiveTask();
|
|
230
|
+
if (cur && (!taskId || cur.taskId === taskId)) {
|
|
231
|
+
setActiveTask(null);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isPidAlive(pid) {
|
|
236
|
+
if (typeof pid !== "number" || pid <= 0) return false;
|
|
237
|
+
try {
|
|
238
|
+
process.kill(pid, 0);
|
|
239
|
+
return true;
|
|
240
|
+
} catch (err) {
|
|
241
|
+
return err.code === "EPERM";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getUpdateStatusFilePath(profileDir) {
|
|
246
|
+
return profileDir ? join(profileDir, UPDATE_STATUS_FILE) : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function writeUpdateStatus(profileDir, statusObj) {
|
|
250
|
+
const filePath = getUpdateStatusFilePath(profileDir);
|
|
251
|
+
if (!filePath || !statusObj || typeof statusObj !== "object") return;
|
|
252
|
+
statusObj.updatedAt = Date.now();
|
|
253
|
+
try {
|
|
254
|
+
if (existsSync(filePath)) {
|
|
255
|
+
const existing = JSON.parse(readFileSync(filePath, "utf8"));
|
|
256
|
+
if (existing && existing.taskId && statusObj.taskId && existing.taskId !== statusObj.taskId) {
|
|
257
|
+
if (isDiskTaskActive(existing) && (existing.startedAt || 0) > (statusObj.startedAt || 0)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} catch (_) {}
|
|
263
|
+
const tmpPath = filePath + ".tmp." + Date.now();
|
|
264
|
+
try {
|
|
265
|
+
writeFileSync(tmpPath, JSON.stringify(statusObj, null, 2), "utf8");
|
|
266
|
+
try {
|
|
267
|
+
renameSync(tmpPath, filePath);
|
|
268
|
+
} catch {
|
|
269
|
+
try { rmSync(filePath, { force: true }); } catch (_) {}
|
|
270
|
+
renameSync(tmpPath, filePath);
|
|
271
|
+
}
|
|
272
|
+
} catch (_) {
|
|
273
|
+
try { if (existsSync(tmpPath)) rmSync(tmpPath, { force: true }); } catch (_) {}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function readUpdateStatus(profileDir) {
|
|
278
|
+
const filePath = getUpdateStatusFilePath(profileDir);
|
|
279
|
+
if (!filePath) return null;
|
|
280
|
+
try {
|
|
281
|
+
if (existsSync(filePath)) {
|
|
282
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
283
|
+
}
|
|
284
|
+
} catch (_) { /* best-effort */ }
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const ACTIVE_TASK_STATUSES = new Set([
|
|
289
|
+
"preparing",
|
|
290
|
+
"removing",
|
|
291
|
+
"removed",
|
|
292
|
+
"installing",
|
|
293
|
+
"verifying",
|
|
294
|
+
"rolling-back"
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
function isDiskTaskActive(statusObj) {
|
|
298
|
+
if (!statusObj || typeof statusObj !== "object") return false;
|
|
299
|
+
if (!ACTIVE_TASK_STATUSES.has(statusObj.status)) return false;
|
|
300
|
+
|
|
301
|
+
// If status was recorded by the current process, consult in-memory task
|
|
302
|
+
if (statusObj.pid === process.pid) {
|
|
303
|
+
const inMem = getActiveTask();
|
|
304
|
+
if (!inMem || inMem.taskId !== statusObj.taskId) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// If status was recorded by another process, check if PID is alive
|
|
311
|
+
if (typeof statusObj.pid === "number") {
|
|
312
|
+
if (!isPidAlive(statusObj.pid)) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const lastActive = typeof statusObj.updatedAt === "number"
|
|
318
|
+
? statusObj.updatedAt
|
|
319
|
+
: (typeof statusObj.startedAt === "number" ? statusObj.startedAt : 0);
|
|
320
|
+
if (Date.now() - lastActive > HEARTBEAT_STALE_TIMEOUT_MS) return false;
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isTaskActive(statusObj) {
|
|
325
|
+
return isDiskTaskActive(statusObj);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function verifyInstalledPackage(profileDir, expectedVersion) {
|
|
329
|
+
if (!profileDir) {
|
|
330
|
+
return { ok: false, error: "无法定位插件运行 profile 目录,无法进行安装校验" };
|
|
331
|
+
}
|
|
332
|
+
const pkgDir = join(profileDir, "node_modules", "dsh-session-manager");
|
|
333
|
+
const pkgFile = join(pkgDir, "package.json");
|
|
334
|
+
const entryIndex = join(pkgDir, "lib", "index.js");
|
|
335
|
+
const entryClient = join(pkgDir, "lib", "client.js");
|
|
336
|
+
|
|
337
|
+
if (!existsSync(pkgFile)) {
|
|
338
|
+
return { ok: false, error: "未找到安装目录或 package.json: " + pkgFile };
|
|
339
|
+
}
|
|
340
|
+
let pkg;
|
|
341
|
+
try {
|
|
342
|
+
pkg = JSON.parse(readFileSync(pkgFile, "utf8"));
|
|
343
|
+
} catch (err) {
|
|
344
|
+
return { ok: false, error: "package.json 解析失败: " + safeErrorMessage(err) };
|
|
345
|
+
}
|
|
346
|
+
if (!pkg || typeof pkg !== "object") {
|
|
347
|
+
return { ok: false, error: "package.json 内容无效" };
|
|
348
|
+
}
|
|
349
|
+
if (pkg.name !== "dsh-session-manager") {
|
|
350
|
+
return { ok: false, error: "安装包名称不符: 预期 dsh-session-manager,实际 " + pkg.name };
|
|
351
|
+
}
|
|
352
|
+
if (expectedVersion && expectedVersion !== "latest" && pkg.version !== expectedVersion) {
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
version: pkg.version,
|
|
356
|
+
error: "安装版本不匹配: 预期 " + expectedVersion + ",实际安装 " + pkg.version
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (!existsSync(entryIndex)) {
|
|
360
|
+
return { ok: false, version: pkg.version, error: "缺少服务端入口文件: lib/index.js" };
|
|
361
|
+
}
|
|
362
|
+
if (!existsSync(entryClient)) {
|
|
363
|
+
return { ok: false, version: pkg.version, error: "缺少客户端入口文件: lib/client.js" };
|
|
364
|
+
}
|
|
365
|
+
return { ok: true, version: pkg.version };
|
|
366
|
+
}
|
|
367
|
+
|
|
171
368
|
export function apply(ctx) {
|
|
172
369
|
try {
|
|
173
|
-
const buildId = "dsh-session-manager v0.7.
|
|
370
|
+
const buildId = "dsh-session-manager v0.7.3 (workspace-archive)";
|
|
174
371
|
if (typeof ctx?.logger?.info === "function") ctx.logger.info(buildId);
|
|
175
372
|
else if (typeof console !== "undefined" && console?.info) console.info(buildId);
|
|
176
373
|
} catch (_) { /* ignore logging failures */ }
|
|
@@ -1252,14 +1449,19 @@ ${rest}`, "utf8");
|
|
|
1252
1449
|
catch (error) { if (error.code === "ENOENT") return undefined; throw error; }
|
|
1253
1450
|
for (const proj of projects) {
|
|
1254
1451
|
if (!proj.isDirectory() || proj.isSymbolicLink()) continue;
|
|
1255
|
-
for (const candidate of
|
|
1452
|
+
for (const candidate of candidateSessionDirs(sessionId)) {
|
|
1256
1453
|
const dir = join(root, proj.name, candidate);
|
|
1257
1454
|
try { await assertSessionDirectory(root, dir, sessionId); }
|
|
1258
1455
|
catch (error) { if (error.code === "ENOENT") continue; throw error; }
|
|
1259
1456
|
for (const filename of ARTIFACT_NAMES) {
|
|
1260
1457
|
try {
|
|
1261
1458
|
const artifact = await read(join(dir, filename));
|
|
1262
|
-
if (artifact.meta.id !== sessionId)
|
|
1459
|
+
if (artifact.meta.id !== sessionId) {
|
|
1460
|
+
if (candidate === candidateSessionDirs(sessionId)[0]) {
|
|
1461
|
+
throw new Error("会话工件 header id 与请求不一致");
|
|
1462
|
+
}
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1263
1465
|
return artifact;
|
|
1264
1466
|
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
1265
1467
|
}
|
|
@@ -2013,14 +2215,91 @@ ${rest}`, "utf8");
|
|
|
2013
2215
|
return send(res, 200, { ok: true, result });
|
|
2014
2216
|
}
|
|
2015
2217
|
if (req.method === "GET" && path === "/update/check") {
|
|
2016
|
-
const currentVersion = adapter.pluginVersion() || "0.7.
|
|
2017
|
-
|
|
2218
|
+
const currentVersion = adapter.pluginVersion() || "0.7.3";
|
|
2219
|
+
const runningVersion = currentVersion;
|
|
2220
|
+
let installedVersion = runningVersion;
|
|
2221
|
+
const profileDir = resolveProfileDir(adapter, ctx);
|
|
2222
|
+
if (profileDir) {
|
|
2223
|
+
const diskCheck = verifyInstalledPackage(profileDir);
|
|
2224
|
+
if (diskCheck.ok && diskCheck.version) {
|
|
2225
|
+
installedVersion = diskCheck.version;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
const pm = adapter.pluginManager;
|
|
2229
|
+
let bundles = [];
|
|
2230
|
+
if (typeof pm?.listBundles === "function") {
|
|
2231
|
+
try { bundles = await pm.listBundles(); } catch (_) {}
|
|
2232
|
+
}
|
|
2233
|
+
const source = detectPluginSource(adapter, ctx, bundles);
|
|
2234
|
+
if (source === "linked") {
|
|
2235
|
+
return send(res, 200, {
|
|
2236
|
+
ok: true,
|
|
2237
|
+
result: {
|
|
2238
|
+
runningVersion,
|
|
2239
|
+
currentVersion: runningVersion,
|
|
2240
|
+
installedVersion,
|
|
2241
|
+
latestVersion: runningVersion,
|
|
2242
|
+
hasUpdate: false,
|
|
2243
|
+
source: "linked",
|
|
2244
|
+
message: "开发模式(本地链接),请手动修改或拉取代码"
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
if (source === "github") {
|
|
2249
|
+
return send(res, 200, {
|
|
2250
|
+
ok: true,
|
|
2251
|
+
result: {
|
|
2252
|
+
runningVersion,
|
|
2253
|
+
currentVersion: runningVersion,
|
|
2254
|
+
installedVersion,
|
|
2255
|
+
latestVersion: runningVersion,
|
|
2256
|
+
hasUpdate: false,
|
|
2257
|
+
source: "github",
|
|
2258
|
+
message: "通过 Git/GitHub 安装,请通过 DSH 插件市场或 git 更新"
|
|
2259
|
+
}
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
if (source === "installation") {
|
|
2263
|
+
return send(res, 200, {
|
|
2264
|
+
ok: true,
|
|
2265
|
+
result: {
|
|
2266
|
+
runningVersion,
|
|
2267
|
+
currentVersion: runningVersion,
|
|
2268
|
+
installedVersion,
|
|
2269
|
+
latestVersion: runningVersion,
|
|
2270
|
+
hasUpdate: false,
|
|
2271
|
+
source: "installation",
|
|
2272
|
+
message: "系统内置插件,无法独立更新"
|
|
2273
|
+
}
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
if (source === "unknown") {
|
|
2277
|
+
return send(res, 200, {
|
|
2278
|
+
ok: true,
|
|
2279
|
+
result: {
|
|
2280
|
+
runningVersion,
|
|
2281
|
+
currentVersion: runningVersion,
|
|
2282
|
+
installedVersion,
|
|
2283
|
+
latestVersion: runningVersion,
|
|
2284
|
+
hasUpdate: false,
|
|
2285
|
+
source: "unknown",
|
|
2286
|
+
message: "无法确认插件安装来源,为保证安全已禁止自动更新"
|
|
2287
|
+
}
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
let latestVersion = installedVersion;
|
|
2018
2291
|
let hasUpdate = false;
|
|
2019
2292
|
try {
|
|
2293
|
+
const envRegistry = process.env.NPM_CONFIG_REGISTRY?.trim();
|
|
2020
2294
|
const requestedRegistry = url.searchParams.get("registry")?.trim();
|
|
2021
|
-
const registry = (requestedRegistry
|
|
2022
|
-
|
|
2023
|
-
|
|
2295
|
+
const registry = resolveTrustedRegistry(requestedRegistry, envRegistry);
|
|
2296
|
+
if (!registry) {
|
|
2297
|
+
return send(res, 400, {
|
|
2298
|
+
ok: false,
|
|
2299
|
+
code: "untrusted-registry",
|
|
2300
|
+
error: "不受信任的 npm 安装源: " + requestedRegistry
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2024
2303
|
const base = registry.endsWith("/") ? registry : registry + "/";
|
|
2025
2304
|
const pkgUrl = new URL("dsh-session-manager/latest", base).toString();
|
|
2026
2305
|
const controller = new AbortController();
|
|
@@ -2039,17 +2318,33 @@ ${rest}`, "utf8");
|
|
|
2039
2318
|
} finally {
|
|
2040
2319
|
clearTimeout(timer);
|
|
2041
2320
|
}
|
|
2042
|
-
hasUpdate = compareSemver(latestVersion,
|
|
2321
|
+
hasUpdate = compareSemver(latestVersion, installedVersion) > 0;
|
|
2322
|
+
const restartRequired = compareSemver(installedVersion, runningVersion) > 0;
|
|
2043
2323
|
return send(res, 200, {
|
|
2044
2324
|
ok: true,
|
|
2045
|
-
result: {
|
|
2325
|
+
result: {
|
|
2326
|
+
runningVersion,
|
|
2327
|
+
currentVersion: runningVersion,
|
|
2328
|
+
installedVersion,
|
|
2329
|
+
latestVersion,
|
|
2330
|
+
hasUpdate,
|
|
2331
|
+
restartRequired,
|
|
2332
|
+
source: "npm"
|
|
2333
|
+
}
|
|
2046
2334
|
});
|
|
2047
2335
|
} catch (err) {
|
|
2048
2336
|
return send(res, 200, {
|
|
2049
2337
|
ok: false,
|
|
2050
2338
|
code: "update-check-failed",
|
|
2051
2339
|
error: "检查更新失败: " + safeErrorMessage(err),
|
|
2052
|
-
result: {
|
|
2340
|
+
result: {
|
|
2341
|
+
runningVersion,
|
|
2342
|
+
currentVersion: runningVersion,
|
|
2343
|
+
installedVersion,
|
|
2344
|
+
latestVersion: installedVersion,
|
|
2345
|
+
hasUpdate: false,
|
|
2346
|
+
source: "npm"
|
|
2347
|
+
}
|
|
2053
2348
|
});
|
|
2054
2349
|
}
|
|
2055
2350
|
}
|
|
@@ -2058,38 +2353,650 @@ ${rest}`, "utf8");
|
|
|
2058
2353
|
if (rawVersion && !/^[a-zA-Z0-9_.-]+$/.test(rawVersion)) {
|
|
2059
2354
|
return send(res, 400, { ok: false, code: "bad-request", error: "Invalid version format" });
|
|
2060
2355
|
}
|
|
2061
|
-
|
|
2062
|
-
const spec = "dsh-session-manager@" + targetVersion;
|
|
2063
|
-
const registry = typeof body?.registry === "string" && /^https?:\/\//i.test(body.registry.trim()) ? body.registry.trim() : "";
|
|
2356
|
+
|
|
2064
2357
|
const pm = adapter.pluginManager;
|
|
2065
|
-
if (pm
|
|
2358
|
+
if (!pm || typeof pm.installBundle !== "function") {
|
|
2359
|
+
return send(res, 501, {
|
|
2360
|
+
ok: false,
|
|
2361
|
+
code: "plugin-manager-unavailable",
|
|
2362
|
+
error: "当前宿主环境未提供 PluginManager 安装服务,请通过 DSH 插件市场或命令行更新"
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
const profileDir = resolveProfileDir(adapter, ctx);
|
|
2367
|
+
|
|
2368
|
+
// P1-3: Concurrency Mutex check & immediate synchronous acquisition
|
|
2369
|
+
const inMemoryTask = getActiveTask();
|
|
2370
|
+
if (inMemoryTask) {
|
|
2371
|
+
return send(res, 409, {
|
|
2372
|
+
ok: false,
|
|
2373
|
+
code: "update-in-progress",
|
|
2374
|
+
error: "已有正在执行的更新任务 (" + inMemoryTask.taskId + "),请勿重复操作",
|
|
2375
|
+
taskId: inMemoryTask.taskId
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
const diskStatus = readUpdateStatus(profileDir);
|
|
2379
|
+
if (isDiskTaskActive(diskStatus)) {
|
|
2380
|
+
return send(res, 409, {
|
|
2381
|
+
ok: false,
|
|
2382
|
+
code: "update-in-progress",
|
|
2383
|
+
error: "已有正在执行的更新任务 (" + diskStatus.taskId + "),请勿重复操作",
|
|
2384
|
+
taskId: diskStatus.taskId
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
const taskId = (typeof body?.taskId === "string" && body.taskId.trim())
|
|
2389
|
+
? body.taskId.trim()
|
|
2390
|
+
: ("update-" + Date.now() + "-" + randomBytes(4).toString("hex"));
|
|
2391
|
+
|
|
2392
|
+
const taskStartedAt = Date.now();
|
|
2393
|
+
const taskRecord = {
|
|
2394
|
+
taskId,
|
|
2395
|
+
startedAt: taskStartedAt,
|
|
2396
|
+
profileDir,
|
|
2397
|
+
pid: process.pid,
|
|
2398
|
+
targetVersion: rawVersion || "latest"
|
|
2399
|
+
};
|
|
2400
|
+
setActiveTask(taskRecord);
|
|
2401
|
+
|
|
2402
|
+
const currentVersion = adapter.pluginVersion() || "0.7.3";
|
|
2403
|
+
let installedVersion = currentVersion;
|
|
2404
|
+
if (profileDir) {
|
|
2405
|
+
const pkgFile = join(profileDir, "node_modules", "dsh-session-manager", "package.json");
|
|
2406
|
+
if (existsSync(pkgFile)) {
|
|
2407
|
+
const diskCheck = verifyInstalledPackage(profileDir);
|
|
2408
|
+
if (diskCheck.ok && diskCheck.version) {
|
|
2409
|
+
installedVersion = diskCheck.version;
|
|
2410
|
+
} else {
|
|
2411
|
+
releaseActiveTask(taskId);
|
|
2412
|
+
return send(res, 500, {
|
|
2413
|
+
ok: false,
|
|
2414
|
+
code: "disk-verify-failed",
|
|
2415
|
+
error: "无法确认已安装版本,磁盘插件目录异常: " + (diskCheck?.error || "package.json 校验失败"),
|
|
2416
|
+
taskId
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
const targetVersion = rawVersion || "latest";
|
|
2423
|
+
let resolvedTargetVersion = (targetVersion !== "latest") ? targetVersion : null;
|
|
2424
|
+
const envRegistry = process.env.NPM_CONFIG_REGISTRY?.trim();
|
|
2425
|
+
const requestedRegistry = typeof body?.registry === "string" ? body.registry.trim() : "";
|
|
2426
|
+
const registry = resolveTrustedRegistry(requestedRegistry, envRegistry);
|
|
2427
|
+
if (!registry) {
|
|
2428
|
+
releaseActiveTask(taskId);
|
|
2429
|
+
return send(res, 400, {
|
|
2430
|
+
ok: false,
|
|
2431
|
+
code: "untrusted-registry",
|
|
2432
|
+
error: "不受信任的 npm 安装源: " + requestedRegistry
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
const forwardRegistry = (requestedRegistry && requestedRegistry !== "https://registry.npmjs.org")
|
|
2436
|
+
? registry
|
|
2437
|
+
: (envRegistry ? registry : "");
|
|
2438
|
+
const isHmr = isHmrActive(adapter, ctx);
|
|
2439
|
+
const prevRegistry = process.env.NPM_CONFIG_REGISTRY;
|
|
2440
|
+
|
|
2441
|
+
let heartbeatTimer = setInterval(() => {
|
|
2066
2442
|
try {
|
|
2067
|
-
const
|
|
2068
|
-
if (
|
|
2069
|
-
|
|
2443
|
+
const curStatus = readUpdateStatus(profileDir);
|
|
2444
|
+
if (curStatus && curStatus.taskId === taskId && ACTIVE_TASK_STATUSES.has(curStatus.status)) {
|
|
2445
|
+
curStatus.updatedAt = Date.now();
|
|
2446
|
+
writeUpdateStatus(profileDir, curStatus);
|
|
2447
|
+
}
|
|
2448
|
+
} catch (_) {}
|
|
2449
|
+
}, 10000);
|
|
2450
|
+
if (typeof heartbeatTimer?.unref === "function") {
|
|
2451
|
+
heartbeatTimer.unref();
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
try {
|
|
2455
|
+
// P1-4: Source check
|
|
2456
|
+
let bundles = [];
|
|
2457
|
+
if (typeof pm?.listBundles === "function") {
|
|
2458
|
+
try { bundles = await pm.listBundles(); } catch (_) {}
|
|
2459
|
+
}
|
|
2460
|
+
const source = detectPluginSource(adapter, ctx, bundles);
|
|
2461
|
+
if (source === "linked" || source === "github" || source === "installation") {
|
|
2462
|
+
releaseActiveTask(taskId);
|
|
2463
|
+
return send(res, 400, {
|
|
2464
|
+
ok: false,
|
|
2465
|
+
code: "source-protected",
|
|
2466
|
+
error: "当前插件为 " + source + " 安装来源,无法通过 npm 覆盖更新"
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
if (source === "unknown") {
|
|
2470
|
+
releaseActiveTask(taskId);
|
|
2471
|
+
return send(res, 400, {
|
|
2472
|
+
ok: false,
|
|
2473
|
+
code: "source-unknown",
|
|
2474
|
+
error: "无法确认插件安装来源,已禁止自动更新以防破坏环境"
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
if (!resolvedTargetVersion) {
|
|
2479
|
+
try {
|
|
2480
|
+
const regUrl = registry || process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org";
|
|
2481
|
+
const base = regUrl.endsWith("/") ? regUrl : regUrl + "/";
|
|
2482
|
+
const pkgUrl = new URL("dsh-session-manager/latest", base).toString();
|
|
2483
|
+
const ctrl = new AbortController();
|
|
2484
|
+
const timer = setTimeout(() => ctrl.abort(), 4000);
|
|
2485
|
+
try {
|
|
2486
|
+
const resp = await fetch(pkgUrl, { signal: ctrl.signal, headers: { accept: "application/json" } });
|
|
2487
|
+
if (resp.ok) {
|
|
2488
|
+
const d = await resp.json();
|
|
2489
|
+
if (d && typeof d.version === "string") {
|
|
2490
|
+
resolvedTargetVersion = d.version;
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
} finally {
|
|
2494
|
+
clearTimeout(timer);
|
|
2495
|
+
}
|
|
2496
|
+
} catch (_) { /* fallback to outcome.version after install */ }
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
const baselineVersion = installedVersion || currentVersion;
|
|
2500
|
+
|
|
2501
|
+
if (resolvedTargetVersion && baselineVersion && baselineVersion !== "unknown") {
|
|
2502
|
+
const cmp = compareSemver(resolvedTargetVersion, baselineVersion);
|
|
2503
|
+
if (cmp === 0) {
|
|
2504
|
+
// If target equals baseline:
|
|
2505
|
+
// Check if already installed on disk pending restart or refresh
|
|
2506
|
+
if (compareSemver(installedVersion, currentVersion) > 0) {
|
|
2507
|
+
releaseActiveTask(taskId);
|
|
2508
|
+
const application = isHmr ? "refresh-required" : "restart-required";
|
|
2509
|
+
writeUpdateStatus(profileDir, {
|
|
2510
|
+
taskId,
|
|
2511
|
+
status: "completed",
|
|
2512
|
+
targetVersion: installedVersion,
|
|
2513
|
+
currentVersion,
|
|
2514
|
+
application,
|
|
2515
|
+
alreadyInstalled: true,
|
|
2516
|
+
pid: process.pid,
|
|
2517
|
+
completedAt: Date.now()
|
|
2518
|
+
});
|
|
2519
|
+
return send(res, 200, {
|
|
2520
|
+
ok: true,
|
|
2521
|
+
result: {
|
|
2522
|
+
application,
|
|
2523
|
+
spec: "dsh-session-manager@" + installedVersion,
|
|
2524
|
+
version: installedVersion,
|
|
2525
|
+
taskId,
|
|
2526
|
+
alreadyInstalled: true
|
|
2527
|
+
}
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
// Target is equal to running & installed version -> not an upgrade
|
|
2531
|
+
releaseActiveTask(taskId);
|
|
2532
|
+
return send(res, 400, {
|
|
2533
|
+
ok: false,
|
|
2534
|
+
code: "not-upgrade",
|
|
2535
|
+
error: "目标版本 (" + resolvedTargetVersion + ") 不高于当前已安装版本 (" + baselineVersion + ")"
|
|
2536
|
+
});
|
|
2537
|
+
} else if (cmp < 0) {
|
|
2538
|
+
releaseActiveTask(taskId);
|
|
2539
|
+
return send(res, 400, {
|
|
2540
|
+
ok: false,
|
|
2541
|
+
code: "not-upgrade",
|
|
2542
|
+
error: "目标版本 (" + resolvedTargetVersion + ") 低于当前已安装版本 (" + baselineVersion + ")"
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
const spec = "dsh-session-manager@" + (resolvedTargetVersion || targetVersion);
|
|
2547
|
+
const oldSpec = "dsh-session-manager@" + currentVersion;
|
|
2548
|
+
|
|
2549
|
+
writeUpdateStatus(profileDir, {
|
|
2550
|
+
taskId,
|
|
2551
|
+
status: "preparing",
|
|
2552
|
+
targetVersion: resolvedTargetVersion || targetVersion,
|
|
2553
|
+
currentVersion,
|
|
2554
|
+
source: "npm",
|
|
2555
|
+
pid: process.pid,
|
|
2556
|
+
startedAt: taskStartedAt
|
|
2557
|
+
});
|
|
2558
|
+
|
|
2559
|
+
if (forwardRegistry) {
|
|
2560
|
+
process.env.NPM_CONFIG_REGISTRY = forwardRegistry;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
// Capture locals before any potential scope teardown
|
|
2564
|
+
const localProfileDir = profileDir;
|
|
2565
|
+
const localSpec = spec;
|
|
2566
|
+
const localOldSpec = oldSpec;
|
|
2567
|
+
const localPm = pm;
|
|
2568
|
+
const localTaskId = taskId;
|
|
2569
|
+
const localTargetVersion = targetVersion;
|
|
2570
|
+
const localCurrentVersion = currentVersion;
|
|
2571
|
+
const localRegistry = forwardRegistry;
|
|
2572
|
+
let localResolvedTargetVersion = resolvedTargetVersion;
|
|
2573
|
+
|
|
2574
|
+
// Helper: perform rollback and record state
|
|
2575
|
+
const performRollback = async (triggerError, { newVersionInstalled = false } = {}) => {
|
|
2576
|
+
// Restore registry environment first so rollback is not blocked by custom registry failure
|
|
2577
|
+
if (forwardRegistry) {
|
|
2578
|
+
if (prevRegistry !== undefined) {
|
|
2579
|
+
process.env.NPM_CONFIG_REGISTRY = prevRegistry;
|
|
2580
|
+
} else {
|
|
2581
|
+
delete process.env.NPM_CONFIG_REGISTRY;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
writeUpdateStatus(localProfileDir, {
|
|
2586
|
+
taskId: localTaskId,
|
|
2587
|
+
status: "rolling-back",
|
|
2588
|
+
targetVersion: localResolvedTargetVersion || localTargetVersion,
|
|
2589
|
+
currentVersion: localCurrentVersion,
|
|
2590
|
+
error: triggerError,
|
|
2591
|
+
pid: process.pid,
|
|
2592
|
+
rollingBackAt: Date.now()
|
|
2593
|
+
});
|
|
2594
|
+
|
|
2595
|
+
// P0-2: In HMR mode, if new version was already installed into profile,
|
|
2596
|
+
// calling installBundle(oldSpec) directly throws "already-installed".
|
|
2597
|
+
// Therefore, we must remove the new version first.
|
|
2598
|
+
let cleanError = null;
|
|
2599
|
+
if (isHmr && newVersionInstalled) {
|
|
2600
|
+
if (typeof localPm.removeBundle === "function") {
|
|
2601
|
+
try {
|
|
2602
|
+
const rmOutcome = await localPm.removeBundle("dsh-session-manager");
|
|
2603
|
+
if (rmOutcome && (rmOutcome.application === "failed" || rmOutcome.application === "cancelled" || rmOutcome.ok === false)) {
|
|
2604
|
+
cleanError = rmOutcome.error?.message || rmOutcome.error?.code || safeErrorMessage(rmOutcome.error) || ("application: " + rmOutcome.application);
|
|
2605
|
+
}
|
|
2606
|
+
} catch (rmErr) {
|
|
2607
|
+
cleanError = safeErrorMessage(rmErr);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2070
2610
|
}
|
|
2071
|
-
|
|
2611
|
+
|
|
2612
|
+
if (cleanError) {
|
|
2613
|
+
const rbDetail = "清理安装失败的新版本失败: " + cleanError;
|
|
2614
|
+
releaseActiveTask(localTaskId);
|
|
2615
|
+
const actualCheck = verifyInstalledPackage(localProfileDir);
|
|
2616
|
+
const installedState = actualCheck.ok ? (actualCheck.version || "unknown") : "unverified";
|
|
2617
|
+
writeUpdateStatus(localProfileDir, {
|
|
2618
|
+
taskId: localTaskId,
|
|
2619
|
+
status: "recovery-required",
|
|
2620
|
+
stage: "cleanup-new-version",
|
|
2621
|
+
targetVersion: localResolvedTargetVersion || localTargetVersion,
|
|
2622
|
+
currentVersion: localCurrentVersion,
|
|
2623
|
+
rolledBack: false,
|
|
2624
|
+
error: triggerError,
|
|
2625
|
+
rollbackError: rbDetail,
|
|
2626
|
+
installedState,
|
|
2627
|
+
pid: process.pid,
|
|
2628
|
+
failedAt: Date.now()
|
|
2629
|
+
});
|
|
2630
|
+
if (!res.writableEnded) {
|
|
2631
|
+
return send(res, 500, {
|
|
2632
|
+
ok: false,
|
|
2633
|
+
code: "recovery-required",
|
|
2634
|
+
stage: "cleanup-new-version",
|
|
2635
|
+
error: "安装更新失败: " + triggerError + "(自动恢复旧版本失败: " + rbDetail + ",请手动重新安装插件)",
|
|
2636
|
+
rolledBack: false,
|
|
2637
|
+
rollbackError: rbDetail,
|
|
2638
|
+
originalVersion: localCurrentVersion,
|
|
2639
|
+
targetVersion: localResolvedTargetVersion || localTargetVersion,
|
|
2640
|
+
installedState,
|
|
2641
|
+
taskId: localTaskId
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
let rbOutcome = null;
|
|
2648
|
+
let rbError = null;
|
|
2072
2649
|
try {
|
|
2073
|
-
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2650
|
+
rbOutcome = await (localRegistry ? localPm.installBundle(localOldSpec, { registry: localRegistry }) : localPm.installBundle(localOldSpec));
|
|
2651
|
+
} catch (err) {
|
|
2652
|
+
rbError = safeErrorMessage(err);
|
|
2653
|
+
// Fallback to default installBundle without registry option if custom registry fails
|
|
2654
|
+
if (localRegistry) {
|
|
2655
|
+
try {
|
|
2656
|
+
rbOutcome = await localPm.installBundle(localOldSpec);
|
|
2657
|
+
rbError = null;
|
|
2658
|
+
} catch (err2) {
|
|
2659
|
+
rbError = safeErrorMessage(err2);
|
|
2080
2660
|
}
|
|
2081
2661
|
}
|
|
2082
2662
|
}
|
|
2083
|
-
|
|
2084
|
-
|
|
2663
|
+
let installOk = rbOutcome && rbOutcome.application !== "failed" && rbOutcome.application !== "cancelled" && rbOutcome.ok !== false && !rbError;
|
|
2664
|
+
|
|
2665
|
+
if (installOk && typeof localPm.selectBundle === "function") {
|
|
2666
|
+
try { await localPm.selectBundle("dsh-session-manager", true); } catch (_) {}
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
let verifiedOk = false;
|
|
2670
|
+
let verifyError = null;
|
|
2671
|
+
if (installOk) {
|
|
2672
|
+
const v = verifyInstalledPackage(localProfileDir, localCurrentVersion);
|
|
2673
|
+
verifiedOk = v.ok;
|
|
2674
|
+
if (!v.ok) verifyError = v.error;
|
|
2675
|
+
}
|
|
2676
|
+
releaseActiveTask(localTaskId);
|
|
2677
|
+
if (installOk && verifiedOk) {
|
|
2678
|
+
writeUpdateStatus(localProfileDir, {
|
|
2679
|
+
taskId: localTaskId,
|
|
2680
|
+
status: "rolled-back",
|
|
2681
|
+
targetVersion: localResolvedTargetVersion || localTargetVersion,
|
|
2682
|
+
currentVersion: localCurrentVersion,
|
|
2683
|
+
rolledBack: true,
|
|
2684
|
+
error: triggerError,
|
|
2685
|
+
pid: process.pid,
|
|
2686
|
+
failedAt: Date.now()
|
|
2687
|
+
});
|
|
2688
|
+
if (!res.writableEnded) {
|
|
2689
|
+
return send(res, 500, {
|
|
2690
|
+
ok: false,
|
|
2691
|
+
code: "install-failed",
|
|
2692
|
+
error: "安装更新失败: " + triggerError + "(已自动恢复旧版本 v" + localCurrentVersion + ")",
|
|
2693
|
+
rolledBack: true,
|
|
2694
|
+
originalVersion: localCurrentVersion,
|
|
2695
|
+
targetVersion: localResolvedTargetVersion || localTargetVersion,
|
|
2696
|
+
taskId: localTaskId
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
} else {
|
|
2700
|
+
const failureStage = !installOk ? "reinstall-old-version" : "verify-rollback";
|
|
2701
|
+
const rbDetail = rbError || verifyError || (rbOutcome?.error ? safeErrorMessage(rbOutcome.error) : "回滚安装未成功");
|
|
2702
|
+
const actualCheck = verifyInstalledPackage(localProfileDir);
|
|
2703
|
+
const installedState = actualCheck.ok ? (actualCheck.version || "unknown") : "unverified";
|
|
2704
|
+
writeUpdateStatus(localProfileDir, {
|
|
2705
|
+
taskId: localTaskId,
|
|
2706
|
+
status: "recovery-required",
|
|
2707
|
+
stage: failureStage,
|
|
2708
|
+
targetVersion: localResolvedTargetVersion || localTargetVersion,
|
|
2709
|
+
currentVersion: localCurrentVersion,
|
|
2710
|
+
rolledBack: false,
|
|
2711
|
+
error: triggerError,
|
|
2712
|
+
rollbackError: rbDetail,
|
|
2713
|
+
installedState,
|
|
2714
|
+
pid: process.pid,
|
|
2715
|
+
failedAt: Date.now()
|
|
2716
|
+
});
|
|
2717
|
+
if (!res.writableEnded) {
|
|
2718
|
+
return send(res, 500, {
|
|
2719
|
+
ok: false,
|
|
2720
|
+
code: "recovery-required",
|
|
2721
|
+
stage: failureStage,
|
|
2722
|
+
error: "安装更新失败: " + triggerError + "(自动恢复旧版本失败: " + rbDetail + ",请手动重新安装插件)",
|
|
2723
|
+
rolledBack: false,
|
|
2724
|
+
rollbackError: rbDetail,
|
|
2725
|
+
originalVersion: localCurrentVersion,
|
|
2726
|
+
targetVersion: localResolvedTargetVersion || localTargetVersion,
|
|
2727
|
+
installedState,
|
|
2728
|
+
taskId: localTaskId
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
};
|
|
2733
|
+
|
|
2734
|
+
if (isHmr && typeof localPm.removeBundle === "function") {
|
|
2735
|
+
// P0-1 & P0-2: Remove old bundle first in HMR
|
|
2736
|
+
writeUpdateStatus(localProfileDir, {
|
|
2737
|
+
taskId: localTaskId,
|
|
2738
|
+
status: "removing",
|
|
2739
|
+
targetVersion: localTargetVersion,
|
|
2740
|
+
currentVersion: localCurrentVersion,
|
|
2741
|
+
source: "npm",
|
|
2742
|
+
pid: process.pid,
|
|
2743
|
+
startedAt: taskStartedAt
|
|
2744
|
+
});
|
|
2745
|
+
|
|
2746
|
+
let removeResult;
|
|
2747
|
+
try {
|
|
2748
|
+
removeResult = await localPm.removeBundle("dsh-session-manager");
|
|
2749
|
+
} catch (rmErr) {
|
|
2750
|
+
const failReason = safeErrorMessage(rmErr);
|
|
2751
|
+
releaseActiveTask(localTaskId);
|
|
2752
|
+
writeUpdateStatus(localProfileDir, {
|
|
2753
|
+
taskId: localTaskId,
|
|
2754
|
+
status: "failed",
|
|
2755
|
+
stage: "removing",
|
|
2756
|
+
targetVersion: localTargetVersion,
|
|
2757
|
+
currentVersion: localCurrentVersion,
|
|
2758
|
+
error: "卸载旧版本失败: " + failReason,
|
|
2759
|
+
pid: process.pid,
|
|
2760
|
+
failedAt: Date.now()
|
|
2761
|
+
});
|
|
2762
|
+
return send(res, 500, {
|
|
2763
|
+
ok: false,
|
|
2764
|
+
code: "remove-failed",
|
|
2765
|
+
error: "卸载旧版本失败: " + failReason,
|
|
2766
|
+
taskId: localTaskId
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// P0-2: Strictly check removeBundle return value
|
|
2771
|
+
if (removeResult && (removeResult.application === "failed" || removeResult.application === "cancelled" || removeResult.ok === false)) {
|
|
2772
|
+
const failReason = removeResult.error?.message || removeResult.error?.code || safeErrorMessage(removeResult.error) || ("application: " + removeResult.application);
|
|
2773
|
+
if (removeResult.changed && typeof localPm.selectBundle === "function") {
|
|
2774
|
+
try { await localPm.selectBundle("dsh-session-manager", true); } catch (_) {}
|
|
2775
|
+
}
|
|
2776
|
+
releaseActiveTask(localTaskId);
|
|
2777
|
+
writeUpdateStatus(localProfileDir, {
|
|
2778
|
+
taskId: localTaskId,
|
|
2779
|
+
status: "failed",
|
|
2780
|
+
stage: "removing",
|
|
2781
|
+
targetVersion: localTargetVersion,
|
|
2782
|
+
currentVersion: localCurrentVersion,
|
|
2783
|
+
error: "卸载旧版本失败: " + failReason,
|
|
2784
|
+
pid: process.pid,
|
|
2785
|
+
failedAt: Date.now()
|
|
2786
|
+
});
|
|
2787
|
+
return send(res, 500, {
|
|
2788
|
+
ok: false,
|
|
2789
|
+
code: "remove-failed",
|
|
2790
|
+
error: "卸载旧版本失败: " + failReason,
|
|
2791
|
+
taskId: localTaskId
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
writeUpdateStatus(localProfileDir, {
|
|
2796
|
+
taskId: localTaskId,
|
|
2797
|
+
status: "removed",
|
|
2798
|
+
targetVersion: localTargetVersion,
|
|
2799
|
+
currentVersion: localCurrentVersion,
|
|
2800
|
+
source: "npm",
|
|
2801
|
+
pid: process.pid,
|
|
2802
|
+
startedAt: taskStartedAt
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// Install target bundle
|
|
2807
|
+
writeUpdateStatus(localProfileDir, {
|
|
2808
|
+
taskId: localTaskId,
|
|
2809
|
+
status: "installing",
|
|
2810
|
+
targetVersion: localResolvedTargetVersion || localTargetVersion,
|
|
2811
|
+
currentVersion: localCurrentVersion,
|
|
2812
|
+
source: "npm",
|
|
2813
|
+
pid: process.pid,
|
|
2814
|
+
startedAt: taskStartedAt
|
|
2815
|
+
});
|
|
2816
|
+
|
|
2817
|
+
let outcome;
|
|
2818
|
+
try {
|
|
2819
|
+
outcome = await (localRegistry ? localPm.installBundle(localSpec, { registry: localRegistry }) : localPm.installBundle(localSpec));
|
|
2085
2820
|
} catch (err) {
|
|
2086
|
-
|
|
2821
|
+
const errMsg = safeErrorMessage(err);
|
|
2822
|
+
if (isHmr) {
|
|
2823
|
+
// In HMR, old bundle was removed -> execute rollback (new version was not installed)
|
|
2824
|
+
return await performRollback(errMsg, { newVersionInstalled: false });
|
|
2825
|
+
} else {
|
|
2826
|
+
// In non-HMR, old bundle was not removed -> fail directly
|
|
2827
|
+
releaseActiveTask(localTaskId);
|
|
2828
|
+
writeUpdateStatus(localProfileDir, {
|
|
2829
|
+
taskId: localTaskId,
|
|
2830
|
+
status: "failed",
|
|
2831
|
+
targetVersion: localResolvedTargetVersion || localTargetVersion,
|
|
2832
|
+
currentVersion: localCurrentVersion,
|
|
2833
|
+
error: errMsg,
|
|
2834
|
+
pid: process.pid,
|
|
2835
|
+
failedAt: Date.now()
|
|
2836
|
+
});
|
|
2837
|
+
return send(res, 500, {
|
|
2838
|
+
ok: false,
|
|
2839
|
+
code: "install-failed",
|
|
2840
|
+
error: "安装更新失败: " + errMsg,
|
|
2841
|
+
taskId: localTaskId
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// Check outcome application
|
|
2847
|
+
if (outcome && (outcome.application === "failed" || outcome.application === "cancelled" || outcome.ok === false)) {
|
|
2848
|
+
const failReason = outcome.error?.message || outcome.error || ("application: " + outcome.application);
|
|
2849
|
+
if (isHmr) {
|
|
2850
|
+
return await performRollback(failReason, { newVersionInstalled: false });
|
|
2851
|
+
} else {
|
|
2852
|
+
releaseActiveTask(localTaskId);
|
|
2853
|
+
writeUpdateStatus(localProfileDir, {
|
|
2854
|
+
taskId: localTaskId,
|
|
2855
|
+
status: "failed",
|
|
2856
|
+
targetVersion: localResolvedTargetVersion || localTargetVersion,
|
|
2857
|
+
currentVersion: localCurrentVersion,
|
|
2858
|
+
error: failReason,
|
|
2859
|
+
pid: process.pid,
|
|
2860
|
+
failedAt: Date.now()
|
|
2861
|
+
});
|
|
2862
|
+
return send(res, 500, {
|
|
2863
|
+
ok: false,
|
|
2864
|
+
code: "install-failed",
|
|
2865
|
+
error: "安装更新失败: " + failReason,
|
|
2866
|
+
taskId: localTaskId
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2087
2869
|
}
|
|
2870
|
+
|
|
2871
|
+
if (!localResolvedTargetVersion && typeof outcome?.version === "string") {
|
|
2872
|
+
localResolvedTargetVersion = outcome.version;
|
|
2873
|
+
}
|
|
2874
|
+
const concreteTargetVersion = localResolvedTargetVersion || (localTargetVersion !== "latest" ? localTargetVersion : null);
|
|
2875
|
+
|
|
2876
|
+
// P1-2: Strict post-install verification
|
|
2877
|
+
writeUpdateStatus(localProfileDir, {
|
|
2878
|
+
taskId: localTaskId,
|
|
2879
|
+
status: "verifying",
|
|
2880
|
+
targetVersion: concreteTargetVersion || localTargetVersion,
|
|
2881
|
+
currentVersion: localCurrentVersion,
|
|
2882
|
+
source: "npm",
|
|
2883
|
+
pid: process.pid,
|
|
2884
|
+
startedAt: taskStartedAt
|
|
2885
|
+
});
|
|
2886
|
+
|
|
2887
|
+
const verifyResult = verifyInstalledPackage(localProfileDir, concreteTargetVersion);
|
|
2888
|
+
if (!verifyResult.ok) {
|
|
2889
|
+
const vError = "安装后校验失败: " + verifyResult.error;
|
|
2890
|
+
return await performRollback(vError, { newVersionInstalled: true });
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
// Completed!
|
|
2894
|
+
const application = outcome?.application || (isHmr ? "refresh-required" : "restart-required");
|
|
2895
|
+
releaseActiveTask(localTaskId);
|
|
2896
|
+
const finalInstalledVersion = verifyResult.version || concreteTargetVersion || localTargetVersion;
|
|
2897
|
+
writeUpdateStatus(localProfileDir, {
|
|
2898
|
+
taskId: localTaskId,
|
|
2899
|
+
status: "completed",
|
|
2900
|
+
targetVersion: finalInstalledVersion,
|
|
2901
|
+
currentVersion: localCurrentVersion,
|
|
2902
|
+
application,
|
|
2903
|
+
pid: process.pid,
|
|
2904
|
+
completedAt: Date.now()
|
|
2905
|
+
});
|
|
2906
|
+
|
|
2907
|
+
if (!res.writableEnded) {
|
|
2908
|
+
return send(res, 200, {
|
|
2909
|
+
ok: true,
|
|
2910
|
+
result: {
|
|
2911
|
+
application,
|
|
2912
|
+
spec: localSpec,
|
|
2913
|
+
version: finalInstalledVersion,
|
|
2914
|
+
taskId: localTaskId
|
|
2915
|
+
}
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
} finally {
|
|
2919
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
2920
|
+
releaseActiveTask(taskId);
|
|
2921
|
+
if (forwardRegistry) {
|
|
2922
|
+
if (prevRegistry !== undefined) {
|
|
2923
|
+
process.env.NPM_CONFIG_REGISTRY = prevRegistry;
|
|
2924
|
+
} else {
|
|
2925
|
+
delete process.env.NPM_CONFIG_REGISTRY;
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
if (req.method === "GET" && path === "/update/status") {
|
|
2931
|
+
const profileDir = resolveProfileDir(adapter, ctx);
|
|
2932
|
+
const currentVersion = adapter.pluginVersion() || "0.7.3";
|
|
2933
|
+
let installedVersion = currentVersion;
|
|
2934
|
+
if (profileDir) {
|
|
2935
|
+
const diskCheck = verifyInstalledPackage(profileDir);
|
|
2936
|
+
if (diskCheck.ok && diskCheck.version) {
|
|
2937
|
+
installedVersion = diskCheck.version;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
const updateStatus = readUpdateStatus(profileDir);
|
|
2941
|
+
const queryTaskId = url.searchParams.get("taskId")?.trim();
|
|
2942
|
+
|
|
2943
|
+
if (queryTaskId) {
|
|
2944
|
+
if (updateStatus && updateStatus.taskId === queryTaskId) {
|
|
2945
|
+
return send(res, 200, {
|
|
2946
|
+
ok: true,
|
|
2947
|
+
result: {
|
|
2948
|
+
...updateStatus,
|
|
2949
|
+
runningVersion: currentVersion,
|
|
2950
|
+
currentVersion,
|
|
2951
|
+
installedVersion
|
|
2952
|
+
}
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
if (updateStatus && updateStatus.taskId && updateStatus.taskId !== queryTaskId) {
|
|
2956
|
+
return send(res, 200, {
|
|
2957
|
+
ok: true,
|
|
2958
|
+
result: {
|
|
2959
|
+
status: "superseded",
|
|
2960
|
+
taskId: queryTaskId,
|
|
2961
|
+
activeTaskId: updateStatus.taskId,
|
|
2962
|
+
runningVersion: currentVersion,
|
|
2963
|
+
currentVersion,
|
|
2964
|
+
installedVersion
|
|
2965
|
+
}
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
return send(res, 200, {
|
|
2969
|
+
ok: true,
|
|
2970
|
+
result: {
|
|
2971
|
+
status: "idle",
|
|
2972
|
+
taskId: queryTaskId,
|
|
2973
|
+
runningVersion: currentVersion,
|
|
2974
|
+
currentVersion,
|
|
2975
|
+
installedVersion
|
|
2976
|
+
}
|
|
2977
|
+
});
|
|
2088
2978
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2979
|
+
|
|
2980
|
+
const now = Date.now();
|
|
2981
|
+
if (updateStatus && (now - (updateStatus.completedAt || updateStatus.failedAt || updateStatus.startedAt || 0) < 10 * 60 * 1000)) {
|
|
2982
|
+
return send(res, 200, {
|
|
2983
|
+
ok: true,
|
|
2984
|
+
result: {
|
|
2985
|
+
...updateStatus,
|
|
2986
|
+
runningVersion: currentVersion,
|
|
2987
|
+
currentVersion,
|
|
2988
|
+
installedVersion
|
|
2989
|
+
}
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
return send(res, 200, {
|
|
2993
|
+
ok: true,
|
|
2994
|
+
result: {
|
|
2995
|
+
status: "idle",
|
|
2996
|
+
runningVersion: currentVersion,
|
|
2997
|
+
currentVersion,
|
|
2998
|
+
installedVersion
|
|
2999
|
+
}
|
|
2093
3000
|
});
|
|
2094
3001
|
}
|
|
2095
3002
|
return send(res, 404, { ok: false, error: "not found: " + req.method + " " + path });
|
|
@@ -2111,4 +3018,4 @@ ${rest}`, "utf8");
|
|
|
2111
3018
|
}), "session-manager: http api");
|
|
2112
3019
|
}
|
|
2113
3020
|
|
|
2114
|
-
export { listSessionHeaders, scanSessionHeaders, encodeSessionSegment, compareSemver };
|
|
3021
|
+
export { listSessionHeaders, scanSessionHeaders, candidateSessionDirs, encodeFsSessionSegment, decodeFsSessionSegment, encodeProjectSegment, encodeSessionSegment, compareSemver };
|