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 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("Windows 平台需要手动配置 native host,跳过自动注册"));
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
- console.log(c.dim(`卸载:rm -rf ${INSTALL_DIR} && rm ~/Library/Application\\ Support/Google/Chrome/NativeMessagingHosts/com.wssniffer.host.json`));
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
 
@@ -59,15 +59,13 @@ function isProxyListening() {
59
59
 
60
60
  function readPid() {
61
61
  try {
62
- const pid = parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
63
- return isNaN(pid) ? null : pid;
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
- writePid(child.pid);
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
- let pid = readPid();
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
- if (!up) {
145
- resolve({ ok: true, status: "stopped", msg: "proxy was not running" });
146
- return;
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
- ? `SIGTERM+SIGKILL 已发(PID ${pid} via ${pidSource})但端口仍被占用,可能有其他进程`
171
- : `proxy stopped (PID ${pid} via ${pidSource})`,
146
+ ? "已发停止信号但端口仍被占用,可能有其他进程"
147
+ : "proxy stopped",
172
148
  });
173
149
  }, 2000);
174
150
  } catch (err) {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-bridge-setup",
3
- "version": "2.6.1",
3
+ "version": "2.7.0",
4
4
  "description": "一次性安装器:释放终端桥接(JumpServer / Arthas)的本地代理 + Chrome 插件,并注册 native messaging host。让 Agent 能通过浏览器 xterm 终端执行命令并拿回输出。",
5
5
  "license": "MIT",
6
6
  "author": "encorearon",