refactor(browser): own Worker lifecycles and inspection readiness

Admit dedicated, nested, shared and service Workers natively. Bind inspection at readiness, preserve bootstrap ordering and issue replay, and keep Runtime admission and failed navigation retirement tied to native state.
This commit is contained in:
ldm0
2026-09-22 23:21:33 +08:00
parent 5d26120f4e
commit 104f1a93d0
109 changed files with 5516 additions and 2915 deletions
Generated
+1
View File
@@ -2973,6 +2973,7 @@ dependencies = [
"moli-site",
"moli-storage-key",
"moli-storage-service",
"moli-test-support",
"moli-trace",
"moli-url",
"moli-url-policy",
+9
View File
@@ -218,6 +218,12 @@ pub struct BrowserContext {
permission_overrides: PermissionOverrides,
download_policy: Option<DownloadPolicy>,
pub(in crate::browser) downloads: super::downloads::DownloadManager,
pub(in crate::browser) dedicated_workers: IndexMap<u64, super::DedicatedWorkerSnapshot>,
pub(in crate::browser) service_workers: IndexMap<u64, super::ServiceWorkerSnapshot>,
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<WebContentsId, WebContents>,
@@ -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,
@@ -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<Item = super::super::WorkerSnapshot> + '_ {
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<RendererWorkerInspectionEndpoint> {
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<String>,
) -> 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<String>,
) -> 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<String>,
) -> 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<String>,
) -> 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()
}
}
+8
View File
@@ -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<JavaScriptDialogOpened>,
pub navigations: Vec<NavigationSnapshot>,
pub downloads: Vec<super::DownloadRecordSnapshot>,
pub workers: Vec<super::WorkerSnapshot>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -209,6 +213,7 @@ impl BrowserEventStream {
downloads: impl Iterator<Item = super::DownloadRecordSnapshot>,
javascript_dialogs: impl Iterator<Item = JavaScriptDialogOpened>,
navigations: impl Iterator<Item = NavigationSnapshot>,
workers: impl Iterator<Item = super::WorkerSnapshot>,
) -> (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));
+5
View File
@@ -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)]
+24 -7
View File
@@ -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<tokio::sync::watch::Receiver<()>>,
local_sender: BrowserLocalSender,
native_sender: mpsc::WeakUnboundedSender<BrowserOwnerMessage>,
events: super::events::BrowserEventStream,
}
impl Browser {
fn new(local_sender: BrowserLocalSender) -> Self {
fn new(
local_sender: BrowserLocalSender,
native_sender: mpsc::WeakUnboundedSender<BrowserOwnerMessage>,
) -> 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::<Vec<_>>();
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::<Vec<_>>();
let dialogs = context.javascript_dialog_snapshots();
context.shutdown();
@@ -263,6 +279,7 @@ impl fmt::Debug for BrowserHandle {
impl BrowserHandle {
fn start() -> Result<Self, String> {
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<u64>;
fn set_service_worker_pause_on_start_for_version(version_id: u64, pause: bool) -> bool;
fn worker_inspection_endpoint(target: crate::runtime::RendererWorkerInspectionTarget) -> Option<crate::runtime::RendererWorkerInspectionEndpoint>;
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<String>) -> bool;
fn detach_shared_worker_inspector_session(instance_id: moli_shared_worker::SharedWorkerInstanceId, session_id: Option<String>) -> bool;
fn detach_dedicated_worker_inspector_session(instance_id: u64, session_id: Option<String>) -> bool;
fn detach_service_worker_inspector_session(version_id: u64, session_id: Option<String>) -> 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;
+423 -1
View File
@@ -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,<script>globalThis.worker = new SharedWorker('data:text/javascript,onconnect = () => {}', 'native-membership')</script>").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,<script>globalThis.worker = new Worker('data:text/javascript,onmessage = () => {}', {name: 'native-dedicated'})</script>").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,<script>globalThis.worker = new Worker('data:text/javascript,close()')</script>").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,<script>globalThis.worker = new Worker('ftp://example.test/worker.js')</script>").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,<script>globalThis.worker = new SharedWorker('data:text/javascript,onconnect = () => close()', 'native-short-lived')</script>").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);
+172
View File
@@ -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()));
}
}
}
+86
View File
@@ -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<std::sync::Arc<crate::page::RendererDedicatedWorkerMainScript>>,
}
#[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),
}
}
}
+12 -9
View File
@@ -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,
+1
View File
@@ -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,
@@ -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 { .. } => {
@@ -36,6 +36,8 @@ enum RuntimeCommandWait {
pub(crate) enum DevToolsRuntimeCommandProgress {
Complete(Box<DevToolsCommandExecution>),
/// No renderer command or Document endpoint has been captured yet.
AwaitingDocument(Box<DevToolsCommand>),
Pending {
pending: Box<PendingDevToolsRuntimeExecution>,
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
@@ -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"<body>admitted document</body>";
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"<body>").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"<body>".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,<iframe srcdoc='<body>child document</body>'></iframe>", "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 {
@@ -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<DevToolsCommand>),
Runtime(Box<PendingDevToolsRuntimeExecution>),
Navigation(DevToolsNavigationCommandWait),
NavigationLifecycle(Box<PendingDevToolsNavigationLifecycle>),
@@ -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(
@@ -65,16 +65,9 @@ impl ClassicPendingRuntime {
receivers: &mut CdpSchedulerEventReceivers,
) -> Option<Self> {
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<Self> {
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<tokio::time::Instant> {
// 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<Self> {
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 {
+1
View File
@@ -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]
+19 -8
View File
@@ -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,
@@ -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));
}
@@ -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")
@@ -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<BackgroundProtocolEvent> {
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
@@ -154,16 +154,11 @@ impl TargetSessionOwnerMut<'_> {
true
}
fn audits_output_snapshot(&mut self) -> Vec<moli_core::page::InspectorIssueSnapshot> {
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
@@ -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,
@@ -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(),
+15 -5
View File
@@ -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<BrowserNavigationWaiter, String> {
@@ -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)
+137 -256
View File
@@ -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<String>,
pending: SharedWorkerRuntimeProtocolDispatchFuture,
pending: WorkerRuntimeProtocolDispatchFuture,
response_route: RuntimeProtocolResponseRoute,
}
pub struct PendingServiceWorkerRuntimeProtocolMessageDispatch {
session_id: Option<String>,
pending: ServiceWorkerRuntimeProtocolDispatchFuture,
pending: WorkerRuntimeProtocolDispatchFuture,
response_route: RuntimeProtocolResponseRoute,
}
@@ -506,11 +488,44 @@ pub struct CompletedRuntimeChildDefaultContextLookupDispatch {
completion: moli_core::page::CompletedPageCommand,
}
type SharedWorkerRuntimeProtocolDispatchFuture =
Pin<Box<dyn Future<Output = Result<CompletedWorkerRuntimeProtocolDispatch, String>>>>;
type ServiceWorkerRuntimeProtocolDispatchFuture =
type WorkerRuntimeProtocolDispatchFuture =
Pin<Box<dyn Future<Output = Result<CompletedWorkerRuntimeProtocolDispatch, String>>>>;
fn worker_runtime_protocol_dispatch(
endpoint: Result<RendererWorkerInspectionEndpoint, String>,
session_id: Option<String>,
raw_json: String,
response: Option<RendererRuntimeInspectorResponseSender>,
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<SharedWorkerRuntimeTargetRoute, String> {
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<bool, String> {
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<ServiceWorkerRuntimeTargetRoute, String> {
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<RendererWorkerInspectionEndpoint, String> {
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<PendingSharedWorkerRuntimeProtocolMessageDispatch, String> {
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<RendererRuntimeInspectorResponseSender>,
response_route: RuntimeProtocolResponseRoute,
) -> Result<PendingSharedWorkerRuntimeProtocolMessageDispatch, String> {
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<PendingServiceWorkerRuntimeProtocolMessageDispatch, String> {
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<RendererRuntimeInspectorResponseSender>,
response_route: RuntimeProtocolResponseRoute,
) -> Result<PendingServiceWorkerRuntimeProtocolMessageDispatch, String> {
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| {
@@ -104,6 +104,8 @@ pub struct BrowserContext {
pub(in crate::conn) automation_download_events_enabled: Option<bool>,
pending_popup_javascript_dialogs:
HashMap<(moli_core::browser::DocumentHandle, u64), Vec<TargetPreparedJavaScriptDialog>>,
/// Snapshot recovery cursor only; ordinary Worker FIFOs are independent.
pub(crate) worker_snapshot_sequence: Option<moli_core::browser::BrowserSequence>,
pub(crate) shared_worker_targets: BTreeMap<SharedWorkerInstanceId, SharedWorkerTargetState>,
pub(crate) dedicated_worker_targets: BTreeMap<u64, DedicatedWorkerTargetState>,
pub(crate) service_worker_targets: BTreeMap<u64, ServiceWorkerTargetState>,
@@ -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(),
@@ -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,
@@ -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,<title>first</title>").await;
let old_document = owner.target_document_id(TARGET).unwrap();
let loaded = prepare_navigation(
&mut owner,
"LOADER-atomic",
"data:text/html,<title>second</title>",
)
.await;
let loaded = prepare_navigation(&mut owner, "data:text/html,<title>second</title>").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,<title>replacement</title>",
)
.await;
let next = prepare_navigation(&mut owner, "data:text/html,<title>replacement</title>").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,<title>native</title>",
)
.await;
let loaded = prepare_navigation(&mut owner, "data:text/html,<title>native</title>").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,<title>native</title>",
)
.await;
let loaded = prepare_navigation(&mut owner, "data:text/html,<title>native</title>").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,<title>first</title>").await;
let previous_document = owner.target_document_id(TARGET).unwrap();
let loaded = prepare_navigation(
&mut owner,
"LOADER-projection",
"data:text/html,<title>committed</title>",
)
.await;
let loaded = prepare_navigation(&mut owner, "data:text/html,<title>committed</title>").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,<title>current</title>",
)
.await;
let loaded = prepare_navigation(&mut owner, "data:text/html,<title>current</title>").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,<title>successor</title>",
)
.await;
let waiter = prepare_navigation(&mut owner, "data:text/html,<title>successor</title>").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,<title>first</title>").await;
let loaded = prepare_navigation(
&mut owner,
"LOADER-restore",
"data:text/html,<title>committed</title><script>Object.defineProperty(globalThis,'protectedBinding',{value:1,configurable:false})</script>",
let loaded = prepare_navigation(&mut owner, "data:text/html,<title>committed</title><script>Object.defineProperty(globalThis,'protectedBinding',{value:1,configurable:false})</script>",
)
.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,<title>stale</title>",
)
.await;
let loaded = prepare_navigation(&mut owner, "data:text/html,<title>stale</title>").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,<title>second</title>",
)
.await;
let loaded = prepare_navigation(&mut owner, "data:text/html,<title>second</title>").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,<p>replacement</p>",
)
.await;
let loaded = prepare_navigation(&mut owner, "data:text/html,<p>replacement</p>").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,<p>replacement policy owner</p>",
)
.await;
let loaded =
prepare_navigation(&mut owner, "data:text/html,<p>replacement policy owner</p>").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,<p>replacement native command owner</p>",
)
.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,<title>second</title><p>second</p>",
)
.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,<p>second</p>").await;
let loaded = prepare_navigation(&mut moved, "data:text/html,<p>second</p>").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,<p>candidate</p>",
)
.await;
let candidate = prepare_navigation(&mut owner, "data:text/html,<p>candidate</p>").await;
let navigation = candidate.request().navigation;
assert_ne!(prepared_renderer(&owner, &candidate), first_renderer);
drop(candidate);
@@ -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
}
@@ -464,12 +464,6 @@ impl TargetRuntimeSlot {
.flatten()
}
pub(crate) fn inspector_issues(&self) -> Option<Vec<moli_core::page::InspectorIssueSnapshot>> {
self.current_renderer_inspection_binding()
.is_some()
.then(|| self.observable_queue.inspector_issues())
}
pub(crate) fn ingest_observable_output_snapshot(
&mut self,
items: &[ScriptObservableOutputItem],
@@ -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<RendererWorkerInspectionEndpoint> {
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<String>,
) -> 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<String>,
) -> 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<String>,
) -> 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<String>,
) -> 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()
}
}
@@ -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<Option<String>>,
pub(crate) inner: SharedWorkerTargetState,
main_script: Option<DedicatedWorkerMainScriptSnapshot>,
main_script: Option<Arc<RendererDedicatedWorkerMainScript>>,
delivered_main_script_sessions: BTreeSet<String>,
replayable_main_script_sessions: BTreeSet<String>,
defer_failed_load_destroy_until_debugger_resume: bool,
renderer_destroyed_while_waiting_for_debugger: bool,
}
#[derive(Clone, Debug)]
pub(crate) enum DedicatedWorkerMainScriptOutcome {
Loaded(Box<NavigationResponse>),
Failed {
error_message: String,
response: Option<Box<NavigationResponse>>,
},
}
#[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<RendererDedicatedWorkerMainScript>,
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 {
+1 -3
View File
@@ -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::{
@@ -179,6 +179,20 @@ impl ServiceWorkerTargetState {
self.runtime_execution_context_id
}
pub(crate) fn inspection_target(
&self,
) -> Option<moli_core::runtime::RendererWorkerInspectionTarget> {
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<TargetServiceWorkerRunIdentity> {
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<TargetServiceWorkerRunIdentity> {
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<TargetServiceWorkerRunRetirement> {
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]
@@ -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(),
@@ -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,
@@ -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),
) => {
@@ -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
@@ -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;
}
+148
View File
@@ -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, "<!doctype html><body>standards</body>").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, "<!doctype html><body>predecessor</body>").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,<!doctype html><body>replacement</body>",
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();
@@ -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<InspectorIssueSnapshot>,
}
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<TargetAuditsOutputCursor> {
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<Vec<InspectorIssueSnapshot>> {
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::<Vec<_>>();
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");
@@ -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::<Vec<_>>();
let replies = retired
.iter()
.filter(|event| event["id"] == 103)
.collect::<Vec<_>>();
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::<Vec<_>>();
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();
@@ -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")
}));
@@ -497,7 +497,6 @@ impl RuntimeObservableEmissionSnapshot {
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(crate) struct TargetRuntimeObservableQueueState {
observable_output_items: Vec<ScriptObservableOutputItem>,
source_inspector_issues: Vec<moli_core::page::InspectorIssueSnapshot>,
latest_source_tail: Option<TargetRuntimeObservableSourceOutput>,
source_tails_by_identity: HashMap<(String, DocumentId), TargetRuntimeObservableSourceOutput>,
source_outputs: Vec<TargetRuntimeObservableSourceOutput>,
@@ -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<moli_core::page::InspectorIssueSnapshot> {
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::<Vec<_>>();
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<TargetRuntimeObservableSourceOutput> {
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(),
+5 -2
View File
@@ -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
));
}
@@ -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,<body>lifecycle</body>").unwrap(),
"GET".into(),
+26 -66
View File
@@ -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<moli_core::runtime::RendererWorkerInspectionEndpoint> {
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::{
@@ -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();
+3 -4
View File
@@ -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.
@@ -55,6 +55,8 @@ async fn dispose_live_session_domains_async(
plan: &SessionDisposalPlan,
) -> anyhow::Result<Option<moli_core::RendererOutputFence>> {
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())
}
@@ -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();
File diff suppressed because it is too large Load Diff
@@ -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::<Vec<_>>()
.join(",");
let url = format!("data:text/html,<script>globalThis.workers = [{script}]</script>");
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<moli_core::browser::BrowserEventRecord>,
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,<script>globalThis.worker = new Worker('data:text/javascript,onmessage = () => {}', {name:'replacement'})</script>".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()
}
-2
View File
@@ -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(),
@@ -163,6 +163,7 @@ pub(crate) fn dispatch_worker_event<'s>(
| WorkerToParentMessage::ServiceWorkerClientsClaim { .. }
| WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. }
| WorkerToParentMessage::SharedWorkerClosed
| WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_)
| WorkerToParentMessage::RuntimeInspectorResponse(_) => false,
}
}
+24 -21
View File
@@ -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::{
@@ -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::<Vec<_>>() {
self.forget_worker(worker_id);
}
}
}
@@ -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<ClipboardSnapshot>,
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<super::RendererWorkerInspectionEndpoint> {
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,
@@ -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<DedicatedWorkerDevToolsTarget> {
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<String>,
raw_json: String,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<String>,
raw_json: String,
deferred_response: RendererRuntimeInspectorResponseSender,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<crate::runtime::CompletedWorkerRuntimeInspectorCommandDispatch, String> {
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<String>,
raw_json: String,
deferred_response: Option<RendererRuntimeInspectorResponseSender>,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<String>,
) -> 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<String>,
) -> bool {
self.dedicated_worker_devtools_handle(instance_id)
.is_some_and(|handle| handle.detach_runtime_inspector_session(inspector_session_id))
) -> Option<RendererWorkerInspectionEndpoint> {
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(
@@ -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<crate::service_worker_runtime::ServiceWorkerRuntimeService> {
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<String>,
raw_json: String,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<String>,
raw_json: String,
deferred_response: RendererRuntimeInspectorResponseSender,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<crate::runtime::CompletedWorkerRuntimeInspectorCommandDispatch, String> {
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<String>,
) -> 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,
@@ -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<LazySharedWorkerRuntimeState>,
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<crate::shared_worker_runtime::SharedWorkerRuntimeService> {
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<String>,
raw_json: String,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<String>,
raw_json: String,
deferred_response: RendererRuntimeInspectorResponseSender,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<crate::runtime::CompletedWorkerRuntimeInspectorCommandDispatch, String> {
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<String>,
) -> bool {
self.shared_worker_runtime_if_initialized()
.is_some_and(|runtime| {
runtime.detach_runtime_inspector_session(instance_id, inspector_session_id)
})
}
}
#[cfg(test)]
+17 -7
View File
@@ -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,
+42 -93
View File
@@ -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<crate::protocol_types::NavigationResponse>,
},
ScriptLoadFailed {
instance_id: u64,
script_url: String,
error_message: String,
response: Option<Box<crate::protocol_types::NavigationResponse>>,
},
/// 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<String>,
messages: Vec<RendererRuntimeInspectorMessage>,
},
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<crate::protocol_types::NavigationResponse>),
Failed {
error_message: String,
response: Option<Box<crate::protocol_types::NavigationResponse>>,
},
}
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<super::RendererServiceWorkerRunIdentity>,
},
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,
@@ -43,7 +43,7 @@ use crate::planning::{
};
use crate::runtime::{
RendererOwnerResourceActivitySource, RendererPageCommand, RendererRuntimeObservableSourceItem,
RendererSharedWorkerTargetEvent,
RendererSharedWorkerObservation,
};
use crate::script_vm::{PostParseLifecycleAdvance, PostParseLifecycleCompletionAction};
use crate::types::{
@@ -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<RendererSharedWorkerTargetEvent>,
Vec<crate::runtime::RendererProtocolObservation>,
)> {
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::<Vec<_>>();
@@ -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::<Vec<_>>();
@@ -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<PendingSubresourceFetchInfo>,
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<RendererPopupOpening>),
JavaScriptDialog(std::sync::Arc<RendererJavaScriptDialogOpening>),
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(
@@ -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
};
@@ -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()]))
}));
}
@@ -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,
}
}
@@ -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<RendererTurnOutputJournal>,
unavailable_message: &'static str,
}
impl RendererWorkerInspectionEndpoint {
pub(crate) fn new(
handle: WorkerDevToolsHandle,
output_journal: Option<RendererTurnOutputJournal>,
unavailable_message: &'static str,
) -> Self {
Self {
handle,
output_journal,
unavailable_message,
}
}
pub fn attach_session(&self, session_id: Option<String>) -> bool {
self.handle.attach_runtime_inspector_session(session_id)
}
pub fn detach_session(&self, session_id: Option<String>) -> bool {
self.handle.detach_runtime_inspector_session(session_id)
}
pub async fn dispatch_protocol_message(
&self,
session_id: Option<String>,
raw_json: String,
response: Option<RendererRuntimeInspectorResponseSender>,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<Output = CompletedWorkerRuntimeInspectorCommandDispatch>,
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,
)
})
}
}
@@ -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<super::RendererDedicatedWorkerMainScript>,
},
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<RendererWorkerLifecycle>,
committed: watch::Receiver<Option<u64>>,
}
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<RendererWorkerLifecycle>,
}
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<RendererCommittedWorkerLifecycle> {
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<RendererWorkerLifecycle>,
committed: watch::Sender<Option<u64>>,
}
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<dyn Fn(RendererWorkerLifecycleInput) + Send + Sync>;
#[derive(Clone)]
pub(crate) struct RendererWorkerLifecycleReporter {
runtime: RendererBrowserContextRuntimeId,
handler: Arc<Mutex<Option<WorkerLifecycleHandler>>>,
}
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);
}
}
}
@@ -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
@@ -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,
@@ -491,6 +491,7 @@ impl ScriptVm {
| WorkerToParentMessage::ServiceWorkerClientsClaim { .. }
| WorkerToParentMessage::ServiceWorkerImportedScriptLoaded { .. }
| WorkerToParentMessage::SharedWorkerClosed
| WorkerToParentMessage::ServiceWorkerBootstrapCompleted(_)
| WorkerToParentMessage::RuntimeInspectorResponse(_) => {}
}
Ok(())
@@ -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<String>,
raw_json: String,
deferred_response: Option<crate::runtime::RendererRuntimeInspectorResponseSender>,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<String>,
raw_json: String,
deferred_response: crate::runtime::RendererRuntimeInspectorResponseSender,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<String>,
raw_json: String,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<String>,
) -> bool {
pub(super) fn inspection_handle(&self) -> Option<crate::worker::WorkerDevToolsHandle> {
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::<WorkerBootstrapCompletion>();
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<WorkerToParentMessage>,
) {
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<WorkerBootstrapCompletion>,
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<WorkerBootstrapCompletion>,
) -> WorkerHandle {
let storage_key = moli_storage_key::deserialize_serialized_storage_key(&params.storage_key)
.unwrap_or_else(|| MoliStorageKey::first_party_from_url(&params.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);
@@ -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<crate::runtime::RendererServiceWorkerTargetEvent> {
fn take_target_output_events_for_test(&self) -> Vec<ServiceWorkerOutputForTest> {
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<SharedRendererServiceWorkerHost> {
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<crate::runtime::RendererTurnOutputJournal> {
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<WorkerRuntimeInspectorMessageBatch>,
) {
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::<Vec<_>>();
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"
);
}
@@ -331,6 +331,7 @@ impl ServiceWorkerRuntimeService {
);
version.running_state =
ServiceWorkerVersionRunningState::Starting { host: host.clone() };
state.record_target_starting(version_id);
Some(ServiceWorkerQueuedLaunch {
params,
host,
@@ -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 {
@@ -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,
@@ -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<RendererWorkerInspectionEndpoint> {
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",
))
}
}
@@ -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<String>,
raw_json: String,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<String>,
raw_json: String,
deferred_response: RendererRuntimeInspectorResponseSender,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<crate::runtime::CompletedWorkerRuntimeInspectorCommandDispatch, String> {
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<String>,
) -> bool {
let Some(host) = self.running_host_for_version(version_id) else {
return false;
};
host.detach_worker_runtime_inspector_session(inspector_session_id)
}
}
@@ -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");
@@ -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<crate::worker::WorkerRuntimeInspectorMessageBatch>),
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)
@@ -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<ServiceWorkerVersionId>,
pub(super) main_script_update_check_diagnostics:
HashMap<ServiceWorkerRegistrationId, ServiceWorkerMainScriptUpdateCheckDiagnostics>,
pub(super) service_worker_target_infos:
HashMap<ServiceWorkerVersionId, RendererServiceWorkerTargetInfo>,
service_worker_target_run_projections:
HashMap<ServiceWorkerVersionId, RendererServiceWorkerTargetRunResidence>,
target_output_streams: ServiceWorkerTargetOutputStreams,
pub(super) devtools_related_pause_on_start_policies:
Vec<ServiceWorkerDevToolsRelatedPauseOnStartPolicy>,
@@ -214,71 +210,9 @@ pub(super) struct ServiceWorkerRuntimeState {
HashMap<ServiceWorkerRegistrationKey, ServiceWorkerStoredRegistration>,
}
/// 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<RendererServiceWorkerRunIdentity>,
live: Option<RendererServiceWorkerRunIdentity>,
}
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<RendererServiceWorkerRunIdentity> {
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<RendererServiceWorkerRunIdentity> {
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<RendererServiceWorkerRunIdentity> {
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<String>,
) -> 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<RendererServiceWorkerRunIdentity>,
) -> 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<String>,
messages: Vec<RendererRuntimeInspectorMessage>,
) -> 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());
}
}
@@ -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<ServiceWorkerTargetOutputStreamsState>,
}
@@ -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<RendererServiceWorkerTargetEvent> {
) -> Vec<ServiceWorkerOutputForTest> {
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
}
@@ -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;
@@ -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());
@@ -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(_) => {
@@ -31,6 +31,7 @@ pub(super) struct RendererSharedWorkerHost {
pub(super) state: Mutex<RendererSharedWorkerHostState>,
pub(super) clients: Mutex<HashMap<SharedWorkerClientId, RendererSharedWorkerClient>>,
target_output: crate::runtime::RendererTurnOutputJournal,
pub(super) worker_lifecycle: crate::runtime::RendererWorkerLifecycleReporter,
target_output_retired: AtomicBool,
pub(super) runtime_response_publications: Mutex<SharedWorkerRuntimeResponsePublicationState>,
}
@@ -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,
@@ -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();
}
}
}
@@ -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,
}
}
}
@@ -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<Self>) {
@@ -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<Self>,
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)
));
}
@@ -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<RendererWorkerInspectionEndpoint> {
let host = self.running_host_for_instance(instance_id)?;
Some(RendererWorkerInspectionEndpoint::new(
host.running_devtools_handle()?,
Some(host.target_output().clone()),
"SharedWorkerRuntimeUnavailable",
))
}
}
@@ -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,
@@ -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<String>,
raw_json: String,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<String>,
raw_json: String,
deferred_response: RendererRuntimeInspectorResponseSender,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<String>,
raw_json: String,
deferred_response: Option<RendererRuntimeInspectorResponseSender>,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<String>,
) -> bool {
self.running_devtools_handle()
.is_some_and(|handle| handle.detach_runtime_inspector_session(inspector_session_id))
}
}
@@ -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<String>,
raw_json: String,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<String>,
raw_json: String,
deferred_response: RendererRuntimeInspectorResponseSender,
) -> Result<Vec<RendererRuntimeInspectorMessage>, 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<crate::runtime::CompletedWorkerRuntimeInspectorCommandDispatch, String> {
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<String>,
) -> bool {
let Some(host) = self.running_host_for_instance(instance_id) else {
return false;
};
host.detach_runtime_inspector_session(inspector_session_id)
}
}
@@ -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(),
));
}
@@ -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();
}
}
@@ -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)));
}
}

Some files were not shown because too many files have changed in this diff Show More