@trim21/personal-pi-extensions 0.0.428 → 0.0.430
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/core.ts +1 -1
- package/src/bwrap/holder.js +2 -15
- package/src/bwrap/holder.ts +4 -18
- package/src/bwrap/network-stack.ts +38 -27
- package/src/bwrap/runtime.ts +22 -29
package/package.json
CHANGED
package/src/bwrap/core.ts
CHANGED
|
@@ -370,7 +370,7 @@ function killChild(child: ChildProcess): void {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
/** 为 net-allowlist
|
|
373
|
+
/** 为 net-allowlist 模式创建网络栈;非该模式返回 undefined。每次命令现建现停。 */
|
|
374
374
|
export async function createNetworkStack(
|
|
375
375
|
resolved: ResolvedBwrap,
|
|
376
376
|
): Promise<NetworkStack | undefined> {
|
package/src/bwrap/holder.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { open, readFile, writeFile } from "node:fs/promises";
|
|
3
2
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
3
|
const args = process.argv.slice(2);
|
|
5
4
|
const configPath = args[0];
|
|
6
5
|
const singBoxPath = args[1];
|
|
7
|
-
|
|
8
|
-
if (!configPath || !singBoxPath || !readyFile) {
|
|
6
|
+
if (!configPath || !singBoxPath) {
|
|
9
7
|
process.exit(2);
|
|
10
8
|
}
|
|
11
9
|
function tap0Exists() {
|
|
@@ -24,11 +22,9 @@ async function waitFor(attempts, delayMs, predicate) {
|
|
|
24
22
|
}
|
|
25
23
|
async function main() {
|
|
26
24
|
await waitFor(100, 100, tap0Exists);
|
|
27
|
-
const logPath = `${configPath}.log`;
|
|
28
|
-
const logFd = await open(logPath, "w");
|
|
29
25
|
const singbox = spawn(singBoxPath, ["run", "-c", configPath], {
|
|
30
26
|
env: { ...process.env, ENABLE_DEPRECATED_MISSING_DOMAIN_RESOLVER: "true" },
|
|
31
|
-
stdio: ["ignore",
|
|
27
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
32
28
|
});
|
|
33
29
|
const stop = () => {
|
|
34
30
|
singbox.kill("SIGTERM");
|
|
@@ -41,15 +37,6 @@ async function main() {
|
|
|
41
37
|
stop();
|
|
42
38
|
process.exit(0);
|
|
43
39
|
});
|
|
44
|
-
await waitFor(200, 100, async () => {
|
|
45
|
-
try {
|
|
46
|
-
const log = await readFile(logPath, "utf8");
|
|
47
|
-
return log.includes("sing-box started");
|
|
48
|
-
} catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
await writeFile(readyFile, "");
|
|
53
40
|
await new Promise((resolve) => singbox.once("exit", () => resolve()));
|
|
54
41
|
}
|
|
55
42
|
try {
|
package/src/bwrap/holder.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/* eslint-disable no-console, unicorn/no-process-exit -- 独立 CLI 脚本,由 unshare 直接执行 */
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { open, readFile, writeFile } from "node:fs/promises";
|
|
4
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
4
|
|
|
6
|
-
// 用法:unshare -Urn node holder.ts <config> <singbox>
|
|
5
|
+
// 用法:unshare -Urn node holder.ts <config> <singbox>
|
|
7
6
|
const args = process.argv.slice(2);
|
|
8
7
|
const configPath = args[0];
|
|
9
8
|
const singBoxPath = args[1];
|
|
10
|
-
|
|
11
|
-
if (!configPath || !singBoxPath || !readyFile) {
|
|
9
|
+
if (!configPath || !singBoxPath) {
|
|
12
10
|
process.exit(2);
|
|
13
11
|
}
|
|
14
12
|
|
|
@@ -36,11 +34,10 @@ async function main(): Promise<void> {
|
|
|
36
34
|
// slirp4netns 由宿主侧 spawn,这里等它建好 tap0
|
|
37
35
|
await waitFor(100, 100, tap0Exists);
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
const logFd = await open(logPath, "w");
|
|
37
|
+
// sing-box 日志透传到 holder 的 stdout/stderr,宿主侧监听 "sing-box started" 判定就绪
|
|
41
38
|
const singbox = spawn(singBoxPath, ["run", "-c", configPath], {
|
|
42
39
|
env: { ...process.env, ENABLE_DEPRECATED_MISSING_DOMAIN_RESOLVER: "true" },
|
|
43
|
-
stdio: ["ignore",
|
|
40
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
44
41
|
});
|
|
45
42
|
|
|
46
43
|
const stop = (): void => {
|
|
@@ -55,17 +52,6 @@ async function main(): Promise<void> {
|
|
|
55
52
|
process.exit(0);
|
|
56
53
|
});
|
|
57
54
|
|
|
58
|
-
// 以 "sing-box started" 日志作为就绪标志(TUN 与 gvisor 数据面此时已可用)
|
|
59
|
-
await waitFor(200, 100, async () => {
|
|
60
|
-
try {
|
|
61
|
-
const log = await readFile(logPath, "utf8");
|
|
62
|
-
return log.includes("sing-box started");
|
|
63
|
-
} catch {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
await writeFile(readyFile, "");
|
|
68
|
-
|
|
69
55
|
await new Promise<void>((resolve) => singbox.once("exit", () => resolve()));
|
|
70
56
|
}
|
|
71
57
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, readlink, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { generateSingboxConfig } from "./singbox-config.js";
|
|
8
8
|
|
|
9
9
|
export interface NetworkStackOptions {
|
|
10
|
-
/** 允许直连的域名 / IP:port
|
|
10
|
+
/** 允许直连的域名 / IP:port 列表(每次命令从配置重新读取)。 */
|
|
11
11
|
readonly allowlist: readonly string[];
|
|
12
12
|
/** 真实 DNS 服务器列表,按顺序 fallback。 */
|
|
13
13
|
readonly dnsServers: readonly string[];
|
|
@@ -72,17 +72,32 @@ async function waitForNewUserns(pid: number, timeoutMs = 5000): Promise<void> {
|
|
|
72
72
|
throw new Error("Timed out waiting for sandbox network namespace");
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
/** 监听 holder 的 stdout/stderr(sing-box 日志透传),以 "sing-box started" 作为就绪标志。 */
|
|
76
|
+
function waitForSingboxStarted(holder: ChildProcess, timeoutMs = 20000): Promise<void> {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
let settled = false;
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
if (settled) return;
|
|
81
|
+
settled = true;
|
|
82
|
+
reject(new Error("Timed out waiting for sing-box to start"));
|
|
83
|
+
}, timeoutMs);
|
|
84
|
+
const onData = (data: Buffer): void => {
|
|
85
|
+
if (settled) return;
|
|
86
|
+
if (data.toString().includes("sing-box started")) {
|
|
87
|
+
settled = true;
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
resolve();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
holder.stdout?.on("data", onData);
|
|
93
|
+
holder.stderr?.on("data", onData);
|
|
94
|
+
holder.once("exit", (code) => {
|
|
95
|
+
if (settled) return;
|
|
96
|
+
settled = true;
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
reject(new Error(`Sandbox holder exited before sing-box started (code ${code})`));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
function killChild(pid: number | undefined): void {
|
|
@@ -117,7 +132,7 @@ const stackFinalizer = new FinalizationRegistry<NetworkStackState>((state) => {
|
|
|
117
132
|
});
|
|
118
133
|
|
|
119
134
|
/**
|
|
120
|
-
*
|
|
135
|
+
* 启动网络栈:holder 进程持有 netns(内部跑 sing-box 的
|
|
121
136
|
* TUN + fakeip + deny-by-default),slirp4netns 提供 egress。返回前
|
|
122
137
|
* netns/sing-box/slirp4netns 均已就绪,命令通过 nsenter 进入该 netns 执行。
|
|
123
138
|
*/
|
|
@@ -125,20 +140,16 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
125
140
|
const { allowlist, dnsServers, singBoxPath, slirp4netnsPath } = options;
|
|
126
141
|
const directory = await mkdtemp(join(tmpdir(), "pi-netns-"));
|
|
127
142
|
const configPath = join(directory, "singbox.json");
|
|
128
|
-
const readyFile = join(directory, "ready");
|
|
129
143
|
await writeFile(configPath, generateSingboxConfig({ allowlist, dnsServers }));
|
|
130
144
|
|
|
131
|
-
const holder = spawn(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
env: {
|
|
136
|
-
HOME: process.env.HOME ?? "",
|
|
137
|
-
PATH: `${process.env.PATH ?? ""}:${SBIN_PATH_SUFFIX}`,
|
|
138
|
-
},
|
|
139
|
-
stdio: "ignore",
|
|
145
|
+
const holder = spawn("unshare", ["-Urn", "--", "node", HOLDER_PATH, configPath, singBoxPath], {
|
|
146
|
+
env: {
|
|
147
|
+
HOME: process.env.HOME ?? "",
|
|
148
|
+
PATH: `${process.env.PATH ?? ""}:${SBIN_PATH_SUFFIX}`,
|
|
140
149
|
},
|
|
141
|
-
|
|
150
|
+
// sing-box 日志经 holder 透传到这里的 stdout/stderr,用于判定就绪
|
|
151
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
152
|
+
});
|
|
142
153
|
if (holder.pid === undefined) {
|
|
143
154
|
throw new Error("Failed to start network namespace holder");
|
|
144
155
|
}
|
|
@@ -157,7 +168,7 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
157
168
|
{ stdio: "ignore" },
|
|
158
169
|
);
|
|
159
170
|
const slirpPid = slirp.pid;
|
|
160
|
-
await
|
|
171
|
+
await waitForSingboxStarted(holder);
|
|
161
172
|
|
|
162
173
|
const state: NetworkStackState = { holderPid, slirpPid, directory };
|
|
163
174
|
const stack: NetworkStack = {
|
package/src/bwrap/runtime.ts
CHANGED
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
resolveHeadlessBwrap,
|
|
39
39
|
} from "./core.js";
|
|
40
40
|
import { dcgSuggestion } from "./dcg-scan.js";
|
|
41
|
-
import { type NetworkStack } from "./network-stack.js";
|
|
42
41
|
|
|
43
42
|
export type EscalationDecision = { kind: "dialog" } | { kind: "deny"; reason: string };
|
|
44
43
|
|
|
@@ -197,6 +196,11 @@ class BashOutput {
|
|
|
197
196
|
});
|
|
198
197
|
}
|
|
199
198
|
|
|
199
|
+
// eslint-disable-next-line unicorn/no-nonstandard-builtin-properties -- ES2024 标准(Node 24 原生支持)
|
|
200
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
201
|
+
await this.close();
|
|
202
|
+
}
|
|
203
|
+
|
|
200
204
|
/** 尾部文本(截断结果的候选,未截断时即完整输出)。 */
|
|
201
205
|
tailText(): string {
|
|
202
206
|
return Buffer.concat(this.tail, this.tailBytes).toString("utf8");
|
|
@@ -242,7 +246,6 @@ export class BwrapRuntime {
|
|
|
242
246
|
private resolved: ResolvedBwrap | undefined;
|
|
243
247
|
private sandboxDisabled = false;
|
|
244
248
|
private bwrapUnavailable = false;
|
|
245
|
-
private networkStack: NetworkStack | undefined;
|
|
246
249
|
|
|
247
250
|
setup(pi: ExtensionAPI): void {
|
|
248
251
|
pi.registerFlag("no-bwrap", {
|
|
@@ -315,8 +318,6 @@ export class BwrapRuntime {
|
|
|
315
318
|
setMode(cwd: string, mode: BwrapMode): ResolvedBwrap {
|
|
316
319
|
this.resolved = resolveBwrap({ ...loadBwrapConfig(cwd), mode });
|
|
317
320
|
this.sandboxDisabled = false;
|
|
318
|
-
void this.networkStack?.stop();
|
|
319
|
-
this.networkStack = undefined;
|
|
320
321
|
return this.resolved;
|
|
321
322
|
}
|
|
322
323
|
|
|
@@ -324,8 +325,6 @@ export class BwrapRuntime {
|
|
|
324
325
|
this.resolved = undefined;
|
|
325
326
|
this.sandboxDisabled = false;
|
|
326
327
|
this.bwrapUnavailable = false;
|
|
327
|
-
void this.networkStack?.stop();
|
|
328
|
-
this.networkStack = undefined;
|
|
329
328
|
}
|
|
330
329
|
|
|
331
330
|
async execute(request: BwrapExecutionRequest): Promise<BwrapExecutionResult> {
|
|
@@ -361,11 +360,23 @@ export class BwrapRuntime {
|
|
|
361
360
|
await this.approveFullAccess(request.ctx, request.command, request.description, execCwd);
|
|
362
361
|
}
|
|
363
362
|
}
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
363
|
+
const local = isWindows || needsApproval || !runtime.bwrapEnabled;
|
|
364
|
+
// 每次命令现建网络栈(启动约 140ms),作用域结束自动停栈;allowlist 变化即时生效
|
|
365
|
+
const networkStack = local ? undefined : await createNetworkStack(runtime);
|
|
366
|
+
await using _stack = {
|
|
367
|
+
// eslint-disable-next-line unicorn/no-nonstandard-builtin-properties -- ES2024 标准(Node 24 原生支持)
|
|
368
|
+
async [Symbol.asyncDispose]() {
|
|
369
|
+
try {
|
|
370
|
+
await networkStack?.stop();
|
|
371
|
+
} catch {
|
|
372
|
+
// best-effort:停栈失败(进程已退出/目录删除失败)不掩盖命令结果
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
const operations = local
|
|
377
|
+
? createLocalBashOperations()
|
|
378
|
+
: createBwrapBashOperations(runtime, workspace, networkStack);
|
|
379
|
+
await using output = new BashOutput(request.ctx.sessionManager.getSessionId());
|
|
369
380
|
const { onUpdate } = request;
|
|
370
381
|
|
|
371
382
|
// 流式进度:节流推送尾部快照(对齐 pi 内置 bash 的实时输出体验)
|
|
@@ -442,7 +453,6 @@ export class BwrapRuntime {
|
|
|
442
453
|
} finally {
|
|
443
454
|
if (updateTimer) clearTimeout(updateTimer);
|
|
444
455
|
if (onUpdate && dirty) emitUpdate();
|
|
445
|
-
await output.close();
|
|
446
456
|
}
|
|
447
457
|
}
|
|
448
458
|
|
|
@@ -454,21 +464,6 @@ export class BwrapRuntime {
|
|
|
454
464
|
return this.resolved;
|
|
455
465
|
}
|
|
456
466
|
|
|
457
|
-
/** net-allowlist 模式的常驻网络栈:惰性创建,session 期间复用(allowlist 固定)。 */
|
|
458
|
-
private async getNetworkStack(runtime: ResolvedBwrap): Promise<NetworkStack | undefined> {
|
|
459
|
-
if (runtime.mode !== "net-allowlist" || runtime.networkAllowlist.length === 0) {
|
|
460
|
-
if (this.networkStack) {
|
|
461
|
-
void this.networkStack.stop();
|
|
462
|
-
this.networkStack = undefined;
|
|
463
|
-
}
|
|
464
|
-
return undefined;
|
|
465
|
-
}
|
|
466
|
-
if (!this.networkStack) {
|
|
467
|
-
this.networkStack = await createNetworkStack(runtime);
|
|
468
|
-
}
|
|
469
|
-
return this.networkStack;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
467
|
private async approveFullAccess(
|
|
473
468
|
ctx: ExtensionContext,
|
|
474
469
|
command: string,
|
|
@@ -744,8 +739,6 @@ export class BwrapRuntime {
|
|
|
744
739
|
private reload(ctx: ExtensionCommandContext): void {
|
|
745
740
|
this.resolved = undefined;
|
|
746
741
|
this.bwrapUnavailable = false;
|
|
747
|
-
void this.networkStack?.stop();
|
|
748
|
-
this.networkStack = undefined;
|
|
749
742
|
const runtime = this.resolve(ctx);
|
|
750
743
|
ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("accent", `bwrap: ${runtime.mode}`));
|
|
751
744
|
ctx.ui.notify(`bwrap config reloaded (mode: ${runtime.mode})`, "info");
|