mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 08:00:56 +00:00
4208 lines
161 KiB
Rust
4208 lines
161 KiB
Rust
use std::{
|
|
cell::OnceCell,
|
|
collections::{HashMap, HashSet},
|
|
path::PathBuf,
|
|
sync::{
|
|
Arc,
|
|
atomic::{AtomicU64, Ordering},
|
|
},
|
|
};
|
|
|
|
use indexmap::IndexMap;
|
|
use moli_cookie_jar::{StoredCookie, StoredCookieQueryReport};
|
|
use moli_fetch::FetchConfig;
|
|
use parking_lot::Mutex;
|
|
use serde_json::json;
|
|
|
|
use crate::devtools_runtime::{
|
|
DevToolsCommandContext, DevToolsTargetFilterEntry, DevToolsTargetInfo, DevToolsTargetKind,
|
|
};
|
|
use crate::domains::command_output::{BackgroundProtocolEventBuffer, CommandOutputBuffer};
|
|
|
|
use moli_core::{
|
|
LayoutPolicy, OptionalResourceFetchMask, RendererOutputPublicationOrdering,
|
|
RendererOutputTransportMessage,
|
|
network::{SharedWebStorageStore, new_shared_web_storage_store},
|
|
page::{NavigationResponse, Page, SubresourceAuthCredentials},
|
|
runtime::{
|
|
NavigationEngine, NavigationRuntimeConfig, storage_partition::StoragePartitionState,
|
|
},
|
|
};
|
|
|
|
pub const DEFAULT_CDP_PAGE_TARGET_ID: &str = "moli-default";
|
|
pub const DEFAULT_CDP_TAB_TARGET_ID: &str = "moli-default-tab";
|
|
|
|
mod activity_source;
|
|
mod bidi_channel_work;
|
|
mod body_spool;
|
|
mod browser_context;
|
|
mod command_owner_scope;
|
|
mod command_view;
|
|
mod cookie_manager_surface;
|
|
mod cookie_owner;
|
|
mod cookie_policy_surface;
|
|
#[cfg(test)]
|
|
mod cookie_store_boundary;
|
|
mod devtools_command;
|
|
mod dispatch;
|
|
mod downloads;
|
|
mod fetch_support;
|
|
mod inspector_route;
|
|
mod output;
|
|
mod page_state;
|
|
mod popup_activation_work;
|
|
mod popup_navigation_work;
|
|
mod protocol_output;
|
|
mod renderer_command_turn;
|
|
mod resource_runtime_support;
|
|
mod runtime_eval;
|
|
mod runtime_load;
|
|
mod scheduler_hooks;
|
|
mod scheduler_state;
|
|
mod settings;
|
|
#[cfg(test)]
|
|
mod site_data_manager_surface;
|
|
mod state;
|
|
mod target;
|
|
mod top_level_navigation_work;
|
|
|
|
pub use crate::domains::network::IoStreamState;
|
|
#[cfg(test)]
|
|
pub(crate) use bidi_channel_work::BidiChannelOwnerActionKind;
|
|
pub(crate) use bidi_channel_work::{
|
|
BidiChannelListenerResidence, BidiChannelOwnerAction, BidiChannelOwnerActionBody,
|
|
BidiChannelPageOwner,
|
|
};
|
|
pub(crate) use body_spool::{CapturedBody, CapturedBodyWriter};
|
|
pub(crate) use browser_context::{
|
|
PageLifecycleEventsEnableResult, SessionOwnerInspectorEnableResult,
|
|
SessionOwnerRuntimeFrontendEnableResult, TargetEmulationStateUpdate,
|
|
TargetNavigationLoadInputs,
|
|
};
|
|
pub(crate) use command_owner_scope::CommandOwnerScope;
|
|
pub use command_view::Cmd;
|
|
pub(crate) use cookie_manager_surface::BrowserContextCookieManagerSurfaceSnapshot;
|
|
#[cfg(test)]
|
|
pub(crate) use cookie_manager_surface::{
|
|
BrowserContextCookieBackendConnectionState, BrowserContextDefaultCookieWriteUrlSource,
|
|
BrowserContextDocumentCookieCacheLookupResult, BrowserContextFirstCookieRequest,
|
|
BrowserContextStructuredCookieCommandVerdict, BrowserContextStructuredCookieWriteBackendStatus,
|
|
BrowserContextStructuredCookieWriteReadinessStatus,
|
|
};
|
|
#[cfg(test)]
|
|
pub(crate) use cookie_owner::{
|
|
BrowserContextCookieGetFreshnessStatus, BrowserContextCookieSetReadinessStatus,
|
|
};
|
|
pub use devtools_command::DevToolsCommandDispatchOutcome;
|
|
pub(crate) use devtools_command::DevToolsCommandExecutionOutput;
|
|
pub use dispatch::{CdpCommandTaskStep, CompletedCdpCommandDispatch, PendingCdpCommandDispatch};
|
|
pub(crate) use downloads::SharedDownloadRegistry;
|
|
pub(crate) use fetch_support::PendingStreamingDocumentResponseNavigation;
|
|
pub(crate) use fetch_support::{
|
|
ClaimedSubresourceContinueRequest, CompletedFetchResponseBodyStreamReadDispatch,
|
|
PendingFetchResponseBodyStreamRead, PendingFetchResponseBodyStreamReadDispatch,
|
|
PendingFetchResponseBodyStreamReadStart, PendingSubresourceFetchResidence,
|
|
};
|
|
pub use fetch_support::{
|
|
DocumentBodySource, FetchAuthChallenge, FetchInterceptionPattern, FetchRequestStage,
|
|
FetchResourceTypeFilter, InFlightSubresourceFetchRequest, PausedDocumentTransfer,
|
|
PausedDocumentTransfers, PendingFetchAuthNavigation, PendingFetchNavigation,
|
|
PendingFetchResponseOpenedBodyStream, PendingSubresourceFetchAuthRequest,
|
|
PendingSubresourceFetchAuthStage, PendingSubresourceFetchAuthStageChain,
|
|
PendingSubresourceFetchOwnerKind, PendingSubresourceFetchRequest,
|
|
PendingSubresourceFetchRequestStage, PendingSubresourceFetchRequestStageChain,
|
|
PendingSubresourceFetchResponseRequest, PendingSubresourceFetchResponseStage,
|
|
PendingSubresourceFetchResponseStageChain, ResponseStageUrlMatchPolicy,
|
|
fetch_subresource_interception_config, fetch_subresource_interception_config_for_patterns,
|
|
};
|
|
pub use moli_protocol_cdp::{
|
|
CdpRendererCommandAccess, CdpRendererCommandPolicy, CdpRendererCommandReplacement,
|
|
CdpRendererCommandReplayDispatch, CdpRequest, ParsedCdpCommand,
|
|
};
|
|
use target::DEFAULT_BROWSER_CONTEXT_ID;
|
|
pub(crate) use target::{
|
|
CdpSessionRoute, DefaultTargetLifecycle, TargetActivationTransition, TargetHandlerAccessMode,
|
|
TargetWorkerProtocolAttachmentIdentity,
|
|
};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum CdpTargetHostLifecycleDelta {
|
|
Created(DevToolsTargetInfo),
|
|
InfoChanged(DevToolsTargetInfo),
|
|
Activated { target_id: String },
|
|
Destroyed { target_id: String },
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct CdpTargetHostLifecycleObserver {
|
|
callback: Arc<dyn Fn(CdpTargetHostLifecycleDelta) + Send + Sync>,
|
|
}
|
|
|
|
impl CdpTargetHostLifecycleObserver {
|
|
pub fn new(callback: impl Fn(CdpTargetHostLifecycleDelta) + Send + Sync + 'static) -> Self {
|
|
Self {
|
|
callback: Arc::new(callback),
|
|
}
|
|
}
|
|
|
|
fn notify(&self, delta: CdpTargetHostLifecycleDelta) {
|
|
(self.callback)(delta);
|
|
}
|
|
}
|
|
|
|
/// The unique authority to publish that one command response has entered the
|
|
/// protocol output sequence.
|
|
///
|
|
/// This value is deliberately not `Clone`: observers may be cloned freely,
|
|
/// but only the command dispatcher may release (or drop/cancel) the waiters
|
|
/// associated with this exact command.
|
|
#[must_use = "dropping the permit cancels observers waiting for this command response"]
|
|
pub struct CommandResponseFlushPermit {
|
|
sender: tokio::sync::watch::Sender<bool>,
|
|
deferred_releases: Arc<Mutex<CommandResponseFlushDeferredReleases>>,
|
|
}
|
|
|
|
struct CommandResponseFlushRelease {
|
|
release: Option<Box<dyn FnOnce() + Send + 'static>>,
|
|
}
|
|
|
|
impl CommandResponseFlushRelease {
|
|
fn new(release: impl FnOnce() + Send + 'static) -> Self {
|
|
Self {
|
|
release: Some(Box::new(release)),
|
|
}
|
|
}
|
|
|
|
fn run(mut self) {
|
|
self.run_inner();
|
|
}
|
|
|
|
fn run_inner(&mut self) {
|
|
if let Some(release) = self.release.take() {
|
|
release();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for CommandResponseFlushRelease {
|
|
fn drop(&mut self) {
|
|
self.run_inner();
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct CommandResponseFlushDeferredReleases {
|
|
finished: bool,
|
|
releases: Vec<CommandResponseFlushRelease>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct DevToolsDocumentLifecycleWaitKey {
|
|
registration_id: state::RendererDocumentLifecycleWaiterId,
|
|
renderer_document: moli_core::page::RendererDocumentToken,
|
|
renderer_epoch: moli_core::page::RendererLifecycleEpoch,
|
|
milestone: moli_core::page::RendererDocumentLifecycleMilestone,
|
|
frame_id: String,
|
|
loader_id: String,
|
|
}
|
|
|
|
impl DevToolsDocumentLifecycleWaitKey {
|
|
pub fn frame_id(&self) -> &str {
|
|
self.frame_id.as_str()
|
|
}
|
|
|
|
pub fn milestone(&self) -> moli_core::page::RendererDocumentLifecycleMilestone {
|
|
self.milestone
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum DevToolsDocumentLifecycleWaitState {
|
|
Pending,
|
|
Reached,
|
|
Interrupted,
|
|
Superseded,
|
|
Unavailable,
|
|
}
|
|
|
|
/// Current top-level Document readiness for one exact DevTools target route.
|
|
///
|
|
/// This deliberately distinguishes a live target that has not committed its
|
|
/// next Document from a target that no longer exists. WebDriver uses the
|
|
/// distinction to wait at the browsing-context boundary instead of probing a
|
|
/// renderer command until it happens to stop returning `NoDocumentLoaded`.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum DevToolsDocumentNavigationState {
|
|
Unavailable,
|
|
PendingNavigation,
|
|
AwaitingCommit,
|
|
Committed { loader_id: String },
|
|
}
|
|
|
|
fn devtools_document_lifecycle_wait_state_for_slot(
|
|
slot: &TargetRuntimeSlot,
|
|
key: &DevToolsDocumentLifecycleWaitKey,
|
|
) -> DevToolsDocumentLifecycleWaitState {
|
|
let page_slot = slot.page_slot();
|
|
let Some(outcome) = page_slot.renderer_document_lifecycle_waiter_outcome(
|
|
key.registration_id,
|
|
key.renderer_document,
|
|
key.renderer_epoch,
|
|
&key.frame_id,
|
|
&key.loader_id,
|
|
) else {
|
|
return DevToolsDocumentLifecycleWaitState::Superseded;
|
|
};
|
|
match outcome {
|
|
moli_core::page::RendererDocumentLifecycleWaitOutcome::Reached(_) => {
|
|
DevToolsDocumentLifecycleWaitState::Reached
|
|
}
|
|
moli_core::page::RendererDocumentLifecycleWaitOutcome::Interrupted(_) => {
|
|
DevToolsDocumentLifecycleWaitState::Interrupted
|
|
}
|
|
moli_core::page::RendererDocumentLifecycleWaitOutcome::Pending => {
|
|
match page_slot.renderer_document_lifecycle_binding() {
|
|
None => DevToolsDocumentLifecycleWaitState::Unavailable,
|
|
Some(binding)
|
|
if binding.renderer_document == key.renderer_document
|
|
&& binding.renderer_epoch == key.renderer_epoch
|
|
&& binding.frame_id == key.frame_id
|
|
&& binding.loader_id == key.loader_id =>
|
|
{
|
|
DevToolsDocumentLifecycleWaitState::Pending
|
|
}
|
|
Some(_) => DevToolsDocumentLifecycleWaitState::Superseded,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CommandResponseFlushPermit {
|
|
fn finish_deferred_releases(&self) -> Vec<CommandResponseFlushRelease> {
|
|
let mut deferred = self.deferred_releases.lock();
|
|
if deferred.finished {
|
|
Vec::new()
|
|
} else {
|
|
deferred.finished = true;
|
|
std::mem::take(&mut deferred.releases)
|
|
}
|
|
}
|
|
|
|
pub fn finish(self) {
|
|
let releases = self.finish_deferred_releases();
|
|
let _ = self.sender.send(true);
|
|
for release in releases {
|
|
release.run();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for CommandResponseFlushPermit {
|
|
fn drop(&mut self) {
|
|
for release in self.finish_deferred_releases() {
|
|
release.run();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Cloneable, read-only observation of one command response flush.
|
|
///
|
|
/// Cloning this context creates another observer of the same command. It never
|
|
/// creates another authority capable of releasing that command's waiters.
|
|
#[derive(Clone, Default)]
|
|
pub struct CommandResponseFlushContext {
|
|
receiver: Option<tokio::sync::watch::Receiver<bool>>,
|
|
deferred_releases: Option<Arc<Mutex<CommandResponseFlushDeferredReleases>>>,
|
|
}
|
|
|
|
impl CommandResponseFlushContext {
|
|
fn new(
|
|
receiver: tokio::sync::watch::Receiver<bool>,
|
|
deferred_releases: Arc<Mutex<CommandResponseFlushDeferredReleases>>,
|
|
) -> Self {
|
|
Self {
|
|
receiver: Some(receiver),
|
|
deferred_releases: Some(deferred_releases),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn is_active(&self) -> bool {
|
|
self.receiver.is_some()
|
|
}
|
|
|
|
pub(crate) fn receiver(&self) -> Option<tokio::sync::watch::Receiver<bool>> {
|
|
self.receiver.clone()
|
|
}
|
|
|
|
pub(crate) fn defer_until_response_flush(&self, release: impl FnOnce() + Send + 'static) {
|
|
let release = CommandResponseFlushRelease::new(release);
|
|
let immediate = match &self.deferred_releases {
|
|
Some(deferred_releases) => {
|
|
let mut deferred = deferred_releases.lock();
|
|
if deferred.finished {
|
|
Some(release)
|
|
} else {
|
|
deferred.releases.push(release);
|
|
None
|
|
}
|
|
}
|
|
None => Some(release),
|
|
};
|
|
if let Some(release) = immediate {
|
|
release.run();
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct CommandDispatchContext {
|
|
response_flush: CommandResponseFlushContext,
|
|
terminal_response_delivery_override: Option<moli_page_types::RendererInspectorResponseDelivery>,
|
|
protocol_events: Vec<BackgroundProtocolEvent>,
|
|
post_renderer_output_events: Vec<BackgroundProtocolEvent>,
|
|
renderer_output_boundary: Option<moli_core::RendererOutputFence>,
|
|
post_response_events: Vec<BackgroundProtocolEvent>,
|
|
renderer_output_predecessor: Option<moli_core::RendererOutputFence>,
|
|
}
|
|
|
|
impl CommandDispatchContext {
|
|
pub fn new(response_flush: CommandResponseFlushContext) -> Self {
|
|
Self {
|
|
response_flush,
|
|
terminal_response_delivery_override: None,
|
|
protocol_events: Vec::new(),
|
|
post_renderer_output_events: Vec::new(),
|
|
renderer_output_boundary: None,
|
|
post_response_events: Vec::new(),
|
|
renderer_output_predecessor: None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn response_flush(&self) -> &CommandResponseFlushContext {
|
|
&self.response_flush
|
|
}
|
|
|
|
pub(crate) fn set_terminal_response_delivery_override(
|
|
&mut self,
|
|
response_delivery: moli_page_types::RendererInspectorResponseDelivery,
|
|
) {
|
|
self.terminal_response_delivery_override = Some(response_delivery);
|
|
}
|
|
|
|
pub(crate) const fn terminal_response_delivery_override(
|
|
&self,
|
|
) -> Option<moli_page_types::RendererInspectorResponseDelivery> {
|
|
self.terminal_response_delivery_override
|
|
}
|
|
|
|
pub(crate) fn push_protocol_event(&mut self, event: BackgroundProtocolEvent) {
|
|
self.protocol_events_mut().push(event);
|
|
}
|
|
|
|
pub(crate) fn protocol_events_mut(&mut self) -> &mut Vec<BackgroundProtocolEvent> {
|
|
if self.renderer_output_boundary.is_some() {
|
|
&mut self.post_renderer_output_events
|
|
} else {
|
|
&mut self.protocol_events
|
|
}
|
|
}
|
|
|
|
pub(crate) fn protocol_events_len(&self) -> usize {
|
|
self.protocol_events.len() + self.post_renderer_output_events.len()
|
|
}
|
|
|
|
pub(crate) fn take_protocol_events(&mut self) -> Vec<BackgroundProtocolEvent> {
|
|
assert!(
|
|
self.renderer_output_boundary.is_none(),
|
|
"an exact renderer boundary must be consumed with both protocol-event segments"
|
|
);
|
|
std::mem::take(&mut self.protocol_events)
|
|
}
|
|
|
|
pub(crate) fn append_renderer_fenced_protocol_events(
|
|
&mut self,
|
|
before_boundary: Vec<BackgroundProtocolEvent>,
|
|
boundary: Option<moli_core::RendererOutputFence>,
|
|
after_boundary: Vec<BackgroundProtocolEvent>,
|
|
) {
|
|
self.protocol_events_mut().extend(before_boundary);
|
|
let Some(boundary) = boundary else {
|
|
assert!(
|
|
after_boundary.is_empty(),
|
|
"post-renderer events require an exact renderer boundary"
|
|
);
|
|
return;
|
|
};
|
|
assert!(
|
|
self.renderer_output_boundary.is_none(),
|
|
"one command turn cannot contain multiple renderer insertion boundaries"
|
|
);
|
|
self.renderer_output_boundary = Some(boundary);
|
|
self.post_renderer_output_events.extend(after_boundary);
|
|
}
|
|
|
|
pub(crate) fn take_renderer_fenced_protocol_events(
|
|
&mut self,
|
|
) -> (
|
|
Vec<BackgroundProtocolEvent>,
|
|
Option<moli_core::RendererOutputFence>,
|
|
Vec<BackgroundProtocolEvent>,
|
|
) {
|
|
(
|
|
std::mem::take(&mut self.protocol_events),
|
|
self.renderer_output_boundary.take(),
|
|
std::mem::take(&mut self.post_renderer_output_events),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn extend_post_response_events(
|
|
&mut self,
|
|
events: impl IntoIterator<Item = BackgroundProtocolEvent>,
|
|
) {
|
|
self.post_response_events.extend(events);
|
|
}
|
|
|
|
pub(crate) fn take_protocol_events_before_events(
|
|
&mut self,
|
|
events: Vec<BackgroundProtocolEvent>,
|
|
) -> Vec<BackgroundProtocolEvent> {
|
|
assert!(
|
|
self.renderer_output_boundary.is_none(),
|
|
"an exact renderer boundary cannot be flattened into protocol events"
|
|
);
|
|
let mut protocol_events = self.take_protocol_events();
|
|
protocol_events.extend(events);
|
|
protocol_events
|
|
}
|
|
|
|
pub(crate) fn take_post_response_events(&mut self) -> Vec<BackgroundProtocolEvent> {
|
|
std::mem::take(&mut self.post_response_events)
|
|
}
|
|
|
|
/// Adds one exact concrete renderer cursor that must cross protocol
|
|
/// ingress before this command's response is exposed.
|
|
///
|
|
/// A cursor is source-stream scoped. Deduplication is exact and never
|
|
/// widens the fence into a Page- or process-wide watermark.
|
|
pub(crate) fn set_renderer_output_predecessor(
|
|
&mut self,
|
|
predecessor: moli_core::RendererOutputFence,
|
|
) {
|
|
predecessor.merge_into_same_stream_tail(&mut self.renderer_output_predecessor);
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
pub fn take_renderer_output_predecessor(&mut self) -> Option<moli_core::RendererOutputFence> {
|
|
self.renderer_output_predecessor.take()
|
|
}
|
|
}
|
|
|
|
pub(crate) use moli_protocol_cdp::{DEFAULT_LOADER_ID, monotonic_timestamp_seconds};
|
|
pub(crate) use output::NavigationBackgroundEvent;
|
|
pub use output::{
|
|
BackgroundCommandResponsePayload, BackgroundEventSender, BackgroundProtocolEvent,
|
|
PageScreencastFrameMetadata, RuntimeInspectorAsyncCompletionReceiver,
|
|
RuntimeInspectorResponseReady, RuntimeInspectorResponseReadySender, build_event,
|
|
};
|
|
pub(crate) use output::{
|
|
BackgroundCommandResponsePayloadRef, BackgroundServiceWorkerErrorMessage,
|
|
BackgroundServiceWorkerRegistration, BackgroundServiceWorkerVersion,
|
|
build_command_success_response,
|
|
};
|
|
pub(crate) use page_state::{LoadedNavigationPageCommit, LoadedNavigationRendererAttachmentCommit};
|
|
pub(crate) use popup_activation_work::PopupTargetActivationAction;
|
|
pub(crate) use popup_navigation_work::{
|
|
PopupTargetNavigationKind, PopupTargetNavigationOwnerAction,
|
|
};
|
|
pub(crate) use runtime_eval::{
|
|
ClaimedPendingInspectorAwait, ClaimedPendingInspectorAwaitOwner, RuntimeBindingCallEvent,
|
|
RuntimeEnableReplayEvent, renderer_command_turn_frontend_protocol_response,
|
|
runtime_remote_object_ids_in_map,
|
|
};
|
|
pub use runtime_eval::{
|
|
CompletedMoliDiagnosticsDispatch, CompletedRuntimeBindingPageCommandDispatch,
|
|
CompletedRuntimeChildDefaultContextLookupDispatch, CompletedRuntimeEnableEventsDispatch,
|
|
CompletedRuntimeProtocolMessageDispatch, CompletedServiceWorkerRuntimeProtocolMessageDispatch,
|
|
CompletedSharedWorkerRuntimeProtocolMessageDispatch, PendingMoliDiagnosticsDispatch,
|
|
PendingRuntimeBindingPageCommandDispatch, PendingRuntimeChildDefaultContextLookupDispatch,
|
|
PendingRuntimeEnableEventsDispatch, PendingRuntimeProtocolMessageDispatch,
|
|
PendingServiceWorkerRuntimeProtocolMessageDispatch,
|
|
PendingSharedWorkerRuntimeProtocolMessageDispatch,
|
|
};
|
|
pub(crate) use runtime_load::decode_data_url_response;
|
|
pub(crate) use runtime_load::{
|
|
BackgroundNavigationBodyCompletionSink, BackgroundNavigationEarlyResult,
|
|
BackgroundNavigationLoadJob, CompletedInitialDocumentPageBuild, FailedInitialDocumentPageBuild,
|
|
InitialDocumentPageInstallResult, InitialDocumentPageOwner, PausedResponsePreparedDocument,
|
|
PendingInitialDocumentPageBuild, ResponseCommitReady,
|
|
};
|
|
use scheduler_hooks::CdpSchedulerHooks;
|
|
use scheduler_state::CdpConnectionSchedulerState;
|
|
pub use scheduler_state::{CdpRendererOwnerTurnOutcome, CdpSchedulerEvent, CdpTurnOutcome};
|
|
#[cfg(test)]
|
|
pub(crate) use site_data_manager_surface::{
|
|
BrowserContextReservedSiteDataOwnerState, BrowserContextSiteDataManagerOwnerState,
|
|
};
|
|
pub use state::{
|
|
BrowserContext, BrowserWindowBounds, DevToolsPageResidenceIdentity, DocumentStartScript,
|
|
DownloadNavigation, EmulatedDeviceMetrics, EmulatedGeolocationOverride,
|
|
EmulatedGeolocationOverrideState, EmulatedMediaOverrides, IsolatedWorldDefinition,
|
|
LoadedNavigation, NavigationDispatchState, NavigationLoadOutcome, NavigationRequestLoadPolicy,
|
|
PageNavigationHistoryEntry, PageTargetHost, PendingNavigationHistoryUpdate,
|
|
RuntimeBindingDefinition, TargetInfo, URL_BASE,
|
|
};
|
|
pub(crate) use state::{
|
|
BrowserContextPageStorageHandles, BrowserContextResourceStorageHandles,
|
|
BrowserContextStoragePartitionHandles, CommittedRendererAgentAttachment,
|
|
CommittedRendererDocumentBinding, CompletedDownloadBody, CompletedDownloadBodyArtifact,
|
|
DedicatedWorkerMainScriptOutcome, DedicatedWorkerMainScriptSnapshot,
|
|
DedicatedWorkerTargetState, DevToolsBrowserIdentityOverride, DevToolsConsoleOutputSessionState,
|
|
DevToolsEmulationSessionState, DevToolsLogViolationThreshold, DocumentNavigationToken,
|
|
DuplicatePendingRendererCommand, EffectiveTargetEmulationState,
|
|
EffectiveTargetEmulationStateDelta, EmulatedNetworkConditions, EmulatedViewportSurface,
|
|
InspectorCommandDispatch, NETWORK_ERROR_PAGE_URL, NavigationResultProjection,
|
|
NavigationSourceDocumentSecurityContext, NetworkErrorPageNavigation, PageScreencastConfig,
|
|
PageScreencastFormat, PendingBidiChannelListener, PendingInspectorAwait,
|
|
PendingRendererCommandKey, PerformanceTimeDomain, PreparedRendererCallDispatch, ProfilerAction,
|
|
ProfilerInspectorCommand, RendererCommandCorrelation, RendererCommandDescriptor,
|
|
RendererCommandReplay, RendererDocumentLifecycleObservation, RendererDocumentLifecycleObserver,
|
|
RendererMainDocumentCommitSeed, RendererPageResidenceIdentity,
|
|
ServiceWorkerRuntimeExceptionSnapshot, ServiceWorkerTargetState, SharedWorkerTargetState,
|
|
SiteDataClearOptions, TargetIdentityState, TargetInitialEmptyDocumentCreator, TargetOwnerState,
|
|
TargetPageAttachmentId, TargetPageProtocolAttachmentIdentity, TargetPageResidenceIdentity,
|
|
TargetPageResidenceObservation, TargetPageResidenceToken, TargetPageSessionState,
|
|
TargetPreparedJavaScriptDialog, TargetPreparedJavaScriptDialogRoute,
|
|
TargetRootDocumentProtocolAttachmentIdentity, TargetRuntimeSlot,
|
|
TargetServiceWorkerProtocolAttachmentIdentity, TargetServiceWorkerProtocolAttachmentRetirement,
|
|
TargetServiceWorkerRunIdentity, TargetServiceWorkerRunRetirement,
|
|
TargetServiceWorkerRuntimeAttachmentIdentity, TargetServiceWorkerVersionIdentity,
|
|
TargetServiceWorkerVersionRetirement, TargetSharedWorkerProtocolAttachmentIdentity,
|
|
TargetSharedWorkerProtocolAttachmentRetirement, TargetWindowSurfaceState,
|
|
viewport_surface_install_script,
|
|
};
|
|
#[cfg(test)]
|
|
pub(crate) use state::{
|
|
DevToolsSessionState, TargetJavaScriptDialog, TargetJavaScriptDialogScopeObserver,
|
|
TargetPageSlot, TargetRuntimeSessionState,
|
|
};
|
|
pub(crate) use target::{
|
|
PreparedTargetAttach, PreparedTargetHostClosure, PreparedTargetHostDelta, SessionDisposalPlan,
|
|
SessionDisposalTarget, TargetAttachSessionCommit, TargetClosureCleanupPlan, TargetEventPlan,
|
|
TargetSessionDetachCleanupPlan,
|
|
};
|
|
use target::{
|
|
TargetClosurePlan, TargetControlPlane, TargetHostDelta, target_destroyed_automation_events,
|
|
};
|
|
pub(crate) use top_level_navigation_work::TopLevelLocationNavigationOwnerAction;
|
|
|
|
pub struct PendingDeferredMainDocumentLoadCompletion {
|
|
inner: crate::domains::activity::PendingDeferredMainDocumentLoadCompletionActivity,
|
|
}
|
|
|
|
pub struct CompletedDeferredMainDocumentLoadCompletion {
|
|
inner: crate::domains::activity::CompletedDeferredMainDocumentLoadCompletionActivity,
|
|
}
|
|
|
|
/// Stable identity of one exact deferred-load lifecycle observation.
|
|
///
|
|
/// The protocol owner allocates this identity before an adapter starts an
|
|
/// asynchronous wait. CDP, BiDi, and Classic carry it through the typed
|
|
/// completion instead of manufacturing adapter-local observation generations.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct DeferredMainDocumentLoadObservationId(u64);
|
|
|
|
impl DeferredMainDocumentLoadObservationId {
|
|
#[cfg(feature = "test-support")]
|
|
pub(crate) fn from_test_value(value: u64) -> Self {
|
|
assert_ne!(value, 0, "load observation identity starts at one");
|
|
Self(value)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct DeferredMainDocumentLoadCompletionOutputInterest {
|
|
renderer_page: Option<RendererPageResidenceIdentity>,
|
|
renderer_document: Option<moli_core::RendererDocumentLifecycleIdentity>,
|
|
}
|
|
|
|
/// Exact scope of concrete renderer output that may still acquire a
|
|
/// main-document load predecessor from the command turn currently completing.
|
|
///
|
|
/// This value is derived while consuming a one-shot renderer publication. It
|
|
/// retains only the Page/Document identity needed for a later load action to
|
|
/// prove causality; it carries neither a renderer source capability nor
|
|
/// permission to rescan Page state.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct DeferredMainDocumentLoadPredecessorCandidate {
|
|
renderer_page: RendererPageResidenceIdentity,
|
|
renderer_document: moli_core::RendererDocumentLifecycleIdentity,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum DeferredMainDocumentLoadCompletionOutputAction {
|
|
ProcessNow,
|
|
Queue,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub(crate) struct ConnectionNetworkRequestIdAllocator {
|
|
next_sequence: u64,
|
|
}
|
|
|
|
impl ConnectionNetworkRequestIdAllocator {
|
|
pub(crate) fn allocate_sequence(&mut self) -> u64 {
|
|
self.next_sequence = self
|
|
.next_sequence
|
|
.checked_add(1)
|
|
.expect("connection network request id sequence exhausted");
|
|
self.next_sequence
|
|
}
|
|
|
|
pub(crate) fn allocate_request_id(&mut self) -> String {
|
|
format!("REQ-{}", self.allocate_sequence())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn next_sequence_for_test(&self) -> u64 {
|
|
self.next_sequence
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) struct IdleNavigationEngineReleaseResult {
|
|
pub(crate) reset: bool,
|
|
pub(crate) reason: &'static str,
|
|
pub(crate) loaded_browser_context_count: usize,
|
|
pub(crate) live_target_browser_context_count: usize,
|
|
}
|
|
|
|
impl IdleNavigationEngineReleaseResult {
|
|
pub(crate) fn to_protocol_json(self) -> serde_json::Value {
|
|
json!({
|
|
"reset": self.reset,
|
|
"reason": self.reason,
|
|
"loadedBrowserContextCount": self.loaded_browser_context_count,
|
|
"liveTargetBrowserContextCount": self.live_target_browser_context_count,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl PendingDeferredMainDocumentLoadCompletion {
|
|
pub(crate) fn new(
|
|
inner: crate::domains::activity::PendingDeferredMainDocumentLoadCompletionActivity,
|
|
) -> Self {
|
|
Self { inner }
|
|
}
|
|
|
|
pub fn session_id(&self) -> Option<&str> {
|
|
self.inner.session_id()
|
|
}
|
|
|
|
pub fn output_interest(&self) -> DeferredMainDocumentLoadCompletionOutputInterest {
|
|
DeferredMainDocumentLoadCompletionOutputInterest::new(
|
|
self.inner.renderer_page_residence_identity(),
|
|
self.inner.renderer_document_identity(),
|
|
)
|
|
}
|
|
|
|
pub fn observation_id(&self) -> DeferredMainDocumentLoadObservationId {
|
|
self.inner.observation_id()
|
|
}
|
|
|
|
pub async fn wait(self) -> CompletedDeferredMainDocumentLoadCompletion {
|
|
CompletedDeferredMainDocumentLoadCompletion {
|
|
inner: self.inner.wait().await,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CompletedDeferredMainDocumentLoadCompletion {
|
|
pub(crate) fn new(
|
|
inner: crate::domains::activity::CompletedDeferredMainDocumentLoadCompletionActivity,
|
|
) -> Self {
|
|
Self { inner }
|
|
}
|
|
|
|
pub fn session_id(&self) -> Option<&str> {
|
|
self.inner.session_id()
|
|
}
|
|
|
|
pub fn observation_id(&self) -> DeferredMainDocumentLoadObservationId {
|
|
self.inner.observation_id()
|
|
}
|
|
}
|
|
|
|
impl DeferredMainDocumentLoadCompletionOutputInterest {
|
|
pub(crate) fn new(
|
|
renderer_page: Option<RendererPageResidenceIdentity>,
|
|
renderer_document: Option<moli_core::RendererDocumentLifecycleIdentity>,
|
|
) -> Self {
|
|
Self {
|
|
renderer_page,
|
|
renderer_document,
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "test-support")]
|
|
pub(crate) fn from_test_residence(
|
|
renderer_page: RendererPageResidenceIdentity,
|
|
renderer_document: Option<moli_core::RendererDocumentLifecycleIdentity>,
|
|
) -> Self {
|
|
Self::new(Some(renderer_page), renderer_document)
|
|
}
|
|
|
|
pub fn route_output_while_waiting(
|
|
&self,
|
|
message: &RendererOutputTransportMessage,
|
|
) -> DeferredMainDocumentLoadCompletionOutputAction {
|
|
let RendererOutputTransportMessage::Publication(publication) = message else {
|
|
return DeferredMainDocumentLoadCompletionOutputAction::ProcessNow;
|
|
};
|
|
let residence = publication.cursor().stream().residence();
|
|
if !self
|
|
.renderer_page
|
|
.is_some_and(|renderer_page| renderer_page.matches_residence(residence))
|
|
{
|
|
return DeferredMainDocumentLoadCompletionOutputAction::ProcessNow;
|
|
}
|
|
match publication.ordering() {
|
|
RendererOutputPublicationOrdering::AfterPendingPageLoad { source_document }
|
|
if self.renderer_document == Some(source_document) =>
|
|
{
|
|
DeferredMainDocumentLoadCompletionOutputAction::Queue
|
|
}
|
|
RendererOutputPublicationOrdering::Unconstrained
|
|
| RendererOutputPublicationOrdering::AfterPendingPageLoad { .. } => {
|
|
DeferredMainDocumentLoadCompletionOutputAction::ProcessNow
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn observes_predecessor_candidate(
|
|
&self,
|
|
candidate: DeferredMainDocumentLoadPredecessorCandidate,
|
|
) -> bool {
|
|
self.renderer_page == Some(candidate.renderer_page)
|
|
&& self.renderer_document == Some(candidate.renderer_document)
|
|
}
|
|
}
|
|
|
|
impl DeferredMainDocumentLoadPredecessorCandidate {
|
|
/// Selects only work whose browser-visible effects are ordered after the
|
|
/// exact Page's load boundary.
|
|
///
|
|
/// Parser, module, child-frame and ordinary lifecycle output are load
|
|
/// prerequisites and therefore return `None`. A timer is Page-scoped;
|
|
/// lifecycle action output additionally carries its exact source
|
|
/// Document.
|
|
pub fn from_renderer_publication(publication: &RendererOutputTransportMessage) -> Option<Self> {
|
|
let RendererOutputTransportMessage::Publication(publication) = publication else {
|
|
return None;
|
|
};
|
|
let RendererOutputPublicationOrdering::AfterPendingPageLoad { source_document } =
|
|
publication.ordering()
|
|
else {
|
|
return None;
|
|
};
|
|
Some(Self {
|
|
renderer_page: RendererPageResidenceIdentity::from_residence(
|
|
publication.cursor().stream().residence(),
|
|
)
|
|
.expect("post-load publication ordering is only valid for a Page stream"),
|
|
renderer_document: source_document,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(crate) struct RuntimeAwaitJob {
|
|
command_id: u64,
|
|
owner: CommandOwnerScope,
|
|
object_group: Option<String>,
|
|
action: &'static str,
|
|
}
|
|
|
|
impl RuntimeAwaitJob {
|
|
pub(crate) fn new(
|
|
command_id: u64,
|
|
owner: &CommandOwnerScope,
|
|
object_group: Option<&str>,
|
|
action: &'static str,
|
|
) -> Self {
|
|
Self {
|
|
command_id,
|
|
owner: owner.clone(),
|
|
object_group: object_group.map(str::to_owned),
|
|
action,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn trace_fields(&self) -> serde_json::Value {
|
|
json!({
|
|
"commandId": self.command_id,
|
|
"sessionId": self.owner.session_id(),
|
|
"ownerRoute": self.owner.explicit_route().map(|route| format!("{route:?}")),
|
|
"objectGroup": self.object_group,
|
|
"action": self.action,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn session_id(&self) -> Option<String> {
|
|
self.owner.session_id().map(str::to_owned)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct CdpInitialStoragePartition {
|
|
handles: BrowserContextStoragePartitionHandles,
|
|
fallback_session_storage_store: SharedWebStorageStore,
|
|
}
|
|
|
|
impl CdpInitialStoragePartition {
|
|
pub fn memory() -> Self {
|
|
Self::new(BrowserContextStoragePartitionHandles::memory())
|
|
}
|
|
|
|
pub fn with_cookies(cookies: Vec<StoredCookie>) -> Self {
|
|
Self::new(BrowserContextStoragePartitionHandles::with_initial_cookies(
|
|
cookies,
|
|
))
|
|
}
|
|
|
|
fn new(handles: BrowserContextStoragePartitionHandles) -> Self {
|
|
Self {
|
|
handles,
|
|
fallback_session_storage_store: new_shared_web_storage_store(),
|
|
}
|
|
}
|
|
|
|
pub fn from_storage_partition(
|
|
cookies: Vec<StoredCookie>,
|
|
storage_partition: &StoragePartitionState,
|
|
) -> Self {
|
|
Self::new(
|
|
BrowserContextStoragePartitionHandles::from_storage_partition(
|
|
cookies,
|
|
storage_partition,
|
|
),
|
|
)
|
|
}
|
|
|
|
fn into_parts(self) -> (BrowserContextStoragePartitionHandles, SharedWebStorageStore) {
|
|
(self.handles, self.fallback_session_storage_store)
|
|
}
|
|
}
|
|
|
|
struct CdpInitialStoragePartitionOwner {
|
|
handles: BrowserContextStoragePartitionHandles,
|
|
fallback_session_storage_store: SharedWebStorageStore,
|
|
}
|
|
|
|
impl CdpInitialStoragePartitionOwner {
|
|
fn new(
|
|
handles: BrowserContextStoragePartitionHandles,
|
|
fallback_session_storage_store: SharedWebStorageStore,
|
|
) -> Self {
|
|
Self {
|
|
handles,
|
|
fallback_session_storage_store,
|
|
}
|
|
}
|
|
|
|
fn from_initial_storage_partition(
|
|
initial_storage_partition: CdpInitialStoragePartition,
|
|
) -> Self {
|
|
let (handles, fallback_session_storage_store) = initial_storage_partition.into_parts();
|
|
Self::new(handles, fallback_session_storage_store)
|
|
}
|
|
|
|
fn new_default_browser_context(
|
|
&self,
|
|
id: String,
|
|
http_cache_root: Option<PathBuf>,
|
|
http_cache_max_bytes: Option<u64>,
|
|
) -> BrowserContext {
|
|
BrowserContext::new_with_storage_partition_handles_and_http_cache(
|
|
id,
|
|
self.handles.clone(),
|
|
http_cache_root,
|
|
http_cache_max_bytes,
|
|
)
|
|
}
|
|
|
|
fn resource_storage_handles(&self) -> BrowserContextResourceStorageHandles {
|
|
self.handles
|
|
.resource_storage_handles(self.fallback_session_storage_store.clone())
|
|
}
|
|
|
|
fn page_storage_handles(&self) -> BrowserContextPageStorageHandles {
|
|
self.handles
|
|
.page_storage_handles(self.fallback_session_storage_store.clone())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct AutoAttachOwnerPolicy {
|
|
wait_for_debugger_on_start: bool,
|
|
target_filter: CdpTargetFilter,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(crate) struct CdpTargetFilterEntry {
|
|
pub(crate) exclude: bool,
|
|
pub(crate) target_type: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(crate) struct CdpTargetFilter {
|
|
entries: Vec<CdpTargetFilterEntry>,
|
|
}
|
|
|
|
impl CdpTargetFilter {
|
|
pub(crate) fn from_entries(entries: Vec<CdpTargetFilterEntry>) -> Self {
|
|
Self { entries }
|
|
}
|
|
|
|
pub(crate) fn from_devtools_entries(entries: Vec<DevToolsTargetFilterEntry>) -> Self {
|
|
Self {
|
|
entries: entries
|
|
.into_iter()
|
|
.map(|entry| CdpTargetFilterEntry {
|
|
exclude: entry.exclude,
|
|
target_type: entry.target_type,
|
|
})
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn to_devtools_entries(&self) -> Vec<DevToolsTargetFilterEntry> {
|
|
self.entries
|
|
.iter()
|
|
.map(|entry| DevToolsTargetFilterEntry {
|
|
exclude: entry.exclude,
|
|
target_type: entry.target_type.clone(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn default_target_discovery() -> Self {
|
|
Self::default_auto_attach()
|
|
}
|
|
|
|
pub(crate) fn default_auto_attach() -> Self {
|
|
Self {
|
|
entries: vec![
|
|
CdpTargetFilterEntry {
|
|
exclude: true,
|
|
target_type: Some("browser".to_owned()),
|
|
},
|
|
CdpTargetFilterEntry {
|
|
exclude: true,
|
|
target_type: Some("tab".to_owned()),
|
|
},
|
|
CdpTargetFilterEntry {
|
|
exclude: false,
|
|
target_type: None,
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
pub(crate) fn matches(&self, target_type: &str) -> bool {
|
|
for entry in &self.entries {
|
|
if entry
|
|
.target_type
|
|
.as_deref()
|
|
.is_none_or(|entry_type| entry_type == target_type)
|
|
{
|
|
return !entry.exclude;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct ServiceWorkerAutoAttachRelatedOwner {
|
|
owner_session_id: Option<String>,
|
|
browser_context_id: String,
|
|
registration_id: u64,
|
|
base_version_id: u64,
|
|
script_url: String,
|
|
scope_url: String,
|
|
allow_service_worker_targets: bool,
|
|
wait_for_debugger_on_start: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(crate) struct ServiceWorkerAutoAttachRelatedOwnerSession {
|
|
pub(crate) owner_session_id: Option<String>,
|
|
pub(crate) wait_for_debugger_on_start: bool,
|
|
}
|
|
|
|
/// Drop-addressable fallback engine for commands without a concrete Page host.
|
|
///
|
|
/// `CdpConnection::drop` must release this engine before joining its extracted
|
|
/// BrowserContext network roots. A plain field would only be dropped after the
|
|
/// Drop implementation returned, which reverses that order.
|
|
struct StandaloneNavigationEngineSlot {
|
|
engine: OnceCell<NavigationEngine>,
|
|
runtime_config: NavigationRuntimeConfig,
|
|
renderer_publication_sender: Option<moli_core::RendererOutputTransportSender>,
|
|
}
|
|
|
|
impl StandaloneNavigationEngineSlot {
|
|
fn materialized(engine: NavigationEngine) -> Self {
|
|
let runtime_config = engine.runtime_config();
|
|
let slot = Self {
|
|
engine: OnceCell::new(),
|
|
runtime_config,
|
|
renderer_publication_sender: None,
|
|
};
|
|
slot.engine
|
|
.set(engine)
|
|
.expect("fresh standalone navigation engine slot must be empty");
|
|
slot
|
|
}
|
|
|
|
fn deferred(runtime_config: NavigationRuntimeConfig) -> Self {
|
|
Self {
|
|
engine: OnceCell::new(),
|
|
runtime_config,
|
|
renderer_publication_sender: None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn is_materialized(&self) -> bool {
|
|
self.engine.get().is_some()
|
|
}
|
|
|
|
fn runtime_config(&self) -> NavigationRuntimeConfig {
|
|
self.engine
|
|
.get()
|
|
.map(NavigationEngine::runtime_config)
|
|
.unwrap_or_else(|| self.runtime_config.clone())
|
|
}
|
|
|
|
fn fetch_config(&self) -> &moli_fetch::FetchConfig {
|
|
self.engine
|
|
.get()
|
|
.map(NavigationEngine::fetch_config)
|
|
.unwrap_or_else(|| self.runtime_config.fetch_config())
|
|
}
|
|
|
|
fn layout_policy(&self) -> LayoutPolicy {
|
|
self.engine
|
|
.get()
|
|
.map(NavigationEngine::layout_policy)
|
|
.unwrap_or_else(|| self.runtime_config.layout_policy())
|
|
}
|
|
|
|
fn set_renderer_output_transport_sender(
|
|
&mut self,
|
|
sender: moli_core::RendererOutputTransportSender,
|
|
) {
|
|
if let Some(engine) = self.engine.get() {
|
|
engine.set_renderer_output_transport_sender(sender.clone());
|
|
}
|
|
self.renderer_publication_sender = Some(sender);
|
|
}
|
|
|
|
fn replace(&mut self, engine: NavigationEngine) -> Option<NavigationEngine> {
|
|
self.runtime_config = engine.runtime_config();
|
|
if let Some(sender) = self.renderer_publication_sender.as_ref() {
|
|
engine.set_renderer_output_transport_sender(sender.clone());
|
|
}
|
|
let previous = self.engine.take();
|
|
self.engine
|
|
.set(engine)
|
|
.expect("standalone navigation engine slot must be empty after take");
|
|
previous
|
|
}
|
|
|
|
fn take(&mut self) -> Option<NavigationEngine> {
|
|
self.engine.take()
|
|
}
|
|
|
|
fn ensure(&self) -> &NavigationEngine {
|
|
self.engine.get_or_init(|| {
|
|
let engine = NavigationEngine::new_with_runtime_config(self.runtime_config.clone());
|
|
if let Some(sender) = self.renderer_publication_sender.as_ref() {
|
|
engine.set_renderer_output_transport_sender(sender.clone());
|
|
}
|
|
engine
|
|
})
|
|
}
|
|
|
|
fn ensure_mut(&mut self) -> &mut NavigationEngine {
|
|
self.ensure();
|
|
self.engine
|
|
.get_mut()
|
|
.expect("standalone navigation engine was just materialized")
|
|
}
|
|
}
|
|
|
|
/// Persistent per-connection state.
|
|
pub struct CdpConnection {
|
|
// Browser/session routing state.
|
|
pub browser_context: Option<BrowserContext>,
|
|
pub inactive_browser_contexts: Vec<BrowserContext>,
|
|
/// Root/browser owner Target discovery mirror for diagnostics and
|
|
/// cross-crate schedulers. Event routing uses `target_handlers`.
|
|
pub(crate) target_discovery_enabled: bool,
|
|
/// Whether URL/title changes should be surfaced through
|
|
/// Target.targetInfoChanged for the root/browser owner. Event routing uses
|
|
/// `target_handlers`.
|
|
pub(crate) target_info_change_events_enabled: bool,
|
|
pub(crate) target_discovery_filter: Option<Vec<DevToolsTargetFilterEntry>>,
|
|
/// Chromium/Playwright auto-attach can ask new targets to wait until
|
|
/// Runtime.runIfWaitingForDebugger before their initial document proceeds.
|
|
// Insertion order is protocol state: the first matching owner supplies
|
|
// the primary auto-attached Page session, while later owners contribute
|
|
// additional attached sessions. A randomized HashMap iteration order made that
|
|
// choice vary between otherwise identical processes.
|
|
auto_attach_owner_sessions: IndexMap<Option<String>, AutoAttachOwnerPolicy>,
|
|
target_control: TargetControlPlane,
|
|
default_target_lifecycle: DefaultTargetLifecycle,
|
|
service_worker_auto_attach_related_owners: Vec<ServiceWorkerAutoAttachRelatedOwner>,
|
|
service_worker_pause_on_start_owner_sessions: HashSet<Option<String>>,
|
|
dedicated_worker_pause_on_start_owner_sessions: HashSet<Option<String>>,
|
|
install_default_target_on_auto_attach: bool,
|
|
next_bc_id: u32,
|
|
next_target_id: u32,
|
|
shared_target_id_allocator: Option<Arc<AtomicU64>>,
|
|
next_tab_target_id: u32,
|
|
shared_tab_target_id_allocator: Option<Arc<AtomicU64>>,
|
|
next_session_id: u32,
|
|
next_page_domain_subscription_generation: u64,
|
|
next_internal_runtime_command_id: u64,
|
|
network_request_id_allocator: ConnectionNetworkRequestIdAllocator,
|
|
pending_runtime_await_jobs: HashMap<PendingRendererCommandKey, RuntimeAwaitJob>,
|
|
claimed_pending_inspector_await_owners:
|
|
HashMap<PendingRendererCommandKey, ClaimedPendingInspectorAwaitOwner>,
|
|
|
|
// Browser profile, permissions, download and global IO state.
|
|
pub window_bounds: BrowserWindowBounds,
|
|
pub download_behavior: BrowserDownloadBehavior,
|
|
pub permission_overrides: Vec<PermissionOverride>,
|
|
next_global_io_stream_id: u64,
|
|
base_browser_identity: moli_browser_profile::BrowserIdentityProfile,
|
|
global_extra_headers: Vec<(String, String)>,
|
|
global_browser_identity_override: Option<moli_browser_profile::BrowserIdentityProfile>,
|
|
global_network_conditions: Option<EmulatedNetworkConditions>,
|
|
global_geolocation_override: Option<EmulatedGeolocationOverrideState>,
|
|
global_cache_disabled: bool,
|
|
pub(crate) network_data_collectors: crate::domains::network::NetworkDataCollectorStore,
|
|
base_http_proxy: Option<String>,
|
|
base_http_no_proxy: Option<String>,
|
|
base_tls_verify_host: bool,
|
|
initial_storage_partition: CdpInitialStoragePartitionOwner,
|
|
pub(crate) global_io_streams: HashMap<String, IoStreamState>,
|
|
pub(crate) tracing_state: crate::domains::tracing::TracingState,
|
|
download_registry: SharedDownloadRegistry,
|
|
|
|
// Transport/scheduler integration hooks. These are channels out of the
|
|
// renderer/browser owner into the outer CDP scheduler; they should not grow
|
|
// into protocol routing state.
|
|
scheduler_hooks: CdpSchedulerHooks,
|
|
target_host_lifecycle_observer: Option<CdpTargetHostLifecycleObserver>,
|
|
|
|
// Scheduler-visible queues that are still stored on the connection while
|
|
// source-specific queue ownership is being migrated outward.
|
|
scheduler_state: CdpConnectionSchedulerState,
|
|
|
|
// Standalone navigation state for commands that have no concrete Page
|
|
// owner. Every page-owned engine lives in its stable PageTargetHost.
|
|
standalone_navigation_engine: StandaloneNavigationEngineSlot,
|
|
}
|
|
|
|
impl Default for CdpConnection {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Drop for CdpConnection {
|
|
fn drop(&mut self) {
|
|
// Separate per-context roots from target/page state so teardown can
|
|
// follow the observable producer order even though NavigationEngines
|
|
// and detached local tasks retain only weak owner access.
|
|
let mut contexts = Vec::new();
|
|
if let Some(context) = self.browser_context.take() {
|
|
contexts.push(context);
|
|
}
|
|
contexts.append(&mut self.inactive_browser_contexts);
|
|
let roots = contexts
|
|
.iter_mut()
|
|
.filter_map(BrowserContext::take_renderer_runtime_owner_for_teardown)
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut roots = roots;
|
|
for root in &mut roots {
|
|
root.terminate_renderer_producers_for_owner_shutdown();
|
|
}
|
|
|
|
// Dropping contexts releases every PageTargetHost and its Page state.
|
|
drop(contexts);
|
|
|
|
// RenderRuntimeOwner joins happen when the last JsRuntime-backed
|
|
// NavigationEngine handle is released. Do that explicitly here rather
|
|
// than relying on field drop after this method returns.
|
|
drop(self.standalone_navigation_engine.take());
|
|
|
|
// Only after every Page engine and the standalone fallback are gone
|
|
// may the context roots close fetch admission and join network owners.
|
|
for root in &mut roots {
|
|
root.shutdown_network_and_join();
|
|
}
|
|
drop(roots);
|
|
}
|
|
}
|
|
|
|
impl CdpConnection {
|
|
pub fn new() -> Self {
|
|
Self::new_with_initial_storage_partition(CdpInitialStoragePartition::memory())
|
|
}
|
|
|
|
pub(crate) fn layout_policy(&self) -> LayoutPolicy {
|
|
self.browser_context
|
|
.as_ref()
|
|
.and_then(|context| context.page_targets.active())
|
|
.and_then(PageTargetHost::navigation_engine)
|
|
.map(NavigationEngine::layout_policy)
|
|
.unwrap_or_else(|| self.standalone_navigation_engine.layout_policy())
|
|
}
|
|
|
|
pub(crate) fn active_navigation_engine(&self) -> &NavigationEngine {
|
|
if let Some(engine) = self
|
|
.browser_context
|
|
.as_ref()
|
|
.and_then(|context| context.page_targets.active())
|
|
.and_then(PageTargetHost::navigation_engine)
|
|
{
|
|
return engine;
|
|
}
|
|
self.standalone_navigation_engine.ensure()
|
|
}
|
|
|
|
pub(crate) fn active_navigation_engine_mut(&mut self) -> &mut NavigationEngine {
|
|
let active_owner = self
|
|
.browser_context
|
|
.as_ref()
|
|
.and_then(|context| Some((context.id.clone(), context.active_target_id()?.to_owned())));
|
|
if let Some((browser_context_id, target_id)) = active_owner {
|
|
return self
|
|
.ensure_page_navigation_engine_for_target(&browser_context_id, &target_id)
|
|
.expect("active PageTargetHost navigation engine disappeared");
|
|
}
|
|
self.standalone_navigation_engine.ensure_mut()
|
|
}
|
|
|
|
pub(crate) fn ensure_page_navigation_engine_for_target(
|
|
&mut self,
|
|
browser_context_id: &str,
|
|
target_id: &str,
|
|
) -> Option<&mut NavigationEngine> {
|
|
let needs_engine = self
|
|
.browser_context_by_id(browser_context_id)?
|
|
.page_navigation_engine(target_id)
|
|
.is_none();
|
|
if needs_engine {
|
|
let config = self.standalone_navigation_engine.runtime_config();
|
|
let sender = self.scheduler_hooks.renderer_publication_sender();
|
|
self.browser_context_by_id_mut(browser_context_id)?
|
|
.bind_page_navigation_engines(config, sender);
|
|
}
|
|
self.browser_context_by_id_mut(browser_context_id)?
|
|
.page_navigation_engine_mut(target_id)
|
|
}
|
|
|
|
pub fn has_pending_javascript_dialog(&self) -> bool {
|
|
self.browser_context
|
|
.iter()
|
|
.chain(self.inactive_browser_contexts.iter())
|
|
.any(BrowserContext::has_pending_javascript_dialog)
|
|
}
|
|
|
|
pub fn set_automation_javascript_dialog_handler_enabled(&mut self, enabled: bool) -> bool {
|
|
let Some(browser_context) = self.browser_context.as_ref() else {
|
|
return false;
|
|
};
|
|
browser_context
|
|
.renderer_runtime()
|
|
.set_javascript_dialog_handler_enabled(enabled);
|
|
true
|
|
}
|
|
|
|
pub fn new_with_initial_cookies(initial_cookies: Vec<StoredCookie>) -> Self {
|
|
Self::new_with_initial_storage_partition(CdpInitialStoragePartition::with_cookies(
|
|
initial_cookies,
|
|
))
|
|
}
|
|
|
|
pub fn new_with_fetch_config(fetch_config: FetchConfig) -> Self {
|
|
Self::new_with_initial_storage_partition_and_fetch_config(
|
|
CdpInitialStoragePartition::memory(),
|
|
fetch_config,
|
|
)
|
|
}
|
|
|
|
pub fn enable_webdriver_bidi_download_events(&mut self) -> bool {
|
|
self.download_behavior.enable_webdriver_bidi_events()
|
|
}
|
|
|
|
pub fn disable_webdriver_bidi_download_events(&mut self) -> bool {
|
|
self.download_behavior.disable_webdriver_bidi_events()
|
|
}
|
|
|
|
pub fn new_with_initial_storage_partition(
|
|
initial_storage_partition: CdpInitialStoragePartition,
|
|
) -> Self {
|
|
Self::new_with_initial_storage_partition_and_fetch_config(
|
|
initial_storage_partition,
|
|
FetchConfig::default(),
|
|
)
|
|
}
|
|
|
|
pub fn new_with_initial_storage_partition_and_fetch_config(
|
|
initial_storage_partition: CdpInitialStoragePartition,
|
|
fetch_config: FetchConfig,
|
|
) -> Self {
|
|
Self::new_with_initial_storage_partition_fetch_config_and_resource_loading(
|
|
initial_storage_partition,
|
|
fetch_config,
|
|
OptionalResourceFetchMask::NONE,
|
|
true,
|
|
)
|
|
}
|
|
|
|
pub fn new_with_initial_storage_partition_fetch_config_and_image_fetch_enabled(
|
|
initial_storage_partition: CdpInitialStoragePartition,
|
|
fetch_config: FetchConfig,
|
|
image_fetch_enabled: bool,
|
|
) -> Self {
|
|
let optional_resource_fetch_mask = if image_fetch_enabled {
|
|
OptionalResourceFetchMask::IMAGE
|
|
} else {
|
|
OptionalResourceFetchMask::NONE
|
|
};
|
|
Self::new_with_initial_storage_partition_fetch_config_and_resource_loading(
|
|
initial_storage_partition,
|
|
fetch_config,
|
|
optional_resource_fetch_mask,
|
|
true,
|
|
)
|
|
}
|
|
|
|
pub fn new_with_initial_storage_partition_fetch_config_and_resource_loading(
|
|
initial_storage_partition: CdpInitialStoragePartition,
|
|
fetch_config: FetchConfig,
|
|
optional_resource_fetch_mask: OptionalResourceFetchMask,
|
|
subframe_loading_enabled: bool,
|
|
) -> Self {
|
|
Self::new_with_initial_storage_partition_and_runtime_config(
|
|
initial_storage_partition,
|
|
NavigationRuntimeConfig::new(
|
|
fetch_config,
|
|
optional_resource_fetch_mask,
|
|
subframe_loading_enabled,
|
|
LayoutPolicy::default(),
|
|
),
|
|
)
|
|
}
|
|
|
|
pub fn new_with_initial_storage_partition_and_runtime_config(
|
|
initial_storage_partition: CdpInitialStoragePartition,
|
|
navigation_runtime_config: NavigationRuntimeConfig,
|
|
) -> Self {
|
|
Self::new_with_initial_storage_partition_owner_and_runtime_config(
|
|
CdpInitialStoragePartitionOwner::from_initial_storage_partition(
|
|
initial_storage_partition,
|
|
),
|
|
navigation_runtime_config,
|
|
)
|
|
}
|
|
|
|
/// Creates protocol state without starting the renderer runtime.
|
|
///
|
|
/// The runtime is materialized on first engine-backed operation. This is
|
|
/// intended for browser-level CDP owners that publish a target before any
|
|
/// frontend attaches to its page.
|
|
pub fn new_with_deferred_navigation_runtime(
|
|
initial_storage_partition: CdpInitialStoragePartition,
|
|
navigation_runtime_config: NavigationRuntimeConfig,
|
|
) -> Self {
|
|
let initial_storage_partition =
|
|
CdpInitialStoragePartitionOwner::from_initial_storage_partition(
|
|
initial_storage_partition,
|
|
);
|
|
Self::new_with_initial_storage_partition_owner_and_engine(
|
|
initial_storage_partition,
|
|
StandaloneNavigationEngineSlot::deferred(navigation_runtime_config),
|
|
)
|
|
}
|
|
|
|
fn new_with_initial_storage_partition_owner_and_runtime_config(
|
|
initial_storage_partition: CdpInitialStoragePartitionOwner,
|
|
navigation_runtime_config: NavigationRuntimeConfig,
|
|
) -> Self {
|
|
Self::new_with_initial_storage_partition_owner_and_engine(
|
|
initial_storage_partition,
|
|
StandaloneNavigationEngineSlot::materialized(
|
|
NavigationEngine::new_with_runtime_config(navigation_runtime_config),
|
|
),
|
|
)
|
|
}
|
|
|
|
fn new_with_initial_storage_partition_owner_and_engine(
|
|
initial_storage_partition: CdpInitialStoragePartitionOwner,
|
|
standalone_navigation_engine: StandaloneNavigationEngineSlot,
|
|
) -> Self {
|
|
let fetch_config = standalone_navigation_engine.fetch_config();
|
|
let base_browser_identity = fetch_config.browser_identity().clone();
|
|
let base_http_proxy = fetch_config.http_proxy().map(str::to_owned);
|
|
let base_http_no_proxy = fetch_config.http_no_proxy().map(str::to_owned);
|
|
let base_tls_verify_host = fetch_config.tls_verify_host();
|
|
Self {
|
|
browser_context: None,
|
|
inactive_browser_contexts: Vec::new(),
|
|
target_discovery_enabled: false,
|
|
target_info_change_events_enabled: false,
|
|
target_discovery_filter: None,
|
|
auto_attach_owner_sessions: IndexMap::new(),
|
|
target_control: TargetControlPlane::default(),
|
|
default_target_lifecycle: DefaultTargetLifecycle::default(),
|
|
service_worker_auto_attach_related_owners: Vec::new(),
|
|
service_worker_pause_on_start_owner_sessions: HashSet::new(),
|
|
dedicated_worker_pause_on_start_owner_sessions: HashSet::new(),
|
|
install_default_target_on_auto_attach: false,
|
|
window_bounds: BrowserWindowBounds::default(),
|
|
download_behavior: BrowserDownloadBehavior::default(),
|
|
permission_overrides: Vec::new(),
|
|
next_bc_id: 0,
|
|
next_global_io_stream_id: 0,
|
|
next_target_id: 0,
|
|
shared_target_id_allocator: None,
|
|
next_tab_target_id: 0,
|
|
shared_tab_target_id_allocator: None,
|
|
next_session_id: 0,
|
|
next_page_domain_subscription_generation: 0,
|
|
next_internal_runtime_command_id: 902_000_000,
|
|
network_request_id_allocator: ConnectionNetworkRequestIdAllocator::default(),
|
|
pending_runtime_await_jobs: HashMap::new(),
|
|
claimed_pending_inspector_await_owners: HashMap::new(),
|
|
base_browser_identity,
|
|
global_extra_headers: Vec::new(),
|
|
global_browser_identity_override: None,
|
|
global_network_conditions: None,
|
|
global_geolocation_override: None,
|
|
global_cache_disabled: false,
|
|
network_data_collectors: crate::domains::network::NetworkDataCollectorStore::default(),
|
|
base_http_proxy,
|
|
base_http_no_proxy,
|
|
base_tls_verify_host,
|
|
initial_storage_partition,
|
|
global_io_streams: HashMap::new(),
|
|
tracing_state: crate::domains::tracing::TracingState::default(),
|
|
download_registry: SharedDownloadRegistry::default(),
|
|
scheduler_hooks: CdpSchedulerHooks::default(),
|
|
target_host_lifecycle_observer: None,
|
|
scheduler_state: CdpConnectionSchedulerState::default(),
|
|
standalone_navigation_engine,
|
|
}
|
|
}
|
|
|
|
pub fn set_background_event_sender(&mut self, sender: BackgroundEventSender) {
|
|
self.scheduler_hooks.set_background_event_sender(sender);
|
|
}
|
|
|
|
pub fn set_shared_target_id_allocator(&mut self, allocator: Arc<AtomicU64>) {
|
|
self.shared_target_id_allocator = Some(allocator);
|
|
}
|
|
|
|
pub fn set_shared_tab_target_id_allocator(&mut self, allocator: Arc<AtomicU64>) {
|
|
self.shared_tab_target_id_allocator = Some(allocator);
|
|
}
|
|
|
|
pub fn set_target_host_lifecycle_observer(&mut self, observer: CdpTargetHostLifecycleObserver) {
|
|
self.target_host_lifecycle_observer = Some(observer);
|
|
}
|
|
|
|
pub fn set_runtime_inspector_response_ready_sender(
|
|
&mut self,
|
|
sender: RuntimeInspectorResponseReadySender,
|
|
) {
|
|
self.scheduler_hooks
|
|
.set_runtime_inspector_response_ready_sender(sender);
|
|
}
|
|
|
|
pub fn set_background_navigation_completion_sender(
|
|
&mut self,
|
|
sender: tokio::sync::mpsc::UnboundedSender<
|
|
crate::domains::page::BackgroundNavigationCompletion,
|
|
>,
|
|
) {
|
|
self.scheduler_hooks
|
|
.set_background_navigation_completion_sender(sender);
|
|
}
|
|
|
|
pub fn set_renderer_publication_sender(
|
|
&mut self,
|
|
sender: moli_core::RendererOutputTransportSender,
|
|
) {
|
|
self.scheduler_hooks
|
|
.set_renderer_publication_sender(sender.clone());
|
|
self.standalone_navigation_engine
|
|
.set_renderer_output_transport_sender(sender.clone());
|
|
for context in self
|
|
.browser_context
|
|
.iter_mut()
|
|
.chain(self.inactive_browser_contexts.iter_mut())
|
|
{
|
|
context.set_renderer_output_transport_sender(sender.clone());
|
|
}
|
|
}
|
|
|
|
pub(crate) fn background_event_sender(&self) -> Option<BackgroundEventSender> {
|
|
self.scheduler_hooks.background_event_sender()
|
|
}
|
|
|
|
pub(crate) fn runtime_inspector_response_ready_sender(
|
|
&self,
|
|
) -> Option<RuntimeInspectorResponseReadySender> {
|
|
self.scheduler_hooks
|
|
.runtime_inspector_response_ready_sender()
|
|
}
|
|
|
|
pub(crate) fn document_navigation_cancellation_handle(
|
|
&self,
|
|
token: &DocumentNavigationToken,
|
|
) -> Option<moli_fetch::FetchCancelHandle> {
|
|
let browser_context_id = self.browser_context_id_for_target(&token.target_id)?;
|
|
self.browser_context_by_id(browser_context_id)?
|
|
.document_navigation_cancellation_handle(token)
|
|
}
|
|
|
|
pub(crate) fn arm_background_navigation_completion(
|
|
&mut self,
|
|
token: &DocumentNavigationToken,
|
|
additional_cancellation: Option<moli_fetch::FetchCancelHandle>,
|
|
) -> bool {
|
|
let browser_context_id = self
|
|
.browser_context_id_for_target(&token.target_id)
|
|
.map(str::to_owned);
|
|
let Some(browser_context_id) = browser_context_id else {
|
|
if let Some(cancellation) = additional_cancellation {
|
|
cancellation.cancel();
|
|
}
|
|
return false;
|
|
};
|
|
self.browser_context_by_id_mut(&browser_context_id)
|
|
.is_some_and(|browser_context| {
|
|
browser_context.arm_background_navigation_completion(token, additional_cancellation)
|
|
})
|
|
}
|
|
|
|
pub(crate) fn settle_background_navigation_completion(
|
|
&mut self,
|
|
token: &DocumentNavigationToken,
|
|
) -> bool {
|
|
let browser_context_id = self
|
|
.browser_context_id_for_target(&token.target_id)
|
|
.map(str::to_owned);
|
|
let Some(browser_context_id) = browser_context_id else {
|
|
return false;
|
|
};
|
|
self.browser_context_by_id_mut(&browser_context_id)
|
|
.is_some_and(|browser_context| {
|
|
browser_context.settle_background_navigation_completion(token)
|
|
})
|
|
}
|
|
|
|
pub fn has_inflight_background_navigation(&self) -> bool {
|
|
self.browser_contexts()
|
|
.any(BrowserContext::has_inflight_background_navigation)
|
|
}
|
|
|
|
pub fn has_inflight_background_navigation_for_target(&self, target_id: &str) -> bool {
|
|
let Some(browser_context_id) = self.browser_context_id_for_target(target_id) else {
|
|
return false;
|
|
};
|
|
self.browser_context_by_id(browser_context_id)
|
|
.is_some_and(|browser_context| {
|
|
browser_context.has_inflight_background_navigation_for_target(target_id)
|
|
})
|
|
}
|
|
|
|
fn target_id_for_session_owner(&self, session_id: Option<&str>) -> Option<String> {
|
|
let owner = CommandOwnerScope::capture(self, session_id);
|
|
self.target_id_for_owner(&owner)
|
|
}
|
|
|
|
fn target_id_for_owner(&self, owner: &CommandOwnerScope) -> Option<String> {
|
|
let (browser_context_id, target_id) = self.target_owner_identity_for_owner(owner)?;
|
|
target_id.or_else(|| {
|
|
self.browser_context_by_id(&browser_context_id)
|
|
.and_then(BrowserContext::active_target_id)
|
|
.map(str::to_owned)
|
|
})
|
|
}
|
|
|
|
pub(crate) fn owner_target_has_waiting_for_debugger_session(
|
|
&self,
|
|
owner: &CommandOwnerScope,
|
|
) -> bool {
|
|
self.target_id_for_owner(owner)
|
|
.is_some_and(|target_id| self.target_has_waiting_for_debugger_session(&target_id))
|
|
}
|
|
|
|
pub fn background_navigation_target_id_for_event(
|
|
&self,
|
|
event: &BackgroundProtocolEvent,
|
|
) -> Option<String> {
|
|
event
|
|
.navigation_gate_target_id()
|
|
.filter(|target_id| self.browser_context_id_for_target(target_id).is_some())
|
|
.map(str::to_owned)
|
|
.or_else(|| self.target_id_for_session_owner(event.protocol_session_id()))
|
|
}
|
|
|
|
pub fn has_inflight_background_navigation_for_devtools_context(
|
|
&self,
|
|
context: &crate::devtools_runtime::DevToolsCommandContext,
|
|
) -> bool {
|
|
context
|
|
.target_id
|
|
.as_ref()
|
|
.map(crate::devtools_runtime::DevToolsTargetId::as_str)
|
|
.map(str::to_owned)
|
|
.or_else(|| {
|
|
self.target_id_for_session_owner(
|
|
context
|
|
.session_id
|
|
.as_ref()
|
|
.map(crate::devtools_runtime::DevToolsSessionId::as_str),
|
|
)
|
|
})
|
|
.is_some_and(|target_id| self.has_inflight_background_navigation_for_target(&target_id))
|
|
}
|
|
|
|
pub fn has_pending_document_navigation_for_session_owner(
|
|
&self,
|
|
session_id: Option<&str>,
|
|
) -> bool {
|
|
let owner = CommandOwnerScope::capture(self, session_id);
|
|
self.has_pending_document_navigation_for_owner(&owner)
|
|
}
|
|
|
|
pub(crate) fn has_pending_document_navigation_for_owner(
|
|
&self,
|
|
owner: &CommandOwnerScope,
|
|
) -> bool {
|
|
let Some((browser_context_id, target_id)) = self.target_owner_identity_for_owner(owner)
|
|
else {
|
|
return false;
|
|
};
|
|
self.browser_context_by_id(&browser_context_id)
|
|
.is_some_and(|browser_context| {
|
|
browser_context.has_pending_document_navigation_for_target(target_id.as_deref())
|
|
})
|
|
}
|
|
|
|
fn document_navigation_state_for_owner(
|
|
&self,
|
|
owner: &CommandOwnerScope,
|
|
) -> DevToolsDocumentNavigationState {
|
|
let Some((browser_context_id, target_id)) = self.target_owner_identity_for_owner(owner)
|
|
else {
|
|
return DevToolsDocumentNavigationState::Unavailable;
|
|
};
|
|
if self
|
|
.browser_context_by_id(&browser_context_id)
|
|
.is_some_and(|browser_context| {
|
|
browser_context.has_pending_document_navigation_for_target(target_id.as_deref())
|
|
})
|
|
{
|
|
return DevToolsDocumentNavigationState::PendingNavigation;
|
|
}
|
|
// The initial empty Document has a real loader identity before any
|
|
// cross-document navigation token exists. Use the same committed
|
|
// frame-tree authority as Page/Runtime routing instead of consulting
|
|
// only `pending_document_navigation` / `committed_document_navigation`.
|
|
// Otherwise a materialized `about:blank` is misclassified as
|
|
// `AwaitingCommit`, and ChromeDriver-style pre-command navigation
|
|
// waits can never complete.
|
|
match self.target_session_owner_frame_tree_loader_id_for_owner(owner) {
|
|
Some(loader_id) => DevToolsDocumentNavigationState::Committed { loader_id },
|
|
None => DevToolsDocumentNavigationState::AwaitingCommit,
|
|
}
|
|
}
|
|
|
|
/// Resolves Document readiness through the exact target captured by a
|
|
/// protocol-neutral command context. A target-id context resolves its
|
|
/// explicit route rather than whichever target is currently active in the
|
|
/// browser context.
|
|
pub fn devtools_context_document_navigation_state(
|
|
&mut self,
|
|
context: &DevToolsCommandContext,
|
|
) -> DevToolsDocumentNavigationState {
|
|
let Some(owner_scope) = self.command_owner_scope_for_devtools_context(context) else {
|
|
return DevToolsDocumentNavigationState::Unavailable;
|
|
};
|
|
self.document_navigation_state_for_owner(&owner_scope)
|
|
}
|
|
|
|
pub fn renderer_document_navigation_is_suspended_for_session_owner(
|
|
&self,
|
|
session_id: Option<&str>,
|
|
) -> bool {
|
|
self.runtime_session_owner_slot(session_id)
|
|
.is_ok_and(TargetRuntimeSlot::renderer_document_navigation_is_suspended)
|
|
}
|
|
|
|
pub(crate) fn accepts_pending_document_navigation_for_owner(
|
|
&self,
|
|
owner: &CommandOwnerScope,
|
|
token: &DocumentNavigationToken,
|
|
) -> bool {
|
|
let Some((browser_context_id, target_id)) = self.target_owner_identity_for_owner(owner)
|
|
else {
|
|
return false;
|
|
};
|
|
if target_id.as_deref() != Some(token.target_id.as_str()) {
|
|
return false;
|
|
}
|
|
self.browser_context_by_id(&browser_context_id)
|
|
.is_some_and(|browser_context| {
|
|
browser_context.accepts_pending_document_navigation_event(token)
|
|
})
|
|
}
|
|
|
|
pub(crate) fn ensure_document_accessible_for_session_owner(
|
|
&self,
|
|
session_id: Option<&str>,
|
|
) -> Result<(), String> {
|
|
let owner = CommandOwnerScope::capture(self, session_id);
|
|
self.ensure_document_accessible_for_owner(&owner)
|
|
}
|
|
|
|
pub(crate) fn ensure_document_accessible_for_owner(
|
|
&self,
|
|
owner: &CommandOwnerScope,
|
|
) -> Result<(), String> {
|
|
if self.has_pending_document_navigation_for_owner(owner) {
|
|
return Err("Navigation is changing the document".to_owned());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn current_document_loader_id_for_session_owner(
|
|
&self,
|
|
session_id: Option<&str>,
|
|
) -> Option<String> {
|
|
let owner = CommandOwnerScope::capture(self, session_id);
|
|
self.current_document_loader_id_for_owner(&owner)
|
|
}
|
|
|
|
pub(crate) fn current_document_loader_id_for_owner(
|
|
&self,
|
|
owner: &CommandOwnerScope,
|
|
) -> Option<String> {
|
|
self.runtime_session_owner_slot_for_owner(owner)
|
|
.ok()
|
|
.and_then(|slot| slot.current_document_loader_id().map(str::to_owned))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn ingest_renderer_network_output_item_and_prepare_live_delivery_for_session_owner(
|
|
&mut self,
|
|
session_id: Option<&str>,
|
|
source_document: moli_core::RendererDocumentLifecycleIdentity,
|
|
item: &moli_core::page::ScriptNetworkOutputItem,
|
|
) -> Option<crate::domains::network::TargetNetworkBacklogPreparedDelivery> {
|
|
let owner = CommandOwnerScope::capture(self, session_id);
|
|
self.ingest_renderer_page_network_output_item_and_prepare_live_delivery_for_owner(
|
|
&owner,
|
|
None,
|
|
source_document,
|
|
item,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn ingest_renderer_page_network_output_item_and_prepare_live_delivery_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
source_renderer_page: Option<RendererPageResidenceIdentity>,
|
|
source_document: moli_core::RendererDocumentLifecycleIdentity,
|
|
item: &moli_core::page::ScriptNetworkOutputItem,
|
|
) -> Option<crate::domains::network::TargetNetworkBacklogPreparedDelivery> {
|
|
let primary_session_id = self.runtime_session_owner_primary_session_id_for_owner(owner);
|
|
let mut request_id_allocator = std::mem::take(&mut self.network_request_id_allocator);
|
|
let delivery = self
|
|
.runtime_session_owner_slot_mut_for_owner(owner)
|
|
.ok()
|
|
.and_then(|slot| {
|
|
slot.ingest_renderer_network_output_item_and_prepare_live_delivery(
|
|
source_renderer_page,
|
|
source_document,
|
|
item,
|
|
owner.session_id(),
|
|
primary_session_id.as_deref(),
|
|
None,
|
|
&mut request_id_allocator,
|
|
)
|
|
});
|
|
self.network_request_id_allocator = request_id_allocator;
|
|
delivery
|
|
}
|
|
|
|
pub(crate) fn loaded_page_mut_for_protocol_access(
|
|
&mut self,
|
|
session_id: Option<&str>,
|
|
) -> Result<&mut Page, String> {
|
|
let owner = CommandOwnerScope::capture(self, session_id);
|
|
self.loaded_page_mut_for_protocol_access_for_owner(&owner)
|
|
}
|
|
|
|
pub(crate) fn loaded_page_mut_for_protocol_access_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
) -> Result<&mut Page, String> {
|
|
self.ensure_document_accessible_for_owner(owner)?;
|
|
self.loaded_page_mut_for_interruptible_protocol_access_for_owner(owner)
|
|
}
|
|
|
|
/// Returns the Page that currently carries target-scoped configuration.
|
|
///
|
|
/// A cross-Document navigation keeps its outgoing Page attached until the
|
|
/// replacement commits. Network and Emulation settings belong to the
|
|
/// stable target/session, so they must remain writable during that window:
|
|
/// the outgoing Page needs the update if navigation fails, while commit
|
|
/// configuration replays the same target state into the replacement Page.
|
|
/// Document-reading commands must continue to use
|
|
/// [`Self::loaded_page_mut_for_protocol_access`] and observe the navigation
|
|
/// gate instead.
|
|
pub(crate) fn loaded_page_mut_for_target_configuration(
|
|
&mut self,
|
|
session_id: Option<&str>,
|
|
) -> Result<&mut Page, String> {
|
|
let owner = CommandOwnerScope::capture(self, session_id);
|
|
self.loaded_page_mut_for_target_configuration_for_owner(&owner)
|
|
}
|
|
|
|
pub(crate) fn loaded_page_mut_for_target_configuration_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
) -> Result<&mut Page, String> {
|
|
self.runtime_session_owner_slot_mut_for_owner(owner)?
|
|
.loaded_page_mut()
|
|
.ok_or_else(|| "NoDocumentLoaded".to_owned())
|
|
}
|
|
|
|
/// Returns the exact Page that remains attached while a cross-Document
|
|
/// navigation is suspended.
|
|
///
|
|
/// Only commands classified as renderer-interruptible at the parsed CDP
|
|
/// boundary may use this access path. Ordinary protocol commands must use
|
|
/// [`Self::loaded_page_mut_for_protocol_access`] so they wait for the
|
|
/// replacement attachment instead of entering the old renderer.
|
|
pub(crate) fn loaded_page_mut_for_interruptible_protocol_access(
|
|
&mut self,
|
|
session_id: Option<&str>,
|
|
) -> Result<&mut Page, String> {
|
|
let owner = CommandOwnerScope::capture(self, session_id);
|
|
self.loaded_page_mut_for_interruptible_protocol_access_for_owner(&owner)
|
|
}
|
|
|
|
pub(crate) fn loaded_page_mut_for_interruptible_protocol_access_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
) -> Result<&mut Page, String> {
|
|
self.runtime_session_owner_slot_mut_for_owner(owner)?
|
|
.loaded_page_mut()
|
|
.ok_or_else(|| "NoDocumentLoaded".to_owned())
|
|
}
|
|
|
|
pub(crate) fn start_document_navigation_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
loader_id: String,
|
|
) -> Option<DocumentNavigationToken> {
|
|
let (browser_context_id, target_id) = self.target_owner_identity_for_owner(owner)?;
|
|
let target_id = target_id?;
|
|
self.browser_context_by_id_mut(&browser_context_id)?
|
|
.start_document_navigation_for_target(&target_id, loader_id)
|
|
}
|
|
|
|
pub(crate) fn commit_document_navigation_for_owner_if_matches(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
token: &DocumentNavigationToken,
|
|
) {
|
|
let Some((browser_context_id, target_id)) = self.target_owner_identity_for_owner(owner)
|
|
else {
|
|
return;
|
|
};
|
|
if target_id.as_deref() != Some(token.target_id.as_str()) {
|
|
return;
|
|
}
|
|
if let Some(browser_context) = self.browser_context_by_id_mut(&browser_context_id) {
|
|
browser_context.commit_document_navigation_if_matches(token);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn bind_renderer_document_lifecycle_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
artifacts: moli_core::page::RendererPageCreationArtifacts,
|
|
navigation: Option<DocumentNavigationToken>,
|
|
frame_id: String,
|
|
loader_id: String,
|
|
) -> (
|
|
Option<CommittedRendererDocumentBinding>,
|
|
Vec<moli_core::page::RendererDocumentLifecycleEvent>,
|
|
) {
|
|
let (binding, events, document_scope_changed) = {
|
|
let Ok(slot) = self.runtime_session_owner_slot_mut_for_owner(owner) else {
|
|
return (None, Vec::new());
|
|
};
|
|
let previous_document_scope = slot
|
|
.page_slot()
|
|
.renderer_document_lifecycle_binding()
|
|
.map(CommittedRendererDocumentBinding::renderer_document_identity);
|
|
let events = slot
|
|
.page_slot_mut()
|
|
.bind_renderer_document_lifecycle(artifacts, navigation, frame_id, loader_id);
|
|
let binding = slot
|
|
.page_slot()
|
|
.renderer_document_lifecycle_binding()
|
|
.cloned();
|
|
let current_document_scope = binding
|
|
.as_ref()
|
|
.map(CommittedRendererDocumentBinding::renderer_document_identity);
|
|
(
|
|
binding,
|
|
events,
|
|
current_document_scope != previous_document_scope,
|
|
)
|
|
};
|
|
if document_scope_changed {
|
|
self.retire_javascript_dialogs_for_owner(owner);
|
|
}
|
|
(binding, events)
|
|
}
|
|
|
|
pub(crate) fn ingest_renderer_document_lifecycle_events_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
events: Vec<moli_core::page::RendererDocumentLifecycleEvent>,
|
|
) -> (
|
|
Option<CommittedRendererDocumentBinding>,
|
|
Vec<moli_core::page::RendererDocumentLifecycleEvent>,
|
|
) {
|
|
let (binding, events, document_scope_changed, document_input_stream_opened) = {
|
|
let Ok(slot) = self.runtime_session_owner_slot_mut_for_owner(owner) else {
|
|
return (None, Vec::new());
|
|
};
|
|
let previous_document_scope = slot
|
|
.page_slot()
|
|
.renderer_document_lifecycle_binding()
|
|
.map(CommittedRendererDocumentBinding::renderer_document_identity);
|
|
let events = slot
|
|
.page_slot_mut()
|
|
.ingest_renderer_document_lifecycle_events(events);
|
|
let binding = slot
|
|
.page_slot()
|
|
.renderer_document_lifecycle_binding()
|
|
.cloned();
|
|
let current_document_scope = binding
|
|
.as_ref()
|
|
.map(CommittedRendererDocumentBinding::renderer_document_identity);
|
|
let document_input_stream_opened = binding.as_ref().is_some_and(|binding| {
|
|
binding.document_open_replacement_epoch == Some(binding.renderer_epoch)
|
|
});
|
|
(
|
|
binding,
|
|
events,
|
|
current_document_scope != previous_document_scope,
|
|
document_input_stream_opened,
|
|
)
|
|
};
|
|
if document_scope_changed {
|
|
self.retire_javascript_dialogs_for_owner(owner);
|
|
}
|
|
if document_input_stream_opened {
|
|
// The concrete lifecycle record is the authoritative notification
|
|
// that `document.open()` replaced the initial empty Document. Do
|
|
// not defer this state transition to a later diagnostics snapshot:
|
|
// that would make a later owner turn rediscover and settle output
|
|
// produced by this renderer turn.
|
|
self.with_target_owner_state_for_owner_mut(owner, |owner_state| {
|
|
owner_state.mark_initial_empty_document_exited()
|
|
});
|
|
}
|
|
(binding, events)
|
|
}
|
|
|
|
fn retire_javascript_dialogs_for_owner(&mut self, owner: &CommandOwnerScope) {
|
|
let event_session_ids = self.page_event_session_ids_for_owner(owner);
|
|
if let Ok(slot) = self.runtime_session_owner_slot_mut_for_owner(owner) {
|
|
slot.retire_javascript_dialog_scope();
|
|
}
|
|
for event_session_id in event_session_ids {
|
|
let event_owner = event_session_id
|
|
.as_deref()
|
|
.map(CommandOwnerScope::for_session)
|
|
.unwrap_or_else(|| owner.clone());
|
|
let _ = self.with_target_devtools_session_state_for_owner_mut(&event_owner, |state| {
|
|
state.page_session_state.javascript_dialog_state.clear()
|
|
});
|
|
}
|
|
}
|
|
|
|
pub(crate) fn begin_renderer_document_load_visibility_barrier_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
loader_id: &str,
|
|
) -> bool {
|
|
self.runtime_session_owner_slot_mut_for_owner(owner)
|
|
.is_ok_and(|slot| {
|
|
slot.page_slot_mut()
|
|
.begin_renderer_document_load_visibility_barrier(loader_id)
|
|
})
|
|
}
|
|
|
|
pub(crate) fn release_renderer_document_load_visibility_barrier_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
loader_id: &str,
|
|
) -> Option<Vec<moli_core::page::RendererDocumentLifecycleEvent>> {
|
|
self.runtime_session_owner_slot_mut_for_owner(owner)
|
|
.ok()?
|
|
.page_slot_mut()
|
|
.release_renderer_document_load_visibility_barrier(loader_id)
|
|
}
|
|
|
|
pub(crate) fn cancel_renderer_document_load_visibility_barrier_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
loader_id: &str,
|
|
) -> bool {
|
|
self.runtime_session_owner_slot_mut_for_owner(owner)
|
|
.is_ok_and(|slot| {
|
|
slot.page_slot_mut()
|
|
.cancel_renderer_document_load_visibility_barrier(loader_id)
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn renderer_document_lifecycle_authoritative_state_for_session_owner(
|
|
&self,
|
|
session_id: Option<&str>,
|
|
) -> Option<(
|
|
CommittedRendererDocumentBinding,
|
|
moli_core::page::RendererDocumentLifecycleSnapshot,
|
|
)> {
|
|
let page_slot = self
|
|
.runtime_session_owner_slot(session_id)
|
|
.ok()?
|
|
.page_slot();
|
|
Some((
|
|
page_slot.renderer_document_lifecycle_binding()?.clone(),
|
|
page_slot.renderer_document_lifecycle_authoritative_snapshot()?,
|
|
))
|
|
}
|
|
|
|
pub(crate) fn register_exact_renderer_document_lifecycle_observer_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
expected_binding: Option<&CommittedRendererDocumentBinding>,
|
|
milestone: moli_core::page::RendererDocumentLifecycleMilestone,
|
|
) -> RendererDocumentLifecycleObserver {
|
|
let Some(expected_binding) = expected_binding else {
|
|
return RendererDocumentLifecycleObserver::resolved(
|
|
RendererDocumentLifecycleObservation::Unavailable,
|
|
);
|
|
};
|
|
let Ok(slot) = self.runtime_session_owner_slot_mut_for_owner(owner) else {
|
|
return RendererDocumentLifecycleObserver::resolved(
|
|
RendererDocumentLifecycleObservation::Unavailable,
|
|
);
|
|
};
|
|
slot.page_slot_mut()
|
|
.register_exact_renderer_document_lifecycle_observer(expected_binding, milestone)
|
|
}
|
|
|
|
pub(crate) fn renderer_document_lifecycle_visible_state_for_session_owner(
|
|
&self,
|
|
session_id: Option<&str>,
|
|
) -> Option<(
|
|
CommittedRendererDocumentBinding,
|
|
moli_core::page::RendererDocumentLifecycleSnapshot,
|
|
)> {
|
|
let page_slot = self
|
|
.runtime_session_owner_slot(session_id)
|
|
.ok()?
|
|
.page_slot();
|
|
Some((
|
|
page_slot.renderer_document_lifecycle_binding()?.clone(),
|
|
page_slot.renderer_document_lifecycle_visible_snapshot()?,
|
|
))
|
|
}
|
|
|
|
pub(crate) fn arm_root_post_load_observation_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
loader_id: &str,
|
|
) -> bool {
|
|
self.runtime_session_owner_slot_mut_for_owner(owner)
|
|
.is_ok_and(|slot| {
|
|
slot.page_slot_mut()
|
|
.arm_root_post_load_observation(loader_id)
|
|
})
|
|
}
|
|
|
|
/// Consumes the exact stopped-loading fact owned by an armed root post-load
|
|
/// observation and, when `Page` has subscribers, publishes its frozen
|
|
/// protocol output.
|
|
///
|
|
/// Having no `Page` subscriber is a normal terminal outcome. The binding
|
|
/// must still be consumed so enabling `Page` later cannot replay a
|
|
/// historical event from an earlier navigation.
|
|
pub(crate) fn settle_root_frame_stopped_loading_observation_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
) -> Result<
|
|
crate::domains::activity::RootFrameStoppedLoadingSettlement,
|
|
crate::domains::activity::RootFrameStoppedLoadingSettlementError,
|
|
> {
|
|
use crate::domains::activity::{
|
|
RootFrameStoppedLoadingSettlement as Settlement,
|
|
RootFrameStoppedLoadingSettlementError as SettlementError,
|
|
};
|
|
|
|
let binding = self
|
|
.runtime_session_owner_slot_mut_for_owner(owner)
|
|
.ok()
|
|
.and_then(|slot| {
|
|
slot.page_slot_mut()
|
|
.take_root_frame_stopped_loading_binding()
|
|
});
|
|
let Some(binding) = binding else {
|
|
return Err(SettlementError::MissingArmedObservation);
|
|
};
|
|
if self
|
|
.subscribed_page_event_session_ids_for_owner(owner)
|
|
.is_empty()
|
|
{
|
|
return Ok(Settlement::Unobserved);
|
|
}
|
|
let attachments = self
|
|
.page_event_protocol_attachments_for_owner(owner)
|
|
.ok_or(SettlementError::SubscribedAttachmentUnavailable)?;
|
|
let publish_sequence = self
|
|
.scheduler_state
|
|
.allocate_protocol_work_publish_sequence();
|
|
let output = crate::domains::activity::ProtocolOutputWork::root_frame_stopped_loading(
|
|
attachments,
|
|
binding.frame_id,
|
|
binding.loader_id,
|
|
);
|
|
let work = crate::domains::activity::ProtocolSchedulerWork::protocol_observation(
|
|
publish_sequence,
|
|
output,
|
|
);
|
|
self.scheduler_state
|
|
.push_scheduler_event(CdpSchedulerEvent::ProtocolWorkPublished { work });
|
|
Ok(Settlement::Published)
|
|
}
|
|
|
|
pub(crate) fn emit_root_network_idle_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
out: &mut Vec<BackgroundProtocolEvent>,
|
|
) -> bool {
|
|
if !self
|
|
.runtime_session_owner_slot_for_owner(owner)
|
|
.is_ok_and(|slot| slot.renderer_subresources_are_idle())
|
|
{
|
|
return false;
|
|
}
|
|
let binding = self
|
|
.runtime_session_owner_slot_mut_for_owner(owner)
|
|
.ok()
|
|
.and_then(|slot| slot.page_slot_mut().take_root_network_idle_binding());
|
|
let Some(binding) = binding else {
|
|
return false;
|
|
};
|
|
let timestamp = monotonic_timestamp_seconds();
|
|
for event_session_id in self.page_event_session_ids_for_owner(owner) {
|
|
let event_owner = event_session_id
|
|
.as_deref()
|
|
.map(CommandOwnerScope::for_session)
|
|
.unwrap_or_else(|| owner.clone());
|
|
let lifecycle_enabled = self
|
|
.target_page_session_state_for_owner(&event_owner)
|
|
.is_some_and(|state| state.page_lifecycle_events);
|
|
crate::domains::page::emit_navigation_network_idle_background_events(
|
|
out,
|
|
event_session_id.as_deref(),
|
|
lifecycle_enabled,
|
|
&binding.frame_id,
|
|
&binding.loader_id,
|
|
timestamp,
|
|
);
|
|
}
|
|
true
|
|
}
|
|
|
|
pub fn devtools_context_routes_to_top_level_target(
|
|
&self,
|
|
context: &DevToolsCommandContext,
|
|
) -> bool {
|
|
context.target_id.as_ref().is_some_and(|target_id| {
|
|
self.target_session_route_for_target_id(target_id.as_str())
|
|
.is_some()
|
|
})
|
|
}
|
|
|
|
pub(crate) fn command_owner_scope_for_devtools_context(
|
|
&self,
|
|
context: &DevToolsCommandContext,
|
|
) -> Option<CommandOwnerScope> {
|
|
if let Some(target_id) = context.target_id.as_ref() {
|
|
let route = self
|
|
.target_session_route_for_target_id(target_id.as_str())
|
|
.or_else(|| self.target_session_route_for_child_frame_id(target_id.as_str()))?;
|
|
if let Some(session_id) = context.session_id.as_ref()
|
|
&& let Some(session_route) = self.session_route(Some(session_id.as_str()))
|
|
{
|
|
if !session_route.addresses_same_target_as(&route) {
|
|
return None;
|
|
}
|
|
return Some(CommandOwnerScope::for_session(session_id.as_str()));
|
|
}
|
|
return Some(CommandOwnerScope::for_route(route));
|
|
}
|
|
Some(CommandOwnerScope::capture(
|
|
self,
|
|
context
|
|
.session_id
|
|
.as_ref()
|
|
.map(|session_id| session_id.as_str()),
|
|
))
|
|
}
|
|
|
|
/// Captures the exact target Page currently addressed by a protocol-neutral
|
|
/// command context. The identity remains stable across Document replacement
|
|
/// within that Page and changes when the Page itself is replaced.
|
|
pub fn page_residence_identity_for_devtools_context(
|
|
&mut self,
|
|
context: &DevToolsCommandContext,
|
|
) -> Option<DevToolsPageResidenceIdentity> {
|
|
let owner_scope = self.command_owner_scope_for_devtools_context(context)?;
|
|
self.target_page_residence_identity_for_owner(&owner_scope)
|
|
}
|
|
|
|
pub fn capture_devtools_document_lifecycle_wait_key(
|
|
&mut self,
|
|
context: &DevToolsCommandContext,
|
|
expected_loader_id: &str,
|
|
milestone: moli_core::page::RendererDocumentLifecycleMilestone,
|
|
) -> Option<DevToolsDocumentLifecycleWaitKey> {
|
|
let owner_scope = self.command_owner_scope_for_devtools_context(context)?;
|
|
let registration = self
|
|
.runtime_session_owner_slot_mut_for_owner(&owner_scope)
|
|
.ok()?
|
|
.page_slot_mut()
|
|
.register_renderer_document_lifecycle_waiter(milestone, expected_loader_id);
|
|
let (registration_id, binding) = registration?;
|
|
Some(DevToolsDocumentLifecycleWaitKey {
|
|
registration_id,
|
|
renderer_document: binding.renderer_document,
|
|
renderer_epoch: binding.renderer_epoch,
|
|
milestone,
|
|
frame_id: binding.frame_id,
|
|
loader_id: binding.loader_id,
|
|
})
|
|
}
|
|
|
|
pub fn devtools_document_lifecycle_wait_state(
|
|
&mut self,
|
|
context: &DevToolsCommandContext,
|
|
key: &DevToolsDocumentLifecycleWaitKey,
|
|
) -> DevToolsDocumentLifecycleWaitState {
|
|
let Some(owner_scope) = self.command_owner_scope_for_devtools_context(context) else {
|
|
return DevToolsDocumentLifecycleWaitState::Unavailable;
|
|
};
|
|
self.runtime_session_owner_slot_for_owner(&owner_scope)
|
|
.ok()
|
|
.map_or(DevToolsDocumentLifecycleWaitState::Unavailable, |slot| {
|
|
devtools_document_lifecycle_wait_state_for_slot(slot, key)
|
|
})
|
|
}
|
|
|
|
pub fn release_devtools_document_lifecycle_wait_key(
|
|
&mut self,
|
|
context: &DevToolsCommandContext,
|
|
key: &DevToolsDocumentLifecycleWaitKey,
|
|
) -> bool {
|
|
let Some(owner_scope) = self.command_owner_scope_for_devtools_context(context) else {
|
|
return false;
|
|
};
|
|
self.runtime_session_owner_slot_mut_for_owner(&owner_scope)
|
|
.is_ok_and(|slot| {
|
|
slot.page_slot_mut()
|
|
.release_renderer_document_lifecycle_waiter(
|
|
key.registration_id,
|
|
key.renderer_document,
|
|
key.renderer_epoch,
|
|
&key.frame_id,
|
|
&key.loader_id,
|
|
)
|
|
})
|
|
}
|
|
|
|
pub(crate) fn accepts_document_body_completion_for_owner(
|
|
&self,
|
|
owner: &CommandOwnerScope,
|
|
token: &DocumentNavigationToken,
|
|
) -> bool {
|
|
let Some((browser_context_id, target_id)) = self.target_owner_identity_for_owner(owner)
|
|
else {
|
|
return false;
|
|
};
|
|
if target_id.as_deref() != Some(token.target_id.as_str()) {
|
|
return false;
|
|
}
|
|
self.browser_context_by_id(&browser_context_id)
|
|
.is_some_and(|browser_context| {
|
|
browser_context.accepts_document_body_completion_event(token)
|
|
})
|
|
}
|
|
|
|
pub(crate) fn clear_pending_document_navigation_for_owner_if_loader_matches(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
loader_id: &str,
|
|
) {
|
|
let Some((browser_context_id, target_id)) = self.target_owner_identity_for_owner(owner)
|
|
else {
|
|
return;
|
|
};
|
|
if let Some(browser_context) = self.browser_context_by_id_mut(&browser_context_id) {
|
|
browser_context.clear_pending_document_navigation_for_target_if_loader_matches(
|
|
target_id.as_deref(),
|
|
loader_id,
|
|
);
|
|
}
|
|
self.discard_uncommitted_main_document_resource_for_owner(owner, loader_id);
|
|
}
|
|
|
|
pub fn take_scheduler_events(&mut self) -> Vec<CdpSchedulerEvent> {
|
|
self.scheduler_state.take_scheduler_events()
|
|
}
|
|
|
|
pub(crate) fn push_scheduler_event(&mut self, event: CdpSchedulerEvent) {
|
|
self.scheduler_state.push_scheduler_event(event);
|
|
}
|
|
|
|
pub fn begin_command_response_flush_permit(
|
|
&mut self,
|
|
) -> (CommandResponseFlushPermit, CommandResponseFlushContext) {
|
|
let (sender, receiver) = tokio::sync::watch::channel(false);
|
|
let deferred_releases: Arc<Mutex<CommandResponseFlushDeferredReleases>> = Arc::default();
|
|
(
|
|
CommandResponseFlushPermit {
|
|
sender,
|
|
deferred_releases: deferred_releases.clone(),
|
|
},
|
|
CommandResponseFlushContext::new(receiver, deferred_releases),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn extend_scheduler_events(&mut self, events: Vec<CdpSchedulerEvent>) {
|
|
self.scheduler_state.extend_scheduler_events(events);
|
|
}
|
|
|
|
pub(crate) fn record_scheduler_activity_trace(&mut self, event: serde_json::Value) {
|
|
self.scheduler_state.push_activity_trace(event);
|
|
}
|
|
|
|
pub(crate) fn scheduler_activity_trace_enabled(&self) -> bool {
|
|
moli_trace::cdp_nav_timing_enabled()
|
|
}
|
|
|
|
pub(crate) fn runtime_await_trace_enabled(&self) -> bool {
|
|
moli_trace::cdp_runtime_trace_enabled() || self.scheduler_activity_trace_enabled()
|
|
}
|
|
|
|
pub(crate) fn record_runtime_await_trace(
|
|
&mut self,
|
|
event: &'static str,
|
|
command_id: Option<u64>,
|
|
session_id: Option<&str>,
|
|
fields: serde_json::Value,
|
|
) {
|
|
if !self.runtime_await_trace_enabled() {
|
|
return;
|
|
}
|
|
self.record_scheduler_activity_trace(json!({
|
|
"kind": event,
|
|
"commandId": command_id,
|
|
"sessionId": session_id,
|
|
"fields": fields,
|
|
"pendingRuntimeAwaitJobCount": self.pending_runtime_await_jobs.len(),
|
|
}));
|
|
}
|
|
|
|
pub(crate) fn background_navigation_completion_sender_for_owner(
|
|
&self,
|
|
owner: &CommandOwnerScope,
|
|
) -> Option<
|
|
tokio::sync::mpsc::UnboundedSender<crate::domains::page::BackgroundNavigationCompletion>,
|
|
> {
|
|
if !self.can_run_background_navigation_for_owner(owner) {
|
|
return None;
|
|
}
|
|
self.scheduler_hooks
|
|
.background_navigation_completion_sender()
|
|
}
|
|
|
|
fn can_run_background_navigation_for_owner(&self, owner: &CommandOwnerScope) -> bool {
|
|
if !self
|
|
.scheduler_hooks
|
|
.has_background_navigation_completion_sender()
|
|
{
|
|
return false;
|
|
}
|
|
self.target_owner_identity_for_owner(owner)
|
|
.is_some_and(|(_, target_id)| target_id.is_some())
|
|
}
|
|
|
|
fn can_run_background_navigation_for_active_session(&self) -> bool {
|
|
if !self
|
|
.scheduler_hooks
|
|
.has_background_navigation_completion_sender()
|
|
|| !self.inactive_browser_contexts.is_empty()
|
|
{
|
|
return false;
|
|
}
|
|
self.browser_context
|
|
.as_ref()
|
|
.is_some_and(|browser_context| browser_context.has_no_background_targets())
|
|
}
|
|
|
|
pub(crate) fn can_defer_initial_document_page_build(&self) -> bool {
|
|
self.can_run_background_navigation_for_active_session()
|
|
}
|
|
|
|
pub async fn drain_background_navigation_completion_turn_async(
|
|
&mut self,
|
|
completion: crate::domains::page::BackgroundNavigationCompletion,
|
|
) -> CdpRendererOwnerTurnOutcome {
|
|
let mut command_context = CommandDispatchContext::default();
|
|
let protocol_events = self
|
|
.drain_background_navigation_completion_events_with_context(
|
|
completion,
|
|
&mut command_context,
|
|
)
|
|
.await;
|
|
command_context
|
|
.protocol_events_mut()
|
|
.extend(protocol_events);
|
|
let (protocol_events, renderer_output_boundary, post_renderer_output_events) =
|
|
command_context.take_renderer_fenced_protocol_events();
|
|
CdpTurnOutcome::new_with_protocol_and_post_response_events(
|
|
protocol_events,
|
|
command_context.take_post_response_events(),
|
|
self.take_scheduler_events(),
|
|
)
|
|
.with_renderer_output_boundary(renderer_output_boundary, post_renderer_output_events)
|
|
.with_renderer_output_predecessor(command_context.take_renderer_output_predecessor())
|
|
}
|
|
|
|
async fn drain_background_navigation_completion_events_with_context(
|
|
&mut self,
|
|
completion: crate::domains::page::BackgroundNavigationCompletion,
|
|
command_context: &mut CommandDispatchContext,
|
|
) -> Vec<BackgroundProtocolEvent> {
|
|
let completion = match completion {
|
|
crate::domains::page::BackgroundNavigationCompletion::Lifecycle(completion) => {
|
|
if !self.settle_background_navigation_completion(completion.navigation_token()) {
|
|
tracing::debug!(
|
|
token = ?completion.navigation_token(),
|
|
"background navigation completion did not match the target-owned request"
|
|
);
|
|
}
|
|
completion
|
|
}
|
|
crate::domains::page::BackgroundNavigationCompletion::MainDocumentBody(completion) => {
|
|
completion.record_if_current(self);
|
|
return command_context.take_protocol_events();
|
|
}
|
|
};
|
|
let timing_started = moli_trace::cdp_nav_timing_enabled().then(std::time::Instant::now);
|
|
if timing_started.is_some() {
|
|
tracing::info!(
|
|
target: "moli_cdp_nav_timing",
|
|
url = %completion.requested_url(),
|
|
stage = "background_completion_enqueue_start",
|
|
ready_to_enqueue_ms = completion.ready_elapsed_ms(),
|
|
);
|
|
}
|
|
// Always materialize the navigation so the client receives a terminal
|
|
// Page.navigate response (success or abort-error) for the outstanding
|
|
// command id. The target retains its NavigationEngine independently of
|
|
// this completion, including when the completion is stale.
|
|
let completion = completion.materialize(self);
|
|
if let Some(started) = timing_started {
|
|
tracing::info!(
|
|
target: "moli_cdp_nav_timing",
|
|
stage = "background_completion_materialized",
|
|
phase_ms = started.elapsed().as_millis(),
|
|
);
|
|
}
|
|
self.drain_materialized_navigation_completion_background_events(completion, command_context)
|
|
.await
|
|
}
|
|
|
|
pub(crate) fn replace_standalone_navigation_engine(&mut self, engine: NavigationEngine) {
|
|
let engine = engine;
|
|
self.apply_scheduler_senders_to_navigation_engine(&engine);
|
|
drop(self.standalone_navigation_engine.replace(engine));
|
|
}
|
|
|
|
pub(crate) fn apply_scheduler_senders_to_navigation_engine(&self, engine: &NavigationEngine) {
|
|
if let Some(sender) = self.scheduler_hooks.renderer_publication_sender() {
|
|
engine.set_renderer_output_transport_sender(sender);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn enqueue_deferred_main_document_load_completion(
|
|
&mut self,
|
|
admission: crate::domains::activity::DeferredMainDocumentLoadCompletionAdmission,
|
|
) {
|
|
if !admission.is_still_current_for_scheduler(self) {
|
|
tracing::debug!(
|
|
session_id = admission.session_id(),
|
|
"dropping obsolete deferred main-document load completion before enqueue"
|
|
);
|
|
return;
|
|
}
|
|
let observation_id = self
|
|
.scheduler_state
|
|
.allocate_deferred_main_document_load_observation_id();
|
|
let completion = admission.bind_lifecycle_observer(self, observation_id);
|
|
let publish_sequence = self
|
|
.scheduler_state
|
|
.allocate_protocol_work_publish_sequence();
|
|
let work = crate::domains::activity::ProtocolSchedulerWork::main_document_load_owner_action(
|
|
publish_sequence,
|
|
completion,
|
|
);
|
|
self.scheduler_state
|
|
.push_scheduler_event(CdpSchedulerEvent::ProtocolWorkPublished { work });
|
|
}
|
|
|
|
/// Publishes a top-level navigation already moved into prepared output.
|
|
///
|
|
/// The prepared value retains its exact Page residence. The route is
|
|
/// captured here, while the output drain is still running under the
|
|
/// producer's owner scope. This boundary deliberately performs no
|
|
/// navigation: the returned scheduler work is the sole execution
|
|
/// authority.
|
|
pub(crate) fn publish_prepared_top_level_location_navigation_owner_action(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
page_owner: TargetPageResidenceIdentity,
|
|
navigation: moli_core::page::RendererDocumentSourcedTopLevelLocationNavigation,
|
|
) {
|
|
let action = TopLevelLocationNavigationOwnerAction::from_prepared(
|
|
owner.clone(),
|
|
page_owner,
|
|
navigation,
|
|
);
|
|
self.publish_top_level_location_navigation_owner_action(action);
|
|
}
|
|
|
|
fn publish_top_level_location_navigation_owner_action(
|
|
&mut self,
|
|
action: TopLevelLocationNavigationOwnerAction,
|
|
) {
|
|
let publish_sequence = self
|
|
.scheduler_state
|
|
.allocate_protocol_work_publish_sequence();
|
|
let work =
|
|
crate::domains::activity::ProtocolSchedulerWork::top_level_location_navigation_owner_action(
|
|
publish_sequence,
|
|
action,
|
|
);
|
|
self.scheduler_state
|
|
.push_scheduler_event(CdpSchedulerEvent::ProtocolWorkPublished { work });
|
|
}
|
|
|
|
pub(crate) fn publish_popup_target_navigation_owner_action(
|
|
&mut self,
|
|
action: PopupTargetNavigationOwnerAction,
|
|
) {
|
|
let publish_sequence = self
|
|
.scheduler_state
|
|
.allocate_protocol_work_publish_sequence();
|
|
let work =
|
|
crate::domains::activity::ProtocolSchedulerWork::popup_target_navigation_owner_action(
|
|
publish_sequence,
|
|
action,
|
|
);
|
|
self.scheduler_state
|
|
.push_scheduler_event(CdpSchedulerEvent::ProtocolWorkPublished { work });
|
|
}
|
|
|
|
pub(crate) fn publish_popup_target_activation_action(
|
|
&mut self,
|
|
action: PopupTargetActivationAction,
|
|
) {
|
|
let publish_sequence = self
|
|
.scheduler_state
|
|
.allocate_protocol_work_publish_sequence();
|
|
let work = crate::domains::activity::ProtocolSchedulerWork::popup_target_activation_action(
|
|
publish_sequence,
|
|
action,
|
|
);
|
|
self.scheduler_state
|
|
.push_scheduler_event(CdpSchedulerEvent::ProtocolWorkPublished { work });
|
|
}
|
|
|
|
pub(crate) fn publish_page_target_termination_owner_action(
|
|
&mut self,
|
|
action: crate::domains::page::PageTargetTerminationOwnerAction,
|
|
) {
|
|
let publish_sequence = self
|
|
.scheduler_state
|
|
.allocate_protocol_work_publish_sequence();
|
|
let work =
|
|
crate::domains::activity::ProtocolSchedulerWork::page_target_termination_owner_action(
|
|
publish_sequence,
|
|
action,
|
|
);
|
|
self.scheduler_state
|
|
.push_scheduler_event(CdpSchedulerEvent::ProtocolWorkPublished { work });
|
|
}
|
|
|
|
pub async fn complete_deferred_main_document_load_completion_for_scheduler(
|
|
&mut self,
|
|
completion: CompletedDeferredMainDocumentLoadCompletion,
|
|
) -> CdpTurnOutcome {
|
|
let mut output = BackgroundProtocolEventBuffer::default();
|
|
completion.inner.emit_async(self, &mut output).await;
|
|
CdpTurnOutcome::new_with_protocol_and_post_response_events(
|
|
output.into_events(),
|
|
Vec::new(),
|
|
self.take_scheduler_events(),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn enqueue_navigation_background_event(&mut self, event: NavigationBackgroundEvent) {
|
|
self.scheduler_state.push_navigation_background_event(event);
|
|
}
|
|
|
|
pub(crate) fn enqueue_navigation_background_protocol_event(
|
|
&mut self,
|
|
token: DocumentNavigationToken,
|
|
event: BackgroundProtocolEvent,
|
|
) {
|
|
self.enqueue_navigation_background_event(NavigationBackgroundEvent::background_event(
|
|
token, event,
|
|
));
|
|
}
|
|
|
|
pub(crate) fn send_navigation_background_protocol_event(
|
|
&mut self,
|
|
token: DocumentNavigationToken,
|
|
event: BackgroundProtocolEvent,
|
|
) {
|
|
self.enqueue_navigation_background_protocol_event(token, event);
|
|
self.flush_navigation_background_events_to_sender();
|
|
}
|
|
|
|
fn drain_navigation_background_protocol_events(&mut self) -> Vec<BackgroundProtocolEvent> {
|
|
let events = self.scheduler_state.take_navigation_background_events();
|
|
events
|
|
.into_iter()
|
|
.filter_map(|event| {
|
|
event.into_background_protocol_event_if_current(self.browser_contexts())
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn drain_navigation_background_events(&mut self) -> Vec<serde_json::Value> {
|
|
self.drain_navigation_background_protocol_events()
|
|
.into_iter()
|
|
.map(BackgroundProtocolEvent::into_protocol_message)
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn flush_navigation_background_events_to_sender(&mut self) {
|
|
let Some(sender) = self.scheduler_hooks.background_event_sender() else {
|
|
return;
|
|
};
|
|
for event in self.drain_navigation_background_protocol_events() {
|
|
let _ = sender.send(event);
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn drain_materialized_navigation_completion_background_events(
|
|
&mut self,
|
|
completion: crate::domains::page::MaterializedNavigationCompletion,
|
|
command_context: &mut CommandDispatchContext,
|
|
) -> Vec<BackgroundProtocolEvent> {
|
|
let command_id = completion.navigate_id();
|
|
let command_session_id = completion.navigate_session_id().map(str::to_owned);
|
|
let mut output = CommandOutputBuffer::default();
|
|
self.drain_materialized_navigation_completion_into_buffer(
|
|
&mut output,
|
|
completion,
|
|
command_context,
|
|
)
|
|
.await;
|
|
let (
|
|
before_renderer_output,
|
|
renderer_output_boundary,
|
|
after_renderer_output,
|
|
post_response_events,
|
|
) = output
|
|
.into_plan()
|
|
.into_renderer_fenced_background_and_post_response_events(
|
|
command_id,
|
|
command_session_id.as_deref(),
|
|
);
|
|
command_context.append_renderer_fenced_protocol_events(
|
|
before_renderer_output,
|
|
renderer_output_boundary,
|
|
after_renderer_output,
|
|
);
|
|
command_context.extend_post_response_events(post_response_events);
|
|
Vec::new()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) async fn drain_materialized_navigation_completion_into(
|
|
&mut self,
|
|
out: &mut Vec<serde_json::Value>,
|
|
completion: crate::domains::page::MaterializedNavigationCompletion,
|
|
command_context: &mut CommandDispatchContext,
|
|
) {
|
|
let mut events = self
|
|
.drain_materialized_navigation_completion_background_events(completion, command_context)
|
|
.await;
|
|
let (before_renderer_output, renderer_output_boundary, after_renderer_output) =
|
|
command_context.take_renderer_fenced_protocol_events();
|
|
assert!(
|
|
renderer_output_boundary.is_none(),
|
|
"message-only navigation helper cannot flatten a renderer output boundary"
|
|
);
|
|
events.extend(before_renderer_output);
|
|
events.extend(after_renderer_output);
|
|
events.extend(command_context.take_post_response_events());
|
|
out.extend(
|
|
events
|
|
.into_iter()
|
|
.map(BackgroundProtocolEvent::into_protocol_message),
|
|
);
|
|
}
|
|
|
|
pub(crate) async fn drain_materialized_navigation_completion_into_buffer(
|
|
&mut self,
|
|
out: &mut CommandOutputBuffer,
|
|
completion: crate::domains::page::MaterializedNavigationCompletion,
|
|
command_context: &mut CommandDispatchContext,
|
|
) {
|
|
let timing_started = moli_trace::cdp_nav_timing_enabled().then(std::time::Instant::now);
|
|
if timing_started.is_some() {
|
|
tracing::info!(
|
|
target: "moli_cdp_nav_timing",
|
|
url = %completion.requested_url(),
|
|
stage = "materialized_completion_drain_start",
|
|
);
|
|
}
|
|
let is_current = completion.is_current_for_connection(self);
|
|
let (token, state, navigation) = completion.into_parts();
|
|
if !is_current {
|
|
crate::domains::page::push_superseded_navigation_result(out, &state);
|
|
return;
|
|
}
|
|
crate::domains::page::complete_materialized_navigation_into_buffer_async(
|
|
self,
|
|
out,
|
|
token.clone(),
|
|
state,
|
|
navigation,
|
|
command_context,
|
|
)
|
|
.await;
|
|
if let Some(started) = timing_started {
|
|
tracing::info!(
|
|
target: "moli_cdp_nav_timing",
|
|
stage = "materialized_completion_drain_end",
|
|
phase_ms = started.elapsed().as_millis(),
|
|
);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn response_body_materialize_limit(&self) -> usize {
|
|
self.fetch_config()
|
|
.http_max_response_size()
|
|
.unwrap_or(body_spool::DEFAULT_BODY_MATERIALIZE_LIMIT)
|
|
}
|
|
|
|
pub(crate) fn moli_memory_diagnostics(&self) -> serde_json::Value {
|
|
let active_browser_context = self
|
|
.browser_context
|
|
.as_ref()
|
|
.map(BrowserContext::moli_memory_diagnostics);
|
|
let inactive_browser_contexts = self
|
|
.inactive_browser_contexts
|
|
.iter()
|
|
.map(BrowserContext::moli_memory_diagnostics)
|
|
.collect::<Vec<_>>();
|
|
let page_engine_keys = self
|
|
.browser_contexts()
|
|
.flat_map(|browser_context| {
|
|
browser_context
|
|
.page_targets
|
|
.iter()
|
|
.filter(|host| host.navigation_engine().is_some())
|
|
.map(|host| {
|
|
json!({
|
|
"browserContextId": browser_context.id,
|
|
"targetId": host.target_id(),
|
|
})
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let browser_context_count = self.browser_contexts().count();
|
|
let loaded_document_page_count = self
|
|
.browser_contexts()
|
|
.map(BrowserContext::loaded_document_page_count)
|
|
.sum::<usize>();
|
|
let pending_document_page_build_count = self
|
|
.browser_contexts()
|
|
.map(BrowserContext::pending_document_page_build_count)
|
|
.sum::<usize>();
|
|
let mut loaded_document_renderer_owner_ids = HashSet::new();
|
|
let mut document_renderer_owner_ids = HashSet::new();
|
|
for browser_context in self.browser_contexts() {
|
|
loaded_document_renderer_owner_ids
|
|
.extend(browser_context.loaded_document_renderer_owner_ids_for_diagnostics());
|
|
document_renderer_owner_ids
|
|
.extend(browser_context.document_renderer_owner_ids_for_diagnostics());
|
|
}
|
|
let loaded_document_renderer_owner_count = loaded_document_renderer_owner_ids.len();
|
|
let shared_worker_target_count = self
|
|
.browser_contexts()
|
|
.map(|context| context.shared_worker_targets.len())
|
|
.sum::<usize>();
|
|
let service_worker_target_count = self
|
|
.browser_contexts()
|
|
.map(|context| context.service_worker_targets.len())
|
|
.sum::<usize>();
|
|
let page_target_pending_inspector_await_count = self
|
|
.browser_contexts()
|
|
.map(BrowserContext::page_target_pending_inspector_await_count_for_diagnostics)
|
|
.sum::<usize>();
|
|
let page_target_with_pending_inspector_await_count = self
|
|
.browser_contexts()
|
|
.map(BrowserContext::page_target_with_pending_inspector_await_count_for_diagnostics)
|
|
.sum::<usize>();
|
|
let shared_worker_target_pending_inspector_await_count = self
|
|
.browser_contexts()
|
|
.map(BrowserContext::shared_worker_target_pending_inspector_await_count_for_diagnostics)
|
|
.sum::<usize>();
|
|
let shared_worker_target_with_pending_inspector_await_count = self
|
|
.browser_contexts()
|
|
.map(
|
|
BrowserContext::shared_worker_target_with_pending_inspector_await_count_for_diagnostics,
|
|
)
|
|
.sum::<usize>();
|
|
let service_worker_target_pending_inspector_await_count = self
|
|
.browser_contexts()
|
|
.map(
|
|
BrowserContext::service_worker_target_pending_inspector_await_count_for_diagnostics,
|
|
)
|
|
.sum::<usize>();
|
|
let service_worker_target_with_pending_inspector_await_count = self
|
|
.browser_contexts()
|
|
.map(
|
|
BrowserContext::service_worker_target_with_pending_inspector_await_count_for_diagnostics,
|
|
)
|
|
.sum::<usize>();
|
|
let pending_inspector_await_count = page_target_pending_inspector_await_count
|
|
+ shared_worker_target_pending_inspector_await_count
|
|
+ service_worker_target_pending_inspector_await_count;
|
|
let dedicated_worker_running_worker_isolate_count = self
|
|
.browser_contexts()
|
|
.map(BrowserContext::dedicated_worker_running_worker_isolate_count_for_diagnostics)
|
|
.sum::<usize>();
|
|
let mut shared_worker_matching_entry_count = 0;
|
|
let mut shared_worker_loading_instance_count = 0;
|
|
let mut shared_worker_running_instance_count = 0;
|
|
let mut shared_worker_client_count = 0;
|
|
let mut shared_worker_loading_host_count = 0;
|
|
let mut shared_worker_running_worker_isolate_count = 0;
|
|
let mut shared_worker_pending_service_lane_event_count = 0;
|
|
for shared_worker_diagnostics in self
|
|
.browser_contexts()
|
|
.map(BrowserContext::shared_worker_runtime_diagnostics_for_diagnostics)
|
|
{
|
|
shared_worker_matching_entry_count += shared_worker_diagnostics.matching_entry_count;
|
|
shared_worker_loading_instance_count +=
|
|
shared_worker_diagnostics.loading_instance_count;
|
|
shared_worker_running_instance_count +=
|
|
shared_worker_diagnostics.running_instance_count;
|
|
shared_worker_client_count += shared_worker_diagnostics.client_count;
|
|
shared_worker_loading_host_count += shared_worker_diagnostics.loading_host_count;
|
|
shared_worker_running_worker_isolate_count +=
|
|
shared_worker_diagnostics.running_worker_isolate_count;
|
|
shared_worker_pending_service_lane_event_count +=
|
|
shared_worker_diagnostics.pending_service_lane_event_count;
|
|
}
|
|
let page_navigation_engine_count = page_engine_keys.len();
|
|
let active_engine = self.active_navigation_engine();
|
|
let active_renderer_owner_id = active_engine.renderer_owner_id_for_diagnostics();
|
|
let mut page_navigation_engine_renderer_owner_ids = HashSet::new();
|
|
let mut estimated_renderer_owner_ids = HashSet::new();
|
|
estimated_renderer_owner_ids.insert(active_renderer_owner_id);
|
|
estimated_renderer_owner_ids.extend(document_renderer_owner_ids.iter().copied());
|
|
for engine in self.browser_contexts().flat_map(|browser_context| {
|
|
browser_context
|
|
.page_targets
|
|
.iter()
|
|
.filter_map(PageTargetHost::navigation_engine)
|
|
}) {
|
|
let renderer_owner_id = engine.renderer_owner_id_for_diagnostics();
|
|
if renderer_owner_id != active_renderer_owner_id {
|
|
page_navigation_engine_renderer_owner_ids.insert(renderer_owner_id);
|
|
}
|
|
estimated_renderer_owner_ids.insert(renderer_owner_id);
|
|
}
|
|
let page_navigation_engine_renderer_owner_count =
|
|
page_navigation_engine_renderer_owner_ids.len();
|
|
let estimated_renderer_owner_count = estimated_renderer_owner_ids.len();
|
|
let document_isolate_model = active_engine.document_isolate_model_for_diagnostics();
|
|
let estimated_document_isolate_count =
|
|
loaded_document_page_count + pending_document_page_build_count;
|
|
let document_isolate_accounting =
|
|
active_engine.document_isolate_accounting_for_diagnostics();
|
|
let document_isolate_accounting = json!({
|
|
"scope": "renderer-process",
|
|
"created": document_isolate_accounting.created,
|
|
"destroyed": document_isolate_accounting.destroyed,
|
|
"live": document_isolate_accounting.live,
|
|
"reserved": document_isolate_accounting.reserved,
|
|
});
|
|
let estimated_worker_isolate_count = dedicated_worker_running_worker_isolate_count
|
|
+ shared_worker_running_worker_isolate_count;
|
|
let estimated_live_v8_isolate_count =
|
|
estimated_document_isolate_count + estimated_worker_isolate_count;
|
|
let active_navigation_engine_resource_runtime = active_engine
|
|
.resource_request_client()
|
|
.map(|client| client.resource_runtime_diagnostics());
|
|
let active_navigation_engine_resource_runtime_id =
|
|
active_navigation_engine_resource_runtime
|
|
.as_ref()
|
|
.map(|diagnostics| diagnostics.runtime_id);
|
|
let active_navigation_engine_memory_cache =
|
|
active_navigation_engine_resource_runtime.map(|diagnostics| diagnostics.memory_cache);
|
|
json!({
|
|
"connection": {
|
|
"hasActiveBrowserContext": self.browser_context.is_some(),
|
|
"inactiveBrowserContextCount": self.inactive_browser_contexts.len(),
|
|
"browserSessionIdCount": self.target_control.browser_session_count(),
|
|
"globalIoStreamCount": self.global_io_streams.len(),
|
|
"tracing": self.tracing_state.diagnostics(),
|
|
"permissionOverrideCount": self.permission_overrides.len(),
|
|
"pageNavigationEngineCount": page_navigation_engine_count,
|
|
"pageNavigationEngineKeys": page_engine_keys,
|
|
"autoAttach": self.auto_attach_enabled(),
|
|
"targetDiscoveryEnabled": self.target_discovery_enabled,
|
|
"targetInfoChangeEventsEnabled": self.target_info_change_events_enabled,
|
|
"activeNavigationEngine": {
|
|
"imageFetchEnabled": active_engine.image_fetch_enabled(),
|
|
"optionalResourceFetchMask": active_engine.optional_resource_fetch_mask().bits(),
|
|
"subframeLoadingEnabled": active_engine.subframe_loading_enabled(),
|
|
"resourceRuntimeId": active_navigation_engine_resource_runtime_id,
|
|
"networkMemoryCache": active_navigation_engine_memory_cache,
|
|
"browserContextRuntime": active_engine
|
|
.browser_context_runtime()
|
|
.moli_memory_diagnostics(),
|
|
},
|
|
},
|
|
"isolateScope": {
|
|
"documentIsolateModel": document_isolate_model,
|
|
"workerIsolateModel": "per-worker-thread",
|
|
"activeNavigationEngineRendererOwnerCount": 1,
|
|
"pageNavigationEngineRendererOwnerCount": page_navigation_engine_renderer_owner_count,
|
|
"estimatedRendererOwnerCount": estimated_renderer_owner_count,
|
|
"browserContextCount": browser_context_count,
|
|
"loadedDocumentPageCount": loaded_document_page_count,
|
|
"loadedDocumentRendererOwnerCount": loaded_document_renderer_owner_count,
|
|
"pendingDocumentPageBuildCount": pending_document_page_build_count,
|
|
"estimatedDocumentIsolateCount": estimated_document_isolate_count,
|
|
"documentIsolateAccounting": document_isolate_accounting,
|
|
"estimatedWorkerIsolateCount": estimated_worker_isolate_count,
|
|
"estimatedLiveV8IsolateCount": estimated_live_v8_isolate_count,
|
|
"runtimeGetHeapUsageV8HeapScope": "page-vm-document-isolate",
|
|
"runtimeGetHeapUsageV8HeapIsTargetLocal": true,
|
|
"runtimeGetHeapUsageMoliCountersScope": "target-document",
|
|
"runtimeCollectGarbageScope": "page-vm-document-isolate",
|
|
"v8ForegroundTaskWakeScope": "page-vm-document-isolate",
|
|
"v8ForegroundTaskWakeContextGroupIdAvailable": false,
|
|
"v8ForegroundTaskWakeInternalPolicy": "page-runtime-queue-and-owner-page-tick",
|
|
"v8ForegroundTaskWakeExternalPolicy": "page-owner-runtime-wake",
|
|
"pendingInspectorAwaitCount": pending_inspector_await_count,
|
|
"pageTargetPendingInspectorAwaitCount": page_target_pending_inspector_await_count,
|
|
"pageTargetWithPendingInspectorAwaitCount": page_target_with_pending_inspector_await_count,
|
|
"sharedWorkerTargetPendingInspectorAwaitCount": shared_worker_target_pending_inspector_await_count,
|
|
"sharedWorkerTargetWithPendingInspectorAwaitCount": shared_worker_target_with_pending_inspector_await_count,
|
|
"serviceWorkerTargetPendingInspectorAwaitCount": service_worker_target_pending_inspector_await_count,
|
|
"serviceWorkerTargetWithPendingInspectorAwaitCount": service_worker_target_with_pending_inspector_await_count,
|
|
"sharedWorkerTargetCount": shared_worker_target_count,
|
|
"serviceWorkerTargetCount": service_worker_target_count,
|
|
"sharedWorkerMatchingEntryCount": shared_worker_matching_entry_count,
|
|
"sharedWorkerLoadingInstanceCount": shared_worker_loading_instance_count,
|
|
"sharedWorkerRunningInstanceCount": shared_worker_running_instance_count,
|
|
"sharedWorkerClientCount": shared_worker_client_count,
|
|
"sharedWorkerLoadingHostCount": shared_worker_loading_host_count,
|
|
"sharedWorkerRunningWorkerIsolateCount": shared_worker_running_worker_isolate_count,
|
|
"sharedWorkerPendingServiceLaneEventCount": shared_worker_pending_service_lane_event_count,
|
|
"sharedWorkerProtocolDispatchRequiresLiveOwnerPageCommand": false,
|
|
},
|
|
"scheduler": self.scheduler_state.moli_memory_diagnostics(),
|
|
"activeBrowserContext": active_browser_context,
|
|
"inactiveBrowserContexts": inactive_browser_contexts,
|
|
})
|
|
}
|
|
|
|
fn idle_navigation_engine_release_counts(&self) -> (usize, usize) {
|
|
let loaded_browser_context_count = self
|
|
.browser_contexts()
|
|
.filter(|browser_context| {
|
|
browser_context.has_loaded_page()
|
|
|| browser_context
|
|
.background_targets()
|
|
.any(PageTargetHost::has_loaded_page)
|
|
})
|
|
.count();
|
|
|
|
let live_target_browser_context_count = self
|
|
.browser_contexts()
|
|
.filter(|browser_context| {
|
|
browser_context.has_active_target() || !browser_context.has_no_background_targets()
|
|
})
|
|
.count();
|
|
|
|
(
|
|
loaded_browser_context_count,
|
|
live_target_browser_context_count,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn release_idle_navigation_engine_memory_if_idle(
|
|
&mut self,
|
|
) -> IdleNavigationEngineReleaseResult {
|
|
let (loaded_browser_context_count, live_target_browser_context_count) =
|
|
self.idle_navigation_engine_release_counts();
|
|
let eligible = loaded_browser_context_count == 0 && live_target_browser_context_count == 0;
|
|
if !eligible {
|
|
return IdleNavigationEngineReleaseResult {
|
|
reset: false,
|
|
reason: "not-idle",
|
|
loaded_browser_context_count,
|
|
live_target_browser_context_count,
|
|
};
|
|
}
|
|
|
|
let replacement = NavigationEngine::new_with_runtime_config(
|
|
self.standalone_navigation_engine.runtime_config(),
|
|
);
|
|
self.replace_standalone_navigation_engine(replacement);
|
|
IdleNavigationEngineReleaseResult {
|
|
reset: true,
|
|
reason: "idle-engine-replaced",
|
|
loaded_browser_context_count,
|
|
live_target_browser_context_count,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn release_idle_navigation_engine_memory_after_target_close(&mut self) {
|
|
let result = self.release_idle_navigation_engine_memory_if_idle();
|
|
if result.reset {
|
|
tracing::debug!(
|
|
target: "moli_cdp_memory",
|
|
reason = result.reason,
|
|
"released idle navigation engine after final target close"
|
|
);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn moli_reset_idle_navigation_engine_for_diagnostics(
|
|
&mut self,
|
|
) -> serde_json::Value {
|
|
self.release_idle_navigation_engine_memory_if_idle()
|
|
.to_protocol_json()
|
|
}
|
|
|
|
pub(crate) fn new_browser_context(&self, id: String) -> BrowserContext {
|
|
let mut browser_context = self.initial_storage_partition.new_default_browser_context(
|
|
id,
|
|
self.fetch_config().http_cache_dir().map(PathBuf::from),
|
|
self.fetch_config().http_cache_max_bytes(),
|
|
);
|
|
self.apply_global_browser_context_state(&mut browser_context);
|
|
browser_context
|
|
}
|
|
|
|
pub(crate) fn ensure_browser_context_for_implicit_target_creation(&mut self) {
|
|
if self.browser_context.is_some() || !self.inactive_browser_contexts.is_empty() {
|
|
return;
|
|
}
|
|
let browser_context_id = if self.default_target_lifecycle.is_placeholder() {
|
|
self.default_browser_context_id().to_owned()
|
|
} else {
|
|
self.gen_bc_id()
|
|
};
|
|
self.insert_browser_context(self.new_browser_context(browser_context_id));
|
|
}
|
|
|
|
fn apply_global_browser_context_state(&self, browser_context: &mut BrowserContext) {
|
|
browser_context.global_cache_disabled = self.global_cache_disabled;
|
|
browser_context.global_extra_headers = self.global_extra_headers.clone();
|
|
browser_context.global_network_conditions = self.global_network_conditions;
|
|
browser_context.global_geolocation_override = self.global_geolocation_override.clone();
|
|
}
|
|
|
|
pub(crate) fn new_ephemeral_browser_context(&self, id: String) -> BrowserContext {
|
|
let mut browser_context = BrowserContext::new_ephemeral_with_http_cache(
|
|
id,
|
|
self.fetch_config().http_cache_dir().map(PathBuf::from),
|
|
self.fetch_config().http_cache_max_bytes(),
|
|
);
|
|
self.apply_global_browser_context_state(&mut browser_context);
|
|
browser_context
|
|
}
|
|
|
|
pub fn snapshot_cookies(&mut self) -> Vec<StoredCookie> {
|
|
self.browser_context
|
|
.iter()
|
|
.chain(self.inactive_browser_contexts.iter())
|
|
.flat_map(BrowserContext::snapshot_cookies)
|
|
.collect()
|
|
}
|
|
|
|
pub fn snapshot_profile_backed_cookies(&mut self) -> Option<Vec<StoredCookie>> {
|
|
let mut saw_profile_backed_context = false;
|
|
let cookies = self
|
|
.browser_context
|
|
.iter()
|
|
.chain(self.inactive_browser_contexts.iter())
|
|
.filter(|context| {
|
|
let is_profile_backed = context.is_profile_backed_storage_partition();
|
|
saw_profile_backed_context |= is_profile_backed;
|
|
is_profile_backed
|
|
})
|
|
.flat_map(BrowserContext::snapshot_cookies)
|
|
.collect();
|
|
saw_profile_backed_context.then_some(cookies)
|
|
}
|
|
|
|
// ── ID generators ────────────────────────────────────────────────────────
|
|
|
|
pub fn gen_bc_id(&mut self) -> String {
|
|
self.next_bc_id = self
|
|
.next_bc_id
|
|
.checked_add(1)
|
|
.expect("browser context id space exhausted");
|
|
format!("BID-{}", self.next_bc_id)
|
|
}
|
|
|
|
pub fn gen_user_browser_context_id(&mut self) -> String {
|
|
loop {
|
|
self.next_bc_id = self
|
|
.next_bc_id
|
|
.checked_add(1)
|
|
.expect("browser context id space exhausted");
|
|
let id = format!("user-context-{}", self.next_bc_id);
|
|
if !self.has_browser_context_id(&id) {
|
|
return id;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn default_browser_context_id(&self) -> &'static str {
|
|
DEFAULT_BROWSER_CONTEXT_ID
|
|
}
|
|
|
|
pub fn default_target_id(&self) -> &'static str {
|
|
DEFAULT_CDP_PAGE_TARGET_ID
|
|
}
|
|
|
|
pub fn default_tab_target_id(&self) -> &'static str {
|
|
DEFAULT_CDP_TAB_TARGET_ID
|
|
}
|
|
|
|
pub(crate) fn devtools_target_info(&self, target_id: &str) -> Option<DevToolsTargetInfo> {
|
|
if let Some(page_target_id) = self.primary_page_target_id_for_tab_target_id(target_id) {
|
|
let page_target_info = self.devtools_page_or_worker_target_info(page_target_id)?;
|
|
return self
|
|
.target_control
|
|
.tab_target_info_for_page_target_info(page_target_info);
|
|
}
|
|
self.devtools_page_or_worker_target_info(target_id)
|
|
}
|
|
|
|
fn devtools_page_or_worker_target_info(&self, target_id: &str) -> Option<DevToolsTargetInfo> {
|
|
self.browser_contexts()
|
|
.find_map(|browser_context| browser_context.devtools_target_info(target_id))
|
|
.or_else(|| {
|
|
(target_id == DEFAULT_CDP_PAGE_TARGET_ID)
|
|
.then(|| self.default_target_lifecycle.placeholder_page_info())
|
|
.flatten()
|
|
})
|
|
}
|
|
|
|
pub(crate) fn devtools_target_infos(&self) -> Vec<DevToolsTargetInfo> {
|
|
let mut target_infos = Vec::new();
|
|
if let Some(page_target_info) = self.default_target_lifecycle.placeholder_page_info() {
|
|
if let Some(tab_target_info) = self
|
|
.target_control
|
|
.tab_target_info_for_page_target_info(page_target_info.clone())
|
|
{
|
|
target_infos.push(tab_target_info);
|
|
}
|
|
target_infos.push(page_target_info);
|
|
}
|
|
for browser_context in self.browser_contexts() {
|
|
for mut page_or_worker_target_info in browser_context.devtools_target_infos() {
|
|
if let Some(target_id) = page_or_worker_target_info.target_id.as_ref() {
|
|
page_or_worker_target_info.moli_popup_id =
|
|
browser_context.target_popup_id(target_id.as_str());
|
|
}
|
|
if let Some(tab_target_info) = self
|
|
.target_control
|
|
.tab_target_info_for_page_target_info(page_or_worker_target_info.clone())
|
|
{
|
|
target_infos.push(tab_target_info);
|
|
}
|
|
target_infos.push(page_or_worker_target_info);
|
|
}
|
|
}
|
|
target_infos
|
|
}
|
|
|
|
pub fn publish_default_browser_target(&mut self) {
|
|
if !self.default_target_lifecycle.publish() {
|
|
return;
|
|
}
|
|
let default_target_id = self.default_target_id().to_owned();
|
|
self.register_top_level_page_target(&default_target_id);
|
|
self.notify_target_host_activated(&default_target_id);
|
|
}
|
|
|
|
/// Crosses the default target's placeholder-to-live boundary when the
|
|
/// requested operation genuinely needs a page owner.
|
|
pub(crate) fn ensure_default_target_live(&mut self, target_id: &str) {
|
|
if self
|
|
.default_target_lifecycle
|
|
.is_placeholder_target(target_id)
|
|
{
|
|
self.install_default_browser_target();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn default_placeholder_is_logically_active(&self, target_id: &str) -> bool {
|
|
self.default_target_lifecycle
|
|
.is_placeholder_target(target_id)
|
|
&& self.browser_contexts().all(|browser_context| {
|
|
!browser_context.has_active_target()
|
|
&& browser_context.background_targets().next().is_none()
|
|
})
|
|
}
|
|
|
|
pub(crate) async fn close_default_target_placeholder(
|
|
&mut self,
|
|
target_id: &str,
|
|
) -> Option<TargetEventPlan> {
|
|
if !self
|
|
.default_target_lifecycle
|
|
.is_placeholder_target(target_id)
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let target_host_closure = self.prepare_target_host_closure(DEFAULT_CDP_PAGE_TARGET_ID);
|
|
let (detached_info_deltas, destroyed_deltas) = target_host_closure.into_parts();
|
|
let mut plan = self.prepared_target_host_deltas_event_plan(detached_info_deltas);
|
|
if let Some(tab_cleanup) = self.take_closed_top_level_target_sessions_cleanup_plan(
|
|
DEFAULT_CDP_PAGE_TARGET_ID,
|
|
Some("Render process gone."),
|
|
) {
|
|
plan.extend(
|
|
self.dispose_target_closure_sessions_event_plan_async(tab_cleanup, None)
|
|
.await,
|
|
);
|
|
}
|
|
let closed = self.default_target_lifecycle.close_placeholder(target_id);
|
|
debug_assert!(closed, "validated default placeholder must close");
|
|
plan.extend(self.prepared_target_host_deltas_event_plan(destroyed_deltas));
|
|
Some(plan)
|
|
}
|
|
|
|
pub(crate) fn mark_default_browser_target_closed(&mut self) {
|
|
self.default_target_lifecycle.mark_closed();
|
|
}
|
|
|
|
pub(crate) fn register_top_level_page_target(&mut self, page_target_id: &str) -> String {
|
|
let tab_target_id = if page_target_id == self.default_target_id() {
|
|
self.default_tab_target_id().to_owned()
|
|
} else {
|
|
self.gen_tab_target_id()
|
|
};
|
|
self.target_control
|
|
.register_tab(tab_target_id.clone(), page_target_id.to_owned());
|
|
for target_id in [&tab_target_id, page_target_id] {
|
|
if let Some(target_info) = self.target_info_for_host_delta(target_id) {
|
|
self.notify_target_host_lifecycle(CdpTargetHostLifecycleDelta::Created(
|
|
target_info,
|
|
));
|
|
}
|
|
}
|
|
tab_target_id
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
pub fn tab_target_id_for_page_target_id(&self, page_target_id: &str) -> Option<&str> {
|
|
self.target_control
|
|
.tab_target_id_for_page_target_id(page_target_id)
|
|
}
|
|
|
|
pub(crate) fn primary_page_target_id_for_tab_target_id(
|
|
&self,
|
|
tab_target_id: &str,
|
|
) -> Option<&str> {
|
|
self.target_control
|
|
.primary_page_target_id_for_tab_target_id(tab_target_id)
|
|
}
|
|
|
|
pub(crate) fn primary_session_id_for_tab_target_id(&self, tab_target_id: &str) -> Option<&str> {
|
|
self.target_control
|
|
.primary_session_id_for_tab_target_id(tab_target_id)
|
|
}
|
|
|
|
pub(crate) fn assign_session_to_tab_target(
|
|
&mut self,
|
|
tab_target_id: &str,
|
|
session_id: String,
|
|
is_attached_session: bool,
|
|
) -> bool {
|
|
self.target_control.assign_session_to_tab_target(
|
|
tab_target_id,
|
|
session_id,
|
|
is_attached_session,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn remove_tab_session(&mut self, session_id: &str) -> Option<String> {
|
|
self.target_control.remove_tab_session(session_id)
|
|
}
|
|
|
|
pub(crate) fn remove_tab_for_page_target(
|
|
&mut self,
|
|
page_target_id: &str,
|
|
) -> Option<TargetClosurePlan> {
|
|
let closure_plan = self
|
|
.target_control
|
|
.remove_tab_by_page_target_id(page_target_id)?;
|
|
for target_id in closure_plan.destroyed_target_ids() {
|
|
self.notify_target_host_lifecycle(CdpTargetHostLifecycleDelta::Destroyed {
|
|
target_id: target_id.to_owned(),
|
|
});
|
|
}
|
|
Some(closure_plan)
|
|
}
|
|
|
|
pub(crate) fn take_closed_top_level_target_sessions_cleanup_plan(
|
|
&mut self,
|
|
page_target_id: &str,
|
|
reason: Option<&str>,
|
|
) -> Option<TargetClosureCleanupPlan> {
|
|
let closure_plan = self.remove_tab_for_page_target(page_target_id)?;
|
|
debug_assert!(
|
|
closure_plan
|
|
.destroyed_target_ids()
|
|
.any(|target_id| target_id == page_target_id)
|
|
);
|
|
let target = closure_plan.tab_target();
|
|
let tab_target_id = target.id().to_owned();
|
|
let tab_session_ids = target.session_ids();
|
|
Some(TargetClosureCleanupPlan::new(
|
|
tab_target_id,
|
|
reason,
|
|
tab_session_ids,
|
|
))
|
|
}
|
|
|
|
pub(crate) fn rollback_top_level_target_tab_sessions_without_event(
|
|
&mut self,
|
|
page_target_id: &str,
|
|
) {
|
|
let Some(closure_plan) = self.remove_tab_for_page_target(page_target_id) else {
|
|
return;
|
|
};
|
|
for session_id in closure_plan.tab_target().session_ids() {
|
|
self.clear_auto_attach_owner(Some(&session_id));
|
|
self.rollback_attached_session_without_event(&session_id);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn tab_target_id_for_session_id(&self, session_id: &str) -> Option<&str> {
|
|
self.target_control.tab_target_id_for_session_id(session_id)
|
|
}
|
|
|
|
pub(crate) fn browser_context_id_for_tab_target_id(
|
|
&self,
|
|
tab_target_id: &str,
|
|
) -> Option<String> {
|
|
let page_target_id = self.primary_page_target_id_for_tab_target_id(tab_target_id)?;
|
|
self.browser_contexts()
|
|
.find(|browser_context| {
|
|
browser_context
|
|
.devtools_target_info(page_target_id)
|
|
.is_some()
|
|
})
|
|
.map(|browser_context| browser_context.id.clone())
|
|
}
|
|
|
|
pub(crate) fn tab_target_info_for_page_target_info(
|
|
&self,
|
|
page_target_info: &DevToolsTargetInfo,
|
|
) -> Option<DevToolsTargetInfo> {
|
|
if page_target_info.kind != DevToolsTargetKind::Page {
|
|
return None;
|
|
}
|
|
self.target_control
|
|
.tab_target_info_for_page_target_info(page_target_info.clone())
|
|
}
|
|
|
|
pub(crate) fn tab_target_info(&self, tab_target_id: &str) -> Option<DevToolsTargetInfo> {
|
|
let target_info = self.devtools_target_info(tab_target_id)?;
|
|
(target_info.kind == DevToolsTargetKind::Tab).then_some(target_info)
|
|
}
|
|
|
|
pub(crate) fn set_target_discovery_for_owner(
|
|
&mut self,
|
|
owner_session_id: Option<&str>,
|
|
filter: CdpTargetFilter,
|
|
) {
|
|
let root_filter = owner_session_id
|
|
.is_none()
|
|
.then(|| filter.to_devtools_entries());
|
|
self.target_control
|
|
.set_discover_targets(owner_session_id, filter);
|
|
if let Some(root_filter) = root_filter {
|
|
self.target_discovery_enabled = true;
|
|
self.target_info_change_events_enabled = true;
|
|
self.target_discovery_filter = Some(root_filter);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn set_target_discovery_for_owner_from_devtools_filter(
|
|
&mut self,
|
|
owner_session_id: Option<&str>,
|
|
filter: Option<Vec<DevToolsTargetFilterEntry>>,
|
|
) {
|
|
let handler_filter = filter
|
|
.clone()
|
|
.map(CdpTargetFilter::from_devtools_entries)
|
|
.unwrap_or_else(CdpTargetFilter::default_target_discovery);
|
|
self.set_target_discovery_for_owner(owner_session_id, handler_filter);
|
|
if owner_session_id.is_none() {
|
|
self.target_discovery_filter = filter;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn clear_target_discovery_for_owner(&mut self, owner_session_id: Option<&str>) {
|
|
self.target_control.clear_discover_targets(owner_session_id);
|
|
if owner_session_id.is_none() {
|
|
self.target_discovery_enabled = false;
|
|
self.target_info_change_events_enabled = false;
|
|
self.target_discovery_filter = None;
|
|
}
|
|
}
|
|
|
|
pub fn root_target_discovery_enabled(&self) -> bool {
|
|
self.target_discovery_enabled
|
|
}
|
|
|
|
pub fn replace_root_target_discovery_enabled(&mut self, enabled: bool) -> bool {
|
|
let previous = self.target_discovery_enabled;
|
|
if previous != enabled {
|
|
self.set_root_target_discovery_enabled(enabled);
|
|
}
|
|
previous
|
|
}
|
|
|
|
pub fn set_root_target_discovery_enabled(&mut self, enabled: bool) {
|
|
if enabled {
|
|
self.set_target_discovery_for_owner(None, CdpTargetFilter::default_target_discovery());
|
|
} else {
|
|
self.clear_target_discovery_for_owner(None);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn target_discovery_filter_for_owner(
|
|
&self,
|
|
owner_session_id: Option<&str>,
|
|
) -> Option<Vec<DevToolsTargetFilterEntry>> {
|
|
self.target_control
|
|
.discover_filter_entries(owner_session_id)
|
|
}
|
|
|
|
pub(crate) fn initial_target_created_events_for_discovery_owner(
|
|
&mut self,
|
|
owner_session_id: Option<&str>,
|
|
target_infos: Vec<DevToolsTargetInfo>,
|
|
) -> Vec<BackgroundProtocolEvent> {
|
|
self.target_control
|
|
.initial_target_created_events_for_owner(owner_session_id, target_infos)
|
|
}
|
|
|
|
pub(crate) fn has_any_target_discovery(&self) -> bool {
|
|
self.target_control.has_any_discovery()
|
|
}
|
|
|
|
pub(crate) fn has_any_target_info_observer(&self) -> bool {
|
|
self.target_control.has_any_target_info_observer()
|
|
}
|
|
|
|
fn exact_target_created_events_for_all_discovery_owners(
|
|
&mut self,
|
|
target_info: DevToolsTargetInfo,
|
|
) -> Vec<BackgroundProtocolEvent> {
|
|
self.target_control
|
|
.target_created_events_for_all_discovery_owners(target_info)
|
|
}
|
|
|
|
pub(crate) fn target_created_event_plan(&mut self, target_id: &str) -> TargetEventPlan {
|
|
self.target_created_event_plan_for_target_delta(target_id)
|
|
}
|
|
|
|
fn target_created_event_plan_for_target_delta(&mut self, target_id: &str) -> TargetEventPlan {
|
|
let deltas = self.target_control.target_created_deltas(target_id);
|
|
self.target_host_delta_events(deltas)
|
|
}
|
|
|
|
pub(crate) fn target_info_changed_event_plan_for_observable_target(
|
|
&mut self,
|
|
browser_context_id: &str,
|
|
target_id: &str,
|
|
) -> TargetEventPlan {
|
|
let Some(target_info) = self
|
|
.browser_context_by_id(browser_context_id)
|
|
.and_then(|browser_context| browser_context.devtools_target_info(target_id))
|
|
else {
|
|
return TargetEventPlan::default();
|
|
};
|
|
let tab_target_info = self.tab_target_info_for_page_target_info(&target_info);
|
|
self.notify_target_host_lifecycle(CdpTargetHostLifecycleDelta::InfoChanged(target_info));
|
|
if let Some(tab_target_info) = tab_target_info {
|
|
self.notify_target_host_lifecycle(CdpTargetHostLifecycleDelta::InfoChanged(
|
|
tab_target_info,
|
|
));
|
|
}
|
|
if !self.has_any_target_info_observer() {
|
|
return TargetEventPlan::default();
|
|
}
|
|
self.exact_target_info_changed_event_plan_for_target_delta(target_id)
|
|
}
|
|
|
|
pub(crate) fn target_info_changed_event_plan_for_owner(
|
|
&mut self,
|
|
owner: &CommandOwnerScope,
|
|
) -> TargetEventPlan {
|
|
let Some((browser_context_id, Some(target_id))) =
|
|
self.target_owner_identity_for_owner(owner)
|
|
else {
|
|
return TargetEventPlan::default();
|
|
};
|
|
self.target_info_changed_event_plan_for_observable_target(&browser_context_id, &target_id)
|
|
}
|
|
|
|
pub(crate) fn exact_target_info_changed_event_plan_for_target_delta(
|
|
&mut self,
|
|
target_id: &str,
|
|
) -> TargetEventPlan {
|
|
self.target_host_delta_events([TargetHostDelta::info_changed(target_id.to_owned())])
|
|
}
|
|
|
|
fn target_host_delta_events(
|
|
&mut self,
|
|
deltas: impl IntoIterator<Item = TargetHostDelta>,
|
|
) -> TargetEventPlan {
|
|
self.prepared_target_host_delta_events(
|
|
deltas
|
|
.into_iter()
|
|
.map(PreparedTargetHostDelta::without_snapshot),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn prepared_target_host_delta_event_plan(
|
|
&mut self,
|
|
prepared_delta: PreparedTargetHostDelta,
|
|
) -> TargetEventPlan {
|
|
self.prepared_target_host_deltas_event_plan([prepared_delta])
|
|
}
|
|
|
|
pub(crate) fn prepared_target_info_changed_event_plan_for_discovery_owners(
|
|
&self,
|
|
prepared_delta: PreparedTargetHostDelta,
|
|
) -> TargetEventPlan {
|
|
let (delta, prepared_snapshot) = prepared_delta.into_parts();
|
|
let TargetHostDelta::InfoChanged { target_id } = delta else {
|
|
debug_assert!(false, "expected a prepared targetInfoChanged delta");
|
|
return TargetEventPlan::default();
|
|
};
|
|
let Some(target_info) =
|
|
prepared_snapshot.or_else(|| self.target_info_for_host_delta(&target_id))
|
|
else {
|
|
return TargetEventPlan::default();
|
|
};
|
|
TargetEventPlan::from_background_events(
|
|
self.target_control
|
|
.target_info_changed_events_for_all_discovery_owners(target_info),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn prepared_target_host_deltas_event_plan(
|
|
&mut self,
|
|
prepared_deltas: impl IntoIterator<Item = PreparedTargetHostDelta>,
|
|
) -> TargetEventPlan {
|
|
self.prepared_target_host_delta_events(prepared_deltas)
|
|
}
|
|
|
|
pub(crate) fn prepare_destroyed_target_host_delta(
|
|
&self,
|
|
target_id: &str,
|
|
) -> Option<PreparedTargetHostDelta> {
|
|
self.target_info_for_host_delta(target_id)
|
|
.map(|target_info| {
|
|
PreparedTargetHostDelta::destroyed(target_id.to_owned(), Some(target_info))
|
|
})
|
|
}
|
|
|
|
pub(crate) fn prepare_target_host_closure(&self, target_id: &str) -> PreparedTargetHostClosure {
|
|
let mut detached_info_deltas = Vec::new();
|
|
let mut destroyed_deltas = Vec::new();
|
|
for delta in self.target_control.target_destroyed_deltas(target_id) {
|
|
let target_id = delta.target_id().to_owned();
|
|
let Some(target_info) = self.target_info_for_host_delta(&target_id) else {
|
|
continue;
|
|
};
|
|
if target_info.attached {
|
|
let mut detached_target_info = target_info.clone();
|
|
detached_target_info.attached = false;
|
|
detached_info_deltas.push(PreparedTargetHostDelta::info_changed(
|
|
target_id.clone(),
|
|
Some(detached_target_info),
|
|
));
|
|
}
|
|
destroyed_deltas.push(PreparedTargetHostDelta::destroyed(
|
|
target_id,
|
|
Some(target_info),
|
|
));
|
|
}
|
|
PreparedTargetHostClosure::new(detached_info_deltas, destroyed_deltas)
|
|
}
|
|
|
|
fn prepared_target_host_delta_events(
|
|
&mut self,
|
|
deltas: impl IntoIterator<Item = PreparedTargetHostDelta>,
|
|
) -> TargetEventPlan {
|
|
TargetEventPlan::from_background_events(
|
|
deltas
|
|
.into_iter()
|
|
.flat_map(|delta| self.single_prepared_target_host_delta_events(delta))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
fn single_prepared_target_host_delta_events(
|
|
&mut self,
|
|
prepared_delta: PreparedTargetHostDelta,
|
|
) -> Vec<BackgroundProtocolEvent> {
|
|
let (delta, prepared_snapshot) = prepared_delta.into_parts();
|
|
match delta {
|
|
TargetHostDelta::Created { target_id } => {
|
|
let Some(target_info) =
|
|
prepared_snapshot.or_else(|| self.target_info_for_host_delta(&target_id))
|
|
else {
|
|
return Vec::new();
|
|
};
|
|
self.exact_target_created_events_for_all_discovery_owners(target_info)
|
|
}
|
|
TargetHostDelta::InfoChanged { target_id } => {
|
|
let Some(target_info) =
|
|
prepared_snapshot.or_else(|| self.target_info_for_host_delta(&target_id))
|
|
else {
|
|
return Vec::new();
|
|
};
|
|
self.exact_target_info_changed_events_for_all_observer_owners(target_info)
|
|
}
|
|
TargetHostDelta::Destroyed { target_id } => {
|
|
let Some(target_info) =
|
|
prepared_snapshot.or_else(|| self.target_info_for_host_delta(&target_id))
|
|
else {
|
|
return Vec::new();
|
|
};
|
|
self.exact_target_destroyed_events_for_all_discovery_owners(target_info)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn target_info_for_host_delta(&self, target_id: &str) -> Option<DevToolsTargetInfo> {
|
|
self.devtools_target_info(target_id)
|
|
}
|
|
|
|
fn exact_target_info_changed_events_for_all_observer_owners(
|
|
&self,
|
|
target_info: DevToolsTargetInfo,
|
|
) -> Vec<BackgroundProtocolEvent> {
|
|
self.target_control
|
|
.target_info_changed_events_for_all_observer_owners(target_info)
|
|
}
|
|
|
|
fn exact_target_destroyed_events_for_all_discovery_owners(
|
|
&mut self,
|
|
target_info: DevToolsTargetInfo,
|
|
) -> Vec<BackgroundProtocolEvent> {
|
|
self.target_control
|
|
.target_destroyed_events_for_all_discovery_owners(target_info)
|
|
}
|
|
|
|
pub(crate) fn target_crashed_events_for_all_discovery_owners(
|
|
&self,
|
|
target_id: &str,
|
|
status: &str,
|
|
error_code: i32,
|
|
) -> Vec<BackgroundProtocolEvent> {
|
|
self.target_control
|
|
.target_crashed_events_for_all_discovery_owners(target_id, status, error_code)
|
|
}
|
|
|
|
pub(crate) fn target_destroyed_automation_events(
|
|
&self,
|
|
target_info: DevToolsTargetInfo,
|
|
) -> Vec<BackgroundProtocolEvent> {
|
|
target_destroyed_automation_events(
|
|
self.project_page_tab_target_infos_for_destruction(target_info),
|
|
)
|
|
}
|
|
|
|
fn project_page_tab_target_infos_for_destruction(
|
|
&self,
|
|
target_info: DevToolsTargetInfo,
|
|
) -> Vec<DevToolsTargetInfo> {
|
|
self.target_control
|
|
.project_page_tab_target_infos_for_destruction(target_info)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn tab_target_count(&self) -> usize {
|
|
self.target_control.len()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn target_registry_host_kind(&self, target_id: &str) -> Option<DevToolsTargetKind> {
|
|
self.devtools_target_info(target_id)
|
|
.map(|target| target.kind)
|
|
}
|
|
|
|
fn has_registered_target_id(&self, target_id: &str) -> bool {
|
|
self.target_control.contains_tab_or_page_relation(target_id)
|
|
|| self
|
|
.browser_contexts()
|
|
.any(|context| context.devtools_target_info(target_id).is_some())
|
|
}
|
|
|
|
pub fn install_default_browser_target(&mut self) {
|
|
if self.default_target_lifecycle.is_live() || self.default_target_lifecycle.is_closed() {
|
|
return;
|
|
}
|
|
|
|
let was_placeholder = self.default_target_lifecycle.is_placeholder();
|
|
let default_browser_context_id = self.default_browser_context_id().to_owned();
|
|
let default_target_id = self.default_target_id().to_owned();
|
|
if !self.has_browser_context_id(&default_browser_context_id) {
|
|
let mut browser_context = self.new_browser_context(default_browser_context_id.clone());
|
|
browser_context.set_active_target_id(default_target_id.clone());
|
|
browser_context.set_target_url("about:blank".to_owned());
|
|
browser_context.begin_active_target_initial_empty_document("about:blank".to_owned());
|
|
self.insert_browser_context(browser_context);
|
|
} else {
|
|
let browser_context = self
|
|
.browser_context_by_id_mut(&default_browser_context_id)
|
|
.expect("known default BrowserContext must remain addressable");
|
|
if !browser_context.is_active_target(&default_target_id)
|
|
&& browser_context
|
|
.background_target(&default_target_id)
|
|
.is_none()
|
|
{
|
|
if browser_context.has_active_target() {
|
|
browser_context.stage_background_target(
|
|
default_target_id.clone(),
|
|
None,
|
|
"about:blank".to_owned(),
|
|
Some("about:blank".to_owned()),
|
|
None,
|
|
);
|
|
} else {
|
|
browser_context.set_active_target_id(default_target_id.clone());
|
|
browser_context.set_target_url("about:blank".to_owned());
|
|
browser_context
|
|
.begin_active_target_initial_empty_document("about:blank".to_owned());
|
|
}
|
|
}
|
|
}
|
|
if !self
|
|
.target_control
|
|
.contains_tab_or_page_relation(&default_target_id)
|
|
{
|
|
self.register_top_level_page_target(&default_target_id);
|
|
}
|
|
self.default_target_lifecycle.mark_live();
|
|
if !was_placeholder {
|
|
self.notify_target_host_activated(&default_target_id);
|
|
}
|
|
}
|
|
|
|
pub fn enable_default_target_on_auto_attach(&mut self) {
|
|
self.install_default_target_on_auto_attach = true;
|
|
}
|
|
|
|
pub(crate) fn install_default_browser_target_for_auto_attach_if_enabled(&mut self) {
|
|
if self.install_default_target_on_auto_attach {
|
|
self.install_default_browser_target();
|
|
}
|
|
}
|
|
|
|
pub fn gen_target_id(&mut self) -> String {
|
|
loop {
|
|
let id = if let Some(allocator) = self.shared_target_id_allocator.as_ref() {
|
|
allocator
|
|
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
|
|
current.checked_add(1)
|
|
})
|
|
.expect("shared target id space exhausted")
|
|
+ 1
|
|
} else {
|
|
self.next_target_id = self
|
|
.next_target_id
|
|
.checked_add(1)
|
|
.expect("target id space exhausted");
|
|
u64::from(self.next_target_id)
|
|
};
|
|
let target_id = format!("TID-{id}");
|
|
// Target ids supplied while restoring or embedding an existing
|
|
// target share the same CDP namespace as ids allocated here.
|
|
// Never let a later worker/page allocation alias such a target:
|
|
// looking it up would otherwise return the pre-existing target's
|
|
// kind and state even though the renderer record names a worker.
|
|
if !self.has_registered_target_id(&target_id) {
|
|
return target_id;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn gen_tab_target_id(&mut self) -> String {
|
|
loop {
|
|
let id = if let Some(allocator) = self.shared_tab_target_id_allocator.as_ref() {
|
|
allocator
|
|
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
|
|
current.checked_add(1)
|
|
})
|
|
.expect("shared tab target id space exhausted")
|
|
+ 1
|
|
} else {
|
|
self.next_tab_target_id = self
|
|
.next_tab_target_id
|
|
.checked_add(1)
|
|
.expect("tab target id space exhausted");
|
|
u64::from(self.next_tab_target_id)
|
|
};
|
|
let target_id = format!("TAB-{id}");
|
|
if !self.has_registered_target_id(&target_id) {
|
|
return target_id;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn gen_session_id(&mut self) -> String {
|
|
loop {
|
|
self.next_session_id = self
|
|
.next_session_id
|
|
.checked_add(1)
|
|
.expect("DevTools session id space exhausted");
|
|
let session_id = format!("SID-{}", self.next_session_id);
|
|
// Embedded callers and test/protocol bootstrap paths may install a
|
|
// caller-supplied session id without advancing this allocator.
|
|
// A generated id must therefore be unique in the live CDP
|
|
// namespace, not merely unique among earlier generated ids.
|
|
//
|
|
// Chromium sidesteps this collision class by assigning each
|
|
// attached DevTools session a fresh UnguessableToken. Moli
|
|
// keeps readable ids, so it must explicitly skip occupied ones.
|
|
if self.session_route(Some(&session_id)).is_none() {
|
|
return session_id;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn open_global_io_stream(&mut self, bytes: Vec<u8>) -> String {
|
|
self.next_global_io_stream_id = self
|
|
.next_global_io_stream_id
|
|
.checked_add(1)
|
|
.expect("global IO stream id space exhausted");
|
|
let handle = format!("BROWSER-STREAM-{}", self.next_global_io_stream_id);
|
|
self.global_io_streams
|
|
.insert(handle.clone(), IoStreamState::from_bytes(bytes, 0));
|
|
handle
|
|
}
|
|
|
|
fn notify_target_host_lifecycle(&self, delta: CdpTargetHostLifecycleDelta) {
|
|
if let Some(observer) = self.target_host_lifecycle_observer.as_ref() {
|
|
observer.notify(delta);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn notify_target_host_activated(&self, target_id: &str) {
|
|
self.notify_target_host_lifecycle(CdpTargetHostLifecycleDelta::Activated {
|
|
target_id: target_id.to_owned(),
|
|
});
|
|
if let Some(tab_target_id) = self.tab_target_id_for_page_target_id(target_id) {
|
|
self.notify_target_host_lifecycle(CdpTargetHostLifecycleDelta::Activated {
|
|
target_id: tab_target_id.to_owned(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct BrowserDownloadBehavior {
|
|
pub behavior: String,
|
|
pub download_path: Option<String>,
|
|
pub automation_events_enabled: bool,
|
|
pub webdriver_bidi_events_enabled: bool,
|
|
pub browser_context_id: Option<String>,
|
|
pub browser_context_overrides: HashMap<String, BrowserDownloadBehaviorSettings>,
|
|
browser_event_subscription_generations: HashMap<Option<String>, u64>,
|
|
next_browser_event_subscription_generation: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct BrowserDownloadBehaviorSettings {
|
|
pub behavior: String,
|
|
pub download_path: Option<String>,
|
|
pub automation_events_enabled: bool,
|
|
}
|
|
|
|
impl Default for BrowserDownloadBehaviorSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
behavior: "default".to_owned(),
|
|
download_path: None,
|
|
automation_events_enabled: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for BrowserDownloadBehavior {
|
|
fn default() -> Self {
|
|
let default_settings = BrowserDownloadBehaviorSettings::default();
|
|
Self {
|
|
behavior: default_settings.behavior,
|
|
download_path: default_settings.download_path,
|
|
automation_events_enabled: default_settings.automation_events_enabled,
|
|
webdriver_bidi_events_enabled: false,
|
|
browser_context_id: None,
|
|
browser_context_overrides: HashMap::new(),
|
|
browser_event_subscription_generations: HashMap::new(),
|
|
next_browser_event_subscription_generation: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BrowserDownloadBehavior {
|
|
pub(crate) fn set_global(
|
|
&mut self,
|
|
behavior: String,
|
|
download_path: Option<String>,
|
|
automation_events_enabled: bool,
|
|
) {
|
|
self.behavior = behavior;
|
|
self.download_path = download_path;
|
|
self.automation_events_enabled = automation_events_enabled;
|
|
self.browser_context_id = None;
|
|
}
|
|
|
|
pub(crate) fn set_global_policy(&mut self, behavior: String, download_path: Option<String>) {
|
|
self.behavior = behavior;
|
|
self.download_path = download_path;
|
|
self.browser_context_id = None;
|
|
}
|
|
|
|
pub(crate) fn set_browser_context(
|
|
&mut self,
|
|
browser_context_id: String,
|
|
behavior: String,
|
|
download_path: Option<String>,
|
|
automation_events_enabled: bool,
|
|
) {
|
|
self.browser_context_overrides.insert(
|
|
browser_context_id.clone(),
|
|
BrowserDownloadBehaviorSettings {
|
|
behavior: behavior.clone(),
|
|
download_path: download_path.clone(),
|
|
automation_events_enabled,
|
|
},
|
|
);
|
|
self.browser_context_id = Some(browser_context_id);
|
|
}
|
|
|
|
pub(crate) fn set_browser_context_policy(
|
|
&mut self,
|
|
browser_context_id: String,
|
|
behavior: String,
|
|
download_path: Option<String>,
|
|
) {
|
|
let automation_events_enabled = self
|
|
.browser_context_overrides
|
|
.get(&browser_context_id)
|
|
.is_some_and(|settings| settings.automation_events_enabled);
|
|
self.browser_context_overrides.insert(
|
|
browser_context_id.clone(),
|
|
BrowserDownloadBehaviorSettings {
|
|
behavior,
|
|
download_path,
|
|
automation_events_enabled,
|
|
},
|
|
);
|
|
self.browser_context_id = Some(browser_context_id);
|
|
}
|
|
|
|
pub(crate) fn reset_global(&mut self) {
|
|
let default = BrowserDownloadBehaviorSettings::default();
|
|
self.behavior = default.behavior;
|
|
self.download_path = default.download_path;
|
|
self.automation_events_enabled = default.automation_events_enabled;
|
|
self.browser_context_id = None;
|
|
}
|
|
|
|
pub(crate) fn reset_browser_context(&mut self, browser_context_id: &str) {
|
|
self.browser_context_overrides.remove(browser_context_id);
|
|
if self.browser_context_id.as_deref() == Some(browser_context_id) {
|
|
self.browser_context_id = None;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn enable_webdriver_bidi_events(&mut self) -> bool {
|
|
let changed = !self.webdriver_bidi_events_enabled;
|
|
self.webdriver_bidi_events_enabled = true;
|
|
changed
|
|
}
|
|
|
|
pub(crate) fn disable_webdriver_bidi_events(&mut self) -> bool {
|
|
let changed = self.webdriver_bidi_events_enabled;
|
|
self.webdriver_bidi_events_enabled = false;
|
|
changed
|
|
}
|
|
|
|
pub(crate) fn clear_browser_context(&mut self, browser_context_id: &str) {
|
|
self.reset_browser_context(browser_context_id);
|
|
}
|
|
|
|
pub(crate) fn set_browser_events_enabled_for_session(
|
|
&mut self,
|
|
session_id: Option<&str>,
|
|
enabled: bool,
|
|
) {
|
|
self.next_browser_event_subscription_generation = self
|
|
.next_browser_event_subscription_generation
|
|
.wrapping_add(1);
|
|
let session_id = session_id.map(str::to_owned);
|
|
if enabled {
|
|
self.browser_event_subscription_generations
|
|
.insert(session_id, self.next_browser_event_subscription_generation);
|
|
} else {
|
|
self.browser_event_subscription_generations
|
|
.remove(&session_id);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn browser_event_observers(&self) -> Vec<(Option<String>, u64)> {
|
|
let mut observers = self
|
|
.browser_event_subscription_generations
|
|
.iter()
|
|
.map(|(session_id, generation)| (session_id.clone(), *generation))
|
|
.collect::<Vec<_>>();
|
|
observers.sort_by(|left, right| left.0.cmp(&right.0));
|
|
observers
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn browser_event_session_ids(&self) -> Vec<Option<String>> {
|
|
self.browser_event_observers()
|
|
.into_iter()
|
|
.map(|(session_id, _)| session_id)
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn browser_event_subscription_is_current(
|
|
&self,
|
|
session_id: Option<&str>,
|
|
generation: u64,
|
|
) -> bool {
|
|
self.browser_event_subscription_generations
|
|
.get(&session_id.map(str::to_owned))
|
|
.is_some_and(|current| *current == generation)
|
|
}
|
|
|
|
pub(crate) fn effective_for_browser_context(
|
|
&self,
|
|
browser_context_id: Option<&str>,
|
|
) -> BrowserDownloadBehaviorSettings {
|
|
if let Some(browser_context_id) = browser_context_id
|
|
&& let Some(settings) = self.browser_context_overrides.get(browser_context_id)
|
|
{
|
|
return settings.clone();
|
|
}
|
|
BrowserDownloadBehaviorSettings {
|
|
behavior: self.behavior.clone(),
|
|
download_path: self.download_path.clone(),
|
|
automation_events_enabled: self.automation_events_enabled,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct PermissionOverride {
|
|
pub permission: serde_json::Value,
|
|
pub setting: String,
|
|
pub origin: Option<String>,
|
|
pub embedded_origin: Option<String>,
|
|
pub browser_context_id: Option<String>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|