weapp-ide-cli 6.2.0 → 6.4.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 +61 -3
- package/dist/automator-BqhUtMJp.js +1 -0
- package/dist/{automator-CRpB6_pA.js → automator-Esp3BjoT.js} +391 -152
- package/dist/automator-session-Ba27VW0_.js +184 -0
- package/dist/{sessionStore-zdzy2UFM.js → automatorProject-DPU-pEF0.js} +497 -240
- package/dist/{cli-ChMQqi5w.js → cli-Dd1RJ5PH.js} +396 -115
- package/dist/cli.js +3 -4
- package/dist/{commands-C1PF1B4P.js → commands-5entj0Rh.js} +5 -6
- package/dist/commands-DR-8MVFm.js +1 -0
- package/dist/{devtoolsProjectOwnership-yDD6h0dO.js → devtoolsProjectOwnership-C4jj8msU.js} +478 -155
- package/dist/index.d.ts +111 -137
- package/dist/index.js +8 -8
- package/dist/{managed-Sq8y8FzP.js → managed-hSCZXTon.js} +109 -59
- package/dist/{run-mcp-Dl_1TH1x.js → run-mcp-CbmskSgr.js} +14 -7
- package/dist/run-mcp-D1w7-oPp.js +1 -0
- package/package.json +12 -12
- 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,17 +1,17 @@
|
|
|
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-DPU-pEF0.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";
|
|
14
|
-
|
|
13
|
+
import { performance as performance$1 } from "node:perf_hooks";
|
|
14
|
+
// src/cli/agentStart.ts
|
|
15
15
|
/** 提取独立的完整 JSON 对象,普通日志和嵌套对象不能替代命令响应。 */
|
|
16
16
|
function readResponseObjects(stdout) {
|
|
17
17
|
const objects = [];
|
|
@@ -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,
|
|
@@ -113,35 +116,179 @@ async function startWechatIdeAgent(options) {
|
|
|
113
116
|
}
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
+
// src/devtoolsProjectOwnership/installationExit/darwinKernelPaths.ts
|
|
120
|
+
const inspectionTimeoutMs$1 = 1e4;
|
|
121
|
+
const kernelPathsScript = `
|
|
122
|
+
ObjC.import('Foundation');
|
|
123
|
+
ObjC.bindFunction('malloc', ['void *', ['unsigned long']]);
|
|
124
|
+
ObjC.bindFunction('free', ['void', ['void *']]);
|
|
125
|
+
ObjC.bindFunction('proc_pidpath', ['int', ['int', 'void *', 'uint32_t']]);
|
|
126
|
+
ObjC.bindFunction('strchr', ['char *', ['void *', 'int']]);
|
|
127
|
+
function run(argv) {
|
|
128
|
+
var pids = JSON.parse(argv[0]);
|
|
129
|
+
var buffer = $.malloc(4096);
|
|
130
|
+
if (!buffer) throw new Error('Cannot allocate process path buffer.');
|
|
131
|
+
try {
|
|
132
|
+
return JSON.stringify(pids.map(function(pid) {
|
|
133
|
+
var count = $.proc_pidpath(pid, buffer, 4096);
|
|
134
|
+
return { pid: pid, executable: count > 0 ? $.strchr(buffer, 47) : null };
|
|
135
|
+
}));
|
|
136
|
+
} finally { $.free(buffer); }
|
|
137
|
+
}
|
|
138
|
+
`;
|
|
139
|
+
/** 在 JSON 边界严格核验内核响应,不让进程身份查询依赖 schema 运行时。 */
|
|
140
|
+
function parseKernelPaths(stdout) {
|
|
141
|
+
const parsed = JSON.parse(stdout);
|
|
142
|
+
if (!Array.isArray(parsed)) throw new TypeError("Invalid kernel process paths.");
|
|
143
|
+
return parsed.map((entry) => {
|
|
144
|
+
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.");
|
|
145
|
+
return {
|
|
146
|
+
pid: entry.pid,
|
|
147
|
+
executable: entry.executable
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** 系统 JXA/libproc 不可用时明确拒绝核验,不安装工具或以进程标题降级。 */
|
|
152
|
+
async function readDarwinKernelPaths(pids, timeoutMs = inspectionTimeoutMs$1) {
|
|
153
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Installation-exit recovery cannot inspect kernel executable paths.");
|
|
154
|
+
const result = await execa("/usr/bin/osascript", [
|
|
155
|
+
"-l",
|
|
156
|
+
"JavaScript",
|
|
157
|
+
"-e",
|
|
158
|
+
kernelPathsScript,
|
|
159
|
+
JSON.stringify(pids)
|
|
160
|
+
], {
|
|
161
|
+
timeout: Math.min(inspectionTimeoutMs$1, timeoutMs),
|
|
162
|
+
reject: false,
|
|
163
|
+
env: { LC_ALL: "C" }
|
|
164
|
+
});
|
|
165
|
+
if (result.exitCode !== 0 || result.timedOut || result.signal || result.stderr?.trim()) throw new Error("Installation-exit recovery cannot inspect kernel executable paths.");
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
parsed = parseKernelPaths(result.stdout);
|
|
169
|
+
} catch {
|
|
170
|
+
throw new Error("Installation-exit recovery received incomplete kernel executable paths.");
|
|
171
|
+
}
|
|
172
|
+
const entries = new Map(parsed.map((entry) => [entry.pid, entry.executable]));
|
|
173
|
+
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.");
|
|
174
|
+
return entries;
|
|
175
|
+
}
|
|
176
|
+
// src/devtoolsProjectOwnership/darwinProcessIdentity.ts
|
|
177
|
+
const inspectionTimeoutMs = 3e3;
|
|
178
|
+
function invalidIdentity$2(stage, reason, details = {}, cause) {
|
|
179
|
+
return new Error(`Managed DevTools process identity could not be verified; no project was closed. ${JSON.stringify({
|
|
180
|
+
stage,
|
|
181
|
+
reason,
|
|
182
|
+
...details
|
|
183
|
+
})}`, { cause });
|
|
184
|
+
}
|
|
185
|
+
async function readStarted(pids, remaining, stage) {
|
|
186
|
+
const result = await execa("/bin/ps", [
|
|
187
|
+
"-p",
|
|
188
|
+
pids.join(","),
|
|
189
|
+
"-o",
|
|
190
|
+
"pid=,stat=,lstart="
|
|
191
|
+
], {
|
|
192
|
+
timeout: remaining(),
|
|
193
|
+
reject: false,
|
|
194
|
+
env: { LC_ALL: "C" }
|
|
195
|
+
});
|
|
196
|
+
remaining();
|
|
197
|
+
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(stage, "birth-query-failed", { requestedCount: pids.length });
|
|
198
|
+
const started = /* @__PURE__ */ new Map();
|
|
199
|
+
for (const line of result.stdout.split(/\r?\n/).filter((line) => line.trim())) {
|
|
200
|
+
const match = /^\s*(\d+)\s+(\S+)\s+(\w{3}\s+\w{3}\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s*$/.exec(line);
|
|
201
|
+
const pid = Number(match?.[1]);
|
|
202
|
+
if (!match || !pids.includes(pid) || started.has(pid)) throw invalidIdentity$2(stage, "invalid-birth-inventory", { requestedCount: pids.length });
|
|
203
|
+
started.set(pid, {
|
|
204
|
+
started: match[3].replace(/\s+/g, " "),
|
|
205
|
+
zombie: match[2].startsWith("Z")
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return started;
|
|
209
|
+
}
|
|
210
|
+
/** 批量读取内核路径,并以前后启动时间绑定 PID;进程标题变化不改变身份。 */
|
|
211
|
+
async function readDarwinManagedProcessIdentities(pids) {
|
|
212
|
+
if (pids.some((pid) => !Number.isSafeInteger(pid) || pid <= 0)) throw invalidIdentity$2("request", "invalid-pid");
|
|
213
|
+
const requested = [...new Set(pids)];
|
|
214
|
+
const identities = new Map(requested.map((pid) => [pid, void 0]));
|
|
215
|
+
if (!requested.length) return identities;
|
|
216
|
+
const deadline = performance.now() + inspectionTimeoutMs;
|
|
217
|
+
let stage = "before";
|
|
218
|
+
const remaining = () => {
|
|
219
|
+
const timeout = Math.floor(deadline - performance.now());
|
|
220
|
+
if (timeout <= 0) throw invalidIdentity$2(stage, "deadline-exceeded", { requestedCount: requested.length });
|
|
221
|
+
return timeout;
|
|
222
|
+
};
|
|
223
|
+
const before = await readStarted(requested, remaining, "before");
|
|
224
|
+
if (!before.size) return identities;
|
|
225
|
+
stage = "kernel";
|
|
226
|
+
const paths = await readDarwinKernelPaths([...before.keys()], remaining()).catch((cause) => {
|
|
227
|
+
throw invalidIdentity$2("kernel", "kernel-query-failed", { requestedCount: before.size }, cause);
|
|
228
|
+
});
|
|
229
|
+
stage = "after";
|
|
230
|
+
const after = await readStarted(requested, remaining, "after");
|
|
231
|
+
for (const pid of requested) {
|
|
232
|
+
const current = after.get(pid);
|
|
233
|
+
if (!current) continue;
|
|
234
|
+
const executable = paths.get(pid);
|
|
235
|
+
const previous = before.get(pid);
|
|
236
|
+
if (previous?.started !== current.started) throw invalidIdentity$2("after", "birth-changed", {
|
|
237
|
+
pid,
|
|
238
|
+
beforePresent: before.has(pid),
|
|
239
|
+
afterPresent: true
|
|
240
|
+
});
|
|
241
|
+
if (previous.zombie && current.zombie) continue;
|
|
242
|
+
if (previous.zombie !== current.zombie) throw invalidIdentity$2("after", "terminal-state-changed", {
|
|
243
|
+
pid,
|
|
244
|
+
beforeZombie: previous.zombie,
|
|
245
|
+
afterZombie: current.zombie
|
|
246
|
+
});
|
|
247
|
+
if (!executable?.startsWith("/")) throw invalidIdentity$2("after", "kernel-path-unavailable", {
|
|
248
|
+
pid,
|
|
249
|
+
beforePresent: true,
|
|
250
|
+
afterPresent: true,
|
|
251
|
+
kernelPath: executable ? "non-absolute" : "empty"
|
|
252
|
+
});
|
|
253
|
+
identities.set(pid, {
|
|
254
|
+
pid,
|
|
255
|
+
started: current.started,
|
|
256
|
+
executable
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
stage = "complete";
|
|
260
|
+
remaining();
|
|
261
|
+
return identities;
|
|
262
|
+
}
|
|
263
|
+
// src/devtoolsProjectOwnership/processIdentity.ts
|
|
264
|
+
const inspectionOptions$2 = {
|
|
119
265
|
timeout: 3e3,
|
|
120
266
|
reject: false,
|
|
121
267
|
windowsHide: true
|
|
122
268
|
};
|
|
123
|
-
const windowsInspectionOptions = {
|
|
124
|
-
...inspectionOptions$
|
|
269
|
+
const windowsInspectionOptions$1 = {
|
|
270
|
+
...inspectionOptions$2,
|
|
125
271
|
timeout: resolveWechatInspectionTimeout("win32")
|
|
126
272
|
};
|
|
127
|
-
function invalidIdentity(result, timeout) {
|
|
273
|
+
function invalidIdentity$1(result, timeout) {
|
|
128
274
|
const details = result ? ` Inspection: exitCode=${result.exitCode ?? "none"}, signal=${result.signal ?? "none"}, timedOut=${result.timedOut ?? false}, timeoutMs=${timeout}.` : "";
|
|
129
275
|
return /* @__PURE__ */ new Error(`Managed DevTools process identity could not be verified; no project was closed.${details}`);
|
|
130
276
|
}
|
|
131
277
|
/** PID 必须连同启动时间和可执行文件核验;进程号复用不继承所有权。 */
|
|
132
278
|
async function readManagedProcessIdentity(pid, platform = process.platform) {
|
|
133
|
-
if (!Number.isSafeInteger(pid) || pid <= 0) throw invalidIdentity();
|
|
279
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw invalidIdentity$1();
|
|
280
|
+
if (platform === "darwin") return (await readDarwinManagedProcessIdentities([pid])).get(pid);
|
|
134
281
|
if (platform === "win32") {
|
|
135
282
|
const result = await execa("powershell.exe", [
|
|
136
283
|
"-NoProfile",
|
|
137
284
|
"-NonInteractive",
|
|
138
285
|
"-Command",
|
|
139
286
|
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);
|
|
287
|
+
], windowsInspectionOptions$1);
|
|
288
|
+
if (result.exitCode !== 0) throw invalidIdentity$1(result, windowsInspectionOptions$1.timeout);
|
|
142
289
|
if (!result.stdout.trim()) return;
|
|
143
290
|
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();
|
|
291
|
+
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
292
|
return {
|
|
146
293
|
pid,
|
|
147
294
|
executable: value.ExecutablePath,
|
|
@@ -151,17 +298,18 @@ async function readManagedProcessIdentity(pid, platform = process.platform) {
|
|
|
151
298
|
if (platform === "linux") try {
|
|
152
299
|
const [executable, stat] = await Promise.all([fs.readlink(`/proc/${pid}/exe`), fs.readFile(`/proc/${pid}/stat`, "utf8")]);
|
|
153
300
|
const boot = await fs.readFile("/proc/sys/kernel/random/boot_id", "utf8").catch(() => {
|
|
154
|
-
throw invalidIdentity();
|
|
301
|
+
throw invalidIdentity$1();
|
|
155
302
|
});
|
|
156
303
|
const started = stat.slice(stat.lastIndexOf(")") + 2).split(/\s+/)[19];
|
|
157
|
-
if (!started || !/^\d+$/.test(started) || !boot.trim()) throw invalidIdentity();
|
|
304
|
+
if (!started || !/^\d+$/.test(started) || !boot.trim()) throw invalidIdentity$1();
|
|
158
305
|
return {
|
|
159
306
|
pid,
|
|
160
307
|
executable,
|
|
161
308
|
started: `${boot.trim()}:${started}`
|
|
162
309
|
};
|
|
163
310
|
} catch (error) {
|
|
164
|
-
|
|
311
|
+
const code = error.code;
|
|
312
|
+
if (code === "ENOENT" || code === "ESRCH") return;
|
|
165
313
|
throw error;
|
|
166
314
|
}
|
|
167
315
|
const result = await execa("ps", [
|
|
@@ -172,12 +320,12 @@ async function readManagedProcessIdentity(pid, platform = process.platform) {
|
|
|
172
320
|
"-o",
|
|
173
321
|
"comm="
|
|
174
322
|
], {
|
|
175
|
-
...inspectionOptions$
|
|
323
|
+
...inspectionOptions$2,
|
|
176
324
|
env: { LC_ALL: "C" }
|
|
177
325
|
});
|
|
178
326
|
if (result.exitCode === 1 && !result.stdout.trim()) return;
|
|
179
327
|
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();
|
|
328
|
+
if (result.exitCode !== 0 || !match) throw invalidIdentity$1();
|
|
181
329
|
return {
|
|
182
330
|
pid,
|
|
183
331
|
started: match[1].replace(/\s+/g, " "),
|
|
@@ -187,6 +335,20 @@ async function readManagedProcessIdentity(pid, platform = process.platform) {
|
|
|
187
335
|
function sameManagedProcess(first, second) {
|
|
188
336
|
return first.pid === second.pid && first.started === second.started && first.executable === second.executable;
|
|
189
337
|
}
|
|
338
|
+
// src/devtoolsProjectOwnership/host.ts
|
|
339
|
+
const inspectionOptions$1 = {
|
|
340
|
+
timeout: 3e3,
|
|
341
|
+
reject: false,
|
|
342
|
+
windowsHide: true
|
|
343
|
+
};
|
|
344
|
+
const windowsInspectionOptions = {
|
|
345
|
+
...inspectionOptions$1,
|
|
346
|
+
timeout: resolveWechatInspectionTimeout("win32")
|
|
347
|
+
};
|
|
348
|
+
function invalidIdentity(result, timeout) {
|
|
349
|
+
const details = result ? ` Inspection: exitCode=${result.exitCode ?? "none"}, signal=${result.signal ?? "none"}, timedOut=${result.timedOut ?? false}, timeoutMs=${timeout}.` : "";
|
|
350
|
+
return /* @__PURE__ */ new Error(`Managed DevTools process identity could not be verified; no project was closed.${details}`);
|
|
351
|
+
}
|
|
190
352
|
async function listenerPid(port, platform = process.platform) {
|
|
191
353
|
const result = platform === "win32" ? await execa("powershell.exe", [
|
|
192
354
|
"-NoProfile",
|
|
@@ -214,7 +376,7 @@ async function inspectManagedProjectHost(target, port, platform = process.platfo
|
|
|
214
376
|
return identity;
|
|
215
377
|
}
|
|
216
378
|
async function assertManagedInstallation(target) {
|
|
217
|
-
const current = await
|
|
379
|
+
const current = await inspectWechatDevtoolsInstallation(target.cliPath);
|
|
218
380
|
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
381
|
}
|
|
220
382
|
function isManagedPortClosed(port) {
|
|
@@ -241,8 +403,7 @@ async function waitForManagedPortClosed(port, timeoutMs = 1e4) {
|
|
|
241
403
|
} while (Date.now() < deadline);
|
|
242
404
|
throw new Error("Managed DevTools project close returned but its automator port remained open.");
|
|
243
405
|
}
|
|
244
|
-
|
|
245
|
-
//#region src/devtoolsProjectOwnership/installationExit/schema.ts
|
|
406
|
+
// src/devtoolsProjectOwnership/installationExit/schema.ts
|
|
246
407
|
const installationExitRecoverySchema = z.object({
|
|
247
408
|
protocol: z.literal("wechat-devtools-installation-exit-v1"),
|
|
248
409
|
recoveredAt: z.string().datetime(),
|
|
@@ -255,7 +416,8 @@ const installationExitRecoverySchema = z.object({
|
|
|
255
416
|
state: z.enum([
|
|
256
417
|
"starting",
|
|
257
418
|
"unconfirmed",
|
|
258
|
-
"failed"
|
|
419
|
+
"failed",
|
|
420
|
+
"borrowed"
|
|
259
421
|
]),
|
|
260
422
|
error: z.string().optional(),
|
|
261
423
|
updatedAt: z.string().min(1),
|
|
@@ -275,14 +437,7 @@ const installationExitRecoverySchema = z.object({
|
|
|
275
437
|
selectedProcessCount: z.literal(0)
|
|
276
438
|
}).strict()
|
|
277
439
|
}).strict();
|
|
278
|
-
|
|
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
|
-
//#region src/devtoolsProjectOwnership/journal/scope.ts
|
|
440
|
+
// src/devtoolsProjectOwnership/journal/scope.ts
|
|
286
441
|
const markerName = ".ownership-scope";
|
|
287
442
|
const scopeSchema = z.object({
|
|
288
443
|
schemaVersion: z.literal(1),
|
|
@@ -335,8 +490,7 @@ async function resolveManagedJournalScope(directory) {
|
|
|
335
490
|
if (path.resolve(rootPath, relative) !== currentPath) throw new Error("Managed DevTools journal path changed from its registered task children.");
|
|
336
491
|
return scope;
|
|
337
492
|
}
|
|
338
|
-
|
|
339
|
-
//#region src/devtoolsProjectOwnership/journal/windowsSelfIdentity/wire.ts
|
|
493
|
+
// src/devtoolsProjectOwnership/journal/windowsSelfIdentity/wire.ts
|
|
340
494
|
const SELF_IDENTITY_WIRE = "WEAPP_JOURNAL_SELF_V1";
|
|
341
495
|
const epochTicks = 621355968000000000n;
|
|
342
496
|
const maximumTicks = 3155378975999999999n;
|
|
@@ -370,8 +524,7 @@ function parseWindowsSelfIdentity(stdout, expectedPid) {
|
|
|
370
524
|
started
|
|
371
525
|
};
|
|
372
526
|
}
|
|
373
|
-
|
|
374
|
-
//#region src/devtoolsProjectOwnership/journal/windowsSelfIdentity/command.ts
|
|
527
|
+
// src/devtoolsProjectOwnership/journal/windowsSelfIdentity/command.ts
|
|
375
528
|
/** 当前 writer 的 PID 由 Node 进程提供;脚本只读查询,不执行任何进程或 IDE 清理。 */
|
|
376
529
|
function windowsSelfIdentityCommand(pid) {
|
|
377
530
|
if (!Number.isSafeInteger(pid) || pid <= 0) throw new TypeError("A positive writer PID is required.");
|
|
@@ -411,8 +564,7 @@ finally {
|
|
|
411
564
|
exit $writerExitCode
|
|
412
565
|
`;
|
|
413
566
|
}
|
|
414
|
-
|
|
415
|
-
//#region src/devtoolsProjectOwnership/journal/windowsSelfIdentity/trace.ts
|
|
567
|
+
// src/devtoolsProjectOwnership/journal/windowsSelfIdentity/trace.ts
|
|
416
568
|
const marker = `${SELF_IDENTITY_WIRE}:phase:`;
|
|
417
569
|
const statements = /* @__PURE__ */ new Map([
|
|
418
570
|
["$writerFirst=[System.Diagnostics.Process]", "lookup-before"],
|
|
@@ -459,15 +611,14 @@ function observeWindowsSelfIdentityPhases(report) {
|
|
|
459
611
|
function stripWindowsSelfIdentityPhases(stderr) {
|
|
460
612
|
return stderr.replace(phaseLines, "");
|
|
461
613
|
}
|
|
462
|
-
|
|
463
|
-
//#region src/devtoolsProjectOwnership/journal/windowsSelfIdentity/index.ts
|
|
614
|
+
// src/devtoolsProjectOwnership/journal/windowsSelfIdentity/index.ts
|
|
464
615
|
const debug = debuglog("weapp-ide-journal-writer");
|
|
465
616
|
/** 仅核验当前 Node 写入进程;不能用于任意宿主、旧锁存活判断或进程清理。 */
|
|
466
617
|
async function readWindowsJournalWriterIdentity() {
|
|
467
618
|
const timeout = resolveWechatInspectionTimeout("win32");
|
|
468
619
|
const trace = debug.enabled;
|
|
469
620
|
const command = windowsSelfIdentityCommand(process.pid);
|
|
470
|
-
const started = performance.now();
|
|
621
|
+
const started = performance$1.now();
|
|
471
622
|
const query = execa("powershell.exe", [
|
|
472
623
|
"-NoProfile",
|
|
473
624
|
"-NonInteractive",
|
|
@@ -481,22 +632,21 @@ async function readWindowsJournalWriterIdentity() {
|
|
|
481
632
|
stripFinalNewline: !trace
|
|
482
633
|
});
|
|
483
634
|
if (trace) {
|
|
484
|
-
const report = (phase) => debug("phase=%s elapsedMs=%s", phase, (performance.now() - started).toFixed(1));
|
|
635
|
+
const report = (phase) => debug("phase=%s elapsedMs=%s", phase, (performance$1.now() - started).toFixed(1));
|
|
485
636
|
report("launch");
|
|
486
637
|
query.nodeChildProcess.once("spawn", () => report("spawn"));
|
|
487
638
|
query.stderr?.on("data", observeWindowsSelfIdentityPhases(report));
|
|
488
639
|
}
|
|
489
640
|
const result = await query;
|
|
490
641
|
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);
|
|
642
|
+
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
643
|
if (result.exitCode !== 0 || result.failed || result.timedOut || result.signal || stderr.trim()) {
|
|
493
644
|
const reason = /^WEAPP_JOURNAL_SELF_V1:error:([\w.]+):-?\d+\r?\n?$/.exec(stderr)?.[1] ?? "unconfirmed";
|
|
494
645
|
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}.`);
|
|
495
646
|
}
|
|
496
647
|
return parseWindowsSelfIdentity(result.stdout, process.pid);
|
|
497
648
|
}
|
|
498
|
-
|
|
499
|
-
//#region src/devtoolsProjectOwnership/journal/writerIdentity.ts
|
|
649
|
+
// src/devtoolsProjectOwnership/journal/writerIdentity.ts
|
|
500
650
|
let currentIdentity$1;
|
|
501
651
|
/** 共享在途与成功身份;失败仍拒绝当前调用,仅允许下一次显式操作重新核验。 */
|
|
502
652
|
function readManagedJournalWriterIdentity() {
|
|
@@ -512,8 +662,26 @@ function readManagedJournalWriterIdentity() {
|
|
|
512
662
|
}
|
|
513
663
|
return currentIdentity$1;
|
|
514
664
|
}
|
|
515
|
-
|
|
516
|
-
|
|
665
|
+
// src/devtoolsProjectOwnership/snapshot/schema.ts
|
|
666
|
+
const managedSnapshotSchema = z.object({
|
|
667
|
+
protocol: z.literal("wechat-devtools-project-snapshot-v1"),
|
|
668
|
+
sourceProjectPath: z.string().min(1),
|
|
669
|
+
token: z.string().uuid(),
|
|
670
|
+
state: z.enum([
|
|
671
|
+
"reserved",
|
|
672
|
+
"ready",
|
|
673
|
+
"removed"
|
|
674
|
+
]),
|
|
675
|
+
directoryIdentity: z.object({
|
|
676
|
+
dev: z.string().min(1),
|
|
677
|
+
ino: z.string().min(1)
|
|
678
|
+
}).strict().optional(),
|
|
679
|
+
dispatchedAt: z.string().datetime().optional(),
|
|
680
|
+
cleanupStartedAt: z.string().datetime().optional(),
|
|
681
|
+
removedAt: z.string().datetime().optional(),
|
|
682
|
+
cleanupError: z.string().optional()
|
|
683
|
+
}).strict();
|
|
684
|
+
// src/devtoolsProjectOwnership/windowClose/schema.ts
|
|
517
685
|
const text = z.string().min(1);
|
|
518
686
|
const cursorSchema = z.object({
|
|
519
687
|
name: z.string().regex(/^[^/\\]+\.log$/),
|
|
@@ -554,8 +722,7 @@ const managedWindowCloseSchema = z.object({
|
|
|
554
722
|
cursors: z.array(cursorSchema).min(1)
|
|
555
723
|
}).optional()
|
|
556
724
|
});
|
|
557
|
-
|
|
558
|
-
//#region src/devtoolsProjectOwnership/journal.ts
|
|
725
|
+
// src/devtoolsProjectOwnership/journal.ts
|
|
559
726
|
const managedProcessToken = randomUUID();
|
|
560
727
|
const idSchema = z.string().uuid();
|
|
561
728
|
const hostSchema = z.object({
|
|
@@ -603,8 +770,10 @@ const recordSchema = z.object({
|
|
|
603
770
|
releasedReason: z.enum([
|
|
604
771
|
"borrowed",
|
|
605
772
|
"project-closed",
|
|
606
|
-
"installation-exited"
|
|
773
|
+
"installation-exited",
|
|
774
|
+
"not-started"
|
|
607
775
|
]).optional(),
|
|
776
|
+
snapshot: managedSnapshotSchema.optional(),
|
|
608
777
|
installationExitRecovery: installationExitRecoverySchema.optional(),
|
|
609
778
|
error: z.string().optional()
|
|
610
779
|
}).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 +788,7 @@ async function readManagedRecord(journalPath, id) {
|
|
|
619
788
|
const file = managedRecordPath(journalPath, id);
|
|
620
789
|
if (!(await fs.lstat(file)).isFile()) throw new Error("Managed DevTools journal record is not a regular file.");
|
|
621
790
|
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.");
|
|
791
|
+
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
792
|
return record;
|
|
624
793
|
}
|
|
625
794
|
async function writeManagedRecord(record) {
|
|
@@ -637,16 +806,15 @@ async function writeManagedRecord(record) {
|
|
|
637
806
|
await fs.rm(temporary, { force: true });
|
|
638
807
|
}
|
|
639
808
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const directory = resolveManagedJournal(journalPath);
|
|
643
|
-
if (!directory) return [];
|
|
809
|
+
async function readManagedJournalDirectory(directory, parentScope) {
|
|
810
|
+
if (parentScope && !(await fs.lstat(directory)).isDirectory()) throw new Error("Managed DevTools journal scope must use regular directories.");
|
|
644
811
|
const entries = await fs.readdir(directory, { withFileTypes: true }).catch((error) => {
|
|
645
|
-
if (error.code === "ENOENT") return;
|
|
812
|
+
if (error.code === "ENOENT" && !parentScope) return;
|
|
646
813
|
throw error;
|
|
647
814
|
});
|
|
648
815
|
if (!entries) return [];
|
|
649
816
|
const scope = await resolveManagedJournalScope(directory);
|
|
817
|
+
if (parentScope && (parentScope.scopeId !== void 0 || scope.scopeId !== void 0) && (parentScope.scopeId !== scope.scopeId || parentScope.rootPath !== scope.rootPath)) throw new Error("Managed DevTools journal scope changed inside its registered children; refusing another task tree.");
|
|
650
818
|
const records = [];
|
|
651
819
|
for (const entry of entries) if (entry.name.endsWith(".json")) records.push(await readManagedRecord(directory, entry.name.slice(0, -5)));
|
|
652
820
|
else if (entry.name === "children") {
|
|
@@ -655,13 +823,17 @@ async function readManagedWechatProjectRecords(journalPath) {
|
|
|
655
823
|
for (const child of children) {
|
|
656
824
|
if (!child.isDirectory()) throw new Error("Managed DevTools child journal is not a regular directory.");
|
|
657
825
|
const childPath = path.join(directory, "children", child.name);
|
|
658
|
-
|
|
659
|
-
if ((scope.scopeId !== void 0 || childScope.scopeId !== void 0) && (scope.scopeId !== childScope.scopeId || scope.rootPath !== childScope.rootPath)) throw new Error("Managed DevTools journal scope changed inside its registered children; refusing another task tree.");
|
|
660
|
-
records.push(...await readManagedWechatProjectRecords(childPath));
|
|
826
|
+
records.push(...await readManagedJournalDirectory(childPath, scope));
|
|
661
827
|
}
|
|
662
828
|
}
|
|
663
829
|
return records;
|
|
664
830
|
}
|
|
831
|
+
/** 仅递归当前任务明确传递的 children;不扫描用户缓存,不跟随符号链接。 */
|
|
832
|
+
async function readManagedWechatProjectRecords(journalPath) {
|
|
833
|
+
const directory = resolveManagedJournal(journalPath);
|
|
834
|
+
if (!directory) return [];
|
|
835
|
+
return await readManagedJournalDirectory(directory);
|
|
836
|
+
}
|
|
665
837
|
const lockOwnerSchema = z.object({
|
|
666
838
|
token: idSchema,
|
|
667
839
|
identity: hostSchema
|
|
@@ -737,8 +909,110 @@ async function withManagedJournalLock(directory, run) {
|
|
|
737
909
|
return await run(scope.rootPath);
|
|
738
910
|
});
|
|
739
911
|
}
|
|
740
|
-
|
|
741
|
-
|
|
912
|
+
// src/devtoolsProjectOwnership/snapshot/index.ts
|
|
913
|
+
const MARKER = ".weapp-ide-snapshot-owner";
|
|
914
|
+
function managedSnapshotPath(journalPath, id) {
|
|
915
|
+
return path.join(journalPath, "snapshots", id);
|
|
916
|
+
}
|
|
917
|
+
/** 窗口已经释放不代表本任务快照也已回收。 */
|
|
918
|
+
function isManagedWechatProjectFullyReleased(record) {
|
|
919
|
+
return record.state === "released" && (!record.snapshot || record.snapshot.state === "removed");
|
|
920
|
+
}
|
|
921
|
+
function markerValue(record) {
|
|
922
|
+
return JSON.stringify({
|
|
923
|
+
protocol: record.snapshot.protocol,
|
|
924
|
+
journalPath: record.journalPath,
|
|
925
|
+
id: record.id,
|
|
926
|
+
generation: record.generation,
|
|
927
|
+
ownerToken: record.ownerToken,
|
|
928
|
+
token: record.snapshot.token,
|
|
929
|
+
sourceProjectPath: record.snapshot.sourceProjectPath,
|
|
930
|
+
projectPath: record.projectPath,
|
|
931
|
+
target: record.target
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
async function directoryStat(directory) {
|
|
935
|
+
const stat = await fs.lstat(directory, { bigint: true });
|
|
936
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Managed DevTools snapshot directory identity changed; refusing filesystem cleanup.");
|
|
937
|
+
return {
|
|
938
|
+
dev: String(stat.dev),
|
|
939
|
+
ino: String(stat.ino)
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
function assertPath(record) {
|
|
943
|
+
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.");
|
|
944
|
+
}
|
|
945
|
+
async function verifySnapshot(record, allowIncompleteCreation = false) {
|
|
946
|
+
assertPath(record);
|
|
947
|
+
const snapshot = record.snapshot;
|
|
948
|
+
await directoryStat(record.journalPath);
|
|
949
|
+
await directoryStat(path.dirname(record.projectPath));
|
|
950
|
+
const actual = await directoryStat(record.projectPath);
|
|
951
|
+
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.");
|
|
952
|
+
if (!snapshot.directoryIdentity || actual.dev !== snapshot.directoryIdentity.dev || actual.ino !== snapshot.directoryIdentity.ino) throw new Error("Managed DevTools snapshot directory was replaced; refusing cleanup.");
|
|
953
|
+
const marker = path.join(record.projectPath, MARKER);
|
|
954
|
+
try {
|
|
955
|
+
const stat = await fs.lstat(marker);
|
|
956
|
+
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.");
|
|
957
|
+
} catch (error) {
|
|
958
|
+
if (error.code !== "ENOENT" || !snapshot.cleanupStartedAt && !(allowIncompleteCreation && snapshot.state === "reserved" && !snapshot.dispatchedAt)) throw error;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
/** 调用方持有 journal 根锁;先登记预算,再独占创建本次 generation 的快照。 */
|
|
962
|
+
async function prepareManagedProjectSnapshot(record, populate) {
|
|
963
|
+
assertPath(record);
|
|
964
|
+
const snapshot = record.snapshot;
|
|
965
|
+
if (record.state !== "starting" || snapshot.state !== "reserved" || snapshot.directoryIdentity || snapshot.dispatchedAt) throw new Error("Managed DevTools snapshot preparation requires its unused project reservation.");
|
|
966
|
+
await directoryStat(record.journalPath);
|
|
967
|
+
const parent = path.dirname(record.projectPath);
|
|
968
|
+
await fs.mkdir(parent, { mode: 448 }).catch((error) => {
|
|
969
|
+
if (error.code !== "EEXIST") throw error;
|
|
970
|
+
});
|
|
971
|
+
await directoryStat(parent);
|
|
972
|
+
await fs.mkdir(record.projectPath, { mode: 448 });
|
|
973
|
+
snapshot.directoryIdentity = await directoryStat(record.projectPath);
|
|
974
|
+
await writeManagedRecord(record);
|
|
975
|
+
await fs.writeFile(path.join(record.projectPath, MARKER), markerValue(record), {
|
|
976
|
+
encoding: "utf8",
|
|
977
|
+
mode: 384,
|
|
978
|
+
flag: "wx"
|
|
979
|
+
});
|
|
980
|
+
await populate(record.projectPath);
|
|
981
|
+
await verifySnapshot(record);
|
|
982
|
+
snapshot.state = "ready";
|
|
983
|
+
await writeManagedRecord(record);
|
|
984
|
+
}
|
|
985
|
+
/** 窗口与端口证据由原有 owner 协议提供;这里仅回收匹配 provenance 的快照。 */
|
|
986
|
+
async function finalizeManagedProjectSnapshot(record) {
|
|
987
|
+
if (!record.snapshot || record.snapshot.state === "removed") return;
|
|
988
|
+
const snapshot = record.snapshot;
|
|
989
|
+
try {
|
|
990
|
+
assertPath(record);
|
|
991
|
+
if (record.state !== "released" || ![
|
|
992
|
+
"project-closed",
|
|
993
|
+
"installation-exited",
|
|
994
|
+
"not-started"
|
|
995
|
+
].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.");
|
|
996
|
+
if (await fs.lstat(record.projectPath).then(() => true).catch((error) => {
|
|
997
|
+
if (error.code === "ENOENT") return false;
|
|
998
|
+
throw error;
|
|
999
|
+
})) {
|
|
1000
|
+
await verifySnapshot(record, true);
|
|
1001
|
+
snapshot.cleanupStartedAt ??= (/* @__PURE__ */ new Date()).toISOString();
|
|
1002
|
+
await writeManagedRecord(record);
|
|
1003
|
+
await fs.rm(record.projectPath, { recursive: true });
|
|
1004
|
+
}
|
|
1005
|
+
snapshot.state = "removed";
|
|
1006
|
+
snapshot.removedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1007
|
+
delete snapshot.cleanupError;
|
|
1008
|
+
await writeManagedRecord(record);
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
snapshot.cleanupError = error instanceof Error ? error.message : String(error);
|
|
1011
|
+
await writeManagedRecord(record);
|
|
1012
|
+
throw error;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
// src/devtoolsProjectOwnership/windowBudget.ts
|
|
742
1016
|
function maximumOwnedWindows(options) {
|
|
743
1017
|
const selected = options.maxOwnedWindows ?? process.env["WEAPP_IDE_MANAGED_PROJECT_MAX_WINDOWS"] ?? 1;
|
|
744
1018
|
if (selected !== 1 && selected !== 2 && selected !== "1" && selected !== "2") throw new Error("Managed DevTools maximum owned windows must be 1 or 2.");
|
|
@@ -751,12 +1025,11 @@ function sameTarget(first, second) {
|
|
|
751
1025
|
function checkManagedWindowBudget(records, options) {
|
|
752
1026
|
const maximum = maximumOwnedWindows(options);
|
|
753
1027
|
const projectPath = path.resolve(options.projectPath);
|
|
754
|
-
if (records.some((record) => record.
|
|
1028
|
+
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
1029
|
const owned = records.filter((record) => record.state === "owned").length;
|
|
756
1030
|
if (owned >= maximum) throw new Error(`Managed DevTools window budget exhausted (${owned}/${maximum}); close the previous owned project before another start.`);
|
|
757
1031
|
}
|
|
758
|
-
|
|
759
|
-
//#region src/devtoolsProjectOwnership/windowClose/logCursor.ts
|
|
1032
|
+
// src/devtoolsProjectOwnership/windowClose/logCursor.ts
|
|
760
1033
|
const readLimit = 2097152;
|
|
761
1034
|
const scanLimit = 33554432;
|
|
762
1035
|
const scanChunkSize = 65536;
|
|
@@ -960,8 +1233,7 @@ async function consumeFreshLogLines(directory, cursors, consume) {
|
|
|
960
1233
|
await Promise.all(streams.map(({ file }) => file.close()));
|
|
961
1234
|
}
|
|
962
1235
|
}
|
|
963
|
-
|
|
964
|
-
//#region src/devtoolsProjectOwnership/windowClose/activeLog.ts
|
|
1236
|
+
// src/devtoolsProjectOwnership/windowClose/activeLog.ts
|
|
965
1237
|
const inspectionOptions = {
|
|
966
1238
|
timeout: 3e3,
|
|
967
1239
|
reject: false,
|
|
@@ -1120,8 +1392,7 @@ async function captureActiveMainLogCursors(record, directory, platform = process
|
|
|
1120
1392
|
host: main
|
|
1121
1393
|
};
|
|
1122
1394
|
}
|
|
1123
|
-
|
|
1124
|
-
//#region src/devtoolsProjectOwnership/windowClose/protocol.ts
|
|
1395
|
+
// src/devtoolsProjectOwnership/windowClose/protocol.ts
|
|
1125
1396
|
function decodeString(value) {
|
|
1126
1397
|
const quote = value[0];
|
|
1127
1398
|
if (!quote || ![
|
|
@@ -1262,8 +1533,7 @@ function consumeWindowCloseTrace(evidence, projectPath, input) {
|
|
|
1262
1533
|
}
|
|
1263
1534
|
}
|
|
1264
1535
|
}
|
|
1265
|
-
|
|
1266
|
-
//#region src/devtoolsProjectOwnership/windowClose.ts
|
|
1536
|
+
// src/devtoolsProjectOwnership/windowClose.ts
|
|
1267
1537
|
/** 只捕获本次显式安装的日志,必须先于关闭命令持久化。 */
|
|
1268
1538
|
async function captureManagedWindowClose(record) {
|
|
1269
1539
|
if (!record.target.version) throw new Error("Managed DevTools window-close evidence requires the selected product version.");
|
|
@@ -1324,41 +1594,44 @@ async function waitForManagedWindowClosed(record, persist, options = {}) {
|
|
|
1324
1594
|
} while (true);
|
|
1325
1595
|
throw new Error("Managed DevTools project close has no complete native-window destruction evidence; stop the acceptance lane.");
|
|
1326
1596
|
}
|
|
1327
|
-
|
|
1328
|
-
//#region src/devtoolsProjectOwnership/installationExit/darwin.ts
|
|
1597
|
+
// src/devtoolsProjectOwnership/installationExit/darwin.ts
|
|
1329
1598
|
const options = {
|
|
1330
1599
|
timeout: 1e4,
|
|
1331
1600
|
reject: false,
|
|
1332
1601
|
env: { LC_ALL: "C" }
|
|
1333
1602
|
};
|
|
1334
|
-
const
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1603
|
+
const textImageDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
1604
|
+
/** 按本机 LC_ALL=C 的 lsof 表示还原字节;有歧义的控制表示不能用于负归属判断。 */
|
|
1605
|
+
function decodeTextImagePath(wire) {
|
|
1606
|
+
if (!wire.startsWith("/") || /[^\u0020-\u007E]|\^[A-GKN-Z[\]\\^_]/.test(wire)) throw new Error("Installation-exit recovery received an unverifiable executable image path.");
|
|
1607
|
+
if (!wire.includes("\\")) return wire;
|
|
1608
|
+
const bytes = Buffer.allocUnsafe(wire.length);
|
|
1609
|
+
let length = 0;
|
|
1610
|
+
for (let index = 0; index < wire.length; index++) {
|
|
1611
|
+
let byte = wire.charCodeAt(index);
|
|
1612
|
+
if (wire[index] === "\\") {
|
|
1613
|
+
const escape = wire[++index];
|
|
1614
|
+
if (escape === "\\") byte = 92;
|
|
1615
|
+
else if (escape === "x" && /^[\da-f]{2}$/.test(wire.slice(index + 1, index + 3))) {
|
|
1616
|
+
byte = Number.parseInt(wire.slice(index + 1, index + 3), 16);
|
|
1617
|
+
index += 2;
|
|
1618
|
+
if (byte < 128) throw new Error("Installation-exit recovery received an unverifiable executable image path.");
|
|
1619
|
+
} else throw new Error("Installation-exit recovery received an unverifiable executable image path.");
|
|
1620
|
+
}
|
|
1621
|
+
bytes[length++] = byte;
|
|
1622
|
+
}
|
|
1623
|
+
return textImageDecoder.decode(bytes.subarray(0, length));
|
|
1354
1624
|
}
|
|
1355
|
-
`;
|
|
1356
1625
|
/** ps 仅用于 PID、启动时间和僵尸状态,不能用可修改的 comm 代替实际可执行文件。 */
|
|
1357
|
-
async function readDarwinProcessStates() {
|
|
1358
|
-
|
|
1626
|
+
async function readDarwinProcessStates(timeoutMs = options.timeout) {
|
|
1627
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Installation-exit recovery cannot inspect the process inventory without a positive timeout.");
|
|
1628
|
+
const inspection = execa("/bin/ps", ["-axo", "pid=,stat=,lstart="], {
|
|
1629
|
+
...options,
|
|
1630
|
+
timeout: Math.min(options.timeout, timeoutMs)
|
|
1631
|
+
});
|
|
1359
1632
|
const inspectorPid = inspection.pid;
|
|
1360
1633
|
const result = await inspection;
|
|
1361
|
-
if (result.exitCode !== 0 || result.stderr?.trim() || !result.stdout.trim()) throw new Error("Installation-exit recovery could not read the complete process inventory.");
|
|
1634
|
+
if (result.exitCode !== 0 || result.timedOut || result.signal || result.stderr?.trim() || !result.stdout.trim()) throw new Error("Installation-exit recovery could not read the complete process inventory.");
|
|
1362
1635
|
const records = result.stdout.trim().split(/\r?\n/).map((line) => {
|
|
1363
1636
|
const match = /^\s*(\d+)\s+(\S+)\s+(\w{3}\s+\w{3}\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s*$/.exec(line);
|
|
1364
1637
|
const pid = Number(match?.[1]);
|
|
@@ -1372,28 +1645,9 @@ async function readDarwinProcessStates() {
|
|
|
1372
1645
|
if (new Set(records.map((record) => record.pid)).size !== records.length) throw new Error("Installation-exit recovery found duplicate process identities.");
|
|
1373
1646
|
return records.filter((record) => record.pid !== inspectorPid);
|
|
1374
1647
|
}
|
|
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
1648
|
/** 已删除可执行文件无法经 proc_pidpath 读取时,核验全部 program-text 映像而非猜测首项。 */
|
|
1396
|
-
async function readDarwinTextImages(pid) {
|
|
1649
|
+
async function readDarwinTextImages(pid, timeoutMs = options.timeout) {
|
|
1650
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Installation-exit recovery cannot inspect executable text images without a positive timeout.");
|
|
1397
1651
|
const result = await execa("/usr/sbin/lsof", [
|
|
1398
1652
|
"-nP",
|
|
1399
1653
|
"-a",
|
|
@@ -1402,45 +1656,54 @@ async function readDarwinTextImages(pid) {
|
|
|
1402
1656
|
"-d",
|
|
1403
1657
|
"txt",
|
|
1404
1658
|
"-F0pftn"
|
|
1405
|
-
],
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1659
|
+
], {
|
|
1660
|
+
...options,
|
|
1661
|
+
timeout: Math.min(options.timeout, timeoutMs)
|
|
1662
|
+
});
|
|
1663
|
+
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: {
|
|
1664
|
+
exitCode: result.exitCode,
|
|
1665
|
+
stderr: result.stderr?.trim()
|
|
1666
|
+
} });
|
|
1667
|
+
const fields = result.stdout.split("\0").map((field) => field.replace(/^\r?\n/, ""));
|
|
1668
|
+
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
1669
|
const paths = [];
|
|
1410
1670
|
for (let index = 0; index < fields.length; index += 3) {
|
|
1411
1671
|
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));
|
|
1672
|
+
paths.push(decodeTextImagePath(fields[index + 2].slice(1)));
|
|
1413
1673
|
}
|
|
1414
1674
|
return paths;
|
|
1415
1675
|
}
|
|
1416
|
-
|
|
1417
|
-
//#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
|
-
}
|
|
1676
|
+
// src/devtoolsProjectOwnership/installationExit/processes.ts
|
|
1428
1677
|
/** 仅支持已验证的 macOS 全安装清单;未知平台、权限错误或不完整映像均拒绝推断退出。 */
|
|
1429
|
-
async function inspectExitedWechatInstallation(target, platform = process.platform) {
|
|
1678
|
+
async function inspectExitedWechatInstallation(target, platform = process.platform, options = {}) {
|
|
1430
1679
|
if (platform !== "darwin") throw new Error("Installation-exit recovery requires the verified macOS process inventory.");
|
|
1680
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
1681
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("Installation-exit recovery requires a positive process inventory timeout.");
|
|
1682
|
+
const deadline = performance.now() + Math.min(timeoutMs, 3e4);
|
|
1683
|
+
const remaining = () => {
|
|
1684
|
+
const timeout = Math.floor(deadline - performance.now());
|
|
1685
|
+
if (timeout <= 0) throw new Error("Installation-exit recovery process inventory deadline expired; continuing new process activity or incomplete inspection prevents proving exit.");
|
|
1686
|
+
return timeout;
|
|
1687
|
+
};
|
|
1688
|
+
const readStates = async () => {
|
|
1689
|
+
const states = await readDarwinProcessStates(remaining());
|
|
1690
|
+
remaining();
|
|
1691
|
+
return states;
|
|
1692
|
+
};
|
|
1431
1693
|
const selectedRoot = resolveWechatDevtoolsInstallationRoot(target, platform);
|
|
1432
1694
|
if (!selectedRoot || !path.isAbsolute(selectedRoot) || !selectedRoot.endsWith(".app") || !target.appPath.includes("/Contents/")) throw new Error("Installation-exit recovery cannot resolve the selected application bundle.");
|
|
1433
1695
|
const installationRoot = await fs.realpath(selectedRoot);
|
|
1434
|
-
let pending = await
|
|
1696
|
+
let pending = await readStates();
|
|
1435
1697
|
const verified = /* @__PURE__ */ new Map();
|
|
1436
1698
|
let kernelPathProcessCount = 0;
|
|
1437
1699
|
let textImageProcessCount = 0;
|
|
1438
1700
|
let exitedProcessCount = 0;
|
|
1439
1701
|
let zombieProcessCount = 0;
|
|
1440
|
-
|
|
1441
|
-
const kernelPaths = await readDarwinKernelPaths(pending.map((entry) => entry.pid));
|
|
1442
|
-
const after = new Map((await
|
|
1702
|
+
while (true) {
|
|
1703
|
+
const kernelPaths = await readDarwinKernelPaths(pending.map((entry) => entry.pid), remaining());
|
|
1704
|
+
const after = new Map((await readStates()).map((entry) => [entry.pid, entry]));
|
|
1443
1705
|
for (const entry of pending) {
|
|
1706
|
+
remaining();
|
|
1444
1707
|
const current = after.get(entry.pid);
|
|
1445
1708
|
if (!current) {
|
|
1446
1709
|
exitedProcessCount++;
|
|
@@ -1453,11 +1716,25 @@ async function inspectExitedWechatInstallation(target, platform = process.platfo
|
|
|
1453
1716
|
continue;
|
|
1454
1717
|
}
|
|
1455
1718
|
const executable = kernelPaths.get(entry.pid);
|
|
1456
|
-
|
|
1719
|
+
let images;
|
|
1720
|
+
try {
|
|
1721
|
+
images = executable ? [executable] : await readDarwinTextImages(entry.pid, remaining());
|
|
1722
|
+
} catch (error) {
|
|
1723
|
+
const latest = await readStates().then((states) => states.find((process) => process.pid === entry.pid), (confirmationError) => {
|
|
1724
|
+
throw new AggregateError([error, confirmationError], "Installation-exit recovery cannot independently confirm process exit after failed image inspection.");
|
|
1725
|
+
});
|
|
1726
|
+
if (!latest) {
|
|
1727
|
+
exitedProcessCount++;
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
if (latest.started !== entry.started) throw new Error("Installation-exit recovery process identity changed during inspection.", { cause: error });
|
|
1731
|
+
throw error;
|
|
1732
|
+
}
|
|
1733
|
+
remaining();
|
|
1457
1734
|
if (!images.length) throw new Error("Installation-exit recovery received no verifiable executable text images.");
|
|
1458
1735
|
for (const image of images) {
|
|
1459
|
-
|
|
1460
|
-
const relative = path.relative(installationRoot,
|
|
1736
|
+
if (!path.isAbsolute(image) || /[\r\n\0]/.test(image)) throw new Error("Installation-exit recovery received an unverifiable executable image path.");
|
|
1737
|
+
const relative = path.relative(installationRoot, image);
|
|
1461
1738
|
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
1739
|
}
|
|
1463
1740
|
if (executable) kernelPathProcessCount++;
|
|
@@ -1467,7 +1744,7 @@ async function inspectExitedWechatInstallation(target, platform = process.platfo
|
|
|
1467
1744
|
zombie: false
|
|
1468
1745
|
});
|
|
1469
1746
|
}
|
|
1470
|
-
const final = await
|
|
1747
|
+
const final = await readStates();
|
|
1471
1748
|
for (const entry of final) {
|
|
1472
1749
|
const previous = verified.get(entry.pid);
|
|
1473
1750
|
if (previous && (previous.started !== entry.started || previous.zombie && !entry.zombie)) throw new Error("Installation-exit recovery process identity changed during inspection.");
|
|
@@ -1475,7 +1752,7 @@ async function inspectExitedWechatInstallation(target, platform = process.platfo
|
|
|
1475
1752
|
pending = final.filter((entry) => !verified.has(entry.pid));
|
|
1476
1753
|
if (!pending.length) break;
|
|
1477
1754
|
}
|
|
1478
|
-
|
|
1755
|
+
remaining();
|
|
1479
1756
|
return {
|
|
1480
1757
|
platform,
|
|
1481
1758
|
installationRoot,
|
|
@@ -1488,8 +1765,7 @@ async function inspectExitedWechatInstallation(target, platform = process.platfo
|
|
|
1488
1765
|
selectedProcessCount: 0
|
|
1489
1766
|
};
|
|
1490
1767
|
}
|
|
1491
|
-
|
|
1492
|
-
//#region src/devtoolsProjectOwnership/installationExit/index.ts
|
|
1768
|
+
// src/devtoolsProjectOwnership/installationExit/index.ts
|
|
1493
1769
|
async function fingerprint(record) {
|
|
1494
1770
|
const file = managedRecordPath(record.journalPath, record.id);
|
|
1495
1771
|
const stat = await fs.lstat(file);
|
|
@@ -1497,7 +1773,7 @@ async function fingerprint(record) {
|
|
|
1497
1773
|
return createHash("sha256").update(await fs.readFile(file)).digest("hex");
|
|
1498
1774
|
}
|
|
1499
1775
|
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;
|
|
1776
|
+
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
1777
|
}
|
|
1502
1778
|
async function recoverAfterExit(options) {
|
|
1503
1779
|
const { target, recoveryScope } = options;
|
|
@@ -1512,9 +1788,12 @@ async function recoverAfterExit(options) {
|
|
|
1512
1788
|
if (new Set(records.map((record) => record.id)).size !== records.length) throw new Error("Installation-exit recovery found duplicate record identities.");
|
|
1513
1789
|
if (records.some((record) => !isDeepStrictEqual(record.target, target))) throw new Error("Installation-exit recovery cannot include another installation or profile.");
|
|
1514
1790
|
if (records.some((record) => record.state !== "released" && !canRecoverAfterExit(record))) throw new Error("Installation-exit recovery refuses confirmed or unresolved project-close ownership.");
|
|
1515
|
-
const
|
|
1791
|
+
const scopeRecords = await readManagedWechatProjectRecords(journalPath);
|
|
1792
|
+
const candidates = scopeRecords.filter(canRecoverAfterExit);
|
|
1793
|
+
const pendingSnapshots = scopeRecords.filter((record) => record.state === "released" && !isManagedWechatProjectFullyReleased(record));
|
|
1516
1794
|
if (!candidates.length) {
|
|
1517
1795
|
await assertMachineE2ELeaseRecoveryScope(recoveryScope);
|
|
1796
|
+
for (const record of pendingSnapshots) await finalizeManagedProjectSnapshot(record);
|
|
1518
1797
|
return { recoveredRecordIds: [] };
|
|
1519
1798
|
}
|
|
1520
1799
|
const before = new Map(await Promise.all(records.map(async (record) => [record.id, await fingerprint(record)])));
|
|
@@ -1544,7 +1823,7 @@ async function recoverAfterExit(options) {
|
|
|
1544
1823
|
await assertMachineE2ELeaseRecoveryScope(recoveryScope);
|
|
1545
1824
|
if (await fingerprint(record) !== before.get(record.id)) throw new Error("Installation-exit recovery journal bytes changed before recording recovery.");
|
|
1546
1825
|
const previousState = record.state;
|
|
1547
|
-
if (previousState !== "starting" && previousState !== "unconfirmed" && previousState !== "failed") throw new Error("Installation-exit recovery cannot replace a confirmed ownership state.");
|
|
1826
|
+
if (previousState !== "starting" && previousState !== "unconfirmed" && previousState !== "failed" && previousState !== "borrowed") throw new Error("Installation-exit recovery cannot replace a confirmed ownership state.");
|
|
1548
1827
|
record.installationExitRecovery = {
|
|
1549
1828
|
protocol: "wechat-devtools-installation-exit-v1",
|
|
1550
1829
|
recoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1566,8 +1845,10 @@ async function recoverAfterExit(options) {
|
|
|
1566
1845
|
record.state = "released";
|
|
1567
1846
|
record.releasedReason = "installation-exited";
|
|
1568
1847
|
await writeManagedRecord(record);
|
|
1848
|
+
await finalizeManagedProjectSnapshot(record);
|
|
1569
1849
|
recoveredRecordIds.push(record.id);
|
|
1570
1850
|
}
|
|
1851
|
+
for (const record of pendingSnapshots) await finalizeManagedProjectSnapshot(record);
|
|
1571
1852
|
return { recoveredRecordIds };
|
|
1572
1853
|
});
|
|
1573
1854
|
}
|
|
@@ -1575,8 +1856,7 @@ async function recoverAfterExit(options) {
|
|
|
1575
1856
|
function recoverManagedWechatProjectsAfterInstallationExit(options) {
|
|
1576
1857
|
return withMachineE2ELeaseRecoveryOperation(options.recoveryScope, () => recoverAfterExit(options));
|
|
1577
1858
|
}
|
|
1578
|
-
|
|
1579
|
-
//#region src/devtoolsProjectOwnership/index.ts
|
|
1859
|
+
// src/devtoolsProjectOwnership/index.ts
|
|
1580
1860
|
const pendingClosures = /* @__PURE__ */ new Map();
|
|
1581
1861
|
function describe(error) {
|
|
1582
1862
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1587,7 +1867,7 @@ async function release(record, reason) {
|
|
|
1587
1867
|
delete record.error;
|
|
1588
1868
|
await writeManagedRecord(record);
|
|
1589
1869
|
}
|
|
1590
|
-
async function
|
|
1870
|
+
async function closeWindowRecord(record) {
|
|
1591
1871
|
if (record.state === "released") return;
|
|
1592
1872
|
if (record.state === "borrowed" && record.openedProjectWindow === false) {
|
|
1593
1873
|
await release(record, "borrowed");
|
|
@@ -1631,6 +1911,17 @@ async function closeRecord(record) {
|
|
|
1631
1911
|
throw error;
|
|
1632
1912
|
}
|
|
1633
1913
|
}
|
|
1914
|
+
async function closeRecord(record) {
|
|
1915
|
+
if (isManagedWechatProjectFullyReleased(record)) return;
|
|
1916
|
+
if (record.state !== "released") {
|
|
1917
|
+
if (record.snapshot && !record.snapshot.dispatchedAt && record.openedProjectWindow === void 0) await release(record, "not-started");
|
|
1918
|
+
else {
|
|
1919
|
+
if (record.snapshot && record.openedProjectWindow === false) throw new Error("Managed DevTools snapshot received a borrowed window receipt; preserving files until installation exit is proven.");
|
|
1920
|
+
await closeWindowRecord(record);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
await finalizeManagedProjectSnapshot(record);
|
|
1924
|
+
}
|
|
1634
1925
|
/** 窗口销毁与连接断开分别验证;失败保留登记供后续继续观察。 */
|
|
1635
1926
|
function closeManagedWechatProject(options) {
|
|
1636
1927
|
const journalPath = resolveManagedJournal(options.journalPath);
|
|
@@ -1648,6 +1939,7 @@ async function beginManagedWechatProject(options) {
|
|
|
1648
1939
|
const journalPath = resolveManagedJournal(options.journalPath);
|
|
1649
1940
|
if (!journalPath) return;
|
|
1650
1941
|
const id = randomUUID();
|
|
1942
|
+
const projectPath = options.snapshotProject ? managedSnapshotPath(journalPath, id) : path.resolve(options.projectPath);
|
|
1651
1943
|
await withManagedJournalLock(journalPath, async (scopeRoot) => {
|
|
1652
1944
|
const previous = await readManagedWechatProjectRecords(scopeRoot);
|
|
1653
1945
|
if (previous.some((record) => [
|
|
@@ -1655,7 +1947,7 @@ async function beginManagedWechatProject(options) {
|
|
|
1655
1947
|
"unconfirmed",
|
|
1656
1948
|
"closing",
|
|
1657
1949
|
"failed"
|
|
1658
|
-
].includes(record.state))) throw new Error("Managed DevTools journal contains unresolved ownership or cleanup; refusing another project start.");
|
|
1950
|
+
].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
1951
|
checkManagedWindowBudget(previous, options);
|
|
1660
1952
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1661
1953
|
await writeManagedRecord({
|
|
@@ -1666,7 +1958,13 @@ async function beginManagedWechatProject(options) {
|
|
|
1666
1958
|
ownerToken: managedProcessToken,
|
|
1667
1959
|
ownerPid: process.pid,
|
|
1668
1960
|
target: { ...options.target },
|
|
1669
|
-
projectPath
|
|
1961
|
+
projectPath,
|
|
1962
|
+
...options.snapshotProject ? { snapshot: {
|
|
1963
|
+
protocol: "wechat-devtools-project-snapshot-v1",
|
|
1964
|
+
sourceProjectPath: path.resolve(options.projectPath),
|
|
1965
|
+
token: randomUUID(),
|
|
1966
|
+
state: "reserved"
|
|
1967
|
+
} } : {},
|
|
1670
1968
|
state: "starting",
|
|
1671
1969
|
port: options.port,
|
|
1672
1970
|
createdAt: now,
|
|
@@ -1676,10 +1974,36 @@ async function beginManagedWechatProject(options) {
|
|
|
1676
1974
|
return {
|
|
1677
1975
|
id,
|
|
1678
1976
|
journalPath,
|
|
1977
|
+
projectPath,
|
|
1978
|
+
prepareSnapshot: async (populate) => {
|
|
1979
|
+
await withManagedJournalLock(journalPath, async () => {
|
|
1980
|
+
const record = await readManagedRecord(journalPath, id);
|
|
1981
|
+
try {
|
|
1982
|
+
await prepareManagedProjectSnapshot(record, populate);
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
if (record.snapshot) {
|
|
1985
|
+
record.snapshot.cleanupError = describe(error);
|
|
1986
|
+
await writeManagedRecord(record);
|
|
1987
|
+
}
|
|
1988
|
+
throw error;
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
},
|
|
1992
|
+
markLaunchDispatched: async () => {
|
|
1993
|
+
await withManagedJournalLock(journalPath, async () => {
|
|
1994
|
+
const record = await readManagedRecord(journalPath, id);
|
|
1995
|
+
if (record.snapshot) {
|
|
1996
|
+
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.");
|
|
1997
|
+
record.snapshot.dispatchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1998
|
+
await writeManagedRecord(record);
|
|
1999
|
+
}
|
|
2000
|
+
});
|
|
2001
|
+
},
|
|
1679
2002
|
confirm: async (result) => {
|
|
1680
2003
|
await withManagedJournalLock(journalPath, async () => {
|
|
1681
2004
|
const record = await readManagedRecord(journalPath, id);
|
|
1682
2005
|
if (record.state !== "starting" && record.state !== "unconfirmed") throw new Error("Managed DevTools start receipt was already recorded.");
|
|
2006
|
+
if (record.snapshot && (!record.snapshot.dispatchedAt || record.snapshot.state !== "ready")) throw new Error("Managed DevTools snapshot start receipt has no prepared dispatch intent.");
|
|
1683
2007
|
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
2008
|
record.openedProjectWindow = result.openedProjectWindow;
|
|
1685
2009
|
record.port = result.port;
|
|
@@ -1722,5 +2046,4 @@ async function cleanupManagedWechatProjects(options = {}) {
|
|
|
1722
2046
|
}
|
|
1723
2047
|
if (errors.length) throw new AggregateError(errors, "Managed DevTools project cleanup remains incomplete; stop the acceptance lane.");
|
|
1724
2048
|
}
|
|
1725
|
-
|
|
1726
|
-
export { readManagedRecord as a, withManagedJournalLock as c, MANAGED_PROJECT_MAX_WINDOWS_ENV as d, assertManagedInstallation as f, startWechatIdeAgent as h, recoverManagedWechatProjectsAfterInstallationExit as i, writeManagedRecord as l, sameManagedProcess as m, cleanupManagedWechatProjects as n, readManagedWechatProjectRecords as o, inspectManagedProjectHost as p, closeManagedWechatProject as r, resolveManagedJournal as s, beginManagedWechatProject as t, MANAGED_PROJECT_JOURNAL_ENV as u };
|
|
2049
|
+
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 };
|