weapp-ide-cli 6.1.9 → 6.2.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 +18 -0
- package/dist/automator-BkZZsGtn.js +1 -0
- package/dist/automator-CRpB6_pA.js +576 -0
- package/dist/automator-session-gWEsSs_A.js +558 -0
- package/dist/{cli-2sY1sAVq.js → cli-ChMQqi5w.js} +1219 -52
- package/dist/cli.js +3 -3
- package/dist/commands-C1PF1B4P.js +528 -0
- package/dist/commands-CZVPfkE9.js +1 -0
- package/dist/devtoolsProjectOwnership-yDD6h0dO.js +1726 -0
- package/dist/index.d.ts +314 -54
- package/dist/index.js +8 -5
- package/dist/managed-Sq8y8FzP.js +212 -0
- package/dist/run-mcp-CPv8cesh.js +1 -0
- package/dist/{run-mcp-LqaWlQJ_.js → run-mcp-Dl_1TH1x.js} +18 -9
- package/dist/sessionStore-zdzy2UFM.js +605 -0
- package/package.json +12 -11
- package/dist/automator-session-fbanL2eW.js +0 -1362
- package/dist/commands-DpMG1w04.js +0 -2
- package/dist/commands-__RJ552L.js +0 -1359
- package/dist/run-mcp-C2SQ_hRJ.js +0 -2
|
@@ -0,0 +1,1726 @@
|
|
|
1
|
+
import { a as assertWechatDevtoolsPort, c as withPowerShellUtf8Output, i as assertWechatDevtoolsHost, o as resolveWechatDevtoolsInstallationRoot, r as resolveWechatDevtoolsTarget, s as resolveWechatInspectionTimeout } from "./sessionStore-zdzy2UFM.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { TextDecoder, debuglog, isDeepStrictEqual, stripVTControlCharacters } from "node:util";
|
|
4
|
+
import { assertMachineE2ELeaseRecoveryScope, mutateLease, withMachineE2ELease, withMachineE2ELeaseRecoveryOperation } from "@weapp-vite/devtools-runtime";
|
|
5
|
+
import { execa } from "execa";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
import { Buffer } from "node:buffer";
|
|
10
|
+
import net from "node:net";
|
|
11
|
+
import { setTimeout } from "node:timers/promises";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { performance } from "node:perf_hooks";
|
|
14
|
+
//#region src/cli/agentStart.ts
|
|
15
|
+
/** 提取独立的完整 JSON 对象,普通日志和嵌套对象不能替代命令响应。 */
|
|
16
|
+
function readResponseObjects(stdout) {
|
|
17
|
+
const objects = [];
|
|
18
|
+
let candidate = "";
|
|
19
|
+
let depth = 0;
|
|
20
|
+
let quoted = false;
|
|
21
|
+
let escaped = false;
|
|
22
|
+
for (const line of stripVTControlCharacters(stdout).split(/\r?\n/)) {
|
|
23
|
+
if (!candidate && !/^[{[]/.test(line.trimStart())) continue;
|
|
24
|
+
candidate += `${line}\n`;
|
|
25
|
+
for (const char of line) if (escaped) escaped = false;
|
|
26
|
+
else if (quoted && char === "\\") escaped = true;
|
|
27
|
+
else if (char === "\"") quoted = !quoted;
|
|
28
|
+
else if (!quoted && (char === "{" || char === "[")) depth += 1;
|
|
29
|
+
else if (!quoted && (char === "}" || char === "]")) depth -= 1;
|
|
30
|
+
if (depth > 0) continue;
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(candidate);
|
|
33
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) objects.push(parsed);
|
|
34
|
+
} catch {}
|
|
35
|
+
candidate = "";
|
|
36
|
+
depth = 0;
|
|
37
|
+
quoted = false;
|
|
38
|
+
escaped = false;
|
|
39
|
+
}
|
|
40
|
+
return objects;
|
|
41
|
+
}
|
|
42
|
+
function parseAgentStartResponse(stdout, port) {
|
|
43
|
+
const responses = readResponseObjects(stdout).filter((value) => value.command === "agent-start");
|
|
44
|
+
const response = responses[0];
|
|
45
|
+
if (responses.length !== 1 || !response || response.status !== "ok" || response.autoPort !== port || typeof response.openedProjectWindow !== "boolean" || typeof response.version !== "string" || !response.version.trim()) throw Object.assign(/* @__PURE__ */ new Error("The official WeChat DevTools agent start response is invalid or ambiguous."), {
|
|
46
|
+
code: "WECHAT_DEVTOOLS_AGENT_START_INVALID_RESPONSE",
|
|
47
|
+
stdout
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
autoPort: port,
|
|
51
|
+
openedProjectWindow: response.openedProjectWindow,
|
|
52
|
+
version: response.version
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** 命令失败仍可能带回完整成功回执;缺失或歧义输出不能授予窗口所有权。 */
|
|
56
|
+
function readFailedCommandReceipt(error, port) {
|
|
57
|
+
if (!error || typeof error !== "object" || !("stdout" in error) || typeof error.stdout !== "string") return;
|
|
58
|
+
try {
|
|
59
|
+
return parseAgentStartResponse(error.stdout, port);
|
|
60
|
+
} catch {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** 使用所选安装的官方 agent start,并保留其窗口复用和端口冲突语义。 */
|
|
65
|
+
async function startWechatIdeAgent(options) {
|
|
66
|
+
options.signal?.throwIfAborted();
|
|
67
|
+
if (!options.projectPath.trim()) throw new TypeError("Agent start projectPath must not be empty.");
|
|
68
|
+
if (options.cliPath !== void 0 && !options.cliPath.trim()) throw new TypeError("Agent start cliPath must not be empty.");
|
|
69
|
+
if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) throw new TypeError("Agent start port must be an integer from 1 to 65535.");
|
|
70
|
+
const timeout = options.timeout ?? 12e4;
|
|
71
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new TypeError("Agent start timeout must be a finite positive number.");
|
|
72
|
+
return await withMachineE2ELease(async () => {
|
|
73
|
+
options.signal?.throwIfAborted();
|
|
74
|
+
try {
|
|
75
|
+
const target = await resolveWechatDevtoolsTarget({
|
|
76
|
+
target: options.target,
|
|
77
|
+
cliPath: options.cliPath
|
|
78
|
+
});
|
|
79
|
+
options.signal?.throwIfAborted();
|
|
80
|
+
await assertWechatDevtoolsHost(target, {
|
|
81
|
+
signal: options.signal,
|
|
82
|
+
timeout
|
|
83
|
+
});
|
|
84
|
+
options.signal?.throwIfAborted();
|
|
85
|
+
const argv = [
|
|
86
|
+
"agent",
|
|
87
|
+
"start",
|
|
88
|
+
"--project",
|
|
89
|
+
path.resolve(options.projectPath),
|
|
90
|
+
"--auto-port",
|
|
91
|
+
String(options.port)
|
|
92
|
+
];
|
|
93
|
+
if (options.trustProject === true) argv.push("--trust-project");
|
|
94
|
+
const started = parseAgentStartResponse((await execa(target.cliPath, argv, {
|
|
95
|
+
cancelSignal: options.signal,
|
|
96
|
+
timeout,
|
|
97
|
+
killDescendants: false
|
|
98
|
+
}).catch(async (error) => {
|
|
99
|
+
const receipt = readFailedCommandReceipt(error, options.port);
|
|
100
|
+
if (receipt) try {
|
|
101
|
+
await options.onStarted?.(receipt);
|
|
102
|
+
} catch (confirmationError) {
|
|
103
|
+
throw new AggregateError([error, confirmationError], "Failed agent command receipt could not be recorded.", { cause: error });
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
})).stdout, options.port);
|
|
107
|
+
await options.onStarted?.(started);
|
|
108
|
+
options.signal?.throwIfAborted();
|
|
109
|
+
return started;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
options.signal?.throwIfAborted();
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/devtoolsProjectOwnership/host.ts
|
|
118
|
+
const inspectionOptions$1 = {
|
|
119
|
+
timeout: 3e3,
|
|
120
|
+
reject: false,
|
|
121
|
+
windowsHide: true
|
|
122
|
+
};
|
|
123
|
+
const windowsInspectionOptions = {
|
|
124
|
+
...inspectionOptions$1,
|
|
125
|
+
timeout: resolveWechatInspectionTimeout("win32")
|
|
126
|
+
};
|
|
127
|
+
function invalidIdentity(result, timeout) {
|
|
128
|
+
const details = result ? ` Inspection: exitCode=${result.exitCode ?? "none"}, signal=${result.signal ?? "none"}, timedOut=${result.timedOut ?? false}, timeoutMs=${timeout}.` : "";
|
|
129
|
+
return /* @__PURE__ */ new Error(`Managed DevTools process identity could not be verified; no project was closed.${details}`);
|
|
130
|
+
}
|
|
131
|
+
/** PID 必须连同启动时间和可执行文件核验;进程号复用不继承所有权。 */
|
|
132
|
+
async function readManagedProcessIdentity(pid, platform = process.platform) {
|
|
133
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw invalidIdentity();
|
|
134
|
+
if (platform === "win32") {
|
|
135
|
+
const result = await execa("powershell.exe", [
|
|
136
|
+
"-NoProfile",
|
|
137
|
+
"-NonInteractive",
|
|
138
|
+
"-Command",
|
|
139
|
+
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);
|
|
142
|
+
if (!result.stdout.trim()) return;
|
|
143
|
+
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();
|
|
145
|
+
return {
|
|
146
|
+
pid,
|
|
147
|
+
executable: value.ExecutablePath,
|
|
148
|
+
started: value.Started
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (platform === "linux") try {
|
|
152
|
+
const [executable, stat] = await Promise.all([fs.readlink(`/proc/${pid}/exe`), fs.readFile(`/proc/${pid}/stat`, "utf8")]);
|
|
153
|
+
const boot = await fs.readFile("/proc/sys/kernel/random/boot_id", "utf8").catch(() => {
|
|
154
|
+
throw invalidIdentity();
|
|
155
|
+
});
|
|
156
|
+
const started = stat.slice(stat.lastIndexOf(")") + 2).split(/\s+/)[19];
|
|
157
|
+
if (!started || !/^\d+$/.test(started) || !boot.trim()) throw invalidIdentity();
|
|
158
|
+
return {
|
|
159
|
+
pid,
|
|
160
|
+
executable,
|
|
161
|
+
started: `${boot.trim()}:${started}`
|
|
162
|
+
};
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error.code === "ENOENT") return;
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
const result = await execa("ps", [
|
|
168
|
+
"-p",
|
|
169
|
+
String(pid),
|
|
170
|
+
"-o",
|
|
171
|
+
"lstart=",
|
|
172
|
+
"-o",
|
|
173
|
+
"comm="
|
|
174
|
+
], {
|
|
175
|
+
...inspectionOptions$1,
|
|
176
|
+
env: { LC_ALL: "C" }
|
|
177
|
+
});
|
|
178
|
+
if (result.exitCode === 1 && !result.stdout.trim()) return;
|
|
179
|
+
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();
|
|
181
|
+
return {
|
|
182
|
+
pid,
|
|
183
|
+
started: match[1].replace(/\s+/g, " "),
|
|
184
|
+
executable: match[2].trim()
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function sameManagedProcess(first, second) {
|
|
188
|
+
return first.pid === second.pid && first.started === second.started && first.executable === second.executable;
|
|
189
|
+
}
|
|
190
|
+
async function listenerPid(port, platform = process.platform) {
|
|
191
|
+
const result = platform === "win32" ? await execa("powershell.exe", [
|
|
192
|
+
"-NoProfile",
|
|
193
|
+
"-NonInteractive",
|
|
194
|
+
"-Command",
|
|
195
|
+
withPowerShellUtf8Output(`$ErrorActionPreference='Stop'; @(Get-NetTCPConnection -LocalPort ${port} -State Listen | Select-Object -ExpandProperty OwningProcess -Unique) | ConvertTo-Json -Compress`)
|
|
196
|
+
], windowsInspectionOptions) : await execa("lsof", [
|
|
197
|
+
"-nP",
|
|
198
|
+
`-iTCP:${port}`,
|
|
199
|
+
"-sTCP:LISTEN",
|
|
200
|
+
"-Fp"
|
|
201
|
+
], inspectionOptions$1);
|
|
202
|
+
if (result.exitCode !== 0) throw invalidIdentity(result, resolveWechatInspectionTimeout(platform));
|
|
203
|
+
const parsed = platform === "win32" ? JSON.parse(result.stdout.trim() || "[]") : void 0;
|
|
204
|
+
const pids = platform === "win32" ? Array.isArray(parsed) ? parsed : [parsed] : result.stdout.split(/\r?\n/).filter((line) => /^p\d+$/.test(line)).map((line) => Number(line.slice(1)));
|
|
205
|
+
const unique = [...new Set(pids)];
|
|
206
|
+
if (unique.length !== 1 || typeof unique[0] !== "number" || !Number.isSafeInteger(unique[0]) || unique[0] <= 0) throw invalidIdentity();
|
|
207
|
+
return unique[0];
|
|
208
|
+
}
|
|
209
|
+
/** 所有权来自官方回执;监听身份仅用于约束后续关闭,不能单独授予所有权。 */
|
|
210
|
+
async function inspectManagedProjectHost(target, port, platform = process.platform) {
|
|
211
|
+
await assertWechatDevtoolsPort(target, port, { platform });
|
|
212
|
+
const identity = await readManagedProcessIdentity(await listenerPid(port, platform), platform);
|
|
213
|
+
if (!identity) throw invalidIdentity();
|
|
214
|
+
return identity;
|
|
215
|
+
}
|
|
216
|
+
async function assertManagedInstallation(target) {
|
|
217
|
+
const current = await resolveWechatDevtoolsTarget({ cliPath: target.cliPath });
|
|
218
|
+
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
|
+
}
|
|
220
|
+
function isManagedPortClosed(port) {
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const socket = net.createConnection({
|
|
223
|
+
host: "127.0.0.1",
|
|
224
|
+
port
|
|
225
|
+
});
|
|
226
|
+
const finish = (error, closed = false) => {
|
|
227
|
+
socket.destroy();
|
|
228
|
+
if (error) reject(error);
|
|
229
|
+
else resolve(closed);
|
|
230
|
+
};
|
|
231
|
+
socket.once("connect", () => finish());
|
|
232
|
+
socket.once("error", (error) => error.code === "ECONNREFUSED" ? finish(void 0, true) : finish(error));
|
|
233
|
+
socket.setTimeout(1e3, () => finish(/* @__PURE__ */ new Error("Managed DevTools port probe timed out.")));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
async function waitForManagedPortClosed(port, timeoutMs = 1e4) {
|
|
237
|
+
const deadline = Date.now() + timeoutMs;
|
|
238
|
+
do {
|
|
239
|
+
if (await isManagedPortClosed(port)) return;
|
|
240
|
+
await setTimeout(100);
|
|
241
|
+
} while (Date.now() < deadline);
|
|
242
|
+
throw new Error("Managed DevTools project close returned but its automator port remained open.");
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/devtoolsProjectOwnership/installationExit/schema.ts
|
|
246
|
+
const installationExitRecoverySchema = z.object({
|
|
247
|
+
protocol: z.literal("wechat-devtools-installation-exit-v1"),
|
|
248
|
+
recoveredAt: z.string().datetime(),
|
|
249
|
+
recoveryScopeId: z.string().uuid(),
|
|
250
|
+
journalScopeId: z.string().uuid(),
|
|
251
|
+
journalRootPath: z.string().min(1),
|
|
252
|
+
installationId: z.string().min(1),
|
|
253
|
+
profileDir: z.string().min(1),
|
|
254
|
+
previous: z.object({
|
|
255
|
+
state: z.enum([
|
|
256
|
+
"starting",
|
|
257
|
+
"unconfirmed",
|
|
258
|
+
"failed"
|
|
259
|
+
]),
|
|
260
|
+
error: z.string().optional(),
|
|
261
|
+
updatedAt: z.string().min(1),
|
|
262
|
+
recordSha256: z.string().regex(/^[\da-f]{64}$/)
|
|
263
|
+
}).strict(),
|
|
264
|
+
stoppedOwnerPids: z.array(z.number().int().positive()).min(1),
|
|
265
|
+
closedPorts: z.array(z.number().int().min(1).max(65535)).min(1),
|
|
266
|
+
processInspection: z.object({
|
|
267
|
+
platform: z.literal("darwin"),
|
|
268
|
+
installationRoot: z.string().min(1),
|
|
269
|
+
checkedAt: z.string().datetime(),
|
|
270
|
+
inspectedProcessCount: z.number().int().nonnegative(),
|
|
271
|
+
kernelPathProcessCount: z.number().int().nonnegative(),
|
|
272
|
+
textImageProcessCount: z.number().int().nonnegative(),
|
|
273
|
+
exitedProcessCount: z.number().int().nonnegative(),
|
|
274
|
+
zombieProcessCount: z.number().int().nonnegative(),
|
|
275
|
+
selectedProcessCount: z.literal(0)
|
|
276
|
+
}).strict()
|
|
277
|
+
}).strict();
|
|
278
|
+
//#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
|
+
//#region src/devtoolsProjectOwnership/journal/scope.ts
|
|
286
|
+
const markerName = ".ownership-scope";
|
|
287
|
+
const scopeSchema = z.object({
|
|
288
|
+
schemaVersion: z.literal(1),
|
|
289
|
+
rootPath: z.string().min(1),
|
|
290
|
+
scopeId: z.string().uuid()
|
|
291
|
+
}).strict();
|
|
292
|
+
async function readScope(directory) {
|
|
293
|
+
const file = path.join(directory, markerName);
|
|
294
|
+
const stat = await fs.lstat(file).catch((error) => {
|
|
295
|
+
if (error.code === "ENOENT") return;
|
|
296
|
+
throw error;
|
|
297
|
+
});
|
|
298
|
+
if (!stat) return;
|
|
299
|
+
if (!stat.isFile()) throw new Error("Managed DevTools journal scope must be a regular metadata file.");
|
|
300
|
+
const scope = scopeSchema.parse(JSON.parse(await fs.readFile(file, "utf8")));
|
|
301
|
+
if (!path.isAbsolute(scope.rootPath)) throw new Error("Managed DevTools journal scope root must be absolute.");
|
|
302
|
+
return scope;
|
|
303
|
+
}
|
|
304
|
+
async function assertDirectory(directory) {
|
|
305
|
+
if (!(await fs.lstat(directory)).isDirectory()) throw new Error("Managed DevTools journal scope must use regular directories.");
|
|
306
|
+
}
|
|
307
|
+
/** 只接受创建时登记的根及 children 链;不通过搜索祖先收养未知任务。 */
|
|
308
|
+
async function resolveManagedJournalScope(directory) {
|
|
309
|
+
const selected = path.resolve(directory);
|
|
310
|
+
await assertDirectory(selected);
|
|
311
|
+
const scope = await readScope(selected);
|
|
312
|
+
if (!scope) {
|
|
313
|
+
if (path.basename(selected).startsWith("task-scope-")) throw new Error("Managed DevTools journal scope metadata is missing; refusing an isolated fallback.");
|
|
314
|
+
return { rootPath: selected };
|
|
315
|
+
}
|
|
316
|
+
await assertDirectory(scope.rootPath);
|
|
317
|
+
const root = await readScope(scope.rootPath);
|
|
318
|
+
if (!root || root.scopeId !== scope.scopeId || root.rootPath !== scope.rootPath) throw new Error("Managed DevTools journal scope identity changed; refusing another task boundary.");
|
|
319
|
+
const rootPath = await fs.realpath(scope.rootPath);
|
|
320
|
+
const currentPath = await fs.realpath(selected);
|
|
321
|
+
const relative = path.relative(scope.rootPath, selected);
|
|
322
|
+
if (relative) {
|
|
323
|
+
const parts = relative.split(path.sep);
|
|
324
|
+
if (path.isAbsolute(relative) || parts.length % 2 !== 0 || parts.some((part, index) => index % 2 === 0 ? part !== "children" : !part || part === "..")) throw new Error("Managed DevTools journal is outside its registered task children.");
|
|
325
|
+
let current = scope.rootPath;
|
|
326
|
+
for (const [index, part] of parts.entries()) {
|
|
327
|
+
current = path.join(current, part);
|
|
328
|
+
await assertDirectory(current);
|
|
329
|
+
if (index % 2 === 1) {
|
|
330
|
+
const registered = await readScope(current);
|
|
331
|
+
if (!registered || registered.rootPath !== scope.rootPath || registered.scopeId !== scope.scopeId) throw new Error("Managed DevTools journal scope chain changed; refusing split task locks.");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (path.resolve(rootPath, relative) !== currentPath) throw new Error("Managed DevTools journal path changed from its registered task children.");
|
|
336
|
+
return scope;
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/devtoolsProjectOwnership/journal/windowsSelfIdentity/wire.ts
|
|
340
|
+
const SELF_IDENTITY_WIRE = "WEAPP_JOURNAL_SELF_V1";
|
|
341
|
+
const epochTicks = 621355968000000000n;
|
|
342
|
+
const maximumTicks = 3155378975999999999n;
|
|
343
|
+
function invalid() {
|
|
344
|
+
return /* @__PURE__ */ new Error("Managed journal writer identity could not be verified; the journal remains unchanged.");
|
|
345
|
+
}
|
|
346
|
+
/** 保留 CIM 的微秒精度与七位小数格式;整数 ticks 不经浮点数或 Date 丢失尾数。 */
|
|
347
|
+
function cimStartedFromTicks(value) {
|
|
348
|
+
if (!/^[1-9]\d{0,18}$/.test(value)) throw invalid();
|
|
349
|
+
const ticks = BigInt(value);
|
|
350
|
+
if (ticks < epochTicks || ticks > maximumTicks) throw invalid();
|
|
351
|
+
const seconds = (ticks - epochTicks) / 10000000n;
|
|
352
|
+
return `${new Date(Number(seconds * 1000n)).toISOString().slice(0, 19)}.${(ticks % 10000000n / 10n * 10n).toString().padStart(7, "0")}Z`;
|
|
353
|
+
}
|
|
354
|
+
function decodeExecutable(value) {
|
|
355
|
+
const bytes = Buffer.from(value, "base64");
|
|
356
|
+
if (!value || bytes.toString("base64") !== value) throw invalid();
|
|
357
|
+
const executable = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
358
|
+
if (!executable.trim() || !Buffer.from(executable).equals(bytes) || [...executable].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) throw invalid();
|
|
359
|
+
return executable;
|
|
360
|
+
}
|
|
361
|
+
/** 只接受一次完整自查结果;额外输出、退出或代次变化均不能授予写入身份。 */
|
|
362
|
+
function parseWindowsSelfIdentity(stdout, expectedPid) {
|
|
363
|
+
if (!Number.isSafeInteger(expectedPid) || expectedPid <= 0 || !/^[\t\r\n\x20-\x7E]*$/.test(stdout)) throw invalid();
|
|
364
|
+
const fields = stdout.replace(/\r?\n$/, "").split(" ");
|
|
365
|
+
const [marker, status, pid, before, after, exited, started, encodedExecutable] = fields;
|
|
366
|
+
if (fields.length !== 8 || marker !== "WEAPP_JOURNAL_SELF_V1" || status !== "present" || pid !== String(expectedPid) || before !== after || exited !== "false" || !before || !started || !encodedExecutable || started !== cimStartedFromTicks(before)) throw invalid();
|
|
367
|
+
return {
|
|
368
|
+
pid: expectedPid,
|
|
369
|
+
executable: decodeExecutable(encodedExecutable),
|
|
370
|
+
started
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/devtoolsProjectOwnership/journal/windowsSelfIdentity/command.ts
|
|
375
|
+
/** 当前 writer 的 PID 由 Node 进程提供;脚本只读查询,不执行任何进程或 IDE 清理。 */
|
|
376
|
+
function windowsSelfIdentityCommand(pid) {
|
|
377
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw new TypeError("A positive writer PID is required.");
|
|
378
|
+
return `
|
|
379
|
+
$ErrorActionPreference='Stop'
|
|
380
|
+
$writerFirst=$null
|
|
381
|
+
$writerSecond=$null
|
|
382
|
+
$writerExitCode=0
|
|
383
|
+
try {
|
|
384
|
+
$writerFirst=[System.Diagnostics.Process]::GetProcessById(${pid})
|
|
385
|
+
$writerBefore=$writerFirst.StartTime.ToUniversalTime()
|
|
386
|
+
$writerExecutable=$writerFirst.MainModule.FileName
|
|
387
|
+
$writerSecond=[System.Diagnostics.Process]::GetProcessById(${pid})
|
|
388
|
+
$writerAfter=$writerSecond.StartTime.ToUniversalTime()
|
|
389
|
+
if ($writerFirst.Id -ne ${pid} -or $writerSecond.Id -ne ${pid} -or [string]::IsNullOrWhiteSpace($writerExecutable) -or $writerBefore.Ticks -ne $writerAfter.Ticks) {
|
|
390
|
+
throw [System.InvalidOperationException]::new('Incomplete or changed writer identity')
|
|
391
|
+
}
|
|
392
|
+
if ($writerFirst.HasExited -or $writerSecond.HasExited) {
|
|
393
|
+
throw [System.InvalidOperationException]::new('Writer exited during identity inspection')
|
|
394
|
+
}
|
|
395
|
+
$writerLegacyTicks=[long]($writerBefore.Ticks - ($writerBefore.Ticks % 10))
|
|
396
|
+
$writerStarted=[DateTime]::new($writerLegacyTicks,[DateTimeKind]::Utc).ToString('o',[Globalization.CultureInfo]::InvariantCulture)
|
|
397
|
+
$writerPath=[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($writerExecutable))
|
|
398
|
+
$writerFields=@('${SELF_IDENTITY_WIRE}','present','${pid}',$writerBefore.Ticks.ToString([Globalization.CultureInfo]::InvariantCulture),$writerAfter.Ticks.ToString([Globalization.CultureInfo]::InvariantCulture),'false',$writerStarted,$writerPath)
|
|
399
|
+
[Console]::Out.WriteLine([string]::Join([string][char]9,[string[]]$writerFields))
|
|
400
|
+
[Console]::Out.Flush()
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
$writerExitCode=1
|
|
404
|
+
$writerError=$_.Exception.GetBaseException()
|
|
405
|
+
[Console]::Error.WriteLine('${SELF_IDENTITY_WIRE}:error:'+ $writerError.GetType().FullName + ':' + $writerError.HResult.ToString([Globalization.CultureInfo]::InvariantCulture))
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
if ($null -ne $writerSecond) { $writerSecond.Dispose() }
|
|
409
|
+
if ($null -ne $writerFirst) { $writerFirst.Dispose() }
|
|
410
|
+
}
|
|
411
|
+
exit $writerExitCode
|
|
412
|
+
`;
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/devtoolsProjectOwnership/journal/windowsSelfIdentity/trace.ts
|
|
416
|
+
const marker = `${SELF_IDENTITY_WIRE}:phase:`;
|
|
417
|
+
const statements = /* @__PURE__ */ new Map([
|
|
418
|
+
["$writerFirst=[System.Diagnostics.Process]", "lookup-before"],
|
|
419
|
+
["$writerBefore=$writerFirst.StartTime", "start-before"],
|
|
420
|
+
["$writerExecutable=$writerFirst.MainModule", "module-path"],
|
|
421
|
+
["$writerSecond=[System.Diagnostics.Process]", "lookup-after"],
|
|
422
|
+
["$writerAfter=$writerSecond.StartTime", "start-after"],
|
|
423
|
+
["if ($writerFirst.Id", "generation-check"],
|
|
424
|
+
["if ($writerFirst.HasExited", "exit-check"],
|
|
425
|
+
["$writerLegacyTicks=", "serialize"],
|
|
426
|
+
["[Console]::Out.Flush()", "stdout-flush"],
|
|
427
|
+
["$writerExitCode=1", "error"],
|
|
428
|
+
["if ($null -ne $writerSecond)", "dispose-after"],
|
|
429
|
+
["if ($null -ne $writerFirst)", "dispose-before"],
|
|
430
|
+
["exit $writerExitCode", "complete"]
|
|
431
|
+
]);
|
|
432
|
+
const phases = ["entry", ...statements.values()];
|
|
433
|
+
const phasePattern = new RegExp(`^${marker}(${phases.join("|")})$`);
|
|
434
|
+
const phaseLines = new RegExp(`^${marker}(?:${phases.join("|")})\\r?\\n`, "gm");
|
|
435
|
+
function phaseStatement(phase) {
|
|
436
|
+
return `[Console]::Error.WriteLine('${marker}${phase}'); [Console]::Error.Flush()`;
|
|
437
|
+
}
|
|
438
|
+
/** 仅增加已知阶段标记;移除标记后必须与原身份查询逐字相同。 */
|
|
439
|
+
function traceWindowsSelfIdentityCommand(command) {
|
|
440
|
+
return `${phaseStatement("entry")}\n${command.split("\n").flatMap((line) => {
|
|
441
|
+
const phase = [...statements].find(([statement]) => line.trimStart().startsWith(statement))?.[1];
|
|
442
|
+
return phase ? [phaseStatement(phase), line] : [line];
|
|
443
|
+
}).join("\n")}`;
|
|
444
|
+
}
|
|
445
|
+
/** 只接收固定 ASCII 阶段名;未知 stderr 不进入诊断日志。 */
|
|
446
|
+
function observeWindowsSelfIdentityPhases(report) {
|
|
447
|
+
let buffered = "";
|
|
448
|
+
return (chunk) => {
|
|
449
|
+
buffered += chunk.toString();
|
|
450
|
+
const lines = buffered.split("\n");
|
|
451
|
+
buffered = lines.pop();
|
|
452
|
+
for (const line of lines) {
|
|
453
|
+
const phase = phasePattern.exec(line.replace(/\r$/, ""))?.[1];
|
|
454
|
+
if (phase) report(phase);
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/** 只有显式诊断调用可剥离完整已知标记;其余 stderr 继续触发失败。 */
|
|
459
|
+
function stripWindowsSelfIdentityPhases(stderr) {
|
|
460
|
+
return stderr.replace(phaseLines, "");
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/devtoolsProjectOwnership/journal/windowsSelfIdentity/index.ts
|
|
464
|
+
const debug = debuglog("weapp-ide-journal-writer");
|
|
465
|
+
/** 仅核验当前 Node 写入进程;不能用于任意宿主、旧锁存活判断或进程清理。 */
|
|
466
|
+
async function readWindowsJournalWriterIdentity() {
|
|
467
|
+
const timeout = resolveWechatInspectionTimeout("win32");
|
|
468
|
+
const trace = debug.enabled;
|
|
469
|
+
const command = windowsSelfIdentityCommand(process.pid);
|
|
470
|
+
const started = performance.now();
|
|
471
|
+
const query = execa("powershell.exe", [
|
|
472
|
+
"-NoProfile",
|
|
473
|
+
"-NonInteractive",
|
|
474
|
+
"-Command",
|
|
475
|
+
trace ? traceWindowsSelfIdentityCommand(command) : command
|
|
476
|
+
], {
|
|
477
|
+
timeout,
|
|
478
|
+
reject: false,
|
|
479
|
+
windowsHide: true,
|
|
480
|
+
maxBuffer: 65536,
|
|
481
|
+
stripFinalNewline: !trace
|
|
482
|
+
});
|
|
483
|
+
if (trace) {
|
|
484
|
+
const report = (phase) => debug("phase=%s elapsedMs=%s", phase, (performance.now() - started).toFixed(1));
|
|
485
|
+
report("launch");
|
|
486
|
+
query.nodeChildProcess.once("spawn", () => report("spawn"));
|
|
487
|
+
query.stderr?.on("data", observeWindowsSelfIdentityPhases(report));
|
|
488
|
+
}
|
|
489
|
+
const result = await query;
|
|
490
|
+
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);
|
|
492
|
+
if (result.exitCode !== 0 || result.failed || result.timedOut || result.signal || stderr.trim()) {
|
|
493
|
+
const reason = /^WEAPP_JOURNAL_SELF_V1:error:([\w.]+):-?\d+\r?\n?$/.exec(stderr)?.[1] ?? "unconfirmed";
|
|
494
|
+
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
|
+
}
|
|
496
|
+
return parseWindowsSelfIdentity(result.stdout, process.pid);
|
|
497
|
+
}
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/devtoolsProjectOwnership/journal/writerIdentity.ts
|
|
500
|
+
let currentIdentity$1;
|
|
501
|
+
/** 共享在途与成功身份;失败仍拒绝当前调用,仅允许下一次显式操作重新核验。 */
|
|
502
|
+
function readManagedJournalWriterIdentity() {
|
|
503
|
+
if (!currentIdentity$1) {
|
|
504
|
+
const pending = (process.platform === "win32" ? readWindowsJournalWriterIdentity() : readManagedProcessIdentity(process.pid)).then((identity) => {
|
|
505
|
+
if (!identity) throw new Error("Cannot identify the managed DevTools journal writer.");
|
|
506
|
+
return identity;
|
|
507
|
+
});
|
|
508
|
+
currentIdentity$1 = pending;
|
|
509
|
+
pending.catch(() => {
|
|
510
|
+
if (currentIdentity$1 === pending) currentIdentity$1 = void 0;
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return currentIdentity$1;
|
|
514
|
+
}
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region src/devtoolsProjectOwnership/windowClose/schema.ts
|
|
517
|
+
const text = z.string().min(1);
|
|
518
|
+
const cursorSchema = z.object({
|
|
519
|
+
name: z.string().regex(/^[^/\\]+\.log$/),
|
|
520
|
+
identity: text,
|
|
521
|
+
offset: z.number().int().nonnegative(),
|
|
522
|
+
anchor: z.string().regex(/^[a-f0-9]{64}$/),
|
|
523
|
+
skipPartialLine: z.boolean()
|
|
524
|
+
});
|
|
525
|
+
const callSchema = z.object({
|
|
526
|
+
fileIdentity: text,
|
|
527
|
+
winId: text,
|
|
528
|
+
browserWindowId: z.number().int().positive(),
|
|
529
|
+
calledAt: text,
|
|
530
|
+
cancelled: z.boolean().optional()
|
|
531
|
+
});
|
|
532
|
+
const managedWindowCloseSchema = z.object({
|
|
533
|
+
protocol: z.literal("wechat-devtools-window-close-trace-v1"),
|
|
534
|
+
profileDir: text,
|
|
535
|
+
productVersion: text,
|
|
536
|
+
capturedAt: text,
|
|
537
|
+
mainHost: z.object({
|
|
538
|
+
pid: z.number().int().positive(),
|
|
539
|
+
executable: text,
|
|
540
|
+
started: text
|
|
541
|
+
}).optional(),
|
|
542
|
+
dispatchedAt: text.optional(),
|
|
543
|
+
cursors: z.array(cursorSchema).min(1),
|
|
544
|
+
calls: z.array(callSchema),
|
|
545
|
+
window: callSchema.extend({
|
|
546
|
+
runtimeId: text,
|
|
547
|
+
requestedAt: text,
|
|
548
|
+
nativeClosedAt: text.optional(),
|
|
549
|
+
webContentsDestroyedAt: text.optional()
|
|
550
|
+
}).optional(),
|
|
551
|
+
failure: text.optional(),
|
|
552
|
+
logInventoryRecovery: z.object({
|
|
553
|
+
failure: text,
|
|
554
|
+
cursors: z.array(cursorSchema).min(1)
|
|
555
|
+
}).optional()
|
|
556
|
+
});
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region src/devtoolsProjectOwnership/journal.ts
|
|
559
|
+
const managedProcessToken = randomUUID();
|
|
560
|
+
const idSchema = z.string().uuid();
|
|
561
|
+
const hostSchema = z.object({
|
|
562
|
+
pid: z.number().int().positive(),
|
|
563
|
+
executable: z.string().min(1),
|
|
564
|
+
started: z.string().min(1)
|
|
565
|
+
});
|
|
566
|
+
const recordSchema = z.object({
|
|
567
|
+
schemaVersion: z.literal(1),
|
|
568
|
+
id: idSchema,
|
|
569
|
+
generation: z.string().min(1),
|
|
570
|
+
journalPath: z.string().min(1),
|
|
571
|
+
ownerToken: idSchema,
|
|
572
|
+
ownerPid: z.number().int().positive(),
|
|
573
|
+
target: z.object({
|
|
574
|
+
cliPath: z.string().min(1),
|
|
575
|
+
installationId: z.string().min(1),
|
|
576
|
+
appPath: z.string().min(1),
|
|
577
|
+
profileDir: z.string().min(1),
|
|
578
|
+
version: z.string().optional(),
|
|
579
|
+
channel: z.enum([
|
|
580
|
+
"stable",
|
|
581
|
+
"rc",
|
|
582
|
+
"nightly",
|
|
583
|
+
"unknown"
|
|
584
|
+
]).optional()
|
|
585
|
+
}),
|
|
586
|
+
projectPath: z.string().min(1),
|
|
587
|
+
state: z.enum([
|
|
588
|
+
"starting",
|
|
589
|
+
"unconfirmed",
|
|
590
|
+
"owned",
|
|
591
|
+
"borrowed",
|
|
592
|
+
"closing",
|
|
593
|
+
"failed",
|
|
594
|
+
"released"
|
|
595
|
+
]),
|
|
596
|
+
openedProjectWindow: z.boolean().optional(),
|
|
597
|
+
port: z.number().int().min(1).max(65535).optional(),
|
|
598
|
+
host: hostSchema.optional(),
|
|
599
|
+
createdAt: z.string().min(1),
|
|
600
|
+
updatedAt: z.string().min(1),
|
|
601
|
+
closeAcknowledgedAt: z.string().min(1).optional(),
|
|
602
|
+
windowClose: managedWindowCloseSchema.optional(),
|
|
603
|
+
releasedReason: z.enum([
|
|
604
|
+
"borrowed",
|
|
605
|
+
"project-closed",
|
|
606
|
+
"installation-exited"
|
|
607
|
+
]).optional(),
|
|
608
|
+
installationExitRecovery: installationExitRecoverySchema.optional(),
|
|
609
|
+
error: z.string().optional()
|
|
610
|
+
}).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." });
|
|
611
|
+
function resolveManagedJournal(journalPath) {
|
|
612
|
+
const selected = journalPath ?? process.env["WEAPP_IDE_MANAGED_PROJECT_JOURNAL"];
|
|
613
|
+
return selected?.trim() ? path.resolve(selected) : void 0;
|
|
614
|
+
}
|
|
615
|
+
function managedRecordPath(journalPath, id) {
|
|
616
|
+
return path.join(journalPath, `${idSchema.parse(id)}.json`);
|
|
617
|
+
}
|
|
618
|
+
async function readManagedRecord(journalPath, id) {
|
|
619
|
+
const file = managedRecordPath(journalPath, id);
|
|
620
|
+
if (!(await fs.lstat(file)).isFile()) throw new Error("Managed DevTools journal record is not a regular file.");
|
|
621
|
+
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.");
|
|
623
|
+
return record;
|
|
624
|
+
}
|
|
625
|
+
async function writeManagedRecord(record) {
|
|
626
|
+
record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
627
|
+
recordSchema.parse(record);
|
|
628
|
+
const file = managedRecordPath(record.journalPath, record.id);
|
|
629
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
630
|
+
try {
|
|
631
|
+
await fs.writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, {
|
|
632
|
+
mode: 384,
|
|
633
|
+
flag: "wx"
|
|
634
|
+
});
|
|
635
|
+
await fs.rename(temporary, file);
|
|
636
|
+
} finally {
|
|
637
|
+
await fs.rm(temporary, { force: true });
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/** 仅递归当前任务明确传递的 children;不扫描用户缓存,不跟随符号链接。 */
|
|
641
|
+
async function readManagedWechatProjectRecords(journalPath) {
|
|
642
|
+
const directory = resolveManagedJournal(journalPath);
|
|
643
|
+
if (!directory) return [];
|
|
644
|
+
const entries = await fs.readdir(directory, { withFileTypes: true }).catch((error) => {
|
|
645
|
+
if (error.code === "ENOENT") return;
|
|
646
|
+
throw error;
|
|
647
|
+
});
|
|
648
|
+
if (!entries) return [];
|
|
649
|
+
const scope = await resolveManagedJournalScope(directory);
|
|
650
|
+
const records = [];
|
|
651
|
+
for (const entry of entries) if (entry.name.endsWith(".json")) records.push(await readManagedRecord(directory, entry.name.slice(0, -5)));
|
|
652
|
+
else if (entry.name === "children") {
|
|
653
|
+
if (!entry.isDirectory()) throw new Error("Managed DevTools child journals must be regular directories.");
|
|
654
|
+
const children = await fs.readdir(path.join(directory, "children"), { withFileTypes: true });
|
|
655
|
+
for (const child of children) {
|
|
656
|
+
if (!child.isDirectory()) throw new Error("Managed DevTools child journal is not a regular directory.");
|
|
657
|
+
const childPath = path.join(directory, "children", child.name);
|
|
658
|
+
const childScope = await resolveManagedJournalScope(childPath);
|
|
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));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return records;
|
|
664
|
+
}
|
|
665
|
+
const lockOwnerSchema = z.object({
|
|
666
|
+
token: idSchema,
|
|
667
|
+
identity: hostSchema
|
|
668
|
+
});
|
|
669
|
+
async function readLockOwner(lock) {
|
|
670
|
+
return fs.readFile(path.join(lock, "owner"), "utf8").then((value) => lockOwnerSchema.parse(JSON.parse(value))).catch((error) => {
|
|
671
|
+
if (error.code === "ENOENT") return;
|
|
672
|
+
throw error;
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
async function releaseDeadLock(lock) {
|
|
676
|
+
const owner = await readLockOwner(lock);
|
|
677
|
+
if (!owner) return false;
|
|
678
|
+
const live = await readManagedProcessIdentity(owner.identity.pid);
|
|
679
|
+
if (live && sameManagedProcess(live, owner.identity)) return false;
|
|
680
|
+
const current = await readLockOwner(lock);
|
|
681
|
+
if (!current || current.token !== owner.token || !sameManagedProcess(current.identity, owner.identity)) return false;
|
|
682
|
+
await fs.rm(lock, { recursive: true });
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
/** 工作进程退出后父进程可回收其锁;未知身份或不完整锁保持阻塞。 */
|
|
686
|
+
async function withManagedJournalDirectoryLock(directory, run) {
|
|
687
|
+
await fs.mkdir(directory, {
|
|
688
|
+
recursive: true,
|
|
689
|
+
mode: 448
|
|
690
|
+
});
|
|
691
|
+
const lock = path.join(directory, ".ownership-lock");
|
|
692
|
+
const token = randomUUID();
|
|
693
|
+
const identity = await readManagedJournalWriterIdentity();
|
|
694
|
+
const deadline = Date.now() + 45e3;
|
|
695
|
+
while (true) {
|
|
696
|
+
if (await mutateLease(lock, async () => {
|
|
697
|
+
try {
|
|
698
|
+
await fs.mkdir(lock, { mode: 448 });
|
|
699
|
+
} catch (error) {
|
|
700
|
+
if (error.code !== "EEXIST") throw error;
|
|
701
|
+
if (!await releaseDeadLock(lock)) return false;
|
|
702
|
+
await fs.mkdir(lock, { mode: 448 });
|
|
703
|
+
}
|
|
704
|
+
await fs.writeFile(path.join(lock, "owner"), JSON.stringify({
|
|
705
|
+
token,
|
|
706
|
+
identity
|
|
707
|
+
}), {
|
|
708
|
+
mode: 384,
|
|
709
|
+
flag: "wx"
|
|
710
|
+
});
|
|
711
|
+
return true;
|
|
712
|
+
}, { timeoutMs: Math.max(0, deadline - Date.now()) })) break;
|
|
713
|
+
if (Date.now() >= deadline) throw new Error("Managed DevTools journal is still owned by another operation; refusing concurrent cleanup.");
|
|
714
|
+
await setTimeout(25);
|
|
715
|
+
}
|
|
716
|
+
const releaseLock = () => mutateLease(lock, async () => {
|
|
717
|
+
const owner = await readLockOwner(lock);
|
|
718
|
+
if (!owner || owner.token !== token || !sameManagedProcess(owner.identity, identity)) throw new Error("Managed DevTools journal lock ownership changed; refusing cleanup.");
|
|
719
|
+
await fs.rm(lock, { recursive: true });
|
|
720
|
+
}, { timeoutMs: 45e3 });
|
|
721
|
+
try {
|
|
722
|
+
return await run();
|
|
723
|
+
} finally {
|
|
724
|
+
await releaseLock();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
/** 兄弟 journal 的检查和登记共用根锁,关闭、确认和失败更新也不能绕过该边界。 */
|
|
728
|
+
async function withManagedJournalLock(directory, run) {
|
|
729
|
+
await fs.mkdir(directory, {
|
|
730
|
+
recursive: true,
|
|
731
|
+
mode: 448
|
|
732
|
+
});
|
|
733
|
+
const scope = await resolveManagedJournalScope(directory);
|
|
734
|
+
return await withManagedJournalDirectoryLock(scope.rootPath, async () => {
|
|
735
|
+
const current = await resolveManagedJournalScope(directory);
|
|
736
|
+
if (current.rootPath !== scope.rootPath || scope.scopeId !== void 0 && current.scopeId !== scope.scopeId) throw new Error("Managed DevTools journal scope changed while waiting for its lock.");
|
|
737
|
+
return await run(scope.rootPath);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
//#endregion
|
|
741
|
+
//#region src/devtoolsProjectOwnership/windowBudget.ts
|
|
742
|
+
function maximumOwnedWindows(options) {
|
|
743
|
+
const selected = options.maxOwnedWindows ?? process.env["WEAPP_IDE_MANAGED_PROJECT_MAX_WINDOWS"] ?? 1;
|
|
744
|
+
if (selected !== 1 && selected !== 2 && selected !== "1" && selected !== "2") throw new Error("Managed DevTools maximum owned windows must be 1 or 2.");
|
|
745
|
+
return Number(selected);
|
|
746
|
+
}
|
|
747
|
+
function sameTarget(first, second) {
|
|
748
|
+
return first.installationId === second.installationId && first.cliPath === second.cliPath && first.appPath === second.appPath && first.profileDir === second.profileDir && first.version === second.version && first.channel === second.channel;
|
|
749
|
+
}
|
|
750
|
+
/** 调用方持有根日志锁;重复项目必须只连接已有端点,不能再次派发可能开窗的启动命令。 */
|
|
751
|
+
function checkManagedWindowBudget(records, options) {
|
|
752
|
+
const maximum = maximumOwnedWindows(options);
|
|
753
|
+
const projectPath = path.resolve(options.projectPath);
|
|
754
|
+
if (records.some((record) => record.state !== "released" && record.projectPath === 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
|
+
const owned = records.filter((record) => record.state === "owned").length;
|
|
756
|
+
if (owned >= maximum) throw new Error(`Managed DevTools window budget exhausted (${owned}/${maximum}); close the previous owned project before another start.`);
|
|
757
|
+
}
|
|
758
|
+
//#endregion
|
|
759
|
+
//#region src/devtoolsProjectOwnership/windowClose/logCursor.ts
|
|
760
|
+
const readLimit = 2097152;
|
|
761
|
+
const scanLimit = 33554432;
|
|
762
|
+
const scanChunkSize = 65536;
|
|
763
|
+
const headerLimit = 4096;
|
|
764
|
+
const emptyAnchor = createHash("sha256").digest("hex");
|
|
765
|
+
async function selectedLogDirectory(profileDir) {
|
|
766
|
+
const selected = await fs.realpath(profileDir);
|
|
767
|
+
const directory = path.join(selected, "WeappLog", "logs");
|
|
768
|
+
if (await fs.realpath(directory) !== directory) throw new Error("Managed DevTools window-close logs must belong to the selected profile without redirected log directories.");
|
|
769
|
+
return directory;
|
|
770
|
+
}
|
|
771
|
+
async function logFiles(directory) {
|
|
772
|
+
const entries = (await fs.readdir(directory, { withFileTypes: true })).filter((entry) => entry.name.endsWith(".log"));
|
|
773
|
+
if (!entries.length || entries.some((entry) => !entry.isFile())) throw new Error("Managed DevTools window-close evidence requires regular logs in the selected profile.");
|
|
774
|
+
return entries.map((entry) => entry.name).sort();
|
|
775
|
+
}
|
|
776
|
+
function identity(stat) {
|
|
777
|
+
return `${stat.dev}:${stat.ino}`;
|
|
778
|
+
}
|
|
779
|
+
/** 文件身份保留完整整数;只有已核验范围的字节位置才能交给 Node 读取 API。 */
|
|
780
|
+
function logSize(stat) {
|
|
781
|
+
if (stat.size < 0n || stat.size > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("Managed DevTools window-close log size exceeds the supported safe integer range.");
|
|
782
|
+
return Number(stat.size);
|
|
783
|
+
}
|
|
784
|
+
async function readBytes(file, offset, size) {
|
|
785
|
+
const buffer = Buffer.alloc(size);
|
|
786
|
+
const { bytesRead } = await file.read(buffer, 0, size, offset);
|
|
787
|
+
return buffer.subarray(0, bytesRead);
|
|
788
|
+
}
|
|
789
|
+
async function anchorAt(file, offset) {
|
|
790
|
+
const start = Math.max(0, offset - 256);
|
|
791
|
+
return createHash("sha256").update(await readBytes(file, start, offset - start)).digest("hex");
|
|
792
|
+
}
|
|
793
|
+
async function anchorAfterBytes(file, offset, bytes) {
|
|
794
|
+
const before = Math.min(offset, Math.max(0, 256 - bytes.length));
|
|
795
|
+
const prefix = before ? await readBytes(file, offset - before, before) : Buffer.alloc(0);
|
|
796
|
+
return createHash("sha256").update(prefix).update(bytes.subarray(Math.max(0, bytes.length - 256))).digest("hex");
|
|
797
|
+
}
|
|
798
|
+
async function openLogFile(directory, name) {
|
|
799
|
+
const filename = path.join(directory, name);
|
|
800
|
+
const before = await fs.lstat(filename, { bigint: true });
|
|
801
|
+
if (!before.isFile()) throw new Error("Managed DevTools window-close log must be a regular file without redirection.");
|
|
802
|
+
const file = await fs.open(filename, "r");
|
|
803
|
+
try {
|
|
804
|
+
const after = await file.stat({ bigint: true });
|
|
805
|
+
if (!after.isFile() || identity(after) !== identity(before)) throw new Error("Managed DevTools window-close log changed while opening its evidence stream.");
|
|
806
|
+
return file;
|
|
807
|
+
} catch (error) {
|
|
808
|
+
await file.close();
|
|
809
|
+
throw error;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
/** 主进程日志汇聚多个来源,轮转后的首条记录可能来自 BACKEND;只识别固定偏移内的完整行。 */
|
|
813
|
+
async function hasMainRecord(file, size, productVersion) {
|
|
814
|
+
const prefix = Buffer.alloc(headerLimit);
|
|
815
|
+
let prefixSize = 0;
|
|
816
|
+
for (let offset = 0; offset < size;) {
|
|
817
|
+
const bytes = await readBytes(file, offset, Math.min(size - offset, scanChunkSize));
|
|
818
|
+
if (!bytes.length) return false;
|
|
819
|
+
offset += bytes.length;
|
|
820
|
+
for (let start = 0; start < bytes.length;) {
|
|
821
|
+
const newline = bytes.indexOf(10, start);
|
|
822
|
+
const end = newline < 0 ? bytes.length : newline;
|
|
823
|
+
prefixSize += bytes.copy(prefix, prefixSize, start, Math.min(end, start + headerLimit - prefixSize));
|
|
824
|
+
if (newline < 0) break;
|
|
825
|
+
const line = prefix.toString("utf8", 0, prefixSize);
|
|
826
|
+
if (/^\[[^\]\r\n]+\]\[[A-Z]+\]\[([^\]\r\n]+)\]\[MAIN\]/.exec(line)?.[1] === productVersion) return true;
|
|
827
|
+
prefixSize = 0;
|
|
828
|
+
start = newline + 1;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
function requireUniqueMain(cursors) {
|
|
834
|
+
if (cursors.length !== 1) throw new Error("Managed DevTools window-close evidence requires one unambiguous MAIN log for the selected product version.");
|
|
835
|
+
return cursors;
|
|
836
|
+
}
|
|
837
|
+
/** 只接受已核验主进程持有的同一常规文件,不按名称或历史日志内容猜测。 */
|
|
838
|
+
async function captureActiveLogCursor(directory, active) {
|
|
839
|
+
if (!/^[^/\\]+\.log$/.test(active.name)) throw new Error("Managed DevTools active log must belong to the selected log directory.");
|
|
840
|
+
const file = await openLogFile(directory, active.name);
|
|
841
|
+
try {
|
|
842
|
+
const stat = await file.stat({ bigint: true });
|
|
843
|
+
if (identity(stat) !== active.identity) throw new Error("Managed DevTools active log changed before its close cursor was captured.");
|
|
844
|
+
const size = logSize(stat);
|
|
845
|
+
const last = size ? await readBytes(file, size - 1, 1) : void 0;
|
|
846
|
+
return [{
|
|
847
|
+
name: active.name,
|
|
848
|
+
identity: identity(stat),
|
|
849
|
+
offset: size,
|
|
850
|
+
anchor: await anchorAt(file, size),
|
|
851
|
+
skipPartialLine: last !== void 0 && last[0] !== 10
|
|
852
|
+
}];
|
|
853
|
+
} finally {
|
|
854
|
+
await file.close();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
async function validateCursor(file, cursor) {
|
|
858
|
+
if (!Number.isSafeInteger(cursor.offset) || cursor.offset < 0) throw new Error("Managed DevTools window-close log cursor offset exceeds the supported safe integer range.");
|
|
859
|
+
const stat = await file.stat({ bigint: true });
|
|
860
|
+
const size = logSize(stat);
|
|
861
|
+
if (!stat.isFile() || identity(stat) !== cursor.identity || size < cursor.offset || await anchorAt(file, cursor.offset) !== cursor.anchor) throw new Error("Managed DevTools window-close log was replaced, truncated, or rewritten; destruction evidence is unresolved.");
|
|
862
|
+
return size;
|
|
863
|
+
}
|
|
864
|
+
/** 关闭前固定唯一 MAIN 流;辅助日志和历史空日志不属于窗口销毁证据。 */
|
|
865
|
+
async function captureLogCursors(directory, productVersion) {
|
|
866
|
+
const cursors = [];
|
|
867
|
+
for (const name of await logFiles(directory)) {
|
|
868
|
+
const file = await openLogFile(directory, name);
|
|
869
|
+
try {
|
|
870
|
+
const stat = await file.stat({ bigint: true });
|
|
871
|
+
const size = logSize(stat);
|
|
872
|
+
if (!await hasMainRecord(file, size, productVersion)) continue;
|
|
873
|
+
const last = size ? await readBytes(file, size - 1, 1) : void 0;
|
|
874
|
+
cursors.push({
|
|
875
|
+
name,
|
|
876
|
+
identity: identity(stat),
|
|
877
|
+
offset: size,
|
|
878
|
+
anchor: await anchorAt(file, size),
|
|
879
|
+
skipPartialLine: last !== void 0 && last[0] !== 10
|
|
880
|
+
});
|
|
881
|
+
} finally {
|
|
882
|
+
await file.close();
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return requireUniqueMain(cursors);
|
|
886
|
+
}
|
|
887
|
+
/** 旧整目录门禁只能从原游标收窄;非空证据丢失或新文件均不能补充候选。 */
|
|
888
|
+
async function recoverLogCursors(directory, cursors, productVersion) {
|
|
889
|
+
const main = [];
|
|
890
|
+
for (const cursor of cursors) {
|
|
891
|
+
let file;
|
|
892
|
+
try {
|
|
893
|
+
file = await openLogFile(directory, cursor.name);
|
|
894
|
+
} catch (error) {
|
|
895
|
+
if (error.code === "ENOENT" && cursor.offset === 0 && cursor.anchor === emptyAnchor && !cursor.skipPartialLine) continue;
|
|
896
|
+
throw error;
|
|
897
|
+
}
|
|
898
|
+
try {
|
|
899
|
+
await validateCursor(file, cursor);
|
|
900
|
+
if (await hasMainRecord(file, cursor.offset, productVersion)) main.push({ ...cursor });
|
|
901
|
+
} finally {
|
|
902
|
+
await file.close();
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return requireUniqueMain(main);
|
|
906
|
+
}
|
|
907
|
+
/** 固定本轮所有流的终点并逐块消费;未完成的新增尾行、文件替换或截断均不能提前证明关闭成功。 */
|
|
908
|
+
async function consumeFreshLogLines(directory, cursors, consume) {
|
|
909
|
+
const streams = [];
|
|
910
|
+
let pending = 0;
|
|
911
|
+
let drained = true;
|
|
912
|
+
try {
|
|
913
|
+
for (const cursor of cursors) {
|
|
914
|
+
const file = await openLogFile(directory, cursor.name);
|
|
915
|
+
const stream = {
|
|
916
|
+
file,
|
|
917
|
+
cursor,
|
|
918
|
+
original: { ...cursor },
|
|
919
|
+
end: cursor.offset
|
|
920
|
+
};
|
|
921
|
+
streams.push(stream);
|
|
922
|
+
stream.end = await validateCursor(file, cursor);
|
|
923
|
+
pending += stream.end - cursor.offset;
|
|
924
|
+
if (pending > scanLimit) throw new Error("Managed DevTools window-close log backlog exceeds the supported evidence limit.");
|
|
925
|
+
}
|
|
926
|
+
for (const { file, cursor, end } of streams) while (cursor.offset < end) {
|
|
927
|
+
const size = Math.min(end - cursor.offset, readLimit);
|
|
928
|
+
const bytes = await readBytes(file, cursor.offset, size);
|
|
929
|
+
if (bytes.length !== size) throw new Error("Managed DevTools window-close log became shorter during its evidence read.");
|
|
930
|
+
const newline = bytes.lastIndexOf(10);
|
|
931
|
+
if (newline < 0) {
|
|
932
|
+
if (bytes.length === readLimit) throw new Error("Managed DevTools window-close log line exceeds the supported evidence limit.");
|
|
933
|
+
drained = false;
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
const anchor = await anchorAfterBytes(file, cursor.offset, bytes.subarray(0, newline + 1));
|
|
937
|
+
for (let start = 0; start <= newline;) {
|
|
938
|
+
const lineEnd = bytes.indexOf(10, start);
|
|
939
|
+
if (cursor.skipPartialLine) cursor.skipPartialLine = false;
|
|
940
|
+
else {
|
|
941
|
+
const textEnd = bytes[lineEnd - 1] === 13 ? lineEnd - 1 : lineEnd;
|
|
942
|
+
consume({
|
|
943
|
+
fileIdentity: cursor.identity,
|
|
944
|
+
line: bytes.toString("utf8", start, textEnd)
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
start = lineEnd + 1;
|
|
948
|
+
}
|
|
949
|
+
cursor.offset += newline + 1;
|
|
950
|
+
cursor.anchor = anchor;
|
|
951
|
+
}
|
|
952
|
+
for (const { file, cursor, original, end } of streams) {
|
|
953
|
+
await validateCursor(file, original);
|
|
954
|
+
await validateCursor(file, cursor);
|
|
955
|
+
const stat = await fs.lstat(path.join(directory, cursor.name), { bigint: true });
|
|
956
|
+
if (!stat.isFile() || identity(stat) !== cursor.identity || logSize(stat) < end) throw new Error("Managed DevTools window-close log changed while reading its evidence stream.");
|
|
957
|
+
}
|
|
958
|
+
return drained;
|
|
959
|
+
} finally {
|
|
960
|
+
await Promise.all(streams.map(({ file }) => file.close()));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
//#endregion
|
|
964
|
+
//#region src/devtoolsProjectOwnership/windowClose/activeLog.ts
|
|
965
|
+
const inspectionOptions = {
|
|
966
|
+
timeout: 3e3,
|
|
967
|
+
reject: false,
|
|
968
|
+
windowsHide: true
|
|
969
|
+
};
|
|
970
|
+
function invalidLog(reason) {
|
|
971
|
+
return /* @__PURE__ */ new Error(`Managed DevTools active MAIN log ${reason}; destruction evidence is unresolved.`);
|
|
972
|
+
}
|
|
973
|
+
async function parentPid(pid) {
|
|
974
|
+
const result = await execa("ps", [
|
|
975
|
+
"-p",
|
|
976
|
+
String(pid),
|
|
977
|
+
"-o",
|
|
978
|
+
"ppid="
|
|
979
|
+
], inspectionOptions);
|
|
980
|
+
const value = result.stdout.trim();
|
|
981
|
+
const parent = Number(value);
|
|
982
|
+
if (result.exitCode !== 0 || !/^\d+$/.test(value) || !Number.isSafeInteger(parent) || parent <= 0 || parent === pid) throw invalidLog("parent process could not be verified");
|
|
983
|
+
return parent;
|
|
984
|
+
}
|
|
985
|
+
async function currentIdentity(expected) {
|
|
986
|
+
const live = await readManagedProcessIdentity(expected.pid, "darwin");
|
|
987
|
+
if (!live || !sameManagedProcess(expected, live)) throw invalidLog("process identity changed");
|
|
988
|
+
return live;
|
|
989
|
+
}
|
|
990
|
+
async function selectedInstallation(record) {
|
|
991
|
+
const cliPath = await fs.realpath(record.target.cliPath);
|
|
992
|
+
const macos = path.dirname(cliPath);
|
|
993
|
+
const contents = path.dirname(macos);
|
|
994
|
+
const bundle = path.dirname(contents);
|
|
995
|
+
const appPath = await fs.realpath(record.target.appPath);
|
|
996
|
+
if (path.basename(macos) !== "MacOS" || path.basename(contents) !== "Contents" || !bundle.endsWith(".app") || ![
|
|
997
|
+
"app.asar",
|
|
998
|
+
"app",
|
|
999
|
+
"package.nw"
|
|
1000
|
+
].some((name) => appPath === path.join(contents, "Resources", name))) throw invalidLog("CLI and application resources do not identify one selected bundle");
|
|
1001
|
+
return {
|
|
1002
|
+
cliPath,
|
|
1003
|
+
appPath,
|
|
1004
|
+
bundle
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
async function assertLogDirectory(record, directory) {
|
|
1008
|
+
const profile = await fs.realpath(record.target.profileDir);
|
|
1009
|
+
if (directory !== path.join(profile, "WeappLog", "logs") || await fs.realpath(directory) !== directory) throw invalidLog("directory was redirected or does not belong to the selected profile");
|
|
1010
|
+
}
|
|
1011
|
+
async function mainHost(record, bundlePath) {
|
|
1012
|
+
const backend = await currentIdentity(record.host);
|
|
1013
|
+
const executable = await fs.realpath(backend.executable);
|
|
1014
|
+
const expected = path.join(bundlePath, "Contents", "MacOS", "Electron");
|
|
1015
|
+
if (executable === expected) return backend;
|
|
1016
|
+
if (!executable.startsWith(`${bundlePath}${path.sep}`)) throw invalidLog("backend belongs to a different installation");
|
|
1017
|
+
const main = await readManagedProcessIdentity(await parentPid(backend.pid), "darwin");
|
|
1018
|
+
if (!main || await fs.realpath(main.executable) !== expected) throw invalidLog("parent does not belong to the selected main executable");
|
|
1019
|
+
const mainStarted = Date.parse(main.started);
|
|
1020
|
+
const backendStarted = Date.parse(backend.started);
|
|
1021
|
+
if (!Number.isFinite(mainStarted) || !Number.isFinite(backendStarted) || mainStarted > backendStarted) throw invalidLog("parent process generation is inconsistent");
|
|
1022
|
+
return main;
|
|
1023
|
+
}
|
|
1024
|
+
/** NUL 字段保留文件名中的空白与换行;每个描述符必须来自唯一指定进程。 */
|
|
1025
|
+
function parseOpenFiles(output, pid) {
|
|
1026
|
+
const files = [];
|
|
1027
|
+
const properties = {
|
|
1028
|
+
a: "access",
|
|
1029
|
+
t: "type",
|
|
1030
|
+
D: "device",
|
|
1031
|
+
i: "inode",
|
|
1032
|
+
k: "links",
|
|
1033
|
+
n: "filename"
|
|
1034
|
+
};
|
|
1035
|
+
let processSeen = false;
|
|
1036
|
+
let current;
|
|
1037
|
+
for (const raw of output.split("\0")) {
|
|
1038
|
+
const field = raw.replace(/^\n/, "");
|
|
1039
|
+
if (!field) continue;
|
|
1040
|
+
const key = field[0];
|
|
1041
|
+
const value = field.slice(1);
|
|
1042
|
+
if (key === "p") {
|
|
1043
|
+
if (processSeen || value !== String(pid)) throw invalidLog("file descriptors belong to an ambiguous process");
|
|
1044
|
+
processSeen = true;
|
|
1045
|
+
} else if (key === "f" && processSeen) {
|
|
1046
|
+
current = { descriptor: value };
|
|
1047
|
+
files.push(current);
|
|
1048
|
+
} else if (current && key in properties) {
|
|
1049
|
+
const property = properties[key];
|
|
1050
|
+
if (current[property] !== void 0) throw invalidLog("file descriptor fields are ambiguous");
|
|
1051
|
+
current[property] = value;
|
|
1052
|
+
} else throw invalidLog("file descriptor output could not be verified");
|
|
1053
|
+
}
|
|
1054
|
+
if (!processSeen) throw invalidLog("main process has no file descriptor evidence");
|
|
1055
|
+
return files;
|
|
1056
|
+
}
|
|
1057
|
+
async function existingLog(file, directory) {
|
|
1058
|
+
if (!/^\d+$/.test(file.descriptor) || !["w", "u"].includes(file.access ?? "") || file.type !== "REG" || !file.filename?.endsWith(".log") || path.dirname(file.filename) !== directory) return;
|
|
1059
|
+
if (!file.links || !/^\d+$/.test(file.links) || !file.device || !/^0x[\da-f]+$/i.test(file.device) || !file.inode || !/^\d+$/.test(file.inode)) throw invalidLog("file descriptor identity could not be verified");
|
|
1060
|
+
if (BigInt(file.links) === 0n) return;
|
|
1061
|
+
const stat = await fs.lstat(file.filename, { bigint: true });
|
|
1062
|
+
if (!stat.isFile() || await fs.realpath(file.filename) !== file.filename || stat.dev !== BigInt(file.device) || stat.ino !== BigInt(file.inode) || stat.nlink !== BigInt(file.links)) throw invalidLog("file was redirected or replaced");
|
|
1063
|
+
return {
|
|
1064
|
+
name: path.basename(file.filename),
|
|
1065
|
+
identity: `${stat.dev}:${stat.ino}`
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
async function readWritableLogs(pid, directory) {
|
|
1069
|
+
const result = await execa("lsof", [
|
|
1070
|
+
"-nP",
|
|
1071
|
+
"-a",
|
|
1072
|
+
"-p",
|
|
1073
|
+
String(pid),
|
|
1074
|
+
"-F0pftainDk"
|
|
1075
|
+
], inspectionOptions);
|
|
1076
|
+
if (result.exitCode !== 0) throw invalidLog("file descriptor inspection failed");
|
|
1077
|
+
const files = parseOpenFiles(result.stdout, pid);
|
|
1078
|
+
const candidates = [];
|
|
1079
|
+
for (const file of files) {
|
|
1080
|
+
const candidate = await existingLog(file, directory);
|
|
1081
|
+
if (candidate) candidates.push({
|
|
1082
|
+
file,
|
|
1083
|
+
...candidate
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
if (!candidates.length) throw invalidLog("requires at least one verified writable file");
|
|
1087
|
+
const descriptors = /* @__PURE__ */ new Set();
|
|
1088
|
+
const identities = /* @__PURE__ */ new Map();
|
|
1089
|
+
for (const candidate of candidates) {
|
|
1090
|
+
if (descriptors.has(candidate.file.descriptor) || identities.has(candidate.identity) && identities.get(candidate.identity) !== candidate.name) throw invalidLog("file descriptors contain ambiguous aliases");
|
|
1091
|
+
descriptors.add(candidate.file.descriptor);
|
|
1092
|
+
identities.set(candidate.identity, candidate.name);
|
|
1093
|
+
}
|
|
1094
|
+
return candidates.sort((a, b) => Number(a.file.descriptor) - Number(b.file.descriptor));
|
|
1095
|
+
}
|
|
1096
|
+
/** 仅用已登记 backend 的直接父进程固定全部活动流;游标捕获前后核验完整描述符集合,不授予父进程清理权。 */
|
|
1097
|
+
async function captureActiveMainLogCursors(record, directory, platform = process.platform) {
|
|
1098
|
+
if (platform !== "darwin" || !record.host) return;
|
|
1099
|
+
await assertLogDirectory(record, directory);
|
|
1100
|
+
const installation = await selectedInstallation(record);
|
|
1101
|
+
const main = await mainHost(record, installation.bundle);
|
|
1102
|
+
const candidates = await readWritableLogs(main.pid, directory);
|
|
1103
|
+
const cursors = [];
|
|
1104
|
+
for (const candidate of candidates) if (!cursors.some((cursor) => cursor.identity === candidate.identity)) cursors.push(...await captureActiveLogCursor(directory, candidate));
|
|
1105
|
+
const retained = await readWritableLogs(main.pid, directory);
|
|
1106
|
+
if (retained.length !== candidates.length || retained.some((file, index) => {
|
|
1107
|
+
const candidate = candidates[index];
|
|
1108
|
+
return file.file.descriptor !== candidate.file.descriptor || file.name !== candidate.name || file.identity !== candidate.identity || file.file.access !== candidate.file.access || file.file.links !== candidate.file.links;
|
|
1109
|
+
})) throw invalidLog("file descriptor set changed during capture");
|
|
1110
|
+
const after = await mainHost(record, installation.bundle);
|
|
1111
|
+
const selected = await selectedInstallation(record);
|
|
1112
|
+
if (!sameManagedProcess(main, after) || selected.appPath !== installation.appPath || selected.cliPath !== installation.cliPath || selected.bundle !== installation.bundle) throw invalidLog("main process or selected directory changed");
|
|
1113
|
+
await assertLogDirectory(record, directory);
|
|
1114
|
+
for (const candidate of candidates) {
|
|
1115
|
+
const verified = await existingLog(candidate.file, directory);
|
|
1116
|
+
if (!verified || verified.identity !== candidate.identity) throw invalidLog("file changed during capture");
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
cursors,
|
|
1120
|
+
host: main
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
//#endregion
|
|
1124
|
+
//#region src/devtoolsProjectOwnership/windowClose/protocol.ts
|
|
1125
|
+
function decodeString(value) {
|
|
1126
|
+
const quote = value[0];
|
|
1127
|
+
if (!quote || ![
|
|
1128
|
+
"'",
|
|
1129
|
+
"\"",
|
|
1130
|
+
"`"
|
|
1131
|
+
].includes(quote) || value.at(-1) !== quote) return;
|
|
1132
|
+
let result = "";
|
|
1133
|
+
for (let index = 1; index < value.length - 1; index++) {
|
|
1134
|
+
const char = value[index];
|
|
1135
|
+
if (char === quote) throw new Error("Unsupported quoted window-close evidence.");
|
|
1136
|
+
if (char !== "\\") {
|
|
1137
|
+
result += char;
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
const escaped = value[++index];
|
|
1141
|
+
if ([
|
|
1142
|
+
"\\",
|
|
1143
|
+
"'",
|
|
1144
|
+
"\"",
|
|
1145
|
+
"`"
|
|
1146
|
+
].includes(escaped)) result += escaped;
|
|
1147
|
+
else if (escaped === "x" || escaped === "u") {
|
|
1148
|
+
const length = escaped === "x" ? 2 : 4;
|
|
1149
|
+
const digits = value.slice(index + 1, index + 1 + length);
|
|
1150
|
+
if (digits.length !== length || !/^[\da-f]+$/i.test(digits)) throw new Error("Unsupported escaped window-close evidence.");
|
|
1151
|
+
result += String.fromCharCode(Number.parseInt(digits, 16));
|
|
1152
|
+
index += length;
|
|
1153
|
+
} else {
|
|
1154
|
+
const escapes = {
|
|
1155
|
+
n: "\n",
|
|
1156
|
+
r: "\r",
|
|
1157
|
+
t: " ",
|
|
1158
|
+
b: "\b",
|
|
1159
|
+
f: "\f",
|
|
1160
|
+
v: "\v",
|
|
1161
|
+
0: "\0"
|
|
1162
|
+
};
|
|
1163
|
+
if (!(escaped in escapes)) throw new Error("Unsupported escaped window-close evidence.");
|
|
1164
|
+
result += escapes[escaped];
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return result;
|
|
1168
|
+
}
|
|
1169
|
+
/** 只读取 inspect 对象的顶层标量;日志从不作为 JavaScript 执行。 */
|
|
1170
|
+
function fields(source) {
|
|
1171
|
+
if (!source.startsWith("{") || !source.endsWith("}")) throw new Error("Incomplete window-close trace object.");
|
|
1172
|
+
const parts = [];
|
|
1173
|
+
let quote = "";
|
|
1174
|
+
let depth = 0;
|
|
1175
|
+
let start = 1;
|
|
1176
|
+
for (let index = 1; index < source.length - 1; index++) {
|
|
1177
|
+
const char = source[index];
|
|
1178
|
+
if (quote) {
|
|
1179
|
+
if (char === "\\") index++;
|
|
1180
|
+
else if (char === quote) quote = "";
|
|
1181
|
+
} else if ([
|
|
1182
|
+
"'",
|
|
1183
|
+
"\"",
|
|
1184
|
+
"`"
|
|
1185
|
+
].includes(char)) quote = char;
|
|
1186
|
+
else if (char === "{" || char === "[") depth++;
|
|
1187
|
+
else if (char === "}" || char === "]") depth--;
|
|
1188
|
+
else if (char === "," && depth === 0) {
|
|
1189
|
+
parts.push(source.slice(start, index));
|
|
1190
|
+
start = index + 1;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (quote || depth !== 0) throw new Error("Incomplete window-close trace fields.");
|
|
1194
|
+
parts.push(source.slice(start, -1));
|
|
1195
|
+
const result = /* @__PURE__ */ new Map();
|
|
1196
|
+
for (const part of parts) {
|
|
1197
|
+
const match = /^(\w+):([\s\S]+)$/.exec(part.trim());
|
|
1198
|
+
if (!match || result.has(match[1])) throw new Error("Ambiguous window-close trace fields.");
|
|
1199
|
+
const value = match[2].trim();
|
|
1200
|
+
result.set(match[1], value === "true" ? true : value === "false" ? false : /^\d+$/.test(value) ? Number(value) : decodeString(value));
|
|
1201
|
+
}
|
|
1202
|
+
return result;
|
|
1203
|
+
}
|
|
1204
|
+
/** 目前核对过的协议来自所选 Stable 2.02.2608070 的主进程关闭日志。 */
|
|
1205
|
+
function consumeWindowCloseTrace(evidence, projectPath, input) {
|
|
1206
|
+
if (/^\[[^\]]+\]\[(?:WARN|INFO)\]\[[^\]]+\]\[MAIN\].*\benvMessagerService setuped\s*$/.test(input.line)) throw new Error("Managed DevTools MAIN log restarted after the close cursor; host generation is unresolved.");
|
|
1207
|
+
const match = /^\[([^\]]+)\]\[(?:WARN|INFO)\]\[([^\]]+)\]\[MAIN\].*? \[win-close-trace\] ([\w:-]+) (.*)$/.exec(input.line);
|
|
1208
|
+
if (!match) return;
|
|
1209
|
+
const [, timestamp, version, event, source] = match;
|
|
1210
|
+
if (![
|
|
1211
|
+
"native-close-call",
|
|
1212
|
+
"close-requested",
|
|
1213
|
+
"native-window-close-cancelled",
|
|
1214
|
+
"native-window-closed",
|
|
1215
|
+
"webcontents-destroyed"
|
|
1216
|
+
].includes(event)) return;
|
|
1217
|
+
if (version !== evidence.productVersion) throw new Error("Managed DevTools window-close trace belongs to a different product version.");
|
|
1218
|
+
const at = new Date(timestamp.replace(" ", "T")).toISOString();
|
|
1219
|
+
if (at < evidence.capturedAt) throw new Error("Managed DevTools window-close trace predates this close attempt.");
|
|
1220
|
+
const values = fields(source);
|
|
1221
|
+
const winId = values.get("winId");
|
|
1222
|
+
if (typeof winId !== "string" || !winId) throw new Error("Managed DevTools window-close trace has no window identity.");
|
|
1223
|
+
const sameWindow = evidence.window?.winId === winId && evidence.window.fileIdentity === input.fileIdentity;
|
|
1224
|
+
if (event === "native-close-call") {
|
|
1225
|
+
const browserWindowId = values.get("browserWindowId");
|
|
1226
|
+
if (typeof browserWindowId !== "number" || !Number.isSafeInteger(browserWindowId) || browserWindowId <= 0) throw new Error("Managed DevTools close call has no native window identity.");
|
|
1227
|
+
if (sameWindow && (browserWindowId !== evidence.window.browserWindowId || values.get("forceClose") !== true)) throw new Error("Managed DevTools window-close identity was reused or another close was requested.");
|
|
1228
|
+
if (values.get("forceClose") === false) {
|
|
1229
|
+
evidence.calls.push({
|
|
1230
|
+
fileIdentity: input.fileIdentity,
|
|
1231
|
+
winId,
|
|
1232
|
+
browserWindowId,
|
|
1233
|
+
calledAt: at
|
|
1234
|
+
});
|
|
1235
|
+
if (evidence.calls.length > 100) throw new Error("Managed DevTools close call evidence is ambiguous.");
|
|
1236
|
+
}
|
|
1237
|
+
} else if (event === "close-requested") {
|
|
1238
|
+
const projectId = values.get("projectId");
|
|
1239
|
+
if (!(typeof projectId === "string" && path.isAbsolute(projectId) && path.normalize(projectId) === path.normalize(projectPath))) {
|
|
1240
|
+
if (sameWindow) throw new Error("Managed DevTools window-close project identity changed.");
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
const calls = evidence.calls.filter((call) => call.winId === winId && call.fileIdentity === input.fileIdentity);
|
|
1244
|
+
const runtimeId = values.get("runtimeId");
|
|
1245
|
+
if (evidence.window || calls.length !== 1 || calls[0].cancelled || typeof runtimeId !== "string" || !runtimeId || values.get("windowType") !== "project" || values.get("status") !== "opened" || calls[0].calledAt > at) throw new Error("Managed DevTools exact project close request has ambiguous window identity.");
|
|
1246
|
+
evidence.window = {
|
|
1247
|
+
...calls[0],
|
|
1248
|
+
runtimeId,
|
|
1249
|
+
requestedAt: at
|
|
1250
|
+
};
|
|
1251
|
+
} else if (event === "native-window-close-cancelled") {
|
|
1252
|
+
for (const call of evidence.calls.filter((call) => call.winId === winId && call.fileIdentity === input.fileIdentity)) call.cancelled = true;
|
|
1253
|
+
if (sameWindow) throw new Error("Managed DevTools exact project close was cancelled.");
|
|
1254
|
+
} else if (sameWindow && evidence.window) {
|
|
1255
|
+
if (at < evidence.window.requestedAt) throw new Error("Managed DevTools window destruction predates its exact close request.");
|
|
1256
|
+
if (event === "native-window-closed") {
|
|
1257
|
+
if (values.get("wasClosing") !== true) throw new Error("Managed DevTools native window closed without its pending close request.");
|
|
1258
|
+
evidence.window.nativeClosedAt = at;
|
|
1259
|
+
} else if (event === "webcontents-destroyed") {
|
|
1260
|
+
if (values.get("browserWindowId") !== evidence.window.browserWindowId || values.get("destroyed") !== true) throw new Error("Managed DevTools webcontents destruction has a different native window identity.");
|
|
1261
|
+
evidence.window.webContentsDestroyedAt = at;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
//#endregion
|
|
1266
|
+
//#region src/devtoolsProjectOwnership/windowClose.ts
|
|
1267
|
+
/** 只捕获本次显式安装的日志,必须先于关闭命令持久化。 */
|
|
1268
|
+
async function captureManagedWindowClose(record) {
|
|
1269
|
+
if (!record.target.version) throw new Error("Managed DevTools window-close evidence requires the selected product version.");
|
|
1270
|
+
const profileDir = await fs.realpath(record.target.profileDir);
|
|
1271
|
+
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1272
|
+
const directory = await selectedLogDirectory(profileDir);
|
|
1273
|
+
const active = await captureActiveMainLogCursors(record, directory);
|
|
1274
|
+
const cursors = active ? active.cursors : await captureLogCursors(directory, record.target.version);
|
|
1275
|
+
return {
|
|
1276
|
+
protocol: "wechat-devtools-window-close-trace-v1",
|
|
1277
|
+
profileDir,
|
|
1278
|
+
productVersion: record.target.version,
|
|
1279
|
+
capturedAt,
|
|
1280
|
+
...active ? { mainHost: active.host } : {},
|
|
1281
|
+
cursors,
|
|
1282
|
+
calls: []
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
/** 持久化每轮证据,worker 退出后只能续读同一游标,不能重新关闭同路径。 */
|
|
1286
|
+
async function waitForManagedWindowClosed(record, persist, options = {}) {
|
|
1287
|
+
const evidence = record.windowClose;
|
|
1288
|
+
if (!evidence || !evidence.dispatchedAt) throw new Error("Managed DevTools project close has no durable native-window evidence cursor.");
|
|
1289
|
+
if (evidence.failure) {
|
|
1290
|
+
if (evidence.failure !== "Managed DevTools window-close log set changed; host generation or log rotation is unresolved.") throw new Error(evidence.failure);
|
|
1291
|
+
if (await fs.realpath(record.target.profileDir) !== evidence.profileDir || record.target.version !== evidence.productVersion) throw new Error("Managed DevTools window-close evidence no longer matches its selected installation.");
|
|
1292
|
+
const cursors = await recoverLogCursors(await selectedLogDirectory(evidence.profileDir), evidence.cursors, evidence.productVersion);
|
|
1293
|
+
const mainIdentity = cursors[0].identity;
|
|
1294
|
+
if (evidence.calls.some((call) => call.fileIdentity !== mainIdentity) || evidence.window && evidence.window.fileIdentity !== mainIdentity) throw new Error("Managed DevTools legacy window-close references do not belong to its original MAIN evidence stream.");
|
|
1295
|
+
evidence.logInventoryRecovery = {
|
|
1296
|
+
failure: evidence.failure,
|
|
1297
|
+
cursors: evidence.cursors.map((cursor) => ({ ...cursor }))
|
|
1298
|
+
};
|
|
1299
|
+
evidence.cursors = cursors;
|
|
1300
|
+
delete evidence.failure;
|
|
1301
|
+
await persist();
|
|
1302
|
+
}
|
|
1303
|
+
const deadline = Date.now() + (options.timeoutMs ?? 15e3);
|
|
1304
|
+
do {
|
|
1305
|
+
let drained = false;
|
|
1306
|
+
try {
|
|
1307
|
+
if (await fs.realpath(record.target.profileDir) !== evidence.profileDir || record.target.version !== evidence.productVersion) throw new Error("Managed DevTools window-close evidence no longer matches its selected installation.");
|
|
1308
|
+
if (evidence.mainHost) {
|
|
1309
|
+
const current = await readManagedProcessIdentity(evidence.mainHost.pid);
|
|
1310
|
+
if (!current || !sameManagedProcess(current, evidence.mainHost)) throw new Error("Managed DevTools main log owner changed; destruction evidence is unresolved.");
|
|
1311
|
+
}
|
|
1312
|
+
drained = await consumeFreshLogLines(await selectedLogDirectory(evidence.profileDir), evidence.cursors, (line) => {
|
|
1313
|
+
consumeWindowCloseTrace(evidence, record.projectPath, line);
|
|
1314
|
+
});
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
evidence.failure = error instanceof Error ? error.message : String(error);
|
|
1317
|
+
await persist();
|
|
1318
|
+
throw error;
|
|
1319
|
+
}
|
|
1320
|
+
await persist();
|
|
1321
|
+
if (drained && evidence.window?.nativeClosedAt && evidence.window.webContentsDestroyedAt) return;
|
|
1322
|
+
if (Date.now() >= deadline) break;
|
|
1323
|
+
await setTimeout(options.pollMs ?? 100);
|
|
1324
|
+
} while (true);
|
|
1325
|
+
throw new Error("Managed DevTools project close has no complete native-window destruction evidence; stop the acceptance lane.");
|
|
1326
|
+
}
|
|
1327
|
+
//#endregion
|
|
1328
|
+
//#region src/devtoolsProjectOwnership/installationExit/darwin.ts
|
|
1329
|
+
const options = {
|
|
1330
|
+
timeout: 1e4,
|
|
1331
|
+
reject: false,
|
|
1332
|
+
env: { LC_ALL: "C" }
|
|
1333
|
+
};
|
|
1334
|
+
const pathSchema = z.array(z.object({
|
|
1335
|
+
pid: z.number().int().positive(),
|
|
1336
|
+
executable: z.string().nullable()
|
|
1337
|
+
}).strict());
|
|
1338
|
+
const kernelPathsScript = `
|
|
1339
|
+
ObjC.import('Foundation');
|
|
1340
|
+
ObjC.bindFunction('malloc', ['void *', ['unsigned long']]);
|
|
1341
|
+
ObjC.bindFunction('free', ['void', ['void *']]);
|
|
1342
|
+
ObjC.bindFunction('proc_pidpath', ['int', ['int', 'void *', 'uint32_t']]);
|
|
1343
|
+
ObjC.bindFunction('strchr', ['char *', ['void *', 'int']]);
|
|
1344
|
+
function run(argv) {
|
|
1345
|
+
var pids = JSON.parse(argv[0]);
|
|
1346
|
+
var buffer = $.malloc(4096);
|
|
1347
|
+
if (!buffer) throw new Error('Cannot allocate process path buffer.');
|
|
1348
|
+
try {
|
|
1349
|
+
return JSON.stringify(pids.map(function(pid) {
|
|
1350
|
+
var count = $.proc_pidpath(pid, buffer, 4096);
|
|
1351
|
+
return { pid: pid, executable: count > 0 ? $.strchr(buffer, 47) : null };
|
|
1352
|
+
}));
|
|
1353
|
+
} finally { $.free(buffer); }
|
|
1354
|
+
}
|
|
1355
|
+
`;
|
|
1356
|
+
/** ps 仅用于 PID、启动时间和僵尸状态,不能用可修改的 comm 代替实际可执行文件。 */
|
|
1357
|
+
async function readDarwinProcessStates() {
|
|
1358
|
+
const inspection = execa("/bin/ps", ["-axo", "pid=,stat=,lstart="], options);
|
|
1359
|
+
const inspectorPid = inspection.pid;
|
|
1360
|
+
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.");
|
|
1362
|
+
const records = result.stdout.trim().split(/\r?\n/).map((line) => {
|
|
1363
|
+
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
|
+
const pid = Number(match?.[1]);
|
|
1365
|
+
if (!match || !Number.isSafeInteger(pid) || pid <= 0) throw new Error("Installation-exit recovery found an unverifiable process inventory entry.");
|
|
1366
|
+
return {
|
|
1367
|
+
pid,
|
|
1368
|
+
zombie: match[2].startsWith("Z"),
|
|
1369
|
+
started: match[3].replace(/\s+/g, " ")
|
|
1370
|
+
};
|
|
1371
|
+
});
|
|
1372
|
+
if (new Set(records.map((record) => record.pid)).size !== records.length) throw new Error("Installation-exit recovery found duplicate process identities.");
|
|
1373
|
+
return records.filter((record) => record.pid !== inspectorPid);
|
|
1374
|
+
}
|
|
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
|
+
/** 已删除可执行文件无法经 proc_pidpath 读取时,核验全部 program-text 映像而非猜测首项。 */
|
|
1396
|
+
async function readDarwinTextImages(pid) {
|
|
1397
|
+
const result = await execa("/usr/sbin/lsof", [
|
|
1398
|
+
"-nP",
|
|
1399
|
+
"-a",
|
|
1400
|
+
"-p",
|
|
1401
|
+
String(pid),
|
|
1402
|
+
"-d",
|
|
1403
|
+
"txt",
|
|
1404
|
+
"-F0pftn"
|
|
1405
|
+
], options);
|
|
1406
|
+
if (result.exitCode !== 0 || result.stderr?.trim()) throw new Error("Installation-exit recovery cannot inspect all executable text images.");
|
|
1407
|
+
const fields = result.stdout.split("\0").map((field) => field.replace(/^\r?\n/, "")).filter(Boolean);
|
|
1408
|
+
if (fields.shift() !== `p${pid}` || fields.length === 0 || fields.length % 3 !== 0) throw new Error("Installation-exit recovery received incomplete executable text images.");
|
|
1409
|
+
const paths = [];
|
|
1410
|
+
for (let index = 0; index < fields.length; index += 3) {
|
|
1411
|
+
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));
|
|
1413
|
+
}
|
|
1414
|
+
return paths;
|
|
1415
|
+
}
|
|
1416
|
+
//#endregion
|
|
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
|
+
}
|
|
1428
|
+
/** 仅支持已验证的 macOS 全安装清单;未知平台、权限错误或不完整映像均拒绝推断退出。 */
|
|
1429
|
+
async function inspectExitedWechatInstallation(target, platform = process.platform) {
|
|
1430
|
+
if (platform !== "darwin") throw new Error("Installation-exit recovery requires the verified macOS process inventory.");
|
|
1431
|
+
const selectedRoot = resolveWechatDevtoolsInstallationRoot(target, platform);
|
|
1432
|
+
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
|
+
const installationRoot = await fs.realpath(selectedRoot);
|
|
1434
|
+
let pending = await readDarwinProcessStates();
|
|
1435
|
+
const verified = /* @__PURE__ */ new Map();
|
|
1436
|
+
let kernelPathProcessCount = 0;
|
|
1437
|
+
let textImageProcessCount = 0;
|
|
1438
|
+
let exitedProcessCount = 0;
|
|
1439
|
+
let zombieProcessCount = 0;
|
|
1440
|
+
for (let round = 0; round < 3; round++) {
|
|
1441
|
+
const kernelPaths = await readDarwinKernelPaths(pending.map((entry) => entry.pid));
|
|
1442
|
+
const after = new Map((await readDarwinProcessStates()).map((entry) => [entry.pid, entry]));
|
|
1443
|
+
for (const entry of pending) {
|
|
1444
|
+
const current = after.get(entry.pid);
|
|
1445
|
+
if (!current) {
|
|
1446
|
+
exitedProcessCount++;
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
if (current.started !== entry.started) throw new Error("Installation-exit recovery process identity changed during inspection.");
|
|
1450
|
+
if (entry.zombie && current.zombie) {
|
|
1451
|
+
zombieProcessCount++;
|
|
1452
|
+
verified.set(entry.pid, current);
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
const executable = kernelPaths.get(entry.pid);
|
|
1456
|
+
const images = executable ? [executable] : await readDarwinTextImages(entry.pid);
|
|
1457
|
+
if (!images.length) throw new Error("Installation-exit recovery received no verifiable executable text images.");
|
|
1458
|
+
for (const image of images) {
|
|
1459
|
+
const resolved = await canonicalImagePath(image);
|
|
1460
|
+
const relative = path.relative(installationRoot, resolved);
|
|
1461
|
+
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
|
+
}
|
|
1463
|
+
if (executable) kernelPathProcessCount++;
|
|
1464
|
+
else textImageProcessCount++;
|
|
1465
|
+
verified.set(entry.pid, {
|
|
1466
|
+
started: entry.started,
|
|
1467
|
+
zombie: false
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
const final = await readDarwinProcessStates();
|
|
1471
|
+
for (const entry of final) {
|
|
1472
|
+
const previous = verified.get(entry.pid);
|
|
1473
|
+
if (previous && (previous.started !== entry.started || previous.zombie && !entry.zombie)) throw new Error("Installation-exit recovery process identity changed during inspection.");
|
|
1474
|
+
}
|
|
1475
|
+
pending = final.filter((entry) => !verified.has(entry.pid));
|
|
1476
|
+
if (!pending.length) break;
|
|
1477
|
+
}
|
|
1478
|
+
if (pending.length) throw new Error("Installation-exit recovery found continuing new process activity; retry after it becomes idle.");
|
|
1479
|
+
return {
|
|
1480
|
+
platform,
|
|
1481
|
+
installationRoot,
|
|
1482
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1483
|
+
inspectedProcessCount: kernelPathProcessCount + textImageProcessCount,
|
|
1484
|
+
kernelPathProcessCount,
|
|
1485
|
+
textImageProcessCount,
|
|
1486
|
+
exitedProcessCount,
|
|
1487
|
+
zombieProcessCount,
|
|
1488
|
+
selectedProcessCount: 0
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
//#endregion
|
|
1492
|
+
//#region src/devtoolsProjectOwnership/installationExit/index.ts
|
|
1493
|
+
async function fingerprint(record) {
|
|
1494
|
+
const file = managedRecordPath(record.journalPath, record.id);
|
|
1495
|
+
const stat = await fs.lstat(file);
|
|
1496
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) throw new Error("Installation-exit recovery requires an unchanged regular journal record.");
|
|
1497
|
+
return createHash("sha256").update(await fs.readFile(file)).digest("hex");
|
|
1498
|
+
}
|
|
1499
|
+
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;
|
|
1501
|
+
}
|
|
1502
|
+
async function recoverAfterExit(options) {
|
|
1503
|
+
const { target, recoveryScope } = options;
|
|
1504
|
+
await assertMachineE2ELeaseRecoveryScope(recoveryScope);
|
|
1505
|
+
const journalPath = recoveryScope.cleanupKey;
|
|
1506
|
+
if (!journalPath || !path.isAbsolute(journalPath) || path.resolve(journalPath) !== journalPath) throw new Error("Installation-exit recovery requires its exact registered journal cleanup key.");
|
|
1507
|
+
const binding = await resolveManagedJournalScope(journalPath);
|
|
1508
|
+
if (!binding.scopeId) throw new Error("Installation-exit recovery requires an explicitly registered task journal scope.");
|
|
1509
|
+
return withManagedJournalLock(journalPath, async (rootPath) => {
|
|
1510
|
+
if (rootPath !== binding.rootPath || !isDeepStrictEqual(await resolveManagedJournalScope(journalPath), binding)) throw new Error("Installation-exit recovery journal ownership changed.");
|
|
1511
|
+
const records = await readManagedWechatProjectRecords(rootPath);
|
|
1512
|
+
if (new Set(records.map((record) => record.id)).size !== records.length) throw new Error("Installation-exit recovery found duplicate record identities.");
|
|
1513
|
+
if (records.some((record) => !isDeepStrictEqual(record.target, target))) throw new Error("Installation-exit recovery cannot include another installation or profile.");
|
|
1514
|
+
if (records.some((record) => record.state !== "released" && !canRecoverAfterExit(record))) throw new Error("Installation-exit recovery refuses confirmed or unresolved project-close ownership.");
|
|
1515
|
+
const candidates = (await readManagedWechatProjectRecords(journalPath)).filter(canRecoverAfterExit);
|
|
1516
|
+
if (!candidates.length) {
|
|
1517
|
+
await assertMachineE2ELeaseRecoveryScope(recoveryScope);
|
|
1518
|
+
return { recoveredRecordIds: [] };
|
|
1519
|
+
}
|
|
1520
|
+
const before = new Map(await Promise.all(records.map(async (record) => [record.id, await fingerprint(record)])));
|
|
1521
|
+
const ownerPids = [...new Set(records.map((record) => record.ownerPid))].sort((a, b) => a - b);
|
|
1522
|
+
const ports = [...new Set(records.map((record) => record.port))];
|
|
1523
|
+
if (ports.includes(void 0)) throw new Error("Installation-exit recovery requires every registered automator port.");
|
|
1524
|
+
const closedPorts = ports.sort((a, b) => a - b);
|
|
1525
|
+
const verifyExit = async () => {
|
|
1526
|
+
await assertMachineE2ELeaseRecoveryScope(recoveryScope);
|
|
1527
|
+
await assertManagedInstallation(target);
|
|
1528
|
+
for (const pid of ownerPids) if (await readManagedProcessIdentity(pid)) throw new Error("Installation-exit recovery requires every journal owner process to have stopped.");
|
|
1529
|
+
for (const port of closedPorts) if (!await isManagedPortClosed(port)) throw new Error("Installation-exit recovery found a live registered automator port.");
|
|
1530
|
+
return inspectExitedWechatInstallation(target);
|
|
1531
|
+
};
|
|
1532
|
+
const assertUnchanged = async () => {
|
|
1533
|
+
if (!isDeepStrictEqual(await resolveManagedJournalScope(journalPath), binding)) throw new Error("Installation-exit recovery journal ownership changed.");
|
|
1534
|
+
const current = await readManagedWechatProjectRecords(rootPath);
|
|
1535
|
+
if (!isDeepStrictEqual(current, records)) throw new Error("Installation-exit recovery journal records changed during verification.");
|
|
1536
|
+
for (const record of current) if (await fingerprint(record) !== before.get(record.id)) throw new Error("Installation-exit recovery journal bytes changed during verification.");
|
|
1537
|
+
};
|
|
1538
|
+
await verifyExit();
|
|
1539
|
+
await assertUnchanged();
|
|
1540
|
+
const inspection = await verifyExit();
|
|
1541
|
+
await assertUnchanged();
|
|
1542
|
+
const recoveredRecordIds = [];
|
|
1543
|
+
for (const record of candidates) {
|
|
1544
|
+
await assertMachineE2ELeaseRecoveryScope(recoveryScope);
|
|
1545
|
+
if (await fingerprint(record) !== before.get(record.id)) throw new Error("Installation-exit recovery journal bytes changed before recording recovery.");
|
|
1546
|
+
const previousState = record.state;
|
|
1547
|
+
if (previousState !== "starting" && previousState !== "unconfirmed" && previousState !== "failed") throw new Error("Installation-exit recovery cannot replace a confirmed ownership state.");
|
|
1548
|
+
record.installationExitRecovery = {
|
|
1549
|
+
protocol: "wechat-devtools-installation-exit-v1",
|
|
1550
|
+
recoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1551
|
+
recoveryScopeId: recoveryScope.id,
|
|
1552
|
+
journalScopeId: binding.scopeId,
|
|
1553
|
+
journalRootPath: rootPath,
|
|
1554
|
+
installationId: target.installationId,
|
|
1555
|
+
profileDir: target.profileDir,
|
|
1556
|
+
previous: {
|
|
1557
|
+
state: previousState,
|
|
1558
|
+
error: record.error,
|
|
1559
|
+
updatedAt: record.updatedAt,
|
|
1560
|
+
recordSha256: before.get(record.id)
|
|
1561
|
+
},
|
|
1562
|
+
stoppedOwnerPids: ownerPids,
|
|
1563
|
+
closedPorts,
|
|
1564
|
+
processInspection: inspection
|
|
1565
|
+
};
|
|
1566
|
+
record.state = "released";
|
|
1567
|
+
record.releasedReason = "installation-exited";
|
|
1568
|
+
await writeManagedRecord(record);
|
|
1569
|
+
recoveredRecordIds.push(record.id);
|
|
1570
|
+
}
|
|
1571
|
+
return { recoveredRecordIds };
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
/** 在精确恢复租约中终结已退出安装的未知启动或失败关窗;保留原证据,不补造销毁事件。 */
|
|
1575
|
+
function recoverManagedWechatProjectsAfterInstallationExit(options) {
|
|
1576
|
+
return withMachineE2ELeaseRecoveryOperation(options.recoveryScope, () => recoverAfterExit(options));
|
|
1577
|
+
}
|
|
1578
|
+
//#endregion
|
|
1579
|
+
//#region src/devtoolsProjectOwnership/index.ts
|
|
1580
|
+
const pendingClosures = /* @__PURE__ */ new Map();
|
|
1581
|
+
function describe(error) {
|
|
1582
|
+
return error instanceof Error ? error.message : String(error);
|
|
1583
|
+
}
|
|
1584
|
+
async function release(record, reason) {
|
|
1585
|
+
record.state = "released";
|
|
1586
|
+
record.releasedReason = reason;
|
|
1587
|
+
delete record.error;
|
|
1588
|
+
await writeManagedRecord(record);
|
|
1589
|
+
}
|
|
1590
|
+
async function closeRecord(record) {
|
|
1591
|
+
if (record.state === "released") return;
|
|
1592
|
+
if (record.state === "borrowed" && record.openedProjectWindow === false) {
|
|
1593
|
+
await release(record, "borrowed");
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
if (record.state === "starting" || record.state === "unconfirmed" || record.openedProjectWindow !== true || !record.host || !record.port) throw new Error("Managed DevTools project ownership is unconfirmed; refusing to close a project or start another task.");
|
|
1597
|
+
try {
|
|
1598
|
+
if (record.closeAcknowledgedAt || record.windowClose?.dispatchedAt) {
|
|
1599
|
+
await waitForManagedWindowClosed(record, () => writeManagedRecord(record));
|
|
1600
|
+
await waitForManagedPortClosed(record.port);
|
|
1601
|
+
await release(record, "project-closed");
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
const live = await readManagedProcessIdentity(record.host.pid);
|
|
1605
|
+
if (!live) throw new Error("Managed DevTools automator listener exited without a confirmed project close; window ownership remains unresolved.");
|
|
1606
|
+
if (!sameManagedProcess(live, record.host)) throw new Error("Managed DevTools host identity changed; no project was closed.");
|
|
1607
|
+
if (await isManagedPortClosed(record.port)) throw new Error("Managed DevTools automator port disappeared without a confirmed project close; window ownership remains unresolved.");
|
|
1608
|
+
await assertManagedInstallation(record.target);
|
|
1609
|
+
if (!sameManagedProcess(await inspectManagedProjectHost(record.target, record.port), record.host)) throw new Error("Managed DevTools port belongs to a different host; no project was closed.");
|
|
1610
|
+
record.state = "closing";
|
|
1611
|
+
record.windowClose = await captureManagedWindowClose(record);
|
|
1612
|
+
record.windowClose.dispatchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1613
|
+
await writeManagedRecord(record);
|
|
1614
|
+
await execa(record.target.cliPath, [
|
|
1615
|
+
"close",
|
|
1616
|
+
"--project",
|
|
1617
|
+
record.projectPath
|
|
1618
|
+
], {
|
|
1619
|
+
timeout: 3e4,
|
|
1620
|
+
windowsHide: true
|
|
1621
|
+
});
|
|
1622
|
+
record.closeAcknowledgedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1623
|
+
await writeManagedRecord(record);
|
|
1624
|
+
await waitForManagedWindowClosed(record, () => writeManagedRecord(record));
|
|
1625
|
+
await waitForManagedPortClosed(record.port);
|
|
1626
|
+
await release(record, "project-closed");
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
record.state = "failed";
|
|
1629
|
+
record.error = describe(error);
|
|
1630
|
+
await writeManagedRecord(record);
|
|
1631
|
+
throw error;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
/** 窗口销毁与连接断开分别验证;失败保留登记供后续继续观察。 */
|
|
1635
|
+
function closeManagedWechatProject(options) {
|
|
1636
|
+
const journalPath = resolveManagedJournal(options.journalPath);
|
|
1637
|
+
if (!journalPath) return Promise.reject(/* @__PURE__ */ new Error("Managed DevTools project cleanup requires its explicit task journal."));
|
|
1638
|
+
const key = `${journalPath}\0${options.id}`;
|
|
1639
|
+
let closing = pendingClosures.get(key);
|
|
1640
|
+
if (!closing) {
|
|
1641
|
+
closing = withMachineE2ELease(async () => withManagedJournalLock(journalPath, async () => closeRecord(await readManagedRecord(journalPath, options.id)))).finally(() => pendingClosures.delete(key));
|
|
1642
|
+
pendingClosures.set(key, closing);
|
|
1643
|
+
}
|
|
1644
|
+
return closing;
|
|
1645
|
+
}
|
|
1646
|
+
/** 未启用受管日志时不改变公共共享会话;意图登记本身没有项目关闭权。 */
|
|
1647
|
+
async function beginManagedWechatProject(options) {
|
|
1648
|
+
const journalPath = resolveManagedJournal(options.journalPath);
|
|
1649
|
+
if (!journalPath) return;
|
|
1650
|
+
const id = randomUUID();
|
|
1651
|
+
await withManagedJournalLock(journalPath, async (scopeRoot) => {
|
|
1652
|
+
const previous = await readManagedWechatProjectRecords(scopeRoot);
|
|
1653
|
+
if (previous.some((record) => [
|
|
1654
|
+
"starting",
|
|
1655
|
+
"unconfirmed",
|
|
1656
|
+
"closing",
|
|
1657
|
+
"failed"
|
|
1658
|
+
].includes(record.state))) throw new Error("Managed DevTools journal contains unresolved ownership or cleanup; refusing another project start.");
|
|
1659
|
+
checkManagedWindowBudget(previous, options);
|
|
1660
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1661
|
+
await writeManagedRecord({
|
|
1662
|
+
schemaVersion: 1,
|
|
1663
|
+
id,
|
|
1664
|
+
generation: options.generation ?? id,
|
|
1665
|
+
journalPath,
|
|
1666
|
+
ownerToken: managedProcessToken,
|
|
1667
|
+
ownerPid: process.pid,
|
|
1668
|
+
target: { ...options.target },
|
|
1669
|
+
projectPath: path.resolve(options.projectPath),
|
|
1670
|
+
state: "starting",
|
|
1671
|
+
port: options.port,
|
|
1672
|
+
createdAt: now,
|
|
1673
|
+
updatedAt: now
|
|
1674
|
+
});
|
|
1675
|
+
});
|
|
1676
|
+
return {
|
|
1677
|
+
id,
|
|
1678
|
+
journalPath,
|
|
1679
|
+
confirm: async (result) => {
|
|
1680
|
+
await withManagedJournalLock(journalPath, async () => {
|
|
1681
|
+
const record = await readManagedRecord(journalPath, id);
|
|
1682
|
+
if (record.state !== "starting" && record.state !== "unconfirmed") throw new Error("Managed DevTools start receipt was already recorded.");
|
|
1683
|
+
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
|
+
record.openedProjectWindow = result.openedProjectWindow;
|
|
1685
|
+
record.port = result.port;
|
|
1686
|
+
record.state = result.openedProjectWindow ? "unconfirmed" : "borrowed";
|
|
1687
|
+
await writeManagedRecord(record);
|
|
1688
|
+
record.host = await inspectManagedProjectHost(record.target, result.port);
|
|
1689
|
+
if (result.openedProjectWindow) record.state = "owned";
|
|
1690
|
+
await writeManagedRecord(record);
|
|
1691
|
+
});
|
|
1692
|
+
},
|
|
1693
|
+
fail: async (error) => {
|
|
1694
|
+
await withManagedJournalLock(journalPath, async () => {
|
|
1695
|
+
const record = await readManagedRecord(journalPath, id);
|
|
1696
|
+
if (record.state === "released") return;
|
|
1697
|
+
if (record.state === "starting") record.state = "unconfirmed";
|
|
1698
|
+
record.error = describe(error);
|
|
1699
|
+
await writeManagedRecord(record);
|
|
1700
|
+
});
|
|
1701
|
+
},
|
|
1702
|
+
close: () => closeManagedWechatProject({
|
|
1703
|
+
journalPath,
|
|
1704
|
+
id
|
|
1705
|
+
})
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
/** 父进程可回收已退出工作进程的确认所有权;未知启动结果严格阻止后续任务。 */
|
|
1709
|
+
async function cleanupManagedWechatProjects(options = {}) {
|
|
1710
|
+
const records = await readManagedWechatProjectRecords(options.journalPath);
|
|
1711
|
+
const errors = [];
|
|
1712
|
+
for (const record of records) {
|
|
1713
|
+
if (options.scope === "process" && record.ownerToken !== managedProcessToken) continue;
|
|
1714
|
+
try {
|
|
1715
|
+
await closeManagedWechatProject({
|
|
1716
|
+
journalPath: record.journalPath,
|
|
1717
|
+
id: record.id
|
|
1718
|
+
});
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
errors.push(error);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
if (errors.length) throw new AggregateError(errors, "Managed DevTools project cleanup remains incomplete; stop the acceptance lane.");
|
|
1724
|
+
}
|
|
1725
|
+
//#endregion
|
|
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 };
|