weapp-ide-cli 6.2.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -3
- package/dist/{automator-CRpB6_pA.js → automator-BOe32upt.js} +388 -142
- package/dist/automator-VSjgHujY.js +1 -0
- package/dist/automator-session-VOB03glx.js +186 -0
- package/dist/{sessionStore-zdzy2UFM.js → automatorProject-DjYLqMj6.js} +495 -219
- package/dist/{cli-ChMQqi5w.js → cli-CFlqFpIw.js} +372 -61
- package/dist/cli.js +2 -2
- package/dist/commands-DmirGBy4.js +1 -0
- package/dist/{commands-C1PF1B4P.js → commands-mslf7-sP.js} +3 -2
- package/dist/{devtoolsProjectOwnership-yDD6h0dO.js → devtoolsProjectOwnership-BMfSAg4n.js} +394 -98
- package/dist/index.d.ts +123 -39
- package/dist/index.js +8 -8
- package/dist/{managed-Sq8y8FzP.js → managed-B04Xo9U_.js} +107 -55
- package/dist/run-mcp-83-VKZVA.js +1 -0
- package/dist/{run-mcp-Dl_1TH1x.js → run-mcp-Ni139Z5C.js} +11 -1
- package/package.json +9 -9
- package/dist/automator-BkZZsGtn.js +0 -1
- package/dist/automator-session-gWEsSs_A.js +0 -558
- package/dist/commands-CZVPfkE9.js +0 -1
- package/dist/run-mcp-CPv8cesh.js +0 -1
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as withPowerShellUtf8Output, T as inspectWechatDevtoolsInstallation, g as resolveWechatInspectionTimeout, h as resolveWechatDevtoolsInstallationRoot, m as assertWechatDevtoolsPort, p as assertWechatDevtoolsHost, u as resolveWechatDevtoolsTarget } from "./automatorProject-DjYLqMj6.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { TextDecoder, debuglog, isDeepStrictEqual, stripVTControlCharacters } from "node:util";
|
|
4
4
|
import { assertMachineE2ELeaseRecoveryScope, mutateLease, withMachineE2ELease, withMachineE2ELeaseRecoveryOperation } from "@weapp-vite/devtools-runtime";
|
|
5
5
|
import { execa } from "execa";
|
|
6
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
7
6
|
import fs from "node:fs/promises";
|
|
8
7
|
import process from "node:process";
|
|
8
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
9
9
|
import { Buffer } from "node:buffer";
|
|
10
10
|
import net from "node:net";
|
|
11
11
|
import { setTimeout } from "node:timers/promises";
|
|
12
12
|
import { z } from "zod";
|
|
13
|
-
import { performance } from "node:perf_hooks";
|
|
13
|
+
import { performance as performance$1 } from "node:perf_hooks";
|
|
14
14
|
//#region src/cli/agentStart.ts
|
|
15
15
|
/** 提取独立的完整 JSON 对象,普通日志和嵌套对象不能替代命令响应。 */
|
|
16
16
|
function readResponseObjects(stdout) {
|
|
@@ -91,6 +91,9 @@ async function startWechatIdeAgent(options) {
|
|
|
91
91
|
String(options.port)
|
|
92
92
|
];
|
|
93
93
|
if (options.trustProject === true) argv.push("--trust-project");
|
|
94
|
+
options.signal?.throwIfAborted();
|
|
95
|
+
await options.onBeforeDispatch?.();
|
|
96
|
+
options.signal?.throwIfAborted();
|
|
94
97
|
const started = parseAgentStartResponse((await execa(target.cliPath, argv, {
|
|
95
98
|
cancelSignal: options.signal,
|
|
96
99
|
timeout,
|
|
@@ -114,34 +117,151 @@ async function startWechatIdeAgent(options) {
|
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
119
|
//#endregion
|
|
117
|
-
//#region src/devtoolsProjectOwnership/
|
|
118
|
-
const
|
|
120
|
+
//#region src/devtoolsProjectOwnership/installationExit/darwinKernelPaths.ts
|
|
121
|
+
const inspectionTimeoutMs$1 = 1e4;
|
|
122
|
+
const kernelPathsScript = `
|
|
123
|
+
ObjC.import('Foundation');
|
|
124
|
+
ObjC.bindFunction('malloc', ['void *', ['unsigned long']]);
|
|
125
|
+
ObjC.bindFunction('free', ['void', ['void *']]);
|
|
126
|
+
ObjC.bindFunction('proc_pidpath', ['int', ['int', 'void *', 'uint32_t']]);
|
|
127
|
+
ObjC.bindFunction('strchr', ['char *', ['void *', 'int']]);
|
|
128
|
+
function run(argv) {
|
|
129
|
+
var pids = JSON.parse(argv[0]);
|
|
130
|
+
var buffer = $.malloc(4096);
|
|
131
|
+
if (!buffer) throw new Error('Cannot allocate process path buffer.');
|
|
132
|
+
try {
|
|
133
|
+
return JSON.stringify(pids.map(function(pid) {
|
|
134
|
+
var count = $.proc_pidpath(pid, buffer, 4096);
|
|
135
|
+
return { pid: pid, executable: count > 0 ? $.strchr(buffer, 47) : null };
|
|
136
|
+
}));
|
|
137
|
+
} finally { $.free(buffer); }
|
|
138
|
+
}
|
|
139
|
+
`;
|
|
140
|
+
/** 在 JSON 边界严格核验内核响应,不让进程身份查询依赖 schema 运行时。 */
|
|
141
|
+
function parseKernelPaths(stdout) {
|
|
142
|
+
const parsed = JSON.parse(stdout);
|
|
143
|
+
if (!Array.isArray(parsed)) throw new TypeError("Invalid kernel process paths.");
|
|
144
|
+
return parsed.map((entry) => {
|
|
145
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry) || Object.keys(entry).length !== 2 || !("pid" in entry) || typeof entry.pid !== "number" || !Number.isSafeInteger(entry.pid) || entry.pid <= 0 || !("executable" in entry) || entry.executable !== null && typeof entry.executable !== "string") throw new TypeError("Invalid kernel process path entry.");
|
|
146
|
+
return {
|
|
147
|
+
pid: entry.pid,
|
|
148
|
+
executable: entry.executable
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/** 系统 JXA/libproc 不可用时明确拒绝核验,不安装工具或以进程标题降级。 */
|
|
153
|
+
async function readDarwinKernelPaths(pids, timeoutMs = inspectionTimeoutMs$1) {
|
|
154
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Installation-exit recovery cannot inspect kernel executable paths.");
|
|
155
|
+
const result = await execa("/usr/bin/osascript", [
|
|
156
|
+
"-l",
|
|
157
|
+
"JavaScript",
|
|
158
|
+
"-e",
|
|
159
|
+
kernelPathsScript,
|
|
160
|
+
JSON.stringify(pids)
|
|
161
|
+
], {
|
|
162
|
+
timeout: Math.min(inspectionTimeoutMs$1, timeoutMs),
|
|
163
|
+
reject: false,
|
|
164
|
+
env: { LC_ALL: "C" }
|
|
165
|
+
});
|
|
166
|
+
if (result.exitCode !== 0 || result.timedOut || result.signal || result.stderr?.trim()) throw new Error("Installation-exit recovery cannot inspect kernel executable paths.");
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = parseKernelPaths(result.stdout);
|
|
170
|
+
} catch {
|
|
171
|
+
throw new Error("Installation-exit recovery received incomplete kernel executable paths.");
|
|
172
|
+
}
|
|
173
|
+
const entries = new Map(parsed.map((entry) => [entry.pid, entry.executable]));
|
|
174
|
+
if (entries.size !== pids.length || parsed.length !== pids.length || pids.some((pid) => !entries.has(pid))) throw new Error("Installation-exit recovery received incomplete kernel process identities.");
|
|
175
|
+
return entries;
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/devtoolsProjectOwnership/darwinProcessIdentity.ts
|
|
179
|
+
const inspectionTimeoutMs = 3e3;
|
|
180
|
+
function invalidIdentity$2() {
|
|
181
|
+
return /* @__PURE__ */ new Error("Managed DevTools process identity could not be verified; no project was closed.");
|
|
182
|
+
}
|
|
183
|
+
async function readStarted(pids, remaining) {
|
|
184
|
+
const result = await execa("/bin/ps", [
|
|
185
|
+
"-p",
|
|
186
|
+
pids.join(","),
|
|
187
|
+
"-o",
|
|
188
|
+
"pid=,lstart="
|
|
189
|
+
], {
|
|
190
|
+
timeout: remaining(),
|
|
191
|
+
reject: false,
|
|
192
|
+
env: { LC_ALL: "C" }
|
|
193
|
+
});
|
|
194
|
+
remaining();
|
|
195
|
+
if (result.timedOut || result.signal || result.stderr?.trim() || result.exitCode === 0 && !result.stdout.trim() || result.exitCode !== 0 && !(result.exitCode === 1 && !result.stdout.trim())) throw invalidIdentity$2();
|
|
196
|
+
const started = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const line of result.stdout.split(/\r?\n/).filter((line) => line.trim())) {
|
|
198
|
+
const match = /^\s*(\d+)\s+(\w{3}\s+\w{3}\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s*$/.exec(line);
|
|
199
|
+
const pid = Number(match?.[1]);
|
|
200
|
+
if (!match || !pids.includes(pid) || started.has(pid)) throw invalidIdentity$2();
|
|
201
|
+
started.set(pid, match[2].replace(/\s+/g, " "));
|
|
202
|
+
}
|
|
203
|
+
return started;
|
|
204
|
+
}
|
|
205
|
+
/** 批量读取内核路径,并以前后启动时间绑定 PID;进程标题变化不改变身份。 */
|
|
206
|
+
async function readDarwinManagedProcessIdentities(pids) {
|
|
207
|
+
if (pids.some((pid) => !Number.isSafeInteger(pid) || pid <= 0)) throw invalidIdentity$2();
|
|
208
|
+
const requested = [...new Set(pids)];
|
|
209
|
+
const identities = new Map(requested.map((pid) => [pid, void 0]));
|
|
210
|
+
if (!requested.length) return identities;
|
|
211
|
+
const deadline = performance.now() + inspectionTimeoutMs;
|
|
212
|
+
const remaining = () => {
|
|
213
|
+
const timeout = Math.floor(deadline - performance.now());
|
|
214
|
+
if (timeout <= 0) throw invalidIdentity$2();
|
|
215
|
+
return timeout;
|
|
216
|
+
};
|
|
217
|
+
const before = await readStarted(requested, remaining);
|
|
218
|
+
if (!before.size) return identities;
|
|
219
|
+
const paths = await readDarwinKernelPaths([...before.keys()], remaining());
|
|
220
|
+
const after = await readStarted(requested, remaining);
|
|
221
|
+
for (const pid of requested) {
|
|
222
|
+
const started = after.get(pid);
|
|
223
|
+
if (!started) continue;
|
|
224
|
+
const executable = paths.get(pid);
|
|
225
|
+
if (before.get(pid) !== started || !executable?.startsWith("/")) throw invalidIdentity$2();
|
|
226
|
+
identities.set(pid, {
|
|
227
|
+
pid,
|
|
228
|
+
started,
|
|
229
|
+
executable
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
remaining();
|
|
233
|
+
return identities;
|
|
234
|
+
}
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/devtoolsProjectOwnership/processIdentity.ts
|
|
237
|
+
const inspectionOptions$2 = {
|
|
119
238
|
timeout: 3e3,
|
|
120
239
|
reject: false,
|
|
121
240
|
windowsHide: true
|
|
122
241
|
};
|
|
123
|
-
const windowsInspectionOptions = {
|
|
124
|
-
...inspectionOptions$
|
|
242
|
+
const windowsInspectionOptions$1 = {
|
|
243
|
+
...inspectionOptions$2,
|
|
125
244
|
timeout: resolveWechatInspectionTimeout("win32")
|
|
126
245
|
};
|
|
127
|
-
function invalidIdentity(result, timeout) {
|
|
246
|
+
function invalidIdentity$1(result, timeout) {
|
|
128
247
|
const details = result ? ` Inspection: exitCode=${result.exitCode ?? "none"}, signal=${result.signal ?? "none"}, timedOut=${result.timedOut ?? false}, timeoutMs=${timeout}.` : "";
|
|
129
248
|
return /* @__PURE__ */ new Error(`Managed DevTools process identity could not be verified; no project was closed.${details}`);
|
|
130
249
|
}
|
|
131
250
|
/** PID 必须连同启动时间和可执行文件核验;进程号复用不继承所有权。 */
|
|
132
251
|
async function readManagedProcessIdentity(pid, platform = process.platform) {
|
|
133
|
-
if (!Number.isSafeInteger(pid) || pid <= 0) throw invalidIdentity();
|
|
252
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw invalidIdentity$1();
|
|
253
|
+
if (platform === "darwin") return (await readDarwinManagedProcessIdentities([pid])).get(pid);
|
|
134
254
|
if (platform === "win32") {
|
|
135
255
|
const result = await execa("powershell.exe", [
|
|
136
256
|
"-NoProfile",
|
|
137
257
|
"-NonInteractive",
|
|
138
258
|
"-Command",
|
|
139
259
|
withPowerShellUtf8Output(`$ErrorActionPreference='Stop'; Get-CimInstance Win32_Process -Filter "ProcessId=${pid}" | Select-Object ProcessId,ExecutablePath,@{Name='Started';Expression={$_.CreationDate.ToUniversalTime().ToString('o')}} | ConvertTo-Json -Compress`)
|
|
140
|
-
], windowsInspectionOptions);
|
|
141
|
-
if (result.exitCode !== 0) throw invalidIdentity(result, windowsInspectionOptions.timeout);
|
|
260
|
+
], windowsInspectionOptions$1);
|
|
261
|
+
if (result.exitCode !== 0) throw invalidIdentity$1(result, windowsInspectionOptions$1.timeout);
|
|
142
262
|
if (!result.stdout.trim()) return;
|
|
143
263
|
const value = JSON.parse(result.stdout);
|
|
144
|
-
if (!value || typeof value !== "object" || !("ProcessId" in value) || value.ProcessId !== pid || !("ExecutablePath" in value) || typeof value.ExecutablePath !== "string" || !value.ExecutablePath || !("Started" in value) || typeof value.Started !== "string" || !value.Started) throw invalidIdentity();
|
|
264
|
+
if (!value || typeof value !== "object" || !("ProcessId" in value) || value.ProcessId !== pid || !("ExecutablePath" in value) || typeof value.ExecutablePath !== "string" || !value.ExecutablePath || !("Started" in value) || typeof value.Started !== "string" || !value.Started) throw invalidIdentity$1();
|
|
145
265
|
return {
|
|
146
266
|
pid,
|
|
147
267
|
executable: value.ExecutablePath,
|
|
@@ -151,17 +271,18 @@ async function readManagedProcessIdentity(pid, platform = process.platform) {
|
|
|
151
271
|
if (platform === "linux") try {
|
|
152
272
|
const [executable, stat] = await Promise.all([fs.readlink(`/proc/${pid}/exe`), fs.readFile(`/proc/${pid}/stat`, "utf8")]);
|
|
153
273
|
const boot = await fs.readFile("/proc/sys/kernel/random/boot_id", "utf8").catch(() => {
|
|
154
|
-
throw invalidIdentity();
|
|
274
|
+
throw invalidIdentity$1();
|
|
155
275
|
});
|
|
156
276
|
const started = stat.slice(stat.lastIndexOf(")") + 2).split(/\s+/)[19];
|
|
157
|
-
if (!started || !/^\d+$/.test(started) || !boot.trim()) throw invalidIdentity();
|
|
277
|
+
if (!started || !/^\d+$/.test(started) || !boot.trim()) throw invalidIdentity$1();
|
|
158
278
|
return {
|
|
159
279
|
pid,
|
|
160
280
|
executable,
|
|
161
281
|
started: `${boot.trim()}:${started}`
|
|
162
282
|
};
|
|
163
283
|
} catch (error) {
|
|
164
|
-
|
|
284
|
+
const code = error.code;
|
|
285
|
+
if (code === "ENOENT" || code === "ESRCH") return;
|
|
165
286
|
throw error;
|
|
166
287
|
}
|
|
167
288
|
const result = await execa("ps", [
|
|
@@ -172,12 +293,12 @@ async function readManagedProcessIdentity(pid, platform = process.platform) {
|
|
|
172
293
|
"-o",
|
|
173
294
|
"comm="
|
|
174
295
|
], {
|
|
175
|
-
...inspectionOptions$
|
|
296
|
+
...inspectionOptions$2,
|
|
176
297
|
env: { LC_ALL: "C" }
|
|
177
298
|
});
|
|
178
299
|
if (result.exitCode === 1 && !result.stdout.trim()) return;
|
|
179
300
|
const match = /^(\w{3}\s+\w{3}\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(\S[^\r\n]*)$/.exec(result.stdout.trim());
|
|
180
|
-
if (result.exitCode !== 0 || !match) throw invalidIdentity();
|
|
301
|
+
if (result.exitCode !== 0 || !match) throw invalidIdentity$1();
|
|
181
302
|
return {
|
|
182
303
|
pid,
|
|
183
304
|
started: match[1].replace(/\s+/g, " "),
|
|
@@ -187,6 +308,21 @@ async function readManagedProcessIdentity(pid, platform = process.platform) {
|
|
|
187
308
|
function sameManagedProcess(first, second) {
|
|
188
309
|
return first.pid === second.pid && first.started === second.started && first.executable === second.executable;
|
|
189
310
|
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/devtoolsProjectOwnership/host.ts
|
|
313
|
+
const inspectionOptions$1 = {
|
|
314
|
+
timeout: 3e3,
|
|
315
|
+
reject: false,
|
|
316
|
+
windowsHide: true
|
|
317
|
+
};
|
|
318
|
+
const windowsInspectionOptions = {
|
|
319
|
+
...inspectionOptions$1,
|
|
320
|
+
timeout: resolveWechatInspectionTimeout("win32")
|
|
321
|
+
};
|
|
322
|
+
function invalidIdentity(result, timeout) {
|
|
323
|
+
const details = result ? ` Inspection: exitCode=${result.exitCode ?? "none"}, signal=${result.signal ?? "none"}, timedOut=${result.timedOut ?? false}, timeoutMs=${timeout}.` : "";
|
|
324
|
+
return /* @__PURE__ */ new Error(`Managed DevTools process identity could not be verified; no project was closed.${details}`);
|
|
325
|
+
}
|
|
190
326
|
async function listenerPid(port, platform = process.platform) {
|
|
191
327
|
const result = platform === "win32" ? await execa("powershell.exe", [
|
|
192
328
|
"-NoProfile",
|
|
@@ -214,7 +350,7 @@ async function inspectManagedProjectHost(target, port, platform = process.platfo
|
|
|
214
350
|
return identity;
|
|
215
351
|
}
|
|
216
352
|
async function assertManagedInstallation(target) {
|
|
217
|
-
const current = await
|
|
353
|
+
const current = await inspectWechatDevtoolsInstallation(target.cliPath);
|
|
218
354
|
if (current.installationId !== target.installationId || current.appPath !== target.appPath || current.cliPath !== target.cliPath || current.profileDir !== target.profileDir || current.version !== target.version || current.channel !== target.channel) throw new Error("Managed DevTools installation changed; no project was closed.");
|
|
219
355
|
}
|
|
220
356
|
function isManagedPortClosed(port) {
|
|
@@ -255,7 +391,8 @@ const installationExitRecoverySchema = z.object({
|
|
|
255
391
|
state: z.enum([
|
|
256
392
|
"starting",
|
|
257
393
|
"unconfirmed",
|
|
258
|
-
"failed"
|
|
394
|
+
"failed",
|
|
395
|
+
"borrowed"
|
|
259
396
|
]),
|
|
260
397
|
error: z.string().optional(),
|
|
261
398
|
updatedAt: z.string().min(1),
|
|
@@ -276,12 +413,6 @@ const installationExitRecoverySchema = z.object({
|
|
|
276
413
|
}).strict()
|
|
277
414
|
}).strict();
|
|
278
415
|
//#endregion
|
|
279
|
-
//#region src/devtoolsProjectOwnership/journal/constants.ts
|
|
280
|
-
/** 受管项目日志的环境变量名称;基础设施预检可在构建包之前读取。 */
|
|
281
|
-
const MANAGED_PROJECT_JOURNAL_ENV = "WEAPP_IDE_MANAGED_PROJECT_JOURNAL";
|
|
282
|
-
/** 仅供显式受管任务声明双项目窗口验收;不允许无限扩大并行窗口。 */
|
|
283
|
-
const MANAGED_PROJECT_MAX_WINDOWS_ENV = "WEAPP_IDE_MANAGED_PROJECT_MAX_WINDOWS";
|
|
284
|
-
//#endregion
|
|
285
416
|
//#region src/devtoolsProjectOwnership/journal/scope.ts
|
|
286
417
|
const markerName = ".ownership-scope";
|
|
287
418
|
const scopeSchema = z.object({
|
|
@@ -467,7 +598,7 @@ async function readWindowsJournalWriterIdentity() {
|
|
|
467
598
|
const timeout = resolveWechatInspectionTimeout("win32");
|
|
468
599
|
const trace = debug.enabled;
|
|
469
600
|
const command = windowsSelfIdentityCommand(process.pid);
|
|
470
|
-
const started = performance.now();
|
|
601
|
+
const started = performance$1.now();
|
|
471
602
|
const query = execa("powershell.exe", [
|
|
472
603
|
"-NoProfile",
|
|
473
604
|
"-NonInteractive",
|
|
@@ -481,14 +612,14 @@ async function readWindowsJournalWriterIdentity() {
|
|
|
481
612
|
stripFinalNewline: !trace
|
|
482
613
|
});
|
|
483
614
|
if (trace) {
|
|
484
|
-
const report = (phase) => debug("phase=%s elapsedMs=%s", phase, (performance.now() - started).toFixed(1));
|
|
615
|
+
const report = (phase) => debug("phase=%s elapsedMs=%s", phase, (performance$1.now() - started).toFixed(1));
|
|
485
616
|
report("launch");
|
|
486
617
|
query.nodeChildProcess.once("spawn", () => report("spawn"));
|
|
487
618
|
query.stderr?.on("data", observeWindowsSelfIdentityPhases(report));
|
|
488
619
|
}
|
|
489
620
|
const result = await query;
|
|
490
621
|
const stderr = trace ? stripWindowsSelfIdentityPhases(result.stderr) : result.stderr;
|
|
491
|
-
if (trace) debug("phase=settled elapsedMs=%s exitCode=%s signal=%s timedOut=%s", (performance.now() - started).toFixed(1), result.exitCode ?? "none", result.signal ?? "none", result.timedOut ?? false);
|
|
622
|
+
if (trace) debug("phase=settled elapsedMs=%s exitCode=%s signal=%s timedOut=%s", (performance$1.now() - started).toFixed(1), result.exitCode ?? "none", result.signal ?? "none", result.timedOut ?? false);
|
|
492
623
|
if (result.exitCode !== 0 || result.failed || result.timedOut || result.signal || stderr.trim()) {
|
|
493
624
|
const reason = /^WEAPP_JOURNAL_SELF_V1:error:([\w.]+):-?\d+\r?\n?$/.exec(stderr)?.[1] ?? "unconfirmed";
|
|
494
625
|
throw new Error(`Managed journal writer identity could not be verified; the journal remains unchanged. Inspection: exitCode=${result.exitCode ?? "none"}, signal=${result.signal ?? "none"}, timedOut=${result.timedOut ?? false}, timeoutMs=${timeout}, reason=${reason}.`);
|
|
@@ -513,6 +644,26 @@ function readManagedJournalWriterIdentity() {
|
|
|
513
644
|
return currentIdentity$1;
|
|
514
645
|
}
|
|
515
646
|
//#endregion
|
|
647
|
+
//#region src/devtoolsProjectOwnership/snapshot/schema.ts
|
|
648
|
+
const managedSnapshotSchema = z.object({
|
|
649
|
+
protocol: z.literal("wechat-devtools-project-snapshot-v1"),
|
|
650
|
+
sourceProjectPath: z.string().min(1),
|
|
651
|
+
token: z.string().uuid(),
|
|
652
|
+
state: z.enum([
|
|
653
|
+
"reserved",
|
|
654
|
+
"ready",
|
|
655
|
+
"removed"
|
|
656
|
+
]),
|
|
657
|
+
directoryIdentity: z.object({
|
|
658
|
+
dev: z.string().min(1),
|
|
659
|
+
ino: z.string().min(1)
|
|
660
|
+
}).strict().optional(),
|
|
661
|
+
dispatchedAt: z.string().datetime().optional(),
|
|
662
|
+
cleanupStartedAt: z.string().datetime().optional(),
|
|
663
|
+
removedAt: z.string().datetime().optional(),
|
|
664
|
+
cleanupError: z.string().optional()
|
|
665
|
+
}).strict();
|
|
666
|
+
//#endregion
|
|
516
667
|
//#region src/devtoolsProjectOwnership/windowClose/schema.ts
|
|
517
668
|
const text = z.string().min(1);
|
|
518
669
|
const cursorSchema = z.object({
|
|
@@ -603,8 +754,10 @@ const recordSchema = z.object({
|
|
|
603
754
|
releasedReason: z.enum([
|
|
604
755
|
"borrowed",
|
|
605
756
|
"project-closed",
|
|
606
|
-
"installation-exited"
|
|
757
|
+
"installation-exited",
|
|
758
|
+
"not-started"
|
|
607
759
|
]).optional(),
|
|
760
|
+
snapshot: managedSnapshotSchema.optional(),
|
|
608
761
|
installationExitRecovery: installationExitRecoverySchema.optional(),
|
|
609
762
|
error: z.string().optional()
|
|
610
763
|
}).refine((record) => record.releasedReason === "installation-exited" ? record.state === "released" && record.installationExitRecovery !== void 0 : record.installationExitRecovery === void 0, { message: "Installation exit recovery requires its distinct release reason and forensic evidence." });
|
|
@@ -619,7 +772,7 @@ async function readManagedRecord(journalPath, id) {
|
|
|
619
772
|
const file = managedRecordPath(journalPath, id);
|
|
620
773
|
if (!(await fs.lstat(file)).isFile()) throw new Error("Managed DevTools journal record is not a regular file.");
|
|
621
774
|
const record = recordSchema.parse(JSON.parse(await fs.readFile(file, "utf8")));
|
|
622
|
-
if (record.id !== id || record.journalPath !== journalPath || !path.isAbsolute(record.projectPath)) throw new Error("Managed DevTools journal identity does not match its task directory.");
|
|
775
|
+
if (record.id !== id || record.journalPath !== journalPath || !path.isAbsolute(record.projectPath) || record.snapshot && (record.projectPath !== path.join(journalPath, "snapshots", id) || !path.isAbsolute(record.snapshot.sourceProjectPath) || record.snapshot.sourceProjectPath === record.projectPath)) throw new Error("Managed DevTools journal identity does not match its task directory.");
|
|
623
776
|
return record;
|
|
624
777
|
}
|
|
625
778
|
async function writeManagedRecord(record) {
|
|
@@ -738,6 +891,110 @@ async function withManagedJournalLock(directory, run) {
|
|
|
738
891
|
});
|
|
739
892
|
}
|
|
740
893
|
//#endregion
|
|
894
|
+
//#region src/devtoolsProjectOwnership/snapshot/index.ts
|
|
895
|
+
const MARKER = ".weapp-ide-snapshot-owner";
|
|
896
|
+
function managedSnapshotPath(journalPath, id) {
|
|
897
|
+
return path.join(journalPath, "snapshots", id);
|
|
898
|
+
}
|
|
899
|
+
/** 窗口已经释放不代表本任务快照也已回收。 */
|
|
900
|
+
function isManagedWechatProjectFullyReleased(record) {
|
|
901
|
+
return record.state === "released" && (!record.snapshot || record.snapshot.state === "removed");
|
|
902
|
+
}
|
|
903
|
+
function markerValue(record) {
|
|
904
|
+
return JSON.stringify({
|
|
905
|
+
protocol: record.snapshot.protocol,
|
|
906
|
+
journalPath: record.journalPath,
|
|
907
|
+
id: record.id,
|
|
908
|
+
generation: record.generation,
|
|
909
|
+
ownerToken: record.ownerToken,
|
|
910
|
+
token: record.snapshot.token,
|
|
911
|
+
sourceProjectPath: record.snapshot.sourceProjectPath,
|
|
912
|
+
projectPath: record.projectPath,
|
|
913
|
+
target: record.target
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
async function directoryStat(directory) {
|
|
917
|
+
const stat = await fs.lstat(directory, { bigint: true });
|
|
918
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Managed DevTools snapshot directory identity changed; refusing filesystem cleanup.");
|
|
919
|
+
return {
|
|
920
|
+
dev: String(stat.dev),
|
|
921
|
+
ino: String(stat.ino)
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function assertPath(record) {
|
|
925
|
+
if (!record.snapshot || record.projectPath !== managedSnapshotPath(record.journalPath, record.id) || !path.isAbsolute(record.snapshot.sourceProjectPath) || record.snapshot.sourceProjectPath === record.projectPath) throw new Error("Managed DevTools snapshot is not bound to its exact task journal.");
|
|
926
|
+
}
|
|
927
|
+
async function verifySnapshot(record, allowIncompleteCreation = false) {
|
|
928
|
+
assertPath(record);
|
|
929
|
+
const snapshot = record.snapshot;
|
|
930
|
+
await directoryStat(record.journalPath);
|
|
931
|
+
await directoryStat(path.dirname(record.projectPath));
|
|
932
|
+
const actual = await directoryStat(record.projectPath);
|
|
933
|
+
if (await fs.realpath(record.projectPath) !== path.join(await fs.realpath(record.journalPath), "snapshots", record.id)) throw new Error("Managed DevTools snapshot escaped its canonical task journal.");
|
|
934
|
+
if (!snapshot.directoryIdentity || actual.dev !== snapshot.directoryIdentity.dev || actual.ino !== snapshot.directoryIdentity.ino) throw new Error("Managed DevTools snapshot directory was replaced; refusing cleanup.");
|
|
935
|
+
const marker = path.join(record.projectPath, MARKER);
|
|
936
|
+
try {
|
|
937
|
+
const stat = await fs.lstat(marker);
|
|
938
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || await fs.readFile(marker, "utf8") !== markerValue(record)) throw new Error("Managed DevTools snapshot creation marker changed; refusing cleanup.");
|
|
939
|
+
} catch (error) {
|
|
940
|
+
if (error.code !== "ENOENT" || !snapshot.cleanupStartedAt && !(allowIncompleteCreation && snapshot.state === "reserved" && !snapshot.dispatchedAt)) throw error;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
/** 调用方持有 journal 根锁;先登记预算,再独占创建本次 generation 的快照。 */
|
|
944
|
+
async function prepareManagedProjectSnapshot(record, populate) {
|
|
945
|
+
assertPath(record);
|
|
946
|
+
const snapshot = record.snapshot;
|
|
947
|
+
if (record.state !== "starting" || snapshot.state !== "reserved" || snapshot.directoryIdentity || snapshot.dispatchedAt) throw new Error("Managed DevTools snapshot preparation requires its unused project reservation.");
|
|
948
|
+
await directoryStat(record.journalPath);
|
|
949
|
+
const parent = path.dirname(record.projectPath);
|
|
950
|
+
await fs.mkdir(parent, { mode: 448 }).catch((error) => {
|
|
951
|
+
if (error.code !== "EEXIST") throw error;
|
|
952
|
+
});
|
|
953
|
+
await directoryStat(parent);
|
|
954
|
+
await fs.mkdir(record.projectPath, { mode: 448 });
|
|
955
|
+
snapshot.directoryIdentity = await directoryStat(record.projectPath);
|
|
956
|
+
await writeManagedRecord(record);
|
|
957
|
+
await fs.writeFile(path.join(record.projectPath, MARKER), markerValue(record), {
|
|
958
|
+
encoding: "utf8",
|
|
959
|
+
mode: 384,
|
|
960
|
+
flag: "wx"
|
|
961
|
+
});
|
|
962
|
+
await populate(record.projectPath);
|
|
963
|
+
await verifySnapshot(record);
|
|
964
|
+
snapshot.state = "ready";
|
|
965
|
+
await writeManagedRecord(record);
|
|
966
|
+
}
|
|
967
|
+
/** 窗口与端口证据由原有 owner 协议提供;这里仅回收匹配 provenance 的快照。 */
|
|
968
|
+
async function finalizeManagedProjectSnapshot(record) {
|
|
969
|
+
if (!record.snapshot || record.snapshot.state === "removed") return;
|
|
970
|
+
const snapshot = record.snapshot;
|
|
971
|
+
try {
|
|
972
|
+
assertPath(record);
|
|
973
|
+
if (record.state !== "released" || ![
|
|
974
|
+
"project-closed",
|
|
975
|
+
"installation-exited",
|
|
976
|
+
"not-started"
|
|
977
|
+
].includes(record.releasedReason ?? "") || record.releasedReason === "not-started" && (snapshot.dispatchedAt || record.openedProjectWindow !== void 0)) throw new Error("Managed DevTools snapshot is still used by an unresolved project; preserving its files.");
|
|
978
|
+
if (await fs.lstat(record.projectPath).then(() => true).catch((error) => {
|
|
979
|
+
if (error.code === "ENOENT") return false;
|
|
980
|
+
throw error;
|
|
981
|
+
})) {
|
|
982
|
+
await verifySnapshot(record, true);
|
|
983
|
+
snapshot.cleanupStartedAt ??= (/* @__PURE__ */ new Date()).toISOString();
|
|
984
|
+
await writeManagedRecord(record);
|
|
985
|
+
await fs.rm(record.projectPath, { recursive: true });
|
|
986
|
+
}
|
|
987
|
+
snapshot.state = "removed";
|
|
988
|
+
snapshot.removedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
989
|
+
delete snapshot.cleanupError;
|
|
990
|
+
await writeManagedRecord(record);
|
|
991
|
+
} catch (error) {
|
|
992
|
+
snapshot.cleanupError = error instanceof Error ? error.message : String(error);
|
|
993
|
+
await writeManagedRecord(record);
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
//#endregion
|
|
741
998
|
//#region src/devtoolsProjectOwnership/windowBudget.ts
|
|
742
999
|
function maximumOwnedWindows(options) {
|
|
743
1000
|
const selected = options.maxOwnedWindows ?? process.env["WEAPP_IDE_MANAGED_PROJECT_MAX_WINDOWS"] ?? 1;
|
|
@@ -751,7 +1008,7 @@ function sameTarget(first, second) {
|
|
|
751
1008
|
function checkManagedWindowBudget(records, options) {
|
|
752
1009
|
const maximum = maximumOwnedWindows(options);
|
|
753
1010
|
const projectPath = path.resolve(options.projectPath);
|
|
754
|
-
if (records.some((record) => record.
|
|
1011
|
+
if (records.some((record) => !isManagedWechatProjectFullyReleased(record) && (record.projectPath === projectPath || record.snapshot?.sourceProjectPath === projectPath) && sameTarget(record.target, options.target))) throw new Error("Managed DevTools project already has an active session; use its existing endpoint in connect-only mode instead of starting it again.");
|
|
755
1012
|
const owned = records.filter((record) => record.state === "owned").length;
|
|
756
1013
|
if (owned >= maximum) throw new Error(`Managed DevTools window budget exhausted (${owned}/${maximum}); close the previous owned project before another start.`);
|
|
757
1014
|
}
|
|
@@ -1331,28 +1588,28 @@ const options = {
|
|
|
1331
1588
|
reject: false,
|
|
1332
1589
|
env: { LC_ALL: "C" }
|
|
1333
1590
|
};
|
|
1334
|
-
const
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1591
|
+
const textImageDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
1592
|
+
/** 按本机 LC_ALL=C 的 lsof 表示还原字节;有歧义的控制表示不能用于负归属判断。 */
|
|
1593
|
+
function decodeTextImagePath(wire) {
|
|
1594
|
+
if (!wire.startsWith("/") || /[^\u0020-\u007E]|\^[A-GKN-Z[\]\\^_]/.test(wire)) throw new Error("Installation-exit recovery received an unverifiable executable image path.");
|
|
1595
|
+
if (!wire.includes("\\")) return wire;
|
|
1596
|
+
const bytes = Buffer.allocUnsafe(wire.length);
|
|
1597
|
+
let length = 0;
|
|
1598
|
+
for (let index = 0; index < wire.length; index++) {
|
|
1599
|
+
let byte = wire.charCodeAt(index);
|
|
1600
|
+
if (wire[index] === "\\") {
|
|
1601
|
+
const escape = wire[++index];
|
|
1602
|
+
if (escape === "\\") byte = 92;
|
|
1603
|
+
else if (escape === "x" && /^[\da-f]{2}$/.test(wire.slice(index + 1, index + 3))) {
|
|
1604
|
+
byte = Number.parseInt(wire.slice(index + 1, index + 3), 16);
|
|
1605
|
+
index += 2;
|
|
1606
|
+
if (byte < 128) throw new Error("Installation-exit recovery received an unverifiable executable image path.");
|
|
1607
|
+
} else throw new Error("Installation-exit recovery received an unverifiable executable image path.");
|
|
1608
|
+
}
|
|
1609
|
+
bytes[length++] = byte;
|
|
1610
|
+
}
|
|
1611
|
+
return textImageDecoder.decode(bytes.subarray(0, length));
|
|
1354
1612
|
}
|
|
1355
|
-
`;
|
|
1356
1613
|
/** ps 仅用于 PID、启动时间和僵尸状态,不能用可修改的 comm 代替实际可执行文件。 */
|
|
1357
1614
|
async function readDarwinProcessStates() {
|
|
1358
1615
|
const inspection = execa("/bin/ps", ["-axo", "pid=,stat=,lstart="], options);
|
|
@@ -1372,28 +1629,9 @@ async function readDarwinProcessStates() {
|
|
|
1372
1629
|
if (new Set(records.map((record) => record.pid)).size !== records.length) throw new Error("Installation-exit recovery found duplicate process identities.");
|
|
1373
1630
|
return records.filter((record) => record.pid !== inspectorPid);
|
|
1374
1631
|
}
|
|
1375
|
-
/** 系统 JXA/libproc 不可用时明确拒绝恢复,不安装工具或以进程标题降级。 */
|
|
1376
|
-
async function readDarwinKernelPaths(pids) {
|
|
1377
|
-
const result = await execa("/usr/bin/osascript", [
|
|
1378
|
-
"-l",
|
|
1379
|
-
"JavaScript",
|
|
1380
|
-
"-e",
|
|
1381
|
-
kernelPathsScript,
|
|
1382
|
-
JSON.stringify(pids)
|
|
1383
|
-
], options);
|
|
1384
|
-
if (result.exitCode !== 0 || result.stderr?.trim()) throw new Error("Installation-exit recovery cannot inspect kernel executable paths.");
|
|
1385
|
-
let parsed;
|
|
1386
|
-
try {
|
|
1387
|
-
parsed = pathSchema.parse(JSON.parse(result.stdout));
|
|
1388
|
-
} catch {
|
|
1389
|
-
throw new Error("Installation-exit recovery received incomplete kernel executable paths.");
|
|
1390
|
-
}
|
|
1391
|
-
const entries = new Map(parsed.map((entry) => [entry.pid, entry.executable]));
|
|
1392
|
-
if (entries.size !== pids.length || parsed.length !== pids.length || pids.some((pid) => !entries.has(pid))) throw new Error("Installation-exit recovery received incomplete kernel process identities.");
|
|
1393
|
-
return entries;
|
|
1394
|
-
}
|
|
1395
1632
|
/** 已删除可执行文件无法经 proc_pidpath 读取时,核验全部 program-text 映像而非猜测首项。 */
|
|
1396
|
-
async function readDarwinTextImages(pid) {
|
|
1633
|
+
async function readDarwinTextImages(pid, timeoutMs = options.timeout) {
|
|
1634
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Installation-exit recovery cannot inspect executable text images without a positive timeout.");
|
|
1397
1635
|
const result = await execa("/usr/sbin/lsof", [
|
|
1398
1636
|
"-nP",
|
|
1399
1637
|
"-a",
|
|
@@ -1402,29 +1640,25 @@ async function readDarwinTextImages(pid) {
|
|
|
1402
1640
|
"-d",
|
|
1403
1641
|
"txt",
|
|
1404
1642
|
"-F0pftn"
|
|
1405
|
-
],
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1643
|
+
], {
|
|
1644
|
+
...options,
|
|
1645
|
+
timeout: Math.min(options.timeout, timeoutMs)
|
|
1646
|
+
});
|
|
1647
|
+
if (result.exitCode !== 0 || result.timedOut || result.signal || result.stderr?.trim()) throw new Error(`Installation-exit recovery cannot inspect all executable text images for PID ${pid}.`, { cause: {
|
|
1648
|
+
exitCode: result.exitCode,
|
|
1649
|
+
stderr: result.stderr?.trim()
|
|
1650
|
+
} });
|
|
1651
|
+
const fields = result.stdout.split("\0").map((field) => field.replace(/^\r?\n/, ""));
|
|
1652
|
+
if (fields.pop() !== "" || fields.shift() !== `p${pid}` || fields.length === 0 || fields.length % 3 !== 0) throw new Error("Installation-exit recovery received incomplete executable text images.");
|
|
1409
1653
|
const paths = [];
|
|
1410
1654
|
for (let index = 0; index < fields.length; index += 3) {
|
|
1411
1655
|
if (fields[index] !== "ftxt" || fields[index + 1] !== "tREG" || !fields[index + 2]?.startsWith("n")) throw new Error("Installation-exit recovery received unverifiable executable text images.");
|
|
1412
|
-
paths.push(fields[index + 2].slice(1));
|
|
1656
|
+
paths.push(decodeTextImagePath(fields[index + 2].slice(1)));
|
|
1413
1657
|
}
|
|
1414
1658
|
return paths;
|
|
1415
1659
|
}
|
|
1416
1660
|
//#endregion
|
|
1417
1661
|
//#region src/devtoolsProjectOwnership/installationExit/processes.ts
|
|
1418
|
-
/** 已删除映像仍有内核 vnode 路径;从现存祖先解析符号链接,不把缺失文件当成已退出进程。 */
|
|
1419
|
-
async function canonicalImagePath(file) {
|
|
1420
|
-
if (!path.isAbsolute(file) || /[\r\n\0]/.test(file)) throw new Error("Installation-exit recovery received an unverifiable executable image path.");
|
|
1421
|
-
try {
|
|
1422
|
-
return await fs.realpath(file);
|
|
1423
|
-
} catch (error) {
|
|
1424
|
-
if (error.code !== "ENOENT" || path.dirname(file) === file) throw new Error("Installation-exit recovery cannot resolve an executable image path.", { cause: error });
|
|
1425
|
-
return path.join(await canonicalImagePath(path.dirname(file)), path.basename(file));
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
1662
|
/** 仅支持已验证的 macOS 全安装清单;未知平台、权限错误或不完整映像均拒绝推断退出。 */
|
|
1429
1663
|
async function inspectExitedWechatInstallation(target, platform = process.platform) {
|
|
1430
1664
|
if (platform !== "darwin") throw new Error("Installation-exit recovery requires the verified macOS process inventory.");
|
|
@@ -1453,11 +1687,24 @@ async function inspectExitedWechatInstallation(target, platform = process.platfo
|
|
|
1453
1687
|
continue;
|
|
1454
1688
|
}
|
|
1455
1689
|
const executable = kernelPaths.get(entry.pid);
|
|
1456
|
-
|
|
1690
|
+
let images;
|
|
1691
|
+
try {
|
|
1692
|
+
images = executable ? [executable] : await readDarwinTextImages(entry.pid);
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
const latest = await readDarwinProcessStates().then((states) => states.find((process) => process.pid === entry.pid), (confirmationError) => {
|
|
1695
|
+
throw new AggregateError([error, confirmationError], "Installation-exit recovery cannot independently confirm process exit after failed image inspection.");
|
|
1696
|
+
});
|
|
1697
|
+
if (!latest) {
|
|
1698
|
+
exitedProcessCount++;
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
if (latest.started !== entry.started) throw new Error("Installation-exit recovery process identity changed during inspection.", { cause: error });
|
|
1702
|
+
throw error;
|
|
1703
|
+
}
|
|
1457
1704
|
if (!images.length) throw new Error("Installation-exit recovery received no verifiable executable text images.");
|
|
1458
1705
|
for (const image of images) {
|
|
1459
|
-
|
|
1460
|
-
const relative = path.relative(installationRoot,
|
|
1706
|
+
if (!path.isAbsolute(image) || /[\r\n\0]/.test(image)) throw new Error("Installation-exit recovery received an unverifiable executable image path.");
|
|
1707
|
+
const relative = path.relative(installationRoot, image);
|
|
1461
1708
|
if (!relative || !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`)) throw new Error("The selected WeChat DevTools installation still has a live process; no ownership was recovered.");
|
|
1462
1709
|
}
|
|
1463
1710
|
if (executable) kernelPathProcessCount++;
|
|
@@ -1497,7 +1744,7 @@ async function fingerprint(record) {
|
|
|
1497
1744
|
return createHash("sha256").update(await fs.readFile(file)).digest("hex");
|
|
1498
1745
|
}
|
|
1499
1746
|
function canRecoverAfterExit(record) {
|
|
1500
|
-
return record.state === "starting" || record.state === "unconfirmed" || record.state === "failed" && record.openedProjectWindow === true && record.host !== void 0 && record.port !== void 0 && record.error !== void 0;
|
|
1747
|
+
return record.state === "starting" || record.state === "unconfirmed" || record.state === "borrowed" && record.snapshot !== void 0 && record.openedProjectWindow === false || record.state === "failed" && record.openedProjectWindow === true && record.host !== void 0 && record.port !== void 0 && record.error !== void 0;
|
|
1501
1748
|
}
|
|
1502
1749
|
async function recoverAfterExit(options) {
|
|
1503
1750
|
const { target, recoveryScope } = options;
|
|
@@ -1512,9 +1759,12 @@ async function recoverAfterExit(options) {
|
|
|
1512
1759
|
if (new Set(records.map((record) => record.id)).size !== records.length) throw new Error("Installation-exit recovery found duplicate record identities.");
|
|
1513
1760
|
if (records.some((record) => !isDeepStrictEqual(record.target, target))) throw new Error("Installation-exit recovery cannot include another installation or profile.");
|
|
1514
1761
|
if (records.some((record) => record.state !== "released" && !canRecoverAfterExit(record))) throw new Error("Installation-exit recovery refuses confirmed or unresolved project-close ownership.");
|
|
1515
|
-
const
|
|
1762
|
+
const scopeRecords = await readManagedWechatProjectRecords(journalPath);
|
|
1763
|
+
const candidates = scopeRecords.filter(canRecoverAfterExit);
|
|
1764
|
+
const pendingSnapshots = scopeRecords.filter((record) => record.state === "released" && !isManagedWechatProjectFullyReleased(record));
|
|
1516
1765
|
if (!candidates.length) {
|
|
1517
1766
|
await assertMachineE2ELeaseRecoveryScope(recoveryScope);
|
|
1767
|
+
for (const record of pendingSnapshots) await finalizeManagedProjectSnapshot(record);
|
|
1518
1768
|
return { recoveredRecordIds: [] };
|
|
1519
1769
|
}
|
|
1520
1770
|
const before = new Map(await Promise.all(records.map(async (record) => [record.id, await fingerprint(record)])));
|
|
@@ -1544,7 +1794,7 @@ async function recoverAfterExit(options) {
|
|
|
1544
1794
|
await assertMachineE2ELeaseRecoveryScope(recoveryScope);
|
|
1545
1795
|
if (await fingerprint(record) !== before.get(record.id)) throw new Error("Installation-exit recovery journal bytes changed before recording recovery.");
|
|
1546
1796
|
const previousState = record.state;
|
|
1547
|
-
if (previousState !== "starting" && previousState !== "unconfirmed" && previousState !== "failed") throw new Error("Installation-exit recovery cannot replace a confirmed ownership state.");
|
|
1797
|
+
if (previousState !== "starting" && previousState !== "unconfirmed" && previousState !== "failed" && previousState !== "borrowed") throw new Error("Installation-exit recovery cannot replace a confirmed ownership state.");
|
|
1548
1798
|
record.installationExitRecovery = {
|
|
1549
1799
|
protocol: "wechat-devtools-installation-exit-v1",
|
|
1550
1800
|
recoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1566,8 +1816,10 @@ async function recoverAfterExit(options) {
|
|
|
1566
1816
|
record.state = "released";
|
|
1567
1817
|
record.releasedReason = "installation-exited";
|
|
1568
1818
|
await writeManagedRecord(record);
|
|
1819
|
+
await finalizeManagedProjectSnapshot(record);
|
|
1569
1820
|
recoveredRecordIds.push(record.id);
|
|
1570
1821
|
}
|
|
1822
|
+
for (const record of pendingSnapshots) await finalizeManagedProjectSnapshot(record);
|
|
1571
1823
|
return { recoveredRecordIds };
|
|
1572
1824
|
});
|
|
1573
1825
|
}
|
|
@@ -1587,7 +1839,7 @@ async function release(record, reason) {
|
|
|
1587
1839
|
delete record.error;
|
|
1588
1840
|
await writeManagedRecord(record);
|
|
1589
1841
|
}
|
|
1590
|
-
async function
|
|
1842
|
+
async function closeWindowRecord(record) {
|
|
1591
1843
|
if (record.state === "released") return;
|
|
1592
1844
|
if (record.state === "borrowed" && record.openedProjectWindow === false) {
|
|
1593
1845
|
await release(record, "borrowed");
|
|
@@ -1631,6 +1883,17 @@ async function closeRecord(record) {
|
|
|
1631
1883
|
throw error;
|
|
1632
1884
|
}
|
|
1633
1885
|
}
|
|
1886
|
+
async function closeRecord(record) {
|
|
1887
|
+
if (isManagedWechatProjectFullyReleased(record)) return;
|
|
1888
|
+
if (record.state !== "released") {
|
|
1889
|
+
if (record.snapshot && !record.snapshot.dispatchedAt && record.openedProjectWindow === void 0) await release(record, "not-started");
|
|
1890
|
+
else {
|
|
1891
|
+
if (record.snapshot && record.openedProjectWindow === false) throw new Error("Managed DevTools snapshot received a borrowed window receipt; preserving files until installation exit is proven.");
|
|
1892
|
+
await closeWindowRecord(record);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
await finalizeManagedProjectSnapshot(record);
|
|
1896
|
+
}
|
|
1634
1897
|
/** 窗口销毁与连接断开分别验证;失败保留登记供后续继续观察。 */
|
|
1635
1898
|
function closeManagedWechatProject(options) {
|
|
1636
1899
|
const journalPath = resolveManagedJournal(options.journalPath);
|
|
@@ -1648,6 +1911,7 @@ async function beginManagedWechatProject(options) {
|
|
|
1648
1911
|
const journalPath = resolveManagedJournal(options.journalPath);
|
|
1649
1912
|
if (!journalPath) return;
|
|
1650
1913
|
const id = randomUUID();
|
|
1914
|
+
const projectPath = options.snapshotProject ? managedSnapshotPath(journalPath, id) : path.resolve(options.projectPath);
|
|
1651
1915
|
await withManagedJournalLock(journalPath, async (scopeRoot) => {
|
|
1652
1916
|
const previous = await readManagedWechatProjectRecords(scopeRoot);
|
|
1653
1917
|
if (previous.some((record) => [
|
|
@@ -1655,7 +1919,7 @@ async function beginManagedWechatProject(options) {
|
|
|
1655
1919
|
"unconfirmed",
|
|
1656
1920
|
"closing",
|
|
1657
1921
|
"failed"
|
|
1658
|
-
].includes(record.state))) throw new Error("Managed DevTools journal contains unresolved ownership or cleanup; refusing another project start.");
|
|
1922
|
+
].includes(record.state) || record.state === "released" && !isManagedWechatProjectFullyReleased(record) || record.state === "borrowed" && record.snapshot?.state !== void 0 && record.snapshot.state !== "removed")) throw new Error("Managed DevTools journal contains unresolved ownership or cleanup; refusing another project start.");
|
|
1659
1923
|
checkManagedWindowBudget(previous, options);
|
|
1660
1924
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1661
1925
|
await writeManagedRecord({
|
|
@@ -1666,7 +1930,13 @@ async function beginManagedWechatProject(options) {
|
|
|
1666
1930
|
ownerToken: managedProcessToken,
|
|
1667
1931
|
ownerPid: process.pid,
|
|
1668
1932
|
target: { ...options.target },
|
|
1669
|
-
projectPath
|
|
1933
|
+
projectPath,
|
|
1934
|
+
...options.snapshotProject ? { snapshot: {
|
|
1935
|
+
protocol: "wechat-devtools-project-snapshot-v1",
|
|
1936
|
+
sourceProjectPath: path.resolve(options.projectPath),
|
|
1937
|
+
token: randomUUID(),
|
|
1938
|
+
state: "reserved"
|
|
1939
|
+
} } : {},
|
|
1670
1940
|
state: "starting",
|
|
1671
1941
|
port: options.port,
|
|
1672
1942
|
createdAt: now,
|
|
@@ -1676,10 +1946,36 @@ async function beginManagedWechatProject(options) {
|
|
|
1676
1946
|
return {
|
|
1677
1947
|
id,
|
|
1678
1948
|
journalPath,
|
|
1949
|
+
projectPath,
|
|
1950
|
+
prepareSnapshot: async (populate) => {
|
|
1951
|
+
await withManagedJournalLock(journalPath, async () => {
|
|
1952
|
+
const record = await readManagedRecord(journalPath, id);
|
|
1953
|
+
try {
|
|
1954
|
+
await prepareManagedProjectSnapshot(record, populate);
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
if (record.snapshot) {
|
|
1957
|
+
record.snapshot.cleanupError = describe(error);
|
|
1958
|
+
await writeManagedRecord(record);
|
|
1959
|
+
}
|
|
1960
|
+
throw error;
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
},
|
|
1964
|
+
markLaunchDispatched: async () => {
|
|
1965
|
+
await withManagedJournalLock(journalPath, async () => {
|
|
1966
|
+
const record = await readManagedRecord(journalPath, id);
|
|
1967
|
+
if (record.snapshot) {
|
|
1968
|
+
if (record.state !== "starting" || record.snapshot.state !== "ready" || record.snapshot.dispatchedAt) throw new Error("Managed DevTools snapshot is not ready for its first start dispatch.");
|
|
1969
|
+
record.snapshot.dispatchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1970
|
+
await writeManagedRecord(record);
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
},
|
|
1679
1974
|
confirm: async (result) => {
|
|
1680
1975
|
await withManagedJournalLock(journalPath, async () => {
|
|
1681
1976
|
const record = await readManagedRecord(journalPath, id);
|
|
1682
1977
|
if (record.state !== "starting" && record.state !== "unconfirmed") throw new Error("Managed DevTools start receipt was already recorded.");
|
|
1978
|
+
if (record.snapshot && (!record.snapshot.dispatchedAt || record.snapshot.state !== "ready")) throw new Error("Managed DevTools snapshot start receipt has no prepared dispatch intent.");
|
|
1683
1979
|
if (typeof result.openedProjectWindow !== "boolean" || !Number.isInteger(result.port) || result.port <= 0 || result.port > 65535 || record.port !== void 0 && record.port !== result.port) throw new Error("Managed DevTools start receipt does not match its requested port.");
|
|
1684
1980
|
record.openedProjectWindow = result.openedProjectWindow;
|
|
1685
1981
|
record.port = result.port;
|
|
@@ -1723,4 +2019,4 @@ async function cleanupManagedWechatProjects(options = {}) {
|
|
|
1723
2019
|
if (errors.length) throw new AggregateError(errors, "Managed DevTools project cleanup remains incomplete; stop the acceptance lane.");
|
|
1724
2020
|
}
|
|
1725
2021
|
//#endregion
|
|
1726
|
-
export {
|
|
2022
|
+
export { isManagedWechatProjectFullyReleased as a, resolveManagedJournal as c, assertManagedInstallation as d, inspectManagedProjectHost as f, recoverManagedWechatProjectsAfterInstallationExit as i, withManagedJournalLock as l, startWechatIdeAgent as m, cleanupManagedWechatProjects as n, readManagedRecord as o, sameManagedProcess as p, closeManagedWechatProject as r, readManagedWechatProjectRecords as s, beginManagedWechatProject as t, writeManagedRecord as u };
|