@trim21/personal-pi-extensions 0.1.443 → 0.1.444
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 +138 -104
package/package.json
CHANGED
|
@@ -59,6 +59,29 @@ function killProcess(pid: number | undefined): void {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* SIGTERM 后等待进程退出;超时仍未退出则 SIGKILL。
|
|
64
|
+
* holder/slirp 持有 userns/netns 引用,进程不退 ns 就不会释放。
|
|
65
|
+
*/
|
|
66
|
+
async function terminateProcess(pid: number | undefined, timeoutMs = 2000): Promise<void> {
|
|
67
|
+
if (!pid) return;
|
|
68
|
+
killProcess(pid);
|
|
69
|
+
const deadline = Date.now() + timeoutMs;
|
|
70
|
+
while (Date.now() < deadline) {
|
|
71
|
+
try {
|
|
72
|
+
process.kill(pid, 0);
|
|
73
|
+
} catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, "SIGKILL");
|
|
80
|
+
} catch {
|
|
81
|
+
// 已退出
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
/** 读宿主机 /etc/resolv.conf 的全部 IPv4 nameserver,按声明顺序返回。 */
|
|
63
86
|
export async function resolveDnsServers(): Promise<string[]> {
|
|
64
87
|
const content = await readFile("/etc/resolv.conf", "utf8");
|
|
@@ -183,114 +206,125 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
183
206
|
const config = generateMihomoConfig({ allowlist, dnsServers });
|
|
184
207
|
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
185
208
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
209
|
+
let holder: ChildProcess | undefined;
|
|
210
|
+
let slirp: ChildProcess | undefined;
|
|
211
|
+
try {
|
|
212
|
+
holder = spawn("unshare", ["-Urn", "--", "node", HOLDER_PATH, configPath, mihomoPath], {
|
|
213
|
+
env: {
|
|
214
|
+
HOME: process.env.HOME ?? "",
|
|
215
|
+
PATH: `${process.env.PATH ?? ""}:${SBIN_PATH_SUFFIX}`,
|
|
216
|
+
},
|
|
217
|
+
// mihomo 日志经 holder 透传到这里的 stdout/stderr,用于判定就绪
|
|
218
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
219
|
+
});
|
|
220
|
+
forwardOutput(holder, holderOutput);
|
|
221
|
+
if (holder.pid === undefined) {
|
|
222
|
+
throw new Error("Failed to start network namespace holder");
|
|
223
|
+
}
|
|
224
|
+
const holderPid = holder.pid;
|
|
225
|
+
await waitForNewUserns(holderPid);
|
|
200
226
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
227
|
+
slirp = spawn(
|
|
228
|
+
slirp4netnsPath,
|
|
229
|
+
[
|
|
230
|
+
"-c",
|
|
231
|
+
`--mtu=${TUN_MTU}`,
|
|
232
|
+
`--userns-path=/proc/${holderPid}/ns/user`,
|
|
233
|
+
"--netns-type=pid",
|
|
234
|
+
String(holderPid),
|
|
235
|
+
"tap0",
|
|
236
|
+
],
|
|
237
|
+
// 默认丢弃 slirp4netns 日志;诊断时改为管道转发
|
|
238
|
+
{ stdio: slirpOutput ? ["ignore", "pipe", "pipe"] : "ignore" },
|
|
239
|
+
);
|
|
240
|
+
forwardOutput(slirp, slirpOutput);
|
|
241
|
+
const slirpPid = slirp.pid;
|
|
242
|
+
await waitForMihomoStarted(holder);
|
|
217
243
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
+
const state: NetworkStackState = { holderPid, slirpPid, directory };
|
|
245
|
+
const stack: NetworkStack = {
|
|
246
|
+
exec: async (execOptions: NetworkStackExecOptions) => {
|
|
247
|
+
const child = spawn(
|
|
248
|
+
"nsenter",
|
|
249
|
+
[
|
|
250
|
+
"-U",
|
|
251
|
+
"-n",
|
|
252
|
+
"--preserve-credentials",
|
|
253
|
+
"-t",
|
|
254
|
+
String(holderPid),
|
|
255
|
+
"--",
|
|
256
|
+
execOptions.bwrapPath,
|
|
257
|
+
...execOptions.bwrapArgs,
|
|
258
|
+
"--",
|
|
259
|
+
execOptions.shell,
|
|
260
|
+
"-lc",
|
|
261
|
+
execOptions.command,
|
|
262
|
+
],
|
|
263
|
+
{
|
|
264
|
+
cwd: execOptions.cwd,
|
|
265
|
+
detached: true,
|
|
266
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
267
|
+
env: execOptions.env,
|
|
268
|
+
},
|
|
269
|
+
);
|
|
244
270
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
271
|
+
return new Promise<{ exitCode: number | null }>((resolve, reject) => {
|
|
272
|
+
let timedOut = false;
|
|
273
|
+
let settled = false;
|
|
274
|
+
const timeoutHandle = execOptions.timeout
|
|
275
|
+
? setTimeout(() => {
|
|
276
|
+
timedOut = true;
|
|
277
|
+
killChild(child.pid);
|
|
278
|
+
}, execOptions.timeout * 1000)
|
|
279
|
+
: undefined;
|
|
280
|
+
const onAbort = (): void => {
|
|
281
|
+
killChild(child.pid);
|
|
282
|
+
};
|
|
257
283
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
284
|
+
child.stdout?.on("data", execOptions.onData);
|
|
285
|
+
child.stderr?.on("data", execOptions.onData);
|
|
286
|
+
execOptions.signal?.addEventListener("abort", onAbort, { once: true });
|
|
261
287
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
288
|
+
child.once("error", (error) => {
|
|
289
|
+
if (settled) return;
|
|
290
|
+
settled = true;
|
|
291
|
+
reject(error);
|
|
292
|
+
});
|
|
293
|
+
child.once("close", (exitCode) => {
|
|
294
|
+
if (settled) return;
|
|
295
|
+
settled = true;
|
|
296
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
297
|
+
execOptions.signal?.removeEventListener("abort", onAbort);
|
|
298
|
+
// 中断:reject signal.reason(默认是 name=AbortError 的 DOMException)
|
|
299
|
+
if (execOptions.signal?.aborted) {
|
|
300
|
+
reject(
|
|
301
|
+
execOptions.signal.reason instanceof Error
|
|
302
|
+
? execOptions.signal.reason
|
|
303
|
+
: new Error("The operation was aborted"),
|
|
304
|
+
);
|
|
305
|
+
} else if (timedOut) {
|
|
306
|
+
// 超时:name=TimeoutError(对齐标准错误分类)
|
|
307
|
+
reject(new TimeoutError(execOptions.timeout));
|
|
308
|
+
} else resolve({ exitCode });
|
|
309
|
+
});
|
|
266
310
|
});
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
stop: async () => {
|
|
287
|
-
killProcess(state.slirpPid);
|
|
288
|
-
killProcess(state.holderPid);
|
|
289
|
-
await rm(state.directory, { recursive: true, force: true }).catch(() => false);
|
|
290
|
-
},
|
|
291
|
-
holderPid,
|
|
292
|
-
configPath,
|
|
293
|
-
};
|
|
294
|
-
stackFinalizer.register(stack, state);
|
|
295
|
-
return stack;
|
|
311
|
+
},
|
|
312
|
+
stop: async () => {
|
|
313
|
+
await terminateProcess(state.slirpPid);
|
|
314
|
+
await terminateProcess(state.holderPid);
|
|
315
|
+
await rm(state.directory, { recursive: true, force: true }).catch(() => false);
|
|
316
|
+
},
|
|
317
|
+
holderPid,
|
|
318
|
+
configPath,
|
|
319
|
+
};
|
|
320
|
+
stackFinalizer.register(stack, state);
|
|
321
|
+
return stack;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
// 失败清理:holder/slirp 持有 userns/netns 引用,不 kill 就会锁住 namespace;
|
|
324
|
+
// 调用方(runInSandbox)拿不到 stack,这里的清理只能靠自己
|
|
325
|
+
if (slirp?.pid) await terminateProcess(slirp.pid);
|
|
326
|
+
if (holder?.pid) await terminateProcess(holder.pid);
|
|
327
|
+
await rm(directory, { recursive: true, force: true }).catch(() => false);
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
296
330
|
}
|