cdp-tunnel 2.10.11 → 2.10.14
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) {
|
|
@@ -501,7 +501,8 @@ var LocalHandler = (function() {
|
|
|
501
501
|
targetId: target.id || String(target.tabId),
|
|
502
502
|
type: target.type || 'page',
|
|
503
503
|
title: target.title || '',
|
|
504
|
-
|
|
504
|
+
// 原生 Chrome CDP 中 page 类型 target 的 url 一定存在(至少 "about:blank")
|
|
505
|
+
url: target.url || 'about:blank',
|
|
505
506
|
attached: !!target.attached,
|
|
506
507
|
canAccessOpener: false,
|
|
507
508
|
browserContextId: 'default'
|
|
@@ -72,6 +72,10 @@ var DebuggerManager = (function() {
|
|
|
72
72
|
})();
|
|
73
73
|
`;
|
|
74
74
|
|
|
75
|
+
// 跟踪每个 tab 最后一次非 about:blank 的导航 url
|
|
76
|
+
// 用于过滤 attach 时 Chrome 重放的 about:blank frameNavigated 事件
|
|
77
|
+
var tabLastRealUrl = {};
|
|
78
|
+
|
|
75
79
|
function attach(tabId, connState) {
|
|
76
80
|
var state = connState || _getAnyStateForTab(tabId);
|
|
77
81
|
if (tabId == null) {
|
|
@@ -289,6 +293,23 @@ var DebuggerManager = (function() {
|
|
|
289
293
|
}
|
|
290
294
|
}
|
|
291
295
|
|
|
296
|
+
// 过滤 attach 时 Chrome 重放的 about:blank frameNavigated 事件
|
|
297
|
+
// 原生 Chrome CDP 中,navigate 成功后不会回退到 about:blank
|
|
298
|
+
if (method === 'Page.frameNavigated' && params && params.frame) {
|
|
299
|
+
var navUrl = params.frame.url || '';
|
|
300
|
+
var tabIdKey = String(source.tabId);
|
|
301
|
+
|
|
302
|
+
if (navUrl && navUrl !== 'about:blank') {
|
|
303
|
+
// 记录真实导航 url
|
|
304
|
+
tabLastRealUrl[tabIdKey] = navUrl;
|
|
305
|
+
} else if (navUrl === 'about:blank' && tabLastRealUrl[tabIdKey]) {
|
|
306
|
+
// tab 已经导航到真实 url,但收到 about:blank 的 frameNavigated
|
|
307
|
+
// 这是 attach 时 Chrome 重放的旧事件,过滤掉
|
|
308
|
+
Logger.info('[Event] Filtering stale about:blank frameNavigated for tab', source.tabId, '(real url:', tabLastRealUrl[tabIdKey] + ')');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
292
313
|
for (var i = 0; i < sessionIds.length; i++) {
|
|
293
314
|
EventBuilder.send(method, params, sessionIds[i], wsManager);
|
|
294
315
|
}
|
|
@@ -297,6 +318,8 @@ var DebuggerManager = (function() {
|
|
|
297
318
|
function handleDetach(source, reason) {
|
|
298
319
|
Logger.info('[Debugger] Detached from tab', source.tabId, ', reason:', reason);
|
|
299
320
|
|
|
321
|
+
delete tabLastRealUrl[String(source.tabId)];
|
|
322
|
+
|
|
300
323
|
var entry = ConnectionManager.getConnectionByTabId(source.tabId);
|
|
301
324
|
var state = entry ? entry.state : null;
|
|
302
325
|
var wsManager = entry ? entry.wsManager : null;
|
package/package.json
CHANGED
package/server/proxy-server.js
CHANGED
|
@@ -361,17 +361,9 @@ async function handleHttpRequest(req, res) {
|
|
|
361
361
|
if (url.pathname === '/json' || url.pathname === '/json/' ||
|
|
362
362
|
url.pathname === '/json/list' || url.pathname === '/json/list/' ||
|
|
363
363
|
url.pathname.match(/^\/json\/list\/[^/]+$/)) {
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
if (!req._takeoverMode) {
|
|
368
|
-
if (shouldLog('info')) {
|
|
369
|
-
console.log(`[JSON LIST] create mode — returning empty list for isolation`);
|
|
370
|
-
}
|
|
371
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
372
|
-
res.end(JSON.stringify([]));
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
364
|
+
// 注意:Playwright connectOverCDP 和 Puppeteer connect 都依赖 /json/list 发现 targets
|
|
365
|
+
// HTTP 端无 clientId 上下文,无法做归属过滤,但 attach 路径(handlePageConnection)有归属校验
|
|
366
|
+
// 所以即使列表可见,无归属 target 也无法被 attach(close 1008)
|
|
375
367
|
const pluginWs = resolvePluginFromUrl(url);
|
|
376
368
|
const targets = await requestTargetsFromPlugin(pluginWs);
|
|
377
369
|
const browserId = pluginWs ? pluginWs.pluginId : BROWSER_ID;
|
|
@@ -402,7 +394,31 @@ async function handleHttpRequest(req, res) {
|
|
|
402
394
|
res.end(JSON.stringify(targetList));
|
|
403
395
|
return;
|
|
404
396
|
}
|
|
405
|
-
|
|
397
|
+
|
|
398
|
+
if (url.pathname === '/debug/maps') {
|
|
399
|
+
const stats = {};
|
|
400
|
+
for (const [pluginWs, ns] of pluginNamespaces) {
|
|
401
|
+
stats.targetIdToClientId = ns.targetIdToClientId.size;
|
|
402
|
+
stats.sessionToClientId = ns.sessionToClientId.size;
|
|
403
|
+
stats.browserContextToClientId = ns.browserContextToClientId.size;
|
|
404
|
+
stats.clientIdToBrowserContext = ns.clientIdToBrowserContext.size;
|
|
405
|
+
stats.pendingAttachedEvents = ns.pendingAttachedEvents.size;
|
|
406
|
+
stats.pendingTargetCreatedEvents = ns.pendingTargetCreatedEvents.size;
|
|
407
|
+
stats.pendingSessionToClientId = (ns.pendingSessionToClientId || new Map()).size;
|
|
408
|
+
stats.discoveringClientIds = ns.discoveringClientIds.size;
|
|
409
|
+
stats.cachedTargets = ns.cachedTargets.length;
|
|
410
|
+
}
|
|
411
|
+
stats.globalRequestIdMap = globalRequestIdMap.size;
|
|
412
|
+
stats.connectionPairs = connectionPairs.size;
|
|
413
|
+
stats.clientById = clientById.size;
|
|
414
|
+
stats.clientIdToPlugin = clientIdToPlugin.size;
|
|
415
|
+
stats.clientConnections = clientConnections.size;
|
|
416
|
+
stats.pluginConnections = pluginConnections.size;
|
|
417
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
418
|
+
res.end(JSON.stringify(stats, null, 2));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
406
422
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
407
423
|
res.end('Not Found');
|
|
408
424
|
}
|
|
@@ -551,6 +567,33 @@ function cleanupClient(ws, id, reason) {
|
|
|
551
567
|
for (const [tId, cId] of ns.targetIdToClientId.entries()) {
|
|
552
568
|
if (cId === id) ns.targetIdToClientId.delete(tId);
|
|
553
569
|
}
|
|
570
|
+
// session 清理:value 可能是 clientId(正常)或 targetId(旧 bug 残留,兼容清理)
|
|
571
|
+
const clientTargetIds = new Set();
|
|
572
|
+
for (const [tId, cId] of ns.targetIdToClientId.entries()) {
|
|
573
|
+
if (cId === id) clientTargetIds.add(tId);
|
|
574
|
+
}
|
|
575
|
+
for (const [sId, val] of ns.sessionToClientId.entries()) {
|
|
576
|
+
if (val === id || clientTargetIds.has(val)) {
|
|
577
|
+
ns.sessionToClientId.delete(sId);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// 清理 pending session(归属未定的暂存)
|
|
581
|
+
if (ns.pendingSessionToClientId) {
|
|
582
|
+
for (const [pSid, pTid] of ns.pendingSessionToClientId.entries()) {
|
|
583
|
+
if (clientTargetIds.has(pTid)) ns.pendingSessionToClientId.delete(pSid);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// 清理 pending 事件(targetCreated/attachedToTarget 缓存,防止泄漏)
|
|
587
|
+
if (ns.pendingAttachedEvents) {
|
|
588
|
+
for (const [pTid] of ns.pendingAttachedEvents.entries()) {
|
|
589
|
+
if (clientTargetIds.has(pTid)) ns.pendingAttachedEvents.delete(pTid);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (ns.pendingTargetCreatedEvents) {
|
|
593
|
+
for (const [pTid] of ns.pendingTargetCreatedEvents.entries()) {
|
|
594
|
+
if (clientTargetIds.has(pTid)) ns.pendingTargetCreatedEvents.delete(pTid);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
554
597
|
for (const [bcId, cId] of ns.browserContextToClientId.entries()) {
|
|
555
598
|
if (cId === id) ns.browserContextToClientId.delete(bcId);
|
|
556
599
|
}
|
|
@@ -882,8 +925,11 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
882
925
|
ns.sessionToClientId.set(sessionId, clientId);
|
|
883
926
|
console.log(`[SESSION MAPPED] sessionId=${sessionId?.substring(0,8) || 'none'} -> clientId=${clientId?.substring(0,8) || 'none'}`);
|
|
884
927
|
} else {
|
|
885
|
-
|
|
886
|
-
|
|
928
|
+
// 以前这里存 targetId 作为 value,导致 cleanupClient 按 clientId 匹配时清不掉(泄漏)
|
|
929
|
+
// 改为:暂存到 pendingSessionToClientId,等 targetId 归属绑定时再补绑
|
|
930
|
+
if (!ns.pendingSessionToClientId) ns.pendingSessionToClientId = new Map();
|
|
931
|
+
ns.pendingSessionToClientId.set(sessionId, targetId);
|
|
932
|
+
console.log(`[SESSION MAPPED] sessionId=${sessionId?.substring(0,8) || 'none'} -> targetId=${targetId?.substring(0,8) || 'none'} (pending, no clientId yet)`);
|
|
887
933
|
}
|
|
888
934
|
}
|
|
889
935
|
}
|
|
@@ -948,7 +994,19 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
948
994
|
const targetId = parsed.result.targetId;
|
|
949
995
|
ns.targetIdToClientId.set(targetId, mapping.clientId);
|
|
950
996
|
console.log(`[TARGET MAPPED] targetId=${targetId} -> clientId=${mapping.clientId} mapSize=${ns.targetIdToClientId.size}`);
|
|
951
|
-
|
|
997
|
+
|
|
998
|
+
// 补绑 pending 的 session(之前 targetId 归属未定时暂存的)
|
|
999
|
+
if (ns.pendingSessionToClientId && ns.pendingSessionToClientId.size > 0) {
|
|
1000
|
+
const pendingSessionId = null;
|
|
1001
|
+
for (const [pSid, pTid] of ns.pendingSessionToClientId.entries()) {
|
|
1002
|
+
if (pTid === targetId) {
|
|
1003
|
+
ns.sessionToClientId.set(pSid, mapping.clientId);
|
|
1004
|
+
ns.pendingSessionToClientId.delete(pSid);
|
|
1005
|
+
console.log(`[SESSION MAPPED from pending] sessionId=${pSid?.substring(0,8)} -> clientId=${mapping.clientId?.substring(0,8)} (targetId=${targetId?.substring(0,8)})`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
952
1010
|
const cachedCreated = ns.pendingTargetCreatedEvents.get(targetId);
|
|
953
1011
|
if (cachedCreated) {
|
|
954
1012
|
clientWs.send(cachedCreated.cdpData);
|