dsh-session-manager 0.7.2 → 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 +11 -0
- package/README.md +5 -5
- package/README.zh.md +3 -3
- package/lib/client.js +401 -66
- package/lib/compat/dsh-adapter.js +177 -7
- package/lib/index.js +928 -32
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -22,12 +22,13 @@
|
|
|
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 { createDshAdapter, normalizeSessionHeader, compareSemver, detectPluginSource, isHmrActive, resolveProfileDir } from "./compat/dsh-adapter.js";
|
|
31
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";
|
|
@@ -174,9 +175,199 @@ const scanSessionHeaders = async (persistence, dshHome) => {
|
|
|
174
175
|
|
|
175
176
|
const listSessionHeaders = async (persistence, dshHome) => (await scanSessionHeaders(persistence, dshHome)).headers;
|
|
176
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
|
+
|
|
177
368
|
export function apply(ctx) {
|
|
178
369
|
try {
|
|
179
|
-
const buildId = "dsh-session-manager v0.7.
|
|
370
|
+
const buildId = "dsh-session-manager v0.7.3 (workspace-archive)";
|
|
180
371
|
if (typeof ctx?.logger?.info === "function") ctx.logger.info(buildId);
|
|
181
372
|
else if (typeof console !== "undefined" && console?.info) console.info(buildId);
|
|
182
373
|
} catch (_) { /* ignore logging failures */ }
|
|
@@ -2024,14 +2215,91 @@ ${rest}`, "utf8");
|
|
|
2024
2215
|
return send(res, 200, { ok: true, result });
|
|
2025
2216
|
}
|
|
2026
2217
|
if (req.method === "GET" && path === "/update/check") {
|
|
2027
|
-
const currentVersion = adapter.pluginVersion() || "0.7.
|
|
2028
|
-
|
|
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;
|
|
2029
2291
|
let hasUpdate = false;
|
|
2030
2292
|
try {
|
|
2293
|
+
const envRegistry = process.env.NPM_CONFIG_REGISTRY?.trim();
|
|
2031
2294
|
const requestedRegistry = url.searchParams.get("registry")?.trim();
|
|
2032
|
-
const registry = (requestedRegistry
|
|
2033
|
-
|
|
2034
|
-
|
|
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
|
+
}
|
|
2035
2303
|
const base = registry.endsWith("/") ? registry : registry + "/";
|
|
2036
2304
|
const pkgUrl = new URL("dsh-session-manager/latest", base).toString();
|
|
2037
2305
|
const controller = new AbortController();
|
|
@@ -2050,17 +2318,33 @@ ${rest}`, "utf8");
|
|
|
2050
2318
|
} finally {
|
|
2051
2319
|
clearTimeout(timer);
|
|
2052
2320
|
}
|
|
2053
|
-
hasUpdate = compareSemver(latestVersion,
|
|
2321
|
+
hasUpdate = compareSemver(latestVersion, installedVersion) > 0;
|
|
2322
|
+
const restartRequired = compareSemver(installedVersion, runningVersion) > 0;
|
|
2054
2323
|
return send(res, 200, {
|
|
2055
2324
|
ok: true,
|
|
2056
|
-
result: {
|
|
2325
|
+
result: {
|
|
2326
|
+
runningVersion,
|
|
2327
|
+
currentVersion: runningVersion,
|
|
2328
|
+
installedVersion,
|
|
2329
|
+
latestVersion,
|
|
2330
|
+
hasUpdate,
|
|
2331
|
+
restartRequired,
|
|
2332
|
+
source: "npm"
|
|
2333
|
+
}
|
|
2057
2334
|
});
|
|
2058
2335
|
} catch (err) {
|
|
2059
2336
|
return send(res, 200, {
|
|
2060
2337
|
ok: false,
|
|
2061
2338
|
code: "update-check-failed",
|
|
2062
2339
|
error: "检查更新失败: " + safeErrorMessage(err),
|
|
2063
|
-
result: {
|
|
2340
|
+
result: {
|
|
2341
|
+
runningVersion,
|
|
2342
|
+
currentVersion: runningVersion,
|
|
2343
|
+
installedVersion,
|
|
2344
|
+
latestVersion: installedVersion,
|
|
2345
|
+
hasUpdate: false,
|
|
2346
|
+
source: "npm"
|
|
2347
|
+
}
|
|
2064
2348
|
});
|
|
2065
2349
|
}
|
|
2066
2350
|
}
|
|
@@ -2069,38 +2353,650 @@ ${rest}`, "utf8");
|
|
|
2069
2353
|
if (rawVersion && !/^[a-zA-Z0-9_.-]+$/.test(rawVersion)) {
|
|
2070
2354
|
return send(res, 400, { ok: false, code: "bad-request", error: "Invalid version format" });
|
|
2071
2355
|
}
|
|
2072
|
-
|
|
2073
|
-
const spec = "dsh-session-manager@" + targetVersion;
|
|
2074
|
-
const registry = typeof body?.registry === "string" && /^https?:\/\//i.test(body.registry.trim()) ? body.registry.trim() : "";
|
|
2356
|
+
|
|
2075
2357
|
const pm = adapter.pluginManager;
|
|
2076
|
-
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(() => {
|
|
2077
2442
|
try {
|
|
2078
|
-
const
|
|
2079
|
-
if (
|
|
2080
|
-
|
|
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);
|
|
2081
2447
|
}
|
|
2082
|
-
|
|
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) {
|
|
2083
2479
|
try {
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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
|
+
}
|
|
2091
2492
|
}
|
|
2493
|
+
} finally {
|
|
2494
|
+
clearTimeout(timer);
|
|
2092
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
|
+
});
|
|
2093
2544
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
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
|
+
}
|
|
2610
|
+
}
|
|
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;
|
|
2649
|
+
try {
|
|
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);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
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));
|
|
2096
2820
|
} catch (err) {
|
|
2097
|
-
|
|
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
|
+
}
|
|
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
|
+
});
|
|
2098
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
|
+
});
|
|
2099
2978
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
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
|
+
}
|
|
2104
3000
|
});
|
|
2105
3001
|
}
|
|
2106
3002
|
return send(res, 404, { ok: false, error: "not found: " + req.method + " " + path });
|