@trim21/personal-pi-extensions 0.1.445 → 0.1.446

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.445",
3
+ "version": "0.1.446",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -1,16 +1,15 @@
1
1
  import { spawn } from "node:child_process";
2
- import { rmSync } from "node:fs";
3
2
  import { readFile } from "node:fs/promises";
4
- import { dirname } from "node:path";
5
3
  import { setTimeout as sleep } from "node:timers/promises";
6
4
  const args = process.argv.slice(2);
7
- const configPath = args[0];
5
+ const configBase64 = args[0];
8
6
  const mihomoPath = args[1];
9
7
  const slirp4netnsPath = args[2];
10
8
  const tunMtu = Number(args[3]);
11
- if (!configPath || !mihomoPath || !slirp4netnsPath || !tunMtu) {
9
+ if (!configBase64 || !mihomoPath || !slirp4netnsPath || !tunMtu) {
12
10
  process.exit(2);
13
11
  }
12
+ process.chdir("/");
14
13
  function tap0Exists() {
15
14
  return new Promise((resolve) => {
16
15
  const child = spawn("ip", ["link", "show", "tap0"], { stdio: "ignore" });
@@ -27,10 +26,7 @@ async function waitFor(attempts, delayMs, predicate) {
27
26
  }
28
27
  async function main() {
29
28
  process.stdin.resume();
30
- process.stdin.on("end", () => {
31
- rmSync(dirname(configPath), { recursive: true, force: true });
32
- process.exit(0);
33
- });
29
+ process.stdin.on("end", () => process.exit(0));
34
30
  const status = await readFile("/proc/self/status", "utf8");
35
31
  const hostPid = /^NSpid:\s+(.+)$/m.exec(status)?.[1].split(/\s+/, 1)[0];
36
32
  if (!hostPid) {
@@ -40,7 +36,7 @@ async function main() {
40
36
  stdio: ["ignore", "inherit", "inherit"]
41
37
  });
42
38
  await waitFor(100, 100, tap0Exists);
43
- const mihomo = spawn(mihomoPath, ["-d", dirname(configPath), "-f", configPath], {
39
+ const mihomo = spawn(mihomoPath, ["-config", configBase64], {
44
40
  stdio: ["ignore", "inherit", "inherit"]
45
41
  });
46
42
  const stop = () => {
@@ -1,22 +1,23 @@
1
1
  /* eslint-disable no-console, unicorn/no-process-exit -- 独立 CLI 脚本,由 unshare 直接执行 */
2
2
  import { spawn } from "node:child_process";
3
- import { rmSync } from "node:fs";
4
3
  import { readFile } from "node:fs/promises";
5
- import { dirname } from "node:path";
6
4
  import { setTimeout as sleep } from "node:timers/promises";
7
5
 
8
- // 用法:unshare -Urnp --fork --kill-child node holder.ts <config> <mihomo> <slirp4netns> <mtu>
6
+ // 用法:unshare -Urnp --fork --kill-child node holder.ts <config-base64> <mihomo> <slirp4netns> <mtu>
9
7
  // 本进程是 pid namespace 的 init:无论以何种方式退出(含 SIGKILL),内核都会清理该 pid ns 内的
10
8
  // slirp4netns / mihomo,userns/netns 引用随之归零。
11
9
  const args = process.argv.slice(2);
12
- const configPath = args[0];
10
+ const configBase64 = args[0];
13
11
  const mihomoPath = args[1];
14
12
  const slirp4netnsPath = args[2];
15
13
  const tunMtu = Number(args[3]);
16
- if (!configPath || !mihomoPath || !slirp4netnsPath || !tunMtu) {
14
+ if (!configBase64 || !mihomoPath || !slirp4netnsPath || !tunMtu) {
17
15
  process.exit(2);
18
16
  }
19
17
 
18
+ // mihomo 不写工作目录(配置经 -config 直传),切到根目录防止在宿主 cwd 意外落盘
19
+ process.chdir("/");
20
+
20
21
  function tap0Exists(): Promise<boolean> {
21
22
  return new Promise((resolve) => {
22
23
  const child = spawn("ip", ["link", "show", "tap0"], { stdio: "ignore" });
@@ -39,12 +40,9 @@ async function waitFor(
39
40
 
40
41
  async function main(): Promise<void> {
41
42
  // 宿主(pi)进程死亡检测:宿主以任何方式退出(含 SIGKILL)都会关闭它持有的 stdin 写端,
42
- // 这里读到 EOF 即自杀(init 退出即触发内核清理整个 pid ns),顺带删除配置目录。
43
+ // 这里读到 EOF 即自杀(init 退出即触发内核清理整个 pid ns)。
43
44
  process.stdin.resume();
44
- process.stdin.on("end", () => {
45
- rmSync(dirname(configPath), { recursive: true, force: true });
46
- process.exit(0);
47
- });
45
+ process.stdin.on("end", () => process.exit(0));
48
46
 
49
47
  // 宿主视角 pid(NSpid 第一项):/proc 挂载是宿主的(-Urnp 不含 -m,宿主视角下
50
48
  // /proc/1 是 systemd 而非本 pid ns 的 init),slirp4netns 的 setns 必须走宿主 /proc
@@ -63,7 +61,7 @@ async function main(): Promise<void> {
63
61
  // mihomo 日志透传到 holder 的 stdout/stderr,宿主侧监听 "Tun adapter listening" 判定就绪
64
62
  await waitFor(100, 100, tap0Exists);
65
63
 
66
- const mihomo = spawn(mihomoPath, ["-d", dirname(configPath), "-f", configPath], {
64
+ const mihomo = spawn(mihomoPath, ["-config", configBase64], {
67
65
  stdio: ["ignore", "inherit", "inherit"],
68
66
  });
69
67
 
@@ -1,10 +1,8 @@
1
1
  import { type ChildProcess, spawn } from "node:child_process";
2
- import { mkdtemp, readFile, readlink, rm, writeFile } from "node:fs/promises";
3
- import { join } from "node:path";
2
+ import { readFile, readlink } from "node:fs/promises";
4
3
  import { fileURLToPath } from "node:url";
5
4
 
6
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
7
-
5
+ import { forEachLine } from "../lib/proc.js";
8
6
  import { generateMihomoConfig, TUN_MTU } from "./mihomo-config.js";
9
7
 
10
8
  /** 命令超时错误:name=TimeoutError(对齐标准错误分类),message 保留 timeout:N 格式。 */
@@ -120,16 +118,23 @@ function waitForMihomoStarted(holder: ChildProcess, timeoutMs = 20000): Promise<
120
118
  settled = true;
121
119
  reject(new Error("Timed out waiting for mihomo to start"));
122
120
  }, timeoutMs);
123
- const onData = (data: Buffer): void => {
121
+ const onReady = (): void => {
124
122
  if (settled) return;
125
- if (data.toString().includes("Tun adapter listening")) {
126
- settled = true;
127
- clearTimeout(timer);
128
- resolve();
129
- }
123
+ settled = true;
124
+ clearTimeout(timer);
125
+ resolve();
130
126
  };
131
- holder.stdout?.on("data", onData);
132
- holder.stderr?.on("data", onData);
127
+ // 按行扫描:mihomo 日志行可能跨 data chunk,由 forEachLine 负责拼接
128
+ for (const stream of [holder.stdout, holder.stderr]) {
129
+ if (stream) {
130
+ // 不提前停止:holder 生命周期内持续消费输出,避免流无消费者触发背压
131
+ void forEachLine(stream, (line) => {
132
+ if (line.includes("Tun adapter listening")) onReady();
133
+ }).catch(() => {
134
+ // 就绪判定由超时与 holder exit 兜底,流的 error 忽略
135
+ });
136
+ }
137
+ }
133
138
  holder.once("exit", (code) => {
134
139
  if (settled) return;
135
140
  settled = true;
@@ -169,20 +174,16 @@ export interface NetworkStack {
169
174
  stop(): Promise<void>;
170
175
  /** holder pid:可 `nsenter -U -n --preserve-credentials -t <pid>` 手动进入该 netns 排查。 */
171
176
  readonly holderPid: number;
172
- /** 生成的 mihomo 配置路径(stop() 后随临时目录一起删除)。 */
173
- readonly configPath: string;
174
177
  }
175
178
 
176
179
  interface NetworkStackState {
177
180
  holderPid: number;
178
- directory: string;
179
181
  }
180
182
 
181
- /** 兜底:调用方忘记 stop() 时,对象被 GC 回收后 kill 残留进程并清理目录。 */
183
+ /** 兜底:调用方忘记 stop() 时,对象被 GC 回收后 kill 残留进程。 */
182
184
  const stackFinalizer = new FinalizationRegistry<NetworkStackState>((state) => {
183
185
  // SIGTERM 经 unshare --kill-child 转发给 init,内核清理 pid ns 内全部进程
184
186
  killProcess(state.holderPid);
185
- void rm(state.directory, { recursive: true, force: true }).catch(() => false);
186
187
  });
187
188
 
188
189
  /**
@@ -198,11 +199,9 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
198
199
  slirp4netnsPath,
199
200
  onHolderOutput: holderOutput,
200
201
  } = options;
201
- // 临时目录放在 agent-dir/tmp(pi 自有临时区),不污染系统 /tmp
202
- const directory = await mkdtemp(join(getAgentDir(), "tmp", "pi-netns-"));
203
- const configPath = join(directory, "mihomo.json");
202
+ // mihomo 支持 -config 直接接收 base64 配置,无需写配置文件中转
204
203
  const config = generateMihomoConfig({ allowlist, dnsServers });
205
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
204
+ const configBase64 = Buffer.from(JSON.stringify(config)).toString("base64");
206
205
 
207
206
  let holder: ChildProcess | undefined;
208
207
  try {
@@ -218,7 +217,7 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
218
217
  "--",
219
218
  "node",
220
219
  HOLDER_PATH,
221
- configPath,
220
+ configBase64,
222
221
  mihomoPath,
223
222
  slirp4netnsPath,
224
223
  String(TUN_MTU),
@@ -242,7 +241,7 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
242
241
 
243
242
  await waitForMihomoStarted(holder);
244
243
 
245
- const state: NetworkStackState = { holderPid, directory };
244
+ const state: NetworkStackState = { holderPid };
246
245
  const stack: NetworkStack = {
247
246
  exec: async (execOptions: NetworkStackExecOptions) => {
248
247
  const child = spawn(
@@ -320,10 +319,8 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
320
319
  for (const pid of children) {
321
320
  killProcess(pid, "SIGKILL");
322
321
  }
323
- await rm(state.directory, { recursive: true, force: true }).catch(() => false);
324
322
  },
325
323
  holderPid,
326
- configPath,
327
324
  };
328
325
  stackFinalizer.register(stack, state);
329
326
  return stack;
@@ -338,7 +335,6 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
338
335
  killProcess(pid, "SIGKILL");
339
336
  }
340
337
  }
341
- await rm(directory, { recursive: true, force: true }).catch(() => false);
342
338
  throw error;
343
339
  }
344
340
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 按行消费输出流:data 事件按 buffer 边界切分,行可能跨多个 chunk,
3
+ * 这里拼接缓冲后按 \n 逐行回调(回调不含换行符)。
4
+ * 回调返回 false 可提前停止消费;流的 error 会以 rejection 形式抛出。
5
+ */
6
+ export async function forEachLine(
7
+ stream: NodeJS.ReadableStream,
8
+ callback: (line: string) => boolean | void,
9
+ ): Promise<void> {
10
+ let buffer = "";
11
+ for await (const chunk of stream) {
12
+ buffer += chunk.toString();
13
+ let newline: number;
14
+ while ((newline = buffer.indexOf("\n")) !== -1) {
15
+ const line = buffer.slice(0, newline);
16
+ buffer = buffer.slice(newline + 1);
17
+ if (callback(line) === false) return;
18
+ }
19
+ }
20
+ }