terminal-bridge-setup 2.6.1 → 2.7.0
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/bin/setup.mjs +66 -4
- package/files/native/host.js +16 -40
- package/files/proxy/server.js +14 -0
- package/package.json +1 -1
package/bin/setup.mjs
CHANGED
|
@@ -131,16 +131,19 @@ function installProxyDeps() {
|
|
|
131
131
|
function registerNativeHost() {
|
|
132
132
|
step(3, STEPS, "注册 Native Messaging Host");
|
|
133
133
|
|
|
134
|
-
// macOS / Linux 路径不同
|
|
135
134
|
const plat = platform();
|
|
135
|
+
if (plat === "win32") {
|
|
136
|
+
return registerNativeHostWindows();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// macOS / Linux 路径不同
|
|
136
140
|
let destDir;
|
|
137
141
|
if (plat === "darwin") {
|
|
138
142
|
destDir = join(homedir(), "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts");
|
|
139
143
|
} else if (plat === "linux") {
|
|
140
144
|
destDir = join(homedir(), ".config", "google-chrome", "NativeMessagingHosts");
|
|
141
145
|
} else {
|
|
142
|
-
console.log(c.yellow(
|
|
143
|
-
console.log(c.dim(" 参考:https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging"));
|
|
146
|
+
console.log(c.yellow(` ⚠ 不支持的平台 ${plat},跳过 native host 注册`));
|
|
144
147
|
return false;
|
|
145
148
|
}
|
|
146
149
|
|
|
@@ -165,6 +168,55 @@ function registerNativeHost() {
|
|
|
165
168
|
return true;
|
|
166
169
|
}
|
|
167
170
|
|
|
171
|
+
// Windows 注册:生成 host.bat + manifest,写注册表 HKCU
|
|
172
|
+
// Chrome 在 Windows 上通过注册表找 native host:
|
|
173
|
+
// HKCU\Software\Google\Chrome\NativeMessagingHosts\com.wssniffer.host
|
|
174
|
+
// 默认值 = manifest json 的绝对路径
|
|
175
|
+
function registerNativeHostWindows() {
|
|
176
|
+
console.log(c.dim(" Windows 平台:写注册表方式注册"));
|
|
177
|
+
|
|
178
|
+
const nativeDir = join(INSTALL_DIR, "native");
|
|
179
|
+
|
|
180
|
+
// 1. 生成 host.bat(等价于 macOS 的 host.sh,用绝对路径调 node)
|
|
181
|
+
// where node 拿 node 绝对路径
|
|
182
|
+
const whereRes = spawnSync("where", ["node"], { encoding: "utf8", shell: true });
|
|
183
|
+
const nodeBin = whereRes.status === 0 ? whereRes.stdout.trim().split(/\r?\n/)[0] : "node";
|
|
184
|
+
const hostBat = join(nativeDir, "host.bat");
|
|
185
|
+
// bat 里路径可能含空格(C:\Program Files\nodejs\node.exe),必须加引号
|
|
186
|
+
writeFileSync(hostBat, `@echo off\r\n"${nodeBin}" "${join(nativeDir, "host.js")}"\r\n`);
|
|
187
|
+
ok(`生成 host.bat → ${hostBat}`);
|
|
188
|
+
|
|
189
|
+
// 2. 生成 manifest json(allowed_origins 同 macOS/Linux)
|
|
190
|
+
const manifest = {
|
|
191
|
+
name: "com.wssniffer.host",
|
|
192
|
+
description: "Terminal bridge native messaging host",
|
|
193
|
+
path: hostBat,
|
|
194
|
+
type: "stdio",
|
|
195
|
+
allowed_origins: [`chrome-extension://${EXTENSION_ID}/`],
|
|
196
|
+
};
|
|
197
|
+
const manifestPath = join(nativeDir, "com.wssniffer.host.json");
|
|
198
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
199
|
+
ok(`生成 manifest → ${manifestPath}`);
|
|
200
|
+
|
|
201
|
+
// 3. 写注册表 HKCU(用户级,不需要管理员权限)
|
|
202
|
+
const regKey = "HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.wssniffer.host";
|
|
203
|
+
const regRes = spawnSync("reg", ["add", regKey, "/ve", "/t", "REG_SZ", "/d", manifestPath, "/f"],
|
|
204
|
+
{ stdio: "pipe", encoding: "utf8", shell: true });
|
|
205
|
+
if (regRes.status !== 0) {
|
|
206
|
+
fail(`注册表写入失败:${regRes.stderr || regRes.stdout}`);
|
|
207
|
+
}
|
|
208
|
+
ok(`已写注册表 ${regKey}`);
|
|
209
|
+
console.log(c.dim(` → ${manifestPath}`));
|
|
210
|
+
|
|
211
|
+
// 4. 校验注册表
|
|
212
|
+
const verifyRes = spawnSync("reg", ["query", regKey, "/ve"], { encoding: "utf8", shell: true });
|
|
213
|
+
if (verifyRes.status !== 0 || !verifyRes.stdout.includes(manifestPath)) {
|
|
214
|
+
fail("注册表校验失败");
|
|
215
|
+
}
|
|
216
|
+
ok("注册表校验通过");
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
168
220
|
// ===================== 步骤 4:安装 skill =====================
|
|
169
221
|
// 把 skill 释放到 ~/.agents/skills/jumpserver-term-bridge/
|
|
170
222
|
// 这样 Agent(如 ZCode)能自动发现并触发,知道怎么调用桥接、怎么处理各种错误。
|
|
@@ -211,6 +263,7 @@ function guideLoadExtension() {
|
|
|
211
263
|
|
|
212
264
|
// ① 打开 chrome://extensions
|
|
213
265
|
// macOS: open -a 指定 Chrome;open URL scheme 更可靠
|
|
266
|
+
// Windows: start 命令走默认关联程序(chrome:// 会由默认浏览器处理)
|
|
214
267
|
// Linux: 直接给 google-chrome 传 URL
|
|
215
268
|
let chromeOpened = false;
|
|
216
269
|
try {
|
|
@@ -221,6 +274,9 @@ function guideLoadExtension() {
|
|
|
221
274
|
spawnSync("open", ["chrome://extensions"], { stdio: "ignore" });
|
|
222
275
|
}
|
|
223
276
|
chromeOpened = true;
|
|
277
|
+
} else if (plat === "win32") {
|
|
278
|
+
spawnSync("cmd", ["/c", "start", "", "chrome://extensions"], { stdio: "ignore", shell: true });
|
|
279
|
+
chromeOpened = true;
|
|
224
280
|
} else if (plat === "linux") {
|
|
225
281
|
// 试几个常见的 Chrome 命令名
|
|
226
282
|
for (const bin of ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"]) {
|
|
@@ -239,6 +295,9 @@ function guideLoadExtension() {
|
|
|
239
295
|
// open <dir> 会用 Finder 打开,并选中该文件夹
|
|
240
296
|
spawnSync("open", [extDir], { stdio: "ignore" });
|
|
241
297
|
ok(`已在 Finder 打开 ${extDir}`);
|
|
298
|
+
} else if (plat === "win32") {
|
|
299
|
+
spawnSync("explorer", [extDir], { stdio: "ignore" });
|
|
300
|
+
ok(`已在资源管理器打开 ${extDir}`);
|
|
242
301
|
} else if (plat === "linux") {
|
|
243
302
|
spawnSync("xdg-open", [extDir], { stdio: "ignore" });
|
|
244
303
|
ok(`已在文件管理器打开 ${extDir}`);
|
|
@@ -301,7 +360,10 @@ async function main() {
|
|
|
301
360
|
console.log(` ${c.cyan("•")} 两个绿灯亮,即可通过 Agent 发命令`);
|
|
302
361
|
console.log("");
|
|
303
362
|
console.log(c.dim(`文件位置:${INSTALL_DIR}`));
|
|
304
|
-
|
|
363
|
+
const uninstallHint = platform() === "win32"
|
|
364
|
+
? `卸载:rd /s /q ${INSTALL_DIR} && reg delete HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.wssniffer.host /f`
|
|
365
|
+
: `卸载:rm -rf ${INSTALL_DIR} && rm ~/Library/Application\\ Support/Google/Chrome/NativeMessagingHosts/com.wssniffer.host.json`;
|
|
366
|
+
console.log(c.dim(uninstallHint));
|
|
305
367
|
console.log("");
|
|
306
368
|
}
|
|
307
369
|
|
package/files/native/host.js
CHANGED
|
@@ -59,15 +59,13 @@ function isProxyListening() {
|
|
|
59
59
|
|
|
60
60
|
function readPid() {
|
|
61
61
|
try {
|
|
62
|
-
const
|
|
63
|
-
|
|
62
|
+
const raw = fs.readFileSync(PID_FILE, "utf8").trim();
|
|
63
|
+
if (!/^\d{1,10}$/.test(raw)) return null; // 纯数字白名单
|
|
64
|
+
const pid = Number(raw);
|
|
65
|
+
return pid > 0 ? pid : null;
|
|
64
66
|
} catch { return null; }
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
function writePid(pid) {
|
|
68
|
-
try { fs.writeFileSync(PID_FILE, String(pid)); } catch {}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
69
|
function clearPid() {
|
|
72
70
|
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
73
71
|
}
|
|
@@ -79,21 +77,6 @@ function isPidAlive(pid) {
|
|
|
79
77
|
} catch { return false; }
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
// 通过端口找占用进程的 PID(PID 文件缺失时的回退方案)
|
|
83
|
-
// 用 lsof -ti :port 拿监听该端口的进程 PID
|
|
84
|
-
function findPidByPort(port) {
|
|
85
|
-
return new Promise((resolve) => {
|
|
86
|
-
const child = spawn("lsof", ["-ti", `-i:${port}`, "-sTCP:LISTEN"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
87
|
-
let out = "";
|
|
88
|
-
child.stdout.on("data", (d) => { out += d.toString(); });
|
|
89
|
-
child.on("error", () => resolve(null)); // lsof 不存在或没权限
|
|
90
|
-
child.on("close", () => {
|
|
91
|
-
const pid = parseInt(out.trim().split("\n")[0], 10);
|
|
92
|
-
resolve(isNaN(pid) ? null : pid);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
80
|
// ============== 命令处理 ==============
|
|
98
81
|
async function startProxy() {
|
|
99
82
|
// 已在运行?
|
|
@@ -117,7 +100,8 @@ async function startProxy() {
|
|
|
117
100
|
});
|
|
118
101
|
// unref 让父进程(host)可以不等子进程就退出
|
|
119
102
|
child.unref();
|
|
120
|
-
|
|
103
|
+
// 先落一份 spawn 返回的数字 PID(非外部输入);代理 listening 后会自写权威值
|
|
104
|
+
try { fs.writeFileSync(PID_FILE, String(child.pid)); } catch {}
|
|
121
105
|
fs.writeFileSync(logFd, `\n[host] proxy started PID=${child.pid} at ${new Date().toISOString()}\n`);
|
|
122
106
|
|
|
123
107
|
// 等一小段时间确认端口起来
|
|
@@ -135,40 +119,32 @@ async function startProxy() {
|
|
|
135
119
|
|
|
136
120
|
function stopProxy() {
|
|
137
121
|
return new Promise(async (resolve) => {
|
|
138
|
-
|
|
139
|
-
let pidSource = pid ? "pid file" : null;
|
|
140
|
-
|
|
141
|
-
// PID 文件缺失 → 回退用 lsof 按端口找
|
|
122
|
+
const pid = readPid();
|
|
142
123
|
if (!pid) {
|
|
124
|
+
// 没有PID 文件。代理启动时会自己写 PID(server.js listening 后落盘),
|
|
125
|
+
// 走到这里说明代理未通过正常途径启动——用端口状态兜底判断。
|
|
143
126
|
const up = await isProxyListening();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
pid = await findPidByPort(PROXY_PORT);
|
|
149
|
-
pidSource = pid ? "port lookup" : null;
|
|
150
|
-
if (!pid) {
|
|
151
|
-
resolve({ ok: false, status: "running", msg: "无法停止:代理在监听但找不到 PID(lsof 失败),请手动 kill" });
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
127
|
+
resolve(up
|
|
128
|
+
? { ok: false, status: "running", msg: "无法停止:PID 文件缺失,请手动停止占用 8787 端口的进程" }
|
|
129
|
+
: { ok: true, status: "stopped", msg: "proxy was not running" });
|
|
130
|
+
return;
|
|
154
131
|
}
|
|
155
132
|
|
|
156
133
|
try {
|
|
157
134
|
process.kill(pid, "SIGTERM");
|
|
158
|
-
// 等 2s
|
|
135
|
+
// 等 2s 确认退出(Windows 上 SIGTERM 即强制终止)
|
|
159
136
|
setTimeout(async () => {
|
|
160
137
|
if (isPidAlive(pid)) {
|
|
161
138
|
try { process.kill(pid, "SIGKILL"); } catch {}
|
|
162
139
|
}
|
|
163
140
|
clearPid();
|
|
164
|
-
// 再确认端口确实释放了
|
|
165
141
|
const stillUp = await isProxyListening();
|
|
166
142
|
resolve({
|
|
167
143
|
ok: !stillUp,
|
|
168
144
|
status: stillUp ? "running" : "stopped",
|
|
169
145
|
msg: stillUp
|
|
170
|
-
?
|
|
171
|
-
:
|
|
146
|
+
? "已发停止信号但端口仍被占用,可能有其他进程"
|
|
147
|
+
: "proxy stopped",
|
|
172
148
|
});
|
|
173
149
|
}, 2000);
|
|
174
150
|
} catch (err) {
|
package/files/proxy/server.js
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import { WebSocketServer } from "ws";
|
|
15
15
|
import { randomBytes } from "node:crypto";
|
|
16
|
+
import { writeFileSync, unlinkSync } from "node:fs";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
// PID 文件:native host「停止代理」时读取。代理自己在 listening 后写入。
|
|
20
|
+
const PID_FILE = fileURLToPath(new URL("./.proxy.pid", import.meta.url));
|
|
16
21
|
import {
|
|
17
22
|
auditArthasCommand,
|
|
18
23
|
isArthasCommand,
|
|
@@ -515,10 +520,19 @@ wss.on("listening", () => {
|
|
|
515
520
|
const { address, port } = wss.address();
|
|
516
521
|
console.log(TAG, `监听 ws://${address}:${port}/ssh`);
|
|
517
522
|
console.log(TAG, "等待插件连接(role: extension)和 Agent 连接(发 run 请求)");
|
|
523
|
+
|
|
524
|
+
// 写 PID 文件:native host 的「停止代理」靠它找到本进程。
|
|
525
|
+
// 由代理自己写(而不是 host 代写)保证准确性——手动 node server.js
|
|
526
|
+
// 启动时也有 PID 文件可停。进程自己写自己的 process.pid,无注入面。
|
|
527
|
+
try {
|
|
528
|
+
writeFileSync(PID_FILE, String(process.pid));
|
|
529
|
+
console.log(TAG, `PID ${process.pid} → ${PID_FILE}`);
|
|
530
|
+
} catch {}
|
|
518
531
|
});
|
|
519
532
|
|
|
520
533
|
function shutdown() {
|
|
521
534
|
console.error(TAG, "shutting down");
|
|
535
|
+
try { unlinkSync(PID_FILE); } catch {}
|
|
522
536
|
wss.clients.forEach((c) => c.close(1001, "shutting down"));
|
|
523
537
|
for (const [, entry] of pending) {
|
|
524
538
|
clearTimeout(entry.timer);
|
package/package.json
CHANGED