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.
This commit is contained in:
ldm0
2026-10-29 17:39:23 +08:00
committed by Donough Liu
parent 34bd52d306
commit 804bf1e357
52 changed files with 2632 additions and 706 deletions
Generated
+1
View File
@@ -2780,6 +2780,7 @@ dependencies = [
"serde_json",
"static_assertions",
"strip-ansi-escapes",
"thiserror",
"tokio",
"tracing",
"url",
+1
View File
@@ -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"
+1 -1
View File
@@ -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};
+69 -3
View File
@@ -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<PendingPageCommand> {
self.0
.enqueue(command)
.map(PendingPageCommand::from_renderer)
}
}
pub struct PendingPageCommand {
pending: RendererPageCommandPending,
renderer_agent_attachment_id: Option<RendererAgentAttachmentId>,
}
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<RendererAgentAttachmentId> {
self.renderer_agent_attachment_id
}
@@ -79,7 +103,42 @@ pub struct CompletedPageCommand {
renderer_agent_attachment_id: Option<RendererAgentAttachmentId>,
}
#[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<crate::page::PendingSubresourceContinueOutcome> {
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<crate::page::RendererDomBidiNodeSharedIdResolution> {
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<RendererCommandTurnOutput> {
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<PageDocumentCommandTarget> {
self.handle
.document_command_target(document)
.map(PageDocumentCommandTarget::new)
}
pub(crate) fn start_page_command(
&self,
mut command: RendererPageCommand,
+2 -2
View File
@@ -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))]
@@ -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<crate::page::PageDocumentCommandTarget> {
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.
///
@@ -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(
+22 -4
View File
@@ -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)]
@@ -15,6 +15,10 @@ use crate::conn::{
PendingSubresourceFetchRequest, PendingSubresourceFetchResponseRequest, TargetRuntimeSlot,
};
#[derive(Debug, thiserror::Error)]
#[error("RequestNotFound")]
struct SubresourceRequestNotFound;
pub(crate) type SessionOwnerPendingFetchState = (
Vec<PendingFetchNavigation>,
Vec<PendingFetchAuthNavigation>,
@@ -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<moli_core::page::PendingPageCommand> {
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<moli_core::RendererDocumentLifecycleIdentity>,
) -> 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<moli_core::RendererDocumentLifecycleIdentity>,
) -> 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<crate::conn::ClaimedSubresourceContinueRequest> {
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<String>,
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<DevToolsNetworkInterceptId>,
) -> 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<String>,
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<Result<moli_core::page::PendingPageCommand>>,
)> {
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<Result<moli_core::page::PendingPageCommand>>,
)> {
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<moli_core::page::SubresourceResourceType>,
) -> Option<Result<moli_core::page::PendingPageCommand>> {
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,
@@ -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<moli_core::page::PageDocumentCommandTarget> {
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<crate::conn::TargetPageProtocolAttachmentIdentity> {
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<CommandOwnerScope> {
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();
+40 -2
View File
@@ -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<moli_core::RendererDocumentLifecycleIdentity> {
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<moli_core::page::PageDocumentCommandTarget>,
pub owner_session_id: Option<String>,
pub action_session_id: Option<String>,
pub owner_kind: PendingSubresourceFetchOwnerKind,
@@ -1535,6 +1564,10 @@ pub struct PendingSubresourceFetchAuthRequest {
}
impl PendingSubresourceFetchAuthRequest {
pub(crate) fn source_document(&self) -> Option<moli_core::RendererDocumentLifecycleIdentity> {
self.command_target.as_ref().map(|target| target.document())
}
pub fn pop_next_auth_required_pause(&mut self) -> Option<PendingSubresourceFetchAuthStage> {
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<moli_core::page::PageDocumentCommandTarget>,
pub owner_session_id: Option<String>,
pub action_session_id: Option<String>,
pub owner_kind: PendingSubresourceFetchOwnerKind,
@@ -1589,6 +1623,10 @@ pub struct PendingSubresourceFetchResponseRequest {
}
impl PendingSubresourceFetchResponseRequest {
pub(crate) fn source_document(&self) -> Option<moli_core::RendererDocumentLifecycleIdentity> {
self.command_target.as_ref().map(|target| target.document())
}
pub fn pop_next_response_stage_pause(
&mut self,
) -> Option<PendingSubresourceFetchResponseStage> {
+28 -11
View File
@@ -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
@@ -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 {
@@ -1195,9 +1195,31 @@ impl BrowserContext {
loader_id: String,
) -> Option<DocumentNavigationToken> {
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);
}
}
+2
View File
@@ -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),
@@ -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
@@ -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,
@@ -33,7 +33,10 @@ pub(crate) struct MainDocumentNavigationActivity {
enum LoadedNavigationResultMode {
Success,
NetworkErrorPage { error_text: String },
NetworkErrorPage {
error_text: String,
commit: std::sync::Arc<moli_core::page::RendererMainDocumentCommit>,
},
}
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<moli_core::page::RendererMainDocumentCommit>,
) -> 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
@@ -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);
@@ -102,6 +102,14 @@ impl RendererOutputProjection {
record: RendererOutputRecord,
context: &mut CommandWorkflowContext,
) -> Option<PreparedPublicationOutputProjection> {
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<PreparedPublicationOutputProjection> {
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) => {
@@ -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,
)
})
})
@@ -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,
@@ -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));
@@ -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<PreparedSubresourceContinueAction> {
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<PreparedSubresourceContinueWait> {
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<Self> {
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<moli_core::page::CompletedPageCommand>,
) -> Vec<BackgroundProtocolEvent> {
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");
}
-1
View File
@@ -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,
+42 -56
View File
@@ -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<Result<CompletedPageCommand>>,
pending: crate::conn::PendingSubresourceFetchAuthRequest,
expose_challenged_response: bool,
correlation: Option<PreparedSubresourceCorrelation>,
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<CompletedPageCommand>>,
) -> Result<()> {
fn finish_continue_subresource_auth(completed: Option<Result<CompletedPageCommand>>) -> 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")
}
+55 -33
View File
@@ -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::<moli_core::RendererPageAccessError>(),
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<TargetRootDocumentProtocolAttachmentIdentity>,
resource: CleanupSubresource,
}
@@ -292,35 +305,41 @@ fn admit_subresource_failure(
resource: CleanupSubresource,
reason: &str,
) -> Option<PreparedSubresourceFailure> {
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<TargetRootDocumentProtocolAttachmentIdentity>,
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();
+117 -212
View File
@@ -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<Result<CompletedPageCommand>>,
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<CompletedPageCommand>>,
) -> 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::<crate::conn::NoDocumentLoaded>() => {
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::<crate::conn::NoDocumentLoaded>() => {
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::<crate::conn::NoDocumentLoaded>() => {
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::<crate::conn::NoDocumentLoaded>() => {
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::<crate::conn::NoDocumentLoaded>() => {
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::<crate::conn::NoDocumentLoaded>() => {
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::<crate::conn::NoDocumentLoaded>() => {
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::<crate::conn::NoDocumentLoaded>() => {
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::<crate::conn::NoDocumentLoaded>() => {
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::<crate::conn::NoDocumentLoaded>() => {
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()),
+31 -7
View File
@@ -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<crate::conn::TargetPageResidenceIdentity>,
residence: Option<crate::conn::PendingSubresourceFetchResidence>,
sources: Vec<PendingSubresourceFetchPauseSource>,
) -> 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 {
@@ -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<Self> {
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<CompletedPageCommand>) {
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");
@@ -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()),
@@ -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,<title>fetch correlation</title>")
.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(),
"<!doctype html><title>fetch correlation</title>".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,
)
})
});
+17 -7
View File
@@ -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<BackgroundProtocolEvent> = 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),
+84 -50
View File
@@ -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<moli_core::page::PageDocumentCommandTarget>,
}
impl PreparedFileChooserActivation {
pub(crate) fn capture(
conn: &CdpConnection,
conn: &mut CdpConnection,
owner: &CommandOwnerScope,
page_owner: TargetPageResidenceIdentity,
activation: RendererPendingFileChooserActivation,
) -> Option<Self> {
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> {
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<crate::conn::RendererPageResidenceIdentity>,
activation: RendererPendingFileChooserActivation,
) -> Option<Self> {
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<Result<PendingPageCommand>> {
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<RendererDomBidiNodeSharedIdResolution> {
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<PendingPageCommand> {
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<BackgroundProtocolEvent> {
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) {
+1 -1
View File
@@ -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)]
@@ -87,7 +87,9 @@ pub(super) fn finish_devtools_get_javascript_dialog_command(
conn: &CdpConnection,
owner: &CommandOwnerScope,
) -> Result<DevToolsJavaScriptDialogResult, DevToolsError> {
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<AutomationResult, DevToolsError> {
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<BackgroundProtocolEvent, DevToolsError> {
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();
@@ -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<crate::conn::BackgroundProtocolEvent> {
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;
+5 -2
View File
@@ -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(),
@@ -36,6 +36,7 @@ struct LoadedNavigationPublication {
renderer_output_predecessor: Option<moli_core::RendererOutputFence>,
progress_gate: crate::domains::network::MainDocumentProgressGate,
network_error_page: Option<crate::conn::NetworkErrorPageNavigation>,
main_document_commit: std::sync::Arc<moli_core::page::RendererMainDocumentCommit>,
}
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(
+21 -24
View File
@@ -3,14 +3,13 @@ use anyhow::Result;
impl PagePreparedOutputs {
pub(crate) fn from_renderer_auxiliary_window_navigation(
conn: &CdpConnection,
owner: &CommandOwnerScope,
page_source: Option<crate::conn::TargetPageResidenceIdentity>,
window: moli_core::page::RendererAuxiliaryWindow,
url: String,
kind: moli_core::page::RendererAuxiliaryNavigationKind,
document_response: Option<moli_core::page::RendererAuxiliaryDocumentResponse>,
) -> 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<crate::conn::TargetPageResidenceIdentity>,
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<crate::conn::TargetPageResidenceIdentity>,
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<crate::conn::TargetPageResidenceIdentity>,
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<crate::conn::TargetPageResidenceIdentity>,
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<crate::conn::TargetPageResidenceIdentity>,
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<crate::conn::TargetPageResidenceIdentity>,
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<crate::conn::TargetPageResidenceIdentity>,
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);
}
}
@@ -160,7 +160,7 @@ impl PreparedRendererNavigation {
action: crate::conn::TopLevelNavigationOwnerAction,
) -> Option<Self> {
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 {
@@ -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(
@@ -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(
@@ -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() =>
+27 -27
View File
@@ -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,
+2 -1
View File
@@ -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::{
@@ -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<super::RendererDocumentCommandTarget> {
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))
}
@@ -140,11 +140,68 @@ pub struct RendererPageHandle {
_not_send: PhantomData<Rc<()>>,
}
#[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<RendererPageCommandPending> {
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<RendererJavaScriptDialogWatch>,
}
impl RendererPageCommandPending {
pub(super) fn from_owner_reply(reply: oneshot::Receiver<Result<RendererOwnerReply>>) -> Self {
Self {
dispatch: RendererPageCommandPendingDispatch::Owner(reply),
javascript_dialog_watch: None,
}
}
}
enum RendererPageCommandPendingDispatch {
Owner(oneshot::Receiver<anyhow::Result<RendererOwnerReply>>),
InspectorMain(Box<RendererRuntimeInspectorMainCommandRoute>),
@@ -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<RendererDocumentCommandTarget> {
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()
}
@@ -1396,18 +1396,22 @@ impl RendererOwnerLocalStore {
fn take_entry_for_command(&mut self, token: RendererPageToken) -> Result<LivePageEntry> {
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())
}
}
}
@@ -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<RendererPageReply> {
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))))
}
@@ -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<RendererPageCommand>,
},
Inspector(RendererInspectorCommandEnvelope),
EvaluateExpression {
expression: String,