fix(connect): stable per-tab relay session id — re-attach auto-recovers (#17)
When a tab's chrome.debugger session was torn down and re-established (cross-process navigation, MV3 service-worker restart wiping the in-memory maps, DevTools stealing the debugger), the extension minted a brand-new monotonic `cb-tab-N` for the same tab. The daemon stays bound to the old id and the relay consumes attach/detach events without telling it to rebind, so the session was orphaned permanently → `stale sessionId / tab is gone`, and re-open never recovered. Derive the session id from the STABLE Chrome tabId (`cb-tab-<tabId>`) instead. Any re-attach of the same tab now restores the SAME session the daemon already holds, so eval/snapshot transparently follow the new page after a navigation. Extension 0.4.3 → 0.4.4. Adds a relay unit test for the detach→reattach-same- session recovery contract.
This commit is contained in:
@@ -330,6 +330,45 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reattach_with_same_session_restores_target() {
|
||||||
|
// Issue #17 recovery contract. A tab's chrome.debugger session is torn
|
||||||
|
// down (cross-process nav, SW restart, …) then re-attached. The fix has
|
||||||
|
// the extension reuse the SAME `cb-tab-<tabId>` id across that churn, so
|
||||||
|
// after detach+reattach the relay must expose the NEW target under the
|
||||||
|
// SAME session — which is exactly the session the daemon is still bound
|
||||||
|
// to, so its eval/snapshot auto-follow the new page instead of going stale.
|
||||||
|
let mut s = RelayState::new();
|
||||||
|
s.handle_ext_message(&attached_event("T_old", "cb-tab-42"), "tok");
|
||||||
|
s.handle_ext_message(
|
||||||
|
&json!({
|
||||||
|
"method": "forwardCDPEvent",
|
||||||
|
"params": { "method": "Target.detachedFromTarget", "params": { "sessionId": "cb-tab-42" } }
|
||||||
|
}),
|
||||||
|
"tok",
|
||||||
|
);
|
||||||
|
s.handle_ext_message(&attached_event("T_new", "cb-tab-42"), "tok");
|
||||||
|
|
||||||
|
let route = s.route_client_command(1, &json!({ "id": 1, "method": "Target.getTargets" }));
|
||||||
|
match route {
|
||||||
|
ClientRoute::Local(v) => {
|
||||||
|
let infos = v["result"]["targetInfos"].as_array().unwrap();
|
||||||
|
assert_eq!(infos.len(), 1, "only the new target should remain");
|
||||||
|
assert_eq!(infos[0]["targetId"], "T_new");
|
||||||
|
}
|
||||||
|
_ => panic!("getTargets must be local"),
|
||||||
|
}
|
||||||
|
// The daemon's existing session id still resolves — to the new target.
|
||||||
|
let route = s.route_client_command(
|
||||||
|
1,
|
||||||
|
&json!({ "id": 2, "method": "Target.attachToTarget", "params": { "targetId": "T_new" } }),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
route,
|
||||||
|
ClientRoute::Local(json!({ "id": 2, "result": { "sessionId": "cb-tab-42" } }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn browser_get_version_is_answered_locally() {
|
fn browser_get_version_is_answered_locally() {
|
||||||
// Liveness probe must NOT be forwarded (the extension can't do
|
// Liveness probe must NOT be forwarded (the extension can't do
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ let port = null
|
|||||||
/** Whether the native-messaging host (the local chrome-use CLI) is linked.
|
/** Whether the native-messaging host (the local chrome-use CLI) is linked.
|
||||||
* Read by the popup status page. */
|
* Read by the popup status page. */
|
||||||
let hostConnected = false
|
let hostConnected = false
|
||||||
let nextSession = 1
|
|
||||||
/** tabId -> { sessionId, targetId } */
|
/** tabId -> { sessionId, targetId } */
|
||||||
const tabs = new Map()
|
const tabs = new Map()
|
||||||
/** sessionId -> tabId (main session per tab) */
|
/** sessionId -> tabId (main session per tab) */
|
||||||
@@ -261,7 +260,16 @@ async function attachTab(tabId) {
|
|||||||
const targetInfo = info?.targetInfo
|
const targetInfo = info?.targetInfo
|
||||||
const targetId = String(targetInfo?.targetId || '')
|
const targetId = String(targetInfo?.targetId || '')
|
||||||
if (!targetId) throw new Error('attachTab: no targetId')
|
if (!targetId) throw new Error('attachTab: no targetId')
|
||||||
const sessionId = `cb-tab-${nextSession++}`
|
// Derive the session id from the STABLE Chrome tabId, not a monotonic counter
|
||||||
|
// (issue #17). A tab's chrome.debugger session can be torn down and
|
||||||
|
// re-established — cross-process navigation, a service-worker restart wiping
|
||||||
|
// these in-memory maps, DevTools stealing the debugger — and each time the tab
|
||||||
|
// re-attaches. With a counter, re-attach minted a BRAND-NEW `cb-tab-N`, which
|
||||||
|
// orphaned the daemon's binding (it's still pinned to the old id and the relay
|
||||||
|
// never tells it to rebind) → permanent "stale sessionId / tab is gone". The
|
||||||
|
// tabId is stable across all of that, so `cb-tab-<tabId>` restores the SAME
|
||||||
|
// session the daemon already holds → eval/snapshot auto-follow the new page.
|
||||||
|
const sessionId = `cb-tab-${tabId}`
|
||||||
const entry = { sessionId, targetId }
|
const entry = { sessionId, targetId }
|
||||||
tabs.set(tabId, entry)
|
tabs.set(tabId, entry)
|
||||||
sessionToTab.set(sessionId, tabId)
|
sessionToTab.set(sessionId, tabId)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "chrome-use",
|
"name": "chrome-use",
|
||||||
"version": "0.4.3",
|
"version": "0.4.4",
|
||||||
"description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.",
|
"description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.",
|
||||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
|||||||
Reference in New Issue
Block a user