From 804bf1e3577e6677d792f98d9f00a4e9cb052981 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 29 Oct 2026 13:54:05 +0800 Subject: [PATCH] fix(protocol): preserve native Document work before Page adoption Capture weak command capabilities for the exact native Document behind early Fetch pauses. Use them for request, response, authentication and cleanup work, and restore pause records when dispatch is rejected. Carry publication Page sources through owner-action projection. Register and attach workers during handoff, revoke abandoned Page workers, and preserve source identity for dialogs, choosers, popups and script navigation. Record native navigation history once, retain observed URLs and titles at adoption, and publish error-Document failure before its frozen frame commit. Add controlled WebSocket regressions for handoff, cancellation and recovery. --- Cargo.lock | 1 + moli-core/Cargo.toml | 1 + moli-core/src/lib.rs | 2 +- moli-core/src/page/command_dispatch.rs | 72 +- moli-core/src/page/mod.rs | 4 +- moli-core/src/runtime/navigation_engine.rs | 13 + .../src/protocol_server/tests/websocket.rs | 1 + .../websocket/prepared_document_actions.rs | 1186 +++++++++++++++++ moli-protocol/src/conn.rs | 26 +- .../src/conn/browser_context/fetch_owner.rs | 152 ++- .../browser_context/target_session_owner.rs | 206 ++- moli-protocol/src/conn/fetch_support.rs | 42 +- moli-protocol/src/conn/page_state/loaded.rs | 39 +- .../src/conn/page_state/page_targets.rs | 53 +- .../src/conn/state/browser_context.rs | 23 + moli-protocol/src/conn/state/fetch.rs | 2 + moli-protocol/src/conn/state/runtime_slot.rs | 9 + moli-protocol/src/conn/state/target_state.rs | 4 + .../src/domains/activity/main_document.rs | 22 +- .../output_ingress/prepared_outputs.rs | 36 +- .../activity/output_ingress/projection.rs | 34 +- .../activity/output_ingress/renderer.rs | 5 +- .../src/domains/activity/publication_route.rs | 5 + .../activity/runtime_command_barrier.rs | 7 + .../src/domains/activity/subresource.rs | 95 +- moli-protocol/src/domains/fetch.rs | 1 - moli-protocol/src/domains/fetch/auth.rs | 98 +- moli-protocol/src/domains/fetch/cleanup.rs | 88 +- moli-protocol/src/domains/fetch/commands.rs | 329 ++--- .../src/domains/fetch/subresource.rs | 38 +- .../domains/fetch/subresource_continuation.rs | 51 +- .../src/domains/fetch/tests/basics.rs | 1 + .../fetch/tests/command_correlation.rs | 47 +- moli-protocol/src/domains/input.rs | 24 +- .../src/domains/input/file_chooser.rs | 134 +- moli-protocol/src/domains/page.rs | 2 +- .../src/domains/page/javascript_dialog.rs | 16 +- .../src/domains/page/main_document_commit.rs | 153 ++- moli-protocol/src/domains/page/navigation.rs | 7 +- .../src/domains/page/navigation_commit.rs | 9 +- moli-protocol/src/domains/page/output.rs | 45 +- .../src/domains/page/renderer_navigation.rs | 2 +- .../target/worker_target/dedicated_worker.rs | 11 +- .../worker_target/lifecycle_projection.rs | 27 +- .../src/devtools/command/nested.rs | 1 + moli-renderer-v8/src/lib.rs | 54 +- moli-renderer-v8/src/runtime/mod.rs | 3 +- .../src/runtime/owner/owner_commands.rs | 25 + moli-renderer-v8/src/runtime/owner_local.rs | 71 + .../src/runtime/owner_local_store/mod.rs | 28 +- moli-renderer-v8/src/runtime/page_commands.rs | 27 + moli-renderer-v8/src/runtime/page_surface.rs | 6 + 52 files changed, 2632 insertions(+), 706 deletions(-) create mode 100644 moli-protocol-server/src/protocol_server/tests/websocket/prepared_document_actions.rs diff --git a/Cargo.lock b/Cargo.lock index 11acf193db..00f4483da4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2780,6 +2780,7 @@ dependencies = [ "serde_json", "static_assertions", "strip-ansi-escapes", + "thiserror", "tokio", "tracing", "url", diff --git a/moli-core/Cargo.toml b/moli-core/Cargo.toml index 0cfeaca1f0..040db2c562 100644 --- a/moli-core/Cargo.toml +++ b/moli-core/Cargo.toml @@ -32,6 +32,7 @@ parking_lot = "0.12" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" tokio = { version = "1.51.0", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] } +thiserror = "2.0.17" tracing = "0.1.41" url = "2.5.7" diff --git a/moli-core/src/lib.rs b/moli-core/src/lib.rs index bcda97685e..e36eacd4fc 100644 --- a/moli-core/src/lib.rs +++ b/moli-core/src/lib.rs @@ -38,4 +38,4 @@ pub use moli_renderer_v8::{ RendererRuntimeInspectorResponseChannel, RendererRuntimeInspectorResponseSender, RendererRuntimeRemoteObjectResolution, }; -pub use moli_renderer_v8::{RendererPageCommand, RendererPageReply}; +pub use moli_renderer_v8::{RendererPageAccessError, RendererPageCommand, RendererPageReply}; diff --git a/moli-core/src/page/command_dispatch.rs b/moli-core/src/page/command_dispatch.rs index f2a6357ad6..80f9648df4 100644 --- a/moli-core/src/page/command_dispatch.rs +++ b/moli-core/src/page/command_dispatch.rs @@ -7,12 +7,36 @@ use super::{ use crate::RendererOutputFence; use anyhow::{Result, bail}; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PageDocumentCommandTarget(moli_renderer_v8::RendererDocumentCommandTarget); + +impl PageDocumentCommandTarget { + pub(crate) fn new(target: moli_renderer_v8::RendererDocumentCommandTarget) -> Self { + Self(target) + } + pub fn document(&self) -> crate::RendererDocumentLifecycleIdentity { + self.0.document() + } + pub fn start(&self, command: RendererPageCommand) -> Result { + self.0 + .enqueue(command) + .map(PendingPageCommand::from_renderer) + } +} + pub struct PendingPageCommand { pending: RendererPageCommandPending, renderer_agent_attachment_id: Option, } impl PendingPageCommand { + pub(crate) fn from_renderer(pending: RendererPageCommandPending) -> Self { + Self { + pending, + renderer_agent_attachment_id: None, + } + } + pub fn renderer_agent_attachment_id(&self) -> Option { self.renderer_agent_attachment_id } @@ -79,7 +103,42 @@ pub struct CompletedPageCommand { renderer_agent_attachment_id: Option, } +#[derive(Debug, thiserror::Error)] +#[error("Document command returned an unexpected renderer reply")] +struct UnexpectedDocumentCommandReply; + impl CompletedPageCommand { + /// Fetch settlement owns its exact renderer output; Page adoption is not + /// required to acknowledge the already-executed native operation. + pub fn into_unit_reply(self) -> Result<()> { + self.into_unit_page_command_turn().map(drop) + } + + pub fn into_subresource_continue_outcome( + self, + ) -> Result { + match self.output.into_reply_and_state().0 { + RendererPageReply::PendingSubresourceContinueOutcome(outcome) => Ok(outcome), + _ => Err(UnexpectedDocumentCommandReply.into()), + } + } + + pub fn into_document_bidi_node_shared_id( + self, + ) -> Result { + match self.output.into_reply_and_state().0 { + RendererPageReply::DocumentBidiNodeSharedId(result) => Ok(result), + _ => Err(UnexpectedDocumentCommandReply.into()), + } + } + + pub fn into_document_bidi_node_binding(self) -> Result<()> { + match self.output.into_reply_and_state().0 { + RendererPageReply::DocumentBidiNodeBindingRegistered => Ok(()), + _ => Err(UnexpectedDocumentCommandReply.into()), + } + } + /// Returns the exact renderer output position produced by this command. /// /// Protocol dispatch must capture this before a command-specific decoder @@ -136,15 +195,22 @@ impl CompletedPageCommand { /// replayed while that replacement Page is installed. pub fn into_unit_page_command_turn(self) -> Result { if !matches!(self.output.completion().reply(), RendererPageReply::Unit) { - return Err(anyhow::anyhow!( - "unit page command returned an unexpected renderer reply" - )); + return Err(UnexpectedDocumentCommandReply.into()); } Ok(self.output) } } impl Page { + pub fn document_command_target( + &self, + document: crate::RendererDocumentLifecycleIdentity, + ) -> Result { + self.handle + .document_command_target(document) + .map(PageDocumentCommandTarget::new) + } + pub(crate) fn start_page_command( &self, mut command: RendererPageCommand, diff --git a/moli-core/src/page/mod.rs b/moli-core/src/page/mod.rs index de51f8f49d..204b6ed94c 100644 --- a/moli-core/src/page/mod.rs +++ b/moli-core/src/page/mod.rs @@ -40,8 +40,8 @@ use crate::renderer::{ use anyhow::Result; pub use command_dispatch::{ CompletedDevToolsIoCommandDispatch, CompletedPageCommand, - CompletedRuntimeInspectorCommandDispatch, PendingDevToolsIoCommandDispatch, PendingPageCommand, - PendingRuntimeInspectorCommandDispatch, + CompletedRuntimeInspectorCommandDispatch, PageDocumentCommandTarget, + PendingDevToolsIoCommandDispatch, PendingPageCommand, PendingRuntimeInspectorCommandDispatch, }; #[cfg(all(test, debug_assertions))] diff --git a/moli-core/src/runtime/navigation_engine.rs b/moli-core/src/runtime/navigation_engine.rs index 572d4bd75a..7f51c837c8 100644 --- a/moli-core/src/runtime/navigation_engine.rs +++ b/moli-core/src/runtime/navigation_engine.rs @@ -595,6 +595,19 @@ impl Default for NavigationEngine { } impl NavigationEngine { + /// Captures a weak command target for one native Document, including the + /// interval before the navigation result installs its Page handle. + pub fn document_command_target( + &self, + renderer_owner: moli_renderer_v8::RendererOwnerLocalHostId, + document: moli_renderer_v8::RendererDocumentLifecycleIdentity, + ) -> Result { + self.js_runtime + .renderer_owner_handle() + .document_command_target(renderer_owner, document) + .map(crate::page::PageDocumentCommandTarget::new) + } + /// Reserves a renderer Page identity before the corresponding creation /// command is enqueued. /// diff --git a/moli-protocol-server/src/protocol_server/tests/websocket.rs b/moli-protocol-server/src/protocol_server/tests/websocket.rs index 06b160b230..24fadd58aa 100644 --- a/moli-protocol-server/src/protocol_server/tests/websocket.rs +++ b/moli-protocol-server/src/protocol_server/tests/websocket.rs @@ -5,6 +5,7 @@ mod browser_close; mod captured_document_navigation; mod child_navigation_wait; mod navigation_debugger; +mod prepared_document_actions; mod target_message; fn assert_cdp_event_precedes_response( diff --git a/moli-protocol-server/src/protocol_server/tests/websocket/prepared_document_actions.rs b/moli-protocol-server/src/protocol_server/tests/websocket/prepared_document_actions.rs new file mode 100644 index 0000000000..d37b089d91 --- /dev/null +++ b/moli-protocol-server/src/protocol_server/tests/websocket/prepared_document_actions.rs @@ -0,0 +1,1186 @@ +use super::*; +use serde_json::Value; + +type TestCdpSocket = + tokio_tungstenite::WebSocketStream>; + +#[derive(Clone, Copy, Debug)] +enum Action { + Continue, + Fulfill, + Fail, +} + +#[tokio::test] +async fn websocket_cdp_prepared_document_request_actions_settle_before_adoption() { + for crashed in [false, true] { + for action in [Action::Continue, Action::Fulfill, Action::Fail] { + verify_fetch(crashed, false, action).await; + } + } +} + +#[tokio::test] +async fn websocket_cdp_prepared_document_response_actions_settle_before_adoption() { + for crashed in [false, true] { + for action in [Action::Continue, Action::Fulfill, Action::Fail] { + verify_fetch(crashed, true, action).await; + } + } +} + +#[tokio::test] +async fn websocket_cdp_prepared_document_error_commit_follows_network_failure() { + for crashed in [false, true] { + // Close every connection without HTTP headers, including network + // retries, so the navigation creates a browser-owned error Document. + let reset = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let failed_url = format!("http://{}/reset", reset.local_addr().unwrap()); + let reset_server = tokio::spawn(async move { + while let Ok((stream, _)) = reset.accept().await { + drop(stream); + } + }); + let (addr, server) = spawn_test_protocol_server().await; + let (mut socket, _) = + connect_async(format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}")) + .await + .unwrap(); + let context = cdp_create_browser_context(&mut socket, 1).await; + let session = cdp_create_attached_target(&mut socket, 2, &context).await; + configure(&mut socket, &session.session_id, crashed, false).await; + send_cdp_command_without_wait( + &mut socket, + 20, + "Page.navigate", + Some(&session.session_id), + json!({"url": failed_url}), + ) + .await; + let mut observed = recv_until_match(&mut socket, |message| { + message["method"] == "Page.loadEventFired" && message["sessionId"] == session.session_id + }) + .await; + if !observed.iter().any(|message| message["id"] == 20) { + observed.extend(recv_until_match(&mut socket, |message| message["id"] == 20).await); + } + let methods = [ + "Network.requestWillBeSent", + "Network.loadingFailed", + "Page.frameNavigated", + "Network.loadingFinished", + "Page.domContentEventFired", + "Page.loadEventFired", + ]; + let positions = methods.map(|method| { + let matching = observed + .iter() + .enumerate() + .filter(|(_, message)| { + message["method"] == method && message["sessionId"] == session.session_id + }) + .map(|(index, _)| index) + .collect::>(); + assert_eq!(matching.len(), 1, "{crashed}/{method}: {observed:?}"); + matching[0] + }); + assert!( + positions.windows(2).all(|pair| pair[0] < pair[1]), + "{crashed}: {observed:?}" + ); + let request = &observed[positions[0]]["params"]["requestId"]; + assert_eq!(&observed[positions[1]]["params"]["requestId"], request); + assert_eq!(&observed[positions[3]]["params"]["requestId"], request); + let navigation = &observed.iter().find(|message| message["id"] == 20).unwrap()["result"]; + assert_eq!( + navigation["errorText"], + observed[positions[1]]["params"]["errorText"] + ); + assert_eq!( + observed[positions[2]]["params"]["frame"]["unreachableUrl"], + failed_url + ); + assert_eq!( + observed[positions[2]]["params"]["frame"]["loaderId"], + navigation["loaderId"] + ); + let _ = socket.close(None).await; + abort_test_cdp_server(server).await; + reset_server.abort(); + let _ = reset_server.await; + } +} + +struct Fixture { + url: String, + requested: Arc, + release: Arc, + replacement_requested: Arc, + _server: DedicatedFixtureServer, +} + +impl Fixture { + fn new() -> Self { + let requested = Arc::new(tokio::sync::Notify::new()); + let release = Arc::new(tokio::sync::Notify::new()); + let request = Arc::clone(&requested); + let gate = Arc::clone(&release); + let replacement_requested = Arc::new(tokio::sync::Notify::new()); + let replacement = Arc::clone(&replacement_requested); + let router = Router::new() + .route( + "/page", + get(|| async { "document is fulfilled by the client" }), + ) + .route("/api", get(|| async { "network-api" })) + .route( + "/replacement", + get(move || { + let replacement = Arc::clone(&replacement); + async move { + replacement.notify_one(); + "replacement" + } + }), + ) + .route("/popup", get(|| async { "popup" })) + .route( + "/slow.js", + get(move || { + let request = Arc::clone(&request); + let gate = Arc::clone(&gate); + async move { + request.notify_one(); + gate.notified().await; + ( + [("Content-Type", "text/javascript")], + "globalThis.slowDone = true;", + ) + } + }), + ); + let (addr, server) = spawn_dedicated_fixture_server(router, "prepared-document-actions"); + Self { + url: format!("http://{addr}/page"), + requested, + release, + replacement_requested, + _server: server, + } + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + self.release.notify_one(); + } +} + +async fn start_document( + socket: &mut TestCdpSocket, + session_id: &str, + fixture: &Fixture, + inline: &str, +) -> Vec { + let observed = begin_document(socket, session_id, fixture, inline).await; + tokio::time::timeout(Duration::from_secs(10), fixture.requested.notified()) + .await + .unwrap(); + assert!( + !observed + .iter() + .any(|message| message["method"] == "Page.domContentEventFired") + ); + observed +} + +async fn begin_document( + socket: &mut TestCdpSocket, + session_id: &str, + fixture: &Fixture, + inline: &str, +) -> Vec { + send_cdp_command_without_wait( + socket, + 20, + "Page.navigate", + Some(session_id), + json!({"url": fixture.url}), + ) + .await; + let observed = recv_until_match(socket, |message| { + message["method"] == "Fetch.requestPaused" + && message["params"]["resourceType"] == "Document" + }) + .await; + let request = observed.last().unwrap()["params"]["requestId"].clone(); + use base64::Engine as _; + let html = format!(""); + send_cdp_command_without_wait( + socket, + 21, + "Fetch.fulfillRequest", + Some(session_id), + json!({ + "requestId": request, "responseCode": 200, + "responseHeaders": [{"name": "Content-Type", "value": "text/html"}], + "body": base64::engine::general_purpose::STANDARD.encode(html), + }), + ) + .await; + observed +} + +async fn configure(socket: &mut TestCdpSocket, session_id: &str, crashed: bool, response: bool) { + for (id, method) in [ + (5, "Runtime.enable"), + (6, "Page.enable"), + (7, "Network.enable"), + ] { + send_cdp_command(socket, id, method, Some(session_id), json!({})).await; + } + if crashed { + let result = send_cdp_command(socket, 8, "Page.crash", Some(session_id), json!({})).await; + assert!( + result + .iter() + .any(|message| message["id"] == 8 && message.get("result").is_some()), + "{result:?}" + ); + } + send_cdp_command(socket, 9, "Fetch.enable", Some(session_id), json!({"patterns": [ + {"urlPattern": "*page", "resourceType": "Document", "requestStage": "Request"}, + {"urlPattern": "*api", "resourceType": "Fetch", "requestStage": if response { "Response" } else { "Request" }}, + ]})).await; +} + +async fn verify_fetch(crashed: bool, response: bool, action: Action) { + eprintln!("prepared Fetch {crashed}/{response}/{action:?}"); + let fixture = Fixture::new(); + let (addr, server) = spawn_test_protocol_server().await; + let (mut socket, _) = + connect_async(format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}")) + .await + .unwrap(); + let context = cdp_create_browser_context(&mut socket, 1).await; + let session = cdp_create_attached_target(&mut socket, 2, &context).await; + configure(&mut socket, &session.session_id, crashed, response).await; + let mut observed = start_document(&mut socket, &session.session_id, &fixture, + "fetch('/api').then(r=>r.text()).then(v=>console.log('api-settled:'+v),()=>console.log('api-settled:failed'));").await; + observed.extend( + recv_until_match(&mut socket, |message| { + message["method"] == "Fetch.requestPaused" + && message["params"]["request"]["url"] + .as_str() + .is_some_and(|url| url.ends_with("/api")) + }) + .await, + ); + let pause = observed.last().unwrap(); + assert_eq!(pause["params"]["resourceType"], "XHR"); + assert_eq!( + pause["params"].get("responseStatusCode").is_some(), + response + ); + let request = pause["params"]["requestId"].clone(); + let method = match (action, response) { + (Action::Continue, false) => "Fetch.continueRequest", + (Action::Continue, true) => "Fetch.continueResponse", + (Action::Fulfill, _) => "Fetch.fulfillRequest", + (Action::Fail, _) => "Fetch.failRequest", + }; + let mut params = json!({"requestId": request}); + match action { + Action::Fulfill => { + params["responseCode"] = json!(200); + params["body"] = json!("ZnVsZmlsbGVkLWFwaQ=="); + } + Action::Fail => params["errorReason"] = json!("Failed"), + Action::Continue => {} + } + let reply = send_cdp_command(&mut socket, 22, method, Some(&session.session_id), params).await; + assert!( + reply + .iter() + .any(|message| message["id"] == 22 && message.get("result").is_some()), + "{crashed}/{response}/{action:?}: {reply:?}" + ); + observed.extend(reply); + let expected = match action { + Action::Continue => "api-settled:network-api", + Action::Fulfill => "api-settled:fulfilled-api", + Action::Fail => "api-settled:failed", + }; + let settled = |message: &Value| { + message["method"] == "Runtime.consoleAPICalled" + && message["params"]["args"][0]["value"] == expected + }; + if !observed.iter().any(settled) { + observed.extend(recv_until_match(&mut socket, settled).await); + } + assert!( + !observed + .iter() + .any(|message| message["method"] == "Page.domContentEventFired"), + "{observed:?}" + ); + fixture.release.notify_one(); + if !observed + .iter() + .any(|message| message["method"] == "Page.loadEventFired") + { + observed.extend( + recv_until_match(&mut socket, |message| { + message["method"] == "Page.loadEventFired" + }) + .await, + ); + } + let duplicate = send_cdp_command( + &mut socket, + 23, + method, + Some(&session.session_id), + json!({"requestId": request, "responseCode": 200, "errorReason": "Failed"}), + ) + .await; + assert!( + duplicate + .iter() + .any(|message| message["id"] == 23 && message["error"]["message"] == "RequestNotFound"), + "{duplicate:?}" + ); + let _ = socket.close(None).await; + abort_test_cdp_server(server).await; +} + +#[tokio::test] +async fn websocket_cdp_prepared_document_worker_is_discovered_attached_and_retired() { + verify_worker(false).await; +} + +#[tokio::test] +async fn websocket_cdp_prepared_document_worker_is_retired_when_navigation_is_replaced() { + verify_worker(true).await; +} + +async fn verify_worker(replace_before_adoption: bool) { + let fixture = Fixture::new(); + let (addr, server) = spawn_test_protocol_server().await; + let (mut socket, _) = + connect_async(format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}")) + .await + .unwrap(); + let context = cdp_create_browser_context(&mut socket, 1).await; + let session = cdp_create_attached_target(&mut socket, 2, &context).await; + configure(&mut socket, &session.session_id, false, false).await; + send_cdp_command( + &mut socket, + 10, + "Target.setDiscoverTargets", + None, + json!({"discover": true}), + ) + .await; + let policy = send_cdp_command(&mut socket, 11, "Target.setAutoAttach", Some(&session.session_id), json!({ + "autoAttach": true, "waitForDebuggerOnStart": false, "flatten": true, "filter": [{"type": "worker"}], + })).await; + assert!( + policy + .iter() + .any(|message| message["id"] == 11 && message.get("result").is_some()), + "{policy:?}" + ); + let crashed = send_cdp_command( + &mut socket, + 12, + "Page.crash", + Some(&session.session_id), + json!({}), + ) + .await; + assert!( + crashed + .iter() + .any(|message| message["id"] == 12 && message.get("result").is_some()), + "{crashed:?}" + ); + let mut observed = start_document( + &mut socket, + &session.session_id, + &fixture, + "window.w=new Worker(URL.createObjectURL(new Blob(['setInterval(()=>{},1000)'])));", + ) + .await; + let attached = |message: &Value| { + message["method"] == "Target.attachedToTarget" + && message["params"]["targetInfo"]["type"] == "worker" + }; + if !observed.iter().any(attached) { + observed.extend(recv_until_match(&mut socket, attached).await); + } + let worker = &observed.iter().find(|message| attached(message)).unwrap()["params"]; + let worker_id = worker["targetInfo"]["targetId"].clone(); + let worker_session = worker["sessionId"].as_str().unwrap().to_owned(); + assert!( + observed + .iter() + .any(|message| message["method"] == "Target.targetCreated" + && message["params"]["targetInfo"]["targetId"] == worker_id), + "{observed:?}" + ); + assert!( + !observed + .iter() + .any(|message| message["method"] == "Page.domContentEventFired") + ); + let targets = send_cdp_command(&mut socket, 22, "Target.getTargets", None, json!({})).await; + assert!( + targets.last().unwrap()["result"]["targetInfos"] + .as_array() + .unwrap() + .iter() + .any(|target| target["targetId"] == worker_id), + "{targets:?}" + ); + let enabled = send_cdp_command( + &mut socket, + 23, + "Runtime.enable", + Some(&worker_session), + json!({}), + ) + .await; + assert!( + enabled + .iter() + .any(|message| message["id"] == 23 && message.get("result").is_some()), + "{enabled:?}" + ); + if !replace_before_adoption { + fixture.release.notify_one(); + recv_until_match(&mut socket, |message| { + message["method"] == "Page.loadEventFired" + }) + .await; + } + let replacement = fixture.url.replace("/page", "/replacement"); + let mut navigation = send_cdp_command( + &mut socket, + 24, + "Page.navigate", + Some(&session.session_id), + json!({"url": replacement}), + ) + .await; + let result = &navigation.last().unwrap()["result"]; + assert!( + result.get("frameId").is_some() && result.get("errorText").is_none(), + "{navigation:?}" + ); + let destroyed = |message: &Value| { + message["method"] == "Target.targetDestroyed" && message["params"]["targetId"] == worker_id + }; + let loaded = |message: &Value| { + message["method"] == "Page.loadEventFired" && message["sessionId"] == session.session_id + }; + // The held request's native timeout is thirty seconds. Retirement must + // finish independently of that timeout and before the fixture is released. + tokio::time::timeout(Duration::from_secs(10), async { + while !navigation.iter().any(destroyed) || !navigation.iter().any(loaded) { + navigation.extend( + recv_until_match(&mut socket, |message| destroyed(message) || loaded(message)) + .await, + ); + } + }) + .await + .expect("replacement must load and retire the original Worker while its parser is held"); + let targets = send_cdp_command(&mut socket, 25, "Target.getTargets", None, json!({})).await; + assert!( + !targets.last().unwrap()["result"]["targetInfos"] + .as_array() + .unwrap() + .iter() + .any(|target| target["targetId"] == worker_id) + ); + let native = send_cdp_command( + &mut socket, + 26, + "Runtime.evaluate", + Some(&session.session_id), + json!({"expression": "location.href", "returnByValue": true}), + ) + .await; + assert_eq!( + native.last().unwrap()["result"]["result"]["value"], + replacement + ); + // Retirement was observed while the old parser was held; release its + // network fixture before waiting for whole-server teardown. + fixture.release.notify_one(); + let _ = socket.close(None).await; + abort_test_cdp_server(server).await; +} + +#[tokio::test] +async fn websocket_cdp_prepared_document_dialog_can_be_answered_before_adoption() { + let fixture = Fixture::new(); + let (addr, server) = spawn_test_protocol_server().await; + let (mut socket, _) = + connect_async(format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}")) + .await + .unwrap(); + let context = cdp_create_browser_context(&mut socket, 1).await; + let session = cdp_create_attached_target(&mut socket, 2, &context).await; + configure(&mut socket, &session.session_id, true, false).await; + send_cdp_command_without_wait( + &mut socket, + 20, + "Page.navigate", + Some(&session.session_id), + json!({"url": fixture.url}), + ) + .await; + let paused = recv_until_match(&mut socket, |message| { + message["method"] == "Fetch.requestPaused" + && message["params"]["resourceType"] == "Document" + }) + .await; + use base64::Engine as _; + send_cdp_command_without_wait(&mut socket, 21, "Fetch.fulfillRequest", Some(&session.session_id), json!({ + "requestId": paused.last().unwrap()["params"]["requestId"], "responseCode": 200, + "responseHeaders": [{"name": "Content-Type", "value": "text/html"}], + "body": base64::engine::general_purpose::STANDARD.encode(""), + })).await; + let opened = recv_until_match(&mut socket, |message| { + message["method"] == "Page.javascriptDialogOpening" + }) + .await; + assert_eq!( + opened.last().unwrap()["params"]["message"], + "prepared-dialog" + ); + let answered = send_cdp_command( + &mut socket, + 22, + "Page.handleJavaScriptDialog", + Some(&session.session_id), + json!({"accept": true}), + ) + .await; + assert!( + answered + .iter() + .any(|message| message["id"] == 22 && message.get("result").is_some()), + "{answered:?}" + ); + tokio::time::timeout(Duration::from_secs(10), fixture.requested.notified()) + .await + .unwrap(); + fixture.release.notify_one(); + recv_until_match(&mut socket, |message| { + message["method"] == "Page.loadEventFired" + }) + .await; + let _ = socket.close(None).await; + abort_test_cdp_server(server).await; +} + +#[tokio::test] +async fn websocket_cdp_prepared_document_popup_keeps_accepted_target_creation() { + let fixture = Fixture::new(); + let (addr, server) = spawn_test_protocol_server().await; + let (mut socket, _) = + connect_async(format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}")) + .await + .unwrap(); + let context = cdp_create_browser_context(&mut socket, 1).await; + let session = cdp_create_attached_target(&mut socket, 2, &context).await; + configure(&mut socket, &session.session_id, true, false).await; + send_cdp_command( + &mut socket, + 10, + "Target.setDiscoverTargets", + None, + json!({"discover": true}), + ) + .await; + let mut observed = start_document( + &mut socket, + &session.session_id, + &fixture, + "window.w=window.open('/popup');", + ) + .await; + let created = |message: &Value| { + message["method"] == "Target.targetCreated" + && message["params"]["targetInfo"]["openerId"] == session.target_id + }; + if !observed.iter().any(created) { + observed.extend(recv_until_match(&mut socket, created).await); + } + let popup_id = observed.iter().find(|message| created(message)).unwrap()["params"]["targetInfo"]["targetId"].clone(); + assert!( + !observed + .iter() + .any(|message| message["method"] == "Page.domContentEventFired") + ); + fixture.release.notify_one(); + recv_until_match(&mut socket, |message| { + message["method"] == "Page.loadEventFired" && message["sessionId"] == session.session_id + }) + .await; + let targets = send_cdp_command(&mut socket, 22, "Target.getTargets", None, json!({})).await; + assert!( + targets.last().unwrap()["result"]["targetInfos"] + .as_array() + .unwrap() + .iter() + .any(|target| target["targetId"] == popup_id), + "{targets:?}" + ); + let _ = socket.close(None).await; + abort_test_cdp_server(server).await; +} + +#[tokio::test] +async fn websocket_cdp_prepared_document_fetch_is_revoked_by_replacement_disable_and_detach() { + for crashed in [false, true] { + for cancellation in ["replace", "disable", "detach"] { + let fixture = Fixture::new(); + let (addr, server) = spawn_test_protocol_server().await; + let (mut socket, _) = + connect_async(format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}")) + .await + .unwrap(); + let context = cdp_create_browser_context(&mut socket, 1).await; + let session = cdp_create_attached_target(&mut socket, 2, &context).await; + configure(&mut socket, &session.session_id, crashed, false).await; + let mut observed = start_document( + &mut socket, + &session.session_id, + &fixture, + "fetch('/api').then(r=>r.text()).catch(()=>{});", + ) + .await; + observed.extend( + recv_until_match(&mut socket, |message| { + message["method"] == "Fetch.requestPaused" + && message["params"]["request"]["url"] + .as_str() + .is_some_and(|url| url.ends_with("/api")) + }) + .await, + ); + let request = observed.last().unwrap()["params"]["requestId"].clone(); + let (method, session_id, params) = match cancellation { + "replace" => ( + "Page.navigate", + Some(session.session_id.as_str()), + json!({"url": fixture.url.replace("/page", "/replacement")}), + ), + "disable" => ( + "Fetch.disable", + Some(session.session_id.as_str()), + json!({}), + ), + "detach" => ( + "Target.detachFromTarget", + None, + json!({"sessionId": session.session_id}), + ), + _ => unreachable!(), + }; + let canceled = send_cdp_command(&mut socket, 22, method, session_id, params).await; + assert!( + canceled + .iter() + .any(|message| message["id"] == 22 && message.get("result").is_some()), + "{crashed}/{cancellation}: {canceled:?}" + ); + let stale = send_cdp_command( + &mut socket, + 23, + "Fetch.fulfillRequest", + Some(&session.session_id), + json!({"requestId": request, "responseCode": 200}), + ) + .await; + assert!( + stale + .iter() + .any(|message| message["id"] == 23 && message.get("error").is_some()), + "{crashed}/{cancellation}: {stale:?}" + ); + fixture.release.notify_one(); + let alive = + send_cdp_command(&mut socket, 24, "Target.getTargets", None, json!({})).await; + assert!( + alive + .iter() + .any(|message| message["id"] == 24 && message.get("result").is_some()) + ); + let live_session = if cancellation == "detach" { + let attached = send_cdp_command( + &mut socket, + 25, + "Target.attachToTarget", + None, + json!({"targetId": session.target_id, "flatten": true}), + ) + .await; + attached.last().unwrap()["result"]["sessionId"] + .as_str() + .unwrap() + .to_owned() + } else { + session.session_id.clone() + }; + let replacement = fixture.url.replace("/page", "/replacement"); + let recovered = send_cdp_command( + &mut socket, + 26, + "Page.navigate", + Some(&live_session), + json!({"url": replacement}), + ) + .await; + let recovered = &recovered.last().unwrap(); + assert!( + recovered.get("result").is_some() && recovered["result"].get("errorText").is_none(), + "{crashed}/{cancellation}: {recovered:?}" + ); + let native = send_cdp_command( + &mut socket, + 27, + "Runtime.evaluate", + Some(&live_session), + json!({"expression": "location.href", "returnByValue": true}), + ) + .await; + assert_eq!( + native.last().unwrap()["result"]["result"]["value"], + replacement, + "{crashed}/{cancellation}: {native:?}" + ); + let _ = socket.close(None).await; + abort_test_cdp_server(server).await; + } + } +} + +#[tokio::test] +async fn websocket_cdp_prepared_document_history_updates_survive_adoption_without_duplicate_entries() + { + for crashed in [false, true] { + for action in ["replaceState", "pushState"] { + let fixture = Fixture::new(); + let (addr, server) = spawn_test_protocol_server().await; + let (mut socket, _) = + connect_async(format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}")) + .await + .unwrap(); + let context = cdp_create_browser_context(&mut socket, 1).await; + let session = cdp_create_attached_target(&mut socket, 2, &context).await; + configure(&mut socket, &session.session_id, false, false).await; + let previous_url = "data:text/html,previous title"; + let initial = send_cdp_command( + &mut socket, + 13, + "Page.navigate", + Some(&session.session_id), + json!({"url": previous_url}), + ) + .await; + if !initial + .iter() + .any(|message| message["method"] == "Page.loadEventFired") + { + recv_until_match(&mut socket, |message| { + message["method"] == "Page.loadEventFired" + }) + .await; + } + if crashed { + let result = send_cdp_command( + &mut socket, + 14, + "Page.crash", + Some(&session.session_id), + json!({}), + ) + .await; + assert!(result.last().unwrap().get("result").is_some(), "{result:?}"); + } + let changed = fixture.url.replace("/page", "/changed"); + let mut observed = start_document( + &mut socket, + &session.session_id, + &fixture, + &format!("history.{action}(null,'','/changed');"), + ) + .await; + let changed_event = |message: &Value| { + message["method"] == "Page.navigatedWithinDocument" + && message["params"]["url"] == changed + }; + if !observed.iter().any(changed_event) { + observed.extend(recv_until_match(&mut socket, changed_event).await); + } + assert!( + !observed + .iter() + .any(|message| message["method"] == "Page.domContentEventFired"), + "{observed:?}" + ); + let targets = + send_cdp_command(&mut socket, 22, "Target.getTargets", None, json!({})).await; + assert!( + targets.last().unwrap()["result"]["targetInfos"] + .as_array() + .unwrap() + .iter() + .any(|target| target["targetId"] == session.target_id + && target["url"] == changed), + "{targets:?}" + ); + let before = send_cdp_command( + &mut socket, + 23, + "Page.getNavigationHistory", + Some(&session.session_id), + json!({}), + ) + .await; + let before = &before.last().unwrap()["result"]; + let entries = before["entries"].as_array().unwrap(); + assert_eq!(entries.last().unwrap()["url"], changed, "{before:?}"); + assert_eq!(entries.last().unwrap()["title"], "", "{before:?}"); + if !crashed { + assert_eq!( + entries + .iter() + .find(|entry| entry["url"] == previous_url) + .expect("ordinary navigation retains the preceding entry")["title"], + "previous title", + "{before:?}" + ); + } + assert_eq!( + entries + .iter() + .filter(|entry| entry["url"] == fixture.url) + .count(), + usize::from(action == "pushState"), + "{before:?}" + ); + fixture.release.notify_one(); + recv_until_match(&mut socket, |message| { + message["method"] == "Page.loadEventFired" + }) + .await; + let after = send_cdp_command( + &mut socket, + 24, + "Page.getNavigationHistory", + Some(&session.session_id), + json!({}), + ) + .await; + assert_eq!( + &after.last().unwrap()["result"], + before, + "{crashed}/{action}" + ); + let native = send_cdp_command( + &mut socket, + 25, + "Runtime.evaluate", + Some(&session.session_id), + json!({"expression": "location.href", "returnByValue": true}), + ) + .await; + assert_eq!(native.last().unwrap()["result"]["result"]["value"], changed); + let _ = socket.close(None).await; + abort_test_cdp_server(server).await; + } + } +} + +#[tokio::test] +async fn websocket_cdp_prepared_document_script_navigation_replaces_pending_adoption() { + for crashed in [false, true] { + for action in ["assign", "replace"] { + let fixture = Fixture::new(); + let (addr, server) = spawn_test_protocol_server().await; + let (mut socket, _) = + connect_async(format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}")) + .await + .unwrap(); + let context = cdp_create_browser_context(&mut socket, 1).await; + let session = cdp_create_attached_target(&mut socket, 2, &context).await; + configure(&mut socket, &session.session_id, crashed, false).await; + let mut observed = begin_document( + &mut socket, + &session.session_id, + &fixture, + &format!("location.{action}('/replacement');"), + ) + .await; + // The replacement must start while the original parser is still blocked. + tokio::time::timeout( + Duration::from_secs(10), + fixture.replacement_requested.notified(), + ) + .await + .unwrap(); + let replacement = fixture.url.replace("/page", "/replacement"); + let loaded = |message: &Value| { + message["method"] == "Page.loadEventFired" + && message["sessionId"] == session.session_id + }; + if !observed.iter().any(loaded) { + observed.extend(recv_until_match(&mut socket, loaded).await); + } + let history = send_cdp_command( + &mut socket, + 22, + "Page.getNavigationHistory", + Some(&session.session_id), + json!({}), + ) + .await; + observed.extend(history.clone()); + let entries = history.last().unwrap()["result"]["entries"] + .as_array() + .unwrap(); + assert_eq!( + entries.last().unwrap()["url"], + replacement, + "{crashed}/{action}: {history:?}" + ); + assert_eq!( + entries + .iter() + .filter(|entry| entry["url"] == fixture.url) + .count(), + usize::from(action == "assign"), + "{history:?}" + ); + for id in [20, 21] { + if !observed.iter().any(|message| message["id"] == id) { + observed + .extend(recv_until_match(&mut socket, |message| message["id"] == id).await); + } + assert_eq!( + observed + .iter() + .filter(|message| message["id"] == id) + .count(), + 1, + "{observed:?}" + ); + } + fixture.release.notify_one(); + let native = send_cdp_command( + &mut socket, + 23, + "Runtime.evaluate", + Some(&session.session_id), + json!({"expression": "location.href", "returnByValue": true}), + ) + .await; + assert_eq!( + native.last().unwrap()["result"]["result"]["value"], + replacement + ); + let _ = socket.close(None).await; + abort_test_cdp_server(server).await; + } + } +} + +#[tokio::test] +async fn websocket_cdp_prepared_document_file_chooser_preserves_source_node() { + for crashed in [false, true] { + let fixture = Fixture::new(); + let (addr, server) = spawn_test_protocol_server().await; + let (mut socket, _) = + connect_async(format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}")) + .await + .unwrap(); + let context = cdp_create_browser_context(&mut socket, 1).await; + let session = cdp_create_attached_target(&mut socket, 2, &context).await; + configure(&mut socket, &session.session_id, false, false).await; + let enabled = send_cdp_command( + &mut socket, + 10, + "Page.setInterceptFileChooserDialog", + Some(&session.session_id), + json!({"enabled": true}), + ) + .await; + assert!( + enabled.last().unwrap().get("result").is_some(), + "{enabled:?}" + ); + if crashed { + send_cdp_command( + &mut socket, + 11, + "Page.crash", + Some(&session.session_id), + json!({}), + ) + .await; + } + let mut observed = start_document(&mut socket, &session.session_id, &fixture, + "var f=document.createElement('input');f.type='file';document.documentElement.appendChild(f);f.click();").await; + let opened = |message: &Value| message["method"] == "Page.fileChooserOpened"; + if !observed.iter().any(opened) { + observed.extend(recv_until_match(&mut socket, opened).await); + } + assert!( + !observed + .iter() + .any(|message| message["method"] == "Page.domContentEventFired"), + "{observed:?}" + ); + let chooser = observed.iter().find(|message| opened(message)).unwrap(); + let backend = chooser["params"]["backendNodeId"].clone(); + assert!(backend.as_u64().is_some_and(|id| id > 0), "{chooser:?}"); + fixture.release.notify_one(); + recv_until_match(&mut socket, |message| { + message["method"] == "Page.loadEventFired" + }) + .await; + let node = send_cdp_command( + &mut socket, + 22, + "DOM.describeNode", + Some(&session.session_id), + json!({"backendNodeId": backend}), + ) + .await; + assert_eq!( + node.last().unwrap()["result"]["node"]["nodeName"], + "INPUT", + "{node:?}" + ); + let _ = socket.close(None).await; + abort_test_cdp_server(server).await; + } +} + +#[tokio::test] +async fn websocket_cdp_prepared_document_popup_navigation_and_close_keep_their_source() { + for close in [false, true] { + let fixture = Fixture::new(); + let (addr, server) = spawn_test_protocol_server().await; + let (mut socket, _) = + connect_async(format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}")) + .await + .unwrap(); + let context = cdp_create_browser_context(&mut socket, 1).await; + let session = cdp_create_attached_target(&mut socket, 2, &context).await; + configure(&mut socket, &session.session_id, true, false).await; + send_cdp_command( + &mut socket, + 10, + "Target.setDiscoverTargets", + None, + json!({"discover": true}), + ) + .await; + let mut observed = start_document( + &mut socket, + &session.session_id, + &fixture, + if close { + "var w=window.open('/popup');fetch('/api').then(()=>w.close());" + } else { + "var w=window.open('/popup');fetch('/api').then(()=>{w.location.href='/replacement'});" + }, + ) + .await; + let created = |message: &Value| { + message["method"] == "Target.targetCreated" + && message["params"]["targetInfo"]["openerId"] == session.target_id + }; + if !observed.iter().any(created) { + observed.extend(recv_until_match(&mut socket, created).await); + } + let popup = observed.iter().find(|message| created(message)).unwrap()["params"]["targetInfo"]["targetId"].clone(); + let paused = |message: &Value| { + message["method"] == "Fetch.requestPaused" + && message["params"]["request"]["url"] + .as_str() + .is_some_and(|url| url.ends_with("/api")) + }; + if !observed.iter().any(paused) { + observed.extend(recv_until_match(&mut socket, paused).await); + } + let request = + observed.iter().find(|message| paused(message)).unwrap()["params"]["requestId"].clone(); + observed.extend( + send_cdp_command( + &mut socket, + 23, + "Fetch.fulfillRequest", + Some(&session.session_id), + json!({"requestId": request, "responseCode": 200}), + ) + .await, + ); + if close { + let destroyed = |message: &Value| { + message["method"] == "Target.targetDestroyed" + && message["params"]["targetId"] == popup + }; + if !observed.iter().any(destroyed) { + observed.extend(recv_until_match(&mut socket, destroyed).await); + } + } else { + tokio::time::timeout( + Duration::from_secs(10), + fixture.replacement_requested.notified(), + ) + .await + .unwrap(); + let replacement = fixture.url.replace("/page", "/replacement"); + let navigated = |message: &Value| { + message["method"] == "Target.targetInfoChanged" + && message["params"]["targetInfo"]["targetId"] == popup + && message["params"]["targetInfo"]["url"] == replacement + }; + if !observed.iter().any(navigated) { + observed.extend(recv_until_match(&mut socket, navigated).await); + } + } + assert!( + !observed + .iter() + .any(|message| message["method"] == "Page.domContentEventFired" + && message["sessionId"] == session.session_id), + "{observed:?}" + ); + fixture.release.notify_one(); + recv_until_match(&mut socket, |message| { + message["method"] == "Page.loadEventFired" && message["sessionId"] == session.session_id + }) + .await; + let targets = send_cdp_command(&mut socket, 22, "Target.getTargets", None, json!({})).await; + assert_eq!( + targets.last().unwrap()["result"]["targetInfos"] + .as_array() + .unwrap() + .iter() + .any(|target| target["targetId"] == popup), + !close, + "{targets:?}" + ); + let _ = socket.close(None).await; + abort_test_cdp_server(server).await; + } +} diff --git a/moli-protocol/src/conn.rs b/moli-protocol/src/conn.rs index 49f978faac..0e74ea9ce0 100644 --- a/moli-protocol/src/conn.rs +++ b/moli-protocol/src/conn.rs @@ -1991,10 +1991,28 @@ impl CdpConnection { else { return Some(false); }; - Some( - self.commit_loaded_navigation_target_identity_for_owner(owner, commit, &target_url) - .is_some(), - ) + if self + .commit_loaded_navigation_target_identity_for_owner(owner, commit, &target_url) + .is_none() + { + return Some(false); + } + if let Some((renderer_page, _)) = self + .runtime_session_owner_slot_for_owner(owner)? + .page_slot() + .prepared_document_output_binding() + && let Some(attachment) = + self.target_page_output_attachment_for_renderer_page(owner, renderer_page) + && let Some(target_id) = attachment.page_owner().target_id() + && let Some(context) = + self.browser_context_by_id(attachment.page_owner().browser_context_id()) + { + context.close_dedicated_workers_outside_navigation_page( + target_id, + Some(attachment.page_owner()), + ); + } + Some(true) } #[cfg(test)] diff --git a/moli-protocol/src/conn/browser_context/fetch_owner.rs b/moli-protocol/src/conn/browser_context/fetch_owner.rs index b5461fe01e..9353e670a5 100644 --- a/moli-protocol/src/conn/browser_context/fetch_owner.rs +++ b/moli-protocol/src/conn/browser_context/fetch_owner.rs @@ -15,6 +15,10 @@ use crate::conn::{ PendingSubresourceFetchRequest, PendingSubresourceFetchResponseRequest, TargetRuntimeSlot, }; +#[derive(Debug, thiserror::Error)] +#[error("RequestNotFound")] +struct SubresourceRequestNotFound; + pub(crate) type SessionOwnerPendingFetchState = ( Vec, Vec, @@ -449,22 +453,57 @@ impl CdpConnection { .find_map(|browser_context| pending_fetch_request_route(browser_context, request_id)) } + pub(crate) fn start_subresource_document_command( + &self, + owner: &CommandOwnerScope, + page_owner: &crate::conn::TargetPageResidenceIdentity, + target: Option<&moli_core::page::PageDocumentCommandTarget>, + command: moli_core::RendererPageCommand, + ) -> Result { + let target = target.context(SubresourceRequestNotFound)?; + if !self.subresource_document_is_current_for_owner( + owner, + page_owner, + Some(target.document()), + ) { + return Err(SubresourceRequestNotFound.into()); + } + target.start(command) + } + + pub(crate) fn subresource_document_is_current( + &self, + page_owner: &crate::conn::TargetPageResidenceIdentity, + document: Option, + ) -> bool { + self.target_page_output_residence_is_current(page_owner) + && document.is_none_or(|document| { + let owner = CommandOwnerScope::for_page_residence(page_owner); + self.target_root_document_output_identity_for_owner(&owner, document) + .is_some_and(|binding| binding.attachment().page_owner() == page_owner) + }) + } + + pub(crate) fn subresource_document_is_current_for_owner( + &self, + owner: &CommandOwnerScope, + page: &crate::conn::TargetPageResidenceIdentity, + document: Option, + ) -> bool { + self.target_owner_identity_for_owner(owner) + .is_some_and(|(context, target)| { + context == page.browser_context_id() && target.as_deref() == page.target_id() + }) + && self.subresource_document_is_current(page, document) + } + pub(crate) fn pending_subresource_fetch_request_residence_is_current( &self, pending: &PendingSubresourceFetchRequest, ) -> bool { - pending - .page_owner() - .is_none_or(|owner| self.target_page_output_residence_is_current(owner)) - } - - fn installed_subresource_fetch_request_is_current( - &self, - pending: &PendingSubresourceFetchRequest, - ) -> bool { - pending - .page_owner() - .is_some_and(|owner| self.target_page_residence_identity_is_current(owner)) + pending.page_owner().is_none_or(|page| { + self.subresource_document_is_current(page, pending.source_document()) + }) } pub(crate) fn claim_subresource_continue_request_for_owner( @@ -474,7 +513,7 @@ impl CdpConnection { internal_id: u64, allow_pending_completion: bool, ) -> Option { - if !self.target_page_residence_identity_is_current(expected_page_owner) { + if !self.target_page_output_residence_is_current(expected_page_owner) { return None; } self.target_session_owner_mut_for_owner(owner)? @@ -808,7 +847,7 @@ impl CdpConnection { let pending = self .target_session_owner_mut_for_owner(owner)? .take_pending_subresource_fetch_auth_request(request_id, action_session_id)?; - self.target_page_residence_identity_is_current(&pending.page_owner) + self.subresource_document_is_current(&pending.page_owner, pending.source_document()) .then_some(pending) } @@ -821,7 +860,7 @@ impl CdpConnection { let pending = self .target_session_owner_mut_for_owner(owner)? .take_pending_subresource_fetch_response_request(request_id, action_session_id)?; - self.target_page_residence_identity_is_current(&pending.page_owner) + self.subresource_document_is_current(&pending.page_owner, pending.source_document()) .then_some(pending) } @@ -882,7 +921,7 @@ impl CdpConnection { request_id: Option, pending: PendingSubresourceFetchRequest, ) -> bool { - if !self.installed_subresource_fetch_request_is_current(&pending) { + if !self.pending_subresource_fetch_request_residence_is_current(&pending) { return false; } self.record_pending_subresource_network_request_identity_for_owner(owner, &pending); @@ -899,7 +938,7 @@ impl CdpConnection { pending: PendingSubresourceFetchRequest, response_stage_blocked_intercepts: Vec, ) -> bool { - if !self.installed_subresource_fetch_request_is_current(&pending) { + if !self.pending_subresource_fetch_request_residence_is_current(&pending) { return false; } self.record_pending_subresource_network_request_identity_for_owner(owner, &pending); @@ -920,7 +959,7 @@ impl CdpConnection { request_id: Option, pending: PendingSubresourceFetchRequest, ) -> bool { - if !self.installed_subresource_fetch_request_is_current(&pending) { + if !self.pending_subresource_fetch_request_residence_is_current(&pending) { return false; } self.record_pending_subresource_network_request_identity_for_owner(owner, &pending); @@ -950,7 +989,7 @@ impl CdpConnection { request_id: String, pending: PendingSubresourceFetchAuthRequest, ) -> bool { - if !self.target_page_residence_identity_is_current(&pending.page_owner) { + if !self.subresource_document_is_current(&pending.page_owner, pending.source_document()) { return false; } self.record_pending_subresource_auth_network_request_identity_for_owner(owner, &pending); @@ -978,7 +1017,7 @@ impl CdpConnection { request_id: String, pending: PendingSubresourceFetchResponseRequest, ) -> bool { - if !self.target_page_residence_identity_is_current(&pending.page_owner) { + if !self.subresource_document_is_current(&pending.page_owner, pending.source_document()) { return false; } self.record_pending_subresource_response_network_request_identity_for_owner( @@ -1170,21 +1209,12 @@ impl CdpConnection { SessionOwnerPendingFetchState, Option>, )> { - let mut owner = self.target_session_owner_mut_for_owner(command_owner)?; - let (pending, (subresource_enabled, subresource_resource_type), page_update_required) = - owner - .reset_fetch_config_for_session_and_drain_pending_state(command_owner.session_id()); - let page_command = if page_update_required - && let Some(page) = owner.runtime_slot_mut().loaded_page_mut() - { - Some(page.start_set_fetch_subresource_interception( - subresource_enabled, - subresource_resource_type, - )) - } else { - None - }; - Some((pending, page_command)) + let (pending, (enabled, resource_type), update_required) = self + .target_session_owner_mut_for_owner(command_owner)? + .reset_fetch_config_for_session_and_drain_pending_state(command_owner.session_id()); + let policy = + self.start_fetch_cleanup_policy(command_owner, update_required, enabled, resource_type); + Some((pending, policy)) } pub(crate) fn start_disable_target_fetch_for_owner( @@ -1194,18 +1224,54 @@ impl CdpConnection { SessionOwnerPendingFetchState, Option>, )> { - let mut owner = self.target_session_owner_mut_for_owner(command_owner)?; - let (pending, (enabled, resource_type), update_required) = - owner.disable_target_fetch_and_drain_pending_state(); + let (pending, (enabled, resource_type), update_required) = self + .target_session_owner_mut_for_owner(command_owner)? + .disable_target_fetch_and_drain_pending_state(); let policy = - if update_required && let Some(page) = owner.runtime_slot_mut().loaded_page_mut() { - Some(page.start_set_fetch_subresource_interception(enabled, resource_type)) - } else { - None - }; + self.start_fetch_cleanup_policy(command_owner, update_required, enabled, resource_type); Some((pending, policy)) } + fn start_fetch_cleanup_policy( + &mut self, + owner: &CommandOwnerScope, + update_required: bool, + enabled: bool, + resource_type: Option, + ) -> Option> { + if !update_required { + return None; + } + let source = self + .runtime_session_owner_slot_for_owner(owner)? + .page_slot() + .prepared_document_output_binding() + .map(|(page, binding)| (page, binding.renderer_document_identity())); + if let Some((renderer_page, document)) = source { + let target: Result<_> = (|| { + let attachment = self + .target_page_output_attachment_for_renderer_page(owner, renderer_page) + .ok_or(crate::conn::NoDocumentLoaded)?; + self.capture_document_output_command_target( + owner, + attachment.page_owner(), + renderer_page, + document, + ) + })(); + return Some(target.and_then(|target| { + target.start( + moli_core::RendererPageCommand::SetFetchSubresourceInterception { + enabled, + resource_type, + }, + ) + })); + } + self.loaded_page_mut_for_owner(owner) + .map(|page| page.start_set_fetch_subresource_interception(enabled, resource_type)) + } + pub(crate) fn take_pending_fetch_state_for_owner( &mut self, owner: &CommandOwnerScope, diff --git a/moli-protocol/src/conn/browser_context/target_session_owner.rs b/moli-protocol/src/conn/browser_context/target_session_owner.rs index 15490d4b17..af7bf415a6 100644 --- a/moli-protocol/src/conn/browser_context/target_session_owner.rs +++ b/moli-protocol/src/conn/browser_context/target_session_owner.rs @@ -754,6 +754,16 @@ impl<'a> TargetSessionOwnerMut<'a> { fn page_snapshot(&self) -> Option<(String, String)> { let target = self.target(); + // Native history already describes the new Document. Until adoption, + // the loaded Page's cached title still belongs to its predecessor. + if target + .runtime_slot() + .page_slot() + .prepared_document_output_binding() + .is_some() + { + return None; + } target .runtime_slot() .loaded_page() @@ -896,6 +906,7 @@ impl<'a> TargetSessionOwnerMut<'a> { let target = self.target_mut(); target.owner_state.target_crash_state.mark_crashed(); target.owner_state.navigation_history_state.clear(); + target.owner_state.navigation_history_document = None; target.owner_state.clear_loaded_document_context_state(); clear_page_loaded_document_session_state(target); target.fetch_owner.clear_pending(); @@ -1141,13 +1152,63 @@ impl<'a> TargetSessionOwnerMut<'a> { let security_origin = main_document_commit.security_origin.clone(); let secure_context_type = main_document_commit.secure_context_type.clone(); let target = self.target_mut(); - target.set_target_url(next_url); + let native_source = target + .runtime_slot + .page_slot() + .prepared_document_output_binding() + .map(|(page, binding)| (page, binding.renderer_document_identity())) + .or_else(|| { + Some(( + crate::conn::RendererPageResidenceIdentity::from_page( + target.runtime_slot.loaded_page()?, + ), + target + .runtime_slot + .page_slot() + .renderer_document_lifecycle_binding()? + .renderer_document_identity(), + )) + }); + if native_source.is_none() + || target.owner_state.navigation_history_document != native_source + { + target.set_target_url(next_url); + } target.set_target_security_origin(security_origin); target.set_target_secure_context_type(secure_context_type); target.owner_state.mark_initial_empty_document_exited(); Some(()) } + pub(super) fn record_native_navigation_history( + &mut self, + renderer_page: crate::conn::RendererPageResidenceIdentity, + document: moli_core::RendererDocumentLifecycleIdentity, + url: &Url, + ) { + let group = self + .browser_context + .renderer_runtime() + .browsing_context_name(&self.target_id) + .group(); + let target = self.target_mut(); + if target.owner_state.navigation_history_document == Some((renderer_page, document)) { + return; + } + if let Some(previous_title) = target.owner_state.committed_document_title.take() { + target + .owner_state + .refresh_current_navigation_history_title(previous_title); + } + target + .owner_state + .record_loaded_page_navigation_history_with_group( + (url.to_string(), String::new()), + Some(group), + ); + target.owner_state.navigation_history_document = Some((renderer_page, document)); + } + pub(super) fn clear_pending_navigation_history_update(&mut self) -> Option<()> { self.target_mut() .owner_state @@ -1355,8 +1416,17 @@ impl CdpConnection { main_document_commit: &RendererMainDocumentCommit, target_url: &Url, ) -> Option<()> { - self.target_session_owner_mut_for_owner(owner)? - .commit_loaded_navigation_target_identity(main_document_commit, target_url) + let prepared_source = self + .runtime_session_owner_slot_for_owner(owner)? + .page_slot() + .prepared_document_output_binding() + .map(|(page, binding)| (page, binding.renderer_document_identity())); + let mut target = self.target_session_owner_mut_for_owner(owner)?; + target.commit_loaded_navigation_target_identity(main_document_commit, target_url)?; + if let Some((page, document)) = prepared_source { + target.record_native_navigation_history(page, document, target_url); + } + Some(()) } pub(crate) fn install_loaded_navigation_page_for_owner( @@ -1958,19 +2028,41 @@ impl CdpConnection { &self, expected: &TargetPageResidenceIdentity, ) -> bool { - if self.target_page_residence_identity_is_current(expected) { - return true; - } - let owner = CommandOwnerScope::for_page_residence(expected); - self.pending_target_page_residence_identity_for_owner(&owner) - .as_ref() - == Some(expected) - && self - .runtime_session_owner_slot_for_owner(&owner) - .and_then(|slot| slot.page_slot().prepared_document_output_binding()) - .is_some_and(|(_, binding)| { - binding.page_attachment_id == expected.page_attachment_id() + self.browser_context_by_id(expected.browser_context_id()) + .is_some_and(|context| context.target_page_output_residence_is_current(expected)) + } + + /// Captures a revocable native capability only after the publication's + /// original Page identity has been admitted. Current Page access remains + /// guarded separately while a navigation is awaiting adoption. + pub(crate) fn capture_document_output_command_target( + &mut self, + owner: &CommandOwnerScope, + page_owner: &crate::conn::TargetPageResidenceIdentity, + renderer_page: crate::conn::RendererPageResidenceIdentity, + document: moli_core::RendererDocumentLifecycleIdentity, + ) -> Result { + if document.frame.page_id != renderer_page.page_id() + || !self + .runtime_session_owner_slot_for_owner(owner) + .is_some_and(|slot| { + slot.routes_current_renderer_page_owner( + renderer_page, + page_owner.page_attachment_id(), + ) }) + { + return Err(crate::conn::NoDocumentLoaded.into()); + } + if let Some(page) = self.loaded_page_mut_for_owner(owner) + && crate::conn::RendererPageResidenceIdentity::from_page(page) == renderer_page + { + return page.document_command_target(document); + } + let inputs = self.navigation_load_inputs_for_owner(owner); + self.navigation_engine_for_load_inputs_mut(&inputs) + .ok_or(crate::conn::NoDocumentLoaded)? + .document_command_target(renderer_page.owner_local_host_id(), document) } pub(crate) fn target_page_output_attachment_for_renderer_page( @@ -2243,6 +2335,20 @@ impl CdpConnection { browser_context_id: &str, target_id: &str, ) -> Option { + let owner = self.target_page_output_owner_for_target(browser_context_id, target_id)?; + let attachment = self.target_page_protocol_attachment_identity_for_owner(&owner)?; + (attachment.page_owner().browser_context_id() == browser_context_id + && attachment.page_owner().target_id() == Some(target_id)) + .then_some(attachment) + } + + /// Selects a frontend destination independently of Page-handle adoption. + /// This resolves session configuration and output routing, not Page access. + pub(crate) fn target_page_output_owner_for_target( + &self, + browser_context_id: &str, + target_id: &str, + ) -> Option { let browser_context = self.browser_context_by_id(browser_context_id)?; let primary_session_id = if browser_context.is_active_target(target_id) { browser_context.active_session_id_owned() @@ -2268,11 +2374,21 @@ impl CdpConnection { .is_some_and(|active| active.id == browser_context_id)) .then_some(None) })?; - let attachment = - self.target_page_protocol_attachment_identity_for_session(session_id.as_deref())?; - (attachment.page_owner().browser_context_id() == browser_context_id - && attachment.page_owner().target_id() == Some(target_id)) - .then_some(attachment) + let owner = session_id + .as_deref() + .map(CommandOwnerScope::for_session) + .unwrap_or_else(|| { + CommandOwnerScope::for_route(CdpSessionRoute::PageTarget { + browser_context_id: browser_context_id.to_owned(), + target_id: target_id.to_owned(), + session_key: DevToolsSessionKey::Primary, + }) + }); + self.target_owner_identity_for_owner(&owner) + .is_some_and(|(context, target)| { + context == browser_context_id && target.as_deref() == Some(target_id) + }) + .then_some(owner) } pub(crate) fn runtime_context_owner_identity_for_session( @@ -2821,6 +2937,56 @@ mod tests { use moli_core::page::SubresourceResourceType; use url::Url; + #[tokio::test] + async fn native_commit_reusing_initial_document_keeps_navigation_history() { + let mut ctx = TestContext::new(); + let mut browser = BrowserContext::new("BID-initial-history".into()); + browser.set_active_target_id("TID-initial-history"); + browser.attach_active_session("SID-initial-history"); + ctx.with_fixture(|conn| conn.install_browser_context_fixture_for_test(browser)); + ctx.install_navigation_fixture_for_session_owner( + "about:blank", + Some("SID-initial-history"), + ) + .await; + ctx.with_fixture(|conn| { + let owner = CommandOwnerScope::for_session("SID-initial-history"); + // An initial auxiliary Document seeds history without a navigation + // commit. Preserve its real Page/Document, not a fake native id. + conn.target_session_owner_mut_for_owner(&owner) + .unwrap() + .target_mut() + .owner_state + .navigation_history_document = None; + let before = conn + .target_session_owner_navigation_history_snapshot_for_owner(&owner) + .unwrap(); + let page = conn + .renderer_page_residence_identity_for_owner(&owner) + .unwrap(); + let document = conn + .target_root_document_lifecycle_identity_for_owner(&owner) + .unwrap(); + conn.start_document_navigation_for_owner(&owner, "LID-reused-blank".into()) + .unwrap(); + // Force publication before protocol adopts a navigation result. + conn.target_session_owner_mut_for_owner(&owner) + .unwrap() + .record_native_navigation_history( + page, + document, + &Url::parse("about:blank").unwrap(), + ); + let after = conn + .target_session_owner_navigation_history_snapshot_for_owner(&owner) + .unwrap(); + assert_eq!( + after, before, + "reusing the initial native Document keeps its existing entry" + ); + }); + } + #[test] fn generated_session_id_skips_caller_supplied_live_session() { let mut conn = CdpConnection::default(); diff --git a/moli-protocol/src/conn/fetch_support.rs b/moli-protocol/src/conn/fetch_support.rs index 9a9bce176e..625123302b 100644 --- a/moli-protocol/src/conn/fetch_support.rs +++ b/moli-protocol/src/conn/fetch_support.rs @@ -1369,6 +1369,10 @@ async fn capture_streaming_raw_response( #[derive(Debug, Clone, PartialEq)] pub(crate) enum PendingSubresourceFetchResidence { Page(super::state::TargetPageResidenceIdentity), + Document { + page_owner: super::state::TargetPageResidenceIdentity, + command_target: moli_core::page::PageDocumentCommandTarget, + }, DetachedParserScript(DetachedParserScriptFetchContinuation), } @@ -1391,16 +1395,40 @@ pub struct PendingSubresourceFetchRequest { impl PendingSubresourceFetchRequest { pub(crate) fn page_owner(&self) -> Option<&super::state::TargetPageResidenceIdentity> { match &self.residence { - PendingSubresourceFetchResidence::Page(owner) => Some(owner), + PendingSubresourceFetchResidence::Page(owner) + | PendingSubresourceFetchResidence::Document { + page_owner: owner, .. + } => Some(owner), PendingSubresourceFetchResidence::DetachedParserScript(_) => None, } } + pub(crate) fn document_command_target( + &self, + ) -> Option<&moli_core::page::PageDocumentCommandTarget> { + match &self.residence { + PendingSubresourceFetchResidence::Document { command_target, .. } => { + Some(command_target) + } + _ => None, + } + } + + pub(crate) fn source_document(&self) -> Option { + match &self.residence { + PendingSubresourceFetchResidence::Document { command_target, .. } => { + Some(command_target.document()) + } + _ => None, + } + } + pub(crate) fn detached_parser_script_fetch_continuation( &self, ) -> Option<&DetachedParserScriptFetchContinuation> { match &self.residence { - PendingSubresourceFetchResidence::Page(_) => None, + PendingSubresourceFetchResidence::Page(_) + | PendingSubresourceFetchResidence::Document { .. } => None, PendingSubresourceFetchResidence::DetachedParserScript(continuation) => { Some(continuation) } @@ -1514,6 +1542,7 @@ pub(crate) enum ClaimedSubresourceContinueRequest { pub struct PendingSubresourceFetchAuthRequest { /// Page residence that owns the paused renderer authentication request. pub(crate) page_owner: super::state::TargetPageResidenceIdentity, + pub(crate) command_target: Option, pub owner_session_id: Option, pub action_session_id: Option, pub owner_kind: PendingSubresourceFetchOwnerKind, @@ -1535,6 +1564,10 @@ pub struct PendingSubresourceFetchAuthRequest { } impl PendingSubresourceFetchAuthRequest { + pub(crate) fn source_document(&self) -> Option { + self.command_target.as_ref().map(|target| target.document()) + } + pub fn pop_next_auth_required_pause(&mut self) -> Option { let chain = self.auth_stage_chain.as_mut()?; let next = chain.remaining_sessions.first().cloned()?; @@ -1564,6 +1597,7 @@ pub struct PendingSubresourceFetchAuthStage { pub struct PendingSubresourceFetchResponseRequest { /// Page residence that owns the paused renderer response request. pub(crate) page_owner: super::state::TargetPageResidenceIdentity, + pub(crate) command_target: Option, pub owner_session_id: Option, pub action_session_id: Option, pub owner_kind: PendingSubresourceFetchOwnerKind, @@ -1589,6 +1623,10 @@ pub struct PendingSubresourceFetchResponseRequest { } impl PendingSubresourceFetchResponseRequest { + pub(crate) fn source_document(&self) -> Option { + self.command_target.as_ref().map(|target| target.document()) + } + pub fn pop_next_response_stage_pause( &mut self, ) -> Option { diff --git a/moli-protocol/src/conn/page_state/loaded.rs b/moli-protocol/src/conn/page_state/loaded.rs index b004aed3e4..2ded06328e 100644 --- a/moli-protocol/src/conn/page_state/loaded.rs +++ b/moli-protocol/src/conn/page_state/loaded.rs @@ -216,20 +216,37 @@ impl PageTargetHost { browsing_context_group: moli_core::page::RendererBrowsingContextGroup, ) { self.owner_state.mark_initial_empty_document_exited(); - if let Some(previous_title) = previous_title { - self.owner_state - .refresh_current_navigation_history_title(previous_title); - } - self.owner_state - .record_loaded_page_navigation_history_with_group( - (history_url.to_string(), committed_document_title.clone()), - Some(browsing_context_group), - ); - let native_document = self + let native_source = self .runtime_slot .page_slot() .prepared_document_output_binding() - .map(|(_, binding)| binding.renderer_document_identity()); + .map(|(page, binding)| (page, binding.renderer_document_identity())); + let already_committed = native_source.is_some() + && self.owner_state.navigation_history_document == native_source; + let committed_document_title = if already_committed { + self.owner_state + .committed_document_title + .clone() + .unwrap_or(committed_document_title) + } else { + committed_document_title + }; + if already_committed { + self.owner_state + .refresh_current_navigation_history_title(committed_document_title.clone()); + } else { + if let Some(previous_title) = previous_title { + self.owner_state + .refresh_current_navigation_history_title(previous_title); + } + self.owner_state + .record_loaded_page_navigation_history_with_group( + (history_url.to_string(), committed_document_title.clone()), + Some(browsing_context_group), + ); + self.owner_state.navigation_history_document = native_source; + } + let native_document = native_source.map(|(_, document)| document); // Concrete child output may precede Page-handle adoption. Retain only // the tree already observed for this exact Native Document. self.owner_state diff --git a/moli-protocol/src/conn/page_state/page_targets.rs b/moli-protocol/src/conn/page_state/page_targets.rs index 40357256d4..e124c3d974 100644 --- a/moli-protocol/src/conn/page_state/page_targets.rs +++ b/moli-protocol/src/conn/page_state/page_targets.rs @@ -984,6 +984,56 @@ impl BrowserContext { current_attachment == Some(expected.page_attachment_id()) } + pub(crate) fn target_page_output_residence_is_current( + &self, + expected: &crate::conn::TargetPageResidenceIdentity, + ) -> bool { + if self.target_page_residence_is_current(expected) { + return true; + } + if expected.browser_context_id() != self.id { + return false; + } + let target = match expected.target_id() { + Some(id) => self.page_target(id), + None if self.active_target_id().is_none() => Some(self.active_page_target()), + None => None, + }; + target.is_some_and(|target| { + let slot = &target.runtime_slot; + slot.pending_page_attachment_id() == Some(expected.page_attachment_id()) + && slot + .page_slot() + .prepared_document_output_binding() + .is_some_and(|(_, binding)| { + binding.page_attachment_id == expected.page_attachment_id() + }) + }) + } + + /// Revoke workers of abandoned Page sources without waiting for their + /// parsers to finish. A native commit also retires the previous loaded + /// Page, which may still accept late network output until adoption. + pub(crate) fn close_dedicated_workers_outside_navigation_page( + &self, + target_id: &str, + committed_page: Option<&crate::conn::TargetPageResidenceIdentity>, + ) { + for worker in self.dedicated_worker_targets.values().filter(|worker| { + worker.owner_page.target_id() == Some(target_id) + && !worker.is_retiring() + && match committed_page { + Some(page) => &worker.owner_page != page, + None => !self.target_page_output_residence_is_current(&worker.owner_page), + } + }) { + self.renderer_runtime() + .close_dedicated_worker_for_devtools(worker.renderer_instance_id); + } + // The original Page journal publishes Destroyed. Its retiring-output + // route owns session disposal and the target's terminal events. + } + pub(crate) fn dedicated_worker_target_id_for_renderer_instance( &self, renderer_instance_id: u64, @@ -1021,7 +1071,8 @@ impl BrowserContext { let Some(renderer_instance_id) = self .dedicated_worker_target(target_id) .filter(|target| { - !target.is_retiring() && self.target_page_residence_is_current(&target.owner_page) + !target.is_retiring() + && self.target_page_output_residence_is_current(&target.owner_page) }) .map(|target| target.renderer_instance_id) else { diff --git a/moli-protocol/src/conn/state/browser_context.rs b/moli-protocol/src/conn/state/browser_context.rs index 0074dd11b4..bfde4b9f59 100644 --- a/moli-protocol/src/conn/state/browser_context.rs +++ b/moli-protocol/src/conn/state/browser_context.rs @@ -1195,9 +1195,31 @@ impl BrowserContext { loader_id: String, ) -> Option { let target = self.page_target_mut(target_id)?; + // Initial Documents already own their placeholder history entry. + // Freeze that native identity before a new handoff can replace the + // installed binding. Reusing the same Document must not append it. + if !target.owner_state.navigation_history_state.is_empty() + && target + .runtime_slot + .page_slot() + .prepared_document_output_binding() + .is_none() + && let Some((page, binding)) = target.runtime_slot.loaded_page().zip( + target + .runtime_slot + .page_slot() + .renderer_document_lifecycle_binding(), + ) + { + target.owner_state.navigation_history_document = Some(( + super::RendererPageResidenceIdentity::from_page(page), + binding.renderer_document_identity(), + )); + } let token = target .runtime_slot .start_document_navigation(target_id.to_owned(), loader_id); + self.close_dedicated_workers_outside_navigation_page(target_id, None); self.mark_target_initial_empty_document_pending_cross_document_navigation(target_id); Some(token) } @@ -1283,6 +1305,7 @@ impl BrowserContext { .clear_pending_document_navigation_if_loader_matches(loader_id) }); if cleared { + self.close_dedicated_workers_outside_navigation_page(target_id, None); self.clear_target_initial_empty_document_pending_cross_document_navigation(target_id); } } diff --git a/moli-protocol/src/conn/state/fetch.rs b/moli-protocol/src/conn/state/fetch.rs index 37105bf688..3b9fee3253 100644 --- a/moli-protocol/src/conn/state/fetch.rs +++ b/moli-protocol/src/conn/state/fetch.rs @@ -2428,6 +2428,7 @@ mod tests { owner_kind: PendingSubresourceFetchOwnerKind, ) -> PendingSubresourceFetchAuthRequest { PendingSubresourceFetchAuthRequest { + command_target: None, page_owner: test_page_owner(), owner_session_id: owner_session_id.map(str::to_owned), action_session_id: owner_session_id.map(str::to_owned), @@ -2472,6 +2473,7 @@ mod tests { owner_kind: PendingSubresourceFetchOwnerKind, ) -> PendingSubresourceFetchResponseRequest { PendingSubresourceFetchResponseRequest { + command_target: None, page_owner: test_page_owner(), owner_session_id: owner_session_id.map(str::to_owned), action_session_id: owner_session_id.map(str::to_owned), diff --git a/moli-protocol/src/conn/state/runtime_slot.rs b/moli-protocol/src/conn/state/runtime_slot.rs index 8b7174e401..27d5895da8 100644 --- a/moli-protocol/src/conn/state/runtime_slot.rs +++ b/moli-protocol/src/conn/state/runtime_slot.rs @@ -753,6 +753,15 @@ impl TargetRuntimeSlot { } } + pub(crate) fn prepared_document_output_is_from( + &self, + source: moli_core::RendererDocumentLifecycleIdentity, + ) -> bool { + self.prepared_document_output + .as_ref() + .is_some_and(|output| output.binding.renderer_document_identity() == source) + } + pub(crate) fn complete_prepared_document_output_adoption(&mut self) { if self .prepared_document_output diff --git a/moli-protocol/src/conn/state/target_state.rs b/moli-protocol/src/conn/state/target_state.rs index c1d6a15ee6..7e91d231d9 100644 --- a/moli-protocol/src/conn/state/target_state.rs +++ b/moli-protocol/src/conn/state/target_state.rs @@ -528,6 +528,10 @@ pub(crate) struct TargetOwnerState { pub(crate) next_document_start_script_id: u32, pub(crate) document_start_scripts: Vec<(String, DocumentStartScript)>, pub(crate) navigation_history_state: TargetNavigationHistoryState, + pub(crate) navigation_history_document: Option<( + crate::conn::RendererPageResidenceIdentity, + moli_core::RendererDocumentLifecycleIdentity, + )>, pub(crate) page_resource_store: TargetPageResourceStore, pub(crate) runtime_observable_state: TargetRuntimeObservableState, pub(crate) console_output_state: TargetConsoleOutputState, diff --git a/moli-protocol/src/domains/activity/main_document.rs b/moli-protocol/src/domains/activity/main_document.rs index bdd2a4adb7..238bc01b8f 100644 --- a/moli-protocol/src/domains/activity/main_document.rs +++ b/moli-protocol/src/domains/activity/main_document.rs @@ -33,7 +33,10 @@ pub(crate) struct MainDocumentNavigationActivity { enum LoadedNavigationResultMode { Success, - NetworkErrorPage { error_text: String }, + NetworkErrorPage { + error_text: String, + commit: std::sync::Arc, + }, } pub(crate) struct MainDocumentFailedNavigationActivity { @@ -86,8 +89,12 @@ impl MainDocumentNavigationActivity { } } - pub(crate) fn with_network_error_page_result(mut self, error_text: String) -> Self { - self.result_mode = LoadedNavigationResultMode::NetworkErrorPage { error_text }; + pub(crate) fn with_network_error_page_result( + mut self, + error_text: String, + commit: std::sync::Arc, + ) -> Self { + self.result_mode = LoadedNavigationResultMode::NetworkErrorPage { error_text, commit }; self } @@ -130,7 +137,9 @@ impl MainDocumentNavigationActivity { } let network_error_text = match &self.result_mode { LoadedNavigationResultMode::Success => None, - LoadedNavigationResultMode::NetworkErrorPage { error_text } => Some(error_text.clone()), + LoadedNavigationResultMode::NetworkErrorPage { error_text, .. } => { + Some(error_text.clone()) + } }; if let Some(error_text) = network_error_text.as_deref() { let mut failure_events = Vec::new(); @@ -161,6 +170,11 @@ impl MainDocumentNavigationActivity { self.expose_loaded_response_metadata(&mut response_metadata_events); out.extend_background_events_after_messages(response_metadata_events); } + if let LoadedNavigationResultMode::NetworkErrorPage { commit, .. } = &self.result_mode { + out.extend_background_events_after_messages( + page::main_document_commit_background_events(conn, &self.state.owner, commit), + ); + } if let Some(renderer_output_boundary) = renderer_output_boundary { // Chromium can send Page.navigate/Fetch command responses and the // main-resource response metadata before Blink exposes the new diff --git a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs index 0f8e2e86b0..4f2216a4cc 100644 --- a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs +++ b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs @@ -375,12 +375,19 @@ impl PreparedProtocolOutputs { conn: &mut CdpConnection, owner: &CommandOwnerScope, action: RendererOwnerAction, + page_source: Option<( + crate::conn::TargetPageResidenceIdentity, + crate::conn::RendererPageResidenceIdentity, + )>, ) -> Self { let mut prepared = Self::empty(); match action { RendererOwnerAction::FileChooser(activation) => { crate::domains::input::InputPreparedOutputs::from_renderer_file_chooser_activation( - conn, owner, activation, + conn, + owner, + page_source, + activation, ) .append_to_output_sink(&mut prepared); } @@ -392,13 +399,17 @@ impl PreparedProtocolOutputs { } RendererOwnerAction::JavaScriptDialog(dialog) => { crate::domains::page::PagePreparedOutputs::from_renderer_javascript_dialog( - conn, owner, dialog, + conn, + owner, + page_source.map(|(page, _)| page), + dialog, ) .append_to_javascript_dialog_output_sink(&mut prepared); } RendererOwnerAction::CloseAuxiliaryWindow(window) => { crate::domains::page::PagePreparedOutputs::from_renderer_auxiliary_window_close( - conn, owner, window, + page_source.map(|(page, _)| page), + window, ) .append_to_auxiliary_window_close_output_sink(&mut prepared); } @@ -409,13 +420,14 @@ impl PreparedProtocolOutputs { document_response, } => { crate::domains::page::PagePreparedOutputs::from_renderer_auxiliary_window_navigation( - conn, owner, window, url, kind, document_response, + page_source.map(|(page, _)| page), window, url, kind, document_response, ) .append_to_auxiliary_window_navigation_output_sink(&mut prepared); } RendererOwnerAction::Popup(activation) => { crate::domains::page::PagePreparedOutputs::from_renderer_popup_activation( - conn, owner, activation, + page_source.map(|(page, _)| page), + activation, ) .append_to_popup_output_sink(&mut prepared); } @@ -474,8 +486,7 @@ impl PreparedProtocolOutputs { update, } => { crate::domains::page::PagePreparedOutputs::from_renderer_session_history_update( - conn, - owner, + page_source.map(|(page, _)| page), source_document, update, ) @@ -483,22 +494,21 @@ impl PreparedProtocolOutputs { } RendererOwnerAction::SameDocumentNavigation(navigation) => { crate::domains::page::PagePreparedOutputs::from_renderer_same_document_navigation( - conn, owner, navigation, + page_source.map(|(page, _)| page), + navigation, ) .append_to_same_document_navigation_output_sink(&mut prepared); } RendererOwnerAction::TopLevelLocationNavigation(navigation) => { crate::domains::page::PagePreparedOutputs:: from_renderer_top_level_location_navigation( - conn, - owner, - navigation, + page_source.map(|(page, _)| page), navigation, ) .append_to_top_level_location_navigation_output_sink(&mut prepared); } RendererOwnerAction::TopLevelHistoryTraversal(traversal) => { crate::domains::page::PagePreparedOutputs:: - from_renderer_top_level_history_traversal(conn, owner, traversal) + from_renderer_top_level_history_traversal(page_source.map(|(page, _)| page), owner, traversal) .append_to_top_level_history_traversal_output_sink(&mut prepared); } RendererOwnerAction::SubresourceFetchPause { @@ -511,6 +521,7 @@ impl PreparedProtocolOutputs { owner, source_document, *info, + page_source, ) .append_to_output_sink(&mut prepared); } @@ -573,6 +584,7 @@ impl PreparedProtocolOutputs { dedicated_worker_target_lifecycle_prepared_outputs_for_event( conn, owner, + page_source, event, ) .append_to_dedicated_worker_target_lifecycle_output_sink(&mut prepared); diff --git a/moli-protocol/src/domains/activity/output_ingress/projection.rs b/moli-protocol/src/domains/activity/output_ingress/projection.rs index d6f80876eb..5414deff6d 100644 --- a/moli-protocol/src/domains/activity/output_ingress/projection.rs +++ b/moli-protocol/src/domains/activity/output_ingress/projection.rs @@ -102,6 +102,14 @@ impl RendererOutputProjection { record: RendererOutputRecord, context: &mut CommandWorkflowContext, ) -> Option { + let page_source = match &publication_owner { + RendererPublicationOwner::PageTarget { + page_owner, + renderer_page, + .. + } => Some((page_owner.clone(), *renderer_page)), + _ => None, + }; let route = publication_owner.resolve(conn); let (owner, projection) = match route { Some(RendererPublicationRoute::AttachedSession { @@ -116,11 +124,21 @@ impl RendererOutputProjection { projection, }) => (Some(CommandOwnerScope::for_route(owner_route)), projection), None => ( - None, + page_source + .as_ref() + .map(|(page, _)| CommandOwnerScope::for_page_residence(page)), RendererPublicationProjection::RetiringNetworkAndResponses, ), }; - Self::prepare_record_for_route(conn, owner, projection, cursor, record, context) + Self::prepare_record_for_route( + conn, + owner, + projection, + cursor, + record, + context, + page_source, + ) } pub(super) fn prepare_record_for_route( @@ -130,6 +148,10 @@ impl RendererOutputProjection { cursor: moli_core::RendererOutputCursor, record: RendererOutputRecord, context: &mut CommandWorkflowContext, + page_source: Option<( + crate::conn::TargetPageResidenceIdentity, + crate::conn::RendererPageResidenceIdentity, + )>, ) -> Option { let (cause, mut item) = record.into_parts(); if !projection.admit_record(&mut item) { @@ -149,8 +171,12 @@ impl RendererOutputProjection { RendererOutputItem::OwnerAction(action) => { let owner = super::renderer::renderer_owner_action_owner(conn, &owner, cause.as_ref()); - let outputs = - PreparedProtocolOutputs::from_renderer_owner_action(conn, &owner, action); + let outputs = PreparedProtocolOutputs::from_renderer_owner_action( + conn, + &owner, + action, + page_source, + ); (owner, outputs) } RendererOutputItem::Observation(observation) => { diff --git a/moli-protocol/src/domains/activity/output_ingress/renderer.rs b/moli-protocol/src/domains/activity/output_ingress/renderer.rs index b7e3af6b30..1e2ba821e5 100644 --- a/moli-protocol/src/domains/activity/output_ingress/renderer.rs +++ b/moli-protocol/src/domains/activity/output_ingress/renderer.rs @@ -25,9 +25,7 @@ pub(super) fn renderer_owner_action_owner( let Some(target_id) = target_id else { return publication_owner.clone(); }; - conn.target_page_protocol_attachment_identity_for_target(&browser_context_id, &target_id) - .as_ref() - .map(CommandOwnerScope::for_page_attachment) + conn.target_page_output_owner_for_target(&browser_context_id, &target_id) .unwrap_or_else(|| publication_owner.clone()) } @@ -75,6 +73,7 @@ mod tests { cursor, record, &mut context, + None, ) }) }) diff --git a/moli-protocol/src/domains/activity/publication_route.rs b/moli-protocol/src/domains/activity/publication_route.rs index ef19f66ada..18ea71bebf 100644 --- a/moli-protocol/src/domains/activity/publication_route.rs +++ b/moli-protocol/src/domains/activity/publication_route.rs @@ -63,6 +63,11 @@ impl RendererPublicationProjection { } match item { RendererOutputItem::NativeTerminal(_) => true, + RendererOutputItem::OwnerAction( + moli_core::RendererOwnerAction::DedicatedWorkerTargetLifecycle( + moli_core::page::RendererDedicatedWorkerTargetEvent::Destroyed { .. }, + ), + ) => true, RendererOutputItem::Observation(RendererProtocolObservation::Network { .. }) => true, RendererOutputItem::Observation(RendererProtocolObservation::RuntimeInspector( batch, diff --git a/moli-protocol/src/domains/activity/runtime_command_barrier.rs b/moli-protocol/src/domains/activity/runtime_command_barrier.rs index b3933a9c44..4f4ee1c9b8 100644 --- a/moli-protocol/src/domains/activity/runtime_command_barrier.rs +++ b/moli-protocol/src/domains/activity/runtime_command_barrier.rs @@ -785,6 +785,12 @@ mod tests { let (command_owner, renderer_cause, outputs) = ctx.with_fixture(|conn| { let command_owner = CommandOwnerScope::for_session(SESSION_ID); let source_document = loaded_page_source_document(conn); + let page_source = Some(( + conn.target_page_residence_identity_for_owner(&command_owner) + .expect("test publication Page attachment"), + conn.renderer_page_residence_identity_for_owner(&command_owner) + .expect("test publication renderer Page"), + )); let outputs = super::super::output_ingress::PreparedProtocolOutputs::from_renderer_owner_action( conn, @@ -798,6 +804,7 @@ mod tests { }, ), ), + page_source, ); let renderer_cause = permit.map(|permit| renderer_cause_for_permit(conn, permit)); diff --git a/moli-protocol/src/domains/activity/subresource.rs b/moli-protocol/src/domains/activity/subresource.rs index ceaa5411ec..362990d920 100644 --- a/moli-protocol/src/domains/activity/subresource.rs +++ b/moli-protocol/src/domains/activity/subresource.rs @@ -8,7 +8,7 @@ use crate::conn::{ PendingSubresourceFetchResponseStageChain, TargetPageResidenceIdentity, }; use crate::domains::fetch; -use anyhow::{Context as _, Result}; +use anyhow::Result; use moli_core::page::{ PendingSubresourceAuthInfo, PendingSubresourceContinueEvent, PendingSubresourceResponseInfo, SubresourceNetworkRequestHandle, @@ -75,12 +75,9 @@ pub(in crate::domains) fn prepare_subresource_continue_action_for_renderer_recor source_document: moli_core::RendererDocumentLifecycleIdentity, event: PendingSubresourceContinueEvent, ) -> Option { - if conn.target_root_document_lifecycle_identity_for_owner(command_owner) - != Some(source_document) - { - return None; - } - let owner = conn.target_page_residence_identity_for_owner(command_owner)?; + let binding = + conn.target_root_document_output_identity_for_owner(command_owner, source_document)?; + let owner = binding.attachment().page_owner().clone(); Some(PreparedSubresourceContinueAction::capture( conn, command_owner, @@ -95,7 +92,7 @@ pub(crate) fn prepare_subresource_continue_action( command_owner: &CommandOwnerScope, action: PreparedSubresourceContinueAction, ) -> Option { - if !conn.target_page_residence_identity_is_current(&action.owner) { + if !conn.target_page_output_residence_is_current(&action.owner) { return None; } let session_id = command_owner.session_id(); @@ -388,9 +385,10 @@ fn pending_auth_request( auth_info: &PendingSubresourceAuthInfo, ) -> PendingSubresourceFetchAuthRequest { PendingSubresourceFetchAuthRequest { + command_target: pending.document_command_target().cloned(), page_owner: pending .page_owner() - .expect("an in-flight auth continuation must belong to an installed Page") + .expect("an in-flight auth continuation must retain its native Page residence") .clone(), owner_session_id: pending.owner_session_id.clone(), action_session_id: pending.action_session_id.clone(), @@ -423,9 +421,10 @@ fn pending_response_request( response_info: &PendingSubresourceResponseInfo, ) -> PendingSubresourceFetchResponseRequest { PendingSubresourceFetchResponseRequest { + command_target: pending.document_command_target().cloned(), page_owner: pending .page_owner() - .expect("an in-flight response continuation must belong to an installed Page") + .expect("an in-flight response continuation must retain its native Page residence") .clone(), owner_session_id: pending.owner_session_id.clone(), action_session_id: pending.action_session_id.clone(), @@ -471,6 +470,11 @@ pub(crate) fn flush_post_subresource_fetch_request_activity_for_owner_background owner: &CommandOwnerScope, pending: &PendingSubresourceFetchRequest, ) { + if pending.page_owner().is_some_and(|page| { + !conn.subresource_document_is_current_for_owner(owner, page, pending.source_document()) + }) { + return; + } record_subresource_network_request_handle( conn, owner, @@ -504,6 +508,13 @@ pub(crate) fn flush_post_subresource_response_activity_for_owner_background_even owner: &CommandOwnerScope, pending: &PendingSubresourceFetchResponseRequest, ) { + if !conn.subresource_document_is_current_for_owner( + owner, + &pending.page_owner, + pending.source_document(), + ) { + return; + } record_subresource_network_request_handle( conn, owner, @@ -535,6 +546,13 @@ pub(crate) fn flush_post_subresource_auth_activity_for_owner_background_events( owner: &CommandOwnerScope, pending: &PendingSubresourceFetchAuthRequest, ) { + if !conn.subresource_document_is_current_for_owner( + owner, + &pending.page_owner, + pending.source_document(), + ) { + return; + } record_subresource_network_request_handle( conn, owner, @@ -618,7 +636,6 @@ pub(crate) struct PreparedSubresourceContinueWait { } pub(crate) struct SubresourceContinueCompletion { - kind: SubresourceContinuationKind, owner: CommandOwnerScope, binding: crate::conn::TargetRootDocumentProtocolAttachmentIdentity, request: PendingSubresourceFetchRequest, @@ -632,27 +649,35 @@ impl PreparedSubresourceContinueWait { request: PendingSubresourceFetchRequest, kind: SubresourceContinuationKind, ) -> Option { - let document = conn.target_root_document_lifecycle_identity_for_owner(owner)?; - let binding = - conn.target_root_document_protocol_attachment_identity_for_owner(owner, document)?; - let pending = conn - .loaded_page_mut_for_owner(owner) - .context(crate::conn::NoDocumentLoaded) - .and_then(|page| match kind { - SubresourceContinuationKind::Response(internal_id) => { - page.start_continue_pending_subresource_response(internal_id, None, None) + let document = request + .source_document() + .or_else(|| conn.target_root_document_lifecycle_identity_for_owner(owner))?; + let binding = conn.target_root_document_output_identity_for_owner(owner, document)?; + let native = match kind { + SubresourceContinuationKind::Response(internal_id) => { + moli_core::RendererPageCommand::ContinuePendingSubresourceResponse { + internal_id, + response_code: None, + response_headers: None, } - SubresourceContinuationKind::FailAuth(internal_id) => page - .start_fail_pending_subresource_auth( - internal_id, - "Fetch auth challenge has no DevTools request".to_owned(), - ), - }); + } + SubresourceContinuationKind::FailAuth(internal_id) => { + moli_core::RendererPageCommand::FailPendingSubresourceAuth { + internal_id, + error_text: "Fetch auth challenge has no DevTools request".to_owned(), + } + } + }; + let pending = conn.start_subresource_document_command( + owner, + request.page_owner()?, + request.document_command_target(), + native, + ); match pending { Ok(pending) => Some(Self { pending, completion: SubresourceContinueCompletion { - kind, owner: owner.clone(), binding, request, @@ -686,22 +711,10 @@ impl SubresourceContinueCompletion { completed: Result, ) -> Vec { let mut events = Vec::new(); - if !conn.target_root_document_protocol_attachment_identity_is_current(&self.binding) { + if !conn.target_root_document_output_identity_is_current(&self.binding) { return events; } - let result = completed.and_then(|completed| { - let page = conn - .loaded_page_mut_for_owner(&self.owner) - .context(crate::conn::NoDocumentLoaded)?; - match self.kind { - SubresourceContinuationKind::Response(_) => { - page.finish_continue_pending_subresource_response(completed) - } - SubresourceContinuationKind::FailAuth(_) => page - .finish_fail_pending_subresource_auth(completed) - .map(|_| ()), - } - }); + let result = completed.and_then(moli_core::page::CompletedPageCommand::into_unit_reply); if let Err(error) = result { tracing::debug!(%error, "automatic subresource response failed"); } diff --git a/moli-protocol/src/domains/fetch.rs b/moli-protocol/src/domains/fetch.rs index 234f5fdc7e..3a622ea0cb 100644 --- a/moli-protocol/src/domains/fetch.rs +++ b/moli-protocol/src/domains/fetch.rs @@ -777,7 +777,6 @@ fn complete_finite_fetch_command( PendingFetchCommandKind::ContinueRequest { state } => { commands::complete_continue_request_command( conn, - &owner, completed.completed.into_page_completion(), *state, &mut output, diff --git a/moli-protocol/src/domains/fetch/auth.rs b/moli-protocol/src/domains/fetch/auth.rs index fb22d12c43..eb4c325b40 100644 --- a/moli-protocol/src/domains/fetch/auth.rs +++ b/moli-protocol/src/domains/fetch/auth.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use moli_core::RendererPageCommand; use crate::automation::{ AutomationCommand, DevToolsAuthChallengeAction, DevToolsContinueWithAuthCommand, @@ -214,20 +215,26 @@ pub(super) fn start_devtools_continue_with_auth_command_for_pending( } else { None }; - let pending_page = match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => match command.action { - DevToolsAuthChallengeAction::Default => page - .start_fail_pending_subresource_auth( - pending.internal_id, - "Fetch auth challenge aborted".to_owned(), - ), - DevToolsAuthChallengeAction::Cancel => { - page.start_cancel_pending_subresource_auth(pending.internal_id) + let native = match command.action { + DevToolsAuthChallengeAction::Default => { + RendererPageCommand::FailPendingSubresourceAuth { + internal_id: pending.internal_id, + error_text: "Fetch auth challenge aborted".to_owned(), } - DevToolsAuthChallengeAction::ProvideCredentials => unreachable!(), - }, - Err(message) => Err(anyhow::Error::msg(message)), + } + DevToolsAuthChallengeAction::Cancel => { + RendererPageCommand::CancelPendingSubresourceAuth { + internal_id: pending.internal_id, + } + } + DevToolsAuthChallengeAction::ProvideCredentials => unreachable!(), }; + let pending_page = conn.start_subresource_document_command( + owner, + &pending.page_owner, + pending.command_target.as_ref(), + native, + ); let pending_page = match pending_page { Ok(pending_page) => pending_page, Err(error) => { @@ -303,12 +310,15 @@ pub(super) fn start_devtools_continue_with_auth_command_for_pending( ))); } }; - let pending_page = match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => page - .start_continue_pending_subresource_auth(pending.internal_id, auth) - .context("subresource auth continue failed"), - Err(message) => Err(anyhow::Error::msg(message)), - }; + let pending_page = conn.start_subresource_document_command( + owner, + &pending.page_owner, + pending.command_target.as_ref(), + RendererPageCommand::ContinuePendingSubresourceAuth { + internal_id: pending.internal_id, + auth, + }, + ); let pending_page = match pending_page { Ok(pending_page) => pending_page, Err(message) => { @@ -422,7 +432,13 @@ fn continued_subresource_request( pending: &PendingSubresourceFetchAuthRequest, ) -> PendingSubresourceFetchRequest { PendingSubresourceFetchRequest { - residence: crate::conn::PendingSubresourceFetchResidence::Page(pending.page_owner.clone()), + residence: match pending.command_target.clone() { + Some(command_target) => crate::conn::PendingSubresourceFetchResidence::Document { + page_owner: pending.page_owner.clone(), + command_target, + }, + None => crate::conn::PendingSubresourceFetchResidence::Page(pending.page_owner.clone()), + }, owner_session_id: pending.owner_session_id.clone(), action_session_id: pending.action_session_id.clone(), owner_kind: pending.owner_kind, @@ -551,21 +567,13 @@ pub(super) fn complete_continue_with_auth_command( pending, correlation, } => { - complete_subresource_auth_terminal( - conn, - owner, - completed, - *pending, - true, - correlation, - out, - ); + complete_subresource_auth_terminal(conn, owner, completed, *pending, correlation, out); } PendingContinueWithAuthState::SubresourceAuthFail { pending } => { - complete_subresource_auth_terminal(conn, owner, completed, *pending, false, None, out); + complete_subresource_auth_terminal(conn, owner, completed, *pending, None, out); } PendingContinueWithAuthState::SubresourceAuthContinue { correlation } => { - if let Err(error) = finish_continue_subresource_auth(conn, owner, completed) { + if let Err(error) = finish_continue_subresource_auth(completed) { correlation.rollback(conn); out.push_error(-32000, format!("{error:#}")); return; @@ -580,14 +588,10 @@ pub(super) fn complete_subresource_auth_terminal( owner: &CommandOwnerScope, completed: Option>, pending: crate::conn::PendingSubresourceFetchAuthRequest, - expose_challenged_response: bool, correlation: Option, out: &mut FetchCommandOutput, ) { let activity_session_id = pending.owner_session_id.as_deref().or(owner.session_id()); - let activity_owner = activity_session_id - .map(CommandOwnerScope::for_session) - .unwrap_or_else(|| owner.clone()); let Some(completed) = completed else { if let Some(correlation) = correlation { correlation.rollback(conn); @@ -605,21 +609,7 @@ pub(super) fn complete_subresource_auth_terminal( return; } }; - let result = match conn.loaded_page_mut_for_protocol_access_for_owner(&activity_owner) { - Ok(page) if expose_challenged_response => page - .finish_cancel_pending_subresource_auth(completion) - .map(|_| ()), - Ok(page) => page - .finish_fail_pending_subresource_auth(completion) - .map(|_| ()), - Err(message) => { - if let Some(correlation) = correlation { - correlation.rollback(conn); - } - out.push_error(-32000, message); - return; - } - }; + let result = completion.into_unit_reply(); if let Err(error) = result { if let Some(correlation) = correlation { correlation.rollback(conn); @@ -641,14 +631,10 @@ pub(super) fn complete_subresource_auth_terminal( out.extend_background_events(events); } -fn finish_continue_subresource_auth( - conn: &mut CdpConnection, - owner: &CommandOwnerScope, - completed: Option>, -) -> Result<()> { +fn finish_continue_subresource_auth(completed: Option>) -> Result<()> { let completion = completed.context("Missing renderer completion")??; - let page = conn.loaded_page_mut_for_protocol_access_for_owner(owner)?; - page.finish_continue_pending_subresource_auth(completion) + completion + .into_subresource_continue_outcome() .map(|_| ()) .context("subresource auth continue failed") } diff --git a/moli-protocol/src/domains/fetch/cleanup.rs b/moli-protocol/src/domains/fetch/cleanup.rs index 4877214da6..b38fae7424 100644 --- a/moli-protocol/src/domains/fetch/cleanup.rs +++ b/moli-protocol/src/domains/fetch/cleanup.rs @@ -213,8 +213,21 @@ impl PreparedFetchCleanup { output.plan.set_renderer_output_predecessor(predecessor); } output.error = completed - .and_then(|completed| completed.into_unit_page_command_turn().map(drop)) - .err(); + .and_then(|completed| completed.into_unit_reply()) + .err() + // Cancellation may retire the original native Page before + // its already-admitted policy update runs. It has no policy + // left to clear; failures on a live Page remain errors. + .filter(|error| { + !matches!( + error.downcast_ref::(), + Some( + moli_core::RendererPageAccessError::Missing { .. } + | moli_core::RendererPageAccessError::Retiring { .. } + | moli_core::RendererPageAccessError::DocumentReplaced + ) + ) + }); } output } @@ -282,7 +295,7 @@ enum CleanupSubresource { struct PreparedSubresourceFailure { pending: PendingPageCommand, owner: CommandOwnerScope, - binding: TargetRootDocumentProtocolAttachmentIdentity, + binding: Option, resource: CleanupSubresource, } @@ -292,35 +305,41 @@ fn admit_subresource_failure( resource: CleanupSubresource, reason: &str, ) -> Option { - let current = match &resource { - CleanupSubresource::Request(pending) => { - conn.pending_subresource_fetch_request_residence_is_current(pending) - } - CleanupSubresource::Auth(pending) => { - conn.target_page_residence_identity_is_current(&pending.page_owner) - } - CleanupSubresource::Response(pending) => { - conn.target_page_residence_identity_is_current(&pending.page_owner) + use moli_core::RendererPageCommand; + let (target, command) = match &resource { + CleanupSubresource::Request(request) => { + if let Some(continuation) = request.detached_parser_script_fetch_continuation() { + continuation.fail(reason.to_owned()); + return None; + } + ( + request.document_command_target(), + RendererPageCommand::FailPendingSubresourceFetch { + internal_id: request.internal_id, + error_text: reason.to_owned(), + }, + ) } + CleanupSubresource::Auth(request) => ( + request.command_target.as_ref(), + RendererPageCommand::FailPendingSubresourceAuth { + internal_id: request.internal_id, + error_text: reason.to_owned(), + }, + ), + CleanupSubresource::Response(request) => ( + request.command_target.as_ref(), + RendererPageCommand::FailPendingSubresourceResponse { + internal_id: request.internal_id, + error_text: reason.to_owned(), + }, + ), }; - if !current { - return None; - } - let root = conn.target_root_document_lifecycle_identity_for_owner(owner)?; - let binding = conn.target_root_document_protocol_attachment_identity_for_owner(owner, root)?; - let page = conn.loaded_page_mut_for_owner(owner)?; - let pending = match &resource { - CleanupSubresource::Request(pending) => { - page.start_fail_pending_subresource_fetch(pending.internal_id, reason.to_owned()) - } - CleanupSubresource::Auth(pending) => { - page.start_fail_pending_subresource_auth(pending.internal_id, reason.to_owned()) - } - CleanupSubresource::Response(pending) => { - page.start_fail_pending_subresource_response(pending.internal_id, reason.to_owned()) - } - } - .ok()?; + let target = target?; + // Cleanup owns this exact request even after session Closing or Page + // retirement. The native Document guard prevents touching a replacement. + let pending = target.start(command).ok()?; + let binding = conn.target_root_document_output_identity_for_owner(owner, target.document()); Some(PreparedSubresourceFailure { pending, owner: owner.clone(), @@ -346,7 +365,7 @@ impl PreparedSubresourceFailure { } pub(crate) struct CompletedSubresourceFailure { - binding: TargetRootDocumentProtocolAttachmentIdentity, + binding: Option, owner: CommandOwnerScope, resource: CleanupSubresource, completed: moli_core::page::CompletedPageCommand, @@ -361,14 +380,17 @@ impl CompletedSubresourceFailure { completed, } = self; let predecessor = completed.renderer_output_predecessor(); - completed.into_unit_page_command_turn().ok()?; + completed.into_unit_reply().ok()?; let mut plan = CommandOutputPlan::default(); if let Some(predecessor) = predecessor { plan.set_renderer_output_predecessor(predecessor); } // A unit ACK belongs to the admitted native turn, never to a replacement // Page's mutable snapshot or network correlation table. - if !conn.target_root_document_protocol_attachment_identity_is_current(&binding) { + if binding + .as_ref() + .is_none_or(|binding| !conn.target_root_document_output_identity_is_current(binding)) + { return Some(plan); } let mut events = Vec::new(); diff --git a/moli-protocol/src/domains/fetch/commands.rs b/moli-protocol/src/domains/fetch/commands.rs index ee3cfffca2..02ef59c9cf 100644 --- a/moli-protocol/src/domains/fetch/commands.rs +++ b/moli-protocol/src/domains/fetch/commands.rs @@ -14,6 +14,7 @@ use crate::conn::{ use crate::domains::command_output::CommandOutputPlan; use crate::domains::{activity, network, page}; use anyhow::Context as _; +use moli_core::RendererPageCommand; use moli_core::page::{ CompletedPageCommand, RendererSyntheticResponseBody, SubresourceResourceType, }; @@ -326,20 +327,22 @@ fn start_devtools_continue_intercepted_request_command( )); } }; - let pending_page = match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => page - .start_continue_pending_subresource_fetch( - pending.internal_id, - parsed_url, - method, - post_data, - headers, - intercept_response, - handle_auth_requests, - ) - .context("subresource fetch continue failed"), - Err(message) => Err(anyhow::Error::msg(message)), - }; + let pending_page = conn.start_subresource_document_command( + owner, + pending + .page_owner() + .expect("native request has Page residence"), + pending.document_command_target(), + RendererPageCommand::ContinuePendingSubresourceFetch { + internal_id: pending.internal_id, + url: parsed_url, + method, + body: post_data, + headers, + intercept_response, + handle_auth_requests, + }, + ); let pending_page = match pending_page { Ok(pending_page) => pending_page, Err(message) => { @@ -434,14 +437,13 @@ fn start_devtools_continue_intercepted_request_command( pub(super) fn complete_continue_request_command( conn: &mut CdpConnection, - owner: &CommandOwnerScope, completed: Option>, state: PendingContinueRequestState, out: &mut FetchCommandOutput, ) { match state { PendingContinueRequestState::SubresourceFetch { correlation } => { - if let Err(error) = finish_continue_subresource_request(conn, owner, completed) { + if let Err(error) = finish_continue_subresource_request(completed) { correlation.rollback(conn); out.push_error(-32000, format!("{error:#}")); return; @@ -452,13 +454,11 @@ pub(super) fn complete_continue_request_command( } fn finish_continue_subresource_request( - conn: &mut CdpConnection, - owner: &CommandOwnerScope, completed: Option>, ) -> Result<()> { let completion = completed.context("Missing renderer completion")??; - let page = conn.loaded_page_mut_for_protocol_access_for_owner(owner)?; - page.finish_continue_pending_subresource_fetch(completion) + completion + .into_subresource_continue_outcome() .map(|_| ()) .context("subresource fetch continue failed") } @@ -579,29 +579,25 @@ fn start_devtools_fail_intercepted_request_command( plan.extend_background_events(events); return FetchCommandTaskStep::Complete(plan); } - let page = match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => page, - Err(error) if error.is::() => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - } - Err(message) => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - format!("{message:#}"), - )); - } - }; - let pending_page = match page - .start_fail_pending_subresource_fetch(pending.internal_id, error_text.clone()) - { + let pending_page = match conn.start_subresource_document_command( + owner, + pending + .page_owner() + .expect("native request has Page residence"), + pending.document_command_target(), + RendererPageCommand::FailPendingSubresourceFetch { + internal_id: pending.internal_id, + error_text: error_text.clone(), + }, + ) { Ok(pending_page) => pending_page, Err(error) => { + conn.register_pending_subresource_fetch_request_for_owner( + owner, request_id, pending, + ); return FetchCommandTaskStep::Complete(CommandOutputPlan::error( -32000, - format!("subresource fetch fail failed: {error}"), + format!("subresource fetch fail failed: {error:#}"), )); } }; @@ -623,29 +619,23 @@ fn start_devtools_fail_intercepted_request_command( action_session_id, &request_id, ) { - let page = match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => page, - Err(error) if error.is::() => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - } - Err(message) => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - format!("{message:#}"), - )); - } - }; - let pending_page = match page - .start_fail_pending_subresource_response(pending.internal_id, error_text.clone()) - { + let pending_page = match conn.start_subresource_document_command( + owner, + &pending.page_owner, + pending.command_target.as_ref(), + RendererPageCommand::FailPendingSubresourceResponse { + internal_id: pending.internal_id, + error_text: error_text.clone(), + }, + ) { Ok(pending_page) => pending_page, Err(error) => { + conn.register_pending_subresource_fetch_response_request_for_owner( + owner, request_id, pending, + ); return FetchCommandTaskStep::Complete(CommandOutputPlan::error( -32000, - format!("subresource response fail failed: {error}"), + format!("subresource response fail failed: {error:#}"), )); } }; @@ -705,21 +695,9 @@ pub(super) fn complete_fail_request_command( return; } }; - match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => { - if let Err(error) = page.finish_fail_pending_subresource_fetch(completion) { - out.push_error(-32000, format!("subresource fetch fail failed: {error}")); - return; - } - } - Err(error) if error.is::() => { - out.push_error(-32000, "NoDocumentLoaded"); - return; - } - Err(message) => { - out.push_error(-32000, message); - return; - } + if let Err(error) = completion.into_unit_reply() { + out.push_error(-32000, format!("{error:#}")); + return; } emit_devtools_empty_success(out); let mut events = Vec::new(); @@ -743,24 +721,9 @@ pub(super) fn complete_fail_request_command( return; } }; - match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => { - if let Err(error) = page.finish_fail_pending_subresource_response(completion) { - out.push_error( - -32000, - format!("subresource response fail failed: {error}"), - ); - return; - } - } - Err(error) if error.is::() => { - out.push_error(-32000, "NoDocumentLoaded"); - return; - } - Err(message) => { - out.push_error(-32000, message); - return; - } + if let Err(error) = completion.into_unit_reply() { + out.push_error(-32000, format!("{error:#}")); + return; } emit_devtools_empty_success(out); let mut events = Vec::new(); @@ -912,32 +875,27 @@ fn start_devtools_fulfill_intercepted_request_command( let websocket_socket_id = pending.websocket_socket_id; let register_synthetic_websocket = pending.resource_type == SubresourceResourceType::WebSocket && response_code == 101; - let page = match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => page, - Err(error) if error.is::() => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - } - Err(message) => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - format!("{message:#}"), - )); - } - }; - let pending_page = match page.start_fulfill_pending_subresource_fetch( - pending.internal_id, - response_code, - response_headers.clone(), - decoded_body.unwrap_or_else(RendererSyntheticResponseBody::empty), + let pending_page = match conn.start_subresource_document_command( + owner, + pending + .page_owner() + .expect("native request has Page residence"), + pending.document_command_target(), + RendererPageCommand::FulfillPendingSubresourceFetch { + internal_id: pending.internal_id, + response_code, + response_headers: response_headers.clone(), + response_body: decoded_body.unwrap_or_else(RendererSyntheticResponseBody::empty), + }, ) { Ok(pending_page) => pending_page, Err(error) => { + conn.register_pending_subresource_fetch_request_for_owner( + owner, request_id, pending, + ); return FetchCommandTaskStep::Complete(CommandOutputPlan::error( -32000, - format!("subresource fetch fulfill failed: {error}"), + format!("subresource fetch fulfill failed: {error:#}"), )); } }; @@ -963,32 +921,25 @@ fn start_devtools_fulfill_intercepted_request_command( action_session_id, &request_id, ) { - let page = match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => page, - Err(error) if error.is::() => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - } - Err(message) => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - format!("{message:#}"), - )); - } - }; - let pending_page = match page.start_fulfill_pending_subresource_response( - pending.internal_id, - response_code, - response_headers.clone(), - decoded_body.unwrap_or_else(RendererSyntheticResponseBody::empty), + let pending_page = match conn.start_subresource_document_command( + owner, + &pending.page_owner, + pending.command_target.as_ref(), + RendererPageCommand::FulfillPendingSubresourceResponse { + internal_id: pending.internal_id, + response_code, + response_headers: response_headers.clone(), + response_body: decoded_body.unwrap_or_else(RendererSyntheticResponseBody::empty), + }, ) { Ok(pending_page) => pending_page, Err(error) => { + conn.register_pending_subresource_fetch_response_request_for_owner( + owner, request_id, pending, + ); return FetchCommandTaskStep::Complete(CommandOutputPlan::error( -32000, - format!("subresource response fulfill failed: {error}"), + format!("subresource response fulfill failed: {error:#}"), )); } }; @@ -1066,26 +1017,20 @@ pub(super) fn complete_fulfill_request_command( return; } }; - match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => { - if let Err(error) = page.finish_fulfill_pending_subresource_fetch(completion) { - out.push_error( - -32000, - format!("subresource fetch fulfill failed: {error}"), - ); - return; - } - } - Err(error) if error.is::() => { - out.push_error(-32000, "NoDocumentLoaded"); - return; - } - Err(message) => { - out.push_error(-32000, message); - return; - } + if let Err(error) = completion.into_unit_reply() { + out.push_error(-32000, format!("{error:#}")); + return; } - if register_synthetic_websocket && let Some(socket_id) = websocket_socket_id { + if register_synthetic_websocket + && pending.page_owner().is_some_and(|page| { + conn.subresource_document_is_current_for_owner( + owner, + page, + pending.source_document(), + ) + }) + && let Some(socket_id) = websocket_socket_id + { conn.register_synthetic_websocket_request_for_owner( owner, request_id, @@ -1115,25 +1060,9 @@ pub(super) fn complete_fulfill_request_command( return; } }; - match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => { - if let Err(error) = page.finish_fulfill_pending_subresource_response(completion) - { - out.push_error( - -32000, - format!("subresource response fulfill failed: {error}"), - ); - return; - } - } - Err(error) if error.is::() => { - out.push_error(-32000, "NoDocumentLoaded"); - return; - } - Err(message) => { - out.push_error(-32000, message); - return; - } + if let Err(error) = completion.into_unit_reply() { + out.push_error(-32000, format!("{error:#}")); + return; } emit_devtools_empty_success(out); let mut events = Vec::new(); @@ -1556,32 +1485,24 @@ fn start_devtools_continue_intercepted_response_command( } else { response_headers }; - let pending_internal_id = pending.internal_id; - let page = match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => page, - Err(error) if error.is::() => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - "NoDocumentLoaded", - )); - } - Err(message) => { - return FetchCommandTaskStep::Complete(CommandOutputPlan::error( - -32000, - format!("{message:#}"), - )); - } - }; - let pending_page = match page.start_continue_pending_subresource_response( - pending_internal_id, - continue_response_code, - continue_response_headers, + let pending_page = match conn.start_subresource_document_command( + owner, + &pending.page_owner, + pending.command_target.as_ref(), + RendererPageCommand::ContinuePendingSubresourceResponse { + internal_id: pending.internal_id, + response_code: continue_response_code, + response_headers: continue_response_headers, + }, ) { Ok(pending_page) => pending_page, Err(error) => { + conn.register_pending_subresource_fetch_response_request_for_owner( + owner, request_id, pending, + ); return FetchCommandTaskStep::Complete(CommandOutputPlan::error( -32000, - format!("subresource response continue failed: {error}"), + format!("subresource response continue failed: {error:#}"), )); } }; @@ -1658,26 +1579,9 @@ pub(super) fn complete_continue_response_command( return; } }; - match conn.loaded_page_mut_for_protocol_access_for_owner(owner) { - Ok(page) => { - if let Err(error) = - page.finish_continue_pending_subresource_response(completion) - { - out.push_error( - -32000, - format!("subresource response continue failed: {error}"), - ); - return; - } - } - Err(error) if error.is::() => { - out.push_error(-32000, "NoDocumentLoaded"); - return; - } - Err(message) => { - out.push_error(-32000, message); - return; - } + if let Err(error) = completion.into_unit_reply() { + out.push_error(-32000, format!("{error:#}")); + return; } emit_devtools_empty_success(out); let mut events = Vec::new(); @@ -2075,6 +1979,7 @@ mod protocol_neutral_tests { .target_page_residence_identity_for_session(Some("SID-primary")) .expect("test target should expose a Page residence identity"); let pending = PendingSubresourceFetchResponseRequest { + command_target: None, page_owner, owner_session_id: Some("SID-primary".to_owned()), action_session_id: Some("SID-primary".to_owned()), diff --git a/moli-protocol/src/domains/fetch/subresource.rs b/moli-protocol/src/domains/fetch/subresource.rs index cab96d5026..e4f2a7e9d7 100644 --- a/moli-protocol/src/domains/fetch/subresource.rs +++ b/moli-protocol/src/domains/fetch/subresource.rs @@ -59,17 +59,38 @@ pub(crate) fn subresource_fetch_pause_prepared_outputs_for_renderer_record( owner: &CommandOwnerScope, source_document: moli_core::RendererDocumentLifecycleIdentity, info: moli_core::page::PendingSubresourceFetchInfo, + page_source: Option<( + crate::conn::TargetPageResidenceIdentity, + crate::conn::RendererPageResidenceIdentity, + )>, ) -> network::NetworkPreparedOutputs { let Some(source) = conn.target_root_document_output_identity_for_owner(owner, source_document) else { return network::NetworkPreparedOutputs::default(); }; + let Some((page_owner, renderer_page)) = page_source else { + return network::NetworkPreparedOutputs::default(); + }; + if source.attachment().page_owner() != &page_owner { + return network::NetworkPreparedOutputs::default(); + } + let Ok(command_target) = conn.capture_document_output_command_target( + owner, + &page_owner, + renderer_page, + source_document, + ) else { + return network::NetworkPreparedOutputs::default(); + }; prepare_subresource_fetch_pause_sources( conn, owner, None, None, - Some(source.attachment().page_owner().clone()), + Some(crate::conn::PendingSubresourceFetchResidence::Document { + page_owner, + command_target, + }), vec![PendingSubresourceFetchPauseSource { info, detached_parser_script_fetch_continuation: None, @@ -108,13 +129,16 @@ fn prepare_subresource_fetch_pause_sources( owner: &CommandOwnerScope, frame_id: Option<&str>, document_url: Option<&Url>, - page_owner: Option, + residence: Option, sources: Vec, ) -> network::NetworkPreparedOutputs { let mut outputs = Vec::new(); let mut automatic = Vec::new(); let session_id = owner.session_id(); - let page_owner = page_owner.or_else(|| conn.target_page_residence_identity_for_owner(owner)); + let residence = residence.or_else(|| { + conn.target_page_residence_identity_for_owner(owner) + .map(crate::conn::PendingSubresourceFetchResidence::Page) + }); let Some((fetch_snapshot, frame_id)) = (|| { let target_frame_id = conn .target_session_owner_frame_tree_identity_for_owner(owner) @@ -157,11 +181,11 @@ fn prepare_subresource_fetch_pause_sources( continuation.continue_request(None); continue; } - let Some(page_owner) = page_owner.clone() else { + let Some(residence) = residence.clone() else { continue; }; let pending = PendingSubresourceFetchRequest { - residence: crate::conn::PendingSubresourceFetchResidence::Page(page_owner.clone()), + residence, owner_session_id: None, action_session_id: None, owner_kind: PendingSubresourceFetchOwnerKind::NetworkOrBidi, @@ -273,10 +297,10 @@ fn prepare_subresource_fetch_pause_sources( crate::conn::PendingSubresourceFetchResidence::DetachedParserScript(continuation) } None => { - let Some(page_owner) = page_owner.clone() else { + let Some(residence) = residence.clone() else { continue; }; - crate::conn::PendingSubresourceFetchResidence::Page(page_owner) + residence } }; let pending = PendingSubresourceFetchRequest { diff --git a/moli-protocol/src/domains/fetch/subresource_continuation.rs b/moli-protocol/src/domains/fetch/subresource_continuation.rs index 99459baa26..2d63864aa3 100644 --- a/moli-protocol/src/domains/fetch/subresource_continuation.rs +++ b/moli-protocol/src/domains/fetch/subresource_continuation.rs @@ -1,4 +1,4 @@ -use anyhow::{Context as _, Result}; +use anyhow::Result; use moli_core::page::{CompletedPageCommand, PendingPageCommand}; use super::state::PreparedSubresourceCorrelation; @@ -17,7 +17,6 @@ pub(super) enum SubresourceResponseInterception { pub(crate) struct PreparedSubresourceAutoContinue { pending: PendingPageCommand, correlation: PreparedSubresourceCorrelation, - owner: CommandOwnerScope, binding: TargetRootDocumentProtocolAttachmentIdentity, } @@ -30,36 +29,35 @@ impl PreparedSubresourceAutoContinue { handle_auth_requests: bool, response: SubresourceResponseInterception, ) -> Option { - let document = conn.target_root_document_lifecycle_identity_for_owner(owner)?; - let binding = - conn.target_root_document_protocol_attachment_identity_for_owner(owner, document)?; + let target = request.document_command_target()?.clone(); + let document = target.document(); + let binding = conn.target_root_document_output_identity_for_owner(owner, document)?; let intercept_response = !matches!(response, SubresourceResponseInterception::None); let internal_id = request.internal_id; + let page_owner = request.page_owner()?.clone(); // The renderer may publish a continuation before its command reply is // consumed. Freeze its original correlation before native dispatch. let correlation = PreparedSubresourceCorrelation::prepare_automatic( conn, owner, request_id, request, response, )?; - let pending = conn - .loaded_page_mut_for_owner(owner) - .context(crate::conn::NoDocumentLoaded) - .and_then(|page| { - page.start_continue_pending_subresource_fetch( - internal_id, - None, - None, - None, - None, - intercept_response, - handle_auth_requests, - ) - .context("subresource fetch continue failed") - }); + let pending = conn.start_subresource_document_command( + owner, + &page_owner, + Some(&target), + moli_core::RendererPageCommand::ContinuePendingSubresourceFetch { + internal_id, + url: None, + method: None, + body: None, + headers: None, + intercept_response, + handle_auth_requests, + }, + ); match pending { Ok(pending) => Some(Self { pending, correlation, - owner: owner.clone(), binding, }), Err(error) => { @@ -74,7 +72,6 @@ impl PreparedSubresourceAutoContinue { let completed = self.pending.wait().await; let completion = SubresourceAutoContinueCompletion { correlation: self.correlation, - owner: self.owner, binding: self.binding, }; driver.with_state(move |state| { @@ -94,22 +91,16 @@ impl std::fmt::Debug for PreparedSubresourceAutoContinue { pub(crate) struct SubresourceAutoContinueCompletion { correlation: PreparedSubresourceCorrelation, - owner: CommandOwnerScope, binding: TargetRootDocumentProtocolAttachmentIdentity, } impl SubresourceAutoContinueCompletion { pub(crate) fn finish(self, conn: &mut CdpConnection, completed: Result) { - if !conn.target_root_document_protocol_attachment_identity_is_current(&self.binding) { + if !conn.target_root_document_output_identity_is_current(&self.binding) { self.correlation.rollback(conn); return; } - let result = completed.and_then(|completed| { - conn.loaded_page_mut_for_owner(&self.owner) - .context(crate::conn::NoDocumentLoaded)? - .finish_continue_pending_subresource_fetch(completed) - .context("subresource fetch continue failed") - }); + let result = completed.and_then(CompletedPageCommand::into_subresource_continue_outcome); if let Err(error) = result { self.correlation.rollback(conn); tracing::debug!(%error, "automatic subresource continuation failed"); diff --git a/moli-protocol/src/domains/fetch/tests/basics.rs b/moli-protocol/src/domains/fetch/tests/basics.rs index 35d33318b4..ba13e12fbd 100644 --- a/moli-protocol/src/domains/fetch/tests/basics.rs +++ b/moli-protocol/src/domains/fetch/tests/basics.rs @@ -50,6 +50,7 @@ fn pending_subresource_fetch_auth( owner_session_id: &str, ) -> PendingSubresourceFetchAuthRequest { PendingSubresourceFetchAuthRequest { + command_target: None, page_owner: crate::conn::TargetPageResidenceIdentity::new_for_test( "BID-session-fetch".to_owned(), Some("TID-session-fetch".to_owned()), diff --git a/moli-protocol/src/domains/fetch/tests/command_correlation.rs b/moli-protocol/src/domains/fetch/tests/command_correlation.rs index aebf0f7e3d..6462076302 100644 --- a/moli-protocol/src/domains/fetch/tests/command_correlation.rs +++ b/moli-protocol/src/domains/fetch/tests/command_correlation.rs @@ -8,19 +8,33 @@ use moli_core::page::SubresourceResourceType; async fn context_with_loaded_fetch_page() -> TestContext { let mut ctx = TestContext::new(); - let page = ctx - .build_fixture_page("data:text/html,fetch correlation") - .await - .expect("fetch correlation page should load"); - let mut browser_context = attached_browser_context(); - browser_context - .active_page_target_mut() - .runtime_slot - .replace_loaded_page(Some(page)); - ctx.with_fixture(|conn| conn.install_browser_context_fixture_for_test(browser_context)); + ctx.with_fixture(|conn| { + conn.install_browser_context_fixture_for_test(attached_browser_context()) + }); + ctx.install_buffered_navigation_fixture_for_session_owner( + Url::parse("https://example.test/page").unwrap(), + "fetch correlation".to_owned(), + Some("SID-1"), + ) + .await; ctx } +fn command_target( + conn: &mut crate::conn::CdpConnection, + page: &crate::conn::TargetPageResidenceIdentity, +) -> moli_core::page::PageDocumentCommandTarget { + let owner = crate::conn::CommandOwnerScope::for_session("SID-1"); + let renderer = conn + .renderer_page_residence_identity_for_owner(&owner) + .unwrap(); + let document = conn + .target_root_document_lifecycle_identity_for_owner(&owner) + .unwrap(); + conn.capture_document_output_command_target(&owner, page, renderer, document) + .unwrap() +} + fn pending_request( page_owner: &crate::conn::TargetPageResidenceIdentity, internal_id: u64, @@ -51,6 +65,7 @@ fn pending_auth( .expect("test request should belong to an installed Page") .clone(); PendingSubresourceFetchAuthRequest { + command_target: None, page_owner, owner_session_id: pending.owner_session_id, action_session_id: pending.action_session_id, @@ -88,6 +103,7 @@ fn pending_response( .expect("test request should belong to an installed Page") .clone(); PendingSubresourceFetchResponseRequest { + command_target: None, page_owner, owner_session_id: pending.owner_session_id, action_session_id: pending.action_session_id, @@ -247,11 +263,16 @@ async fn continue_request_registers_correlation_before_renderer_completion() { .expect("test Page residence should exist") }); ctx.with_fixture(|conn| { + let mut pending = pending_request(&page_owner, 61); + pending.residence = crate::conn::PendingSubresourceFetchResidence::Document { + page_owner: page_owner.clone(), + command_target: command_target(conn, &page_owner), + }; assert!({ conn.register_pending_subresource_fetch_request_for_session_owner( Some("SID-1"), "INT-61".to_owned(), - pending_request(&page_owner, 61), + pending, ) }) }); @@ -657,11 +678,13 @@ async fn continue_with_auth_registers_retry_correlation_before_renderer_completi .expect("test Page residence should exist") }); ctx.with_fixture(|conn| { + let mut pending = pending_auth(&page_owner, 62); + pending.command_target = Some(command_target(conn, &page_owner)); assert!({ conn.register_pending_subresource_fetch_auth_request_for_session_owner( Some("SID-1"), "INT-62".to_owned(), - pending_auth(&page_owner, 62), + pending, ) }) }); diff --git a/moli-protocol/src/domains/input.rs b/moli-protocol/src/domains/input.rs index f893d5dd75..2fa31e4755 100644 --- a/moli-protocol/src/domains/input.rs +++ b/moli-protocol/src/domains/input.rs @@ -1512,16 +1512,26 @@ impl InputPreparedOutputs { } pub(crate) fn from_renderer_file_chooser_activation( - conn: &CdpConnection, + conn: &mut CdpConnection, owner: &CommandOwnerScope, + page_source: Option<( + TargetPageResidenceIdentity, + crate::conn::RendererPageResidenceIdentity, + )>, activation: RendererPendingFileChooserActivation, ) -> Self { - let Some(page_owner) = conn.target_page_residence_identity_for_owner(owner) else { + let Some((page_owner, renderer_page)) = page_source else { return Self::default(); }; - let Some(activation) = file_chooser::PreparedFileChooserActivation::capture( - conn, owner, page_owner, activation, - ) else { + let Some(activation) = + file_chooser::PreparedFileChooserActivation::capture_for_renderer_record( + conn, + owner, + page_owner, + renderer_page, + activation, + ) + else { return Self::default(); }; Self { @@ -2143,7 +2153,7 @@ mod producer_tests { let mut out: Vec = Vec::new(); let prepared = super::file_chooser::PreparedFileChooserActivation::capture( - &conn, + &mut conn, &CommandOwnerScope::for_session("SID-typed"), owner, renderer_file_chooser_for_test(source_document, Some("FRAME-typed"), 77, false), @@ -2201,7 +2211,7 @@ mod producer_tests { let owner = page_residence_identity_for_test(&conn, "SID-root-capture"); let prepared = super::file_chooser::PreparedFileChooserActivation::capture( - &conn, + &mut conn, &CommandOwnerScope::for_session("SID-root-capture"), owner, renderer_file_chooser_for_test(source_document, None, 41, false), diff --git a/moli-protocol/src/domains/input/file_chooser.rs b/moli-protocol/src/domains/input/file_chooser.rs index 3a2812f270..ff23efd287 100644 --- a/moli-protocol/src/domains/input/file_chooser.rs +++ b/moli-protocol/src/domains/input/file_chooser.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context as _, Result}; use moli_core::page::{ CompletedPageCommand, PendingPageCommand, RendererDocumentLifecycleIdentity, @@ -29,23 +29,37 @@ pub(crate) struct PreparedFileChooserActivation { source_frame_id: String, backend_node_id: u32, allow_multiple: bool, + command_target: Option, } impl PreparedFileChooserActivation { pub(crate) fn capture( - conn: &CdpConnection, + conn: &mut CdpConnection, owner: &CommandOwnerScope, page_owner: TargetPageResidenceIdentity, activation: RendererPendingFileChooserActivation, ) -> Option { - if !conn.target_page_residence_identity_is_current(&page_owner) - || conn - .target_page_residence_identity_for_owner(owner) - .as_ref() - != Some(&page_owner) - { - return None; - } + let renderer_page = conn.renderer_page_residence_identity_for_owner(owner); + Self::capture_from_source(conn, owner, page_owner, renderer_page, activation) + } + + pub(crate) fn capture_for_renderer_record( + conn: &mut CdpConnection, + owner: &CommandOwnerScope, + page_owner: TargetPageResidenceIdentity, + renderer_page: crate::conn::RendererPageResidenceIdentity, + activation: RendererPendingFileChooserActivation, + ) -> Option { + Self::capture_from_source(conn, owner, page_owner, Some(renderer_page), activation) + } + + fn capture_from_source( + conn: &mut CdpConnection, + owner: &CommandOwnerScope, + page_owner: TargetPageResidenceIdentity, + renderer_page: Option, + activation: RendererPendingFileChooserActivation, + ) -> Option { let source_document = activation.source_document(); let source_frame_id = activation .source_frame_id() @@ -54,7 +68,17 @@ impl PreparedFileChooserActivation { conn.target_session_owner_frame_tree_identity_for_owner(owner) .map(|(frame_id, _, _, _)| frame_id) })?; + let command_target = renderer_page.and_then(|renderer_page| { + conn.capture_document_output_command_target( + owner, + &page_owner, + renderer_page, + source_document, + ) + .ok() + }); Some(Self { + command_target, page_owner, source_document, source_frame_id, @@ -75,6 +99,7 @@ impl PreparedFileChooserActivation { .unwrap_or(root_frame_id) .to_owned(); Self { + command_target: None, page_owner, source_document, source_frame_id, @@ -185,12 +210,7 @@ impl PreparedFileChooserActivation { conn: &mut CdpConnection, owner: &CommandOwnerScope, ) -> Option> { - if !activation_is_current(conn, self) - || conn - .target_page_residence_identity_for_owner(owner) - .as_ref() - != Some(&self.page_owner) - { + if !activation_is_current(conn, self) || !owner_matches_activation(conn, owner, self) { trace_stale_activation(owner.session_id(), self); return None; } @@ -204,12 +224,19 @@ impl PreparedFileChooserActivation { return None; } let inspector = conn.target_renderer_runtime_inspector_session_id_for_owner(owner); - Some(self.page(conn).and_then(|page| { - page.start_document_bidi_node_shared_id_for_backend_node_id( - inspector, - self.backend_node_id, - ) - })) + Some( + self.command_target + .as_ref() + .context(NoDocumentLoaded) + .and_then(|target| { + target.start( + moli_core::RendererPageCommand::DocumentBidiNodeSharedIdForBackendNodeId { + inspector_session_id: inspector, + backend_node_id: self.backend_node_id, + }, + ) + }), + ) } pub(crate) fn finish_query( @@ -217,8 +244,10 @@ impl PreparedFileChooserActivation { conn: &mut CdpConnection, completed: CompletedPageCommand, ) -> Result { - self.page(conn)? - .finish_document_bidi_node_shared_id_for_backend_node_id(completed) + if !activation_is_current(conn, self) { + return Err(NoDocumentLoaded.into()); + } + completed.into_document_bidi_node_shared_id() } pub(crate) fn start_binding( @@ -227,19 +256,21 @@ impl PreparedFileChooserActivation { owner: &CommandOwnerScope, shared_id: String, ) -> Result { - if conn - .target_page_residence_identity_for_owner(owner) - .as_ref() - != Some(&self.page_owner) - { - return Err(anyhow::Error::new(NoDocumentLoaded)); + if !activation_is_current(conn, self) || !owner_matches_activation(conn, owner, self) { + return Err(NoDocumentLoaded.into()); } - let inspector = conn.target_renderer_runtime_inspector_session_id_for_owner(owner); - self.page(conn)?.start_register_document_bidi_node_binding( - inspector, - shared_id, - self.backend_node_id, - ) + let inspector_session_id = + conn.target_renderer_runtime_inspector_session_id_for_owner(owner); + self.command_target + .as_ref() + .context(NoDocumentLoaded)? + .start( + moli_core::RendererPageCommand::RegisterDocumentBidiNodeBinding { + inspector_session_id, + shared_id, + backend_node_id: self.backend_node_id, + }, + ) } pub(crate) fn finish_binding( @@ -247,17 +278,10 @@ impl PreparedFileChooserActivation { conn: &mut CdpConnection, completed: CompletedPageCommand, ) -> Result<()> { - self.page(conn)? - .finish_register_document_bidi_node_binding(completed) - } - - fn page<'a>(&self, conn: &'a mut CdpConnection) -> Result<&'a mut moli_core::page::Page> { if !activation_is_current(conn, self) { - return Err(anyhow::Error::new(NoDocumentLoaded)); + return Err(NoDocumentLoaded.into()); } - conn.loaded_page_mut_for_protocol_access_for_owner(&CommandOwnerScope::for_page_residence( - &self.page_owner, - )) + completed.into_document_bidi_node_binding() } pub(crate) fn finish_event( @@ -267,10 +291,8 @@ impl PreparedFileChooserActivation { shared_id: DevToolsRemoteHandleId, ) -> Option { if !activation_is_current(conn, &self) - || session_id.is_some_and(|session_id| { - conn.target_page_residence_identity_for_session(Some(session_id)) - .as_ref() - != Some(&self.page_owner) + || session_id.is_some_and(|session| { + !owner_matches_activation(conn, &CommandOwnerScope::for_session(session), &self) }) { trace_stale_activation(session_id, &self); @@ -291,7 +313,19 @@ impl PreparedFileChooserActivation { } fn activation_is_current(conn: &CdpConnection, activation: &PreparedFileChooserActivation) -> bool { - conn.target_page_residence_identity_is_current(&activation.page_owner) + conn.target_page_output_residence_is_current(&activation.page_owner) +} + +fn owner_matches_activation( + conn: &CdpConnection, + owner: &CommandOwnerScope, + activation: &PreparedFileChooserActivation, +) -> bool { + conn.target_owner_identity_for_owner(owner) + .is_some_and(|(context, target)| { + context == activation.page_owner.browser_context_id() + && target.as_deref() == activation.page_owner.target_id() + }) } fn trace_stale_activation(session_id: Option<&str>, activation: &PreparedFileChooserActivation) { diff --git a/moli-protocol/src/domains/page.rs b/moli-protocol/src/domains/page.rs index 1d83e5b50f..fb450363c5 100644 --- a/moli-protocol/src/domains/page.rs +++ b/moli-protocol/src/domains/page.rs @@ -128,7 +128,7 @@ pub(crate) use lifecycle::{ }; pub(in crate::domains) use main_document_commit::{ MainDocumentCommitPreparedOutput, append_renderer_main_document_commit_to_output_sink, - project_main_document_commit, + main_document_commit_background_events, project_main_document_commit, }; pub use navigation::BackgroundNavigationCompletion; #[cfg(test)] diff --git a/moli-protocol/src/domains/page/javascript_dialog.rs b/moli-protocol/src/domains/page/javascript_dialog.rs index 04ce8d36fb..c942490b76 100644 --- a/moli-protocol/src/domains/page/javascript_dialog.rs +++ b/moli-protocol/src/domains/page/javascript_dialog.rs @@ -87,7 +87,9 @@ pub(super) fn finish_devtools_get_javascript_dialog_command( conn: &CdpConnection, owner: &CommandOwnerScope, ) -> Result { - let current_page_owner = conn.target_page_residence_identity_for_owner(owner); + let current_page_owner = conn + .pending_target_page_residence_identity_for_owner(owner) + .or_else(|| conn.target_page_residence_identity_for_owner(owner)); let Some(dialog) = conn .target_page_session_state_for_owner(owner) .and_then(|page_state| page_state.javascript_dialog_state.peek_next()) @@ -110,7 +112,9 @@ pub(super) fn finish_devtools_set_javascript_dialog_prompt_text_command( command: DevToolsSetJavaScriptDialogPromptTextCommand, owner: &CommandOwnerScope, ) -> Result { - let current_page_owner = conn.target_page_residence_identity_for_owner(owner); + let current_page_owner = conn + .pending_target_page_residence_identity_for_owner(owner) + .or_else(|| conn.target_page_residence_identity_for_owner(owner)); let Some(result) = conn.with_target_devtools_session_state_for_owner_mut(owner, |state| { let dialog_state = &mut state.page_session_state.javascript_dialog_state; let Some(dialog) = dialog_state @@ -151,7 +155,9 @@ pub(super) fn finish_devtools_handle_javascript_dialog_command( ) -> Result { let session_id = command.context.session_id.as_ref().map(|id| id.as_str()); let command_prompt_text = command.prompt_text; - let current_page_owner = conn.target_page_residence_identity_for_owner(owner); + let current_page_owner = conn + .pending_target_page_residence_identity_for_owner(owner) + .or_else(|| conn.target_page_residence_identity_for_owner(owner)); let Some(dialog) = conn .with_target_devtools_session_state_for_owner_mut(owner, |state| { let dialog_state = &mut state.page_session_state.javascript_dialog_state; @@ -308,7 +314,7 @@ fn emit_one( } fn source_is_current(conn: &CdpConnection, dialog: &PreparedJavaScriptDialog) -> bool { - conn.target_page_protocol_attachment_identity_is_current(dialog.source_attachment()) + conn.target_page_output_attachment_is_current(dialog.source_attachment()) && conn .runtime_session_owner_slot(dialog.source_attachment().session_id()) .is_some_and(|slot| slot.observes_javascript_dialog_scope(dialog.source_dialog_scope())) @@ -336,7 +342,7 @@ fn emit_to_attachment( dialog: PreparedJavaScriptDialog, ) { if !source_is_current(conn, &dialog) - || !conn.target_page_protocol_attachment_identity_is_current(&destination) + || !conn.target_page_output_attachment_is_current(&destination) { trace_stale_source(&dialog); dialog.dismiss(); diff --git a/moli-protocol/src/domains/page/main_document_commit.rs b/moli-protocol/src/domains/page/main_document_commit.rs index 8abe8ca170..c618ebeb7e 100644 --- a/moli-protocol/src/domains/page/main_document_commit.rs +++ b/moli-protocol/src/domains/page/main_document_commit.rs @@ -78,79 +78,98 @@ pub(in crate::domains) fn project_main_document_commit( // commit fact from being projected after replacement. continue; } - - let session_ids = conn.page_event_session_ids_for_owner(owner); - let mut events = Vec::new(); - let Some(frame) = super::lifecycle::frame_record_for_event( - conn, - owner, - &commit.frame_id, - None, - &commit.loader_id, - &commit.url, - ) else { + // Error Documents have no author work to unblock before adoption. + // Their navigation activity owns the loadingFailed prefix and must + // publish it before the frame commit. Keep the native source identity + // above, but leave public frame/lifecycle observations to that activity. + if commit.unreachable_url.is_some() { continue; - }; - super::lifecycle::push_navigation_frame_fact( - &mut events, - &frame, - crate::automation::NavigationFrameEventKind::Navigated, - ); - super::lifecycle::push_document_lifecycle_fact( - &mut events, - &frame, - crate::automation::DocumentLifecyclePhase::Init, - commit.timestamp, - ); - for session_id in session_ids { - let event_owner = session_id - .as_deref() - .map(CommandOwnerScope::for_session) - .unwrap_or_else(|| owner.clone()); - let lifecycle_enabled = conn - .target_page_session_state_for_owner(&event_owner) - .is_some_and(|state| state.page_lifecycle_events); - let dom_enabled = crate::domains::dom::dom_agent_enabled_for_owner(conn, &event_owner); - if !conn - .target_page_session_state_for_owner(&event_owner) - .is_some_and(|state| state.page_domain_enabled) - { - if dom_enabled { - events.push(crate::conn::BackgroundProtocolEvent::page_navigation_frame( - session_id.as_deref(), - super::lifecycle::navigation_frame_fact( - &frame, - crate::automation::NavigationFrameEventKind::DocumentUpdated, - ), - )); - } - continue; - } - - super::emit_navigation_lifecycle_init_background_events( - &mut events, - session_id.as_deref(), - lifecycle_enabled, - &commit.frame_id, - &commit.loader_id, - commit.timestamp, - ); - super::emit_navigation_frame_commit_background_events( - &mut events, - session_id.as_deref(), - dom_enabled, - &commit.frame_id, - &commit.loader_id, - &commit.url, - commit.unreachable_url.as_deref(), - &commit.security_origin, - &commit.secure_context_type, - ); } + + let events = main_document_commit_background_events(conn, owner, &commit); context.command.protocol_events_mut().extend(events); } } +/// Project one frozen commit at the output boundary owned by its navigation. +/// Ordinary Documents use native ingress; error Documents use the activity +/// which first publishes their network failure. +pub(in crate::domains) fn main_document_commit_background_events( + conn: &CdpConnection, + owner: &CommandOwnerScope, + commit: &RendererMainDocumentCommit, +) -> Vec { + let session_ids = conn.page_event_session_ids_for_owner(owner); + let mut events = Vec::new(); + let Some(frame) = super::lifecycle::frame_record_for_event( + conn, + owner, + &commit.frame_id, + None, + &commit.loader_id, + &commit.url, + ) else { + return events; + }; + super::lifecycle::push_navigation_frame_fact( + &mut events, + &frame, + crate::automation::NavigationFrameEventKind::Navigated, + ); + super::lifecycle::push_document_lifecycle_fact( + &mut events, + &frame, + crate::automation::DocumentLifecyclePhase::Init, + commit.timestamp, + ); + for session_id in session_ids { + let event_owner = session_id + .as_deref() + .map(CommandOwnerScope::for_session) + .unwrap_or_else(|| owner.clone()); + let lifecycle_enabled = conn + .target_page_session_state_for_owner(&event_owner) + .is_some_and(|state| state.page_lifecycle_events); + let dom_enabled = crate::domains::dom::dom_agent_enabled_for_owner(conn, &event_owner); + if !conn + .target_page_session_state_for_owner(&event_owner) + .is_some_and(|state| state.page_domain_enabled) + { + if dom_enabled { + events.push(crate::conn::BackgroundProtocolEvent::page_navigation_frame( + session_id.as_deref(), + super::lifecycle::navigation_frame_fact( + &frame, + crate::automation::NavigationFrameEventKind::DocumentUpdated, + ), + )); + } + continue; + } + + super::emit_navigation_lifecycle_init_background_events( + &mut events, + session_id.as_deref(), + lifecycle_enabled, + &commit.frame_id, + &commit.loader_id, + commit.timestamp, + ); + super::emit_navigation_frame_commit_background_events( + &mut events, + session_id.as_deref(), + dom_enabled, + &commit.frame_id, + &commit.loader_id, + &commit.url, + commit.unreachable_url.as_deref(), + &commit.security_origin, + &commit.secure_context_type, + ); + } + events +} + pub(in crate::domains) const SLOT_MAIN_DOCUMENT_COMMIT: ProtocolOutputSlot = ProtocolOutputSlot::MainDocumentCommit; diff --git a/moli-protocol/src/domains/page/navigation.rs b/moli-protocol/src/domains/page/navigation.rs index 5dbf51cc46..4797076adc 100644 --- a/moli-protocol/src/domains/page/navigation.rs +++ b/moli-protocol/src/domains/page/navigation.rs @@ -3051,7 +3051,7 @@ pub(crate) fn emit_same_document_navigation_background_events( for navigation in navigations { let session_id = owner.session_id(); let source_document = navigation.source_document(); - if !conn.target_page_residence_identity_is_current(navigation.owner()) { + if !conn.target_page_output_residence_is_current(navigation.owner()) { tracing::debug!( session_id, ?source_document, @@ -3066,7 +3066,10 @@ pub(crate) fn emit_same_document_navigation_background_events( let Ok(url) = Url::parse(&navigation.url) else { continue; }; - if conn.has_pending_document_navigation_for_owner(owner) { + let source_is_prepared = conn + .runtime_session_owner_slot_for_owner(owner) + .is_some_and(|slot| slot.prepared_document_output_is_from(source_document)); + if conn.has_pending_document_navigation_for_owner(owner) && !source_is_prepared { tracing::debug!( session_id, url = url.as_str(), diff --git a/moli-protocol/src/domains/page/navigation_commit.rs b/moli-protocol/src/domains/page/navigation_commit.rs index 1162dbf332..08bc9861b4 100644 --- a/moli-protocol/src/domains/page/navigation_commit.rs +++ b/moli-protocol/src/domains/page/navigation_commit.rs @@ -36,6 +36,7 @@ struct LoadedNavigationPublication { renderer_output_predecessor: Option, progress_gate: crate::domains::network::MainDocumentProgressGate, network_error_page: Option, + main_document_commit: std::sync::Arc, } impl LoadedNavigationPublication { @@ -77,6 +78,7 @@ impl LoadedNavigationPublication { renderer_output_predecessor, progress_gate, network_error_page, + main_document_commit: commit.clone(), }, page, initial_runtime_realms, @@ -126,6 +128,7 @@ impl LoadedNavigationPublication { renderer_output_predecessor, progress_gate, network_error_page, + main_document_commit, } = self; let is_network_error_page = network_error_page.is_some(); let (page_creation_artifacts, mut deferred_initial_renderer_document_lifecycle_events) = @@ -137,8 +140,10 @@ impl LoadedNavigationPublication { token.clone(), ); if let Some(error) = network_error_page { - navigation_activity = - navigation_activity.with_network_error_page_result(error.error_text().to_owned()); + navigation_activity = navigation_activity.with_network_error_page_result( + error.error_text().to_owned(), + main_document_commit, + ); } if !is_network_error_page { let _ = conn.commit_main_document_resource_for_owner( diff --git a/moli-protocol/src/domains/page/output.rs b/moli-protocol/src/domains/page/output.rs index 49d282a2e4..189b393ccd 100644 --- a/moli-protocol/src/domains/page/output.rs +++ b/moli-protocol/src/domains/page/output.rs @@ -3,14 +3,13 @@ use anyhow::Result; impl PagePreparedOutputs { pub(crate) fn from_renderer_auxiliary_window_navigation( - conn: &CdpConnection, - owner: &CommandOwnerScope, + page_source: Option, window: moli_core::page::RendererAuxiliaryWindow, url: String, kind: moli_core::page::RendererAuxiliaryNavigationKind, document_response: Option, ) -> Self { - let Some(residence) = conn.target_page_residence_identity_for_owner(owner) else { + let Some(residence) = page_source else { return Self::default(); }; Self { @@ -38,11 +37,10 @@ impl PagePreparedOutputs { } pub(crate) fn from_renderer_auxiliary_window_close( - conn: &CdpConnection, - owner: &CommandOwnerScope, + page_source: Option, window: moli_core::page::RendererAuxiliaryWindow, ) -> Self { - let Some(residence) = conn.target_page_residence_identity_for_owner(owner) else { + let Some(residence) = page_source else { return Self::default(); }; Self { @@ -64,14 +62,17 @@ impl PagePreparedOutputs { pub(crate) fn from_renderer_javascript_dialog( conn: &CdpConnection, owner: &CommandOwnerScope, + page_source: Option, dialog: moli_core::page::RendererPendingJavaScriptDialog, ) -> Self { - let Some(source_attachment) = - conn.target_page_protocol_attachment_identity_for_owner(owner) - else { + let Some(page_owner) = page_source else { let _ = dialog.finish(false, String::new()); return Self::default(); }; + let source_attachment = crate::conn::TargetPageProtocolAttachmentIdentity::new( + page_owner, + owner.session_id().map(str::to_owned), + ); let Some((root_frame_id, _, _, _)) = conn.target_session_owner_frame_tree_identity_for_owner(owner) else { @@ -94,11 +95,10 @@ impl PagePreparedOutputs { } pub(crate) fn from_renderer_popup_activation( - conn: &CdpConnection, - owner: &CommandOwnerScope, + page_owner: Option, activation: moli_core::page::RendererPendingPopupActivation, ) -> Self { - let Some(page_owner) = conn.target_page_residence_identity_for_owner(owner) else { + let Some(page_owner) = page_owner else { return Self::default(); }; Self { @@ -124,12 +124,11 @@ impl PagePreparedOutputs { } pub(crate) fn from_renderer_session_history_update( - conn: &CdpConnection, - owner: &CommandOwnerScope, + page_source: Option, source_document: RendererDocumentLifecycleIdentity, update: moli_page_types::SessionHistoryUpdate, ) -> Self { - let Some(residence) = conn.target_page_residence_identity_for_owner(owner) else { + let Some(residence) = page_source else { return Self::default(); }; Self { @@ -149,11 +148,10 @@ impl PagePreparedOutputs { } pub(crate) fn from_renderer_same_document_navigation( - conn: &CdpConnection, - owner: &CommandOwnerScope, + page_source: Option, navigation: RendererDocumentSourcedSameDocumentNavigation, ) -> Self { - let Some(page_owner) = conn.target_page_residence_identity_for_owner(owner) else { + let Some(page_owner) = page_source else { return Self::default(); }; Self { @@ -165,11 +163,10 @@ impl PagePreparedOutputs { } pub(crate) fn from_renderer_top_level_location_navigation( - conn: &CdpConnection, - owner: &CommandOwnerScope, + page_source: Option, navigation: RendererDocumentSourcedTopLevelLocationNavigation, ) -> Self { - let Some(page_owner) = conn.target_page_residence_identity_for_owner(owner) else { + let Some(page_owner) = page_source else { return Self::default(); }; Self { @@ -181,11 +178,11 @@ impl PagePreparedOutputs { } pub(crate) fn from_renderer_top_level_history_traversal( - conn: &CdpConnection, + page_source: Option, owner: &CommandOwnerScope, traversal: RendererPendingTopLevelHistoryTraversal, ) -> Self { - let Some(page_owner) = conn.target_page_residence_identity_for_owner(owner) else { + let Some(page_owner) = page_source else { return Self::default(); }; Self { @@ -866,7 +863,7 @@ pub(in crate::domains) fn project_page_output_finite( { // document.open() does not undo committed history. Page // residence, not current Document identity, is authority. - if conn.target_page_residence_identity_is_current(&residence) { + if conn.target_page_output_residence_is_current(&residence) { conn.record_session_history_update_for_owner(&owner, &update); } } diff --git a/moli-protocol/src/domains/page/renderer_navigation.rs b/moli-protocol/src/domains/page/renderer_navigation.rs index 11e81f0c6f..9209202d2a 100644 --- a/moli-protocol/src/domains/page/renderer_navigation.rs +++ b/moli-protocol/src/domains/page/renderer_navigation.rs @@ -160,7 +160,7 @@ impl PreparedRendererNavigation { action: crate::conn::TopLevelNavigationOwnerAction, ) -> Option { let (owner, page, request) = action.into_parts(); - if !conn.target_page_residence_identity_is_current(&page) { + if !conn.target_page_output_residence_is_current(&page) { return None; } let step = match request { diff --git a/moli-protocol/src/domains/target/worker_target/dedicated_worker.rs b/moli-protocol/src/domains/target/worker_target/dedicated_worker.rs index fae1eb59ae..83dd1900c3 100644 --- a/moli-protocol/src/domains/target/worker_target/dedicated_worker.rs +++ b/moli-protocol/src/domains/target/worker_target/dedicated_worker.rs @@ -14,7 +14,7 @@ pub(super) fn dedicated_worker_target_is_current( .get(&renderer_instance_id) .is_some_and(|target| { target.target_id == target_id - && context.target_page_residence_is_current(&target.owner_page) + && conn.target_page_output_residence_is_current(&target.owner_page) }) } @@ -282,9 +282,16 @@ pub(in crate::domains::target) fn dedicated_worker_auto_attach_owner_session_all let Some(owner_session_id) = owner_session_id else { return false; }; - conn.target_page_residence_identity_for_session(Some(owner_session_id)) + let owner = CommandOwnerScope::for_session(owner_session_id); + (conn + .target_page_residence_identity_for_owner(&owner) .as_ref() == Some(owner_page) + || conn + .pending_target_page_residence_identity_for_owner(&owner) + .as_ref() + == Some(owner_page)) + && conn.target_page_output_residence_is_current(owner_page) } pub(super) fn shared_worker_auto_attach_owner_sessions( diff --git a/moli-protocol/src/domains/target/worker_target/lifecycle_projection.rs b/moli-protocol/src/domains/target/worker_target/lifecycle_projection.rs index 6e0397e4ee..b5e9355e8b 100644 --- a/moli-protocol/src/domains/target/worker_target/lifecycle_projection.rs +++ b/moli-protocol/src/domains/target/worker_target/lifecycle_projection.rs @@ -122,14 +122,35 @@ pub(in crate::domains) fn service_worker_target_lifecycle_prepared_outputs_for_e pub(in crate::domains) fn dedicated_worker_target_lifecycle_prepared_outputs_for_event( conn: &mut CdpConnection, owner: &CommandOwnerScope, + page_source: Option<(TargetPageResidenceIdentity, RendererPageResidenceIdentity)>, event: RendererDedicatedWorkerTargetEvent, ) -> TargetPreparedOutputs { - let Some(owner_page) = conn.target_page_residence_identity_for_owner(owner) else { + let Some((owner_page, owner_renderer_page)) = page_source else { return TargetPreparedOutputs::default(); }; - let Some(owner_renderer_page) = conn.renderer_page_residence_identity_for_owner(owner) else { + if let RendererDedicatedWorkerTargetEvent::Destroyed { instance_id } = event { + let context_id = owner_page.browser_context_id(); + if conn + .browser_context_by_id(context_id) + .and_then(|context| context.dedicated_worker_targets.get(&instance_id)) + .is_none_or(|target| { + target.owner_page != owner_page + || target.renderer_owner_local_host_id + != owner_renderer_page.owner_local_host_id() + }) + { + return TargetPreparedOutputs::default(); + } + return prepare_dedicated_worker_target_retirement( + conn, + context_id, + instance_id, + DedicatedWorkerRetirementCause::RendererDestroyed, + ); + } + if !conn.target_page_output_residence_is_current(&owner_page) { return TargetPreparedOutputs::default(); - }; + } let browser_context_id = owner_page.browser_context_id().to_owned(); let owner_page_network_sessions = conn.network_event_session_ids_for_owner(owner); dedicated_worker_target_lifecycle_outputs_for_events( diff --git a/moli-renderer-v8/src/devtools/command/nested.rs b/moli-renderer-v8/src/devtools/command/nested.rs index 61fe1d12e1..4c38ae7447 100644 --- a/moli-renderer-v8/src/devtools/command/nested.rs +++ b/moli-renderer-v8/src/devtools/command/nested.rs @@ -9,6 +9,7 @@ impl RendererPageCommand { use RendererDevToolsMainNestedDispatch::{InspectorSession, OwnerOnly, PageAgent}; match self { Self::WebMcp { .. } => OwnerOnly, + Self::ForDocument { command, .. } => command.nested_dispatch(), Self::Native(command) => command.operation.nested_dispatch(), Self::Inspector(envelope) if envelope.can_dispatch_at_nested_inspector_session_boundary() => diff --git a/moli-renderer-v8/src/lib.rs b/moli-renderer-v8/src/lib.rs index 6d0ac3d696..cabebd4eb6 100644 --- a/moli-renderer-v8/src/lib.rs +++ b/moli-renderer-v8/src/lib.rs @@ -248,17 +248,17 @@ pub use runtime::{ RendererDedicatedWorkerTargetEvent, RendererDedicatedWorkerTargetInfo, RendererDevToolsAgentToken, RendererDevToolsMainCommandEnvelope, RendererDocumentBoxModel, RendererDocumentChildNodeSnapshotEvent, RendererDocumentChildNodeSnapshotEvents, - RendererDocumentChildNodeSnapshots, RendererDocumentCommitPermit, - RendererDocumentFrontendNodeIdsResolution, RendererDocumentHitTestResult, - RendererDocumentIsolateAccountingDiagnostics, RendererDocumentLifecycleEvent, - RendererDocumentLifecycleEventKind, RendererDocumentLifecycleIdentity, - RendererDocumentLifecycleMilestone, RendererDocumentLifecycleSnapshot, - RendererDocumentLifecycleWaitOutcome, RendererDocumentLifecycleWaiter, - RendererDocumentNodeAttributesResolution, RendererDocumentNodeClientRect, - RendererDocumentNodeGeometry, RendererDocumentNodePropertyResolution, - RendererDocumentNodeReference, RendererDocumentNodeTextResolution, RendererDocumentOptions, - RendererDocumentPreparationTarget, RendererDocumentQuerySelectorNode, - RendererDocumentQuerySelectorResolution, + RendererDocumentChildNodeSnapshots, RendererDocumentCommandTarget, + RendererDocumentCommitPermit, RendererDocumentFrontendNodeIdsResolution, + RendererDocumentHitTestResult, RendererDocumentIsolateAccountingDiagnostics, + RendererDocumentLifecycleEvent, RendererDocumentLifecycleEventKind, + RendererDocumentLifecycleIdentity, RendererDocumentLifecycleMilestone, + RendererDocumentLifecycleSnapshot, RendererDocumentLifecycleWaitOutcome, + RendererDocumentLifecycleWaiter, RendererDocumentNodeAttributesResolution, + RendererDocumentNodeClientRect, RendererDocumentNodeGeometry, + RendererDocumentNodePropertyResolution, RendererDocumentNodeReference, + RendererDocumentNodeTextResolution, RendererDocumentOptions, RendererDocumentPreparationTarget, + RendererDocumentQuerySelectorNode, RendererDocumentQuerySelectorResolution, RendererDocumentQuerySelectorWithChildNodeSnapshotEvents, RendererDocumentSourcedSameDocumentNavigation, RendererDocumentSourcedTopLevelLocationNavigation, RendererDocumentTerminationReason, @@ -295,22 +295,22 @@ pub use runtime::{ RendererOutputTransportMessage, RendererOutputTransportReceiver, RendererOutputTransportSendError, RendererOutputTransportSender, RendererOwnerAction, RendererOwnerCommand, RendererOwnerHandle, RendererOwnerLocalHostId, RendererOwnerReply, - RendererOwnerResourceActivitySource, RendererOwnerRuntimeActivitySource, RendererPageCommand, - RendererPageCommandPending, RendererPageCommandPostResponseContinuation, - RendererPageCookieFacadeSnapshotReply, RendererPageCreationArtifacts, - RendererPageCreationDiagnostics, RendererPageCreationPhase, RendererPageCreationProgress, - RendererPageDiagnosticsSnapshot, RendererPageDumpFormat, RendererPageDumpOptions, - RendererPageDumpStripOptions, RendererPageHandle, RendererPageReplacementCommit, - RendererPageReplacementError, RendererPageReplacementFailureDisposition, - RendererPageReplacementReservationRequest, RendererPageReplacementTarget, RendererPageReply, - RendererPageReservationToken, RendererPageState, RendererPageStateRevision, - RendererPageTestingHandle, RendererPageView, RendererPdfOptions, RendererPendingAuxiliaryPage, - RendererPendingDownloadActivation, RendererPendingDownloadResponse, - RendererPendingFileChooserActivation, RendererPendingJavaScriptDialog, - RendererPendingPopupActivation, RendererPendingSameDocumentNavigation, - RendererPendingTopLevelHistoryTraversal, RendererPendingWindowOpenEvent, - RendererPerformanceMetricSnapshot, RendererPointerEventProperties, - RendererPopupActivationSource, RendererPopupDisposition, + RendererOwnerResourceActivitySource, RendererOwnerRuntimeActivitySource, + RendererPageAccessError, RendererPageCommand, RendererPageCommandPending, + RendererPageCommandPostResponseContinuation, RendererPageCookieFacadeSnapshotReply, + RendererPageCreationArtifacts, RendererPageCreationDiagnostics, RendererPageCreationPhase, + RendererPageCreationProgress, RendererPageDiagnosticsSnapshot, RendererPageDumpFormat, + RendererPageDumpOptions, RendererPageDumpStripOptions, RendererPageHandle, + RendererPageReplacementCommit, RendererPageReplacementError, + RendererPageReplacementFailureDisposition, RendererPageReplacementReservationRequest, + RendererPageReplacementTarget, RendererPageReply, RendererPageReservationToken, + RendererPageState, RendererPageStateRevision, RendererPageTestingHandle, RendererPageView, + RendererPdfOptions, RendererPendingAuxiliaryPage, RendererPendingDownloadActivation, + RendererPendingDownloadResponse, RendererPendingFileChooserActivation, + RendererPendingJavaScriptDialog, RendererPendingPopupActivation, + RendererPendingSameDocumentNavigation, RendererPendingTopLevelHistoryTraversal, + RendererPendingWindowOpenEvent, RendererPerformanceMetricSnapshot, + RendererPointerEventProperties, RendererPopupActivationSource, RendererPopupDisposition, RendererPreparedDocumentCommitConfiguration, RendererPrintToPdfReply, RendererPrintToPdfRequest, RendererProtocolObservation, RendererReservedServiceWorkerClient, RendererResourceContentBody, RendererResourceSearchRequest, RendererResourceTextSearchOutcome, diff --git a/moli-renderer-v8/src/runtime/mod.rs b/moli-renderer-v8/src/runtime/mod.rs index 5bea836814..8776e974bd 100644 --- a/moli-renderer-v8/src/runtime/mod.rs +++ b/moli-renderer-v8/src/runtime/mod.rs @@ -312,7 +312,8 @@ pub use self::owner::{ pub use self::owner_local::RendererPageReplacementCommit; pub use self::owner_local::RendererPageTestingHandle; pub use self::owner_local::{ - RendererPageCommandPending, RendererPageHandle, RendererRuntimeInspectorSessionDetachGuard, + RendererDocumentCommandTarget, RendererPageAccessError, RendererPageCommandPending, + RendererPageHandle, RendererRuntimeInspectorSessionDetachGuard, }; pub(crate) use self::owner_local_store::RendererPageToken; pub use self::page::{ diff --git a/moli-renderer-v8/src/runtime/owner/owner_commands.rs b/moli-renderer-v8/src/runtime/owner/owner_commands.rs index 7af4193415..d217db2372 100644 --- a/moli-renderer-v8/src/runtime/owner/owner_commands.rs +++ b/moli-renderer-v8/src/runtime/owner/owner_commands.rs @@ -176,6 +176,31 @@ impl RendererOwnerHandle { self.state.page_table.remove(page_id); } + /// Uses the renderer owner rather than an adopted Page handle. The queued + /// command rechecks the captured Document before touching native requests. + pub fn document_command_target( + &self, + owner_local_host_id: RendererOwnerLocalHostId, + source_document: super::RendererDocumentLifecycleIdentity, + ) -> Result { + use crate::runtime::page_commands::DocumentCommandError; + if owner_local_host_id != self.state.owner_local_host_id { + return Err(DocumentCommandError::WrongOwner.into()); + } + let token = RendererPageToken { + local_host_id: owner_local_host_id, + #[cfg(debug_assertions)] + local_thread_id: (*self.state.owner_local_thread_id.lock()) + .ok_or(DocumentCommandError::OwnerUnavailable)?, + page_id: source_document.frame.page_id, + }; + Ok(super::RendererDocumentCommandTarget { + render_runtime: self.render_runtime.clone(), + token, + document: source_document, + }) + } + pub fn allocate_page_id(&self) -> PageId { PageId::new(self.state.next_page_id.fetch_add(1, Ordering::Relaxed)) } diff --git a/moli-renderer-v8/src/runtime/owner_local.rs b/moli-renderer-v8/src/runtime/owner_local.rs index a51bcfbfc1..9793974673 100644 --- a/moli-renderer-v8/src/runtime/owner_local.rs +++ b/moli-renderer-v8/src/runtime/owner_local.rs @@ -140,11 +140,68 @@ pub struct RendererPageHandle { _not_send: PhantomData>, } +#[derive(Clone, Copy, Debug, thiserror::Error)] +pub enum RendererPageAccessError { + #[error("Renderer Document is no longer current")] + DocumentReplaced, + #[error("renderer local host page {} is already running an owner turn", .page_id.as_u64())] + Busy { page_id: super::PageId }, + #[error("renderer local host no longer tracks page {}", .page_id.as_u64())] + Missing { page_id: super::PageId }, + #[error("renderer local host page {} is retiring", .page_id.as_u64())] + Retiring { page_id: super::PageId }, +} + +/// A revocable command capability for one exact native Document. It keeps +/// neither a Page handle nor the renderer runtime owner alive. +#[derive(Clone, Debug)] +pub struct RendererDocumentCommandTarget { + pub(super) render_runtime: RenderRuntimeHandle, + pub(super) token: RendererPageToken, + pub(super) document: RendererDocumentLifecycleIdentity, +} + +impl PartialEq for RendererDocumentCommandTarget { + fn eq(&self, other: &Self) -> bool { + self.token == other.token && self.document == other.document + } +} + +impl Eq for RendererDocumentCommandTarget {} + +impl RendererDocumentCommandTarget { + pub fn document(&self) -> RendererDocumentLifecycleIdentity { + self.document + } + + pub fn enqueue(&self, command: RendererPageCommand) -> Result { + let reply = self + .render_runtime + .enqueue(RendererOwnerCommand::RunProtocolPageCommand { + token: self.token, + command: RendererPageCommand::ForDocument { + source_document: self.document, + command: Box::new(command), + }, + })?; + Ok(RendererPageCommandPending::from_owner_reply(reply)) + } +} + pub struct RendererPageCommandPending { dispatch: RendererPageCommandPendingDispatch, javascript_dialog_watch: Option, } +impl RendererPageCommandPending { + pub(super) fn from_owner_reply(reply: oneshot::Receiver>) -> Self { + Self { + dispatch: RendererPageCommandPendingDispatch::Owner(reply), + javascript_dialog_watch: None, + } + } +} + enum RendererPageCommandPendingDispatch { Owner(oneshot::Receiver>), InspectorMain(Box), @@ -210,6 +267,20 @@ impl RendererPageHandle { .expect("renderer page handle should remain open while in use") } + pub fn document_command_target( + &self, + document: RendererDocumentLifecycleIdentity, + ) -> Result { + if self.token().page_id != document.frame.page_id { + return Err(super::page_commands::DocumentCommandError::WrongPage.into()); + } + Ok(RendererDocumentCommandTarget { + render_runtime: self.render_runtime.clone(), + token: self.token(), + document, + }) + } + pub fn page_id(&self) -> u64 { self.token().page_id.as_u64() } diff --git a/moli-renderer-v8/src/runtime/owner_local_store/mod.rs b/moli-renderer-v8/src/runtime/owner_local_store/mod.rs index dc58fbc3af..047e37eb80 100644 --- a/moli-renderer-v8/src/runtime/owner_local_store/mod.rs +++ b/moli-renderer-v8/src/runtime/owner_local_store/mod.rs @@ -1396,18 +1396,22 @@ impl RendererOwnerLocalStore { fn take_entry_for_command(&mut self, token: RendererPageToken) -> Result { match self.checkout_entry_for_owner_turn(token) { Ok(entry) => Ok(entry), - Err(LivePageEntryCheckoutError::Busy) => Err(anyhow!( - "renderer local host page {} is already running an owner turn", - token.page_id.as_u64() - )), - Err(LivePageEntryCheckoutError::Retired) => Err(anyhow!( - "renderer local host page {} is retiring", - token.page_id.as_u64() - )), - Err(LivePageEntryCheckoutError::Missing) => Err(anyhow!( - "renderer local host no longer tracks page {}", - token.page_id.as_u64() - )), + Err(LivePageEntryCheckoutError::Busy) => Err(super::RendererPageAccessError::Busy { + page_id: token.page_id, + } + .into()), + Err(LivePageEntryCheckoutError::Retired) => { + Err(super::RendererPageAccessError::Retiring { + page_id: token.page_id, + } + .into()) + } + Err(LivePageEntryCheckoutError::Missing) => { + Err(super::RendererPageAccessError::Missing { + page_id: token.page_id, + } + .into()) + } } } diff --git a/moli-renderer-v8/src/runtime/page_commands.rs b/moli-renderer-v8/src/runtime/page_commands.rs index 51f6507128..493ef1eb9f 100644 --- a/moli-renderer-v8/src/runtime/page_commands.rs +++ b/moli-renderer-v8/src/runtime/page_commands.rs @@ -1,15 +1,42 @@ use super::page_surface::RendererInspectorPageCommand; use super::*; +#[derive(Debug, thiserror::Error)] +pub(super) enum DocumentCommandError { + #[error("Document command belongs to another renderer owner")] + WrongOwner, + #[error("Document command belongs to another renderer Page")] + WrongPage, + #[cfg(debug_assertions)] + #[error("Renderer owner is unavailable")] + OwnerUnavailable, +} + impl PageVm { pub(in crate::runtime) fn dispatch_renderer_page_command( &mut self, command: RendererPageCommand, ) -> Result { + let (source_document, command) = match command { + RendererPageCommand::ForDocument { + source_document, + command, + } => (Some(source_document), *command), + command => (None, command), + }; + if source_document.is_some_and(|source| self.document_lifecycle.identity() != source) { + return Err(RendererPageAccessError::DocumentReplaced.into()); + } if let Some(barrier) = renderer_page_command_action_barrier(&command) { self.flush_page_action_window(barrier)?; } + // Flushing queued input can run script, including document.open(). + // Recheck immediately before touching a Document's native request IDs. + if source_document.is_some_and(|source| self.document_lifecycle.identity() != source) { + return Err(RendererPageAccessError::DocumentReplaced.into()); + } match command { + command @ RendererPageCommand::ForDocument { .. } => self.dispatch_renderer_page_command(command), RendererPageCommand::Native(command) => { Ok(RendererPageReply::NativeCommandReady(Box::new(command.dispatch(self)))) } diff --git a/moli-renderer-v8/src/runtime/page_surface.rs b/moli-renderer-v8/src/runtime/page_surface.rs index 13b6a9b462..0bbe352eb3 100644 --- a/moli-renderer-v8/src/runtime/page_surface.rs +++ b/moli-renderer-v8/src/runtime/page_surface.rs @@ -4750,6 +4750,12 @@ pub enum RendererPageCommand { }, // The call fits inline; its operation boxes the recursively contained page command. Native(RendererCdpCall), + /// Checks the original root Document on the renderer before executing an + /// operation admitted while its browser-side Page is still being adopted. + ForDocument { + source_document: RendererDocumentLifecycleIdentity, + command: Box, + }, Inspector(RendererInspectorCommandEnvelope), EvaluateExpression { expression: String,