use std::sync::{ Arc, atomic::{AtomicBool, AtomicU64, Ordering}, }; use std::{ cell::RefCell, collections::HashMap, rc::{Rc, Weak}, }; use crate::planning::{ PreparedScript, SharedScriptSourceLoadCompleter, external_script_source_load_outcome_from_result, load_prepared_script_source_outcome_with_document_character_set, }; use crate::shared_worker_runtime::RendererSharedWorkerRuntimeDiagnostics; use moli_fetch::{ResponseBody, ResponseHead}; use parking_lot::Mutex; use serde_json::{Value, json}; use url::Url; mod dedicated_workers; mod service_worker_runtime; mod service_workers; mod shared_workers; static NEXT_RENDERER_STORAGE_PARTITION_ID: AtomicU64 = AtomicU64::new(0); static NEXT_RENDERER_BROWSER_CONTEXT_RUNTIME_ID: AtomicU64 = AtomicU64::new(1); #[derive(Clone, Debug, Default)] pub(crate) struct RendererOutputTransportSenderSlot { sender: Arc>>, } impl RendererOutputTransportSenderSlot { pub(crate) fn set(&self, sender: super::RendererOutputTransportSender) { let mut slot = self.sender.lock(); if let Some(existing) = slot.as_ref() { assert!( existing.same_channel(&sender), "one BrowserContext renderer output stream cannot change protocol transport" ); return; } *slot = Some(sender); } pub(crate) fn sender(&self) -> Option { self.sender.lock().clone() } } pub(crate) use crate::service_worker_runtime::ServiceWorkerControlState; pub use service_workers::{ RendererReservedServiceWorkerClient, RendererServiceWorkerMainResourceFetch, }; /// Browser-context/partition scoped renderer runtime state. /// /// This is the Moli-side equivalent of the communication/runtime state /// Chromium hangs from `StoragePartitionImpl`: page and worker VMs borrow this /// handle so same-context MessagePort, BroadcastChannel, and SharedWorker /// routing share one state owner instead of following individual pages. #[derive(Clone, Debug)] pub struct RendererBrowserContextRuntime { inner: Arc, } /// Thread-affine owner returned by constructors that create a network runtime. /// /// Clones obtained through Deref are renderer handles only. The owner set is /// kept outside `RendererBrowserContextRuntimeInner`, so its fetch JoinHandles /// are not reachable through the cloneable renderer context graph. #[derive(Debug)] pub struct RendererBrowserContextRuntimeOwner { runtime: Option, producer_registry: RendererProducerRegistry, resource_runtime_owner_root: Option, } /// Cloneable, root-bound access for a NavigationEngine that shares this /// renderer browser context. The weak registrar and renderer handle are minted /// together, so replacement cannot accidentally target another context's /// current binding. #[derive(Clone, Debug)] pub struct RendererBrowserContextRuntimeOwnerAccess { runtime: RendererBrowserContextRuntime, producer_registrar: RendererProducerRegistrar, resource_runtime_registrar: crate::network::BrowserResourceRuntimeOwnerRegistrar, } #[derive(Debug)] struct RendererProducerRegistry { inner: Rc>, } #[derive(Clone, Debug)] struct RendererProducerRegistrar { inner: Weak>, } #[derive(Debug, Default)] struct RendererProducerRegistryState { terminal: bool, producers: HashMap, } impl RendererProducerRegistry { fn new() -> Self { Self { inner: Rc::new(RefCell::new(RendererProducerRegistryState::default())), } } fn registrar(&self) -> RendererProducerRegistrar { RendererProducerRegistrar { inner: Rc::downgrade(&self.inner), } } fn cancel_all(&self) { let producers = { let mut state = self.inner.borrow_mut(); state.terminal = true; std::mem::take(&mut state.producers) }; for producer in producers.into_values() { producer.cancel_page_producers(); } } } impl RendererProducerRegistrar { fn register(&self, runtime: &super::JsRuntime) -> Result<(), &'static str> { let Some(registry) = self.inner.upgrade() else { return Err("renderer browser context owner has been dropped"); }; let mut state = registry.borrow_mut(); if state.terminal { return Err("renderer browser context owner is shut down"); } state.producers.retain(|_, producer| producer.is_live()); let producer = runtime.producer_shutdown_handle(); state .producers .insert(producer.renderer_owner_id(), producer); Ok(()) } } /// Worker-thread view of browser-context runtime state. /// /// Workers inherit owner-scoped communication registries, but they are not /// SharedWorker requesters: the current browser-compatible surface only exposes /// the SharedWorker constructor on Window. #[derive(Clone, Debug)] pub(crate) struct RendererWorkerContextRuntime { message_port_registry: crate::message_port_runtime::SharedMessagePortRegistry, broadcast_channel_registry: crate::broadcast_channel_runtime::SharedBroadcastChannelRegistry, storage_partition_identity: RendererStoragePartitionIdentity, } /// Process-local storage partition identity shared by pages and workers in one /// browser-context runtime. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct RendererStoragePartitionIdentity { browser_context_id: String, profile_partition_id: String, } #[derive(Debug)] struct RendererBrowserContextRuntimeInner { id: super::RendererBrowserContextRuntimeId, message_port_registry: crate::message_port_runtime::SharedMessagePortRegistry, broadcast_channel_registry: crate::broadcast_channel_runtime::SharedBroadcastChannelRegistry, browser_resource_runtime: crate::network::BrowserResourceRuntimeBinding, shared_worker_runtime: shared_workers::LazySharedWorkerRuntime, service_worker_runtime: service_worker_runtime::LazyServiceWorkerRuntime, storage_partition_identity: RendererStoragePartitionIdentity, next_web_storage_opaque_context_nonce: AtomicU64, next_child_document_loader_id: AtomicU64, next_detached_parser_script_fetch_id: AtomicU64, next_dedicated_worker_instance_id: AtomicU64, dedicated_worker_devtools_handles: Mutex>, dedicated_worker_pause_on_start_for_devtools: AtomicBool, javascript_dialog_handler_enabled: AtomicBool, renderer_output_transport_tx: RendererOutputTransportSenderSlot, } #[derive(Debug)] struct DetachedParserScriptFetchContinuationInner { script: PreparedScript, request_client: crate::network::ResourceRequestClient, task_runner: crate::network::RendererResourceTaskRunner, document_character_set: Option, completer: SharedScriptSourceLoadCompleter, } #[derive(Clone, Debug)] pub struct DetachedParserScriptFetchContinuation { inner: Arc>>, } impl PartialEq for DetachedParserScriptFetchContinuation { fn eq(&self, other: &Self) -> bool { Arc::ptr_eq(&self.inner, &other.inner) } } impl DetachedParserScriptFetchContinuation { fn new( script: PreparedScript, request_client: crate::network::ResourceRequestClient, task_runner: crate::network::RendererResourceTaskRunner, document_character_set: Option, completer: SharedScriptSourceLoadCompleter, ) -> Self { Self { inner: Arc::new(Mutex::new(Some( DetachedParserScriptFetchContinuationInner { script, request_client, task_runner, document_character_set, completer, }, ))), } } fn take(&self) -> Option { self.inner.lock().take() } pub fn fail(&self, error_text: String) -> bool { let Some(inner) = self.take() else { return false; }; inner .completer .finish(external_script_source_load_outcome_from_result( &inner.script, Err(error_text), inner.document_character_set.as_deref(), )); true } pub fn fulfill( &self, response_code: u16, response_headers: Vec<(String, String)>, response_body: Vec, ) -> bool { let Some(inner) = self.take() else { return false; }; let text = String::from_utf8_lossy(&response_body).into_owned(); let response = crate::protocol_types::NavigationResponse::from_head_and_materialized_body( ResponseHead { final_url: inner.script.url.clone(), status: response_code, headers: response_headers, request_cookie_report: None, cookie_set_reports: Vec::new(), redirected: false, redirect_chain: Vec::new(), from_cache: false, negotiated_http_version: None, }, ResponseBody::materialized_text(text, response_body), ); inner .completer .finish(external_script_source_load_outcome_from_result( &inner.script, Ok(response), inner.document_character_set.as_deref(), )); true } pub fn continue_request(&self, url: Option) -> bool { let Some(mut inner) = self.take() else { return false; }; if let Some(url) = url { inner.script.url = url; } let task_runner = inner.task_runner.clone(); task_runner.spawn(async move { let outcome = load_prepared_script_source_outcome_with_document_character_set( &inner.script, &inner.request_client, inner.document_character_set.as_deref(), Some(moli_fetch::RequestResourceType::ParserBlockingScript), ) .await; inner.completer.finish(outcome); }); true } } impl Drop for RendererBrowserContextRuntimeInner { fn drop(&mut self) { terminate_browser_context_resource_producers(self); } } fn terminate_browser_context_resource_producers(inner: &RendererBrowserContextRuntimeInner) { let dedicated_worker_handles = std::mem::take(&mut *inner.dedicated_worker_devtools_handles.lock()); for handle in dedicated_worker_handles.into_values() { let _ = handle.terminate_for_devtools(); } if let Some(shared_worker_runtime) = inner.shared_worker_runtime.get() { shared_worker_runtime.terminate_all_for_context_shutdown(); } if let Some(service_worker_runtime) = inner.service_worker_runtime.get() { service_worker_runtime.terminate_all_for_context_shutdown(); } } impl Default for RendererBrowserContextRuntimeOwner { fn default() -> Self { RendererBrowserContextRuntime::new() } } impl RendererBrowserContextRuntime { // A browser-context runtime is only valid while its thread-affine owner is // retained, so construction returns that owner rather than a bare handle. #[allow(clippy::new_ret_no_self)] pub fn new() -> RendererBrowserContextRuntimeOwner { Self::new_with_service_worker_resource_store( crate::new_shared_service_worker_resource_store(), ) } pub fn new_with_service_worker_resource_store( service_worker_resource_store: crate::SharedServiceWorkerResourceStore, ) -> RendererBrowserContextRuntimeOwner { let browser_resource_runtime_owner = crate::network::BrowserResourceRuntimeOwner::new( &moli_fetch::FetchConfig::default(), moli_cookie_jar::new_shared_browser_cookie_store(), ); Self::new_owned_with_service_worker_resource_store_and_browser_resource_runtime( service_worker_resource_store, browser_resource_runtime_owner, ) } pub fn new_owned_with_service_worker_resource_store_and_browser_resource_runtime( service_worker_resource_store: crate::SharedServiceWorkerResourceStore, browser_resource_runtime_owner: crate::network::BrowserResourceRuntimeOwnerRegistration, ) -> RendererBrowserContextRuntimeOwner { let (resource_runtime_owner_root, browser_resource_runtime_binding) = crate::network::BrowserResourceRuntimeOwnerRoot::new(browser_resource_runtime_owner); let runtime = Self::new_with_service_worker_resource_store_and_browser_resource_runtime_binding( service_worker_resource_store, browser_resource_runtime_binding, ); RendererBrowserContextRuntimeOwner { runtime: Some(runtime), producer_registry: RendererProducerRegistry::new(), resource_runtime_owner_root: Some(resource_runtime_owner_root), } } fn new_with_service_worker_resource_store_and_browser_resource_runtime_binding( service_worker_resource_store: crate::SharedServiceWorkerResourceStore, browser_resource_runtime: crate::network::BrowserResourceRuntimeBinding, ) -> Self { let message_port_registry = crate::message_port_runtime::new_message_port_registry(); let broadcast_channel_registry = crate::broadcast_channel_runtime::new_broadcast_channel_registry(); Self::from_parts( message_port_registry, broadcast_channel_registry, None, service_worker_resource_store, browser_resource_runtime, ) } #[cfg(test)] pub(crate) fn new_with_registries_for_test( message_port_registry: crate::message_port_runtime::SharedMessagePortRegistry, broadcast_channel_registry: crate::broadcast_channel_runtime::SharedBroadcastChannelRegistry, ) -> RendererBrowserContextRuntimeOwner { let browser_resource_runtime_owner = crate::network::BrowserResourceRuntimeOwner::new( &moli_fetch::FetchConfig::default(), moli_cookie_jar::new_shared_browser_cookie_store(), ); let (resource_runtime_owner_root, browser_resource_runtime) = crate::network::BrowserResourceRuntimeOwnerRoot::new(browser_resource_runtime_owner); let runtime = Self::from_parts( message_port_registry, broadcast_channel_registry, None, crate::new_shared_service_worker_resource_store(), browser_resource_runtime, ); RendererBrowserContextRuntimeOwner { runtime: Some(runtime), producer_registry: RendererProducerRegistry::new(), resource_runtime_owner_root: Some(resource_runtime_owner_root), } } #[cfg(test)] pub(crate) fn new_with_parts_for_test( message_port_registry: crate::message_port_runtime::SharedMessagePortRegistry, broadcast_channel_registry: crate::broadcast_channel_runtime::SharedBroadcastChannelRegistry, shared_worker_runtime: crate::shared_worker_runtime::SharedWorkerRuntimeService, ) -> RendererBrowserContextRuntimeOwner { let browser_resource_runtime_owner = crate::network::BrowserResourceRuntimeOwner::new( &moli_fetch::FetchConfig::default(), moli_cookie_jar::new_shared_browser_cookie_store(), ); let (resource_runtime_owner_root, browser_resource_runtime) = crate::network::BrowserResourceRuntimeOwnerRoot::new(browser_resource_runtime_owner); let runtime = Self::from_parts( message_port_registry, broadcast_channel_registry, Some(shared_worker_runtime), crate::new_shared_service_worker_resource_store(), browser_resource_runtime, ); RendererBrowserContextRuntimeOwner { runtime: Some(runtime), producer_registry: RendererProducerRegistry::new(), resource_runtime_owner_root: Some(resource_runtime_owner_root), } } #[cfg(test)] pub(crate) fn new_with_worker_context_and_service_worker_store_for_test( restored_worker_context_runtime: RendererWorkerContextRuntime, service_worker_resource_store: crate::SharedServiceWorkerResourceStore, ) -> RendererBrowserContextRuntimeOwner { let browser_resource_runtime_owner = crate::network::BrowserResourceRuntimeOwner::new( &moli_fetch::FetchConfig::default(), moli_cookie_jar::new_shared_browser_cookie_store(), ); let (resource_runtime_owner_root, browser_resource_runtime) = crate::network::BrowserResourceRuntimeOwnerRoot::new(browser_resource_runtime_owner); let runtime = Self::from_parts_with_worker_context_runtime( restored_worker_context_runtime, None, service_worker_resource_store, browser_resource_runtime, ); RendererBrowserContextRuntimeOwner { runtime: Some(runtime), producer_registry: RendererProducerRegistry::new(), resource_runtime_owner_root: Some(resource_runtime_owner_root), } } fn from_parts( message_port_registry: crate::message_port_runtime::SharedMessagePortRegistry, broadcast_channel_registry: crate::broadcast_channel_runtime::SharedBroadcastChannelRegistry, shared_worker_runtime: Option, service_worker_resource_store: crate::SharedServiceWorkerResourceStore, browser_resource_runtime: crate::network::BrowserResourceRuntimeBinding, ) -> Self { let storage_partition_identity = RendererStoragePartitionIdentity::new_process_local(); let service_worker_context_runtime = RendererWorkerContextRuntime::with_identity( message_port_registry.clone(), broadcast_channel_registry.clone(), storage_partition_identity.clone(), ); Self::from_parts_with_worker_context_runtime( service_worker_context_runtime, shared_worker_runtime, service_worker_resource_store, browser_resource_runtime, ) } fn from_parts_with_worker_context_runtime( service_worker_context_runtime: RendererWorkerContextRuntime, shared_worker_runtime: Option, service_worker_resource_store: crate::SharedServiceWorkerResourceStore, browser_resource_runtime: crate::network::BrowserResourceRuntimeBinding, ) -> Self { let message_port_registry = service_worker_context_runtime.message_port_registry(); let broadcast_channel_registry = service_worker_context_runtime.broadcast_channel_registry(); let storage_partition_identity = service_worker_context_runtime.storage_partition_identity(); let id = super::RendererBrowserContextRuntimeId::new( NEXT_RENDERER_BROWSER_CONTEXT_RUNTIME_ID.fetch_add(1, Ordering::Relaxed), ); let renderer_output_transport_tx = RendererOutputTransportSenderSlot::default(); let shared_worker_runtime = match shared_worker_runtime { Some(service) => shared_workers::LazySharedWorkerRuntime::from_service( service, id, renderer_output_transport_tx.clone(), ), None => shared_workers::LazySharedWorkerRuntime::new( id, renderer_output_transport_tx.clone(), ), }; let service_worker_runtime = service_worker_runtime::LazyServiceWorkerRuntime::new( service_worker_resource_store, service_worker_context_runtime, browser_resource_runtime.clone(), id, renderer_output_transport_tx.clone(), ); Self { inner: Arc::new(RendererBrowserContextRuntimeInner { id, message_port_registry, broadcast_channel_registry, browser_resource_runtime: browser_resource_runtime.clone(), shared_worker_runtime, service_worker_runtime, storage_partition_identity, next_web_storage_opaque_context_nonce: AtomicU64::default(), next_child_document_loader_id: AtomicU64::default(), next_detached_parser_script_fetch_id: AtomicU64::default(), next_dedicated_worker_instance_id: AtomicU64::default(), dedicated_worker_devtools_handles: Mutex::new(HashMap::new()), dedicated_worker_pause_on_start_for_devtools: AtomicBool::new(false), javascript_dialog_handler_enabled: AtomicBool::new(false), renderer_output_transport_tx, }), } } pub fn browser_resource_runtime(&self) -> crate::network::BrowserResourceRuntime { self.inner.browser_resource_runtime.current() } /// Stops browser-context producers before the external network owner root /// broadcasts shutdown and joins fetch threads. pub fn terminate_resource_producers_for_owner_shutdown(&self) { terminate_browser_context_resource_producers(&self.inner); } pub fn shares_state_with(&self, other: &Self) -> bool { Arc::ptr_eq(&self.inner, &other.inner) } pub fn id(&self) -> super::RendererBrowserContextRuntimeId { self.inner.id } pub(crate) fn set_renderer_output_transport_sender( &self, sender: super::RendererOutputTransportSender, ) { self.inner.renderer_output_transport_tx.set(sender.clone()); if let Some(shared_worker_runtime) = self.inner.shared_worker_runtime.get() { shared_worker_runtime.bind_target_output_transport(sender.clone()); } if let Some(service_worker_runtime) = self.inner.service_worker_runtime.get() { service_worker_runtime.bind_target_output_transport(sender); } } pub(crate) fn renderer_output_transport_sender( &self, ) -> Option { self.inner.renderer_output_transport_tx.sender() } pub(crate) fn storage_partition_identity(&self) -> RendererStoragePartitionIdentity { self.inner.storage_partition_identity.clone() } pub fn moli_memory_diagnostics(&self) -> Value { let shared_worker_runtime_initialized = self.inner.shared_worker_runtime.is_initialized(); let mut shared_worker_diagnostics = self.inner.shared_worker_runtime.get().map_or_else( || { json!({ "matchingEntryCount": 0, "loadingInstanceCount": 0, "runningInstanceCount": 0, "clientCount": 0, "loadingHostCount": 0, "runningWorkerIsolateCount": 0, "pendingServiceLaneEventCount": 0, }) }, |runtime| runtime.moli_memory_diagnostics(), ); shared_worker_diagnostics["runtimeInitialized"] = shared_worker_runtime_initialized.into(); let service_worker_runtime_initialized = self.inner.service_worker_runtime.is_initialized(); let deferred_service_worker_window_client_count = self .inner .service_worker_runtime .deferred_window_client_count(); let service_worker_diagnostics = self .inner .service_worker_runtime .get() .map_or_else(Default::default, |runtime| runtime.diagnostics_snapshot()); json!({ "rendererOutputTransport": self .inner .renderer_output_transport_tx .sender() .map(|sender| sender.diagnostics()), "sharedWorker": shared_worker_diagnostics, "serviceWorker": { "runtimeInitialized": service_worker_runtime_initialized, "deferredWindowClients": deferred_service_worker_window_client_count, "registrations": service_worker_diagnostics.registration_count, "runtimeRegistrations": service_worker_diagnostics.registration_count, "versions": service_worker_diagnostics.version_count, "installingVersions": service_worker_diagnostics.installing_version_count, "activatedVersions": service_worker_diagnostics.activated_version_count, "redundantVersions": service_worker_diagnostics.redundant_version_count, "stoppedVersions": service_worker_diagnostics.stopped_version_count, "startingVersions": service_worker_diagnostics.starting_version_count, "runningVersions": service_worker_diagnostics.running_version_count, "stoppingVersions": service_worker_diagnostics.stopping_version_count, "runningWorkers": service_worker_diagnostics.running_host_count, "pendingUnregistrations": service_worker_diagnostics.pending_unregistration_count, "inFlightEvents": service_worker_diagnostics.in_flight_event_count, "liveClients": service_worker_diagnostics.live_client_count, "controlledClients": service_worker_diagnostics.controlled_client_count, "pendingServiceLaneEventCount": service_worker_diagnostics.pending_service_lane_event_count, }, }) } pub fn shared_worker_running_worker_isolate_count_for_diagnostics(&self) -> usize { self.shared_worker_runtime_diagnostics_for_diagnostics() .running_worker_isolate_count } pub fn shared_worker_runtime_diagnostics_for_diagnostics( &self, ) -> RendererSharedWorkerRuntimeDiagnostics { self.inner .shared_worker_runtime .get() .map_or_else(RendererSharedWorkerRuntimeDiagnostics::default, |runtime| { runtime.diagnostics_snapshot() }) } pub(crate) fn message_port_registry( &self, ) -> crate::message_port_runtime::SharedMessagePortRegistry { self.inner.message_port_registry.clone() } pub(crate) fn broadcast_channel_registry( &self, ) -> crate::broadcast_channel_runtime::SharedBroadcastChannelRegistry { self.inner.broadcast_channel_registry.clone() } pub(crate) fn worker_context_runtime(&self) -> RendererWorkerContextRuntime { RendererWorkerContextRuntime { message_port_registry: self.message_port_registry(), broadcast_channel_registry: self.broadcast_channel_registry(), storage_partition_identity: self.storage_partition_identity(), } } pub(crate) fn service_worker_runtime( &self, ) -> crate::service_worker_runtime::ServiceWorkerRuntimeService { self.inner.service_worker_runtime.get_or_init() } pub(crate) fn service_worker_runtime_if_initialized( &self, ) -> Option { self.inner.service_worker_runtime.get() } pub(crate) fn ensure_service_worker_runtime_for_navigation(&self) -> bool { self.inner .service_worker_runtime .get_or_init_for_navigation() .is_some() } pub(crate) fn next_web_storage_opaque_context_nonce( &self, ) -> moli_storage_key::OpaqueOriginNonce { moli_storage_key::OpaqueOriginNonce::new( self.inner .next_web_storage_opaque_context_nonce .fetch_add(1, Ordering::Relaxed) .saturating_add(1), ) } pub(crate) fn allocate_child_document_loader_id(&self) -> String { let next_id = self .inner .next_child_document_loader_id .fetch_add(1, Ordering::Relaxed) .checked_add(1) .expect("browser-context child Document loader id space exhausted"); format!("LID-CHILD-{next_id:010}") } pub(crate) fn prepare_detached_parser_script_fetch( &self, mut info: crate::protocol_types::PendingSubresourceFetchInfo, script: PreparedScript, request_client: crate::network::ResourceRequestClient, task_runner: crate::network::RendererResourceTaskRunner, document_character_set: Option, completer: SharedScriptSourceLoadCompleter, ) -> ( crate::protocol_types::PendingSubresourceFetchInfo, DetachedParserScriptFetchContinuation, ) { info.internal_id = self .inner .next_detached_parser_script_fetch_id .fetch_add(1, Ordering::Relaxed) .saturating_add(1); ( info, DetachedParserScriptFetchContinuation::new( script, request_client, task_runner, document_character_set, completer, ), ) } pub fn set_javascript_dialog_handler_enabled(&self, enabled: bool) { self.inner .javascript_dialog_handler_enabled .store(enabled, Ordering::Relaxed); } pub fn javascript_dialog_handler_enabled(&self) -> bool { self.inner .javascript_dialog_handler_enabled .load(Ordering::Relaxed) } } impl RendererBrowserContextRuntimeOwner { pub fn handle(&self) -> RendererBrowserContextRuntime { self.runtime .as_ref() .expect("renderer browser context owner was already split") .clone() } pub fn owner_access(&self) -> RendererBrowserContextRuntimeOwnerAccess { RendererBrowserContextRuntimeOwnerAccess { runtime: self.handle(), producer_registrar: self.producer_registry.registrar(), resource_runtime_registrar: self .resource_runtime_owner_root .as_ref() .expect("renderer browser context owner was already shut down") .registrar(), } } pub fn replace_browser_resource_runtime( &self, registration: crate::network::BrowserResourceRuntimeOwnerRegistration, ) -> Result { self.owner_access() .replace_owned(registration) .map_err(str::to_owned) } /// Close renderer-producer admission and stop context-owned workers while /// leaving network owner roots available for an outer lifetime boundary to /// join after all of its `JsRuntime` handles have been dropped. pub fn terminate_renderer_producers_for_owner_shutdown(&mut self) { self.producer_registry.cancel_all(); if let Some(runtime) = self.runtime.take() { runtime.terminate_resource_producers_for_owner_shutdown(); drop(runtime); } } /// Broadcast shutdown and join every active or retired network owner. /// Callers with separate renderer handles first use /// [`Self::terminate_renderer_producers_for_owner_shutdown`] and drop those /// handles before entering this terminal network boundary. pub fn shutdown_network_and_join(&mut self) { self.terminate_renderer_producers_for_owner_shutdown(); if let Some(owner_root) = self.resource_runtime_owner_root.take() { owner_root.shutdown_and_join(); drop(owner_root); } } pub fn shutdown_and_join(&mut self) { self.terminate_renderer_producers_for_owner_shutdown(); self.shutdown_network_and_join(); } #[cfg(test)] fn registered_producer_count_for_testing(&self) -> usize { self.producer_registry.inner.borrow().producers.len() } } impl RendererBrowserContextRuntimeOwnerAccess { pub fn runtime(&self) -> RendererBrowserContextRuntime { self.runtime.clone() } pub fn register_renderer_producer( &self, runtime: &super::JsRuntime, ) -> Result<(), &'static str> { self.producer_registrar.register(runtime) } pub fn current_browser_resource_runtime( &self, ) -> Result { self.resource_runtime_registrar.current_registered() } pub fn replace_owned( &self, registration: crate::network::BrowserResourceRuntimeOwnerRegistration, ) -> Result { self.resource_runtime_registrar.replace_owned(registration) } pub fn adopt_registered( &self, runtime: crate::network::BrowserResourceRuntime, ) -> Result<(), &'static str> { self.resource_runtime_registrar.adopt_registered(runtime) } pub fn validate_registered( &self, runtime: &crate::network::BrowserResourceRuntime, ) -> Result<(), &'static str> { self.resource_runtime_registrar.validate_registered(runtime) } pub fn reap_retired_resource_runtimes(&self) { self.resource_runtime_registrar.reap_retired(); } } impl Drop for RendererBrowserContextRuntimeOwner { fn drop(&mut self) { self.shutdown_and_join(); } } impl std::ops::Deref for RendererBrowserContextRuntimeOwner { type Target = RendererBrowserContextRuntime; fn deref(&self) -> &Self::Target { self.runtime .as_ref() .expect("renderer browser context owner was already split") } } impl RendererStoragePartitionIdentity { fn new_process_local() -> Self { let id = NEXT_RENDERER_STORAGE_PARTITION_ID .fetch_add(1, Ordering::Relaxed) .saturating_add(1); Self { browser_context_id: format!("renderer-browser-context:{id}"), profile_partition_id: format!("renderer-profile-partition:{id}"), } } pub(crate) fn browser_context_id(&self) -> &str { &self.browser_context_id } pub(crate) fn profile_partition_id(&self) -> &str { &self.profile_partition_id } } impl RendererWorkerContextRuntime { pub(crate) fn new( message_port_registry: crate::message_port_runtime::SharedMessagePortRegistry, broadcast_channel_registry: crate::broadcast_channel_runtime::SharedBroadcastChannelRegistry, ) -> Self { Self::with_identity( message_port_registry, broadcast_channel_registry, RendererStoragePartitionIdentity::new_process_local(), ) } fn with_identity( message_port_registry: crate::message_port_runtime::SharedMessagePortRegistry, broadcast_channel_registry: crate::broadcast_channel_runtime::SharedBroadcastChannelRegistry, storage_partition_identity: RendererStoragePartitionIdentity, ) -> Self { Self { message_port_registry, broadcast_channel_registry, storage_partition_identity, } } pub(crate) fn message_port_registry( &self, ) -> crate::message_port_runtime::SharedMessagePortRegistry { self.message_port_registry.clone() } pub(crate) fn broadcast_channel_registry( &self, ) -> crate::broadcast_channel_runtime::SharedBroadcastChannelRegistry { self.broadcast_channel_registry.clone() } pub(crate) fn storage_partition_identity(&self) -> RendererStoragePartitionIdentity { self.storage_partition_identity.clone() } } #[cfg(test)] mod tests { use std::time::Duration; use moli_fetch::{FetchCancelHandle, Request}; use super::RendererBrowserContextRuntime; use crate::{ network::ResourceRequestClient, runtime::{ JsRuntime, RendererOutputTransportMessage, RendererPageContextCancelReason, RendererPageReservationToken, renderer_output_transport_channel, }, }; async fn assert_single_page_reservation_release( output_rx: &mut crate::runtime::RendererOutputTransportReceiver, token: RendererPageReservationToken, ) { let message = tokio::time::timeout(Duration::from_secs(3), output_rx.recv()) .await .expect("page reservation release should arrive") .expect("renderer output transport should remain live"); assert!(matches!( message, RendererOutputTransportMessage::PageReservationReleased { owner_local_host_id, page_id, } if owner_local_host_id == token.local_host_id() && page_id == token.page_id() )); assert!(matches!( output_rx.try_recv(), Err(tokio::sync::mpsc::error::TryRecvError::Empty) )); } #[test] fn cloned_runtime_shares_partition_state() { let runtime = RendererBrowserContextRuntime::new(); let clone = runtime.clone(); assert!(runtime.shares_state_with(&clone)); } #[test] fn worker_services_stay_deferred_until_first_use() { let runtime = RendererBrowserContextRuntime::new(); assert_eq!( runtime.moli_memory_diagnostics()["sharedWorker"]["runtimeInitialized"], false ); assert_eq!( runtime.moli_memory_diagnostics()["serviceWorker"]["runtimeInitialized"], false ); let _ = runtime.next_shared_worker_client_owner_id(); let document_url = url::Url::parse("https://deferred-client.test/").unwrap(); let client_queue = crate::page_task_queue::RendererPageServiceWorkerTestHarness::new(); runtime.register_service_worker_client( document_url.clone(), moli_storage_key::MoliStorageKey::first_party_from_url(&document_url, None) .serialized_storage_key(), crate::service_worker_runtime::ServiceWorkerClientFrameType::TopLevel, Some(crate::native_bridge::WindowDocumentOwner::for_test(1)), client_queue.sender(), ); let (shared_wake_tx, _shared_wake_rx) = crate::shared_worker_runtime::shared_worker_owner_wake_channel(); runtime.add_shared_worker_owner_wake_sender(shared_wake_tx); let (service_wake_tx, _service_wake_rx) = crate::service_worker_runtime::service_worker_owner_wake_channel(); runtime.add_service_worker_owner_wake_sender(service_wake_tx); runtime.set_service_worker_pause_on_start_for_devtools(true); assert!(!runtime.ensure_service_worker_runtime_for_navigation()); assert_eq!( runtime.moli_memory_diagnostics()["sharedWorker"]["runtimeInitialized"], false ); assert_eq!( runtime.moli_memory_diagnostics()["serviceWorker"]["runtimeInitialized"], false ); assert_eq!( runtime.moli_memory_diagnostics()["serviceWorker"]["deferredWindowClients"], 1 ); runtime.inner.shared_worker_runtime.get_or_init(); runtime.service_worker_runtime(); assert_eq!( runtime.moli_memory_diagnostics()["sharedWorker"]["runtimeInitialized"], true ); assert_eq!( runtime.moli_memory_diagnostics()["serviceWorker"]["runtimeInitialized"], true ); assert_eq!( runtime.moli_memory_diagnostics()["serviceWorker"]["deferredWindowClients"], 0 ); assert_eq!( runtime .service_worker_runtime() .diagnostics_snapshot() .live_client_count, 1 ); assert!(runtime.service_worker_pause_on_start_for_devtools()); } #[test] fn cloned_runtime_shares_child_document_loader_id_sequence() { let runtime = RendererBrowserContextRuntime::new(); let clone = runtime.clone(); assert_eq!( runtime.allocate_child_document_loader_id(), "LID-CHILD-0000000001" ); assert_eq!( clone.allocate_child_document_loader_id(), "LID-CHILD-0000000002" ); } #[test] fn fresh_runtime_gets_isolated_partition_state() { let left = RendererBrowserContextRuntime::new(); let right = RendererBrowserContextRuntime::new(); assert!(!left.shares_state_with(&right)); } #[test] fn worker_context_runtime_can_outlive_browser_context_without_runtime_service_edge() { let worker_runtime = { let runtime = RendererBrowserContextRuntime::new(); runtime.worker_context_runtime() }; let _ = worker_runtime.message_port_registry(); let _ = worker_runtime.broadcast_channel_registry(); } #[test] fn worker_context_runtime_inherits_storage_partition_identity() { let runtime = RendererBrowserContextRuntime::new(); let worker_runtime = runtime.worker_context_runtime(); assert_eq!( worker_runtime.storage_partition_identity(), runtime.storage_partition_identity() ); } #[test] fn fresh_runtime_gets_isolated_storage_partition_identity() { let left = RendererBrowserContextRuntime::new(); let right = RendererBrowserContextRuntime::new(); assert_ne!( left.storage_partition_identity(), right.storage_partition_identity() ); } #[test] fn producer_registry_reaps_dead_weak_entries_on_registration() { let owner = RendererBrowserContextRuntime::new(); let access = owner.owner_access(); let first = JsRuntime::initialize_with_browser_context_owner_access(&access) .expect("first producer should register"); assert_eq!(owner.registered_producer_count_for_testing(), 1); drop(first); let second = JsRuntime::initialize_with_browser_context_owner_access(&access) .expect("second producer should register"); assert_eq!( owner.registered_producer_count_for_testing(), 1, "dead producer history must not accumulate for a long-lived context" ); drop(second); } #[tokio::test(flavor = "multi_thread")] async fn terminal_context_rejects_queued_command_new_page_slot_and_network_submit() { let mut owner = RendererBrowserContextRuntime::new(); let access = owner.owner_access(); let runtime = JsRuntime::initialize_with_browser_context_owner_access(&access) .expect("producer should register"); let stale_client = ResourceRequestClient::from_browser_resource_runtime( access .current_browser_resource_runtime() .expect("resource runtime should be live before shutdown"), ); let (entered_rx, release_tx) = runtime.install_owner_command_dispatch_gate_for_testing(); let reply_rx = runtime .enqueue_owner_command_probe_for_testing() .expect("probe should enqueue before shutdown"); entered_rx .recv_timeout(Duration::from_secs(3)) .expect("renderer command should reach deterministic dispatch barrier"); owner.shutdown_and_join(); assert!( access.current_browser_resource_runtime().is_err(), "terminal weak owner access must reject stale runtime lookup" ); let submit_error = stale_client .fetch_raw_stream_with_cancel( Request::get("http://127.0.0.1:9/post-shutdown") .expect("test request should build"), FetchCancelHandle::new(), ) .await .expect_err("terminal fetch owner must reject new submission"); assert_eq!( submit_error.to_string(), "fetch runtime is shutting down", "terminal BrowserContext handles must reject at fetch admission" ); release_tx .send(()) .expect("release renderer command dispatch barrier"); let command_result = tokio::time::timeout(Duration::from_secs(3), reply_rx) .await .expect("terminal command reply should not hang") .expect("renderer reply channel should remain explicit"); let command_error = match command_result { Err(error) => error, Ok(_) => panic!("queued pre-terminal command must fail after terminal boundary"), }; assert!(command_error.to_string().contains("dropped")); let (attach_result, cancel_rx) = runtime.try_attach_page_slot_for_testing(); assert!( attach_result.is_err(), "terminal PageTable must reject attach" ); assert_eq!( cancel_rx.reason(), Some(RendererPageContextCancelReason::ContextDropped) ); } #[tokio::test(flavor = "multi_thread")] async fn create_html_page_reservation_releases_once_when_context_is_already_terminal() { let mut owner = RendererBrowserContextRuntime::new(); let runtime = JsRuntime::initialize_with_browser_context_owner_access(&owner.owner_access()) .expect("producer should register"); let loader_owner = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()) .expect("test loader should initialize"); let (output_tx, mut output_rx) = renderer_output_transport_channel(); runtime.set_renderer_output_transport_sender(output_tx); let token = runtime.reserve_page_for_creation(); owner.shutdown_and_join(); assert!( runtime .start_minimal_html_page_for_reservation_testing(token, &loader_owner) .is_err(), "terminal context must reject CreateHtmlPage" ); assert_single_page_reservation_release(&mut output_rx, token).await; } #[tokio::test(flavor = "multi_thread")] async fn create_html_page_reservation_releases_once_when_render_admission_send_fails() { let mut owner = RendererBrowserContextRuntime::new(); let runtime = JsRuntime::initialize_with_browser_context_owner_access(&owner.owner_access()) .expect("producer should register"); let loader_owner = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()) .expect("test loader should initialize"); let (output_tx, mut output_rx) = renderer_output_transport_channel(); runtime.set_renderer_output_transport_sender(output_tx); let token = runtime.reserve_page_for_creation(); runtime.close_owner_command_admission_for_testing(); assert!( runtime .start_minimal_html_page_for_reservation_testing(token, &loader_owner) .is_err(), "closed render admission must return the owned CreateHtmlPage envelope" ); assert_single_page_reservation_release(&mut output_rx, token).await; owner.shutdown_and_join(); } #[tokio::test(flavor = "multi_thread")] async fn queued_create_html_page_reservation_releases_once_during_terminal_drain() { let mut owner = RendererBrowserContextRuntime::new(); let runtime = JsRuntime::initialize_with_browser_context_owner_access(&owner.owner_access()) .expect("producer should register"); let loader_owner = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()) .expect("test loader should initialize"); let (output_tx, mut output_rx) = renderer_output_transport_channel(); runtime.set_renderer_output_transport_sender(output_tx); let token = runtime.reserve_page_for_creation(); let (entered_rx, release_tx) = runtime.install_owner_command_dispatch_gate_for_testing(); let pending = runtime .start_minimal_html_page_for_reservation_testing(token, &loader_owner) .expect("CreateHtmlPage should enqueue before the terminal boundary"); entered_rx .recv_timeout(Duration::from_secs(3)) .expect("CreateHtmlPage should reach the deterministic dispatch gate"); // PendingHtmlPage intentionally remains live across root shutdown. It // may retain a terminal renderer handle, but cannot submit or revive // work after producer admission closes; network children are joined by // the root independently. owner.shutdown_and_join(); release_tx .send(()) .expect("release CreateHtmlPage terminal drain"); assert!( pending.await_ready().await.is_err(), "queued CreateHtmlPage must complete with an explicit terminal error" ); assert_single_page_reservation_release(&mut output_rx, token).await; let stale_token = runtime.reserve_page_for_creation(); assert!( runtime .start_minimal_html_page_for_reservation_testing(stale_token, &loader_owner) .is_err(), "escaped terminal renderer handle must not admit new work" ); assert_single_page_reservation_release(&mut output_rx, stale_token).await; } }