@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.443",
3
+ "version": "0.1.444",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -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
- const holder = spawn("unshare", ["-Urn", "--", "node", HOLDER_PATH, configPath, mihomoPath], {
187
- env: {
188
- HOME: process.env.HOME ?? "",
189
- PATH: `${process.env.PATH ?? ""}:${SBIN_PATH_SUFFIX}`,
190
- },
191
- // mihomo 日志经 holder 透传到这里的 stdout/stderr,用于判定就绪
192
- stdio: ["ignore", "pipe", "pipe"],
193
- });
194
- forwardOutput(holder, holderOutput);
195
- if (holder.pid === undefined) {
196
- throw new Error("Failed to start network namespace holder");
197
- }
198
- const holderPid = holder.pid;
199
- await waitForNewUserns(holderPid);
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
- const slirp = spawn(
202
- slirp4netnsPath,
203
- [
204
- "-c",
205
- `--mtu=${TUN_MTU}`,
206
- `--userns-path=/proc/${holderPid}/ns/user`,
207
- "--netns-type=pid",
208
- String(holderPid),
209
- "tap0",
210
- ],
211
- // 默认丢弃 slirp4netns 日志;诊断时改为管道转发
212
- { stdio: slirpOutput ? ["ignore", "pipe", "pipe"] : "ignore" },
213
- );
214
- forwardOutput(slirp, slirpOutput);
215
- const slirpPid = slirp.pid;
216
- await waitForMihomoStarted(holder);
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
- const state: NetworkStackState = { holderPid, slirpPid, directory };
219
- const stack: NetworkStack = {
220
- exec: async (execOptions: NetworkStackExecOptions) => {
221
- const child = spawn(
222
- "nsenter",
223
- [
224
- "-U",
225
- "-n",
226
- "--preserve-credentials",
227
- "-t",
228
- String(holderPid),
229
- "--",
230
- execOptions.bwrapPath,
231
- ...execOptions.bwrapArgs,
232
- "--",
233
- execOptions.shell,
234
- "-lc",
235
- execOptions.command,
236
- ],
237
- {
238
- cwd: execOptions.cwd,
239
- detached: true,
240
- stdio: ["ignore", "pipe", "pipe"],
241
- env: execOptions.env,
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
- return new Promise<{ exitCode: number | null }>((resolve, reject) => {
246
- let timedOut = false;
247
- let settled = false;
248
- const timeoutHandle = execOptions.timeout
249
- ? setTimeout(() => {
250
- timedOut = true;
251
- killChild(child.pid);
252
- }, execOptions.timeout * 1000)
253
- : undefined;
254
- const onAbort = (): void => {
255
- killChild(child.pid);
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
- child.stdout?.on("data", execOptions.onData);
259
- child.stderr?.on("data", execOptions.onData);
260
- execOptions.signal?.addEventListener("abort", onAbort, { once: true });
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
- child.once("error", (error) => {
263
- if (settled) return;
264
- settled = true;
265
- reject(error);
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
- child.once("close", (exitCode) => {
268
- if (settled) return;
269
- settled = true;
270
- if (timeoutHandle) clearTimeout(timeoutHandle);
271
- execOptions.signal?.removeEventListener("abort", onAbort);
272
- // 中断:reject signal.reason(默认是 name=AbortError 的 DOMException)
273
- if (execOptions.signal?.aborted) {
274
- reject(
275
- execOptions.signal.reason instanceof Error
276
- ? execOptions.signal.reason
277
- : new Error("The operation was aborted"),
278
- );
279
- } else if (timedOut) {
280
- // 超时:name=TimeoutError(对齐标准错误分类)
281
- reject(new TimeoutError(execOptions.timeout));
282
- } else resolve({ exitCode });
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
  }