Files
moli/moli-protocol/src/conn/runtime_eval.rs
T

8673 lines
336 KiB
Rust

use std::{collections::hash_map::Entry, future::Future, pin::Pin};
use moli_page_types::{FrontendCommandId, RendererCallId, RendererInspectorResponseDelivery};
use moli_protocol_cdp::CdpRendererCommandReplayDispatch;
use moli_shared_worker::SharedWorkerInstanceId;
use serde_json::{Map, Value, json};
use crate::devtools_runtime::{
AutomationEvent, DevToolsFrameId, DevToolsRealmId, DevToolsRemoteValue,
DevToolsResultOwnership, DevToolsTargetId, RuntimeExecutionContextEvent, ScriptMessageEvent,
};
use moli_core::{
RendererOutputFence, RendererRuntimeCommandCausalIdentity,
RendererRuntimeInspectorResponseSender,
page::{
DocumentNodeObjectSnapshot, DocumentNodeRuntimeObjectResolution,
MAX_INSPECTOR_PROTOCOL_VALUE_DEPTH, RendererAgentAttachmentId, RendererCommandTurnOutput,
RendererDomBidiNodeBindingResolution, RendererDomBidiNodeSharedIdResolution,
RendererInspectorCommandRoute, RendererRuntimeCommandOutput,
RendererRuntimeInspectorMessage, RendererRuntimeRealmInfo,
},
};
use crate::conn::state::{
DevToolsSessionState, PreparedRendererCallTermination, SessionRendererCallReplay,
SessionRendererCallTermination,
};
use crate::domains::command_output::protocol_message_background_event;
use crate::domains::runtime_context_events::{
RuntimeContextProtocolEvent, apply_runtime_context_protocol_event_side_effects_for_owner_typed,
emit_runtime_context_protocol_background_event_typed,
qualify_runtime_context_protocol_event_for_owner_typed,
};
use super::*;
type RuntimeInspectorResponseReceiver = RuntimeInspectorAsyncCompletionReceiver;
const SHARED_WORKER_RUNTIME_REMOTE_OBJECT_CLEANUP_COMMAND_ID_BASE: u64 = 900_600_000;
const BIDI_SCRIPT_RESULT_OBJECT_GROUP: &str = "webdriver-bidi";
const BIDI_CHANNEL_OBJECT_GROUP_PREFIX: &str = "webdriver-bidi-channel-";
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct RuntimeBindingCallEvent {
source: moli_core::page::RuntimeBindingCallSourceIdentity,
name: String,
payload: String,
execution_context_id: i64,
}
impl RuntimeBindingCallEvent {
pub(crate) fn from_renderer_call(call: moli_core::page::PendingRuntimeBindingCall) -> Self {
Self {
source: call.source,
name: call.name,
payload: call.payload,
execution_context_id: call.execution_context_id,
}
}
#[cfg(test)]
pub(crate) fn new_for_test(
local_window_id: u64,
realm_generation: u64,
name: impl Into<String>,
payload: impl Into<String>,
execution_context_id: i64,
) -> Self {
Self {
source: moli_core::page::RuntimeBindingCallSourceIdentity::new(
local_window_id,
realm_generation,
),
name: name.into(),
payload: payload.into(),
execution_context_id,
}
}
#[cfg(test)]
pub(crate) fn source(&self) -> moli_core::page::RuntimeBindingCallSourceIdentity {
self.source
}
pub(crate) fn into_background_protocol_event(
self,
session_id: Option<&str>,
) -> BackgroundProtocolEvent {
BackgroundProtocolEvent::runtime_binding_called(
session_id,
self.name,
self.payload,
self.execution_context_id,
)
}
}
#[cfg(test)]
fn runtime_protocol_message_id(raw_json: &str) -> Option<u64> {
let message = serde_json::from_str::<Value>(raw_json).ok()?;
match message.get("id")? {
Value::Number(number) => number
.as_u64()
.or_else(|| number.as_i64().and_then(|id| u64::try_from(id).ok())),
_ => None,
}
}
fn rewrite_runtime_inspector_command_for_renderer(
raw_json: &str,
command_id_rewrite: Option<(FrontendCommandId, RendererCallId)>,
owner_target_id: Option<&str>,
) -> Result<String, String> {
let mut message = serde_json::from_str::<Value>(raw_json)
.map_err(|error| format!("invalid runtime Inspector command JSON: {error}"))?;
let Some(object) = message.as_object_mut() else {
return Err("runtime Inspector command must be a JSON object".to_owned());
};
if let Some((frontend_command_id, renderer_call_id)) = command_id_rewrite {
let wire_command_id = object.get("id").and_then(Value::as_u64);
if wire_command_id != Some(frontend_command_id.get()) {
return Err(format!(
"runtime Inspector command id mismatch: expected {}, got {}",
frontend_command_id.get(),
object
.get("id")
.map(Value::to_string)
.unwrap_or_else(|| "missing".to_owned())
));
}
object.insert("id".to_owned(), json!(renderer_call_id.get()));
}
let targets_realm_by_unique_id = matches!(
object.get("method").and_then(Value::as_str),
Some("Runtime.evaluate" | "Runtime.callFunctionOn")
);
// External realm ids are target-qualified because native V8 unique ids are
// only unique inside one renderer runtime. V8 Inspector accepts only the
// native suffix, so undo the qualification at the owning renderer boundary.
// An id owned by another target intentionally remains unmodified and V8
// rejects it as an invalid uniqueContextId.
if targets_realm_by_unique_id
&& let Some(owner_target_id) = owner_target_id
&& let Some(Value::String(unique_context_id)) = object
.get_mut("params")
.and_then(Value::as_object_mut)
.and_then(|params| params.get_mut("uniqueContextId"))
{
let owner_prefix = format!("{owner_target_id}:");
if let Some(native_realm_id) = unique_context_id.strip_prefix(&owner_prefix)
&& !native_realm_id.is_empty()
{
*unique_context_id = native_realm_id.to_owned();
}
}
serde_json::to_string(&message)
.map_err(|error| format!("failed to encode runtime Inspector command: {error}"))
}
pub(crate) fn renderer_command_turn_frontend_protocol_response(
output: &RendererCommandTurnOutput,
frontend_command_id: u64,
) -> Option<&Value> {
output.runtime_inspector_output().and_then(|output| {
runtime_inspector_frontend_response(output.messages(), frontend_command_id)
})
}
fn runtime_inspector_frontend_response(
messages: &[RendererRuntimeInspectorMessage],
command_id: u64,
) -> Option<&Value> {
messages.iter().find_map(|message| {
let RendererRuntimeInspectorMessage::Protocol(message) = message else {
return None;
};
(message.get("id").and_then(Value::as_u64) == Some(command_id)).then(|| message.value())
})
}
#[derive(Debug)]
enum RuntimeRemoteObjectOwnerIdentity {
Page {
browser_context_id: String,
target_id: Option<String>,
devtools_session_id: Option<String>,
},
SharedWorker {
browser_context_id: String,
instance_id: SharedWorkerInstanceId,
session_id: String,
},
DedicatedWorker {
browser_context_id: String,
instance_id: u64,
session_id: String,
},
ServiceWorker {
browser_context_id: String,
version_id: u64,
session_id: String,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct SharedWorkerRuntimeTargetRoute {
browser_context_id: String,
worker: WorkerRuntimeTarget,
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum WorkerRuntimeTarget {
Shared(SharedWorkerInstanceId),
Dedicated(u64),
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ServiceWorkerRuntimeTargetRoute {
browser_context_id: String,
version_id: u64,
}
enum BidiChannelListenerRoute {
NotListener,
Consumed,
Event(BackgroundProtocolEvent),
}
#[derive(Debug)]
pub(crate) struct OwnerRuntimeResponse {
command_id: u64,
owner: CommandOwnerScope,
object_group: Option<String>,
message: Value,
bidi_channel_listener: Option<BidiChannelListenerResidence>,
}
#[derive(Debug)]
pub(crate) struct ClaimedPendingInspectorAwait {
command_id: u64,
owner: CommandOwnerScope,
entry: PendingInspectorAwait,
}
#[derive(Debug)]
pub(crate) struct ClaimedPendingInspectorAwaitOwner {
command_id: u64,
owner: CommandOwnerScope,
bidi_channel_object_group: Option<String>,
renderer_correlation: Option<RendererCommandCorrelation>,
}
impl ClaimedPendingInspectorAwaitOwner {
fn from_claimed(claimed: &ClaimedPendingInspectorAwait) -> Self {
Self {
command_id: claimed.command_id,
owner: claimed.owner.clone(),
bidi_channel_object_group: claimed
.entry
.bidi_channel_listener()
.map(|listener| listener.channel_object_group().to_owned()),
renderer_correlation: claimed.entry.renderer_correlation(),
}
}
fn session_id(&self) -> Option<&str> {
self.owner.session_id()
}
fn matches_session_owner(&self, session_id: Option<&str>) -> bool {
self.session_id() == session_id
}
fn matches_owner(&self, owner: &CommandOwnerScope) -> bool {
&self.owner == owner
}
}
impl OwnerRuntimeResponse {
fn from_pending_inspector_await(
command_id: u64,
entry: PendingInspectorAwait,
owner: &CommandOwnerScope,
message: Value,
) -> Self {
Self {
command_id,
owner: owner.clone(),
object_group: entry.object_group().map(str::to_owned),
message,
bidi_channel_listener: entry.bidi_channel_listener().cloned(),
}
}
fn session_id(&self) -> Option<&str> {
self.owner.session_id()
}
fn owner(&self) -> &CommandOwnerScope {
&self.owner
}
fn object_group(&self) -> Option<&str> {
self.object_group.as_deref()
}
fn bidi_channel_listener(&self) -> Option<&BidiChannelListenerResidence> {
self.bidi_channel_listener.as_ref()
}
fn into_protocol_message(self) -> Value {
self.message
}
}
pub struct PendingRuntimeProtocolMessageDispatch {
owner: CommandOwnerScope,
route: RuntimeProtocolMessagePageRoute,
pending: PendingRuntimeProtocolMessageDispatchKind,
response_route: RuntimeProtocolResponseRoute,
}
enum RuntimeProtocolResponseRoute {
// The receiver is consumed before renderer completion is projected. It is
// absent for fire-and-forget commands and for replay, where the original
// dispatch still owns the adapter-reply waiter.
AdapterReply(Option<RuntimeInspectorResponseReceiver>),
SessionSink,
}
impl RuntimeProtocolResponseRoute {
fn for_registered_delivery(
delivery: RendererInspectorResponseDelivery,
receiver: Option<RuntimeInspectorResponseReceiver>,
) -> Self {
match delivery {
RendererInspectorResponseDelivery::AdapterReply => Self::AdapterReply(Some(
receiver.expect("a registered adapter-reply route must allocate its receiver"),
)),
RendererInspectorResponseDelivery::SessionSink => {
assert!(
receiver.is_none(),
"a session-sink response cannot retain an adapter-reply receiver"
);
Self::SessionSink
}
}
}
const fn without_local_receiver_for_delivery(
delivery: RendererInspectorResponseDelivery,
) -> Self {
match delivery {
RendererInspectorResponseDelivery::AdapterReply => Self::AdapterReply(None),
RendererInspectorResponseDelivery::SessionSink => Self::SessionSink,
}
}
const fn adapter_reply_without_receiver() -> Self {
Self::AdapterReply(None)
}
const fn delivery(&self) -> RendererInspectorResponseDelivery {
match self {
Self::AdapterReply(_) => RendererInspectorResponseDelivery::AdapterReply,
Self::SessionSink => RendererInspectorResponseDelivery::SessionSink,
}
}
fn take_adapter_reply_receiver(&mut self) -> Option<RuntimeInspectorResponseReceiver> {
match self {
Self::AdapterReply(receiver) => receiver.take(),
Self::SessionSink => None,
}
}
}
enum PendingRuntimeProtocolMessageDispatchKind {
Page(moli_core::page::PendingPageCommand),
Routable(moli_core::page::PendingRuntimeInspectorCommandDispatch),
}
pub struct PendingSharedWorkerRuntimeProtocolMessageDispatch {
session_id: Option<String>,
pending: SharedWorkerRuntimeProtocolDispatchFuture,
response_route: RuntimeProtocolResponseRoute,
}
pub struct PendingServiceWorkerRuntimeProtocolMessageDispatch {
session_id: Option<String>,
pending: ServiceWorkerRuntimeProtocolDispatchFuture,
response_route: RuntimeProtocolResponseRoute,
}
pub struct PendingMoliDiagnosticsDispatch {
pending: Vec<PendingMoliDiagnosticsPageSnapshot>,
}
struct PendingMoliDiagnosticsPageSnapshot {
browser_context_id: String,
target_id: Option<String>,
pending: moli_core::page::PendingPageCommand,
}
pub struct PendingRuntimeEnableEventsDispatch {
owner: CommandOwnerScope,
route: RuntimeProtocolMessagePageRoute,
pending: moli_core::page::PendingPageCommand,
}
pub struct PendingRuntimeBindingPageCommandDispatch {
owner: CommandOwnerScope,
operation: &'static str,
pending: moli_core::page::PendingPageCommand,
}
pub struct PendingRuntimeChildDefaultContextLookupDispatch {
owner: CommandOwnerScope,
pending: moli_core::page::PendingPageCommand,
}
pub struct CompletedRuntimeProtocolMessageDispatch {
owner: CommandOwnerScope,
route: RuntimeProtocolMessagePageRoute,
completion: moli_core::page::CompletedRuntimeInspectorCommandDispatch,
response_route: RuntimeProtocolResponseRoute,
}
pub struct CompletedSharedWorkerRuntimeProtocolMessageDispatch {
session_id: Option<String>,
dispatch: CompletedWorkerRuntimeProtocolDispatch,
response_route: RuntimeProtocolResponseRoute,
}
pub struct CompletedServiceWorkerRuntimeProtocolMessageDispatch {
session_id: Option<String>,
dispatch: CompletedWorkerRuntimeProtocolDispatch,
response_route: RuntimeProtocolResponseRoute,
}
struct CompletedWorkerRuntimeProtocolDispatch {
messages: Vec<RendererRuntimeInspectorMessage>,
session_response_predecessor: Option<RendererOutputFence>,
session_response_succeeded: Option<bool>,
pending_session_response: Option<moli_core::page::PendingWorkerRuntimeInspectorSessionResponse>,
}
impl CompletedWorkerRuntimeProtocolDispatch {
fn adapter_reply(messages: Vec<RendererRuntimeInspectorMessage>) -> Self {
Self {
messages,
session_response_predecessor: None,
session_response_succeeded: None,
pending_session_response: None,
}
}
fn devtools_session(
completed: moli_core::page::CompletedWorkerRuntimeInspectorCommandDispatch,
) -> Self {
let (messages, pending_session_response) = completed.into_parts();
Self {
messages,
session_response_predecessor: None,
session_response_succeeded: None,
pending_session_response: Some(pending_session_response),
}
}
async fn wait_for_session_response(&mut self) -> Result<(), String> {
let Some(pending) = self.pending_session_response.take() else {
return Ok(());
};
let (predecessor, response_succeeded) = pending.wait().await?;
self.session_response_predecessor = Some(predecessor);
self.session_response_succeeded = Some(response_succeeded);
Ok(())
}
}
pub struct CompletedMoliDiagnosticsDispatch {
completed: Vec<CompletedMoliDiagnosticsPageSnapshot>,
}
struct CompletedMoliDiagnosticsPageSnapshot {
browser_context_id: String,
target_id: Option<String>,
completion: Result<moli_core::page::CompletedPageCommand, String>,
}
pub struct CompletedRuntimeEnableEventsDispatch {
owner: CommandOwnerScope,
route: RuntimeProtocolMessagePageRoute,
completion: moli_core::page::CompletedPageCommand,
}
pub(crate) struct RuntimeEnableEventsReplay {
events: Vec<RuntimeEnableReplayEvent>,
}
pub(crate) enum RuntimeEnableReplayEvent {
Context(RuntimeContextProtocolEvent),
Background(BackgroundProtocolEvent),
}
impl RuntimeEnableEventsReplay {
fn from_renderer_messages(messages: Vec<RendererRuntimeInspectorMessage>) -> Self {
Self {
events: messages
.into_iter()
.map(RuntimeEnableReplayEvent::from_renderer_message)
.collect(),
}
}
pub(crate) fn into_events(self) -> Vec<RuntimeEnableReplayEvent> {
self.events
}
fn events_mut(&mut self) -> &mut [RuntimeEnableReplayEvent] {
&mut self.events
}
}
impl RuntimeEnableReplayEvent {
fn from_renderer_message(message: RendererRuntimeInspectorMessage) -> Self {
match message {
RendererRuntimeInspectorMessage::RuntimeContext(event) => {
Self::Context(RuntimeContextProtocolEvent::from_restore_event(event))
}
RendererRuntimeInspectorMessage::Protocol(message) => {
Self::Background(protocol_message_background_event(message.into_value()))
}
}
}
}
pub struct CompletedRuntimeBindingPageCommandDispatch {
owner: CommandOwnerScope,
operation: &'static str,
completion: moli_core::page::CompletedPageCommand,
}
pub struct CompletedRuntimeChildDefaultContextLookupDispatch {
owner: CommandOwnerScope,
completion: moli_core::page::CompletedPageCommand,
}
type SharedWorkerRuntimeProtocolDispatchFuture =
Pin<Box<dyn Future<Output = Result<CompletedWorkerRuntimeProtocolDispatch, String>>>>;
type ServiceWorkerRuntimeProtocolDispatchFuture =
Pin<Box<dyn Future<Output = Result<CompletedWorkerRuntimeProtocolDispatch, String>>>>;
#[derive(Clone, Debug)]
struct RuntimeProtocolMessagePageRoute {
browser_context_id: String,
target_id: Option<String>,
renderer_agent_attachment_id: RendererAgentAttachmentId,
}
fn collect_moli_diagnostics_pending_snapshots(
browser_context: &mut BrowserContext,
pending: &mut Vec<PendingMoliDiagnosticsPageSnapshot>,
) -> Result<(), String> {
if browser_context
.active_page_target()
.runtime_slot
.has_loaded_page()
{
let pending_snapshot = browser_context
.active_page_target()
.runtime_slot
.loaded_page()
.expect("active target loaded page should exist")
.start_page_diagnostics_snapshot()
.map_err(|error| error.to_string())?;
pending.push(PendingMoliDiagnosticsPageSnapshot {
browser_context_id: browser_context.id.clone(),
target_id: None,
pending: pending_snapshot,
});
}
let browser_context_id = browser_context.id.clone();
for target in browser_context.background_targets_mut() {
if !target.has_loaded_page() {
continue;
}
let pending_snapshot = target
.loaded_page()
.expect("background target loaded page should exist")
.start_page_diagnostics_snapshot()
.map_err(|error| error.to_string())?;
pending.push(PendingMoliDiagnosticsPageSnapshot {
browser_context_id: browser_context_id.clone(),
target_id: Some(target.target_id().to_owned()),
pending: pending_snapshot,
});
}
Ok(())
}
impl PendingRuntimeProtocolMessageDispatch {
pub async fn wait(self) -> Result<CompletedRuntimeProtocolMessageDispatch, String> {
let completion = match self.pending {
PendingRuntimeProtocolMessageDispatchKind::Page(pending) => {
moli_core::page::CompletedRuntimeInspectorCommandDispatch::Owner(Box::new(
pending
.wait()
.await
.map_err(|error| format!("runtime inspector dispatch failed: {error}"))?,
))
}
PendingRuntimeProtocolMessageDispatchKind::Routable(pending) => pending
.wait()
.await
.map_err(|error| format!("runtime inspector dispatch failed: {error}"))?,
};
Ok(CompletedRuntimeProtocolMessageDispatch {
owner: self.owner,
route: self.route,
completion,
response_route: self.response_route,
})
}
}
impl PendingSharedWorkerRuntimeProtocolMessageDispatch {
pub async fn wait(self) -> Result<CompletedSharedWorkerRuntimeProtocolMessageDispatch, String> {
let dispatch = self.pending.await?;
Ok(CompletedSharedWorkerRuntimeProtocolMessageDispatch {
session_id: self.session_id,
dispatch,
response_route: self.response_route,
})
}
}
impl PendingServiceWorkerRuntimeProtocolMessageDispatch {
pub async fn wait(
self,
) -> Result<CompletedServiceWorkerRuntimeProtocolMessageDispatch, String> {
let dispatch = self.pending.await?;
Ok(CompletedServiceWorkerRuntimeProtocolMessageDispatch {
session_id: self.session_id,
dispatch,
response_route: self.response_route,
})
}
}
impl CompletedRuntimeProtocolMessageDispatch {
pub(crate) fn owner(&self) -> &CommandOwnerScope {
&self.owner
}
pub(crate) fn session_response_succeeded(&self) -> Option<bool> {
match &self.completion {
moli_core::page::CompletedRuntimeInspectorCommandDispatch::OwnerSessionResponse {
response_succeeded,
..
}
| moli_core::page::CompletedRuntimeInspectorCommandDispatch::InspectorSessionResponse {
response_succeeded,
..
} => Some(*response_succeeded),
moli_core::page::CompletedRuntimeInspectorCommandDispatch::OwnerSessionErrorSettled(
_,
) => Some(false),
moli_core::page::CompletedRuntimeInspectorCommandDispatch::Owner(_)
| moli_core::page::CompletedRuntimeInspectorCommandDispatch::Inspector => None,
}
}
pub(crate) fn session_response_predecessor(&self) -> Option<RendererOutputFence> {
match &self.completion {
moli_core::page::CompletedRuntimeInspectorCommandDispatch::OwnerSessionResponse {
completion,
..
} => completion.renderer_output_predecessor(),
moli_core::page::CompletedRuntimeInspectorCommandDispatch::OwnerSessionErrorSettled(
predecessor,
) => Some(predecessor.clone()),
moli_core::page::CompletedRuntimeInspectorCommandDispatch::InspectorSessionResponse {
predecessor,
..
} => Some(predecessor.clone()),
moli_core::page::CompletedRuntimeInspectorCommandDispatch::Owner(_)
| moli_core::page::CompletedRuntimeInspectorCommandDispatch::Inspector => None,
}
}
pub(crate) fn take_deferred_response_receiver(
&mut self,
) -> Option<RuntimeInspectorResponseReceiver> {
self.response_route.take_adapter_reply_receiver()
}
pub(crate) const fn response_delivery(&self) -> RendererInspectorResponseDelivery {
self.response_route.delivery()
}
}
impl CompletedSharedWorkerRuntimeProtocolMessageDispatch {
pub(crate) async fn wait_for_session_response(&mut self) -> Result<(), String> {
self.dispatch.wait_for_session_response().await
}
pub(crate) fn take_deferred_response_receiver(
&mut self,
) -> Option<RuntimeInspectorResponseReceiver> {
self.response_route.take_adapter_reply_receiver()
}
pub(crate) const fn response_delivery(&self) -> RendererInspectorResponseDelivery {
self.response_route.delivery()
}
pub(crate) fn session_response_predecessor(&self) -> Option<RendererOutputFence> {
self.dispatch.session_response_predecessor.clone()
}
pub(crate) fn session_response_succeeded(&self) -> Option<bool> {
self.dispatch.session_response_succeeded
}
}
impl CompletedServiceWorkerRuntimeProtocolMessageDispatch {
pub(crate) async fn wait_for_session_response(&mut self) -> Result<(), String> {
self.dispatch.wait_for_session_response().await
}
pub(crate) fn take_deferred_response_receiver(
&mut self,
) -> Option<RuntimeInspectorResponseReceiver> {
self.response_route.take_adapter_reply_receiver()
}
pub(crate) const fn response_delivery(&self) -> RendererInspectorResponseDelivery {
self.response_route.delivery()
}
pub(crate) fn session_response_predecessor(&self) -> Option<RendererOutputFence> {
self.dispatch.session_response_predecessor.clone()
}
pub(crate) fn session_response_succeeded(&self) -> Option<bool> {
self.dispatch.session_response_succeeded
}
}
impl PendingMoliDiagnosticsDispatch {
pub async fn wait(self) -> Result<CompletedMoliDiagnosticsDispatch, String> {
let mut completed = Vec::with_capacity(self.pending.len());
for pending in self.pending {
completed.push(CompletedMoliDiagnosticsPageSnapshot {
browser_context_id: pending.browser_context_id,
target_id: pending.target_id,
completion: pending
.pending
.wait()
.await
.map_err(|error| format!("moli diagnostics snapshot failed: {error}")),
});
}
Ok(CompletedMoliDiagnosticsDispatch { completed })
}
}
impl PendingRuntimeEnableEventsDispatch {
pub async fn wait(self) -> Result<CompletedRuntimeEnableEventsDispatch, String> {
let completion = self
.pending
.wait()
.await
.map_err(|error| format!("runtime enable event replay failed: {error}"))?;
Ok(CompletedRuntimeEnableEventsDispatch {
owner: self.owner,
route: self.route,
completion,
})
}
}
impl PendingRuntimeBindingPageCommandDispatch {
pub async fn wait(self) -> Result<CompletedRuntimeBindingPageCommandDispatch, String> {
let completion = self
.pending
.wait()
.await
.map_err(|error| format!("{} failed: {error}", self.operation))?;
Ok(CompletedRuntimeBindingPageCommandDispatch {
owner: self.owner,
operation: self.operation,
completion,
})
}
}
impl PendingRuntimeChildDefaultContextLookupDispatch {
pub async fn wait(self) -> Result<CompletedRuntimeChildDefaultContextLookupDispatch, String> {
let completion = self
.pending
.wait()
.await
.map_err(|error| format!("runtime child default context lookup failed: {error}"))?;
Ok(CompletedRuntimeChildDefaultContextLookupDispatch {
owner: self.owner,
completion,
})
}
}
fn push_pending_inspector_await_error_background_event(
out: &mut Vec<BackgroundProtocolEvent>,
cdp_id: u64,
session_id: Option<&str>,
reason: &'static str,
) {
out.push(BackgroundProtocolEvent::command_error(
Some(cdp_id),
session_id,
-32000,
reason.to_owned(),
None,
));
}
fn push_terminated_renderer_call_error_background_events(
out: &mut Vec<BackgroundProtocolEvent>,
terminated: Vec<RendererCommandCorrelation>,
session_id: Option<&str>,
reason: &'static str,
) {
out.extend(terminated.into_iter().map(|correlation| {
BackgroundProtocolEvent::command_error(
Some(correlation.frontend_command_id().get()),
session_id,
-32000,
reason.to_owned(),
None,
)
}));
}
fn bidi_channel_listener_call_function_json(
command_id: u64,
listener: &PendingBidiChannelListener,
) -> String {
let serialization_options = listener
.properties()
.serialization_options
.as_ref()
.map(crate::domains::runtime::devtools_deep_serialization_options_json)
.unwrap_or_else(|| {
json!({
"serialization": "deep",
})
});
let params = json!({
"functionDeclaration": "(async function() { return await this.getMessage(); })",
"objectId": listener.channel_handle().as_str(),
"awaitPromise": true,
"returnByValue": !matches!(listener.properties().ownership, DevToolsResultOwnership::Root),
"objectGroup": BIDI_SCRIPT_RESULT_OBJECT_GROUP,
"serializationOptions": serialization_options,
});
json!({
"id": command_id,
"method": "Runtime.callFunctionOn",
"params": params,
})
.to_string()
}
impl CdpConnection {
/// Registers a CDP request as awaiting a deferred V8 inspector reply.
/// Used by `Runtime.evaluate`/`Runtime.callFunctionOn` when `awaitPromise=true`
/// is dispatched directly to V8 inspector. V8 calls back after the promise
/// settles, and the reply is routed via [`Self::route_inspector_messages_into`].
#[cfg(test)]
pub(crate) fn register_pending_inspector_await(
&mut self,
cdp_request_id: u64,
session_id: Option<&str>,
) {
let owner = CommandOwnerScope::capture(self, session_id);
self.try_register_pending_inspector_await_with_object_group_for_owner(
cdp_request_id,
&owner,
None,
)
.expect("pending Inspector await frontend command id must be unique per session");
}
pub(crate) fn try_register_pending_inspector_await_with_object_group_for_owner(
&mut self,
cdp_request_id: u64,
owner: &CommandOwnerScope,
object_group: Option<&str>,
) -> Result<(), DuplicatePendingRendererCommand> {
let session_id = owner.session_id();
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
return target.try_register_pending_inspector_await(
owner_session_id,
cdp_request_id,
session_id,
object_group,
);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
return target.try_register_pending_inspector_await(
owner_session_id,
cdp_request_id,
session_id,
object_group,
);
}
self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.try_register_pending_inspector_await(cdp_request_id, session_id, object_group)
})
.unwrap_or(Ok(()))
}
pub(crate) fn try_register_renderer_call_for_session_owner(
&mut self,
session_id: Option<&str>,
cdp_request_id: u64,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
descriptor: RendererCommandDescriptor,
) -> Result<PreparedRendererCallDispatch, String> {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
return target
.try_register_renderer_call(
owner_session_id,
cdp_request_id,
dispatched_attachment_id,
descriptor,
)
.ok_or_else(|| "UnknownSession".to_owned())?
.map_err(|error| error.to_string());
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
return target
.try_register_renderer_call(
owner_session_id,
cdp_request_id,
dispatched_attachment_id,
descriptor,
)
.ok_or_else(|| "UnknownSession".to_owned())?
.map_err(|error| error.to_string());
}
self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.try_register_renderer_call(cdp_request_id, dispatched_attachment_id, descriptor)
})
.ok_or_else(|| "UnknownSession".to_owned())?
.map_err(|error| error.to_string())
}
fn try_register_renderer_call_for_owner(
&mut self,
owner: &CommandOwnerScope,
cdp_request_id: u64,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
descriptor: RendererCommandDescriptor,
) -> Result<PreparedRendererCallDispatch, String> {
if owner.session_id().is_some() {
return self.try_register_renderer_call_for_session_owner(
owner.session_id(),
cdp_request_id,
dispatched_attachment_id,
descriptor,
);
}
self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.try_register_renderer_call(cdp_request_id, dispatched_attachment_id, descriptor)
})
.ok_or_else(|| "UnknownSession".to_owned())?
.map_err(|error| error.to_string())
}
pub(crate) fn take_renderer_call_for_frontend_for_session_owner(
&mut self,
session_id: Option<&str>,
cdp_request_id: u64,
) -> Option<RendererCommandCorrelation> {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
return target.take_renderer_call_for_frontend(owner_session_id, cdp_request_id);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
return target.take_renderer_call_for_frontend(owner_session_id, cdp_request_id);
}
self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.take_renderer_call_for_frontend(cdp_request_id)
})
.flatten()
}
pub(crate) fn take_renderer_call_for_frontend_for_owner(
&mut self,
owner: &CommandOwnerScope,
cdp_request_id: u64,
) -> Option<RendererCommandCorrelation> {
if owner.session_id().is_some() {
return self.take_renderer_call_for_frontend_for_session_owner(
owner.session_id(),
cdp_request_id,
);
}
self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.take_renderer_call_for_frontend(cdp_request_id)
})
.flatten()
}
fn renderer_call_for_frontend_for_session_owner(
&self,
session_id: Option<&str>,
cdp_request_id: u64,
) -> Option<RendererCommandCorrelation> {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session(session_id)
{
return target.renderer_call_for_frontend(owner_session_id, cdp_request_id);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session(session_id)
{
return target.renderer_call_for_frontend(owner_session_id, cdp_request_id);
}
self.target_devtools_session_state_for_session(session_id)?
.renderer_call_for_frontend(cdp_request_id)
}
fn renderer_command_descriptor_for_renderer_if_attachment_matches_for_session_owner(
&self,
session_id: Option<&str>,
renderer_call_id: RendererCallId,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
) -> Option<RendererCommandDescriptor> {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session(session_id)
{
return target.renderer_command_descriptor_for_renderer_if_attachment_matches(
owner_session_id,
renderer_call_id,
dispatched_attachment_id,
);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session(session_id)
{
return target.renderer_command_descriptor_for_renderer_if_attachment_matches(
owner_session_id,
renderer_call_id,
dispatched_attachment_id,
);
}
self.target_devtools_session_state_for_session(session_id)?
.renderer_command_descriptor_for_renderer_if_attachment_matches(
renderer_call_id,
dispatched_attachment_id,
)
}
fn renderer_command_descriptor_for_renderer_if_attachment_matches_for_owner(
&self,
owner: &CommandOwnerScope,
renderer_call_id: RendererCallId,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
) -> Option<RendererCommandDescriptor> {
if owner.session_id().is_some() {
return self
.renderer_command_descriptor_for_renderer_if_attachment_matches_for_session_owner(
owner.session_id(),
renderer_call_id,
dispatched_attachment_id,
);
}
self.target_devtools_session_state_for_owner(owner)?
.renderer_command_descriptor_for_renderer_if_attachment_matches(
renderer_call_id,
dispatched_attachment_id,
)
}
pub(crate) fn renderer_runtime_command_cause_for_frontend(
&self,
session_id: Option<&str>,
cdp_request_id: u64,
) -> Option<RendererRuntimeCommandCausalIdentity> {
let owner = CommandOwnerScope::capture(self, session_id);
self.renderer_runtime_command_cause_for_owner(&owner, cdp_request_id)
}
pub(crate) fn renderer_runtime_command_cause_for_owner(
&self,
owner: &CommandOwnerScope,
cdp_request_id: u64,
) -> Option<RendererRuntimeCommandCausalIdentity> {
let correlation = if owner.session_id().is_some() {
self.renderer_call_for_frontend_for_session_owner(owner.session_id(), cdp_request_id)
} else {
self.target_devtools_session_state_for_owner(owner)?
.renderer_call_for_frontend(cdp_request_id)
}?;
Some(RendererRuntimeCommandCausalIdentity::new(
self.target_renderer_runtime_inspector_session_id_for_owner(owner),
correlation.renderer_call_id().get(),
))
}
fn take_renderer_call_for_frontend_if_matches_for_session_owner(
&mut self,
session_id: Option<&str>,
cdp_request_id: u64,
renderer_call_id: RendererCallId,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
) -> Option<RendererCommandCorrelation> {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
return target.take_renderer_call_for_frontend_if_matches(
owner_session_id,
cdp_request_id,
renderer_call_id,
dispatched_attachment_id,
);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
return target.take_renderer_call_for_frontend_if_matches(
owner_session_id,
cdp_request_id,
renderer_call_id,
dispatched_attachment_id,
);
}
self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.take_renderer_call_for_frontend_if_matches(
cdp_request_id,
renderer_call_id,
dispatched_attachment_id,
)
})
.flatten()
}
fn take_renderer_call_for_frontend_if_matches_for_owner(
&mut self,
owner: &CommandOwnerScope,
cdp_request_id: u64,
renderer_call_id: RendererCallId,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
) -> Option<RendererCommandCorrelation> {
if owner.session_id().is_some() {
return self.take_renderer_call_for_frontend_if_matches_for_session_owner(
owner.session_id(),
cdp_request_id,
renderer_call_id,
dispatched_attachment_id,
);
}
self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.take_renderer_call_for_frontend_if_matches(
cdp_request_id,
renderer_call_id,
dispatched_attachment_id,
)
})
.flatten()
}
pub(crate) fn take_renderer_call_if_correlation_matches_for_session_owner(
&mut self,
session_id: Option<&str>,
correlation: RendererCommandCorrelation,
) -> bool {
self.take_renderer_call_for_frontend_if_matches_for_session_owner(
session_id,
correlation.frontend_command_id().get(),
correlation.renderer_call_id(),
correlation.dispatched_attachment_id(),
) == Some(correlation)
}
pub(crate) fn take_renderer_call_if_correlation_matches_for_owner(
&mut self,
owner: &CommandOwnerScope,
correlation: RendererCommandCorrelation,
) -> bool {
if owner.session_id().is_some() {
return self.take_renderer_call_if_correlation_matches_for_session_owner(
owner.session_id(),
correlation,
);
}
self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.take_renderer_call_for_frontend_if_matches(
correlation.frontend_command_id().get(),
correlation.renderer_call_id(),
correlation.dispatched_attachment_id(),
)
})
.flatten()
== Some(correlation)
}
fn take_frontend_command_for_renderer_if_attachment_matches_for_session_owner(
&mut self,
session_id: Option<&str>,
renderer_call_id: RendererCallId,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
) -> Option<RendererCommandCorrelation> {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
return target.take_frontend_command_for_renderer_if_attachment_matches(
owner_session_id,
renderer_call_id,
dispatched_attachment_id,
);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
return target.take_frontend_command_for_renderer_if_attachment_matches(
owner_session_id,
renderer_call_id,
dispatched_attachment_id,
);
}
self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.take_frontend_command_for_renderer_if_attachment_matches(
renderer_call_id,
dispatched_attachment_id,
)
})
.flatten()
}
fn take_frontend_command_for_renderer_if_attachment_matches_for_owner(
&mut self,
owner: &CommandOwnerScope,
renderer_call_id: RendererCallId,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
) -> Option<RendererCommandCorrelation> {
if owner.session_id().is_some() {
return self
.take_frontend_command_for_renderer_if_attachment_matches_for_session_owner(
owner.session_id(),
renderer_call_id,
dispatched_attachment_id,
);
}
self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.take_frontend_command_for_renderer_if_attachment_matches(
renderer_call_id,
dispatched_attachment_id,
)
})
.flatten()
}
fn prepare_renderer_call_for_session_owner(
&mut self,
session_id: Option<&str>,
descriptor: RendererCommandDescriptor,
cdp_request_id: u64,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
) -> Result<
(
RendererCommandCorrelation,
String,
RendererRuntimeInspectorResponseSender,
RuntimeProtocolResponseRoute,
),
String,
> {
let raw_json = descriptor.frontend_payload().to_owned();
let response_delivery = descriptor.response_delivery();
let prepared = self.try_register_renderer_call_for_session_owner(
session_id,
cdp_request_id,
dispatched_attachment_id,
descriptor,
)?;
let correlation = prepared.correlation();
match self.rewrite_runtime_inspector_command_for_session_owner(
session_id,
&raw_json,
Some((
FrontendCommandId::new(cdp_request_id),
correlation.renderer_call_id(),
)),
) {
Ok(raw_json) => {
let (correlation, response_sender, response_receiver) = prepared.into_parts();
Ok((
correlation,
raw_json,
response_sender,
RuntimeProtocolResponseRoute::for_registered_delivery(
response_delivery,
response_receiver,
),
))
}
Err(error) => {
let removed = self
.take_renderer_call_for_frontend_for_session_owner(session_id, cdp_request_id);
debug_assert_eq!(removed, Some(correlation));
Err(error)
}
}
}
fn prepare_renderer_call_for_owner(
&mut self,
owner: &CommandOwnerScope,
descriptor: RendererCommandDescriptor,
cdp_request_id: u64,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
) -> Result<
(
RendererCommandCorrelation,
String,
RendererRuntimeInspectorResponseSender,
RuntimeProtocolResponseRoute,
),
String,
> {
let raw_json = descriptor.frontend_payload().to_owned();
let response_delivery = descriptor.response_delivery();
let prepared = self.try_register_renderer_call_for_owner(
owner,
cdp_request_id,
dispatched_attachment_id,
descriptor,
)?;
let correlation = prepared.correlation();
match self.rewrite_runtime_inspector_command_for_owner(
owner,
&raw_json,
Some((
FrontendCommandId::new(cdp_request_id),
correlation.renderer_call_id(),
)),
) {
Ok(raw_json) => {
let (correlation, response_sender, response_receiver) = prepared.into_parts();
Ok((
correlation,
raw_json,
response_sender,
RuntimeProtocolResponseRoute::for_registered_delivery(
response_delivery,
response_receiver,
),
))
}
Err(error) => {
let removed = self.take_renderer_call_for_frontend_for_owner(owner, cdp_request_id);
debug_assert_eq!(removed, Some(correlation));
Err(error)
}
}
}
fn rewrite_runtime_inspector_command_for_session_owner(
&self,
session_id: Option<&str>,
raw_json: &str,
command_id_rewrite: Option<(FrontendCommandId, RendererCallId)>,
) -> Result<String, String> {
let owner_target_id = self
.runtime_context_owner_identity_for_session(session_id)
.and_then(|(_, target_id)| target_id);
rewrite_runtime_inspector_command_for_renderer(
raw_json,
command_id_rewrite,
owner_target_id.as_deref(),
)
}
fn rewrite_runtime_inspector_command_for_owner(
&self,
owner: &CommandOwnerScope,
raw_json: &str,
command_id_rewrite: Option<(FrontendCommandId, RendererCallId)>,
) -> Result<String, String> {
if owner.session_id().is_some() {
return self.rewrite_runtime_inspector_command_for_session_owner(
owner.session_id(),
raw_json,
command_id_rewrite,
);
}
let owner_target_id = self
.target_owner_identity_for_owner(owner)
.and_then(|(_, target_id)| target_id);
rewrite_runtime_inspector_command_for_renderer(
raw_json,
command_id_rewrite,
owner_target_id.as_deref(),
)
}
pub(crate) fn register_runtime_await_job_for_owner(
&mut self,
cdp_request_id: u64,
owner: &CommandOwnerScope,
object_group: Option<&str>,
action: &'static str,
) {
let session_id = owner.session_id();
let job = RuntimeAwaitJob::new(cdp_request_id, owner, object_group, action);
let trace_fields = job.trace_fields();
let key = PendingRendererCommandKey::new(session_id, cdp_request_id);
match self.pending_runtime_await_jobs.entry(key) {
Entry::Vacant(entry) => {
entry.insert(job);
}
Entry::Occupied(_) => {
tracing::error!(
cdp_request_id,
session_id,
"runtime await trace job already exists for frontend command"
);
return;
}
}
self.record_runtime_await_trace(
"runtime_await_job_start",
Some(cdp_request_id),
session_id,
trace_fields,
);
}
pub(crate) fn trace_runtime_await_pending_registered(
&mut self,
cdp_request_id: u64,
session_id: Option<&str>,
) {
self.record_runtime_await_trace(
"runtime_await_pending_registered",
Some(cdp_request_id),
session_id,
self.runtime_await_job_trace_fields(cdp_request_id, session_id),
);
}
pub(crate) fn trace_runtime_await_initial_dispatch_done(
&mut self,
cdp_request_id: Option<u64>,
session_id: Option<&str>,
messages: usize,
saw_current_response: bool,
) {
self.record_runtime_await_trace(
"runtime_await_initial_dispatch_done",
cdp_request_id,
session_id,
json!({
"messages": messages,
"matchingResponseSeen": saw_current_response,
"job": cdp_request_id
.map(|id| self.runtime_await_job_trace_fields(id, session_id)),
}),
);
}
pub(crate) fn complete_runtime_await_job(
&mut self,
cdp_request_id: u64,
session_id: Option<&str>,
) {
let key = PendingRendererCommandKey::new(session_id, cdp_request_id);
let Some(job) = self.pending_runtime_await_jobs.remove(&key) else {
return;
};
let session_id = job.session_id();
let fields = job.trace_fields();
self.record_runtime_await_trace(
"runtime_await_completed",
Some(cdp_request_id),
session_id.as_deref(),
fields,
);
}
pub(crate) fn cancel_runtime_await_job(
&mut self,
cdp_request_id: u64,
session_id: Option<&str>,
reason: &'static str,
) {
let key = PendingRendererCommandKey::new(session_id, cdp_request_id);
let Some(job) = self.pending_runtime_await_jobs.remove(&key) else {
return;
};
let session_id = job.session_id();
let mut fields = job.trace_fields();
if let Some(object) = fields.as_object_mut() {
object.insert("reason".to_owned(), json!(reason));
}
self.record_runtime_await_trace(
"runtime_await_cancelled",
Some(cdp_request_id),
session_id.as_deref(),
fields,
);
}
fn runtime_await_job_trace_fields(
&self,
cdp_request_id: u64,
session_id: Option<&str>,
) -> Value {
let key = PendingRendererCommandKey::new(session_id, cdp_request_id);
self.pending_runtime_await_jobs
.get(&key)
.map(RuntimeAwaitJob::trace_fields)
.unwrap_or_else(|| json!({}))
}
pub(crate) fn runtime_await_owner_route_for_session(
&self,
session_id: Option<&str>,
) -> Option<CdpSessionRoute> {
if let Some(route) = self.session_route(session_id) {
return Some(route);
}
self.target_owner_identity_for_session(session_id).and_then(
|(browser_context_id, target_id)| {
target_id.map(|target_id| CdpSessionRoute::PageTarget {
browser_context_id,
target_id,
session_key: moli_page_types::DevToolsSessionKey::Primary,
})
},
)
}
pub(crate) fn next_internal_runtime_command_id(&mut self) -> u64 {
let id = self.next_internal_runtime_command_id;
self.next_internal_runtime_command_id = self
.next_internal_runtime_command_id
.checked_add(1)
.expect("internal Runtime command id space exhausted");
id
}
pub(crate) fn next_bidi_channel_object_group(&mut self) -> String {
format!(
"{BIDI_CHANNEL_OBJECT_GROUP_PREFIX}{}",
self.next_internal_runtime_command_id()
)
}
#[cfg(test)]
pub(crate) fn register_pending_bidi_channel_listener(
&mut self,
cdp_request_id: u64,
session_id: Option<&str>,
listener: BidiChannelListenerResidence,
) {
assert_eq!(
listener.owner().session_id(),
session_id,
"BiDi listener residence must be registered under its exact Page attachment"
);
let _ = self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.register_pending_bidi_channel_listener(
cdp_request_id,
session_id,
Some(BIDI_SCRIPT_RESULT_OBJECT_GROUP),
listener,
);
});
}
fn register_pending_bidi_channel_listener_for_owner(
&mut self,
cdp_request_id: u64,
owner: &CommandOwnerScope,
listener: BidiChannelListenerResidence,
) {
assert_eq!(
listener.owner().command_owner(),
owner,
"BiDi listener residence must be registered under its exact Page owner"
);
let _ = self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.register_pending_bidi_channel_listener(
cdp_request_id,
owner.session_id(),
Some(BIDI_SCRIPT_RESULT_OBJECT_GROUP),
listener,
);
});
}
pub(crate) fn publish_bidi_channel_listener_start(
&mut self,
listener: BidiChannelListenerResidence,
) {
self.publish_bidi_channel_owner_action(BidiChannelOwnerAction::start_listener(listener));
}
fn publish_bidi_channel_object_group_release(
&mut self,
owner: BidiChannelPageOwner,
object_group: impl Into<String>,
) {
self.publish_bidi_channel_owner_action(BidiChannelOwnerAction::release_object_group(
owner,
object_group,
));
}
fn publish_bidi_channel_owner_action(&mut self, action: BidiChannelOwnerAction) {
let publish_sequence = self
.scheduler_state
.allocate_protocol_work_publish_sequence();
let work = crate::domains::activity::ProtocolSchedulerWork::bidi_channel_owner_action(
publish_sequence,
action,
);
self.scheduler_state
.push_scheduler_event(CdpSchedulerEvent::ProtocolWorkPublished { work });
}
pub(crate) fn forget_pending_inspector_await(
&mut self,
cdp_request_id: u64,
session_id: Option<&str>,
) {
self.cancel_runtime_await_job(cdp_request_id, session_id, "forgotten");
let _ = self.remove_pending_inspector_await_for_cancellation(cdp_request_id, session_id);
}
pub(crate) fn forget_pending_inspector_await_for_owner(
&mut self,
cdp_request_id: u64,
owner: &CommandOwnerScope,
) {
self.cancel_runtime_await_job(cdp_request_id, owner.session_id(), "forgotten");
let _ =
self.remove_pending_inspector_await_for_cancellation_for_owner(cdp_request_id, owner);
}
pub(crate) fn claim_pending_inspector_await_for_scheduler_deferred_reply(
&mut self,
cdp_request_id: u64,
owner: &CommandOwnerScope,
) -> Option<ClaimedPendingInspectorAwait> {
let claimed = self
.remove_pending_inspector_await_for_owner(cdp_request_id, owner)
.map(|entry| ClaimedPendingInspectorAwait {
command_id: cdp_request_id,
owner: owner.clone(),
entry,
})?;
let key = PendingRendererCommandKey::new(owner.session_id(), cdp_request_id);
match self.claimed_pending_inspector_await_owners.entry(key) {
Entry::Vacant(entry) => {
entry.insert(ClaimedPendingInspectorAwaitOwner::from_claimed(&claimed));
}
Entry::Occupied(_) => {
panic!("claimed pending Inspector await owner must be unique per session");
}
}
Some(claimed)
}
fn remove_claimed_pending_inspector_await_owner(
&mut self,
cdp_request_id: u64,
session_id: Option<&str>,
) -> Option<ClaimedPendingInspectorAwaitOwner> {
let key = PendingRendererCommandKey::new(session_id, cdp_request_id);
self.claimed_pending_inspector_await_owners.remove(&key)
}
fn drain_claimed_pending_inspector_await_owners_for_session(
&mut self,
session_id: Option<&str>,
) -> Vec<ClaimedPendingInspectorAwaitOwner> {
let to_remove = self
.claimed_pending_inspector_await_owners
.iter()
.filter_map(|(key, owner)| {
owner
.matches_session_owner(session_id)
.then_some(key.clone())
})
.collect::<Vec<_>>();
to_remove
.into_iter()
.filter_map(|key| self.claimed_pending_inspector_await_owners.remove(&key))
.collect()
}
fn drain_claimed_pending_inspector_await_owners_for_owner(
&mut self,
owner: &CommandOwnerScope,
) -> Vec<ClaimedPendingInspectorAwaitOwner> {
let to_remove = self
.claimed_pending_inspector_await_owners
.iter()
.filter_map(|(key, claimed)| claimed.matches_owner(owner).then_some(key.clone()))
.collect::<Vec<_>>();
to_remove
.into_iter()
.filter_map(|key| self.claimed_pending_inspector_await_owners.remove(&key))
.collect()
}
fn push_claimed_pending_inspector_await_owner_errors(
&mut self,
background_events: &mut Vec<BackgroundProtocolEvent>,
owners: Vec<ClaimedPendingInspectorAwaitOwner>,
reason: &'static str,
) {
for owner in owners {
self.cancel_runtime_await_job(owner.command_id, owner.session_id(), reason);
if let Some(correlation) = owner.renderer_correlation {
let _ = self.take_renderer_call_for_frontend_if_matches_for_owner(
&owner.owner,
correlation.frontend_command_id().get(),
correlation.renderer_call_id(),
correlation.dispatched_attachment_id(),
);
}
if let Some(object_group) = owner.bidi_channel_object_group.as_deref() {
self.unregister_runtime_remote_object_group_for_owner(&owner.owner, object_group);
continue;
}
let mut response = RuntimeInspectorResponseReady::for_owner(
owner.command_id,
&owner.owner,
Err(reason.to_owned()),
);
if let Some(correlation) = owner.renderer_correlation {
response.bind_renderer_call_id(correlation.renderer_call_id());
}
background_events.push(BackgroundProtocolEvent::runtime_inspector_response_ready(
response,
));
}
}
#[cfg(test)]
pub(crate) fn has_claimed_pending_inspector_awaits_for_session_owner(
&self,
session_id: Option<&str>,
) -> bool {
self.claimed_pending_inspector_await_owners
.values()
.any(|owner| owner.matches_session_owner(session_id))
}
#[cfg(test)]
pub(crate) fn has_unclaimed_pending_inspector_awaits_for_session_owner(
&self,
session_id: Option<&str>,
) -> bool {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session(session_id)
{
return target.has_pending_inspector_awaits_for_session(owner_session_id);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session(session_id)
{
return target.has_pending_inspector_awaits_for_session(owner_session_id);
}
self.target_devtools_session_state_for_session(session_id)
.is_some_and(DevToolsSessionState::has_pending_inspector_awaits)
}
pub(crate) fn complete_claimed_pending_inspector_await_for_scheduler_deferred_reply(
&mut self,
claimed: Option<ClaimedPendingInspectorAwait>,
protocol_events: &[BackgroundProtocolEvent],
) {
let Some(claimed) = claimed else {
return;
};
let ClaimedPendingInspectorAwait {
command_id,
owner,
entry,
} = claimed;
let session_id = owner.session_id().map(str::to_owned);
self.remove_claimed_pending_inspector_await_owner(command_id, session_id.as_deref());
self.complete_runtime_await_job(command_id, session_id.as_deref());
self.apply_completed_pending_inspector_await_entry(&owner, entry, protocol_events);
}
pub(crate) fn cancel_claimed_pending_inspector_await_for_scheduler_deferred_reply(
&mut self,
claimed: Option<ClaimedPendingInspectorAwait>,
reason: &'static str,
) {
let Some(claimed) = claimed else {
return;
};
let ClaimedPendingInspectorAwait {
command_id,
owner,
entry,
} = claimed;
let session_id = owner.session_id().map(str::to_owned);
self.remove_claimed_pending_inspector_await_owner(command_id, session_id.as_deref());
self.cancel_runtime_await_job(command_id, session_id.as_deref(), reason);
if let Some(correlation) = entry.renderer_correlation() {
let _ = self.take_renderer_call_for_frontend_if_matches_for_owner(
&owner,
correlation.frontend_command_id().get(),
correlation.renderer_call_id(),
correlation.dispatched_attachment_id(),
);
}
if let Some(listener) = entry.bidi_channel_listener() {
self.unregister_runtime_remote_object_group_for_owner(
&owner,
listener.channel_object_group(),
);
}
}
fn apply_completed_pending_inspector_await_entry(
&mut self,
owner: &CommandOwnerScope,
entry: PendingInspectorAwait,
protocol_events: &[BackgroundProtocolEvent],
) {
if let Some(object_group) = entry.object_group() {
for event in protocol_events {
if let Some((_, _, BackgroundCommandResponsePayloadRef::Success { result })) =
event.command_response_payload_ref()
{
self.register_runtime_remote_object_ids_from_value_for_owner_with_group(
owner,
result,
object_group,
);
} else if let Some(message) = event.protocol_message() {
self.register_runtime_remote_object_ids_from_value_for_owner_with_group(
owner,
message,
object_group,
);
}
}
} else {
for event in protocol_events {
if let Some((_, _, BackgroundCommandResponsePayloadRef::Success { result })) =
event.command_response_payload_ref()
{
self.register_runtime_remote_object_ids_from_value_for_owner(owner, result);
} else if let Some(message) = event.protocol_message() {
self.register_runtime_remote_object_ids_from_value_for_owner(owner, message);
}
}
}
if let Some(listener) = entry.bidi_channel_listener() {
self.unregister_runtime_remote_object_group_for_owner(
owner,
listener.channel_object_group(),
);
}
}
fn remove_pending_inspector_await(
&mut self,
cdp_request_id: u64,
session_id: Option<&str>,
) -> Option<PendingInspectorAwait> {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
return target.remove_pending_inspector_await(owner_session_id, cdp_request_id);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
return target.remove_pending_inspector_await(owner_session_id, cdp_request_id);
}
self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.remove_pending_inspector_await(cdp_request_id)
})
.flatten()
}
fn remove_pending_inspector_await_for_owner(
&mut self,
cdp_request_id: u64,
owner: &CommandOwnerScope,
) -> Option<PendingInspectorAwait> {
if owner.session_id().is_some() {
return self.remove_pending_inspector_await(cdp_request_id, owner.session_id());
}
self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.remove_pending_inspector_await(cdp_request_id)
})
.flatten()
}
fn remove_pending_inspector_await_for_cancellation(
&mut self,
cdp_request_id: u64,
session_id: Option<&str>,
) -> Option<PendingInspectorAwait> {
let entry = self.remove_pending_inspector_await(cdp_request_id, session_id);
if let Some(correlation) = entry
.as_ref()
.and_then(PendingInspectorAwait::renderer_correlation)
{
self.discard_renderer_call_for_session_owner_if_matches(session_id, correlation);
} else if entry.is_none() {
let _ =
self.take_renderer_call_for_frontend_for_session_owner(session_id, cdp_request_id);
}
entry
}
fn remove_pending_inspector_await_for_cancellation_for_owner(
&mut self,
cdp_request_id: u64,
owner: &CommandOwnerScope,
) -> Option<PendingInspectorAwait> {
let entry = self.remove_pending_inspector_await_for_owner(cdp_request_id, owner);
if let Some(correlation) = entry
.as_ref()
.and_then(PendingInspectorAwait::renderer_correlation)
{
let _ = self.take_renderer_call_for_frontend_if_matches_for_owner(
owner,
correlation.frontend_command_id().get(),
correlation.renderer_call_id(),
correlation.dispatched_attachment_id(),
);
} else if entry.is_none() {
let _ = self.take_renderer_call_for_frontend_for_owner(owner, cdp_request_id);
}
entry
}
fn discard_renderer_call_for_session_owner_if_matches(
&mut self,
session_id: Option<&str>,
correlation: RendererCommandCorrelation,
) {
let _ = self.take_renderer_call_for_frontend_if_matches_for_session_owner(
session_id,
correlation.frontend_command_id().get(),
correlation.renderer_call_id(),
correlation.dispatched_attachment_id(),
);
}
pub fn has_pending_inspector_awaits(&self) -> bool {
if !self.claimed_pending_inspector_await_owners.is_empty() {
return true;
}
self.browser_contexts().any(|browser_context| {
browser_context
.page_targets
.iter()
.any(|target| target.has_pending_inspector_awaits())
|| browser_context
.shared_worker_targets
.values()
.any(SharedWorkerTargetState::has_pending_inspector_awaits)
|| browser_context
.dedicated_worker_targets
.values()
.any(|target| target.has_pending_inspector_awaits())
|| browser_context
.service_worker_targets
.values()
.any(ServiceWorkerTargetState::has_pending_inspector_awaits)
})
}
pub fn has_pending_inspector_awaits_for_session_owner(&self, session_id: Option<&str>) -> bool {
if self
.claimed_pending_inspector_await_owners
.values()
.any(|owner| owner.matches_session_owner(session_id))
{
return true;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session(session_id)
{
return target.has_pending_inspector_awaits_for_session(owner_session_id);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session(session_id)
{
return target.has_pending_inspector_awaits_for_session(owner_session_id);
}
self.target_devtools_session_state_for_session(session_id)
.is_some_and(DevToolsSessionState::has_pending_inspector_awaits)
}
pub(crate) fn fail_pending_inspector_awaits_from_shared_worker_target_session_background_events_into(
out: &mut Vec<BackgroundProtocolEvent>,
target: &mut SharedWorkerTargetState,
owner_session_id: &str,
reason: &'static str,
) {
for (cdp_id, entry) in target.drain_pending_inspector_awaits_for_session(owner_session_id) {
if let Some(listener) = entry.bidi_channel_listener() {
let object_owner_session_id = entry.session_id().unwrap_or(owner_session_id);
target.unregister_runtime_remote_object_group(
object_owner_session_id,
listener.channel_object_group(),
);
continue;
}
push_pending_inspector_await_error_background_event(
out,
cdp_id,
entry.session_id(),
reason,
);
}
for correlation in target.terminate_renderer_calls_for_session(owner_session_id, reason) {
push_terminated_renderer_call_error_background_events(
out,
vec![correlation],
Some(owner_session_id),
reason,
);
}
}
pub(crate) fn fail_pending_inspector_awaits_from_service_worker_target_state_background_events_into(
out: &mut Vec<BackgroundProtocolEvent>,
target: &mut ServiceWorkerTargetState,
reason: &'static str,
) {
for (cdp_id, entry) in target.drain_pending_inspector_awaits() {
if let Some(listener) = entry.bidi_channel_listener() {
if let Some(session_id) = entry.session_id() {
target.unregister_runtime_remote_object_group(
session_id,
listener.channel_object_group(),
);
}
continue;
}
push_pending_inspector_await_error_background_event(
out,
cdp_id,
entry.session_id(),
reason,
);
}
for (session_id, correlation) in target.terminate_renderer_calls(reason) {
push_terminated_renderer_call_error_background_events(
out,
vec![correlation],
Some(&session_id),
reason,
);
}
}
pub(crate) fn fail_pending_inspector_awaits_for_session_owner_background_events_into(
&mut self,
out: &mut Vec<BackgroundProtocolEvent>,
claimed_background_events: &mut Vec<BackgroundProtocolEvent>,
session_id: Option<&str>,
reason: &'static str,
) {
let claimed = self.drain_claimed_pending_inspector_await_owners_for_session(session_id);
self.push_claimed_pending_inspector_await_owner_errors(
claimed_background_events,
claimed,
reason,
);
if let Some(owner_session_id) = session_id
&& self.shared_worker_target_for_session(session_id).is_some()
{
let drained = self
.shared_worker_target_for_session_mut(session_id)
.map(|target| target.drain_pending_inspector_awaits_for_session(owner_session_id))
.unwrap_or_default();
let mut listener_groups_to_unregister = Vec::new();
for (cdp_id, entry) in drained {
self.cancel_runtime_await_job(cdp_id, entry.session_id(), reason);
if let Some(listener) = entry.bidi_channel_listener() {
listener_groups_to_unregister.push((
entry.session_id().map(str::to_owned),
listener.channel_object_group().to_owned(),
));
continue;
}
push_pending_inspector_await_error_background_event(
out,
cdp_id,
entry.session_id(),
reason,
);
}
if let Some(target) = self.shared_worker_target_for_session_mut(session_id) {
for (entry_session_id, object_group) in listener_groups_to_unregister {
let object_owner_session_id =
entry_session_id.as_deref().unwrap_or(owner_session_id);
target.unregister_runtime_remote_object_group(
object_owner_session_id,
&object_group,
);
}
let terminated =
target.terminate_renderer_calls_for_session(owner_session_id, reason);
push_terminated_renderer_call_error_background_events(
out,
terminated,
Some(owner_session_id),
reason,
);
}
return;
}
if let Some(owner_session_id) = session_id
&& self.service_worker_target_for_session(session_id).is_some()
{
let drained = self
.service_worker_target_for_session_mut(session_id)
.map(|target| target.drain_pending_inspector_awaits_for_session(owner_session_id))
.unwrap_or_default();
let mut listener_groups_to_unregister = Vec::new();
for (cdp_id, entry) in drained {
self.cancel_runtime_await_job(cdp_id, entry.session_id(), reason);
if let Some(listener) = entry.bidi_channel_listener() {
listener_groups_to_unregister.push((
entry.session_id().map(str::to_owned),
listener.channel_object_group().to_owned(),
));
continue;
}
push_pending_inspector_await_error_background_event(
out,
cdp_id,
entry.session_id(),
reason,
);
}
if let Some(target) = self.service_worker_target_for_session_mut(session_id) {
for (entry_session_id, object_group) in listener_groups_to_unregister {
let object_owner_session_id =
entry_session_id.as_deref().unwrap_or(owner_session_id);
target.unregister_runtime_remote_object_group(
object_owner_session_id,
&object_group,
);
}
let terminated =
target.terminate_renderer_calls_for_session(owner_session_id, reason);
push_terminated_renderer_call_error_background_events(
out,
terminated,
Some(owner_session_id),
reason,
);
}
return;
}
let drained = self
.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.drain_pending_inspector_awaits()
})
.unwrap_or_default();
for (cdp_id, entry) in drained {
self.cancel_runtime_await_job(cdp_id, entry.session_id(), reason);
if let Some(listener) = entry.bidi_channel_listener() {
self.unregister_runtime_remote_object_group_for_session_owner(
entry.session_id(),
listener.channel_object_group(),
);
continue;
}
push_pending_inspector_await_error_background_event(
out,
cdp_id,
entry.session_id(),
reason,
);
}
let terminated = self
.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.terminate_all_renderer_calls(reason)
})
.unwrap_or_default();
push_terminated_renderer_call_error_background_events(out, terminated, session_id, reason);
}
pub(crate) fn fail_pending_inspector_awaits_for_owner_background_events_into(
&mut self,
out: &mut Vec<BackgroundProtocolEvent>,
claimed_background_events: &mut Vec<BackgroundProtocolEvent>,
owner: &CommandOwnerScope,
reason: &'static str,
) {
if owner.session_id().is_some() {
self.fail_pending_inspector_awaits_for_session_owner_background_events_into(
out,
claimed_background_events,
owner.session_id(),
reason,
);
return;
}
let claimed = self.drain_claimed_pending_inspector_await_owners_for_owner(owner);
self.push_claimed_pending_inspector_await_owner_errors(
claimed_background_events,
claimed,
reason,
);
let drained = self
.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.drain_pending_inspector_awaits()
})
.unwrap_or_default();
for (cdp_id, entry) in drained {
self.cancel_runtime_await_job(cdp_id, entry.session_id(), reason);
if let Some(listener) = entry.bidi_channel_listener() {
self.unregister_runtime_remote_object_group_for_owner(
owner,
listener.channel_object_group(),
);
continue;
}
push_pending_inspector_await_error_background_event(
out,
cdp_id,
entry.session_id(),
reason,
);
}
let terminated = self
.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.terminate_all_renderer_calls(reason)
})
.unwrap_or_default();
push_terminated_renderer_call_error_background_events(out, terminated, None, reason);
}
pub(crate) fn validate_runtime_remote_object_ids_for_session_owner(
&self,
session_id: Option<&str>,
object_ids: &[String],
) -> Result<(), String> {
if object_ids.is_empty() {
return Ok(());
}
let Some(owner) = self.runtime_remote_object_owner_identity_for_session(session_id) else {
return Ok(());
};
for object_id in object_ids {
// V8 remote object ids are scoped to an Inspector session. Two
// sessions connected to the same context can therefore emit the
// same wire id for different objects. Prefer the current
// session's registration before using the cross-owner check to
// reject a handle borrowed from another session.
if self.runtime_remote_object_id_known_for_session_owner(session_id, object_id) {
continue;
}
if self.runtime_remote_object_id_known_for_different_owner(&owner, object_id) {
return Err("Cannot find object with given id".to_owned());
}
}
Ok(())
}
pub(crate) fn validate_runtime_remote_object_ids_for_owner(
&self,
owner: &CommandOwnerScope,
object_ids: &[String],
) -> Result<(), String> {
if owner.session_id().is_some() {
return self.validate_runtime_remote_object_ids_for_session_owner(
owner.session_id(),
object_ids,
);
}
if object_ids.is_empty() {
return Ok(());
}
let Some(owner_identity) = self.runtime_remote_object_owner_identity_for_owner(owner)
else {
return Ok(());
};
for object_id in object_ids {
if self.runtime_remote_object_id_known_for_owner(owner, object_id) {
continue;
}
if self.runtime_remote_object_id_known_for_different_owner(&owner_identity, object_id) {
return Err("Cannot find object with given id".to_owned());
}
}
Ok(())
}
pub(crate) fn runtime_remote_object_id_known_for_session_owner(
&self,
session_id: Option<&str>,
object_id: &str,
) -> bool {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session(session_id)
{
return target.has_runtime_remote_object_id(owner_session_id, object_id);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session(session_id)
{
return target.has_runtime_remote_object_id(owner_session_id, object_id);
}
self.target_devtools_session_state_for_session(session_id)
.is_some_and(|state| state.has_runtime_remote_object_id(object_id))
}
pub(crate) fn runtime_remote_object_id_known_for_owner(
&self,
owner: &CommandOwnerScope,
object_id: &str,
) -> bool {
if owner.session_id().is_some() {
return self
.runtime_remote_object_id_known_for_session_owner(owner.session_id(), object_id);
}
self.target_devtools_session_state_for_owner(owner)
.is_some_and(|state| state.has_runtime_remote_object_id(object_id))
}
pub(crate) fn register_runtime_remote_object_ids_from_value_for_session_owner(
&mut self,
session_id: Option<&str>,
value: &Value,
) {
let object_ids = runtime_remote_object_ids_in_value(value);
self.register_runtime_remote_object_ids_for_session_owner(session_id, object_ids);
}
pub(crate) fn register_runtime_remote_object_ids_from_value_for_owner(
&mut self,
owner: &CommandOwnerScope,
value: &Value,
) {
if owner.session_id().is_some() {
self.register_runtime_remote_object_ids_from_value_for_session_owner(
owner.session_id(),
value,
);
return;
}
let object_ids = runtime_remote_object_ids_in_value(value);
let _ = self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.register_runtime_remote_object_ids(object_ids)
});
}
pub(crate) fn register_runtime_remote_object_ids_from_value_for_session_owner_with_group(
&mut self,
session_id: Option<&str>,
value: &Value,
object_group: &str,
) {
let object_ids = runtime_remote_object_ids_in_value(value);
self.register_runtime_remote_object_ids_for_session_owner_with_group(
session_id,
object_ids,
object_group,
);
}
pub(crate) fn register_runtime_remote_object_ids_from_value_for_owner_with_group(
&mut self,
owner: &CommandOwnerScope,
value: &Value,
object_group: &str,
) {
if owner.session_id().is_some() {
self.register_runtime_remote_object_ids_from_value_for_session_owner_with_group(
owner.session_id(),
value,
object_group,
);
return;
}
let object_ids = runtime_remote_object_ids_in_value(value);
let _ = self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.register_runtime_remote_object_ids_with_group(object_ids, object_group)
});
}
pub(crate) fn runtime_remote_object_group_for_session_owner(
&self,
session_id: Option<&str>,
object_id: &str,
) -> Option<String> {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session(session_id)
{
return target
.runtime_remote_object_group(owner_session_id, object_id)
.map(str::to_owned);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session(session_id)
{
return target
.runtime_remote_object_group(owner_session_id, object_id)
.map(str::to_owned);
}
self.target_devtools_session_state_for_session(session_id)?
.runtime_remote_object_group(object_id)
.map(str::to_owned)
}
pub(crate) fn runtime_remote_object_group_for_owner(
&self,
owner: &CommandOwnerScope,
object_id: &str,
) -> Option<String> {
if owner.session_id().is_some() {
return self
.runtime_remote_object_group_for_session_owner(owner.session_id(), object_id);
}
self.target_devtools_session_state_for_owner(owner)?
.runtime_remote_object_group(object_id)
.map(str::to_owned)
}
pub(crate) fn runtime_remote_object_realm_for_session_owner(
&self,
session_id: Option<&str>,
object_id: &str,
) -> Option<String> {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session(session_id)
{
return target
.runtime_remote_object_realm(owner_session_id, object_id)
.map(str::to_owned);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session(session_id)
{
return target
.runtime_remote_object_realm(owner_session_id, object_id)
.map(str::to_owned);
}
self.target_devtools_session_state_for_session(session_id)?
.runtime_remote_object_realm(object_id)
.map(str::to_owned)
}
pub(crate) fn runtime_remote_object_realm_for_owner(
&self,
owner: &CommandOwnerScope,
object_id: &str,
) -> Option<String> {
if owner.session_id().is_some() {
return self
.runtime_remote_object_realm_for_session_owner(owner.session_id(), object_id);
}
self.target_devtools_session_state_for_owner(owner)?
.runtime_remote_object_realm(object_id)
.map(str::to_owned)
}
pub(crate) fn runtime_remote_object_alias_for_session_owner(
&self,
session_id: Option<&str>,
object_id: &str,
) -> Option<String> {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session(session_id)
{
return target
.runtime_remote_object_alias(owner_session_id, object_id)
.map(str::to_owned);
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session(session_id)
{
return target
.runtime_remote_object_alias(owner_session_id, object_id)
.map(str::to_owned);
}
self.target_devtools_session_state_for_session(session_id)?
.runtime_remote_object_alias(object_id)
.map(str::to_owned)
}
pub(crate) fn runtime_remote_object_alias_for_owner(
&self,
owner: &CommandOwnerScope,
object_id: &str,
) -> Option<String> {
if owner.session_id().is_some() {
return self
.runtime_remote_object_alias_for_session_owner(owner.session_id(), object_id);
}
self.target_devtools_session_state_for_owner(owner)?
.runtime_remote_object_alias(object_id)
.map(str::to_owned)
}
pub(crate) fn register_runtime_remote_object_alias_for_owner_with_realm(
&mut self,
owner: &CommandOwnerScope,
alias_id: String,
object_id: String,
realm_id: &str,
) {
if owner.session_id().is_some() {
self.register_runtime_remote_object_alias_for_session_owner_with_realm(
owner.session_id(),
alias_id,
object_id,
realm_id,
);
return;
}
let _ = self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.register_runtime_remote_object_alias_with_realm(alias_id, object_id, realm_id);
});
}
pub(crate) fn unregister_runtime_remote_object_ids_for_session_owner(
&mut self,
session_id: Option<&str>,
object_ids: &[String],
) {
if object_ids.is_empty() {
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
target.unregister_runtime_remote_object_ids(owner_session_id, object_ids);
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
target.unregister_runtime_remote_object_ids(owner_session_id, object_ids);
return;
}
let _ = self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.unregister_runtime_remote_object_ids(object_ids);
});
}
pub(crate) fn unregister_runtime_remote_object_ids_for_owner(
&mut self,
owner: &CommandOwnerScope,
object_ids: &[String],
) {
if owner.session_id().is_some() {
self.unregister_runtime_remote_object_ids_for_session_owner(
owner.session_id(),
object_ids,
);
return;
}
let _ = self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.unregister_runtime_remote_object_ids(object_ids)
});
}
pub(crate) fn unregister_runtime_remote_object_group_for_session_owner(
&mut self,
session_id: Option<&str>,
object_group: &str,
) {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
target.unregister_runtime_remote_object_group(owner_session_id, object_group);
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
target.unregister_runtime_remote_object_group(owner_session_id, object_group);
return;
}
let _ = self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.unregister_runtime_remote_object_group(object_group);
});
}
pub(crate) fn unregister_runtime_remote_object_group_for_owner(
&mut self,
owner: &CommandOwnerScope,
object_group: &str,
) {
if owner.session_id().is_some() {
self.unregister_runtime_remote_object_group_for_session_owner(
owner.session_id(),
object_group,
);
return;
}
let _ = self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.unregister_runtime_remote_object_group(object_group)
});
}
pub(crate) fn clear_runtime_remote_object_tracking_for_session_owner(
&mut self,
session_id: Option<&str>,
) {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
target.clear_runtime_remote_object_tracking(owner_session_id);
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
target.clear_runtime_remote_object_tracking(owner_session_id);
return;
}
let _ = self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.clear_runtime_remote_object_tracking();
});
}
pub(crate) fn clear_runtime_remote_object_tracking_for_owner(
&mut self,
owner: &CommandOwnerScope,
) {
if owner.session_id().is_some() {
self.clear_runtime_remote_object_tracking_for_session_owner(owner.session_id());
return;
}
let _ = self.with_target_devtools_session_state_for_owner_mut(
owner,
DevToolsSessionState::clear_runtime_remote_object_tracking,
);
}
pub(crate) fn record_runtime_contexts_reported_for_session_owner(
&mut self,
session_id: Option<&str>,
) {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
target.record_runtime_contexts_reported_to_frontend(owner_session_id);
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
target.record_runtime_contexts_reported_to_frontend(owner_session_id);
return;
}
let _ = self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.record_runtime_contexts_reported_to_frontend();
});
}
pub(crate) fn record_runtime_contexts_reported_for_owner(&mut self, owner: &CommandOwnerScope) {
if owner.session_id().is_some() {
self.record_runtime_contexts_reported_for_session_owner(owner.session_id());
return;
}
let _ = self.with_target_devtools_session_state_for_owner_mut(
owner,
DevToolsSessionState::record_runtime_contexts_reported_to_frontend,
);
}
pub(crate) fn record_runtime_contexts_cleared_for_session_owner(
&mut self,
session_id: Option<&str>,
) {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
target.record_runtime_contexts_cleared_for_frontend(owner_session_id);
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
target.record_runtime_contexts_cleared_for_frontend(owner_session_id);
return;
}
let _ = self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.record_runtime_contexts_cleared_for_frontend();
});
}
pub(crate) fn record_runtime_contexts_cleared_for_owner(&mut self, owner: &CommandOwnerScope) {
if owner.session_id().is_some() {
self.record_runtime_contexts_cleared_for_session_owner(owner.session_id());
return;
}
let _ = self.with_target_devtools_session_state_for_owner_mut(
owner,
DevToolsSessionState::record_runtime_contexts_cleared_for_frontend,
);
}
pub(crate) fn record_runtime_context_protocol_event_for_session_owner(
&mut self,
session_id: Option<&str>,
event: &RuntimeContextProtocolEvent,
) {
if session_id.is_none() {
return;
}
if let Some(target) = self.shared_worker_target_for_session_mut(session_id) {
match event {
RuntimeContextProtocolEvent::Created(event) => {
target.record_runtime_execution_context_created_event(event);
}
RuntimeContextProtocolEvent::Destroyed(event) => {
target.record_runtime_execution_context_destroyed_event(event);
}
RuntimeContextProtocolEvent::Cleared(_) => {
target.record_runtime_execution_contexts_cleared_event();
}
}
return;
}
if let Some(target) = self.service_worker_target_for_session_mut(session_id) {
match event {
RuntimeContextProtocolEvent::Created(event) => {
target.record_runtime_execution_context_created_event(event);
}
RuntimeContextProtocolEvent::Destroyed(event) => {
target.record_runtime_execution_context_destroyed_event(event);
}
RuntimeContextProtocolEvent::Cleared(_) => {
target.record_runtime_execution_contexts_cleared_event();
}
}
}
}
pub(crate) fn record_runtime_context_protocol_event_for_owner(
&mut self,
owner: &CommandOwnerScope,
event: &RuntimeContextProtocolEvent,
) {
if owner.session_id().is_some() {
self.record_runtime_context_protocol_event_for_session_owner(owner.session_id(), event);
}
}
pub(crate) fn clear_runtime_remote_objects_for_realm_for_session_owner(
&mut self,
session_id: Option<&str>,
realm_id: &str,
) {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
target.clear_runtime_remote_objects_for_realm(owner_session_id, realm_id);
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
target.clear_runtime_remote_objects_for_realm(owner_session_id, realm_id);
return;
}
let _ = self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.clear_runtime_remote_objects_for_realm(realm_id);
});
}
pub(crate) fn clear_runtime_remote_objects_for_realm_for_owner(
&mut self,
owner: &CommandOwnerScope,
realm_id: &str,
) {
if owner.session_id().is_some() {
self.clear_runtime_remote_objects_for_realm_for_session_owner(
owner.session_id(),
realm_id,
);
return;
}
let _ = self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.clear_runtime_remote_objects_for_realm(realm_id)
});
}
pub(crate) fn register_runtime_remote_object_ids_for_session_owner_with_realm(
&mut self,
session_id: Option<&str>,
object_ids: Vec<String>,
realm_id: &str,
) {
if object_ids.is_empty() {
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
target.register_runtime_remote_object_ids_with_realm(
owner_session_id,
object_ids,
realm_id,
);
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
target.register_runtime_remote_object_ids_with_realm(
owner_session_id,
object_ids,
realm_id,
);
return;
}
let _ = self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.register_runtime_remote_object_ids_with_realm(object_ids, realm_id);
});
}
pub(crate) fn register_runtime_remote_object_ids_for_owner_with_realm(
&mut self,
owner: &CommandOwnerScope,
object_ids: Vec<String>,
realm_id: &str,
) {
if owner.session_id().is_some() {
self.register_runtime_remote_object_ids_for_session_owner_with_realm(
owner.session_id(),
object_ids,
realm_id,
);
return;
}
if object_ids.is_empty() {
return;
}
let _ = self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
state.register_runtime_remote_object_ids_with_realm(object_ids, realm_id)
});
}
pub(crate) fn register_runtime_remote_object_alias_for_session_owner_with_realm(
&mut self,
session_id: Option<&str>,
alias_id: String,
object_id: String,
realm_id: &str,
) {
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
target.register_runtime_remote_object_alias_with_realm(
owner_session_id,
alias_id,
object_id,
realm_id,
);
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
target.register_runtime_remote_object_alias_with_realm(
owner_session_id,
alias_id,
object_id,
realm_id,
);
return;
}
let _ = self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.register_runtime_remote_object_alias_with_realm(alias_id, object_id, realm_id);
});
}
fn register_runtime_remote_object_ids_for_session_owner(
&mut self,
session_id: Option<&str>,
object_ids: Vec<String>,
) {
if object_ids.is_empty() {
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
target.register_runtime_remote_object_ids_for_session(owner_session_id, object_ids);
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
target.register_runtime_remote_object_ids_for_session(owner_session_id, object_ids);
return;
}
let _ = self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.register_runtime_remote_object_ids(object_ids);
});
}
fn register_runtime_remote_object_ids_for_session_owner_with_group(
&mut self,
session_id: Option<&str>,
object_ids: Vec<String>,
object_group: &str,
) {
if object_ids.is_empty() {
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.shared_worker_target_for_session_mut(session_id)
{
target.register_runtime_remote_object_ids_with_group(
owner_session_id,
object_ids,
object_group,
);
return;
}
if let Some(owner_session_id) = session_id
&& let Some(target) = self.service_worker_target_for_session_mut(session_id)
{
target.register_runtime_remote_object_ids_with_group(
owner_session_id,
object_ids,
object_group,
);
return;
}
let _ = self.with_target_devtools_session_state_for_session_mut(session_id, |state| {
state.register_runtime_remote_object_ids_with_group(object_ids, object_group);
});
}
fn runtime_remote_object_owner_identity_for_session(
&self,
session_id: Option<&str>,
) -> Option<RuntimeRemoteObjectOwnerIdentity> {
if let Some(CdpSessionRoute::SharedWorkerTarget {
browser_context_id,
target_id,
}) = self.session_route(session_id)
{
let target = self
.browser_context_by_id(&browser_context_id)?
.shared_worker_target(&target_id)?;
return Some(RuntimeRemoteObjectOwnerIdentity::SharedWorker {
browser_context_id,
instance_id: target.renderer_instance_id,
session_id: session_id?.to_owned(),
});
}
if let Some(CdpSessionRoute::DedicatedWorkerTarget {
browser_context_id,
target_id,
}) = self.session_route(session_id)
{
let target = self
.browser_context_by_id(&browser_context_id)?
.dedicated_worker_target(&target_id)?;
return Some(RuntimeRemoteObjectOwnerIdentity::DedicatedWorker {
browser_context_id,
instance_id: target.renderer_instance_id,
session_id: session_id?.to_owned(),
});
}
if let Some(CdpSessionRoute::ServiceWorkerTarget {
browser_context_id,
target_id,
}) = self.session_route(session_id)
{
let target = self
.browser_context_by_id(&browser_context_id)?
.service_worker_target(&target_id)?;
return Some(RuntimeRemoteObjectOwnerIdentity::ServiceWorker {
browser_context_id,
version_id: target.renderer_version_id,
session_id: session_id?.to_owned(),
});
}
let (browser_context_id, target_id) = self.target_owner_identity_for_session(session_id)?;
let devtools_session_id =
self.target_devtools_attached_session_id_for_session(session_id)?;
Some(RuntimeRemoteObjectOwnerIdentity::Page {
browser_context_id,
target_id,
devtools_session_id,
})
}
fn runtime_remote_object_owner_identity_for_owner(
&self,
owner: &CommandOwnerScope,
) -> Option<RuntimeRemoteObjectOwnerIdentity> {
if owner.session_id().is_some() {
return self.runtime_remote_object_owner_identity_for_session(owner.session_id());
}
let (browser_context_id, target_id) = self.target_owner_identity_for_owner(owner)?;
Some(RuntimeRemoteObjectOwnerIdentity::Page {
browser_context_id,
target_id,
devtools_session_id: None,
})
}
fn runtime_remote_object_id_known_for_different_owner(
&self,
owner: &RuntimeRemoteObjectOwnerIdentity,
object_id: &str,
) -> bool {
for browser_context in self.browser_contexts() {
let current_page_owner = match owner {
RuntimeRemoteObjectOwnerIdentity::Page {
browser_context_id,
target_id,
devtools_session_id,
} if browser_context_id == &browser_context.id => Some((
target_id
.as_deref()
.or_else(|| browser_context.active_target_id()),
devtools_session_id.as_deref(),
)),
_ => None,
};
if browser_context.page_targets.iter().any(|target| {
if current_page_owner
.is_some_and(|(target_id, _)| target_id == Some(target.target_id()))
{
target.has_runtime_remote_object_id_for_different_session(
current_page_owner.and_then(|(_, session_id)| session_id),
object_id,
)
} else {
target.has_runtime_remote_object_id(object_id)
}
}) {
return true;
}
for target in browser_context.shared_worker_targets.values() {
let shared_worker_is_current_owner = matches!(
owner,
RuntimeRemoteObjectOwnerIdentity::SharedWorker {
browser_context_id,
instance_id,
session_id,
} if browser_context_id == &browser_context.id
&& instance_id == &target.renderer_instance_id
&& target.is_session(session_id)
);
if !shared_worker_is_current_owner
&& target.any_session_has_runtime_remote_object_id(object_id)
{
return true;
}
}
for target in browser_context.dedicated_worker_targets.values() {
let dedicated_worker_is_current_owner = matches!(
owner,
RuntimeRemoteObjectOwnerIdentity::DedicatedWorker {
browser_context_id,
instance_id,
session_id,
} if browser_context_id == &browser_context.id
&& *instance_id == target.renderer_instance_id
&& target.is_session(session_id)
);
if !dedicated_worker_is_current_owner
&& target.any_session_has_runtime_remote_object_id(object_id)
{
return true;
}
}
for target in browser_context.service_worker_targets.values() {
let service_worker_is_current_owner = matches!(
owner,
RuntimeRemoteObjectOwnerIdentity::ServiceWorker {
browser_context_id,
version_id,
session_id,
} if browser_context_id == &browser_context.id
&& *version_id == target.renderer_version_id
&& target.is_session(session_id)
);
if !service_worker_is_current_owner
&& target.any_session_has_runtime_remote_object_id(object_id)
{
return true;
}
}
}
false
}
pub(crate) async fn release_worker_runtime_remote_objects_for_session_best_effort_async(
&mut self,
session_id: &str,
) {
let service_worker = matches!(
self.session_route(Some(session_id)),
Some(CdpSessionRoute::ServiceWorkerTarget { .. })
);
let cleanup_plan = if service_worker {
self.service_worker_target_for_session_mut(Some(session_id))
.map(|target| target.take_runtime_remote_object_cleanup_plan(session_id))
} else {
self.shared_worker_target_for_session_mut(Some(session_id))
.map(|target| target.take_runtime_remote_object_cleanup_plan(session_id))
};
let Some((object_groups, object_ids)) = cleanup_plan else {
return;
};
if object_groups.is_empty() && object_ids.is_empty() {
return;
}
let mut command_id = SHARED_WORKER_RUNTIME_REMOTE_OBJECT_CLEANUP_COMMAND_ID_BASE;
for object_group in object_groups {
let raw_json = json!({
"id": command_id,
"method": "Runtime.releaseObjectGroup",
"params": { "objectGroup": object_group }
})
.to_string();
let release = if service_worker {
self.dispatch_service_worker_runtime_helper_protocol_message_for_session_async(
Some(session_id),
&raw_json,
command_id,
)
.await
} else {
self.dispatch_shared_worker_runtime_helper_protocol_message_for_session_async(
Some(session_id),
&raw_json,
command_id,
)
.await
};
if let Err(error) = release {
tracing::warn!(
object_group = %object_group,
error = %error,
"failed to release worker Runtime object group during target detach"
);
}
command_id = command_id.saturating_add(1);
}
for object_id in object_ids {
let raw_json = json!({
"id": command_id,
"method": "Runtime.releaseObject",
"params": { "objectId": object_id }
})
.to_string();
let release = if service_worker {
self.dispatch_service_worker_runtime_helper_protocol_message_for_session_async(
Some(session_id),
&raw_json,
command_id,
)
.await
} else {
self.dispatch_shared_worker_runtime_helper_protocol_message_for_session_async(
Some(session_id),
&raw_json,
command_id,
)
.await
};
if let Err(error) = release {
tracing::warn!(
object_id = %object_id,
error = %error,
"failed to release worker Runtime object during target detach"
);
}
command_id = command_id.saturating_add(1);
}
}
/// Routes a batch of inspector messages into `out`, demultiplexing by id.
///
/// For each message:
/// - if it carries an `id` matching a pending inspector await registry entry,
/// the entry is consumed and the message is sent with that entry's
/// `session_id` (regardless of `current_session_id`);
/// - otherwise if its `id` matches `current_cmd_id`, the message is sent
/// with `current_session_id`;
/// - otherwise the message is dropped (orphan id; logs at warn);
/// - notifications (no `id`) are routed as background events with
/// `current_session_id`.
///
/// Returns true if a message matching `current_cmd_id` was produced (either
/// via a pending entry or directly).
#[cfg(test)]
pub(crate) fn route_inspector_messages_into(
&mut self,
messages: Vec<Value>,
current_cmd_id: Option<u64>,
current_session_id: Option<&str>,
response_events: &mut Vec<BackgroundProtocolEvent>,
background_events: &mut Vec<BackgroundProtocolEvent>,
) -> bool {
self.route_inspector_messages_with_background_events_into(
messages,
current_cmd_id,
current_session_id,
response_events,
background_events,
)
}
#[cfg(test)]
pub(crate) fn route_inspector_messages_with_background_events_into(
&mut self,
messages: Vec<Value>,
current_cmd_id: Option<u64>,
current_session_id: Option<&str>,
response_events: &mut Vec<BackgroundProtocolEvent>,
background_events: &mut Vec<BackgroundProtocolEvent>,
) -> bool {
let owner = CommandOwnerScope::capture(self, current_session_id);
let mut current_seen = false;
for message in messages {
current_seen |= self
.route_runtime_inspector_protocol_message_for_owner_with_background_events_into(
message,
current_cmd_id,
&owner,
response_events,
background_events,
);
}
current_seen
}
pub(crate) fn route_renderer_runtime_inspector_messages_for_owner_with_background_events_into(
&mut self,
messages: Vec<RendererRuntimeInspectorMessage>,
current_cmd_id: Option<u64>,
owner: &CommandOwnerScope,
response_events: &mut Vec<BackgroundProtocolEvent>,
background_events: &mut Vec<BackgroundProtocolEvent>,
) -> bool {
let current_session_id = owner.session_id();
let mut current_seen = false;
for message in messages {
match message {
RendererRuntimeInspectorMessage::RuntimeContext(event) => {
let mut event = RuntimeContextProtocolEvent::from_restore_event(event);
qualify_runtime_context_protocol_event_for_owner_typed(self, &mut event, owner);
apply_runtime_context_protocol_event_side_effects_for_owner_typed(
self, &event, owner,
);
let mut runtime_context_events = Vec::new();
emit_runtime_context_protocol_background_event_typed(
&mut runtime_context_events,
event,
current_session_id,
);
if current_cmd_id.is_some() {
response_events.extend(runtime_context_events);
} else {
background_events.extend(runtime_context_events);
}
}
RendererRuntimeInspectorMessage::Protocol(message) => {
current_seen |= self
.route_runtime_inspector_protocol_message_for_owner_with_background_events_into(
message.into_value(),
current_cmd_id,
owner,
response_events,
background_events,
);
}
}
}
current_seen
}
pub(crate) fn route_renderer_runtime_command_output_for_owner_into(
&mut self,
output: RendererRuntimeCommandOutput,
current_cmd_id: Option<u64>,
owner: &CommandOwnerScope,
ordered_events: &mut Vec<BackgroundProtocolEvent>,
) -> bool {
let current_session_id = owner.session_id();
let attachment_is_current =
output
.renderer_agent_attachment_id()
.is_none_or(|attachment_id| {
self.current_renderer_agent_attachment_id_for_owner(owner)
== Some(attachment_id)
});
if !attachment_is_current {
tracing::debug!(
attachment_id = ?output.renderer_agent_attachment_id(),
session_id = current_session_id,
"dropping renderer command output from a stale attachment"
);
return false;
}
if output.renderer_agent_attachment_id().is_some()
&& let Some(state) = output.v8_state_update().cloned()
{
let _ = self.merge_v8_inspector_session_state_for_owner(owner, state);
}
let mut current_seen = false;
for message in output.into_messages() {
let mut response_events = Vec::new();
let mut background_events = Vec::new();
current_seen |= self
.route_renderer_runtime_inspector_messages_for_owner_with_background_events_into(
vec![message],
current_cmd_id,
owner,
&mut response_events,
&mut background_events,
);
ordered_events.extend(response_events);
ordered_events.extend(background_events);
}
current_seen
}
pub(crate) fn route_renderer_command_turn_output_for_owner_into(
&mut self,
output: RendererCommandTurnOutput,
current_cmd_id: Option<u64>,
owner: &CommandOwnerScope,
response_flush: &CommandResponseFlushContext,
ordered_events: &mut Vec<BackgroundProtocolEvent>,
post_response_events: &mut Vec<BackgroundProtocolEvent>,
) -> (bool, Option<moli_core::RendererOutputFence>) {
let mut command = CommandDispatchContext::new(response_flush.clone());
let completion = command.consume_renderer_command_turn_output(output);
ordered_events.extend(command.take_protocol_events());
post_response_events.extend(command.take_post_response_events());
let renderer_output_predecessor = command.take_renderer_output_predecessor();
let Some(output) = completion.into_runtime_inspector_output() else {
tracing::error!("Runtime command turn completed with a non-Runtime reply");
return (false, renderer_output_predecessor);
};
let response_seen = self.route_renderer_runtime_command_output_for_owner_into(
output,
current_cmd_id,
owner,
ordered_events,
);
(response_seen, renderer_output_predecessor)
}
fn route_runtime_inspector_protocol_message_for_owner_with_background_events_into(
&mut self,
message: Value,
current_cmd_id: Option<u64>,
owner: &CommandOwnerScope,
response_events: &mut Vec<BackgroundProtocolEvent>,
background_events: &mut Vec<BackgroundProtocolEvent>,
) -> bool {
let current_session_id = owner.session_id();
let mut message = message;
let id = message.get("id").and_then(Value::as_u64);
match id {
Some(id) => {
let entry = self.remove_pending_inspector_await_for_owner(id, owner);
if let Some(entry) = entry {
let response = OwnerRuntimeResponse::from_pending_inspector_await(
id, entry, owner, message,
);
return self.route_owner_runtime_response_into(
response,
current_cmd_id,
response_events,
background_events,
);
}
if Some(id) == current_cmd_id {
self.register_runtime_remote_object_ids_from_value_for_owner(owner, &message);
let response =
BackgroundCommandResponsePayload::from_owned_runtime_inspector_message(
message,
);
response_events.push(BackgroundProtocolEvent::command_response(
Some(id),
current_session_id,
response,
));
return true;
}
tracing::warn!(
id,
"dropping inspector reply with no matching pending await"
);
}
None => {
if message.get("method").and_then(Value::as_str) == Some("Page.windowOpen") {
let params = message.get("params").unwrap_or(&Value::Null);
let Some(url) = params.get("url").and_then(Value::as_str) else {
tracing::warn!("dropping Page.windowOpen without a string url");
return false;
};
let Some(window_name) = params.get("windowName").and_then(Value::as_str) else {
tracing::warn!("dropping Page.windowOpen without a string windowName");
return false;
};
let Some(window_features) =
params.get("windowFeatures").and_then(Value::as_array)
else {
tracing::warn!("dropping Page.windowOpen without windowFeatures");
return false;
};
let Some(user_gesture) = params.get("userGesture").and_then(Value::as_bool)
else {
tracing::warn!("dropping Page.windowOpen without userGesture");
return false;
};
let Some(window_features) = window_features
.iter()
.map(Value::as_str)
.collect::<Option<Vec<_>>>()
else {
tracing::warn!("dropping Page.windowOpen with a non-string window feature");
return false;
};
let window_features = window_features
.into_iter()
.map(str::to_owned)
.collect::<Vec<_>>();
crate::domains::page::emit_page_window_open_background_events_for_owner(
self,
background_events,
owner,
url,
window_name,
&window_features,
user_gesture,
);
return false;
}
self.register_runtime_remote_object_ids_from_value_for_owner(owner, &message);
if let Some(mut event) =
RuntimeContextProtocolEvent::from_context_protocol_message(message.clone())
{
qualify_runtime_context_protocol_event_for_owner_typed(self, &mut event, owner);
apply_runtime_context_protocol_event_side_effects_for_owner_typed(
self, &event, owner,
);
let mut runtime_context_events = Vec::new();
emit_runtime_context_protocol_background_event_typed(
&mut runtime_context_events,
event,
current_session_id,
);
if current_cmd_id.is_some() {
response_events.extend(runtime_context_events);
} else {
background_events.extend(runtime_context_events);
}
return false;
}
if let Some(session_id) = current_session_id {
message["sessionId"] = json!(session_id);
} else if let Some(map) = message.as_object_mut() {
map.remove("sessionId");
}
background_events.push(protocol_message_background_event(message));
}
}
false
}
pub(crate) async fn route_scheduler_deferred_runtime_inspector_response_into(
&mut self,
mut response: RuntimeInspectorResponseReady,
owner: &CommandOwnerScope,
response_events: &mut Vec<BackgroundProtocolEvent>,
background_events: &mut Vec<BackgroundProtocolEvent>,
) -> (bool, Option<moli_core::RendererOutputFence>) {
response.bind_owner(owner);
let Some(response) = self.resolve_runtime_inspector_response_ready(response) else {
return (false, None);
};
// The V8 response and the renderer turn publication travel over
// separate channels. Preserve the exact Page-stream cursor while
// consuming the response so the command completion cannot overtake
// owner actions (for example popup target creation) produced by the
// same turn.
let (current_cmd_id, output, renderer_output_predecessor) =
response.into_renderer_command_output();
let (renderer_agent_attachment_id, v8_state_update, messages) = output.into_parts();
let mut ordered_events = Vec::new();
let output = RendererRuntimeCommandOutput::from_parts(
renderer_agent_attachment_id,
v8_state_update,
messages,
);
let current_seen = self.route_renderer_runtime_command_output_for_owner_into(
output,
Some(current_cmd_id),
owner,
&mut ordered_events,
);
response_events.extend(ordered_events);
let _ = background_events;
(current_seen, renderer_output_predecessor)
}
pub fn route_registered_runtime_inspector_response_into(
&mut self,
mut response: RuntimeInspectorResponseReady,
response_events: &mut Vec<BackgroundProtocolEvent>,
background_events: &mut Vec<BackgroundProtocolEvent>,
) {
let owner = response
.owner()
.cloned()
.or_else(|| response.session_id().map(CommandOwnerScope::for_session));
let Some(owner) = owner else {
tracing::debug!(
command_id = response.command_id(),
"dropping implicit runtime Inspector response without an exact owner"
);
return;
};
response.bind_owner(&owner);
let Some(response) = self.resolve_runtime_inspector_response_ready(response) else {
return;
};
let message = response.into_protocol_message_for_typed_runtime_route();
self.route_runtime_inspector_protocol_message_for_owner_with_background_events_into(
message,
None,
&owner,
response_events,
background_events,
);
}
pub(crate) fn resolve_runtime_inspector_response_ready(
&mut self,
mut response: RuntimeInspectorResponseReady,
) -> Option<RuntimeInspectorResponseReady> {
if response.has_bound_renderer_call_id() {
return Some(response);
}
let command_id = response.command_id();
let session_id = response.session_id().map(str::to_owned);
let owner = response.owner().cloned();
let correlation = if let Some(renderer_call_id) = response.renderer_call_id() {
let dispatched_attachment_id = response.renderer_agent_attachment_id();
// A lease can complete immediately before attachment cutover while
// its response-ready event is still queued. The registry mapping
// proves that this exact old lease won before rotation; requiring
// the attachment to remain current here would lose that response.
match owner.as_ref() {
Some(owner) => self.take_renderer_call_for_frontend_if_matches_for_owner(
owner,
command_id,
renderer_call_id,
dispatched_attachment_id,
),
None => self.take_renderer_call_for_frontend_if_matches_for_session_owner(
session_id.as_deref(),
command_id,
renderer_call_id,
dispatched_attachment_id,
),
}
} else {
match owner.as_ref() {
Some(owner) => self.take_renderer_call_for_frontend_for_owner(owner, command_id),
None => self.take_renderer_call_for_frontend_for_session_owner(
session_id.as_deref(),
command_id,
),
}
};
let Some(correlation) = correlation else {
tracing::debug!(
command_id,
session_id,
"dropping runtime Inspector response without a pending renderer correlation"
);
return None;
};
debug_assert!(
response.renderer_call_id().is_none()
|| correlation.dispatched_attachment_id()
== response.renderer_agent_attachment_id()
);
response.bind_renderer_call_id(correlation.renderer_call_id());
Some(response)
}
fn restore_frontend_command_ids_in_runtime_messages(
&mut self,
session_id: Option<&str>,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
messages: &mut [RendererRuntimeInspectorMessage],
) {
if dispatched_attachment_id.is_some_and(|attachment_id| {
!self.renderer_agent_attachment_is_current_for_session_owner(session_id, attachment_id)
}) {
return;
}
for message in messages {
let RendererRuntimeInspectorMessage::Protocol(message) = message else {
continue;
};
let Some(renderer_call_id) = message.renderer_call_id() else {
continue;
};
let Some(correlation) = self
.take_frontend_command_for_renderer_if_attachment_matches_for_session_owner(
session_id,
renderer_call_id,
dispatched_attachment_id,
)
else {
continue;
};
debug_assert_eq!(
correlation.dispatched_attachment_id(),
dispatched_attachment_id
);
message.value_mut()["id"] = json!(correlation.frontend_command_id().get());
}
}
fn restore_frontend_command_ids_in_runtime_messages_for_owner(
&mut self,
owner: &CommandOwnerScope,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
messages: &mut [RendererRuntimeInspectorMessage],
) {
if dispatched_attachment_id.is_some_and(|attachment_id| {
self.current_renderer_agent_attachment_id_for_owner(owner) != Some(attachment_id)
}) {
return;
}
for message in messages {
let RendererRuntimeInspectorMessage::Protocol(message) = message else {
continue;
};
let Some(renderer_call_id) = message.renderer_call_id() else {
continue;
};
let Some(correlation) = self
.take_frontend_command_for_renderer_if_attachment_matches_for_owner(
owner,
renderer_call_id,
dispatched_attachment_id,
)
else {
continue;
};
debug_assert_eq!(
correlation.dispatched_attachment_id(),
dispatched_attachment_id
);
message.value_mut()["id"] = json!(correlation.frontend_command_id().get());
}
}
/// Resolves terminal responses carried by a concrete renderer DevTools
/// session stream.
///
/// The command owner, renderer call id, and attachment id form the complete
/// response authority. Document observations are validated separately, so
/// a response that won its lease immediately before a navigation can still
/// settle the session without granting the retired document permission to
/// publish notifications or mutate replacement-document state.
pub(crate) fn restore_frontend_command_ids_in_devtools_session_output_for_owner(
&mut self,
owner: &CommandOwnerScope,
dispatched_attachment_id: Option<RendererAgentAttachmentId>,
messages: &mut Vec<RendererRuntimeInspectorMessage>,
project_runtime_object_ownership: bool,
) {
let session_id = owner.session_id().map(str::to_owned);
messages.retain_mut(|message| {
let RendererRuntimeInspectorMessage::Protocol(message) = message else {
return true;
};
let Some(renderer_call_id) = message.renderer_call_id() else {
return true;
};
let descriptor = self
.renderer_command_descriptor_for_renderer_if_attachment_matches_for_owner(
owner,
renderer_call_id,
dispatched_attachment_id,
);
let result_object_group = descriptor.as_ref().and_then(|descriptor| {
self.runtime_result_object_group_for_renderer_command_descriptor(
session_id.as_deref(),
descriptor,
)
});
let Some(correlation) = self
.take_frontend_command_for_renderer_if_attachment_matches_for_owner(
owner,
renderer_call_id,
dispatched_attachment_id,
)
else {
tracing::debug!(
session_id = session_id.as_deref(),
renderer_call_id = renderer_call_id.get(),
attachment_id = ?dispatched_attachment_id.map(RendererAgentAttachmentId::get),
"dropping DevTools session response without a live renderer correlation"
);
return false;
};
let frontend_command_id = correlation.frontend_command_id().get();
message.value_mut()["id"] = json!(frontend_command_id);
if project_runtime_object_ownership && message.value().get("result").is_some() {
if let Some(object_group) = result_object_group.as_deref() {
self.register_runtime_remote_object_ids_from_value_for_owner_with_group(
owner,
message.value(),
object_group,
);
} else {
self.register_runtime_remote_object_ids_from_value_for_owner(
owner,
message.value(),
);
}
}
self.complete_runtime_await_job(frontend_command_id, session_id.as_deref());
let _ = self.remove_pending_inspector_await_for_owner(frontend_command_id, owner);
true
});
}
fn runtime_result_object_group_for_renderer_command_descriptor(
&self,
session_id: Option<&str>,
descriptor: &RendererCommandDescriptor,
) -> Option<String> {
let command = serde_json::from_str::<Value>(descriptor.frontend_payload()).ok()?;
let method = command.get("method")?.as_str()?;
let params = command.get("params")?.as_object()?;
match method {
"Runtime.evaluate" | "Runtime.runScript" => params
.get("objectGroup")
.and_then(Value::as_str)
.map(str::to_owned),
"Runtime.callFunctionOn" => params
.get("objectGroup")
.and_then(Value::as_str)
.map(str::to_owned)
.or_else(|| {
self.runtime_remote_object_group_for_session_owner(
session_id,
params.get("objectId")?.as_str()?,
)
}),
"Runtime.getProperties" => self.runtime_remote_object_group_for_session_owner(
session_id,
params.get("objectId")?.as_str()?,
),
"Runtime.awaitPromise" => self.runtime_remote_object_group_for_session_owner(
session_id,
params.get("promiseObjectId")?.as_str()?,
),
"Runtime.queryObjects" => params
.get("objectGroup")
.and_then(Value::as_str)
.map(str::to_owned)
.or_else(|| {
self.runtime_remote_object_group_for_session_owner(
session_id,
params.get("prototypeObjectId")?.as_str()?,
)
}),
_ => None,
}
}
fn start_or_enqueue_registered_runtime_inspector_response_ready(
&self,
command_id: u64,
owner: &CommandOwnerScope,
mut response_rx: RuntimeInspectorResponseReceiver,
) -> bool {
let Some(response_tx) = self.runtime_inspector_response_ready_sender() else {
return false;
};
let owner = owner.clone();
// Keep both completion timings on the same response-ready lane. If the
// renderer callback has already completed, enqueue it immediately; if
// not, spawn a waiter that will enqueue the same event later.
match response_rx.try_recv() {
Ok(completion) => {
let _ = response_tx.send(crate::conn::RuntimeInspectorResponseReady::for_owner(
command_id,
&owner,
Ok(completion),
));
return true;
}
Err(tokio::sync::oneshot::error::TryRecvError::Closed) => {
let _ = response_tx.send(crate::conn::RuntimeInspectorResponseReady::for_owner(
command_id,
&owner,
Err("RuntimeInspectorResponseCanceled".to_owned()),
));
return true;
}
Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {}
}
tokio::task::spawn_local(async move {
let response = response_rx
.await
.map_err(|_| "RuntimeInspectorResponseCanceled".to_owned());
let _ = response_tx.send(crate::conn::RuntimeInspectorResponseReady::for_owner(
command_id, &owner, response,
));
});
true
}
fn route_owner_runtime_response_into(
&mut self,
response: OwnerRuntimeResponse,
current_cmd_id: Option<u64>,
response_events: &mut Vec<BackgroundProtocolEvent>,
background_events: &mut Vec<BackgroundProtocolEvent>,
) -> bool {
let command_id = response.command_id;
self.complete_runtime_await_job(command_id, response.session_id());
self.trace_owner_runtime_response_route(&response);
let current_seen = Some(command_id) == current_cmd_id;
match self.route_bidi_channel_listener_owner_runtime_response(&response) {
BidiChannelListenerRoute::NotListener => {}
BidiChannelListenerRoute::Consumed => return current_seen,
BidiChannelListenerRoute::Event(event) => {
background_events.push(event);
return current_seen;
}
}
let routed_session_id = response.session_id().map(str::to_owned);
if let Some(object_group) = response.object_group() {
self.register_runtime_remote_object_ids_from_value_for_owner_with_group(
response.owner(),
&response.message,
object_group,
);
} else {
self.register_runtime_remote_object_ids_from_value_for_owner(
response.owner(),
&response.message,
);
}
let mut message = response.into_protocol_message();
if let Some(session_id) = routed_session_id.as_deref() {
message["sessionId"] = json!(session_id);
} else if let Some(map) = message.as_object_mut() {
map.remove("sessionId");
}
response_events.push(protocol_message_background_event(message));
current_seen
}
fn trace_owner_runtime_response_route(&mut self, response: &OwnerRuntimeResponse) {
let current_route = self.runtime_await_owner_route_for_session(response.session_id());
let response_owner_route = response.owner().resolve_route(self);
if current_route != response_owner_route {
tracing::debug!(
command_id = response.command_id,
session_id = response.session_id(),
?response_owner_route,
current_owner_route = ?current_route,
"owner runtime response route no longer matches current session owner"
);
}
self.record_runtime_await_trace(
"owner_runtime_response_route",
Some(response.command_id),
response.session_id(),
json!({
"ownerRoute": response_owner_route.as_ref().map(|route| format!("{route:?}")),
"currentOwnerRoute": current_route.as_ref().map(|route| format!("{route:?}")),
}),
);
}
fn route_bidi_channel_listener_owner_runtime_response(
&mut self,
response: &OwnerRuntimeResponse,
) -> BidiChannelListenerRoute {
let Some(residence) = response.bidi_channel_listener().cloned() else {
return BidiChannelListenerRoute::NotListener;
};
let owner = residence.owner().clone();
if !owner.is_current(self) {
tracing::debug!(
command_id = response.command_id,
session_id = owner.session_id(),
"discarding BiDi channel listener reply for a stale Page attachment"
);
return BidiChannelListenerRoute::Consumed;
}
let listener = residence.listener();
let message = &response.message;
if let Some(error) = message.get("error") {
tracing::debug!(
?error,
channel = %listener.properties().channel,
"BiDi channel listener stopped after inspector error"
);
self.publish_bidi_channel_object_group_release(
owner,
listener.channel_object_group().to_owned(),
);
return BidiChannelListenerRoute::Consumed;
}
let result = message.get("result").unwrap_or(&Value::Null);
if let Some(exception_details) = result.get("exceptionDetails") {
tracing::debug!(
?exception_details,
channel = %listener.properties().channel,
"BiDi channel listener stopped after JavaScript exception"
);
self.publish_bidi_channel_object_group_release(
owner,
listener.channel_object_group().to_owned(),
);
return BidiChannelListenerRoute::Consumed;
}
let remote = result.get("result").unwrap_or(&Value::Null);
let properties = listener.properties().clone();
let realm_id = listener.realm_id().clone();
let data = DevToolsRemoteValue::from_cdp_remote_object(
remote,
matches!(properties.ownership, DevToolsResultOwnership::Root),
Some(realm_id.clone()),
);
if let Some(remote_object_id) = data.handle.as_ref().or(data.shared_id.as_ref()) {
self.register_runtime_remote_object_ids_for_owner_with_realm(
owner.command_owner(),
vec![remote_object_id.as_str().to_owned()],
realm_id.as_str(),
);
}
let event = BackgroundProtocolEvent::immediate_automation_event(
json!({
"method": "Moli.scriptMessage",
"params": {}
}),
AutomationEvent::ScriptMessage(ScriptMessageEvent {
target_id: Some(listener.target_id().clone()),
realm_id: Some(realm_id),
channel: properties.channel.clone(),
data,
}),
);
self.publish_bidi_channel_listener_start(residence);
BidiChannelListenerRoute::Event(event)
}
pub(crate) async fn document_node_snapshot_for_runtime_remote_object_id_for_owner_async(
&mut self,
owner: &CommandOwnerScope,
object_id: &str,
depth: i32,
pierce: bool,
) -> Result<Option<DocumentNodeObjectSnapshot>, String> {
let include_whitespace =
crate::domains::dom::dom_agent_includes_whitespace_for_owner(self, owner);
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_owner(owner);
let pending = {
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.start_document_node_snapshot_for_object_id_in_inspector_session(
inspector_session_id,
include_whitespace,
object_id,
depth,
pierce,
)
.map_err(|error| format!("resolve runtime node snapshot failed: {error}"))?
};
let completion = pending
.wait()
.await
.map_err(|error| format!("resolve runtime node snapshot failed: {error}"))?;
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.finish_document_node_snapshot_for_object_id(completion)
.map_err(|error| format!("resolve runtime node snapshot failed: {error}"))
}
pub(crate) async fn document_node_snapshot_for_backend_node_id_for_owner_async(
&mut self,
owner: &CommandOwnerScope,
backend_node_id: u32,
depth: i32,
pierce: bool,
) -> Result<Option<DocumentNodeObjectSnapshot>, String> {
let pending = {
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.start_document_node_snapshot_for_backend_node_id(backend_node_id, depth, pierce)
.map_err(|error| format!("resolve backend node snapshot failed: {error}"))?
};
let completion = pending
.wait()
.await
.map_err(|error| format!("resolve backend node snapshot failed: {error}"))?;
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.finish_document_node_snapshot_for_backend_node_id(completion)
.map_err(|error| format!("resolve backend node snapshot failed: {error}"))
}
pub(crate) async fn register_document_bidi_node_binding_for_owner_async(
&mut self,
owner: &CommandOwnerScope,
shared_id: &str,
backend_node_id: u32,
) -> Result<(), String> {
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_owner(owner);
let pending = {
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.start_register_document_bidi_node_binding(
inspector_session_id,
shared_id.to_owned(),
backend_node_id,
)
.map_err(|error| format!("register BiDi node binding failed: {error}"))?
};
let completion = pending
.wait()
.await
.map_err(|error| format!("register BiDi node binding failed: {error}"))?;
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.finish_register_document_bidi_node_binding(completion)
.map_err(|error| format!("register BiDi node binding failed: {error}"))
}
pub(crate) async fn document_bidi_node_binding_for_owner_async(
&mut self,
owner: &CommandOwnerScope,
shared_id: &str,
) -> Result<RendererDomBidiNodeBindingResolution, String> {
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_owner(owner);
let pending = {
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.start_document_bidi_node_binding(inspector_session_id, shared_id.to_owned())
.map_err(|error| format!("resolve BiDi node binding failed: {error}"))?
};
let completion = pending
.wait()
.await
.map_err(|error| format!("resolve BiDi node binding failed: {error}"))?;
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.finish_document_bidi_node_binding(completion)
.map_err(|error| format!("resolve BiDi node binding failed: {error}"))
}
pub(crate) async fn document_bidi_node_shared_id_for_backend_node_id_for_owner_async(
&mut self,
owner: &CommandOwnerScope,
backend_node_id: u32,
) -> Result<RendererDomBidiNodeSharedIdResolution, String> {
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_owner(owner);
let pending = {
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.start_document_bidi_node_shared_id_for_backend_node_id(
inspector_session_id,
backend_node_id,
)
.map_err(|error| format!("resolve BiDi node shared id failed: {error}"))?
};
let completion = pending
.wait()
.await
.map_err(|error| format!("resolve BiDi node shared id failed: {error}"))?;
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.finish_document_bidi_node_shared_id_for_backend_node_id(completion)
.map_err(|error| format!("resolve BiDi node shared id failed: {error}"))
}
pub(crate) async fn runtime_remote_object_for_backend_node_id_for_owner_async(
&mut self,
owner: &CommandOwnerScope,
backend_node_id: u32,
execution_context_id: Option<i64>,
object_group: Option<&str>,
) -> Result<Option<Value>, String> {
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_owner(owner);
let pending = {
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.start_resolve_runtime_object_for_backend_node_id_in_inspector_session(
inspector_session_id,
backend_node_id,
execution_context_id,
object_group,
)
.map_err(|error| format!("resolve runtime object for backend node failed: {error}"))?
};
let completion = pending
.wait()
.await
.map_err(|error| format!("resolve runtime object for backend node failed: {error}"))?;
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
let result = page
.finish_resolve_runtime_object_for_backend_node_id(completion)
.map_err(|error| format!("resolve runtime object for backend node failed: {error}"))?;
match result {
DocumentNodeRuntimeObjectResolution::Found(remote_object) => {
Ok(Some(remote_object.into_protocol_value()))
}
DocumentNodeRuntimeObjectResolution::MissingNode => Ok(None),
DocumentNodeRuntimeObjectResolution::MissingContext => Err(
"resolve runtime object for backend node failed: missing execution context"
.to_owned(),
),
}
}
pub async fn evaluate_runtime_expression_with_await_async(
&mut self,
expression: &str,
await_promise: bool,
) -> Result<Value, String> {
self.evaluate_runtime_expression_with_await_for_session_owner_async(
None,
expression,
await_promise,
)
.await
}
#[cfg(test)]
pub(crate) async fn evaluate_runtime_expression_for_session_owner_async(
&mut self,
session_id: Option<&str>,
expression: &str,
) -> Result<Value, String> {
self.evaluate_runtime_expression_with_await_for_session_owner_async(
session_id, expression, false,
)
.await
}
pub(crate) async fn evaluate_runtime_expression_with_await_for_session_owner_async(
&mut self,
session_id: Option<&str>,
expression: &str,
await_promise: bool,
) -> Result<Value, String> {
self.evaluate_runtime_expression_for_session_owner_once_async(
session_id,
expression,
await_promise,
)
.await
}
async fn evaluate_runtime_expression_for_session_owner_once_async(
&mut self,
session_id: Option<&str>,
expression: &str,
await_promise: bool,
) -> Result<Value, String> {
let payload = {
let page = self.runtime_session_owner_page_mut(session_id)?;
page.evaluate_runtime_expression_without_navigation_follow_with_await_async(
expression,
await_promise,
)
.await
.map_err(|error| format!("runtime evaluation failed: {error}"))?
};
self.ingest_runtime_session_owner_output_updates(session_id);
Ok(payload)
}
#[cfg(test)]
pub async fn dispatch_runtime_protocol_message_async(
&mut self,
raw_json: &str,
) -> Result<Vec<Value>, String> {
self.dispatch_runtime_protocol_message_for_session_owner_async(None, raw_json)
.await
}
pub(crate) fn start_runtime_enable_events_for_owner(
&mut self,
owner: &CommandOwnerScope,
) -> Result<PendingRuntimeEnableEventsDispatch, String> {
let route = self.runtime_protocol_message_page_route_for_owner(owner)?;
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_owner(owner);
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
let pending = page
.start_runtime_enable_events_for_inspector_session(inspector_session_id.as_deref())
.map_err(|error| format!("runtime enable event replay failed: {error}"))?;
Ok(PendingRuntimeEnableEventsDispatch {
owner: owner.clone(),
route,
pending,
})
}
pub(crate) fn complete_runtime_enable_events(
&mut self,
completed: CompletedRuntimeEnableEventsDispatch,
) -> Result<RuntimeEnableEventsReplay, String> {
let owner = completed.owner;
let session_id = owner.session_id();
let page = self.runtime_protocol_message_started_page_mut(&completed.route)?;
let output = page
.finish_runtime_enable_output(completed.completion)
.map_err(|error| format!("runtime enable event replay failed: {error}"))?;
let (attachment_id, v8_state_update, messages) = output.into_parts();
if attachment_id != Some(completed.route.renderer_agent_attachment_id) {
return Err(
"Runtime.enable completed from an unexpected renderer attachment".to_owned(),
);
}
if let Some(state) = v8_state_update
&& !self.merge_v8_inspector_session_state_for_owner(&owner, state)
{
return Err("Runtime.enable completed after session owner disappeared".to_owned());
}
let mut replay = RuntimeEnableEventsReplay::from_renderer_messages(messages);
let _ =
self.set_renderer_runtime_agent_owns_page_console_api_events_for_owner(&owner, true);
self.ingest_runtime_session_owner_output_updates_for_owner(&owner);
for event in replay.events_mut() {
match event {
RuntimeEnableReplayEvent::Context(event) => {
qualify_runtime_context_protocol_event_for_owner_typed(self, event, &owner);
}
RuntimeEnableReplayEvent::Background(event) => {
event.ensure_protocol_session_id(session_id);
}
}
}
Ok(replay)
}
fn runtime_session_owner_page_mut(
&mut self,
session_id: Option<&str>,
) -> Result<&mut Page, String> {
self.loaded_page_mut_for_protocol_access(session_id)
}
fn runtime_session_owner_page_mut_for_owner(
&mut self,
owner: &CommandOwnerScope,
) -> Result<&mut Page, String> {
self.loaded_page_mut_for_protocol_access_for_owner(owner)
}
fn runtime_session_owner_page_mut_for_interruptible_control_for_owner(
&mut self,
owner: &CommandOwnerScope,
) -> Result<&mut Page, String> {
self.loaded_page_mut_for_interruptible_protocol_access_for_owner(owner)
}
fn runtime_protocol_message_page_route_for_session_owner(
&self,
session_id: Option<&str>,
) -> Result<RuntimeProtocolMessagePageRoute, String> {
let (browser_context_id, target_id) = self
.target_owner_identity_for_session(session_id)
.ok_or_else(|| "NoDocumentLoaded".to_owned())?;
let slot = self.runtime_session_owner_slot(session_id)?;
let renderer_agent_attachment_id = slot
.current_renderer_attachment()
.ok_or_else(|| "NoDocumentLoaded".to_owned())?
.id();
Ok(RuntimeProtocolMessagePageRoute {
browser_context_id,
target_id,
renderer_agent_attachment_id,
})
}
fn runtime_protocol_message_page_route_for_owner(
&self,
owner: &CommandOwnerScope,
) -> Result<RuntimeProtocolMessagePageRoute, String> {
let (browser_context_id, target_id) = self
.target_owner_identity_for_owner(owner)
.ok_or_else(|| "NoDocumentLoaded".to_owned())?;
let slot = self.runtime_session_owner_slot_for_owner(owner)?;
let renderer_agent_attachment_id = slot
.current_renderer_attachment()
.ok_or_else(|| "NoDocumentLoaded".to_owned())?
.id();
Ok(RuntimeProtocolMessagePageRoute {
browser_context_id,
target_id,
renderer_agent_attachment_id,
})
}
fn runtime_protocol_message_started_slot_mut(
&mut self,
route: &RuntimeProtocolMessagePageRoute,
) -> Result<&mut TargetRuntimeSlot, String> {
let browser_context = self
.browser_context_by_id_mut(&route.browser_context_id)
.ok_or_else(|| "NoDocumentLoaded".to_owned())?;
let slot = if browser_context.active_target_id() == route.target_id.as_deref() {
&mut browser_context.active_page_target_mut().runtime_slot
} else {
let target_id = route
.target_id
.as_deref()
.ok_or_else(|| "NoDocumentLoaded".to_owned())?;
browser_context
.background_target_mut(target_id)
.map(|target| &mut target.runtime_slot)
.ok_or_else(|| "NoDocumentLoaded".to_owned())?
};
if slot
.current_renderer_attachment()
.map(|attachment| attachment.id())
!= Some(route.renderer_agent_attachment_id)
{
return Err("Renderer attachment changed".to_owned());
}
Ok(slot)
}
fn runtime_protocol_message_started_page_mut(
&mut self,
route: &RuntimeProtocolMessagePageRoute,
) -> Result<&mut Page, String> {
self.runtime_protocol_message_started_slot_mut(route)?
.loaded_page_mut()
.ok_or_else(|| "NoDocumentLoaded".to_owned())
}
fn consume_runtime_protocol_message_completion(
&mut self,
route: &RuntimeProtocolMessagePageRoute,
completion: moli_core::page::CompletedPageCommand,
) -> Result<RendererCommandTurnOutput, String> {
let output = if let Ok(page) = self.runtime_protocol_message_started_page_mut(route) {
page.finish_runtime_protocol_message_command_turn(completion)
} else {
// Completion means the renderer owner has already committed the
// command's Page state and concrete protocol publication. The
// target can install a successor attachment before this protocol
// task resumes (for example, form.submit() followed by a normal
// command response). Preserve that immutable result; there is
// simply no current Page cache belonging to this route to update.
completion.into_runtime_protocol_message_command_turn()
};
output.map_err(|error| format!("runtime inspector dispatch failed: {error}"))
}
fn ingest_runtime_protocol_message_started_route_output_updates(
&mut self,
route: &RuntimeProtocolMessagePageRoute,
) {
if let Ok(slot) = self.runtime_protocol_message_started_slot_mut(route) {
let _ = slot.ingest_owner_page_observable_output_updates();
}
}
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 target = self
.browser_context_by_id(&browser_context_id)
.and_then(|context| context.shared_worker_target(&target_id))
.ok_or_else(|| "UnknownSession".to_owned())?;
Ok(SharedWorkerRuntimeTargetRoute {
browser_context_id,
worker: WorkerRuntimeTarget::Shared(target.renderer_instance_id),
})
}
CdpSessionRoute::DedicatedWorkerTarget {
browser_context_id,
target_id,
} => {
let target = self
.browser_context_by_id(&browser_context_id)
.and_then(|context| context.dedicated_worker_target(&target_id))
.ok_or_else(|| "UnknownSession".to_owned())?;
Ok(SharedWorkerRuntimeTargetRoute {
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 renderer_runtime = self
.browser_context_by_id(&route.browser_context_id)
.map(|context| context.renderer_runtime())
.ok_or_else(|| "UnknownSession".to_owned())?;
Ok(renderer_runtime.run_dedicated_worker_if_waiting_for_debugger_for_devtools(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 {
browser_context_id,
target_id,
} = self
.session_route(Some(session_id))
.ok_or_else(|| "UnknownSession".to_owned())?
else {
return Err("UnknownSession".to_owned());
};
let target = self
.browser_context_by_id(&browser_context_id)
.and_then(|context| context.service_worker_target(&target_id))
.ok_or_else(|| "UnknownSession".to_owned())?;
Ok(ServiceWorkerRuntimeTargetRoute {
browser_context_id,
version_id: target.renderer_version_id,
})
}
pub(crate) fn start_shared_worker_runtime_protocol_message_for_session(
&mut self,
session_id: Option<&str>,
raw_json: String,
) -> Result<PendingSharedWorkerRuntimeProtocolMessageDispatch, String> {
let raw_json =
self.rewrite_runtime_inspector_command_for_session_owner(session_id, &raw_json, None)?;
self.start_shared_worker_runtime_protocol_message_for_session_with_optional_deferred_response(
session_id,
raw_json,
None,
RuntimeProtocolResponseRoute::adapter_reply_without_receiver(),
)
}
pub(crate) fn start_shared_worker_runtime_protocol_message_for_session_with_deferred_response(
&mut self,
session_id: Option<&str>,
descriptor: RendererCommandDescriptor,
command_id: u64,
) -> Result<PendingSharedWorkerRuntimeProtocolMessageDispatch, String> {
self.shared_worker_runtime_target_for_session(session_id)?;
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(
session_id,
raw_json,
Some(response_sender),
response_route,
)
}
fn start_shared_worker_runtime_protocol_message_for_session_with_optional_deferred_response(
&mut self,
session_id: Option<&str>,
raw_json: String,
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_id(&route.browser_context_id)
.map(|context| context.renderer_runtime())
.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,
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());
}
};
Ok(PendingSharedWorkerRuntimeProtocolMessageDispatch {
session_id: session_id.map(str::to_owned),
pending,
response_route,
})
}
pub(crate) async fn dispatch_shared_worker_runtime_helper_protocol_message_for_session_async(
&mut self,
session_id: Option<&str>,
raw_json: &str,
command_id: u64,
) -> anyhow::Result<Vec<RendererRuntimeInspectorMessage>> {
let descriptor = RendererCommandDescriptor::from_synthesized_payload(raw_json.to_owned())
.map_err(anyhow::Error::msg)?;
let pending = self
.start_shared_worker_runtime_protocol_message_for_session_with_deferred_response(
session_id, descriptor, command_id,
)
.map_err(anyhow::Error::msg)?;
let mut completed = pending.wait().await.map_err(anyhow::Error::msg)?;
let response_rx = completed.take_deferred_response_receiver();
let mut messages = self
.complete_shared_worker_runtime_protocol_message_for_session(completed)
.map_err(anyhow::Error::msg)?;
if let Some(response_rx) = response_rx
&& let Some(message) = self
.await_registered_runtime_inspector_response_for_session_owner_async(
session_id,
command_id,
response_rx,
)
.await
{
messages.push(message);
}
Ok(messages)
}
async fn dispatch_service_worker_runtime_helper_protocol_message_for_session_async(
&mut self,
session_id: Option<&str>,
raw_json: &str,
command_id: u64,
) -> anyhow::Result<Vec<RendererRuntimeInspectorMessage>> {
let descriptor = RendererCommandDescriptor::from_synthesized_payload(raw_json.to_owned())
.map_err(anyhow::Error::msg)?;
let pending = self
.start_service_worker_runtime_protocol_message_for_session_with_deferred_response(
session_id, descriptor, command_id,
)
.map_err(anyhow::Error::msg)?;
let mut completed = pending.wait().await.map_err(anyhow::Error::msg)?;
let response_rx = completed.take_deferred_response_receiver();
let mut messages = self
.complete_service_worker_runtime_protocol_message_for_session(completed)
.map_err(anyhow::Error::msg)?;
if let Some(response_rx) = response_rx
&& let Some(message) = self
.await_registered_runtime_inspector_response_for_session_owner_async(
session_id,
command_id,
response_rx,
)
.await
{
messages.push(message);
}
Ok(messages)
}
pub(crate) fn complete_shared_worker_runtime_protocol_message_for_session(
&mut self,
mut completed: CompletedSharedWorkerRuntimeProtocolMessageDispatch,
) -> Result<Vec<RendererRuntimeInspectorMessage>, String> {
self.restore_frontend_command_ids_in_runtime_messages(
completed.session_id.as_deref(),
None,
&mut completed.dispatch.messages,
);
Ok(completed.dispatch.messages)
}
pub(crate) fn start_service_worker_runtime_protocol_message_for_session(
&mut self,
session_id: Option<&str>,
raw_json: String,
) -> Result<PendingServiceWorkerRuntimeProtocolMessageDispatch, String> {
let raw_json =
self.rewrite_runtime_inspector_command_for_session_owner(session_id, &raw_json, None)?;
self.start_service_worker_runtime_protocol_message_for_session_with_optional_deferred_response(
session_id,
raw_json,
None,
RuntimeProtocolResponseRoute::adapter_reply_without_receiver(),
)
}
pub(crate) fn start_service_worker_runtime_protocol_message_for_session_with_deferred_response(
&mut self,
session_id: Option<&str>,
descriptor: RendererCommandDescriptor,
command_id: u64,
) -> Result<PendingServiceWorkerRuntimeProtocolMessageDispatch, String> {
self.service_worker_runtime_target_for_session(session_id)?;
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(
session_id,
raw_json,
Some(response_sender),
response_route,
)
}
fn start_service_worker_runtime_protocol_message_for_session_with_optional_deferred_response(
&mut self,
session_id: Option<&str>,
raw_json: String,
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_id(&route.browser_context_id)
.map(|context| context.renderer_runtime())
.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())
}
}
});
Ok(PendingServiceWorkerRuntimeProtocolMessageDispatch {
session_id: session_id.map(str::to_owned),
pending,
response_route,
})
}
pub(crate) fn complete_service_worker_runtime_protocol_message_for_session(
&mut self,
mut completed: CompletedServiceWorkerRuntimeProtocolMessageDispatch,
) -> Result<Vec<RendererRuntimeInspectorMessage>, String> {
self.restore_frontend_command_ids_in_runtime_messages(
completed.session_id.as_deref(),
None,
&mut completed.dispatch.messages,
);
Ok(completed.dispatch.messages)
}
pub(crate) fn start_moli_diagnostics(
&mut self,
) -> Result<PendingMoliDiagnosticsDispatch, String> {
let mut pending = Vec::new();
if let Some(browser_context) = self.browser_context.as_mut() {
collect_moli_diagnostics_pending_snapshots(browser_context, &mut pending)?;
}
for browser_context in &mut self.inactive_browser_contexts {
collect_moli_diagnostics_pending_snapshots(browser_context, &mut pending)?;
}
Ok(PendingMoliDiagnosticsDispatch { pending })
}
pub(crate) fn complete_moli_diagnostics(
&mut self,
completed: CompletedMoliDiagnosticsDispatch,
) -> Value {
let mut dedicated_worker_loading_count = 0;
let mut dedicated_worker_running_worker_isolate_count = 0;
let mut document_context_count = 0;
let mut isolated_world_context_count = 0;
let mut child_default_context_count = 0;
let mut failed_page_snapshot_count = 0;
for completed in completed.completed {
let Some(page) = self
.browser_context_by_id_mut(&completed.browser_context_id)
.and_then(|browser_context| {
if let Some(target_id) = completed.target_id.as_deref() {
browser_context
.background_target_mut(target_id)
.and_then(|target| target.loaded_page_mut())
} else {
browser_context
.active_page_target_mut()
.runtime_slot
.loaded_page_mut()
}
})
else {
failed_page_snapshot_count += 1;
continue;
};
let Ok(completion) = completed.completion else {
failed_page_snapshot_count += 1;
continue;
};
let Ok(snapshot) = page.finish_page_diagnostics_snapshot(completion) else {
failed_page_snapshot_count += 1;
continue;
};
document_context_count += snapshot.diagnostics.document_context_count;
isolated_world_context_count += snapshot.diagnostics.isolated_world_context_count;
child_default_context_count += snapshot.diagnostics.child_default_context_count;
dedicated_worker_loading_count += snapshot.diagnostics.dedicated_worker_loading_count;
dedicated_worker_running_worker_isolate_count += snapshot
.diagnostics
.dedicated_worker_running_worker_isolate_count;
}
let estimated_document_isolate_count = self
.browser_contexts()
.map(|browser_context| {
browser_context.loaded_document_page_count()
+ browser_context.pending_document_page_build_count()
})
.sum::<usize>();
let shared_worker_running_worker_isolate_count = self
.browser_contexts()
.map(|browser_context| {
browser_context
.shared_worker_runtime_diagnostics_for_diagnostics()
.running_worker_isolate_count
})
.sum::<usize>();
let estimated_worker_isolate_count = dedicated_worker_running_worker_isolate_count
+ shared_worker_running_worker_isolate_count;
let estimated_live_v8_isolate_count =
estimated_document_isolate_count + estimated_worker_isolate_count;
let mut diagnostics = self.moli_memory_diagnostics();
diagnostics["isolateScope"]["documentContextCount"] = json!(document_context_count);
diagnostics["isolateScope"]["isolatedWorldContextCount"] =
json!(isolated_world_context_count);
diagnostics["isolateScope"]["childDefaultContextCount"] =
json!(child_default_context_count);
diagnostics["isolateScope"]["dedicatedWorkerLoadingCount"] =
json!(dedicated_worker_loading_count);
diagnostics["isolateScope"]["dedicatedWorkerRunningWorkerIsolateCount"] =
json!(dedicated_worker_running_worker_isolate_count);
diagnostics["isolateScope"]["dedicatedWorkerDiagnosticsFailedPageSnapshotCount"] =
json!(failed_page_snapshot_count);
diagnostics["isolateScope"]["estimatedWorkerIsolateCount"] =
json!(estimated_worker_isolate_count);
diagnostics["isolateScope"]["estimatedLiveV8IsolateCount"] =
json!(estimated_live_v8_isolate_count);
diagnostics
}
pub(crate) fn ingest_runtime_session_owner_output_updates(&mut self, session_id: Option<&str>) {
let owner = CommandOwnerScope::capture(self, session_id);
self.ingest_runtime_session_owner_output_updates_for_owner(&owner)
}
pub(crate) fn ingest_runtime_session_owner_output_updates_for_owner(
&mut self,
owner: &CommandOwnerScope,
) {
if let Ok(slot) = self.runtime_session_owner_slot_mut_for_owner(owner) {
let _ = slot.ingest_owner_page_observable_output_updates();
}
}
pub(crate) fn runtime_session_owner_frame_id(
&self,
session_id: Option<&str>,
) -> Option<String> {
let owner = CommandOwnerScope::capture(self, session_id);
self.runtime_session_owner_frame_id_for_owner(&owner)
}
pub(crate) fn runtime_session_owner_frame_id_for_owner(
&self,
owner: &CommandOwnerScope,
) -> Option<String> {
match owner.resolve_route(self)? {
CdpSessionRoute::Browser => self
.browser_context
.as_ref()
.and_then(|bc| bc.active_target_id_owned()),
CdpSessionRoute::BrowserContext { browser_context_id } => self
.browser_context_by_id(&browser_context_id)
.and_then(|bc| bc.active_target_id_owned()),
CdpSessionRoute::PageTarget { target_id, .. } => Some(target_id),
CdpSessionRoute::TabTarget { .. }
| CdpSessionRoute::SharedWorkerTarget { .. }
| CdpSessionRoute::DedicatedWorkerTarget { .. }
| CdpSessionRoute::ServiceWorkerTarget { .. } => None,
}
}
#[cfg(test)]
async fn dispatch_runtime_protocol_message_for_session_owner_async(
&mut self,
session_id: Option<&str>,
raw_json: &str,
) -> Result<Vec<Value>, String> {
// Direct raw protocol compatibility still accepts id-bearing messages.
// Internal helpers should call the explicit helper variant so the
// callback owner is visible at the call site.
if let Some(command_id) = runtime_protocol_message_id(raw_json) {
return Ok(self
.dispatch_runtime_helper_protocol_message_for_session_owner_async(
session_id, raw_json, command_id,
)
.await?
.into_iter()
.map(RendererRuntimeInspectorMessage::into_v8_inspector_message)
.collect());
}
let timing_started = moli_trace::cdp_nav_timing_enabled().then(std::time::Instant::now);
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_session(session_id);
let messages = {
let page = self.runtime_session_owner_page_mut(session_id)?;
page.dispatch_runtime_protocol_message_for_inspector_session_async(
inspector_session_id,
raw_json,
)
.await
.map_err(|error| format!("runtime inspector dispatch failed: {error}"))?
.into_iter()
.map(RendererRuntimeInspectorMessage::into_v8_inspector_message)
.collect::<Vec<_>>()
};
if let Some(started) = timing_started {
tracing::info!(
target: "moli_cdp_nav_timing",
stage = "runtime_inspector_page_dispatch_done",
messages = messages.len(),
elapsed_ms = started.elapsed().as_millis(),
);
}
self.ingest_runtime_session_owner_output_updates(session_id);
if let Some(started) = timing_started {
tracing::info!(
target: "moli_cdp_nav_timing",
stage = "runtime_inspector_output_ingested",
elapsed_ms = started.elapsed().as_millis(),
);
}
Ok(messages)
}
#[cfg(test)]
pub(crate) async fn dispatch_runtime_helper_protocol_message_for_session_owner_async(
&mut self,
session_id: Option<&str>,
raw_json: &str,
command_id: u64,
) -> Result<Vec<RendererRuntimeInspectorMessage>, String> {
let descriptor = RendererCommandDescriptor::from_synthesized_payload(raw_json.to_owned())?;
let owner = CommandOwnerScope::capture(self, session_id);
let pending = self.start_runtime_protocol_message_for_owner_with_deferred_response(
&owner, descriptor, command_id,
)?;
let completed = pending.wait().await?;
self.complete_runtime_helper_protocol_message_for_session_owner_async(completed, command_id)
.await
}
pub(crate) async fn await_registered_runtime_inspector_response_for_session_owner_async(
&mut self,
session_id: Option<&str>,
command_id: u64,
response_rx: RuntimeInspectorResponseReceiver,
) -> Option<RendererRuntimeInspectorMessage> {
let response = crate::conn::RuntimeInspectorResponseReady::new(
command_id,
session_id,
response_rx
.await
.map_err(|_| "RuntimeInspectorResponseCanceled".to_owned()),
);
let mut response = self.resolve_runtime_inspector_response_ready(response)?;
if response
.renderer_agent_attachment_id()
.is_some_and(|attachment_id| {
!self.renderer_agent_attachment_is_current_for_session_owner(
session_id,
attachment_id,
)
})
{
response.replace_with_error("Execution context was destroyed by navigation");
}
let message = response.into_protocol_message_for_typed_runtime_route();
Some(RendererRuntimeInspectorMessage::protocol(message))
}
pub(crate) fn start_runtime_protocol_message_for_owner(
&mut self,
owner: &CommandOwnerScope,
raw_json: String,
) -> Result<PendingRuntimeProtocolMessageDispatch, String> {
self.start_runtime_protocol_message_for_owner_with_access(
owner,
raw_json,
RendererInspectorCommandRoute::MainThread,
)
}
pub(crate) fn start_runtime_io_protocol_message_for_owner(
&mut self,
owner: &CommandOwnerScope,
raw_json: String,
) -> Result<PendingRuntimeProtocolMessageDispatch, String> {
self.start_runtime_protocol_message_for_owner_with_access(
owner,
raw_json,
RendererInspectorCommandRoute::Io,
)
}
fn start_runtime_protocol_message_for_owner_with_access(
&mut self,
owner: &CommandOwnerScope,
raw_json: String,
inspector_route: RendererInspectorCommandRoute,
) -> Result<PendingRuntimeProtocolMessageDispatch, String> {
let route = self.runtime_protocol_message_page_route_for_owner(owner)?;
let raw_json = self.rewrite_runtime_inspector_command_for_owner(owner, &raw_json, None)?;
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_owner(owner);
let page = match inspector_route {
RendererInspectorCommandRoute::MainThread => {
self.runtime_session_owner_page_mut_for_owner(owner)?
}
RendererInspectorCommandRoute::Io => {
self.runtime_session_owner_page_mut_for_interruptible_control_for_owner(owner)?
}
};
let pending = match inspector_route {
RendererInspectorCommandRoute::MainThread => page
.start_runtime_protocol_message_for_inspector_session(
inspector_session_id,
raw_json,
)
.map(PendingRuntimeProtocolMessageDispatchKind::Page),
RendererInspectorCommandRoute::Io => page
.start_runtime_inspector_io_message_without_response(inspector_session_id, raw_json)
.map(PendingRuntimeProtocolMessageDispatchKind::Routable),
}
.map_err(|error| format!("runtime inspector dispatch failed: {error}"))?;
Ok(PendingRuntimeProtocolMessageDispatch {
owner: owner.clone(),
route,
pending,
response_route: RuntimeProtocolResponseRoute::adapter_reply_without_receiver(),
})
}
pub(crate) fn start_runtime_protocol_message_for_owner_with_deferred_response(
&mut self,
owner: &CommandOwnerScope,
descriptor: RendererCommandDescriptor,
command_id: u64,
) -> Result<PendingRuntimeProtocolMessageDispatch, String> {
self.start_runtime_protocol_message_for_owner_with_deferred_response_and_access(
owner,
descriptor,
command_id,
RendererInspectorCommandRoute::MainThread,
)
}
pub(crate) fn start_runtime_io_protocol_message_for_owner_with_deferred_response(
&mut self,
owner: &CommandOwnerScope,
descriptor: RendererCommandDescriptor,
command_id: u64,
) -> Result<PendingRuntimeProtocolMessageDispatch, String> {
self.start_runtime_protocol_message_for_owner_with_deferred_response_and_access(
owner,
descriptor,
command_id,
RendererInspectorCommandRoute::Io,
)
}
fn start_runtime_protocol_message_for_owner_with_deferred_response_and_access(
&mut self,
owner: &CommandOwnerScope,
descriptor: RendererCommandDescriptor,
command_id: u64,
inspector_route: RendererInspectorCommandRoute,
) -> Result<PendingRuntimeProtocolMessageDispatch, String> {
let route = self.runtime_protocol_message_page_route_for_owner(owner)?;
let (correlation, raw_json, response_sender, response_route) = self
.prepare_renderer_call_for_owner(
owner,
descriptor,
command_id,
Some(route.renderer_agent_attachment_id),
)?;
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_owner(owner);
let page_result = match inspector_route {
RendererInspectorCommandRoute::MainThread => {
self.runtime_session_owner_page_mut_for_owner(owner)
}
RendererInspectorCommandRoute::Io => {
self.runtime_session_owner_page_mut_for_interruptible_control_for_owner(owner)
}
};
let page = match page_result {
Ok(page) => page,
Err(error) => {
let removed = self.take_renderer_call_for_frontend_for_owner(owner, command_id);
debug_assert_eq!(removed, Some(correlation));
return Err(error);
}
};
let pending = match page.start_routable_runtime_protocol_message_for_inspector_session(
inspector_session_id,
inspector_route,
None,
raw_json,
response_sender,
) {
Ok(pending) => pending,
Err(error) => {
let removed = self.take_renderer_call_for_frontend_for_owner(owner, command_id);
debug_assert_eq!(removed, Some(correlation));
return Err(format!("runtime inspector dispatch failed: {error}"));
}
};
Ok(PendingRuntimeProtocolMessageDispatch {
owner: owner.clone(),
route,
pending: PendingRuntimeProtocolMessageDispatchKind::Routable(pending),
response_route,
})
}
pub(crate) fn start_runtime_protocol_message_with_context_resolution_for_owner(
&mut self,
owner: &CommandOwnerScope,
action: &str,
raw_json: String,
) -> Result<PendingRuntimeProtocolMessageDispatch, String> {
let route = self.runtime_protocol_message_page_route_for_owner(owner)?;
let raw_json = self.rewrite_runtime_inspector_command_for_owner(owner, &raw_json, None)?;
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_owner(owner);
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
let pending = page
.start_runtime_protocol_message_for_inspector_session_with_context_resolution(
inspector_session_id,
action.to_owned(),
raw_json,
)
.map_err(|error| format!("runtime inspector dispatch failed: {error}"))?;
Ok(PendingRuntimeProtocolMessageDispatch {
owner: owner.clone(),
route,
pending: PendingRuntimeProtocolMessageDispatchKind::Page(pending),
response_route: RuntimeProtocolResponseRoute::adapter_reply_without_receiver(),
})
}
pub(crate) fn start_runtime_protocol_message_with_context_resolution_for_owner_with_deferred_response(
&mut self,
owner: &CommandOwnerScope,
action: &str,
descriptor: RendererCommandDescriptor,
command_id: u64,
) -> Result<PendingRuntimeProtocolMessageDispatch, String> {
let route = self.runtime_protocol_message_page_route_for_owner(owner)?;
let (correlation, raw_json, response_sender, response_route) = self
.prepare_renderer_call_for_owner(
owner,
descriptor,
command_id,
Some(route.renderer_agent_attachment_id),
)?;
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_owner(owner);
let page = match self.runtime_session_owner_page_mut_for_owner(owner) {
Ok(page) => page,
Err(error) => {
let removed = self.take_renderer_call_for_frontend_for_owner(owner, command_id);
debug_assert_eq!(removed, Some(correlation));
return Err(error);
}
};
let pending = match page.start_routable_runtime_protocol_message_for_inspector_session(
inspector_session_id,
RendererInspectorCommandRoute::MainThread,
Some(action.to_owned()),
raw_json,
response_sender,
) {
Ok(pending) => pending,
Err(error) => {
let removed = self.take_renderer_call_for_frontend_for_owner(owner, command_id);
debug_assert_eq!(removed, Some(correlation));
return Err(format!("runtime inspector dispatch failed: {error}"));
}
};
Ok(PendingRuntimeProtocolMessageDispatch {
owner: owner.clone(),
route,
pending: PendingRuntimeProtocolMessageDispatchKind::Routable(pending),
response_route,
})
}
pub(crate) async fn complete_runtime_protocol_message_async(
&mut self,
completed: CompletedRuntimeProtocolMessageDispatch,
) -> Result<Option<RendererCommandTurnOutput>, String> {
let timing_started = moli_trace::cdp_nav_timing_enabled().then(std::time::Instant::now);
let owner = completed.owner().clone();
let completion = match completed.completion {
moli_core::page::CompletedRuntimeInspectorCommandDispatch::Owner(completion) => {
*completion
}
moli_core::page::CompletedRuntimeInspectorCommandDispatch::OwnerSessionResponse {
completion,
..
} => *completion,
moli_core::page::CompletedRuntimeInspectorCommandDispatch::Inspector
| moli_core::page::CompletedRuntimeInspectorCommandDispatch::InspectorSessionResponse {
..
}
| moli_core::page::CompletedRuntimeInspectorCommandDispatch::OwnerSessionErrorSettled(
_,
) => {
return Ok(None);
}
};
let mut output =
self.consume_runtime_protocol_message_completion(&completed.route, completion)?;
if let Some(started) = timing_started {
tracing::info!(
target: "moli_cdp_nav_timing",
stage = "runtime_inspector_page_dispatch_done",
output_messages = output
.runtime_inspector_output()
.map_or(0, |messages| messages.len()),
elapsed_ms = started.elapsed().as_millis(),
);
}
self.ingest_runtime_protocol_message_started_route_output_updates(&completed.route);
if let Some(started) = timing_started {
tracing::info!(
target: "moli_cdp_nav_timing",
stage = "runtime_inspector_output_ingested",
elapsed_ms = started.elapsed().as_millis(),
);
}
let runtime_messages = output.runtime_inspector_output_mut().ok_or_else(|| {
"runtime inspector dispatch completed with a non-Runtime renderer reply".to_owned()
})?;
runtime_messages
.bind_renderer_agent_attachment(completed.route.renderer_agent_attachment_id);
let runtime_messages = runtime_messages.messages_mut();
self.restore_frontend_command_ids_in_runtime_messages_for_owner(
&owner,
Some(completed.route.renderer_agent_attachment_id),
runtime_messages,
);
if let Some(started) = timing_started {
tracing::info!(
target: "moli_cdp_nav_timing",
stage = "runtime_inspector_command_output_ready",
output_messages = output
.runtime_inspector_output()
.map_or(0, |messages| messages.len()),
elapsed_ms = started.elapsed().as_millis(),
);
}
Ok(Some(output))
}
pub(crate) async fn replay_prepared_renderer_calls_after_navigation_async(
&mut self,
replays: Vec<SessionRendererCallReplay>,
new_attachment_id: RendererAgentAttachmentId,
) -> Result<Vec<BackgroundProtocolEvent>, String> {
let mut events = Vec::new();
for replay in replays {
let frontend_session_id = replay.frontend_session_id().map(str::to_owned);
let owner = CommandOwnerScope::capture(self, frontend_session_id.as_deref());
let renderer_inspector_session_id =
replay.renderer_inspector_session_id().map(str::to_owned);
let (correlation, replay, response_delivery, frontend_payload, response_sender) =
replay.into_replay().into_parts();
let route = match self.runtime_protocol_message_page_route_for_session_owner(
frontend_session_id.as_deref(),
) {
Ok(route) => route,
Err(error) => {
self.settle_renderer_replacement_error(
&mut events,
frontend_session_id.as_deref(),
response_delivery,
&response_sender,
correlation,
&error,
);
continue;
}
};
if route.renderer_agent_attachment_id != new_attachment_id {
self.settle_renderer_replacement_error(
&mut events,
frontend_session_id.as_deref(),
response_delivery,
&response_sender,
correlation,
"renderer replay attachment is no longer current",
);
continue;
}
let dispatch = match replay {
RendererCommandReplay::Inspector(dispatch) => dispatch,
RendererCommandReplay::PerformanceGetMetrics => {
debug_assert_eq!(
response_delivery,
RendererInspectorResponseDelivery::SessionSink
);
let pending = self
.runtime_session_owner_page_mut(frontend_session_id.as_deref())
.map_err(|error| error.to_string())
.and_then(|page| {
let result = crate::domains::performance::performance_metrics_result(
&page.cached_performance_metric_snapshot(),
);
page.start_performance_get_metrics_from_io_with_response(
renderer_inspector_session_id.clone(),
result,
response_sender.clone(),
)
.map_err(|error| error.to_string())
});
let completion = match pending {
Ok(pending) => pending.wait().await.map_err(|error| error.to_string()),
Err(error) => Err(error),
};
match completion {
Ok(
moli_core::page::CompletedDevToolsIoCommandDispatch::SessionResponse {
..
},
)
| Ok(moli_core::page::CompletedDevToolsIoCommandDispatch::Dispatched) => {}
Err(error) => {
self.settle_renderer_replacement_error(
&mut events,
frontend_session_id.as_deref(),
response_delivery,
&response_sender,
correlation,
&format!("Performance replay dispatch failed: {error}"),
);
}
}
continue;
}
RendererCommandReplay::SetScriptExecutionDisabled { disabled } => {
debug_assert_eq!(
response_delivery,
RendererInspectorResponseDelivery::SessionSink
);
let pending = self
.runtime_session_owner_page_mut(frontend_session_id.as_deref())
.map_err(|error| error.to_string())
.and_then(|page| {
page.start_set_script_execution_disabled_from_io_with_response(
renderer_inspector_session_id.clone(),
disabled,
response_sender.clone(),
)
.map_err(|error| error.to_string())
});
let completion = match pending {
Ok(pending) => pending.wait().await.map_err(|error| error.to_string()),
Err(error) => Err(error),
};
match completion {
Ok(
moli_core::page::CompletedDevToolsIoCommandDispatch::SessionResponse {
..
},
)
| Ok(moli_core::page::CompletedDevToolsIoCommandDispatch::Dispatched) => {}
Err(error) => {
self.settle_renderer_replacement_error(
&mut events,
frontend_session_id.as_deref(),
response_delivery,
&response_sender,
correlation,
&format!("Emulation replay dispatch failed: {error}"),
);
}
}
continue;
}
};
let raw_json = match self.rewrite_runtime_inspector_command_for_session_owner(
frontend_session_id.as_deref(),
&frontend_payload,
Some((
correlation.frontend_command_id(),
correlation.renderer_call_id(),
)),
) {
Ok(raw_json) => raw_json,
Err(error) => {
self.settle_renderer_replacement_error(
&mut events,
frontend_session_id.as_deref(),
response_delivery,
&response_sender,
correlation,
&error,
);
continue;
}
};
let pending = {
let page = match self.runtime_session_owner_page_mut(frontend_session_id.as_deref())
{
Ok(page) => page,
Err(error) => {
self.settle_renderer_replacement_error(
&mut events,
frontend_session_id.as_deref(),
response_delivery,
&response_sender,
correlation,
&error,
);
continue;
}
};
let dispatch_sender = response_sender.clone();
match response_delivery {
RendererInspectorResponseDelivery::AdapterReply => match dispatch {
CdpRendererCommandReplayDispatch::ResolveRuntimeContext => page
.start_runtime_protocol_message_for_inspector_session_with_context_resolution_and_deferred_response(
renderer_inspector_session_id,
"addBinding".to_owned(),
raw_json,
dispatch_sender,
)
.map(PendingRuntimeProtocolMessageDispatchKind::Page),
CdpRendererCommandReplayDispatch::Direct => page
.start_runtime_protocol_message_for_inspector_session_with_deferred_response(
renderer_inspector_session_id,
raw_json,
dispatch_sender,
)
.map(PendingRuntimeProtocolMessageDispatchKind::Page),
},
RendererInspectorResponseDelivery::SessionSink => {
debug_assert_eq!(
dispatch,
CdpRendererCommandReplayDispatch::Direct,
"the migrated synchronous IO family must replay directly"
);
page.start_routable_runtime_protocol_message_for_inspector_session(
renderer_inspector_session_id,
RendererInspectorCommandRoute::Io,
None,
raw_json,
dispatch_sender,
)
.map(PendingRuntimeProtocolMessageDispatchKind::Routable)
}
}
};
let pending = match pending {
Ok(pending) => PendingRuntimeProtocolMessageDispatch {
owner,
route,
pending,
response_route:
RuntimeProtocolResponseRoute::without_local_receiver_for_delivery(
response_delivery,
),
},
Err(error) => {
self.settle_renderer_replacement_error(
&mut events,
frontend_session_id.as_deref(),
response_delivery,
&response_sender,
correlation,
&format!("runtime inspector replay dispatch failed: {error}"),
);
continue;
}
};
let completed = match pending.wait().await {
Ok(completed) => completed,
Err(error) => {
self.settle_renderer_replacement_error(
&mut events,
frontend_session_id.as_deref(),
response_delivery,
&response_sender,
correlation,
&error,
);
continue;
}
};
let completed_owner = completed.owner().clone();
let completion = match completed.completion {
moli_core::page::CompletedRuntimeInspectorCommandDispatch::Owner(completion) => {
*completion
}
moli_core::page::CompletedRuntimeInspectorCommandDispatch::Inspector => {
continue;
}
moli_core::page::CompletedRuntimeInspectorCommandDispatch::InspectorSessionResponse {
..
} => {
continue;
}
moli_core::page::CompletedRuntimeInspectorCommandDispatch::OwnerSessionErrorSettled(
_,
) => {
continue;
}
moli_core::page::CompletedRuntimeInspectorCommandDispatch::OwnerSessionResponse {
completion,
..
} => *completion,
};
let mut command_turn_output = match self
.consume_runtime_protocol_message_completion(&completed.route, completion)
{
Ok(output) => output,
Err(error) => {
send_renderer_replacement_error(
&response_sender,
correlation,
&format!("runtime inspector replay dispatch failed: {error}"),
);
continue;
}
};
command_turn_output.bind_renderer_agent_attachment(new_attachment_id);
self.ingest_runtime_protocol_message_started_route_output_updates(&completed.route);
let mut command = CommandDispatchContext::default();
let completion = command.consume_renderer_command_turn_output(command_turn_output);
events.extend(command.take_protocol_events());
events.extend(command.take_post_response_events());
let Some(output) = completion.into_runtime_inspector_output() else {
send_renderer_replacement_error(
&response_sender,
correlation,
"runtime inspector replay completed with a non-Runtime renderer reply",
);
continue;
};
if output
.protocol_response(correlation.renderer_call_id().get())
.is_some()
{
let _ = response_sender.send_output(output);
continue;
}
let _ = self.route_renderer_runtime_command_output_for_owner_into(
output,
None,
&completed_owner,
&mut events,
);
}
Ok(events)
}
fn settle_renderer_replacement_error(
&mut self,
events: &mut Vec<BackgroundProtocolEvent>,
frontend_session_id: Option<&str>,
response_delivery: RendererInspectorResponseDelivery,
response_sender: &RendererRuntimeInspectorResponseSender,
correlation: RendererCommandCorrelation,
message: &str,
) {
if response_delivery == RendererInspectorResponseDelivery::AdapterReply {
send_renderer_replacement_error(response_sender, correlation, message);
return;
}
self.settle_devtools_session_renderer_error(
events,
frontend_session_id,
correlation,
message,
);
}
fn settle_devtools_session_renderer_error(
&mut self,
events: &mut Vec<BackgroundProtocolEvent>,
frontend_session_id: Option<&str>,
correlation: RendererCommandCorrelation,
message: &str,
) {
let Some(resolved) = self
.take_frontend_command_for_renderer_if_attachment_matches_for_session_owner(
frontend_session_id,
correlation.renderer_call_id(),
correlation.dispatched_attachment_id(),
)
else {
return;
};
debug_assert_eq!(resolved, correlation);
let frontend_command_id = resolved.frontend_command_id().get();
self.complete_runtime_await_job(frontend_command_id, frontend_session_id);
let _ = self.remove_pending_inspector_await(frontend_command_id, frontend_session_id);
let mut response = json!({
"id": frontend_command_id,
"error": {
"code": -32000,
"message": message,
},
});
if let Some(session_id) = frontend_session_id {
response["sessionId"] = json!(session_id);
}
events.push(protocol_message_background_event(response));
}
pub(crate) fn terminate_prepared_renderer_calls_after_navigation(
&mut self,
terminations: Vec<SessionRendererCallTermination>,
reason: &str,
) -> Vec<BackgroundProtocolEvent> {
let mut events = Vec::new();
for termination in terminations {
let (frontend_session_id, termination) = termination.into_parts();
match termination {
PreparedRendererCallTermination::AdapterReply {
correlation,
response_sender,
} => send_renderer_replacement_error(&response_sender, correlation, reason),
PreparedRendererCallTermination::SessionSink { correlation } => self
.settle_devtools_session_renderer_error(
&mut events,
frontend_session_id.as_deref(),
correlation,
reason,
),
}
}
events
}
#[cfg(test)]
pub(crate) async fn complete_runtime_helper_protocol_message_for_session_owner_async(
&mut self,
mut completed: CompletedRuntimeProtocolMessageDispatch,
command_id: u64,
) -> Result<Vec<RendererRuntimeInspectorMessage>, String> {
let response_rx = completed.take_deferred_response_receiver();
let session_id = completed.owner().session_id().map(str::to_owned);
let output = self
.complete_runtime_protocol_message_async(completed)
.await?;
let response_in_output = output.as_ref().is_some_and(|output| {
renderer_command_turn_frontend_protocol_response(output, command_id).is_some()
});
let mut messages = Vec::new();
let mut runtime_messages = Vec::new();
if let Some(output) = output {
let (completion, _) = output.into_completion_and_predecessor();
let Some(runtime_output) = completion.into_runtime_inspector_output() else {
return Err(
"runtime inspector dispatch completed with a non-Runtime renderer reply"
.to_owned(),
);
};
runtime_messages = runtime_output.into_messages();
}
if !response_in_output && let Some(response_rx) = response_rx {
let response = RuntimeInspectorResponseReady::new(
command_id,
session_id.as_deref(),
response_rx
.await
.map_err(|_| "RuntimeInspectorResponseCanceled".to_owned()),
);
if let Some(response) = self.resolve_runtime_inspector_response_ready(response) {
let (_, output, renderer_output_predecessor) =
response.into_renderer_command_output();
assert!(
renderer_output_predecessor.is_none(),
"message-only Runtime test helper cannot discard a concrete output cursor"
);
runtime_messages.extend(output.into_messages());
}
}
messages.extend(runtime_messages);
Ok(messages)
}
/// Completes one concrete BiDi channel owner action under its frozen Page
/// route.
///
/// Stale work is consumed without entering a replacement runtime. The old
/// Page or detached session owns any renderer-side cleanup; applying an
/// object-group release to the new attachment would be the more dangerous
/// outcome because group names belong to the producing runtime.
pub(crate) async fn complete_bidi_channel_owner_action_with_background_events_async(
&mut self,
action: BidiChannelOwnerAction,
background_events: &mut Vec<BackgroundProtocolEvent>,
) {
let (owner, body) = action.into_parts();
if !owner.is_current(self) {
tracing::debug!(
session_id = owner.session_id(),
action = ?body,
"discarding stale BiDi channel owner action"
);
return;
}
let command_owner = owner.command_owner().clone();
match body {
BidiChannelOwnerActionBody::StartListener(listener) => {
self.start_bidi_channel_listener_once_for_owner_with_background_events_async(
&command_owner,
BidiChannelListenerResidence::from_boxed(owner, listener),
background_events,
)
.await;
}
BidiChannelOwnerActionBody::ReleaseObjectGroup(object_group) => {
self.release_bidi_channel_object_group_for_owner_best_effort_async(
&command_owner,
&object_group,
)
.await;
}
}
}
pub(crate) async fn release_bidi_channel_object_group_for_owner_best_effort_async(
&mut self,
owner: &CommandOwnerScope,
object_group: &str,
) {
let command_id = self.next_internal_runtime_command_id();
let raw_json = json!({
"id": command_id,
"method": "Runtime.releaseObjectGroup",
"params": { "objectGroup": object_group }
})
.to_string();
let descriptor = match RendererCommandDescriptor::from_synthesized_payload(raw_json) {
Ok(descriptor) => descriptor,
Err(error) => {
tracing::debug!(%error, object_group, "failed to prepare BiDi object group release");
self.unregister_runtime_remote_object_group_for_owner(owner, object_group);
return;
}
};
let pending = match self
.start_runtime_protocol_message_with_context_resolution_for_owner_with_deferred_response(
owner,
"releaseObjectGroup",
descriptor,
command_id,
) {
Ok(pending) => pending,
Err(error) => {
tracing::debug!(
%error,
object_group,
"failed to start BiDi channel object group release"
);
self.unregister_runtime_remote_object_group_for_owner(owner, object_group);
return;
}
};
let mut completed = match pending.wait().await {
Ok(completed) => completed,
Err(error) => {
self.forget_pending_inspector_await_for_owner(command_id, owner);
tracing::debug!(
%error,
object_group,
"BiDi channel object group release dispatch failed"
);
self.unregister_runtime_remote_object_group_for_owner(owner, object_group);
return;
}
};
let mut renderer_response_rx = completed.take_deferred_response_receiver();
let messages = match self
.complete_runtime_protocol_message_async(completed)
.await
{
Ok(messages) => messages,
Err(error) => {
self.forget_pending_inspector_await_for_owner(command_id, owner);
tracing::debug!(
%error,
object_group,
"BiDi channel object group release completion failed"
);
self.unregister_runtime_remote_object_group_for_owner(owner, object_group);
return;
}
};
let mut release_events = Vec::new();
let mut release_post_response_events = Vec::new();
let response_flush = CommandResponseFlushContext::default();
let release_response_seen = if let Some(messages) = messages {
self.route_renderer_command_turn_output_for_owner_into(
messages,
Some(command_id),
owner,
&response_flush,
&mut release_events,
&mut release_post_response_events,
)
.0
} else {
false
};
if !release_response_seen {
tracing::debug!(
command_id,
"internal object group release inspector response was not routed as current command"
);
}
if release_response_seen {
renderer_response_rx.take();
}
release_events.extend(release_post_response_events);
if let Some(renderer_response_rx) = renderer_response_rx {
let response = RuntimeInspectorResponseReady::for_owner(
command_id,
owner,
renderer_response_rx
.await
.map_err(|_| "RuntimeInspectorResponseCanceled".to_owned()),
);
if let Some(response) = self.resolve_runtime_inspector_response_ready(response) {
let (_, output, renderer_output_predecessor) =
response.into_renderer_command_output();
assert!(
renderer_output_predecessor.is_none(),
"internal object-group cleanup cannot discard a concrete output cursor"
);
let release_response_seen = self
.route_renderer_runtime_command_output_for_owner_into(
output,
Some(command_id),
owner,
&mut release_events,
);
if !release_response_seen {
tracing::debug!(
command_id,
"internal object group release deferred inspector response was not routed as current command"
);
}
}
}
self.unregister_runtime_remote_object_group_for_owner(owner, object_group);
}
async fn start_bidi_channel_listener_once_for_owner_with_background_events_async(
&mut self,
owner: &CommandOwnerScope,
residence: BidiChannelListenerResidence,
background_events: &mut Vec<BackgroundProtocolEvent>,
) {
let listener = residence.listener();
if self.runtime_inspector_response_ready_sender().is_none() {
self.release_bidi_channel_object_group_for_owner_best_effort_async(
owner,
listener.channel_object_group(),
)
.await;
return;
}
let command_id = self.next_internal_runtime_command_id();
let raw_json = bidi_channel_listener_call_function_json(command_id, listener);
let descriptor = match RendererCommandDescriptor::from_synthesized_payload(raw_json) {
Ok(descriptor) => descriptor,
Err(error) => {
tracing::debug!(%error, "failed to prepare BiDi channel listener command");
self.release_bidi_channel_object_group_for_owner_best_effort_async(
owner,
listener.channel_object_group(),
)
.await;
return;
}
};
let pending = match self
.start_runtime_protocol_message_with_context_resolution_for_owner_with_deferred_response(
owner,
"callFunctionOn",
descriptor,
command_id,
) {
Ok(pending) => pending,
Err(error) => {
tracing::debug!(
%error,
channel = %listener.properties().channel,
"failed to start BiDi channel listener"
);
self.release_bidi_channel_object_group_for_owner_best_effort_async(
owner,
listener.channel_object_group(),
)
.await;
return;
}
};
self.register_pending_bidi_channel_listener_for_owner(command_id, owner, residence);
let mut completed = match pending.wait().await {
Ok(completed) => completed,
Err(error) => {
let object_group = self
.remove_pending_inspector_await_for_cancellation_for_owner(command_id, owner)
.and_then(|entry| {
entry
.bidi_channel_listener()
.map(|listener| listener.channel_object_group().to_owned())
});
tracing::debug!(%error, "BiDi channel listener dispatch failed");
if let Some(object_group) = object_group {
self.release_bidi_channel_object_group_for_owner_best_effort_async(
owner,
&object_group,
)
.await;
}
return;
}
};
let mut renderer_response_rx = completed.take_deferred_response_receiver();
let messages = match self
.complete_runtime_protocol_message_async(completed)
.await
{
Ok(messages) => messages,
Err(error) => {
let object_group = self
.remove_pending_inspector_await_for_cancellation_for_owner(command_id, owner)
.and_then(|entry| {
entry
.bidi_channel_listener()
.map(|listener| listener.channel_object_group().to_owned())
});
tracing::debug!(%error, "BiDi channel listener completion failed");
if let Some(object_group) = object_group {
self.release_bidi_channel_object_group_for_owner_best_effort_async(
owner,
&object_group,
)
.await;
}
return;
}
};
let mut listener_events = Vec::new();
let mut listener_post_response_events = Vec::new();
let response_flush = CommandResponseFlushContext::default();
let listener_response_seen = if let Some(messages) = messages {
self.route_renderer_command_turn_output_for_owner_into(
messages,
Some(command_id),
owner,
&response_flush,
&mut listener_events,
&mut listener_post_response_events,
)
.0
} else {
false
};
if !listener_response_seen {
tracing::debug!(
command_id,
"BiDi channel listener inspector response was consumed before command response routing"
);
}
if listener_response_seen {
renderer_response_rx.take();
}
listener_events.extend(listener_post_response_events);
let non_listener_response_count = listener_events
.iter()
.filter(|event| event.protocol_message_id().is_some())
.count();
if non_listener_response_count > 0 {
tracing::debug!(
messages = non_listener_response_count,
"BiDi channel listener produced non-listener protocol messages on background route"
);
}
background_events.extend(
listener_events
.into_iter()
.filter(|event| event.protocol_message_id().is_none()),
);
if let Some(renderer_response_rx) = renderer_response_rx {
// Listener responses use the same response-ready lane whether the
// oneshot is already completed or still pending.
if self.start_or_enqueue_registered_runtime_inspector_response_ready(
command_id,
owner,
renderer_response_rx,
) {
return;
}
let object_group = self
.remove_pending_inspector_await_for_cancellation_for_owner(command_id, owner)
.and_then(|entry| {
entry
.bidi_channel_listener()
.map(|listener| listener.channel_object_group().to_owned())
});
tracing::debug!(
channel_object_group = object_group.as_deref(),
"BiDi channel listener started without scheduler runtime response hook"
);
if let Some(object_group) = object_group {
self.release_bidi_channel_object_group_for_owner_best_effort_async(
owner,
&object_group,
)
.await;
}
}
}
pub(crate) async fn runtime_realm_inventory_for_owner_async(
&mut self,
owner: &CommandOwnerScope,
) -> Result<Vec<RuntimeExecutionContextEvent>, String> {
let target_id = self
.target_owner_identity_for_owner(owner)
.and_then(|(_, target_id)| target_id);
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
let target_id = target_id.as_deref();
let devtools_target_id = target_id.map(DevToolsTargetId::from);
let realms = page
.runtime_realm_inventory_async()
.await
.map_err(|error| format!("runtime realm inventory failed: {error}"))?;
realms
.into_iter()
.map(|realm| {
runtime_realm_info_to_execution_context_event(
realm,
target_id,
devtools_target_id.clone(),
)
})
.collect()
}
pub async fn runtime_default_execution_context_id_async(
&mut self,
) -> Result<Option<i64>, String> {
self.runtime_default_execution_context_id_for_session_owner_async(None)
.await
}
pub async fn runtime_default_execution_context_id_for_session_owner_async(
&mut self,
session_id: Option<&str>,
) -> Result<Option<i64>, String> {
let page = self.runtime_session_owner_page_mut(session_id)?;
page.default_execution_context_id_async()
.await
.map_err(|error| format!("runtime default execution context lookup failed: {error}"))
}
pub(crate) async fn runtime_default_or_initial_execution_context_id_for_owner_async(
&mut self,
owner: &CommandOwnerScope,
) -> Result<Option<i64>, String> {
let page = self
.runtime_session_owner_slot_mut_for_owner(owner)?
.loaded_page_mut()
.ok_or_else(|| "NoDocumentLoaded".to_owned())?;
page.default_or_initial_execution_context_id_async()
.await
.map_err(|error| format!("runtime default execution context lookup failed: {error}"))
}
pub(crate) async fn runtime_ensure_isolated_world_for_owner_async(
&mut self,
owner: &CommandOwnerScope,
frame_id: Option<&str>,
world_name: &str,
) -> Result<i64, String> {
let owner_target_id = self
.target_owner_identity_for_owner(owner)
.and_then(|(_, target_id)| target_id);
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
let result = if let Some(frame_id) = frame_id
&& owner_target_id.as_deref() != Some(frame_id)
{
page.create_isolated_world_for_frame_async(frame_id, world_name, false)
.await
} else {
page.create_isolated_world_async(world_name, false).await
};
result.map_err(|error| format!("runtime isolated world creation failed: {error}"))
}
pub async fn has_isolated_execution_context_id_async(
&mut self,
execution_context_id: i64,
) -> Result<bool, String> {
self.has_isolated_execution_context_id_for_session_owner_async(None, execution_context_id)
.await
}
pub async fn has_isolated_execution_context_id_for_session_owner_async(
&mut self,
session_id: Option<&str>,
execution_context_id: i64,
) -> Result<bool, String> {
let page = self.runtime_session_owner_page_mut(session_id)?;
page.has_isolated_execution_context_id_async(execution_context_id)
.await
.map_err(|error| format!("runtime isolated context lookup failed: {error}"))
}
pub async fn has_child_default_execution_context_id_for_session_owner_async(
&mut self,
session_id: Option<&str>,
execution_context_id: i64,
) -> Result<bool, String> {
let page = self.runtime_session_owner_page_mut(session_id)?;
page.child_frame_id_for_default_execution_context_id_async(execution_context_id)
.await
.map(|frame_id| frame_id.is_some())
.map_err(|error| format!("runtime child default context lookup failed: {error}"))
}
pub async fn child_default_execution_context_id_for_frame_id_for_session_owner_async(
&mut self,
session_id: Option<&str>,
frame_id: &str,
) -> Result<Option<i64>, String> {
let owner = CommandOwnerScope::capture(self, session_id);
self.child_default_execution_context_id_for_frame_id_for_owner_async(&owner, frame_id)
.await
}
pub(crate) async fn child_default_execution_context_id_for_frame_id_for_owner_async(
&mut self,
owner: &CommandOwnerScope,
frame_id: &str,
) -> Result<Option<i64>, String> {
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.child_default_execution_context_id_for_frame_id_async(frame_id)
.await
.map_err(|error| format!("runtime child default context lookup failed: {error}"))
}
pub(crate) fn start_child_default_execution_context_lookup_for_owner(
&mut self,
owner: &CommandOwnerScope,
execution_context_id: i64,
) -> Result<PendingRuntimeChildDefaultContextLookupDispatch, String> {
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
let pending = page
.start_child_frame_id_for_default_execution_context_id(execution_context_id)
.map_err(|error| format!("runtime child default context lookup failed: {error}"))?;
Ok(PendingRuntimeChildDefaultContextLookupDispatch {
owner: owner.clone(),
pending,
})
}
pub(crate) fn complete_child_default_execution_context_lookup(
&mut self,
completed: CompletedRuntimeChildDefaultContextLookupDispatch,
) -> Result<bool, String> {
let page = self.runtime_session_owner_page_mut_for_owner(&completed.owner)?;
page.finish_child_frame_id_for_default_execution_context_id(completed.completion)
.map(|frame_id| frame_id.is_some())
.map_err(|error| format!("runtime child default context lookup failed: {error}"))
}
pub async fn inspector_execution_context_id_for_isolated_context_async(
&mut self,
execution_context_id: i64,
) -> Result<Option<i64>, String> {
self.inspector_execution_context_id_for_isolated_context_for_session_owner_async(
None,
execution_context_id,
)
.await
}
pub async fn inspector_execution_context_id_for_isolated_context_for_session_owner_async(
&mut self,
session_id: Option<&str>,
execution_context_id: i64,
) -> Result<Option<i64>, String> {
let page = self.runtime_session_owner_page_mut(session_id)?;
page.ensure_isolated_worlds_attached_to_inspector_async()
.await
.map_err(|error| {
format!("runtime isolated inspector context attachment failed: {error}")
})?;
page.inspector_execution_context_id_for_isolated_context_async(execution_context_id)
.await
.map_err(|error| format!("runtime isolated inspector context lookup failed: {error}"))
}
pub async fn isolated_execution_context_id_for_inspector_context_async(
&mut self,
execution_context_id: i64,
) -> Result<Option<i64>, String> {
self.isolated_execution_context_id_for_inspector_context_for_session_owner_async(
None,
execution_context_id,
)
.await
}
pub async fn isolated_execution_context_id_for_inspector_context_for_session_owner_async(
&mut self,
session_id: Option<&str>,
execution_context_id: i64,
) -> Result<Option<i64>, String> {
let page = self.runtime_session_owner_page_mut(session_id)?;
page.ensure_isolated_worlds_attached_to_inspector_async()
.await
.map_err(|error| {
format!("runtime isolated compatibility context attachment failed: {error}")
})?;
page.isolated_execution_context_id_for_inspector_context_async(execution_context_id)
.await
.map_err(|error| {
format!("runtime isolated compatibility context lookup failed: {error}")
})
}
pub async fn evaluate_runtime_expression_in_execution_context_with_await_async(
&mut self,
execution_context_id: i64,
expression: &str,
await_promise: bool,
) -> Result<Value, String> {
self.evaluate_runtime_expression_in_execution_context_for_session_owner_async(
None,
execution_context_id,
expression,
await_promise,
)
.await
}
pub async fn evaluate_runtime_expression_in_execution_context_for_session_owner_async(
&mut self,
session_id: Option<&str>,
execution_context_id: i64,
expression: &str,
await_promise: bool,
) -> Result<Value, String> {
self.evaluate_runtime_expression_in_execution_context_for_session_owner_once_async(
session_id,
execution_context_id,
expression,
await_promise,
)
.await
}
async fn evaluate_runtime_expression_in_execution_context_for_session_owner_once_async(
&mut self,
session_id: Option<&str>,
execution_context_id: i64,
expression: &str,
await_promise: bool,
) -> Result<Value, String> {
let payload = {
let page = self.runtime_session_owner_page_mut(session_id)?;
page.evaluate_runtime_expression_in_execution_context_without_navigation_follow_with_await_async(
execution_context_id,
expression,
await_promise,
)
.await
.map_err(|error| format!("runtime evaluation failed: {error}"))?
};
self.ingest_runtime_session_owner_output_updates(session_id);
Ok(payload)
}
pub async fn install_runtime_binding_async(
&mut self,
name: &str,
execution_context_name: Option<&str>,
execution_context_id: Option<i64>,
) -> Result<(), String> {
self.install_runtime_binding_for_session_owner_async(
None,
name,
execution_context_name,
execution_context_id,
)
.await
}
pub async fn install_runtime_binding_for_session_owner_async(
&mut self,
session_id: Option<&str>,
name: &str,
execution_context_name: Option<&str>,
execution_context_id: Option<i64>,
) -> Result<(), String> {
let page = self.runtime_session_owner_page_mut(session_id)?;
page.install_runtime_binding_async(name, execution_context_name, execution_context_id)
.await
.map_err(|error| format!("runtime binding install failed: {error}"))
}
pub(crate) fn start_install_runtime_binding_for_owner(
&mut self,
owner: &CommandOwnerScope,
name: &str,
execution_context_name: Option<&str>,
execution_context_id: Option<i64>,
) -> Result<PendingRuntimeBindingPageCommandDispatch, String> {
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
let pending = page
.start_install_runtime_binding(name, execution_context_name, execution_context_id)
.map_err(|error| format!("runtime binding install failed: {error}"))?;
Ok(PendingRuntimeBindingPageCommandDispatch {
owner: owner.clone(),
operation: "runtime binding install",
pending,
})
}
pub(crate) fn start_apply_stored_runtime_bindings_for_owner(
&mut self,
owner: &CommandOwnerScope,
) -> Result<PendingRuntimeBindingPageCommandDispatch, String> {
let stored_runtime_bindings = self.target_runtime_bindings_for_renderer_owner(owner);
let session_runtime_bindings =
self.target_runtime_bindings_for_current_inspector_owner(owner);
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_owner(owner);
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
let pending = page
.start_set_runtime_binding_state(
inspector_session_id,
&stored_runtime_bindings,
&session_runtime_bindings,
)
.map_err(|error| format!("runtime binding state update failed: {error}"))?;
Ok(PendingRuntimeBindingPageCommandDispatch {
owner: owner.clone(),
operation: "runtime binding state update",
pending,
})
}
pub(crate) async fn apply_runtime_binding_state_for_owner_async(
&mut self,
owner: &CommandOwnerScope,
) -> Result<(), String> {
let stored_runtime_bindings = self.target_runtime_bindings_for_renderer_owner(owner);
let session_runtime_bindings =
self.target_runtime_bindings_for_current_inspector_owner(owner);
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_owner(owner);
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
page.set_runtime_binding_state_async(
inspector_session_id,
&stored_runtime_bindings,
&session_runtime_bindings,
)
.await
.map_err(|error| format!("runtime binding state update failed: {error}"))
}
pub async fn remove_runtime_binding_async(&mut self, name: &str) -> Result<(), String> {
self.remove_runtime_binding_for_session_owner_async(None, name)
.await
}
pub async fn remove_runtime_binding_for_session_owner_async(
&mut self,
session_id: Option<&str>,
name: &str,
) -> Result<(), String> {
let page = self.runtime_session_owner_page_mut(session_id)?;
page.remove_runtime_binding_async(name)
.await
.map_err(|error| format!("runtime binding removal failed: {error}"))
}
pub(crate) fn start_remove_runtime_binding_for_owner(
&mut self,
owner: &CommandOwnerScope,
name: &str,
) -> Result<PendingRuntimeBindingPageCommandDispatch, String> {
let page = self.runtime_session_owner_page_mut_for_owner(owner)?;
let pending = page
.start_remove_runtime_binding(name)
.map_err(|error| format!("runtime binding removal failed: {error}"))?;
Ok(PendingRuntimeBindingPageCommandDispatch {
owner: owner.clone(),
operation: "runtime binding removal",
pending,
})
}
pub(crate) fn complete_runtime_binding_page_command(
&mut self,
completed: CompletedRuntimeBindingPageCommandDispatch,
) -> Result<(), String> {
let page = self.runtime_session_owner_page_mut_for_owner(&completed.owner)?;
page.finish_unit_runtime_page_command(completed.completion, completed.operation)
.map_err(|error| format!("{} failed: {error}", completed.operation))
}
pub async fn remove_default_runtime_binding_async(&mut self, name: &str) -> Result<(), String> {
self.remove_default_runtime_binding_for_session_owner_async(None, name)
.await
}
pub async fn remove_default_runtime_binding_for_session_owner_async(
&mut self,
session_id: Option<&str>,
name: &str,
) -> Result<(), String> {
let page = self.runtime_session_owner_page_mut(session_id)?;
page.remove_default_runtime_binding_async(name)
.await
.map_err(|error| format!("runtime default binding removal failed: {error}"))
}
pub(crate) async fn detach_runtime_inspector_session_for_session_owner_async(
&mut self,
session_id: Option<&str>,
) -> Result<bool, String> {
let inspector_session_id =
self.target_renderer_runtime_inspector_session_id_for_session(session_id);
let page = self.runtime_session_owner_page_mut(session_id)?;
page.detach_runtime_inspector_session_async(inspector_session_id.as_deref())
.await
.map_err(|error| format!("runtime inspector session detach failed: {error}"))
}
}
fn send_renderer_replacement_error(
response_sender: &RendererRuntimeInspectorResponseSender,
correlation: RendererCommandCorrelation,
message: &str,
) {
let _ = response_sender.clone().send(json!({
"id": correlation.renderer_call_id().get(),
"error": {
"code": -32000,
"message": message,
},
}));
}
fn runtime_realm_info_to_execution_context_event(
realm: RendererRuntimeRealmInfo,
owner_frame_id: Option<&str>,
target_id: Option<DevToolsTargetId>,
) -> Result<RuntimeExecutionContextEvent, String> {
let realm_frame_id = realm
.frame_id
.as_deref()
.filter(|frame_id| !frame_id.is_empty())
.or(owner_frame_id);
let realm_id = realm
.realm_id
.filter(|realm_id| !realm_id.is_empty())
.map(|realm_id| protocol_global_realm_id(realm_id, target_id.as_ref(), realm_frame_id))
.map(DevToolsRealmId::from);
Ok(RuntimeExecutionContextEvent {
target_id,
context_id: Some(realm.context_id),
realm_id,
frame_id: realm_frame_id.map(DevToolsFrameId::from),
origin: Some(realm.origin),
name: Some(realm.name),
is_default: Some(realm.is_default),
context_type: Some(realm.context_type),
grant_universal_access: None,
})
}
fn protocol_global_realm_id(
native_realm_id: String,
target_id: Option<&DevToolsTargetId>,
frame_id: Option<&str>,
) -> String {
let Some(owner_id) = target_id.map(DevToolsTargetId::as_str).or(frame_id) else {
return native_realm_id;
};
format!("{owner_id}:{native_realm_id}")
}
fn collect_runtime_remote_object_ids(
mut object_ids: Vec<String>,
mut stack: Vec<(&Value, usize)>,
) -> Vec<String> {
while let Some((value, remaining_tree_depth)) = stack.pop() {
let Some(next_tree_depth) = remaining_tree_depth.checked_sub(1) else {
continue;
};
match value {
Value::Object(map) => {
for key in ["objectId", "promiseObjectId", "errorObjectId"] {
if let Some(object_id) = map.get(key).and_then(Value::as_str) {
object_ids.push(object_id.to_owned());
}
}
for child in map.values() {
stack.push((child, next_tree_depth));
}
}
Value::Array(values) => {
for child in values {
stack.push((child, next_tree_depth));
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
}
object_ids.sort();
object_ids.dedup();
object_ids
}
pub(crate) fn runtime_remote_object_ids_in_value(value: &Value) -> Vec<String> {
collect_runtime_remote_object_ids(
Vec::new(),
vec![(value, MAX_INSPECTOR_PROTOCOL_VALUE_DEPTH)],
)
}
pub(crate) fn runtime_remote_object_ids_in_map(map: &Map<String, Value>) -> Vec<String> {
let mut object_ids = Vec::new();
for key in ["objectId", "promiseObjectId", "errorObjectId"] {
if let Some(object_id) = map.get(key).and_then(Value::as_str) {
object_ids.push(object_id.to_owned());
}
}
let Some(next_tree_depth) = MAX_INSPECTOR_PROTOCOL_VALUE_DEPTH.checked_sub(1) else {
return object_ids;
};
collect_runtime_remote_object_ids(
object_ids,
map.values().map(|value| (value, next_tree_depth)).collect(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::TestContext;
use moli_core::page::{MAX_INSPECTOR_PROTOCOL_VALUE_DEPTH, is_renderer_backend_node_id};
#[test]
fn registered_adapter_reply_route_owns_exactly_one_receiver() {
let (_response_tx, response_rx) = tokio::sync::oneshot::channel();
let mut route = RuntimeProtocolResponseRoute::for_registered_delivery(
RendererInspectorResponseDelivery::AdapterReply,
Some(response_rx),
);
assert_eq!(
route.delivery(),
RendererInspectorResponseDelivery::AdapterReply
);
assert!(route.take_adapter_reply_receiver().is_some());
assert!(route.take_adapter_reply_receiver().is_none());
}
#[test]
#[should_panic(expected = "registered adapter-reply route must allocate its receiver")]
fn registered_adapter_reply_route_rejects_a_missing_receiver() {
let _ = RuntimeProtocolResponseRoute::for_registered_delivery(
RendererInspectorResponseDelivery::AdapterReply,
None,
);
}
#[test]
fn registered_devtools_session_route_has_no_adapter_reply_receiver() {
let mut route = RuntimeProtocolResponseRoute::for_registered_delivery(
RendererInspectorResponseDelivery::SessionSink,
None,
);
assert_eq!(
route.delivery(),
RendererInspectorResponseDelivery::SessionSink
);
assert!(route.take_adapter_reply_receiver().is_none());
}
#[test]
#[should_panic(expected = "session-sink response cannot retain an adapter-reply receiver")]
fn devtools_session_route_rejects_adapter_reply_receiver() {
let (_response_tx, response_rx) = tokio::sync::oneshot::channel();
let _ = RuntimeProtocolResponseRoute::for_registered_delivery(
RendererInspectorResponseDelivery::SessionSink,
Some(response_rx),
);
}
#[test]
fn replay_response_routes_never_claim_a_second_local_receiver() {
let mut adapter_reply = RuntimeProtocolResponseRoute::without_local_receiver_for_delivery(
RendererInspectorResponseDelivery::AdapterReply,
);
let mut devtools_session =
RuntimeProtocolResponseRoute::without_local_receiver_for_delivery(
RendererInspectorResponseDelivery::SessionSink,
);
assert_eq!(
adapter_reply.delivery(),
RendererInspectorResponseDelivery::AdapterReply
);
assert_eq!(
devtools_session.delivery(),
RendererInspectorResponseDelivery::SessionSink
);
assert!(adapter_reply.take_adapter_reply_receiver().is_none());
assert!(devtools_session.take_adapter_reply_receiver().is_none());
}
#[test]
fn runtime_inspector_command_rewrites_large_frontend_id_to_renderer_call_id() {
let frontend_command_id = FrontendCommandId::new(i32::MAX as u64 + 73);
let raw_json = json!({
"id": frontend_command_id.get(),
"method": "Runtime.evaluate",
"params": { "expression": "42" },
"sessionId": "SID-large-id",
})
.to_string();
let rewritten = rewrite_runtime_inspector_command_for_renderer(
&raw_json,
Some((frontend_command_id, RendererCallId::new(11))),
None,
)
.unwrap();
let rewritten: Value = serde_json::from_str(&rewritten).unwrap();
assert_eq!(rewritten["id"], json!(11));
assert_eq!(rewritten["method"], json!("Runtime.evaluate"));
assert_eq!(rewritten["params"]["expression"], json!("42"));
assert_eq!(rewritten["sessionId"], json!("SID-large-id"));
}
#[test]
fn runtime_inspector_command_rewrite_rejects_mismatched_wire_id() {
let error = rewrite_runtime_inspector_command_for_renderer(
r#"{"id":8,"method":"Runtime.evaluate","params":{}}"#,
Some((FrontendCommandId::new(9), RendererCallId::new(1))),
None,
)
.unwrap_err();
assert_eq!(
error,
"runtime Inspector command id mismatch: expected 9, got 8"
);
}
#[test]
fn runtime_inspector_command_dequalifies_current_owner_unique_context_id() {
let raw_json = json!({
"id": 8,
"method": "Runtime.callFunctionOn",
"params": {
"functionDeclaration": "function() { return 42; }",
"uniqueContextId": "TID-current:17.23"
}
})
.to_string();
let rewritten =
rewrite_runtime_inspector_command_for_renderer(&raw_json, None, Some("TID-current"))
.unwrap();
let rewritten: Value = serde_json::from_str(&rewritten).unwrap();
assert_eq!(rewritten["params"]["uniqueContextId"], json!("17.23"));
}
#[test]
fn runtime_inspector_command_does_not_dequalify_another_owners_realm() {
let raw_json = json!({
"id": 8,
"method": "Runtime.evaluate",
"params": {
"expression": "42",
"uniqueContextId": "TID-stale:17.23"
}
})
.to_string();
let rewritten =
rewrite_runtime_inspector_command_for_renderer(&raw_json, None, Some("TID-current"))
.unwrap();
let rewritten: Value = serde_json::from_str(&rewritten).unwrap();
assert_eq!(
rewritten["params"]["uniqueContextId"],
json!("TID-stale:17.23")
);
}
fn connection_with_bidi_page_session() -> CdpConnection {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-owner".to_owned());
browser_context.set_active_target_id("TID-active");
browser_context.attach_active_session("SID-active".to_owned());
browser_context
.active_page_target_mut()
.runtime_slot
.set_page_attachment_id_for_test(1);
conn.install_browser_context_fixture_for_test(browser_context);
conn
}
#[test]
fn runtime_remote_object_validation_allows_session_local_id_collisions() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-owner".to_owned());
browser_context.set_active_target_id("TID-active");
browser_context.attach_active_session("SID-active".to_owned());
assert!(
browser_context
.assign_attached_session_to_target("TID-active", "SID-attached".to_owned(),)
);
conn.install_browser_context_fixture_for_test(browser_context);
conn.register_runtime_remote_object_ids_for_session_owner(
Some("SID-active"),
vec!["same-wire-id".to_owned()],
);
conn.register_runtime_remote_object_ids_for_session_owner(
Some("SID-attached"),
vec!["same-wire-id".to_owned(), "attached-only".to_owned()],
);
assert!(
conn.validate_runtime_remote_object_ids_for_session_owner(
Some("SID-active"),
&["same-wire-id".to_owned()],
)
.is_ok(),
"a current-session handle must win over an identical wire id in another session"
);
assert!(
conn.validate_runtime_remote_object_ids_for_session_owner(
Some("SID-attached"),
&["same-wire-id".to_owned()],
)
.is_ok(),
"the same V8 wire id can independently belong to the attached session"
);
assert_eq!(
conn.validate_runtime_remote_object_ids_for_session_owner(
Some("SID-active"),
&["attached-only".to_owned()],
),
Err("Cannot find object with given id".to_owned()),
"an id known only to another session must remain inaccessible"
);
}
#[test]
fn runtime_remote_object_validation_tolerates_an_empty_browser_context() {
let mut conn = connection_with_bidi_page_session();
conn.insert_browser_context(BrowserContext::new("BID-empty".to_owned()));
assert!(
conn.validate_runtime_remote_object_ids_for_session_owner(
Some("SID-active"),
&["unregistered-wire-id".to_owned()],
)
.is_ok(),
"an unrelated BrowserContext without a Page target must not be dereferenced as active"
);
}
fn bidi_channel_listener_for_test(channel: &str) -> PendingBidiChannelListener {
PendingBidiChannelListener::new(
Some(DevToolsTargetId::from("TID-active")),
Some(crate::devtools_runtime::DevToolsRealmId::from(
"realm-active",
)),
crate::devtools_runtime::DevToolsRemoteHandleId::from(format!(
"channel-proxy-{channel}"
)),
format!("webdriver-bidi-channel-{channel}"),
crate::devtools_runtime::DevToolsBidiChannelProperties {
channel: channel.to_owned(),
ownership: DevToolsResultOwnership::None,
serialization_options: None,
},
)
.expect("test listener should include target and realm")
}
fn renderer_command_descriptor_for_test(command_id: u64) -> RendererCommandDescriptor {
RendererCommandDescriptor::from_synthesized_payload(
json!({
"id": command_id,
"method": "Runtime.evaluate",
"params": { "expression": "1" },
})
.to_string(),
)
.unwrap()
}
fn devtools_session_renderer_command_descriptor_for_test(
command_id: u64,
) -> RendererCommandDescriptor {
let frontend_payload = json!({
"id": command_id,
"method": "Runtime.evaluate",
"params": { "expression": command_id.to_string() },
})
.to_string();
let frontend =
ParsedCdpCommand::parse_str(&frontend_payload).expect("frontend command should parse");
RendererCommandDescriptor::from_frontend_policy(
frontend.json().to_owned(),
frontend.renderer_policy(),
RendererInspectorResponseDelivery::SessionSink,
)
}
fn register_devtools_session_response_for_test(
conn: &mut CdpConnection,
session_id: &str,
frontend_command_id: u64,
attachment_id: RendererAgentAttachmentId,
frontend_payload: &str,
) -> RendererCommandCorrelation {
let frontend =
ParsedCdpCommand::parse_str(frontend_payload).expect("frontend command should parse");
let prepared = conn
.try_register_renderer_call_for_session_owner(
Some(session_id),
frontend_command_id,
Some(attachment_id),
RendererCommandDescriptor::from_frontend_policy(
frontend.json().to_owned(),
frontend.renderer_policy(),
RendererInspectorResponseDelivery::SessionSink,
),
)
.expect("frontend response correlation should register");
let (correlation, response_sender, response_receiver) = prepared.into_parts();
assert!(
response_receiver.is_none(),
"SessionSink delivery must not allocate an adapter-reply receiver"
);
drop(response_sender);
correlation
}
#[test]
fn navigation_termination_consumes_a_devtools_session_frontend_call() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-navigation-termination".to_owned());
browser_context.set_active_target_id("TID-navigation-termination".to_owned());
browser_context.attach_active_session("SID-navigation-termination".to_owned());
conn.install_browser_context_fixture_for_test(browser_context);
let old_attachment = RendererAgentAttachmentId::allocate();
let terminal_attachment = RendererAgentAttachmentId::allocate();
let prepared = conn
.try_register_renderer_call_for_session_owner(
Some("SID-navigation-termination"),
69,
Some(old_attachment),
devtools_session_renderer_command_descriptor_for_test(69),
)
.expect("frontend response correlation should register");
let (old_correlation, old_sender, response_receiver) = prepared.into_parts();
assert!(response_receiver.is_none());
let replacements = {
let browser_context = conn
.browser_context
.as_mut()
.expect("test browser context should remain loaded");
let page_state = browser_context.active_page_target_mut();
page_state
.devtools_sessions
.prepare_renderer_call_replacements(
Some("SID-navigation-termination"),
old_attachment,
terminal_attachment,
)
.expect("navigation replacement should prepare")
};
let (replacement_attachment, terminations, replays) = replacements.into_parts();
assert_eq!(replacement_attachment, terminal_attachment);
assert_eq!(terminations.len(), 1);
assert!(replays.is_empty());
assert!(
old_sender
.send(json!({
"id": old_correlation.renderer_call_id().get(),
"result": {},
}))
.is_err(),
"navigation termination must invalidate the old renderer lease"
);
assert_eq!(
conn.renderer_call_for_frontend_for_session_owner(
Some("SID-navigation-termination"),
69,
),
Some(old_correlation),
"direct session termination must retain the original correlation until settlement"
);
let termination_events = conn.terminate_prepared_renderer_calls_after_navigation(
terminations,
"Inspected target navigated or closed",
);
assert_eq!(termination_events.len(), 1);
let response = termination_events[0]
.protocol_message()
.expect("DevToolsSession termination should emit a frontend response");
assert_eq!(response["id"], json!(69));
assert_eq!(response["sessionId"], json!("SID-navigation-termination"));
assert_eq!(response["error"]["code"], json!(-32000));
assert_eq!(
response["error"]["message"],
json!("Inspected target navigated or closed")
);
assert!(
conn.renderer_runtime_command_cause_for_frontend(
Some("SID-navigation-termination"),
69,
)
.is_none(),
"navigation termination must consume the frontend correlation"
);
}
#[test]
fn navigation_termination_isolated_same_frontend_id_by_devtools_session() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-navigation-sessions".to_owned());
browser_context.set_active_target_id("TID-navigation-sessions".to_owned());
browser_context.attach_active_session("SID-navigation-primary".to_owned());
assert!(browser_context.assign_attached_session_to_target(
"TID-navigation-sessions",
"SID-navigation-attached".to_owned(),
));
conn.install_browser_context_fixture_for_test(browser_context);
let old_attachment = RendererAgentAttachmentId::allocate();
let terminal_attachment = RendererAgentAttachmentId::allocate();
let primary = conn
.try_register_renderer_call_for_session_owner(
Some("SID-navigation-primary"),
71,
Some(old_attachment),
devtools_session_renderer_command_descriptor_for_test(71),
)
.expect("primary frontend response correlation should register");
let attached = conn
.try_register_renderer_call_for_session_owner(
Some("SID-navigation-attached"),
71,
Some(old_attachment),
devtools_session_renderer_command_descriptor_for_test(71),
)
.expect("attached frontend response correlation should register");
let (primary_correlation, primary_sender, primary_receiver) = primary.into_parts();
let (attached_correlation, attached_sender, attached_receiver) = attached.into_parts();
assert!(primary_receiver.is_none());
assert!(attached_receiver.is_none());
let replacements = {
let browser_context = conn
.browser_context
.as_mut()
.expect("test browser context should remain loaded");
let page_state = browser_context.active_page_target_mut();
page_state
.devtools_sessions
.prepare_renderer_call_replacements(
Some("SID-navigation-primary"),
old_attachment,
terminal_attachment,
)
.expect("navigation replacement should prepare for every session")
};
let (_, terminations, replays) = replacements.into_parts();
assert_eq!(terminations.len(), 2);
assert!(replays.is_empty());
for (correlation, sender) in [
(primary_correlation, primary_sender),
(attached_correlation, attached_sender),
] {
assert!(
sender
.send(json!({
"id": correlation.renderer_call_id().get(),
"result": {},
}))
.is_err(),
"navigation replacement must invalidate every old session lease"
);
}
assert_eq!(
conn.renderer_call_for_frontend_for_session_owner(Some("SID-navigation-primary"), 71,),
Some(primary_correlation)
);
assert_eq!(
conn.renderer_call_for_frontend_for_session_owner(Some("SID-navigation-attached"), 71,),
Some(attached_correlation)
);
let termination_events = conn.terminate_prepared_renderer_calls_after_navigation(
terminations,
"Inspected target navigated or closed",
);
assert_eq!(termination_events.len(), 2);
let session_ids = termination_events
.iter()
.map(|event| {
let response = event
.protocol_message()
.expect("each session termination should emit a frontend response");
assert_eq!(response["id"], json!(71));
assert_eq!(response["error"]["code"], json!(-32000));
response["sessionId"]
.as_str()
.expect("attached sessions must retain sessionId")
.to_owned()
})
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
session_ids,
std::collections::BTreeSet::from([
"SID-navigation-primary".to_owned(),
"SID-navigation-attached".to_owned(),
])
);
assert!(
conn.renderer_runtime_command_cause_for_frontend(Some("SID-navigation-primary"), 71,)
.is_none()
);
assert!(
conn.renderer_runtime_command_cause_for_frontend(Some("SID-navigation-attached"), 71,)
.is_none()
);
}
#[test]
fn navigation_termination_preserves_sessionless_page_response_shape() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-navigation-sessionless".to_owned());
browser_context.set_active_target_id("TID-navigation-sessionless".to_owned());
conn.install_browser_context_fixture_for_test(browser_context);
let old_attachment = RendererAgentAttachmentId::allocate();
let terminal_attachment = RendererAgentAttachmentId::allocate();
let prepared = conn
.try_register_renderer_call_for_session_owner(
None,
72,
Some(old_attachment),
devtools_session_renderer_command_descriptor_for_test(72),
)
.expect("sessionless frontend response correlation should register");
let (old_correlation, old_sender, response_receiver) = prepared.into_parts();
assert!(response_receiver.is_none());
let replacements = {
let browser_context = conn
.browser_context
.as_mut()
.expect("test browser context should remain loaded");
let page_state = browser_context.active_page_target_mut();
page_state
.devtools_sessions
.prepare_renderer_call_replacements(None, old_attachment, terminal_attachment)
.expect("sessionless navigation replacement should prepare")
};
let (_, terminations, replays) = replacements.into_parts();
assert_eq!(terminations.len(), 1);
assert!(replays.is_empty());
drop(old_sender);
assert_eq!(
conn.renderer_call_for_frontend_for_session_owner(None, 72),
Some(old_correlation)
);
let termination_events = conn.terminate_prepared_renderer_calls_after_navigation(
terminations,
"Inspected target navigated or closed",
);
assert_eq!(termination_events.len(), 1);
let response = termination_events[0]
.protocol_message()
.expect("sessionless termination should emit a frontend response");
assert_eq!(response["id"], json!(72));
assert!(response.get("sessionId").is_none());
assert_eq!(response["error"]["code"], json!(-32000));
assert!(
conn.renderer_runtime_command_cause_for_frontend(None, 72)
.is_none()
);
}
#[test]
fn devtools_session_output_wrong_attachment_does_not_consume_live_correlation() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-attachment-race".to_owned());
browser_context.set_active_target_id("TID-attachment-race".to_owned());
browser_context.attach_active_session("SID-attachment-race".to_owned());
conn.install_browser_context_fixture_for_test(browser_context);
let live_attachment = RendererAgentAttachmentId::allocate();
let stale_attachment = RendererAgentAttachmentId::allocate();
let correlation = register_devtools_session_response_for_test(
&mut conn,
"SID-attachment-race",
70,
live_attachment,
r#"{"id":70,"method":"Runtime.evaluate","params":{"expression":"70"}}"#,
);
let response = || {
RendererRuntimeInspectorMessage::protocol(json!({
"id": correlation.renderer_call_id().get(),
"result": { "result": { "type": "number", "value": 70 } },
}))
};
let mut stale_messages = vec![response()];
conn.restore_frontend_command_ids_in_devtools_session_output_for_owner(
&CommandOwnerScope::for_session("SID-attachment-race"),
Some(stale_attachment),
&mut stale_messages,
true,
);
assert!(
stale_messages.is_empty(),
"a response from the retired attachment must be dropped"
);
assert!(
conn.renderer_runtime_command_cause_for_frontend(Some("SID-attachment-race"), 70,)
.is_some(),
"the retired attachment must not consume the live correlation"
);
let mut live_messages = vec![response()];
conn.restore_frontend_command_ids_in_devtools_session_output_for_owner(
&CommandOwnerScope::for_session("SID-attachment-race"),
Some(live_attachment),
&mut live_messages,
true,
);
let [RendererRuntimeInspectorMessage::Protocol(message)] = live_messages.as_slice() else {
panic!("the live attachment must publish exactly one response");
};
assert_eq!(message.value()["id"], json!(70));
assert!(
conn.renderer_runtime_command_cause_for_frontend(Some("SID-attachment-race"), 70,)
.is_none(),
"the live attachment must consume the correlation exactly once"
);
}
#[test]
fn devtools_session_output_keeps_first_of_duplicate_terminal_responses() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-duplicate-response".to_owned());
browser_context.set_active_target_id("TID-duplicate-response".to_owned());
browser_context.attach_active_session("SID-duplicate-response".to_owned());
conn.install_browser_context_fixture_for_test(browser_context);
let attachment_id = RendererAgentAttachmentId::allocate();
let correlation = register_devtools_session_response_for_test(
&mut conn,
"SID-duplicate-response",
71,
attachment_id,
r#"{"id":71,"method":"Runtime.evaluate","params":{"expression":"71"}}"#,
);
let mut messages = vec![
RendererRuntimeInspectorMessage::protocol(json!({
"id": correlation.renderer_call_id().get(),
"result": { "result": { "type": "string", "value": "first" } },
})),
RendererRuntimeInspectorMessage::protocol(json!({
"id": correlation.renderer_call_id().get(),
"result": { "result": { "type": "string", "value": "duplicate" } },
})),
];
conn.restore_frontend_command_ids_in_devtools_session_output_for_owner(
&CommandOwnerScope::for_session("SID-duplicate-response"),
Some(attachment_id),
&mut messages,
true,
);
let [RendererRuntimeInspectorMessage::Protocol(message)] = messages.as_slice() else {
panic!("duplicate terminal responses must collapse to one frontend response");
};
assert_eq!(message.value()["id"], json!(71));
assert_eq!(message.value()["result"]["result"]["value"], json!("first"));
}
#[test]
fn devtools_session_output_restores_interleaved_calls_without_reordering() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-interleaved-response".to_owned());
browser_context.set_active_target_id("TID-interleaved-response".to_owned());
browser_context.attach_active_session("SID-interleaved-response".to_owned());
conn.install_browser_context_fixture_for_test(browser_context);
let attachment_id = RendererAgentAttachmentId::allocate();
let first = register_devtools_session_response_for_test(
&mut conn,
"SID-interleaved-response",
80,
attachment_id,
r#"{"id":80,"method":"Runtime.evaluate","params":{"expression":"80"}}"#,
);
let second = register_devtools_session_response_for_test(
&mut conn,
"SID-interleaved-response",
81,
attachment_id,
r#"{"id":81,"method":"Runtime.evaluate","params":{"expression":"81"}}"#,
);
let mut messages = vec![
RendererRuntimeInspectorMessage::protocol(json!({
"id": second.renderer_call_id().get(),
"result": { "result": { "type": "number", "value": 81 } },
})),
RendererRuntimeInspectorMessage::protocol(json!({
"method": "Debugger.scriptParsed",
"params": { "scriptId": "interleaved" },
})),
RendererRuntimeInspectorMessage::protocol(json!({
"id": first.renderer_call_id().get(),
"result": { "result": { "type": "number", "value": 80 } },
})),
];
conn.restore_frontend_command_ids_in_devtools_session_output_for_owner(
&CommandOwnerScope::for_session("SID-interleaved-response"),
Some(attachment_id),
&mut messages,
true,
);
assert_eq!(messages.len(), 3);
let RendererRuntimeInspectorMessage::Protocol(second_response) = &messages[0] else {
panic!("the second call response must remain first");
};
let RendererRuntimeInspectorMessage::Protocol(notification) = &messages[1] else {
panic!("the notification must remain between the responses");
};
let RendererRuntimeInspectorMessage::Protocol(first_response) = &messages[2] else {
panic!("the first call response must remain last");
};
assert_eq!(second_response.value()["id"], json!(81));
assert_eq!(
notification.value()["method"],
json!("Debugger.scriptParsed")
);
assert_eq!(first_response.value()["id"], json!(80));
}
#[tokio::test]
async fn devtools_session_output_restores_only_the_exact_registered_frontend_response() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-session-output".to_owned());
browser_context.set_active_target_id("TID-session-output".to_owned());
browser_context.attach_active_session("SID-session-output".to_owned());
conn.install_browser_context_fixture_for_test(browser_context);
let attachment_id = RendererAgentAttachmentId::allocate();
let frontend = ParsedCdpCommand::parse_str(
r#"{"id":44,"method":"Runtime.evaluate","params":{"expression":"({ answer: 42 })","objectGroup":"nested-main"}}"#,
)
.expect("frontend command should parse");
let prepared = conn
.try_register_renderer_call_for_session_owner(
Some("SID-session-output"),
44,
Some(attachment_id),
RendererCommandDescriptor::from_frontend_policy(
frontend.json().to_owned(),
frontend.renderer_policy(),
RendererInspectorResponseDelivery::SessionSink,
),
)
.expect("frontend response correlation should register");
let (correlation, response_sender, response_receiver) = prepared.into_parts();
assert!(
response_receiver.is_none(),
"SessionSink delivery must not allocate an adapter-reply receiver"
);
drop(response_sender);
let mut messages = vec![
RendererRuntimeInspectorMessage::protocol(json!({
"method": "Debugger.scriptParsed",
"params": { "scriptId": "7" },
})),
RendererRuntimeInspectorMessage::protocol(json!({
"id": correlation.renderer_call_id().get(),
"result": {
"result": {
"type": "object",
"objectId": "nested-main-object"
}
},
})),
RendererRuntimeInspectorMessage::protocol(json!({
"id": correlation.renderer_call_id().get() + 1,
"result": { "scriptSource": "stale" },
})),
];
conn.restore_frontend_command_ids_in_devtools_session_output_for_owner(
&CommandOwnerScope::for_session("SID-session-output"),
Some(attachment_id),
&mut messages,
true,
);
assert_eq!(
messages.len(),
2,
"stale renderer responses must be dropped"
);
let RendererRuntimeInspectorMessage::Protocol(response) = &messages[1] else {
panic!("expected a protocol response");
};
assert_eq!(response.value()["id"], json!(44));
assert_eq!(
conn.runtime_remote_object_group_for_session_owner(
Some("SID-session-output"),
"nested-main-object",
),
Some("nested-main".to_owned()),
"session output must retain Runtime object ownership metadata",
);
assert!(
conn.renderer_runtime_command_cause_for_frontend(Some("SID-session-output"), 44,)
.is_none(),
"publishing the session response must consume its exact correlation"
);
}
#[test]
fn devtools_session_output_preserves_remote_object_group_projection() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-session-projection".to_owned());
browser_context.set_active_target_id("TID-session-projection".to_owned());
browser_context.attach_active_session("SID-session-projection".to_owned());
conn.install_browser_context_fixture_for_test(browser_context);
let session_id = Some("SID-session-projection");
let attachment_id = RendererAgentAttachmentId::allocate();
conn.register_runtime_remote_object_ids_from_value_for_session_owner_with_group(
session_id,
&json!({ "objectId": "nested-parent-object" }),
"nested-object-group",
);
let get_properties = ParsedCdpCommand::parse_str(
r#"{"id":45,"method":"Runtime.getProperties","params":{"objectId":"nested-parent-object","ownProperties":true}}"#,
)
.expect("getProperties command should parse");
let prepared = conn
.try_register_renderer_call_for_session_owner(
session_id,
45,
Some(attachment_id),
RendererCommandDescriptor::from_frontend_policy(
get_properties.json().to_owned(),
get_properties.renderer_policy(),
RendererInspectorResponseDelivery::SessionSink,
),
)
.expect("getProperties response correlation should register");
let (correlation, response_sender, response_receiver) = prepared.into_parts();
assert!(response_receiver.is_none());
drop(response_sender);
let mut messages = vec![RendererRuntimeInspectorMessage::protocol(json!({
"id": correlation.renderer_call_id().get(),
"result": {
"result": [{
"name": "child",
"value": {
"type": "object",
"objectId": "nested-child-object"
}
}]
},
}))];
conn.restore_frontend_command_ids_in_devtools_session_output_for_owner(
&CommandOwnerScope::for_session("SID-session-projection"),
Some(attachment_id),
&mut messages,
true,
);
assert_eq!(
conn.runtime_remote_object_group_for_session_owner(session_id, "nested-child-object",),
Some("nested-object-group".to_owned()),
"getProperties results must inherit the receiver object's group",
);
}
fn bidi_channel_listener_residence_for_test(
conn: &CdpConnection,
session_id: &str,
channel: &str,
) -> BidiChannelListenerResidence {
BidiChannelListenerResidence::new(
BidiChannelPageOwner::capture_for_owner(
conn,
CommandOwnerScope::for_session(session_id),
)
.expect("test Page attachment"),
bidi_channel_listener_for_test(channel),
)
}
fn deeply_nested_plain_value(mut value: Value, depth: usize) -> Value {
for _ in 0..depth {
value = json!({ "child": [value] });
}
value
}
fn run_deep_protocol_value_test(name: &'static str, test: impl FnOnce() + Send + 'static) {
let result = std::thread::Builder::new()
.name(name.to_owned())
.stack_size(32 * 1024 * 1024)
.spawn(test)
.expect("large-stack protocol value test thread should spawn")
.join();
if let Err(payload) = result {
std::panic::resume_unwind(payload);
}
}
fn take_only_protocol_work(
conn: &mut CdpConnection,
) -> crate::domains::activity::ProtocolSchedulerWork {
let scheduler_events = conn.take_scheduler_events();
let [CdpSchedulerEvent::ProtocolWorkPublished { work }] =
<[_; 1]>::try_from(scheduler_events)
.expect("test action must publish exactly one protocol work")
else {
unreachable!("array pattern fixes the only event kind")
};
work
}
#[test]
fn runtime_remote_object_ids_include_await_promise_handles() {
let value = json!({
"params": {
"errorObjectId": "error-1",
"promiseObjectId": "promise-1",
"arguments": [{ "objectId": "arg-1" }]
}
});
let object_ids = runtime_remote_object_ids_in_value(&value);
assert_eq!(
object_ids,
vec![
"arg-1".to_owned(),
"error-1".to_owned(),
"promise-1".to_owned()
],
"Runtime object-owner validation must include objectId, promiseObjectId, and errorObjectId handles"
);
assert_eq!(
runtime_remote_object_ids_in_map(
value
.as_object()
.expect("the test protocol payload must be an object")
),
object_ids,
"validated object params must preserve the existing recursive handle scan"
);
}
#[test]
fn runtime_remote_object_ids_respect_protocol_depth_cap() {
run_deep_protocol_value_test("runtime-remote-object-ids-depth-cap", || {
let object_ids = runtime_remote_object_ids_in_value(&deeply_nested_plain_value(
json!({ "objectId": "too-deep" }),
MAX_INSPECTOR_PROTOCOL_VALUE_DEPTH + 8,
));
assert!(object_ids.is_empty());
});
}
#[test]
fn bidi_channel_listener_owner_work_publishes_concrete_scheduler_work() {
let mut conn = connection_with_bidi_page_session();
let listener = bidi_channel_listener_residence_for_test(&conn, "SID-active", "wake");
conn.publish_bidi_channel_listener_start(listener);
let scheduler_events = conn.take_scheduler_events();
let [CdpSchedulerEvent::ProtocolWorkPublished { work }] = scheduler_events.as_slice()
else {
panic!("listener start must publish one concrete protocol work: {scheduler_events:?}");
};
assert_eq!(
work.kind(),
crate::domains::activity::ProtocolSchedulerWorkKind::BidiChannelOwnerAction
);
assert_eq!(
work.bidi_channel_owner_action_kind(),
Some(BidiChannelOwnerActionKind::StartListener)
);
assert_eq!(work.publish_sequence().get(), 1);
}
#[test]
fn bidi_channel_actions_keep_causal_publication_order() {
let mut conn = connection_with_bidi_page_session();
let listener = bidi_channel_listener_residence_for_test(&conn, "SID-active", "ordered");
let owner = listener.owner().clone();
conn.publish_bidi_channel_listener_start(listener);
conn.publish_bidi_channel_object_group_release(owner, "webdriver-bidi-channel-ordered");
let scheduler_events = conn.take_scheduler_events();
let works = scheduler_events
.iter()
.map(|event| {
let CdpSchedulerEvent::ProtocolWorkPublished { work } = event else {
panic!("BiDi action must not fall back to source-shaped capture: {event:?}");
};
(
work.publish_sequence().get(),
work.bidi_channel_owner_action_kind(),
)
})
.collect::<Vec<_>>();
assert_eq!(
works,
vec![
(1, Some(BidiChannelOwnerActionKind::StartListener)),
(2, Some(BidiChannelOwnerActionKind::ReleaseObjectGroup)),
],
"concrete actions must retain publication order instead of regrouping releases"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn document_node_snapshot_for_backend_node_id_reads_live_renderer_snapshot() {
let mut ctx = TestContext::new();
let mut browser_context = BrowserContext::new("BID-runtime-node-snapshot".to_owned());
browser_context.set_active_target_id("TID-runtime-node-snapshot".to_owned());
ctx.conn
.install_browser_context_fixture_for_test(browser_context);
ctx.install_navigation_fixture_for_session_owner(
"data:text/html,<html><body><article id='target'>live</article></body></html>",
None,
)
.await;
ctx.sent.clear();
ctx.process_async(json!({
"id": 1,
"method": "Runtime.evaluate",
"params": { "expression": "document.querySelector('#target')" }
}))
.await;
let evaluated = ctx.take_response_by_id(1);
let object_id = evaluated["result"]["result"]["objectId"]
.as_str()
.map(str::to_owned)
.unwrap_or_else(|| {
panic!("Runtime.evaluate should return target objectId: {evaluated}")
});
ctx.process_async(json!({
"id": 2,
"method": "DOM.describeNode",
"params": { "objectId": object_id, "depth": 0 }
}))
.await;
let described = ctx.take_response_by_id(2);
let backend_node_id = described["result"]["node"]["backendNodeId"]
.as_u64()
.and_then(|node_id| u32::try_from(node_id).ok())
.expect("DOM.describeNode should return backendNodeId");
let owner = CommandOwnerScope::capture(&ctx.conn, None);
let snapshot = ctx
.conn
.document_node_snapshot_for_backend_node_id_for_owner_async(
&owner,
backend_node_id,
1,
false,
)
.await
.expect("document node id snapshot command should complete")
.expect("target node snapshot should exist");
assert_eq!(snapshot.snapshot.local_name, "article");
assert!(
snapshot
.snapshot
.attributes
.iter()
.any(|attribute| attribute.local_name == "id" && attribute.value == "target"),
"snapshot should preserve target id attribute: {snapshot:?}"
);
let backend_node_id = snapshot
.snapshot
.backend_node_id
.expect("renderer snapshot should assign backendNodeId");
assert!(
is_renderer_backend_node_id(backend_node_id),
"node-id snapshot helper should return renderer backend id namespace: {snapshot:?}"
);
assert!(
snapshot
.snapshot
.children
.iter()
.any(|child| child.node_value == "live"),
"depth=1 snapshot should include text child: {snapshot:?}"
);
}
#[test]
fn runtime_realm_inventory_conversion_keeps_context_without_native_realm_id() {
let event = runtime_realm_info_to_execution_context_event(
RendererRuntimeRealmInfo {
context_id: 7,
realm_id: None,
frame_id: Some("FRAME-child".to_owned()),
origin: "https://example.test".to_owned(),
name: String::new(),
is_default: true,
context_type: "default".to_owned(),
grant_universal_access: None,
},
Some("FRAME-owner"),
None,
)
.expect("Script.getRealms must not fail when DevTools attaches after context creation");
assert_eq!(event.context_id, Some(7));
assert_eq!(
event.frame_id.as_ref().map(|frame_id| frame_id.as_str()),
Some("FRAME-child")
);
assert_eq!(
event.realm_id, None,
"protocol should not synthesize a realm id when renderer did not capture V8 uniqueId"
);
}
#[test]
fn runtime_realm_inventory_conversion_uses_owner_frame_when_renderer_frame_is_missing() {
let event = runtime_realm_info_to_execution_context_event(
RendererRuntimeRealmInfo {
context_id: 9,
realm_id: Some("native-realm-9".to_owned()),
frame_id: None,
origin: "https://example.test".to_owned(),
name: "https://example.test/page".to_owned(),
is_default: true,
context_type: "default".to_owned(),
grant_universal_access: None,
},
Some("FRAME-owner"),
Some(DevToolsTargetId::from("TARGET-1")),
)
.expect("native renderer realm ids should convert");
assert_eq!(
event.realm_id.as_ref().map(|realm_id| realm_id.as_str()),
Some("TARGET-1:native-realm-9"),
"external realm ids must include the target owner because native V8 uniqueIds are only unique within a renderer runtime"
);
assert_eq!(
event.frame_id.as_ref().map(|frame_id| frame_id.as_str()),
Some("FRAME-owner"),
"owner frame is used only when renderer realm inventory has no per-realm frame id"
);
assert_eq!(
event.target_id.as_ref().map(|target_id| target_id.as_str()),
Some("TARGET-1")
);
}
#[test]
fn route_inspector_notifications_strip_stale_session_id_without_current_session() {
let mut conn = CdpConnection::default();
let mut response_events = Vec::new();
let mut background_events = Vec::new();
let current_seen = conn.route_inspector_messages_into(
vec![json!({
"method": "Runtime.executionContextCreated",
"sessionId": "STALE",
"params": {
"context": {
"id": 7,
"origin": "https://example.test",
"name": "",
"uniqueId": "realm-7",
"auxData": {
"isDefault": true,
"type": "default",
"frameId": "FRAME-1"
}
}
}
})],
None,
None,
&mut response_events,
&mut background_events,
);
assert!(!current_seen);
assert!(
response_events.is_empty(),
"inspector notifications must not be routed through command response output"
);
assert_eq!(background_events.len(), 1);
assert!(
background_events[0].protocol_message().is_none(),
"runtime context notification should remain typed until wire projection"
);
let (message, automation_event) = background_events[0].clone().into_parts();
assert!(matches!(
automation_event,
Some(AutomationEvent::RuntimeExecutionContextCreated(_))
));
assert!(
message.get("sessionId").is_none(),
"notifications routed without a current session must not leak a stale sessionId"
);
assert_eq!(message["method"], json!("Runtime.executionContextCreated"));
assert_eq!(message["params"]["context"]["id"], json!(7));
assert_eq!(message["params"]["context"]["uniqueId"], json!("realm-7"));
}
#[test]
fn route_inspector_runtime_context_notifications_use_current_session() {
let mut conn = CdpConnection::default();
let mut response_events = Vec::new();
let mut background_events = Vec::new();
let current_seen = conn.route_inspector_messages_into(
vec![
json!({
"method": "Runtime.executionContextDestroyed",
"sessionId": "STALE",
"params": {
"executionContextId": 7,
"executionContextUniqueId": "realm-7"
}
}),
json!({
"method": "Runtime.executionContextsCleared",
"sessionId": "STALE",
"params": {}
}),
],
None,
Some("SID-1"),
&mut response_events,
&mut background_events,
);
assert!(!current_seen);
assert!(
response_events.is_empty(),
"inspector notifications must not be routed through command response output"
);
assert_eq!(background_events.len(), 2);
assert!(
background_events[0].protocol_message().is_none(),
"destroyed notification should remain typed until wire projection"
);
assert!(
background_events[1].protocol_message().is_none(),
"cleared notification should remain typed until wire projection"
);
let (destroyed, destroyed_automation_event) = background_events[0].clone().into_parts();
let (cleared, cleared_automation_event) = background_events[1].clone().into_parts();
assert!(matches!(
destroyed_automation_event,
Some(AutomationEvent::RuntimeExecutionContextDestroyed(_))
));
assert!(matches!(
cleared_automation_event,
Some(AutomationEvent::RuntimeExecutionContextsCleared(_))
));
assert_eq!(destroyed["sessionId"], json!("SID-1"));
assert_eq!(
destroyed["method"],
json!("Runtime.executionContextDestroyed")
);
assert_eq!(destroyed["params"]["executionContextId"], json!(7));
assert_eq!(
destroyed["params"]["executionContextUniqueId"],
json!("realm-7")
);
assert_eq!(cleared["sessionId"], json!("SID-1"));
assert_eq!(cleared["method"], json!("Runtime.executionContextsCleared"));
assert_eq!(cleared["params"], json!({}));
}
#[test]
fn pending_inspector_await_registry_scopes_entries_to_devtools_session() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-owner".to_owned());
browser_context.set_active_target_id("TID-active".to_owned());
browser_context.attach_active_session("SID-active".to_owned());
browser_context.insert_page_target_host(PageTargetHost::with_url(
"TID-bg".to_owned(),
Some("SID-bg".to_owned()),
"about:blank#bg".to_owned(),
));
conn.install_browser_context_fixture_for_test(browser_context);
conn.register_pending_inspector_await(1, Some("SID-active"));
conn.register_pending_inspector_await(2, Some("SID-bg"));
{
let browser_context = conn.browser_context.as_ref().expect("browser context");
assert!(
browser_context.active_page_target().devtools_sessions
[moli_page_types::DevToolsSessionKey::Primary]
.has_pending_inspector_awaits(),
"active DevTools session should physically store its pending await"
);
assert!(
browser_context
.background_target("TID-bg")
.filter(|target| target.has_non_default_session_state())
.is_some_and(|state| state.devtools_sessions
[moli_page_types::DevToolsSessionKey::Primary]
.has_pending_inspector_awaits()),
"background DevTools session should physically store its pending await"
);
}
assert!(conn.has_pending_inspector_awaits_for_session_owner(Some("SID-active")));
assert!(conn.has_pending_inspector_awaits_for_session_owner(Some("SID-bg")));
let mut direct_events = Vec::new();
let mut claimed_events = Vec::new();
conn.fail_pending_inspector_awaits_for_session_owner_background_events_into(
&mut direct_events,
&mut claimed_events,
Some("SID-active"),
"Page closed",
);
assert!(claimed_events.is_empty());
assert_eq!(direct_events.len(), 1);
let (message, automation_event) = direct_events.remove(0).into_parts();
assert!(automation_event.is_none());
assert_eq!(message["id"], json!(1));
assert_eq!(message["sessionId"], json!("SID-active"));
assert!(!conn.has_pending_inspector_awaits_for_session_owner(Some("SID-active")));
assert!(conn.has_pending_inspector_awaits_for_session_owner(Some("SID-bg")));
let mut response_events = Vec::new();
let mut background_events = Vec::new();
let seen = conn.route_inspector_messages_into(
vec![json!({
"id": 2,
"result": { "result": { "type": "string", "value": "bg" } }
})],
None,
Some("SID-bg"),
&mut response_events,
&mut background_events,
);
assert!(background_events.is_empty());
assert!(!seen);
assert_eq!(response_events.len(), 1);
let message = response_events[0]
.protocol_message()
.expect("owner routed response should carry protocol message");
assert_eq!(message["id"], json!(2));
assert_eq!(message["sessionId"], json!("SID-bg"));
assert!(!conn.has_pending_inspector_awaits());
}
#[test]
fn same_pending_inspector_await_id_is_isolated_by_devtools_session() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-same-id".to_owned());
browser_context.set_active_target_id("TID-active".to_owned());
browser_context.attach_active_session("SID-active".to_owned());
browser_context.insert_page_target_host(PageTargetHost::with_url(
"TID-bg".to_owned(),
Some("SID-bg".to_owned()),
"about:blank#bg".to_owned(),
));
conn.install_browser_context_fixture_for_test(browser_context);
conn.register_pending_inspector_await(1, Some("SID-active"));
conn.register_pending_inspector_await(1, Some("SID-bg"));
conn.register_runtime_await_job_for_owner(
1,
&CommandOwnerScope::for_session("SID-active"),
None,
"evaluate",
);
conn.register_runtime_await_job_for_owner(
1,
&CommandOwnerScope::for_session("SID-bg"),
None,
"evaluate",
);
assert_eq!(conn.pending_runtime_await_jobs.len(), 2);
let claimed = conn
.claim_pending_inspector_await_for_scheduler_deferred_reply(
1,
&CommandOwnerScope::for_session("SID-active"),
)
.expect("active session await should be independently claimable");
assert!(conn.has_claimed_pending_inspector_awaits_for_session_owner(Some("SID-active")));
assert!(!conn.has_unclaimed_pending_inspector_awaits_for_session_owner(Some("SID-active")));
assert!(conn.has_unclaimed_pending_inspector_awaits_for_session_owner(Some("SID-bg")));
conn.cancel_claimed_pending_inspector_await_for_scheduler_deferred_reply(
Some(claimed),
"forgotten",
);
assert!(!conn.has_pending_inspector_awaits_for_session_owner(Some("SID-active")));
assert!(conn.has_pending_inspector_awaits_for_session_owner(Some("SID-bg")));
assert_eq!(conn.pending_runtime_await_jobs.len(), 1);
assert_eq!(
conn.runtime_await_job_trace_fields(1, Some("SID-bg"))["sessionId"],
json!("SID-bg")
);
let mut response_events = Vec::new();
let mut background_events = Vec::new();
let stale_session_seen = conn.route_inspector_messages_into(
vec![json!({
"id": 1,
"result": { "result": { "type": "string", "value": "stale" } }
})],
None,
Some("SID-active"),
&mut response_events,
&mut background_events,
);
assert!(!stale_session_seen);
assert!(response_events.is_empty());
assert!(background_events.is_empty());
assert!(conn.has_pending_inspector_awaits_for_session_owner(Some("SID-bg")));
assert_eq!(conn.pending_runtime_await_jobs.len(), 1);
let background_seen = conn.route_inspector_messages_into(
vec![json!({
"id": 1,
"result": { "result": { "type": "string", "value": "bg" } }
})],
Some(1),
Some("SID-bg"),
&mut response_events,
&mut background_events,
);
assert!(background_seen);
assert!(background_events.is_empty());
assert_eq!(response_events.len(), 1);
let message = response_events[0]
.protocol_message()
.expect("background response should remain a protocol response");
assert_eq!(message["id"], json!(1));
assert_eq!(message["sessionId"], json!("SID-bg"));
assert!(!conn.has_pending_inspector_awaits());
assert!(conn.pending_runtime_await_jobs.is_empty());
}
#[test]
fn failed_await_registration_does_not_discard_existing_renderer_owner() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-duplicate-owner".to_owned());
browser_context.set_active_target_id("TID-duplicate-owner".to_owned());
browser_context.attach_active_session("SID-duplicate-owner".to_owned());
conn.install_browser_context_fixture_for_test(browser_context);
let original = conn
.try_register_renderer_call_for_session_owner(
Some("SID-duplicate-owner"),
17,
None,
renderer_command_descriptor_for_test(17),
)
.expect("first renderer command should own the frontend id")
.correlation();
conn.try_register_pending_inspector_await_with_object_group_for_owner(
17,
&CommandOwnerScope::for_session("SID-duplicate-owner"),
None,
)
.expect("await state is registered before renderer dispatch");
assert_eq!(
conn.try_register_renderer_call_for_session_owner(
Some("SID-duplicate-owner"),
17,
None,
renderer_command_descriptor_for_test(17),
)
.unwrap_err(),
"Duplicate `id` in protocol request"
);
conn.forget_pending_inspector_await(17, Some("SID-duplicate-owner"));
assert_eq!(
conn.take_renderer_call_for_frontend_for_session_owner(
Some("SID-duplicate-owner"),
17,
),
Some(original),
"failed await dispatch must not consume the older command's correlation"
);
}
#[test]
fn bidi_listener_cancellation_discards_correlation_registered_first() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-listener-cancel".to_owned());
browser_context.set_active_target_id("TID-listener-cancel".to_owned());
browser_context.attach_active_session("SID-listener-cancel".to_owned());
browser_context
.active_page_target_mut()
.runtime_slot
.set_page_attachment_id_for_test(1);
conn.install_browser_context_fixture_for_test(browser_context);
conn.try_register_renderer_call_for_session_owner(
Some("SID-listener-cancel"),
23,
Some(RendererAgentAttachmentId::allocate()),
renderer_command_descriptor_for_test(23),
)
.expect("listener renderer command should register before listener ownership");
let listener =
bidi_channel_listener_residence_for_test(&conn, "SID-listener-cancel", "cancel");
conn.register_pending_bidi_channel_listener(23, Some("SID-listener-cancel"), listener);
let mut direct_events = Vec::new();
let mut claimed_events = Vec::new();
conn.fail_pending_inspector_awaits_for_session_owner_background_events_into(
&mut direct_events,
&mut claimed_events,
Some("SID-listener-cancel"),
"Page navigated",
);
assert!(direct_events.is_empty());
assert!(claimed_events.is_empty());
assert!(
conn.try_register_renderer_call_for_session_owner(
Some("SID-listener-cancel"),
23,
None,
renderer_command_descriptor_for_test(23),
)
.is_ok(),
"listener cancellation must release the frontend command id"
);
}
#[test]
fn non_await_cancellation_releases_frontend_command_id() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-command-cancel".to_owned());
browser_context.set_active_target_id("TID-command-cancel".to_owned());
browser_context.attach_active_session("SID-command-cancel".to_owned());
conn.install_browser_context_fixture_for_test(browser_context);
conn.try_register_renderer_call_for_session_owner(
Some("SID-command-cancel"),
29,
None,
renderer_command_descriptor_for_test(29),
)
.expect("non-await command should register a renderer correlation");
conn.forget_pending_inspector_await(29, Some("SID-command-cancel"));
assert!(
conn.try_register_renderer_call_for_session_owner(
Some("SID-command-cancel"),
29,
None,
renderer_command_descriptor_for_test(29),
)
.is_ok(),
"cancelled non-await command must release the frontend command id"
);
}
#[tokio::test]
async fn terminal_session_cleanup_completes_non_await_once_and_releases_frontend_id() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-terminal".to_owned());
browser_context.set_active_target_id("TID-terminal".to_owned());
browser_context.attach_active_session("SID-terminal".to_owned());
conn.install_browser_context_fixture_for_test(browser_context);
let attachment = RendererAgentAttachmentId::allocate();
let prepared = conn
.try_register_renderer_call_for_session_owner(
Some("SID-terminal"),
31,
Some(attachment),
RendererCommandDescriptor::from_synthesized_payload(
json!({
"id": 31,
"method": "Console.clearMessages",
"params": {},
})
.to_string(),
)
.unwrap(),
)
.expect("non-await command should register");
let (correlation, old_sender, response_receiver) = prepared.into_parts();
let response_receiver = response_receiver
.expect("a synthesized AdapterReply call must allocate a response receiver");
let mut direct_events = Vec::new();
let mut claimed_events = Vec::new();
conn.fail_pending_inspector_awaits_for_session_owner_background_events_into(
&mut direct_events,
&mut claimed_events,
Some("SID-terminal"),
"Inspector detached",
);
assert!(claimed_events.is_empty());
assert_eq!(direct_events.len(), 1);
let (message, automation_event) = direct_events.remove(0).into_parts();
assert!(automation_event.is_none());
assert_eq!(message["id"], json!(31));
assert_eq!(message["sessionId"], json!("SID-terminal"));
assert_eq!(message["error"]["code"], json!(-32000));
assert_eq!(message["error"]["message"], json!("Inspector detached"));
assert!(
old_sender
.send(json!({
"id": correlation.renderer_call_id().get(),
"result": {},
}))
.is_err(),
"terminal transition must invalidate the renderer's old response lease"
);
let completion = response_receiver
.await
.expect("terminal transition should complete the shared receiver");
assert_eq!(completion.renderer_agent_attachment_id(), None);
let terminal = completion
.output
.protocol_response(completion.call_id)
.expect("terminal response payload");
assert_eq!(terminal["error"]["code"], json!(-32000));
assert_eq!(terminal["error"]["message"], json!("Inspector detached"));
assert!(
conn.try_register_renderer_call_for_session_owner(
Some("SID-terminal"),
31,
None,
renderer_command_descriptor_for_test(31),
)
.is_ok(),
"terminal cleanup must release the frontend command id"
);
}
#[test]
fn pending_inspector_await_response_routes_through_owner_runtime_response() {
let mut conn = CdpConnection::default();
let mut browser_context = BrowserContext::new("BID-owner-output".to_owned());
browser_context.set_active_target_id("TID-active".to_owned());
browser_context.attach_active_session("SID-active".to_owned());
browser_context.insert_page_target_host(PageTargetHost::with_url(
"TID-bg".to_owned(),
Some("SID-bg".to_owned()),
"about:blank#bg".to_owned(),
));
conn.install_browser_context_fixture_for_test(browser_context);
conn.try_register_pending_inspector_await_with_object_group_for_owner(
77,
&CommandOwnerScope::for_session("SID-bg"),
Some("runtime-group"),
)
.unwrap();
conn.register_runtime_await_job_for_owner(
77,
&CommandOwnerScope::for_session("SID-bg"),
Some("runtime-group"),
"evaluate",
);
let mut response_events = Vec::new();
let mut background_events = Vec::new();
let seen = conn.route_inspector_messages_with_background_events_into(
vec![json!({
"id": 77,
"result": {
"result": {
"type": "object",
"objectId": "object-bg-1"
}
}
})],
Some(77),
Some("SID-bg"),
&mut response_events,
&mut background_events,
);
assert!(
seen,
"matching owner runtime response should complete the current command"
);
assert!(
background_events.is_empty(),
"plain Runtime.evaluate response should not become a side event"
);
assert_eq!(response_events.len(), 1);
let message = response_events[0]
.protocol_message()
.expect("owner routed response should carry protocol message");
assert_eq!(message["id"], json!(77));
assert_eq!(message["sessionId"], json!("SID-bg"));
assert!(
!conn.has_pending_inspector_awaits(),
"owner runtime response should consume pending inspector await state"
);
assert_eq!(
conn.runtime_remote_object_group_for_session_owner(Some("SID-bg"), "object-bg-1"),
Some("runtime-group".to_owned()),
"owner runtime response should register handles against the producing owner"
);
}
#[tokio::test]
async fn failing_pending_awaits_retire_concrete_listener_work_without_client_error() {
let mut conn = connection_with_bidi_page_session();
conn.register_pending_inspector_await(1, Some("SID-active"));
conn.register_runtime_remote_object_ids_for_session_owner_with_group(
Some("SID-active"),
vec!["channel-proxy".to_owned()],
"webdriver-bidi-channel-test",
);
let listener = bidi_channel_listener_residence_for_test(&conn, "SID-active", "test");
conn.register_pending_bidi_channel_listener(2, Some("SID-active"), listener.clone());
conn.publish_bidi_channel_listener_start(listener);
let held_listener_work = take_only_protocol_work(&mut conn);
conn.runtime_session_owner_slot_mut(Some("SID-active"))
.expect("test runtime slot")
.replace_page_attachment_id_for_test();
let mut direct_events = Vec::new();
let mut claimed_events = Vec::new();
conn.fail_pending_inspector_awaits_for_session_owner_background_events_into(
&mut direct_events,
&mut claimed_events,
Some("SID-active"),
"Page navigated",
);
assert!(claimed_events.is_empty());
assert_eq!(direct_events.len(), 1);
let (message, automation_event) = direct_events.remove(0).into_parts();
assert!(automation_event.is_none());
assert_eq!(message["id"], json!(1));
assert_eq!(message["sessionId"], json!("SID-active"));
assert!(!conn.has_pending_inspector_awaits_for_session_owner(Some("SID-active")));
assert!(
conn.runtime_remote_object_group_for_session_owner(Some("SID-active"), "channel-proxy")
.is_none(),
"invalidated BiDi listener should remove its channel object group"
);
let outcome = conn
.complete_ready_protocol_scheduler_work_turn(held_listener_work)
.await;
assert!(
outcome.into_parts().0.is_empty(),
"stale concrete listener work must not produce protocol output"
);
assert!(
!conn.has_pending_inspector_awaits_for_session_owner(Some("SID-active")),
"held listener work must not enter the replacement Page runtime"
);
let mut response_events = Vec::new();
let mut background_events = Vec::new();
let seen = conn.route_inspector_messages_with_background_events_into(
vec![json!({
"id": 2,
"result": { "result": { "type": "string", "value": "late" } }
})],
None,
Some("SID-active"),
&mut response_events,
&mut background_events,
);
assert!(!seen);
assert!(
response_events.is_empty(),
"stale listener reply must not surface as a protocol message: {response_events:?}"
);
assert!(
background_events.is_empty(),
"stale listener reply must not surface as script.message"
);
}
#[tokio::test]
async fn failed_bidi_listener_reply_publishes_concrete_object_group_release() {
let mut conn = connection_with_bidi_page_session();
let listener = bidi_channel_listener_residence_for_test(&conn, "SID-active", "error");
conn.register_runtime_remote_object_ids_for_session_owner_with_group(
Some("SID-active"),
vec!["channel-proxy-error".to_owned()],
"webdriver-bidi-channel-error",
);
conn.register_pending_bidi_channel_listener(7, Some("SID-active"), listener);
let mut response_events = Vec::new();
let mut background_events = Vec::new();
let seen = conn.route_inspector_messages_with_background_events_into(
vec![json!({
"id": 7,
"error": {
"code": -32000,
"message": "Cannot find context with specified id"
}
})],
None,
Some("SID-active"),
&mut response_events,
&mut background_events,
);
assert!(!seen);
assert!(response_events.is_empty());
assert!(background_events.is_empty());
let work = take_only_protocol_work(&mut conn);
assert_eq!(
work.bidi_channel_owner_action_kind(),
Some(BidiChannelOwnerActionKind::ReleaseObjectGroup)
);
conn.complete_ready_protocol_scheduler_work_turn(work).await;
assert!(
conn.runtime_remote_object_group_for_session_owner(
Some("SID-active"),
"channel-proxy-error"
)
.is_none(),
"the concrete release must own and consume the listener's object group"
);
}
#[tokio::test]
async fn stale_bidi_object_group_release_does_not_mutate_replacement_page_state() {
let mut conn = connection_with_bidi_page_session();
let listener =
bidi_channel_listener_residence_for_test(&conn, "SID-active", "stale-release");
conn.register_pending_bidi_channel_listener(8, Some("SID-active"), listener);
let mut response_events = Vec::new();
let mut background_events = Vec::new();
conn.route_inspector_messages_with_background_events_into(
vec![json!({
"id": 8,
"error": {
"code": -32000,
"message": "Cannot find context with specified id"
}
})],
None,
Some("SID-active"),
&mut response_events,
&mut background_events,
);
let work = take_only_protocol_work(&mut conn);
conn.runtime_session_owner_slot_mut(Some("SID-active"))
.expect("test runtime slot")
.replace_page_attachment_id_for_test();
conn.register_runtime_remote_object_ids_for_session_owner_with_group(
Some("SID-active"),
vec!["replacement-object".to_owned()],
"webdriver-bidi-channel-stale-release",
);
conn.complete_ready_protocol_scheduler_work_turn(work).await;
assert_eq!(
conn.runtime_remote_object_group_for_session_owner(
Some("SID-active"),
"replacement-object"
),
Some("webdriver-bidi-channel-stale-release".to_owned()),
"an old release must not touch an identically named group in the replacement Page"
);
}
#[test]
fn stale_bidi_listener_reply_does_not_emit_or_restart_on_replacement_page() {
let mut conn = connection_with_bidi_page_session();
let listener = bidi_channel_listener_residence_for_test(&conn, "SID-active", "stale-reply");
conn.register_pending_bidi_channel_listener(9, Some("SID-active"), listener);
conn.runtime_session_owner_slot_mut(Some("SID-active"))
.expect("test runtime slot")
.replace_page_attachment_id_for_test();
conn.register_runtime_remote_object_ids_for_session_owner_with_group(
Some("SID-active"),
vec!["replacement-object".to_owned()],
"webdriver-bidi-channel-stale-reply",
);
let mut response_events = Vec::new();
let mut background_events = Vec::new();
let seen = conn.route_inspector_messages_with_background_events_into(
vec![json!({
"id": 9,
"result": {
"result": {
"type": "string",
"value": "late message"
}
}
})],
None,
Some("SID-active"),
&mut response_events,
&mut background_events,
);
assert!(!seen);
assert!(
response_events.is_empty() && background_events.is_empty(),
"a stale listener reply must not emit script.message: response={response_events:?}, background={background_events:?}"
);
assert!(
conn.take_scheduler_events().is_empty(),
"a stale listener reply must not restart itself on the replacement Page"
);
assert_eq!(
conn.runtime_remote_object_group_for_session_owner(
Some("SID-active"),
"replacement-object"
),
Some("webdriver-bidi-channel-stale-reply".to_owned()),
"discarding the old reply must not clean up replacement runtime state"
);
}
#[test]
fn bidi_listener_result_handles_stay_in_user_object_group() {
let listener = PendingBidiChannelListener::new(
Some(DevToolsTargetId::from("TID-active")),
Some(crate::devtools_runtime::DevToolsRealmId::from(
"realm-active",
)),
crate::devtools_runtime::DevToolsRemoteHandleId::from("channel-proxy"),
"webdriver-bidi-channel-infra".to_owned(),
crate::devtools_runtime::DevToolsBidiChannelProperties {
channel: "preload".to_owned(),
ownership: DevToolsResultOwnership::Root,
serialization_options: None,
},
)
.expect("test listener should include target and realm");
let command: Value =
serde_json::from_str(&bidi_channel_listener_call_function_json(9, &listener))
.expect("listener command should serialize as JSON");
assert_eq!(command["params"]["objectId"], json!("channel-proxy"));
assert_eq!(command["params"]["objectGroup"], json!("webdriver-bidi"));
assert_ne!(
command["params"]["objectGroup"],
json!(listener.channel_object_group()),
"script.message data handles must not be tied to the channel infra group"
);
}
#[test]
fn bidi_listener_uses_deep_serialization_additional_parameters() {
let listener = PendingBidiChannelListener::new(
Some(DevToolsTargetId::from("TID-active")),
Some(crate::devtools_runtime::DevToolsRealmId::from(
"realm-active",
)),
crate::devtools_runtime::DevToolsRemoteHandleId::from("channel-proxy"),
"webdriver-bidi-channel-infra".to_owned(),
crate::devtools_runtime::DevToolsBidiChannelProperties {
channel: "preload".to_owned(),
ownership: DevToolsResultOwnership::None,
serialization_options: Some(
crate::devtools_runtime::DevToolsSerializationOptions {
max_object_depth: Some(1),
max_dom_depth: Some(2),
include_shadow_tree: Some("open".to_owned()),
},
),
},
)
.expect("test listener should include target and realm");
let command: Value =
serde_json::from_str(&bidi_channel_listener_call_function_json(9, &listener))
.expect("listener command should serialize as JSON");
assert_eq!(
command["params"]["serializationOptions"]["serialization"],
json!("deep")
);
assert_eq!(
command["params"]["serializationOptions"]["maxDepth"],
json!(1)
);
assert_eq!(
command["params"]["serializationOptions"]["additionalParameters"]["maxNodeDepth"],
json!(2)
);
assert_eq!(
command["params"]["serializationOptions"]["additionalParameters"]["includeShadowTree"],
json!("open")
);
}
}