terminal-bridge-setup 2.2.0 → 2.4.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.
@@ -288,7 +288,9 @@ function connectBridge() {
288
288
 
289
289
  // ============== 手动扫描 xterm(popup 触发)==============
290
290
  // 遍历所有 tab 的所有 frame,发 term-ping 探测有没有 xterm。
291
- // 找到就登记到 termReadyTabs attach debugger,免去刷新页面的麻烦。
291
+ // 关键:reload 插件后已打开的页面里没有 content script,ping 无人响应。
292
+ // 所以 ping 失败时用 chrome.scripting 主动注入 content.js,再 ping 一次。
293
+ // 这样用户 reload 插件后不用刷新页面,点「捕捉终端」就能自动恢复。
292
294
  async function scanAllTabsForXterm() {
293
295
  const tabs = await chrome.tabs.query({});
294
296
  let found = 0;
@@ -306,26 +308,25 @@ async function scanAllTabsForXterm() {
306
308
  } catch { continue; }
307
309
  if (!frames) continue;
308
310
 
309
- // 对每个 frame 发 term-ping(必须指定 frameId 才能到 iframe)
310
311
  for (const frame of frames) {
311
- const res = await new Promise((resolve) => {
312
- chrome.tabs.sendMessage(
313
- tab.id,
314
- { type: "term-ping" },
315
- { frameId: frame.frameId },
316
- (r) => {
317
- if (chrome.runtime.lastError) { resolve(null); return; }
318
- resolve(r);
319
- }
320
- );
321
- });
312
+ // 第一次 ping(content script 可能已经在跑)
313
+ let res = await pingFrame(tab.id, frame.frameId);
314
+
315
+ // ping 失败:content script 没注入(reload 插件后常见)→ 主动注入再 ping
316
+ if (!res) {
317
+ console.log("[bg] ping 失败,尝试注入 content.js 到 tab", tab.id, "frame", frame.frameId);
318
+ await injectContentScript(tab.id, frame.frameId);
319
+ // 注入后等一下让 IIFE 执行 + term-ping 监听器注册
320
+ await new Promise(r => setTimeout(r, 200));
321
+ res = await pingFrame(tab.id, frame.frameId);
322
+ }
323
+
322
324
  if (res && res.ready) {
323
325
  found++;
324
326
  termReadyTabs.add(tab.id);
325
- termReadyFrames.set(tab.id, frame.frameId); // 记下 frame,注入时直接用
327
+ termReadyFrames.set(tab.id, frame.frameId);
326
328
  foundTabs.push({ tabId: tab.id, href: res.href });
327
329
  console.log("[bg] 扫描发现 xterm: tab", tab.id, "frame", frame.frameId, res.href);
328
- // 自动 attach
329
330
  attachDebugger(tab.id);
330
331
  break; // 一个 tab 找到一个就够
331
332
  }
@@ -341,6 +342,39 @@ async function scanAllTabsForXterm() {
341
342
  };
342
343
  }
343
344
 
345
+ // 向指定 frame 发 term-ping
346
+ function pingFrame(tabId, frameId) {
347
+ return new Promise((resolve) => {
348
+ try {
349
+ chrome.tabs.sendMessage(
350
+ tabId,
351
+ { type: "term-ping" },
352
+ { frameId },
353
+ (r) => {
354
+ if (chrome.runtime.lastError) { resolve(null); return; }
355
+ resolve(r);
356
+ }
357
+ );
358
+ } catch { resolve(null); }
359
+ });
360
+ }
361
+
362
+ // 用 chrome.scripting 主动注入 content.js 到指定 frame
363
+ // 解决 reload 插件后已打开页面没有 content script 的问题
364
+ async function injectContentScript(tabId, frameId) {
365
+ try {
366
+ await chrome.scripting.executeScript({
367
+ target: { tabId, frameIds: [frameId] },
368
+ files: ["content.js"],
369
+ });
370
+ return true;
371
+ } catch (err) {
372
+ // 有些 frame(如 about:blank)注入会失败,忽略
373
+ console.log("[bg] 注入 content.js 失败 tab", tabId, "frame", frameId, ":", err.message);
374
+ return false;
375
+ }
376
+ }
377
+
344
378
  function sendToBridge(obj) {
345
379
  if (bridgeWs && bridgeWs.readyState === WebSocket.OPEN) {
346
380
  try { bridgeWs.send(JSON.stringify(obj)); } catch {}
@@ -401,15 +435,35 @@ async function injectToXterm(text, reqId) {
401
435
  return;
402
436
  }
403
437
 
438
+ // 第一轮:ping 现有 content script
404
439
  for (const frame of frames) {
405
440
  const r = await sendToFrame(tabId, frame.frameId, text, reqId, true /*pingOnly*/);
406
441
  if (r) {
407
442
  targetFrameId = frame.frameId;
408
- termReadyFrames.set(tabId, targetFrameId); // 记下来,下次直接用
443
+ termReadyFrames.set(tabId, targetFrameId);
409
444
  ok = await sendToFrame(tabId, targetFrameId, text, reqId);
410
445
  break;
411
446
  }
412
447
  }
448
+
449
+ // 第二轮:第一轮全失败说明 content script 没注入(reload 插件后常见)
450
+ // 主动注入 content.js 到每个 frame,再 ping + 注入
451
+ if (!ok) {
452
+ console.log("[bg] 降级探测全失败,尝试主动注入 content.js");
453
+ for (const frame of frames) {
454
+ await injectContentScript(tabId, frame.frameId);
455
+ }
456
+ await new Promise(r => setTimeout(r, 200)); // 等 IIFE 执行
457
+ for (const frame of frames) {
458
+ const r = await sendToFrame(tabId, frame.frameId, text, reqId, true /*pingOnly*/);
459
+ if (r) {
460
+ targetFrameId = frame.frameId;
461
+ termReadyFrames.set(tabId, targetFrameId);
462
+ ok = await sendToFrame(tabId, targetFrameId, text, reqId);
463
+ break;
464
+ }
465
+ }
466
+ }
413
467
  }
414
468
 
415
469
  if (ok) {
@@ -4,7 +4,7 @@
4
4
  "version": "2.0.0",
5
5
  "description": "桥接 JumpServer Web 终端,让 Agent 能远程执行命令并获取输出",
6
6
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtsnqR6PcFUueZwYria79tVbstvjk+tM7PpvIXILm5xbd6bAdjDIhzg3lsnKioVfvxjfvT+s6vJsiOYa9ojVZyJMFc5m/05TYqr770ovYwQmz0e88fmiy6dUoSulbtKvBCSLbN6OOL7u+ul8ixLZ/HautxSmou/eNgAFPmhE+4UueE7wfCqcgMYvjLvEzlqTVumMW+5LKw9YsRk6WhHPghY1a3MVUn3eQOWXBtQTEUy3wBM3v4wHxLwDeinVOR4f/P87IlUNo84C5DeimoFit0qCj3K04hS8MIYCLCYZc3v9ftRJDJBkAoah6Eaqj7JajbS3KvLR1ctOYfDhubgmURQIDAQAB",
7
- "permissions": ["debugger", "tabs", "alarms", "webNavigation", "nativeMessaging"],
7
+ "permissions": ["debugger", "tabs", "alarms", "webNavigation", "nativeMessaging", "scripting"],
8
8
  "background": {
9
9
  "service_worker": "background.js"
10
10
  },
@@ -79,6 +79,21 @@ function isPidAlive(pid) {
79
79
  } catch { return false; }
80
80
  }
81
81
 
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
+
82
97
  // ============== 命令处理 ==============
83
98
  async function startProxy() {
84
99
  // 已在运行?
@@ -120,24 +135,41 @@ async function startProxy() {
120
135
 
121
136
  function stopProxy() {
122
137
  return new Promise(async (resolve) => {
123
- const pid = readPid();
138
+ let pid = readPid();
139
+ let pidSource = pid ? "pid file" : null;
140
+
141
+ // PID 文件缺失 → 回退用 lsof 按端口找
124
142
  if (!pid) {
125
- // 没有 PID 文件,但端口可能在监听(手动启动的),尝试用 lsof 找
126
143
  const up = await isProxyListening();
127
- resolve(up
128
- ? { ok: false, status: "running", msg: "无法停止:PID 文件缺失(可能是手动启动的代理),请手动 kill" }
129
- : { ok: true, status: "stopped", msg: "proxy was not running" });
130
- return;
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
+ }
131
154
  }
155
+
132
156
  try {
133
157
  process.kill(pid, "SIGTERM");
134
158
  // 等 2s 确认退出
135
- setTimeout(() => {
159
+ setTimeout(async () => {
136
160
  if (isPidAlive(pid)) {
137
161
  try { process.kill(pid, "SIGKILL"); } catch {}
138
162
  }
139
163
  clearPid();
140
- resolve({ ok: true, status: "stopped", msg: "proxy stopped" });
164
+ // 再确认端口确实释放了
165
+ const stillUp = await isProxyListening();
166
+ resolve({
167
+ ok: !stillUp,
168
+ status: stillUp ? "running" : "stopped",
169
+ msg: stillUp
170
+ ? `SIGTERM+SIGKILL 已发(PID ${pid} via ${pidSource})但端口仍被占用,可能有其他进程`
171
+ : `proxy stopped (PID ${pid} via ${pidSource})`,
172
+ });
141
173
  }, 2000);
142
174
  } catch (err) {
143
175
  clearPid();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-bridge-setup",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "一次性安装器:释放终端桥接(JumpServer / Arthas)的本地代理 + Chrome 插件,并注册 native messaging host。让 Agent 能通过浏览器 xterm 终端执行命令并拿回输出。",
5
5
  "license": "MIT",
6
6
  "author": "encorearon",