diff --git a/Cargo.lock b/Cargo.lock index bd5230347..ccace617a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2973,6 +2973,7 @@ dependencies = [ "moli-site", "moli-storage-key", "moli-storage-service", + "moli-test-support", "moli-trace", "moli-url", "moli-url-policy", diff --git a/moli-core/src/browser/browser_context.rs b/moli-core/src/browser/browser_context.rs index f6f09d777..13cc09133 100644 --- a/moli-core/src/browser/browser_context.rs +++ b/moli-core/src/browser/browser_context.rs @@ -218,6 +218,12 @@ pub struct BrowserContext { permission_overrides: PermissionOverrides, download_policy: Option, pub(in crate::browser) downloads: super::downloads::DownloadManager, + pub(in crate::browser) dedicated_workers: IndexMap, + pub(in crate::browser) service_workers: IndexMap, + pub(in crate::browser) shared_workers: IndexMap< + moli_shared_worker::SharedWorkerInstanceId, + crate::page::RendererSharedWorkerTargetInfo, + >, // The Browser collection and its only selector have the same lifetime. // Keep insertion order when choosing a replacement foreground page. web_contents: IndexMap, @@ -355,6 +361,9 @@ impl BrowserContext { permission_overrides: PermissionOverrides::default(), download_policy: None, downloads: super::downloads::DownloadManager::default(), + shared_workers: IndexMap::new(), + service_workers: IndexMap::new(), + dedicated_workers: IndexMap::new(), web_contents: IndexMap::new(), selected_web_contents: None, renderer_output_transport_sender: None, diff --git a/moli-core/src/browser/browser_context/workers.rs b/moli-core/src/browser/browser_context/workers.rs index 7209dcd25..9806656e2 100644 --- a/moli-core/src/browser/browser_context/workers.rs +++ b/moli-core/src/browser/browser_context/workers.rs @@ -1,9 +1,56 @@ -use crate::{browser::ServiceWorkerCommand, runtime::RendererBrowserContextRuntime}; +use crate::{ + browser::ServiceWorkerCommand, + runtime::{RendererWorkerInspectionEndpoint, RendererWorkerInspectionTarget}, +}; use moli_shared_worker::SharedWorkerInstanceId; use super::BrowserContext; impl BrowserContext { + pub(in crate::browser) fn install_worker_lifecycle_handler( + &self, + handler: impl Fn(crate::page::RendererWorkerLifecycleInput) + Send + Sync + 'static, + ) { + self.renderer_runtime() + .install_worker_lifecycle_handler(handler); + } + + pub(in crate::browser) fn worker_snapshots( + &self, + ) -> impl Iterator + '_ { + self.shared_workers + .values() + .cloned() + .map(|info| super::super::WorkerSnapshot::Shared { + context: self.id(), + info, + }) + .chain(self.service_workers.values().cloned().map(|worker| { + super::super::WorkerSnapshot::Service { + context: self.id(), + worker, + } + })) + .chain(self.dedicated_workers.values().cloned().map(|worker| { + super::super::WorkerSnapshot::Dedicated { + context: self.id(), + worker, + } + })) + } + + pub fn worker_inspection_endpoint( + &self, + target: RendererWorkerInspectionTarget, + ) -> Option { + self.renderer_runtime().worker_inspection_endpoint(target) + } + + #[cfg(test)] + pub(crate) fn worker_runtime_for_test(&self) -> crate::runtime::RendererBrowserContextRuntime { + self.renderer_runtime() + } + pub fn execute_service_worker_command( &self, command: ServiceWorkerCommand, @@ -113,8 +160,7 @@ impl BrowserContext { } pub fn close_shared_worker(&self, instance_id: SharedWorkerInstanceId) -> bool { - self.renderer_runtime() - .close_shared_worker_for_target_close(instance_id) + self.renderer_runtime().close_shared_worker(instance_id) } pub fn close_dedicated_worker(&self, instance_id: u64) -> bool { @@ -122,42 +168,6 @@ impl BrowserContext { .close_dedicated_worker_for_devtools(instance_id) } - pub fn attach_dedicated_worker_inspector_session( - &self, - instance_id: u64, - session_id: Option, - ) -> bool { - self.renderer_runtime() - .attach_dedicated_worker_runtime_inspector_session(instance_id, session_id) - } - - pub fn detach_shared_worker_inspector_session( - &self, - instance_id: SharedWorkerInstanceId, - session_id: Option, - ) -> bool { - self.renderer_runtime() - .detach_shared_worker_runtime_inspector_session(instance_id, session_id) - } - - pub fn detach_dedicated_worker_inspector_session( - &self, - instance_id: u64, - session_id: Option, - ) -> bool { - self.renderer_runtime() - .detach_dedicated_worker_runtime_inspector_session(instance_id, session_id) - } - - pub fn detach_service_worker_inspector_session( - &self, - version_id: u64, - session_id: Option, - ) -> bool { - self.renderer_runtime() - .detach_service_worker_runtime_inspector_session(version_id, session_id) - } - pub fn run_dedicated_worker_if_waiting_for_debugger(&self, instance_id: u64) -> bool { self.renderer_runtime() .run_dedicated_worker_if_waiting_for_debugger_for_devtools(instance_id) @@ -167,8 +177,4 @@ impl BrowserContext { self.renderer_runtime() .run_service_worker_if_waiting_for_debugger_for_devtools(version_id) } - - pub fn worker_runtime_inspection_endpoint(&self) -> RendererBrowserContextRuntime { - self.renderer_runtime() - } } diff --git a/moli-core/src/browser/events.rs b/moli-core/src/browser/events.rs index f1f10db61..31662239c 100644 --- a/moli-core/src/browser/events.rs +++ b/moli-core/src/browser/events.rs @@ -9,6 +9,9 @@ use super::{ pub enum BrowserEvent { ContextCreated(BrowserContextId), ContextDisposed(BrowserContextId), + WorkerCreated(super::WorkerSnapshot), + WorkerUpdated(super::WorkerSnapshot), + WorkerDestroyed(super::WorkerHandle), WebContentsCreated(WebContentsHandle), WebContentsActivated { web_contents: WebContentsHandle, @@ -71,6 +74,7 @@ pub struct BrowserSnapshot { pub javascript_dialogs: Vec, pub navigations: Vec, pub downloads: Vec, + pub workers: Vec, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -209,6 +213,7 @@ impl BrowserEventStream { downloads: impl Iterator, javascript_dialogs: impl Iterator, navigations: impl Iterator, + workers: impl Iterator, ) -> (BrowserSnapshot, BrowserEventReceiver) { ( BrowserSnapshot { @@ -221,6 +226,7 @@ impl BrowserEventStream { downloads: downloads.collect(), javascript_dialogs: javascript_dialogs.collect(), navigations: navigations.collect(), + workers: workers.collect(), }, self.sender.subscribe(), ) @@ -422,6 +428,7 @@ mod tests { std::iter::empty(), std::iter::empty(), std::iter::empty(), + std::iter::empty(), ); for _ in 0..257 { stream.publish(BrowserEvent::ContextCreated(context)); @@ -436,6 +443,7 @@ mod tests { std::iter::empty(), std::iter::empty(), std::iter::empty(), + std::iter::empty(), ); assert_eq!(snapshot.contexts, vec![context]); assert_eq!(recovered.try_recv(), Err(TryRecvError::Empty)); diff --git a/moli-core/src/browser/mod.rs b/moli-core/src/browser/mod.rs index ec698c8be..fb18591df 100644 --- a/moli-core/src/browser/mod.rs +++ b/moli-core/src/browser/mod.rs @@ -22,6 +22,7 @@ mod popup; mod renderer_residence; mod service_workers; pub mod web_contents; +mod workers; pub use browser_context::*; pub use captured_body::{ @@ -58,6 +59,10 @@ pub use permissions::{PermissionDefaults, PermissionOverrides}; pub use popup::{BrowserPopupAdmission, BrowserPopupCreation}; pub use renderer_residence::RendererPageResidenceIdentity; pub use service_workers::ServiceWorkerCommand; +pub use workers::{ + DedicatedWorkerSnapshot, ServiceWorkerExecution, ServiceWorkerSnapshot, WorkerHandle, + WorkerSnapshot, +}; /// Navigation semantics, independent of the protocol that requested the load. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] diff --git a/moli-core/src/browser/owner.rs b/moli-core/src/browser/owner.rs index 1e87e8f39..cc406ae5f 100644 --- a/moli-core/src/browser/owner.rs +++ b/moli-core/src/browser/owner.rs @@ -24,6 +24,7 @@ mod navigation_driver; pub use navigation_driver::{BrowserNavigationOutcome, BrowserNavigationWaiter}; mod navigation_events; mod popup; +mod workers; use super::{ BrowserContext, BrowserContextId, BrowserContextStoragePartitionHandles, MainFrameSlotId, @@ -93,11 +94,15 @@ struct Browser { popup_admissions: popup::PopupAdmissions, document_decision_provider: Option>, local_sender: BrowserLocalSender, + native_sender: mpsc::WeakUnboundedSender, events: super::events::BrowserEventStream, } impl Browser { - fn new(local_sender: BrowserLocalSender) -> Self { + fn new( + local_sender: BrowserLocalSender, + native_sender: mpsc::WeakUnboundedSender, + ) -> Self { Self { contexts: IndexMap::new(), permission_defaults: super::PermissionDefaults::default(), @@ -105,6 +110,7 @@ impl Browser { popup_admissions: popup::PopupAdmissions::default(), document_decision_provider: None, local_sender, + native_sender, events: super::events::BrowserEventStream::default(), } } @@ -123,6 +129,14 @@ impl Browser { fn insert_context(&mut self, context: BrowserContext) -> BrowserContextId { let id = context.id(); + let sender = self.native_sender.clone(); + context.install_worker_lifecycle_handler(move |input| { + if let Some(sender) = sender.upgrade() { + let _ = sender.send(BrowserOwnerMessage::Execute(Box::new(move |browser| { + browser.commit_worker_lifecycle(id, input); + }))); + } + }); let previous = self.contexts.insert(id, context); debug_assert!(previous.is_none(), "BrowserContext identity must be unique"); self.events.publish(super::BrowserEvent::ContextCreated(id)); @@ -135,6 +149,7 @@ impl Browser { }; let navigations = context.navigation_snapshots().collect::>(); let dialogs = context.javascript_dialog_snapshots(); + self.publish_retired_workers(&context); self.events .publish(super::BrowserEvent::ContextDisposed(id)); context.shutdown(); @@ -153,6 +168,7 @@ impl Browser { .publish(super::BrowserEvent::ContextDisposed(id)); } for (_, context) in contexts { + self.publish_retired_workers(&context); let navigations = context.navigation_snapshots().collect::>(); let dialogs = context.javascript_dialog_snapshots(); context.shutdown(); @@ -263,6 +279,7 @@ impl fmt::Debug for BrowserHandle { impl BrowserHandle { fn start() -> Result { let (tx, mut rx) = mpsc::unbounded_channel(); + let native_sender = tx.downgrade(); let (ready_tx, ready_rx) = std_mpsc::sync_channel(1); let join = thread::Builder::new() .name("moli-browser-owner".to_owned()) @@ -284,7 +301,7 @@ impl BrowserHandle { let _ = ready_tx.send(Ok(())); local.block_on(&runtime, async move { let (local_tx, mut local_rx) = mpsc::unbounded_channel(); - let mut browser = Browser::new(local_tx); + let mut browser = Browser::new(local_tx, native_sender); loop { tokio::select! { message = rx.recv() => match message { @@ -494,6 +511,10 @@ impl BrowserHandle { .contexts .values() .flat_map(BrowserContext::navigation_snapshots), + browser + .contexts + .values() + .flat_map(BrowserContext::worker_snapshots), ) }) } @@ -1278,15 +1299,11 @@ impl BrowserContextHandle { forward_context_read! { fn controlled_service_worker_window_client_ids(registration_id: u64, version_id: u64) -> Vec; fn set_service_worker_pause_on_start_for_version(version_id: u64, pause: bool) -> bool; + fn worker_inspection_endpoint(target: crate::runtime::RendererWorkerInspectionTarget) -> Option; fn close_shared_worker(instance_id: moli_shared_worker::SharedWorkerInstanceId) -> bool; fn close_dedicated_worker(instance_id: u64) -> bool; - fn attach_dedicated_worker_inspector_session(instance_id: u64, session_id: Option) -> bool; - fn detach_shared_worker_inspector_session(instance_id: moli_shared_worker::SharedWorkerInstanceId, session_id: Option) -> bool; - fn detach_dedicated_worker_inspector_session(instance_id: u64, session_id: Option) -> bool; - fn detach_service_worker_inspector_session(version_id: u64, session_id: Option) -> bool; fn run_dedicated_worker_if_waiting_for_debugger(instance_id: u64) -> bool; fn run_service_worker_if_waiting_for_debugger(version_id: u64) -> bool; - fn worker_runtime_inspection_endpoint() -> crate::runtime::RendererBrowserContextRuntime; fn permission_override_count() -> usize; fn storage_partition_kind() -> super::StoragePartitionKind; fn storage_partition_kind_label() -> &'static str; diff --git a/moli-core/src/browser/owner/navigation/tests.rs b/moli-core/src/browser/owner/navigation/tests.rs index 8512e4474..facaf22e8 100644 --- a/moli-core/src/browser/owner/navigation/tests.rs +++ b/moli-core/src/browser/owner/navigation/tests.rs @@ -449,6 +449,428 @@ async fn native_document_lifecycle_advances_without_a_devtools_output_consumer() service.shutdown(); } +#[tokio::test] +async fn native_service_worker_version_survives_stop_and_restart_without_devtools() { + use crate::browser::{ServiceWorkerCommand, ServiceWorkerExecution, WorkerSnapshot}; + use crate::page::RendererServiceWorkerVersionStatus; + + let server = FixtureServer::spawn().await.unwrap(); + let service = BrowserService::start().unwrap(); + let browser = service.handle(); + let (context, contents) = context_with_contents(&service); + let (_, mut events) = browser.subscribe().unwrap(); + navigate(&context, contents, &server.url("/native-service-worker/")).await; + tokio::time::timeout(std::time::Duration::from_secs(5), async { + let mut created = None; + let mut first_run = None; + let mut restarted = None; + let mut phase = 0; + loop { + let record = events.recv().await.unwrap(); + match record.event { + BrowserEvent::WorkerCreated(worker @ WorkerSnapshot::Service { .. }) => { + assert!( + created + .replace((worker.handle(), record.sequence)) + .is_none() + ); + let WorkerSnapshot::Service { worker, .. } = worker else { + unreachable!() + }; + let ServiceWorkerExecution::Starting(run) = worker.execution else { + panic!("creation must expose the already installed physical host"); + }; + first_run = Some(run); + } + BrowserEvent::WorkerUpdated(snapshot @ WorkerSnapshot::Service { .. }) => { + let (handle, created_sequence) = created.as_ref().unwrap(); + assert_eq!(snapshot.handle(), *handle); + assert!(record.sequence > *created_sequence); + let WorkerSnapshot::Service { worker, .. } = &snapshot else { + unreachable!() + }; + match (phase, &worker.execution) { + (0, ServiceWorkerExecution::Running(run)) + if worker.info.status + == RendererServiceWorkerVersionStatus::Activated => + { + assert_eq!(Some(run), first_run.as_ref()); + context + .execute_service_worker_command(ServiceWorkerCommand::StopVersion { + version_id: worker.info.version_id, + }) + .unwrap(); + phase = 1; + } + (1, ServiceWorkerExecution::Stopped) => { + assert_eq!( + browser.subscribe().unwrap().0.workers, + vec![snapshot.clone()] + ); + context + .execute_service_worker_command(ServiceWorkerCommand::Start { + scope: worker.info.scope_url.parse().unwrap(), + }) + .unwrap(); + phase = 2; + } + (2, ServiceWorkerExecution::Starting(run)) => { + assert_ne!(Some(run), first_run.as_ref()); + assert!(restarted.replace(run.clone()).is_none()); + } + (2, ServiceWorkerExecution::Running(run)) => { + assert_eq!(Some(run), restarted.as_ref()); + assert_eq!( + browser.subscribe().unwrap().0.workers, + vec![snapshot.clone()] + ); + assert!(context.remove().unwrap()); + phase = 3; + } + _ => {} + } + } + BrowserEvent::WorkerDestroyed(handle) => { + assert_eq!(phase, 3); + assert_eq!(handle, created.unwrap().0); + break; + } + _ => {} + } + } + }) + .await + .expect("native ServiceWorker creation, restart and retirement need no Protocol consumer"); + assert!(browser.subscribe().unwrap().0.workers.is_empty()); + service.shutdown(); + while let Ok(record) = events.try_recv() { + assert!(!matches!(record.event, BrowserEvent::WorkerDestroyed(_))); + } + server.shutdown().await; +} + +#[tokio::test] +async fn native_service_worker_failed_install_preserves_creation_and_exact_retirement() { + use crate::browser::WorkerSnapshot; + let server = FixtureServer::spawn().await.unwrap(); + let service = BrowserService::start().unwrap(); + let browser = service.handle(); + let (context, contents) = context_with_contents(&service); + let (_, mut events) = browser.subscribe().unwrap(); + // Inline script runs in the resident Browser-owned Document. The generic + // test-only Context-borrowing evaluator temporarily removes its registry + // entry and cannot be used to test native lifecycle admission. + navigate( + &context, + contents, + &server.url("/native-service-worker/failed-install"), + ) + .await; + let mut observed = Vec::new(); + let result = tokio::time::timeout(std::time::Duration::from_secs(5), async { + let mut created = None; + loop { + let record = events.recv().await.unwrap(); + observed.push(record.event.clone()); + match record.event { + BrowserEvent::WorkerCreated(worker @ WorkerSnapshot::Service { .. }) => { + assert!( + created + .replace((worker.handle(), record.sequence)) + .is_none() + ); + } + BrowserEvent::WorkerDestroyed(handle) => { + let (expected, sequence) = + created.expect("even a failed initial run publishes its version creation"); + assert_eq!(handle, expected); + assert!(record.sequence > sequence); + break; + } + _ => {} + } + } + }) + .await; + assert!( + result.is_ok(), + "failed install facts: {observed:?}; snapshot: {:?}", + browser.subscribe().unwrap().0.workers + ); + assert!(browser.subscribe().unwrap().0.workers.is_empty()); + service.shutdown(); + while let Ok(record) = events.try_recv() { + assert!(!matches!(record.event, BrowserEvent::WorkerDestroyed(_))); + } + server.shutdown().await; +} + +#[tokio::test] +async fn native_shared_worker_membership_and_retirement_do_not_require_devtools() { + use crate::browser::{WorkerHandle, WorkerSnapshot}; + for retirement in ["worker", "context", "browser"] { + let service = BrowserService::start().unwrap(); + let browser = service.handle(); + let (context, contents) = context_with_contents(&service); + let (_, mut events) = browser.subscribe().unwrap(); + navigate(&context, contents, "data:text/html,").await; + let created = tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + let record = events.recv().await.unwrap(); + if let BrowserEvent::WorkerCreated(WorkerSnapshot::Shared { context: id, info }) = + &record.event + && *id == context.id() + && info.name == "native-membership" + { + break record; + } + } + }) + .await + .expect("Browser must observe the real worker without a DevTools transport"); + let BrowserEvent::WorkerCreated(worker) = &created.event else { + unreachable!() + }; + assert_eq!(browser.subscribe().unwrap().0.workers, vec![worker.clone()]); + let WorkerHandle::Shared { instance, .. } = worker.handle() else { + unreachable!() + }; + match retirement { + "worker" => assert!(context.close_shared_worker(instance)), + "context" => assert!(context.remove().unwrap()), + "browser" => service.shutdown(), + _ => unreachable!(), + } + let destroyed = tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + let record = events.recv().await.unwrap(); + if record.event == BrowserEvent::WorkerDestroyed(worker.handle()) { + break record; + } + } + }) + .await + .expect("retirement must publish its exact native Worker occurrence"); + assert!(destroyed.sequence > created.sequence); + if retirement != "browser" { + // An owner boundary also drains callbacks queued by physical close. + assert!(browser.subscribe().unwrap().0.workers.is_empty()); + service.shutdown(); + } + while let Ok(record) = events.try_recv() { + assert_ne!( + record.event, + BrowserEvent::WorkerDestroyed(worker.handle()), + "late renderer close must not publish a duplicate destruction" + ); + } + } +} + +#[tokio::test] +async fn native_dedicated_worker_membership_and_document_retirement_need_no_devtools() { + for retirement in ["worker", "document", "contents", "context", "browser"] { + let service = BrowserService::start().unwrap(); + let browser = service.handle(); + let (context, contents) = context_with_contents(&service); + let (_, mut events) = browser.subscribe().unwrap(); + navigate(&context, contents, "data:text/html,").await; + let created = tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + let record = events.recv().await.unwrap(); + if let BrowserEvent::WorkerCreated(worker) = record.event { + break (worker, record.sequence); + } + } + }) + .await + .expect("DedicatedWorker creation must reach Browser without DevTools"); + assert_eq!(browser.subscribe().unwrap().0.workers.len(), 1); + let crate::browser::WorkerHandle::Dedicated { instance, .. } = created.0.handle() else { + panic!("DedicatedWorker must have its own physical identity"); + }; + let script = tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + if let BrowserEvent::WorkerUpdated(worker) = events.recv().await.unwrap().event + && worker.handle() == created.0.handle() + && let crate::browser::WorkerSnapshot::Dedicated { worker, .. } = worker + { + break worker + .main_script + .expect("completion stores the main-script fact"); + } + } + }) + .await + .expect("main-script completion must be native too"); + assert!(matches!( + script.outcome, + crate::page::RendererDedicatedWorkerMainScriptOutcome::Loaded(_) + )); + let snapshot = browser.subscribe().unwrap().0; + let crate::browser::WorkerSnapshot::Dedicated { worker, .. } = &snapshot.workers[0] else { + unreachable!() + }; + assert!(std::sync::Arc::ptr_eq( + worker.main_script.as_ref().unwrap(), + &script + )); + match retirement { + "worker" => assert!(context.close_dedicated_worker(instance)), + "document" => { + navigate(&context, contents, "data:text/html,replacement").await; + } + "contents" => { + browser + .close_web_contents(contents) + .unwrap() + .close_async() + .await; + } + "context" => { + assert!(context.remove().unwrap()); + } + "browser" => service.shutdown(), + _ => unreachable!(), + } + tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + let record = events.recv().await.unwrap(); + if record.event == BrowserEvent::WorkerDestroyed(created.0.handle()) { + assert!(record.sequence > created.1); + break; + } + } + }) + .await + .expect("exact DedicatedWorker retirement must reach Browser"); + if retirement != "browser" { + assert!(browser.subscribe().unwrap().0.workers.is_empty()); + service.shutdown(); + } + while let Ok(record) = events.try_recv() { + assert_ne!( + record.event, + BrowserEvent::WorkerDestroyed(created.0.handle()) + ); + } + } +} + +#[tokio::test] +async fn native_short_lived_dedicated_worker_preserves_occurrences_without_devtools() { + let service = BrowserService::start().unwrap(); + let browser = service.handle(); + let (context, contents) = context_with_contents(&service); + let (_, mut events) = browser.subscribe().unwrap(); + navigate(&context, contents, "data:text/html,").await; + tokio::time::timeout(std::time::Duration::from_secs(5), async { + let mut created = None; + loop { + let record = events.recv().await.unwrap(); + match record.event { + BrowserEvent::WorkerCreated(worker) => { + assert!( + created + .replace((worker.handle(), record.sequence)) + .is_none() + ); + } + BrowserEvent::WorkerDestroyed(handle) => { + let (expected, sequence) = created.expect("creation precedes destruction"); + assert_eq!(handle, expected); + assert!(record.sequence > sequence); + break; + } + _ => {} + } + } + }) + .await + .expect("a transient DedicatedWorker cannot disappear between native observations"); + assert!(browser.subscribe().unwrap().0.workers.is_empty()); + service.shutdown(); +} + +#[tokio::test] +async fn native_dedicated_worker_failed_script_retires_after_its_completion_fact() { + let service = BrowserService::start().unwrap(); + let browser = service.handle(); + let (context, contents) = context_with_contents(&service); + let (_, mut events) = browser.subscribe().unwrap(); + navigate(&context, contents, "data:text/html,").await; + tokio::time::timeout(std::time::Duration::from_secs(5), async { + let mut created = None; + let mut completed = None; + loop { + let record = events.recv().await.unwrap(); + match record.event { + BrowserEvent::WorkerCreated(worker) => { + assert!(created.replace((worker.handle(), record.sequence)).is_none()); + } + BrowserEvent::WorkerUpdated(worker) => { + let (handle, sequence) = created.expect("script completion follows creation"); + assert_eq!(worker.handle(), handle); + assert!(record.sequence > sequence); + let crate::browser::WorkerSnapshot::Dedicated { worker, .. } = worker else { unreachable!() }; + let script = worker.main_script.unwrap(); + assert!(matches!(&script.outcome, + crate::page::RendererDedicatedWorkerMainScriptOutcome::Failed { error_message, .. } + if !error_message.is_empty())); + assert!(completed.replace(record.sequence).is_none()); + } + BrowserEvent::WorkerDestroyed(handle) => { + assert_eq!(handle, created.unwrap().0); + assert!(record.sequence > completed.expect("failure must be observable before retirement")); + break; + } + _ => {} + } + } + }).await.expect("failed Worker script must complete and retire without DevTools"); + assert!(browser.subscribe().unwrap().0.workers.is_empty()); + service.shutdown(); +} + +#[tokio::test] +async fn native_short_lived_shared_worker_preserves_both_occurrences_without_devtools() { + use crate::browser::WorkerSnapshot; + let service = BrowserService::start().unwrap(); + let browser = service.handle(); + let (context, contents) = context_with_contents(&service); + let (_, mut events) = browser.subscribe().unwrap(); + navigate(&context, contents, "data:text/html,").await; + tokio::time::timeout(std::time::Duration::from_secs(5), async { + let mut created = None; + loop { + let record = events.recv().await.unwrap(); + match record.event { + BrowserEvent::WorkerCreated( + worker @ WorkerSnapshot::Shared { context: id, .. }, + ) if id == context.id() => { + assert!( + created + .replace((worker.handle(), record.sequence)) + .is_none() + ); + } + BrowserEvent::WorkerDestroyed(handle) => { + let (expected, sequence) = + created.expect("destruction must follow the actual creation"); + assert_eq!(handle, expected); + assert!(record.sequence > sequence); + break; + } + _ => {} + } + } + }) + .await + .expect("a transient Worker cannot disappear between native observations"); + assert!(browser.subscribe().unwrap().0.workers.is_empty()); + service.shutdown(); +} + fn next_document_commit( events: &mut crate::browser::BrowserEventReceiver, document: DocumentHandle, @@ -2102,7 +2524,7 @@ async fn browser_runtime_retirement_cancels_worker_graph_and_pending_fetches_wit context .finish_document_diagnostics_snapshot(snapshot) .unwrap(); - let runtime = context.worker_runtime_inspection_endpoint(); + let runtime = context.read_live(crate::browser::BrowserContext::worker_runtime_for_test); let active = runtime.moli_memory_diagnostics(); assert_eq!(context.dedicated_worker_running_isolate_count(), 1); assert_eq!(active["sharedWorker"]["runningInstanceCount"], 1); diff --git a/moli-core/src/browser/owner/workers.rs b/moli-core/src/browser/owner/workers.rs new file mode 100644 index 000000000..afc7ece10 --- /dev/null +++ b/moli-core/src/browser/owner/workers.rs @@ -0,0 +1,172 @@ +use crate::browser::{BrowserContextId, BrowserEvent, WorkerHandle, WorkerSnapshot}; +use crate::page::{RendererWorkerLifecycle, RendererWorkerLifecycleInput}; + +use super::Browser; + +impl Browser { + pub(super) fn commit_worker_lifecycle( + &mut self, + id: BrowserContextId, + input: RendererWorkerLifecycleInput, + ) { + let Ok(context) = self.context_mut(id) else { + return; + }; + if !context.routes_renderer_browser_context_runtime(input.runtime) { + return; + } + let event = match input.lifecycle.as_ref() { + RendererWorkerLifecycle::Service(lifecycle) => { + use crate::browser::{ServiceWorkerExecution, ServiceWorkerSnapshot}; + use crate::page::RendererServiceWorkerLifecycle; + match lifecycle { + RendererServiceWorkerLifecycle::Created { info, active_run } => { + if context.service_workers.contains_key(&info.version_id) { + return; + } + let worker = ServiceWorkerSnapshot { + info: info.clone(), + execution: active_run.clone().map_or( + ServiceWorkerExecution::Stopped, + ServiceWorkerExecution::Starting, + ), + }; + context + .service_workers + .insert(info.version_id, worker.clone()); + BrowserEvent::WorkerCreated(WorkerSnapshot::Service { + context: id, + worker, + }) + } + RendererServiceWorkerLifecycle::Destroyed { + version_id, + active_run, + } => { + let Some(worker) = context.service_workers.get(version_id) else { + return; + }; + if worker.execution.active_run() != active_run.as_ref() { + return; + } + context.service_workers.shift_remove(version_id); + BrowserEvent::WorkerDestroyed(WorkerHandle::Service { + context: id, + version: *version_id, + }) + } + RendererServiceWorkerLifecycle::Starting { version_id, .. } + | RendererServiceWorkerLifecycle::Started { version_id, .. } + | RendererServiceWorkerLifecycle::Stopped { version_id, .. } + | RendererServiceWorkerLifecycle::VersionUpdated { version_id, .. } => { + let Some(worker) = context.service_workers.get_mut(version_id) else { + return; + }; + match lifecycle { + RendererServiceWorkerLifecycle::Starting { run, .. } => { + if worker.execution != ServiceWorkerExecution::Stopped { + return; + } + worker.execution = ServiceWorkerExecution::Starting(run.clone()); + } + RendererServiceWorkerLifecycle::Started { run, .. } => { + if worker.execution != ServiceWorkerExecution::Starting(run.clone()) + { + return; + } + worker.execution = ServiceWorkerExecution::Running(run.clone()); + } + RendererServiceWorkerLifecycle::Stopped { run, .. } => { + if worker.execution.active_run() != Some(run) { + return; + } + worker.execution = ServiceWorkerExecution::Stopped; + } + RendererServiceWorkerLifecycle::VersionUpdated { status, .. } => { + worker.info.status = *status; + } + _ => unreachable!("creation and retirement handled above"), + } + BrowserEvent::WorkerUpdated(WorkerSnapshot::Service { + context: id, + worker: worker.clone(), + }) + } + } + } + RendererWorkerLifecycle::DedicatedCreated(info) => { + if context.dedicated_workers.contains_key(&info.instance_id) { + return; + } + let worker = crate::browser::DedicatedWorkerSnapshot { + info: info.clone(), + main_script: None, + }; + context + .dedicated_workers + .insert(info.instance_id, worker.clone()); + BrowserEvent::WorkerCreated(WorkerSnapshot::Dedicated { + context: id, + worker, + }) + } + RendererWorkerLifecycle::DedicatedScriptCompleted { + instance_id, + script, + } => { + let Some(worker) = context.dedicated_workers.get_mut(instance_id) else { + return; + }; + if worker.main_script.is_some() { + return; + } + worker.main_script = Some(script.clone()); + BrowserEvent::WorkerUpdated(WorkerSnapshot::Dedicated { + context: id, + worker: worker.clone(), + }) + } + RendererWorkerLifecycle::DedicatedDestroyed(instance) => { + if context.dedicated_workers.shift_remove(instance).is_none() { + return; + } + BrowserEvent::WorkerDestroyed(WorkerHandle::Dedicated { + context: id, + instance: *instance, + }) + } + RendererWorkerLifecycle::SharedCreated(info) => { + if context.shared_workers.contains_key(&info.instance_id) { + return; + } + context + .shared_workers + .insert(info.instance_id, info.clone()); + BrowserEvent::WorkerCreated(WorkerSnapshot::Shared { + context: id, + info: info.clone(), + }) + } + RendererWorkerLifecycle::SharedDestroyed(instance) => { + if context.shared_workers.shift_remove(instance).is_none() { + return; + } + BrowserEvent::WorkerDestroyed(WorkerHandle::Shared { + context: id, + instance: *instance, + }) + } + }; + // Mutation, native occurrence and FIFO acknowledgement form one owner + // turn. Protocol cannot invent or commit this lifecycle by draining output. + let record = self.events.publish(event); + input.commit(record.sequence.get()); + } + + pub(super) fn publish_retired_workers(&mut self, context: &super::BrowserContext) { + for worker in context.worker_snapshots() { + self.events + .publish(BrowserEvent::WorkerDestroyed(worker.handle())); + } + } +} diff --git a/moli-core/src/browser/workers.rs b/moli-core/src/browser/workers.rs new file mode 100644 index 000000000..582aafdb7 --- /dev/null +++ b/moli-core/src/browser/workers.rs @@ -0,0 +1,86 @@ +use moli_shared_worker::SharedWorkerInstanceId; + +use super::BrowserContextId; +use crate::page::RendererSharedWorkerTargetInfo; + +/// Physical Worker identity, scoped to its original BrowserContext incarnation. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum WorkerHandle { + Service { + context: BrowserContextId, + version: u64, + }, + Dedicated { + context: BrowserContextId, + instance: u64, + }, + Shared { + context: BrowserContextId, + instance: SharedWorkerInstanceId, + }, +} + +/// Live native facts, with no protocol target, attachment or session state. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum WorkerSnapshot { + Service { + context: BrowserContextId, + worker: ServiceWorkerSnapshot, + }, + Dedicated { + context: BrowserContextId, + worker: DedicatedWorkerSnapshot, + }, + Shared { + context: BrowserContextId, + info: RendererSharedWorkerTargetInfo, + }, +} + +impl WorkerSnapshot { + pub fn handle(&self) -> WorkerHandle { + match self { + Self::Service { context, worker } => WorkerHandle::Service { + context: *context, + version: worker.info.version_id, + }, + Self::Dedicated { context, worker } => WorkerHandle::Dedicated { + context: *context, + instance: worker.info.instance_id, + }, + Self::Shared { context, info } => WorkerHandle::Shared { + context: *context, + instance: info.instance_id, + }, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DedicatedWorkerSnapshot { + pub info: crate::page::RendererDedicatedWorkerTargetInfo, + pub main_script: Option>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ServiceWorkerSnapshot { + pub info: crate::page::RendererServiceWorkerTargetInfo, + pub execution: ServiceWorkerExecution, +} + +/// A stable version may outlive many exact physical runs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ServiceWorkerExecution { + Stopped, + Starting(crate::page::RendererServiceWorkerRunIdentity), + Running(crate::page::RendererServiceWorkerRunIdentity), +} + +impl ServiceWorkerExecution { + pub fn active_run(&self) -> Option<&crate::page::RendererServiceWorkerRunIdentity> { + match self { + Self::Stopped => None, + Self::Starting(run) | Self::Running(run) => Some(run), + } + } +} diff --git a/moli-core/src/page/mod.rs b/moli-core/src/page/mod.rs index 60e5a566a..86f078357 100644 --- a/moli-core/src/page/mod.rs +++ b/moli-core/src/page/mod.rs @@ -88,8 +88,10 @@ pub use moli_renderer_v8::{ RendererAutofillTriggerOutcome, RendererAutofillTriggerRequest, RendererCaptureScreencastFrameReply, RendererCaptureScreencastFrameRequest, RendererCaptureScreenshotReply, RendererCaptureScreenshotRequest, - RendererCommandTurnCompletion, RendererCommandTurnOutput, RendererDedicatedWorkerTargetEvent, - RendererDedicatedWorkerTargetInfo, RendererDevToolsAgentToken, RendererDocumentHitTestResult, + RendererCommandTurnCompletion, RendererCommandTurnOutput, RendererCommittedWorkerLifecycle, + RendererDedicatedWorkerMainScript, RendererDedicatedWorkerMainScriptOutcome, + RendererDedicatedWorkerObservation, RendererDedicatedWorkerTargetInfo, + RendererDevToolsAgentToken, RendererDocumentHitTestResult, RendererDocumentIsolateAccountingDiagnostics, RendererDocumentLifecycleEvent, RendererDocumentLifecycleEventKind, RendererDocumentLifecycleIdentity, RendererDocumentLifecycleMilestone, RendererDocumentLifecycleSnapshot, @@ -126,13 +128,14 @@ pub use moli_renderer_v8::{ RendererScreenshotFormat, RendererScreenshotPurpose, RendererScreenshotRegion, RendererScrollIntoViewResult, RendererServiceWorkerConsoleMessage, RendererServiceWorkerExceptionMessage, RendererServiceWorkerFetchDiagnostic, - RendererServiceWorkerFetchDiagnosticResult, RendererServiceWorkerRunIdentity, - RendererServiceWorkerTargetEvent, RendererServiceWorkerTargetInfo, - RendererServiceWorkerVersionStatus, RendererSetDocumentContentResult, - RendererSharedWorkerConsoleMessage, RendererSharedWorkerTargetEvent, - RendererSharedWorkerTargetInfo, RendererSyntheticResponseBody, RendererTextSearchMatch, - RendererTouchPoint, RendererVisualStateToken, RendererWindowDocumentSource, - RuntimeConsoleMessageSnapshot, + RendererServiceWorkerFetchDiagnosticResult, RendererServiceWorkerLifecycle, + RendererServiceWorkerObservation, RendererServiceWorkerRunIdentity, + RendererServiceWorkerTargetInfo, RendererServiceWorkerVersionStatus, + RendererSetDocumentContentResult, RendererSharedWorkerConsoleMessage, + RendererSharedWorkerObservation, RendererSharedWorkerTargetInfo, RendererSyntheticResponseBody, + RendererTextSearchMatch, RendererTouchPoint, RendererVisualStateToken, + RendererWindowDocumentSource, RendererWorkerLifecycle, RendererWorkerLifecycleInput, + RendererWorkerLifecycleObservation, RuntimeConsoleMessageSnapshot, }; pub use moli_renderer_v8::{ RendererAppManifest, RendererAppManifestDisplayMode, RendererAppManifestError, diff --git a/moli-core/src/runtime/mod.rs b/moli-core/src/runtime/mod.rs index e115a0a08..2c2190f59 100644 --- a/moli-core/src/runtime/mod.rs +++ b/moli-core/src/runtime/mod.rs @@ -43,6 +43,7 @@ pub use moli_renderer_v8::{ RendererLifecycleDecider, RendererLifecycleDecision, RendererLifecycleSnapshot, RendererPageReservationToken, RendererReservedServiceWorkerClient, RendererServiceWorkerMainResourceFetch, RendererSharedWorkerRuntimeDiagnostics, + RendererWorkerInspectionEndpoint, RendererWorkerInspectionTarget, }; pub use navigation_engine::{ BuiltDocumentPage, CommittedDocumentResourceSource, NavigationEngine, diff --git a/moli-protocol-server/src/cdp_scheduler/browser_events.rs b/moli-protocol-server/src/cdp_scheduler/browser_events.rs index 748e2bf09..4ca622505 100644 --- a/moli-protocol-server/src/cdp_scheduler/browser_events.rs +++ b/moli-protocol-server/src/cdp_scheduler/browser_events.rs @@ -95,7 +95,10 @@ impl CdpScheduler { .project_browser_navigation(request.web_contents) .await } - BrowserEvent::DocumentLifecycleChanged(_) + BrowserEvent::WorkerCreated(_) + | BrowserEvent::WorkerUpdated(_) + | BrowserEvent::WorkerDestroyed(_) + | BrowserEvent::DocumentLifecycleChanged(_) | BrowserEvent::DocumentTitleChanged(_) | BrowserEvent::DialogOpened(_) | BrowserEvent::DialogClosed { .. } => { diff --git a/moli-protocol-server/src/cdp_scheduler/runtime_dispatch.rs b/moli-protocol-server/src/cdp_scheduler/runtime_dispatch.rs index 7ceb33122..276f4b9ca 100644 --- a/moli-protocol-server/src/cdp_scheduler/runtime_dispatch.rs +++ b/moli-protocol-server/src/cdp_scheduler/runtime_dispatch.rs @@ -36,6 +36,8 @@ enum RuntimeCommandWait { pub(crate) enum DevToolsRuntimeCommandProgress { Complete(Box), + /// No renderer command or Document endpoint has been captured yet. + AwaitingDocument(Box), Pending { pending: Box, protocol_output: ProtocolOutputSequence, @@ -57,6 +59,21 @@ pub(super) fn devtools_command_uses_interleaved_runtime_dispatch( } impl CdpScheduler { + pub(crate) fn devtools_runtime_command_waits_for_document( + &mut self, + command: &DevToolsCommand, + ) -> bool { + // Only context-targeted commands follow a Page's navigation. Child + // frame routes share that Page's hold; Worker routes have no pending + // Document. Realm commands and termination retain their own binding. + !matches!(command, DevToolsCommand::TerminateExecution(_)) + && command.context().target_id.is_some() + && (self.devtools_context_has_pending_document_navigation(command.context()) + || self + .conn + .devtools_context_document_projection_is_pending(command.context())) + } + async fn route_renderer_response_to_devtools_pending( &mut self, pending: &mut PendingDevToolsRuntimeCommandDispatch, @@ -165,6 +182,9 @@ impl CdpScheduler { receivers: &mut CdpSchedulerEventReceivers, command: DevToolsCommand, ) -> DevToolsRuntimeCommandProgress { + if self.devtools_runtime_command_waits_for_document(&command) { + return DevToolsRuntimeCommandProgress::AwaitingDocument(Box::new(command)); + } let protocol_output = ProtocolOutputSequence::empty(); let step = self .conn diff --git a/moli-protocol-server/src/protocol_server/tests/bidi.rs b/moli-protocol-server/src/protocol_server/tests/bidi.rs index 22fcaa180..e0a88cc0c 100644 --- a/moli-protocol-server/src/protocol_server/tests/bidi.rs +++ b/moli-protocol-server/src/protocol_server/tests/bidi.rs @@ -3174,6 +3174,32 @@ async fn websocket_bidi_shared_worker_subscription_projects_runtime_listener_pre assert_eq!(probe["echoed"], json!("bidi")); assert_eq!(probe["isSharedWorker"], json!(true)); + let worker_context = messages + .iter() + .find(|message| { + message["method"] == "browsingContext.contextCreated" + && message["params"]["url"] == worker_url + }) + .unwrap()["params"]["context"] + .as_str() + .unwrap(); + let realms = timeout( + Duration::from_secs(5), + send_bidi_command_response( + &mut socket, + 7, + "script.getRealms", + json!({"context": worker_context, "type": "shared-worker"}), + ), + ) + .await + .expect("a Worker has no main Document to await"); + assert_eq!(realms["type"], "success", "{realms:?}"); + assert_eq!( + realms["result"]["realms"][0]["type"], "shared-worker", + "{realms:?}" + ); + let end = send_bidi_command_response(&mut socket, 6, "session.end", json!({})).await; assert_eq!(end["type"], json!("success"), "{end:?}"); let closed = timeout(Duration::from_secs(1), socket.next()) @@ -14989,7 +15015,7 @@ async fn websocket_bidi_wait_none_navigation_drains_before_next_command() { .await .expect("send script.evaluate after wait=none navigation"); let evaluate = recv_ws_json(&mut socket).await; - assert_eq!(evaluate["type"], json!("success")); + assert_eq!(evaluate["type"], json!("success"), "{evaluate:?}"); assert_eq!(evaluate["id"], json!(4_u64)); assert_eq!(evaluate["result"]["type"], json!("success")); assert_eq!(evaluate["result"]["result"]["type"], json!("string")); @@ -15002,6 +15028,382 @@ async fn websocket_bidi_wait_none_navigation_drains_before_next_command() { protocol_server.abort(); } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum BidiNavigationAdmissionHold { + Headers, + Body, + Request, + Auth, + Response, +} + +#[derive(Clone, Copy)] +enum BidiNavigationAdmissionFinish { + Commit, + Close, + RetireChild, +} + +async fn assert_bidi_navigation_admission_remains_interleavable( + hold: BidiNavigationAdmissionHold, + finish: BidiNavigationAdmissionFinish, +) { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let url = format!("http://{}/admission", listener.local_addr().unwrap()); + let (received_tx, received) = tokio::sync::oneshot::channel(); + let (release, released) = tokio::sync::oneshot::channel(); + let fixture = tokio::spawn(async move { + let mut accepted = 0; + let mut stream = loop { + let (mut stream, _) = listener.accept().await.unwrap(); + let mut request = Vec::new(); + let mut byte = [0]; + while !request.ends_with(b"\r\n\r\n") { + stream.read_exact(&mut byte).await.unwrap(); + request.push(byte[0]); + } + accepted += 1; + if hold == BidiNavigationAdmissionHold::Auth && accepted == 1 { + stream.write_all(b"HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"admission\"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").await.unwrap(); + stream.shutdown().await.unwrap(); + continue; + } + if hold == BidiNavigationAdmissionHold::Auth { + assert!( + String::from_utf8(request) + .unwrap() + .to_ascii_lowercase() + .contains("authorization: basic") + ); + } + break stream; + }; + let body = b"admitted document"; + let head = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + let streaming = matches!( + hold, + BidiNavigationAdmissionHold::Body | BidiNavigationAdmissionHold::Response + ); + if streaming { + stream.write_all(head.as_bytes()).await.unwrap(); + stream.write_all(b"").await.unwrap(); + } + let _ = received_tx.send(()); + if released.await.is_err() { + return; + } + if !streaming { + stream.write_all(head.as_bytes()).await.unwrap(); + stream.write_all(body).await.unwrap(); + } else { + stream.write_all(&body[b"".len()..]).await.unwrap(); + } + stream.shutdown().await.unwrap(); + }); + let (addr, protocol_server) = spawn_test_protocol_server().await; + let (mut socket, _) = connect_async(format!("ws://{addr}/session")).await.unwrap(); + assert_eq!( + send_bidi_command_response(&mut socket, 1, "session.new", json!({})).await["type"], + "success" + ); + let created = send_bidi_command_response( + &mut socket, + 2, + "browsingContext.create", + json!({"type": "tab"}), + ) + .await; + let context = created["result"]["context"].as_str().unwrap().to_owned(); + let script_context = if matches!(finish, BidiNavigationAdmissionFinish::RetireChild) { + let initial = send_bidi_command_response(&mut socket, 20, "browsingContext.navigate", + json!({"context": context, "url": "data:text/html,", "wait": "complete"}), + ).await; + assert_eq!(initial["type"], "success", "{initial:?}"); + let tree = send_bidi_command_response( + &mut socket, + 21, + "browsingContext.getTree", + json!({"root": context}), + ) + .await; + tree["result"]["contexts"][0]["children"][0]["context"] + .as_str() + .unwrap_or_else(|| panic!("real child frame: {tree:?}")) + .to_owned() + } else { + context.clone() + }; + let event = match hold { + BidiNavigationAdmissionHold::Headers => None, + BidiNavigationAdmissionHold::Body | BidiNavigationAdmissionHold::Response => { + Some("network.responseStarted") + } + BidiNavigationAdmissionHold::Request => Some("network.beforeRequestSent"), + BidiNavigationAdmissionHold::Auth => Some("network.authRequired"), + }; + if let Some(event) = event { + assert_eq!( + send_bidi_command_response( + &mut socket, + 10, + "session.subscribe", + json!({"events": [event], "contexts": [context]}) + ) + .await["type"], + "success" + ); + } + let phase = match hold { + BidiNavigationAdmissionHold::Request => Some("beforeRequestSent"), + BidiNavigationAdmissionHold::Auth => Some("authRequired"), + BidiNavigationAdmissionHold::Response => Some("responseStarted"), + _ => None, + }; + if let Some(phase) = phase { + let intercept = send_bidi_command_response( + &mut socket, + 11, + "network.addIntercept", + json!({"phases": [phase], "contexts": [context]}), + ) + .await; + assert_eq!(intercept["type"], "success", "{intercept:?}"); + } + socket.send(WsMessage::Text(json!({"id": 3, "method": "browsingContext.navigate", "params": {"context": context, "url": url, "wait": "none"}}).to_string().into())).await.unwrap(); + let mut messages = recv_until_id(&mut socket, 3).await; + assert_eq!( + bidi_message_by_id(&messages, 3)["type"], + "success", + "{messages:?}" + ); + if let Some(event) = event { + while !messages + .iter() + .any(|message| message["method"] == event && message["params"]["request"]["url"] == url) + { + messages.push( + timeout(Duration::from_secs(5), recv_ws_json(&mut socket)) + .await + .expect("held navigation occurrence"), + ); + } + } + let intercepted = phase.map(|_| { + let pause = messages + .iter() + .find(|message| { + message["method"] == event.unwrap() && message["params"]["isBlocked"] == true + }) + .expect("exact navigation pause"); + pause["params"]["request"]["request"] + .as_str() + .unwrap() + .to_owned() + }); + let mut received = Some(received); + if phase.is_none() { + timeout(Duration::from_secs(5), received.take().unwrap()) + .await + .unwrap() + .unwrap(); + } + // Another page must remain usable while the requested page is navigating. + // Creating it also changes the active target before the original command + // is admitted: neither the wait nor dispatch may resolve that active page. + let other = timeout( + Duration::from_secs(5), + send_bidi_command_response( + &mut socket, + 12, + "browsingContext.create", + json!({"type": "tab"}), + ), + ) + .await + .expect("another target must not wait for this navigation"); + let other_context = other["result"]["context"].as_str().unwrap().to_owned(); + let other_script = send_bidi_command_response(&mut socket, 13, "script.evaluate", + json!({"expression": "document.body.textContent = 'other document'", "target": {"context": other_context}}), + ).await; + assert_eq!( + other_script["result"]["result"]["value"], "other document", + "{other_script:?}" + ); + // Both commands traverse the same socket FIFO. The status reply proves the + // preceding script has been considered without blocking the shared owner. + for command in [ + json!({"id": 4, "method": "script.evaluate", "params": {"expression": "globalThis.admissionCalls = (globalThis.admissionCalls || 0) + 1; document.body.textContent", "target": {"context": script_context}}}), + json!({"id": 5, "method": "session.status", "params": {}}), + ] { + socket + .send(WsMessage::Text(command.to_string().into())) + .await + .unwrap(); + } + let progress = timeout(Duration::from_secs(5), recv_until_id(&mut socket, 5)) + .await + .expect("pending admission must leave the owner responsive"); + assert_eq!(bidi_message_by_id(&progress, 5)["type"], "success"); + assert!( + progress.iter().all(|message| message["id"] != 4), + "{hold:?}: script must remain unadmitted: {progress:?}" + ); + if matches!(finish, BidiNavigationAdmissionFinish::Close) { + socket + .send(WsMessage::Text( + json!({"id": 6, "method": "browsingContext.close", "params": {"context": context}}) + .to_string() + .into(), + )) + .await + .unwrap(); + let mut closed = timeout(Duration::from_secs(5), recv_until_id(&mut socket, 6)) + .await + .unwrap(); + assert_eq!( + bidi_message_by_id(&closed, 6)["type"], + "success", + "{closed:?}" + ); + if closed.iter().all(|message| message["id"] != 4) { + closed.extend( + timeout(Duration::from_secs(5), recv_until_id(&mut socket, 4)) + .await + .expect("closing the exact target must end admission"), + ); + } + assert_eq!( + bidi_message_by_id(&closed, 4)["error"], + "no such frame", + "{closed:?}" + ); + drop(release); + } else { + if let Some(request) = intercepted { + let (method, params) = match hold { + BidiNavigationAdmissionHold::Auth => ( + "network.continueWithAuth", + json!({"request": request, "action": "provideCredentials", "credentials": {"type": "password", "username": "user", "password": "pass"}}), + ), + BidiNavigationAdmissionHold::Response => { + ("network.continueResponse", json!({"request": request})) + } + _ => ("network.continueRequest", json!({"request": request})), + }; + let continued = timeout( + Duration::from_secs(5), + send_bidi_command_response(&mut socket, 6, method, params), + ) + .await + .expect("the waiting script must allow its navigation decision"); + assert_eq!(continued["type"], "success", "{continued:?}"); + timeout(Duration::from_secs(5), received.take().unwrap()) + .await + .unwrap() + .unwrap(); + } + release.send(()).unwrap(); + let completed = timeout(Duration::from_secs(5), recv_until_id(&mut socket, 4)) + .await + .unwrap(); + let evaluated = bidi_message_by_id(&completed, 4); + if matches!(finish, BidiNavigationAdmissionFinish::RetireChild) { + assert_eq!(evaluated["error"], "no such frame", "{completed:?}"); + } else { + assert_eq!(evaluated["type"], "success", "{completed:?}"); + assert_eq!(evaluated["result"]["result"]["value"], "admitted document"); + let once = send_bidi_command_response( + &mut socket, + 7, + "script.evaluate", + json!({"expression": "admissionCalls", "target": {"context": context}}), + ) + .await; + assert_eq!(once["result"]["result"]["value"], 1, "{once:?}"); + } + } + let untouched = send_bidi_command_response( + &mut socket, + 8, + "script.evaluate", + json!({"expression": "typeof admissionCalls", "target": {"context": other_context}}), + ) + .await; + assert_eq!( + untouched["result"]["result"]["value"], "undefined", + "{untouched:?}" + ); + fixture.await.unwrap(); + socket.close(None).await.unwrap(); + protocol_server.abort(); +} + +#[tokio::test] +async fn websocket_bidi_navigation_admission_before_headers() { + assert_bidi_navigation_admission_remains_interleavable( + BidiNavigationAdmissionHold::Headers, + BidiNavigationAdmissionFinish::Commit, + ) + .await; +} + +#[tokio::test] +async fn websocket_bidi_navigation_admission_during_body() { + assert_bidi_navigation_admission_remains_interleavable( + BidiNavigationAdmissionHold::Body, + BidiNavigationAdmissionFinish::Commit, + ) + .await; +} + +#[tokio::test] +async fn websocket_bidi_navigation_admission_allows_request_decision() { + assert_bidi_navigation_admission_remains_interleavable( + BidiNavigationAdmissionHold::Request, + BidiNavigationAdmissionFinish::Commit, + ) + .await; +} + +#[tokio::test] +async fn websocket_bidi_navigation_admission_allows_auth_decision() { + assert_bidi_navigation_admission_remains_interleavable( + BidiNavigationAdmissionHold::Auth, + BidiNavigationAdmissionFinish::Commit, + ) + .await; +} + +#[tokio::test] +async fn websocket_bidi_navigation_admission_allows_response_decision() { + assert_bidi_navigation_admission_remains_interleavable( + BidiNavigationAdmissionHold::Response, + BidiNavigationAdmissionFinish::Commit, + ) + .await; +} + +#[tokio::test] +async fn websocket_bidi_navigation_admission_ends_when_target_closes() { + assert_bidi_navigation_admission_remains_interleavable( + BidiNavigationAdmissionHold::Headers, + BidiNavigationAdmissionFinish::Close, + ) + .await; +} + +#[tokio::test] +async fn websocket_bidi_navigation_admission_preserves_child_identity() { + assert_bidi_navigation_admission_remains_interleavable( + BidiNavigationAdmissionHold::Headers, + BidiNavigationAdmissionFinish::RetireChild, + ) + .await; +} + #[tokio::test] async fn websocket_bidi_wait_none_navigation_returns_before_parser_blocking_script() { async fn page() -> impl IntoResponse { diff --git a/moli-protocol-server/src/protocol_server/webdriver_bidi.rs b/moli-protocol-server/src/protocol_server/webdriver_bidi.rs index 79ba682ad..1257f3585 100644 --- a/moli-protocol-server/src/protocol_server/webdriver_bidi.rs +++ b/moli-protocol-server/src/protocol_server/webdriver_bidi.rs @@ -209,7 +209,7 @@ impl BidiSocketActor { Some(BidiPendingCommandWait::NavigationLifecycle(wait)) => { scheduler.cancel_devtools_navigation_lifecycle(*wait); } - None => {} + Some(BidiPendingCommandWait::DocumentAdmission(_)) | None => {} } if let Some(previous_target_discovery) = pending.completion.previous_target_discovery { scheduler.replace_target_discovery_enabled(previous_target_discovery); @@ -344,6 +344,36 @@ impl BidiSocketActor { output: ProtocolOutputSequence, owner_context: Option<&str>, ) -> bool { + if self.pending_command.as_ref().is_some_and(|command| { + matches!( + command.pending, + Some(BidiPendingCommandWait::DocumentAdmission(_)) + ) + }) { + // Publish pauses before polling admission so the frontend can + // continue Fetch/auth on this same socket. Never await navigation + // completion inside the shared owner turn. + if !self + .send_protocol_output(scheduler, receivers, output, owner_context) + .await + { + return false; + } + let mut pending = self + .pending_command + .take() + .expect("pending document admission"); + let Some(BidiPendingCommandWait::DocumentAdmission(command)) = pending.pending.take() + else { + unreachable!() + }; + let progress = scheduler + .start_devtools_runtime_command(receivers, *command) + .await; + return self + .apply_pending_runtime_progress(scheduler, receivers, pending, progress) + .await; + } if self.pending_command.as_ref().is_some_and(|command| { matches!( command.pending, @@ -446,6 +476,11 @@ impl BidiSocketActor { progress: DevToolsRuntimeCommandProgress, ) -> bool { match progress { + DevToolsRuntimeCommandProgress::AwaitingDocument(command) => { + pending_command.pending = Some(BidiPendingCommandWait::DocumentAdmission(command)); + self.pending_command = Some(pending_command); + true + } DevToolsRuntimeCommandProgress::Complete(execution) => { complete_and_send_bidi_pending_command( &mut self.socket, @@ -1382,6 +1417,7 @@ struct BidiPendingCommand { } enum BidiPendingCommandWait { + DocumentAdmission(Box), Runtime(Box), Navigation(DevToolsNavigationCommandWait), NavigationLifecycle(Box), @@ -1406,7 +1442,8 @@ impl BidiPendingCommand { fn runtime_pending(&self) -> Option<&PendingDevToolsRuntimeExecution> { match self.pending.as_ref()? { BidiPendingCommandWait::Runtime(pending) => Some(pending), - BidiPendingCommandWait::Navigation(_) + BidiPendingCommandWait::DocumentAdmission(_) + | BidiPendingCommandWait::Navigation(_) | BidiPendingCommandWait::NavigationLifecycle(_) => None, } } @@ -2023,20 +2060,20 @@ async fn start_bidi_devtools_command( &dispatch.command, DevToolsCommand::EvaluateScript(_) | DevToolsCommand::CallFunction(_) ); - let mut event_sources = - match drain_bidi_background_navigation_before_command(scheduler, receivers).await { - Ok(event_sources) => event_sources, - Err(failure) => { - let (event_sources, error) = failure.into_parts(); - return BidiDevToolsCommandStart::Complete(BidiDevToolsCommandOutput { - response: bidi_response_from_devtools_error(dispatch.id, error), - event_sources: event_sources.into_sources(), - post_response_event_sources: Vec::new(), - post_response_background_navigation_drain: BidiBackgroundNavigationDrain::None, - event_context, - }); - } - }; + let mut event_sources = match drain_ready_bidi_background_navigation(scheduler, receivers).await + { + Ok(event_sources) => event_sources, + Err(failure) => { + let (event_sources, error) = failure.into_parts(); + return BidiDevToolsCommandStart::Complete(BidiDevToolsCommandOutput { + response: bidi_response_from_devtools_error(dispatch.id, error), + event_sources: event_sources.into_sources(), + post_response_event_sources: Vec::new(), + post_response_background_navigation_drain: BidiBackgroundNavigationDrain::None, + event_context, + }); + } + }; event_sources.extend_protocol_output( scheduler .complete_ready_protocol_residences_after_command() @@ -2083,6 +2120,16 @@ async fn start_bidi_devtools_command( .start_devtools_runtime_command(receivers, dispatch.command) .await { + DevToolsRuntimeCommandProgress::AwaitingDocument(command) => { + BidiDevToolsCommandStart::Pending(Box::new(BidiPendingCommand { + command_method: None, + command_params: None, + command_channel: None, + pending_navigation_candidate: None, + pending: Some(BidiPendingCommandWait::DocumentAdmission(command)), + completion, + })) + } DevToolsRuntimeCommandProgress::Complete(execution) => { BidiDevToolsCommandStart::Complete( complete_bidi_devtools_command_execution( diff --git a/moli-protocol-server/src/protocol_server/webdriver_classic/state/command.rs b/moli-protocol-server/src/protocol_server/webdriver_classic/state/command.rs index 25b34eb90..ce3a8c042 100644 --- a/moli-protocol-server/src/protocol_server/webdriver_classic/state/command.rs +++ b/moli-protocol-server/src/protocol_server/webdriver_classic/state/command.rs @@ -65,16 +65,9 @@ impl ClassicPendingRuntime { receivers: &mut CdpSchedulerEventReceivers, ) -> Option { if matches!(self.phase, RuntimePhase::Command) - && scheduler.devtools_context_has_pending_document_navigation(self.command.context()) + && scheduler.devtools_runtime_command_waits_for_document(&self.command) { - if self.navigation_deadline.is_none() { - self.navigation_deadline = self - .navigation_timeout - .and_then(|timeout| tokio::time::Instant::now().checked_add(timeout)); - } - self.deadline = self.navigation_deadline; - self.wait = RuntimeWait::Navigation; - return Some(self); + return Some(self.await_document()); } self.page_residence = scheduler.page_residence_identity_for_devtools_context(self.command.context()); @@ -108,6 +101,10 @@ impl ClassicPendingRuntime { progress: DevToolsRuntimeCommandProgress, ) -> Option { match progress { + DevToolsRuntimeCommandProgress::AwaitingDocument(command) => { + self.command = *command; + Some(self.await_document()) + } DevToolsRuntimeCommandProgress::Pending { pending, protocol_output, @@ -135,15 +132,7 @@ impl ClassicPendingRuntime { &execution.result, ) && self.navigation_timeout.is_some() => { - if self.navigation_deadline.is_none() { - self.navigation_deadline = - self.navigation_timeout.and_then(|timeout| { - tokio::time::Instant::now().checked_add(timeout) - }); - } - self.deadline = self.navigation_deadline; - self.wait = RuntimeWait::Navigation; - Some(self) + Some(self.await_document()) } RuntimePhase::Command => { self.reply(execution.result); @@ -154,6 +143,17 @@ impl ClassicPendingRuntime { } } + fn await_document(mut self) -> Self { + if self.navigation_deadline.is_none() { + self.navigation_deadline = self + .navigation_timeout + .and_then(|timeout| tokio::time::Instant::now().checked_add(timeout)); + } + self.deadline = self.navigation_deadline; + self.wait = RuntimeWait::Navigation; + self + } + pub(super) fn deadline(&self) -> Option { // Dispatch and navigation retry already install their own deadline. // Retained navigation bookkeeping must not time out a later script. @@ -214,7 +214,7 @@ impl ClassicPendingRuntime { receivers: &mut CdpSchedulerEventReceivers, ) -> Option { if matches!(self.wait, RuntimeWait::Navigation) - && !scheduler.devtools_context_has_pending_document_navigation(self.command.context()) + && !scheduler.devtools_runtime_command_waits_for_document(&self.command) { self.dispatch(scheduler, receivers).await } else { diff --git a/moli-protocol/Cargo.toml b/moli-protocol/Cargo.toml index d9d7d72a8..2447f8e75 100644 --- a/moli-protocol/Cargo.toml +++ b/moli-protocol/Cargo.toml @@ -55,6 +55,7 @@ moli-core = { path = "../moli-core", features = ["test-support"] } moli-cookie-jar = { path = "../moli-cookie-jar", features = ["test-support"] } moli-image = { path = "../moli-image" } moli-storage-service = { path = "../moli-storage-service" } +moli-test-support = { path = "../moli-test-support" } moli-v8-init = { path = "../moli-v8-init" } [build-dependencies] diff --git a/moli-protocol/src/conn.rs b/moli-protocol/src/conn.rs index 80f7be08c..5da9b7b32 100644 --- a/moli-protocol/src/conn.rs +++ b/moli-protocol/src/conn.rs @@ -583,14 +583,14 @@ pub use state::{ }; pub(crate) use state::{ BrowserContextPageStorageHandles, BrowserContextStoragePartitionHandles, - CommittedRendererDocumentBinding, ContextNetworkPolicy, DedicatedWorkerMainScriptOutcome, - DedicatedWorkerMainScriptSnapshot, DedicatedWorkerTargetState, DevToolsBrowserIdentityOverride, - DevToolsConsoleOutputSessionState, DevToolsLogViolationThreshold, DocumentId, - DocumentProjectionOutputRelease, DuplicatePendingRendererCommand, EmulatedNetworkConditions, - EmulatedViewportSurface, EmulationPolicyChange, InitialDocumentCreator, - InspectorCommandDispatch, NETWORK_ERROR_PAGE_URL, NavigationId, NavigationResultProjection, - PageScreencastConfig, PageScreencastFormat, PendingBidiChannelListener, PendingInspectorAwait, - PerformanceTimeDomain, PreparedRendererCallDispatch, ProfilerAction, ProfilerInspectorCommand, + CommittedRendererDocumentBinding, ContextNetworkPolicy, DedicatedWorkerTargetState, + DevToolsBrowserIdentityOverride, DevToolsConsoleOutputSessionState, + DevToolsLogViolationThreshold, DocumentId, DocumentProjectionOutputRelease, + DuplicatePendingRendererCommand, EmulatedNetworkConditions, EmulatedViewportSurface, + EmulationPolicyChange, InitialDocumentCreator, InspectorCommandDispatch, + NETWORK_ERROR_PAGE_URL, NavigationId, NavigationResultProjection, PageScreencastConfig, + PageScreencastFormat, PendingBidiChannelListener, PendingInspectorAwait, PerformanceTimeDomain, + PreparedRendererCallDispatch, ProfilerAction, ProfilerInspectorCommand, RendererCommandCorrelation, RendererCommandDescriptor, RendererCommandReplay, RendererDocumentLifecycleObservation, RendererDocumentLifecycleObserver, RendererPageResidenceIdentity, ServiceWorkerRuntimeExceptionSnapshot, ServiceWorkerTargetState, @@ -1390,6 +1390,17 @@ impl CdpConnection { .is_ok_and(TargetRuntimeSlot::document_projection_is_pending) } + pub fn devtools_context_document_projection_is_pending( + &self, + context: &DevToolsCommandContext, + ) -> bool { + self.command_owner_scope_for_devtools_context(context) + .is_some_and(|owner| { + self.runtime_session_owner_slot_for_owner(&owner) + .is_ok_and(TargetRuntimeSlot::document_projection_is_pending) + }) + } + pub(crate) fn accepts_pending_document_navigation_for_owner( &self, owner: &CommandOwnerScope, diff --git a/moli-protocol/src/conn/browser_context/lifecycle.rs b/moli-protocol/src/conn/browser_context/lifecycle.rs index 23ad50836..7e7081359 100644 --- a/moli-protocol/src/conn/browser_context/lifecycle.rs +++ b/moli-protocol/src/conn/browser_context/lifecycle.rs @@ -216,6 +216,10 @@ impl CdpConnection { for selected in snapshot.selected_web_contents { events.extend(self.project_browser_selection(selected, None, snapshot.sequence)); } + events.extend( + self.project_browser_workers(snapshot.workers, snapshot.sequence) + .await, + ); for download in snapshot.downloads { events.extend(self.project_created_browser_download(download)); } diff --git a/moli-protocol/src/conn/browser_context/navigation_decisions.rs b/moli-protocol/src/conn/browser_context/navigation_decisions.rs index 69fd588ab..d8188e842 100644 --- a/moli-protocol/src/conn/browser_context/navigation_decisions.rs +++ b/moli-protocol/src/conn/browser_context/navigation_decisions.rs @@ -145,7 +145,7 @@ impl CdpConnection { )) } - fn project_native_navigation_network( + pub(super) fn project_native_navigation_network( &mut self, response: &moli_core::browser::NavigationResponseSnapshot, complete: bool, @@ -719,14 +719,8 @@ impl CdpConnection { let (context_id, target_id) = self .resolved_page_owner_identity_for_owner(&state.owner) .ok_or("navigation projection unavailable")?; - let failure = previous.and_then(|navigation| { - self.browser_context_by_id_mut(&context_id)? - .take_failed_native_navigation(&target_id, navigation) - }); - let mut events = failure - .map(|(state, emit_network)| { - self.native_navigation_retirement_events(&state, emit_network) - }) + let mut events = previous + .map(|navigation| self.native_navigation_retirement_events(contents, navigation)) .unwrap_or_default(); self.browser_context_by_id_mut(&context_id) .expect("resolved Context") diff --git a/moli-protocol/src/conn/browser_context/navigation_events.rs b/moli-protocol/src/conn/browser_context/navigation_events.rs index b1baf491e..20444da83 100644 --- a/moli-protocol/src/conn/browser_context/navigation_events.rs +++ b/moli-protocol/src/conn/browser_context/navigation_events.rs @@ -3,7 +3,7 @@ use crate::conn::{ TargetPageResidenceIdentity, }; use crate::domains::{command_output::CommandOutputBuffer, page}; -use moli_core::browser::{NavigationAttempt, WebContentsHandle}; +use moli_core::browser::{NavigationAttempt, NavigationId, WebContentsHandle}; impl CdpConnection { /// Reconcile only this physical WebContents. Native completion releases @@ -61,35 +61,32 @@ impl CdpConnection { document, )) }); - let mut releases = Vec::new(); - let mut native_failures = Vec::new(); - for navigation in retired { - if let Some(failure) = context.take_failed_native_navigation(&target_id, navigation) - { - native_failures.push(failure); - } - context.discard_target_navigation_projection(&target_id, &navigation); - if let Ok(release) = context - .page_targets - .get_mut(&target_id)? - .runtime_slot - .finish_navigation_without_document_projection(&navigation) - { - releases.push(release); - } - } - Some((owner, releases, native_failures)) + Some((target_id, owner, retired)) })(); self.network_request_id_allocator = allocator; - let Some((owner, releases, native_failures)) = prepared else { + let Some((target_id, owner, retired)) = prepared else { return events; }; let mut out = CommandOutputBuffer::default(); let mut command_context = CommandDispatchContext::default(); - for (state, emit_network) in native_failures { + let mut releases = Vec::new(); + for navigation in retired { out.extend_background_events_after_messages( - self.native_navigation_retirement_events(&state, emit_network), + self.native_navigation_retirement_events(contents, navigation), ); + let context = self + .browser_context_by_browser_id_mut(contents.context()) + .expect("resolved Context"); + context.discard_target_navigation_projection(&target_id, &navigation); + if let Ok(release) = context + .page_targets + .get_mut(&target_id) + .expect("resolved Target") + .runtime_slot + .finish_navigation_without_document_projection(&navigation) + { + releases.push(release); + } } if let Some(owner) = owner { for release in releases { @@ -110,15 +107,42 @@ impl CdpConnection { pub(crate) fn native_navigation_retirement_events( &mut self, - pending: &crate::conn::PendingFetchNavigation, - emit_network: bool, + contents: WebContentsHandle, + navigation: NavigationId, ) -> Vec { - let state = &pending.navigation; - let mut events = if emit_network { - crate::domains::network::native_navigation_failure_events(self, state) - } else { - Vec::new() + // Retirement was observed after the earlier response read. Browser may + // have failed in between: consume its exact terminal response before + // spending publication cursors on a synthetic supersession result. + let response = self + .browser + .context_handle(contents.context()) + .and_then(|context| context.navigation_responses(contents)) + .ok() + .and_then(|responses| { + responses + .into_iter() + .find(|response| response.request.navigation == navigation) + }); + let mut events = response + .map(|response| self.project_native_navigation_network(&response, true)) + .unwrap_or_default(); + let Some((pending, emit_network)) = self + .browser_context_by_browser_id_mut(contents.context()) + .and_then(|context| { + let target = context + .target_id_for_web_contents(contents.id())? + .to_owned(); + context.take_failed_native_navigation(&target, navigation) + }) + else { + return events; }; + let state = &pending.navigation; + if emit_network { + events.extend(crate::domains::network::native_navigation_failure_events( + self, state, + )); + } if state.navigate_id.is_some() { let mut output = CommandOutputBuffer::default(); let interrupted = self diff --git a/moli-protocol/src/conn/browser_context/page_owner.rs b/moli-protocol/src/conn/browser_context/page_owner.rs index 16f56255c..87297d518 100644 --- a/moli-protocol/src/conn/browser_context/page_owner.rs +++ b/moli-protocol/src/conn/browser_context/page_owner.rs @@ -154,16 +154,11 @@ impl TargetSessionOwnerMut<'_> { true } - fn audits_output_snapshot(&mut self) -> Vec { - self.browser_context - .ingest_owner_page_observable_output_updates_for_target(&self.target_id); - self.runtime_slot_mut() - .inspector_issues() - .unwrap_or_default() - } - fn enable_audits(mut self) -> SessionOwnerAuditsEnableResult { - let storage = self.sync_audits_storage(); + // Concrete, exact-Document facts already own this replay storage. + // A cumulative renderer report can lag behind those FIFO facts and + // must never overwrite them when another session enables Audits. + let storage = self.mutate_target_owner_state(|state| state.audits_storage_state.clone()); let cursor = self.mutate_session_state_ref(|state| state.enable_audits_with_storage(&storage)); let replay = cursor.and_then(|cursor| { @@ -176,18 +171,6 @@ impl TargetSessionOwnerMut<'_> { SessionOwnerAuditsEnableResult::Handled { replay } } - fn sync_audits_storage( - &mut self, - ) -> crate::domains::audits_output_state::TargetAuditsStorageState { - let source_issues = self.audits_output_snapshot(); - self.mutate_target_owner_state(|owner_state| { - owner_state - .audits_storage_state - .ingest_source_issues(&source_issues); - owner_state.audits_storage_state.clone() - }) - } - fn disable_audits(mut self) -> bool { self.mutate_session_state_ref(|state| state.disable_audits()); true diff --git a/moli-protocol/src/conn/browser_context/target_session_owner.rs b/moli-protocol/src/conn/browser_context/target_session_owner.rs index 5a26b8ba8..580db20dd 100644 --- a/moli-protocol/src/conn/browser_context/target_session_owner.rs +++ b/moli-protocol/src/conn/browser_context/target_session_owner.rs @@ -726,10 +726,6 @@ impl<'a> TargetSessionOwnerMut<'a> { .await } - pub(super) fn runtime_slot_mut(&mut self) -> &mut TargetRuntimeSlot { - &mut self.target_mut().runtime_slot - } - pub(super) fn into_runtime_slot_mut(self) -> &'a mut TargetRuntimeSlot { &mut self.into_target_mut().runtime_slot } @@ -1468,6 +1464,7 @@ impl CdpConnection { self.renderer_page_residence_identity_for_owner(&owner) } + #[cfg(test)] pub(crate) fn renderer_page_residence_identity_for_owner( &self, owner: &CommandOwnerScope, diff --git a/moli-protocol/src/conn/downloads/lifecycle_tests.rs b/moli-protocol/src/conn/downloads/lifecycle_tests.rs index 191d4a3c9..e49e786a8 100644 --- a/moli-protocol/src/conn/downloads/lifecycle_tests.rs +++ b/moli-protocol/src/conn/downloads/lifecycle_tests.rs @@ -403,7 +403,6 @@ async fn pending_native_download( let waiter = conn .start_native_navigation_fixture_for_test( &CommandOwnerScope::for_session("SID-source"), - "LOADER-source", NavigationRequestInterception::new( Url::parse("https://source.test/report.txt").unwrap(), "GET".into(), diff --git a/moli-protocol/src/conn/navigation_fixtures.rs b/moli-protocol/src/conn/navigation_fixtures.rs index 16b4fc12f..e8947e3db 100644 --- a/moli-protocol/src/conn/navigation_fixtures.rs +++ b/moli-protocol/src/conn/navigation_fixtures.rs @@ -44,7 +44,6 @@ impl CdpConnection { let owner = CommandOwnerScope::capture(self, None); let waiter = self.start_native_navigation_fixture_for_test( &owner, - crate::domains::page::LOADER_ID, NavigationRequestInterception::new( url, method, @@ -86,7 +85,6 @@ impl CdpConnection { pub(crate) fn start_native_navigation_fixture_for_test( &mut self, owner: &CommandOwnerScope, - loader_id: &str, request: NavigationRequestInterception, decision: NavigationDecision, ) -> Result { @@ -103,9 +101,21 @@ impl CdpConnection { let admitted_request = request.clone(); let waiter = native.navigate_document(contents, request)?; let request = waiter.request(); - self.browser_context_by_id_mut(&context_id) - .expect("resolved fixture context") - .observe_navigation_fixture_for_test(&target_id, request, loader_id); + // Use the production projection allocator: hand-written fixture IDs + // can alias the next real navigation and falsely satisfy its fences. + let mut allocator = std::mem::take(&mut self.network_request_id_allocator); + let context = self + .browser_context_by_id_mut(&context_id) + .expect("resolved fixture context"); + assert!(context.observe_target_navigation_started(&target_id, request)); + context + .project_document_navigation_loader_for_target( + &target_id, + Some(request.navigation), + &mut allocator, + ) + .expect("fixture navigation must have a unique projection identity"); + self.network_request_id_allocator = allocator; let paused = native .navigation_decision(contents)? .filter(|paused| paused.permit.navigation() == request.navigation) diff --git a/moli-protocol/src/conn/runtime_eval.rs b/moli-protocol/src/conn/runtime_eval.rs index a645080b8..d4d2c72dd 100644 --- a/moli-protocol/src/conn/runtime_eval.rs +++ b/moli-protocol/src/conn/runtime_eval.rs @@ -12,7 +12,6 @@ use crate::devtools_runtime::{ use moli_core::{ RendererOutputFence, RendererRuntimeCommandCausalIdentity, RendererRuntimeInspectorResponseSender, - browser::BrowserContextId, page::{ DocumentNodeObjectSnapshot, DocumentNodeRuntimeObjectResolution, MAX_INSPECTOR_PROTOCOL_VALUE_DEPTH, RendererAgentAttachmentId, RendererCommandTurnOutput, @@ -20,6 +19,7 @@ use moli_core::{ RendererInspectorCommandRoute, RendererRuntimeCommandOutput, RendererRuntimeInspectorMessage, RendererRuntimeRealmInfo, }, + runtime::{RendererWorkerInspectionEndpoint, RendererWorkerInspectionTarget}, }; use crate::conn::state::{ @@ -206,24 +206,6 @@ enum RuntimeRemoteObjectOwnerIdentity { }, } -#[derive(Clone, Debug, Eq, PartialEq)] -struct SharedWorkerRuntimeTargetRoute { - browser_context: BrowserContextId, - worker: WorkerRuntimeTarget, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum WorkerRuntimeTarget { - Shared(SharedWorkerInstanceId), - Dedicated(u64), -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct ServiceWorkerRuntimeTargetRoute { - browser_context: BrowserContextId, - version_id: u64, -} - enum BidiChannelListenerRoute { NotListener, Consumed, @@ -346,13 +328,13 @@ impl RuntimeProtocolResponseRoute { pub struct PendingSharedWorkerRuntimeProtocolMessageDispatch { session_id: Option, - pending: SharedWorkerRuntimeProtocolDispatchFuture, + pending: WorkerRuntimeProtocolDispatchFuture, response_route: RuntimeProtocolResponseRoute, } pub struct PendingServiceWorkerRuntimeProtocolMessageDispatch { session_id: Option, - pending: ServiceWorkerRuntimeProtocolDispatchFuture, + pending: WorkerRuntimeProtocolDispatchFuture, response_route: RuntimeProtocolResponseRoute, } @@ -506,11 +488,44 @@ pub struct CompletedRuntimeChildDefaultContextLookupDispatch { completion: moli_core::page::CompletedPageCommand, } -type SharedWorkerRuntimeProtocolDispatchFuture = - Pin>>>; -type ServiceWorkerRuntimeProtocolDispatchFuture = +type WorkerRuntimeProtocolDispatchFuture = Pin>>>; +fn worker_runtime_protocol_dispatch( + endpoint: Result, + session_id: Option, + raw_json: String, + response: Option, + delivery: RendererInspectorResponseDelivery, +) -> WorkerRuntimeProtocolDispatchFuture { + // Freeze both the concrete worker and the response stream before yielding. + match (response, delivery) { + (response, RendererInspectorResponseDelivery::AdapterReply) => Box::pin(async move { + endpoint? + .dispatch_protocol_message(session_id, raw_json, response) + .await + .map(CompletedWorkerRuntimeProtocolDispatch::adapter_reply) + }), + (Some(response), RendererInspectorResponseDelivery::SessionSink) => { + let pending = endpoint.and_then(|endpoint| { + endpoint.dispatch_protocol_message_to_session( + session_id.ok_or_else(|| "UnknownSession".to_owned())?, + raw_json, + response, + ) + }); + Box::pin(async move { + Ok(CompletedWorkerRuntimeProtocolDispatch::devtools_session( + pending?.await, + )) + }) + } + (None, RendererInspectorResponseDelivery::SessionSink) => { + Box::pin(async { Err("SessionResponseSenderMissing".to_owned()) }) + } + } +} + #[derive(Clone, Debug)] struct RuntimeProtocolMessagePageRoute { browser_context_id: String, @@ -4352,91 +4367,86 @@ impl CdpConnection { } } - fn shared_worker_runtime_target_for_session( - &self, - session_id: Option<&str>, - ) -> Result { - let session_id = session_id.ok_or_else(|| "UnknownSession".to_owned())?; - let route = self - .session_route(Some(session_id)) - .ok_or_else(|| "UnknownSession".to_owned())?; - match route { - CdpSessionRoute::SharedWorkerTarget { - browser_context_id, - target_id, - } => { - let context = self - .browser_context_by_id(&browser_context_id) - .ok_or_else(|| "UnknownSession".to_owned())?; - let target = context - .shared_worker_target(&target_id) - .ok_or_else(|| "UnknownSession".to_owned())?; - Ok(SharedWorkerRuntimeTargetRoute { - browser_context: context.browser_context_id(), - worker: WorkerRuntimeTarget::Shared(target.renderer_instance_id), - }) - } - CdpSessionRoute::DedicatedWorkerTarget { - browser_context_id, - target_id, - } => { - let context = self - .browser_context_by_id(&browser_context_id) - .ok_or_else(|| "UnknownSession".to_owned())?; - let target = context - .dedicated_worker_target(&target_id) - .ok_or_else(|| "UnknownSession".to_owned())?; - Ok(SharedWorkerRuntimeTargetRoute { - browser_context: context.browser_context_id(), - worker: WorkerRuntimeTarget::Dedicated(target.renderer_instance_id), - }) - } - _ => Err("UnknownSession".to_owned()), - } - } - pub(crate) fn run_dedicated_worker_if_waiting_for_debugger_for_session( &mut self, session_id: Option<&str>, ) -> Result { let session_id = session_id.ok_or_else(|| "UnknownSession".to_owned())?; - let route = self.shared_worker_runtime_target_for_session(Some(session_id))?; - let WorkerRuntimeTarget::Dedicated(instance_id) = route.worker else { - return Ok(false); - }; - if let Some(target) = self.dedicated_worker_target_for_session_mut(Some(session_id)) { - target.discard_main_script_network_replay_for(session_id); - } - let browser_context = self - .browser_context_by_browser_id(route.browser_context) + let route = self + .session_route(Some(session_id)) .ok_or_else(|| "UnknownSession".to_owned())?; - Ok(browser_context.run_dedicated_worker_if_waiting_for_debugger(instance_id)) - } - - fn service_worker_runtime_target_for_session( - &self, - session_id: Option<&str>, - ) -> Result { - let session_id = session_id.ok_or_else(|| "UnknownSession".to_owned())?; - let CdpSessionRoute::ServiceWorkerTarget { + let CdpSessionRoute::DedicatedWorkerTarget { browser_context_id, target_id, - } = self - .session_route(Some(session_id)) - .ok_or_else(|| "UnknownSession".to_owned())? + } = route else { - return Err("UnknownSession".to_owned()); + return if matches!(route, CdpSessionRoute::SharedWorkerTarget { .. }) { + Ok(false) + } else { + Err("UnknownSession".to_owned()) + }; }; let context = self - .browser_context_by_id(&browser_context_id) + .browser_context_by_id_mut(&browser_context_id) .ok_or_else(|| "UnknownSession".to_owned())?; let target = context - .service_worker_target(&target_id) + .dedicated_worker_target_mut(&target_id) .ok_or_else(|| "UnknownSession".to_owned())?; - Ok(ServiceWorkerRuntimeTargetRoute { - browser_context: context.browser_context_id(), - version_id: target.renderer_version_id, - }) + let instance_id = target.renderer_instance_id; + target.discard_main_script_network_replay_for(session_id); + Ok(context.run_dedicated_worker_if_waiting_for_debugger(instance_id)) + } + + pub(crate) fn worker_inspection_endpoint_for_session( + &self, + session_id: Option<&str>, + ) -> Result { + let route = self + .session_route(session_id) + .ok_or_else(|| "UnknownSession".to_owned())?; + let browser_context_id = match &route { + CdpSessionRoute::SharedWorkerTarget { + browser_context_id, .. + } + | CdpSessionRoute::DedicatedWorkerTarget { + browser_context_id, .. + } + | CdpSessionRoute::ServiceWorkerTarget { + browser_context_id, .. + } => browser_context_id, + _ => return Err("UnknownSession".to_owned()), + }; + let context = self + .browser_context_by_id(browser_context_id) + .ok_or_else(|| "UnknownSession".to_owned())?; + let worker = match &route { + CdpSessionRoute::SharedWorkerTarget { target_id, .. } => { + RendererWorkerInspectionTarget::Shared( + context + .shared_worker_target(target_id) + .ok_or_else(|| "UnknownSession".to_owned())? + .renderer_instance_id, + ) + } + CdpSessionRoute::DedicatedWorkerTarget { target_id, .. } => { + RendererWorkerInspectionTarget::Dedicated( + context + .dedicated_worker_target(target_id) + .ok_or_else(|| "UnknownSession".to_owned())? + .renderer_instance_id, + ) + } + CdpSessionRoute::ServiceWorkerTarget { target_id, .. } => context + .service_worker_target(target_id) + .ok_or_else(|| "UnknownSession".to_owned())? + .inspection_target() + .ok_or_else(|| "ServiceWorkerRuntimeUnavailable".to_owned())?, + _ => unreachable!("validated worker route"), + }; + let unavailable = worker.unavailable_message(); + context + .worker_inspection_endpoint(worker) + .ok_or_else(|| unavailable.to_owned()) } pub(crate) fn start_shared_worker_runtime_protocol_message_for_session( @@ -4460,7 +4470,15 @@ impl CdpConnection { descriptor: RendererCommandDescriptor, command_id: u64, ) -> Result { - self.shared_worker_runtime_target_for_session(session_id)?; + if !matches!( + self.session_route(session_id), + Some( + CdpSessionRoute::SharedWorkerTarget { .. } + | CdpSessionRoute::DedicatedWorkerTarget { .. } + ) + ) { + return Err("UnknownSession".to_owned()); + } let (_correlation, raw_json, response_sender, response_route) = self.prepare_renderer_call_for_session_owner(session_id, descriptor, command_id, None)?; self.start_shared_worker_runtime_protocol_message_for_session_with_optional_deferred_response( @@ -4478,119 +4496,13 @@ impl CdpConnection { response_sender: Option, response_route: RuntimeProtocolResponseRoute, ) -> Result { - let route = self.shared_worker_runtime_target_for_session(session_id)?; - let renderer_runtime = self - .browser_context_by_browser_id(route.browser_context) - .map(BrowserContext::worker_runtime_inspection_endpoint) - .ok_or_else(|| "UnknownSession".to_owned())?; - let worker = route.worker; - let inspector_session_id = session_id.map(str::to_owned); - let response_delivery = response_route.delivery(); - let pending: SharedWorkerRuntimeProtocolDispatchFuture = match ( - worker, + let pending = worker_runtime_protocol_dispatch( + self.worker_inspection_endpoint_for_session(session_id), + session_id.map(str::to_owned), + raw_json, response_sender, - response_delivery, - ) { - ( - WorkerRuntimeTarget::Shared(instance_id), - Some(response), - RendererInspectorResponseDelivery::AdapterReply, - ) => Box::pin(async move { - renderer_runtime - .dispatch_shared_worker_runtime_protocol_message_with_deferred_response( - instance_id, - inspector_session_id, - raw_json, - response, - ) - .await - .map(CompletedWorkerRuntimeProtocolDispatch::adapter_reply) - }), - ( - WorkerRuntimeTarget::Shared(instance_id), - Some(response), - RendererInspectorResponseDelivery::SessionSink, - ) => { - let inspector_session_id = - inspector_session_id.ok_or_else(|| "UnknownSession".to_owned())?; - Box::pin(async move { - renderer_runtime - .dispatch_shared_worker_runtime_protocol_message_with_devtools_session_response( - instance_id, - inspector_session_id, - raw_json, - response, - ) - .await - .map(CompletedWorkerRuntimeProtocolDispatch::devtools_session) - }) - } - ( - WorkerRuntimeTarget::Shared(instance_id), - None, - RendererInspectorResponseDelivery::AdapterReply, - ) => Box::pin(async move { - renderer_runtime - .dispatch_shared_worker_runtime_protocol_message( - instance_id, - inspector_session_id, - raw_json, - ) - .await - .map(CompletedWorkerRuntimeProtocolDispatch::adapter_reply) - }), - ( - WorkerRuntimeTarget::Dedicated(instance_id), - Some(response), - RendererInspectorResponseDelivery::AdapterReply, - ) => Box::pin(async move { - renderer_runtime - .dispatch_dedicated_worker_runtime_protocol_message_with_deferred_response( - instance_id, - inspector_session_id, - raw_json, - response, - ) - .await - .map(CompletedWorkerRuntimeProtocolDispatch::adapter_reply) - }), - ( - WorkerRuntimeTarget::Dedicated(instance_id), - Some(response), - RendererInspectorResponseDelivery::SessionSink, - ) => { - let inspector_session_id = - inspector_session_id.ok_or_else(|| "UnknownSession".to_owned())?; - Box::pin(async move { - renderer_runtime - .dispatch_dedicated_worker_runtime_protocol_message_with_devtools_session_response( - instance_id, - inspector_session_id, - raw_json, - response, - ) - .await - .map(CompletedWorkerRuntimeProtocolDispatch::devtools_session) - }) - } - ( - WorkerRuntimeTarget::Dedicated(instance_id), - None, - RendererInspectorResponseDelivery::AdapterReply, - ) => Box::pin(async move { - renderer_runtime - .dispatch_dedicated_worker_runtime_protocol_message( - instance_id, - inspector_session_id, - raw_json, - ) - .await - .map(CompletedWorkerRuntimeProtocolDispatch::adapter_reply) - }), - (_, None, RendererInspectorResponseDelivery::SessionSink) => { - return Err("SessionResponseSenderMissing".to_owned()); - } - }; + response_route.delivery(), + ); Ok(PendingSharedWorkerRuntimeProtocolMessageDispatch { session_id: session_id.map(str::to_owned), pending, @@ -4695,7 +4607,8 @@ impl CdpConnection { descriptor: RendererCommandDescriptor, command_id: u64, ) -> Result { - self.service_worker_runtime_target_for_session(session_id)?; + self.service_worker_target_for_session(session_id) + .ok_or_else(|| "UnknownSession".to_owned())?; let (_correlation, raw_json, response_sender, response_route) = self.prepare_renderer_call_for_session_owner(session_id, descriptor, command_id, None)?; self.start_service_worker_runtime_protocol_message_for_session_with_optional_deferred_response( @@ -4713,53 +4626,13 @@ impl CdpConnection { response_sender: Option, response_route: RuntimeProtocolResponseRoute, ) -> Result { - let route = self.service_worker_runtime_target_for_session(session_id)?; - let renderer_runtime = self - .browser_context_by_browser_id(route.browser_context) - .map(BrowserContext::worker_runtime_inspection_endpoint) - .ok_or_else(|| "UnknownSession".to_owned())?; - let version_id = route.version_id; - let inspector_session_id = session_id.map(str::to_owned); - let response_delivery = response_route.delivery(); - let pending: ServiceWorkerRuntimeProtocolDispatchFuture = Box::pin(async move { - match (response_sender, response_delivery) { - (Some(response), RendererInspectorResponseDelivery::AdapterReply) => { - renderer_runtime - .dispatch_service_worker_runtime_protocol_message_with_deferred_response( - version_id, - inspector_session_id, - raw_json, - response, - ) - .await - .map(CompletedWorkerRuntimeProtocolDispatch::adapter_reply) - } - (Some(response), RendererInspectorResponseDelivery::SessionSink) => { - let inspector_session_id = - inspector_session_id.ok_or_else(|| "UnknownSession".to_owned())?; - renderer_runtime - .dispatch_service_worker_runtime_protocol_message_with_devtools_session_response( - version_id, - inspector_session_id, - raw_json, - response, - ) - .await - .map(CompletedWorkerRuntimeProtocolDispatch::devtools_session) - } - (None, RendererInspectorResponseDelivery::AdapterReply) => renderer_runtime - .dispatch_service_worker_runtime_protocol_message( - version_id, - inspector_session_id, - raw_json, - ) - .await - .map(CompletedWorkerRuntimeProtocolDispatch::adapter_reply), - (None, RendererInspectorResponseDelivery::SessionSink) => { - Err("SessionResponseSenderMissing".to_owned()) - } - } - }); + let pending = worker_runtime_protocol_dispatch( + self.worker_inspection_endpoint_for_session(session_id), + session_id.map(str::to_owned), + raw_json, + response_sender, + response_route.delivery(), + ); Ok(PendingServiceWorkerRuntimeProtocolMessageDispatch { session_id: session_id.map(str::to_owned), pending, @@ -8055,6 +7928,10 @@ mod tests { .as_ref() .unwrap() .target_document_id("TID-replay-exhaustion"); + let old_loader = ctx + .conn + .current_document_loader_id_for_session_owner(sessions[2]) + .unwrap(); let outgoing = ctx .conn .current_renderer_agent_attachment_id_for_owner(&owner) @@ -8118,6 +7995,10 @@ mod tests { let response = ctx.take_response_by_id(2000); assert!(response.get("error").is_none()); let loader = response["result"]["loaderId"].as_str().unwrap().to_owned(); + assert_ne!( + loader, old_loader, + "fixture and subsequent navigation must not alias a loader identity" + ); ctx.wait_until_scheduler_state("all session replay responses", |conn| { sessions.iter().all(|session| { (71..=73).all(|id| { diff --git a/moli-protocol/src/conn/state/browser_context.rs b/moli-protocol/src/conn/state/browser_context.rs index d86a248e4..3c08cc218 100644 --- a/moli-protocol/src/conn/state/browser_context.rs +++ b/moli-protocol/src/conn/state/browser_context.rs @@ -104,6 +104,8 @@ pub struct BrowserContext { pub(in crate::conn) automation_download_events_enabled: Option, pending_popup_javascript_dialogs: HashMap<(moli_core::browser::DocumentHandle, u64), Vec>, + /// Snapshot recovery cursor only; ordinary Worker FIFOs are independent. + pub(crate) worker_snapshot_sequence: Option, pub(crate) shared_worker_targets: BTreeMap, pub(crate) dedicated_worker_targets: BTreeMap, pub(crate) service_worker_targets: BTreeMap, @@ -329,6 +331,7 @@ impl BrowserContext { default_document_cookie_manager_surface: BrowserContextCookieManagerSurface::default(), automation_download_events_enabled: None, pending_popup_javascript_dialogs: HashMap::new(), + worker_snapshot_sequence: None, shared_worker_targets: BTreeMap::new(), dedicated_worker_targets: BTreeMap::new(), service_worker_targets: BTreeMap::new(), diff --git a/moli-protocol/src/conn/state/browser_context/page_slot.rs b/moli-protocol/src/conn/state/browser_context/page_slot.rs index 508c59f1b..f4efe9ac8 100644 --- a/moli-protocol/src/conn/state/browser_context/page_slot.rs +++ b/moli-protocol/src/conn/state/browser_context/page_slot.rs @@ -953,20 +953,6 @@ impl BrowserContext { .root_post_load_observation = None; } - #[cfg(test)] - pub(crate) fn observe_navigation_fixture_for_test( - &mut self, - target_id: &str, - request: moli_core::browser::NavigationRequest, - loader_id: &str, - ) { - assert!(self.observe_target_navigation_started(target_id, request)); - self.page_slot_for_target_mut(target_id) - .expect("registered fixture target") - .cdp_navigation_loaders - .push((request.navigation, loader_id.to_owned().into())); - } - #[cfg(any(test, feature = "test-support"))] pub(crate) fn begin_target_document_navigation( &mut self, diff --git a/moli-protocol/src/conn/state/browser_context/page_slot/document_host_tests.rs b/moli-protocol/src/conn/state/browser_context/page_slot/document_host_tests.rs index e1cc13cd6..d91481bb2 100644 --- a/moli-protocol/src/conn/state/browser_context/page_slot/document_host_tests.rs +++ b/moli-protocol/src/conn/state/browser_context/page_slot/document_host_tests.rs @@ -33,7 +33,6 @@ impl DerefMut for DocumentOwnerFixture { async fn prepare_navigation( owner: &mut DocumentOwnerFixture, - loader_id: &str, url: &str, ) -> BrowserNavigationWaiter { let command_owner = CommandOwnerScope::capture(&owner.conn, None); @@ -41,7 +40,6 @@ async fn prepare_navigation( .conn .start_native_navigation_fixture_for_test( &command_owner, - loader_id, moli_core::browser::web_contents::NavigationRequestInterception::new( url::Url::parse(url).unwrap(), "GET".into(), @@ -153,7 +151,7 @@ async fn context_with_document_and_runtime_config( runtime_config: moli_core::runtime::NavigationRuntimeConfig, ) -> DocumentOwnerFixture { let mut owner = empty_document_context_with_runtime_config(runtime_config); - let waiter = prepare_navigation(&mut owner, "LOADER-fixture", url).await; + let waiter = prepare_navigation(&mut owner, url).await; let (metadata, projection) = observe_navigation_commit(&mut owner, waiter).await.unwrap(); owner.initial_artifacts = Some(metadata.lifecycle.artifacts.clone()); let projection_fence = projection @@ -179,13 +177,12 @@ async fn context_with_document(url: &str) -> DocumentOwnerFixture { async fn loaded_navigation_commit_settles_document_history_and_navigation_together() { let mut owner = context_with_document("data:text/html,first").await; let old_document = owner.target_document_id(TARGET).unwrap(); - let loaded = prepare_navigation( - &mut owner, - "LOADER-atomic", - "data:text/html,second", - ) - .await; + let loaded = prepare_navigation(&mut owner, "data:text/html,second").await; let expected_document = loaded.request().document; + let expected_loader = owner + .current_document_loader_id_for_target(TARGET) + .unwrap() + .to_owned(); let (committed, projection) = observe_navigation_commit(&mut owner, loaded).await.unwrap(); let url = &committed.info.as_ref().unwrap().url; assert!(projection.unwrap().fence.is_ok()); @@ -201,7 +198,7 @@ async fn loaded_navigation_commit_settles_document_history_and_navigation_togeth ); assert_eq!( owner.committed_document_loader_id_for_target(TARGET), - Some("LOADER-atomic") + Some(expected_loader.as_str()) ); let (_, entries) = owner.target_navigation_history_snapshot(TARGET).unwrap(); assert_eq!(entries.last().unwrap().url, url.as_str()); @@ -238,12 +235,7 @@ async fn late_title_projection_does_not_rewind_native_history_or_cross_documents let (_, history) = owner.target_navigation_history_snapshot(TARGET).unwrap(); assert_eq!(history.last().unwrap().title, "native current"); - let next = prepare_navigation( - &mut owner, - "LOADER-title-replacement", - "data:text/html,replacement", - ) - .await; + let next = prepare_navigation(&mut owner, "data:text/html,replacement").await; observe_navigation_commit(&mut owner, next).await.unwrap(); assert_eq!(owner.commit_target_document_title(TARGET, &earlier), None); let (_, history) = owner.target_navigation_history_snapshot(TARGET).unwrap(); @@ -254,12 +246,7 @@ async fn late_title_projection_does_not_rewind_native_history_or_cross_documents #[tokio::test] async fn disappearing_agent_host_cannot_cancel_an_admitted_browser_commit() { let mut owner = empty_document_context(); - let loaded = prepare_navigation( - &mut owner, - "LOADER-native", - "data:text/html,native", - ) - .await; + let loaded = prepare_navigation(&mut owner, "data:text/html,native").await; let request = loaded.request(); let navigation = request.navigation; let contents = request.web_contents; @@ -419,16 +406,15 @@ async fn disappearing_agent_host_cannot_cancel_an_admitted_browser_commit() { async fn creation_projection_cannot_rewind_native_progress_or_retarget_a_replacement() { use crate::conn::CommittedDocumentLifecycle; let mut owner = empty_document_context(); - let loaded = prepare_navigation( - &mut owner, - "LOADER-native", - "data:text/html,native", - ) - .await; + let loaded = prepare_navigation(&mut owner, "data:text/html,native").await; let navigation = loaded.request().navigation; let renderer_page = prepared_renderer(&owner, &loaded); let (commit, _) = observe_navigation_commit(&mut owner, loaded).await.unwrap(); let artifacts = commit.lifecycle.artifacts.clone(); + let loader = owner + .committed_document_loader_id_for_target(TARGET) + .unwrap() + .to_owned(); let snapshot = artifacts.lifecycle_snapshot; let loaded_snapshot = owner .renderer_document_lifecycle_authoritative_snapshot_for_target(TARGET) @@ -469,7 +455,7 @@ async fn creation_projection_cannot_rewind_native_progress_or_retarget_a_replace }, Some(navigation), TARGET.into(), - "LOADER-native".into(), + loader.clone(), ); assert_eq!(projected, artifacts.initial_lifecycle_events); assert_eq!( @@ -527,7 +513,7 @@ async fn creation_projection_cannot_rewind_native_progress_or_retarget_a_replace native ); - let loaded = prepare_navigation(&mut owner, "LOADER-next", "data:text/html,next").await; + let loaded = prepare_navigation(&mut owner, "data:text/html,next").await; observe_navigation_commit(&mut owner, loaded).await.unwrap(); let next_snapshot = owner .renderer_document_lifecycle_authoritative_snapshot_for_target(TARGET) @@ -548,7 +534,7 @@ async fn creation_projection_cannot_rewind_native_progress_or_retarget_a_replace commit.lifecycle.clone(), Some(navigation), TARGET.into(), - "LOADER-native".into(), + loader, ) .is_empty() ); @@ -567,12 +553,7 @@ async fn creation_projection_cannot_rewind_native_progress_or_retarget_a_replace async fn failed_inspection_projection_cannot_veto_browser_document_commit() { let mut owner = context_with_document("data:text/html,first").await; let previous_document = owner.target_document_id(TARGET).unwrap(); - let loaded = prepare_navigation( - &mut owner, - "LOADER-projection", - "data:text/html,committed", - ) - .await; + let loaded = prepare_navigation(&mut owner, "data:text/html,committed").await; // Fail only the DevTools projection. The Browser WebContents and its pending // navigation remain live and must not require that projection's approval. owner @@ -627,12 +608,7 @@ async fn committed_occurrence_retains_the_previous_document_output_projection() .renderer_document_lifecycle_binding_for_target(TARGET) .is_some() ); - let loaded = prepare_navigation( - &mut owner, - "LOADER-current", - "data:text/html,current", - ) - .await; + let loaded = prepare_navigation(&mut owner, "data:text/html,current").await; let current_renderer = prepared_renderer(&owner, &loaded); observe_navigation_commit(&mut owner, loaded).await.unwrap(); let current_document = owner.target_document_id(TARGET).unwrap(); @@ -700,12 +676,7 @@ async fn old_network_ingress_survives_native_commit_before_projection() { .is_some() ); - let waiter = prepare_navigation( - &mut owner, - "LOADER-successor", - "data:text/html,successor", - ) - .await; + let waiter = prepare_navigation(&mut owner, "data:text/html,successor").await; // Deliberately finish the Browser transaction without projecting its commit. // The old renderer has closed; its final FIFO facts still belong to the // observer's old request correlations, not to the successor Document. @@ -795,10 +766,7 @@ async fn old_network_ingress_survives_native_commit_before_projection() { #[tokio::test] async fn inspection_configuration_failure_cannot_roll_back_a_committed_browser_document() { let mut owner = context_with_document("data:text/html,first").await; - let loaded = prepare_navigation( - &mut owner, - "LOADER-restore", - "data:text/html,committed", + let loaded = prepare_navigation(&mut owner, "data:text/html,committed", ) .await; let (committed, projection) = observe_navigation_commit(&mut owner, loaded).await.unwrap(); @@ -856,12 +824,7 @@ async fn rejected_browser_candidate_cannot_rotate_inspection_or_document_project .unwrap() .runtime_slot .current_renderer_attachment(); - let loaded = prepare_navigation( - &mut owner, - "LOADER-stale", - "data:text/html,stale", - ) - .await; + let loaded = prepare_navigation(&mut owner, "data:text/html,stale").await; let request = loaded.request(); let stale_permit = owner .browser_context @@ -869,8 +832,7 @@ async fn rejected_browser_candidate_cannot_rotate_inspection_or_document_project .unwrap() .unwrap() .permit; - let replacement = - prepare_navigation(&mut owner, "LOADER-current", "data:text/html,current").await; + let replacement = prepare_navigation(&mut owner, "data:text/html,current").await; let current = replacement.request().navigation; let history = owner.target_navigation_history_snapshot(TARGET).unwrap(); let target_url = owner @@ -921,12 +883,7 @@ async fn document_replacement_updates_inspection_binding_with_physical_page() { .runtime_slot .current_renderer_attachment() .unwrap(); - let loaded = prepare_navigation( - &mut owner, - "LOADER-second", - "data:text/html,second", - ) - .await; + let loaded = prepare_navigation(&mut owner, "data:text/html,second").await; observe_navigation_commit(&mut owner, loaded).await.unwrap(); let attachment = owner .active_page_target() @@ -1018,12 +975,7 @@ async fn page_with_installed_dialog_for_test() -> ( #[tokio::test] async fn document_replacement_dismisses_dialog_without_protocol_session_cleanup() { let (mut owner, completion) = page_with_installed_dialog_for_test().await; - let loaded = prepare_navigation( - &mut owner, - "LOADER-replacement", - "data:text/html,

replacement

", - ) - .await; + let loaded = prepare_navigation(&mut owner, "data:text/html,

replacement

").await; observe_navigation_commit(&mut owner, loaded).await.unwrap(); assert!( @@ -1328,12 +1280,8 @@ async fn document_policy_completion_rejects_replacement_document() { .wait() .await; - let loaded = prepare_navigation( - &mut owner, - "LOADER-replacement", - "data:text/html,

replacement policy owner

", - ) - .await; + let loaded = + prepare_navigation(&mut owner, "data:text/html,

replacement policy owner

").await; observe_navigation_commit(&mut owner, loaded).await.unwrap(); let replacement = owner.document_handle_for_target(TARGET).unwrap(); assert_ne!(replacement, document); @@ -1395,7 +1343,6 @@ async fn document_native_command_completions_reject_replacement_document() { let loaded = prepare_navigation( &mut owner, - "LOADER-replacement", "data:text/html,

replacement native command owner

", ) .await; @@ -1513,7 +1460,6 @@ async fn document_replacement_preserves_stable_page_engine_history_and_storage() let loaded = prepare_navigation( &mut owner, - "second-loader", "data:text/html,second

second

", ) .await; @@ -1590,7 +1536,7 @@ async fn web_contents_owns_live_document_and_navigation_after_protocol_residence let observer = owner.document_lifetime_observer_for_target(TARGET).unwrap(); let handle = owner.web_contents_handle_for_target(TARGET).unwrap(); let (stable_id, frame_id) = owner.browser_context.web_contents_identity(handle).unwrap(); - let pending = prepare_navigation(&mut owner, "pending-loader", "data:text/html,pending").await; + let pending = prepare_navigation(&mut owner, "data:text/html,pending").await; let navigation = pending.request().navigation; let cancellation = owner .document_navigation_cancellation_handle_for_target(TARGET, &navigation) @@ -1671,7 +1617,7 @@ async fn replacement_retires_document_identity_lifecycle_and_lifetime_together() // Moving the whole Context or failing a pending navigation must not retire // the current Document. Its Page/lifecycle/identity move as one object. let mut moved = owner; - let failed = prepare_navigation(&mut moved, "failed-loader", "data:text/html,canceled").await; + let failed = prepare_navigation(&mut moved, "data:text/html,canceled").await; let failed_navigation = failed.request().navigation; let before = moved.renderer_document_lifecycle_authoritative_snapshot_for_target(TARGET); assert!( @@ -1686,8 +1632,7 @@ async fn replacement_retires_document_identity_lifecycle_and_lifetime_together() let mut context = Context::from_waker(Waker::noop()); assert_eq!(first_wait.as_mut().poll(&mut context), Poll::Pending); - let loaded = - prepare_navigation(&mut moved, "second-loader", "data:text/html,

second

").await; + let loaded = prepare_navigation(&mut moved, "data:text/html,

second

").await; let navigation = loaded.request().navigation; let reserved_id = loaded.request().document; let second_renderer = prepared_renderer(&moved, &loaded); @@ -1789,12 +1734,7 @@ async fn discarded_prepared_candidate_preserves_current_document_until_owner_los .unwrap(); let first_id = owner.target_document_id(TARGET); let observer = owner.document_lifetime_observer_for_target(TARGET).unwrap(); - let candidate = prepare_navigation( - &mut owner, - "candidate-loader", - "data:text/html,

candidate

", - ) - .await; + let candidate = prepare_navigation(&mut owner, "data:text/html,

candidate

").await; let navigation = candidate.request().navigation; assert_ne!(prepared_renderer(&owner, &candidate), first_renderer); drop(candidate); diff --git a/moli-protocol/src/conn/state/browser_context/page_state/page_targets.rs b/moli-protocol/src/conn/state/browser_context/page_state/page_targets.rs index 7ea2aa0e8..61cf15c8e 100644 --- a/moli-protocol/src/conn/state/browser_context/page_state/page_targets.rs +++ b/moli-protocol/src/conn/state/browser_context/page_state/page_targets.rs @@ -727,8 +727,11 @@ impl BrowserContext { // binding observable so normal target retirement can detach it, while // best-effort registering the live renderer session before the attach // event is published. - let _ = - self.attach_dedicated_worker_inspector_session(renderer_instance_id, Some(session_id)); + if let Some(endpoint) = self.worker_inspection_endpoint( + moli_core::runtime::RendererWorkerInspectionTarget::Dedicated(renderer_instance_id), + ) { + endpoint.attach_session(Some(session_id)); + } true } diff --git a/moli-protocol/src/conn/state/browser_context/runtime_slot.rs b/moli-protocol/src/conn/state/browser_context/runtime_slot.rs index 266d8e6b3..8648014b8 100644 --- a/moli-protocol/src/conn/state/browser_context/runtime_slot.rs +++ b/moli-protocol/src/conn/state/browser_context/runtime_slot.rs @@ -464,12 +464,6 @@ impl TargetRuntimeSlot { .flatten() } - pub(crate) fn inspector_issues(&self) -> Option> { - self.current_renderer_inspection_binding() - .is_some() - .then(|| self.observable_queue.inspector_issues()) - } - pub(crate) fn ingest_observable_output_snapshot( &mut self, items: &[ScriptObservableOutputItem], diff --git a/moli-protocol/src/conn/state/browser_context/workers.rs b/moli-protocol/src/conn/state/browser_context/workers.rs index 115980db6..7344709f2 100644 --- a/moli-protocol/src/conn/state/browser_context/workers.rs +++ b/moli-protocol/src/conn/state/browser_context/workers.rs @@ -1,9 +1,19 @@ -use moli_core::{browser::ServiceWorkerCommand, runtime::RendererBrowserContextRuntime}; +use moli_core::{ + browser::ServiceWorkerCommand, + runtime::{RendererWorkerInspectionEndpoint, RendererWorkerInspectionTarget}, +}; use moli_shared_worker::SharedWorkerInstanceId; use super::BrowserContext; impl BrowserContext { + pub(crate) fn worker_inspection_endpoint( + &self, + target: RendererWorkerInspectionTarget, + ) -> Option { + self.browser_context.worker_inspection_endpoint(target) + } + pub(in crate::conn) fn execute_service_worker_command( &self, command: ServiceWorkerCommand, @@ -72,42 +82,6 @@ impl BrowserContext { self.browser_context.close_dedicated_worker(instance_id) } - pub(crate) fn attach_dedicated_worker_inspector_session( - &self, - instance_id: u64, - session_id: Option, - ) -> bool { - self.browser_context - .attach_dedicated_worker_inspector_session(instance_id, session_id) - } - - pub(crate) fn detach_shared_worker_inspector_session( - &self, - instance_id: SharedWorkerInstanceId, - session_id: Option, - ) -> bool { - self.browser_context - .detach_shared_worker_inspector_session(instance_id, session_id) - } - - pub(crate) fn detach_dedicated_worker_inspector_session( - &self, - instance_id: u64, - session_id: Option, - ) -> bool { - self.browser_context - .detach_dedicated_worker_inspector_session(instance_id, session_id) - } - - pub(crate) fn detach_service_worker_inspector_session( - &self, - version_id: u64, - session_id: Option, - ) -> bool { - self.browser_context - .detach_service_worker_inspector_session(version_id, session_id) - } - pub(in crate::conn) fn run_dedicated_worker_if_waiting_for_debugger( &self, instance_id: u64, @@ -120,12 +94,4 @@ impl BrowserContext { self.browser_context .run_service_worker_if_waiting_for_debugger(version_id) } - - /// Layered DevTools inspection is the only Protocol path allowed to retain - /// the cloneable renderer endpoint while a worker command is in flight. - pub(in crate::conn) fn worker_runtime_inspection_endpoint( - &self, - ) -> RendererBrowserContextRuntime { - self.browser_context.worker_runtime_inspection_endpoint() - } } diff --git a/moli-protocol/src/conn/state/dedicated_worker_target.rs b/moli-protocol/src/conn/state/dedicated_worker_target.rs index ecb9828ed..e431d2d33 100644 --- a/moli-protocol/src/conn/state/dedicated_worker_target.rs +++ b/moli-protocol/src/conn/state/dedicated_worker_target.rs @@ -1,9 +1,12 @@ use std::{ collections::BTreeSet, ops::{Deref, DerefMut}, + sync::Arc, }; -use moli_core::page::NavigationResponse; +use moli_core::page::{ + RendererDedicatedWorkerMainScript, RendererDedicatedWorkerMainScriptOutcome, +}; use moli_shared_worker::SharedWorkerInstanceId; use super::{SharedWorkerTargetState, TargetPageResidenceIdentity}; @@ -19,27 +22,13 @@ pub(crate) struct DedicatedWorkerTargetState { pub(crate) owner_page: TargetPageResidenceIdentity, pub(crate) owner_page_network_sessions: Vec>, pub(crate) inner: SharedWorkerTargetState, - main_script: Option, + main_script: Option>, delivered_main_script_sessions: BTreeSet, replayable_main_script_sessions: BTreeSet, defer_failed_load_destroy_until_debugger_resume: bool, renderer_destroyed_while_waiting_for_debugger: bool, } -#[derive(Clone, Debug)] -pub(crate) enum DedicatedWorkerMainScriptOutcome { - Loaded(Box), - Failed { - error_message: String, - response: Option>, - }, -} - -#[derive(Clone, Debug)] -pub(crate) struct DedicatedWorkerMainScriptSnapshot { - pub(crate) outcome: DedicatedWorkerMainScriptOutcome, -} - impl DedicatedWorkerTargetState { pub(crate) fn new( owner_page: TargetPageResidenceIdentity, @@ -71,21 +60,23 @@ impl DedicatedWorkerTargetState { pub(crate) fn record_main_script( &mut self, - script_url: String, - outcome: DedicatedWorkerMainScriptOutcome, + script: Arc, pause_failed_target_until_debugger_resume: bool, ) { self.defer_failed_load_destroy_until_debugger_resume = pause_failed_target_until_debugger_resume - && matches!(&outcome, DedicatedWorkerMainScriptOutcome::Failed { .. }); - self.inner.url = script_url.clone(); - self.main_script = Some(DedicatedWorkerMainScriptSnapshot { outcome }); + && matches!( + &script.outcome, + RendererDedicatedWorkerMainScriptOutcome::Failed { .. } + ); + self.inner.url = script.script_url.clone(); + self.main_script = Some(script); self.delivered_main_script_sessions.clear(); self.replayable_main_script_sessions.clear(); } - pub(crate) fn main_script(&self) -> Option<&DedicatedWorkerMainScriptSnapshot> { - self.main_script.as_ref() + pub(crate) fn main_script(&self) -> Option<&RendererDedicatedWorkerMainScript> { + self.main_script.as_deref() } pub(crate) fn main_script_was_delivered_to(&self, session_id: &str) -> bool { diff --git a/moli-protocol/src/conn/state/mod.rs b/moli-protocol/src/conn/state/mod.rs index 9d782afe7..52b3c2cb1 100644 --- a/moli-protocol/src/conn/state/mod.rs +++ b/moli-protocol/src/conn/state/mod.rs @@ -44,9 +44,7 @@ pub(crate) use devtools_renderer_channel::{ RendererAgentBinding, }; -pub(crate) use dedicated_worker_target::{ - DedicatedWorkerMainScriptOutcome, DedicatedWorkerMainScriptSnapshot, DedicatedWorkerTargetState, -}; +pub(crate) use dedicated_worker_target::DedicatedWorkerTargetState; #[cfg(test)] pub(crate) use devtools_session::DevToolsEmulationSessionState; pub(crate) use devtools_session::{ diff --git a/moli-protocol/src/conn/state/service_worker_target.rs b/moli-protocol/src/conn/state/service_worker_target.rs index fbc4713d8..9c65aca92 100644 --- a/moli-protocol/src/conn/state/service_worker_target.rs +++ b/moli-protocol/src/conn/state/service_worker_target.rs @@ -179,6 +179,20 @@ impl ServiceWorkerTargetState { self.runtime_execution_context_id } + pub(crate) fn inspection_target( + &self, + ) -> Option { + let ServiceWorkerTargetRunState::Live { run, .. } = &self.run_state else { + return None; + }; + Some( + moli_core::runtime::RendererWorkerInspectionTarget::Service { + version_id: self.renderer_version_id, + run: run.scope.renderer_run().clone(), + }, + ) + } + fn rebind_synthetic_runtime_snapshots(&mut self, synthetic_id: i64, real_id: i64) { for message in &mut self.console_messages { if message.execution_context_id == synthetic_id { @@ -289,13 +303,25 @@ impl ServiceWorkerTargetState { == Some(runtime.attachment()) } - /// Projects one exact renderer run into this protocol target. - /// - /// Runtime inspector output can arrive before the public `Started` event, - /// so the first run-specific fact may establish a `Starting` projection. - /// A different identity can be admitted only after the prior run moved to - /// an ordered retirement output. + pub(crate) fn active_renderer_run(&self) -> Option<&RendererServiceWorkerRunIdentity> { + match &self.run_state { + ServiceWorkerTargetRunState::Live { run, .. } => Some(run.scope.renderer_run()), + ServiceWorkerTargetRunState::Stopped { .. } => None, + } + } + pub(crate) fn observe_worker_run( + &self, + browser_context_id: &str, + renderer_run: RendererServiceWorkerRunIdentity, + ) -> Option { + let identity = self.current_run_identity(browser_context_id)?; + (identity.renderer_run() == &renderer_run).then_some(identity) + } + + /// Projects an explicit Browser-committed physical host installation. + /// Observations cannot introduce a run or replace one awaiting retirement. + pub(crate) fn mark_worker_starting( &mut self, browser_context_id: &str, renderer_run: RendererServiceWorkerRunIdentity, @@ -328,7 +354,7 @@ impl ServiceWorkerTargetState { browser_context_id: &str, renderer_run: RendererServiceWorkerRunIdentity, ) -> Option { - let run = self.observe_worker_run(browser_context_id, renderer_run)?; + let run = self.mark_worker_starting(browser_context_id, renderer_run)?; match &mut self.run_state { ServiceWorkerTargetRunState::Live { phase, .. } if *phase == ServiceWorkerTargetLiveRunPhase::Starting => @@ -349,7 +375,7 @@ impl ServiceWorkerTargetState { renderer_run: RendererServiceWorkerRunIdentity, _reason: &str, ) -> Option { - let identity = self.observe_worker_run(browser_context_id, renderer_run.clone())?; + let identity = self.mark_worker_starting(browser_context_id, renderer_run.clone())?; let live_state = std::mem::replace( &mut self.run_state, ServiceWorkerTargetRunState::Stopped { @@ -1405,7 +1431,7 @@ mod tests { fn a_different_renderer_identity_cannot_replace_a_live_protocol_run() { let (mut target, _) = target_with_started_run(); - let _ = target.observe_worker_run("BID-1", RendererServiceWorkerRunIdentity::fresh()); + let _ = target.mark_worker_starting("BID-1", RendererServiceWorkerRunIdentity::fresh()); } #[test] diff --git a/moli-protocol/src/conn/tests/resource_runtime.rs b/moli-protocol/src/conn/tests/resource_runtime.rs index 20699a6ed..9ccb62d37 100644 --- a/moli-protocol/src/conn/tests/resource_runtime.rs +++ b/moli-protocol/src/conn/tests/resource_runtime.rs @@ -239,7 +239,6 @@ async fn load_native_navigation_for_test( let waiter = conn .start_native_navigation_fixture_for_test( &owner, - crate::domains::page::LOADER_ID, moli_core::browser::web_contents::NavigationRequestInterception::new( Url::parse(url).unwrap(), "GET".into(), diff --git a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs index dbc42c4b8..47d0cf4ae 100644 --- a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs +++ b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs @@ -66,6 +66,30 @@ impl PreparedProtocolOutputs { Some(prepared) } + pub(in crate::domains::activity) fn from_browser_worker_lifecycle( + conn: &mut CdpConnection, + committed: moli_core::page::RendererCommittedWorkerLifecycle, + ) -> Self { + let mut prepared = Self::empty(); + let slot = match committed.lifecycle() { + moli_core::page::RendererWorkerLifecycle::Service(_) => { + crate::domains::activity::ProtocolOutputSlot::ServiceWorkerTargetLifecycle + } + moli_core::page::RendererWorkerLifecycle::SharedCreated(_) + | moli_core::page::RendererWorkerLifecycle::SharedDestroyed(_) => { + crate::domains::activity::ProtocolOutputSlot::SharedWorkerTargetLifecycle + } + moli_core::page::RendererWorkerLifecycle::DedicatedCreated(_) + | moli_core::page::RendererWorkerLifecycle::DedicatedScriptCompleted { .. } + | moli_core::page::RendererWorkerLifecycle::DedicatedDestroyed(_) => { + crate::domains::activity::ProtocolOutputSlot::DedicatedWorkerTargetLifecycle + } + }; + let outputs = crate::domains::target::worker_lifecycle_prepared_outputs(conn, committed); + outputs.append_to_target_lifecycle_output_sink_for_slots(&mut prepared, &[slot]); + prepared + } + pub(in crate::domains::activity) fn from_renderer_observation( conn: &mut CdpConnection, owner: &CommandOwnerScope, @@ -75,6 +99,37 @@ impl PreparedProtocolOutputs { ) -> Self { let mut prepared = Self::empty(); match observation { + RendererProtocolObservation::WorkerLifecycle(_) => { + unreachable!("Worker projection requires native commit acknowledgement first") + } + RendererProtocolObservation::DedicatedWorker(event) => { + crate::domains::target::dedicated_worker_observation_prepared_outputs( + conn, + owner, + event.clone(), + ) + .append_to_dedicated_worker_target_lifecycle_output_sink(&mut prepared); + } + RendererProtocolObservation::ServiceWorker(event) => { + if let Some((browser_context_id, _)) = conn.target_owner_identity_for_owner(owner) { + crate::domains::target::service_worker_observation_prepared_outputs( + conn, + browser_context_id, + event.clone(), + ) + .append_to_service_worker_target_lifecycle_output_sink(&mut prepared); + } + } + RendererProtocolObservation::SharedWorker(event) => { + if let Some((browser_context_id, _)) = conn.target_owner_identity_for_owner(owner) { + crate::domains::target::shared_worker_observation_prepared_outputs( + conn, + browser_context_id, + event.clone(), + ) + .append_to_shared_worker_target_lifecycle_output_sink(&mut prepared); + } + } RendererProtocolObservation::JavaScriptDialog(_) => { unreachable!("dialog projection must observe native admission first") } @@ -359,41 +414,6 @@ impl PreparedProtocolOutputs { .await .append_to_output_sink(&mut prepared); } - RendererOwnerAction::SharedWorkerTargetLifecycle(event) => { - if let Some((browser_context_id, _)) = conn.target_owner_identity_for_owner(owner) { - crate::domains::target:: - shared_worker_target_lifecycle_prepared_outputs_for_event( - conn, - browser_context_id, - event, - ) - .append_to_shared_worker_target_lifecycle_output_sink( - &mut prepared, - ); - } - } - RendererOwnerAction::ServiceWorkerTargetLifecycle(event) => { - if let Some((browser_context_id, _)) = conn.target_owner_identity_for_owner(owner) { - crate::domains::target:: - service_worker_target_lifecycle_prepared_outputs_for_event( - conn, - browser_context_id, - event, - ) - .append_to_service_worker_target_lifecycle_output_sink( - &mut prepared, - ); - } - } - RendererOwnerAction::DedicatedWorkerTargetLifecycle(event) => { - crate::domains::target:: - dedicated_worker_target_lifecycle_prepared_outputs_for_event( - conn, - owner, - event, - ) - .append_to_dedicated_worker_target_lifecycle_output_sink(&mut prepared); - } } prepared } @@ -535,7 +555,7 @@ impl PreparedProtocolOutputs { (!self.ordered_slots.is_empty()).then_some(self) } - /// Completes browser-owner cleanup for a canceled or superseded barrier + /// Completes owner/attachment cleanup for a canceled or superseded barrier /// without projecting protocol-only observations from the stale command. pub(in crate::domains::activity) async fn project_owner_actions_async( mut self, diff --git a/moli-protocol/src/domains/activity/output_ingress/renderer.rs b/moli-protocol/src/domains/activity/output_ingress/renderer.rs index 405ade60a..e9af65c09 100644 --- a/moli-protocol/src/domains/activity/output_ingress/renderer.rs +++ b/moli-protocol/src/domains/activity/output_ingress/renderer.rs @@ -213,6 +213,25 @@ async fn project_renderer_output_records_for_owner( continue; } match item { + RendererOutputItem::Observation( + moli_core::RendererProtocolObservation::WorkerLifecycle(observation), + ) => { + let Some(committed) = observation.committed().await else { + continue; + }; + let outputs = + PreparedProtocolOutputs::from_browser_worker_lifecycle(conn, committed); + order + .route_publication_outputs( + conn, + owner, + renderer_cause.as_ref(), + Some(cursor), + outputs, + command_context, + ) + .await; + } RendererOutputItem::Observation( moli_core::RendererProtocolObservation::JavaScriptDialog(opening), ) => { diff --git a/moli-protocol/src/domains/activity/output_slot.rs b/moli-protocol/src/domains/activity/output_slot.rs index 3f6e5c321..fce862bf5 100644 --- a/moli-protocol/src/domains/activity/output_slot.rs +++ b/moli-protocol/src/domains/activity/output_slot.rs @@ -5,8 +5,8 @@ use crate::conn::CdpConnection; use super::contextual_projection::ProtocolOutputProjectionContext; use super::output_payloads::{ProtocolOutputPayload, ProtocolOutputPayloads}; -/// Whether consuming prepared output is required for browser-owner progress -/// or only projects an already-settled fact to a protocol subscriber. +/// Whether consuming prepared output is required for owner/attachment cleanup +/// or only publishes an observation to a protocol subscriber. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(in crate::domains) enum ProtocolOutputDelivery { OwnerAction, @@ -67,6 +67,8 @@ impl ProtocolOutputSlot { | Self::Download | Self::JavascriptDialog | Self::Popup + // Native Worker lifetime already advanced, but canceled command + // barriers must still finish DevTools attachment/target cleanup. | Self::SharedWorkerTargetLifecycle | Self::ServiceWorkerTargetLifecycle | Self::DedicatedWorkerTargetLifecycle diff --git a/moli-protocol/src/domains/activity/publication_route.rs b/moli-protocol/src/domains/activity/publication_route.rs index cc1367f0b..e5b31cf45 100644 --- a/moli-protocol/src/domains/activity/publication_route.rs +++ b/moli-protocol/src/domains/activity/publication_route.rs @@ -129,11 +129,25 @@ impl RendererPublicationOwner { match self { Self::Unobserved => None, Self::BrowserContext { browser_context_id } => { + let runtime = match stream.residence() { + RendererOutputResidenceIdentity::SharedWorker { + browser_context_runtime_id, + .. + } + | RendererOutputResidenceIdentity::ServiceWorker { + browser_context_runtime_id, + .. + } => browser_context_runtime_id, + RendererOutputResidenceIdentity::Page { .. } => return None, + }; if !conn .browser_context .iter() .chain(conn.inactive_browser_contexts.iter()) - .any(|browser_context| browser_context.id == *browser_context_id) + .any(|browser_context| { + browser_context.id == *browser_context_id + && browser_context.routes_renderer_browser_context_runtime(runtime) + }) { return None; } diff --git a/moli-protocol/src/domains/audits.rs b/moli-protocol/src/domains/audits.rs index 5a5ea02f8..7e6f0b06b 100644 --- a/moli-protocol/src/domains/audits.rs +++ b/moli-protocol/src/domains/audits.rs @@ -76,8 +76,17 @@ fn unknown_session_output_plan() -> CommandOutputPlan { #[cfg(test)] mod tests { + use moli_core::page::{ + ContentSecurityPolicyIssueSnapshot, ContentSecurityPolicyViolationType, + InspectorIssueSnapshot, + }; use serde_json::json; + use crate::conn::CommandOwnerScope; + use crate::domains::observable_output::{ + ObservableOutputProjectionStep, ObservablePreparedOutputSlot, + inspector_issue_prepared_outputs, + }; use crate::testing::TestContext; async fn load_document(ctx: &mut TestContext, html: &str) { @@ -96,6 +105,145 @@ mod tests { .await; } + fn csp_issue() -> InspectorIssueSnapshot { + InspectorIssueSnapshot::ContentSecurityPolicy(ContentSecurityPolicyIssueSnapshot::new( + false, + "script-src-elem".to_owned(), + ContentSecurityPolicyViolationType::Inline, + )) + } + + #[tokio::test(flavor = "multi_thread")] + async fn reenable_replays_concrete_issue_before_lagging_report_snapshot() { + let mut ctx = TestContext::new(); + load_document(&mut ctx, "standards").await; + assert!( + ctx.conn + .browser_context + .as_mut() + .unwrap() + .assign_attached_session_to_target("TID-audits", "SID-audits-peer".to_owned()) + ); + for (id, method, session) in [ + (1, "Audits.enable", "SID-audits"), + (2, "Audits.enable", "SID-audits-peer"), + (3, "Audits.disable", "SID-audits-peer"), + ] { + ctx.process_async(json!({"id": id, "method": method, "sessionId": session})) + .await; + ctx.expect_result(id, json!({}), Some(session)); + } + + let owner = CommandOwnerScope::capture(&ctx.conn, Some("SID-audits")); + let source_document = ctx + .conn + .committed_renderer_document_binding_for_owner(&owner) + .unwrap() + .renderer_document_identity(); + // Admit the concrete FIFO fact while the cumulative Page report still + // describes the issue-free fixture. This is the production race's exact + // ordering, without depending on how quickly a renderer snapshot arrives. + let prepared = + inspector_issue_prepared_outputs(&mut ctx.conn, source_document, csp_issue(), &owner); + let mut slot = ObservablePreparedOutputSlot::from_outputs(prepared); + let mut live = Vec::new(); + slot.emit_activity_background_events_async( + ObservableOutputProjectionStep::Audits, + &mut ctx.conn, + &mut live, + owner.session_id(), + ) + .await; + assert_eq!(live.len(), 1); + let live = live.pop().unwrap().into_protocol_message(); + assert_eq!(live["sessionId"], json!("SID-audits")); + assert_eq!(live["method"], json!("Audits.issueAdded")); + + for (id, method) in [ + (4, "Audits.enable"), + (5, "Audits.enable"), + (6, "Audits.disable"), + (7, "Audits.enable"), + ] { + ctx.process_async(json!({ + "id": id, "method": method, "sessionId": "SID-audits-peer", + })) + .await; + if id == 4 || id == 7 { + let replay = ctx.take_one(); + assert_eq!(replay["method"], json!("Audits.issueAdded")); + assert_eq!(replay["sessionId"], json!("SID-audits-peer")); + assert_eq!(replay["params"], live["params"]); + } + ctx.expect_result(id, json!({}), Some("SID-audits-peer")); + assert!( + ctx.sent.is_empty(), + "unexpected replay or peer fanout: {:?}", + ctx.sent + ); + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn replacement_document_retires_stored_issues_and_rejects_late_facts() { + let mut ctx = TestContext::new(); + load_document(&mut ctx, "predecessor").await; + let owner = CommandOwnerScope::capture(&ctx.conn, Some("SID-audits")); + let source_document = ctx + .conn + .committed_renderer_document_binding_for_owner(&owner) + .unwrap() + .renderer_document_identity(); + assert!( + inspector_issue_prepared_outputs(&mut ctx.conn, source_document, csp_issue(), &owner) + .is_empty() + ); + assert_eq!( + ctx.conn + .target_owner_state_for_owner(&owner) + .unwrap() + .audits_storage_state + .issue_end(), + 1 + ); + + ctx.install_quiet_navigation_fixture_for_session_owner( + "data:text/html,replacement", + Some("SID-audits"), + ) + .await; + let owner = CommandOwnerScope::capture(&ctx.conn, Some("SID-audits")); + assert_ne!( + ctx.conn + .committed_renderer_document_binding_for_owner(&owner) + .unwrap() + .renderer_document_identity(), + source_document + ); + assert!( + inspector_issue_prepared_outputs(&mut ctx.conn, source_document, csp_issue(), &owner) + .is_empty() + ); + assert_eq!( + ctx.conn + .target_owner_state_for_owner(&owner) + .unwrap() + .audits_storage_state + .issue_end(), + 0 + ); + ctx.process_async(json!({ + "id": 1, "method": "Audits.enable", "sessionId": "SID-audits", + })) + .await; + ctx.expect_result(1, json!({}), Some("SID-audits")); + assert!( + ctx.sent.is_empty(), + "retired issues must not replay: {:?}", + ctx.sent + ); + } + #[tokio::test(flavor = "multi_thread")] async fn enable_replays_quirks_issue_before_response() { let mut ctx = TestContext::new(); diff --git a/moli-protocol/src/domains/audits_output_state.rs b/moli-protocol/src/domains/audits_output_state.rs index fe4f2c9df..cc78bfc26 100644 --- a/moli-protocol/src/domains/audits_output_state.rs +++ b/moli-protocol/src/domains/audits_output_state.rs @@ -91,57 +91,25 @@ impl TargetAuditsOutputCursor { #[derive(Clone, Debug, Default, Eq, PartialEq)] pub(crate) struct TargetAuditsStorageState { generation: u64, - source_issue_count: usize, - first_stored_issue_index: usize, + issue_end: usize, issues: VecDeque, } impl TargetAuditsStorageState { pub(crate) fn reset_for_new_document(&mut self) { - self.reset_storage(); - } - - fn reset_storage(&mut self) { self.generation = self.generation.wrapping_add(1); - self.source_issue_count = 0; - self.first_stored_issue_index = 0; + self.issue_end = 0; self.issues.clear(); } - pub(crate) fn ingest_source_issues(&mut self, source: &[InspectorIssueSnapshot]) { - let source_prefix_changed = self.source_issue_count > source.len() - || (self.source_issue_count > 0 - && self - .issues - .back() - .zip(source.get(self.source_issue_count - 1)) - .is_some_and(|(stored, current)| stored != current)); - if source_prefix_changed { - self.reset_storage(); - } - - for issue in source.iter().skip(self.source_issue_count) { - self.issues.push_back(issue.clone()); - self.source_issue_count += 1; - if self.issues.len() > MAX_STORED_INSPECTOR_ISSUES { - self.issues.pop_front(); - self.first_stored_issue_index += 1; - } - } - } - pub(crate) fn append_concrete_issue(&mut self, issue: InspectorIssueSnapshot) { - self.issues.push_back(issue); - self.source_issue_count = self - .source_issue_count + self.issue_end = self + .issue_end .checked_add(1) - .expect("Audits source issue count exhausted"); + .expect("Audits issue index exhausted"); + self.issues.push_back(issue); if self.issues.len() > MAX_STORED_INSPECTOR_ISSUES { self.issues.pop_front(); - self.first_stored_issue_index = self - .first_stored_issue_index - .checked_add(1) - .expect("Audits first stored issue index exhausted"); } } @@ -150,12 +118,12 @@ impl TargetAuditsStorageState { } pub(crate) fn first_issue_index(&self) -> usize { - self.first_stored_issue_index + self.issue_end - self.issues.len() } #[cfg(test)] pub(crate) fn issue_end(&self) -> usize { - self.source_issue_count + self.issue_end } pub(crate) fn pending_cursor_from( @@ -164,14 +132,14 @@ impl TargetAuditsStorageState { issue_index: usize, ) -> Option { let issue_start = if generation == self.generation { - issue_index.max(self.first_stored_issue_index) + issue_index.max(self.first_issue_index()) } else { - self.first_stored_issue_index + self.first_issue_index() }; - (issue_start < self.source_issue_count).then_some(TargetAuditsOutputCursor { + (issue_start < self.issue_end).then_some(TargetAuditsOutputCursor { generation: self.generation, issue_start, - issue_end: self.source_issue_count, + issue_end: self.issue_end, }) } @@ -180,13 +148,13 @@ impl TargetAuditsStorageState { cursor: TargetAuditsOutputCursor, ) -> Option> { if cursor.generation != self.generation - || cursor.issue_start < self.first_stored_issue_index + || cursor.issue_start < self.first_issue_index() || cursor.issue_start > cursor.issue_end - || cursor.issue_end > self.source_issue_count + || cursor.issue_end > self.issue_end { return None; } - let start = cursor.issue_start - self.first_stored_issue_index; + let start = cursor.issue_start - self.first_issue_index(); let len = cursor.issue_end - cursor.issue_start; Some(self.issues.iter().skip(start).take(len).cloned().collect()) } @@ -255,7 +223,8 @@ mod tests { #[test] fn enable_replays_storage_once_and_reenable_replays_current_storage() { let mut storage = TargetAuditsStorageState::default(); - storage.ingest_source_issues(&[issue(0), issue(1)]); + storage.append_concrete_issue(issue(0)); + storage.append_concrete_issue(issue(1)); let mut session = TargetAuditsSessionState::default(); let replay = session @@ -277,10 +246,9 @@ mod tests { #[test] fn storage_matches_chromium_issue_limit_and_advances_slow_sessions() { let mut storage = TargetAuditsStorageState::default(); - let source = (0..=MAX_STORED_INSPECTOR_ISSUES) - .map(issue) - .collect::>(); - storage.ingest_source_issues(&source); + for index in 0..=MAX_STORED_INSPECTOR_ISSUES { + storage.append_concrete_issue(issue(index)); + } assert_eq!(storage.first_issue_index(), 1); assert_eq!(storage.issue_end(), MAX_STORED_INSPECTOR_ISSUES + 1); @@ -293,13 +261,14 @@ mod tests { #[test] fn new_document_invalidates_session_cursor_without_disabling_domain() { let mut storage = TargetAuditsStorageState::default(); - storage.ingest_source_issues(&[issue(0)]); + storage.append_concrete_issue(issue(0)); let mut session = TargetAuditsSessionState::default(); let cursor = session.enable(&storage).unwrap(); session.mark_emitted(cursor); storage.reset_for_new_document(); - storage.ingest_source_issues(&[issue(2)]); + storage.append_concrete_issue(issue(2)); + assert!(storage.issues_for_cursor(cursor).is_none()); let cursor = session .pending_cursor(&storage) .expect("enabled session should observe the replacement document"); diff --git a/moli-protocol/src/domains/fetch/tests/navigation_control.rs b/moli-protocol/src/domains/fetch/tests/navigation_control.rs index 46080108a..cf77803bb 100644 --- a/moli-protocol/src/domains/fetch/tests/navigation_control.rs +++ b/moli-protocol/src/domains/fetch/tests/navigation_control.rs @@ -2,6 +2,133 @@ use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; use super::*; +#[tokio::test] +async fn native_navigation_retirement_recovers_failure_after_an_earlier_response_read() { + let mut ctx = TestContext::new(); + with_loaded_http_document( + &mut ctx, + "https://navigation.example/committed", + "SID-1", + "TID-1", + ) + .await; + for (id, method, params) in [ + (100, "Network.enable", json!({})), + ( + 102, + "Fetch.enable", + json!({"patterns": [{"resourceType": "Document", "requestStage": "Request"}]}), + ), + ] { + ctx.process_async( + json!({"id": id, "sessionId": "SID-1", "method": method, "params": params}), + ) + .await; + ctx.expect_result(id, json!({}), Some("SID-1")); + } + ctx.process_async(json!({ + "id": 103, "sessionId": "SID-1", "method": "Page.navigate", + "params": {"url": "http://example.test/rejected"} + })) + .await; + let pause = ctx + .wait_for_scheduler_message("held failing navigation", |event| { + event["method"] == "Fetch.requestPaused" + && event["params"]["request"]["url"] == "http://example.test/rejected" + }) + .await; + let (contents, decision) = ctx + .conn + .native_navigation_decision_for_target("TID-1") + .unwrap(); + let navigation = decision.permit.navigation(); + + // Freeze the real race boundary: response publication has read the still + // paused request, but the following retirement will observe its failure. + let earlier = ctx + .conn + .project_browser_navigation_responses(contents) + .await; + assert!( + earlier + .into_iter() + .all(|event| event.into_protocol_message()["id"] != 103) + ); + assert!(ctx.sent.iter().all(|event| event["id"] != 103)); + let owner = crate::conn::CommandOwnerScope::capture(&ctx.conn, Some("SID-1")); + let claimed = ctx + .conn + .take_pending_fetch_navigation_for_owner( + &owner, + Some("SID-1"), + pause["params"]["requestId"].as_str().unwrap(), + ) + .unwrap(); + let (_, mut events) = ctx.conn.subscribe_browser_events().unwrap(); + let (pending, _request) = claimed.into_parts(); + ctx.conn.update_native_navigation_dispatch(&pending); + assert!(ctx.conn.resolve_native_navigation_decision( + contents, + pending.navigation_permit, + moli_core::browser::NavigationDecision::Fail { + error_text: "net::ERR_BLOCKED_BY_CLIENT".into(), + }, + )); + tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + if matches!(events.recv().await.unwrap().event, + moli_core::browser::BrowserEvent::NavigationFailed { request, .. } + if request.web_contents == contents && request.navigation == navigation) + { + break; + } + } + }) + .await + .unwrap(); + + let retired = ctx + .conn + .native_navigation_retirement_events(contents, navigation) + .into_iter() + .map(crate::conn::BackgroundProtocolEvent::into_protocol_message) + .collect::>(); + let replies = retired + .iter() + .filter(|event| event["id"] == 103) + .collect::>(); + assert_eq!(replies.len(), 1, "{retired:?}"); + assert_eq!(replies[0]["sessionId"], "SID-1"); + assert_eq!( + replies[0]["error"]["message"], "net::ERR_BLOCKED_BY_CLIENT", + "{retired:?}" + ); + let failures = retired + .iter() + .filter(|event| event["method"] == "Network.loadingFailed") + .collect::>(); + assert_eq!(failures.len(), 1, "{retired:?}"); + assert_eq!( + failures[0]["params"]["requestId"], + pause["params"]["networkId"] + ); + assert_eq!( + failures[0]["params"]["errorText"], + "net::ERR_BLOCKED_BY_CLIENT" + ); + assert!( + ctx.conn + .native_navigation_retirement_events(contents, navigation) + .is_empty() + ); + assert!( + ctx.conn + .project_browser_navigation_responses(contents) + .await + .is_empty() + ); +} + async fn assert_stale_fetch_completion_preserves_winning_navigation(method: &str) { for loaded in [true, false] { let mut ctx = TestContext::new(); diff --git a/moli-protocol/src/domains/network/tests/session.rs b/moli-protocol/src/domains/network/tests/session.rs index 6be90574c..7b3ffc7fb 100644 --- a/moli-protocol/src/domains/network/tests/session.rs +++ b/moli-protocol/src/domains/network/tests/session.rs @@ -1109,9 +1109,13 @@ async fn main_document_navigation_broadcasts_to_attached_network_session() { "params": { "url": next_url } })) .await; + let loader_id = ctx + .conn + .current_document_loader_id_for_session_owner(Some("SID-primary")) + .unwrap(); ctx.expect_result( 7_260, - json!({ "frameId": "TID-1", "loaderId": LOADER_ID }), + json!({ "frameId": "TID-1", "loaderId": loader_id }), Some("SID-primary"), ); @@ -1123,7 +1127,7 @@ async fn main_document_navigation_broadcasts_to_attached_network_session() { messages.iter().any(|message| { message["sessionId"] == json!("SID-attached") && message["method"] == json!("Network.loadingFinished") - && message["params"]["requestId"] == json!(LOADER_ID) + && message["params"]["requestId"] == json!(loader_id) }) }, ) @@ -1139,19 +1143,19 @@ async fn main_document_navigation_broadcasts_to_attached_network_session() { && message["params"]["request"]["url"] == json!(next_url) }) .expect("attached document request event"); - assert_eq!(aux_request["params"]["requestId"], json!(LOADER_ID)); - assert_eq!(aux_request["params"]["loaderId"], json!(LOADER_ID)); + assert_eq!(aux_request["params"]["requestId"], json!(loader_id)); + assert_eq!(aux_request["params"]["loaderId"], json!(loader_id)); assert!(messages.iter().any(|message| { message["sessionId"] == json!("SID-attached") && message["method"] == json!("Network.responseReceived") - && message["params"]["requestId"] == json!(LOADER_ID) + && message["params"]["requestId"] == json!(loader_id) && message["params"]["type"] == json!("Document") && message["params"]["response"]["url"] == json!(next_url) })); assert!(messages.iter().any(|message| { message["sessionId"] == json!("SID-primary") && message["method"] == json!("Network.responseReceived") - && message["params"]["requestId"] == json!(LOADER_ID) + && message["params"]["requestId"] == json!(loader_id) && message["params"]["type"] == json!("Document") })); diff --git a/moli-protocol/src/domains/observable_output/runtime_queue.rs b/moli-protocol/src/domains/observable_output/runtime_queue.rs index 4b6e78ce4..e2b3a27ee 100644 --- a/moli-protocol/src/domains/observable_output/runtime_queue.rs +++ b/moli-protocol/src/domains/observable_output/runtime_queue.rs @@ -497,7 +497,6 @@ impl RuntimeObservableEmissionSnapshot { #[derive(Clone, Debug, Default, Eq, PartialEq)] pub(crate) struct TargetRuntimeObservableQueueState { observable_output_items: Vec, - source_inspector_issues: Vec, latest_source_tail: Option, source_tails_by_identity: HashMap<(String, DocumentId), TargetRuntimeObservableSourceOutput>, source_outputs: Vec, @@ -506,7 +505,6 @@ pub(crate) struct TargetRuntimeObservableQueueState { impl TargetRuntimeObservableQueueState { pub(crate) fn reset(&mut self) { self.observable_output_items.clear(); - self.source_inspector_issues.clear(); self.latest_source_tail = None; self.source_tails_by_identity.clear(); self.source_outputs.clear(); @@ -550,28 +548,6 @@ impl TargetRuntimeObservableQueueState { .count() } - pub(crate) fn inspector_issues(&self) -> Vec { - let report_issues = self - .observable_output_items - .iter() - .filter_map(|item| match item { - ScriptObservableOutputItem::InspectorIssue(issue) => Some((**issue).clone()), - ScriptObservableOutputItem::ConsoleMessage(_) - | ScriptObservableOutputItem::LifecycleError(_) => None, - }) - .collect::>(); - if report_issues.len() <= self.source_inspector_issues.len() - && report_issues - .iter() - .zip(&self.source_inspector_issues) - .all(|(report, source)| report == source) - { - self.source_inspector_issues.clone() - } else { - report_issues - } - } - #[cfg(test)] pub(crate) fn sync_source_from_renderer_snapshot( &mut self, @@ -590,7 +566,6 @@ impl TargetRuntimeObservableQueueState { document_id: DocumentId, renderer_source: &RendererRuntimeObservableSourceSummary, ) -> Option { - self.source_inspector_issues = renderer_source.inspector_issues().to_vec(); let source_items = source_items_from_renderer_runtime_source(renderer_source); let summary = TargetRuntimeObservableSourceSummary::from_source_items( renderer_source.default_execution_context_id(), diff --git a/moli-protocol/src/domains/page.rs b/moli-protocol/src/domains/page.rs index 661ae19f5..60f9a63c9 100644 --- a/moli-protocol/src/domains/page.rs +++ b/moli-protocol/src/domains/page.rs @@ -5189,14 +5189,17 @@ mod producer_tests { assert_eq!(message["sessionId"], json!("SID-location")); assert_eq!(message["params"]["frameId"], json!("TID-location")); - assert_eq!(message["params"]["loaderId"], json!(super::LOADER_ID)); + let loader_id = conn + .current_document_loader_id_for_session_owner(Some("SID-location")) + .unwrap(); + assert_eq!(message["params"]["loaderId"], json!(loader_id)); assert_eq!(message["params"]["url"], json!(target_url)); assert!(matches!( automation_event, Some(AutomationEvent::NavigationFrame(event)) if event.kind == NavigationFrameEventKind::StartedNavigating && event.frame_id.as_str() == "TID-location" - && event.loader_id.as_ref().map(|id| id.as_str()) == Some(super::LOADER_ID) + && event.loader_id.as_ref().map(|id| id.as_str()) == Some(loader_id.as_str()) && event.url == target_url )); } diff --git a/moli-protocol/src/domains/page/tests/lifecycle.rs b/moli-protocol/src/domains/page/tests/lifecycle.rs index 0a6c5af0b..3535fd30a 100644 --- a/moli-protocol/src/domains/page/tests/lifecycle.rs +++ b/moli-protocol/src/domains/page/tests/lifecycle.rs @@ -383,7 +383,6 @@ async fn lifecycle_events_enable_without_renderer_binding_does_not_synthesize_re .conn .start_native_navigation_fixture_for_test( &owner, - LOADER_ID, moli_core::browser::web_contents::NavigationRequestInterception::new( url::Url::parse("data:text/html,lifecycle").unwrap(), "GET".into(), diff --git a/moli-protocol/src/domains/runtime.rs b/moli-protocol/src/domains/runtime.rs index 606eb7711..b67a1fb86 100644 --- a/moli-protocol/src/domains/runtime.rs +++ b/moli-protocol/src/domains/runtime.rs @@ -55,75 +55,35 @@ pub(in crate::domains) async fn dispose_session_handler_async( Ok(()) } -/// Retires renderer-owned session state through its lifecycle endpoint. -pub(in crate::domains) async fn detach_session_inspector_async( - conn: &mut CdpConnection, +/// Resolve from the disposal plan, including an uncommitted attach that has no +/// session route yet. The caller keeps this exact endpoint across async cleanup. +pub(in crate::domains) fn worker_inspection_endpoint_for_disposal( + conn: &CdpConnection, plan: &SessionDisposalPlan, -) -> anyhow::Result<()> { - let session_id = plan.session_id(); - match plan.target() { - SessionDisposalTarget::PageTarget { .. } => { - conn.detach_runtime_inspector_session_for_session_owner(Some(session_id)) - .await - .map_err(anyhow::Error::msg)?; +) -> Option { + use moli_core::runtime::RendererWorkerInspectionTarget; + let context = conn.browser_context_by_id(plan.browser_context_id()?)?; + let worker = match plan.target() { + SessionDisposalTarget::SharedWorkerTarget { target_id, .. } => { + RendererWorkerInspectionTarget::Shared( + context + .shared_worker_target(target_id)? + .renderer_instance_id, + ) } - SessionDisposalTarget::SharedWorkerTarget { - browser_context_id, - target_id, - } => { - if let Some((browser_context, instance_id)) = conn - .browser_context_by_id(browser_context_id) - .and_then(|browser_context| { - browser_context - .shared_worker_target(target_id) - .map(|target| (browser_context, target.renderer_instance_id)) - }) - { - browser_context.detach_shared_worker_inspector_session( - instance_id, - Some(session_id.to_owned()), - ); - } + SessionDisposalTarget::DedicatedWorkerTarget { target_id, .. } => { + RendererWorkerInspectionTarget::Dedicated( + context + .dedicated_worker_target(target_id)? + .renderer_instance_id, + ) } - SessionDisposalTarget::DedicatedWorkerTarget { - browser_context_id, - target_id, - } => { - if let Some((browser_context, instance_id)) = conn - .browser_context_by_id(browser_context_id) - .and_then(|browser_context| { - browser_context - .dedicated_worker_target(target_id) - .map(|target| (browser_context, target.renderer_instance_id)) - }) - { - browser_context.detach_dedicated_worker_inspector_session( - instance_id, - Some(session_id.to_owned()), - ); - } - } - SessionDisposalTarget::ServiceWorkerTarget { - browser_context_id, - target_id, - } => { - if let Some((browser_context, version_id)) = conn - .browser_context_by_id(browser_context_id) - .and_then(|browser_context| { - browser_context - .service_worker_target(target_id) - .map(|target| (browser_context, target.renderer_version_id)) - }) - { - browser_context.detach_service_worker_inspector_session( - version_id, - Some(session_id.to_owned()), - ); - } - } - SessionDisposalTarget::Browser | SessionDisposalTarget::TabTarget { .. } => {} - } - Ok(()) + SessionDisposalTarget::ServiceWorkerTarget { target_id, .. } => context + .service_worker_target(target_id)? + .inspection_target()?, + _ => return None, + }; + context.worker_inspection_endpoint(worker) } pub(in crate::domains) use activity::{ diff --git a/moli-protocol/src/domains/runtime/tests/service_worker.rs b/moli-protocol/src/domains/runtime/tests/service_worker.rs index fd696cebd..7516feadf 100644 --- a/moli-protocol/src/domains/runtime/tests/service_worker.rs +++ b/moli-protocol/src/domains/runtime/tests/service_worker.rs @@ -623,8 +623,8 @@ async fn network_enable_on_service_worker_session_toggles_target_local_cursor() ); } -#[tokio::test(flavor = "multi_thread")] -async fn runtime_evaluate_on_real_service_worker_session_enters_worker_global() { +async fn real_service_worker_session() -> (TestContext, String, String, tokio::task::JoinHandle<()>) +{ async fn page() -> impl IntoResponse { ( [(CONTENT_TYPE.as_str(), "text/html")], @@ -719,6 +719,17 @@ self.addEventListener("activate", event => { .expect("service worker session id") .to_owned(); + ( + ctx, + service_worker_session_id, + format!("http://{addr}/"), + server, + ) +} + +#[tokio::test(flavor = "multi_thread")] +async fn runtime_evaluate_on_real_service_worker_session_enters_worker_global() { + let (mut ctx, service_worker_session_id, scope, server) = real_service_worker_session().await; ctx.process_async(json!({ "id": 86, "method": "Runtime.evaluate", @@ -732,12 +743,155 @@ self.addEventListener("activate", event => { let probe_response = take_response_by_id(&mut ctx, 86); assert_eq!( probe_response["result"]["result"]["value"], - json!(format!("service:true:http://{addr}/")) + json!(format!("service:true:{scope}")) ); server.abort(); } +async fn restart_service_worker_session(ctx: &mut TestContext, session_id: &str, scope: &str) { + let version_id = ctx + .conn + .service_worker_target_for_session(Some(session_id)) + .expect("attached worker") + .renderer_version_id + .to_string(); + ctx.process_async(json!({"id": 90, "method": "ServiceWorker.enable", "sessionId": "SID-page"})) + .await; + ctx.expect_result(90, json!({}), Some("SID-page")); + ctx.process_async( + json!({"id": 91, "method": "ServiceWorker.stopWorker", "sessionId": "SID-page", + "params": {"versionId": version_id}}), + ) + .await; + ctx.expect_result(91, json!({}), Some("SID-page")); + wait_until_message(ctx, Some("SID-page"), "the exact worker stops", |message| { + message["method"] == "ServiceWorker.workerVersionUpdated" + && message["params"]["versions"] + .as_array() + .is_some_and(|versions| { + versions.iter().any(|version| { + version["versionId"] == version_id && version["runningStatus"] == "stopped" + }) + }) + }) + .await; + ctx.sent.clear(); + ctx.process_async( + json!({"id": 92, "method": "ServiceWorker.startWorker", "sessionId": "SID-page", + "params": {"scopeURL": scope}}), + ) + .await; + ctx.expect_result(92, json!({}), Some("SID-page")); + wait_until_message( + ctx, + Some("SID-page"), + "the same version starts a new run", + |message| { + message["method"] == "ServiceWorker.workerVersionUpdated" + && message["params"]["versions"] + .as_array() + .is_some_and(|versions| { + versions.iter().any(|version| { + version["versionId"] == version_id + && version["runningStatus"] == "running" + }) + }) + }, + ) + .await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn pending_service_worker_inspection_never_dispatches_to_a_restarted_run() { + let (mut ctx, session_id, scope, server) = real_service_worker_session().await; + let old_target = ctx + .conn + .service_worker_target_for_session(Some(&session_id)) + .expect("worker") + .inspection_target() + .expect("live run"); + let old_endpoint = ctx + .conn + .worker_inspection_endpoint_for_session(Some(&session_id)) + .expect("live inspection endpoint"); + let disposal = ctx + .conn + .session_disposal_plan(&session_id) + .expect("disposal plan"); + let disposal_endpoint = + crate::domains::runtime::worker_inspection_endpoint_for_disposal(&ctx.conn, &disposal) + .expect("capture the worker before async session cleanup"); + let pending = ctx + .conn + .start_service_worker_runtime_protocol_message_for_session( + Some(&session_id), + json!({"id": 9001, "method": "Runtime.evaluate", "params": { + "expression": "globalThis.__staleWorkerDispatch = true", "returnByValue": true + }}) + .to_string(), + ) + .expect("capture the original worker before polling dispatch"); + + restart_service_worker_session(&mut ctx, &session_id, &scope).await; + assert!( + ctx.conn + .browser_context + .as_ref() + .expect("fixture context") + .worker_inspection_endpoint(old_target) + .is_none(), + "the old run identity must not bind the restarted version" + ); + assert!(!old_endpoint.attach_session(Some(session_id.clone()))); + assert!(!old_endpoint.detach_session(Some(session_id.clone()))); + assert!(!disposal_endpoint.detach_session(Some(session_id.clone()))); + + let stale_dispatch = pending.wait().await; + ctx.process_async(json!({"id": 93, "method": "Runtime.evaluate", "sessionId": session_id, + "params": {"expression": "globalThis.__staleWorkerDispatch === undefined", "returnByValue": true}})).await; + let probe = take_response_by_id(&mut ctx, 93); + server.abort(); + assert_eq!( + probe["result"]["result"]["value"], true, + "a captured inspection command must not mutate the replacement run: {probe}" + ); + assert!( + stale_dispatch.is_err(), + "the retired endpoint must reject the captured command" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn pending_service_worker_frontend_inspection_never_dispatches_to_a_restarted_run() { + let (mut ctx, session_id, scope, server) = real_service_worker_session().await; + let raw = json!({"id": 9002, "method": "Runtime.evaluate", "sessionId": session_id, + "params": {"expression": "globalThis.__staleWorkerDispatch = true", "returnByValue": true} + }) + .to_string(); + let pending = ctx + .conn + .try_start_pending_command_dispatch(&raw) + .expect("capture a real frontend Worker command before polling"); + restart_service_worker_session(&mut ctx, &session_id, &scope).await; + let completed = pending.wait().await; + assert!( + matches!( + ctx.conn.complete_pending_command_dispatch(completed).await, + CdpCommandTaskStep::Complete(_) + ), + "retired frontend inspection must complete without replay" + ); + ctx.process_async(json!({"id": 94, "method": "Runtime.evaluate", "sessionId": session_id, + "params": {"expression": "globalThis.__staleWorkerDispatch === undefined", "returnByValue": true}})).await; + let probe = take_response_by_id(&mut ctx, 94); + server.abort(); + assert_eq!( + probe["result"]["result"]["value"], true, + "the old frontend command must not mutate the replacement run: {probe}" + ); +} + #[tokio::test] async fn console_enable_on_service_worker_session_starts_at_current_target_cursor() { let mut ctx = TestContext::new(); diff --git a/moli-protocol/src/domains/target.rs b/moli-protocol/src/domains/target.rs index 91f3b600e..60d956ef8 100644 --- a/moli-protocol/src/domains/target.rs +++ b/moli-protocol/src/domains/target.rs @@ -45,11 +45,10 @@ pub(crate) use popup::{ pub(crate) use worker_target::retire_dedicated_worker_targets_for_replaced_page_async; pub(in crate::domains) use worker_target::{ TargetPreparedOutputSlot, dedicated_worker_main_script_network_replay_for_session, - dedicated_worker_target_lifecycle_prepared_outputs_for_event, - project_worker_target_output_async, + dedicated_worker_observation_prepared_outputs, project_worker_target_output_async, release_failed_dedicated_worker_target_after_debugger_resume, - service_worker_target_lifecycle_prepared_outputs_for_event, - shared_worker_target_lifecycle_prepared_outputs_for_event, + service_worker_observation_prepared_outputs, shared_worker_observation_prepared_outputs, + worker_lifecycle_prepared_outputs, }; /// Browser-owned auto-attach policies may observe browser-level targets. diff --git a/moli-protocol/src/domains/target/session_disposal.rs b/moli-protocol/src/domains/target/session_disposal.rs index 0bd4bc4bc..24a31bb19 100644 --- a/moli-protocol/src/domains/target/session_disposal.rs +++ b/moli-protocol/src/domains/target/session_disposal.rs @@ -55,6 +55,8 @@ async fn dispose_live_session_domains_async( plan: &SessionDisposalPlan, ) -> anyhow::Result> { let is_page = matches!(plan.target(), SessionDisposalTarget::PageTarget { .. }); + let worker_inspection = + crate::domains::runtime::worker_inspection_endpoint_for_disposal(conn, plan); let renderer_disposal = if is_page { crate::domains::runtime::fail_pending_session_calls( conn, @@ -62,7 +64,9 @@ async fn dispose_live_session_domains_async( protocol_events, plan.session_id(), ); - crate::domains::runtime::detach_session_inspector_async(conn, plan).await + conn.detach_runtime_inspector_session_for_session_owner(Some(plan.session_id())) + .await + .map_err(anyhow::Error::msg) } else { Ok(()) }; @@ -78,8 +82,8 @@ async fn dispose_live_session_domains_async( if let Some(error) = disposal.first_error() { return Err(anyhow::anyhow!("{error:#}")); } - if !is_page { - crate::domains::runtime::detach_session_inspector_async(conn, plan).await?; + if let Some(endpoint) = worker_inspection { + endpoint.detach_session(Some(plan.session_id().to_owned())); } Ok(disposal.into_renderer_output_predecessor()) } diff --git a/moli-protocol/src/domains/target/tests/tests_same_context_state.rs b/moli-protocol/src/domains/target/tests/tests_same_context_state.rs index 102fb904f..26aa32936 100644 --- a/moli-protocol/src/domains/target/tests/tests_same_context_state.rs +++ b/moli-protocol/src/domains/target/tests/tests_same_context_state.rs @@ -65,7 +65,7 @@ async fn same_context_targets_restore_their_own_script_execution_disabled_after_ consume_main_document_navigation_start(&mut ctx); ctx.expect_result( 104174, - json!({ "frameId": second_target_id, "loaderId": "LID-0000000001" }), + json!({ "frameId": second_target_id, "loaderId": "LID-0000000002" }), Some(&second_session_id), ); ctx.take_all(); @@ -119,7 +119,7 @@ async fn same_context_targets_restore_their_own_script_execution_disabled_after_ consume_main_document_navigation_start(&mut ctx); ctx.expect_result( 104177, - json!({ "frameId": "TID-000000000A", "loaderId": "LID-0000000002" }), + json!({ "frameId": "TID-000000000A", "loaderId": "LID-0000000003" }), Some("SID-active"), ); ctx.take_all(); @@ -173,7 +173,7 @@ async fn same_context_targets_restore_their_own_script_execution_disabled_after_ consume_main_document_navigation_start(&mut ctx); ctx.expect_result( 104180, - json!({ "frameId": second_target_id, "loaderId": "LID-0000000003" }), + json!({ "frameId": second_target_id, "loaderId": "LID-0000000004" }), Some(&second_session_id), ); ctx.take_all(); diff --git a/moli-protocol/src/domains/target/worker_target.rs b/moli-protocol/src/domains/target/worker_target.rs index 14ee87f29..a0d0a75af 100644 --- a/moli-protocol/src/domains/target/worker_target.rs +++ b/moli-protocol/src/domains/target/worker_target.rs @@ -1,26 +1,22 @@ -//! Ordered protocol projection for renderer-owned worker targets. +//! Ordered DevTools projection for Worker targets. //! -//! SharedWorker and ServiceWorker lifecycle events share one physical activity -//! slot so a capture batch retains source order through target attach, output, -//! detach, and destruction. They do not share lifecycle authority: -//! SharedWorker binds one stable target attachment, while ServiceWorker binds -//! independent stable-version, protocol-attachment, and per-run identities in -//! `conn::state`. ServiceWorker run-specific events already carry the opaque -//! identity created by the renderer authority; protocol capture projects that -//! exact run into a target/session identity before appending output. The -//! projection coordinator only validates and consumes the captured value. +//! Worker membership and ServiceWorker version/run transitions follow Browser +//! commits. Source FIFOs preserve attach, output and retirement order; Protocol +//! owns attachments and delivery progress, not execution membership. use moli_core::{ browser::ServiceWorkerCommand, page::{ - RendererDedicatedWorkerTargetEvent, RendererDedicatedWorkerTargetInfo, + RendererDedicatedWorkerMainScript, RendererDedicatedWorkerMainScriptOutcome, + RendererDedicatedWorkerObservation, RendererDedicatedWorkerTargetInfo, RendererRuntimeInspectorMessage, RendererServiceWorkerConsoleMessage, RendererServiceWorkerExceptionMessage, RendererServiceWorkerFetchDiagnostic, - RendererServiceWorkerFetchDiagnosticResult, RendererServiceWorkerRunIdentity, - RendererServiceWorkerTargetEvent, RendererServiceWorkerTargetInfo, - RendererServiceWorkerVersionStatus, RendererSharedWorkerConsoleMessage, - RendererSharedWorkerTargetEvent, RendererSharedWorkerTargetInfo, - RuntimeConsoleMessageSnapshot, SubresourceRequestInitiatorType, + RendererServiceWorkerFetchDiagnosticResult, RendererServiceWorkerLifecycle, + RendererServiceWorkerObservation, RendererServiceWorkerRunIdentity, + RendererServiceWorkerTargetInfo, RendererServiceWorkerVersionStatus, + RendererSharedWorkerConsoleMessage, RendererSharedWorkerObservation, + RendererSharedWorkerTargetInfo, RendererWorkerLifecycle, RuntimeConsoleMessageSnapshot, + SubresourceRequestInitiatorType, }, }; use moli_shared_worker::SharedWorkerInstanceId; @@ -62,6 +58,9 @@ use serde_json::Value; use super::events; +#[cfg(test)] +mod native_worker_tests; + #[derive(Debug, Default, PartialEq)] pub(in crate::domains) struct TargetPreparedOutputs { worker_target_lifecycle_outputs: Vec, @@ -355,305 +354,438 @@ pub(in crate::domains) const SLOT_SERVICE_WORKER_TARGET_LIFECYCLE: ProtocolOutpu pub(in crate::domains) const SLOT_DEDICATED_WORKER_TARGET_LIFECYCLE: ProtocolOutputSlot = ProtocolOutputSlot::DedicatedWorkerTargetLifecycle; -fn shared_worker_target_lifecycle_outputs_for_events( +/// Only Browser-acknowledged occurrences can create or retire Worker projections. +pub(in crate::domains) fn worker_lifecycle_prepared_outputs( conn: &mut CdpConnection, - browser_context_id: String, - events: Vec, + committed: moli_core::page::RendererCommittedWorkerLifecycle, ) -> TargetPreparedOutputs { - let mut outputs = TargetPreparedOutputs::default(); - for event in events { - match event { - RendererSharedWorkerTargetEvent::Created(info) => { - let owner_target_id = - conn.browser_context_by_id(&browser_context_id) - .and_then(|context| { - context.target_id_for_renderer_owner_local_host_id( - info.owner_local_host_id, - ) - }); - outputs.extend(register_shared_worker_target( - conn, - &browser_context_id, - owner_target_id, - info, - )); - } - RendererSharedWorkerTargetEvent::Destroyed { instance_id } => { - outputs.extend(remove_shared_worker_target( - conn, - &browser_context_id, - instance_id, - )); - } - RendererSharedWorkerTargetEvent::Console { - instance_id, - message, - } => { - outputs.extend(record_shared_worker_target_console_message( - conn, - &browser_context_id, - instance_id, - message, - )); - } - RendererSharedWorkerTargetEvent::RuntimeInspectorMessages { - instance_id, - inspector_session_id, - messages, - } => { - outputs.extend(record_shared_worker_target_runtime_inspector_messages( - conn, - &browser_context_id, - instance_id, - inspector_session_id, - messages, - )); - } + let Some(context) = conn + .browser_contexts() + .find(|context| context.routes_renderer_browser_context_runtime(committed.runtime())) + else { + return TargetPreparedOutputs::default(); + }; + if context + .worker_snapshot_sequence + .is_some_and(|sequence| committed.browser_sequence() <= sequence.get()) + { + return TargetPreparedOutputs::default(); + } + let browser_context_id = context.id.clone(); + match committed.lifecycle() { + RendererWorkerLifecycle::Service(event) => { + project_service_worker_lifecycle(conn, &browser_context_id, event.clone()) + } + RendererWorkerLifecycle::DedicatedCreated(info) => { + register_native_dedicated_worker_projection(conn, &browser_context_id, info.clone()) + } + RendererWorkerLifecycle::DedicatedScriptCompleted { + instance_id, + script, + } => project_dedicated_worker_main_script( + conn, + &browser_context_id, + *instance_id, + script.clone(), + ), + RendererWorkerLifecycle::DedicatedDestroyed(instance_id) => { + prepare_dedicated_worker_target_retirement( + conn, + &browser_context_id, + *instance_id, + DedicatedWorkerRetirementCause::RendererDestroyed, + ) + } + moli_core::page::RendererWorkerLifecycle::SharedCreated(info) => { + register_native_shared_worker_projection(conn, &browser_context_id, info.clone()) + } + moli_core::page::RendererWorkerLifecycle::SharedDestroyed(instance_id) => { + remove_shared_worker_target(conn, &browser_context_id, *instance_id) } } - outputs } -pub(in crate::domains) fn shared_worker_target_lifecycle_prepared_outputs_for_event( +fn register_native_dedicated_worker_projection( conn: &mut CdpConnection, - browser_context_id: String, - event: RendererSharedWorkerTargetEvent, + browser_context_id: &str, + info: RendererDedicatedWorkerTargetInfo, ) -> TargetPreparedOutputs { - shared_worker_target_lifecycle_outputs_for_events(conn, browser_context_id, vec![event]) -} - -pub(in crate::domains) fn service_worker_target_lifecycle_prepared_outputs_for_event( - conn: &mut CdpConnection, - browser_context_id: String, - event: RendererServiceWorkerTargetEvent, -) -> TargetPreparedOutputs { - service_worker_target_lifecycle_outputs_for_events(conn, browser_context_id, vec![event]) -} - -pub(in crate::domains) fn dedicated_worker_target_lifecycle_prepared_outputs_for_event( - conn: &mut CdpConnection, - owner: &CommandOwnerScope, - event: RendererDedicatedWorkerTargetEvent, -) -> TargetPreparedOutputs { - let Some(owner_page) = conn.target_page_residence_identity_for_owner(owner) else { + let Some((owner_page, renderer)) = (|| { + let context = conn.browser_context_by_id(browser_context_id)?; + let target = + context.target_id_for_renderer_owner_local_host_id(info.owner_local_host_id)?; + let renderer = context.target_renderer_page_residence_identity(&target)?; + let document = context.target_document_id(&target)?; + Some(( + TargetPageResidenceIdentity::new(browser_context_id.to_owned(), Some(target), document), + renderer, + )) + })() else { return TargetPreparedOutputs::default(); }; - let Some(owner_renderer_page) = conn.renderer_page_residence_identity_for_owner(owner) else { - return TargetPreparedOutputs::default(); - }; - let browser_context_id = owner_page.browser_context_id().to_owned(); - let owner_page_network_sessions = conn.network_event_session_ids_for_owner(owner); - dedicated_worker_target_lifecycle_outputs_for_events( + let sessions = conn + .network_event_session_ids_for_owner(&CommandOwnerScope::for_page_residence(&owner_page)); + register_dedicated_worker_target( conn, browser_context_id, owner_page, - owner_renderer_page, - owner_page_network_sessions, - vec![event], + renderer, + sessions, + info, ) } -fn dedicated_worker_target_lifecycle_outputs_for_events( +fn register_native_shared_worker_projection( + conn: &mut CdpConnection, + browser_context_id: &str, + info: RendererSharedWorkerTargetInfo, +) -> TargetPreparedOutputs { + let owner_target_id = conn + .browser_context_by_id(browser_context_id) + .and_then(|context| { + context.target_id_for_renderer_owner_local_host_id(info.owner_local_host_id) + }); + register_shared_worker_target(conn, browser_context_id, owner_target_id, info) +} + +pub(in crate::domains) fn shared_worker_observation_prepared_outputs( conn: &mut CdpConnection, browser_context_id: String, - owner_page: TargetPageResidenceIdentity, - owner_renderer_page: RendererPageResidenceIdentity, - owner_page_network_sessions: Vec>, - events: Vec, + event: RendererSharedWorkerObservation, ) -> TargetPreparedOutputs { - let mut outputs = TargetPreparedOutputs::default(); - for event in events { - match event { - RendererDedicatedWorkerTargetEvent::Created(info) => { - outputs.extend(register_dedicated_worker_target( - conn, - &browser_context_id, - owner_page.clone(), - owner_renderer_page, - owner_page_network_sessions.clone(), - info, - )); - } - RendererDedicatedWorkerTargetEvent::ScriptLoaded { - instance_id, - script_url, - response, - } => { - outputs.extend(record_dedicated_worker_main_script( - conn, - &browser_context_id, - instance_id, - script_url, - crate::conn::DedicatedWorkerMainScriptOutcome::Loaded(response), - )); - } - RendererDedicatedWorkerTargetEvent::ScriptLoadFailed { - instance_id, - script_url, - error_message, - response, - } => { - outputs.extend(record_dedicated_worker_main_script( - conn, - &browser_context_id, - instance_id, - script_url, - crate::conn::DedicatedWorkerMainScriptOutcome::Failed { - error_message, - response, - }, - )); - } - RendererDedicatedWorkerTargetEvent::Console { - instance_id, - message, - } => { - outputs.extend(record_dedicated_worker_target_console_message( - conn, - &browser_context_id, - instance_id, - message, - )); - } - RendererDedicatedWorkerTargetEvent::RuntimeInspectorMessages { - instance_id, - inspector_session_id, - messages, - } => { - outputs.extend(record_dedicated_worker_target_runtime_inspector_messages( - conn, - &browser_context_id, - instance_id, - inspector_session_id, - messages, - )); - } - RendererDedicatedWorkerTargetEvent::Destroyed { instance_id } => { - outputs.extend(prepare_dedicated_worker_target_retirement( - conn, - &browser_context_id, - instance_id, - DedicatedWorkerRetirementCause::RendererDestroyed, - )); + match event { + RendererSharedWorkerObservation::Console { + instance_id, + message, + } => record_shared_worker_target_console_message( + conn, + &browser_context_id, + instance_id, + message, + ), + RendererSharedWorkerObservation::RuntimeInspectorMessages { + instance_id, + inspector_session_id, + messages, + } => record_shared_worker_target_runtime_inspector_messages( + conn, + &browser_context_id, + instance_id, + inspector_session_id, + messages, + ), + } +} + +impl CdpConnection { + pub(crate) async fn project_browser_workers( + &mut self, + workers: Vec, + sequence: moli_core::browser::BrowserSequence, + ) -> Vec { + let mut outputs = TargetPreparedOutputs::default(); + let retired = self + .browser_contexts() + .flat_map(|context| { + context + .shared_worker_targets + .keys() + .map(|instance| moli_core::browser::WorkerHandle::Shared { + context: context.browser_context_id(), + instance: *instance, + }) + .chain(context.dedicated_worker_targets.keys().map(|instance| { + moli_core::browser::WorkerHandle::Dedicated { + context: context.browser_context_id(), + instance: *instance, + } + })) + .chain(context.service_worker_targets.keys().map(|version| { + moli_core::browser::WorkerHandle::Service { + context: context.browser_context_id(), + version: *version, + } + })) + .filter(|handle| !workers.iter().any(|worker| worker.handle() == *handle)) + .map(|handle| (context.id.clone(), handle)) + }) + .collect::>(); + for (context, worker) in retired { + outputs.extend(match worker { + moli_core::browser::WorkerHandle::Service { version, .. } => { + let run = self + .browser_context_by_id(&context) + .and_then(|context| context.service_worker_targets.get(&version)) + .and_then(|target| target.active_renderer_run().cloned()); + remove_service_worker_target(self, &context, version, run) + } + moli_core::browser::WorkerHandle::Shared { instance, .. } => { + remove_shared_worker_target(self, &context, instance) + } + moli_core::browser::WorkerHandle::Dedicated { instance, .. } => { + prepare_dedicated_worker_target_retirement( + self, + &context, + instance, + DedicatedWorkerRetirementCause::RendererDestroyed, + ) + } + }); + } + for worker in workers { + match worker { + moli_core::browser::WorkerSnapshot::Service { context, worker } => { + let Some(context) = self.browser_context_by_browser_id(context) else { + continue; + }; + let context = context.id.clone(); + outputs.extend(project_service_worker_snapshot(self, &context, worker)); + } + moli_core::browser::WorkerSnapshot::Dedicated { context, worker } => { + let Some(context) = self.browser_context_by_browser_id(context) else { + continue; + }; + let context = context.id.clone(); + let instance = worker.info.instance_id; + outputs.extend(register_native_dedicated_worker_projection( + self, + &context, + worker.info, + )); + if let Some(script) = worker.main_script { + outputs.extend(project_dedicated_worker_main_script( + self, &context, instance, script, + )); + } + } + moli_core::browser::WorkerSnapshot::Shared { context, info } => { + let Some(context) = self.browser_context_by_browser_id(context) else { + continue; + }; + let context = context.id.clone(); + outputs.extend(register_native_shared_worker_projection( + self, &context, info, + )); + } } } + let contexts = self + .browser_contexts() + .map(|context| context.id.clone()) + .collect::>(); + for id in contexts { + let context = self + .browser_context_by_id_mut(&id) + .expect("projected Context remains live"); + // Recovery covers all occurrences at or before this atomic snapshot. + // Normal interleaved source FIFOs must not advance this watermark: + // another source may still carry an earlier committed creation. + context.worker_snapshot_sequence = Some(sequence); + } + worker_target_background_events_async(self, outputs).await } +} + +pub(in crate::domains) fn dedicated_worker_observation_prepared_outputs( + conn: &mut CdpConnection, + owner: &CommandOwnerScope, + event: RendererDedicatedWorkerObservation, +) -> TargetPreparedOutputs { + let Some((browser_context_id, _)) = conn.target_owner_identity_for_owner(owner) else { + return TargetPreparedOutputs::default(); + }; + match event { + RendererDedicatedWorkerObservation::Console { + instance_id, + message, + } => record_dedicated_worker_target_console_message( + conn, + &browser_context_id, + instance_id, + message, + ), + RendererDedicatedWorkerObservation::RuntimeInspectorMessages { + instance_id, + inspector_session_id, + messages, + } => record_dedicated_worker_target_runtime_inspector_messages( + conn, + &browser_context_id, + instance_id, + inspector_session_id, + messages, + ), + } +} + +fn project_service_worker_snapshot( + conn: &mut CdpConnection, + context: &str, + worker: moli_core::browser::ServiceWorkerSnapshot, +) -> TargetPreparedOutputs { + let version_id = worker.info.version_id; + let mut outputs = TargetPreparedOutputs::default(); + let previous_run = conn + .browser_context_by_id(context) + .and_then(|context| context.service_worker_targets.get(&version_id)) + .and_then(|target| target.active_renderer_run().cloned()); + if let Some(previous) = previous_run + && Some(&previous) != worker.execution.active_run() + { + outputs.extend(record_service_worker_target_stopped( + conn, + context, + version_id, + previous, + "snapshot_recovery".into(), + )); + } + let status = worker.info.status; + outputs.extend(register_service_worker_target_with_active_run( + conn, + context, + worker.info, + worker.execution.active_run().cloned(), + )); + if let Some(run) = worker.execution.active_run() { + outputs.extend(project_service_worker_lifecycle( + conn, + context, + RendererServiceWorkerLifecycle::Starting { + version_id, + run: run.clone(), + }, + )); + } + if let moli_core::browser::ServiceWorkerExecution::Running(run) = worker.execution { + outputs.extend(record_service_worker_target_started( + conn, context, version_id, run, + )); + } + outputs.extend(record_service_worker_target_version_updated( + conn, context, version_id, status, + )); outputs } -fn service_worker_target_lifecycle_outputs_for_events( +fn project_service_worker_lifecycle( conn: &mut CdpConnection, - browser_context_id: String, - events: Vec, + browser_context_id: &str, + event: RendererServiceWorkerLifecycle, ) -> TargetPreparedOutputs { - let mut outputs = TargetPreparedOutputs::default(); - for event in events { - match event { - RendererServiceWorkerTargetEvent::Created { info, active_run } => { - outputs.extend(register_service_worker_target_with_active_run( - conn, - &browser_context_id, - info, - active_run, - )); - } - RendererServiceWorkerTargetEvent::Started { version_id, run } => { - outputs.extend(record_service_worker_target_started( - conn, - &browser_context_id, - version_id, - run, - )); - } - RendererServiceWorkerTargetEvent::Stopped { - version_id, - run, - reason, - } => { - outputs.extend(record_service_worker_target_stopped( - conn, - &browser_context_id, - version_id, - run, - reason, - )); - } - RendererServiceWorkerTargetEvent::Destroyed { - version_id, + match event { + RendererServiceWorkerLifecycle::Created { info, active_run } => { + register_service_worker_target_with_active_run( + conn, + browser_context_id, + info, active_run, - } => { - outputs.extend(remove_service_worker_target( - conn, - &browser_context_id, - version_id, - active_run, - )); - } - RendererServiceWorkerTargetEvent::VersionUpdated { version_id, status } => { - outputs.extend(record_service_worker_target_version_updated( - conn, - &browser_context_id, - version_id, - status, - )); - } - RendererServiceWorkerTargetEvent::Console { + ) + } + RendererServiceWorkerLifecycle::Started { version_id, run } => { + record_service_worker_target_started(conn, browser_context_id, version_id, run) + } + RendererServiceWorkerLifecycle::Stopped { + version_id, + run, + reason, + } => { + record_service_worker_target_stopped(conn, browser_context_id, version_id, run, reason) + } + RendererServiceWorkerLifecycle::Destroyed { + version_id, + active_run, + } => remove_service_worker_target(conn, browser_context_id, version_id, active_run), + RendererServiceWorkerLifecycle::VersionUpdated { version_id, status } => { + record_service_worker_target_version_updated( + conn, + browser_context_id, version_id, - run, - message, - } => { - outputs.extend(record_service_worker_target_console_message( - conn, - &browser_context_id, - version_id, - run, - message, - )); + status, + ) + } + + RendererServiceWorkerLifecycle::Starting { version_id, run } => { + let mut outputs = TargetPreparedOutputs::default(); + let Some(target) = conn + .browser_context_by_id_mut(browser_context_id) + .and_then(|context| context.service_worker_targets.get_mut(&version_id)) + else { + return outputs; + }; + if target.active_renderer_run().is_some() + || target + .mark_worker_starting(browser_context_id, run) + .is_none() + { + return outputs; } - RendererServiceWorkerTargetEvent::Exception { - version_id, - run, - message, - } => { - outputs.extend(record_service_worker_target_exception_message( + if let Some(version) = target.version_identity(browser_context_id) { + append_service_worker_domain_snapshot( conn, - &browser_context_id, - version_id, - run, - message, - )); - } - RendererServiceWorkerTargetEvent::FetchDiagnostic { - version_id, - run, - diagnostic, - } => { - outputs.extend(record_service_worker_target_fetch_diagnostic( - conn, - &browser_context_id, - version_id, - run, - diagnostic, - )); - } - RendererServiceWorkerTargetEvent::RuntimeInspectorMessages { - version_id, - run, - inspector_session_id, - messages, - } => { - outputs.extend(record_service_worker_target_runtime_inspector_messages( - conn, - &browser_context_id, - version_id, - run, - inspector_session_id, - messages, - )); + browser_context_id, + version, + &mut outputs, + ); } + outputs } } - outputs +} + +pub(in crate::domains) fn service_worker_observation_prepared_outputs( + conn: &mut CdpConnection, + browser_context_id: String, + event: RendererServiceWorkerObservation, +) -> TargetPreparedOutputs { + match event { + RendererServiceWorkerObservation::Console { + version_id, + run, + message, + } => record_service_worker_target_console_message( + conn, + &browser_context_id, + version_id, + run, + message, + ), + RendererServiceWorkerObservation::Exception { + version_id, + run, + message, + } => record_service_worker_target_exception_message( + conn, + &browser_context_id, + version_id, + run, + message, + ), + RendererServiceWorkerObservation::FetchDiagnostic { + version_id, + run, + diagnostic, + } => record_service_worker_target_fetch_diagnostic( + conn, + &browser_context_id, + version_id, + run, + diagnostic, + ), + RendererServiceWorkerObservation::RuntimeInspectorMessages { + version_id, + run, + inspector_session_id, + messages, + } => record_service_worker_target_runtime_inspector_messages( + conn, + &browser_context_id, + version_id, + run, + inspector_session_id, + messages, + ), + } } fn append_service_worker_domain_snapshot( @@ -781,17 +913,17 @@ fn register_dedicated_worker_target( outputs } -fn record_dedicated_worker_main_script( +fn project_dedicated_worker_main_script( conn: &mut CdpConnection, browser_context_id: &str, renderer_instance_id: u64, - script_url: String, - outcome: crate::conn::DedicatedWorkerMainScriptOutcome, + script: std::sync::Arc, ) -> TargetPreparedOutputs { let mut outputs = TargetPreparedOutputs::default(); let Some(owner_page) = conn .browser_context_by_id(browser_context_id) .and_then(|context| context.dedicated_worker_targets.get(&renderer_instance_id)) + .filter(|target| target.main_script().is_none()) .map(|target| target.owner_page.clone()) else { return outputs; @@ -823,13 +955,9 @@ fn record_dedicated_worker_main_script( let page_extra_events = dedicated_worker_main_script_page_extra_events( &target_id, &owner_page_network_sessions, - &outcome, - ); - target.record_main_script( - script_url, - outcome, - pause_failed_target_until_debugger_resume, + &script.outcome, ); + target.record_main_script(script, pause_failed_target_until_debugger_resume); (target_id, page_extra_events) }; for (_session_id, events) in page_extra_events { @@ -998,13 +1126,11 @@ fn dedicated_worker_auto_attach_owner_sessions( fn dedicated_worker_main_script_page_extra_events( request_id: &str, sessions: &[Option], - outcome: &crate::conn::DedicatedWorkerMainScriptOutcome, + outcome: &RendererDedicatedWorkerMainScriptOutcome, ) -> Vec<(Option, Vec)> { let response = match outcome { - crate::conn::DedicatedWorkerMainScriptOutcome::Loaded(response) => Some(response.as_ref()), - crate::conn::DedicatedWorkerMainScriptOutcome::Failed { response, .. } => { - response.as_deref() - } + RendererDedicatedWorkerMainScriptOutcome::Loaded(response) => Some(response.as_ref()), + RendererDedicatedWorkerMainScriptOutcome::Failed { response, .. } => response.as_deref(), }; let Some(response) = response else { return Vec::new(); @@ -1047,15 +1173,13 @@ fn dedicated_worker_main_script_page_extra_events( fn dedicated_worker_main_script_worker_events( target_id: &str, session_id: Option<&str>, - script: &crate::conn::DedicatedWorkerMainScriptSnapshot, + script: &RendererDedicatedWorkerMainScript, ) -> Vec { let mut events = Vec::new(); let timestamp = monotonic_timestamp_seconds(); let response = match &script.outcome { - crate::conn::DedicatedWorkerMainScriptOutcome::Loaded(response) => Some(response.as_ref()), - crate::conn::DedicatedWorkerMainScriptOutcome::Failed { response, .. } => { - response.as_deref() - } + RendererDedicatedWorkerMainScriptOutcome::Loaded(response) => Some(response.as_ref()), + RendererDedicatedWorkerMainScriptOutcome::Failed { response, .. } => response.as_deref(), }; if let Some(response) = response { let has_extra_info = response.network_request_headers().is_some() @@ -1079,7 +1203,7 @@ fn dedicated_worker_main_script_worker_events( ); } match &script.outcome { - crate::conn::DedicatedWorkerMainScriptOutcome::Loaded(response) => { + RendererDedicatedWorkerMainScriptOutcome::Loaded(response) => { network::emit_loading_finished( &mut events, session_id, @@ -1091,7 +1215,7 @@ fn dedicated_worker_main_script_worker_events( DevToolsNetworkResourceType::Script, ); } - crate::conn::DedicatedWorkerMainScriptOutcome::Failed { error_message, .. } => { + RendererDedicatedWorkerMainScriptOutcome::Failed { error_message, .. } => { network::emit_loading_failed( &mut events, session_id, @@ -2315,10 +2439,10 @@ pub(super) async fn close_browser_context_worker_targets_for_dispose_async( )); } - worker_target_removal_background_events_async(conn, outputs).await + worker_target_background_events_async(conn, outputs).await } -async fn worker_target_removal_background_events_async( +async fn worker_target_background_events_async( conn: &mut CdpConnection, outputs: TargetPreparedOutputs, ) -> Vec { @@ -3635,6 +3759,30 @@ mod worker_target_attachment_tests; #[cfg(test)] mod tests { + enum ServiceWorkerOutputForTest { + Lifecycle(RendererServiceWorkerLifecycle), + Observation(RendererServiceWorkerObservation), + } + + fn prepare_service_worker_outputs_for_test( + conn: &mut CdpConnection, + context: String, + events: Vec, + ) -> TargetPreparedOutputs { + let mut outputs = TargetPreparedOutputs::default(); + for event in events { + outputs.extend(match event { + ServiceWorkerOutputForTest::Lifecycle(event) => { + project_service_worker_lifecycle(conn, &context, event) + } + ServiceWorkerOutputForTest::Observation(event) => { + service_worker_observation_prepared_outputs(conn, context.clone(), event) + } + }); + } + outputs + } + use super::*; use crate::conn::{CdpTargetFilter, CdpTargetFilterEntry, DedicatedWorkerTargetState}; @@ -3759,6 +3907,24 @@ mod tests { } } + fn record_dedicated_worker_main_script( + conn: &mut CdpConnection, + context: &str, + instance: u64, + script_url: String, + outcome: RendererDedicatedWorkerMainScriptOutcome, + ) -> TargetPreparedOutputs { + project_dedicated_worker_main_script( + conn, + context, + instance, + std::sync::Arc::new(RendererDedicatedWorkerMainScript { + script_url, + outcome, + }), + ) + } + fn dedicated_worker_fixture() -> ( CdpConnection, TargetPageResidenceIdentity, @@ -4071,7 +4237,7 @@ mod tests { "BID-1", 81, "https://example.test/worker.js".to_owned(), - crate::conn::DedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( + RendererDedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( "https://example.test/worker.js", 200, true, @@ -4121,7 +4287,7 @@ mod tests { "BID-1", 9, "https://example.test/worker.js".to_owned(), - crate::conn::DedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( + RendererDedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( "https://example.test/worker.js", 200, true, @@ -4226,7 +4392,7 @@ mod tests { "BID-1", 90, "https://example.test/worker.js".to_owned(), - crate::conn::DedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( + RendererDedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( "https://example.test/worker.js", 200, true, @@ -4261,7 +4427,7 @@ mod tests { "BID-1", 91, "https://example.test/worker.js".to_owned(), - crate::conn::DedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( + RendererDedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( "https://example.test/worker.js", 200, true, @@ -4306,7 +4472,7 @@ mod tests { "BID-1", 92, "https://example.test/worker.js".to_owned(), - crate::conn::DedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( + RendererDedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( "https://example.test/worker.js", 200, true, @@ -4338,35 +4504,35 @@ mod tests { #[tokio::test] async fn dedicated_worker_blob_and_failed_main_scripts_publish_chromium_terminal_shapes() { - for (instance_id, url, outcome, terminal_method, expected_protocol, has_extra_info) in - [ - ( - 10, + for (instance_id, url, outcome, terminal_method, expected_protocol, has_extra_info) in [ + ( + 10, + "blob:https://example.test/worker-blob", + RendererDedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( "blob:https://example.test/worker-blob", - crate::conn::DedicatedWorkerMainScriptOutcome::Loaded(Box::new( - worker_response("blob:https://example.test/worker-blob", 200, false), - )), - "Network.loadingFinished", - Some("blob"), + 200, false, - ), - ( - 11, - "https://example.test/missing-worker.js", - crate::conn::DedicatedWorkerMainScriptOutcome::Failed { - error_message: "load failed: net::ERR_FAILED".to_owned(), - response: Some(Box::new(worker_response( - "https://example.test/missing-worker.js", - 404, - true, - ))), - }, - "Network.loadingFailed", - Some("h2"), - true, - ), - ] - { + ))), + "Network.loadingFinished", + Some("blob"), + false, + ), + ( + 11, + "https://example.test/missing-worker.js", + RendererDedicatedWorkerMainScriptOutcome::Failed { + error_message: "load failed: net::ERR_FAILED".to_owned(), + response: Some(Box::new(worker_response( + "https://example.test/missing-worker.js", + 404, + true, + ))), + }, + "Network.loadingFailed", + Some("h2"), + true, + ), + ] { let (mut conn, owner_page, owner_renderer_page) = dedicated_worker_fixture(); enable_dedicated_worker_auto_attach_for_owner_page(&mut conn, true); let created = register_dedicated_worker_target( @@ -4419,27 +4585,30 @@ mod tests { conn.set_target_discovery_for_owner(None, CdpTargetFilter::default_target_discovery()); enable_dedicated_worker_auto_attach_for_owner_page(&mut conn, false); let script_url = "https://example.test/missing-worker.js"; - let outputs = dedicated_worker_target_lifecycle_outputs_for_events( + let mut outputs = register_dedicated_worker_target( &mut conn, - "BID-1".to_owned(), + "BID-1", owner_page, owner_renderer_page, vec![None], - vec![ - RendererDedicatedWorkerTargetEvent::Created(dedicated_worker_info( - 13, - owner_renderer_page, - script_url, - )), - RendererDedicatedWorkerTargetEvent::ScriptLoadFailed { - instance_id: 13, - script_url: script_url.to_owned(), - error_message: "load failed: net::ERR_FAILED".to_owned(), - response: Some(Box::new(worker_response(script_url, 404, true))), - }, - RendererDedicatedWorkerTargetEvent::Destroyed { instance_id: 13 }, - ], + dedicated_worker_info(13, owner_renderer_page, script_url), ); + outputs.extend(record_dedicated_worker_main_script( + &mut conn, + "BID-1", + 13, + script_url.to_owned(), + RendererDedicatedWorkerMainScriptOutcome::Failed { + error_message: "load failed: net::ERR_FAILED".to_owned(), + response: Some(Box::new(worker_response(script_url, 404, true))), + }, + )); + outputs.extend(prepare_dedicated_worker_target_retirement( + &mut conn, + "BID-1", + 13, + DedicatedWorkerRetirementCause::RendererDestroyed, + )); let target_id = dedicated_created_target_id(&outputs); assert_eq!( conn.target_registry_host_kind(&target_id), @@ -4493,27 +4662,30 @@ mod tests { CdpTargetFilter::default_auto_attach(), ); let script_url = "https://example.test/missing-worker.js"; - let outputs = dedicated_worker_target_lifecycle_outputs_for_events( + let mut outputs = register_dedicated_worker_target( &mut conn, - "BID-1".to_owned(), + "BID-1", owner_page, owner_renderer_page, vec![None], - vec![ - RendererDedicatedWorkerTargetEvent::Created(dedicated_worker_info( - 14, - owner_renderer_page, - script_url, - )), - RendererDedicatedWorkerTargetEvent::ScriptLoadFailed { - instance_id: 14, - script_url: script_url.to_owned(), - error_message: "load failed: net::ERR_FAILED".to_owned(), - response: Some(Box::new(worker_response(script_url, 404, true))), - }, - RendererDedicatedWorkerTargetEvent::Destroyed { instance_id: 14 }, - ], + dedicated_worker_info(14, owner_renderer_page, script_url), ); + outputs.extend(record_dedicated_worker_main_script( + &mut conn, + "BID-1", + 14, + script_url.to_owned(), + RendererDedicatedWorkerMainScriptOutcome::Failed { + error_message: "load failed: net::ERR_FAILED".to_owned(), + response: Some(Box::new(worker_response(script_url, 404, true))), + }, + )); + outputs.extend(prepare_dedicated_worker_target_retirement( + &mut conn, + "BID-1", + 14, + DedicatedWorkerRetirementCause::RendererDestroyed, + )); let target_id = dedicated_created_target_id(&outputs); let session_id = dedicated_attached_session_id(&outputs); let messages = @@ -4624,7 +4796,7 @@ mod tests { "BID-1", 12, "blob:https://example.test/worker-blob".to_owned(), - crate::conn::DedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( + RendererDedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( "blob:https://example.test/worker-blob", 200, false, @@ -4724,7 +4896,7 @@ mod tests { "BID-1", 14, "https://example.test/worker.js".to_owned(), - crate::conn::DedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( + RendererDedicatedWorkerMainScriptOutcome::Loaded(Box::new(worker_response( "https://example.test/worker.js", 200, true, @@ -5267,13 +5439,15 @@ mod tests { "starting" ); - let outputs = service_worker_target_lifecycle_outputs_for_events( + let outputs = prepare_service_worker_outputs_for_test( &mut conn, "BID-1".to_owned(), - vec![RendererServiceWorkerTargetEvent::Started { - version_id: 14, - run, - }], + vec![ServiceWorkerOutputForTest::Lifecycle( + RendererServiceWorkerLifecycle::Started { + version_id: 14, + run, + }, + )], ) .worker_target_lifecycle_outputs; let started_version = @@ -5316,13 +5490,15 @@ mod tests { assert_eq!(version["params"]["versions"][0]["status"], "installing"); let renderer_run = renderer_run(); - let outputs = service_worker_target_lifecycle_outputs_for_events( + let outputs = prepare_service_worker_outputs_for_test( &mut conn, "BID-1".to_owned(), - vec![RendererServiceWorkerTargetEvent::Started { - version_id: 12, - run: renderer_run.clone(), - }], + vec![ServiceWorkerOutputForTest::Lifecycle( + RendererServiceWorkerLifecycle::Started { + version_id: 12, + run: renderer_run.clone(), + }, + )], ) .worker_target_lifecycle_outputs; let started_version = @@ -5333,14 +5509,16 @@ mod tests { "running" ); - let outputs = service_worker_target_lifecycle_outputs_for_events( + let outputs = prepare_service_worker_outputs_for_test( &mut conn, "BID-1".to_owned(), - vec![RendererServiceWorkerTargetEvent::Stopped { - version_id: 12, - run: renderer_run, - reason: "idle_timeout".to_owned(), - }], + vec![ServiceWorkerOutputForTest::Lifecycle( + RendererServiceWorkerLifecycle::Stopped { + version_id: 12, + run: renderer_run, + reason: "idle_timeout".to_owned(), + }, + )], ) .worker_target_lifecycle_outputs; let stopped_version = @@ -5351,13 +5529,15 @@ mod tests { "stopped" ); - let outputs = service_worker_target_lifecycle_outputs_for_events( + let outputs = prepare_service_worker_outputs_for_test( &mut conn, "BID-1".to_owned(), - vec![RendererServiceWorkerTargetEvent::VersionUpdated { - version_id: 12, - status: RendererServiceWorkerVersionStatus::Activated, - }], + vec![ServiceWorkerOutputForTest::Lifecycle( + RendererServiceWorkerLifecycle::VersionUpdated { + version_id: 12, + status: RendererServiceWorkerVersionStatus::Activated, + }, + )], ) .worker_target_lifecycle_outputs; let updated_version = @@ -5442,24 +5622,32 @@ mod tests { context.attach_active_session("SID-page".to_owned()); context.set_service_worker_domain_enabled(Some("SID-page"), true); conn.install_browser_context_fixture_for_test(context); - let _ = register_service_worker_target(&mut conn, "BID-1", service_worker_info(13)); + let run = renderer_run(); + let _ = register_service_worker_target_with_active_run( + &mut conn, + "BID-1", + service_worker_info(13), + Some(run.clone()), + ); - let outputs = service_worker_target_lifecycle_outputs_for_events( + let outputs = prepare_service_worker_outputs_for_test( &mut conn, "BID-1".to_owned(), - vec![RendererServiceWorkerTargetEvent::Exception { - version_id: 13, - run: renderer_run(), - message: RendererServiceWorkerExceptionMessage { - message: "boom".to_owned(), - filename: "https://example.test/service-worker.js".to_owned(), - lineno: 9, - colno: 4, - event_kind: "fetch".to_owned(), - phase: "dispatch".to_owned(), - source: "exception".to_owned(), + vec![ServiceWorkerOutputForTest::Observation( + RendererServiceWorkerObservation::Exception { + version_id: 13, + run, + message: RendererServiceWorkerExceptionMessage { + message: "boom".to_owned(), + filename: "https://example.test/service-worker.js".to_owned(), + lineno: 9, + colno: 4, + event_kind: "fetch".to_owned(), + phase: "dispatch".to_owned(), + source: "exception".to_owned(), + }, }, - }], + )], ) .worker_target_lifecycle_outputs; let error = lifecycle_protocol_message(&outputs, "ServiceWorker.workerErrorReported") @@ -5697,8 +5885,14 @@ mod tests { conn.set_root_target_discovery_enabled(true); conn.browser_context = Some(conn.new_browser_context_fixture_for_test("BID-1".to_owned())); - let outputs = register_service_worker_target(&mut conn, "BID-1", service_worker_info(29)) - .worker_target_lifecycle_outputs; + let first_run = renderer_run(); + let outputs = register_service_worker_target_with_active_run( + &mut conn, + "BID-1", + service_worker_info(29), + Some(first_run.clone()), + ) + .worker_target_lifecycle_outputs; let (target_id, attached_session_id) = outputs .iter() .find_map(|output| { @@ -5716,30 +5910,31 @@ mod tests { target.set_runtime_frontend_enabled(&attached_session_id, true); target.set_inspector_enabled(&attached_session_id, true); - let first_run = renderer_run(); - let outputs = service_worker_target_lifecycle_outputs_for_events( + let outputs = prepare_service_worker_outputs_for_test( &mut conn, "BID-1".to_owned(), - vec![RendererServiceWorkerTargetEvent::RuntimeInspectorMessages { - version_id: 29, - run: first_run.clone(), - inspector_session_id: None, - messages: runtime_inspector_messages(vec![json!({ - "method": "Runtime.executionContextCreated", - "params": { - "context": { - "id": 2901, - "uniqueId": "service-worker-realm-1", - "origin": "https://example.test", - "name": "", - "auxData": { - "isDefault": true, - "type": "service-worker" + vec![ServiceWorkerOutputForTest::Observation( + RendererServiceWorkerObservation::RuntimeInspectorMessages { + version_id: 29, + run: first_run.clone(), + inspector_session_id: None, + messages: runtime_inspector_messages(vec![json!({ + "method": "Runtime.executionContextCreated", + "params": { + "context": { + "id": 2901, + "uniqueId": "service-worker-realm-1", + "origin": "https://example.test", + "name": "", + "auxData": { + "isDefault": true, + "type": "service-worker" + } } } - } - })]), - }], + })]), + }, + )], ); let out = drain_target_lifecycle_events_for_test(&mut conn, outputs) .await @@ -5758,39 +5953,41 @@ mod tests { ); let second_run = renderer_run(); - let outputs = service_worker_target_lifecycle_outputs_for_events( + let outputs = prepare_service_worker_outputs_for_test( &mut conn, "BID-1".to_owned(), vec![ - RendererServiceWorkerTargetEvent::Stopped { + ServiceWorkerOutputForTest::Lifecycle(RendererServiceWorkerLifecycle::Stopped { version_id: 29, run: first_run, reason: "idle_timeout".to_owned(), - }, - RendererServiceWorkerTargetEvent::Started { + }), + ServiceWorkerOutputForTest::Lifecycle(RendererServiceWorkerLifecycle::Started { version_id: 29, run: second_run.clone(), - }, - RendererServiceWorkerTargetEvent::RuntimeInspectorMessages { - version_id: 29, - run: second_run, - inspector_session_id: None, - messages: runtime_inspector_messages(vec![json!({ - "method": "Runtime.executionContextCreated", - "params": { - "context": { - "id": 2902, - "uniqueId": "service-worker-realm-2", - "origin": "https://example.test", - "name": "", - "auxData": { - "isDefault": true, - "type": "service-worker" + }), + ServiceWorkerOutputForTest::Observation( + RendererServiceWorkerObservation::RuntimeInspectorMessages { + version_id: 29, + run: second_run, + inspector_session_id: None, + messages: runtime_inspector_messages(vec![json!({ + "method": "Runtime.executionContextCreated", + "params": { + "context": { + "id": 2902, + "uniqueId": "service-worker-realm-2", + "origin": "https://example.test", + "name": "", + "auxData": { + "isDefault": true, + "type": "service-worker" + } } } - } - })]), - }, + })]), + }, + ), ], ); let out = drain_target_lifecycle_events_for_test(&mut conn, outputs) @@ -6411,6 +6608,7 @@ mod tests { fn service_worker_fetch_diagnostics_bind_the_exact_run_and_enabled_sessions() { let mut conn = crate::test_support::connection(); let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); + let renderer_run = renderer_run(); let mut target = ServiceWorkerTargetState::new( 41, 25, @@ -6418,7 +6616,7 @@ mod tests { "https://example.test/service-worker.js".to_owned(), "https://example.test/".to_owned(), RendererServiceWorkerVersionStatus::Activated, - None, + Some(renderer_run.clone()), ); target.attach_session("SID-first".to_owned()); target.attach_session("SID-second".to_owned()); @@ -6442,7 +6640,6 @@ mod tests { body_len: 2, }, }; - let renderer_run = renderer_run(); let outputs = record_service_worker_target_fetch_diagnostic( &mut conn, "BID-1", @@ -6668,6 +6865,7 @@ mod tests { fn service_worker_runtime_inspector_messages_bind_and_prepare_the_exact_run() { let mut conn = crate::test_support::connection(); let mut browser_context = conn.new_browser_context_fixture_for_test("BID-1".to_owned()); + let renderer_run = renderer_run(); let mut target = ServiceWorkerTargetState::new( 41, 23, @@ -6675,7 +6873,7 @@ mod tests { "https://example.test/service-worker.js".to_owned(), "https://example.test/".to_owned(), RendererServiceWorkerVersionStatus::Activated, - None, + Some(renderer_run.clone()), ); target.attach_session("SID-first".to_owned()); target.attach_session("SID-second".to_owned()); @@ -6691,7 +6889,6 @@ mod tests { } } })]); - let renderer_run = renderer_run(); let outputs = record_service_worker_target_runtime_inspector_messages( &mut conn, "BID-1", diff --git a/moli-protocol/src/domains/target/worker_target/native_worker_tests.rs b/moli-protocol/src/domains/target/worker_target/native_worker_tests.rs new file mode 100644 index 000000000..bedf1fd8b --- /dev/null +++ b/moli-protocol/src/domains/target/worker_target/native_worker_tests.rs @@ -0,0 +1,758 @@ +use super::*; +use crate::conn::{BrowserContext, CdpTargetFilter}; +use crate::domains::activity::RendererPublicationOwner; +use moli_core::browser::{ + BrowserContextHandle, BrowserContextStoragePartitionHandles, BrowserEvent, BrowserService, + NavigationRequestLoadPolicy, StoragePartitionKind, WebContentsCreation, WorkerSnapshot, +}; + +struct NativeWorkers { + service: BrowserService, + context: BrowserContextHandle, + output: moli_core::RendererOutputTransportReceiver, + occurrences: std::collections::VecDeque<( + moli_core::RendererOutputStreamIdentity, + moli_core::page::RendererWorkerLifecycleObservation, + )>, +} + +impl NativeWorkers { + async fn navigate_service_worker( + &self, + url: &str, + ) -> moli_core::browser::ServiceWorkerSnapshot { + let (snapshot, mut events) = self.service.handle().subscribe().unwrap(); + let contents = snapshot + .web_contents + .into_iter() + .find(|contents| self.context.contains_web_contents(*contents)) + .unwrap(); + let navigation = self + .context + .navigate_document( + contents, + moli_core::browser::web_contents::NavigationRequestInterception::new( + url.parse().unwrap(), + "GET".into(), + None, + Vec::new().into(), + NavigationRequestLoadPolicy::BrowserInitiated, + ), + ) + .unwrap(); + assert!(matches!( + tokio::time::timeout(std::time::Duration::from_secs(5), navigation.wait()) + .await + .unwrap_or_else(|_| panic!( + "navigation pending: {:?}", + self.service.handle().subscribe().unwrap().0 + )) + .unwrap(), + moli_core::browser::BrowserNavigationOutcome::Document(_) + )); + wait_service_worker_state(&mut events, |worker| { + worker.info.status == RendererServiceWorkerVersionStatus::Activated + && matches!( + worker.execution, + moli_core::browser::ServiceWorkerExecution::Running(_) + ) + }) + .await + } + + async fn start(names: &[&str]) -> Self { + Self::start_named(names, false).await + } + + async fn start_named(names: &[&str], dedicated: bool) -> Self { + let service = BrowserService::start().unwrap(); + let browser = service.handle(); + let context = browser + .create_context( + BrowserContextStoragePartitionHandles::memory(), + StoragePartitionKind::Ephemeral, + None, + None, + ) + .unwrap(); + context.bind_page_navigation_engines(Default::default(), None); + let (sender, output) = moli_core::renderer_output_transport_channel(); + context + .set_renderer_output_transport_sender(sender) + .unwrap(); + let (contents, _) = context + .create_web_contents(WebContentsCreation::default()) + .unwrap(); + let (_, mut events) = browser.subscribe().unwrap(); + let script = names + .iter() + .map(|name| { + if dedicated { + format!( + "new Worker('data:text/javascript,onmessage = () => {{}}', {})", + serde_json::json!({ "name": name }) + ) + } else { + format!( + "new SharedWorker('data:text/javascript,onconnect = () => {{}}', {})", + serde_json::to_string(name).unwrap() + ) + } + }) + .collect::>() + .join(","); + let url = format!("data:text/html,"); + let navigation = context + .navigate_document( + contents, + moli_core::browser::web_contents::NavigationRequestInterception::new( + url.parse().unwrap(), + "GET".into(), + None, + Vec::new().into(), + NavigationRequestLoadPolicy::BrowserInitiated, + ), + ) + .unwrap(); + assert!(matches!( + navigation.wait().await.unwrap(), + moli_core::browser::BrowserNavigationOutcome::Document(_) + )); + tokio::time::timeout(std::time::Duration::from_secs(5), async { + let mut created = std::collections::BTreeSet::new(); + while created.len() < names.len() { + let name = match events.recv().await.unwrap().event { + BrowserEvent::WorkerCreated(WorkerSnapshot::Shared { context: id, info }) + if !dedicated && id == context.id() => + { + info.name + } + BrowserEvent::WorkerUpdated(WorkerSnapshot::Dedicated { + context: id, + worker, + }) if dedicated && id == context.id() => worker.info.name, + _ => continue, + }; + assert!(names.contains(&name.as_str())); + assert!(created.insert(name)); + } + }) + .await + .expect("real workers must commit without a Protocol consumer"); + Self { + service, + context, + output, + occurrences: Default::default(), + } + } + + fn connection(&self) -> CdpConnection { + let mut conn = CdpConnection::new( + self.service.handle(), + crate::CdpInitialStoragePartition::memory(), + Default::default(), + ); + conn.set_target_discovery_for_owner(None, CdpTargetFilter::default_target_discovery()); + conn + } + + async fn next_occurrence( + &mut self, + ) -> ( + moli_core::RendererOutputStreamIdentity, + moli_core::page::RendererCommittedWorkerLifecycle, + ) { + tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + if let Some((stream, observation)) = self.occurrences.pop_front() { + return ( + stream, + observation + .committed() + .await + .expect("live native input is committed"), + ); + } + let moli_core::RendererOutputTransportMessage::Publication(publication) = self + .output + .recv() + .await + .expect("worker transport closed before its lifecycle occurrence") + else { + continue; + }; + let stream = publication.cursor().stream(); + for record in publication.into_records() { + if let moli_core::RendererOutputItem::Observation( + moli_core::RendererProtocolObservation::WorkerLifecycle(observation), + ) = record.into_parts().1 + { + self.occurrences.push_back((stream, observation)); + } + } + } + }) + .await + .expect("concrete Worker stream must retain its lifecycle receipt") + } +} + +async fn wait_service_worker_state( + events: &mut tokio::sync::broadcast::Receiver, + predicate: impl Fn(&moli_core::browser::ServiceWorkerSnapshot) -> bool, +) -> moli_core::browser::ServiceWorkerSnapshot { + tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + if let BrowserEvent::WorkerUpdated(WorkerSnapshot::Service { worker, .. }) = + events.recv().await.unwrap().event + && predicate(&worker) + { + return worker; + } + } + }) + .await + .expect("ServiceWorker must make native progress without Protocol draining its output") +} + +#[tokio::test] +async fn native_service_worker_snapshot_recovers_missed_restart_without_rotating_version() { + use moli_core::browser::ServiceWorkerExecution; + let server = moli_test_support::FixtureServer::spawn().await.unwrap(); + let mut fixture = NativeWorkers::start(&[]).await; + let first = fixture + .navigate_service_worker(&server.url("/native-service-worker/")) + .await; + let (_, created) = fixture.next_occurrence().await; + assert!(matches!( + created.lifecycle(), + RendererWorkerLifecycle::Service(RendererServiceWorkerLifecycle::Created { .. }) + )); + let browser = fixture.service.handle(); + let mut conn = fixture.connection(); + conn.project_browser_snapshot(browser.subscribe().unwrap().0) + .await; + let context_id = conn + .browser_context_by_browser_id(fixture.context.id()) + .unwrap() + .id + .clone(); + let target = &conn + .browser_context_by_id(&context_id) + .unwrap() + .service_worker_targets[&first.info.version_id]; + let target_id = target.target_id.clone(); + let version = target.version_identity(&context_id).unwrap(); + assert!(target.worker_running()); + assert_eq!(target.active_renderer_run(), first.execution.active_run()); + + let (_, mut events) = browser.subscribe().unwrap(); + fixture + .context + .execute_service_worker_command(ServiceWorkerCommand::StopVersion { + version_id: first.info.version_id, + }) + .unwrap(); + wait_service_worker_state(&mut events, |worker| { + worker.execution == ServiceWorkerExecution::Stopped + }) + .await; + fixture + .context + .execute_service_worker_command(ServiceWorkerCommand::Start { + scope: first.info.scope_url.parse().unwrap(), + }) + .unwrap(); + let restarted = wait_service_worker_state(&mut events, |worker| { + matches!(worker.execution, ServiceWorkerExecution::Running(_)) + }) + .await; + assert_ne!( + first.execution.active_run(), + restarted.execution.active_run() + ); + let recovered = conn + .project_browser_snapshot(browser.subscribe().unwrap().0) + .await; + assert_eq!( + protocol_event_count(&recovered, "Target.targetCreated", "service_worker"), + 0 + ); + let target = &conn + .browser_context_by_id(&context_id) + .unwrap() + .service_worker_targets[&first.info.version_id]; + assert_eq!(target.target_id, target_id); + assert_eq!(target.version_identity(&context_id), Some(version.clone())); + assert_eq!( + target.active_renderer_run(), + restarted.execution.active_run() + ); + assert!(target.worker_running()); + assert!(worker_lifecycle_prepared_outputs(&mut conn, created).is_empty()); + loop { + let (_, committed) = fixture.next_occurrence().await; + let restarted_fact = matches!(committed.lifecycle(), RendererWorkerLifecycle::Service(RendererServiceWorkerLifecycle::Started { run, .. }) if Some(run) == restarted.execution.active_run()); + assert!(worker_lifecycle_prepared_outputs(&mut conn, committed).is_empty()); + if restarted_fact { + break; + } + } + + fixture + .context + .execute_service_worker_command(ServiceWorkerCommand::StopVersion { + version_id: first.info.version_id, + }) + .unwrap(); + wait_service_worker_state(&mut events, |worker| { + worker.execution == ServiceWorkerExecution::Stopped + }) + .await; + conn.project_browser_snapshot(browser.subscribe().unwrap().0) + .await; + let target = &conn + .browser_context_by_id(&context_id) + .unwrap() + .service_worker_targets[&first.info.version_id]; + assert_eq!(target.target_id, target_id); + assert_eq!(target.version_identity(&context_id), Some(version)); + assert!(target.active_renderer_run().is_none()); + let late = service_worker_observation_prepared_outputs( + &mut conn, + context_id.clone(), + RendererServiceWorkerObservation::Console { + version_id: first.info.version_id, + run: restarted.execution.active_run().unwrap().clone(), + message: RendererServiceWorkerConsoleMessage { + message: "late retired run".into(), + args: Vec::new(), + stack: None, + }, + }, + ); + assert!(late.is_empty()); + assert!( + conn.browser_context_by_id(&context_id) + .unwrap() + .service_worker_targets[&first.info.version_id] + .active_renderer_run() + .is_none() + ); + fixture.service.shutdown(); + server.shutdown().await; +} + +#[tokio::test] +async fn native_service_worker_fifo_admits_the_real_host_before_early_console() { + let server = moli_test_support::FixtureServer::spawn().await.unwrap(); + let mut fixture = NativeWorkers::start(&[]).await; + // Create the worker before installing a live Protocol navigation-decision + // provider. This test consumes the Worker FIFO, not a Page command loop. + let worker = fixture + .navigate_service_worker(&server.url("/native-service-worker/")) + .await; + let mut conn = fixture.connection(); + conn.project_created_browser_context(fixture.context.id()); + let context_id = conn + .browser_context_by_browser_id(fixture.context.id()) + .unwrap() + .id + .clone(); + let mut console_seen = false; + let mut started_runs = 0; + tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + let moli_core::RendererOutputTransportMessage::Publication(publication) = + fixture.output.recv().await.unwrap() + else { + continue; + }; + for record in publication.into_records() { + match record.into_parts().1 { + moli_core::RendererOutputItem::Observation( + moli_core::RendererProtocolObservation::WorkerLifecycle(observation), + ) => { + let committed = observation.committed().await.unwrap(); + let started = matches!( + committed.lifecycle(), + RendererWorkerLifecycle::Service( + RendererServiceWorkerLifecycle::Started { .. } + ) + ); + let outputs = worker_lifecycle_prepared_outputs(&mut conn, committed); + worker_target_background_events_async(&mut conn, outputs).await; + if started { + assert!( + console_seen, + "top-level Console precedes public Started in the concrete FIFO" + ); + started_runs += 1; + if started_runs == 2 { + return; + } + console_seen = false; + fixture + .context + .execute_service_worker_command(ServiceWorkerCommand::StopVersion { + version_id: worker.info.version_id, + }) + .unwrap(); + fixture + .context + .execute_service_worker_command(ServiceWorkerCommand::Start { + scope: worker.info.scope_url.parse().unwrap(), + }) + .unwrap(); + } + } + moli_core::RendererOutputItem::Observation( + moli_core::RendererProtocolObservation::ServiceWorker(observation), + ) => { + if let RendererServiceWorkerObservation::Console { run, message, .. } = + &observation + { + assert_eq!(message.message, "log: before native Started"); + if started_runs == 0 { + assert_eq!(Some(run), worker.execution.active_run()); + } else { + assert_ne!(Some(run), worker.execution.active_run()); + } + let target = &conn + .browser_context_by_id(&context_id) + .unwrap() + .service_worker_targets[&worker.info.version_id]; + assert_eq!(target.active_renderer_run(), Some(run)); + assert!(!target.worker_running()); + console_seen = true; + } + let outputs = service_worker_observation_prepared_outputs( + &mut conn, + context_id.clone(), + observation, + ); + worker_target_background_events_async(&mut conn, outputs).await; + } + _ => {} + } + } + } + }) + .await + .expect("native receipt and early output must retain source FIFO order"); + fixture.service.shutdown(); + server.shutdown().await; +} + +#[tokio::test] +async fn native_shared_worker_snapshots_recover_lag_without_replaying_old_fifo_receipts() { + use tokio::sync::broadcast::error::TryRecvError; + let mut fixture = NativeWorkers::start(&["snapshot-worker"]).await; + let (_, created) = fixture.next_occurrence().await; + let browser = fixture.service.handle(); + let (snapshot, mut slow) = browser.subscribe().unwrap(); + let worker = snapshot.workers[0].clone(); + let mut conn = fixture.connection(); + let events = conn.project_browser_snapshot(snapshot).await; + assert_eq!( + protocol_event_count(&events, "Target.targetCreated", "shared_worker"), + 1 + ); + assert!(worker_lifecycle_prepared_outputs(&mut conn, created.clone()).is_empty()); + let moli_core::browser::WorkerHandle::Shared { instance, .. } = worker.handle() else { + unreachable!() + }; + assert!(fixture.context.close_shared_worker(instance)); + let (_, destroyed) = fixture.next_occurrence().await; + assert!( + matches!(destroyed.lifecycle(), moli_core::page::RendererWorkerLifecycle::SharedDestroyed(actual) if *actual == instance) + ); + // Force real loss in the bounded Browser stream, including the terminal + // occurrence. Recovery must reconcile native membership, not guess pairs. + for _ in 0..129 { + browser + .create_context( + BrowserContextStoragePartitionHandles::memory(), + StoragePartitionKind::Ephemeral, + None, + None, + ) + .unwrap() + .remove() + .unwrap(); + } + assert!(matches!(slow.try_recv(), Err(TryRecvError::Lagged(_)))); + let (snapshot, _) = browser.subscribe().unwrap(); + assert!(snapshot.workers.is_empty()); + let events = conn.project_browser_snapshot(snapshot).await; + assert_eq!( + events + .iter() + .filter(|event| (*event).clone().into_protocol_message()["method"] + == "Target.targetDestroyed") + .count(), + 1 + ); + assert!(worker_lifecycle_prepared_outputs(&mut conn, created).is_empty()); + assert!(worker_lifecycle_prepared_outputs(&mut conn, destroyed).is_empty()); + assert!( + conn.browser_context_by_browser_id(fixture.context.id()) + .unwrap() + .shared_worker_targets + .is_empty() + ); + fixture.service.shutdown(); +} + +#[tokio::test] +async fn native_dedicated_worker_snapshot_recovers_script_without_replaying_fifo_receipts() { + let mut fixture = NativeWorkers::start_named(&["native-snapshot"], true).await; + let (_, created) = fixture.next_occurrence().await; + let (_, completed) = fixture.next_occurrence().await; + assert!(matches!( + created.lifecycle(), + RendererWorkerLifecycle::DedicatedCreated(_) + )); + let RendererWorkerLifecycle::DedicatedScriptCompleted { + instance_id, + script, + } = completed.lifecycle() + else { + panic!("script completion follows creation in the source FIFO") + }; + let instance = *instance_id; + let browser = fixture.service.handle(); + let snapshot = browser.subscribe().unwrap().0; + let WorkerSnapshot::Dedicated { worker, .. } = &snapshot.workers[0] else { + unreachable!() + }; + assert!(std::sync::Arc::ptr_eq( + worker.main_script.as_ref().unwrap(), + script + )); + let mut conn = fixture.connection(); + let events = conn.project_browser_snapshot(snapshot).await; + assert_eq!( + protocol_event_count(&events, "Target.targetCreated", "worker"), + 1 + ); + let target = &conn + .browser_context_by_browser_id(fixture.context.id()) + .unwrap() + .dedicated_worker_targets[&instance]; + let target_id = target.target_id.clone(); + assert_eq!(target.url, script.script_url); + assert!( + std::ptr::eq(target.main_script().unwrap(), script.as_ref()), + "Protocol replay must share the native fact, not copy its response" + ); + assert!(worker_lifecycle_prepared_outputs(&mut conn, created.clone()).is_empty()); + assert!(worker_lifecycle_prepared_outputs(&mut conn, completed.clone()).is_empty()); + let events = conn + .project_browser_snapshot(browser.subscribe().unwrap().0) + .await; + assert_eq!( + protocol_event_count(&events, "Target.targetCreated", "worker"), + 0 + ); + assert_eq!( + protocol_event_count(&events, "Target.targetInfoChanged", "worker"), + 0 + ); + + assert!(fixture.context.close_dedicated_worker(instance)); + let (_, destroyed) = fixture.next_occurrence().await; + assert_eq!( + destroyed.lifecycle(), + &RendererWorkerLifecycle::DedicatedDestroyed(instance) + ); + let snapshot = browser.subscribe().unwrap().0; + assert!(snapshot.workers.is_empty()); + let events = conn.project_browser_snapshot(snapshot).await; + assert_eq!( + events + .iter() + .filter(|event| { + let message = (*event).clone().into_protocol_message(); + message["method"] == "Target.targetDestroyed" + && message["params"]["targetId"] == target_id + }) + .count(), + 1 + ); + for receipt in [created, completed, destroyed] { + assert!(worker_lifecycle_prepared_outputs(&mut conn, receipt).is_empty()); + } + assert!( + conn.browser_context_by_browser_id(fixture.context.id()) + .unwrap() + .dedicated_worker_targets + .is_empty() + ); + fixture.service.shutdown(); +} + +#[tokio::test] +async fn native_dedicated_worker_old_document_receipts_cannot_create_on_replacement() { + let mut fixture = NativeWorkers::start_named(&["old-document"], true).await; + let (_, created) = fixture.next_occurrence().await; + let (_, completed) = fixture.next_occurrence().await; + let RendererWorkerLifecycle::DedicatedCreated(old) = created.lifecycle() else { + unreachable!() + }; + let old_instance = old.instance_id; + let browser = fixture.service.handle(); + let (snapshot, mut events) = browser.subscribe().unwrap(); + let contents = snapshot.web_contents[0]; + let navigation = fixture.context.navigate_document( + contents, + moli_core::browser::web_contents::NavigationRequestInterception::new( + "data:text/html,".parse().unwrap(), + "GET".into(), None, Vec::new().into(), NavigationRequestLoadPolicy::BrowserInitiated, + ), + ).unwrap(); + let moli_core::browser::BrowserNavigationOutcome::Document(document) = + navigation.wait().await.unwrap() + else { + unreachable!() + }; + let replacement = tokio::time::timeout(std::time::Duration::from_secs(5), async { + let mut retired = false; + let mut replacement = None; + loop { + match events.recv().await.unwrap().event { + BrowserEvent::WorkerUpdated(WorkerSnapshot::Dedicated { worker, .. }) + if worker.info.instance_id != old_instance => + { + replacement = Some(worker) + } + BrowserEvent::WorkerDestroyed(moli_core::browser::WorkerHandle::Dedicated { + instance, + .. + }) if instance == old_instance => retired = true, + _ => {} + } + if retired && let Some(worker) = replacement.take() { + break worker; + } + } + }) + .await + .expect("replacement must complete while the exact old Worker retires"); + let mut conn = fixture.connection(); + conn.project_created_browser_context(fixture.context.id()); + conn.project_created_web_contents(contents).await; + conn.project_browser_document_commit(document.document) + .await; + assert!( + conn.browser_context_by_browser_id(fixture.context.id()) + .unwrap() + .worker_snapshot_sequence + .is_none(), + "exercise exact owner rejection, not recovery suppression" + ); + assert!(worker_lifecycle_prepared_outputs(&mut conn, created).is_empty()); + assert!(worker_lifecycle_prepared_outputs(&mut conn, completed).is_empty()); + let mut projected = false; + while !projected { + let (_, receipt) = fixture.next_occurrence().await; + projected = matches!(receipt.lifecycle(), + RendererWorkerLifecycle::DedicatedScriptCompleted { instance_id, .. } + if *instance_id == replacement.info.instance_id); + let outputs = worker_lifecycle_prepared_outputs(&mut conn, receipt); + worker_target_background_events_async(&mut conn, outputs).await; + } + let context = conn + .browser_context_by_browser_id(fixture.context.id()) + .unwrap(); + assert_eq!(context.dedicated_worker_targets.len(), 1); + let target = &context.dedicated_worker_targets[&replacement.info.instance_id]; + assert_eq!(target.name, "replacement"); + assert_eq!(target.url, replacement.main_script.unwrap().script_url); + fixture.service.shutdown(); +} + +#[tokio::test] +async fn native_shared_worker_interleaved_sources_do_not_share_a_fifo_progress_cursor() { + let mut fixture = NativeWorkers::start(&["first-source", "second-source"]).await; + let (_, first) = fixture.next_occurrence().await; + let (_, second) = fixture.next_occurrence().await; + let mut occurrences = [first, second]; + occurrences.sort_by_key(|occurrence| occurrence.browser_sequence()); + let mut conn = fixture.connection(); + conn.project_created_browser_context(fixture.context.id()); + // Each worker has its own FIFO; arrival order across sources is not the + // Browser total order. Neither valid creation may be discarded as stale. + for occurrence in occurrences.into_iter().rev() { + let outputs = worker_lifecycle_prepared_outputs(&mut conn, occurrence); + assert!(!outputs.is_empty()); + let events = worker_target_background_events_async(&mut conn, outputs).await; + assert_eq!( + protocol_event_count(&events, "Target.targetCreated", "shared_worker"), + 1 + ); + } + assert_eq!( + conn.browser_context_by_browser_id(fixture.context.id()) + .unwrap() + .shared_worker_targets + .len(), + 2 + ); + fixture.service.shutdown(); +} + +#[tokio::test] +async fn native_shared_worker_late_receipt_and_stream_cannot_rebind_a_reused_context_wire_id() { + let mut fixture = NativeWorkers::start(&["retired-context-worker"]).await; + let (stream, created) = fixture.next_occurrence().await; + let mut conn = fixture.connection(); + conn.browser_context = Some(BrowserContext::from_browser_handle( + "BID-reused".into(), + fixture.context.clone(), + )); + let owner = RendererPublicationOwner::BrowserContext { + browser_context_id: "BID-reused".into(), + }; + assert!(owner.resolve(&conn, stream).is_some()); + fixture.context.remove().unwrap(); + let replacement = fixture + .service + .handle() + .create_context( + BrowserContextStoragePartitionHandles::memory(), + StoragePartitionKind::Ephemeral, + None, + None, + ) + .unwrap(); + conn.browser_context = Some(BrowserContext::from_browser_handle( + "BID-reused".into(), + replacement, + )); + assert!(owner.resolve(&conn, stream).is_none()); + assert!(worker_lifecycle_prepared_outputs(&mut conn, created).is_empty()); + assert!( + conn.browser_context + .as_ref() + .unwrap() + .shared_worker_targets + .is_empty() + ); + fixture.service.shutdown(); +} + +fn protocol_event_count( + events: &[BackgroundProtocolEvent], + method: &str, + target_type: &str, +) -> usize { + events + .iter() + .filter(|event| { + let message = (*event).clone().into_protocol_message(); + message["method"] == method && message["params"]["targetInfo"]["type"] == target_type + }) + .count() +} diff --git a/moli-protocol/src/testing.rs b/moli-protocol/src/testing.rs index 1e6e3611f..f2d931dd4 100644 --- a/moli-protocol/src/testing.rs +++ b/moli-protocol/src/testing.rs @@ -81,7 +81,6 @@ impl CdpConnection { let waiter = self .start_native_navigation_fixture_for_test( owner, - crate::domains::page::LOADER_ID, moli_core::browser::web_contents::NavigationRequestInterception::new( url::Url::parse(raw_url).expect("fixture URL"), "GET".into(), @@ -361,7 +360,6 @@ impl TestContext { .conn .start_native_navigation_fixture_for_test( &owner, - crate::domains::page::LOADER_ID, moli_core::browser::web_contents::NavigationRequestInterception::new( requested_url, "GET".into(), diff --git a/moli-renderer-v8/src/context_bootstrap/worker_host/dispatch.rs b/moli-renderer-v8/src/context_bootstrap/worker_host/dispatch.rs index 5523fa31f..1a7378d6e 100644 --- a/moli-renderer-v8/src/context_bootstrap/worker_host/dispatch.rs +++ b/moli-renderer-v8/src/context_bootstrap/worker_host/dispatch.rs @@ -163,6 +163,7 @@ pub(crate) fn dispatch_worker_event<'s>( | WorkerToParentMessage::ServiceWorkerClientsClaim { .. } | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } | WorkerToParentMessage::SharedWorkerClosed + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) => false, } } diff --git a/moli-renderer-v8/src/lib.rs b/moli-renderer-v8/src/lib.rs index 592d4bea8..81ee8e806 100644 --- a/moli-renderer-v8/src/lib.rs +++ b/moli-renderer-v8/src/lib.rs @@ -232,20 +232,21 @@ pub use runtime::{ RendererBrowserContextRuntimeOwnerAccess, RendererCaptureScreencastFrameReply, RendererCaptureScreencastFrameRequest, RendererCaptureScreenshotReply, RendererCaptureScreenshotRequest, RendererCapturedScreencastFrame, RendererCapturedScreenshot, - RendererCommandTurnCompletion, RendererCommandTurnOutput, RendererCountEntry, - RendererCssInspection, RendererDedicatedWorkerTargetEvent, RendererDedicatedWorkerTargetInfo, - RendererDevToolsAgentToken, RendererDocumentBoxModel, RendererDocumentChildNodeSnapshotEvent, - RendererDocumentChildNodeSnapshotEvents, RendererDocumentChildNodeSnapshots, - RendererDocumentFrontendNodeIdsResolution, RendererDocumentHitTestResult, - RendererDocumentIsolateAccountingDiagnostics, RendererDocumentLifecycleEvent, - RendererDocumentLifecycleEventKind, RendererDocumentLifecycleIdentity, - RendererDocumentLifecycleMilestone, RendererDocumentLifecycleObservation, - RendererDocumentLifecycleSnapshot, RendererDocumentLifecycleWaitOutcome, - RendererDocumentLifecycleWaiter, RendererDocumentNodeAttributesResolution, - RendererDocumentNodeClientRect, RendererDocumentNodeGeometry, - RendererDocumentNodePropertyResolution, RendererDocumentNodeReference, - RendererDocumentNodeTextResolution, RendererDocumentOptions, RendererDocumentQuerySelectorNode, - RendererDocumentQuerySelectorResolution, + RendererCommandTurnCompletion, RendererCommandTurnOutput, RendererCommittedWorkerLifecycle, + RendererCountEntry, RendererCssInspection, RendererDedicatedWorkerMainScript, + RendererDedicatedWorkerMainScriptOutcome, RendererDedicatedWorkerObservation, + RendererDedicatedWorkerTargetInfo, RendererDevToolsAgentToken, RendererDocumentBoxModel, + RendererDocumentChildNodeSnapshotEvent, RendererDocumentChildNodeSnapshotEvents, + RendererDocumentChildNodeSnapshots, RendererDocumentFrontendNodeIdsResolution, + RendererDocumentHitTestResult, RendererDocumentIsolateAccountingDiagnostics, + RendererDocumentLifecycleEvent, RendererDocumentLifecycleEventKind, + RendererDocumentLifecycleIdentity, RendererDocumentLifecycleMilestone, + RendererDocumentLifecycleObservation, RendererDocumentLifecycleSnapshot, + RendererDocumentLifecycleWaitOutcome, RendererDocumentLifecycleWaiter, + RendererDocumentNodeAttributesResolution, RendererDocumentNodeClientRect, + RendererDocumentNodeGeometry, RendererDocumentNodePropertyResolution, + RendererDocumentNodeReference, RendererDocumentNodeTextResolution, RendererDocumentOptions, + RendererDocumentQuerySelectorNode, RendererDocumentQuerySelectorResolution, RendererDocumentQuerySelectorWithChildNodeSnapshotEvents, RendererDocumentSourcedSameDocumentNavigation, RendererDocumentSourcedTopLevelLocationNavigation, RendererDocumentTerminationReason, @@ -311,13 +312,15 @@ pub use runtime::{ RendererScriptSourceMemoryDiagnostics, RendererScrollIntoViewResult, RendererServiceWorkerConsoleMessage, RendererServiceWorkerExceptionMessage, RendererServiceWorkerFetchDiagnostic, RendererServiceWorkerFetchDiagnosticResult, - RendererServiceWorkerMainResourceFetch, RendererServiceWorkerRunIdentity, - RendererServiceWorkerTargetEvent, RendererServiceWorkerTargetInfo, - RendererServiceWorkerVersionStatus, RendererSetDocumentContentResult, - RendererSharedWorkerConsoleMessage, RendererSharedWorkerTargetEvent, - RendererSharedWorkerTargetInfo, RendererStyleSheetHeader, RendererStyleSheetInventoryUpdate, - RendererStyleSheetPayload, RendererSyntheticResponseBody, RendererTextSearchMatch, - RendererTouchPoint, RendererVisualStateToken, RendererWindowDocumentSource, + RendererServiceWorkerLifecycle, RendererServiceWorkerMainResourceFetch, + RendererServiceWorkerObservation, RendererServiceWorkerRunIdentity, + RendererServiceWorkerTargetInfo, RendererServiceWorkerVersionStatus, + RendererSetDocumentContentResult, RendererSharedWorkerConsoleMessage, + RendererSharedWorkerObservation, RendererSharedWorkerTargetInfo, RendererStyleSheetHeader, + RendererStyleSheetInventoryUpdate, RendererStyleSheetPayload, RendererSyntheticResponseBody, + RendererTextSearchMatch, RendererTouchPoint, RendererVisualStateToken, + RendererWindowDocumentSource, RendererWorkerInspectionEndpoint, RendererWorkerInspectionTarget, + RendererWorkerLifecycle, RendererWorkerLifecycleInput, RendererWorkerLifecycleObservation, RuntimeConsoleMessageSnapshot, renderer_output_transport_channel, }; pub use service_worker_runtime::{ diff --git a/moli-renderer-v8/src/native_bridge/context_host/workers/registration.rs b/moli-renderer-v8/src/native_bridge/context_host/workers/registration.rs index 511f81568..8b8d57a1f 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/workers/registration.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/workers/registration.rs @@ -607,7 +607,7 @@ impl JsContextHost { .page_dedicated_worker_client_event_sender() .page_token(); self.append_dedicated_worker_target_lifecycle( - crate::runtime::RendererDedicatedWorkerTargetEvent::Created( + crate::runtime::RendererWorkerLifecycle::DedicatedCreated( crate::runtime::RendererDedicatedWorkerTargetInfo { owner_local_host_id: page_token.local_host_id(), page_id: page_token.page_id(), @@ -629,10 +629,13 @@ impl JsContextHost { /// deliberately omit that journal while still exercising Worker state. fn append_dedicated_worker_target_lifecycle( &self, - event: crate::runtime::RendererDedicatedWorkerTargetEvent, + event: crate::runtime::RendererWorkerLifecycle, ) { - let appended = self.append_live_turn_owner_action( - crate::runtime::RendererOwnerAction::DedicatedWorkerTargetLifecycle(event), + let observation = self + .browser_context_runtime() + .report_worker_lifecycle(event); + let appended = self.append_live_turn_observation( + crate::runtime::RendererProtocolObservation::WorkerLifecycle(observation), ); debug_assert!( appended || cfg!(test), @@ -654,10 +657,14 @@ impl JsContextHost { } let instance_id = state.renderer_instance_id; self.append_dedicated_worker_target_lifecycle( - crate::runtime::RendererDedicatedWorkerTargetEvent::ScriptLoaded { + crate::runtime::RendererWorkerLifecycle::DedicatedScriptCompleted { instance_id, - script_url, - response, + script: std::sync::Arc::new(crate::runtime::RendererDedicatedWorkerMainScript { + script_url, + outcome: crate::runtime::RendererDedicatedWorkerMainScriptOutcome::Loaded( + response, + ), + }), }, ); true @@ -678,11 +685,15 @@ impl JsContextHost { } let instance_id = state.renderer_instance_id; self.append_dedicated_worker_target_lifecycle( - crate::runtime::RendererDedicatedWorkerTargetEvent::ScriptLoadFailed { + crate::runtime::RendererWorkerLifecycle::DedicatedScriptCompleted { instance_id, - script_url, - error_message, - response, + script: std::sync::Arc::new(crate::runtime::RendererDedicatedWorkerMainScript { + script_url, + outcome: crate::runtime::RendererDedicatedWorkerMainScriptOutcome::Failed { + error_message, + response, + }, + }), }, ); true @@ -701,12 +712,14 @@ impl JsContextHost { } let instance_id = state.renderer_instance_id; for batch in batches { - self.append_dedicated_worker_target_lifecycle( - crate::runtime::RendererDedicatedWorkerTargetEvent::RuntimeInspectorMessages { - instance_id, - inspector_session_id: batch.inspector_session_id, - messages: batch.messages, - }, + self.append_live_turn_observation( + crate::runtime::RendererProtocolObservation::DedicatedWorker( + crate::runtime::RendererDedicatedWorkerObservation::RuntimeInspectorMessages { + instance_id, + inspector_session_id: batch.inspector_session_id, + messages: batch.messages, + }, + ), ); } true @@ -723,15 +736,17 @@ impl JsContextHost { if !state.target_created { return true; } - self.append_dedicated_worker_target_lifecycle( - crate::runtime::RendererDedicatedWorkerTargetEvent::Console { - instance_id: state.renderer_instance_id, - message: crate::runtime::RendererSharedWorkerConsoleMessage { - message: message.message, - args: message.args, - stack: message.stack, + self.append_live_turn_observation( + crate::runtime::RendererProtocolObservation::DedicatedWorker( + crate::runtime::RendererDedicatedWorkerObservation::Console { + instance_id: state.renderer_instance_id, + message: crate::runtime::RendererSharedWorkerConsoleMessage { + message: message.message, + args: message.args, + stack: message.stack, + }, }, - }, + ), ); true } @@ -1356,9 +1371,9 @@ impl JsContextHost { .unregister_dedicated_worker_devtools_handle(state.renderer_instance_id); if state.target_created { self.append_dedicated_worker_target_lifecycle( - crate::runtime::RendererDedicatedWorkerTargetEvent::Destroyed { - instance_id: state.renderer_instance_id, - }, + crate::runtime::RendererWorkerLifecycle::DedicatedDestroyed( + state.renderer_instance_id, + ), ); } let owner = state.owner.owner(); @@ -1394,28 +1409,11 @@ impl JsContextHost { } pub(crate) fn shutdown_workers(&mut self) { - let browser_context_runtime = self.browser_context_runtime(); - let workers = std::mem::take(&mut self.workers); - for (_, state) in workers { - browser_context_runtime - .unregister_dedicated_worker_devtools_handle(state.renderer_instance_id); - match state.execution { - WorkerExecutionState::Loading { - load_task, - reserved_service_worker_client_id, - .. - } => { - if let Some(load_task) = load_task { - load_task.abort(); - } - if let Some(client_id) = reserved_service_worker_client_id { - browser_context_runtime.unregister_service_worker_client(client_id); - } - } - WorkerExecutionState::Running { handle } => { - handle.terminate(); - } - } + // Use the same single-consumption retirement as explicit terminate and + // realm destruction, including the native occurrence before dropping + // each running handle (whose Drop terminates its thread). + for worker_id in self.workers.keys().copied().collect::>() { + self.forget_worker(worker_id); } } } diff --git a/moli-renderer-v8/src/runtime/browser_context_runtime.rs b/moli-renderer-v8/src/runtime/browser_context_runtime.rs index 125c91aaa..81f2e9c51 100644 --- a/moli-renderer-v8/src/runtime/browser_context_runtime.rs +++ b/moli-renderer-v8/src/runtime/browser_context_runtime.rs @@ -201,6 +201,7 @@ struct RendererBrowserContextRuntimeInner { // Commit style and representations together, independently of any page's // V8 objects, so other pages can read a coherent snapshot in their own realm. clipboard_snapshot: Mutex, + worker_lifecycle: super::RendererWorkerLifecycleReporter, message_port_registry: crate::message_port_runtime::SharedMessagePortRegistry, broadcast_channel_registry: crate::broadcast_channel_runtime::SharedBroadcastChannelRegistry, browser_resource_runtime: crate::network::BrowserResourceRuntimeBinding, @@ -390,6 +391,30 @@ impl RendererBrowserContextRuntime { *self.inner.clipboard_snapshot.lock() = snapshot; } + /// Resolves once, before the caller yields. The returned capability can + /// dispatch inspection only; it cannot control the Context or other workers. + pub fn worker_inspection_endpoint( + &self, + target: super::RendererWorkerInspectionTarget, + ) -> Option { + match target { + super::RendererWorkerInspectionTarget::Dedicated(instance_id) => { + self.dedicated_worker_inspection_endpoint(instance_id) + } + super::RendererWorkerInspectionTarget::Shared(instance_id) => self + .shared_worker_runtime_if_initialized()? + .inspection_endpoint(instance_id), + super::RendererWorkerInspectionTarget::Service { version_id, run } => self + .service_worker_runtime_for_existing_registration()? + .inspection_endpoint( + crate::service_worker_runtime::ServiceWorkerVersionId::from_u64_for_binding( + version_id, + ), + &run, + ), + } + } + // 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)] @@ -563,14 +588,15 @@ impl RendererBrowserContextRuntime { NEXT_RENDERER_BROWSER_CONTEXT_RUNTIME_ID.fetch_add(1, Ordering::Relaxed), ); let renderer_output_transport_tx = RendererOutputTransportSenderSlot::default(); + let worker_lifecycle = super::RendererWorkerLifecycleReporter::new(id); let shared_worker_runtime = match shared_worker_runtime { Some(service) => shared_workers::LazySharedWorkerRuntime::from_service( service, - id, + worker_lifecycle.clone(), renderer_output_transport_tx.clone(), ), None => shared_workers::LazySharedWorkerRuntime::new( - id, + worker_lifecycle.clone(), renderer_output_transport_tx.clone(), ), }; @@ -578,13 +604,14 @@ impl RendererBrowserContextRuntime { service_worker_resource_store, service_worker_context_runtime, browser_resource_runtime.clone(), - id, + worker_lifecycle.clone(), renderer_output_transport_tx.clone(), ); let runtime = Self { inner: Arc::new(RendererBrowserContextRuntimeInner { id, clipboard_snapshot: Mutex::new(ClipboardSnapshot::default()), + worker_lifecycle, message_port_registry, broadcast_channel_registry, browser_resource_runtime: browser_resource_runtime.clone(), @@ -622,6 +649,22 @@ impl RendererBrowserContextRuntime { self.inner.id } + /// The physical Browser owner installs this independently of DevTools output + /// transport. The callback must enqueue work, never wait on the owner. + pub fn install_worker_lifecycle_handler( + &self, + handler: impl Fn(super::RendererWorkerLifecycleInput) + Send + Sync + 'static, + ) { + self.inner.worker_lifecycle.install_handler(handler); + } + + pub(crate) fn report_worker_lifecycle( + &self, + lifecycle: super::RendererWorkerLifecycle, + ) -> super::RendererWorkerLifecycleObservation { + self.inner.worker_lifecycle.report(lifecycle) + } + pub(crate) fn set_renderer_output_transport_sender( &self, sender: super::RendererOutputTransportSender, diff --git a/moli-renderer-v8/src/runtime/browser_context_runtime/dedicated_workers.rs b/moli-renderer-v8/src/runtime/browser_context_runtime/dedicated_workers.rs index a23cf7305..93d2c8177 100644 --- a/moli-renderer-v8/src/runtime/browser_context_runtime/dedicated_workers.rs +++ b/moli-renderer-v8/src/runtime/browser_context_runtime/dedicated_workers.rs @@ -1,6 +1,4 @@ -use tokio::sync::oneshot; - -use crate::runtime::{RendererRuntimeInspectorMessage, RendererRuntimeInspectorResponseSender}; +use crate::runtime::RendererWorkerInspectionEndpoint; use crate::worker::{WorkerDevToolsHandle, WorkerHandle}; use super::{DedicatedWorkerDevToolsTarget, RendererBrowserContextRuntime}; @@ -58,130 +56,17 @@ impl RendererBrowserContextRuntime { .map(|target| target.handle.clone()) } - fn dedicated_worker_devtools_target( + pub(super) fn dedicated_worker_inspection_endpoint( &self, instance_id: u64, - ) -> Option { - self.inner - .dedicated_worker_devtools_targets - .lock() - .get(&instance_id) - .cloned() - } - - pub async fn dispatch_dedicated_worker_runtime_protocol_message( - &self, - instance_id: u64, - inspector_session_id: Option, - raw_json: String, - ) -> Result, String> { - self.dispatch_dedicated_worker_runtime_protocol_message_with_optional_deferred_response( - instance_id, - inspector_session_id, - raw_json, - None, - ) - .await - } - - pub async fn dispatch_dedicated_worker_runtime_protocol_message_with_deferred_response( - &self, - instance_id: u64, - inspector_session_id: Option, - raw_json: String, - deferred_response: RendererRuntimeInspectorResponseSender, - ) -> Result, String> { - self.dispatch_dedicated_worker_runtime_protocol_message_with_optional_deferred_response( - instance_id, - inspector_session_id, - raw_json, - Some(deferred_response), - ) - .await - } - - pub async fn dispatch_dedicated_worker_runtime_protocol_message_with_devtools_session_response( - &self, - instance_id: u64, - inspector_session_id: String, - raw_json: String, - response: RendererRuntimeInspectorResponseSender, - ) -> Result { - let Some(target) = self.dedicated_worker_devtools_target(instance_id) else { - return Err("DedicatedWorkerRuntimeUnavailable".to_owned()); - }; - let Some(output_journal) = target.output_journal else { - return Err("DedicatedWorkerRuntimeUnavailable".to_owned()); - }; - let response = response - .route_to_worker_devtools_session_output(inspector_session_id.clone(), output_journal); - let error_response = response.clone(); - let settlement = response - .take_session_response_settlement_receiver() - .expect("a Worker DevTools response must own one settlement receiver"); - let (response_tx, response_rx) = oneshot::channel(); - let dispatched = target.handle.dispatch_runtime_protocol_message( - Some(inspector_session_id), - raw_json, - Some(response), - response_tx, - ); - let dispatch = if dispatched { - response_rx - .await - .unwrap_or_else(|_| Err("DedicatedWorkerRuntimeUnavailable".to_owned())) - } else { - Err("DedicatedWorkerRuntimeUnavailable".to_owned()) - }; - Ok( - crate::runtime::CompletedWorkerRuntimeInspectorCommandDispatch::finish( - dispatch, - settlement, - error_response, - ), - ) - } - - async fn dispatch_dedicated_worker_runtime_protocol_message_with_optional_deferred_response( - &self, - instance_id: u64, - inspector_session_id: Option, - raw_json: String, - deferred_response: Option, - ) -> Result, String> { - let Some(handle) = self.dedicated_worker_devtools_handle(instance_id) else { - return Err("DedicatedWorkerRuntimeUnavailable".to_owned()); - }; - let (response_tx, response_rx) = oneshot::channel(); - if !handle.dispatch_runtime_protocol_message( - inspector_session_id, - raw_json, - deferred_response, - response_tx, - ) { - return Err("DedicatedWorkerRuntimeUnavailable".to_owned()); - } - response_rx - .await - .map_err(|_| "DedicatedWorkerRuntimeUnavailable".to_owned())? - } - - pub fn attach_dedicated_worker_runtime_inspector_session( - &self, - instance_id: u64, - inspector_session_id: Option, - ) -> bool { - self.dedicated_worker_devtools_handle(instance_id) - .is_some_and(|handle| handle.attach_runtime_inspector_session(inspector_session_id)) - } - - pub fn detach_dedicated_worker_runtime_inspector_session( - &self, - instance_id: u64, - inspector_session_id: Option, - ) -> bool { - self.dedicated_worker_devtools_handle(instance_id) - .is_some_and(|handle| handle.detach_runtime_inspector_session(inspector_session_id)) + ) -> Option { + let targets = self.inner.dedicated_worker_devtools_targets.lock(); + let target = targets.get(&instance_id)?; + Some(RendererWorkerInspectionEndpoint::new( + target.handle.clone(), + target.output_journal.clone(), + "DedicatedWorkerRuntimeUnavailable", + )) } pub fn run_dedicated_worker_if_waiting_for_debugger_for_devtools( diff --git a/moli-renderer-v8/src/runtime/browser_context_runtime/service_worker_runtime.rs b/moli-renderer-v8/src/runtime/browser_context_runtime/service_worker_runtime.rs index 0c2ac115b..e21c2883c 100644 --- a/moli-renderer-v8/src/runtime/browser_context_runtime/service_worker_runtime.rs +++ b/moli-renderer-v8/src/runtime/browser_context_runtime/service_worker_runtime.rs @@ -5,7 +5,6 @@ use url::Url; use crate::{ page_task_queue::RendererPageServiceWorkerTaskSender, - runtime::{RendererRuntimeInspectorMessage, RendererRuntimeInspectorResponseSender}, service_worker_runtime::{ ServiceWorkerClientFrameType, ServiceWorkerClientId, ServiceWorkerRegistrationId, ServiceWorkerRuntimeOwnerWake, ServiceWorkerRuntimeOwnerWakeSender, ServiceWorkerVersionId, @@ -24,7 +23,7 @@ pub(super) struct LazyServiceWorkerRuntime { restored_worker_context_runtime: super::RendererWorkerContextRuntime, browser_resource_runtime: crate::network::BrowserResourceRuntimeBinding, client_id_allocator: crate::service_worker_runtime::ServiceWorkerClientIdAllocator, - browser_context_runtime_id: crate::runtime::RendererBrowserContextRuntimeId, + worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, output_transport: crate::runtime::RendererOutputTransportSenderSlot, } @@ -60,7 +59,7 @@ impl LazyServiceWorkerRuntime { resource_store: crate::SharedServiceWorkerResourceStore, restored_worker_context_runtime: super::RendererWorkerContextRuntime, browser_resource_runtime: crate::network::BrowserResourceRuntimeBinding, - browser_context_runtime_id: crate::runtime::RendererBrowserContextRuntimeId, + worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, output_transport: crate::runtime::RendererOutputTransportSenderSlot, ) -> Self { Self { @@ -75,7 +74,7 @@ impl LazyServiceWorkerRuntime { restored_worker_context_runtime, browser_resource_runtime, client_id_allocator: Default::default(), - browser_context_runtime_id, + worker_lifecycle, output_transport, } } @@ -106,7 +105,7 @@ impl LazyServiceWorkerRuntime { self.restored_worker_context_runtime.clone(), self.browser_resource_runtime.clone(), self.client_id_allocator.clone(), - self.browser_context_runtime_id, + self.worker_lifecycle.clone(), self.output_transport.clone(), ); for sender in owner_wake_senders.into_senders() { @@ -353,7 +352,7 @@ impl LazyServiceWorkerRuntime { } impl RendererBrowserContextRuntime { - fn service_worker_runtime_for_existing_registration( + pub(super) fn service_worker_runtime_for_existing_registration( &self, ) -> Option { self.inner @@ -377,78 +376,6 @@ impl RendererBrowserContextRuntime { .map_or(0, |runtime| runtime.drain_service_lane()) } - pub async fn dispatch_service_worker_runtime_protocol_message( - &self, - version_id: u64, - inspector_session_id: Option, - raw_json: String, - ) -> Result, String> { - let Some(runtime) = self.service_worker_runtime_for_existing_registration() else { - return Err("ServiceWorkerRuntimeUnavailable".to_owned()); - }; - runtime - .dispatch_runtime_protocol_message( - ServiceWorkerVersionId::from_u64_for_binding(version_id), - inspector_session_id, - raw_json, - ) - .await - } - - pub async fn dispatch_service_worker_runtime_protocol_message_with_deferred_response( - &self, - version_id: u64, - inspector_session_id: Option, - raw_json: String, - deferred_response: RendererRuntimeInspectorResponseSender, - ) -> Result, String> { - let Some(runtime) = self.service_worker_runtime_for_existing_registration() else { - return Err("ServiceWorkerRuntimeUnavailable".to_owned()); - }; - runtime - .dispatch_runtime_protocol_message_with_deferred_response( - ServiceWorkerVersionId::from_u64_for_binding(version_id), - inspector_session_id, - raw_json, - deferred_response, - ) - .await - } - - pub async fn dispatch_service_worker_runtime_protocol_message_with_devtools_session_response( - &self, - version_id: u64, - inspector_session_id: String, - raw_json: String, - response: RendererRuntimeInspectorResponseSender, - ) -> Result { - let Some(runtime) = self.service_worker_runtime_for_existing_registration() else { - return Err("ServiceWorkerRuntimeUnavailable".to_owned()); - }; - runtime - .dispatch_runtime_protocol_message_with_devtools_session_response( - ServiceWorkerVersionId::from_u64_for_binding(version_id), - inspector_session_id, - raw_json, - response, - ) - .await - } - - pub fn detach_service_worker_runtime_inspector_session( - &self, - version_id: u64, - inspector_session_id: Option, - ) -> bool { - self.service_worker_runtime_for_existing_registration() - .is_some_and(|runtime| { - runtime.detach_runtime_inspector_session( - ServiceWorkerVersionId::from_u64_for_binding(version_id), - inspector_session_id, - ) - }) - } - pub fn unregister_service_worker_scope_for_devtools( &self, scope_url: &url::Url, diff --git a/moli-renderer-v8/src/runtime/browser_context_runtime/shared_workers.rs b/moli-renderer-v8/src/runtime/browser_context_runtime/shared_workers.rs index b5528c18c..ff95d0b30 100644 --- a/moli-renderer-v8/src/runtime/browser_context_runtime/shared_workers.rs +++ b/moli-renderer-v8/src/runtime/browser_context_runtime/shared_workers.rs @@ -12,10 +12,7 @@ use moli_shared_worker::{ use parking_lot::Mutex; use super::RendererBrowserContextRuntime; -use crate::runtime::{ - RendererOwnerLocalHostId, RendererRuntimeInspectorMessage, - RendererRuntimeInspectorResponseSender, -}; +use crate::runtime::RendererOwnerLocalHostId; /// Defers the browser-context SharedWorker registry until the first actual /// `connect_shared_worker` call. ID allocation and owner routing do not require @@ -23,7 +20,7 @@ use crate::runtime::{ pub(super) struct LazySharedWorkerRuntime { state: Mutex, client_owner_id_allocator: crate::shared_worker_runtime::SharedWorkerClientOwnerIdAllocator, - browser_context_runtime_id: crate::runtime::RendererBrowserContextRuntimeId, + worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, output_transport: crate::runtime::RendererOutputTransportSenderSlot, } @@ -45,7 +42,7 @@ impl std::fmt::Debug for LazySharedWorkerRuntime { impl LazySharedWorkerRuntime { pub(super) fn new( - browser_context_runtime_id: crate::runtime::RendererBrowserContextRuntimeId, + worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, output_transport: crate::runtime::RendererOutputTransportSenderSlot, ) -> Self { Self { @@ -54,22 +51,21 @@ impl LazySharedWorkerRuntime { owner_local_host_id: None, }), client_owner_id_allocator: Default::default(), - browser_context_runtime_id, + worker_lifecycle, output_transport, } } pub(super) fn from_service( service: crate::shared_worker_runtime::SharedWorkerRuntimeService, - browser_context_runtime_id: crate::runtime::RendererBrowserContextRuntimeId, + worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, output_transport: crate::runtime::RendererOutputTransportSenderSlot, ) -> Self { - service - .configure_target_output_streams(browser_context_runtime_id, output_transport.clone()); + service.configure_target_output_streams(worker_lifecycle.clone(), output_transport.clone()); Self { client_owner_id_allocator: service.client_owner_id_allocator(), state: Mutex::new(LazySharedWorkerRuntimeState::Live(service)), - browser_context_runtime_id, + worker_lifecycle, output_transport, } } @@ -93,7 +89,7 @@ impl LazySharedWorkerRuntime { self.client_owner_id_allocator.clone(), ); service.configure_target_output_streams( - self.browser_context_runtime_id, + self.worker_lifecycle.clone(), self.output_transport.clone(), ); for sender in owner_wake_senders.into_senders() { @@ -147,7 +143,7 @@ impl LazySharedWorkerRuntime { } impl RendererBrowserContextRuntime { - fn shared_worker_runtime_if_initialized( + pub(super) fn shared_worker_runtime_if_initialized( &self, ) -> Option { self.inner.shared_worker_runtime.get() @@ -193,12 +189,9 @@ impl RendererBrowserContextRuntime { .map_or(0, |runtime| runtime.drain_service_lane()) } - pub fn close_shared_worker_for_target_close( - &self, - instance_id: SharedWorkerInstanceId, - ) -> bool { + pub fn close_shared_worker(&self, instance_id: SharedWorkerInstanceId) -> bool { self.shared_worker_runtime_if_initialized() - .is_some_and(|runtime| runtime.close_instance_for_devtools_target_close(instance_id)) + .is_some_and(|runtime| runtime.close_instance(instance_id)) } pub(crate) fn remove_shared_worker_client(&self, client_id: SharedWorkerClientId) { @@ -447,71 +440,6 @@ impl RendererBrowserContextRuntime { ) }) } - - pub async fn dispatch_shared_worker_runtime_protocol_message( - &self, - instance_id: SharedWorkerInstanceId, - inspector_session_id: Option, - raw_json: String, - ) -> Result, String> { - let Some(runtime) = self.shared_worker_runtime_if_initialized() else { - return Err("SharedWorkerRuntimeUnavailable".to_owned()); - }; - runtime - .dispatch_runtime_protocol_message(instance_id, inspector_session_id, raw_json) - .await - } - - pub async fn dispatch_shared_worker_runtime_protocol_message_with_deferred_response( - &self, - instance_id: SharedWorkerInstanceId, - inspector_session_id: Option, - raw_json: String, - deferred_response: RendererRuntimeInspectorResponseSender, - ) -> Result, String> { - let Some(runtime) = self.shared_worker_runtime_if_initialized() else { - return Err("SharedWorkerRuntimeUnavailable".to_owned()); - }; - runtime - .dispatch_runtime_protocol_message_with_deferred_response( - instance_id, - inspector_session_id, - raw_json, - deferred_response, - ) - .await - } - - pub async fn dispatch_shared_worker_runtime_protocol_message_with_devtools_session_response( - &self, - instance_id: SharedWorkerInstanceId, - inspector_session_id: String, - raw_json: String, - response: RendererRuntimeInspectorResponseSender, - ) -> Result { - let Some(runtime) = self.shared_worker_runtime_if_initialized() else { - return Err("SharedWorkerRuntimeUnavailable".to_owned()); - }; - runtime - .dispatch_runtime_protocol_message_with_devtools_session_response( - instance_id, - inspector_session_id, - raw_json, - response, - ) - .await - } - - pub fn detach_shared_worker_runtime_inspector_session( - &self, - instance_id: SharedWorkerInstanceId, - inspector_session_id: Option, - ) -> bool { - self.shared_worker_runtime_if_initialized() - .is_some_and(|runtime| { - runtime.detach_runtime_inspector_session(instance_id, inspector_session_id) - }) - } } #[cfg(test)] diff --git a/moli-renderer-v8/src/runtime/mod.rs b/moli-renderer-v8/src/runtime/mod.rs index 8b2591565..d9a9bc294 100644 --- a/moli-renderer-v8/src/runtime/mod.rs +++ b/moli-renderer-v8/src/runtime/mod.rs @@ -42,6 +42,14 @@ pub(crate) enum RendererPageStateCapturePolicy { mod access; mod browser_context_runtime; +mod worker_inspection; +pub use worker_inspection::{RendererWorkerInspectionEndpoint, RendererWorkerInspectionTarget}; +mod worker_lifecycle; +pub(crate) use worker_lifecycle::RendererWorkerLifecycleReporter; +pub use worker_lifecycle::{ + RendererCommittedWorkerLifecycle, RendererWorkerLifecycle, RendererWorkerLifecycleInput, + RendererWorkerLifecycleObservation, +}; mod document_lifecycle; mod document_lifecycle_turn; mod javascript_dialog; @@ -323,7 +331,8 @@ pub use self::page_surface::{ RendererAutofillCreditCard, RendererAutofillTriggerOutcome, RendererAutofillTriggerRequest, RendererCaptureScreencastFrameReply, RendererCaptureScreenshotReply, RendererCapturedScreencastFrame, RendererCapturedScreenshot, RendererCommandTurnCompletion, - RendererCommandTurnOutput, RendererCountEntry, RendererDedicatedWorkerTargetEvent, + RendererCommandTurnOutput, RendererCountEntry, RendererDedicatedWorkerMainScript, + RendererDedicatedWorkerMainScriptOutcome, RendererDedicatedWorkerObservation, RendererDedicatedWorkerTargetInfo, RendererDevToolsAgentToken, RendererDocumentBoxModel, RendererDocumentChildNodeSnapshotEvent, RendererDocumentChildNodeSnapshotEvents, RendererDocumentChildNodeSnapshots, RendererDocumentFrontendNodeIdsResolution, @@ -371,12 +380,13 @@ pub use self::page_surface::{ RendererScriptSourceMemoryDiagnostics, RendererScrollIntoViewResult, RendererServiceWorkerConsoleMessage, RendererServiceWorkerExceptionMessage, RendererServiceWorkerFetchDiagnostic, RendererServiceWorkerFetchDiagnosticResult, - RendererServiceWorkerTargetEvent, RendererServiceWorkerTargetInfo, - RendererServiceWorkerVersionStatus, RendererSetDocumentContentResult, - RendererSharedWorkerConsoleMessage, RendererSharedWorkerTargetEvent, - RendererSharedWorkerTargetInfo, RendererStyleSheetHeader, RendererStyleSheetInventoryUpdate, - RendererStyleSheetPayload, RendererSyntheticResponseBody, RendererTextSearchMatch, - RendererTouchPoint, RendererWindowDocumentSource, RuntimeConsoleMessageSnapshot, + RendererServiceWorkerLifecycle, RendererServiceWorkerObservation, + RendererServiceWorkerTargetInfo, RendererServiceWorkerVersionStatus, + RendererSetDocumentContentResult, RendererSharedWorkerConsoleMessage, + RendererSharedWorkerObservation, RendererSharedWorkerTargetInfo, RendererStyleSheetHeader, + RendererStyleSheetInventoryUpdate, RendererStyleSheetPayload, RendererSyntheticResponseBody, + RendererTextSearchMatch, RendererTouchPoint, RendererWindowDocumentSource, + RuntimeConsoleMessageSnapshot, }; pub(crate) use self::page_surface::{ RendererCommandTurnOutputRecorder, RendererDevToolsSessionOutputHost, diff --git a/moli-renderer-v8/src/runtime/page_surface.rs b/moli-renderer-v8/src/runtime/page_surface.rs index ad859c0df..48326e3a9 100644 --- a/moli-renderer-v8/src/runtime/page_surface.rs +++ b/moli-renderer-v8/src/runtime/page_surface.rs @@ -503,11 +503,7 @@ pub struct RendererSharedWorkerConsoleMessage { } #[derive(Debug, Clone, PartialEq)] -pub enum RendererSharedWorkerTargetEvent { - Created(RendererSharedWorkerTargetInfo), - Destroyed { - instance_id: SharedWorkerInstanceId, - }, +pub enum RendererSharedWorkerObservation { Console { instance_id: SharedWorkerInstanceId, message: RendererSharedWorkerConsoleMessage, @@ -529,27 +525,9 @@ pub struct RendererDedicatedWorkerTargetInfo { pub name: String, } -/// Page-owned lifecycle facts for one DedicatedWorker target. -/// -/// Chromium publishes the initial main-script request through the creator -/// Page's Network agent, but publishes the response and terminal event through -/// the Worker target's Network agent. Keeping the response on this target -/// event stream lets protocol preserve that split without manufacturing a -/// complete Page subresource record. -#[derive(Debug, Clone)] -pub enum RendererDedicatedWorkerTargetEvent { - Created(RendererDedicatedWorkerTargetInfo), - ScriptLoaded { - instance_id: u64, - script_url: String, - response: Box, - }, - ScriptLoadFailed { - instance_id: u64, - script_url: String, - error_message: String, - response: Option>, - }, +/// Read-only output from a DedicatedWorker, in its creator Page's source FIFO. +#[derive(Debug, Clone, PartialEq)] +pub enum RendererDedicatedWorkerObservation { Console { instance_id: u64, message: RendererSharedWorkerConsoleMessage, @@ -559,49 +537,43 @@ pub enum RendererDedicatedWorkerTargetEvent { inspector_session_id: Option, messages: Vec, }, - Destroyed { - instance_id: u64, +} + +/// One immutable native main-script fact, shared by Browser snapshots and +/// protocol replay. Session delivery progress is not part of this fact. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RendererDedicatedWorkerMainScript { + pub script_url: String, + pub outcome: RendererDedicatedWorkerMainScriptOutcome, +} + +#[derive(Debug, Clone)] +pub enum RendererDedicatedWorkerMainScriptOutcome { + Loaded(Box), + Failed { + error_message: String, + response: Option>, }, } -impl PartialEq for RendererDedicatedWorkerTargetEvent { +impl PartialEq for RendererDedicatedWorkerMainScriptOutcome { fn eq(&self, other: &Self) -> bool { match (self, other) { - (Self::Created(left), Self::Created(right)) => left == right, - ( - Self::ScriptLoaded { - instance_id: left_instance, - script_url: left_url, - response: left_response, - }, - Self::ScriptLoaded { - instance_id: right_instance, - script_url: right_url, - response: right_response, - }, - ) => { - left_instance == right_instance - && left_url == right_url - && dedicated_worker_navigation_response_eq(left_response, right_response) + (Self::Loaded(left), Self::Loaded(right)) => { + dedicated_worker_navigation_response_eq(left, right) } ( - Self::ScriptLoadFailed { - instance_id: left_instance, - script_url: left_url, + Self::Failed { error_message: left_error, - response: left_response, + response: left, }, - Self::ScriptLoadFailed { - instance_id: right_instance, - script_url: right_url, + Self::Failed { error_message: right_error, - response: right_response, + response: right, }, ) => { - left_instance == right_instance - && left_url == right_url - && left_error == right_error - && match (left_response, right_response) { + left_error == right_error + && match (left, right) { (Some(left), Some(right)) => { dedicated_worker_navigation_response_eq(left, right) } @@ -609,45 +581,13 @@ impl PartialEq for RendererDedicatedWorkerTargetEvent { _ => false, } } - ( - Self::Console { - instance_id: left_instance, - message: left_message, - }, - Self::Console { - instance_id: right_instance, - message: right_message, - }, - ) => left_instance == right_instance && left_message == right_message, - ( - Self::RuntimeInspectorMessages { - instance_id: left_instance, - inspector_session_id: left_session, - messages: left_messages, - }, - Self::RuntimeInspectorMessages { - instance_id: right_instance, - inspector_session_id: right_session, - messages: right_messages, - }, - ) => { - left_instance == right_instance - && left_session == right_session - && left_messages == right_messages - } - ( - Self::Destroyed { - instance_id: left_instance, - }, - Self::Destroyed { - instance_id: right_instance, - }, - ) => left_instance == right_instance, _ => false, } } } +impl Eq for RendererDedicatedWorkerMainScriptOutcome {} + fn dedicated_worker_navigation_response_eq( left: &crate::protocol_types::NavigationResponse, right: &crate::protocol_types::NavigationResponse, @@ -750,8 +690,8 @@ pub enum RendererServiceWorkerFetchDiagnosticResult { /// `VersionUpdated` never manufactures a run. `Destroyed` is a version-level /// terminal but snapshots the exact active run, when one exists, so a delayed /// terminal cannot retire a restarted worker beneath the same version id. -#[derive(Debug, Clone, PartialEq)] -pub enum RendererServiceWorkerTargetEvent { +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RendererServiceWorkerLifecycle { Created { info: RendererServiceWorkerTargetInfo, /// Exact run already owned by a live worker host when this stable @@ -759,6 +699,10 @@ pub enum RendererServiceWorkerTargetEvent { /// `None`; target creation alone must never manufacture a run. active_run: Option, }, + Starting { + version_id: u64, + run: super::RendererServiceWorkerRunIdentity, + }, Started { version_id: u64, run: super::RendererServiceWorkerRunIdentity, @@ -776,6 +720,11 @@ pub enum RendererServiceWorkerTargetEvent { version_id: u64, status: RendererServiceWorkerVersionStatus, }, +} + +/// Run-scoped output can observe an existing host, never create a run. +#[derive(Debug, Clone, PartialEq)] +pub enum RendererServiceWorkerObservation { Console { version_id: u64, run: super::RendererServiceWorkerRunIdentity, diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs index 471ba3769..49ed9b40a 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs @@ -43,7 +43,7 @@ use crate::planning::{ }; use crate::runtime::{ RendererOwnerResourceActivitySource, RendererPageCommand, RendererRuntimeObservableSourceItem, - RendererSharedWorkerTargetEvent, + RendererSharedWorkerObservation, }; use crate::script_vm::{PostParseLifecycleAdvance, PostParseLifecycleCompletionAction}; use crate::types::{ diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs b/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs index 664f47d11..93554dcce 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/worker.rs @@ -1,5 +1,5 @@ use super::*; -use crate::{RendererOutputItem, RendererOwnerAction}; +use crate::RendererOutputItem; const SHARED_WORKER_CONSOLE_MESSAGE: &str = "log: shared-console console-probe 7"; @@ -1662,22 +1662,24 @@ fn shared_worker_console_entry( }) } -fn has_console_probe_created_event(events: &[RendererSharedWorkerTargetEvent]) -> bool { +fn has_console_probe_created_event(events: &[crate::runtime::RendererProtocolObservation]) -> bool { events.iter().any(|event| { matches!( event, - RendererSharedWorkerTargetEvent::Created(info) - if info.name == "console-probe" - && info.url.starts_with("data:text/javascript,") + crate::runtime::RendererProtocolObservation::WorkerLifecycle(observation) + if matches!(observation.lifecycle(), crate::runtime::RendererWorkerLifecycle::SharedCreated(info) + if info.name == "console-probe" && info.url.starts_with("data:text/javascript,")) ) }) } -fn has_console_probe_target_console_event(events: &[RendererSharedWorkerTargetEvent]) -> bool { +fn has_console_probe_target_console_event( + events: &[crate::runtime::RendererProtocolObservation], +) -> bool { events.iter().any(|event| { matches!( event, - RendererSharedWorkerTargetEvent::Console { message, .. } + crate::runtime::RendererProtocolObservation::SharedWorker(RendererSharedWorkerObservation::Console { message, .. }) if message.message == SHARED_WORKER_CONSOLE_MESSAGE ) }) @@ -1688,7 +1690,7 @@ async fn drain_until_shared_worker_console_activity( output_rx: &mut crate::runtime::RendererOutputTransportReceiver, ) -> anyhow::Result<( crate::runtime::RendererPageDiagnosticsSnapshot, - Vec, + Vec, )> { let deadline = Instant::now() + Duration::from_secs(10); let mut target_events = Vec::new(); @@ -1711,8 +1713,9 @@ async fn drain_until_shared_worker_console_activity( }; target_events.extend(output.records().iter().filter_map( |record| match record.item() { - RendererOutputItem::OwnerAction( - RendererOwnerAction::SharedWorkerTargetLifecycle(event), + RendererOutputItem::Observation( + event @ (crate::runtime::RendererProtocolObservation::SharedWorker(_) + | crate::runtime::RendererProtocolObservation::WorkerLifecycle(_)), ) => Some(event.clone()), _ => None, }, @@ -1985,6 +1988,23 @@ async fn dedicated_worker_main_scripts_publish_split_target_lifecycle_records() let document_url = Url::parse(&format!("{base_url}/page.html")).expect("document URL"); let external_url = Url::parse(&format!("{base_url}/worker.js")).expect("worker URL"); let mut page_vm = test_page_vm_with_document_url(document_url.clone()); + let runtime = std::sync::Arc::new( + page_vm.vm_mut().context_host_weak_for_test().upgrade().unwrap() + .borrow().browser_context_runtime(), + ); + let weak_runtime = std::sync::Arc::downgrade(&runtime); + let native_script_readiness = std::sync::Arc::new(parking_lot::Mutex::new(Vec::new())); + let readiness = native_script_readiness.clone(); + runtime.install_worker_lifecycle_handler(move |input| { + if let crate::runtime::RendererWorkerLifecycle::DedicatedScriptCompleted { instance_id, script } = input.lifecycle.as_ref() + && matches!(script.outcome, crate::runtime::RendererDedicatedWorkerMainScriptOutcome::Loaded(_)) + { + let ready = weak_runtime.upgrade().unwrap() + .worker_inspection_endpoint(crate::runtime::RendererWorkerInspectionTarget::Dedicated(*instance_id)) + .is_some(); + readiness.lock().push((script.script_url.clone(), ready)); + } + }); let output_journal = crate::runtime::RendererTurnOutputJournal::new( crate::runtime::RendererOutputStreamIdentity::new_page_for_protocol_test( page_vm.page_id, @@ -2041,8 +2061,8 @@ async fn dedicated_worker_main_scripts_publish_split_target_lifecycle_records() .into_records() .into_iter() .filter_map(|record| match record.into_parts().1 { - RendererOutputItem::OwnerAction( - RendererOwnerAction::DedicatedWorkerTargetLifecycle(event), + RendererOutputItem::Observation( + crate::runtime::RendererProtocolObservation::WorkerLifecycle(event), ) => Some(event), _ => None, }) @@ -2052,13 +2072,21 @@ async fn dedicated_worker_main_scripts_publish_split_target_lifecycle_records() .await .expect("worker main-script Network test should run on owner lane"); + { + let readiness = native_script_readiness.lock(); + assert_eq!(readiness.len(), 2); + assert!(readiness.iter().all(|(_, ready)| *ready), + "a native script completion must already expose its exact inspection endpoint: {readiness:?}"); + } + drop(runtime); + server .await .expect("worker main-script Network server should finish"); let created = target_events .iter() - .filter_map(|event| match event { - crate::runtime::RendererDedicatedWorkerTargetEvent::Created(info) => Some(info), + .filter_map(|event| match event.lifecycle() { + crate::runtime::RendererWorkerLifecycle::DedicatedCreated(info) => Some(info), _ => None, }) .collect::>(); @@ -2081,12 +2109,16 @@ async fn dedicated_worker_main_scripts_publish_split_target_lifecycle_records() let loaded = target_events .iter() - .filter_map(|event| match event { - crate::runtime::RendererDedicatedWorkerTargetEvent::ScriptLoaded { + .filter_map(|event| match event.lifecycle() { + crate::runtime::RendererWorkerLifecycle::DedicatedScriptCompleted { instance_id, - script_url, - response, - } => Some((*instance_id, script_url, response.as_ref())), + script, + } => match &script.outcome { + crate::runtime::RendererDedicatedWorkerMainScriptOutcome::Loaded(response) => { + Some((*instance_id, &script.script_url, response.as_ref())) + } + _ => None, + }, _ => None, }) .collect::>(); diff --git a/moli-renderer-v8/src/runtime/protocol_output/item.rs b/moli-renderer-v8/src/runtime/protocol_output/item.rs index 00fe39e48..3763a6121 100644 --- a/moli-renderer-v8/src/runtime/protocol_output/item.rs +++ b/moli-renderer-v8/src/runtime/protocol_output/item.rs @@ -3,15 +3,15 @@ use crate::protocol_types::{ ChildFrameNavigationSnapshot, ChildFrameTreeEventSnapshot, }; use crate::runtime::{ - DetachedParserScriptFetchContinuation, RendererDedicatedWorkerTargetEvent, + DetachedParserScriptFetchContinuation, RendererDedicatedWorkerObservation, RendererDocumentLifecycleEvent, RendererDocumentLifecycleIdentity, RendererDocumentSourcedSameDocumentNavigation, RendererDocumentSourcedTopLevelLocationNavigation, RendererDomMutationEventBatch, RendererJavaScriptDialogOpening, RendererMainDocumentCommit, RendererPendingDownloadActivation, RendererPendingFileChooserActivation, RendererPendingTopLevelHistoryTraversal, RendererPopupOpening, RendererRuntimeCommandCausalIdentity, - RendererRuntimeInspectorMessageBatch, RendererServiceWorkerTargetEvent, - RendererSharedWorkerTargetEvent, + RendererRuntimeInspectorMessageBatch, RendererServiceWorkerObservation, + RendererSharedWorkerObservation, }; use moli_page_types::{ PendingRuntimeBindingCall, PendingSubresourceContinueEvent, PendingSubresourceFetchInfo, @@ -65,9 +65,6 @@ pub enum RendererOwnerAction { info: Box, continuation: DetachedParserScriptFetchContinuation, }, - SharedWorkerTargetLifecycle(RendererSharedWorkerTargetEvent), - ServiceWorkerTargetLifecycle(RendererServiceWorkerTargetEvent), - DedicatedWorkerTargetLifecycle(RendererDedicatedWorkerTargetEvent), } /// A concrete renderer fact that protocol may project to interested sessions. @@ -77,6 +74,10 @@ pub enum RendererOwnerAction { /// protocol-boundary responsibility. #[derive(Clone, Debug, PartialEq)] pub enum RendererProtocolObservation { + WorkerLifecycle(crate::runtime::RendererWorkerLifecycleObservation), + SharedWorker(RendererSharedWorkerObservation), + ServiceWorker(RendererServiceWorkerObservation), + DedicatedWorker(RendererDedicatedWorkerObservation), Popup(std::sync::Arc), JavaScriptDialog(std::sync::Arc), MainDocumentCommit(RendererMainDocumentCommit), @@ -171,8 +172,8 @@ impl PendingRendererOutputRecord { batch, )) => (!batch.has_resolved_source_identities()) .then_some(RendererOutputResolutionError::RuntimeInspector), - RendererOutputItem::OwnerAction(RendererOwnerAction::SharedWorkerTargetLifecycle( - crate::runtime::RendererSharedWorkerTargetEvent::RuntimeInspectorMessages { + RendererOutputItem::Observation(RendererProtocolObservation::SharedWorker( + crate::runtime::RendererSharedWorkerObservation::RuntimeInspectorMessages { messages, .. }, @@ -180,8 +181,8 @@ impl PendingRendererOutputRecord { crate::runtime::RendererRuntimeInspectorMessage::has_resolved_source_identity, )) .then_some(RendererOutputResolutionError::SharedWorkerRuntimeInspector), - RendererOutputItem::OwnerAction(RendererOwnerAction::ServiceWorkerTargetLifecycle( - crate::runtime::RendererServiceWorkerTargetEvent::RuntimeInspectorMessages { + RendererOutputItem::Observation(RendererProtocolObservation::ServiceWorker( + crate::runtime::RendererServiceWorkerObservation::RuntimeInspectorMessages { messages, .. }, @@ -189,14 +190,12 @@ impl PendingRendererOutputRecord { crate::runtime::RendererRuntimeInspectorMessage::has_resolved_source_identity, )) .then_some(RendererOutputResolutionError::ServiceWorkerRuntimeInspector), - RendererOutputItem::OwnerAction( - RendererOwnerAction::DedicatedWorkerTargetLifecycle( - crate::runtime::RendererDedicatedWorkerTargetEvent::RuntimeInspectorMessages { - messages, - .. - }, - ), - ) => (!messages.iter().all( + RendererOutputItem::Observation(RendererProtocolObservation::DedicatedWorker( + crate::runtime::RendererDedicatedWorkerObservation::RuntimeInspectorMessages { + messages, + .. + }, + )) => (!messages.iter().all( crate::runtime::RendererRuntimeInspectorMessage::has_resolved_source_identity, )) .then_some(RendererOutputResolutionError::DedicatedWorkerRuntimeInspector), @@ -250,8 +249,17 @@ impl RendererOutputRecord { &self.item } - pub(crate) fn is_owner_action(&self) -> bool { - matches!(self.item, RendererOutputItem::OwnerAction(_)) + pub(crate) fn requires_essential_transport_admission(&self) -> bool { + matches!( + self.item, + RendererOutputItem::OwnerAction(_) + | RendererOutputItem::Observation( + RendererProtocolObservation::WorkerLifecycle(_) + | RendererProtocolObservation::SharedWorker(_) + | RendererProtocolObservation::ServiceWorker(_) + | RendererProtocolObservation::DedicatedWorker(_) + ) + ) } pub fn into_parts( diff --git a/moli-renderer-v8/src/runtime/protocol_output/mod.rs b/moli-renderer-v8/src/runtime/protocol_output/mod.rs index 04102e78d..4eb5eeef1 100644 --- a/moli-renderer-v8/src/runtime/protocol_output/mod.rs +++ b/moli-renderer-v8/src/runtime/protocol_output/mod.rs @@ -30,7 +30,7 @@ pub(crate) use turn_journal::{RendererSettledOutput, RendererTurnOutputJournal}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum RendererOutputPublicationAdmission { Observation, - EssentialOwnerProgress, + EssentialOrderedOutput, EssentialTerminalResponse, } @@ -64,8 +64,11 @@ impl RendererOutputPublication { !records.is_empty(), "renderer output publications must be non-empty" ); - let admission = if records.iter().any(RendererOutputRecord::is_owner_action) { - RendererOutputPublicationAdmission::EssentialOwnerProgress + let admission = if records + .iter() + .any(RendererOutputRecord::requires_essential_transport_admission) + { + RendererOutputPublicationAdmission::EssentialOrderedOutput } else { RendererOutputPublicationAdmission::Observation }; diff --git a/moli-renderer-v8/src/runtime/protocol_output/transport.rs b/moli-renderer-v8/src/runtime/protocol_output/transport.rs index e226fc3d0..d3895d19e 100644 --- a/moli-renderer-v8/src/runtime/protocol_output/transport.rs +++ b/moli-renderer-v8/src/runtime/protocol_output/transport.rs @@ -556,13 +556,11 @@ pub(in crate::runtime) fn renderer_output_transport_channel_with_test_limits( mod tests { use std::sync::{Arc, Barrier}; - use moli_shared_worker::SharedWorkerInstanceId; - use super::*; use crate::runtime::{ RendererOutputItem, RendererOutputRecord, RendererOwnerAction, RendererPendingDownloadActivation, RendererPendingDownloadResponse, - RendererProtocolObservation, RendererSharedWorkerTargetEvent, + RendererProtocolObservation, }; fn test_limits() -> RendererOutputTransportLimits { @@ -588,14 +586,60 @@ mod tests { )) } - fn owner_action_record() -> RendererOutputRecord { - RendererOutputRecord::new_for_test(RendererOutputItem::OwnerAction( - RendererOwnerAction::SharedWorkerTargetLifecycle( - RendererSharedWorkerTargetEvent::Destroyed { - instance_id: SharedWorkerInstanceId::from_u64(7), + fn essential_worker_record() -> RendererOutputRecord { + let reporter = crate::runtime::RendererWorkerLifecycleReporter::new( + crate::runtime::RendererBrowserContextRuntimeId::new_for_testing(17), + ); + RendererOutputRecord::new_for_test(RendererOutputItem::Observation( + RendererProtocolObservation::WorkerLifecycle(reporter.report( + crate::runtime::RendererWorkerLifecycle::Service( + crate::runtime::RendererServiceWorkerLifecycle::Destroyed { + version_id: 7, + active_run: None, + }, + ), + )), + )) + } + + #[test] + fn native_worker_lifecycle_and_inspector_output_keep_essential_admission() { + let reporter = crate::runtime::RendererWorkerLifecycleReporter::new( + crate::runtime::RendererBrowserContextRuntimeId::new_for_testing(17), + ); + for observation in [ + RendererProtocolObservation::WorkerLifecycle(reporter.report( + crate::runtime::RendererWorkerLifecycle::DedicatedDestroyed(7), + )), + RendererProtocolObservation::DedicatedWorker( + crate::runtime::RendererDedicatedWorkerObservation::RuntimeInspectorMessages { + instance_id: 7, + inspector_session_id: None, + messages: Vec::new(), }, ), - )) + RendererProtocolObservation::WorkerLifecycle(reporter.report( + crate::runtime::RendererWorkerLifecycle::Service( + crate::runtime::RendererServiceWorkerLifecycle::Stopped { + version_id: 7, + run: crate::runtime::RendererServiceWorkerRunIdentity::fresh(), + reason: "idle_timeout".into(), + }, + ), + )), + RendererProtocolObservation::ServiceWorker( + crate::runtime::RendererServiceWorkerObservation::RuntimeInspectorMessages { + version_id: 7, + run: crate::runtime::RendererServiceWorkerRunIdentity::fresh(), + inspector_session_id: None, + messages: Vec::new(), + }, + ), + ] { + let record = + RendererOutputRecord::new_for_test(RendererOutputItem::Observation(observation)); + assert!(record.requires_essential_transport_admission()); + } } fn publication( @@ -647,7 +691,7 @@ mod tests { async fn mixed_publication_is_admitted_atomically_from_the_essential_reserve() { let (sender, mut receiver) = renderer_output_transport_channel_with_limits(test_limits()); let observation = publication(1, vec![observation_record(1)]); - let mixed = publication(2, vec![observation_record(1), owner_action_record()]); + let mixed = publication(2, vec![observation_record(1), essential_worker_record()]); sender .send(observation.clone()) @@ -735,7 +779,7 @@ mod tests { let barrier = barrier.clone(); workers.push(std::thread::spawn(move || { barrier.wait(); - sender.send(publication(sequence, vec![owner_action_record()])) + sender.send(publication(sequence, vec![essential_worker_record()])) })); } diff --git a/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs b/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs index b3ea35ccf..f469ae2ef 100644 --- a/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs +++ b/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs @@ -97,6 +97,50 @@ fn observation_transport_charge_bytes(observation: &RendererProtocolObservation) .sum(), RendererProtocolObservation::DocumentTitleChanged(change) => string_charge(&change.title), RendererProtocolObservation::DocumentLifecycle(_) => 0, + RendererProtocolObservation::WorkerLifecycle(observation) => { + match observation.lifecycle() { + crate::runtime::RendererWorkerLifecycle::Service(event) => { + service_worker_lifecycle_payload_bytes(event) + } + crate::runtime::RendererWorkerLifecycle::SharedCreated(info) => { + string_charge(&info.url).saturating_add(string_charge(&info.name)) + } + crate::runtime::RendererWorkerLifecycle::SharedDestroyed(_) + | crate::runtime::RendererWorkerLifecycle::DedicatedDestroyed(_) => 0, + crate::runtime::RendererWorkerLifecycle::DedicatedCreated(info) => { + [&info.request_url, &info.document_url, &info.name] + .into_iter() + .map(|value| string_charge(value)) + .sum() + } + crate::runtime::RendererWorkerLifecycle::DedicatedScriptCompleted { + script, + .. + } => string_charge(&script.script_url).saturating_add(match &script.outcome { + crate::runtime::RendererDedicatedWorkerMainScriptOutcome::Loaded(response) => { + navigation_response_transport_charge_bytes(response) + } + crate::runtime::RendererDedicatedWorkerMainScriptOutcome::Failed { + error_message, + response, + } => string_charge(error_message).saturating_add( + response + .as_deref() + .map(navigation_response_transport_charge_bytes) + .unwrap_or(0), + ), + }), + } + } + RendererProtocolObservation::DedicatedWorker(event) => { + dedicated_worker_event_transport_charge_bytes(event) + } + RendererProtocolObservation::ServiceWorker(event) => { + service_worker_event_transport_charge_bytes(event) + } + RendererProtocolObservation::SharedWorker(event) => { + shared_worker_event_transport_charge_bytes(event) + } RendererProtocolObservation::Network { item, .. } => item.renderer_transport_charge_bytes(), RendererProtocolObservation::RuntimeBinding(call) => { string_charge(&call.name).saturating_add(string_charge(&call.payload)) @@ -230,15 +274,6 @@ fn owner_action_transport_charge_bytes(action: &RendererOwnerAction) -> usize { RendererOwnerAction::DetachedParserScriptFetchPause { info, .. } => { info.renderer_transport_charge_bytes() } - RendererOwnerAction::SharedWorkerTargetLifecycle(event) => { - shared_worker_event_transport_charge_bytes(event) - } - RendererOwnerAction::ServiceWorkerTargetLifecycle(event) => { - service_worker_event_transport_charge_bytes(event) - } - RendererOwnerAction::DedicatedWorkerTargetLifecycle(event) => { - dedicated_worker_event_transport_charge_bytes(event) - } } } @@ -308,20 +343,17 @@ fn dom_mutation_transport_charge_bytes(event: &crate::runtime::RendererDomMutati } fn shared_worker_event_transport_charge_bytes( - event: &crate::runtime::RendererSharedWorkerTargetEvent, + event: &crate::runtime::RendererSharedWorkerObservation, ) -> usize { match event { - crate::runtime::RendererSharedWorkerTargetEvent::Created(info) => { - string_charge(&info.url).saturating_add(string_charge(&info.name)) - } - crate::runtime::RendererSharedWorkerTargetEvent::Console { message, .. } => { + crate::runtime::RendererSharedWorkerObservation::Console { message, .. } => { message.args.iter().fold( string_charge(&message.message) .saturating_add(message.stack.as_deref().map(string_charge).unwrap_or(0)), |total, value| total.saturating_add(json_charge(value)), ) } - crate::runtime::RendererSharedWorkerTargetEvent::RuntimeInspectorMessages { + crate::runtime::RendererSharedWorkerObservation::RuntimeInspectorMessages { inspector_session_id, messages, .. @@ -334,28 +366,21 @@ fn shared_worker_event_transport_charge_bytes( total.saturating_add(runtime_inspector_message_transport_charge_bytes(message)) }, ), - crate::runtime::RendererSharedWorkerTargetEvent::Destroyed { .. } => 0, } } fn service_worker_event_transport_charge_bytes( - event: &crate::runtime::RendererServiceWorkerTargetEvent, + event: &crate::runtime::RendererServiceWorkerObservation, ) -> usize { match event { - crate::runtime::RendererServiceWorkerTargetEvent::Created { info, .. } => { - string_charge(&info.script_url).saturating_add(string_charge(&info.scope_url)) - } - crate::runtime::RendererServiceWorkerTargetEvent::Stopped { reason, .. } => { - string_charge(reason) - } - crate::runtime::RendererServiceWorkerTargetEvent::Console { message, .. } => { + crate::runtime::RendererServiceWorkerObservation::Console { message, .. } => { message.args.iter().fold( string_charge(&message.message) .saturating_add(message.stack.as_deref().map(string_charge).unwrap_or(0)), |total, value| total.saturating_add(json_charge(value)), ) } - crate::runtime::RendererServiceWorkerTargetEvent::Exception { message, .. } => [ + crate::runtime::RendererServiceWorkerObservation::Exception { message, .. } => [ message.message.as_str(), message.filename.as_str(), message.event_kind.as_str(), @@ -365,7 +390,7 @@ fn service_worker_event_transport_charge_bytes( .into_iter() .map(string_charge) .sum(), - crate::runtime::RendererServiceWorkerTargetEvent::FetchDiagnostic { + crate::runtime::RendererServiceWorkerObservation::FetchDiagnostic { diagnostic, .. } => { let mut total = [ @@ -414,7 +439,7 @@ fn service_worker_event_transport_charge_bytes( } } } - crate::runtime::RendererServiceWorkerTargetEvent::RuntimeInspectorMessages { + crate::runtime::RendererServiceWorkerObservation::RuntimeInspectorMessages { inspector_session_id, messages, .. @@ -427,51 +452,21 @@ fn service_worker_event_transport_charge_bytes( total.saturating_add(runtime_inspector_message_transport_charge_bytes(message)) }, ), - crate::runtime::RendererServiceWorkerTargetEvent::Started { .. } - | crate::runtime::RendererServiceWorkerTargetEvent::Destroyed { .. } - | crate::runtime::RendererServiceWorkerTargetEvent::VersionUpdated { .. } => 0, } } fn dedicated_worker_event_transport_charge_bytes( - event: &crate::runtime::RendererDedicatedWorkerTargetEvent, + event: &crate::runtime::RendererDedicatedWorkerObservation, ) -> usize { match event { - crate::runtime::RendererDedicatedWorkerTargetEvent::Created(info) => [ - info.request_url.as_str(), - info.document_url.as_str(), - info.name.as_str(), - ] - .into_iter() - .map(string_charge) - .sum(), - crate::runtime::RendererDedicatedWorkerTargetEvent::ScriptLoaded { - script_url, - response, - .. - } => string_charge(script_url) - .saturating_add(navigation_response_transport_charge_bytes(response)), - crate::runtime::RendererDedicatedWorkerTargetEvent::ScriptLoadFailed { - script_url, - error_message, - response, - .. - } => string_charge(script_url) - .saturating_add(string_charge(error_message)) - .saturating_add( - response - .as_deref() - .map(navigation_response_transport_charge_bytes) - .unwrap_or(0), - ), - crate::runtime::RendererDedicatedWorkerTargetEvent::Console { message, .. } => { + crate::runtime::RendererDedicatedWorkerObservation::Console { message, .. } => { message.args.iter().fold( string_charge(&message.message) .saturating_add(message.stack.as_deref().map(string_charge).unwrap_or(0)), |total, value| total.saturating_add(json_charge(value)), ) } - crate::runtime::RendererDedicatedWorkerTargetEvent::RuntimeInspectorMessages { + crate::runtime::RendererDedicatedWorkerObservation::RuntimeInspectorMessages { inspector_session_id, messages, .. @@ -484,7 +479,6 @@ fn dedicated_worker_event_transport_charge_bytes( total.saturating_add(runtime_inspector_message_transport_charge_bytes(message)) }, ), - crate::runtime::RendererDedicatedWorkerTargetEvent::Destroyed { .. } => 0, } } @@ -501,3 +495,20 @@ fn navigation_response_transport_charge_bytes( .unwrap_or(0), ) } + +fn service_worker_lifecycle_payload_bytes( + event: &crate::runtime::RendererServiceWorkerLifecycle, +) -> usize { + match event { + crate::runtime::RendererServiceWorkerLifecycle::Created { info, .. } => { + string_charge(&info.script_url).saturating_add(string_charge(&info.scope_url)) + } + crate::runtime::RendererServiceWorkerLifecycle::Stopped { reason, .. } => { + string_charge(reason) + } + crate::runtime::RendererServiceWorkerLifecycle::Starting { .. } + | crate::runtime::RendererServiceWorkerLifecycle::Started { .. } + | crate::runtime::RendererServiceWorkerLifecycle::Destroyed { .. } + | crate::runtime::RendererServiceWorkerLifecycle::VersionUpdated { .. } => 0, + } +} diff --git a/moli-renderer-v8/src/runtime/worker_inspection.rs b/moli-renderer-v8/src/runtime/worker_inspection.rs new file mode 100644 index 000000000..7586e1759 --- /dev/null +++ b/moli-renderer-v8/src/runtime/worker_inspection.rs @@ -0,0 +1,115 @@ +use moli_shared_worker::SharedWorkerInstanceId; + +use super::{ + CompletedWorkerRuntimeInspectorCommandDispatch, RendererRuntimeInspectorMessage, + RendererRuntimeInspectorResponseSender, RendererServiceWorkerRunIdentity, + RendererTurnOutputJournal, +}; +use crate::worker::WorkerDevToolsHandle; + +/// Renderer identity used only to bind an inspection endpoint. A stable +/// ServiceWorker version is insufficient: inspection must name its exact run. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RendererWorkerInspectionTarget { + Shared(SharedWorkerInstanceId), + Dedicated(u64), + Service { + version_id: u64, + run: RendererServiceWorkerRunIdentity, + }, +} + +impl RendererWorkerInspectionTarget { + pub fn unavailable_message(&self) -> &'static str { + match self { + Self::Shared(_) => "SharedWorkerRuntimeUnavailable", + Self::Dedicated(_) => "DedicatedWorkerRuntimeUnavailable", + Self::Service { .. } => "ServiceWorkerRuntimeUnavailable", + } + } +} + +/// Inspection-only capability for one concrete worker. It retains neither the +/// Context registry nor a Worker owner/JoinHandle. Retirement disposes the +/// underlying task runner; delayed dispatch cannot look up a replacement run. +#[derive(Clone, Debug)] +pub struct RendererWorkerInspectionEndpoint { + handle: WorkerDevToolsHandle, + output_journal: Option, + unavailable_message: &'static str, +} + +impl RendererWorkerInspectionEndpoint { + pub(crate) fn new( + handle: WorkerDevToolsHandle, + output_journal: Option, + unavailable_message: &'static str, + ) -> Self { + Self { + handle, + output_journal, + unavailable_message, + } + } + + pub fn attach_session(&self, session_id: Option) -> bool { + self.handle.attach_runtime_inspector_session(session_id) + } + + pub fn detach_session(&self, session_id: Option) -> bool { + self.handle.detach_runtime_inspector_session(session_id) + } + + pub async fn dispatch_protocol_message( + &self, + session_id: Option, + raw_json: String, + response: Option, + ) -> Result, String> { + let (response_tx, response_rx) = tokio::sync::oneshot::channel(); + if !self.handle.dispatch_runtime_protocol_message( + session_id, + raw_json, + response, + response_tx, + ) { + return Err(self.unavailable_message.to_owned()); + } + response_rx + .await + .map_err(|_| self.unavailable_message.to_owned())? + } + + pub fn dispatch_protocol_message_to_session( + self, + session_id: String, + raw_json: String, + response: RendererRuntimeInspectorResponseSender, + ) -> Result< + impl std::future::Future, + String, + > { + let output_journal = self + .output_journal + .clone() + .ok_or_else(|| self.unavailable_message.to_owned())?; + let response = + response.route_to_worker_devtools_session_output(session_id.clone(), output_journal); + let error_response = response.clone(); + let settlement = response + .take_session_response_settlement_receiver() + .ok_or_else(|| self.unavailable_message.to_owned())?; + // Bind the output stream and take its single settlement receiver before + // yielding. Retirement may settle this response before dispatch runs. + Ok(async move { + let dispatch = self + .dispatch_protocol_message(Some(session_id), raw_json, Some(response)) + .await; + CompletedWorkerRuntimeInspectorCommandDispatch::finish( + dispatch, + settlement, + error_response, + ) + }) + } +} diff --git a/moli-renderer-v8/src/runtime/worker_lifecycle.rs b/moli-renderer-v8/src/runtime/worker_lifecycle.rs new file mode 100644 index 000000000..fe96ea9b4 --- /dev/null +++ b/moli-renderer-v8/src/runtime/worker_lifecycle.rs @@ -0,0 +1,194 @@ +use std::sync::Arc; + +use parking_lot::Mutex; +use tokio::sync::watch; + +use super::{RendererBrowserContextRuntimeId, RendererSharedWorkerTargetInfo}; + +/// Execution facts, without Inspector commands, sessions or protocol target IDs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RendererWorkerLifecycle { + Service(super::RendererServiceWorkerLifecycle), + SharedCreated(RendererSharedWorkerTargetInfo), + SharedDestroyed(moli_shared_worker::SharedWorkerInstanceId), + DedicatedCreated(super::RendererDedicatedWorkerTargetInfo), + DedicatedScriptCompleted { + instance_id: u64, + script: Arc, + }, + DedicatedDestroyed(u64), +} + +/// The source FIFO carries an observation of the Browser commit, never its +/// decision authority. A missing/dropped Browser consumer cannot acknowledge it. +#[derive(Clone, Debug)] +pub struct RendererWorkerLifecycleObservation { + runtime: RendererBrowserContextRuntimeId, + lifecycle: Arc, + committed: watch::Receiver>, +} + +impl PartialEq for RendererWorkerLifecycleObservation { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.lifecycle, &other.lifecycle) + } +} + +impl Eq for RendererWorkerLifecycleObservation {} + +/// A complete occurrence already committed by Browser. `browser_sequence` is +/// supplied by that owner; it is not a renderer output cursor or turn sequence. +#[derive(Clone, Debug)] +pub struct RendererCommittedWorkerLifecycle { + runtime: RendererBrowserContextRuntimeId, + browser_sequence: u64, + lifecycle: Arc, +} + +impl RendererCommittedWorkerLifecycle { + pub fn runtime(&self) -> RendererBrowserContextRuntimeId { + self.runtime + } + pub fn browser_sequence(&self) -> u64 { + self.browser_sequence + } + pub fn lifecycle(&self) -> &RendererWorkerLifecycle { + &self.lifecycle + } +} + +impl RendererWorkerLifecycleObservation { + pub async fn committed(mut self) -> Option { + loop { + let sequence = *self.committed.borrow_and_update(); + if let Some(browser_sequence) = sequence { + return Some(RendererCommittedWorkerLifecycle { + runtime: self.runtime, + browser_sequence, + lifecycle: self.lifecycle, + }); + } + self.committed.changed().await.ok()?; + } + } + + pub(crate) fn lifecycle(&self) -> &RendererWorkerLifecycle { + &self.lifecycle + } +} + +/// Move-only acknowledgement owned by the Browser input consumer. Dropping a +/// stale Context input rejects its FIFO observation without an extra cancel flag. +pub struct RendererWorkerLifecycleInput { + pub runtime: RendererBrowserContextRuntimeId, + pub lifecycle: Arc, + committed: watch::Sender>, +} + +impl RendererWorkerLifecycleInput { + pub fn commit(self, browser_sequence: u64) { + assert_ne!( + browser_sequence, 0, + "Browser occurrence must have a sequence" + ); + self.committed.send_replace(Some(browser_sequence)); + } +} + +type WorkerLifecycleHandler = Box; + +#[derive(Clone)] +pub(crate) struct RendererWorkerLifecycleReporter { + runtime: RendererBrowserContextRuntimeId, + handler: Arc>>, +} + +impl std::fmt::Debug for RendererWorkerLifecycleReporter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RendererWorkerLifecycleReporter") + .field("runtime", &self.runtime) + .finish_non_exhaustive() + } +} + +impl RendererWorkerLifecycleReporter { + pub(crate) fn new(runtime: RendererBrowserContextRuntimeId) -> Self { + Self { + runtime, + handler: Arc::new(Mutex::new(None)), + } + } + + pub(crate) fn runtime(&self) -> RendererBrowserContextRuntimeId { + self.runtime + } + + pub(crate) fn install_handler( + &self, + handler: impl Fn(RendererWorkerLifecycleInput) + Send + Sync + 'static, + ) { + let mut slot = self.handler.lock(); + assert!( + slot.is_none(), + "one runtime can only have one Browser lifecycle owner" + ); + *slot = Some(Box::new(handler)); + } + + pub(crate) fn report( + &self, + lifecycle: RendererWorkerLifecycle, + ) -> RendererWorkerLifecycleObservation { + let lifecycle = Arc::new(lifecycle); + let (committed, observation) = watch::channel(None); + let input = RendererWorkerLifecycleInput { + runtime: self.runtime, + lifecycle: lifecycle.clone(), + committed, + }; + // Enqueue under the reporter lock: concurrent producers in this Context + // cannot reorder native transitions before the Browser consumes them. + if let Some(handler) = self.handler.lock().as_ref() { + handler(input); + } + RendererWorkerLifecycleObservation { + runtime: self.runtime, + lifecycle, + committed: observation, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn worker_lifecycle_observations_require_their_exact_native_acknowledgement() { + let reporter = RendererWorkerLifecycleReporter::new( + RendererBrowserContextRuntimeId::new_for_testing(31), + ); + let fact = RendererWorkerLifecycle::SharedDestroyed( + moli_shared_worker::SharedWorkerInstanceId::from_u64(7), + ); + assert!(reporter.report(fact.clone()).committed().await.is_none()); + let inputs = Arc::new(Mutex::new(Vec::new())); + let pending = inputs.clone(); + reporter.install_handler(move |input| pending.lock().push(input)); + let rejected = reporter.report(fact.clone()); + let accepted = reporter.report(fact.clone()); + let clone = accepted.clone(); + assert!(accepted.committed.borrow().is_none()); + let accepted_input = inputs.lock().pop().unwrap(); + let rejected_input = inputs.lock().pop().unwrap(); + accepted_input.commit(43); + drop(rejected_input); + assert!(rejected.committed().await.is_none()); + for observation in [accepted, clone] { + let committed = observation.committed().await.unwrap(); + assert_eq!(committed.runtime(), reporter.runtime()); + assert_eq!(committed.browser_sequence(), 43); + assert_eq!(committed.lifecycle(), &fact); + } + } +} diff --git a/moli-renderer-v8/src/script_vm/dedicated_worker_client_event_body.rs b/moli-renderer-v8/src/script_vm/dedicated_worker_client_event_body.rs index 860838879..3a423e613 100644 --- a/moli-renderer-v8/src/script_vm/dedicated_worker_client_event_body.rs +++ b/moli-renderer-v8/src/script_vm/dedicated_worker_client_event_body.rs @@ -72,20 +72,9 @@ impl ScriptVm { content_security_report_only_policies, content_security_reporting_endpoints, } => { - let recorded = self - ._context_host - .borrow_mut() - .record_dedicated_worker_target_script_loaded( - worker_id, - script_url.clone(), - network_response, - ); - if !recorded { - return Ok(DedicatedWorkerClientEventBodyEffect::CurrentTargetDisappeared); - } let handled = self._context_host.borrow_mut().finish_loading_worker( worker_id, - script_url, + script_url.clone(), script_source, script_kind, secure_context, @@ -96,7 +85,19 @@ impl ScriptVm { content_security_report_only_policies, content_security_reporting_endpoints, ); - Ok(if handled { + // A native completion can be observed before this Page turn + // settles. Install the physical inspection endpoint first; + // later Inspector/Console tasks still follow this FIFO record. + let recorded = handled + && self + ._context_host + .borrow_mut() + .record_dedicated_worker_target_script_loaded( + worker_id, + script_url, + network_response, + ); + Ok(if recorded { DedicatedWorkerClientEventBodyEffect::StateTransitionApplied } else { DedicatedWorkerClientEventBodyEffect::CurrentTargetDisappeared diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index f5053692e..751f8d0a6 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -14454,23 +14454,23 @@ async fn navigator_service_worker_register_changed_main_script_fires_updatefound const registration = await sw.register("worker.js", { scope: "./" }); await sw.ready; const events = []; - registration.addEventListener("updatefound", () => { - const installing = registration.installing; - events.push([ - "updatefound", - installing && installing.scriptURL, - installing && installing.state - ].join(":")); - installing.addEventListener("statechange", () => { - events.push("statechange:" + installing.state); + const installed = new Promise(resolve => { + registration.addEventListener("updatefound", () => { + const installing = registration.installing; + events.push([ + "updatefound", + installing && installing.scriptURL, + installing && installing.state + ].join(":")); + installing.addEventListener("statechange", () => { + events.push("statechange:" + installing.state); + if (installing.state === "installed") resolve(); + }); }); }); const second = await sw.register("worker.js", { scope: "./" }); - let lookup = await sw.getRegistration(); - for (let i = 0; i < 20 && !events.includes("statechange:installed"); i++) { - await new Promise(resolve => setTimeout(resolve, 0)); - lookup = await sw.getRegistration(); - } + await installed; + const lookup = await sw.getRegistration(); globalThis.__serviceWorkerChangedUpdateProbe = [ events.join(","), second.installing === null, diff --git a/moli-renderer-v8/src/script_vm/worker_host_bridge_body.rs b/moli-renderer-v8/src/script_vm/worker_host_bridge_body.rs index f3121fbf8..5abf710b5 100644 --- a/moli-renderer-v8/src/script_vm/worker_host_bridge_body.rs +++ b/moli-renderer-v8/src/script_vm/worker_host_bridge_body.rs @@ -491,6 +491,7 @@ impl ScriptVm { | WorkerToParentMessage::ServiceWorkerClientsClaim { .. } | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } | WorkerToParentMessage::SharedWorkerClosed + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) => {} } Ok(()) diff --git a/moli-renderer-v8/src/service_worker_runtime/host.rs b/moli-renderer-v8/src/service_worker_runtime/host.rs index ca46feb24..3e1c80c54 100644 --- a/moli-renderer-v8/src/service_worker_runtime/host.rs +++ b/moli-renderer-v8/src/service_worker_runtime/host.rs @@ -6,12 +6,11 @@ use tokio::sync::mpsc; use crate::{ runtime::{ - RendererRuntimeInspectorMessage, RendererServiceWorkerRunIdentity, ServiceWorkerFetchEvent, - ServiceWorkerLifecycleEvent, ServiceWorkerMessageEvent, - ServiceWorkerNavigationPreloadFailure, ServiceWorkerNavigationPreloadResponseStarted, - ServiceWorkerNavigationPreloadStreamChunk, ServiceWorkerNavigationPreloadStreamFinished, - ServiceWorkerNotificationEvent, ServiceWorkerPeriodicSyncEvent, ServiceWorkerPushEvent, - ServiceWorkerSyncEvent, + RendererServiceWorkerRunIdentity, ServiceWorkerFetchEvent, ServiceWorkerLifecycleEvent, + ServiceWorkerMessageEvent, ServiceWorkerNavigationPreloadFailure, + ServiceWorkerNavigationPreloadResponseStarted, ServiceWorkerNavigationPreloadStreamChunk, + ServiceWorkerNavigationPreloadStreamFinished, ServiceWorkerNotificationEvent, + ServiceWorkerPeriodicSyncEvent, ServiceWorkerPushEvent, ServiceWorkerSyncEvent, }, types::{NetworkBodySourceId, SubresourcePolicyContext}, worker::{ @@ -28,6 +27,7 @@ use super::{ LoadedServiceWorkerScript, ServiceWorkerScriptResource, load_service_worker_script_source, }, service::ServiceWorkerRuntimeService, + start_completion::ServiceWorkerTargetOutput, version::{ServiceWorkerFetchHandlerType, ServiceWorkerVersionStartFailure}, }; @@ -134,71 +134,14 @@ impl RendererServiceWorkerHost { ) } - pub(super) async fn dispatch_worker_runtime_protocol_message( - &self, - inspector_session_id: Option, - raw_json: String, - deferred_response: Option, - ) -> Result, String> { - let (response_tx, response_rx) = tokio::sync::oneshot::channel(); - let dispatched = { - let state = self.state.lock(); - let RendererServiceWorkerHostState::Running { - handle: Some(handle), - } = &*state - else { - return Err("ServiceWorkerRuntimeUnavailable".to_owned()); - }; - handle.dispatch_runtime_protocol_message( - inspector_session_id, - raw_json, - deferred_response, - response_tx, - ) - }; - if !dispatched { - return Err("ServiceWorkerRuntimeUnavailable".to_owned()); - } - response_rx - .await - .map_err(|_| "ServiceWorkerRuntimeUnavailable".to_owned())? - } - - pub(super) async fn dispatch_worker_runtime_protocol_message_with_deferred_response( - &self, - inspector_session_id: Option, - raw_json: String, - deferred_response: crate::runtime::RendererRuntimeInspectorResponseSender, - ) -> Result, String> { - self.dispatch_worker_runtime_protocol_message( - inspector_session_id, - raw_json, - Some(deferred_response), - ) - .await - } - - pub(super) async fn dispatch_worker_runtime_protocol_message_without_deferred_response( - &self, - inspector_session_id: Option, - raw_json: String, - ) -> Result, String> { - self.dispatch_worker_runtime_protocol_message(inspector_session_id, raw_json, None) - .await - } - - pub(super) fn detach_worker_runtime_inspector_session( - &self, - inspector_session_id: Option, - ) -> bool { + pub(super) fn inspection_handle(&self) -> Option { let state = self.state.lock(); - let RendererServiceWorkerHostState::Running { - handle: Some(handle), - } = &*state - else { - return false; - }; - handle.detach_runtime_inspector_session(inspector_session_id) + match &*state { + RendererServiceWorkerHostState::Running { + handle: Some(handle), + } => Some(handle.devtools_handle()), + _ => None, + } } pub(super) fn run_if_waiting_for_debugger_for_devtools(&self) -> bool { @@ -643,7 +586,6 @@ impl RendererServiceWorkerHost { } }; let script_resource = script.resource.clone(); - let final_script_url = script_resource.final_url.to_string(); if service.finish_worker_start_identical_script_update( params.run_owner.version_id(), params.run_owner.cloned_run_identity(), @@ -652,17 +594,10 @@ impl RendererServiceWorkerHost { self.mark_failed(); return; } - let (bootstrap_completion_tx, bootstrap_completion_rx) = - mpsc::unbounded_channel::(); - let mut handle = spawn_service_worker( - service.clone(), - params.clone(), - script, - bootstrap_completion_tx, - ); - if let Some(receiver) = handle.take_receiver() { - spawn_parent_message_pump(service.clone(), Arc::clone(self), receiver); - } + let mut handle = spawn_service_worker(service.clone(), params.clone(), script); + let receiver = handle + .take_receiver() + .expect("new ServiceWorker owns its parent FIFO"); let mut state = self.state.lock(); if !matches!(*state, RendererServiceWorkerHostState::Loading) { drop(state); @@ -673,16 +608,21 @@ impl RendererServiceWorkerHost { handle: Some(handle), }; drop(state); + // No parent output may expose a run before its real host is installed. + if let Err(error) = + spawn_parent_message_pump(service.clone(), Arc::clone(self), receiver, script_resource) + { + service.enqueue_worker_start_failed( + params.run_owner, + ServiceWorkerVersionStartFailure::HostThreadSpawn { + message: error.to_string(), + }, + ); + return; + } if service.take_devtools_evaluation_release_for_version(params.run_owner.version_id()) { self.run_if_waiting_for_debugger_for_devtools(); } - report_bootstrap_completion( - service, - params.run_owner, - final_script_url, - script_resource, - bootstrap_completion_rx, - ); } } @@ -690,14 +630,26 @@ fn spawn_parent_message_pump( service: ServiceWorkerRuntimeService, source_host: SharedRendererServiceWorkerHost, mut receiver: mpsc::UnboundedReceiver, -) { + script_resource: ServiceWorkerScriptResource, +) -> std::io::Result<()> { let version_id = source_host.version_id(); - let source_run = source_host.run_identity(); - let _ = std::thread::Builder::new() + let owner = source_host.run_owner(); + std::thread::Builder::new() .name(format!("service-worker-pump-{}", version_id.as_u64())) .spawn(move || { + let mut pending_script = Some(script_resource); while let Some(message) = receiver.blocking_recv() { match message { + WorkerToParentMessage::ServiceWorkerBootstrapCompleted(completion) => { + if let Some(script) = pending_script.take() { + report_bootstrap_completion( + &service, + owner.clone(), + script, + completion, + ); + } + } WorkerToParentMessage::ServiceWorkerLifecycleCompleted(completion) => { service.enqueue_lifecycle_event_completed(completion); } @@ -827,10 +779,9 @@ fn spawn_parent_message_pump( event_kind, phase, source, - } => { - service.enqueue_target_exception_message( - version_id, - source_run.clone(), + } => service.enqueue_target_output( + owner.clone(), + ServiceWorkerTargetOutput::Exception { message, filename, lineno, @@ -838,24 +789,25 @@ fn spawn_parent_message_pump( event_kind, phase, source, - ); - } + }, + ), WorkerToParentMessage::Console(message) => { - service.enqueue_target_console_message( - version_id, - source_run.clone(), - message, + service.enqueue_target_output( + owner.clone(), + ServiceWorkerTargetOutput::Console(message), ); } WorkerToParentMessage::RuntimeInspectorMessages(messages) => { - service.enqueue_target_runtime_inspector_messages( - version_id, - source_run.clone(), - messages, + service.enqueue_target_output( + owner.clone(), + ServiceWorkerTargetOutput::InspectorMessages(messages), ); } WorkerToParentMessage::RuntimeInspectorResponse(publication) => { - let _ = publication.commit(None); + service.enqueue_target_output( + owner.clone(), + ServiceWorkerTargetOutput::InspectorResponse(publication), + ); } WorkerToParentMessage::Post(_) | WorkerToParentMessage::SharedWorkerClosed @@ -868,41 +820,33 @@ fn spawn_parent_message_pump( | WorkerToParentMessage::WebSocketFrame(_) => {} } } - }); + if pending_script.is_some() { + service.enqueue_worker_start_failed( + owner, + ServiceWorkerVersionStartFailure::BootstrapChannelClosed, + ); + } + }) + .map(|_| ()) } fn report_bootstrap_completion( - service: ServiceWorkerRuntimeService, + service: &ServiceWorkerRuntimeService, owner: ServiceWorkerRunOwner, - final_script_url: String, script_resource: ServiceWorkerScriptResource, - mut receiver: mpsc::UnboundedReceiver, + completion: WorkerBootstrapCompletion, ) { - match receiver.blocking_recv() { - Some(WorkerBootstrapCompletion { - result: Ok(success), - }) => { - service.enqueue_worker_start_completed( - owner, - final_script_url, - script_resource, - service_worker_fetch_handler_type(success), - ); - } - Some(WorkerBootstrapCompletion { - result: Err(failure), - }) => { - service.enqueue_worker_start_failed( - owner, - ServiceWorkerVersionStartFailure::Bootstrap { failure }, - ); - } - None => { - service.enqueue_worker_start_failed( - owner, - ServiceWorkerVersionStartFailure::BootstrapChannelClosed, - ); - } + match completion.result { + Ok(success) => service.enqueue_worker_start_completed( + owner, + script_resource.final_url.to_string(), + script_resource, + service_worker_fetch_handler_type(success), + ), + Err(failure) => service.enqueue_worker_start_failed( + owner, + ServiceWorkerVersionStartFailure::Bootstrap { failure }, + ), } } @@ -922,7 +866,6 @@ fn spawn_service_worker( service: ServiceWorkerRuntimeService, params: ServiceWorkerLaunchParams, script: LoadedServiceWorkerScript, - bootstrap_completion_tx: mpsc::UnboundedSender, ) -> WorkerHandle { let storage_key = moli_storage_key::deserialize_serialized_storage_key(¶ms.storage_key) .unwrap_or_else(|| MoliStorageKey::first_party_from_url(¶ms.scope_url, None)); @@ -957,7 +900,7 @@ fn spawn_service_worker( .with_broadcast_channel_top_level_site(params.broadcast_channel_top_level_site) .with_indexed_db_manager(params.indexed_db_manager) .with_storage_bucket_store(params.storage_bucket_store) - .with_bootstrap_completion_sender(bootstrap_completion_tx) + .with_parent_bootstrap_completion() .with_pause_evaluation_until_debugger(params.pause_evaluation_until_debugger), ) } @@ -982,6 +925,39 @@ fn service_worker_script_policy_context( mod tests { use super::*; + #[tokio::test] + async fn service_worker_parent_fifo_eof_queues_bootstrap_failure() { + let service = super::super::service::new_service_worker_runtime_service(); + let (wake_tx, mut wake_rx) = super::super::owner_wake::service_worker_owner_wake_channel(); + service.add_owner_wake_sender(wake_tx); + let owner = ServiceWorkerRunOwner::fresh(ServiceWorkerVersionId(7)); + let host = RendererServiceWorkerHost::new_loading(&owner); + let script_url = url::Url::parse("https://example.test/app/sw.js").unwrap(); + let resource = ServiceWorkerScriptResource { + request_url: script_url.clone(), + final_url: script_url, + kind: crate::worker::WorkerScriptResourceKind::JavaScript, + status: 200, + headers: Vec::new(), + body_len: 0, + body_sha256: String::new(), + response_time_ms: 0, + mime_type: Some("text/javascript".to_owned()), + }; + let (parent_tx, parent_rx) = mpsc::unbounded_channel(); + drop(parent_tx); + spawn_parent_message_pump(service.clone(), host, parent_rx, resource).unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(5), wake_rx.recv()) + .await + .expect("closed bootstrap FIFO must wake its service owner") + .expect("service owner wake remains open"); + assert_eq!(service.pending_service_lane_event_count(), 1); + // This physical run has already disappeared; its failure is consumed once. + assert_eq!(service.drain_service_lane(), 1); + assert_eq!(service.pending_service_lane_event_count(), 0); + assert_eq!(service.diagnostics_snapshot().version_count, 0); + } + #[test] fn worker_hosts_preserve_their_reserved_exact_run_identity() { let version_id = ServiceWorkerVersionId(7); diff --git a/moli-renderer-v8/src/service_worker_runtime/service.rs b/moli-renderer-v8/src/service_worker_runtime/service.rs index 620db6594..b1e1f7d7b 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service.rs @@ -1,3 +1,6 @@ +#[cfg(test)] +use super::target_output_streams::ServiceWorkerOutputForTest; + use std::{ collections::{HashMap, HashSet, VecDeque}, sync::{Arc, atomic::Ordering}, @@ -23,9 +26,8 @@ use crate::{ ServiceWorkerReadyCompletion, }, worker::{ - WorkerBootstrapFailure, WorkerConsoleMessage, WorkerErrorPhase, WorkerErrorSource, - WorkerNetworkPolicy, WorkerParentErrorEventKind, WorkerRuntimeInspectorMessageBatch, - WorkerScriptKind, WorkerScriptResource, + WorkerBootstrapFailure, WorkerErrorPhase, WorkerErrorSource, WorkerNetworkPolicy, + WorkerParentErrorEventKind, WorkerScriptKind, WorkerScriptResource, }, }; @@ -111,7 +113,7 @@ use super::{ snapshots::{ ServiceWorkerControlState, ServiceWorkerRegistrationSnapshot, ServiceWorkerVersionSnapshot, }, - start_completion::ServiceWorkerRuntimeCompletion, + start_completion::{ServiceWorkerRuntimeCompletion, ServiceWorkerTargetOutput}, state::{ LifecycleProgress, ServiceWorkerClient, ServiceWorkerClientEndpoint, ServiceWorkerControllerChangeDelivery, ServiceWorkerDevToolsRelatedPauseOnStartPolicy, @@ -140,12 +142,12 @@ mod event_start; mod fetch_settlement; mod functional_requests; mod idle_scheduling; +mod inspection; mod lifecycle_completion; mod lifecycle_requests; mod registration_jobs; mod registration_lookup; mod registration_store; -mod runtime_protocol; mod service_lane_completions; mod worker_completion; use client_registry::{ @@ -300,7 +302,7 @@ pub(crate) fn new_service_worker_runtime_service_with_resource_store_and_browser restored_worker_context_runtime: RendererWorkerContextRuntime, browser_resource_runtime: crate::network::BrowserResourceRuntimeBinding, client_id_allocator: super::ids::ServiceWorkerClientIdAllocator, - browser_context_runtime_id: crate::runtime::RendererBrowserContextRuntimeId, + worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, output_transport: crate::runtime::RendererOutputTransportSenderSlot, ) -> ServiceWorkerRuntimeService { let service = ServiceWorkerRuntimeService { @@ -310,7 +312,7 @@ pub(crate) fn new_service_worker_runtime_service_with_resource_store_and_browser restored_worker_context_runtime, browser_resource_runtime, client_id_allocator, - browser_context_runtime_id, + worker_lifecycle, output_transport, )), }; @@ -367,9 +369,7 @@ impl ServiceWorkerRuntimeService { } #[cfg(test)] - fn take_target_output_events_for_test( - &self, - ) -> Vec { + fn take_target_output_events_for_test(&self) -> Vec { let mut receiver = self.inner.target_output_test_rx.lock(); super::target_output_streams::drain_service_worker_target_events_for_test( receiver @@ -378,75 +378,95 @@ impl ServiceWorkerRuntimeService { ) } - pub(super) fn running_host_for_version( + pub(super) fn enqueue_target_output( &self, - version_id: ServiceWorkerVersionId, - ) -> Option { - let state = self.inner.state.lock(); - let version = state.versions.get(&version_id)?; - let ServiceWorkerVersionRunningState::Running { host } = &version.running_state else { - return None; - }; - if !host.has_running_worker() { - return None; - } - Some(host.clone()) - } - - pub(super) fn target_output_journal( - &self, - version_id: ServiceWorkerVersionId, - ) -> Option { - self.inner.state.lock().target_output_journal(version_id) - } - - pub(super) fn enqueue_target_console_message( - &self, - version_id: ServiceWorkerVersionId, - run: RendererServiceWorkerRunIdentity, - message: WorkerConsoleMessage, + owner: ServiceWorkerRunOwner, + output: ServiceWorkerTargetOutput, ) { - let mut state = self.inner.state.lock(); - if state.observes_live_target_run(version_id, &run) { - state.record_target_console_message( - version_id, - run, - RendererServiceWorkerConsoleMessage { - message: message.message, - args: message.args, - stack: message.stack, - }, - ); - } + self.enqueue_service_lane_completion(ServiceWorkerRuntimeCompletion::target_output( + self.downgrade(), + owner, + output, + )); + self.signal_service_lane_wake(); } - pub(super) fn enqueue_target_exception_message( + pub(super) fn finish_target_output( &self, - version_id: ServiceWorkerVersionId, - run: RendererServiceWorkerRunIdentity, - message: String, - filename: String, - lineno: u32, - colno: u32, - event_kind: WorkerParentErrorEventKind, - phase: WorkerErrorPhase, - source: WorkerErrorSource, + owner: ServiceWorkerRunOwner, + output: ServiceWorkerTargetOutput, ) { + let (version_id, run) = owner.into_parts(); let mut state = self.inner.state.lock(); - if state.observes_live_target_run(version_id, &run) { - state.record_target_exception_message( - version_id, - run, - RendererServiceWorkerExceptionMessage { - message, - filename, - lineno, - colno, - event_kind: worker_error_event_kind_label(event_kind).to_owned(), - phase: worker_error_phase_label(phase).to_owned(), - source: worker_error_source_label(source).to_owned(), - }, - ); + if !state.observes_live_target_run(version_id, &run) { + return; + } + match output { + ServiceWorkerTargetOutput::Console(message) => { + state.record_target_console_message( + version_id, + run, + RendererServiceWorkerConsoleMessage { + message: message.message, + args: message.args, + stack: message.stack, + }, + ); + } + ServiceWorkerTargetOutput::Exception { + message, + filename, + lineno, + colno, + event_kind, + phase, + source, + } => { + state.record_target_exception_message( + version_id, + run, + RendererServiceWorkerExceptionMessage { + message, + filename, + lineno, + colno, + event_kind: worker_error_event_kind_label(event_kind).to_owned(), + phase: worker_error_phase_label(phase).to_owned(), + source: worker_error_source_label(source).to_owned(), + }, + ); + } + ServiceWorkerTargetOutput::InspectorMessages(batches) => { + for batch in batches { + let (responses, notifications): (Vec<_>, Vec<_>) = batch + .messages + .into_iter() + .partition(|message| match message { + RendererRuntimeInspectorMessage::Protocol(message) => { + message.get("id").is_some() + } + RendererRuntimeInspectorMessage::RuntimeContext(_) => false, + }); + for message in responses { + tracing::trace!( + version_id = version_id.as_u64(), + inspector_session_id = ?batch.inspector_session_id, + message = ?message, + "dropping stale service worker runtime inspector response without a deferred callback" + ); + } + state.record_target_runtime_inspector_messages( + version_id, + run.clone(), + batch.inspector_session_id, + notifications, + ); + } + } + ServiceWorkerTargetOutput::InspectorResponse(publication) => { + drop(state); + let _ = publication.commit(None); + } } } @@ -462,43 +482,6 @@ impl ServiceWorkerRuntimeService { } state.record_target_fetch_diagnostic(version_id, run, diagnostic); } - - pub(super) fn enqueue_target_runtime_inspector_messages( - &self, - version_id: ServiceWorkerVersionId, - run: RendererServiceWorkerRunIdentity, - batches: Vec, - ) { - let mut state = self.inner.state.lock(); - if !state.observes_live_target_run(version_id, &run) { - return; - } - for batch in batches { - let (responses, notifications): (Vec<_>, Vec<_>) = batch - .messages - .into_iter() - .partition(|message| match message { - RendererRuntimeInspectorMessage::Protocol(message) => { - message.get("id").is_some() - } - RendererRuntimeInspectorMessage::RuntimeContext(_) => false, - }); - for message in responses { - tracing::trace!( - version_id = version_id.as_u64(), - inspector_session_id = ?batch.inspector_session_id, - message = ?message, - "dropping stale service worker runtime inspector response without a deferred callback" - ); - } - state.record_target_runtime_inspector_messages( - version_id, - run.clone(), - batch.inspector_session_id, - notifications, - ); - } - } } fn worker_error_event_kind_label(kind: WorkerParentErrorEventKind) -> &'static str { @@ -776,16 +759,18 @@ mod tests { } fn exact_created_target_run( - events: &[crate::runtime::RendererServiceWorkerTargetEvent], + events: &[ServiceWorkerOutputForTest], version_id: ServiceWorkerVersionId, ) -> crate::runtime::RendererServiceWorkerRunIdentity { events .iter() .find_map(|event| match event { - crate::runtime::RendererServiceWorkerTargetEvent::Created { - info, - active_run: Some(active_run), - } if info.version_id == version_id.as_u64() => Some(active_run.clone()), + ServiceWorkerOutputForTest::Lifecycle( + crate::runtime::RendererServiceWorkerLifecycle::Created { + info, + active_run: Some(active_run), + }, + ) if info.version_id == version_id.as_u64() => Some(active_run.clone()), _ => None, }) .expect("a target created for a concrete worker host must expose its exact run") @@ -2319,10 +2304,10 @@ mod tests { assert!( target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::VersionUpdated { + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::VersionUpdated { version_id: updated_version_id, status, - } if *updated_version_id == version_id.as_u64() + }) if *updated_version_id == version_id.as_u64() && *status == crate::runtime::RendererServiceWorkerVersionStatus::Activated )), "newly controlled client should refresh ServiceWorker version projection: {target_events:?}" @@ -6252,10 +6237,10 @@ self.addEventListener("message", event => { assert!( target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Destroyed { + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::Destroyed { version_id: destroyed_version_id, active_run: Some(active_run), - } if *destroyed_version_id == version_id.as_u64() + }) if *destroyed_version_id == version_id.as_u64() && active_run == &created_run )), "failed installing target should destroy its exact starting run: {target_events:?}" @@ -6263,10 +6248,10 @@ self.addEventListener("message", event => { assert!( !target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Stopped { + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::Stopped { version_id: stopped_version_id, .. - } if *stopped_version_id == version_id.as_u64() + }) if *stopped_version_id == version_id.as_u64() )), "failed installing target should not be retained as stopped: {target_events:?}" ); @@ -6324,10 +6309,10 @@ self.addEventListener("message", event => { assert!( install_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::VersionUpdated { + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::VersionUpdated { version_id: updated_version_id, status: crate::runtime::RendererServiceWorkerVersionStatus::Installed, - } if *updated_version_id == version_id.as_u64() + }) if *updated_version_id == version_id.as_u64() )), "install completion should refresh target status to installed: {install_events:?}" ); @@ -6339,6 +6324,217 @@ self.addEventListener("message", event => { ); } + #[test] + fn service_worker_fifo_orders_target_output_with_script_and_start_completion() { + use crate::runtime::{RendererServiceWorkerLifecycle, RendererServiceWorkerObservation}; + + let service = new_service_worker_runtime_service(); + let (script_url, scope_url, registration_id, version_id) = + insert_starting_version(&service); + let run = exact_version_run(&service, version_id); + let owner = test_run_owner(version_id, &run); + service.inner.state.lock().record_target_created( + registration_id, + version_id, + script_url.clone(), + scope_url, + ); + service.take_target_output_events_for_test(); + let console = |message: &str| { + ServiceWorkerTargetOutput::Console(crate::worker::WorkerConsoleMessage { + message: message.to_owned(), + args: Vec::new(), + stack: None, + }) + }; + service.enqueue_target_output(owner.clone(), console("before")); + service.enqueue_imported_script_loaded( + registration_id, + owner.clone(), + test_worker_script_resource(&url("https://example.test/app/dep.js")), + ); + service.enqueue_worker_start_completed( + owner.clone(), + script_url.to_string(), + test_script_resource(&script_url), + ServiceWorkerFetchHandlerType::NoHandler, + ); + service.enqueue_target_output( + owner.clone(), + ServiceWorkerTargetOutput::InspectorMessages(vec![ + crate::worker::WorkerRuntimeInspectorMessageBatch { + inspector_session_id: Some("fifo-session".to_owned()), + messages: vec![RendererRuntimeInspectorMessage::from_v8_inspector_message( + serde_json::json!({ + "method": "Debugger.scriptParsed", "params": {"scriptId": "fifo-script"} + }), + )], + }, + ]), + ); + service.enqueue_target_output(owner, console("after")); + assert!( + service.take_target_output_events_for_test().is_empty(), + "output cannot bypass the service lane" + ); + assert_eq!(service.drain_service_lane(), 5); + assert_eq!( + service.diagnostics_snapshot().versions[0].imported_script_count, + 1 + ); + let events = service.take_target_output_events_for_test(); + let order = events + .iter() + .map(|event| match event { + ServiceWorkerOutputForTest::Observation( + RendererServiceWorkerObservation::Console { + version_id: output_version, + run: output_run, + message, + }, + ) => { + assert_eq!(*output_version, version_id.as_u64()); + assert_eq!(output_run, &run); + message.message.as_str() + } + ServiceWorkerOutputForTest::Lifecycle( + RendererServiceWorkerLifecycle::Started { + version_id: output_version, + run: output_run, + }, + ) => { + assert_eq!(*output_version, version_id.as_u64()); + assert_eq!(output_run, &run); + "started" + } + ServiceWorkerOutputForTest::Observation( + RendererServiceWorkerObservation::RuntimeInspectorMessages { + version_id: output_version, + run: output_run, + inspector_session_id, + messages, + }, + ) => { + assert_eq!(*output_version, version_id.as_u64()); + assert_eq!(output_run, &run); + assert_eq!(inspector_session_id.as_deref(), Some("fifo-session")); + assert_eq!(messages.len(), 1); + "inspector" + } + other => panic!("unexpected service FIFO output: {other:?}"), + }) + .collect::>(); + assert_eq!(order, ["before", "started", "inspector", "after"]); + } + + #[test] + fn service_worker_fifo_publishes_exception_before_failed_run_retirement() { + use crate::runtime::{RendererServiceWorkerLifecycle, RendererServiceWorkerObservation}; + + let service = new_service_worker_runtime_service(); + let (script_url, scope_url, registration_id, version_id) = + insert_starting_version(&service); + let run = exact_version_run(&service, version_id); + let owner = test_run_owner(version_id, &run); + service.inner.state.lock().record_target_created( + registration_id, + version_id, + script_url.clone(), + scope_url, + ); + service.take_target_output_events_for_test(); + service.enqueue_target_output( + owner.clone(), + ServiceWorkerTargetOutput::Exception { + message: "bootstrap broken".to_owned(), + filename: script_url.to_string(), + lineno: 1, + colno: 1, + event_kind: WorkerParentErrorEventKind::ErrorEvent, + phase: WorkerErrorPhase::Bootstrap, + source: WorkerErrorSource::InitialScriptEvaluation, + }, + ); + service.enqueue_worker_start_failed( + owner.clone(), + ServiceWorkerVersionStartFailure::BootstrapChannelClosed, + ); + service.enqueue_target_output( + owner, + ServiceWorkerTargetOutput::Console(crate::worker::WorkerConsoleMessage { + message: "after retirement".to_owned(), + args: Vec::new(), + stack: None, + }), + ); + assert!(service.take_target_output_events_for_test().is_empty()); + assert_eq!(service.drain_service_lane(), 3); + let events = service.take_target_output_events_for_test(); + assert!( + matches!(events.first(), Some(ServiceWorkerOutputForTest::Observation( + RendererServiceWorkerObservation::Exception {version_id: output_version, run: output_run, message} + )) if *output_version == version_id.as_u64() && output_run == &run && message.message == "bootstrap broken"), + "{events:?}" + ); + assert!( + matches!(events.last(), Some(ServiceWorkerOutputForTest::Lifecycle( + RendererServiceWorkerLifecycle::Destroyed {version_id: output_version, active_run: Some(output_run)} + )) if *output_version == version_id.as_u64() && output_run == &run), + "{events:?}" + ); + assert!( + !events.iter().any(|event| matches!( + event, + ServiceWorkerOutputForTest::Observation( + RendererServiceWorkerObservation::Console { .. } + ) + )), + "late output must not outlive its failed run: {events:?}" + ); + } + + #[test] + fn service_worker_fifo_drops_queued_output_from_a_replaced_run() { + let service = new_service_worker_runtime_service(); + let (script_url, scope_url, registration_id, version_id) = + insert_starting_version(&service); + let old_run = exact_version_run(&service, version_id); + service.inner.state.lock().record_target_created( + registration_id, + version_id, + script_url, + scope_url, + ); + service.take_target_output_events_for_test(); + service.enqueue_target_output( + test_run_owner(version_id, &old_run), + ServiceWorkerTargetOutput::Console(crate::worker::WorkerConsoleMessage { + message: "stale".to_owned(), + args: Vec::new(), + stack: None, + }), + ); + let new_run = RendererServiceWorkerRunIdentity::fresh(); + { + let mut state = service.inner.state.lock(); + let version = state.versions.get_mut(&version_id).unwrap(); + version.run = new_run.clone(); + version.running_state = ServiceWorkerVersionRunningState::Starting { + host: new_running_test_host(version_id, &new_run), + }; + } + assert!( + service.take_target_output_events_for_test().is_empty(), + "output must await owner validation" + ); + assert_eq!(service.drain_service_lane(), 1); + assert!( + service.take_target_output_events_for_test().is_empty(), + "an old parent FIFO cannot publish into the replacement run" + ); + assert_eq!(exact_version_run(&service, version_id), new_run); + } + #[test] fn stale_worker_start_completion_does_not_advance_version_state() { let service = new_service_worker_runtime_service(); @@ -6603,7 +6799,7 @@ self.addEventListener("message", event => { assert!( target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Created { info, .. } + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::Created { info, .. }) if info.version_id == version_id.as_u64() )), "deferred install launch should still expose the installing target: {target_events:?}" @@ -6614,10 +6810,10 @@ self.addEventListener("message", event => { assert!( shutdown_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Destroyed { + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::Destroyed { version_id: destroyed_version_id, active_run: Some(active_run), - } if *destroyed_version_id == version_id.as_u64() + }) if *destroyed_version_id == version_id.as_u64() && active_run == &created_run )), "context shutdown should destroy the pending launch's exact run: {shutdown_events:?}" @@ -6896,7 +7092,7 @@ self.addEventListener("message", event => { assert!( target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Created { info, .. } + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::Created { info, .. }) if info.version_id != active_version_id.as_u64() )), "deferred update check should still expose the precreated installing target: {target_events:?}" @@ -7072,7 +7268,7 @@ self.addEventListener("message", event => { assert!( target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Created { info, .. } + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::Created { info, .. }) if info.version_id == new_version_id.as_u64() )), "force-update update check should precreate a Service Worker target: {target_events:?}" @@ -7080,7 +7276,9 @@ self.addEventListener("message", event => { assert!( target_events.iter().all(|event| !matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Console { .. } + ServiceWorkerOutputForTest::Observation( + crate::runtime::RendererServiceWorkerObservation::Console { .. } + ) )), "force-update warning should wait until the install launch starts: {target_events:?}" ); @@ -7097,11 +7295,11 @@ self.addEventListener("message", event => { assert!( target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Console { + ServiceWorkerOutputForTest::Observation(crate::runtime::RendererServiceWorkerObservation::Console { version_id, message, .. - } if *version_id == new_version_id.as_u64() + }) if *version_id == new_version_id.as_u64() && message.message == SERVICE_WORKER_FORCE_UPDATE_DEVTOOLS_CONSOLE_MESSAGE && message.args.is_empty() && message.stack.is_none() @@ -7112,7 +7310,7 @@ self.addEventListener("message", event => { } #[test] - fn resource_store_restores_registration_at_service_startup_for_client_observation() { + fn native_service_worker_restore_does_not_manufacture_a_run_before_transport_binding() { let resource_store = new_shared_service_worker_resource_store(); let first_service = new_service_worker_runtime_service_with_resource_store( resource_store.clone(), @@ -7145,9 +7343,24 @@ self.addEventListener("message", event => { .expect("stored registration resources should persist") ); - let second_service = new_service_worker_runtime_service_with_resource_store( - resource_store, - test_worker_context_runtime(), + let restored_owner = + RendererBrowserContextRuntime::new_with_worker_context_and_service_worker_store_for_test( + test_worker_context_runtime(), resource_store, + ); + let facts = Arc::new(parking_lot::Mutex::new(Vec::new())); + let observed = facts.clone(); + restored_owner.install_worker_lifecycle_handler(move |input| { + observed.lock().push(input.lifecycle.as_ref().clone()); + }); + let second_service = restored_owner.service_worker_runtime(); + assert!( + matches!( + facts.lock().as_slice(), + [crate::runtime::RendererWorkerLifecycle::Service( + crate::runtime::RendererServiceWorkerLifecycle::Created { info, active_run: None }, + )] if info.version_id == version_id.as_u64() + ), + "restoration reports a stable stopped version before any output transport is bound" ); assert_eq!(second_service.diagnostics_snapshot().registration_count, 1); @@ -8026,7 +8239,7 @@ self.addEventListener("message", event => { assert!( target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Created { info, .. } + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::Created { info, .. }) if info.version_id == new_version_id.as_u64() && info.registration_id == registration_id.as_u64() && info.script_url == script_url.as_str() @@ -8049,10 +8262,10 @@ self.addEventListener("message", event => { assert!( target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Destroyed { + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::Destroyed { version_id, active_run: Some(active_run), - } if *version_id == new_version_id.as_u64() + }) if *version_id == new_version_id.as_u64() && active_run == &created_run )), "identical update check should destroy the precreated target's exact run: {target_events:?}" @@ -8912,14 +9125,18 @@ self.addEventListener("message", event => { assert!(!completion_queue.has_ready_completion()); let target_events = service.take_target_output_events_for_test(); - let Some(crate::runtime::RendererServiceWorkerTargetEvent::FetchDiagnostic { - version_id: diagnostic_version_id, - diagnostic, - .. - }) = target_events.iter().find(|event| { + let Some(ServiceWorkerOutputForTest::Observation( + crate::runtime::RendererServiceWorkerObservation::FetchDiagnostic { + version_id: diagnostic_version_id, + diagnostic, + .. + }, + )) = target_events.iter().find(|event| { matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::FetchDiagnostic { .. } + ServiceWorkerOutputForTest::Observation( + crate::runtime::RendererServiceWorkerObservation::FetchDiagnostic { .. } + ) ) }) else { @@ -9958,11 +10175,11 @@ self.addEventListener("message", event => { assert!( target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Stopped { + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::Stopped { version_id: stopped_version_id, reason, .. - } if *stopped_version_id == version_id.as_u64() + }) if *stopped_version_id == version_id.as_u64() && reason == "idle_timeout" )), "idle timeout should enqueue a stopped target lifecycle event: {target_events:?}" @@ -9970,10 +10187,10 @@ self.addEventListener("message", event => { assert!( !target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Destroyed { + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::Destroyed { version_id: destroyed_version_id, .. - } if *destroyed_version_id == version_id.as_u64() + }) if *destroyed_version_id == version_id.as_u64() )), "idle timeout must retain the service worker target: {target_events:?}" ); @@ -10022,11 +10239,11 @@ self.addEventListener("message", event => { assert!( target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Stopped { + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::Stopped { version_id: stopped_version_id, reason, .. - } if *stopped_version_id == version_id.as_u64() + }) if *stopped_version_id == version_id.as_u64() && reason == "devtools_stop" )), "DevTools stopWorker should enqueue a stopped target event: {target_events:?}" @@ -10068,10 +10285,10 @@ self.addEventListener("message", event => { assert!( target_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::Destroyed { + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::Destroyed { version_id: destroyed_version_id, active_run: None, - } if *destroyed_version_id == version_id.as_u64() + }) if *destroyed_version_id == version_id.as_u64() )), "DevTools unregister should destroy the runtime-owned target: {target_events:?}" ); @@ -11552,10 +11769,10 @@ self.addEventListener("message", event => { assert!( activation_start_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::VersionUpdated { + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::VersionUpdated { version_id: updated_version_id, status: crate::runtime::RendererServiceWorkerVersionStatus::Activating, - } if *updated_version_id == waiting_version_id.as_u64() + }) if *updated_version_id == waiting_version_id.as_u64() )), "activation start should refresh target status to activating: {activation_start_events:?}" ); @@ -11678,10 +11895,10 @@ self.addEventListener("message", event => { assert!( activation_events.iter().any(|event| matches!( event, - crate::runtime::RendererServiceWorkerTargetEvent::VersionUpdated { + ServiceWorkerOutputForTest::Lifecycle(crate::runtime::RendererServiceWorkerLifecycle::VersionUpdated { version_id: updated_version_id, status: crate::runtime::RendererServiceWorkerVersionStatus::Activated, - } if *updated_version_id == version_id.as_u64() + }) if *updated_version_id == version_id.as_u64() )), "activate completion should refresh target status to activated: {activation_events:?}" ); @@ -12303,20 +12520,24 @@ self.addEventListener("message", event => { let target_events = service.take_target_output_events_for_test(); assert_eq!(target_events.len(), 2); - let crate::runtime::RendererServiceWorkerTargetEvent::Stopped { - version_id, - run: _, - reason, - } = &target_events[0] + let ServiceWorkerOutputForTest::Lifecycle( + crate::runtime::RendererServiceWorkerLifecycle::Stopped { + version_id, + run: _, + reason, + }, + ) = &target_events[0] else { panic!("active replacement must first retire its exact run: {target_events:?}"); }; assert_eq!(*version_id, active_version_id.as_u64()); assert_eq!(reason, "replaced_by_newer_active_worker"); - let crate::runtime::RendererServiceWorkerTargetEvent::Destroyed { - version_id, - active_run, - } = &target_events[1] + let ServiceWorkerOutputForTest::Lifecycle( + crate::runtime::RendererServiceWorkerLifecycle::Destroyed { + version_id, + active_run, + }, + ) = &target_events[1] else { panic!("active replacement must then destroy its version: {target_events:?}"); }; @@ -12335,9 +12556,7 @@ self.addEventListener("message", event => { ServiceWorkerVersionLifecycleState::Redundant ); assert!( - !state - .service_worker_target_infos - .contains_key(&active_version_id), + state.target_output_journal(active_version_id).is_none(), "doomed active version should no longer be exposed as a Service Worker target" ); } diff --git a/moli-renderer-v8/src/service_worker_runtime/service/devtools_commands.rs b/moli-renderer-v8/src/service_worker_runtime/service/devtools_commands.rs index bfd66f05e..dca123ad6 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/devtools_commands.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/devtools_commands.rs @@ -331,6 +331,7 @@ impl ServiceWorkerRuntimeService { ); version.running_state = ServiceWorkerVersionRunningState::Starting { host: host.clone() }; + state.record_target_starting(version_id); Some(ServiceWorkerQueuedLaunch { params, host, diff --git a/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs b/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs index 578bec6cb..3a1d9f4d4 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/event_dispatch.rs @@ -407,6 +407,7 @@ impl ServiceWorkerRuntimeService { .push_back(ServiceWorkerPendingStartEvent::Fetch(event)); version.running_state = ServiceWorkerVersionRunningState::Starting { host: host.clone() }; + state.record_target_starting(version_id); ( None, Some(ServiceWorkerQueuedLaunch { diff --git a/moli-renderer-v8/src/service_worker_runtime/service/event_start.rs b/moli-renderer-v8/src/service_worker_runtime/service/event_start.rs index b85df0125..8d29e8dc8 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/event_start.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/event_start.rs @@ -359,6 +359,7 @@ impl ServiceWorkerRuntimeService { ); version.running_state = ServiceWorkerVersionRunningState::Starting { host: host.clone() }; + state.record_target_starting(host.version_id()); Some(ServiceWorkerLifecycleStart::Start(Box::new( ServiceWorkerQueuedLaunch { params, @@ -427,6 +428,7 @@ impl ServiceWorkerRuntimeService { .push_back(ServiceWorkerPendingStartEvent::Message(event)); version.running_state = ServiceWorkerVersionRunningState::Starting { host: host.clone() }; + state.record_target_starting(host.version_id()); ServiceWorkerMessageStart::Start(Box::new(ServiceWorkerQueuedLaunch { params, host, @@ -489,6 +491,7 @@ impl ServiceWorkerRuntimeService { .push_back(ServiceWorkerPendingStartEvent::Notification(event)); version.running_state = ServiceWorkerVersionRunningState::Starting { host: host.clone() }; + state.record_target_starting(host.version_id()); ServiceWorkerNotificationStart::Start(Box::new(ServiceWorkerQueuedLaunch { params, host, @@ -553,6 +556,7 @@ impl ServiceWorkerRuntimeService { .push_back(ServiceWorkerPendingStartEvent::Push(event)); version.running_state = ServiceWorkerVersionRunningState::Starting { host: host.clone() }; + state.record_target_starting(host.version_id()); ServiceWorkerPushStart::Start(Box::new(ServiceWorkerQueuedLaunch { params, host, @@ -615,6 +619,7 @@ impl ServiceWorkerRuntimeService { .push_back(ServiceWorkerPendingStartEvent::Sync(event)); version.running_state = ServiceWorkerVersionRunningState::Starting { host: host.clone() }; + state.record_target_starting(host.version_id()); ServiceWorkerSyncStart::Start(Box::new(ServiceWorkerQueuedLaunch { params, host, @@ -677,6 +682,7 @@ impl ServiceWorkerRuntimeService { .push_back(ServiceWorkerPendingStartEvent::PeriodicSync(event)); version.running_state = ServiceWorkerVersionRunningState::Starting { host: host.clone() }; + state.record_target_starting(host.version_id()); ServiceWorkerPeriodicSyncStart::Start(Box::new(ServiceWorkerQueuedLaunch { params, host, diff --git a/moli-renderer-v8/src/service_worker_runtime/service/inspection.rs b/moli-renderer-v8/src/service_worker_runtime/service/inspection.rs new file mode 100644 index 000000000..65d586190 --- /dev/null +++ b/moli-renderer-v8/src/service_worker_runtime/service/inspection.rs @@ -0,0 +1,25 @@ +use super::super::{ids::ServiceWorkerVersionId, version::ServiceWorkerVersionRunningState}; +use super::ServiceWorkerRuntimeService; +use crate::runtime::{RendererServiceWorkerRunIdentity, RendererWorkerInspectionEndpoint}; + +impl ServiceWorkerRuntimeService { + pub(crate) fn inspection_endpoint( + &self, + version_id: ServiceWorkerVersionId, + run: &RendererServiceWorkerRunIdentity, + ) -> Option { + let state = self.inner.state.lock(); + let version = state.versions.get(&version_id)?; + let ServiceWorkerVersionRunningState::Running { host } = &version.running_state else { + return None; + }; + if &host.run_identity() != run { + return None; + } + Some(RendererWorkerInspectionEndpoint::new( + host.inspection_handle()?, + state.target_output_journal(version_id), + "ServiceWorkerRuntimeUnavailable", + )) + } +} diff --git a/moli-renderer-v8/src/service_worker_runtime/service/runtime_protocol.rs b/moli-renderer-v8/src/service_worker_runtime/service/runtime_protocol.rs deleted file mode 100644 index e11e75ea9..000000000 --- a/moli-renderer-v8/src/service_worker_runtime/service/runtime_protocol.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::runtime::{RendererRuntimeInspectorMessage, RendererRuntimeInspectorResponseSender}; - -use super::super::ids::ServiceWorkerVersionId; -use super::ServiceWorkerRuntimeService; - -impl ServiceWorkerRuntimeService { - pub(crate) async fn dispatch_runtime_protocol_message( - &self, - version_id: ServiceWorkerVersionId, - inspector_session_id: Option, - raw_json: String, - ) -> Result, String> { - let Some(host) = self.running_host_for_version(version_id) else { - return Err("ServiceWorkerRuntimeUnavailable".to_owned()); - }; - host.dispatch_worker_runtime_protocol_message_without_deferred_response( - inspector_session_id, - raw_json, - ) - .await - } - - pub(crate) async fn dispatch_runtime_protocol_message_with_deferred_response( - &self, - version_id: ServiceWorkerVersionId, - inspector_session_id: Option, - raw_json: String, - deferred_response: RendererRuntimeInspectorResponseSender, - ) -> Result, String> { - let Some(host) = self.running_host_for_version(version_id) else { - return Err("ServiceWorkerRuntimeUnavailable".to_owned()); - }; - host.dispatch_worker_runtime_protocol_message_with_deferred_response( - inspector_session_id, - raw_json, - deferred_response, - ) - .await - } - - pub(crate) async fn dispatch_runtime_protocol_message_with_devtools_session_response( - &self, - version_id: ServiceWorkerVersionId, - inspector_session_id: String, - raw_json: String, - response: RendererRuntimeInspectorResponseSender, - ) -> Result { - let Some(host) = self.running_host_for_version(version_id) else { - return Err("ServiceWorkerRuntimeUnavailable".to_owned()); - }; - let Some(output_journal) = self.target_output_journal(version_id) else { - return Err("ServiceWorkerRuntimeUnavailable".to_owned()); - }; - let response = response - .route_to_worker_devtools_session_output(inspector_session_id.clone(), output_journal); - let error_response = response.clone(); - let settlement = response - .take_session_response_settlement_receiver() - .expect("a Worker DevTools response must own one settlement receiver"); - let dispatch = host - .dispatch_worker_runtime_protocol_message_with_deferred_response( - Some(inspector_session_id), - raw_json, - response, - ) - .await; - Ok( - crate::runtime::CompletedWorkerRuntimeInspectorCommandDispatch::finish( - dispatch, - settlement, - error_response, - ), - ) - } - - pub(crate) fn detach_runtime_inspector_session( - &self, - version_id: ServiceWorkerVersionId, - inspector_session_id: Option, - ) -> bool { - let Some(host) = self.running_host_for_version(version_id) else { - return false; - }; - host.detach_worker_runtime_inspector_session(inspector_session_id) - } -} diff --git a/moli-renderer-v8/src/service_worker_runtime/service/worker_completion.rs b/moli-renderer-v8/src/service_worker_runtime/service/worker_completion.rs index 8c2e4e655..594b7a35e 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/worker_completion.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/worker_completion.rs @@ -874,7 +874,7 @@ impl ServiceWorkerRuntimeService { Vec::new() }; if failed_installing_version { - state.record_target_destroyed(version_id); + state.record_target_destroyed_with_run(version_id, target_run); state.versions.remove(&version_id); } else if let Some(run) = target_run { state.record_target_stopped(version_id, run, "start_failed"); diff --git a/moli-renderer-v8/src/service_worker_runtime/start_completion.rs b/moli-renderer-v8/src/service_worker_runtime/start_completion.rs index 9eafad303..1ca7859b0 100644 --- a/moli-renderer-v8/src/service_worker_runtime/start_completion.rs +++ b/moli-renderer-v8/src/service_worker_runtime/start_completion.rs @@ -26,12 +26,34 @@ use super::{ }; use crate::worker::WorkerScriptResource; +/// Output work from one physical Worker parent FIFO, not published facts. +/// It shares the existing service lane with bootstrap and lifecycle decisions. +#[derive(Debug)] +pub(super) enum ServiceWorkerTargetOutput { + Console(crate::worker::WorkerConsoleMessage), + Exception { + message: String, + filename: String, + lineno: u32, + colno: u32, + event_kind: crate::worker::WorkerParentErrorEventKind, + phase: crate::worker::WorkerErrorPhase, + source: crate::worker::WorkerErrorSource, + }, + InspectorMessages(Vec), + InspectorResponse(crate::runtime::RendererRuntimeInspectorResponsePublication), +} + pub(super) struct ServiceWorkerRuntimeCompletion { runtime_service: WeakServiceWorkerRuntimeService, kind: ServiceWorkerRuntimeCompletionKind, } enum ServiceWorkerRuntimeCompletionKind { + TargetOutput { + owner: ServiceWorkerRunOwner, + output: ServiceWorkerTargetOutput, + }, VersionStartCompleted { owner: ServiceWorkerRunOwner, final_script_url: String, @@ -165,6 +187,17 @@ enum ServiceWorkerRuntimeCompletionKind { } impl ServiceWorkerRuntimeCompletion { + pub(super) fn target_output( + runtime_service: WeakServiceWorkerRuntimeService, + owner: ServiceWorkerRunOwner, + output: ServiceWorkerTargetOutput, + ) -> Self { + Self { + runtime_service, + kind: ServiceWorkerRuntimeCompletionKind::TargetOutput { owner, output }, + } + } + pub(super) fn version_start_completed( runtime_service: WeakServiceWorkerRuntimeService, owner: ServiceWorkerRunOwner, @@ -599,6 +632,11 @@ impl ServiceWorkerRuntimeCompletion { pub(super) fn complete(self) { match self.kind { + ServiceWorkerRuntimeCompletionKind::TargetOutput { owner, output } => { + if let Some(service) = self.runtime_service.upgrade() { + service.finish_target_output(owner, output); + } + } ServiceWorkerRuntimeCompletionKind::VersionStartCompleted { owner, final_script_url, @@ -828,6 +866,10 @@ impl ServiceWorkerRuntimeCompletion { impl fmt::Debug for ServiceWorkerRuntimeCompletion { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.kind { + ServiceWorkerRuntimeCompletionKind::TargetOutput { owner, .. } => f + .debug_struct("ServiceWorkerRuntimeCompletion::TargetOutput") + .field("owner", owner) + .finish_non_exhaustive(), ServiceWorkerRuntimeCompletionKind::VersionStartCompleted { owner, .. } => f .debug_struct("ServiceWorkerRuntimeCompletion::VersionStartCompleted") .field("owner", owner) diff --git a/moli-renderer-v8/src/service_worker_runtime/state.rs b/moli-renderer-v8/src/service_worker_runtime/state.rs index bbc6bdb26..70bcb1775 100644 --- a/moli-renderer-v8/src/service_worker_runtime/state.rs +++ b/moli-renderer-v8/src/service_worker_runtime/state.rs @@ -15,9 +15,9 @@ use crate::{ runtime::{ RendererRuntimeInspectorMessage, RendererServiceWorkerConsoleMessage, RendererServiceWorkerExceptionMessage, RendererServiceWorkerFetchDiagnostic, - RendererServiceWorkerRunIdentity, RendererServiceWorkerTargetEvent, - RendererServiceWorkerTargetInfo, RendererServiceWorkerVersionStatus, - RendererWorkerContextRuntime, + RendererServiceWorkerLifecycle, RendererServiceWorkerObservation, + RendererServiceWorkerRunIdentity, RendererServiceWorkerTargetInfo, + RendererServiceWorkerVersionStatus, RendererWorkerContextRuntime, }, types::{ AsyncSubresourceNetworkContext, ServiceWorkerClientMessageCompletion, @@ -148,7 +148,7 @@ impl ServiceWorkerRuntimeInner { restored_worker_context_runtime: RendererWorkerContextRuntime, browser_resource_runtime: crate::network::BrowserResourceRuntimeBinding, client_id_allocator: super::ids::ServiceWorkerClientIdAllocator, - browser_context_runtime_id: crate::runtime::RendererBrowserContextRuntimeId, + worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, output_transport: crate::runtime::RendererOutputTransportSenderSlot, ) -> Self { Self { @@ -162,7 +162,7 @@ impl ServiceWorkerRuntimeInner { force_update_on_page_load: AtomicBool::new(false), pause_new_workers_on_start_for_devtools: AtomicBool::new(false), state: Mutex::new(ServiceWorkerRuntimeState::new( - browser_context_runtime_id, + worker_lifecycle, output_transport, )), service_lane: ServiceWorkerServiceLane::default(), @@ -202,10 +202,6 @@ pub(super) struct ServiceWorkerRuntimeState { pub(super) devtools_attached_versions: HashSet, pub(super) main_script_update_check_diagnostics: HashMap, - pub(super) service_worker_target_infos: - HashMap, - service_worker_target_run_projections: - HashMap, target_output_streams: ServiceWorkerTargetOutputStreams, pub(super) devtools_related_pause_on_start_policies: Vec, @@ -214,71 +210,9 @@ pub(super) struct ServiceWorkerRuntimeState { HashMap, } -/// Renderer-owned protocol identity residence for one stable ServiceWorker -/// version target. -/// -/// This is a projection journal, not a second run authority. The concrete -/// worker host creates the identity when its V8 run is created; this residence -/// only remembers which exact identity has been exposed to protocol and which -/// one has already reached its terminal. -#[derive(Debug, Default)] -struct RendererServiceWorkerTargetRunResidence { - last_retired: Option, - live: Option, -} - -impl RendererServiceWorkerTargetRunResidence { - /// Returns the exact current run or establishes the next renderer-owned - /// run projection. - /// - /// Run-specific output can precede the public `Started` event, so the - /// first such fact is allowed to establish the projection. A different - /// exact identity may not replace a live run; the renderer must publish - /// and retire the old run first. - fn observe_run( - &mut self, - run: RendererServiceWorkerRunIdentity, - ) -> Option { - if self.last_retired.as_ref() == Some(&run) { - return None; - } - if let Some(live) = &self.live { - assert!( - live == &run, - "a different ServiceWorker host must not replace a live renderer run" - ); - return Some(live.clone()); - } - - self.live = Some(run.clone()); - Some(run) - } - - fn retire_run( - &mut self, - run: RendererServiceWorkerRunIdentity, - ) -> Option { - let identity = self.observe_run(run)?; - let live = self - .live - .take() - .expect("an observed ServiceWorker run must remain resident until retirement"); - assert_eq!( - live, identity, - "ServiceWorker retirement must consume the exact renderer run" - ); - self.last_retired = Some(identity.clone()); - Some(identity) - } - - fn active_run(&self) -> Option { - self.live.clone() - } -} - impl ServiceWorkerRuntimeState { fn new( - browser_context_runtime_id: crate::runtime::RendererBrowserContextRuntimeId, + worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, output_transport: crate::runtime::RendererOutputTransportSenderSlot, ) -> Self { Self { @@ -300,10 +234,8 @@ impl ServiceWorkerRuntimeState { pending_devtools_evaluation_releases: HashSet::new(), devtools_attached_versions: HashSet::new(), main_script_update_check_diagnostics: HashMap::new(), - service_worker_target_infos: HashMap::new(), - service_worker_target_run_projections: HashMap::new(), target_output_streams: ServiceWorkerTargetOutputStreams::new( - browser_context_runtime_id, + worker_lifecycle, output_transport, ), devtools_related_pause_on_start_policies: Vec::new(), @@ -320,7 +252,9 @@ impl ServiceWorkerRuntimeState { output_transport.set(sender); ( Self::new( - crate::runtime::RendererBrowserContextRuntimeId::new_for_testing(0), + crate::runtime::RendererWorkerLifecycleReporter::new( + crate::runtime::RendererBrowserContextRuntimeId::new_for_testing(0), + ), output_transport, ), receiver, @@ -370,36 +304,40 @@ impl ServiceWorkerRuntimeState { script_url: Url, scope_url: Url, ) { - if self.service_worker_target_infos.contains_key(&version_id) { + if self.target_output_journal(version_id).is_some() { return; } - let status = self.target_status_for_version(version_id); - let active_run = self.live_target_run(version_id); let info = RendererServiceWorkerTargetInfo { registration_id: registration_id.as_u64(), version_id: version_id.as_u64(), script_url: script_url.to_string(), scope_url: scope_url.to_string(), - status, + status: self.target_status_for_version(version_id), }; - self.service_worker_target_infos - .insert(version_id, info.clone()); - let mut projection = RendererServiceWorkerTargetRunResidence::default(); - if let Some(active_run) = &active_run { - projection - .observe_run(active_run.clone()) - .expect("a new target may project its exact live worker host"); - } - let previous = self - .service_worker_target_run_projections - .insert(version_id, projection); - assert!( - previous.is_none(), - "a newly created ServiceWorker target must own one run residence" - ); self.target_output_streams.publish_created( version_id, - RendererServiceWorkerTargetEvent::Created { info, active_run }, + RendererServiceWorkerLifecycle::Created { + info, + active_run: self.live_target_run(version_id), + }, + ); + } + + /// Called under the runtime state lock immediately after installing a host. + /// Console and inspector output cannot introduce a run on their own. + pub(super) fn record_target_starting(&self, version_id: ServiceWorkerVersionId) { + if self.target_output_journal(version_id).is_none() { + return; + } + let Some(run) = self.live_target_run(version_id) else { + return; + }; + self.target_output_streams.publish( + version_id, + RendererServiceWorkerLifecycle::Starting { + version_id: version_id.as_u64(), + run, + }, ); } @@ -408,19 +346,14 @@ impl ServiceWorkerRuntimeState { version_id: ServiceWorkerVersionId, run: RendererServiceWorkerRunIdentity, ) -> bool { - if !self.service_worker_target_infos.contains_key(&version_id) { + if self.target_output_journal(version_id).is_none() + || !self.observes_live_target_run(version_id, &run) + { return false; } - let Some(run) = self - .service_worker_target_run_projections - .get_mut(&version_id) - .and_then(|residence| residence.observe_run(run)) - else { - return false; - }; self.target_output_streams.publish( version_id, - RendererServiceWorkerTargetEvent::Started { + RendererServiceWorkerLifecycle::Started { version_id: version_id.as_u64(), run, }, @@ -434,19 +367,17 @@ impl ServiceWorkerRuntimeState { run: RendererServiceWorkerRunIdentity, reason: impl Into, ) -> bool { - if !self.service_worker_target_infos.contains_key(&version_id) { + if self.target_output_journal(version_id).is_none() + || !self + .versions + .get(&version_id) + .is_some_and(|version| version.run == run) + { return false; } - let Some(run) = self - .service_worker_target_run_projections - .get_mut(&version_id) - .and_then(|residence| residence.retire_run(run)) - else { - return false; - }; self.target_output_streams.publish( version_id, - RendererServiceWorkerTargetEvent::Stopped { + RendererServiceWorkerLifecycle::Stopped { version_id: version_id.as_u64(), run, reason: reason.into(), @@ -456,22 +387,21 @@ impl ServiceWorkerRuntimeState { } pub(super) fn record_target_destroyed(&mut self, version_id: ServiceWorkerVersionId) -> bool { + self.record_target_destroyed_with_run(version_id, self.live_target_run(version_id)) + } + + pub(super) fn record_target_destroyed_with_run( + &mut self, + version_id: ServiceWorkerVersionId, + active_run: Option, + ) -> bool { self.devtools_attached_versions.remove(&version_id); - if self - .service_worker_target_infos - .remove(&version_id) - .is_none() - { + if self.target_output_journal(version_id).is_none() { return false; } - let active_run = self - .service_worker_target_run_projections - .remove(&version_id) - .expect("a ServiceWorker target must retain its renderer run residence") - .active_run(); self.target_output_streams.publish_destroyed( version_id, - RendererServiceWorkerTargetEvent::Destroyed { + RendererServiceWorkerLifecycle::Destroyed { version_id: version_id.as_u64(), active_run, }, @@ -483,16 +413,14 @@ impl ServiceWorkerRuntimeState { &mut self, version_id: ServiceWorkerVersionId, ) -> bool { - let status = self.target_status_for_version(version_id); - let Some(info) = self.service_worker_target_infos.get_mut(&version_id) else { + if self.target_output_journal(version_id).is_none() { return false; - }; - info.status = status; + } self.target_output_streams.publish( version_id, - RendererServiceWorkerTargetEvent::VersionUpdated { + RendererServiceWorkerLifecycle::VersionUpdated { version_id: version_id.as_u64(), - status, + status: self.target_status_for_version(version_id), }, ); true @@ -514,19 +442,14 @@ impl ServiceWorkerRuntimeState { run: RendererServiceWorkerRunIdentity, message: RendererServiceWorkerConsoleMessage, ) -> bool { - if !self.service_worker_target_infos.contains_key(&version_id) { + if self.target_output_journal(version_id).is_none() + || !self.observes_live_target_run(version_id, &run) + { return false; } - let Some(run) = self - .service_worker_target_run_projections - .get_mut(&version_id) - .and_then(|residence| residence.observe_run(run)) - else { - return false; - }; - self.target_output_streams.publish( + self.target_output_streams.publish_observation( version_id, - RendererServiceWorkerTargetEvent::Console { + RendererServiceWorkerObservation::Console { version_id: version_id.as_u64(), run, message, @@ -541,19 +464,14 @@ impl ServiceWorkerRuntimeState { run: RendererServiceWorkerRunIdentity, message: RendererServiceWorkerExceptionMessage, ) -> bool { - if !self.service_worker_target_infos.contains_key(&version_id) { + if self.target_output_journal(version_id).is_none() + || !self.observes_live_target_run(version_id, &run) + { return false; } - let Some(run) = self - .service_worker_target_run_projections - .get_mut(&version_id) - .and_then(|residence| residence.observe_run(run)) - else { - return false; - }; - self.target_output_streams.publish( + self.target_output_streams.publish_observation( version_id, - RendererServiceWorkerTargetEvent::Exception { + RendererServiceWorkerObservation::Exception { version_id: version_id.as_u64(), run, message, @@ -568,19 +486,14 @@ impl ServiceWorkerRuntimeState { run: RendererServiceWorkerRunIdentity, diagnostic: RendererServiceWorkerFetchDiagnostic, ) -> bool { - if !self.service_worker_target_infos.contains_key(&version_id) { + if self.target_output_journal(version_id).is_none() + || !self.observes_live_target_run(version_id, &run) + { return false; } - let Some(run) = self - .service_worker_target_run_projections - .get_mut(&version_id) - .and_then(|residence| residence.observe_run(run)) - else { - return false; - }; - self.target_output_streams.publish( + self.target_output_streams.publish_observation( version_id, - RendererServiceWorkerTargetEvent::FetchDiagnostic { + RendererServiceWorkerObservation::FetchDiagnostic { version_id: version_id.as_u64(), run, diagnostic, @@ -596,22 +509,17 @@ impl ServiceWorkerRuntimeState { inspector_session_id: Option, messages: Vec, ) -> bool { - if !self.service_worker_target_infos.contains_key(&version_id) { + if self.target_output_journal(version_id).is_none() + || !self.observes_live_target_run(version_id, &run) + { return false; } if messages.is_empty() { return false; } - let Some(run) = self - .service_worker_target_run_projections - .get_mut(&version_id) - .and_then(|residence| residence.observe_run(run)) - else { - return false; - }; - self.target_output_streams.publish( + self.target_output_streams.publish_observation( version_id, - RendererServiceWorkerTargetEvent::RuntimeInspectorMessages { + RendererServiceWorkerObservation::RuntimeInspectorMessages { version_id: version_id.as_u64(), run, inspector_session_id, @@ -987,65 +895,13 @@ impl ServiceWorkerClient { #[cfg(test)] mod target_run_identity_tests { - use super::{ - RendererServiceWorkerTargetRunResidence, ServiceWorkerRegistrationId, - ServiceWorkerRuntimeState, ServiceWorkerVersionId, + use super::{ServiceWorkerRegistrationId, ServiceWorkerRuntimeState, ServiceWorkerVersionId}; + use crate::runtime::{RendererServiceWorkerConsoleMessage, RendererServiceWorkerRunIdentity}; + use crate::service_worker_runtime::target_output_streams::{ + ServiceWorkerOutputForTest, drain_service_worker_target_events_for_test, }; - use crate::runtime::{ - RendererServiceWorkerConsoleMessage, RendererServiceWorkerRunIdentity, - RendererServiceWorkerTargetEvent, - }; - use crate::service_worker_runtime::target_output_streams::drain_service_worker_target_events_for_test; use url::Url; - #[test] - fn one_host_identity_reuses_one_target_run_projection() { - let mut residence = RendererServiceWorkerTargetRunResidence::default(); - let run = RendererServiceWorkerRunIdentity::fresh(); - - let first = residence - .observe_run(run.clone()) - .expect("first run-specific fact should establish an identity"); - let second = residence - .observe_run(run) - .expect("same host identity should observe the established run"); - - assert_eq!(first, second); - assert_eq!(residence.active_run(), Some(first)); - } - - #[test] - fn retirement_blocks_late_facts_and_accepts_a_fresh_host_identity() { - let mut residence = RendererServiceWorkerTargetRunResidence::default(); - let first_run = RendererServiceWorkerRunIdentity::fresh(); - let retired = residence - .retire_run(first_run.clone()) - .expect("a stop terminal may establish and immediately retire a failed run"); - - assert!( - residence.observe_run(first_run).is_none(), - "late facts must not reopen the retired renderer run" - ); - - let next_run = RendererServiceWorkerRunIdentity::fresh(); - let restarted = residence - .observe_run(next_run) - .expect("a fresh worker host should establish a fresh exact run"); - assert_ne!(retired, restarted); - assert_eq!(residence.active_run(), Some(restarted)); - } - - #[test] - #[should_panic(expected = "must not replace a live renderer run")] - fn a_different_host_identity_cannot_replace_a_live_run() { - let mut residence = RendererServiceWorkerTargetRunResidence::default(); - residence - .observe_run(RendererServiceWorkerRunIdentity::fresh()) - .expect("test run should become live"); - - let _ = residence.observe_run(RendererServiceWorkerRunIdentity::fresh()); - } - #[test] fn version_facts_do_not_manufacture_a_worker_run() { let (mut state, mut target_output_rx) = @@ -1067,94 +923,43 @@ mod target_run_identity_tests { assert!(matches!( events.as_slice(), [ - RendererServiceWorkerTargetEvent::VersionUpdated { .. }, - RendererServiceWorkerTargetEvent::Destroyed { - active_run: None, - .. - } + ServiceWorkerOutputForTest::Lifecycle( + crate::runtime::RendererServiceWorkerLifecycle::VersionUpdated { .. } + ), + ServiceWorkerOutputForTest::Lifecycle( + crate::runtime::RendererServiceWorkerLifecycle::Destroyed { + active_run: None, + .. + } + ) ] )); } #[test] - fn target_event_producer_carries_one_identity_across_one_run() { + fn observations_cannot_create_a_host_for_a_stopped_version() { let (mut state, mut target_output_rx) = ServiceWorkerRuntimeState::new_with_target_output_for_test(); - let registration_id = ServiceWorkerRegistrationId(1); - let version_id = ServiceWorkerVersionId(7); + let version = ServiceWorkerVersionId(7); state.record_target_created( - registration_id, - version_id, + ServiceWorkerRegistrationId(1), + version, Url::parse("https://example.test/service-worker.js").unwrap(), Url::parse("https://example.test/").unwrap(), ); - assert!(matches!( - drain_service_worker_target_events_for_test(&mut target_output_rx).as_slice(), - [RendererServiceWorkerTargetEvent::Created { .. }] - )); - + drain_service_worker_target_events_for_test(&mut target_output_rx); let run = RendererServiceWorkerRunIdentity::fresh(); - assert!(state.record_target_console_message( - version_id, + assert!(!state.record_target_console_message( + version, run.clone(), RendererServiceWorkerConsoleMessage { - message: "before Started".to_owned(), + message: "no physical host".into(), args: Vec::new(), stack: None, }, )); - assert!(state.record_target_started(version_id, run.clone())); - assert!(state.record_target_stopped(version_id, run.clone(), "idle_timeout")); - - let events = drain_service_worker_target_events_for_test(&mut target_output_rx); - let [ - RendererServiceWorkerTargetEvent::Console { - run: console_run, .. - }, - RendererServiceWorkerTargetEvent::Started { - run: started_run, .. - }, - RendererServiceWorkerTargetEvent::Stopped { - run: stopped_run, .. - }, - ] = events.as_slice() - else { - panic!("one run should publish console/start/stop in source order: {events:?}"); - }; - assert_eq!(console_run, started_run); - assert_eq!(started_run, stopped_run); - - assert!( - !state.record_target_console_message( - version_id, - run, - RendererServiceWorkerConsoleMessage { - message: "late old run".to_owned(), - args: Vec::new(), - stack: None, - }, - ), - "a late local callback must not republish the retired exact run" - ); - let restarted = RendererServiceWorkerRunIdentity::fresh(); - assert!(state.record_target_console_message( - version_id, - restarted, - RendererServiceWorkerConsoleMessage { - message: "new run".to_owned(), - args: Vec::new(), - stack: None, - }, - )); - let restarted_events = drain_service_worker_target_events_for_test(&mut target_output_rx); - let [ - RendererServiceWorkerTargetEvent::Console { - run: restarted_run, .. - }, - ] = restarted_events.as_slice() - else { - panic!("the later worker host should publish one fresh exact run"); - }; - assert_ne!(stopped_run, restarted_run); + assert!(!state.record_target_started(version, run)); + state.record_target_starting(version); + assert!(drain_service_worker_target_events_for_test(&mut target_output_rx).is_empty()); } } diff --git a/moli-renderer-v8/src/service_worker_runtime/target_output_streams.rs b/moli-renderer-v8/src/service_worker_runtime/target_output_streams.rs index 02c82daf4..454f749be 100644 --- a/moli-renderer-v8/src/service_worker_runtime/target_output_streams.rs +++ b/moli-renderer-v8/src/service_worker_runtime/target_output_streams.rs @@ -3,10 +3,10 @@ use std::collections::HashMap; use parking_lot::Mutex; use crate::runtime::{ - PendingRendererOutputRecord, RendererBrowserContextRuntimeId, RendererOutputRecord, - RendererOutputStreamCloseReason, RendererOutputStreamIdentity, - RendererOutputTransportSenderSlot, RendererOwnerAction, RendererServiceWorkerTargetEvent, - RendererTurnOutputJournal, + PendingRendererOutputRecord, RendererOutputRecord, RendererOutputStreamCloseReason, + RendererOutputStreamIdentity, RendererOutputTransportSenderSlot, RendererProtocolObservation, + RendererServiceWorkerLifecycle, RendererServiceWorkerObservation, RendererTurnOutputJournal, + RendererWorkerLifecycle, RendererWorkerLifecycleReporter, }; use super::ids::ServiceWorkerVersionId; @@ -18,7 +18,7 @@ use super::ids::ServiceWorkerVersionId; /// worker thread or run run: `Created` opens it, run/status events append /// to it, and `Destroyed` is its final record before closure. pub(super) struct ServiceWorkerTargetOutputStreams { - browser_context_runtime_id: RendererBrowserContextRuntimeId, + worker_lifecycle: RendererWorkerLifecycleReporter, transport: RendererOutputTransportSenderSlot, state: Mutex, } @@ -34,11 +34,11 @@ struct ServiceWorkerTargetOutputStreamsState { impl ServiceWorkerTargetOutputStreams { pub(super) fn new( - browser_context_runtime_id: RendererBrowserContextRuntimeId, + worker_lifecycle: RendererWorkerLifecycleReporter, transport: RendererOutputTransportSenderSlot, ) -> Self { Self { - browser_context_runtime_id, + worker_lifecycle, transport, state: Mutex::new(ServiceWorkerTargetOutputStreamsState::default()), } @@ -58,11 +58,11 @@ impl ServiceWorkerTargetOutputStreams { pub(super) fn publish_created( &self, version_id: ServiceWorkerVersionId, - event: RendererServiceWorkerTargetEvent, + event: RendererServiceWorkerLifecycle, ) { let mut state = self.state.lock(); let stream = RendererOutputStreamIdentity::new_service_worker( - self.browser_context_runtime_id, + self.worker_lifecycle.runtime(), version_id.as_u64(), ); let journal = match self.transport.sender() { @@ -74,13 +74,13 @@ impl ServiceWorkerTargetOutputStreams { "ServiceWorker output stream opened twice for one version" ); drop(state); - journal.publish_record(Self::record(event)); + journal.publish_record(self.lifecycle_record(event)); } pub(super) fn publish( &self, version_id: ServiceWorkerVersionId, - event: RendererServiceWorkerTargetEvent, + event: RendererServiceWorkerLifecycle, ) { let journal = self .state @@ -89,7 +89,7 @@ impl ServiceWorkerTargetOutputStreams { .get(&version_id) .cloned() .expect("ServiceWorker target output requires a live version stream"); - journal.publish_record(Self::record(event)); + journal.publish_record(self.lifecycle_record(event)); } pub(super) fn journal( @@ -102,50 +102,87 @@ impl ServiceWorkerTargetOutputStreams { pub(super) fn publish_destroyed( &self, version_id: ServiceWorkerVersionId, - event: RendererServiceWorkerTargetEvent, + event: RendererServiceWorkerLifecycle, ) { let mut state = self.state.lock(); let journal = state .live .remove(&version_id) .expect("ServiceWorker target stream must exist until target destruction"); - journal.publish_record(Self::record(event)); + journal.publish_record(self.lifecycle_record(event)); journal.retire(RendererOutputStreamCloseReason::ResidenceRetired); if !journal.transport_is_bound() { state.retired_before_transport.push(journal); } } - fn record(event: RendererServiceWorkerTargetEvent) -> RendererOutputRecord { - PendingRendererOutputRecord::owner_action( + pub(super) fn publish_observation( + &self, + version_id: ServiceWorkerVersionId, + event: RendererServiceWorkerObservation, + ) { + let Some(journal) = self.journal(version_id) else { + return; + }; + journal.publish_record( + PendingRendererOutputRecord::observation( + None, + RendererProtocolObservation::ServiceWorker(event), + ) + .resolve() + .expect("ServiceWorker observation must have resolved source identity"), + ); + } + + fn lifecycle_record(&self, event: RendererServiceWorkerLifecycle) -> RendererOutputRecord { + PendingRendererOutputRecord::observation( None, - RendererOwnerAction::ServiceWorkerTargetLifecycle(event), + RendererProtocolObservation::WorkerLifecycle( + self.worker_lifecycle + .report(RendererWorkerLifecycle::Service(event)), + ), ) .resolve() - .unwrap_or_else(|_| { - panic!("ServiceWorker target output must have resolved source identity") - }) + .expect("ServiceWorker lifecycle has a concrete version stream") } } +/// Producer tests inspect both record kinds without forging a Browser acknowledgement. +#[cfg(test)] +#[derive(Clone, Debug, PartialEq)] +pub(super) enum ServiceWorkerOutputForTest { + Lifecycle(RendererServiceWorkerLifecycle), + Observation(RendererServiceWorkerObservation), +} + #[cfg(test)] pub(super) fn drain_service_worker_target_events_for_test( receiver: &mut crate::runtime::RendererOutputTransportReceiver, -) -> Vec { +) -> Vec { let mut events = Vec::new(); while let Ok(message) = receiver.try_recv() { let crate::runtime::RendererOutputTransportMessage::Publication(output) = message else { continue; }; - events.extend(output.records().iter().filter_map(|record| { - let crate::runtime::RendererOutputItem::OwnerAction( - RendererOwnerAction::ServiceWorkerTargetLifecycle(event), - ) = record.item() - else { - return None; - }; - Some(event.clone()) - })); + events.extend( + output + .records() + .iter() + .filter_map(|record| match record.item() { + crate::runtime::RendererOutputItem::Observation( + RendererProtocolObservation::WorkerLifecycle(observation), + ) => match observation.lifecycle() { + RendererWorkerLifecycle::Service(event) => { + Some(ServiceWorkerOutputForTest::Lifecycle(event.clone())) + } + _ => None, + }, + crate::runtime::RendererOutputItem::Observation( + RendererProtocolObservation::ServiceWorker(event), + ) => Some(ServiceWorkerOutputForTest::Observation(event.clone())), + _ => None, + }), + ); } events } diff --git a/moli-renderer-v8/src/shared_worker_runtime.rs b/moli-renderer-v8/src/shared_worker_runtime.rs index f547a9673..ae5402ad0 100644 --- a/moli-renderer-v8/src/shared_worker_runtime.rs +++ b/moli-renderer-v8/src/shared_worker_runtime.rs @@ -19,13 +19,12 @@ mod host_loading_task; mod host_removal; mod host_target_output; mod host_worker; +mod inspection; mod instances; mod load_completion; mod loading; mod matching; mod owner_wake; -mod protocol_commands; -mod protocol_routing; mod pump; mod resource_commands; mod resource_routing; diff --git a/moli-renderer-v8/src/shared_worker_runtime/connection.rs b/moli-renderer-v8/src/shared_worker_runtime/connection.rs index b64790380..4c51594f5 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/connection.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/connection.rs @@ -136,6 +136,7 @@ impl SharedWorkerRuntimeService { params.key.script_url().to_owned(), params.launch_context.name.clone(), target_output, + self.worker_lifecycle(), )); host.add_client(client_id, client); self.store_loading_host_for_connect(instance_id, host.clone()); diff --git a/moli-renderer-v8/src/shared_worker_runtime/events.rs b/moli-renderer-v8/src/shared_worker_runtime/events.rs index bc81e89af..bf363608b 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/events.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/events.rs @@ -55,7 +55,8 @@ impl RendererSharedWorkerHost { WorkerToParentMessage::RuntimeInspectorResponse(publication) => { self.publish_runtime_inspector_response(publication); } - WorkerToParentMessage::ServiceWorkerLifecycleCompleted(_) => { + WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) + | WorkerToParentMessage::ServiceWorkerLifecycleCompleted(_) => { trace!(url = %script_url, ?message, "dropping service worker lifecycle message on shared worker host"); } WorkerToParentMessage::ServiceWorkerFetchCompleted(_) => { diff --git a/moli-renderer-v8/src/shared_worker_runtime/host.rs b/moli-renderer-v8/src/shared_worker_runtime/host.rs index 8c1058ca3..10f399fd4 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/host.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/host.rs @@ -31,6 +31,7 @@ pub(super) struct RendererSharedWorkerHost { pub(super) state: Mutex, pub(super) clients: Mutex>, target_output: crate::runtime::RendererTurnOutputJournal, + pub(super) worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, target_output_retired: AtomicBool, pub(super) runtime_response_publications: Mutex, } @@ -57,6 +58,7 @@ impl RendererSharedWorkerHost { initial_script_url: String, name: String, target_output: crate::runtime::RendererTurnOutputJournal, + worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, ) -> Self { Self { instance_id, @@ -67,6 +69,7 @@ impl RendererSharedWorkerHost { state: Mutex::new(RendererSharedWorkerHostState::Loading { task: None }), clients: Mutex::new(HashMap::new()), target_output, + worker_lifecycle, target_output_retired: AtomicBool::new(false), runtime_response_publications: Mutex::new( SharedWorkerRuntimeResponsePublicationState::Active, diff --git a/moli-renderer-v8/src/shared_worker_runtime/host_loading.rs b/moli-renderer-v8/src/shared_worker_runtime/host_loading.rs index 8eda662dd..50c7e6f05 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/host_loading.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/host_loading.rs @@ -77,7 +77,7 @@ impl RendererSharedWorkerHost { task.cancel(); } if retired_loading { - self.retire_target_output_without_destroyed(); + self.retire_unstarted_output(); } } @@ -93,7 +93,7 @@ impl RendererSharedWorkerHost { }; drop(state); if retired_loading { - self.retire_target_output_without_destroyed(); + self.retire_unstarted_output(); } } } diff --git a/moli-renderer-v8/src/shared_worker_runtime/host_removal.rs b/moli-renderer-v8/src/shared_worker_runtime/host_removal.rs index b59ebf2b3..a02cc88c7 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/host_removal.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/host_removal.rs @@ -61,18 +61,20 @@ impl SharedWorkerRemovedHost { } } - pub(super) fn terminate_for_context_shutdown(self) { + pub(super) fn terminate(self) -> bool { match self { Self::Running { host, clients } => { host.publish_destroyed_target_event(); host.close_worker_ports_and_send_closed(clients); host.terminate(); + true } Self::Loading { host, clients } => { host.cancel_loading(); host.close_worker_ports_and_send_closed(clients); + true } - Self::Missing => {} + Self::Missing => false, } } } diff --git a/moli-renderer-v8/src/shared_worker_runtime/host_target_output.rs b/moli-renderer-v8/src/shared_worker_runtime/host_target_output.rs index 0c393c9eb..11e76b8b2 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/host_target_output.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/host_target_output.rs @@ -5,9 +5,9 @@ use tracing::trace; use crate::{ runtime::{ - PendingRendererOutputRecord, RendererOwnerAction, RendererRuntimeInspectorMessage, - RendererSharedWorkerConsoleMessage, RendererSharedWorkerTargetEvent, - RendererSharedWorkerTargetInfo, + PendingRendererOutputRecord, RendererProtocolObservation, RendererRuntimeInspectorMessage, + RendererSharedWorkerConsoleMessage, RendererSharedWorkerObservation, + RendererSharedWorkerTargetInfo, RendererWorkerLifecycle, }, worker::{WorkerConsoleMessage, WorkerRuntimeInspectorMessageBatch}, }; @@ -17,7 +17,7 @@ use super::host::SharedWorkerRuntimeResponsePublicationState; impl RendererSharedWorkerHost { pub(super) fn publish_created_target_event(&self) { - self.publish_target_event(RendererSharedWorkerTargetEvent::Created(self.target_info())); + self.publish_lifecycle(RendererWorkerLifecycle::SharedCreated(self.target_info())); } pub(super) fn publish_destroyed_target_event(self: &Arc) { @@ -25,14 +25,12 @@ impl RendererSharedWorkerHost { if !self.claim_target_output_retirement() { return; } - self.publish_target_event(RendererSharedWorkerTargetEvent::Destroyed { - instance_id: self.instance_id(), - }); + self.publish_lifecycle(RendererWorkerLifecycle::SharedDestroyed(self.instance_id())); self.finish_runtime_response_retirement(); self.finish_target_output_retirement(); } - pub(super) fn retire_target_output_without_destroyed(&self) { + pub(super) fn retire_unstarted_output(&self) { self.begin_runtime_response_retirement(); if self.claim_target_output_retirement() { self.finish_runtime_response_retirement(); @@ -136,7 +134,7 @@ impl RendererSharedWorkerHost { continue; } recorded |= self.publish_target_event_if_running( - RendererSharedWorkerTargetEvent::RuntimeInspectorMessages { + RendererSharedWorkerObservation::RuntimeInspectorMessages { instance_id: self.instance_id(), inspector_session_id: batch.inspector_session_id, messages: notifications, @@ -158,7 +156,7 @@ impl RendererSharedWorkerHost { console: &WorkerConsoleMessage, ) -> bool { self.publish_target_event_if_running( - RendererSharedWorkerTargetEvent::Console { + RendererSharedWorkerObservation::Console { instance_id: self.instance_id(), message: RendererSharedWorkerConsoleMessage { message: console.message.clone(), @@ -172,7 +170,7 @@ impl RendererSharedWorkerHost { fn publish_target_event_if_running( self: &Arc, - event: RendererSharedWorkerTargetEvent, + event: RendererSharedWorkerObservation, on_not_running: impl FnOnce(), ) -> bool { if !self.is_running_in_runtime_service() { @@ -196,16 +194,23 @@ impl RendererSharedWorkerHost { } } - fn publish_target_event(&self, event: RendererSharedWorkerTargetEvent) { + fn publish_lifecycle(&self, lifecycle: RendererWorkerLifecycle) { + self.publish_observation(RendererProtocolObservation::WorkerLifecycle( + self.worker_lifecycle.report(lifecycle), + )); + } + + fn publish_target_event(&self, event: RendererSharedWorkerObservation) { + self.publish_observation(RendererProtocolObservation::SharedWorker(event)); + } + + fn publish_observation(&self, observation: RendererProtocolObservation) { self.target_output().publish_record( - PendingRendererOutputRecord::owner_action( - None, - RendererOwnerAction::SharedWorkerTargetLifecycle(event), - ) - .resolve() - .unwrap_or_else(|_| { - panic!("SharedWorker target output must have resolved source identity") - }), + PendingRendererOutputRecord::observation(None, observation) + .resolve() + .unwrap_or_else(|_| { + panic!("SharedWorker target output must have resolved source identity") + }), ); } @@ -235,8 +240,8 @@ mod tests { use crate::{ runtime::{ - RendererOutputItem, RendererOwnerAction, RendererRuntimeInspectorMessage, - RendererSharedWorkerTargetEvent, + RendererOutputItem, RendererProtocolObservation, RendererRuntimeInspectorMessage, + RendererWorkerLifecycle, }, shared_worker_runtime::test_support, worker::{WorkerConsoleMessage, WorkerRuntimeInspectorMessageBatch}, @@ -296,11 +301,8 @@ mod tests { }; assert!(matches!( record.item(), - RendererOutputItem::OwnerAction( - RendererOwnerAction::SharedWorkerTargetLifecycle( - RendererSharedWorkerTargetEvent::Created(info) - ) - ) if info.instance_id == instance_id + RendererOutputItem::Observation(RendererProtocolObservation::WorkerLifecycle(observation)) + if matches!(observation.lifecycle(), RendererWorkerLifecycle::SharedCreated(info) if info.instance_id == instance_id) )); } diff --git a/moli-renderer-v8/src/shared_worker_runtime/inspection.rs b/moli-renderer-v8/src/shared_worker_runtime/inspection.rs new file mode 100644 index 000000000..60269a4ba --- /dev/null +++ b/moli-renderer-v8/src/shared_worker_runtime/inspection.rs @@ -0,0 +1,18 @@ +use moli_shared_worker::SharedWorkerInstanceId; + +use super::service::SharedWorkerRuntimeService; +use crate::runtime::RendererWorkerInspectionEndpoint; + +impl SharedWorkerRuntimeService { + pub(crate) fn inspection_endpoint( + &self, + instance_id: SharedWorkerInstanceId, + ) -> Option { + let host = self.running_host_for_instance(instance_id)?; + Some(RendererWorkerInspectionEndpoint::new( + host.running_devtools_handle()?, + Some(host.target_output().clone()), + "SharedWorkerRuntimeUnavailable", + )) + } +} diff --git a/moli-renderer-v8/src/shared_worker_runtime/load_completion.rs b/moli-renderer-v8/src/shared_worker_runtime/load_completion.rs index 65bbe3e09..5227d9c51 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/load_completion.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/load_completion.rs @@ -374,6 +374,7 @@ mod tests { key.script_url().to_owned(), "loader".to_owned(), runtime_service.open_target_output_stream(instance_id), + runtime_service.worker_lifecycle(), )); host.add_client( client_id, diff --git a/moli-renderer-v8/src/shared_worker_runtime/protocol_commands.rs b/moli-renderer-v8/src/shared_worker_runtime/protocol_commands.rs deleted file mode 100644 index c1c8a89ad..000000000 --- a/moli-renderer-v8/src/shared_worker_runtime/protocol_commands.rs +++ /dev/null @@ -1,64 +0,0 @@ -use tokio::sync::oneshot; - -use super::host::RendererSharedWorkerHost; -use crate::runtime::{RendererRuntimeInspectorMessage, RendererRuntimeInspectorResponseSender}; - -impl RendererSharedWorkerHost { - pub(super) async fn dispatch_runtime_protocol_message( - &self, - inspector_session_id: Option, - raw_json: String, - ) -> Result, String> { - self.dispatch_runtime_protocol_message_with_optional_deferred_response( - inspector_session_id, - raw_json, - None, - ) - .await - } - - pub(super) async fn dispatch_runtime_protocol_message_with_deferred_response( - &self, - inspector_session_id: Option, - raw_json: String, - deferred_response: RendererRuntimeInspectorResponseSender, - ) -> Result, String> { - self.dispatch_runtime_protocol_message_with_optional_deferred_response( - inspector_session_id, - raw_json, - Some(deferred_response), - ) - .await - } - - async fn dispatch_runtime_protocol_message_with_optional_deferred_response( - &self, - inspector_session_id: Option, - raw_json: String, - deferred_response: Option, - ) -> Result, String> { - let Some(handle) = self.running_devtools_handle() else { - return Err("SharedWorkerRuntimeUnavailable".to_owned()); - }; - let (response_tx, response_rx) = oneshot::channel(); - if !handle.dispatch_runtime_protocol_message( - inspector_session_id, - raw_json, - deferred_response, - response_tx, - ) { - return Err("SharedWorkerRuntimeUnavailable".to_owned()); - } - response_rx - .await - .map_err(|_| "SharedWorkerRuntimeUnavailable".to_owned())? - } - - pub(super) fn detach_runtime_inspector_session( - &self, - inspector_session_id: Option, - ) -> bool { - self.running_devtools_handle() - .is_some_and(|handle| handle.detach_runtime_inspector_session(inspector_session_id)) - } -} diff --git a/moli-renderer-v8/src/shared_worker_runtime/protocol_routing.rs b/moli-renderer-v8/src/shared_worker_runtime/protocol_routing.rs deleted file mode 100644 index 2912f13f3..000000000 --- a/moli-renderer-v8/src/shared_worker_runtime/protocol_routing.rs +++ /dev/null @@ -1,83 +0,0 @@ -use moli_shared_worker::SharedWorkerInstanceId; - -use crate::runtime::{RendererRuntimeInspectorMessage, RendererRuntimeInspectorResponseSender}; - -use super::service::SharedWorkerRuntimeService; - -impl SharedWorkerRuntimeService { - pub(crate) async fn dispatch_runtime_protocol_message( - &self, - instance_id: SharedWorkerInstanceId, - inspector_session_id: Option, - raw_json: String, - ) -> Result, String> { - let Some(host) = self.running_host_for_instance(instance_id) else { - return Err("SharedWorkerRuntimeUnavailable".to_owned()); - }; - host.dispatch_runtime_protocol_message(inspector_session_id, raw_json) - .await - } - - pub(crate) async fn dispatch_runtime_protocol_message_with_deferred_response( - &self, - instance_id: SharedWorkerInstanceId, - inspector_session_id: Option, - raw_json: String, - deferred_response: RendererRuntimeInspectorResponseSender, - ) -> Result, String> { - let Some(host) = self.running_host_for_instance(instance_id) else { - return Err("SharedWorkerRuntimeUnavailable".to_owned()); - }; - host.dispatch_runtime_protocol_message_with_deferred_response( - inspector_session_id, - raw_json, - deferred_response, - ) - .await - } - - pub(crate) async fn dispatch_runtime_protocol_message_with_devtools_session_response( - &self, - instance_id: SharedWorkerInstanceId, - inspector_session_id: String, - raw_json: String, - response: RendererRuntimeInspectorResponseSender, - ) -> Result { - let Some(host) = self.running_host_for_instance(instance_id) else { - return Err("SharedWorkerRuntimeUnavailable".to_owned()); - }; - let response = response.route_to_worker_devtools_session_output( - inspector_session_id.clone(), - host.target_output().clone(), - ); - let error_response = response.clone(); - let settlement = response - .take_session_response_settlement_receiver() - .expect("a Worker DevTools response must own one settlement receiver"); - let dispatch = host - .dispatch_runtime_protocol_message_with_deferred_response( - Some(inspector_session_id), - raw_json, - response, - ) - .await; - Ok( - crate::runtime::CompletedWorkerRuntimeInspectorCommandDispatch::finish( - dispatch, - settlement, - error_response, - ), - ) - } - - pub(crate) fn detach_runtime_inspector_session( - &self, - instance_id: SharedWorkerInstanceId, - inspector_session_id: Option, - ) -> bool { - let Some(host) = self.running_host_for_instance(instance_id) else { - return false; - }; - host.detach_runtime_inspector_session(inspector_session_id) - } -} diff --git a/moli-renderer-v8/src/shared_worker_runtime/service.rs b/moli-renderer-v8/src/shared_worker_runtime/service.rs index 1670fb039..8339520d3 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/service.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/service.rs @@ -63,13 +63,13 @@ impl SharedWorkerRuntimeService { pub(crate) fn configure_target_output_streams( &self, - browser_context_runtime_id: crate::runtime::RendererBrowserContextRuntimeId, + worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, transport: crate::runtime::RendererOutputTransportSenderSlot, ) { self.inner .target_output_streams .set(SharedWorkerTargetOutputStreams::new( - browser_context_runtime_id, + worker_lifecycle, transport, )) .unwrap_or_else(|_| { @@ -91,6 +91,10 @@ impl SharedWorkerRuntimeService { self.target_output_streams().open(instance_id) } + pub(super) fn worker_lifecycle(&self) -> crate::runtime::RendererWorkerLifecycleReporter { + self.target_output_streams().worker_lifecycle.clone() + } + pub(super) fn retire_target_output_stream(&self, instance_id: SharedWorkerInstanceId) { self.target_output_streams().retire(instance_id); } @@ -108,7 +112,9 @@ impl SharedWorkerRuntimeService { .inner .target_output_streams .set(SharedWorkerTargetOutputStreams::new( - crate::runtime::RendererBrowserContextRuntimeId::new_for_testing(0), + crate::runtime::RendererWorkerLifecycleReporter::new( + crate::runtime::RendererBrowserContextRuntimeId::new_for_testing(0), + ), crate::runtime::RendererOutputTransportSenderSlot::default(), )); } diff --git a/moli-renderer-v8/src/shared_worker_runtime/shutdown.rs b/moli-renderer-v8/src/shared_worker_runtime/shutdown.rs index 70f1935de..8b5954d32 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/shutdown.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/shutdown.rs @@ -3,7 +3,7 @@ use super::{host_removal::SharedWorkerRemovedHost, service::SharedWorkerRuntimeS impl SharedWorkerRuntimeService { pub(crate) fn terminate_all_for_context_shutdown(&self) { for removed in self.take_context_shutdown_hosts() { - removed.terminate_for_context_shutdown(); + removed.terminate(); } } diff --git a/moli-renderer-v8/src/shared_worker_runtime/target_close.rs b/moli-renderer-v8/src/shared_worker_runtime/target_close.rs index f213201c4..6dc540018 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/target_close.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/target_close.rs @@ -1,26 +1,10 @@ use moli_shared_worker::SharedWorkerInstanceId; -use super::{host_removal::SharedWorkerRemovedHost, service::SharedWorkerRuntimeService}; +use super::service::SharedWorkerRuntimeService; impl SharedWorkerRuntimeService { - pub(crate) fn close_instance_for_devtools_target_close( - &self, - instance_id: SharedWorkerInstanceId, - ) -> bool { - match self.downgrade().remove_host_for_instance(instance_id) { - SharedWorkerRemovedHost::Running { host, clients } => { - host.close_worker_ports_and_send_closed(clients); - host.terminate(); - host.retire_target_output_without_destroyed(); - true - } - SharedWorkerRemovedHost::Loading { host, clients } => { - host.cancel_loading(); - host.close_worker_ports_and_send_closed(clients); - true - } - SharedWorkerRemovedHost::Missing => false, - } + pub(crate) fn close_instance(&self, instance_id: SharedWorkerInstanceId) -> bool { + self.remove_host_for_instance(instance_id).terminate() } } @@ -45,7 +29,7 @@ mod tests { }; #[test] - fn devtools_target_close_removes_running_host_and_closes_clients_without_lifecycle_duplicate() { + fn explicit_close_removes_running_host_and_publishes_one_native_destruction() { let service = test_support::runtime_service(); let message_port_registry = new_message_port_registry(); let (client_port_id, worker_port_id, message_port_owner) = @@ -78,15 +62,36 @@ mod tests { SharedWorkerLoadReady::Running { .. } )); assert!(!host.is_closed()); + let (sender, mut output) = crate::runtime::renderer_output_transport_channel(); + service.bind_target_output_transport(sender); + host.publish_created_target_event(); - assert!(service.close_instance_for_devtools_target_close(instance_id)); + assert!(service.close_instance(instance_id)); + assert!(!service.close_instance(instance_id)); + let mut destructions = 0; + while let Ok(message) = output.try_recv() { + if let crate::runtime::RendererOutputTransportMessage::Publication(publication) = + message + { + for record in publication.records() { + if let crate::runtime::RendererOutputItem::Observation( + crate::runtime::RendererProtocolObservation::WorkerLifecycle(observation), + ) = record.item() + && matches!(observation.lifecycle(), crate::runtime::RendererWorkerLifecycle::SharedDestroyed(actual) if *actual == instance_id) + { + destructions += 1; + } + } + } + } + assert_eq!(destructions, 1); assert!(host.is_closed()); assert!(test_support::matching_is_empty(&service)); assert!( host.target_output_retired() .load(std::sync::atomic::Ordering::Acquire), - "CDP Target.closeTarget must retire the worker stream without publishing a second target teardown" + "explicit close must retire the worker stream after one native destruction" ); let task = message_port_owner .pop_shared_worker_client_event() @@ -142,7 +147,7 @@ mod tests { let (closed_tx, closed_rx) = std_mpsc::channel(); let close_service = service.clone(); let close_thread = std::thread::spawn(move || { - let closed = close_service.close_instance_for_devtools_target_close(instance_id); + let closed = close_service.close_instance(instance_id); let _ = closed_tx.send(closed); }); let close_result = closed_rx.recv_timeout(Duration::from_millis(250)); @@ -165,9 +170,6 @@ mod tests { fn devtools_target_close_reports_missing_instance() { let service = test_support::runtime_service(); - assert!( - !service - .close_instance_for_devtools_target_close(SharedWorkerInstanceId::from_u64(404)) - ); + assert!(!service.close_instance(SharedWorkerInstanceId::from_u64(404))); } } diff --git a/moli-renderer-v8/src/shared_worker_runtime/target_output_streams.rs b/moli-renderer-v8/src/shared_worker_runtime/target_output_streams.rs index 0fd2bb92f..2b8177357 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/target_output_streams.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/target_output_streams.rs @@ -4,7 +4,7 @@ use moli_shared_worker::SharedWorkerInstanceId; use parking_lot::Mutex; use crate::runtime::{ - RendererBrowserContextRuntimeId, RendererOutputStreamCloseReason, RendererOutputStreamIdentity, + RendererOutputStreamCloseReason, RendererOutputStreamIdentity, RendererOutputTransportSenderSlot, RendererTurnOutputJournal, }; @@ -15,7 +15,7 @@ use crate::runtime::{ /// concrete facts before CDP installs its channel without falling back to a /// service-wide lifecycle queue. pub(super) struct SharedWorkerTargetOutputStreams { - browser_context_runtime_id: RendererBrowserContextRuntimeId, + pub(super) worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, transport: RendererOutputTransportSenderSlot, state: Mutex, } @@ -31,11 +31,11 @@ struct SharedWorkerTargetOutputStreamsState { impl SharedWorkerTargetOutputStreams { pub(super) fn new( - browser_context_runtime_id: RendererBrowserContextRuntimeId, + worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter, transport: RendererOutputTransportSenderSlot, ) -> Self { Self { - browser_context_runtime_id, + worker_lifecycle, transport, state: Mutex::new(SharedWorkerTargetOutputStreamsState::default()), } @@ -44,7 +44,7 @@ impl SharedWorkerTargetOutputStreams { pub(super) fn open(&self, instance_id: SharedWorkerInstanceId) -> RendererTurnOutputJournal { let mut state = self.state.lock(); let stream = RendererOutputStreamIdentity::new_shared_worker( - self.browser_context_runtime_id, + self.worker_lifecycle.runtime(), instance_id.as_u64(), ); let journal = match self.transport.sender() { @@ -88,26 +88,31 @@ impl SharedWorkerTargetOutputStreams { #[cfg(test)] mod tests { use super::*; + use crate::runtime::RendererBrowserContextRuntimeId; use crate::runtime::{ PendingRendererOutputRecord, RendererOutputItem, RendererOutputStreamControl, - RendererOutputTransportMessage, RendererOwnerAction, RendererSharedWorkerTargetEvent, + RendererOutputTransportMessage, RendererProtocolObservation, RendererWorkerLifecycle, }; #[test] fn retired_pre_transport_stream_is_delivered_once_when_transport_binds() { let transport_slot = RendererOutputTransportSenderSlot::default(); let streams = SharedWorkerTargetOutputStreams::new( - RendererBrowserContextRuntimeId::new_for_testing(31), + crate::runtime::RendererWorkerLifecycleReporter::new( + RendererBrowserContextRuntimeId::new_for_testing(31), + ), transport_slot.clone(), ); let instance_id = SharedWorkerInstanceId::from_u64(7); let journal = streams.open(instance_id); let stream = journal.stream(); journal.publish_record( - PendingRendererOutputRecord::owner_action( + PendingRendererOutputRecord::observation( None, - RendererOwnerAction::SharedWorkerTargetLifecycle( - RendererSharedWorkerTargetEvent::Destroyed { instance_id }, + RendererProtocolObservation::WorkerLifecycle( + streams + .worker_lifecycle + .report(RendererWorkerLifecycle::SharedDestroyed(instance_id)), ), ) .resolve() @@ -139,11 +144,8 @@ mod tests { [record] if matches!( record.item(), - RendererOutputItem::OwnerAction( - RendererOwnerAction::SharedWorkerTargetLifecycle( - RendererSharedWorkerTargetEvent::Destroyed { instance_id: actual } - ) - ) if *actual == instance_id + RendererOutputItem::Observation(RendererProtocolObservation::WorkerLifecycle(observation)) + if matches!(observation.lifecycle(), RendererWorkerLifecycle::SharedDestroyed(actual) if *actual == instance_id) ) )); assert_eq!( diff --git a/moli-renderer-v8/src/shared_worker_runtime/test_support.rs b/moli-renderer-v8/src/shared_worker_runtime/test_support.rs index 1efe316dc..84f780da9 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/test_support.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/test_support.rs @@ -168,6 +168,9 @@ pub(super) fn loading_host( key.script_url().to_owned(), "loader".to_owned(), target_output, + crate::runtime::RendererWorkerLifecycleReporter::new( + crate::runtime::RendererBrowserContextRuntimeId::new_for_testing(0), + ), )) } @@ -186,6 +189,7 @@ pub(super) fn loading_host_with_runtime_service( key.script_url().to_owned(), "loader".to_owned(), runtime_service.open_target_output_stream(instance_id), + runtime_service.worker_lifecycle(), )) } diff --git a/moli-renderer-v8/src/shared_worker_runtime/worker_close.rs b/moli-renderer-v8/src/shared_worker_runtime/worker_close.rs index fab0a2ab0..21b0b7315 100644 --- a/moli-renderer-v8/src/shared_worker_runtime/worker_close.rs +++ b/moli-renderer-v8/src/shared_worker_runtime/worker_close.rs @@ -62,8 +62,8 @@ mod tests { use crate::{ message_port_runtime::new_message_port_registry, runtime::{ - RendererOutputItem, RendererOutputStreamControl, RendererOwnerAction, - RendererSharedWorkerTargetEvent, + RendererOutputItem, RendererOutputStreamControl, RendererProtocolObservation, + RendererWorkerLifecycle, }, shared_worker_runtime::{ SharedWorkerClientEndpointDisposition, SharedWorkerClientEvent, @@ -98,9 +98,8 @@ mod tests { }; assert!(matches!( created.records()[0].item(), - RendererOutputItem::OwnerAction(RendererOwnerAction::SharedWorkerTargetLifecycle( - RendererSharedWorkerTargetEvent::Created(_) - )) + RendererOutputItem::Observation(RendererProtocolObservation::WorkerLifecycle(observation)) + if matches!(observation.lifecycle(), RendererWorkerLifecycle::SharedCreated(_)) )); let crate::runtime::RendererOutputTransportMessage::Publication(destroyed) = rx.try_recv().expect("destroyed fact must be published") @@ -109,13 +108,8 @@ mod tests { }; assert!(matches!( destroyed.records()[0].item(), - RendererOutputItem::OwnerAction( - RendererOwnerAction::SharedWorkerTargetLifecycle( - RendererSharedWorkerTargetEvent::Destroyed { - instance_id: destroyed_instance_id - } - ) - ) if *destroyed_instance_id == instance_id + RendererOutputItem::Observation(RendererProtocolObservation::WorkerLifecycle(observation)) + if matches!(observation.lifecycle(), RendererWorkerLifecycle::SharedDestroyed(destroyed_instance_id) if *destroyed_instance_id == instance_id) )); assert!(matches!( rx.try_recv().expect( diff --git a/moli-renderer-v8/src/worker/handle.rs b/moli-renderer-v8/src/worker/handle.rs index 334992129..cf6753a66 100644 --- a/moli-renderer-v8/src/worker/handle.rs +++ b/moli-renderer-v8/src/worker/handle.rs @@ -299,6 +299,8 @@ pub(crate) enum WorkerToParentMessage { /// target-stream cursor that precedes it instead of racing a separate /// response channel against target retirement. RuntimeInspectorResponse(crate::runtime::RendererRuntimeInspectorResponsePublication), + /// ServiceWorker bootstrap result, ordered after its initial script output. + ServiceWorkerBootstrapCompleted(WorkerBootstrapCompletion), /// A Service Worker lifecycle event finished dispatch and all `waitUntil()` promises. ServiceWorkerLifecycleCompleted(ServiceWorkerLifecycleCompletion), /// A Service Worker fetch event finished dispatch and `respondWith()` settled or fell back. @@ -820,21 +822,6 @@ impl WorkerHandle { self.thread.join(); } - pub(crate) fn dispatch_runtime_protocol_message( - &self, - inspector_session_id: Option, - raw_json: String, - deferred_response: Option, - response_tx: oneshot::Sender, String>>, - ) -> bool { - self.thread.devtools.dispatch_runtime_protocol_message( - inspector_session_id, - raw_json, - deferred_response, - response_tx, - ) - } - #[cfg(test)] pub(crate) fn attach_runtime_inspector_session( &self, @@ -845,15 +832,6 @@ impl WorkerHandle { .attach_runtime_inspector_session(inspector_session_id) } - pub(crate) fn detach_runtime_inspector_session( - &self, - inspector_session_id: Option, - ) -> bool { - self.thread - .devtools - .detach_runtime_inspector_session(inspector_session_id) - } - pub(crate) fn run_if_waiting_for_debugger_for_devtools(&self) -> bool { self.thread.devtools.run_if_waiting_for_debugger() } diff --git a/moli-renderer-v8/src/worker/thread/mod.rs b/moli-renderer-v8/src/worker/thread/mod.rs index b71df4cfc..35f4ea06d 100644 --- a/moli-renderer-v8/src/worker/thread/mod.rs +++ b/moli-renderer-v8/src/worker/thread/mod.rs @@ -202,7 +202,7 @@ pub(crate) struct WorkerSpawnOptions { pub(crate) reserved_service_worker_client_id: Option, pub(crate) indexed_db_manager: Option, pub(crate) storage_bucket_store: Option, - pub(crate) bootstrap_completion_tx: Option>, + pub(crate) bootstrap_completion_target: Option, pub(crate) pause_evaluation_until_debugger: bool, #[cfg(test)] test_request_client_owner: Option, @@ -336,7 +336,7 @@ impl WorkerSpawnOptions { reserved_service_worker_client_id: None, indexed_db_manager: None, storage_bucket_store: None, - bootstrap_completion_tx: None, + bootstrap_completion_target: None, pause_evaluation_until_debugger: false, #[cfg(test)] test_request_client_owner: None, @@ -496,11 +496,17 @@ impl WorkerSpawnOptions { self } + pub(crate) fn with_parent_bootstrap_completion(mut self) -> Self { + self.bootstrap_completion_target = Some(WorkerBootstrapCompletionTarget::Parent); + self + } + + #[cfg(test)] pub(crate) fn with_bootstrap_completion_sender( mut self, sender: mpsc::UnboundedSender, ) -> Self { - self.bootstrap_completion_tx = Some(sender); + self.bootstrap_completion_target = Some(WorkerBootstrapCompletionTarget::Observer(sender)); self } @@ -1361,7 +1367,7 @@ pub(crate) fn spawn_worker_with_options(options: WorkerSpawnOptions) -> WorkerHa reserved_service_worker_client_id, indexed_db_manager, storage_bucket_store, - bootstrap_completion_tx, + bootstrap_completion_target, pause_evaluation_until_debugger, #[cfg(test)] test_request_client_owner: _, @@ -1417,7 +1423,7 @@ pub(crate) fn spawn_worker_with_options(options: WorkerSpawnOptions) -> WorkerHa reserved_service_worker_client_id, indexed_db_manager, storage_bucket_store, - bootstrap_completion_tx, + bootstrap_completion_target, pause_evaluation_until_debugger, worker_wake_tx, parent_to_worker_rx, @@ -1542,7 +1548,7 @@ async fn worker_main( reserved_service_worker_client_id: Option, indexed_db_manager: Option, storage_bucket_store: Option, - bootstrap_completion_tx: Option>, + bootstrap_completion_target: Option, pause_evaluation_until_debugger: bool, worker_wake_tx: mpsc::UnboundedSender, mut rx: mpsc::UnboundedReceiver, @@ -1552,7 +1558,8 @@ async fn worker_main( inspector_task_runner: WorkerInspectorTaskRunner, ) { debug!(url = %script_url, "worker started"); - let mut bootstrap_completion = WorkerBootstrapCompletionReporter::new(bootstrap_completion_tx); + let mut bootstrap_completion = + WorkerBootstrapCompletionReporter::new(bootstrap_completion_target); let resource_task_runner = crate::network::RendererResourceTaskRunner::from_current_tokio() .expect("Worker owner loop must expose its resource task runner"); let loader = resource_loader_for_worker_context( @@ -1767,8 +1774,11 @@ async fn worker_main( error = %error, "failed to prepare worker global templates" ); - bootstrap_completion - .mark_install_global_failure(&script_url, error.to_string()); + bootstrap_completion.mark_install_global_failure( + &script_url, + error.to_string(), + &parent_tx, + ); install_global_failed = true; None } @@ -1806,7 +1816,11 @@ async fn worker_main( install_worker_global_scope(scope, global, state.clone(), worker_templates.as_ref()) { tracing::error!(url = %script_url, error = %e, "failed to install worker global scope"); - bootstrap_completion.mark_install_global_failure(&script_url, e.to_string()); + bootstrap_completion.mark_install_global_failure( + &script_url, + e.to_string(), + &parent_tx, + ); install_global_failed = true; } else if script_kind == WorkerScriptKind::Classic { let referrer_policy = { state.borrow().referrer_policy.clone() }; @@ -1867,7 +1881,8 @@ async fn worker_main( module_evaluation_tx.clone(), ) { WorkerBootstrapStart::Complete => { - bootstrap_completion.mark_success(worker_bootstrap_success(scope, global, &state)); + bootstrap_completion + .mark_success(worker_bootstrap_success(scope, global, &state), &parent_tx); } WorkerBootstrapStart::Pending(pending) => { if let Some(requests) = pending.pending_requests().cloned() { @@ -1883,7 +1898,7 @@ async fn worker_main( } else { WorkerErrorSource::Runtime }; - bootstrap_completion.mark_failure( + let bootstrap_failure = WorkerBootstrapFailure::from_exception_report( &report, &script_url, parent_event_kind, @@ -1901,6 +1916,10 @@ async fn worker_main( &parent_tx, &script_url, ); + bootstrap_completion.send( + WorkerBootstrapCompletion::failure(bootstrap_failure), + &parent_tx, + ); bootstrap_failed = phase == WorkerErrorPhase::Bootstrap && !handled && parent_event_kind == WorkerParentErrorEventKind::Event; @@ -3125,8 +3144,10 @@ async fn worker_main( match resume { WorkerModuleBootstrapResume::Complete => { pending_module_bootstrap = None; - bootstrap_completion - .mark_success(worker_bootstrap_success(scope, global, &state)); + bootstrap_completion.mark_success( + worker_bootstrap_success(scope, global, &state), + &parent_tx, + ); perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); drain_worker_dynamic_module_imports( scope, @@ -3149,7 +3170,7 @@ async fn worker_main( let exception = exception.as_ref().map(|value| v8::Local::new(scope, value)); let global = ctx.global(scope); - bootstrap_completion.mark_failure( + let bootstrap_failure = WorkerBootstrapFailure::from_exception_report( &report, &script_url, parent_event_kind, @@ -3166,6 +3187,10 @@ async fn worker_main( &parent_tx, &script_url, ); + bootstrap_completion.send( + WorkerBootstrapCompletion::failure(bootstrap_failure), + &parent_tx, + ); forward_worker_script_loaded(&runtime_inspector, &parent_tx); break; } @@ -3210,8 +3235,10 @@ async fn worker_main( match resume { WorkerModuleBootstrapResume::Complete => { pending_module_bootstrap = None; - bootstrap_completion - .mark_success(worker_bootstrap_success(scope, global, &state)); + bootstrap_completion.mark_success( + worker_bootstrap_success(scope, global, &state), + &parent_tx, + ); perform_worker_microtask_checkpoint_and_report_pending_promise_rejections(scope); drain_worker_dynamic_module_imports( scope, @@ -3234,7 +3261,7 @@ async fn worker_main( let exception = exception.as_ref().map(|value| v8::Local::new(scope, value)); let global = ctx.global(scope); - bootstrap_completion.mark_failure( + let bootstrap_failure = WorkerBootstrapFailure::from_exception_report( &report, &script_url, parent_event_kind, @@ -3251,6 +3278,10 @@ async fn worker_main( &parent_tx, &script_url, ); + bootstrap_completion.send( + WorkerBootstrapCompletion::failure(bootstrap_failure), + &parent_tx, + ); forward_worker_script_loaded(&runtime_inspector, &parent_tx); break; } @@ -3514,35 +3545,35 @@ enum WorkerBootstrapStart { }, } +pub(crate) enum WorkerBootstrapCompletionTarget { + Parent, + #[cfg(test)] + Observer(mpsc::UnboundedSender), +} + struct WorkerBootstrapCompletionReporter { - sender: Option>, + target: Option, } impl WorkerBootstrapCompletionReporter { - fn new(sender: Option>) -> Self { - Self { sender } + fn new(target: Option) -> Self { + Self { target } } - fn mark_success(&mut self, success: WorkerBootstrapSuccess) { - self.send(WorkerBootstrapCompletion::success(success)); - } - - fn mark_failure( + fn mark_success( &mut self, - report: &V8ExceptionReport, - script_url: &str, - event_kind: WorkerParentErrorEventKind, - phase: WorkerErrorPhase, - source: WorkerErrorSource, + success: WorkerBootstrapSuccess, + parent: &mpsc::UnboundedSender, ) { - self.send(WorkerBootstrapCompletion::failure( - WorkerBootstrapFailure::from_exception_report( - report, script_url, event_kind, phase, source, - ), - )); + self.send(WorkerBootstrapCompletion::success(success), parent); } - fn mark_install_global_failure(&mut self, script_url: &str, message: String) { + fn mark_install_global_failure( + &mut self, + script_url: &str, + message: String, + parent: &mpsc::UnboundedSender, + ) { let failure = WorkerBootstrapFailure { message, filename: script_url.to_owned(), @@ -3552,12 +3583,25 @@ impl WorkerBootstrapCompletionReporter { phase: WorkerErrorPhase::Bootstrap, source: WorkerErrorSource::Runtime, }; - self.send(WorkerBootstrapCompletion::failure(failure)); + self.send(WorkerBootstrapCompletion::failure(failure), parent); } - fn send(&mut self, completion: WorkerBootstrapCompletion) { - if let Some(sender) = self.sender.take() { - let _ = sender.send(completion); + fn send( + &mut self, + completion: WorkerBootstrapCompletion, + parent: &mpsc::UnboundedSender, + ) { + match self.target.take() { + Some(WorkerBootstrapCompletionTarget::Parent) => { + let _ = parent.send(WorkerToParentMessage::ServiceWorkerBootstrapCompleted( + completion, + )); + } + #[cfg(test)] + Some(WorkerBootstrapCompletionTarget::Observer(sender)) => { + let _ = sender.send(completion); + } + None => {} } } } diff --git a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs index 535ca5da4..c5a00ba4b 100644 --- a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs +++ b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs @@ -142,6 +142,124 @@ async fn readable_stream_controller_error_rejects_pending_read() { handle.terminate_and_join(); } +#[tokio::test] +async fn service_worker_parent_fifo_orders_console_around_bootstrap() { + ensure_v8(); + for (script_kind, prefix) in [ + (WorkerScriptKind::Classic, ""), + (WorkerScriptKind::Module, "export {};"), + ] { + let mut handle = spawn_test_worker_with_options( + WorkerSpawnOptions::new( + format!( + "{prefix} console.log('before'); setTimeout(() => console.log('after'), 0);" + ), + "https://example.test/app/fifo-sw.js".to_owned(), + ) + .with_script_kind(script_kind) + .with_global_kind(WorkerGlobalKind::Service { + registration_id: ServiceWorkerRegistrationId::from_u64_for_test(1), + version_id: ServiceWorkerVersionId::from_u64_for_test(1), + scope_url: url::Url::parse("https://example.test/app/").unwrap(), + }) + .with_parent_bootstrap_completion(), + ); + let order = timeout(TIMEOUT, async { + let mut order = Vec::new(); + while let Some(message) = handle.recv().await { + match message { + WorkerToParentMessage::Console(message) => { + let after = message.message == "log: after"; + order.push(message.message); + if after { + return order; + } + } + WorkerToParentMessage::ServiceWorkerBootstrapCompleted(completion) => { + assert!(completion.result.is_ok(), "{completion:?}"); + order.push("bootstrap".to_owned()); + } + WorkerToParentMessage::RuntimeInspectorMessages(_) => {} + other => panic!("unexpected bootstrap output: {other:?}"), + } + } + panic!("Worker parent FIFO closed before its timer ran"); + }) + .await + .expect("ServiceWorker bootstrap FIFO must finish"); + assert_eq!( + order, + ["log: before", "bootstrap", "log: after"], + "{script_kind:?}" + ); + handle.terminate_and_join(); + } +} + +#[tokio::test] +async fn service_worker_parent_fifo_reports_exception_before_bootstrap_failure() { + ensure_v8(); + for (script_kind, prefix) in [ + (WorkerScriptKind::Classic, ""), + (WorkerScriptKind::Module, "export {};"), + ] { + let mut handle = spawn_test_worker_with_options( + WorkerSpawnOptions::new( + format!( + "{prefix} console.log('before'); \ + self.addEventListener('error', () => console.log('error handler')); \ + throw new Error('bootstrap broken');" + ), + "https://example.test/app/failing-fifo-sw.js".to_owned(), + ) + .with_script_kind(script_kind) + .with_global_kind(WorkerGlobalKind::Service { + registration_id: ServiceWorkerRegistrationId::from_u64_for_test(1), + version_id: ServiceWorkerVersionId::from_u64_for_test(1), + scope_url: url::Url::parse("https://example.test/app/").unwrap(), + }) + .with_parent_bootstrap_completion(), + ); + let order = timeout(TIMEOUT, async { + let mut order = Vec::new(); + while let Some(message) = handle.recv().await { + match message { + WorkerToParentMessage::Console(message) => order.push(message.message), + WorkerToParentMessage::Error { + message, filename, .. + } => { + assert!(message.contains("bootstrap broken"), "{message}"); + assert_eq!(filename, "https://example.test/app/failing-fifo-sw.js"); + order.push("exception".to_owned()); + } + WorkerToParentMessage::ServiceWorkerBootstrapCompleted(completion) => { + let failure = completion.result.expect_err("bootstrap must fail"); + assert!(failure.message.contains("bootstrap broken"), "{failure:?}"); + order.push("bootstrap failure".to_owned()); + return order; + } + WorkerToParentMessage::RuntimeInspectorMessages(_) => {} + other => panic!("unexpected bootstrap output: {other:?}"), + } + } + panic!("Worker parent FIFO closed before reporting bootstrap failure"); + }) + .await + .expect("ServiceWorker failure FIFO must finish"); + assert_eq!( + order, + [ + "log: before", + "log: error handler", + "exception", + "bootstrap failure" + ], + "{script_kind:?}" + ); + handle.terminate_and_join(); + } +} + #[tokio::test] async fn service_worker_bootstrap_reports_fetch_handler_presence() { ensure_v8(); @@ -322,7 +440,7 @@ async fn worker_pause_evaluation_until_debugger_exposes_context_before_bootstrap }) .to_string(); assert!( - handle.dispatch_runtime_protocol_message( + handle.devtools_handle().dispatch_runtime_protocol_message( Some("SID-worker-pause".to_owned()), raw_json, None, @@ -503,7 +621,7 @@ async fn worker_inspector_interrupt_overtakes_js_running_command_during_active_j message["params"] = params; } assert!( - handle.dispatch_runtime_protocol_message( + handle.devtools_handle().dispatch_runtime_protocol_message( Some("SID-worker-interrupt".to_owned()), message.to_string(), None, @@ -1913,6 +2031,7 @@ async fn dispatch_service_worker_fetch_event_and_handled_console_for_test( | WorkerToParentMessage::ServiceWorkerSkipWaiting { .. } | WorkerToParentMessage::ServiceWorkerClientsClaim { .. } | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -2311,6 +2430,7 @@ async fn service_worker_fetch_respond_with_readable_stream_body_posts_stream_chu | WorkerToParentMessage::ServiceWorkerSkipWaiting { .. } | WorkerToParentMessage::ServiceWorkerClientsClaim { .. } | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -4320,6 +4440,7 @@ async fn service_worker_skip_waiting_posts_runtime_request() { | WorkerToParentMessage::Console(_) | WorkerToParentMessage::RuntimeInspectorMessages(_) | WorkerToParentMessage::Post(_) + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -4410,6 +4531,7 @@ async fn service_worker_clients_claim_posts_runtime_request() { | WorkerToParentMessage::RuntimeInspectorMessages(_) | WorkerToParentMessage::Post(_) | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -4678,6 +4800,7 @@ async fn service_worker_clients_match_all_and_get_resolve_from_parent_query_resu | WorkerToParentMessage::RuntimeInspectorMessages(_) | WorkerToParentMessage::Post(_) | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -4797,6 +4920,7 @@ async fn service_worker_worker_client_query_builds_base_client_object() { } WorkerToParentMessage::Console(_) | WorkerToParentMessage::RuntimeInspectorMessages(_) + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} other => panic!("unexpected worker message: {other:?}"), @@ -4987,6 +5111,7 @@ self.addEventListener("message", event => { | WorkerToParentMessage::RuntimeInspectorMessages(_) | WorkerToParentMessage::Post(_) | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -5131,6 +5256,7 @@ self.addEventListener("message", event => { | WorkerToParentMessage::RuntimeInspectorMessages(_) | WorkerToParentMessage::Post(_) | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -5343,6 +5469,7 @@ self.addEventListener("message", event => { | WorkerToParentMessage::RuntimeInspectorMessages(_) | WorkerToParentMessage::Post(_) | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -5487,6 +5614,7 @@ self.addEventListener("message", event => { | WorkerToParentMessage::RuntimeInspectorMessages(_) | WorkerToParentMessage::Post(_) | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -5704,6 +5832,7 @@ self.addEventListener("message", event => { | WorkerToParentMessage::RuntimeInspectorMessages(_) | WorkerToParentMessage::Post(_) | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} WorkerToParentMessage::ServiceWorkerClientsOpenWindow(open_window) => { @@ -7156,6 +7285,7 @@ self.addEventListener("notificationclick", event => { | WorkerToParentMessage::RuntimeInspectorMessages(_) | WorkerToParentMessage::Post(_) | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -7316,6 +7446,7 @@ self.addEventListener("notificationclose", event => { | WorkerToParentMessage::RuntimeInspectorMessages(_) | WorkerToParentMessage::Post(_) | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -9566,6 +9697,7 @@ async fn worker_error_report_ignores_throwing_accessors() { | WorkerToParentMessage::ServiceWorkerSkipWaiting { .. } | WorkerToParentMessage::ServiceWorkerClientsClaim { .. } | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => panic!("expected worker error"), } diff --git a/moli-renderer-v8/src/worker/thread/tests/mod.rs b/moli-renderer-v8/src/worker/thread/tests/mod.rs index da57d3007..3e155a7fe 100644 --- a/moli-renderer-v8/src/worker/thread/tests/mod.rs +++ b/moli-renderer-v8/src/worker/thread/tests/mod.rs @@ -373,6 +373,7 @@ async fn dispatch_service_worker_lifecycle_event_for_test( | WorkerToParentMessage::ServiceWorkerSkipWaiting { .. } | WorkerToParentMessage::ServiceWorkerClientsClaim { .. } | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -469,6 +470,7 @@ async fn dispatch_service_worker_fetch_event_with_request_for_test( | WorkerToParentMessage::ServiceWorkerSkipWaiting { .. } | WorkerToParentMessage::ServiceWorkerClientsClaim { .. } | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -555,6 +557,7 @@ async fn dispatch_service_worker_message_event_object_for_test( | WorkerToParentMessage::ServiceWorkerSkipWaiting { .. } | WorkerToParentMessage::ServiceWorkerClientsClaim { .. } | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -618,6 +621,7 @@ async fn dispatch_service_worker_notification_event_for_test( | WorkerToParentMessage::ServiceWorkerSkipWaiting { .. } | WorkerToParentMessage::ServiceWorkerClientsClaim { .. } | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -687,6 +691,7 @@ async fn dispatch_service_worker_push_event_for_test( | WorkerToParentMessage::ServiceWorkerSkipWaiting { .. } | WorkerToParentMessage::ServiceWorkerClientsClaim { .. } | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -758,6 +763,7 @@ async fn dispatch_service_worker_sync_event_for_test( | WorkerToParentMessage::ServiceWorkerSkipWaiting { .. } | WorkerToParentMessage::ServiceWorkerClientsClaim { .. } | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } @@ -830,6 +836,7 @@ async fn dispatch_service_worker_periodic_sync_event_for_test( | WorkerToParentMessage::ServiceWorkerSkipWaiting { .. } | WorkerToParentMessage::ServiceWorkerClientsClaim { .. } | WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. } + | WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_) | WorkerToParentMessage::RuntimeInspectorResponse(_) | WorkerToParentMessage::SharedWorkerClosed => {} } diff --git a/moli-renderer-v8/src/worker/thread_owner.rs b/moli-renderer-v8/src/worker/thread_owner.rs index ba87a5e9f..7aa2f0ac5 100644 --- a/moli-renderer-v8/src/worker/thread_owner.rs +++ b/moli-renderer-v8/src/worker/thread_owner.rs @@ -283,7 +283,7 @@ mod tests { .all(|thread| !thread.is_joined()) ); let (reply_tx, reply_rx) = tokio::sync::oneshot::channel(); - assert!(peer_handle.dispatch_runtime_protocol_message( + assert!(peer_handle.devtools_handle().dispatch_runtime_protocol_message( None, r#"{"id":71,"method":"Runtime.evaluate","params":{"expression":"41 + 1","returnByValue":true}}"#.into(), None, diff --git a/moli-test-support/src/server_routes.rs b/moli-test-support/src/server_routes.rs index 68e4fdc3f..e3cd0d338 100644 --- a/moli-test-support/src/server_routes.rs +++ b/moli-test-support/src/server_routes.rs @@ -2,6 +2,17 @@ use super::*; pub(super) fn build_router() -> Router { routes_wait::add_wait_routes(Router::new()) + .route("/native-service-worker/", get(|| async { + Html("") + })) + .route("/native-service-worker/failed-install", get(|| async { + Html("") + })) + .route("/native-service-worker/worker.js", get(|| async { + ([(CONTENT_TYPE, "text/javascript")], + "console.log('before native Started'); oninstall = event => event.waitUntil(skipWaiting()); onactivate = event => event.waitUntil(clients.claim()); onfetch = event => event.respondWith(new Response('native worker'));" + ) + })) .route("/static", get(static_page)) .route( "/compat/child-dynamic-markup-document",