cdp-tunnel 2.10.12 → 3.0.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.
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
var ForwardHandler = (function() {
|
|
2
|
+
// 合成输入事件(keyboard/mouse)需要页面 visibility=visible 才能投递到 DOM。
|
|
3
|
+
// cdp-tunnel 的隔离 tab(active:false + 折叠分组)默认 visibility=hidden,
|
|
4
|
+
// 导致 Input.dispatchKeyEvent/dispatchMouseEvent 被 Chromium 丢弃。
|
|
5
|
+
// 这些命令发送前需要 Page.bringToFront 让页面变 visible + 恢复焦点。
|
|
6
|
+
var SYNTHETIC_INPUT_METHODS = [
|
|
7
|
+
'Input.dispatchKeyEvent',
|
|
8
|
+
'Input.dispatchMouseEvent'
|
|
9
|
+
];
|
|
10
|
+
|
|
2
11
|
function execute(context) {
|
|
3
12
|
var id = context.id;
|
|
4
13
|
var method = context.method;
|
|
@@ -19,11 +28,52 @@ var ForwardHandler = (function() {
|
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
Logger.debug('[Forward]', method, '-> tabId:', tabId);
|
|
31
|
+
|
|
32
|
+
// 合成输入事件需要页面 visible:先 ensureVisible 再发命令
|
|
33
|
+
if (SYNTHETIC_INPUT_METHODS.indexOf(method) >= 0) {
|
|
34
|
+
return ensureVisible(tabId).then(function() {
|
|
35
|
+
return chrome.debugger.sendCommand({ tabId: tabId }, method, params);
|
|
36
|
+
}).then(function(result) {
|
|
37
|
+
return result || {};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
22
41
|
return chrome.debugger.sendCommand({ tabId: tabId }, method, params).then(function(result) {
|
|
23
42
|
return result || {};
|
|
24
43
|
});
|
|
25
44
|
}
|
|
26
45
|
|
|
46
|
+
/**
|
|
47
|
+
* 让 tab 变 visible:Page.bringToFront + 等 visibilitychange + 恢复焦点。
|
|
48
|
+
* bringToFront 会重置页面元素焦点,需要保存/恢复。
|
|
49
|
+
*/
|
|
50
|
+
function ensureVisible(tabId) {
|
|
51
|
+
// 1. 保存焦点:给 activeElement 打标记
|
|
52
|
+
return chrome.debugger.sendCommand({ tabId: tabId }, 'Runtime.evaluate', {
|
|
53
|
+
expression: '(function(){var el=document.activeElement;if(el&&el!==document.body&&el.focus){el.setAttribute("data-cdp-saved-focus","1");return 1}return 0})()',
|
|
54
|
+
returnByValue: true
|
|
55
|
+
}).catch(function() { return { result: { value: 0 } }; }).then(function(res) {
|
|
56
|
+
var hadFocus = res && res.result && res.result.value;
|
|
57
|
+
|
|
58
|
+
// 2. bringToFront 让 visibility 从 hidden→visible
|
|
59
|
+
return chrome.debugger.sendCommand({ tabId: tabId }, 'Page.bringToFront', {}).then(function() {
|
|
60
|
+
// 3. 等 visibilitychange 事件 + 双 rAF(确保 renderer 完成切换)
|
|
61
|
+
return chrome.debugger.sendCommand({ tabId: tabId }, 'Runtime.evaluate', {
|
|
62
|
+
expression: 'new Promise(function(r){function ok(){requestAnimationFrame(function(){requestAnimationFrame(function(){r(1)})})}if(document.visibilityState==="visible"){ok()}else{var d=function(){if(document.visibilityState==="visible"){document.removeEventListener("visibilitychange",d);ok()}};document.addEventListener("visibilitychange",d);setTimeout(function(){document.removeEventListener("visibilitychange",d);ok()},3000)}})',
|
|
63
|
+
awaitPromise: true
|
|
64
|
+
});
|
|
65
|
+
}).then(function() {
|
|
66
|
+
// 4. 恢复焦点
|
|
67
|
+
if (hadFocus) {
|
|
68
|
+
return chrome.debugger.sendCommand({ tabId: tabId }, 'Runtime.evaluate', {
|
|
69
|
+
expression: '(function(){var el=document.querySelector("[data-cdp-saved-focus]");if(el){el.removeAttribute("data-cdp-saved-focus");el.focus();return 1}return 0})()',
|
|
70
|
+
returnByValue: true
|
|
71
|
+
}).catch(function() {});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
27
77
|
function resolveTabId(sessionId, state) {
|
|
28
78
|
if (!state) return null;
|
|
29
79
|
if (sessionId) {
|
package/package.json
CHANGED
package/server/modules/config.js
CHANGED
|
@@ -11,7 +11,10 @@ const CONFIG = {
|
|
|
11
11
|
AUTO_RESTART: process.env.AUTO_RESTART === 'true',
|
|
12
12
|
CHROME_RESTART_COOLDOWN: 30000,
|
|
13
13
|
PLUGIN_MAX_MISSED_PINGS: 3,
|
|
14
|
-
TAKEOVER_PORT: process.env.TAKEOVER_PORT ? parseInt(process.env.TAKEOVER_PORT) : (parseInt(process.env.PORT || '9221') + 1)
|
|
14
|
+
TAKEOVER_PORT: process.env.TAKEOVER_PORT ? parseInt(process.env.TAKEOVER_PORT) : (parseInt(process.env.PORT || '9221') + 1),
|
|
15
|
+
POOL_TAKEOVER_PORT: process.env.POOL_TAKEOVER_PORT ? parseInt(process.env.POOL_TAKEOVER_PORT) : 9220,
|
|
16
|
+
POOL_START: process.env.POOL_START ? parseInt(process.env.POOL_START) : 9222,
|
|
17
|
+
POOL_SIZE: process.env.POOL_SIZE ? parseInt(process.env.POOL_SIZE) : 9
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
const LOG_LEVELS = {
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 端口池管理器(v3.0)
|
|
5
|
+
*
|
|
6
|
+
* 在现有 proxy 之外,额外启动一组 create 端口(9222-9230)+ 一个 takeover 端口(9220)。
|
|
7
|
+
* 每个 create 端口 = 一个独立的隔离环境。
|
|
8
|
+
*
|
|
9
|
+
* 核心设计:
|
|
10
|
+
* - 复用现有的 plugin 连接(扩展只连一次 9221/plugin)
|
|
11
|
+
* - 每个 create 端口有独立的 PortSession(targetId 集合)
|
|
12
|
+
* - 命令转发给 plugin 时带 __portIndex 标记
|
|
13
|
+
* - plugin 返回的事件按 __portIndex 路由回对应端口
|
|
14
|
+
* - 对齐原生 Chrome:多客户端共享、断开不清理 tab
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const http = require('http');
|
|
18
|
+
const WebSocket = require('ws');
|
|
19
|
+
const { CONFIG } = require('./config');
|
|
20
|
+
|
|
21
|
+
class PortPoolManager {
|
|
22
|
+
constructor(mainProxy) {
|
|
23
|
+
this.mainProxy = mainProxy; // 现有 proxy 的引用(拿 plugin 连接)
|
|
24
|
+
this.createServers = []; // [http.Server] 每个 create 端口一个
|
|
25
|
+
this.createWss = []; // [WebSocket.Server] 每个 create 端口一个
|
|
26
|
+
this.portSessions = []; // [PortSession] 每个 create 端口一个
|
|
27
|
+
this.takeoverServer = null;
|
|
28
|
+
this.takeoverWss = null;
|
|
29
|
+
this.targetToPort = new Map(); // targetId → portIndex(事件路由用)
|
|
30
|
+
this.sessionToPort = new Map(); // CDP sessionId → portIndex
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* PortSession:一个 create 端口的隔离状态
|
|
35
|
+
*/
|
|
36
|
+
static PortSession = class {
|
|
37
|
+
constructor(portIndex, port) {
|
|
38
|
+
this.portIndex = portIndex;
|
|
39
|
+
this.port = port;
|
|
40
|
+
this.targetIds = new Set(); // 这个端口创建的所有 targetId
|
|
41
|
+
this.tabIds = new Set(); // Chrome tabId(扩展端返回的)
|
|
42
|
+
this.clients = new Set(); // 连接到这个端口的 client WebSocket
|
|
43
|
+
this.cdpIdCounter = 1; // CDP 命令 id 计数器
|
|
44
|
+
this.pendingRequests = new Map(); // id → {clientWs, resolve, reject}
|
|
45
|
+
this.targetToClient = new Map(); // targetId → clientWs(attachToTarget 时绑定)
|
|
46
|
+
this.browserWsUrl = null; // /json/version 返回的 webSocketDebuggerUrl
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
start() {
|
|
51
|
+
// 启动 create 端口(9222-9230)
|
|
52
|
+
for (let i = 0; i < CONFIG.POOL_SIZE; i++) {
|
|
53
|
+
const port = CONFIG.POOL_START + i;
|
|
54
|
+
this._startCreatePort(i, port);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 启动 takeover 端口(9220)
|
|
58
|
+
this._startTakeoverPort();
|
|
59
|
+
|
|
60
|
+
console.log(`\n[PORT POOL] Started: takeover=${CONFIG.POOL_TAKEOVER_PORT}, create=${CONFIG.POOL_START}-${CONFIG.POOL_START + CONFIG.POOL_SIZE - 1}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_startCreatePort(portIndex, port) {
|
|
64
|
+
const session = new PortPoolManager.PortSession(portIndex, port);
|
|
65
|
+
this.portSessions[portIndex] = session;
|
|
66
|
+
|
|
67
|
+
const server = http.createServer((req, res) => {
|
|
68
|
+
this._handleHttp(req, res, session);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const wss = new WebSocket.Server({ noServer: true });
|
|
72
|
+
|
|
73
|
+
server.on('upgrade', (req, socket, head) => {
|
|
74
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
75
|
+
const path = url.pathname;
|
|
76
|
+
|
|
77
|
+
// 只允许 client 连接(plugin 连的是 9221)
|
|
78
|
+
if (path !== '/client' && !path.startsWith('/client/') &&
|
|
79
|
+
!path.startsWith('/devtools/browser/') && !path.startsWith('/devtools/page/')) {
|
|
80
|
+
socket.destroy();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
85
|
+
this._handleClientConnect(ws, req, session);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
server.on('error', (err) => {
|
|
90
|
+
if (err.code === 'EADDRINUSE') {
|
|
91
|
+
console.error(`[PORT POOL] Port ${port} in use, skipping create port ${portIndex}`);
|
|
92
|
+
} else {
|
|
93
|
+
console.error(`[PORT POOL] Create port ${port} error:`, err.message);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
server.listen(port, '0.0.0.0', () => {
|
|
98
|
+
console.log(`[CREATE PORT ${portIndex}] Listening on ${port}`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.createServers[portIndex] = server;
|
|
102
|
+
this.createWss[portIndex] = wss;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_startTakeoverPort() {
|
|
106
|
+
const port = CONFIG.POOL_TAKEOVER_PORT;
|
|
107
|
+
const server = http.createServer((req, res) => {
|
|
108
|
+
// takeover 的 HTTP 请求转发给主 proxy 的 handleHttpRequest
|
|
109
|
+
req._takeoverMode = true;
|
|
110
|
+
this.mainProxy.handleHttpRequest(req, res);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
server.on('upgrade', (req, socket, head) => {
|
|
114
|
+
req._takeoverMode = true;
|
|
115
|
+
// takeover 的 WS 连接转发给主 proxy 的 wss
|
|
116
|
+
this.mainProxy.handleTakeoverUpgrade(req, socket, head);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
server.on('error', (err) => {
|
|
120
|
+
if (err.code === 'EADDRINUSE') {
|
|
121
|
+
console.error(`[PORT POOL] Takeover port ${port} in use`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
server.listen(port, '0.0.0.0', () => {
|
|
126
|
+
console.log(`[TAKEOVER POOL] Listening on ${port}`);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
this.takeoverServer = server;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* HTTP 请求处理(/json/version, /json/list 等)
|
|
134
|
+
* 每个 create 端口只返回自己的 target
|
|
135
|
+
*/
|
|
136
|
+
async _handleHttp(req, res, session) {
|
|
137
|
+
const url = new URL(req.url, `http://localhost:${session.port}`);
|
|
138
|
+
const path = url.pathname;
|
|
139
|
+
|
|
140
|
+
if (path === '/json/version') {
|
|
141
|
+
// 返回版本信息,webSocketDebuggerUrl 指向当前端口
|
|
142
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
143
|
+
res.end(JSON.stringify({
|
|
144
|
+
Browser: 'Chrome/131.0.6778.86 (cdp-tunnel)',
|
|
145
|
+
'Protocol-Version': '1.3',
|
|
146
|
+
'User-Agent': 'cdp-tunnel/3.0',
|
|
147
|
+
'webSocketDebuggerUrl': `ws://localhost:${session.port}/devtools/browser/pool_${session.portIndex}`
|
|
148
|
+
}));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (path === '/json' || path === '/json/list') {
|
|
153
|
+
// 只返回这个端口的 target(从主 proxy 拿全量后过滤)
|
|
154
|
+
const allTargets = await this.mainProxy.getAllTargets(session.portIndex);
|
|
155
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
156
|
+
res.end(JSON.stringify(allTargets));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (path === '/json/new') {
|
|
161
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
162
|
+
res.end(JSON.stringify({ error: 'Use Target.createTarget via WebSocket' }));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
167
|
+
res.end('Not Found');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Client WebSocket 连接处理
|
|
172
|
+
*/
|
|
173
|
+
_handleClientConnect(ws, req, session) {
|
|
174
|
+
session.clients.add(ws);
|
|
175
|
+
console.log(`[PORT ${session.port}] Client connected (total: ${session.clients.size})`);
|
|
176
|
+
|
|
177
|
+
// 找到 plugin 连接(从主 proxy 获取)
|
|
178
|
+
const pluginWs = this.mainProxy.getPluginConnection();
|
|
179
|
+
if (!pluginWs) {
|
|
180
|
+
ws.close(1011, 'No extension connected');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 消息处理:client → plugin(带 portIndex 标记)
|
|
185
|
+
ws.on('message', (data) => {
|
|
186
|
+
let msg;
|
|
187
|
+
try { msg = JSON.parse(data.toString()); } catch { return; }
|
|
188
|
+
|
|
189
|
+
if (msg.id !== undefined) {
|
|
190
|
+
// 命令:分配新 id,记录映射,转发给 plugin
|
|
191
|
+
const newId = `pool${session.portIndex}_${msg.id}`;
|
|
192
|
+
session.pendingRequests.set(newId, {
|
|
193
|
+
originalId: msg.id,
|
|
194
|
+
clientWs: ws
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// 特殊处理 createTarget:记录 targetId 归属
|
|
198
|
+
if (msg.method === 'Target.createTarget') {
|
|
199
|
+
msg.params = msg.params || {};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const forwarded = { ...msg, id: newId, __portIndex: session.portIndex };
|
|
203
|
+
pluginWs.send(JSON.stringify(forwarded));
|
|
204
|
+
} else {
|
|
205
|
+
// 无 id 的消息(事件),直接转发
|
|
206
|
+
pluginWs.send(JSON.stringify({ ...msg, __portIndex: session.portIndex }));
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
ws.on('close', () => {
|
|
211
|
+
session.clients.delete(ws);
|
|
212
|
+
console.log(`[PORT ${session.port}] Client disconnected (remaining: ${session.clients.size})`);
|
|
213
|
+
// 对齐原生 Chrome:断开不清理 tab
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
ws.on('error', () => {
|
|
217
|
+
session.clients.delete(ws);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 处理从 plugin 返回的消息(按 portIndex 路由回对应端口的 client)
|
|
223
|
+
* 返回 true 表示已处理(端口池的消息),false 表示不是端口池的
|
|
224
|
+
*/
|
|
225
|
+
handlePluginMessage(msg, pluginWs) {
|
|
226
|
+
if (!msg) return false;
|
|
227
|
+
|
|
228
|
+
// 1. 响应消息:id 以 pool 开头
|
|
229
|
+
if (msg.id && typeof msg.id === 'string' && msg.id.startsWith('pool')) {
|
|
230
|
+
const match = msg.id.match(/^pool(\d+)_(.+)$/);
|
|
231
|
+
if (!match) return false;
|
|
232
|
+
|
|
233
|
+
const portIndex = parseInt(match[1]);
|
|
234
|
+
const originalId = match[2];
|
|
235
|
+
const session = this.portSessions[portIndex];
|
|
236
|
+
if (!session) return false;
|
|
237
|
+
|
|
238
|
+
const pending = session.pendingRequests.get(msg.id);
|
|
239
|
+
session.pendingRequests.delete(msg.id);
|
|
240
|
+
|
|
241
|
+
// 如果是 createTarget 响应,记录 targetId → portIndex 归属
|
|
242
|
+
if (msg.result && msg.result.targetId) {
|
|
243
|
+
session.targetIds.add(msg.result.targetId);
|
|
244
|
+
this.targetToPort.set(msg.result.targetId, portIndex);
|
|
245
|
+
console.log(`[PORT POOL] targetId=${msg.result.targetId.slice(0,12)} → port ${session.port}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 如果是 attachToTarget 响应,记录 sessionId → portIndex
|
|
249
|
+
if (msg.result && msg.result.sessionId) {
|
|
250
|
+
this.sessionToPort.set(msg.result.sessionId, portIndex);
|
|
251
|
+
console.log(`[PORT POOL] sessionId=${msg.result.sessionId.slice(0,12)} → port ${session.port}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 恢复原始 id,发给发起请求的 client
|
|
255
|
+
const response = { ...msg, id: this._parseOriginalId(originalId) };
|
|
256
|
+
if (pending && pending.clientWs && pending.clientWs.readyState === WebSocket.OPEN) {
|
|
257
|
+
pending.clientWs.send(JSON.stringify(response));
|
|
258
|
+
} else {
|
|
259
|
+
// 广播给这个端口的所有 client
|
|
260
|
+
this._broadcastToPort(portIndex, response);
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 2. 事件消息(无 id):按 targetId 路由
|
|
266
|
+
if (msg.method && msg.params) {
|
|
267
|
+
// 从事件参数里提取 targetId
|
|
268
|
+
let targetId = null;
|
|
269
|
+
if (msg.params.targetId) targetId = msg.params.targetId;
|
|
270
|
+
else if (msg.params.targetInfo && msg.params.targetInfo.targetId) targetId = msg.params.targetInfo.targetId;
|
|
271
|
+
|
|
272
|
+
if (targetId) {
|
|
273
|
+
const portIndex = this.targetToPort.get(targetId);
|
|
274
|
+
if (portIndex !== undefined) {
|
|
275
|
+
const session = this.portSessions[portIndex];
|
|
276
|
+
if (session) {
|
|
277
|
+
this._broadcastToPort(portIndex, msg);
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// sessionId 路由(attachedToTarget 后的 session 事件)
|
|
284
|
+
if (msg.sessionId) {
|
|
285
|
+
const portIndex = this.sessionToPort.get(msg.sessionId);
|
|
286
|
+
if (portIndex !== undefined) {
|
|
287
|
+
this._broadcastToPort(portIndex, msg);
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return false; // 不是端口池的消息
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
_parseOriginalId(idStr) {
|
|
297
|
+
const n = parseInt(idStr);
|
|
298
|
+
return isNaN(n) ? idStr : n;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
_broadcastToPort(portIndex, msg) {
|
|
302
|
+
const session = this.portSessions[portIndex];
|
|
303
|
+
if (!session) return;
|
|
304
|
+
const data = JSON.stringify(msg);
|
|
305
|
+
session.clients.forEach(clientWs => {
|
|
306
|
+
if (clientWs.readyState === WebSocket.OPEN) {
|
|
307
|
+
clientWs.send(data);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
stop() {
|
|
313
|
+
this.createServers.forEach(s => { try { s.close(); } catch {} });
|
|
314
|
+
if (this.takeoverServer) { try { this.takeoverServer.close(); } catch {} }
|
|
315
|
+
console.log('[PORT POOL] Stopped');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = { PortPoolManager };
|
package/server/proxy-server.js
CHANGED
|
@@ -16,7 +16,11 @@ const path = require('path');
|
|
|
16
16
|
const os = require('os');
|
|
17
17
|
const { execSync, spawn: spawnProcess } = require('child_process');
|
|
18
18
|
const { CONFIG, BROWSER_ID, shouldLog } = require('./modules/config');
|
|
19
|
+
const { PortPoolManager } = require('./modules/port-pool');
|
|
19
20
|
const TAKEOVER_PORT = CONFIG.TAKEOVER_PORT;
|
|
21
|
+
|
|
22
|
+
// v3.0 端口池(提前声明,后面初始化)
|
|
23
|
+
let portPool = null;
|
|
20
24
|
const { logCDP, logEvent, clearLog, logStatus, logConnectionEvent, flushAllLogs, logDisconnect } = require('./modules/logger');
|
|
21
25
|
|
|
22
26
|
try {
|
|
@@ -394,7 +398,31 @@ async function handleHttpRequest(req, res) {
|
|
|
394
398
|
res.end(JSON.stringify(targetList));
|
|
395
399
|
return;
|
|
396
400
|
}
|
|
397
|
-
|
|
401
|
+
|
|
402
|
+
if (url.pathname === '/debug/maps') {
|
|
403
|
+
const stats = {};
|
|
404
|
+
for (const [pluginWs, ns] of pluginNamespaces) {
|
|
405
|
+
stats.targetIdToClientId = ns.targetIdToClientId.size;
|
|
406
|
+
stats.sessionToClientId = ns.sessionToClientId.size;
|
|
407
|
+
stats.browserContextToClientId = ns.browserContextToClientId.size;
|
|
408
|
+
stats.clientIdToBrowserContext = ns.clientIdToBrowserContext.size;
|
|
409
|
+
stats.pendingAttachedEvents = ns.pendingAttachedEvents.size;
|
|
410
|
+
stats.pendingTargetCreatedEvents = ns.pendingTargetCreatedEvents.size;
|
|
411
|
+
stats.pendingSessionToClientId = (ns.pendingSessionToClientId || new Map()).size;
|
|
412
|
+
stats.discoveringClientIds = ns.discoveringClientIds.size;
|
|
413
|
+
stats.cachedTargets = ns.cachedTargets.length;
|
|
414
|
+
}
|
|
415
|
+
stats.globalRequestIdMap = globalRequestIdMap.size;
|
|
416
|
+
stats.connectionPairs = connectionPairs.size;
|
|
417
|
+
stats.clientById = clientById.size;
|
|
418
|
+
stats.clientIdToPlugin = clientIdToPlugin.size;
|
|
419
|
+
stats.clientConnections = clientConnections.size;
|
|
420
|
+
stats.pluginConnections = pluginConnections.size;
|
|
421
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
422
|
+
res.end(JSON.stringify(stats, null, 2));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
398
426
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
399
427
|
res.end('Not Found');
|
|
400
428
|
}
|
|
@@ -543,6 +571,33 @@ function cleanupClient(ws, id, reason) {
|
|
|
543
571
|
for (const [tId, cId] of ns.targetIdToClientId.entries()) {
|
|
544
572
|
if (cId === id) ns.targetIdToClientId.delete(tId);
|
|
545
573
|
}
|
|
574
|
+
// session 清理:value 可能是 clientId(正常)或 targetId(旧 bug 残留,兼容清理)
|
|
575
|
+
const clientTargetIds = new Set();
|
|
576
|
+
for (const [tId, cId] of ns.targetIdToClientId.entries()) {
|
|
577
|
+
if (cId === id) clientTargetIds.add(tId);
|
|
578
|
+
}
|
|
579
|
+
for (const [sId, val] of ns.sessionToClientId.entries()) {
|
|
580
|
+
if (val === id || clientTargetIds.has(val)) {
|
|
581
|
+
ns.sessionToClientId.delete(sId);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// 清理 pending session(归属未定的暂存)
|
|
585
|
+
if (ns.pendingSessionToClientId) {
|
|
586
|
+
for (const [pSid, pTid] of ns.pendingSessionToClientId.entries()) {
|
|
587
|
+
if (clientTargetIds.has(pTid)) ns.pendingSessionToClientId.delete(pSid);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// 清理 pending 事件(targetCreated/attachedToTarget 缓存,防止泄漏)
|
|
591
|
+
if (ns.pendingAttachedEvents) {
|
|
592
|
+
for (const [pTid] of ns.pendingAttachedEvents.entries()) {
|
|
593
|
+
if (clientTargetIds.has(pTid)) ns.pendingAttachedEvents.delete(pTid);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (ns.pendingTargetCreatedEvents) {
|
|
597
|
+
for (const [pTid] of ns.pendingTargetCreatedEvents.entries()) {
|
|
598
|
+
if (clientTargetIds.has(pTid)) ns.pendingTargetCreatedEvents.delete(pTid);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
546
601
|
for (const [bcId, cId] of ns.browserContextToClientId.entries()) {
|
|
547
602
|
if (cId === id) ns.browserContextToClientId.delete(bcId);
|
|
548
603
|
}
|
|
@@ -739,7 +794,7 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
739
794
|
// 消息转发: Plugin -> Client
|
|
740
795
|
ws.on('message', (data) => {
|
|
741
796
|
console.log(`[PLUGIN MESSAGE] size=${data.length}`);
|
|
742
|
-
|
|
797
|
+
|
|
743
798
|
const messageSize = data.length;
|
|
744
799
|
let messagePreview;
|
|
745
800
|
let parsed;
|
|
@@ -754,7 +809,12 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
754
809
|
if (shouldLog('debug')) {
|
|
755
810
|
console.log(`[PLUGIN -> CLIENT] ${id}: ${messagePreview}`);
|
|
756
811
|
}
|
|
757
|
-
|
|
812
|
+
|
|
813
|
+
// v3.0 端口池 hook:先让 PortPoolManager 处理端口池的消息
|
|
814
|
+
if (parsed && portPool && portPool.handlePluginMessage(parsed, ws)) {
|
|
815
|
+
return; // 已被端口池处理
|
|
816
|
+
}
|
|
817
|
+
|
|
758
818
|
// 处理 keepalive 消息
|
|
759
819
|
if (parsed && parsed.type === 'keepalive') {
|
|
760
820
|
ws.isAlive = true;
|
|
@@ -874,8 +934,11 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
874
934
|
ns.sessionToClientId.set(sessionId, clientId);
|
|
875
935
|
console.log(`[SESSION MAPPED] sessionId=${sessionId?.substring(0,8) || 'none'} -> clientId=${clientId?.substring(0,8) || 'none'}`);
|
|
876
936
|
} else {
|
|
877
|
-
|
|
878
|
-
|
|
937
|
+
// 以前这里存 targetId 作为 value,导致 cleanupClient 按 clientId 匹配时清不掉(泄漏)
|
|
938
|
+
// 改为:暂存到 pendingSessionToClientId,等 targetId 归属绑定时再补绑
|
|
939
|
+
if (!ns.pendingSessionToClientId) ns.pendingSessionToClientId = new Map();
|
|
940
|
+
ns.pendingSessionToClientId.set(sessionId, targetId);
|
|
941
|
+
console.log(`[SESSION MAPPED] sessionId=${sessionId?.substring(0,8) || 'none'} -> targetId=${targetId?.substring(0,8) || 'none'} (pending, no clientId yet)`);
|
|
879
942
|
}
|
|
880
943
|
}
|
|
881
944
|
}
|
|
@@ -940,7 +1003,19 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
940
1003
|
const targetId = parsed.result.targetId;
|
|
941
1004
|
ns.targetIdToClientId.set(targetId, mapping.clientId);
|
|
942
1005
|
console.log(`[TARGET MAPPED] targetId=${targetId} -> clientId=${mapping.clientId} mapSize=${ns.targetIdToClientId.size}`);
|
|
943
|
-
|
|
1006
|
+
|
|
1007
|
+
// 补绑 pending 的 session(之前 targetId 归属未定时暂存的)
|
|
1008
|
+
if (ns.pendingSessionToClientId && ns.pendingSessionToClientId.size > 0) {
|
|
1009
|
+
const pendingSessionId = null;
|
|
1010
|
+
for (const [pSid, pTid] of ns.pendingSessionToClientId.entries()) {
|
|
1011
|
+
if (pTid === targetId) {
|
|
1012
|
+
ns.sessionToClientId.set(pSid, mapping.clientId);
|
|
1013
|
+
ns.pendingSessionToClientId.delete(pSid);
|
|
1014
|
+
console.log(`[SESSION MAPPED from pending] sessionId=${pSid?.substring(0,8)} -> clientId=${mapping.clientId?.substring(0,8)} (targetId=${targetId?.substring(0,8)})`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
944
1019
|
const cachedCreated = ns.pendingTargetCreatedEvents.get(targetId);
|
|
945
1020
|
if (cachedCreated) {
|
|
946
1021
|
clientWs.send(cachedCreated.cdpData);
|
|
@@ -2048,3 +2123,29 @@ takeoverServer.on('error', (err) => {
|
|
|
2048
2123
|
takeoverServer.listen(TAKEOVER_PORT, '0.0.0.0', () => {
|
|
2049
2124
|
console.log(`[TAKEOVER] Listening on port ${TAKEOVER_PORT}`);
|
|
2050
2125
|
});
|
|
2126
|
+
|
|
2127
|
+
// v3.0 端口池启动
|
|
2128
|
+
portPool = new PortPoolManager({
|
|
2129
|
+
getPluginConnection: () => {
|
|
2130
|
+
for (const ws of pluginConnections) return ws;
|
|
2131
|
+
return null;
|
|
2132
|
+
},
|
|
2133
|
+
getAllTargets: async (portIndex) => {
|
|
2134
|
+
const session = portPool.portSessions[portIndex];
|
|
2135
|
+
if (!session) return [];
|
|
2136
|
+
// 从主 proxy 的 namespace 拿所有 target,过滤出这个端口的
|
|
2137
|
+
const targets = [];
|
|
2138
|
+
for (const [, ns] of pluginNamespaces) {
|
|
2139
|
+
if (ns.cachedTargets) {
|
|
2140
|
+
for (const t of ns.cachedTargets) {
|
|
2141
|
+
if (session.targetIds.has(t.targetId)) {
|
|
2142
|
+
targets.push(t);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return targets;
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
portPool.start();
|
|
2151
|
+
|