mirror of
https://github.com/lexmount/moli.git
synced 2026-10-04 16:00:47 +00:00
1276 lines
49 KiB
Rust
1276 lines
49 KiB
Rust
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<Mutex<Option<super::RendererOutputTransportSender>>>,
|
|
}
|
|
|
|
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<super::RendererOutputTransportSender> {
|
|
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<RendererBrowserContextRuntimeInner>,
|
|
}
|
|
|
|
/// 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<RendererBrowserContextRuntime>,
|
|
producer_registry: RendererProducerRegistry,
|
|
resource_runtime_owner_root: Option<crate::network::BrowserResourceRuntimeOwnerRoot>,
|
|
}
|
|
|
|
/// 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<RefCell<RendererProducerRegistryState>>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct RendererProducerRegistrar {
|
|
inner: Weak<RefCell<RendererProducerRegistryState>>,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct RendererProducerRegistryState {
|
|
terminal: bool,
|
|
producers: HashMap<u64, super::page::RendererProducerShutdownHandle>,
|
|
}
|
|
|
|
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<HashMap<u64, crate::worker::WorkerDevToolsHandle>>,
|
|
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<String>,
|
|
completer: SharedScriptSourceLoadCompleter,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct DetachedParserScriptFetchContinuation {
|
|
inner: Arc<Mutex<Option<DetachedParserScriptFetchContinuationInner>>>,
|
|
}
|
|
|
|
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<String>,
|
|
completer: SharedScriptSourceLoadCompleter,
|
|
) -> Self {
|
|
Self {
|
|
inner: Arc::new(Mutex::new(Some(
|
|
DetachedParserScriptFetchContinuationInner {
|
|
script,
|
|
request_client,
|
|
task_runner,
|
|
document_character_set,
|
|
completer,
|
|
},
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn take(&self) -> Option<DetachedParserScriptFetchContinuationInner> {
|
|
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<u8>,
|
|
) -> 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<Url>) -> 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<crate::shared_worker_runtime::SharedWorkerRuntimeService>,
|
|
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<crate::shared_worker_runtime::SharedWorkerRuntimeService>,
|
|
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<super::RendererOutputTransportSender> {
|
|
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<crate::service_worker_runtime::ServiceWorkerRuntimeService> {
|
|
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<String>,
|
|
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<crate::network::BrowserResourceRuntime, String> {
|
|
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<crate::network::BrowserResourceRuntime, &'static str> {
|
|
self.resource_runtime_registrar.current_registered()
|
|
}
|
|
|
|
pub fn replace_owned(
|
|
&self,
|
|
registration: crate::network::BrowserResourceRuntimeOwnerRegistration,
|
|
) -> Result<crate::network::BrowserResourceRuntime, &'static str> {
|
|
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;
|
|
}
|
|
}
|