@trim21/personal-pi-extensions 0.1.519 → 0.1.520
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/package.json +1 -1
- package/src/bwrap/network-stack.ts +96 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { mkdir, readFile, readlink } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, readlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
@@ -25,8 +25,8 @@ export interface NetworkStackOptions {
|
|
|
25
25
|
readonly mihomoPath: string;
|
|
26
26
|
readonly slirp4netnsPath: string;
|
|
27
27
|
/**
|
|
28
|
-
* holder(unshare + mihomo
|
|
29
|
-
*
|
|
28
|
+
* holder(unshare + mihomo)输出透传。输出同时始终写入诊断缓冲,启动
|
|
29
|
+
* 失败时落盘到 agent-dir/tmp 并把路径附进错误信息,没有它也能拿到死因。
|
|
30
30
|
*/
|
|
31
31
|
readonly onHolderOutput?: (chunk: string) => void;
|
|
32
32
|
}
|
|
@@ -45,6 +45,38 @@ export interface NetworkStackExecOptions {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const NAMESERVER_PATTERN = /^\s*nameserver\s+(\S+)/;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 启动失败时把收集到的子进程完整 stdout/stderr 与错误本身落盘到
|
|
51
|
+
* agent-dir/tmp,返回日志路径;写入失败(如目录不可写)静默返回 undefined,
|
|
52
|
+
* 不掩盖原错误。日志不进工具结果文本:holder 输出可能很长且与命令无关,
|
|
53
|
+
* 只回路径。
|
|
54
|
+
*/
|
|
55
|
+
async function writeFailureLog(
|
|
56
|
+
error: unknown,
|
|
57
|
+
logs: readonly string[],
|
|
58
|
+
): Promise<string | undefined> {
|
|
59
|
+
const sections = logs.map(
|
|
60
|
+
(log, index) => `## child ${index}\n${log.length > 0 ? log : "(no output captured)"}`,
|
|
61
|
+
);
|
|
62
|
+
const content = [
|
|
63
|
+
`# ${new Date().toISOString()}`,
|
|
64
|
+
"",
|
|
65
|
+
error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
66
|
+
"",
|
|
67
|
+
...sections,
|
|
68
|
+
"",
|
|
69
|
+
].join("\n");
|
|
70
|
+
const dir = join(getAgentDir(), "tmp");
|
|
71
|
+
const path = join(dir, `bwrap-netstack-${randomUUID()}.log`);
|
|
72
|
+
try {
|
|
73
|
+
await mkdir(dir, { recursive: true });
|
|
74
|
+
await writeFile(path, content);
|
|
75
|
+
return path;
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
48
80
|
// 构建产物 holder.js(esbuild 编译):node 对 node_modules 下的 .ts 拒绝 type stripping,
|
|
49
81
|
// 扩展从 npm 包加载时 holder.ts 落在 node_modules 下,直接运行会 ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING
|
|
50
82
|
const HOLDER_PATH = fileURLToPath(new URL("holder.js", import.meta.url));
|
|
@@ -113,7 +145,10 @@ async function waitForNewUserns(pid: number, timeoutMs = 5000): Promise<void> {
|
|
|
113
145
|
throw new Error("Timed out waiting for sandbox network namespace");
|
|
114
146
|
}
|
|
115
147
|
|
|
116
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* 监听 holder 的 stdout/stderr(mihomo 日志透传),以 "Tun adapter listening" 作为就绪标志。
|
|
150
|
+
* holder 提前退出的情形由 startNetworkStack 统一注册的 exit 监听兜底。
|
|
151
|
+
*/
|
|
117
152
|
function waitForMihomoStarted(holder: ChildProcess, timeoutMs = 20000): Promise<void> {
|
|
118
153
|
return new Promise((resolve, reject) => {
|
|
119
154
|
let settled = false;
|
|
@@ -139,12 +174,6 @@ function waitForMihomoStarted(holder: ChildProcess, timeoutMs = 20000): Promise<
|
|
|
139
174
|
});
|
|
140
175
|
}
|
|
141
176
|
}
|
|
142
|
-
holder.once("exit", (code) => {
|
|
143
|
-
if (settled) return;
|
|
144
|
-
settled = true;
|
|
145
|
-
clearTimeout(timer);
|
|
146
|
-
reject(new Error(`Sandbox holder exited before mihomo started (code ${code})`));
|
|
147
|
-
});
|
|
148
177
|
});
|
|
149
178
|
}
|
|
150
179
|
|
|
@@ -162,15 +191,26 @@ function killChild(pid: number | undefined): void {
|
|
|
162
191
|
}
|
|
163
192
|
|
|
164
193
|
/**
|
|
165
|
-
*
|
|
194
|
+
* 额外转发子进程输出给诊断回调(就绪探测的监听器不受影响),并把全部输出
|
|
195
|
+
* 收进 collect:启动失败时落盘,否则没有别的渠道能看到 holder 的真实死因。
|
|
166
196
|
* 注册 error 监听器后 spawn 失败(如 unshare 缺失)不再以未捕获异常结束进程。
|
|
167
197
|
*/
|
|
168
|
-
function forwardOutput(
|
|
169
|
-
|
|
170
|
-
|
|
198
|
+
function forwardOutput(
|
|
199
|
+
child: ChildProcess,
|
|
200
|
+
onOutput: ((chunk: string) => void) | undefined,
|
|
201
|
+
collect: string[],
|
|
202
|
+
): void {
|
|
203
|
+
const write = (chunk: Buffer): void => {
|
|
204
|
+
const text = chunk.toString();
|
|
205
|
+
collect.push(text);
|
|
206
|
+
onOutput?.(text);
|
|
207
|
+
};
|
|
171
208
|
child.stdout?.on("data", write);
|
|
172
209
|
child.stderr?.on("data", write);
|
|
173
|
-
child.once("error", (error) =>
|
|
210
|
+
child.once("error", (error) => {
|
|
211
|
+
collect.push(String(error));
|
|
212
|
+
onOutput?.(String(error));
|
|
213
|
+
});
|
|
174
214
|
}
|
|
175
215
|
|
|
176
216
|
export interface NetworkStack {
|
|
@@ -218,6 +258,8 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
218
258
|
|
|
219
259
|
let holder: ChildProcess | undefined;
|
|
220
260
|
let slirp: ChildProcess | undefined;
|
|
261
|
+
const holderLog: string[] = [];
|
|
262
|
+
const slirpLog: string[] = [];
|
|
221
263
|
try {
|
|
222
264
|
// unshare -p --fork:node 成为 pid namespace 的 init,任何方式退出(含 SIGKILL)
|
|
223
265
|
// 内核都会清理 pid ns 内全部进程(mihomo),ns 引用随之归零;
|
|
@@ -247,12 +289,34 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
247
289
|
stdio: ["pipe", "pipe", "pipe", "pipe"],
|
|
248
290
|
},
|
|
249
291
|
);
|
|
250
|
-
forwardOutput(holder, holderOutput);
|
|
251
|
-
|
|
252
|
-
|
|
292
|
+
forwardOutput(holder, holderOutput, holderLog);
|
|
293
|
+
let holderPid = holder.pid;
|
|
294
|
+
if (holderPid === undefined) {
|
|
295
|
+
// spawn 失败(如 unshare 缺失):error 事件异步到达,等一拍让它落进诊断缓冲
|
|
296
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
297
|
+
holderPid = holder.pid;
|
|
298
|
+
if (holderPid === undefined) {
|
|
299
|
+
throw new Error("Failed to start network namespace holder");
|
|
300
|
+
}
|
|
253
301
|
}
|
|
254
|
-
|
|
255
|
-
|
|
302
|
+
// holder 提前退出是启动失败最常见的形态(unshare 被拒、node 崩溃、mihomo 起
|
|
303
|
+
// 不来):立即注册 exit 监听并参与后续所有等待的 race,避免「进程秒死却被
|
|
304
|
+
// 呈现为 5s/20s 超时」。stop() 正常杀 holder 也走这里,下方 catch 防
|
|
305
|
+
// unhandled rejection。
|
|
306
|
+
const { promise: holderExited, reject: rejectHolderExited } = Promise.withResolvers<never>();
|
|
307
|
+
holder.once("exit", (code) => {
|
|
308
|
+
rejectHolderExited(new Error(`Sandbox holder exited before mihomo started (code ${code})`));
|
|
309
|
+
});
|
|
310
|
+
// stop() 正常终止 holder 也会 reject:吞掉,防 unhandled rejection
|
|
311
|
+
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
312
|
+
holderExited.catch(() => undefined);
|
|
313
|
+
// race 输掉的 promise 之后仍可能迟到 reject(如 userns 轮询到点才超时),
|
|
314
|
+
// 补 no-op catch 防止 unhandled rejection 让进程崩溃
|
|
315
|
+
const usernsReady = waitForNewUserns(holderPid);
|
|
316
|
+
await Promise.race([usernsReady, holderExited]).finally(() => {
|
|
317
|
+
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
318
|
+
usernsReady.catch(() => undefined);
|
|
319
|
+
});
|
|
256
320
|
|
|
257
321
|
// slirp4netns 提供 egress,必须在宿主 netns 启动:它的 egress socket 决定出站
|
|
258
322
|
// 视角,留在沙盒 netns 里会被 mihomo 的 TUN 策略路由 + dns-hijack 自劫持成环
|
|
@@ -285,12 +349,16 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
285
349
|
],
|
|
286
350
|
{ stdio: ["ignore", "pipe", "pipe", exitReadFd] },
|
|
287
351
|
);
|
|
288
|
-
forwardOutput(slirp, holderOutput);
|
|
352
|
+
forwardOutput(slirp, holderOutput, slirpLog);
|
|
289
353
|
if (slirp.pid === undefined) {
|
|
290
354
|
throw new Error("Failed to start slirp4netns");
|
|
291
355
|
}
|
|
292
356
|
|
|
293
|
-
|
|
357
|
+
const mihomoReady = waitForMihomoStarted(holder);
|
|
358
|
+
await Promise.race([mihomoReady, holderExited]).finally(() => {
|
|
359
|
+
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
360
|
+
mihomoReady.catch(() => undefined);
|
|
361
|
+
});
|
|
294
362
|
|
|
295
363
|
const state: NetworkStackState = { holderPid, slirpPid: slirp.pid };
|
|
296
364
|
const stack: NetworkStack = {
|
|
@@ -394,6 +462,12 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
394
462
|
killProcess(pid, "SIGKILL");
|
|
395
463
|
}
|
|
396
464
|
}
|
|
465
|
+
const logPath = await writeFailureLog(error, [holderLog.join(""), slirpLog.join("")]);
|
|
466
|
+
if (logPath !== undefined && error instanceof Error) {
|
|
467
|
+
throw new Error(`${error.message}\n(sandbox startup diagnostics: ${logPath})`, {
|
|
468
|
+
cause: error,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
397
471
|
throw error;
|
|
398
472
|
}
|
|
399
473
|
}
|