fix(cdp): preserve session routing for child default bindings

This commit is contained in:
ldm0
2026-09-27 06:43:23 +08:00
parent 2bd780fffa
commit 4de034ec2e
6 changed files with 138 additions and 49 deletions
@@ -1040,6 +1040,15 @@ async fn runtime_add_binding_default_world_replays_into_runtime_materialized_chi
)
.await;
// V8 replays bindings into future contexts while Runtime is enabled.
ctx.process_async(json!({
"id": 420,
"method": "Runtime.enable",
"sessionId": "SID-1",
}))
.await;
ctx.expect_result(420, json!({}), Some("SID-1"));
ctx.process_async(json!({
"id": 421,
"method": "Runtime.addBinding",
@@ -1384,3 +1393,93 @@ async fn runtime_evaluate_child_history_back_emits_child_frame_navigation_and_li
)
.await;
}
#[tokio::test(flavor = "multi_thread")]
async fn attached_runtime_binding_reports_child_calls_without_primary_registration() {
let mut ctx = TestContext::new();
load_bc_with_session(&mut ctx, "BID-1", "TID-1", "SID-1", "about:blank");
ctx.enable_page_events_for_test(Some("SID-1"));
ctx.process_async(json!({
"id": 48_000,
"method": "Target.attachToTarget",
"params": {"targetId": "TID-1", "flatten": true}
}))
.await;
let attached_session_id = take_response_by_id(&mut ctx, 48_000)["result"]["sessionId"]
.as_str()
.expect("second target session")
.to_owned();
assert_ne!(attached_session_id, "SID-1");
ctx.process_async(json!({
"id": 48_001,
"method": "Runtime.enable",
"sessionId": attached_session_id,
}))
.await;
ctx.expect_result(48_001, json!({}), Some(&attached_session_id));
ctx.process_async(json!({
"id": 48_002,
"method": "Runtime.addBinding",
"sessionId": attached_session_id,
"params": {"name": "attachedChildBinding"}
}))
.await;
ctx.expect_result(48_002, json!({}), Some(&attached_session_id));
ctx.process_async(json!({
"id": 48_003,
"method": "Page.navigate",
"sessionId": "SID-1",
"params": {"url": "data:text/html,<iframe srcdoc=\"<body>child</body>\"></iframe>"}
}))
.await;
let _ = take_response_by_id(&mut ctx, 48_003);
let frame_id = child_frame_id_for_single_iframe(&mut ctx, 48_004).await;
wait_until_frame_stopped_loading(&mut ctx, &frame_id).await;
let child_context_id = wait_for_child_default_execution_context_id(
&mut ctx,
&frame_id,
"attached child binding context",
)
.await;
for (offset, scoped) in [(0_u64, false), (3, true)] {
if scoped {
ctx.process_async(json!({
"id": 48_005 + offset,
"method": "Runtime.addBinding",
"sessionId": attached_session_id,
"params": {"name": "attachedChildBinding", "executionContextId": child_context_id}
}))
.await;
ctx.expect_result(48_005 + offset, json!({}), Some(&attached_session_id));
}
ctx.sent.clear();
ctx.process_async(json!({
"id": 48_006 + offset,
"method": "Runtime.evaluate",
"sessionId": "SID-1",
"params": {"expression": "frames[0].attachedChildBinding('child-call'); 'called'"}
}))
.await;
assert_eq!(
take_response_by_id(&mut ctx, 48_006 + offset)["result"]["result"]["value"],
json!("called")
);
let events = ctx
.sent
.iter()
.filter(|message| message["method"] == "Runtime.bindingCalled")
.collect::<Vec<_>>();
assert_eq!(
events.len(),
1,
"one notification for the registering session (scoped={scoped}): {events:?}"
);
assert_eq!(events[0]["sessionId"], json!(attached_session_id));
assert_eq!(events[0]["params"]["name"], "attachedChildBinding");
assert_eq!(events[0]["params"]["payload"], "child-call");
assert_eq!(
events[0]["params"]["executionContextId"],
json!(child_context_id)
);
}
}
+15 -37
View File
@@ -8654,46 +8654,24 @@ fn complete_pending_runtime_binding_context_lookup_command(
mut task: RuntimeBindingCommandTask,
completed_lookup: Result<CompletedRuntimeChildDefaultContextLookupDispatch, String>,
) -> RuntimeCommandTaskStep {
let is_child_default_context = match completed_lookup {
Ok(completed_lookup) => {
match conn.complete_child_default_execution_context_lookup(completed_lookup) {
Ok(is_child_default_context) => is_child_default_context,
Err(message) => {
return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan(
completed.command_id,
message,
));
}
}
}
Err(message) => {
return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan(
completed.command_id,
message,
));
}
};
if !is_child_default_context {
if matches!(task.action, RuntimeBindingCommand::Add)
|| matches!(task.action, RuntimeBindingCommand::Remove)
&& runtime_remove_binding_should_skip_live_page_update(conn, &completed.owner_scope)
{
task.skip_live_page_update_after_inspector_success = true;
}
return start_pending_runtime_binding_inspector_phase(conn, &completed, task);
if let Err(message) = completed_lookup
.and_then(|lookup| conn.complete_child_default_execution_context_lookup(lookup))
{
return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan(
completed.command_id,
message,
));
}
task.command_response = Some(RuntimeBindingCommandResponse::empty_success());
match start_pending_runtime_binding_page_phase(
conn,
completed.command_id,
task.clone(),
completed.owner_scope.clone(),
) {
Some(pending) => RuntimeCommandTaskStep::Pending(Box::new(pending)),
None => complete_runtime_binding_after_live_update(conn, completed, task),
// Child default realms are registered V8 Inspector contexts too. Installing
// a native callback here would bypass the registering Inspector session.
if matches!(task.action, RuntimeBindingCommand::Add)
|| matches!(task.action, RuntimeBindingCommand::Remove)
&& runtime_remove_binding_should_skip_live_page_update(conn, &completed.owner_scope)
{
task.skip_live_page_update_after_inspector_success = true;
}
start_pending_runtime_binding_inspector_phase(conn, &completed, task)
}
async fn complete_pending_runtime_binding_inspector_command(
@@ -461,10 +461,15 @@ impl JsContextHost {
self.stored_document_start_scripts.clone()
}
pub(crate) fn stored_default_runtime_binding_names(&self) -> Vec<String> {
pub(crate) fn stored_default_native_runtime_binding_names(&self) -> Vec<String> {
self.stored_runtime_bindings
.iter()
.filter(|binding| binding.execution_context_name.is_none())
// Inspector installs session-owned bindings when each V8 context
// is reported. Replacing them with a native callback would lose
// the registering sessions and their context filters.
.filter(|binding| {
binding.devtools_session.is_none() && binding.execution_context_name.is_none()
})
.map(|binding| binding.name.clone())
.collect()
}
@@ -42,6 +42,10 @@ impl JsContextHost {
handle: DomHandle,
window: v8::Local<'_, v8::Object>,
) {
let binding_names = self.stored_default_native_runtime_binding_names();
if binding_names.is_empty() {
return;
}
let execution_context_id = self.child_default_execution_context_id(handle).unwrap_or(0);
let Some(document_owner) = self.current_child_document_task_owner(handle) else {
return;
@@ -54,12 +58,6 @@ impl JsContextHost {
if !self.register_runtime_binding_execution_context(execution_context, document_owner) {
return;
}
let binding_names = self
.stored_runtime_bindings
.iter()
.filter(|binding| binding.execution_context_name.is_none())
.map(|binding| binding.name.clone())
.collect::<Vec<_>>();
for binding_name in binding_names {
let Some(key) = v8_string(scope, &binding_name) else {
continue;
@@ -213,9 +213,18 @@ async fn detached_session_binding_is_not_installed_in_a_later_child_realm() {
binding(session_b, "liveChildBinding"),
];
page_vm.set_stored_runtime_bindings(&bindings);
page_vm
.vm_mut()
.ensure_runtime_inspector_session(Some("session-a"));
for (session_id, name) in [
("session-a", "detachedChildBinding"),
("session-b", "liveChildBinding"),
] {
page_vm
.vm_mut()
.dispatch_inspector_protocol_message_for_session(
Some(session_id),
r#"{"id":1,"method":"Runtime.enable"}"#,
)?;
page_vm.add_runtime_binding(Some(session_id), name, None, None)?;
}
assert!(page_vm.detach_runtime_inspector_session(Some("session-a")));
queue_child_realm_materialization(&mut page_vm, "post-detach-binding-child")?;
@@ -327,7 +327,7 @@ impl ScriptVm {
let binding_names = self
._context_host
.borrow()
.stored_default_runtime_binding_names();
.stored_default_native_runtime_binding_names();
for name in binding_names {
if let Err(error) =
self.install_runtime_binding_in_child_default_context(execution_context_id, &name)