mirror of
https://github.com/lexmount/moli.git
synced 2026-10-03 16:00:45 +00:00
refactor(devtools): bind layered dispatch to sessions and projection fences
Make AgentHost dispatch and session ownership explicit. Gate commands on the committed Document projection instead of treating protocol completion as native navigation authority.
This commit is contained in:
@@ -92,6 +92,31 @@ define_browser_identity!(
|
||||
"Identity of one browser-owned request decision, independent of protocol request IDs."
|
||||
);
|
||||
|
||||
static NEXT_BROWSER_SEQUENCE: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
/// Process-monotonic sequence of completed Browser semantic occurrences.
|
||||
///
|
||||
/// Unlike the object identities above, this value defines publication order.
|
||||
/// It is allocated only after the Browser mutation represented by an
|
||||
/// occurrence has completed successfully.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct BrowserSequence(NonZeroU64);
|
||||
|
||||
impl BrowserSequence {
|
||||
/// Allocates the next process-wide Browser occurrence sequence.
|
||||
pub fn allocate() -> Self {
|
||||
Self(allocate_nonzero_u64(
|
||||
&NEXT_BROWSER_SEQUENCE,
|
||||
"Browser sequence",
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns the ordered value for diagnostics and projection fences.
|
||||
pub const fn get(self) -> u64 {
|
||||
self.0.get()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl DocumentId {
|
||||
/// Constructs a deterministic identity for cross-crate tests.
|
||||
@@ -123,6 +148,7 @@ mod tests {
|
||||
assert_ne!(DocumentId::allocate(), DocumentId::allocate());
|
||||
assert_ne!(NavigationId::allocate(), NavigationId::allocate());
|
||||
assert_ne!(BrowserRequestId::allocate(), BrowserRequestId::allocate());
|
||||
assert!(BrowserSequence::allocate() < BrowserSequence::allocate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -145,5 +171,9 @@ mod tests {
|
||||
size_of::<Option<BrowserRequestId>>(),
|
||||
size_of::<BrowserRequestId>()
|
||||
);
|
||||
assert_eq!(
|
||||
size_of::<Option<BrowserSequence>>(),
|
||||
size_of::<BrowserSequence>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::{
|
||||
Page, RendererAgentAttachmentId, RendererCommandTurnOutput, RendererPageCommand,
|
||||
RendererPageCommandPending, RendererPageReply, RendererRuntimeInspectorIoCommandClaim,
|
||||
RendererRuntimeInspectorIoCommandRoute, RendererRuntimeInspectorMainCommandCompletion,
|
||||
RendererRuntimeInspectorMainCommandRoute,
|
||||
Page, RendererAgentAttachmentId, RendererCommandTurnOutput, RendererInspectorCommandRoute,
|
||||
RendererPageCommand, RendererPageCommandPending, RendererPageReply,
|
||||
RendererRuntimeInspectorIoCommandClaim, RendererRuntimeInspectorIoCommandRoute,
|
||||
RendererRuntimeInspectorMainCommandCompletion, RendererRuntimeInspectorMainCommandRoute,
|
||||
};
|
||||
use crate::RendererOutputFence;
|
||||
use anyhow::{Result, bail};
|
||||
@@ -43,6 +43,26 @@ impl PendingRuntimeInspectorCommandDispatch {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn renderer_route(&self) -> RendererInspectorCommandRoute {
|
||||
match &self.kind {
|
||||
PendingRuntimeInspectorCommandDispatchKind::MainIngress(_) => {
|
||||
RendererInspectorCommandRoute::MainThread
|
||||
}
|
||||
PendingRuntimeInspectorCommandDispatchKind::Io(_) => RendererInspectorCommandRoute::Io,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn renderer_agent_attachment_id(&self) -> Option<RendererAgentAttachmentId> {
|
||||
match &self.kind {
|
||||
PendingRuntimeInspectorCommandDispatchKind::MainIngress(route) => {
|
||||
route.ticket().attachment()
|
||||
}
|
||||
PendingRuntimeInspectorCommandDispatchKind::Io(pending) => {
|
||||
pending.renderer_agent_attachment_id()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One non-V8 renderer agent command admitted through the Page IO
|
||||
@@ -486,6 +506,10 @@ impl PendingDevToolsIoCommandDispatch {
|
||||
Self { route }
|
||||
}
|
||||
|
||||
pub fn renderer_agent_attachment_id(&self) -> Option<RendererAgentAttachmentId> {
|
||||
self.route.ticket().attachment()
|
||||
}
|
||||
|
||||
pub async fn wait(self) -> Result<CompletedDevToolsIoCommandDispatch> {
|
||||
match self
|
||||
.route
|
||||
|
||||
@@ -21,5 +21,5 @@ pub use target::{CdpTargetKindWire, CdpTargetType, cdp_target_type_wire_value};
|
||||
pub use time::{DEFAULT_LOADER_ID, monotonic_timestamp_seconds};
|
||||
pub use wire::{
|
||||
CdpInspectorTaskMode, CdpRendererCommandPolicy, CdpRendererCommandReplacement,
|
||||
CdpRendererCommandReplayDispatch, CdpRendererDispatchLane, CdpRequest, ParsedCdpCommand,
|
||||
CdpRendererCommandReplayDispatch, CdpRequest, ParsedCdpCommand,
|
||||
};
|
||||
|
||||
@@ -155,21 +155,6 @@ impl CdpRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Renderer lane selected after a command falls through its DevTools handler.
|
||||
///
|
||||
/// Chromium physically separates renderer main-thread and IO
|
||||
/// DevTools routes. Commands completed by a service or Browser handler have no
|
||||
/// renderer lane at all.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum CdpRendererDispatchLane {
|
||||
/// The command must wait for a cross-document navigation to install its
|
||||
/// replacement renderer attachment.
|
||||
Main,
|
||||
/// The command may address the suspended renderer in order to inspect,
|
||||
/// interrupt, or release it while navigation is in flight.
|
||||
Io,
|
||||
}
|
||||
|
||||
/// V8 execution capability of a command delivered through a renderer
|
||||
/// DevTools IO receiver.
|
||||
///
|
||||
@@ -217,14 +202,13 @@ pub enum CdpRendererCommandReplayDispatch {
|
||||
ResolveRuntimeContext,
|
||||
}
|
||||
|
||||
/// Immutable renderer scheduling policy derived once from a validated CDP
|
||||
/// method at command ingress.
|
||||
/// Immutable renderer behavior derived once from a validated CDP method at
|
||||
/// command ingress.
|
||||
///
|
||||
/// Downstream dispatch and renderer-call registration copy this value instead
|
||||
/// of rebuilding policy from serialized Inspector JSON.
|
||||
/// This contains V8 task and replacement behavior only. Renderer routing is a
|
||||
/// DevTools AgentHost decision made after the domain handler runs.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct CdpRendererCommandPolicy {
|
||||
renderer_lane: Option<CdpRendererDispatchLane>,
|
||||
inspector_task_mode: CdpInspectorTaskMode,
|
||||
renderer_replacement: CdpRendererCommandReplacement,
|
||||
renderer_replay_dispatch: CdpRendererCommandReplayDispatch,
|
||||
@@ -232,10 +216,6 @@ pub struct CdpRendererCommandPolicy {
|
||||
}
|
||||
|
||||
impl CdpRendererCommandPolicy {
|
||||
pub const fn renderer_lane(self) -> Option<CdpRendererDispatchLane> {
|
||||
self.renderer_lane
|
||||
}
|
||||
|
||||
pub const fn inspector_task_mode(self) -> CdpInspectorTaskMode {
|
||||
self.inspector_task_mode
|
||||
}
|
||||
@@ -380,10 +360,6 @@ impl ParsedCdpCommand {
|
||||
self.renderer_policy
|
||||
}
|
||||
|
||||
pub fn renderer_lane(&self) -> Option<CdpRendererDispatchLane> {
|
||||
self.renderer_policy.renderer_lane()
|
||||
}
|
||||
|
||||
pub fn inspector_task_mode(&self) -> CdpInspectorTaskMode {
|
||||
self.renderer_policy.inspector_task_mode()
|
||||
}
|
||||
@@ -399,17 +375,7 @@ impl ParsedCdpCommand {
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum CdpMethodDomain {
|
||||
Accessibility,
|
||||
Console,
|
||||
Css,
|
||||
Debugger,
|
||||
Dom,
|
||||
DomSnapshot,
|
||||
Emulation,
|
||||
HeapProfiler,
|
||||
Page,
|
||||
Performance,
|
||||
Profiler,
|
||||
Runtime,
|
||||
Other,
|
||||
}
|
||||
@@ -417,17 +383,7 @@ enum CdpMethodDomain {
|
||||
impl CdpMethodDomain {
|
||||
fn parse(domain: &str) -> Self {
|
||||
match domain {
|
||||
"Accessibility" => Self::Accessibility,
|
||||
"Console" => Self::Console,
|
||||
"CSS" => Self::Css,
|
||||
"Debugger" => Self::Debugger,
|
||||
"DOM" => Self::Dom,
|
||||
"DOMSnapshot" => Self::DomSnapshot,
|
||||
"Emulation" => Self::Emulation,
|
||||
"HeapProfiler" => Self::HeapProfiler,
|
||||
"Page" => Self::Page,
|
||||
"Performance" => Self::Performance,
|
||||
"Profiler" => Self::Profiler,
|
||||
"Runtime" => Self::Runtime,
|
||||
_ => Self::Other,
|
||||
}
|
||||
@@ -466,96 +422,23 @@ impl RuntimeWireAction {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum PageWireAction {
|
||||
MainThread,
|
||||
Crash,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl PageWireAction {
|
||||
fn parse(action: &str) -> Self {
|
||||
match action {
|
||||
"captureScreenshot"
|
||||
| "captureSnapshot"
|
||||
| "createIsolatedWorld"
|
||||
| "getFrameTree"
|
||||
| "getResourceTree"
|
||||
| "searchInResource"
|
||||
| "getLayoutMetrics"
|
||||
| "printToPDF" => Self::MainThread,
|
||||
"crash" => Self::Crash,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum DebuggerWireAction {
|
||||
Interruptible,
|
||||
IoExecutionControl,
|
||||
MainThreadExecutionControl,
|
||||
ExecutionControl,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl DebuggerWireAction {
|
||||
fn is_interruptible(self) -> bool {
|
||||
matches!(self, Self::Interruptible | Self::IoExecutionControl)
|
||||
}
|
||||
|
||||
fn executes_page_javascript(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::IoExecutionControl | Self::MainThreadExecutionControl
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum PerformanceWireAction {
|
||||
GetMetrics,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl PerformanceWireAction {
|
||||
fn parse(action: &str) -> Self {
|
||||
match action {
|
||||
"getMetrics" => Self::GetMetrics,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum EmulationWireAction {
|
||||
SetScriptExecutionDisabled,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl EmulationWireAction {
|
||||
fn parse(action: &str) -> Self {
|
||||
match action {
|
||||
"setScriptExecutionDisabled" => Self::SetScriptExecutionDisabled,
|
||||
_ => Self::Other,
|
||||
}
|
||||
matches!(self, Self::ExecutionControl)
|
||||
}
|
||||
}
|
||||
|
||||
impl DebuggerWireAction {
|
||||
fn parse(action: &str) -> Self {
|
||||
match action {
|
||||
"getPossibleBreakpoints"
|
||||
| "getScriptSource"
|
||||
| "getStackTrace"
|
||||
| "pause"
|
||||
| "removeBreakpoint"
|
||||
| "setBreakpoint"
|
||||
| "setBreakpointByUrl"
|
||||
| "setBreakpointsActive" => Self::Interruptible,
|
||||
"resume" => Self::IoExecutionControl,
|
||||
"continueToLocation" | "restartFrame" | "stepInto" | "stepOut" | "stepOver" => {
|
||||
Self::MainThreadExecutionControl
|
||||
}
|
||||
"resume" | "continueToLocation" | "restartFrame" | "stepInto" | "stepOut"
|
||||
| "stepOver" => Self::ExecutionControl,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
@@ -565,7 +448,6 @@ impl CdpRendererCommandPolicy {
|
||||
fn for_method(method: &str) -> Self {
|
||||
let Some((domain, action)) = method.split_once('.') else {
|
||||
return Self {
|
||||
renderer_lane: None,
|
||||
inspector_task_mode: CdpInspectorTaskMode::for_method(method),
|
||||
renderer_replacement: CdpRendererCommandReplacement::Replay,
|
||||
renderer_replay_dispatch: CdpRendererCommandReplayDispatch::Direct,
|
||||
@@ -577,53 +459,7 @@ impl CdpRendererCommandPolicy {
|
||||
(domain == CdpMethodDomain::Runtime).then(|| RuntimeWireAction::parse(action));
|
||||
let debugger_action =
|
||||
(domain == CdpMethodDomain::Debugger).then(|| DebuggerWireAction::parse(action));
|
||||
let renderer_lane = match domain {
|
||||
CdpMethodDomain::Runtime => {
|
||||
if runtime_action == Some(RuntimeWireAction::TerminateExecution) {
|
||||
Some(CdpRendererDispatchLane::Io)
|
||||
} else {
|
||||
Some(CdpRendererDispatchLane::Main)
|
||||
}
|
||||
}
|
||||
CdpMethodDomain::Debugger => {
|
||||
if debugger_action.is_some_and(DebuggerWireAction::is_interruptible) {
|
||||
Some(CdpRendererDispatchLane::Io)
|
||||
} else {
|
||||
Some(CdpRendererDispatchLane::Main)
|
||||
}
|
||||
}
|
||||
CdpMethodDomain::Performance => {
|
||||
if PerformanceWireAction::parse(action) == PerformanceWireAction::GetMetrics {
|
||||
Some(CdpRendererDispatchLane::Io)
|
||||
} else {
|
||||
Some(CdpRendererDispatchLane::Main)
|
||||
}
|
||||
}
|
||||
CdpMethodDomain::Emulation => {
|
||||
if EmulationWireAction::parse(action)
|
||||
== EmulationWireAction::SetScriptExecutionDisabled
|
||||
{
|
||||
Some(CdpRendererDispatchLane::Io)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
CdpMethodDomain::Page => match PageWireAction::parse(action) {
|
||||
PageWireAction::MainThread => Some(CdpRendererDispatchLane::Main),
|
||||
PageWireAction::Crash => Some(CdpRendererDispatchLane::Io),
|
||||
PageWireAction::Other => None,
|
||||
},
|
||||
CdpMethodDomain::Accessibility
|
||||
| CdpMethodDomain::Console
|
||||
| CdpMethodDomain::Css
|
||||
| CdpMethodDomain::Dom
|
||||
| CdpMethodDomain::DomSnapshot
|
||||
| CdpMethodDomain::HeapProfiler
|
||||
| CdpMethodDomain::Profiler => Some(CdpRendererDispatchLane::Main),
|
||||
CdpMethodDomain::Other => None,
|
||||
};
|
||||
Self {
|
||||
renderer_lane,
|
||||
inspector_task_mode: CdpInspectorTaskMode::for_method(method),
|
||||
renderer_replacement: if runtime_action.is_some_and(|action| {
|
||||
matches!(
|
||||
@@ -669,76 +505,6 @@ mod tests {
|
||||
assert_eq!(command.session_id(), Some("s1"));
|
||||
assert_eq!(command.command_output_session_id(), Some("s1"));
|
||||
assert!(command.runtime_command_executes_page_javascript());
|
||||
assert_eq!(command.renderer_lane(), Some(CdpRendererDispatchLane::Main));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsed_cdp_command_keeps_navigation_control_commands_unblocked() {
|
||||
let command = parse(
|
||||
r#"{"id":9,"method":"Fetch.continueResponse","params":{"requestId":"r"},"sessionId":"s1"}"#,
|
||||
);
|
||||
|
||||
assert_eq!(command.method(), "Fetch.continueResponse");
|
||||
assert_eq!(command.session_id(), Some("s1"));
|
||||
assert_eq!(command.renderer_lane(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renderer_inspector_control_commands_wait_for_navigation_resume() {
|
||||
for method in [
|
||||
"Runtime.getIsolateId",
|
||||
"Console.clearMessages",
|
||||
"Profiler.enable",
|
||||
"HeapProfiler.enable",
|
||||
] {
|
||||
let command = parse(format!(r#"{{"id":11,"method":"{method}"}}"#));
|
||||
assert!(
|
||||
command.renderer_lane() == Some(CdpRendererDispatchLane::Main),
|
||||
"{method} must not bind to the suspended attachment"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renderer_navigation_suspension_matches_chromium_io_route_exceptions() {
|
||||
for method in [
|
||||
"Debugger.getPossibleBreakpoints",
|
||||
"Debugger.getScriptSource",
|
||||
"Debugger.getStackTrace",
|
||||
"Debugger.pause",
|
||||
"Debugger.removeBreakpoint",
|
||||
"Debugger.resume",
|
||||
"Debugger.setBreakpoint",
|
||||
"Debugger.setBreakpointByUrl",
|
||||
"Debugger.setBreakpointsActive",
|
||||
"Emulation.setScriptExecutionDisabled",
|
||||
"Page.crash",
|
||||
"Performance.getMetrics",
|
||||
"Runtime.terminateExecution",
|
||||
] {
|
||||
let command = parse(format!(r#"{{"id":12,"method":"{method}"}}"#));
|
||||
assert!(
|
||||
command.renderer_lane() == Some(CdpRendererDispatchLane::Io),
|
||||
"{method} must remain dispatchable while the renderer attachment is suspended"
|
||||
);
|
||||
}
|
||||
|
||||
for method in [
|
||||
"Debugger.continueToLocation",
|
||||
"Debugger.enable",
|
||||
"Debugger.restartFrame",
|
||||
"Debugger.stepInto",
|
||||
"Debugger.stepOut",
|
||||
"Debugger.stepOver",
|
||||
"Performance.enable",
|
||||
"Runtime.getIsolateId",
|
||||
] {
|
||||
let command = parse(format!(r#"{{"id":13,"method":"{method}"}}"#));
|
||||
assert!(
|
||||
command.renderer_lane() == Some(CdpRendererDispatchLane::Main),
|
||||
"{method} must bind to the replacement renderer attachment"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -836,15 +602,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_resource_search_waits_for_document_navigation() {
|
||||
let command = parse(
|
||||
r#"{"id":10,"method":"Page.searchInResource","params":{"frameId":"F","url":"https://example.test/","query":"needle"},"sessionId":"s1"}"#,
|
||||
);
|
||||
|
||||
assert_eq!(command.renderer_lane(), Some(CdpRendererDispatchLane::Main));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsed_command_rejects_invalid_json_instead_of_storing_parse_failure() {
|
||||
let error = ParsedCdpCommand::parse_str("{")
|
||||
@@ -908,7 +665,6 @@ mod tests {
|
||||
|
||||
assert_eq!(command.request().id(), 14);
|
||||
assert_eq!(command.session_id(), Some("s2"));
|
||||
assert_eq!(command.renderer_lane(), Some(CdpRendererDispatchLane::Io));
|
||||
assert!(!command.runtime_command_executes_page_javascript());
|
||||
}
|
||||
|
||||
|
||||
@@ -11,16 +11,16 @@ use moli_core::{
|
||||
runtime::NavigationRuntimeConfig,
|
||||
};
|
||||
use moli_protocol::{
|
||||
BackgroundNavigationCompletion, BackgroundProtocolEvent, CdpCommandTaskStep, CdpConnection,
|
||||
CdpInitialStoragePartition, CdpRendererDispatchLane, CdpSchedulerEvent,
|
||||
CdpTargetHostLifecycleObserver, CommandDispatchContext, CompletedCdpCommandDispatch,
|
||||
AgentHostDispatchResult, BackgroundNavigationCompletion, BackgroundProtocolEvent,
|
||||
CdpConnection, CdpInitialStoragePartition, CdpSchedulerEvent, CdpTargetHostLifecycleObserver,
|
||||
CommandDispatchContext, CompletedCdpCommandDispatch,
|
||||
CompletedDeferredMainDocumentLoadCompletion, DeferredMainDocumentLoadCompletionOutputAction,
|
||||
DeferredMainDocumentLoadCompletionOutputInterest, DeferredMainDocumentLoadObservationId,
|
||||
DeferredMainDocumentLoadPredecessorCandidate, DevToolsPageResidenceIdentity,
|
||||
PageScreencastCaptureCompletion, PageScreencastCaptureStart, PageScreencastRegistration,
|
||||
PageScreencastSubscriptionStatus, ParsedCdpCommand, PendingCdpCommandDispatch,
|
||||
PendingDeferredMainDocumentLoadCompletion, PendingPageScreencastCapture,
|
||||
ProtocolSchedulerWorkKind, RuntimeCommandOutputBarriers,
|
||||
ProtocolSchedulerWorkKind, RendererCommandResponseOrder,
|
||||
conn::{RuntimeInspectorResponseReady, RuntimeInspectorResponseReadySender},
|
||||
devtools_runtime::{
|
||||
DevToolsCommand, DevToolsCommandResult, DevToolsError, DevToolsNavigationWait,
|
||||
@@ -39,7 +39,7 @@ mod adapter_scheduler;
|
||||
mod command_dispatch;
|
||||
mod frontend_control;
|
||||
mod protocol_residence;
|
||||
mod runtime_command_barrier;
|
||||
mod renderer_command_response_order;
|
||||
mod runtime_dispatch;
|
||||
|
||||
pub(crate) use actor::spawn_cdp_scheduler_actor;
|
||||
@@ -51,7 +51,7 @@ pub(crate) use frontend_control::{CdpCookieSnapshot, CdpOwnerActorLifecycle};
|
||||
use protocol_residence::{
|
||||
ClientTurnPredecessor, ProtocolSchedulerResidence, ProtocolSchedulerStep, SchedulerQueues,
|
||||
};
|
||||
use runtime_command_barrier::CommandOutputReleasePermit;
|
||||
use renderer_command_response_order::CommandOutputReleasePermit;
|
||||
pub(crate) use runtime_dispatch::{
|
||||
DevToolsRuntimeCommandProgress, PendingDevToolsRuntimeDeferredReplyExecution,
|
||||
};
|
||||
@@ -113,7 +113,7 @@ pub(crate) enum CommandStartAction {
|
||||
pub(crate) struct CdpScheduler {
|
||||
conn: CdpConnection,
|
||||
pending_navigation_background_events: VecDeque<PendingNavigationBackgroundEvent>,
|
||||
runtime_command_output_barriers: RuntimeCommandOutputBarriers,
|
||||
renderer_command_response_order: RendererCommandResponseOrder,
|
||||
queues: SchedulerQueues,
|
||||
page_screencasts: HashMap<Option<String>, PageScreencastSchedule>,
|
||||
page_screencast_interval_ms: u32,
|
||||
@@ -704,7 +704,7 @@ impl CdpScheduler {
|
||||
Self {
|
||||
conn,
|
||||
pending_navigation_background_events: VecDeque::new(),
|
||||
runtime_command_output_barriers: RuntimeCommandOutputBarriers::default(),
|
||||
renderer_command_response_order: RendererCommandResponseOrder::default(),
|
||||
queues: SchedulerQueues::default(),
|
||||
page_screencasts: HashMap::new(),
|
||||
page_screencast_interval_ms,
|
||||
@@ -844,11 +844,11 @@ impl CdpScheduler {
|
||||
.conn
|
||||
.start_parsed_command_dispatch_with_context(command, &mut command_context);
|
||||
// Dispatch registers a session-local renderer call id before the
|
||||
// renderer task can be observed by this actor. The response barrier
|
||||
// renderer task can be observed by this actor. The response-order permit
|
||||
// must use that exact id rather than infer one from the frontend CDP
|
||||
// request id.
|
||||
let runtime_output_barrier = if command.runtime_command_executes_page_javascript() {
|
||||
self.runtime_command_output_barriers.admit(
|
||||
let renderer_response_permit = if command.runtime_command_executes_page_javascript() {
|
||||
self.renderer_command_response_order.admit(
|
||||
&self.conn,
|
||||
command.request().id(),
|
||||
command.command_output_session_id(),
|
||||
@@ -857,14 +857,20 @@ impl CdpScheduler {
|
||||
None
|
||||
};
|
||||
let output_release_permit =
|
||||
CommandOutputReleasePermit::new(response_flush_permit, runtime_output_barrier);
|
||||
CommandOutputReleasePermit::new(response_flush_permit, renderer_response_permit);
|
||||
let step = match dispatch_step {
|
||||
CdpCommandTaskStep::Pending(mut pending) => {
|
||||
AgentHostDispatchResult::PendingService(mut pending) => {
|
||||
let scheduler_events = pending.take_scheduler_events();
|
||||
self.apply_scheduler_events(scheduler_events);
|
||||
CommandTaskStep::Pending(pending)
|
||||
}
|
||||
CdpCommandTaskStep::Complete(result) => {
|
||||
AgentHostDispatchResult::FallThrough(dispatch) => {
|
||||
let mut pending = dispatch.into_pending();
|
||||
let scheduler_events = pending.take_scheduler_events();
|
||||
self.apply_scheduler_events(scheduler_events);
|
||||
CommandTaskStep::Pending(pending)
|
||||
}
|
||||
AgentHostDispatchResult::Complete(result) => {
|
||||
let (
|
||||
events,
|
||||
post_renderer_output_events,
|
||||
@@ -905,12 +911,18 @@ impl CdpScheduler {
|
||||
.complete_pending_command_dispatch_with_context(completed, command_context)
|
||||
.await
|
||||
{
|
||||
CdpCommandTaskStep::Pending(mut pending) => {
|
||||
AgentHostDispatchResult::PendingService(mut pending) => {
|
||||
let scheduler_events = pending.take_scheduler_events();
|
||||
self.apply_scheduler_events(scheduler_events);
|
||||
CommandTaskStep::Pending(pending)
|
||||
}
|
||||
CdpCommandTaskStep::Complete(result) => {
|
||||
AgentHostDispatchResult::FallThrough(dispatch) => {
|
||||
let mut pending = dispatch.into_pending();
|
||||
let scheduler_events = pending.take_scheduler_events();
|
||||
self.apply_scheduler_events(scheduler_events);
|
||||
CommandTaskStep::Pending(pending)
|
||||
}
|
||||
AgentHostDispatchResult::Complete(result) => {
|
||||
let (
|
||||
events,
|
||||
post_renderer_output_events,
|
||||
@@ -2032,10 +2044,7 @@ impl CdpScheduler {
|
||||
}
|
||||
|
||||
pub(crate) fn command_waits_for_navigation_flush(&self, command: &ParsedCdpCommand) -> bool {
|
||||
command.renderer_lane() == Some(CdpRendererDispatchLane::Main)
|
||||
&& self
|
||||
.conn
|
||||
.renderer_document_navigation_is_suspended_for_session_owner(command.session_id())
|
||||
self.conn.command_waits_for_document_projection(command)
|
||||
}
|
||||
|
||||
pub(crate) fn route_background_event_around_inflight_navigation(
|
||||
@@ -2327,7 +2336,7 @@ impl CdpScheduler {
|
||||
.conn
|
||||
.ingest_renderer_output_turn_async(
|
||||
publication,
|
||||
&mut self.runtime_command_output_barriers,
|
||||
&mut self.renderer_command_response_order,
|
||||
)
|
||||
.await;
|
||||
let (
|
||||
@@ -2471,20 +2480,20 @@ impl CdpScheduler {
|
||||
let Some(permit) = output_release_permit else {
|
||||
return ProtocolOutputSequence::empty();
|
||||
};
|
||||
let Some(runtime_barrier) = permit.finish_response() else {
|
||||
let Some(renderer_response) = permit.finish_response() else {
|
||||
return ProtocolOutputSequence::empty();
|
||||
};
|
||||
let completion = self
|
||||
.conn
|
||||
.release_runtime_command_output_barrier_turn_async(
|
||||
&mut self.runtime_command_output_barriers,
|
||||
runtime_barrier,
|
||||
.release_renderer_command_response_permit_turn_async(
|
||||
&mut self.renderer_command_response_order,
|
||||
renderer_response,
|
||||
)
|
||||
.await;
|
||||
if moli_trace::cdp_runtime_trace_enabled() {
|
||||
tracing::info!(
|
||||
target: "moli_cdp_runtime",
|
||||
stage = "runtime_command_output_barrier_terminal",
|
||||
stage = "renderer_command_response_terminal",
|
||||
terminal = ?completion.terminal(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ impl CommandTurnOutput {
|
||||
///
|
||||
/// The frozen sequence has already been split by its domain-declared
|
||||
/// `BeforeResponse`/`AfterResponse` order. Only the former reaches this
|
||||
/// method; the latter remains in the command barrier until the response
|
||||
/// method; the latter remains held by response ordering until the response
|
||||
/// flush consumes its single-use permit.
|
||||
pub(crate) fn prepend_protocol_output(&mut self, output: ProtocolOutputSequence) {
|
||||
if output.is_empty() {
|
||||
|
||||
@@ -234,7 +234,7 @@ impl CdpFrontendTargetControl {
|
||||
.await;
|
||||
assert!(
|
||||
output.is_empty(),
|
||||
"a frontend-control command without a Runtime barrier cannot release held output"
|
||||
"a frontend-control command without a renderer response permit cannot release held output"
|
||||
);
|
||||
if method == "Target.attachToBrowserTarget"
|
||||
&& let Some(session_id) = response
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
use moli_protocol::{CommandResponseFlushPermit, RendererCommandResponsePermit};
|
||||
|
||||
/// The single-use authority that finishes one command's protocol-output
|
||||
/// boundary.
|
||||
///
|
||||
/// The frontend flush permit and optional renderer-response permit are one
|
||||
/// value so a command cannot publish its response while forgetting to release
|
||||
/// the concrete Page output held on its behalf. Commands that do not execute
|
||||
/// Page JavaScript still carry the frontend permit and simply have no
|
||||
/// renderer response-order permit.
|
||||
#[must_use = "a command output release permit must be completed exactly once"]
|
||||
pub(crate) struct CommandOutputReleasePermit {
|
||||
response_flush: CommandResponseFlushPermit,
|
||||
renderer_response_permit: Option<RendererCommandResponsePermit>,
|
||||
}
|
||||
|
||||
impl CommandOutputReleasePermit {
|
||||
pub(super) fn new(
|
||||
response_flush: CommandResponseFlushPermit,
|
||||
renderer_response_permit: Option<RendererCommandResponsePermit>,
|
||||
) -> Self {
|
||||
Self {
|
||||
response_flush,
|
||||
renderer_response_permit,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn finish_response(self) -> Option<RendererCommandResponsePermit> {
|
||||
self.response_flush.finish();
|
||||
self.renderer_response_permit
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
use moli_protocol::{CommandResponseFlushPermit, RuntimeCommandOutputBarrierPermit};
|
||||
|
||||
/// The single-use authority that finishes one command's protocol-output
|
||||
/// boundary.
|
||||
///
|
||||
/// The response permit and optional Runtime-output barrier are one value so a
|
||||
/// command cannot publish its response while forgetting to release the
|
||||
/// concrete Page output held on its behalf. Commands that do not execute
|
||||
/// Page JavaScript still carry the response permit and simply have no Runtime
|
||||
/// barrier.
|
||||
#[must_use = "a command output release permit must be completed exactly once"]
|
||||
pub(crate) struct CommandOutputReleasePermit {
|
||||
response_flush: CommandResponseFlushPermit,
|
||||
runtime_output_barrier: Option<RuntimeCommandOutputBarrierPermit>,
|
||||
}
|
||||
|
||||
impl CommandOutputReleasePermit {
|
||||
pub(super) fn new(
|
||||
response_flush: CommandResponseFlushPermit,
|
||||
runtime_output_barrier: Option<RuntimeCommandOutputBarrierPermit>,
|
||||
) -> Self {
|
||||
Self {
|
||||
response_flush,
|
||||
runtime_output_barrier,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn finish_response(self) -> Option<RuntimeCommandOutputBarrierPermit> {
|
||||
self.response_flush.finish();
|
||||
self.runtime_output_barrier
|
||||
}
|
||||
}
|
||||
+66
-102
@@ -109,7 +109,11 @@ pub(crate) use cookie_owner::{
|
||||
};
|
||||
pub use devtools_command::DevToolsCommandDispatchOutcome;
|
||||
pub(crate) use devtools_command::DevToolsCommandExecutionOutput;
|
||||
pub use dispatch::{CdpCommandTaskStep, CompletedCdpCommandDispatch, PendingCdpCommandDispatch};
|
||||
pub use dispatch::{
|
||||
AgentHostDispatchResult, CdpCommandTaskStep, CompletedCdpCommandDispatch,
|
||||
PendingCdpCommandDispatch, RendererDispatch, RendererDispatchBinding, RendererDispatchLane,
|
||||
RendererPageDispatchBinding,
|
||||
};
|
||||
pub(crate) use fetch_support::PendingStreamingDocumentResponseNavigation;
|
||||
pub(crate) use fetch_support::{
|
||||
ClaimedFetchNavigation, ClaimedFetchResponseNavigation, ClaimedSubresourceContinueRequest,
|
||||
@@ -131,7 +135,7 @@ pub use fetch_support::{
|
||||
};
|
||||
pub use moli_protocol_cdp::{
|
||||
CdpRendererCommandPolicy, CdpRendererCommandReplacement, CdpRendererCommandReplayDispatch,
|
||||
CdpRendererDispatchLane, CdpRequest, ParsedCdpCommand,
|
||||
CdpRequest, ParsedCdpCommand,
|
||||
};
|
||||
use target::DEFAULT_BROWSER_CONTEXT_ID;
|
||||
pub(crate) use target::{
|
||||
@@ -526,9 +530,8 @@ pub(crate) use popup_navigation_work::{
|
||||
PopupTargetNavigationKind, PopupTargetNavigationOwnerAction,
|
||||
};
|
||||
pub(crate) use runtime_eval::{
|
||||
ClaimedPendingInspectorAwait, ClaimedPendingInspectorAwaitOwner, RuntimeBindingCallEvent,
|
||||
RuntimeEnableReplayEvent, renderer_command_turn_frontend_protocol_response,
|
||||
runtime_remote_object_ids_in_map,
|
||||
ClaimedPendingInspectorAwait, RuntimeBindingCallEvent, RuntimeEnableReplayEvent,
|
||||
renderer_command_turn_frontend_protocol_response, runtime_remote_object_ids_in_map,
|
||||
};
|
||||
pub use runtime_eval::{
|
||||
CompletedMoliDiagnosticsDispatch, CompletedRuntimeBindingPageCommandDispatch,
|
||||
@@ -561,20 +564,20 @@ pub use state::{
|
||||
DownloadNavigation, EmulatedDeviceMetrics, EmulatedGeolocationOverride,
|
||||
EmulatedGeolocationOverrideState, EmulatedMediaOverrides, IsolatedWorldDefinition,
|
||||
LoadedNavigation, NavigationDispatchState, NavigationLoadOutcome, NavigationRequestLoadPolicy,
|
||||
PageNavigationHistoryEntry, PageTargetHost, RuntimeBindingDefinition, TargetInfo, URL_BASE,
|
||||
PageAgentHost, PageNavigationHistoryEntry, RuntimeBindingDefinition, TargetInfo, URL_BASE,
|
||||
};
|
||||
pub(crate) use state::{
|
||||
BrowserContextPageStorageHandles, BrowserContextStoragePartitionHandles,
|
||||
CommittedRendererDocumentBinding, CompletedDownloadBodyArtifact, ContextNetworkPolicy,
|
||||
DedicatedWorkerMainScriptOutcome, DedicatedWorkerMainScriptSnapshot,
|
||||
DedicatedWorkerTargetState, DevToolsBrowserIdentityOverride, DevToolsConsoleOutputSessionState,
|
||||
DevToolsLogViolationThreshold, DocumentId, DuplicatePendingRendererCommand,
|
||||
EmulatedNetworkConditions, EmulatedViewportSurface, EmulationPolicyChange,
|
||||
InitialDocumentCreator, InspectorCommandDispatch, NETWORK_ERROR_PAGE_URL, NavigationId,
|
||||
NavigationResultProjection, NavigationSourceDocumentSecurityContext,
|
||||
NetworkErrorPageNavigation, PageScreencastConfig, PageScreencastFormat,
|
||||
PendingBidiChannelListener, PendingInspectorAwait, PendingRendererCommandKey,
|
||||
PerformanceTimeDomain, PreparedRendererCallDispatch, ProfilerAction, ProfilerInspectorCommand,
|
||||
DevToolsLogViolationThreshold, DocumentId, DocumentProjectionFence,
|
||||
DocumentProjectionOutputRelease, DuplicatePendingRendererCommand, EmulatedNetworkConditions,
|
||||
EmulatedViewportSurface, EmulationPolicyChange, InitialDocumentCreator,
|
||||
InspectorCommandDispatch, NETWORK_ERROR_PAGE_URL, NavigationId, NavigationResultProjection,
|
||||
NavigationSourceDocumentSecurityContext, NetworkErrorPageNavigation, PageScreencastConfig,
|
||||
PageScreencastFormat, PendingBidiChannelListener, PendingInspectorAwait, PerformanceTimeDomain,
|
||||
PreparedRendererCallDispatch, ProfilerAction, ProfilerInspectorCommand,
|
||||
RendererCommandCorrelation, RendererCommandDescriptor, RendererCommandReplay,
|
||||
RendererDocumentLifecycleObservation, RendererDocumentLifecycleObserver,
|
||||
RendererMainDocumentCommitSeed, RendererPageResidenceIdentity,
|
||||
@@ -600,13 +603,14 @@ pub(crate) use state::{
|
||||
TargetRuntimeSessionState,
|
||||
};
|
||||
pub(crate) use state::{HistoryTraversalDestination, ResolvedHistoryTraversal};
|
||||
pub(crate) use target::{
|
||||
PreparedTargetAttach, PreparedTargetHostClosure, PreparedTargetHostDelta, SessionDisposalPlan,
|
||||
SessionDisposalTarget, TargetAttachSessionCommit, TargetClosureCleanupPlan, TargetEventPlan,
|
||||
TargetSessionDetachCleanupPlan,
|
||||
};
|
||||
use target::{
|
||||
TargetClosurePlan, TargetControlPlane, TargetHostDelta, target_destroyed_automation_events,
|
||||
DevToolsAgentHostRegistry, TargetClosurePlan, TargetHostDelta,
|
||||
target_destroyed_automation_events,
|
||||
};
|
||||
pub(crate) use target::{
|
||||
DevToolsSessionHandlerSet, PreparedTargetAttach, PreparedTargetHostClosure,
|
||||
PreparedTargetHostDelta, SessionDisposalPlan, SessionDisposalTarget, TargetAttachSessionCommit,
|
||||
TargetClosureCleanupPlan, TargetEventPlan, TargetSessionDetachCleanupPlan,
|
||||
};
|
||||
pub(crate) use top_level_navigation_work::TopLevelLocationNavigationOwnerAction;
|
||||
|
||||
@@ -829,44 +833,6 @@ impl DeferredMainDocumentLoadPredecessorCandidate {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct RuntimeAwaitJob {
|
||||
command_id: u64,
|
||||
owner: CommandOwnerScope,
|
||||
object_group: Option<String>,
|
||||
action: &'static str,
|
||||
}
|
||||
|
||||
impl RuntimeAwaitJob {
|
||||
pub(crate) fn new(
|
||||
command_id: u64,
|
||||
owner: &CommandOwnerScope,
|
||||
object_group: Option<&str>,
|
||||
action: &'static str,
|
||||
) -> Self {
|
||||
Self {
|
||||
command_id,
|
||||
owner: owner.clone(),
|
||||
object_group: object_group.map(str::to_owned),
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn trace_fields(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"commandId": self.command_id,
|
||||
"sessionId": self.owner.session_id(),
|
||||
"ownerRoute": self.owner.explicit_route().map(|route| format!("{route:?}")),
|
||||
"objectGroup": self.object_group,
|
||||
"action": self.action,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn session_id(&self) -> Option<String> {
|
||||
self.owner.session_id().map(str::to_owned)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CdpInitialStoragePartition {
|
||||
handles: BrowserContextStoragePartitionHandles,
|
||||
@@ -1194,7 +1160,7 @@ pub struct CdpConnection {
|
||||
// additional attached sessions. A randomized HashMap iteration order made that
|
||||
// choice vary between otherwise identical processes.
|
||||
auto_attach_owner_sessions: IndexMap<Option<String>, AutoAttachOwnerPolicy>,
|
||||
target_control: TargetControlPlane,
|
||||
agent_hosts: DevToolsAgentHostRegistry,
|
||||
default_target_lifecycle: DefaultTargetLifecycle,
|
||||
service_worker_auto_attach_related_owners: Vec<ServiceWorkerAutoAttachRelatedOwner>,
|
||||
service_worker_pause_on_start_owner_sessions: HashSet<Option<String>>,
|
||||
@@ -1209,10 +1175,6 @@ pub struct CdpConnection {
|
||||
next_page_domain_subscription_generation: u64,
|
||||
next_internal_runtime_command_id: u64,
|
||||
network_request_id_allocator: ConnectionNetworkRequestIdAllocator,
|
||||
pending_runtime_await_jobs: HashMap<PendingRendererCommandKey, RuntimeAwaitJob>,
|
||||
claimed_pending_inspector_await_owners:
|
||||
HashMap<PendingRendererCommandKey, ClaimedPendingInspectorAwaitOwner>,
|
||||
|
||||
// Browser profile, permissions, download and global IO state.
|
||||
pub window_bounds: BrowserWindowBounds,
|
||||
download_policy: moli_core::browser::DownloadPolicy,
|
||||
@@ -1246,7 +1208,7 @@ pub struct CdpConnection {
|
||||
scheduler_state: CdpConnectionSchedulerState,
|
||||
|
||||
// Standalone navigation state for commands that have no concrete Page
|
||||
// owner. Every page-owned engine lives in its stable PageTargetHost.
|
||||
// owner. Every page-owned engine lives in its stable PageAgentHost.
|
||||
standalone_navigation_engine: StandaloneNavigationEngineSlot,
|
||||
}
|
||||
|
||||
@@ -1276,7 +1238,7 @@ impl Drop for CdpConnection {
|
||||
root.terminate_renderer_producers_for_owner_shutdown();
|
||||
}
|
||||
|
||||
// Dropping contexts releases every PageTargetHost and its Page state.
|
||||
// Dropping contexts releases every PageAgentHost and its Page state.
|
||||
drop(contexts);
|
||||
|
||||
// RenderRuntimeOwner joins happen when the last JsRuntime-backed
|
||||
@@ -1326,7 +1288,7 @@ impl CdpConnection {
|
||||
if let Some((browser_context_id, target_id)) = active_owner {
|
||||
return self
|
||||
.ensure_page_navigation_engine_for_target(&browser_context_id, &target_id)
|
||||
.expect("active PageTargetHost navigation engine disappeared");
|
||||
.expect("active PageAgentHost navigation engine disappeared");
|
||||
}
|
||||
self.standalone_navigation_engine.ensure_mut()
|
||||
}
|
||||
@@ -1504,7 +1466,7 @@ impl CdpConnection {
|
||||
target_info_change_events_enabled: false,
|
||||
target_discovery_filter: None,
|
||||
auto_attach_owner_sessions: IndexMap::new(),
|
||||
target_control: TargetControlPlane::default(),
|
||||
agent_hosts: DevToolsAgentHostRegistry::default(),
|
||||
default_target_lifecycle: DefaultTargetLifecycle::default(),
|
||||
service_worker_auto_attach_related_owners: Vec::new(),
|
||||
service_worker_pause_on_start_owner_sessions: HashSet::new(),
|
||||
@@ -1524,8 +1486,6 @@ impl CdpConnection {
|
||||
next_page_domain_subscription_generation: 0,
|
||||
next_internal_runtime_command_id: 902_000_000,
|
||||
network_request_id_allocator: ConnectionNetworkRequestIdAllocator::default(),
|
||||
pending_runtime_await_jobs: HashMap::new(),
|
||||
claimed_pending_inspector_await_owners: HashMap::new(),
|
||||
base_browser_identity,
|
||||
global_extra_headers: Default::default(),
|
||||
global_browser_identity_override: None,
|
||||
@@ -1780,12 +1740,12 @@ impl CdpConnection {
|
||||
self.document_navigation_state_for_owner(&owner_scope)
|
||||
}
|
||||
|
||||
pub fn renderer_document_navigation_is_suspended_for_session_owner(
|
||||
pub fn document_projection_is_pending_for_session_owner(
|
||||
&self,
|
||||
session_id: Option<&str>,
|
||||
) -> bool {
|
||||
self.runtime_session_owner_slot(session_id)
|
||||
.is_ok_and(TargetRuntimeSlot::renderer_document_navigation_is_suspended)
|
||||
.is_ok_and(TargetRuntimeSlot::document_projection_is_pending)
|
||||
}
|
||||
|
||||
pub(crate) fn accepts_pending_document_navigation_for_owner(
|
||||
@@ -1993,10 +1953,16 @@ impl CdpConnection {
|
||||
return (None, Vec::new());
|
||||
};
|
||||
context.install_document_lifecycle_for_test(&target_id, lifecycle);
|
||||
let browser_sequence = context
|
||||
.renderer_document_lifecycle_binding_for_target(&target_id)
|
||||
.filter(|binding| binding.document_id == document)
|
||||
.map(|binding| binding.browser_sequence)
|
||||
.unwrap_or_else(moli_core::browser::BrowserSequence::allocate);
|
||||
self.project_committed_document_lifecycle_for_owner(
|
||||
owner,
|
||||
CommittedDocumentLifecycle {
|
||||
document,
|
||||
browser_sequence,
|
||||
artifacts,
|
||||
},
|
||||
navigation,
|
||||
@@ -2528,7 +2494,6 @@ impl CdpConnection {
|
||||
"commandId": command_id,
|
||||
"sessionId": session_id,
|
||||
"fields": fields,
|
||||
"pendingRuntimeAwaitJobCount": self.pending_runtime_await_jobs.len(),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -3093,7 +3058,7 @@ impl CdpConnection {
|
||||
"connection": {
|
||||
"hasActiveBrowserContext": self.browser_context.is_some(),
|
||||
"inactiveBrowserContextCount": self.inactive_browser_contexts.len(),
|
||||
"browserSessionIdCount": self.target_control.browser_session_count(),
|
||||
"browserSessionIdCount": self.agent_hosts.browser_session_count(),
|
||||
"globalIoStreamCount": self.global_io_streams.len(),
|
||||
"tracing": self.tracing_state.diagnostics(),
|
||||
"permissionOverrideCount": self.permission_override_count(),
|
||||
@@ -3324,7 +3289,7 @@ impl CdpConnection {
|
||||
if let Some(page_target_id) = self.primary_page_target_id_for_tab_target_id(target_id) {
|
||||
let page_target_info = self.devtools_page_or_worker_target_info(page_target_id)?;
|
||||
return self
|
||||
.target_control
|
||||
.agent_hosts
|
||||
.tab_target_info_for_page_target_info(page_target_info);
|
||||
}
|
||||
self.devtools_page_or_worker_target_info(target_id)
|
||||
@@ -3344,7 +3309,7 @@ impl CdpConnection {
|
||||
let mut target_infos = Vec::new();
|
||||
if let Some(page_target_info) = self.default_target_lifecycle.placeholder_page_info() {
|
||||
if let Some(tab_target_info) = self
|
||||
.target_control
|
||||
.agent_hosts
|
||||
.tab_target_info_for_page_target_info(page_target_info.clone())
|
||||
{
|
||||
target_infos.push(tab_target_info);
|
||||
@@ -3358,7 +3323,7 @@ impl CdpConnection {
|
||||
browser_context.target_popup_id(target_id.as_str());
|
||||
}
|
||||
if let Some(tab_target_info) = self
|
||||
.target_control
|
||||
.agent_hosts
|
||||
.tab_target_info_for_page_target_info(page_or_worker_target_info.clone())
|
||||
{
|
||||
target_infos.push(tab_target_info);
|
||||
@@ -3437,7 +3402,7 @@ impl CdpConnection {
|
||||
} else {
|
||||
self.gen_tab_target_id()
|
||||
};
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.register_tab(tab_target_id.clone(), page_target_id.to_owned());
|
||||
for target_id in [&tab_target_id, page_target_id] {
|
||||
if let Some(target_info) = self.target_info_for_host_delta(target_id) {
|
||||
@@ -3451,7 +3416,7 @@ impl CdpConnection {
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn tab_target_id_for_page_target_id(&self, page_target_id: &str) -> Option<&str> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.tab_target_id_for_page_target_id(page_target_id)
|
||||
}
|
||||
|
||||
@@ -3459,12 +3424,12 @@ impl CdpConnection {
|
||||
&self,
|
||||
tab_target_id: &str,
|
||||
) -> Option<&str> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.primary_page_target_id_for_tab_target_id(tab_target_id)
|
||||
}
|
||||
|
||||
pub(crate) fn primary_session_id_for_tab_target_id(&self, tab_target_id: &str) -> Option<&str> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.primary_session_id_for_tab_target_id(tab_target_id)
|
||||
}
|
||||
|
||||
@@ -3474,7 +3439,7 @@ impl CdpConnection {
|
||||
session_id: String,
|
||||
is_attached_session: bool,
|
||||
) -> bool {
|
||||
self.target_control.assign_session_to_tab_target(
|
||||
self.agent_hosts.assign_session_to_tab_target(
|
||||
tab_target_id,
|
||||
session_id,
|
||||
is_attached_session,
|
||||
@@ -3482,7 +3447,7 @@ impl CdpConnection {
|
||||
}
|
||||
|
||||
pub(crate) fn remove_tab_session(&mut self, session_id: &str) -> Option<String> {
|
||||
self.target_control.remove_tab_session(session_id)
|
||||
self.agent_hosts.remove_tab_session(session_id)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_tab_for_page_target(
|
||||
@@ -3490,7 +3455,7 @@ impl CdpConnection {
|
||||
page_target_id: &str,
|
||||
) -> Option<TargetClosurePlan> {
|
||||
let closure_plan = self
|
||||
.target_control
|
||||
.agent_hosts
|
||||
.remove_tab_by_page_target_id(page_target_id)?;
|
||||
for target_id in closure_plan.destroyed_target_ids() {
|
||||
self.notify_target_host_lifecycle(CdpTargetHostLifecycleDelta::Destroyed {
|
||||
@@ -3535,7 +3500,7 @@ impl CdpConnection {
|
||||
}
|
||||
|
||||
pub(crate) fn tab_target_id_for_session_id(&self, session_id: &str) -> Option<&str> {
|
||||
self.target_control.tab_target_id_for_session_id(session_id)
|
||||
self.agent_hosts.tab_target_id_for_session_id(session_id)
|
||||
}
|
||||
|
||||
pub(crate) fn browser_context_id_for_tab_target_id(
|
||||
@@ -3559,7 +3524,7 @@ impl CdpConnection {
|
||||
if page_target_info.kind != DevToolsTargetKind::Page {
|
||||
return None;
|
||||
}
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.tab_target_info_for_page_target_info(page_target_info.clone())
|
||||
}
|
||||
|
||||
@@ -3576,7 +3541,7 @@ impl CdpConnection {
|
||||
let root_filter = owner_session_id
|
||||
.is_none()
|
||||
.then(|| filter.to_devtools_entries());
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.set_discover_targets(owner_session_id, filter);
|
||||
if let Some(root_filter) = root_filter {
|
||||
self.target_discovery_enabled = true;
|
||||
@@ -3601,7 +3566,7 @@ impl CdpConnection {
|
||||
}
|
||||
|
||||
pub(crate) fn clear_target_discovery_for_owner(&mut self, owner_session_id: Option<&str>) {
|
||||
self.target_control.clear_discover_targets(owner_session_id);
|
||||
self.agent_hosts.clear_discover_targets(owner_session_id);
|
||||
if owner_session_id.is_none() {
|
||||
self.target_discovery_enabled = false;
|
||||
self.target_info_change_events_enabled = false;
|
||||
@@ -3633,8 +3598,7 @@ impl CdpConnection {
|
||||
&self,
|
||||
owner_session_id: Option<&str>,
|
||||
) -> Option<Vec<DevToolsTargetFilterEntry>> {
|
||||
self.target_control
|
||||
.discover_filter_entries(owner_session_id)
|
||||
self.agent_hosts.discover_filter_entries(owner_session_id)
|
||||
}
|
||||
|
||||
pub(crate) fn initial_target_created_events_for_discovery_owner(
|
||||
@@ -3642,23 +3606,23 @@ impl CdpConnection {
|
||||
owner_session_id: Option<&str>,
|
||||
target_infos: Vec<DevToolsTargetInfo>,
|
||||
) -> Vec<BackgroundProtocolEvent> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.initial_target_created_events_for_owner(owner_session_id, target_infos)
|
||||
}
|
||||
|
||||
pub(crate) fn has_any_target_discovery(&self) -> bool {
|
||||
self.target_control.has_any_discovery()
|
||||
self.agent_hosts.has_any_discovery()
|
||||
}
|
||||
|
||||
pub(crate) fn has_any_target_info_observer(&self) -> bool {
|
||||
self.target_control.has_any_target_info_observer()
|
||||
self.agent_hosts.has_any_target_info_observer()
|
||||
}
|
||||
|
||||
fn exact_target_created_events_for_all_discovery_owners(
|
||||
&mut self,
|
||||
target_info: DevToolsTargetInfo,
|
||||
) -> Vec<BackgroundProtocolEvent> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.target_created_events_for_all_discovery_owners(target_info)
|
||||
}
|
||||
|
||||
@@ -3667,7 +3631,7 @@ impl CdpConnection {
|
||||
}
|
||||
|
||||
fn target_created_event_plan_for_target_delta(&mut self, target_id: &str) -> TargetEventPlan {
|
||||
let deltas = self.target_control.target_created_deltas(target_id);
|
||||
let deltas = self.agent_hosts.target_created_deltas(target_id);
|
||||
self.target_host_delta_events(deltas)
|
||||
}
|
||||
|
||||
@@ -3747,7 +3711,7 @@ impl CdpConnection {
|
||||
return TargetEventPlan::default();
|
||||
};
|
||||
TargetEventPlan::from_background_events(
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.target_info_changed_events_for_all_discovery_owners(target_info),
|
||||
)
|
||||
}
|
||||
@@ -3772,7 +3736,7 @@ impl CdpConnection {
|
||||
pub(crate) fn prepare_target_host_closure(&self, target_id: &str) -> PreparedTargetHostClosure {
|
||||
let mut detached_info_deltas = Vec::new();
|
||||
let mut destroyed_deltas = Vec::new();
|
||||
for delta in self.target_control.target_destroyed_deltas(target_id) {
|
||||
for delta in self.agent_hosts.target_destroyed_deltas(target_id) {
|
||||
let target_id = delta.target_id().to_owned();
|
||||
let Some(target_info) = self.target_info_for_host_delta(&target_id) else {
|
||||
continue;
|
||||
@@ -3846,7 +3810,7 @@ impl CdpConnection {
|
||||
&self,
|
||||
target_info: DevToolsTargetInfo,
|
||||
) -> Vec<BackgroundProtocolEvent> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.target_info_changed_events_for_all_observer_owners(target_info)
|
||||
}
|
||||
|
||||
@@ -3854,7 +3818,7 @@ impl CdpConnection {
|
||||
&mut self,
|
||||
target_info: DevToolsTargetInfo,
|
||||
) -> Vec<BackgroundProtocolEvent> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.target_destroyed_events_for_all_discovery_owners(target_info)
|
||||
}
|
||||
|
||||
@@ -3864,7 +3828,7 @@ impl CdpConnection {
|
||||
status: &str,
|
||||
error_code: i32,
|
||||
) -> Vec<BackgroundProtocolEvent> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.target_crashed_events_for_all_discovery_owners(target_id, status, error_code)
|
||||
}
|
||||
|
||||
@@ -3881,13 +3845,13 @@ impl CdpConnection {
|
||||
&self,
|
||||
target_info: DevToolsTargetInfo,
|
||||
) -> Vec<DevToolsTargetInfo> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.project_page_tab_target_infos_for_destruction(target_info)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn tab_target_count(&self) -> usize {
|
||||
self.target_control.len()
|
||||
self.agent_hosts.len()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -3897,7 +3861,7 @@ impl CdpConnection {
|
||||
}
|
||||
|
||||
fn has_registered_target_id(&self, target_id: &str) -> bool {
|
||||
self.target_control.contains_tab_or_page_relation(target_id)
|
||||
self.agent_hosts.contains_tab_or_page_relation(target_id)
|
||||
|| self
|
||||
.browser_contexts()
|
||||
.any(|context| context.devtools_target_info(target_id).is_some())
|
||||
@@ -3943,7 +3907,7 @@ impl CdpConnection {
|
||||
}
|
||||
}
|
||||
if !self
|
||||
.target_control
|
||||
.agent_hosts
|
||||
.contains_tab_or_page_relation(&default_target_id)
|
||||
{
|
||||
self.register_top_level_page_target(&default_target_id);
|
||||
|
||||
@@ -12,7 +12,7 @@ use moli_core::page::PendingPageCommand;
|
||||
use moli_page_types::DevToolsSessionKey;
|
||||
|
||||
struct TargetNetworkListenerOwnerMut<'a> {
|
||||
target: &'a mut crate::conn::PageTargetHost,
|
||||
target: &'a mut crate::conn::PageAgentHost,
|
||||
session_key: DevToolsSessionKey,
|
||||
}
|
||||
|
||||
|
||||
@@ -985,7 +985,7 @@ fn runtime_observable_console_payloads(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conn::PageTargetHost;
|
||||
use crate::conn::PageAgentHost;
|
||||
|
||||
#[test]
|
||||
fn csp_policy_preserves_peer_contributions_across_update_disable_and_detach() {
|
||||
@@ -1072,7 +1072,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn background_session_state_mut(state: &mut PageTargetHost) -> TargetSessionStateMut<'_> {
|
||||
fn background_session_state_mut(state: &mut PageAgentHost) -> TargetSessionStateMut<'_> {
|
||||
TargetSessionStateMut {
|
||||
devtools_session_state: &mut state.devtools_sessions
|
||||
[moli_page_types::DevToolsSessionKey::Primary],
|
||||
|
||||
@@ -282,8 +282,9 @@ impl CdpConnection {
|
||||
Some(0),
|
||||
session_id.as_deref(),
|
||||
) {
|
||||
crate::conn::CdpCommandTaskStep::Complete(outcome) => outcome,
|
||||
crate::conn::CdpCommandTaskStep::Pending(_) => {
|
||||
crate::conn::AgentHostDispatchResult::Complete(outcome) => outcome,
|
||||
crate::conn::AgentHostDispatchResult::PendingService(_)
|
||||
| crate::conn::AgentHostDispatchResult::FallThrough(_) => {
|
||||
unreachable!("a completed Runtime listener plan cannot become pending")
|
||||
}
|
||||
}
|
||||
@@ -397,7 +398,7 @@ impl CdpConnection {
|
||||
session_id: &str,
|
||||
route: &CdpSessionRoute,
|
||||
) -> Option<()> {
|
||||
if self.target_control.attached_session_route(session_id) == Some(route) {
|
||||
if self.agent_hosts.attached_session_route(session_id) == Some(route) {
|
||||
return Some(());
|
||||
}
|
||||
let target_id = match route {
|
||||
@@ -406,7 +407,7 @@ impl CdpConnection {
|
||||
| CdpSessionRoute::ServiceWorkerTarget { target_id, .. } => target_id.clone(),
|
||||
_ => return None,
|
||||
};
|
||||
self.target_control.commit_attached_session(
|
||||
self.agent_hosts.commit_attached_session(
|
||||
session_id.to_owned(),
|
||||
None,
|
||||
&target_id,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::conn::{CdpConnection, CdpSessionRoute, CommandOwnerScope, PageTargetHost};
|
||||
use crate::conn::{CdpConnection, CdpSessionRoute, CommandOwnerScope, PageAgentHost};
|
||||
use moli_page_types::DevToolsSessionKey;
|
||||
|
||||
pub(super) struct TargetSessionOwner {
|
||||
@@ -33,7 +33,7 @@ impl CdpConnection {
|
||||
pub(crate) fn mutate_target_page_state_for_session(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
f: impl FnOnce(&mut PageTargetHost),
|
||||
f: impl FnOnce(&mut PageAgentHost),
|
||||
) -> bool {
|
||||
self.target_session_owner_mut(session_id)
|
||||
.map(|mut owner| owner.mutate_page_state(|state, _| f(state)))
|
||||
|
||||
@@ -4,9 +4,10 @@ use crate::conn::DocumentStartScript;
|
||||
#[cfg(test)]
|
||||
use crate::conn::state::BrowserContextResourceStorageHandles;
|
||||
use crate::conn::state::{
|
||||
BrowserContextPageStorageHandles, DevToolsSessionState, PageNavigationHistoryEntry,
|
||||
PageTargetHost, RendererMainDocumentCommitSeed, TargetFetchConfig, TargetOwnerState,
|
||||
TargetPageResidenceIdentity, TargetRuntimeSessionState, TargetRuntimeSlot, WindowSurfaceState,
|
||||
BrowserContextPageStorageHandles, DevToolsSessionState, PageAgentHost,
|
||||
PageNavigationHistoryEntry, RendererMainDocumentCommitSeed, TargetFetchConfig,
|
||||
TargetOwnerState, TargetPageResidenceIdentity, TargetRuntimeSessionState, TargetRuntimeSlot,
|
||||
WindowSurfaceState,
|
||||
};
|
||||
use crate::conn::{
|
||||
BackgroundProtocolEvent, CommandOwnerScope, ConnectionNetworkRequestIdAllocator,
|
||||
@@ -506,7 +507,7 @@ impl<'a> TargetSessionOwnerRef<'a> {
|
||||
.target_navigation_history_entry_url(&self.target_id, entry_id)
|
||||
}
|
||||
|
||||
fn target(&self) -> &'a crate::conn::PageTargetHost {
|
||||
fn target(&self) -> &'a crate::conn::PageAgentHost {
|
||||
self.browser_context
|
||||
.page_target(&self.target_id)
|
||||
.expect("resolved Page target owner must remain live")
|
||||
@@ -660,19 +661,19 @@ impl TargetSessionStateMut<'_> {
|
||||
}
|
||||
|
||||
impl<'a> TargetSessionOwnerMut<'a> {
|
||||
fn target(&self) -> &crate::conn::PageTargetHost {
|
||||
fn target(&self) -> &crate::conn::PageAgentHost {
|
||||
self.browser_context
|
||||
.page_target(&self.target_id)
|
||||
.expect("resolved Page target owner must remain live")
|
||||
}
|
||||
|
||||
fn target_mut(&mut self) -> &mut crate::conn::PageTargetHost {
|
||||
fn target_mut(&mut self) -> &mut crate::conn::PageAgentHost {
|
||||
self.browser_context
|
||||
.page_target_mut(&self.target_id)
|
||||
.expect("resolved Page target owner must remain live")
|
||||
}
|
||||
|
||||
fn into_target_mut(self) -> &'a mut crate::conn::PageTargetHost {
|
||||
fn into_target_mut(self) -> &'a mut crate::conn::PageAgentHost {
|
||||
self.browser_context
|
||||
.page_target_mut(&self.target_id)
|
||||
.expect("resolved Page target owner must remain live")
|
||||
@@ -702,7 +703,7 @@ impl<'a> TargetSessionOwnerMut<'a> {
|
||||
|
||||
pub(super) fn mutate_page_state<T>(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut PageTargetHost, &DevToolsSessionKey) -> T,
|
||||
f: impl FnOnce(&mut PageAgentHost, &DevToolsSessionKey) -> T,
|
||||
) -> T {
|
||||
let session_key = self.session_key.clone();
|
||||
f(self.target_mut(), &session_key)
|
||||
@@ -1522,6 +1523,17 @@ impl CdpConnection {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn page_agent_host_main_frame_slot_for_owner(
|
||||
&self,
|
||||
owner: &CommandOwnerScope,
|
||||
) -> Option<moli_core::browser::MainFrameSlotId> {
|
||||
Some(
|
||||
self.target_session_owner_ref_for_owner(owner)?
|
||||
.target()
|
||||
.main_frame_slot_id(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_page_owner_identity_for_owner(
|
||||
&self,
|
||||
owner: &CommandOwnerScope,
|
||||
|
||||
@@ -24,8 +24,9 @@ pub struct Cmd<'a> {
|
||||
pub session_id: Option<&'a str>,
|
||||
/// Raw JSON of the entire message, useful for inspector passthrough.
|
||||
pub json: &'a str,
|
||||
/// Renderer scheduling facts derived from the same validated method as
|
||||
/// every other field in this view.
|
||||
/// Renderer execution policy derived from the same validated method as
|
||||
/// every other field in this view. Dispatch ownership and Main/IO routing
|
||||
/// are decided later by the domain handler.
|
||||
///
|
||||
/// This is private so callers cannot pair one command with another
|
||||
/// command's policy. Domain dispatchers consume it through
|
||||
|
||||
@@ -9,17 +9,106 @@ use super::*;
|
||||
pub struct PendingCdpCommandDispatch {
|
||||
inner: PendingCdpCommandDispatchKind,
|
||||
scheduler_events: Vec<CdpSchedulerEvent>,
|
||||
owner_scope: CommandOwnerScope,
|
||||
}
|
||||
|
||||
pub struct CompletedCdpCommandDispatch {
|
||||
inner: CompletedCdpCommandDispatchKind,
|
||||
owner_scope: CommandOwnerScope,
|
||||
}
|
||||
|
||||
/// Renderer receiver selected only after a DevTools domain handler falls
|
||||
/// through its browser/content phase.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RendererDispatchLane {
|
||||
Main,
|
||||
Io,
|
||||
}
|
||||
|
||||
/// Exact Page AgentHost binding that admitted one renderer command.
|
||||
///
|
||||
/// `MainFrameSlotId` remains stable across cross-document navigation while
|
||||
/// `DocumentId` and `RendererAgentAttachmentId` identify the admitted binding
|
||||
/// generation. A completion cannot be rebound to whichever Document is current
|
||||
/// later.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct RendererPageDispatchBinding {
|
||||
main_frame_slot: moli_core::browser::MainFrameSlotId,
|
||||
document: moli_core::browser::DocumentId,
|
||||
attachment: moli_core::page::RendererAgentAttachmentId,
|
||||
}
|
||||
|
||||
impl RendererPageDispatchBinding {
|
||||
pub const fn main_frame_slot(self) -> moli_core::browser::MainFrameSlotId {
|
||||
self.main_frame_slot
|
||||
}
|
||||
|
||||
pub const fn document(self) -> moli_core::browser::DocumentId {
|
||||
self.document
|
||||
}
|
||||
|
||||
pub const fn attachment(self) -> moli_core::page::RendererAgentAttachmentId {
|
||||
self.attachment
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum RendererDispatchBinding {
|
||||
Page(RendererPageDispatchBinding),
|
||||
Worker { target_id: String },
|
||||
}
|
||||
|
||||
/// One concrete renderer fallthrough plus the DevTools-owned pending reply.
|
||||
pub struct RendererDispatch {
|
||||
lane: RendererDispatchLane,
|
||||
binding: RendererDispatchBinding,
|
||||
pending: Box<PendingCdpCommandDispatch>,
|
||||
}
|
||||
|
||||
impl RendererDispatch {
|
||||
pub const fn lane(&self) -> RendererDispatchLane {
|
||||
self.lane
|
||||
}
|
||||
|
||||
pub const fn binding(&self) -> &RendererDispatchBinding {
|
||||
&self.binding
|
||||
}
|
||||
|
||||
pub fn into_pending(self) -> Box<PendingCdpCommandDispatch> {
|
||||
self.pending
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of the DevToolsSession/AgentHost browser-content dispatch phase.
|
||||
///
|
||||
/// Completion ownership and renderer routing deliberately remain separate:
|
||||
/// only `FallThrough` has a renderer binding and Main/IO lane.
|
||||
pub enum AgentHostDispatchResult {
|
||||
Complete(CdpRendererOwnerTurnOutcome),
|
||||
PendingService(Box<PendingCdpCommandDispatch>),
|
||||
FallThrough(RendererDispatch),
|
||||
}
|
||||
|
||||
/// Compatibility task step used by direct in-process/test callers. Production
|
||||
/// frontend dispatch consumes `AgentHostDispatchResult` so it cannot erase the
|
||||
/// service/fallthrough distinction at ingress.
|
||||
pub enum CdpCommandTaskStep {
|
||||
Pending(Box<PendingCdpCommandDispatch>),
|
||||
Complete(CdpRendererOwnerTurnOutcome),
|
||||
}
|
||||
|
||||
impl From<AgentHostDispatchResult> for CdpCommandTaskStep {
|
||||
fn from(result: AgentHostDispatchResult) -> Self {
|
||||
match result {
|
||||
AgentHostDispatchResult::Complete(outcome) => Self::Complete(outcome),
|
||||
AgentHostDispatchResult::PendingService(pending) => Self::Pending(pending),
|
||||
AgentHostDispatchResult::FallThrough(dispatch) => {
|
||||
Self::Pending(dispatch.into_pending())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CdpCommandTaskStep {
|
||||
#[cfg(test)]
|
||||
pub fn into_parts(self) -> (Vec<Value>, Vec<CdpSchedulerEvent>) {
|
||||
@@ -103,6 +192,31 @@ impl PendingCdpCommandDispatchKind {
|
||||
Self::Tracing(_) => "Tracing",
|
||||
}
|
||||
}
|
||||
|
||||
fn renderer_dispatch_lane(&self) -> Option<RendererDispatchLane> {
|
||||
match self {
|
||||
Self::Runtime(pending) => pending.renderer_dispatch_lane(),
|
||||
Self::Accessibility(pending) => pending.renderer_dispatch_lane(),
|
||||
Self::Css(pending) => pending.renderer_dispatch_lane(),
|
||||
Self::Dom(pending) => pending.renderer_dispatch_lane(),
|
||||
Self::DomDebugger(pending) => pending.renderer_dispatch_lane(),
|
||||
Self::DomSnapshot(pending) => pending.renderer_dispatch_lane(),
|
||||
Self::Page(pending) => pending.renderer_dispatch_lane(),
|
||||
Self::Performance(pending) => pending.renderer_dispatch_lane(),
|
||||
Self::Emulation(pending) => pending.renderer_dispatch_lane(),
|
||||
Self::Autofill(_)
|
||||
| Self::Input(_)
|
||||
| Self::DomStorage(_)
|
||||
| Self::Storage(_)
|
||||
| Self::Network(_)
|
||||
| Self::Fetch(_)
|
||||
| Self::Io(_)
|
||||
| Self::Security(_)
|
||||
| Self::Browser(_)
|
||||
| Self::Target(_)
|
||||
| Self::Tracing(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletedCdpCommandDispatchKind {
|
||||
@@ -133,10 +247,15 @@ impl CompletedCdpCommandDispatchKind {
|
||||
}
|
||||
|
||||
impl PendingCdpCommandDispatch {
|
||||
fn new(inner: PendingCdpCommandDispatchKind, scheduler_events: Vec<CdpSchedulerEvent>) -> Self {
|
||||
fn new(
|
||||
inner: PendingCdpCommandDispatchKind,
|
||||
scheduler_events: Vec<CdpSchedulerEvent>,
|
||||
owner_scope: CommandOwnerScope,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
scheduler_events,
|
||||
owner_scope,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,11 +354,17 @@ impl PendingCdpCommandDispatch {
|
||||
self,
|
||||
conn: &mut CdpConnection,
|
||||
) -> CompletedCdpCommandDispatch {
|
||||
match self.inner {
|
||||
let Self {
|
||||
inner,
|
||||
owner_scope,
|
||||
scheduler_events: _,
|
||||
} = self;
|
||||
match inner {
|
||||
PendingCdpCommandDispatchKind::Runtime(pending) => CompletedCdpCommandDispatch {
|
||||
inner: CompletedCdpCommandDispatchKind::Runtime(Box::new(
|
||||
pending.complete_scheduler_deferred_inspector_reply(conn),
|
||||
)),
|
||||
owner_scope,
|
||||
},
|
||||
_ => {
|
||||
unreachable!(
|
||||
@@ -256,7 +381,12 @@ impl PendingCdpCommandDispatch {
|
||||
}
|
||||
|
||||
pub async fn wait(self) -> CompletedCdpCommandDispatch {
|
||||
let kind = self.inner.name();
|
||||
let Self {
|
||||
inner,
|
||||
scheduler_events: _,
|
||||
owner_scope,
|
||||
} = self;
|
||||
let kind = inner.name();
|
||||
let trace_started = moli_trace::cdp_runtime_trace_enabled().then(std::time::Instant::now);
|
||||
if trace_started.is_some() {
|
||||
tracing::info!(
|
||||
@@ -265,7 +395,7 @@ impl PendingCdpCommandDispatch {
|
||||
pending_kind = kind,
|
||||
);
|
||||
}
|
||||
let inner = match self.inner {
|
||||
let inner = match inner {
|
||||
PendingCdpCommandDispatchKind::Runtime(pending) => {
|
||||
CompletedCdpCommandDispatchKind::Runtime(Box::new(Box::pin(pending.wait()).await))
|
||||
}
|
||||
@@ -337,7 +467,7 @@ impl PendingCdpCommandDispatch {
|
||||
elapsed_us = %started.elapsed().as_micros(),
|
||||
);
|
||||
}
|
||||
CompletedCdpCommandDispatch { inner }
|
||||
CompletedCdpCommandDispatch { inner, owner_scope }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,11 +501,52 @@ fn append_command_output_plan(
|
||||
}
|
||||
|
||||
impl CdpConnection {
|
||||
/// Whether a command must wait for the Page AgentHost to publish its next
|
||||
/// Document binding before its browser/content handler may run.
|
||||
///
|
||||
/// This is deliberately not a renderer-lane classifier. A lane exists
|
||||
/// only after a handler returns `FallThrough`; this admission check merely
|
||||
/// prevents a known Main-thread fallthrough from binding to the outgoing
|
||||
/// Document while a replacement is being projected.
|
||||
pub fn command_waits_for_document_projection(&self, command: &ParsedCdpCommand) -> bool {
|
||||
if !self.document_projection_is_pending_for_session_owner(command.session_id()) {
|
||||
return false;
|
||||
}
|
||||
let Some(cmd) = Cmd::from_parsed(command) else {
|
||||
return false;
|
||||
};
|
||||
let Some((domain, _)) = cmd.method.split_once('.') else {
|
||||
return false;
|
||||
};
|
||||
match domain {
|
||||
"Runtime" => crate::domains::runtime::command_waits_for_document_projection(&cmd),
|
||||
"Debugger" => crate::domains::debugger::command_waits_for_document_projection(&cmd),
|
||||
"Console" => crate::domains::console::command_waits_for_document_projection(&cmd),
|
||||
"Profiler" => crate::domains::profiler::command_waits_for_document_projection(&cmd),
|
||||
"HeapProfiler" => {
|
||||
crate::domains::heap_profiler::command_waits_for_document_projection(&cmd)
|
||||
}
|
||||
"Accessibility" => {
|
||||
crate::domains::accessibility::command_waits_for_document_projection(&cmd)
|
||||
}
|
||||
"CSS" => crate::domains::css::command_waits_for_document_projection(&cmd),
|
||||
"DOM" => crate::domains::dom::command_waits_for_document_projection(&cmd),
|
||||
"DOMDebugger" => {
|
||||
crate::domains::dom_debugger::command_waits_for_document_projection(&cmd)
|
||||
}
|
||||
"DOMSnapshot" => {
|
||||
crate::domains::dom_snapshot::command_waits_for_document_projection(&cmd)
|
||||
}
|
||||
"Page" => crate::domains::page::command_waits_for_document_projection(&cmd),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_with_protocol_events(
|
||||
&mut self,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
protocol_events: Vec<BackgroundProtocolEvent>,
|
||||
) -> CdpCommandTaskStep {
|
||||
) -> AgentHostDispatchResult {
|
||||
// `CommandDispatchContext::protocol_events` contains concrete output
|
||||
// explicitly classified as preceding this command's response. Keep
|
||||
// that ordering at the one common completion boundary instead of
|
||||
@@ -389,7 +560,7 @@ impl CdpConnection {
|
||||
self.record_tracing_protocol_events(&protocol_events);
|
||||
self.record_tracing_protocol_events(&post_renderer_output_events);
|
||||
self.record_tracing_protocol_events(&post_response_events);
|
||||
CdpCommandTaskStep::Complete(
|
||||
AgentHostDispatchResult::Complete(
|
||||
CdpTurnOutcome::new_with_protocol_and_post_response_events(
|
||||
protocol_events,
|
||||
post_response_events,
|
||||
@@ -406,7 +577,7 @@ impl CdpConnection {
|
||||
plan: CommandOutputPlan,
|
||||
command_id: Option<u64>,
|
||||
session_id: Option<&str>,
|
||||
) -> CdpCommandTaskStep {
|
||||
) -> AgentHostDispatchResult {
|
||||
let mut protocol_events = Vec::new();
|
||||
append_command_output_plan(
|
||||
&mut protocol_events,
|
||||
@@ -418,12 +589,53 @@ impl CdpConnection {
|
||||
self.complete_with_protocol_events(command_context, protocol_events)
|
||||
}
|
||||
|
||||
fn pending_step(&mut self, inner: PendingCdpCommandDispatchKind) -> CdpCommandTaskStep {
|
||||
fn pending_step(
|
||||
&mut self,
|
||||
owner_scope: CommandOwnerScope,
|
||||
inner: PendingCdpCommandDispatchKind,
|
||||
) -> AgentHostDispatchResult {
|
||||
let renderer_lane = inner.renderer_dispatch_lane();
|
||||
let scheduler_events = self.take_scheduler_events();
|
||||
CdpCommandTaskStep::Pending(Box::new(PendingCdpCommandDispatch::new(
|
||||
let pending = Box::new(PendingCdpCommandDispatch::new(
|
||||
inner,
|
||||
scheduler_events,
|
||||
)))
|
||||
owner_scope.clone(),
|
||||
));
|
||||
let Some(lane) = renderer_lane else {
|
||||
return AgentHostDispatchResult::PendingService(pending);
|
||||
};
|
||||
let binding = self
|
||||
.renderer_dispatch_binding_for_owner(&owner_scope)
|
||||
.expect("a renderer fallthrough must retain its exact AgentHost binding");
|
||||
AgentHostDispatchResult::FallThrough(RendererDispatch {
|
||||
lane,
|
||||
binding,
|
||||
pending,
|
||||
})
|
||||
}
|
||||
|
||||
fn renderer_dispatch_binding_for_owner(
|
||||
&self,
|
||||
owner: &CommandOwnerScope,
|
||||
) -> Option<RendererDispatchBinding> {
|
||||
match owner.resolve_route(self)? {
|
||||
CdpSessionRoute::PageTarget { .. } => {
|
||||
let attachment = self.current_renderer_agent_attachment_for_owner(owner)?;
|
||||
Some(RendererDispatchBinding::Page(RendererPageDispatchBinding {
|
||||
main_frame_slot: self.page_agent_host_main_frame_slot_for_owner(owner)?,
|
||||
document: attachment.document(),
|
||||
attachment: attachment.id(),
|
||||
}))
|
||||
}
|
||||
CdpSessionRoute::SharedWorkerTarget { target_id, .. }
|
||||
| CdpSessionRoute::DedicatedWorkerTarget { target_id, .. }
|
||||
| CdpSessionRoute::ServiceWorkerTarget { target_id, .. } => {
|
||||
Some(RendererDispatchBinding::Worker { target_id })
|
||||
}
|
||||
CdpSessionRoute::Browser
|
||||
| CdpSessionRoute::BrowserContext { .. }
|
||||
| CdpSessionRoute::TabTarget { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -448,15 +660,17 @@ impl CdpConnection {
|
||||
Ok(command) => command,
|
||||
Err(error) => {
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
return self.complete_with_output_plan(
|
||||
&mut command_context,
|
||||
CommandOutputPlan::error_without_session(
|
||||
error.response_code(),
|
||||
error.response_message(),
|
||||
),
|
||||
error.command_id(),
|
||||
None,
|
||||
);
|
||||
return self
|
||||
.complete_with_output_plan(
|
||||
&mut command_context,
|
||||
CommandOutputPlan::error_without_session(
|
||||
error.response_code(),
|
||||
error.response_message(),
|
||||
),
|
||||
error.command_id(),
|
||||
None,
|
||||
)
|
||||
.into();
|
||||
}
|
||||
};
|
||||
self.start_parsed_command_dispatch(&command)
|
||||
@@ -469,13 +683,14 @@ impl CdpConnection {
|
||||
) -> CdpCommandTaskStep {
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
self.start_parsed_command_dispatch_with_context(command, &mut command_context)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn start_parsed_command_dispatch_with_context(
|
||||
&mut self,
|
||||
command: &ParsedCdpCommand,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) -> CdpCommandTaskStep {
|
||||
) -> AgentHostDispatchResult {
|
||||
let req = command.request();
|
||||
let Some(dot) = req.method().find('.') else {
|
||||
return self.complete_with_output_plan(
|
||||
@@ -520,12 +735,16 @@ impl CdpConnection {
|
||||
.with_terminal_response_delivery_override(
|
||||
command_context.terminal_response_delivery_override(),
|
||||
);
|
||||
let command_owner = CommandOwnerScope::capture(self, cmd.session_id);
|
||||
self.record_tracing_command(cmd.method, cmd.session_id);
|
||||
let step = match domain {
|
||||
"Browser" => Some(
|
||||
match crate::domains::browser::try_start_browser_command_dispatch(self, &cmd) {
|
||||
crate::domains::browser::BrowserCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Browser(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Browser(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::browser::BrowserCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -535,7 +754,10 @@ impl CdpConnection {
|
||||
"Runtime" => crate::domains::runtime::try_start_runtime_command_dispatch(self, &cmd)
|
||||
.map(|step| match step {
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Runtime(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -548,7 +770,10 @@ impl CdpConnection {
|
||||
)
|
||||
.map(|step| match step {
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Runtime(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -558,7 +783,10 @@ impl CdpConnection {
|
||||
"Profiler" => crate::domains::profiler::try_start_profiler_command_dispatch(self, &cmd)
|
||||
.map(|step| match step {
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Runtime(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -571,7 +799,10 @@ impl CdpConnection {
|
||||
)
|
||||
.map(|step| match step {
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Runtime(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(
|
||||
@@ -589,7 +820,10 @@ impl CdpConnection {
|
||||
match step {
|
||||
crate::domains::accessibility::AccessibilityCommandDispatchStep::Pending(
|
||||
pending,
|
||||
) => self.pending_step(PendingCdpCommandDispatchKind::Accessibility(pending)),
|
||||
) => self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Accessibility(pending),
|
||||
),
|
||||
crate::domains::accessibility::AccessibilityCommandDispatchStep::Complete(
|
||||
plan,
|
||||
) => self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id),
|
||||
@@ -599,7 +833,10 @@ impl CdpConnection {
|
||||
"Input" => Some(
|
||||
match crate::domains::input::try_start_input_command_dispatch(self, &cmd) {
|
||||
crate::domains::input::InputCommandDispatchStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Input(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Input(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::input::InputCommandDispatchStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -609,7 +846,10 @@ impl CdpConnection {
|
||||
"CSS" => crate::domains::css::try_start_css_command_dispatch(self, &cmd).map(|step| {
|
||||
match step {
|
||||
crate::domains::css::CssCommandDispatchStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Css(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Css(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::css::CssCommandDispatchStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -619,7 +859,10 @@ impl CdpConnection {
|
||||
"DOM" => crate::domains::dom::try_start_dom_command_dispatch(self, &cmd).map(|step| {
|
||||
match step {
|
||||
crate::domains::dom::DomCommandDispatchStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Dom(*pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Dom(*pending),
|
||||
)
|
||||
}
|
||||
crate::domains::dom::DomCommandDispatchStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -631,7 +874,10 @@ impl CdpConnection {
|
||||
self, &cmd,
|
||||
) {
|
||||
crate::domains::dom_storage::DomStorageCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::DomStorage(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::DomStorage(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::dom_storage::DomStorageCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -645,7 +891,10 @@ impl CdpConnection {
|
||||
)
|
||||
.map(|step| match step {
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Runtime(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -655,7 +904,7 @@ impl CdpConnection {
|
||||
}
|
||||
"Emulation" | "Network" if let Some(step) = crate::domains::runtime::try_start_worker_emulation_command_dispatch(self, &cmd) => {
|
||||
Some(match step {
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending)),
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => self.pending_step(command_owner.clone(), PendingCdpCommandDispatchKind::Runtime(pending)),
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id),
|
||||
})
|
||||
}
|
||||
@@ -663,13 +912,19 @@ impl CdpConnection {
|
||||
match crate::domains::network::start_network_domain_command_dispatch(self, &cmd) {
|
||||
crate::domains::network::NetworkDomainCommandTaskStep::Network(
|
||||
crate::domains::network::NetworkCommandTaskStep::Pending(pending),
|
||||
) => self.pending_step(PendingCdpCommandDispatchKind::Network(pending)),
|
||||
) => self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Network(pending),
|
||||
),
|
||||
crate::domains::network::NetworkDomainCommandTaskStep::Network(
|
||||
crate::domains::network::NetworkCommandTaskStep::Complete(plan),
|
||||
) => self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id),
|
||||
crate::domains::network::NetworkDomainCommandTaskStep::Storage(
|
||||
crate::domains::storage::StorageCommandTaskStep::Pending(pending),
|
||||
) => self.pending_step(PendingCdpCommandDispatchKind::Storage(pending)),
|
||||
) => self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Storage(pending),
|
||||
),
|
||||
crate::domains::network::NetworkDomainCommandTaskStep::Storage(
|
||||
crate::domains::storage::StorageCommandTaskStep::Complete(plan),
|
||||
) => self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id),
|
||||
@@ -682,7 +937,10 @@ impl CdpConnection {
|
||||
crate::domains::target::try_start_target_command_dispatch(self, &cmd).map(|step| {
|
||||
match step {
|
||||
crate::domains::target::TargetCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Target(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Target(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::target::TargetCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -693,7 +951,10 @@ impl CdpConnection {
|
||||
"Tracing" => Some(
|
||||
match crate::domains::tracing::try_start_tracing_command_dispatch(self, &cmd) {
|
||||
crate::domains::tracing::TracingCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Tracing(*pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Tracing(*pending),
|
||||
)
|
||||
}
|
||||
crate::domains::tracing::TracingCommandTaskStep::Complete(plan) => self
|
||||
.complete_with_output_plan(
|
||||
@@ -708,7 +969,10 @@ impl CdpConnection {
|
||||
crate::domains::fetch::try_start_fetch_command_dispatch(self, &cmd).map(|step| {
|
||||
match step {
|
||||
crate::domains::fetch::FetchCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Fetch(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Fetch(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::fetch::FetchCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -720,7 +984,10 @@ impl CdpConnection {
|
||||
crate::domains::page::try_start_page_command_dispatch(self, &cmd).map(|step| {
|
||||
match step {
|
||||
crate::domains::page::PageCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Page(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Page(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::page::PageCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -739,7 +1006,10 @@ impl CdpConnection {
|
||||
"Storage" => Some(
|
||||
match crate::domains::storage::try_start_storage_command_dispatch(self, &cmd) {
|
||||
crate::domains::storage::StorageCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Storage(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Storage(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::storage::StorageCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -753,7 +1023,10 @@ impl CdpConnection {
|
||||
) {
|
||||
crate::domains::dom_snapshot::DomSnapshotCommandDispatchStep::Pending(
|
||||
pending,
|
||||
) => self.pending_step(PendingCdpCommandDispatchKind::DomSnapshot(pending)),
|
||||
) => self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::DomSnapshot(pending),
|
||||
),
|
||||
crate::domains::dom_snapshot::DomSnapshotCommandDispatchStep::Complete(
|
||||
plan,
|
||||
) => self.complete_with_output_plan(
|
||||
@@ -768,7 +1041,10 @@ impl CdpConnection {
|
||||
"Security" => Some(
|
||||
match crate::domains::security::try_start_security_command_dispatch(self, &cmd) {
|
||||
crate::domains::security::SecurityCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Security(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Security(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::security::SecurityCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -782,7 +1058,10 @@ impl CdpConnection {
|
||||
"IO" => Some(
|
||||
match crate::domains::io::try_start_io_command_dispatch(self, &cmd) {
|
||||
crate::domains::io::IoCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Io(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Io(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::io::IoCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -792,7 +1071,10 @@ impl CdpConnection {
|
||||
"Autofill" => Some(
|
||||
match crate::domains::autofill::try_start_autofill_command_dispatch(self, &cmd) {
|
||||
crate::domains::autofill::AutofillCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Autofill(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Autofill(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::autofill::AutofillCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(
|
||||
@@ -826,7 +1108,10 @@ impl CdpConnection {
|
||||
&cmd,
|
||||
) {
|
||||
crate::domains::performance::PerformanceCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Performance(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Performance(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::performance::PerformanceCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -838,7 +1123,10 @@ impl CdpConnection {
|
||||
self, &cmd,
|
||||
) {
|
||||
crate::domains::dom_debugger::DomDebuggerCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::DomDebugger(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::DomDebugger(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::dom_debugger::DomDebuggerCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -850,7 +1138,10 @@ impl CdpConnection {
|
||||
)
|
||||
.map(|step| match step {
|
||||
crate::domains::emulation::EmulationCommandTaskStep::Pending(pending) => {
|
||||
self.pending_step(PendingCdpCommandDispatchKind::Emulation(pending))
|
||||
self.pending_step(
|
||||
command_owner.clone(),
|
||||
PendingCdpCommandDispatchKind::Emulation(pending),
|
||||
)
|
||||
}
|
||||
crate::domains::emulation::EmulationCommandTaskStep::Complete(plan) => {
|
||||
self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id)
|
||||
@@ -884,15 +1175,17 @@ impl CdpConnection {
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
self.complete_pending_command_dispatch_with_context(completed, &mut command_context)
|
||||
.await
|
||||
.into()
|
||||
}
|
||||
|
||||
pub async fn complete_pending_command_dispatch_with_context(
|
||||
&mut self,
|
||||
completed: CompletedCdpCommandDispatch,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) -> CdpCommandTaskStep {
|
||||
) -> AgentHostDispatchResult {
|
||||
let CompletedCdpCommandDispatch { inner, owner_scope } = completed;
|
||||
let mut out = Vec::new();
|
||||
match completed.inner {
|
||||
match inner {
|
||||
CompletedCdpCommandDispatchKind::Runtime(completed) => {
|
||||
let completed = *completed;
|
||||
let command_id = completed.command_id();
|
||||
@@ -906,7 +1199,10 @@ impl CdpConnection {
|
||||
.await
|
||||
{
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => {
|
||||
return self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending));
|
||||
return self.pending_step(
|
||||
owner_scope.clone(),
|
||||
PendingCdpCommandDispatchKind::Runtime(pending),
|
||||
);
|
||||
}
|
||||
crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => {
|
||||
append_command_output_plan(
|
||||
@@ -937,8 +1233,10 @@ impl CdpConnection {
|
||||
crate::domains::accessibility::AccessibilityCommandDispatchStep::Pending(
|
||||
pending,
|
||||
) => {
|
||||
return self
|
||||
.pending_step(PendingCdpCommandDispatchKind::Accessibility(pending));
|
||||
return self.pending_step(
|
||||
owner_scope.clone(),
|
||||
PendingCdpCommandDispatchKind::Accessibility(pending),
|
||||
);
|
||||
}
|
||||
crate::domains::accessibility::AccessibilityCommandDispatchStep::Complete(
|
||||
plan,
|
||||
@@ -980,7 +1278,10 @@ impl CdpConnection {
|
||||
let session_id = completed.session_id().map(str::to_owned);
|
||||
match crate::domains::css::complete_pending_css_command(self, completed) {
|
||||
crate::domains::css::CssCommandDispatchStep::Pending(pending) => {
|
||||
return self.pending_step(PendingCdpCommandDispatchKind::Css(pending));
|
||||
return self.pending_step(
|
||||
owner_scope.clone(),
|
||||
PendingCdpCommandDispatchKind::Css(pending),
|
||||
);
|
||||
}
|
||||
crate::domains::css::CssCommandDispatchStep::Complete(plan) => {
|
||||
append_command_output_plan(
|
||||
@@ -1009,7 +1310,10 @@ impl CdpConnection {
|
||||
);
|
||||
match step {
|
||||
crate::domains::dom::DomCommandTaskStep::Pending(pending) => {
|
||||
return self.pending_step(PendingCdpCommandDispatchKind::Dom(*pending));
|
||||
return self.pending_step(
|
||||
owner_scope.clone(),
|
||||
PendingCdpCommandDispatchKind::Dom(*pending),
|
||||
);
|
||||
}
|
||||
crate::domains::dom::DomCommandTaskStep::Complete => {}
|
||||
}
|
||||
@@ -1029,8 +1333,10 @@ impl CdpConnection {
|
||||
self, *completed,
|
||||
) {
|
||||
crate::domains::dom_storage::DomStorageCommandTaskStep::Pending(pending) => {
|
||||
return self
|
||||
.pending_step(PendingCdpCommandDispatchKind::DomStorage(pending));
|
||||
return self.pending_step(
|
||||
owner_scope.clone(),
|
||||
PendingCdpCommandDispatchKind::DomStorage(pending),
|
||||
);
|
||||
}
|
||||
crate::domains::dom_storage::DomStorageCommandTaskStep::Complete(plan) => {
|
||||
append_command_output_plan(
|
||||
@@ -1052,8 +1358,10 @@ impl CdpConnection {
|
||||
crate::domains::dom_snapshot::DomSnapshotCommandDispatchStep::Pending(
|
||||
pending,
|
||||
) => {
|
||||
return self
|
||||
.pending_step(PendingCdpCommandDispatchKind::DomSnapshot(pending));
|
||||
return self.pending_step(
|
||||
owner_scope.clone(),
|
||||
PendingCdpCommandDispatchKind::DomSnapshot(pending),
|
||||
);
|
||||
}
|
||||
crate::domains::dom_snapshot::DomSnapshotCommandDispatchStep::Complete(
|
||||
plan,
|
||||
@@ -1079,7 +1387,10 @@ impl CdpConnection {
|
||||
.await
|
||||
{
|
||||
crate::domains::page::PageCommandTaskStep::Pending(pending) => {
|
||||
return self.pending_step(PendingCdpCommandDispatchKind::Page(pending));
|
||||
return self.pending_step(
|
||||
owner_scope.clone(),
|
||||
PendingCdpCommandDispatchKind::Page(pending),
|
||||
);
|
||||
}
|
||||
crate::domains::page::PageCommandTaskStep::Complete(plan) => {
|
||||
append_command_output_plan(
|
||||
@@ -1138,7 +1449,10 @@ impl CdpConnection {
|
||||
let session_id = completed.session_id().map(str::to_owned);
|
||||
match crate::domains::network::complete_pending_network_command(self, completed) {
|
||||
crate::domains::network::NetworkCommandTaskStep::Pending(pending) => {
|
||||
return self.pending_step(PendingCdpCommandDispatchKind::Network(pending));
|
||||
return self.pending_step(
|
||||
owner_scope.clone(),
|
||||
PendingCdpCommandDispatchKind::Network(pending),
|
||||
);
|
||||
}
|
||||
crate::domains::network::NetworkCommandTaskStep::Complete(plan) => {
|
||||
append_command_output_plan(
|
||||
@@ -1216,7 +1530,10 @@ impl CdpConnection {
|
||||
.await
|
||||
{
|
||||
crate::domains::target::TargetCommandTaskStep::Pending(pending) => {
|
||||
return self.pending_step(PendingCdpCommandDispatchKind::Target(pending));
|
||||
return self.pending_step(
|
||||
owner_scope.clone(),
|
||||
PendingCdpCommandDispatchKind::Target(pending),
|
||||
);
|
||||
}
|
||||
crate::domains::target::TargetCommandTaskStep::Complete(plan) => {
|
||||
append_command_output_plan(
|
||||
@@ -1242,7 +1559,7 @@ impl CdpConnection {
|
||||
command_context.take_renderer_fenced_protocol_events();
|
||||
let post_response_events = command_context.take_post_response_events();
|
||||
let scheduler_events = self.take_scheduler_events();
|
||||
CdpCommandTaskStep::Complete(
|
||||
AgentHostDispatchResult::Complete(
|
||||
CdpTurnOutcome::new_with_protocol_and_post_response_events(
|
||||
out,
|
||||
post_response_events,
|
||||
@@ -1334,19 +1651,21 @@ impl CdpConnection {
|
||||
if let Some(response_delivery) = response_delivery_override {
|
||||
command_context.set_terminal_response_delivery_override(response_delivery);
|
||||
}
|
||||
let mut step = match command.as_ref() {
|
||||
Ok(command) => {
|
||||
self.start_parsed_command_dispatch_with_context(command, &mut command_context)
|
||||
}
|
||||
Err(error) => self.complete_with_output_plan(
|
||||
&mut command_context,
|
||||
CommandOutputPlan::error_without_session(
|
||||
error.response_code(),
|
||||
error.response_message(),
|
||||
),
|
||||
error.command_id(),
|
||||
None,
|
||||
),
|
||||
let mut step: CdpCommandTaskStep = match command.as_ref() {
|
||||
Ok(command) => self
|
||||
.start_parsed_command_dispatch_with_context(command, &mut command_context)
|
||||
.into(),
|
||||
Err(error) => self
|
||||
.complete_with_output_plan(
|
||||
&mut command_context,
|
||||
CommandOutputPlan::error_without_session(
|
||||
error.response_code(),
|
||||
error.response_message(),
|
||||
),
|
||||
error.command_id(),
|
||||
None,
|
||||
)
|
||||
.into(),
|
||||
};
|
||||
loop {
|
||||
match step {
|
||||
@@ -1379,7 +1698,8 @@ impl CdpConnection {
|
||||
completed,
|
||||
&mut command_context,
|
||||
))
|
||||
.await;
|
||||
.await
|
||||
.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9104,3 +9104,207 @@ fn command_dispatch_completes_additional_sync_domain_unknown_methods_without_leg
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn loaded_agent_host_dispatch_connection_for_test() -> CdpConnection {
|
||||
let mut conn = CdpConnection::new();
|
||||
let mut browser_context = BrowserContext::new("BID-agent-host-dispatch".to_owned());
|
||||
browser_context.set_active_target_id("TID-agent-host-dispatch".to_owned());
|
||||
conn.install_browser_context_fixture_for_test(browser_context);
|
||||
let page = conn
|
||||
.load_page_via_runtime_async("data:text/html,<p>agent host dispatch</p>")
|
||||
.await
|
||||
.expect("agent host dispatch page should load");
|
||||
conn.browser_context
|
||||
.as_mut()
|
||||
.expect("browser context")
|
||||
.replace_active_page_for_test(Some(page));
|
||||
conn
|
||||
}
|
||||
|
||||
fn parsed_agent_host_command(id: u64, method: &str, params: Value) -> ParsedCdpCommand {
|
||||
ParsedCdpCommand::parse_str(
|
||||
serde_json::to_string(&json!({
|
||||
"id": id,
|
||||
"method": method,
|
||||
"params": params,
|
||||
}))
|
||||
.expect("agent host command should serialize"),
|
||||
)
|
||||
.expect("agent host command should parse")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agent_host_dispatch_exposes_only_actual_renderer_fallthrough_binding() {
|
||||
let mut conn = loaded_agent_host_dispatch_connection_for_test().await;
|
||||
let owner = CommandOwnerScope::capture(&conn, None);
|
||||
let expected_document = conn
|
||||
.current_document_id_for_owner(&owner)
|
||||
.expect("loaded page should have a current Document");
|
||||
let expected_main_frame_slot = conn
|
||||
.page_agent_host_main_frame_slot_for_owner(&owner)
|
||||
.expect("loaded Page AgentHost should retain its main-frame slot");
|
||||
let expected_attachment = conn
|
||||
.current_renderer_agent_attachment_id_for_owner(&owner)
|
||||
.expect("loaded page should have a renderer attachment");
|
||||
|
||||
let evaluate =
|
||||
parsed_agent_host_command(20_001, "Runtime.evaluate", json!({"expression": "1 + 1"}));
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
let dispatch = conn.start_parsed_command_dispatch_with_context(&evaluate, &mut command_context);
|
||||
let AgentHostDispatchResult::FallThrough(dispatch) = dispatch else {
|
||||
panic!("Runtime.evaluate should fall through to the renderer");
|
||||
};
|
||||
assert_eq!(dispatch.lane(), RendererDispatchLane::Main);
|
||||
assert_eq!(
|
||||
dispatch.binding(),
|
||||
&RendererDispatchBinding::Page(RendererPageDispatchBinding {
|
||||
main_frame_slot: expected_main_frame_slot,
|
||||
document: expected_document,
|
||||
attachment: expected_attachment,
|
||||
})
|
||||
);
|
||||
let completed = dispatch.into_pending().wait().await;
|
||||
assert!(matches!(
|
||||
conn.complete_pending_command_dispatch_with_context(completed, &mut command_context)
|
||||
.await,
|
||||
AgentHostDispatchResult::Complete(_)
|
||||
));
|
||||
|
||||
let terminate = parsed_agent_host_command(20_002, "Runtime.terminateExecution", json!({}));
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
let dispatch =
|
||||
conn.start_parsed_command_dispatch_with_context(&terminate, &mut command_context);
|
||||
let AgentHostDispatchResult::FallThrough(dispatch) = dispatch else {
|
||||
panic!("Runtime.terminateExecution should fall through to renderer IO");
|
||||
};
|
||||
assert_eq!(dispatch.lane(), RendererDispatchLane::Io);
|
||||
assert_eq!(
|
||||
dispatch.binding(),
|
||||
&RendererDispatchBinding::Page(RendererPageDispatchBinding {
|
||||
main_frame_slot: expected_main_frame_slot,
|
||||
document: expected_document,
|
||||
attachment: expected_attachment,
|
||||
})
|
||||
);
|
||||
let completed = dispatch.into_pending().wait().await;
|
||||
assert!(matches!(
|
||||
conn.complete_pending_command_dispatch_with_context(completed, &mut command_context)
|
||||
.await,
|
||||
AgentHostDispatchResult::Complete(_)
|
||||
));
|
||||
|
||||
let screenshot = parsed_agent_host_command(20_003, "Page.captureScreenshot", json!({}));
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
let dispatch =
|
||||
conn.start_parsed_command_dispatch_with_context(&screenshot, &mut command_context);
|
||||
match dispatch {
|
||||
AgentHostDispatchResult::PendingService(pending) => {
|
||||
let completed = pending.wait().await;
|
||||
assert!(matches!(
|
||||
conn.complete_pending_command_dispatch_with_context(
|
||||
completed,
|
||||
&mut command_context,
|
||||
)
|
||||
.await,
|
||||
AgentHostDispatchResult::Complete(_)
|
||||
));
|
||||
}
|
||||
AgentHostDispatchResult::Complete(_) => {}
|
||||
AgentHostDispatchResult::FallThrough(_) => {
|
||||
panic!("native Page.captureScreenshot must not become renderer fallthrough")
|
||||
}
|
||||
}
|
||||
|
||||
let permission = parsed_agent_host_command(
|
||||
20_004,
|
||||
"Browser.setPermission",
|
||||
json!({
|
||||
"permission": {"name": "geolocation"},
|
||||
"setting": "denied"
|
||||
}),
|
||||
);
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
let dispatch =
|
||||
conn.start_parsed_command_dispatch_with_context(&permission, &mut command_context);
|
||||
let AgentHostDispatchResult::PendingService(pending) = dispatch else {
|
||||
panic!("live Browser.setPermission should be a service continuation");
|
||||
};
|
||||
let completed = pending.wait().await;
|
||||
assert!(matches!(
|
||||
conn.complete_pending_command_dispatch_with_context(completed, &mut command_context)
|
||||
.await,
|
||||
AgentHostDispatchResult::Complete(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn document_projection_gate_uses_handler_disposition_not_wire_method_lane() {
|
||||
let mut conn = loaded_agent_host_dispatch_connection_for_test().await;
|
||||
let owner = CommandOwnerScope::capture(&conn, None);
|
||||
let navigation = conn
|
||||
.start_document_navigation_for_owner(&owner, "LOADER-agent-host-gate".to_owned())
|
||||
.expect("cross-Document navigation should start");
|
||||
|
||||
for (id, method, params) in [
|
||||
(20_101, "Runtime.evaluate", json!({"expression": "1"})),
|
||||
(20_102, "Debugger.enable", json!({})),
|
||||
(20_103, "Console.enable", json!({})),
|
||||
(20_104, "Profiler.enable", json!({})),
|
||||
(20_105, "HeapProfiler.enable", json!({})),
|
||||
(20_106, "Accessibility.getFullAXTree", json!({})),
|
||||
(20_107, "CSS.enable", json!({})),
|
||||
(20_108, "DOM.getDocument", json!({})),
|
||||
(
|
||||
20_109,
|
||||
"DOMDebugger.getEventListeners",
|
||||
json!({"objectId": "1"}),
|
||||
),
|
||||
(20_110, "DOMSnapshot.captureSnapshot", json!({})),
|
||||
(20_111, "Page.getFrameTree", json!({})),
|
||||
(20_112, "Page.getLayoutMetrics", json!({})),
|
||||
] {
|
||||
let command = parsed_agent_host_command(id, method, params);
|
||||
assert!(
|
||||
conn.command_waits_for_document_projection(&command),
|
||||
"{method} must bind only after the replacement Document is projected"
|
||||
);
|
||||
}
|
||||
|
||||
for (id, method, params) in [
|
||||
(20_201, "Runtime.terminateExecution", json!({})),
|
||||
(
|
||||
20_202,
|
||||
"Runtime.addBinding",
|
||||
json!({"name": "duringNavigation"}),
|
||||
),
|
||||
(20_203, "Debugger.pause", json!({})),
|
||||
(20_204, "Performance.getMetrics", json!({})),
|
||||
(
|
||||
20_205,
|
||||
"Emulation.setScriptExecutionDisabled",
|
||||
json!({"value": true}),
|
||||
),
|
||||
(20_206, "Page.captureScreenshot", json!({})),
|
||||
(20_207, "Page.printToPDF", json!({})),
|
||||
(20_208, "Page.searchInResource", json!({})),
|
||||
(
|
||||
20_209,
|
||||
"Input.dispatchKeyEvent",
|
||||
json!({"type": "keyDown", "key": "a"}),
|
||||
),
|
||||
(20_210, "Browser.setPermission", json!({})),
|
||||
(20_211, "HeapProfiler.moliDiagnostics", json!({})),
|
||||
(20_212, "Accessibility.enable", json!({})),
|
||||
(20_213, "DOM.enable", json!({})),
|
||||
(20_214, "DOMSnapshot.enable", json!({})),
|
||||
] {
|
||||
let command = parsed_agent_host_command(id, method, params);
|
||||
assert!(
|
||||
!conn.command_waits_for_document_projection(&command),
|
||||
"{method} must remain service/native/IO-dispatchable during projection"
|
||||
);
|
||||
}
|
||||
|
||||
let _ = conn.finish_navigation_without_document_projection_for_owner(&owner, &navigation);
|
||||
conn.clear_pending_document_navigation_for_owner_if_matches(&owner, &navigation);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ use moli_core::page::{
|
||||
DevToolsSessionKey, RendererAgentAttachmentId, RendererRuntimeInspectorMessageBatch,
|
||||
};
|
||||
|
||||
use super::state::{FinishedRendererDocumentNavigation, RendererAgentAttachment};
|
||||
use super::state::{
|
||||
CommittedRendererDocumentBinding, DocumentProjectionFence, DocumentProjectionOutputRelease,
|
||||
RendererAgentAttachment,
|
||||
};
|
||||
use super::{CdpConnection, CommandOwnerScope, NavigationId};
|
||||
|
||||
impl CdpConnection {
|
||||
@@ -21,10 +24,17 @@ impl CdpConnection {
|
||||
&self,
|
||||
owner: &CommandOwnerScope,
|
||||
) -> Option<RendererAgentAttachmentId> {
|
||||
self.current_renderer_agent_attachment_for_owner(owner)
|
||||
.map(RendererAgentAttachment::id)
|
||||
}
|
||||
|
||||
pub(crate) fn current_renderer_agent_attachment_for_owner(
|
||||
&self,
|
||||
owner: &CommandOwnerScope,
|
||||
) -> Option<RendererAgentAttachment> {
|
||||
self.runtime_session_owner_slot_for_owner(owner)
|
||||
.ok()
|
||||
.and_then(|slot| slot.current_renderer_attachment())
|
||||
.map(RendererAgentAttachment::id)
|
||||
}
|
||||
|
||||
pub(crate) fn route_current_renderer_inspector_output_for_owner(
|
||||
@@ -116,11 +126,11 @@ impl CdpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish_renderer_document_navigation_for_owner(
|
||||
pub(crate) fn finish_navigation_without_document_projection_for_owner(
|
||||
&mut self,
|
||||
owner: &CommandOwnerScope,
|
||||
token: &NavigationId,
|
||||
) -> Option<FinishedRendererDocumentNavigation> {
|
||||
) -> Option<DocumentProjectionOutputRelease> {
|
||||
if self
|
||||
.validate_navigation_target_owner_for_scope(owner, token)
|
||||
.is_err()
|
||||
@@ -130,7 +140,7 @@ impl CdpConnection {
|
||||
match self
|
||||
.runtime_session_owner_slot_mut_for_owner(owner)
|
||||
.and_then(|slot| {
|
||||
slot.finish_renderer_document_navigation(token)
|
||||
slot.finish_navigation_without_document_projection(token)
|
||||
.map_err(|error| error.to_string())
|
||||
}) {
|
||||
Ok(finish) => Some(finish),
|
||||
@@ -146,6 +156,40 @@ impl CdpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn publish_document_projection_fence_for_owner(
|
||||
&mut self,
|
||||
owner: &CommandOwnerScope,
|
||||
projected: &CommittedRendererDocumentBinding,
|
||||
fence: DocumentProjectionFence,
|
||||
) -> DocumentProjectionOutputRelease {
|
||||
let current_attachment = self
|
||||
.current_renderer_agent_attachment_for_owner(owner)
|
||||
.expect("a rebound Document projection must retain its renderer attachment");
|
||||
assert_eq!(
|
||||
(
|
||||
projected.document_id,
|
||||
projected.browser_sequence,
|
||||
current_attachment.id(),
|
||||
current_attachment.document(),
|
||||
current_attachment.browser_sequence(),
|
||||
),
|
||||
(
|
||||
fence.document(),
|
||||
fence.browser_sequence(),
|
||||
fence.renderer_attachment(),
|
||||
fence.document(),
|
||||
fence.browser_sequence(),
|
||||
),
|
||||
"a Document projection fence requires its exact frame and renderer binding"
|
||||
);
|
||||
self.runtime_session_owner_slot_mut_for_owner(owner)
|
||||
.and_then(|slot| {
|
||||
slot.publish_document_projection_fence(fence)
|
||||
.map_err(|error| error.to_string())
|
||||
})
|
||||
.expect("an exact Document projection fence must publish once")
|
||||
}
|
||||
|
||||
fn filter_renderer_inspector_batches_for_target_owner(
|
||||
&self,
|
||||
owner: &CommandOwnerScope,
|
||||
@@ -389,11 +433,11 @@ mod tests {
|
||||
assert!(!conn.clear_pending_document_navigation_for_owner_if_matches(&owner, &first));
|
||||
assert!(conn.accepts_pending_document_navigation_for_owner(&owner, &second));
|
||||
assert!(
|
||||
conn.finish_renderer_document_navigation_for_owner(&owner, &first)
|
||||
conn.finish_navigation_without_document_projection_for_owner(&owner, &first)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
conn.finish_renderer_document_navigation_for_owner(&owner, &second)
|
||||
conn.finish_navigation_without_document_projection_for_owner(&owner, &second)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{CdpConnection, CdpTurnOutcome};
|
||||
use crate::domains::activity::{
|
||||
ReadyProtocolSchedulerWork, RuntimeCommandOutputBarrierCompletion,
|
||||
RuntimeCommandOutputBarrierPermit, RuntimeCommandOutputBarriers,
|
||||
ReadyProtocolSchedulerWork, RendererCommandResponseCompletion, RendererCommandResponseOrder,
|
||||
RendererCommandResponsePermit,
|
||||
};
|
||||
use moli_core::RendererOutputTransportMessage;
|
||||
|
||||
@@ -254,13 +254,13 @@ impl CdpConnection {
|
||||
pub async fn ingest_renderer_output_turn_async(
|
||||
&mut self,
|
||||
publication: RendererOutputTransportMessage,
|
||||
barriers: &mut RuntimeCommandOutputBarriers,
|
||||
order: &mut RendererCommandResponseOrder,
|
||||
) -> CdpTurnOutcome {
|
||||
let mut command_context = super::CommandDispatchContext::default();
|
||||
let protocol_events = crate::domains::activity::ingest_renderer_output_transport_async(
|
||||
self,
|
||||
publication,
|
||||
barriers,
|
||||
order,
|
||||
&mut command_context,
|
||||
)
|
||||
.await;
|
||||
@@ -290,14 +290,14 @@ impl CdpConnection {
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn release_runtime_command_output_barrier_turn_async(
|
||||
pub async fn release_renderer_command_response_permit_turn_async(
|
||||
&mut self,
|
||||
barriers: &mut RuntimeCommandOutputBarriers,
|
||||
permit: RuntimeCommandOutputBarrierPermit,
|
||||
) -> RuntimeCommandOutputBarrierCompletion {
|
||||
order: &mut RendererCommandResponseOrder,
|
||||
permit: RendererCommandResponsePermit,
|
||||
) -> RendererCommandResponseCompletion {
|
||||
let mut command_context = super::CommandDispatchContext::default();
|
||||
let terminal = barriers.release(self, permit, &mut command_context).await;
|
||||
RuntimeCommandOutputBarrierCompletion::new(
|
||||
let terminal = order.release(self, permit, &mut command_context).await;
|
||||
RendererCommandResponseCompletion::new(
|
||||
terminal,
|
||||
CdpTurnOutcome::new_with_protocol_and_post_response_events(
|
||||
command_context.take_protocol_events(),
|
||||
@@ -307,14 +307,14 @@ impl CdpConnection {
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn cancel_runtime_command_output_barrier_turn_async(
|
||||
pub async fn cancel_renderer_command_response_permit_turn_async(
|
||||
&mut self,
|
||||
barriers: &mut RuntimeCommandOutputBarriers,
|
||||
permit: RuntimeCommandOutputBarrierPermit,
|
||||
) -> RuntimeCommandOutputBarrierCompletion {
|
||||
order: &mut RendererCommandResponseOrder,
|
||||
permit: RendererCommandResponsePermit,
|
||||
) -> RendererCommandResponseCompletion {
|
||||
let mut command_context = super::CommandDispatchContext::default();
|
||||
let terminal = barriers.cancel(self, permit, &mut command_context).await;
|
||||
RuntimeCommandOutputBarrierCompletion::new(
|
||||
let terminal = order.cancel(self, permit, &mut command_context).await;
|
||||
RendererCommandResponseCompletion::new(
|
||||
terminal,
|
||||
CdpTurnOutcome::new_with_protocol_and_post_response_events(
|
||||
command_context.take_protocol_events(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{collections::hash_map::Entry, future::Future, pin::Pin};
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
use moli_page_types::{FrontendCommandId, RendererCallId, RendererInspectorResponseDelivery};
|
||||
use moli_protocol_cdp::CdpRendererCommandReplayDispatch;
|
||||
@@ -242,41 +242,6 @@ pub(crate) struct OwnerRuntimeResponse {
|
||||
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 {
|
||||
@@ -583,6 +548,10 @@ fn collect_moli_diagnostics_pending_snapshots(
|
||||
}
|
||||
|
||||
impl PendingRuntimeProtocolMessageDispatch {
|
||||
pub(crate) fn renderer_route(&self) -> moli_core::page::RendererInspectorCommandRoute {
|
||||
self.pending.renderer_route()
|
||||
}
|
||||
|
||||
pub async fn wait(self) -> Result<CompletedRuntimeProtocolMessageDispatch, String> {
|
||||
let completion = self
|
||||
.pending
|
||||
@@ -798,6 +767,33 @@ fn push_pending_inspector_await_error_background_event(
|
||||
));
|
||||
}
|
||||
|
||||
fn push_drained_pending_inspector_await_error(
|
||||
direct_events: &mut Vec<BackgroundProtocolEvent>,
|
||||
claimed_events: &mut Vec<BackgroundProtocolEvent>,
|
||||
cdp_id: u64,
|
||||
owner: &CommandOwnerScope,
|
||||
entry: &PendingInspectorAwait,
|
||||
reason: &'static str,
|
||||
) {
|
||||
if entry.scheduler_deferred_reply_claimed() {
|
||||
let mut response =
|
||||
RuntimeInspectorResponseReady::for_owner(cdp_id, owner, Err(reason.to_owned()));
|
||||
if let Some(correlation) = entry.renderer_correlation() {
|
||||
response.bind_renderer_call_id(correlation.renderer_call_id());
|
||||
}
|
||||
claimed_events.push(BackgroundProtocolEvent::runtime_inspector_response_ready(
|
||||
response,
|
||||
));
|
||||
return;
|
||||
}
|
||||
push_pending_inspector_await_error_background_event(
|
||||
direct_events,
|
||||
cdp_id,
|
||||
entry.session_id(),
|
||||
reason,
|
||||
);
|
||||
}
|
||||
|
||||
fn push_terminated_renderer_call_error_background_events(
|
||||
out: &mut Vec<BackgroundProtocolEvent>,
|
||||
terminated: Vec<RendererCommandCorrelation>,
|
||||
@@ -1392,35 +1388,22 @@ impl CdpConnection {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn register_runtime_await_job_for_owner(
|
||||
pub(crate) fn trace_runtime_await_started(
|
||||
&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",
|
||||
"runtime_await_started",
|
||||
Some(cdp_request_id),
|
||||
session_id,
|
||||
trace_fields,
|
||||
owner.session_id(),
|
||||
json!({
|
||||
"ownerRoute": owner.explicit_route().map(|route| format!("{route:?}")),
|
||||
"objectGroup": object_group,
|
||||
"action": action,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1433,7 +1416,7 @@ impl CdpConnection {
|
||||
"runtime_await_pending_registered",
|
||||
Some(cdp_request_id),
|
||||
session_id,
|
||||
self.runtime_await_job_trace_fields(cdp_request_id, session_id),
|
||||
json!({}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1451,66 +1434,37 @@ impl CdpConnection {
|
||||
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(
|
||||
pub(crate) fn trace_runtime_await_completed(
|
||||
&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,
|
||||
session_id,
|
||||
json!({}),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_runtime_await_job(
|
||||
pub(crate) fn trace_runtime_await_cancelled(
|
||||
&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,
|
||||
session_id,
|
||||
json!({ "reason": reason }),
|
||||
);
|
||||
}
|
||||
|
||||
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>,
|
||||
@@ -1623,8 +1577,12 @@ impl CdpConnection {
|
||||
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);
|
||||
if self
|
||||
.remove_pending_inspector_await_for_cancellation(cdp_request_id, session_id)
|
||||
.is_some()
|
||||
{
|
||||
self.trace_runtime_await_cancelled(cdp_request_id, session_id, "forgotten");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn forget_pending_inspector_await_for_owner(
|
||||
@@ -1632,9 +1590,12 @@ impl CdpConnection {
|
||||
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);
|
||||
if self
|
||||
.remove_pending_inspector_await_for_cancellation_for_owner(cdp_request_id, owner)
|
||||
.is_some()
|
||||
{
|
||||
self.trace_runtime_await_cancelled(cdp_request_id, owner.session_id(), "forgotten");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn claim_pending_inspector_await_for_scheduler_deferred_reply(
|
||||
@@ -1642,100 +1603,11 @@ impl CdpConnection {
|
||||
cdp_request_id: u64,
|
||||
owner: &CommandOwnerScope,
|
||||
) -> Option<ClaimedPendingInspectorAwait> {
|
||||
let claimed = self
|
||||
.remove_pending_inspector_await_for_owner(cdp_request_id, owner)
|
||||
.map(|entry| ClaimedPendingInspectorAwait {
|
||||
self.claim_pending_inspector_await_for_owner(cdp_request_id, owner)
|
||||
.then(|| 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)]
|
||||
@@ -1743,9 +1615,18 @@ impl CdpConnection {
|
||||
&self,
|
||||
session_id: Option<&str>,
|
||||
) -> bool {
|
||||
self.claimed_pending_inspector_await_owners
|
||||
.values()
|
||||
.any(|owner| owner.matches_session_owner(session_id))
|
||||
if let Some(owner_session_id) = session_id
|
||||
&& let Some(target) = self.shared_worker_target_for_session(session_id)
|
||||
{
|
||||
return target.has_claimed_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_claimed_pending_inspector_awaits_for_session(owner_session_id);
|
||||
}
|
||||
self.target_devtools_session_state_for_session(session_id)
|
||||
.is_some_and(DevToolsSessionState::has_claimed_pending_inspector_awaits)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1756,15 +1637,15 @@ impl CdpConnection {
|
||||
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);
|
||||
return target.has_unclaimed_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);
|
||||
return target.has_unclaimed_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)
|
||||
.is_some_and(DevToolsSessionState::has_unclaimed_pending_inspector_awaits)
|
||||
}
|
||||
|
||||
pub(crate) fn complete_claimed_pending_inspector_await_for_scheduler_deferred_reply(
|
||||
@@ -1775,14 +1656,12 @@ impl CdpConnection {
|
||||
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());
|
||||
let ClaimedPendingInspectorAwait { command_id, owner } = claimed;
|
||||
let Some(entry) = self.take_claimed_pending_inspector_await_for_owner(command_id, &owner)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.trace_runtime_await_completed(command_id, owner.session_id());
|
||||
self.apply_completed_pending_inspector_await_entry(&owner, entry, protocol_events);
|
||||
}
|
||||
|
||||
@@ -1794,14 +1673,12 @@ impl CdpConnection {
|
||||
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);
|
||||
let ClaimedPendingInspectorAwait { command_id, owner } = claimed;
|
||||
let Some(entry) = self.take_claimed_pending_inspector_await_for_owner(command_id, &owner)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.trace_runtime_await_cancelled(command_id, owner.session_id(), reason);
|
||||
if let Some(correlation) = entry.renderer_correlation() {
|
||||
let _ = self.take_renderer_call_for_frontend_if_matches_for_owner(
|
||||
&owner,
|
||||
@@ -1896,6 +1773,46 @@ impl CdpConnection {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn claim_pending_inspector_await_for_owner(
|
||||
&mut self,
|
||||
cdp_request_id: u64,
|
||||
owner: &CommandOwnerScope,
|
||||
) -> bool {
|
||||
if let Some(owner_session_id) = owner.session_id() {
|
||||
if let Some(target) = self.shared_worker_target_for_session_mut(owner.session_id()) {
|
||||
return target.claim_pending_inspector_await(owner_session_id, cdp_request_id);
|
||||
}
|
||||
if let Some(target) = self.service_worker_target_for_session_mut(owner.session_id()) {
|
||||
return target.claim_pending_inspector_await(owner_session_id, cdp_request_id);
|
||||
}
|
||||
}
|
||||
self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
|
||||
state.claim_pending_inspector_await(cdp_request_id)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn take_claimed_pending_inspector_await_for_owner(
|
||||
&mut self,
|
||||
cdp_request_id: u64,
|
||||
owner: &CommandOwnerScope,
|
||||
) -> Option<PendingInspectorAwait> {
|
||||
if let Some(owner_session_id) = owner.session_id() {
|
||||
if let Some(target) = self.shared_worker_target_for_session_mut(owner.session_id()) {
|
||||
return target
|
||||
.take_claimed_pending_inspector_await(owner_session_id, cdp_request_id);
|
||||
}
|
||||
if let Some(target) = self.service_worker_target_for_session_mut(owner.session_id()) {
|
||||
return target
|
||||
.take_claimed_pending_inspector_await(owner_session_id, cdp_request_id);
|
||||
}
|
||||
}
|
||||
self.with_target_devtools_session_state_for_owner_mut(owner, |state| {
|
||||
state.take_claimed_pending_inspector_await(cdp_request_id)
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn remove_pending_inspector_await_for_cancellation(
|
||||
&mut self,
|
||||
cdp_request_id: u64,
|
||||
@@ -1950,9 +1867,6 @@ impl CdpConnection {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1974,13 +1888,6 @@ impl CdpConnection {
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -1997,6 +1904,7 @@ impl CdpConnection {
|
||||
|
||||
pub(crate) fn fail_pending_inspector_awaits_from_shared_worker_target_session_background_events_into(
|
||||
out: &mut Vec<BackgroundProtocolEvent>,
|
||||
claimed_events: &mut Vec<BackgroundProtocolEvent>,
|
||||
target: &mut SharedWorkerTargetState,
|
||||
owner_session_id: &str,
|
||||
reason: &'static str,
|
||||
@@ -2010,10 +1918,12 @@ impl CdpConnection {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
push_pending_inspector_await_error_background_event(
|
||||
push_drained_pending_inspector_await_error(
|
||||
out,
|
||||
claimed_events,
|
||||
cdp_id,
|
||||
entry.session_id(),
|
||||
&CommandOwnerScope::for_session(owner_session_id),
|
||||
&entry,
|
||||
reason,
|
||||
);
|
||||
}
|
||||
@@ -2029,6 +1939,7 @@ impl CdpConnection {
|
||||
|
||||
pub(crate) fn fail_pending_inspector_awaits_from_service_worker_target_state_background_events_into(
|
||||
out: &mut Vec<BackgroundProtocolEvent>,
|
||||
claimed_events: &mut Vec<BackgroundProtocolEvent>,
|
||||
target: &mut ServiceWorkerTargetState,
|
||||
reason: &'static str,
|
||||
) {
|
||||
@@ -2042,10 +1953,17 @@ impl CdpConnection {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
push_pending_inspector_await_error_background_event(
|
||||
let owner = CommandOwnerScope::for_session(
|
||||
entry
|
||||
.session_id()
|
||||
.expect("service-worker await must belong to an attached session"),
|
||||
);
|
||||
push_drained_pending_inspector_await_error(
|
||||
out,
|
||||
claimed_events,
|
||||
cdp_id,
|
||||
entry.session_id(),
|
||||
&owner,
|
||||
&entry,
|
||||
reason,
|
||||
);
|
||||
}
|
||||
@@ -2066,12 +1984,9 @@ impl CdpConnection {
|
||||
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,
|
||||
);
|
||||
let owner = session_id
|
||||
.map(CommandOwnerScope::for_session)
|
||||
.unwrap_or_else(|| CommandOwnerScope::capture(self, None));
|
||||
if let Some(owner_session_id) = session_id
|
||||
&& self.shared_worker_target_for_session(session_id).is_some()
|
||||
{
|
||||
@@ -2081,7 +1996,7 @@ impl CdpConnection {
|
||||
.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);
|
||||
self.trace_runtime_await_cancelled(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),
|
||||
@@ -2089,10 +2004,12 @@ impl CdpConnection {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
push_pending_inspector_await_error_background_event(
|
||||
push_drained_pending_inspector_await_error(
|
||||
out,
|
||||
claimed_background_events,
|
||||
cdp_id,
|
||||
entry.session_id(),
|
||||
&owner,
|
||||
&entry,
|
||||
reason,
|
||||
);
|
||||
}
|
||||
@@ -2125,7 +2042,7 @@ impl CdpConnection {
|
||||
.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);
|
||||
self.trace_runtime_await_cancelled(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),
|
||||
@@ -2133,10 +2050,12 @@ impl CdpConnection {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
push_pending_inspector_await_error_background_event(
|
||||
push_drained_pending_inspector_await_error(
|
||||
out,
|
||||
claimed_background_events,
|
||||
cdp_id,
|
||||
entry.session_id(),
|
||||
&owner,
|
||||
&entry,
|
||||
reason,
|
||||
);
|
||||
}
|
||||
@@ -2166,7 +2085,7 @@ impl CdpConnection {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
for (cdp_id, entry) in drained {
|
||||
self.cancel_runtime_await_job(cdp_id, entry.session_id(), reason);
|
||||
self.trace_runtime_await_cancelled(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(),
|
||||
@@ -2174,10 +2093,12 @@ impl CdpConnection {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
push_pending_inspector_await_error_background_event(
|
||||
push_drained_pending_inspector_await_error(
|
||||
out,
|
||||
claimed_background_events,
|
||||
cdp_id,
|
||||
entry.session_id(),
|
||||
&owner,
|
||||
&entry,
|
||||
reason,
|
||||
);
|
||||
}
|
||||
@@ -2206,19 +2127,13 @@ impl CdpConnection {
|
||||
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);
|
||||
self.trace_runtime_await_cancelled(cdp_id, entry.session_id(), reason);
|
||||
if let Some(listener) = entry.bidi_channel_listener() {
|
||||
self.unregister_runtime_remote_object_group_for_owner(
|
||||
owner,
|
||||
@@ -2226,10 +2141,12 @@ impl CdpConnection {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
push_pending_inspector_await_error_background_event(
|
||||
push_drained_pending_inspector_await_error(
|
||||
out,
|
||||
claimed_background_events,
|
||||
cdp_id,
|
||||
entry.session_id(),
|
||||
owner,
|
||||
&entry,
|
||||
reason,
|
||||
);
|
||||
}
|
||||
@@ -3740,8 +3657,12 @@ impl CdpConnection {
|
||||
);
|
||||
}
|
||||
}
|
||||
self.complete_runtime_await_job(frontend_command_id, session_id.as_deref());
|
||||
let _ = self.remove_pending_inspector_await_for_owner(frontend_command_id, owner);
|
||||
if self
|
||||
.remove_pending_inspector_await_for_owner(frontend_command_id, owner)
|
||||
.is_some()
|
||||
{
|
||||
self.trace_runtime_await_completed(frontend_command_id, session_id.as_deref());
|
||||
}
|
||||
true
|
||||
});
|
||||
}
|
||||
@@ -3842,7 +3763,7 @@ impl CdpConnection {
|
||||
background_events: &mut Vec<BackgroundProtocolEvent>,
|
||||
) -> bool {
|
||||
let command_id = response.command_id;
|
||||
self.complete_runtime_await_job(command_id, response.session_id());
|
||||
self.trace_runtime_await_completed(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) {
|
||||
@@ -5597,8 +5518,12 @@ impl CdpConnection {
|
||||
};
|
||||
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);
|
||||
if self
|
||||
.remove_pending_inspector_await(frontend_command_id, frontend_session_id)
|
||||
.is_some()
|
||||
{
|
||||
self.trace_runtime_await_completed(frontend_command_id, frontend_session_id);
|
||||
}
|
||||
let mut response = json!({
|
||||
"id": frontend_command_id,
|
||||
"error": {
|
||||
@@ -8071,10 +7996,6 @@ mod tests {
|
||||
let descriptor = if id == 71 {
|
||||
devtools_session_renderer_command_descriptor_for_test(id)
|
||||
} else {
|
||||
assert_eq!(
|
||||
frontend.renderer_lane(),
|
||||
Some(moli_protocol_cdp::CdpRendererDispatchLane::Io)
|
||||
);
|
||||
RendererCommandDescriptor::set_script_execution_disabled(
|
||||
payload.clone(),
|
||||
frontend.renderer_policy(),
|
||||
@@ -9180,20 +9101,19 @@ mod tests {
|
||||
|
||||
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(
|
||||
conn.trace_runtime_await_started(
|
||||
1,
|
||||
&CommandOwnerScope::for_session("SID-active"),
|
||||
None,
|
||||
"evaluate",
|
||||
);
|
||||
conn.register_runtime_await_job_for_owner(
|
||||
conn.trace_runtime_await_started(
|
||||
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,
|
||||
@@ -9210,11 +9130,6 @@ mod tests {
|
||||
);
|
||||
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();
|
||||
@@ -9232,7 +9147,6 @@ mod tests {
|
||||
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!({
|
||||
@@ -9253,7 +9167,53 @@ mod tests {
|
||||
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 session_detach_settles_claimed_await_before_late_scheduler_completion() {
|
||||
let mut conn = CdpConnection::default();
|
||||
let mut browser_context = BrowserContext::new("BID-claimed-detach".to_owned());
|
||||
browser_context.set_active_target_id("TID-claimed-detach".to_owned());
|
||||
browser_context.attach_active_session("SID-claimed-detach".to_owned());
|
||||
conn.install_browser_context_fixture_for_test(browser_context);
|
||||
let owner = CommandOwnerScope::for_session("SID-claimed-detach");
|
||||
conn.try_register_pending_inspector_await_with_object_group_for_owner(
|
||||
3,
|
||||
&owner,
|
||||
Some("claimed-group"),
|
||||
)
|
||||
.unwrap();
|
||||
let claimed = conn
|
||||
.claim_pending_inspector_await_for_scheduler_deferred_reply(3, &owner)
|
||||
.expect("registered await should be claimable");
|
||||
|
||||
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-claimed-detach"),
|
||||
"Target detached",
|
||||
);
|
||||
|
||||
assert!(direct_events.is_empty());
|
||||
assert_eq!(claimed_events.len(), 1);
|
||||
assert!(!conn.has_pending_inspector_awaits_for_session_owner(Some("SID-claimed-detach")));
|
||||
conn.complete_claimed_pending_inspector_await_for_scheduler_deferred_reply(
|
||||
Some(claimed),
|
||||
&[BackgroundProtocolEvent::command_success(
|
||||
Some(3),
|
||||
Some("SID-claimed-detach"),
|
||||
json!({ "result": { "objectId": "late-object" } }),
|
||||
)],
|
||||
);
|
||||
assert!(
|
||||
!conn.runtime_remote_object_id_known_for_session_owner(
|
||||
Some("SID-claimed-detach"),
|
||||
"late-object",
|
||||
),
|
||||
"a late scheduler completion must not mutate a detached session"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -9472,7 +9432,7 @@ mod tests {
|
||||
Some("runtime-group"),
|
||||
)
|
||||
.unwrap();
|
||||
conn.register_runtime_await_job_for_owner(
|
||||
conn.trace_runtime_await_started(
|
||||
77,
|
||||
&CommandOwnerScope::for_session("SID-bg"),
|
||||
Some("runtime-group"),
|
||||
|
||||
@@ -33,8 +33,8 @@ use super::{
|
||||
EmulatedDeviceMetrics, EmulatedGeolocationOverrideState, EmulatedNetworkConditions,
|
||||
},
|
||||
javascript_dialog::TargetPreparedJavaScriptDialog,
|
||||
page_agent_host::{PageAgentHost, PageAgentHostRegistry},
|
||||
page_slot::DocumentStartScript,
|
||||
page_target_host::{PageTargetHost, PageTargetRegistry},
|
||||
service_worker_target::ServiceWorkerTargetState,
|
||||
shared_worker_target::SharedWorkerTargetState,
|
||||
web_contents::WebContents,
|
||||
@@ -68,7 +68,7 @@ pub(crate) use storage_partition::{OriginStorageUsage, SiteDataClearOptions};
|
||||
/// This migration wrapper is removed at Commits 24b/30.
|
||||
pub struct BrowserContext {
|
||||
pub id: String,
|
||||
pub(crate) page_targets: PageTargetRegistry,
|
||||
pub(crate) page_targets: PageAgentHostRegistry,
|
||||
/// Test-only cookie overrides, inherited by the first page fixture.
|
||||
#[cfg(test)]
|
||||
pub(crate) default_document_cookie_manager_surface: BrowserContextCookieManagerSurface,
|
||||
@@ -286,13 +286,13 @@ impl BrowserContext {
|
||||
self.physical.id
|
||||
}
|
||||
|
||||
pub(crate) fn active_page_target(&self) -> &PageTargetHost {
|
||||
pub(crate) fn active_page_target(&self) -> &PageAgentHost {
|
||||
self.page_targets
|
||||
.active(self.physical.selected_web_contents_id())
|
||||
.expect("BrowserContext has no active page target")
|
||||
}
|
||||
|
||||
pub(crate) fn active_page_target_mut(&mut self) -> &mut PageTargetHost {
|
||||
pub(crate) fn active_page_target_mut(&mut self) -> &mut PageAgentHost {
|
||||
self.page_targets
|
||||
.active_mut(self.physical.selected_web_contents_id())
|
||||
.expect("BrowserContext has no active page target")
|
||||
@@ -425,7 +425,7 @@ impl BrowserContext {
|
||||
|
||||
Self {
|
||||
id,
|
||||
page_targets: PageTargetRegistry::default(),
|
||||
page_targets: PageAgentHostRegistry::default(),
|
||||
#[cfg(test)]
|
||||
default_document_cookie_manager_surface: BrowserContextCookieManagerSurface::default(),
|
||||
target_popup_ids: HashMap::new(),
|
||||
@@ -820,7 +820,7 @@ impl BrowserContext {
|
||||
})
|
||||
}
|
||||
|
||||
fn target_owner_diagnostics(&self, target: &PageTargetHost) -> Value {
|
||||
fn target_owner_diagnostics(&self, target: &PageAgentHost) -> Value {
|
||||
let navigation = self
|
||||
.web_contents_for_target(target.target_id())
|
||||
.expect("live WebContents")
|
||||
@@ -829,7 +829,7 @@ impl BrowserContext {
|
||||
let creator = document.creator().map(|creator| json!({
|
||||
"targetId": self.page_targets.iter()
|
||||
.find(|target| target.web_contents_id() == creator.web_contents_id())
|
||||
.map(PageTargetHost::target_id),
|
||||
.map(PageAgentHost::target_id),
|
||||
"securityOrigin": creator.security_origin(),
|
||||
"secureContextType": creator.secure_context_type(),
|
||||
}));
|
||||
@@ -1314,7 +1314,7 @@ impl BrowserContext {
|
||||
pub(crate) fn active_target_id(&self) -> Option<&str> {
|
||||
self.page_targets
|
||||
.get_for_web_contents(self.physical.selected_web_contents_id()?)
|
||||
.map(PageTargetHost::target_id)
|
||||
.map(PageAgentHost::target_id)
|
||||
}
|
||||
|
||||
pub(crate) fn active_target_id_owned(&self) -> Option<String> {
|
||||
@@ -1336,7 +1336,7 @@ impl BrowserContext {
|
||||
pub(crate) fn reported_active_user_agent_override(&self) -> Option<&str> {
|
||||
self.page_targets
|
||||
.active(self.physical.selected_web_contents_id())
|
||||
.and_then(PageTargetHost::reported_user_agent_override)
|
||||
.and_then(PageAgentHost::reported_user_agent_override)
|
||||
.or_else(|| {
|
||||
self.default_browser_identity_override()
|
||||
.map(moli_browser_profile::BrowserIdentityProfile::user_agent)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::BrowserContext;
|
||||
use crate::conn::state::{
|
||||
PageTargetHost, TargetIdentityState,
|
||||
PageAgentHost, TargetIdentityState,
|
||||
page_slot::TargetPageSlot,
|
||||
web_contents::{
|
||||
EmulationPolicy, EmulationPolicyChange, WebContents, WindowSurface, WindowSurfaceState,
|
||||
@@ -81,7 +81,7 @@ impl BrowserContext {
|
||||
return false;
|
||||
}
|
||||
let id = contents.id();
|
||||
let mut projection = PageTargetHost::new(
|
||||
let mut projection = PageAgentHost::new(
|
||||
target_id,
|
||||
primary_session_id,
|
||||
identity,
|
||||
@@ -118,7 +118,7 @@ impl BrowserContext {
|
||||
let Some(id) = self
|
||||
.page_targets
|
||||
.get(target_id)
|
||||
.map(PageTargetHost::web_contents_id)
|
||||
.map(PageAgentHost::web_contents_id)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -198,7 +198,7 @@ async fn close_retires_projection_waiters_and_channel_before_the_owned_page_tear
|
||||
assert!(matches!(
|
||||
projection
|
||||
.runtime_slot
|
||||
.finish_renderer_document_navigation(&NavigationId::allocate()),
|
||||
.finish_navigation_without_document_projection(&NavigationId::allocate()),
|
||||
Err(DevToolsRendererChannelError::Closed)
|
||||
));
|
||||
assert_eq!(
|
||||
|
||||
@@ -3,8 +3,8 @@ use super::BrowserContext;
|
||||
use moli_core::page::RendererLifecycleEventStamp;
|
||||
use moli_core::{
|
||||
browser::{
|
||||
DocumentId, DocumentLifecycle, DocumentLifetime, DocumentLifetimeObserver, NavigationId,
|
||||
RendererPageResidenceIdentity,
|
||||
BrowserSequence, DocumentId, DocumentLifecycle, DocumentLifetime, DocumentLifetimeObserver,
|
||||
NavigationId, RendererPageResidenceIdentity,
|
||||
},
|
||||
page::{
|
||||
Page, RendererDocumentLifecycleEvent, RendererDocumentLifecycleEventKind,
|
||||
@@ -60,6 +60,7 @@ pub(crate) struct CommittedRendererDocumentBinding {
|
||||
pub(crate) frame_id: String,
|
||||
pub(crate) loader_id: String,
|
||||
pub(crate) document_id: DocumentId,
|
||||
pub(crate) browser_sequence: BrowserSequence,
|
||||
pub(crate) document_open_replacement_epoch: Option<RendererLifecycleEpoch>,
|
||||
}
|
||||
|
||||
@@ -980,7 +981,7 @@ impl BrowserContext {
|
||||
.get_mut(target_id)
|
||||
.expect("registered Target projection")
|
||||
.runtime_slot
|
||||
.start_renderer_document_navigation(token);
|
||||
.begin_document_projection(token);
|
||||
token
|
||||
}
|
||||
|
||||
@@ -1278,10 +1279,16 @@ impl BrowserContext {
|
||||
return Vec::new();
|
||||
};
|
||||
self.install_document_lifecycle_for_test(target_id, lifecycle);
|
||||
let browser_sequence = self
|
||||
.renderer_document_lifecycle_binding_for_target(target_id)
|
||||
.filter(|binding| binding.document_id == document)
|
||||
.map(|binding| binding.browser_sequence)
|
||||
.unwrap_or_else(BrowserSequence::allocate);
|
||||
self.project_committed_document_lifecycle_for_target(
|
||||
target_id,
|
||||
crate::conn::CommittedDocumentLifecycle {
|
||||
document,
|
||||
browser_sequence,
|
||||
artifacts,
|
||||
},
|
||||
navigation,
|
||||
@@ -1300,6 +1307,7 @@ impl BrowserContext {
|
||||
) -> Vec<RendererDocumentLifecycleEvent> {
|
||||
let crate::conn::CommittedDocumentLifecycle {
|
||||
document,
|
||||
browser_sequence,
|
||||
artifacts,
|
||||
} = lifecycle;
|
||||
if self.target_document_id(target_id) != Some(document) {
|
||||
@@ -1337,6 +1345,7 @@ impl BrowserContext {
|
||||
frame_id,
|
||||
loader_id,
|
||||
document_id,
|
||||
browser_sequence,
|
||||
document_open_replacement_epoch: None,
|
||||
};
|
||||
if self
|
||||
|
||||
@@ -250,11 +250,13 @@ async fn creation_projection_cannot_rewind_native_progress_or_retarget_a_replace
|
||||
let native = owner.renderer_document_lifecycle_authoritative_snapshot_for_target(TARGET);
|
||||
// Projection is delayed until after the Browser has accepted more progress.
|
||||
// Replaying/rebinding the creation occurrence may only affect visibility.
|
||||
let browser_sequence = moli_core::browser::BrowserSequence::allocate();
|
||||
for _ in 0..2 {
|
||||
let projected = owner.project_committed_document_lifecycle_for_target(
|
||||
TARGET,
|
||||
CommittedDocumentLifecycle {
|
||||
document,
|
||||
browser_sequence,
|
||||
artifacts: artifacts.clone(),
|
||||
},
|
||||
Some(navigation),
|
||||
@@ -669,8 +671,10 @@ async fn document_replacement_dismisses_dialog_without_protocol_session_cleanup(
|
||||
async fn browser_drop_dismisses_dialog_even_when_session_snapshot_survives() {
|
||||
let browser = Browser::new(BrowserConfig::default()).unwrap();
|
||||
let (mut owner, completion) = page_with_installed_dialog_for_test(&browser).await;
|
||||
let snapshot = owner.active_page_target().devtools_sessions
|
||||
let dialog_projection_snapshot = owner.active_page_target().devtools_sessions
|
||||
[moli_page_types::DevToolsSessionKey::Primary]
|
||||
.page_session_state
|
||||
.javascript_dialog_state
|
||||
.clone();
|
||||
let id = owner.selected_web_contents_id().unwrap();
|
||||
drop(owner.page_targets.remove(TARGET).unwrap());
|
||||
@@ -681,10 +685,10 @@ async fn browser_drop_dismisses_dialog_even_when_session_snapshot_survives() {
|
||||
|
||||
assert!(
|
||||
!completion.finish(true, "late reply".into()),
|
||||
"Browser drop must dismiss the dialog even if a cloned session projection survives"
|
||||
"Browser drop must dismiss the dialog even if a dialog projection snapshot survives"
|
||||
);
|
||||
assert!(!completion.wait().accepted);
|
||||
drop(snapshot);
|
||||
drop(dialog_projection_snapshot);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#[cfg(test)]
|
||||
use crate::conn::fetch_support::OpenBodyStreamError;
|
||||
use crate::conn::{BrowserContext, ConnectionNetworkRequestIdAllocator, PageTargetHost};
|
||||
use crate::conn::{BrowserContext, ConnectionNetworkRequestIdAllocator, PageAgentHost};
|
||||
#[cfg(test)]
|
||||
use crate::conn::{
|
||||
DocumentBodySource, NavigationDispatchState, NavigationId, PausedDocumentTransfer,
|
||||
@@ -12,29 +12,29 @@ fn document_navigation_loader_id(sequence: u64) -> String {
|
||||
}
|
||||
|
||||
impl BrowserContext {
|
||||
pub(crate) fn page_target(&self, target_id: &str) -> Option<&PageTargetHost> {
|
||||
pub(crate) fn page_target(&self, target_id: &str) -> Option<&PageAgentHost> {
|
||||
self.page_targets.get(target_id)
|
||||
}
|
||||
|
||||
pub(crate) fn page_target_mut(&mut self, target_id: &str) -> Option<&mut PageTargetHost> {
|
||||
pub(crate) fn page_target_mut(&mut self, target_id: &str) -> Option<&mut PageAgentHost> {
|
||||
self.page_targets.get_mut(target_id)
|
||||
}
|
||||
|
||||
pub fn background_target(&self, target_id: &str) -> Option<&PageTargetHost> {
|
||||
pub fn background_target(&self, target_id: &str) -> Option<&PageAgentHost> {
|
||||
(!self.is_active_target(target_id))
|
||||
.then(|| self.page_target(target_id))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn background_target_mut(&mut self, target_id: &str) -> Option<&mut PageTargetHost> {
|
||||
pub(crate) fn background_target_mut(&mut self, target_id: &str) -> Option<&mut PageAgentHost> {
|
||||
if self.is_active_target(target_id) {
|
||||
return None;
|
||||
}
|
||||
self.page_target_mut(target_id)
|
||||
}
|
||||
|
||||
pub(crate) fn background_targets(&self) -> impl DoubleEndedIterator<Item = &PageTargetHost> {
|
||||
pub(crate) fn background_targets(&self) -> impl DoubleEndedIterator<Item = &PageAgentHost> {
|
||||
self.page_targets
|
||||
.background(self.physical.selected_web_contents_id())
|
||||
}
|
||||
@@ -327,7 +327,7 @@ impl BrowserContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl PageTargetHost {
|
||||
impl PageAgentHost {
|
||||
pub(crate) fn prepare_document_navigation_request_ids(
|
||||
&mut self,
|
||||
network_request_id_allocator: &mut ConnectionNetworkRequestIdAllocator,
|
||||
|
||||
@@ -3,13 +3,13 @@ use crate::conn::state::TargetPageAbsenceReason;
|
||||
use crate::conn::state::web_contents::{
|
||||
DocumentNavigationDestination, PreparedDocumentNavigation, RetiringDocument,
|
||||
};
|
||||
use crate::conn::state::{DevToolsRendererChannelError, DocumentId};
|
||||
use crate::conn::{BrowserContext, PageTargetHost, TargetRuntimeSlot};
|
||||
use crate::conn::state::{DevToolsRendererChannelError, DocumentId, DocumentProjectionFence};
|
||||
use crate::conn::{BrowserContext, PageAgentHost, TargetRuntimeSlot};
|
||||
use moli_core::page::{Page, RendererPageCommandPostResponseContinuation};
|
||||
|
||||
pub(crate) struct LoadedNavigationPageCommit {
|
||||
pub(crate) lifecycle: crate::conn::state::web_contents::CommittedDocumentLifecycle,
|
||||
pub(crate) inspection_projection: Result<(), DevToolsRendererChannelError>,
|
||||
pub(crate) inspection_projection: Result<DocumentProjectionFence, DevToolsRendererChannelError>,
|
||||
pub(crate) replaced_page_owner: Option<TargetPageResidenceIdentity>,
|
||||
pub(crate) previous_document_retirement: RetiringDocument,
|
||||
pub(crate) committed_document_post_response_continuation:
|
||||
@@ -80,10 +80,11 @@ impl BrowserContext {
|
||||
.page_targets
|
||||
.get_mut(target_id)
|
||||
.expect("resolved projection");
|
||||
if let Err(error) = target
|
||||
.runtime_slot
|
||||
.project_initial_document_inspection(commit.inspection_endpoint)
|
||||
{
|
||||
if let Err(error) = target.runtime_slot.project_initial_document_inspection(
|
||||
commit.key.document(),
|
||||
commit.lifecycle.browser_sequence,
|
||||
commit.inspection_endpoint,
|
||||
) {
|
||||
tracing::warn!(%error, "initial document inspection projection failed");
|
||||
}
|
||||
target
|
||||
@@ -370,8 +371,13 @@ impl BrowserContext {
|
||||
.expect("resolved target projection");
|
||||
let inspection_projection = target
|
||||
.runtime_slot
|
||||
.project_committed_document_inspection(commit.navigation, commit.inspection_endpoint)
|
||||
.map(|previous| {
|
||||
.project_committed_document_inspection(
|
||||
commit.navigation,
|
||||
commit.document,
|
||||
commit.lifecycle.browser_sequence,
|
||||
commit.inspection_endpoint,
|
||||
)
|
||||
.map(|(previous, fence)| {
|
||||
if let Some(previous) = previous {
|
||||
let new_attachment = target
|
||||
.runtime_slot
|
||||
@@ -387,6 +393,7 @@ impl BrowserContext {
|
||||
.runtime_slot
|
||||
.install_pending_renderer_call_replacements(replacements);
|
||||
}
|
||||
fence
|
||||
});
|
||||
self.reset_document_projection_for_target(
|
||||
target_id,
|
||||
@@ -431,7 +438,7 @@ impl BrowserContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl PageTargetHost {
|
||||
impl PageAgentHost {
|
||||
pub(crate) fn target_url(&self) -> &str {
|
||||
self.target_identity.url()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use crate::conn::state::{SessionStorageNamespace, TargetPageAbsenceReason};
|
||||
use crate::conn::state::{page_slot::TargetPageSlot, web_contents::WebContents};
|
||||
use crate::conn::{
|
||||
BrowserContext, DedicatedWorkerTargetState, InitialDocumentCreator, PageTargetHost,
|
||||
BrowserContext, DedicatedWorkerTargetState, InitialDocumentCreator, PageAgentHost,
|
||||
ServiceWorkerTargetState, SharedWorkerTargetState, TargetIdentityState,
|
||||
};
|
||||
use crate::devtools_runtime::{
|
||||
@@ -16,7 +16,7 @@ impl BrowserContext {
|
||||
&mut self,
|
||||
target_id: &str,
|
||||
) -> Option<(
|
||||
PageTargetHost,
|
||||
PageAgentHost,
|
||||
crate::conn::state::web_contents::ClosingWebContents,
|
||||
)> {
|
||||
let id = self.page_targets.get(target_id)?.web_contents_id();
|
||||
@@ -159,7 +159,7 @@ impl BrowserContext {
|
||||
.id();
|
||||
self.page_targets
|
||||
.get_for_web_contents(id)
|
||||
.map(PageTargetHost::target_id)
|
||||
.map(PageAgentHost::target_id)
|
||||
}
|
||||
|
||||
pub(crate) fn has_attached_child_frame_id(&self, frame_id: &str) -> bool {
|
||||
@@ -171,7 +171,7 @@ impl BrowserContext {
|
||||
pub(crate) fn remember_target_window_name(&mut self, target_name: &str, target_id: &str) {
|
||||
let Some(id) = self
|
||||
.page_target(target_id)
|
||||
.map(PageTargetHost::web_contents_id)
|
||||
.map(PageAgentHost::web_contents_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -291,7 +291,7 @@ impl BrowserContext {
|
||||
self.page_targets
|
||||
.iter()
|
||||
.find(|target| target.devtools_sessions.attached(session_id).is_some())
|
||||
.map(PageTargetHost::target_id)
|
||||
.map(PageAgentHost::target_id)
|
||||
}
|
||||
|
||||
pub(crate) fn attached_session_ids_for_target(&self, target_id: &str) -> Vec<String> {
|
||||
@@ -1470,7 +1470,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_target_host_keeps_protocol_and_owner_state_together() {
|
||||
fn page_agent_host_keeps_protocol_and_owner_state_together() {
|
||||
let mut context = BrowserContext::new("BC-1".to_owned());
|
||||
context.stage_background_target(
|
||||
"TID-bg".to_owned(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(test)]
|
||||
use crate::conn::cookie_manager_surface::BrowserContextCookieManagerSurfaceSnapshot;
|
||||
use crate::conn::state::PageSurface;
|
||||
use crate::conn::{BrowserContext, DocumentStartScript, EmulatedViewportSurface, PageTargetHost};
|
||||
use crate::conn::{BrowserContext, DocumentStartScript, EmulatedViewportSurface, PageAgentHost};
|
||||
#[cfg(test)]
|
||||
use moli_cookie_jar::{BrowserCookieFacadeContextOverrides, BrowserCookieFacadeOverrides};
|
||||
|
||||
@@ -289,7 +289,7 @@ impl BrowserContext {
|
||||
|
||||
// Context default resolution stays in this residence until Commit 7;
|
||||
// source generation itself only reads the embedded Browser object.
|
||||
fn page_surface_for_state(&self, state: &PageTargetHost, foreground: bool) -> PageSurface {
|
||||
fn page_surface_for_state(&self, state: &PageAgentHost, foreground: bool) -> PageSurface {
|
||||
let mut surface = self
|
||||
.web_contents_for_target(state.target_id())
|
||||
.expect("live WebContents")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::BrowserContext;
|
||||
use moli_core::browser::BrowserSequence;
|
||||
use moli_core::page::{
|
||||
Page, RendererAgentAttachmentId, RendererDocumentLifecycleIdentity,
|
||||
RendererRuntimeInspectorMessageBatch, ScriptNetworkOutputItem, ScriptObservableOutputItem,
|
||||
@@ -24,7 +25,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use crate::conn::state::devtools_renderer_channel::{
|
||||
DevToolsRendererChannel, RendererAgentBinding, RendererAgentDetachReason,
|
||||
DevToolsRendererChannel, DocumentProjectionFence, RendererAgentBinding,
|
||||
RendererAgentDetachReason, RendererOutputHoldRelease,
|
||||
};
|
||||
use crate::conn::state::page_slot::{TargetPageAbsenceReason, TargetPageSlot};
|
||||
use crate::conn::state::{
|
||||
@@ -33,7 +35,7 @@ use crate::conn::state::{
|
||||
TargetJavaScriptDialogScope, TargetJavaScriptDialogScopeObserver,
|
||||
};
|
||||
|
||||
pub(crate) struct FinishedRendererDocumentNavigation {
|
||||
pub(crate) struct DocumentProjectionOutputRelease {
|
||||
pub(crate) released_output: Vec<RendererRuntimeInspectorMessageBatch>,
|
||||
pub(crate) renderer_call_replacements: Option<PreparedRendererCallReplacements>,
|
||||
}
|
||||
@@ -162,28 +164,39 @@ impl TargetRuntimeSlot {
|
||||
self.javascript_dialog_scope.retire();
|
||||
}
|
||||
|
||||
pub(super) fn start_renderer_document_navigation(&mut self, navigation: NavigationId) {
|
||||
pub(super) fn begin_document_projection(&mut self, navigation: NavigationId) {
|
||||
self.devtools_renderer_channel.reopen_after_target_crash();
|
||||
self.devtools_renderer_channel
|
||||
.navigation_started(navigation)
|
||||
.begin_document_projection(navigation)
|
||||
.expect("an open target runtime slot must accept a new document navigation");
|
||||
}
|
||||
|
||||
pub(in crate::conn::state) fn project_committed_document_inspection(
|
||||
&mut self,
|
||||
navigation: NavigationId,
|
||||
document: DocumentId,
|
||||
browser_sequence: BrowserSequence,
|
||||
endpoint: moli_renderer_v8::RendererInspectionEndpoint,
|
||||
) -> Result<Option<RendererAgentAttachment>, DevToolsRendererChannelError> {
|
||||
self.devtools_renderer_channel
|
||||
.document_committed(navigation, endpoint)
|
||||
) -> Result<
|
||||
(Option<RendererAgentAttachment>, DocumentProjectionFence),
|
||||
DevToolsRendererChannelError,
|
||||
> {
|
||||
self.devtools_renderer_channel.document_committed(
|
||||
navigation,
|
||||
document,
|
||||
browser_sequence,
|
||||
endpoint,
|
||||
)
|
||||
}
|
||||
|
||||
pub(in crate::conn::state) fn project_initial_document_inspection(
|
||||
&mut self,
|
||||
document: DocumentId,
|
||||
browser_sequence: BrowserSequence,
|
||||
endpoint: moli_renderer_v8::RendererInspectionEndpoint,
|
||||
) -> Result<(), DevToolsRendererChannelError> {
|
||||
self.devtools_renderer_channel
|
||||
.attach_current(endpoint)
|
||||
.attach_current(document, browser_sequence, endpoint)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
@@ -196,19 +209,38 @@ impl TargetRuntimeSlot {
|
||||
.route_current_output(attachment_id, batches)
|
||||
}
|
||||
|
||||
pub(crate) fn finish_renderer_document_navigation(
|
||||
pub(crate) fn finish_navigation_without_document_projection(
|
||||
&mut self,
|
||||
token: &NavigationId,
|
||||
) -> Result<FinishedRendererDocumentNavigation, DevToolsRendererChannelError> {
|
||||
let resume = self.devtools_renderer_channel.navigation_finished(token)?;
|
||||
) -> Result<DocumentProjectionOutputRelease, DevToolsRendererChannelError> {
|
||||
let resume = self
|
||||
.devtools_renderer_channel
|
||||
.finish_navigation_without_document_projection(token)?;
|
||||
Ok(self.release_renderer_document_output(resume))
|
||||
}
|
||||
|
||||
pub(crate) fn publish_document_projection_fence(
|
||||
&mut self,
|
||||
fence: DocumentProjectionFence,
|
||||
) -> Result<DocumentProjectionOutputRelease, DevToolsRendererChannelError> {
|
||||
let resume = self
|
||||
.devtools_renderer_channel
|
||||
.publish_document_projection(fence)?;
|
||||
Ok(self.release_renderer_document_output(resume))
|
||||
}
|
||||
|
||||
fn release_renderer_document_output(
|
||||
&mut self,
|
||||
resume: Option<RendererOutputHoldRelease>,
|
||||
) -> DocumentProjectionOutputRelease {
|
||||
let renderer_call_replacements = resume
|
||||
.is_some()
|
||||
.then(|| std::mem::take(&mut self.pending_renderer_call_replacements))
|
||||
.filter(|replacements| !replacements.is_empty());
|
||||
Ok(FinishedRendererDocumentNavigation {
|
||||
DocumentProjectionOutputRelease {
|
||||
released_output: self.devtools_renderer_channel.take_released_output(),
|
||||
renderer_call_replacements,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn install_pending_renderer_call_replacements(
|
||||
@@ -218,8 +250,9 @@ impl TargetRuntimeSlot {
|
||||
self.pending_renderer_call_replacements = replacements;
|
||||
}
|
||||
|
||||
pub(crate) fn renderer_document_navigation_is_suspended(&self) -> bool {
|
||||
self.devtools_renderer_channel.output_is_suspended()
|
||||
pub(crate) fn document_projection_is_pending(&self) -> bool {
|
||||
self.devtools_renderer_channel
|
||||
.document_projection_is_pending()
|
||||
}
|
||||
|
||||
pub(crate) fn current_renderer_attachment(&self) -> Option<RendererAgentAttachment> {
|
||||
@@ -235,8 +268,18 @@ impl TargetRuntimeSlot {
|
||||
&mut self,
|
||||
endpoint: moli_renderer_v8::RendererInspectionEndpoint,
|
||||
) {
|
||||
let document = self
|
||||
.devtools_renderer_channel
|
||||
.current()
|
||||
.expect("reattachment requires a current renderer binding")
|
||||
.document();
|
||||
let browser_sequence = self
|
||||
.devtools_renderer_channel
|
||||
.current()
|
||||
.expect("reattachment requires a current renderer binding")
|
||||
.browser_sequence();
|
||||
self.devtools_renderer_channel
|
||||
.attach_current(endpoint)
|
||||
.attach_current(document, browser_sequence, endpoint)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -344,19 +387,21 @@ impl TargetRuntimeSlot {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn ensure_renderer_attachment_for_replacement(&mut self, page: Option<&mut Page>) {
|
||||
let Some(page) = page else {
|
||||
return;
|
||||
};
|
||||
fn ensure_renderer_attachment_for_replacement(
|
||||
&mut self,
|
||||
document: DocumentId,
|
||||
agent_token: moli_core::page::RendererDevToolsAgentToken,
|
||||
endpoint: moli_renderer_v8::RendererInspectionEndpoint,
|
||||
) {
|
||||
if !self
|
||||
.devtools_renderer_channel
|
||||
.current()
|
||||
.is_some_and(|attachment| {
|
||||
attachment.agent_token() == page.renderer_devtools_agent_token()
|
||||
attachment.agent_token() == agent_token && attachment.document() == document
|
||||
})
|
||||
{
|
||||
self.devtools_renderer_channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(document, BrowserSequence::allocate(), endpoint)
|
||||
.expect("a loaded page cannot be installed into a closed renderer channel");
|
||||
}
|
||||
}
|
||||
@@ -883,20 +928,31 @@ impl BrowserContext {
|
||||
pub(super) fn replace_loaded_page_for_target(
|
||||
&mut self,
|
||||
target_id: &str,
|
||||
mut page: Option<Page>,
|
||||
page: Option<Page>,
|
||||
) -> Option<Page> {
|
||||
let inspection = page.as_ref().map(|page| {
|
||||
(
|
||||
page.renderer_devtools_agent_token(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
});
|
||||
let previous_document = self.target_document_id(target_id).zip(
|
||||
self.loaded_page_for_target(target_id)
|
||||
.map(RendererPageResidenceIdentity::from_page),
|
||||
);
|
||||
let retiring =
|
||||
self.begin_document_projection_replacement_for_target(target_id, previous_document);
|
||||
self.page_targets
|
||||
.get_mut(target_id)
|
||||
.expect("resolved target projection")
|
||||
.runtime_slot
|
||||
.ensure_renderer_attachment_for_replacement(page.as_mut());
|
||||
let previous = self.replace_target_document(target_id, page);
|
||||
if let Some((agent_token, endpoint)) = inspection {
|
||||
let document = self
|
||||
.target_document_id(target_id)
|
||||
.expect("a replacement Page must install a Document");
|
||||
self.page_targets
|
||||
.get_mut(target_id)
|
||||
.expect("resolved target projection")
|
||||
.runtime_slot
|
||||
.ensure_renderer_attachment_for_replacement(document, agent_token, endpoint);
|
||||
}
|
||||
self.finish_document_projection_replacement_for_target(target_id, retiring);
|
||||
previous
|
||||
}
|
||||
@@ -1021,8 +1077,8 @@ impl BrowserContext {
|
||||
"rendererChannelClosed": self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.devtools_renderer_channel.is_closed(),
|
||||
"rendererChannelHasCurrentAttachment":
|
||||
self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.devtools_renderer_channel.current().is_some(),
|
||||
"rendererChannelInflightNavigationCount":
|
||||
self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.devtools_renderer_channel.inflight_navigation_count(),
|
||||
"pendingDocumentProjectionCount":
|
||||
self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.devtools_renderer_channel.pending_navigation_count(),
|
||||
"hasNetworkEventListeners": self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.has_network_event_listeners(),
|
||||
"nextFetchRequestId": self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.request_counters.next_fetch_request_id,
|
||||
"nextSubresourceFetchRequestId": self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.request_counters.next_subresource_fetch_request_id,
|
||||
@@ -1269,6 +1325,7 @@ mod tests {
|
||||
frame_id: "FRAME-old".to_owned(),
|
||||
loader_id: "LOADER-old".to_owned(),
|
||||
document_id: DocumentId::from_raw_for_test(1),
|
||||
browser_sequence: BrowserSequence::allocate(),
|
||||
document_open_replacement_epoch: None,
|
||||
},
|
||||
network_agent: retiring_agent,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::BrowserContext;
|
||||
use crate::conn::state::devtools_session::DevToolsNetworkSessionState;
|
||||
use crate::conn::state::javascript_dialog::TargetJavaScriptDialogState;
|
||||
use crate::conn::state::page_target_host::PageTargetHost;
|
||||
use crate::conn::state::page_agent_host::PageAgentHost;
|
||||
use crate::conn::state::web_contents::NetworkRequestPolicy;
|
||||
use crate::domains::audits_output_state::TargetAuditsSessionState;
|
||||
use moli_core::page::V8InspectorSessionState;
|
||||
@@ -59,7 +59,7 @@ impl TargetPerformanceSessionState {
|
||||
}
|
||||
}
|
||||
|
||||
impl PageTargetHost {
|
||||
impl PageAgentHost {
|
||||
pub(crate) fn reported_user_agent_override(&self) -> Option<&str> {
|
||||
self.devtools_sessions
|
||||
.reported_user_agent_override()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
|
||||
use moli_core::browser::BrowserSequence;
|
||||
use moli_core::page::{
|
||||
PendingDevToolsIoCommandDispatch, PendingPageCommand, PendingRuntimeInspectorCommandDispatch,
|
||||
RendererAgentAttachmentId, RendererDevToolsAgentToken, RendererInspectorCommandRoute,
|
||||
@@ -11,19 +12,27 @@ use moli_renderer_v8::{
|
||||
RendererRuntimeInspectorMainCommandRoute, RendererRuntimeInspectorResponseSender,
|
||||
};
|
||||
|
||||
use super::NavigationId;
|
||||
use super::{DocumentId, NavigationId};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct RendererAgentAttachment {
|
||||
id: RendererAgentAttachmentId,
|
||||
agent_token: RendererDevToolsAgentToken,
|
||||
document: DocumentId,
|
||||
browser_sequence: BrowserSequence,
|
||||
}
|
||||
|
||||
impl RendererAgentAttachment {
|
||||
fn new(agent_token: RendererDevToolsAgentToken) -> Self {
|
||||
fn new(
|
||||
document: DocumentId,
|
||||
browser_sequence: BrowserSequence,
|
||||
agent_token: RendererDevToolsAgentToken,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: RendererAgentAttachmentId::allocate(),
|
||||
agent_token,
|
||||
document,
|
||||
browser_sequence,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +43,52 @@ impl RendererAgentAttachment {
|
||||
pub(crate) fn agent_token(self) -> RendererDevToolsAgentToken {
|
||||
self.agent_token
|
||||
}
|
||||
|
||||
pub(crate) fn document(self) -> DocumentId {
|
||||
self.document
|
||||
}
|
||||
|
||||
pub(crate) fn browser_sequence(self) -> BrowserSequence {
|
||||
self.browser_sequence
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct DocumentProjectionFenceKey {
|
||||
document: DocumentId,
|
||||
browser_sequence: BrowserSequence,
|
||||
renderer_attachment: RendererAgentAttachmentId,
|
||||
}
|
||||
|
||||
/// Move-only authority to expose output from one rebound renderer Document.
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[must_use = "a Document projection fence must be published before its channel is retired"]
|
||||
pub(crate) struct DocumentProjectionFence {
|
||||
key: DocumentProjectionFenceKey,
|
||||
}
|
||||
|
||||
impl DocumentProjectionFence {
|
||||
fn new(attachment: RendererAgentAttachment) -> Self {
|
||||
Self {
|
||||
key: DocumentProjectionFenceKey {
|
||||
document: attachment.document(),
|
||||
browser_sequence: attachment.browser_sequence(),
|
||||
renderer_attachment: attachment.id(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn document(&self) -> DocumentId {
|
||||
self.key.document
|
||||
}
|
||||
|
||||
pub(crate) fn browser_sequence(&self) -> BrowserSequence {
|
||||
self.key.browser_sequence
|
||||
}
|
||||
|
||||
pub(crate) fn renderer_attachment(&self) -> RendererAgentAttachmentId {
|
||||
self.key.renderer_attachment
|
||||
}
|
||||
}
|
||||
|
||||
/// The DevTools binding owns inspection ingress, never the Browser Page.
|
||||
@@ -270,25 +325,32 @@ enum DevToolsRendererChannelLifecycle {
|
||||
pub(crate) struct DevToolsRendererChannel {
|
||||
lifecycle: DevToolsRendererChannelLifecycle,
|
||||
current: Option<RendererAgentBinding>,
|
||||
inflight_cross_document_navigations: HashSet<NavigationId>,
|
||||
suspended_attachment: Option<RendererAgentAttachment>,
|
||||
pending_document_navigations: HashSet<NavigationId>,
|
||||
pending_document_projection: Option<PendingDocumentProjection>,
|
||||
held_attachment: Option<RendererAgentAttachment>,
|
||||
buffered_output: Vec<BufferedRendererInspectorBatch>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PendingDocumentProjection {
|
||||
navigation: NavigationId,
|
||||
fence: DocumentProjectionFenceKey,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct RendererChannelResume {
|
||||
suspended_attachment: Option<RendererAgentAttachment>,
|
||||
pub(crate) struct RendererOutputHoldRelease {
|
||||
held_attachment: Option<RendererAgentAttachment>,
|
||||
current_attachment: Option<RendererAgentAttachment>,
|
||||
}
|
||||
|
||||
impl RendererChannelResume {
|
||||
impl RendererOutputHoldRelease {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn replacement(
|
||||
self,
|
||||
) -> Option<(RendererAgentAttachmentId, RendererAgentAttachmentId)> {
|
||||
let suspended = self.suspended_attachment?;
|
||||
let held = self.held_attachment?;
|
||||
let current = self.current_attachment?;
|
||||
(suspended.id() != current.id()).then_some((suspended.id(), current.id()))
|
||||
(held.id() != current.id()).then_some((held.id(), current.id()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,13 +363,19 @@ struct BufferedRendererInspectorBatch {
|
||||
impl DevToolsRendererChannel {
|
||||
pub(crate) fn attach_current(
|
||||
&mut self,
|
||||
document: DocumentId,
|
||||
browser_sequence: BrowserSequence,
|
||||
endpoint: RendererInspectionEndpoint,
|
||||
) -> Result<Option<RendererAgentAttachment>, DevToolsRendererChannelError> {
|
||||
self.ensure_open()?;
|
||||
Ok(self
|
||||
.current
|
||||
.replace(RendererAgentBinding {
|
||||
attachment: RendererAgentAttachment::new(endpoint.agent_token()),
|
||||
attachment: RendererAgentAttachment::new(
|
||||
document,
|
||||
browser_sequence,
|
||||
endpoint.agent_token(),
|
||||
),
|
||||
endpoint,
|
||||
})
|
||||
.map(|previous| previous.attachment()))
|
||||
@@ -321,17 +389,17 @@ impl DevToolsRendererChannel {
|
||||
self.current.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn navigation_started(
|
||||
pub(crate) fn begin_document_projection(
|
||||
&mut self,
|
||||
navigation: NavigationId,
|
||||
) -> Result<(), DevToolsRendererChannelError> {
|
||||
self.ensure_open()?;
|
||||
let was_suspended = self.output_is_suspended();
|
||||
if !self.inflight_cross_document_navigations.insert(navigation) {
|
||||
let was_pending = self.document_projection_is_pending();
|
||||
if !self.pending_document_navigations.insert(navigation) {
|
||||
return Err(DevToolsRendererChannelError::DuplicateNavigation);
|
||||
}
|
||||
if !was_suspended {
|
||||
self.suspended_attachment = self.current();
|
||||
if !was_pending {
|
||||
self.held_attachment = self.current();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -340,12 +408,32 @@ impl DevToolsRendererChannel {
|
||||
pub(crate) fn document_committed(
|
||||
&mut self,
|
||||
navigation: NavigationId,
|
||||
document: DocumentId,
|
||||
browser_sequence: BrowserSequence,
|
||||
endpoint: RendererInspectionEndpoint,
|
||||
) -> Result<Option<RendererAgentAttachment>, DevToolsRendererChannelError> {
|
||||
let previous = self.attach_current(endpoint)?;
|
||||
self.inflight_cross_document_navigations
|
||||
) -> Result<
|
||||
(Option<RendererAgentAttachment>, DocumentProjectionFence),
|
||||
DevToolsRendererChannelError,
|
||||
> {
|
||||
self.ensure_open()?;
|
||||
if self.pending_document_projection.is_some() {
|
||||
return Err(DevToolsRendererChannelError::ProjectionPending);
|
||||
}
|
||||
if !self.pending_document_navigations.contains(&navigation) {
|
||||
return Err(DevToolsRendererChannelError::UnknownNavigation);
|
||||
}
|
||||
let previous = self.attach_current(document, browser_sequence, endpoint)?;
|
||||
self.pending_document_navigations
|
||||
.retain(|pending| *pending == navigation);
|
||||
Ok(previous)
|
||||
let fence = DocumentProjectionFence::new(
|
||||
self.current()
|
||||
.expect("a successful renderer rebind must install its attachment"),
|
||||
);
|
||||
self.pending_document_projection = Some(PendingDocumentProjection {
|
||||
navigation,
|
||||
fence: fence.key,
|
||||
});
|
||||
Ok((previous, fence))
|
||||
}
|
||||
|
||||
pub(crate) fn route_current_output(
|
||||
@@ -363,37 +451,78 @@ impl DevToolsRendererChannel {
|
||||
self.route_validated_output(attachment_id, batches)
|
||||
}
|
||||
|
||||
pub(crate) fn navigation_finished(
|
||||
pub(crate) fn finish_navigation_without_document_projection(
|
||||
&mut self,
|
||||
navigation: &NavigationId,
|
||||
) -> Result<Option<RendererChannelResume>, DevToolsRendererChannelError> {
|
||||
) -> Result<Option<RendererOutputHoldRelease>, DevToolsRendererChannelError> {
|
||||
self.ensure_open()?;
|
||||
if !self.inflight_cross_document_navigations.remove(navigation)
|
||||
|| self.output_is_suspended()
|
||||
if self
|
||||
.pending_document_projection
|
||||
.as_ref()
|
||||
.is_some_and(|pending| &pending.navigation == navigation)
|
||||
{
|
||||
return Err(DevToolsRendererChannelError::ProjectionPending);
|
||||
}
|
||||
if !self.pending_document_navigations.remove(navigation)
|
||||
|| self.document_projection_is_pending()
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(RendererChannelResume {
|
||||
suspended_attachment: self.suspended_attachment.take(),
|
||||
Ok(Some(RendererOutputHoldRelease {
|
||||
held_attachment: self.held_attachment.take(),
|
||||
current_attachment: self.current(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn output_is_suspended(&self) -> bool {
|
||||
!self.inflight_cross_document_navigations.is_empty()
|
||||
pub(crate) fn publish_document_projection(
|
||||
&mut self,
|
||||
fence: DocumentProjectionFence,
|
||||
) -> Result<Option<RendererOutputHoldRelease>, DevToolsRendererChannelError> {
|
||||
self.ensure_open()?;
|
||||
let Some(pending) = self.pending_document_projection.as_ref() else {
|
||||
return Err(DevToolsRendererChannelError::StaleProjectionFence);
|
||||
};
|
||||
if pending.fence != fence.key
|
||||
|| self.current().is_none_or(|current| {
|
||||
current.document() != fence.document()
|
||||
|| current.browser_sequence() != fence.browser_sequence()
|
||||
|| current.id() != fence.renderer_attachment()
|
||||
})
|
||||
{
|
||||
return Err(DevToolsRendererChannelError::StaleProjectionFence);
|
||||
}
|
||||
let pending = self
|
||||
.pending_document_projection
|
||||
.take()
|
||||
.expect("validated pending Document projection");
|
||||
assert!(
|
||||
self.pending_document_navigations
|
||||
.remove(&pending.navigation),
|
||||
"a pending Document projection must retain its navigation hold"
|
||||
);
|
||||
if self.document_projection_is_pending() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(RendererOutputHoldRelease {
|
||||
held_attachment: self.held_attachment.take(),
|
||||
current_attachment: self.current(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn document_projection_is_pending(&self) -> bool {
|
||||
!self.pending_document_navigations.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn has_navigation(&self, navigation: &NavigationId) -> bool {
|
||||
self.inflight_cross_document_navigations
|
||||
.contains(navigation)
|
||||
self.pending_document_navigations.contains(navigation)
|
||||
}
|
||||
|
||||
pub(crate) fn inflight_navigation_count(&self) -> usize {
|
||||
self.inflight_cross_document_navigations.len()
|
||||
pub(crate) fn pending_navigation_count(&self) -> usize {
|
||||
self.pending_document_navigations.len()
|
||||
}
|
||||
|
||||
pub(crate) fn take_released_output(&mut self) -> Vec<RendererRuntimeInspectorMessageBatch> {
|
||||
if self.output_is_suspended() {
|
||||
if self.document_projection_is_pending() {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(current) = self.current() else {
|
||||
@@ -410,6 +539,10 @@ impl DevToolsRendererChannel {
|
||||
_reason: RendererAgentDetachReason,
|
||||
) -> Result<Option<RendererAgentAttachment>, DevToolsRendererChannelError> {
|
||||
self.ensure_open()?;
|
||||
self.pending_document_navigations.clear();
|
||||
self.pending_document_projection = None;
|
||||
self.held_attachment = None;
|
||||
self.buffered_output.clear();
|
||||
Ok(self.current.take().map(|current| current.attachment()))
|
||||
}
|
||||
|
||||
@@ -421,8 +554,9 @@ impl DevToolsRendererChannel {
|
||||
return None;
|
||||
}
|
||||
self.lifecycle = DevToolsRendererChannelLifecycle::Closed(reason);
|
||||
self.inflight_cross_document_navigations.clear();
|
||||
self.suspended_attachment = None;
|
||||
self.pending_document_navigations.clear();
|
||||
self.pending_document_projection = None;
|
||||
self.held_attachment = None;
|
||||
self.buffered_output.clear();
|
||||
self.current.take().map(|current| current.attachment())
|
||||
}
|
||||
@@ -463,10 +597,11 @@ impl DevToolsRendererChannel {
|
||||
{
|
||||
return Err(DevToolsRendererChannelError::MismatchedAgent);
|
||||
}
|
||||
if self.output_is_suspended() {
|
||||
let releases_current_prefix = batches
|
||||
.iter()
|
||||
.any(RendererRuntimeInspectorMessageBatch::has_renderer_protocol_response);
|
||||
if self.document_projection_is_pending() {
|
||||
let releases_current_prefix = self.pending_document_projection.is_none()
|
||||
&& batches
|
||||
.iter()
|
||||
.any(RendererRuntimeInspectorMessageBatch::has_renderer_protocol_response);
|
||||
self.buffer_output(attachment_id, batches);
|
||||
if releases_current_prefix {
|
||||
// Main ingress remains suspended, but Chromium's existing
|
||||
@@ -521,6 +656,9 @@ impl DevToolsRendererChannel {
|
||||
pub(crate) enum DevToolsRendererChannelError {
|
||||
Closed,
|
||||
DuplicateNavigation,
|
||||
UnknownNavigation,
|
||||
ProjectionPending,
|
||||
StaleProjectionFence,
|
||||
StaleAttachment,
|
||||
MismatchedAgent,
|
||||
}
|
||||
@@ -530,6 +668,9 @@ impl fmt::Display for DevToolsRendererChannelError {
|
||||
formatter.write_str(match self {
|
||||
Self::Closed => "renderer channel is closed",
|
||||
Self::DuplicateNavigation => "renderer channel navigation is already in flight",
|
||||
Self::UnknownNavigation => "renderer channel navigation is not in flight",
|
||||
Self::ProjectionPending => "renderer Document projection is still pending",
|
||||
Self::StaleProjectionFence => "renderer Document projection fence is stale",
|
||||
Self::StaleAttachment => "renderer Inspector output belongs to a stale attachment",
|
||||
Self::MismatchedAgent => {
|
||||
"renderer Inspector output agent does not match its attachment"
|
||||
@@ -602,17 +743,24 @@ mod tests {
|
||||
.unwrap();
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
channel
|
||||
.attach_current(outgoing.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
outgoing.renderer_inspection_endpoint(),
|
||||
)
|
||||
.unwrap();
|
||||
let original = channel.current().unwrap();
|
||||
let navigation = NavigationId::allocate();
|
||||
channel.navigation_started(navigation).unwrap();
|
||||
assert_eq!(
|
||||
channel
|
||||
.document_committed(navigation, committed_page.renderer_inspection_endpoint())
|
||||
.unwrap(),
|
||||
Some(original)
|
||||
);
|
||||
channel.begin_document_projection(navigation).unwrap();
|
||||
let (previous, _fence) = channel
|
||||
.document_committed(
|
||||
navigation,
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
committed_page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(previous, Some(original));
|
||||
let attachment = channel.current().unwrap();
|
||||
let registration = moli_core::page::RuntimeBindingRegistration {
|
||||
devtools_session: None,
|
||||
@@ -688,14 +836,22 @@ mod tests {
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
|
||||
assert_eq!(
|
||||
channel.attach_current(page.renderer_inspection_endpoint()),
|
||||
channel.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
let first = channel.current().expect("first attachment");
|
||||
assert_eq!(first.agent_token(), agent);
|
||||
|
||||
let replaced = channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.expect("reattach")
|
||||
.expect("replaced attachment");
|
||||
let second = channel.current().expect("second attachment");
|
||||
@@ -710,54 +866,73 @@ mod tests {
|
||||
let request = NavigationId::allocate();
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.expect("initial attach");
|
||||
let current = channel.current();
|
||||
|
||||
channel
|
||||
.navigation_started(request)
|
||||
.begin_document_projection(request)
|
||||
.expect("navigation start");
|
||||
assert!(channel.output_is_suspended());
|
||||
assert!(channel.document_projection_is_pending());
|
||||
assert!(
|
||||
channel
|
||||
.navigation_finished(&request)
|
||||
.finish_navigation_without_document_projection(&request)
|
||||
.expect("navigation finish")
|
||||
.is_some(),
|
||||
"a failed load finishes without committing its candidate"
|
||||
);
|
||||
|
||||
assert_eq!(channel.current(), current);
|
||||
assert!(!channel.output_is_suspended());
|
||||
assert!(!channel.document_projection_is_pending());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn browser_commit_retires_superseded_output_suspensions() {
|
||||
async fn browser_commit_retires_superseded_projection_holds() {
|
||||
let (_browser, page) = inspection_page().await;
|
||||
let (_next_browser, next_page) = inspection_page().await;
|
||||
let first = NavigationId::allocate();
|
||||
let committed = NavigationId::allocate();
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.unwrap();
|
||||
let original = channel.current().unwrap();
|
||||
channel.navigation_started(first).unwrap();
|
||||
channel.navigation_started(committed).unwrap();
|
||||
channel.begin_document_projection(first).unwrap();
|
||||
channel.begin_document_projection(committed).unwrap();
|
||||
// Browser has already selected the winning Document. This is projection,
|
||||
// not a second validation/commit state machine.
|
||||
assert_eq!(
|
||||
channel
|
||||
.document_committed(committed, next_page.renderer_inspection_endpoint())
|
||||
.unwrap(),
|
||||
Some(original)
|
||||
);
|
||||
let (previous, fence) = channel
|
||||
.document_committed(
|
||||
committed,
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
next_page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(previous, Some(original));
|
||||
assert_eq!(
|
||||
channel.current().unwrap().agent_token(),
|
||||
next_page.renderer_devtools_agent_token()
|
||||
);
|
||||
assert_eq!(channel.inflight_navigation_count(), 1);
|
||||
assert!(channel.navigation_finished(&committed).unwrap().is_some());
|
||||
assert_eq!(channel.navigation_finished(&first), Ok(None));
|
||||
assert_eq!(channel.pending_navigation_count(), 1);
|
||||
assert!(
|
||||
channel
|
||||
.publish_document_projection(fence)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
assert_eq!(
|
||||
channel.finish_navigation_without_document_projection(&first),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@@ -765,7 +940,11 @@ mod tests {
|
||||
let (_browser, page) = inspection_page().await;
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.unwrap();
|
||||
drop(page);
|
||||
let binding = channel.current_binding().unwrap();
|
||||
@@ -793,7 +972,11 @@ mod tests {
|
||||
let (_browser, mut page) = inspection_page().await;
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.unwrap();
|
||||
channel
|
||||
.detach_current(RendererAgentDetachReason::ExplicitDetach)
|
||||
@@ -809,41 +992,56 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_remains_suspended_until_all_overlapping_navigations_finish() {
|
||||
fn output_remains_held_until_all_overlapping_navigations_finish() {
|
||||
let request_a = NavigationId::allocate();
|
||||
let request_b = NavigationId::allocate();
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
|
||||
channel.navigation_started(request_a).expect("navigation A");
|
||||
channel.navigation_started(request_b).expect("navigation B");
|
||||
assert_eq!(channel.inflight_navigation_count(), 2);
|
||||
assert!(channel.output_is_suspended());
|
||||
channel
|
||||
.begin_document_projection(request_a)
|
||||
.expect("navigation A");
|
||||
channel
|
||||
.begin_document_projection(request_b)
|
||||
.expect("navigation B");
|
||||
assert_eq!(channel.pending_navigation_count(), 2);
|
||||
assert!(channel.document_projection_is_pending());
|
||||
|
||||
assert_eq!(channel.navigation_finished(&request_b), Ok(None));
|
||||
assert!(channel.output_is_suspended());
|
||||
assert_eq!(
|
||||
channel.finish_navigation_without_document_projection(&request_b),
|
||||
Ok(None)
|
||||
);
|
||||
assert!(channel.document_projection_is_pending());
|
||||
assert!(
|
||||
channel
|
||||
.navigation_finished(&request_a)
|
||||
.finish_navigation_without_document_projection(&request_a)
|
||||
.expect("final overlapping navigation")
|
||||
.is_some()
|
||||
);
|
||||
assert!(!channel.output_is_suspended());
|
||||
assert!(!channel.document_projection_is_pending());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn output_suspension_rejects_duplicate_and_ignores_unknown_completion() {
|
||||
async fn projection_hold_rejects_duplicate_and_ignores_unknown_completion() {
|
||||
let request = NavigationId::allocate();
|
||||
let unknown = NavigationId::allocate();
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
channel.navigation_started(request).unwrap();
|
||||
channel.begin_document_projection(request).unwrap();
|
||||
assert_eq!(
|
||||
channel.navigation_started(request),
|
||||
channel.begin_document_projection(request),
|
||||
Err(DevToolsRendererChannelError::DuplicateNavigation)
|
||||
);
|
||||
assert_eq!(channel.navigation_finished(&unknown), Ok(None));
|
||||
assert!(channel.output_is_suspended());
|
||||
assert!(channel.navigation_finished(&request).unwrap().is_some());
|
||||
assert!(!channel.output_is_suspended());
|
||||
assert_eq!(
|
||||
channel.finish_navigation_without_document_projection(&unknown),
|
||||
Ok(None)
|
||||
);
|
||||
assert!(channel.document_projection_is_pending());
|
||||
assert!(
|
||||
channel
|
||||
.finish_navigation_without_document_projection(&request)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
assert!(!channel.document_projection_is_pending());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@@ -853,10 +1051,14 @@ mod tests {
|
||||
let agent = page.renderer_devtools_agent_token();
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.expect("initial attach");
|
||||
channel
|
||||
.navigation_started(request)
|
||||
.begin_document_projection(request)
|
||||
.expect("navigation start");
|
||||
|
||||
let detached = channel
|
||||
@@ -864,17 +1066,26 @@ mod tests {
|
||||
.expect("current attachment");
|
||||
assert_eq!(detached.agent_token(), agent);
|
||||
assert!(channel.is_closed());
|
||||
assert_eq!(channel.inflight_navigation_count(), 0);
|
||||
assert_eq!(channel.pending_navigation_count(), 0);
|
||||
assert_eq!(
|
||||
channel.attach_current(page.renderer_inspection_endpoint()),
|
||||
channel.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint()
|
||||
),
|
||||
Err(DevToolsRendererChannelError::Closed)
|
||||
);
|
||||
assert_eq!(
|
||||
channel.navigation_started(NavigationId::allocate()),
|
||||
channel.begin_document_projection(NavigationId::allocate()),
|
||||
Err(DevToolsRendererChannelError::Closed)
|
||||
);
|
||||
assert_eq!(
|
||||
channel.document_committed(request, page.renderer_inspection_endpoint()),
|
||||
channel.document_committed(
|
||||
request,
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
),
|
||||
Err(DevToolsRendererChannelError::Closed)
|
||||
);
|
||||
assert_eq!(channel.close(RendererAgentDetachReason::TargetClosed), None);
|
||||
@@ -887,7 +1098,11 @@ mod tests {
|
||||
let agent = page.renderer_devtools_agent_token();
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.expect("initial attach");
|
||||
|
||||
let detached = channel
|
||||
@@ -898,7 +1113,11 @@ mod tests {
|
||||
assert!(channel.reopen_after_target_crash());
|
||||
assert!(!channel.is_closed());
|
||||
assert!(!channel.reopen_after_target_crash());
|
||||
assert!(channel.navigation_started(NavigationId::allocate()).is_ok());
|
||||
assert!(
|
||||
channel
|
||||
.begin_document_projection(NavigationId::allocate())
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@@ -910,11 +1129,15 @@ mod tests {
|
||||
let request = NavigationId::allocate();
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.expect("old attach");
|
||||
let old_attachment = channel.current().expect("old attachment");
|
||||
channel
|
||||
.navigation_started(request)
|
||||
.begin_document_projection(request)
|
||||
.expect("navigation start");
|
||||
|
||||
assert!(
|
||||
@@ -923,10 +1146,20 @@ mod tests {
|
||||
.expect("route old output")
|
||||
.is_empty()
|
||||
);
|
||||
channel
|
||||
.document_committed(request, candidate_page.renderer_inspection_endpoint())
|
||||
let committed_document = DocumentId::allocate();
|
||||
let committed_sequence = BrowserSequence::allocate();
|
||||
let (_, fence) = channel
|
||||
.document_committed(
|
||||
request,
|
||||
committed_document,
|
||||
committed_sequence,
|
||||
candidate_page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.unwrap();
|
||||
let current = channel.current().unwrap();
|
||||
assert_eq!(fence.document(), committed_document);
|
||||
assert_eq!(fence.browser_sequence(), committed_sequence);
|
||||
assert_eq!(fence.renderer_attachment(), current.id());
|
||||
assert!(
|
||||
channel
|
||||
.route_current_output(current.id(), vec![batch(new_agent, "new")])
|
||||
@@ -934,9 +1167,33 @@ mod tests {
|
||||
.is_empty(),
|
||||
"committing Browser state does not release new-generation output before projection finishes"
|
||||
);
|
||||
assert_eq!(
|
||||
channel.finish_navigation_without_document_projection(&request),
|
||||
Err(DevToolsRendererChannelError::ProjectionPending),
|
||||
"a generic navigation terminal must not bypass the committed projection fence"
|
||||
);
|
||||
assert!(
|
||||
channel
|
||||
.route_current_output(current.id(), vec![response_batch(new_agent, 18)])
|
||||
.expect("route new-generation response")
|
||||
.is_empty(),
|
||||
"even an IO response from the rebound attachment must wait for frame projection"
|
||||
);
|
||||
let stale_fence = DocumentProjectionFence {
|
||||
key: DocumentProjectionFenceKey {
|
||||
document: fence.document(),
|
||||
browser_sequence: BrowserSequence::allocate(),
|
||||
renderer_attachment: fence.renderer_attachment(),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
channel.publish_document_projection(stale_fence),
|
||||
Err(DevToolsRendererChannelError::StaleProjectionFence),
|
||||
"a mismatched Browser occurrence must not consume the exact projection fence"
|
||||
);
|
||||
let resume = channel
|
||||
.navigation_finished(&request)
|
||||
.expect("navigation finish")
|
||||
.publish_document_projection(fence)
|
||||
.expect("publish projection fence")
|
||||
.expect("channel resume");
|
||||
assert_eq!(
|
||||
resume.replacement(),
|
||||
@@ -944,8 +1201,9 @@ mod tests {
|
||||
);
|
||||
|
||||
let released = channel.take_released_output();
|
||||
assert_eq!(released.len(), 1);
|
||||
assert_eq!(released.len(), 2);
|
||||
assert_eq!(batch_marker(&released[0]), Some("new"));
|
||||
assert!(released[1].has_renderer_protocol_response());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@@ -955,11 +1213,15 @@ mod tests {
|
||||
let request = NavigationId::allocate();
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.expect("current attach");
|
||||
let attachment = channel.current().expect("current attachment");
|
||||
channel
|
||||
.navigation_started(request)
|
||||
.begin_document_projection(request)
|
||||
.expect("navigation start");
|
||||
assert!(
|
||||
channel
|
||||
@@ -969,7 +1231,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let resume = channel
|
||||
.navigation_finished(&request)
|
||||
.finish_navigation_without_document_projection(&request)
|
||||
.expect("navigation finish")
|
||||
.expect("channel resume");
|
||||
assert_eq!(resume.replacement(), None);
|
||||
@@ -985,11 +1247,15 @@ mod tests {
|
||||
let request = NavigationId::allocate();
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.expect("current attach");
|
||||
let attachment = channel.current().expect("current attachment");
|
||||
channel
|
||||
.navigation_started(request)
|
||||
.begin_document_projection(request)
|
||||
.expect("navigation start");
|
||||
|
||||
assert!(
|
||||
@@ -1005,7 +1271,7 @@ mod tests {
|
||||
assert_eq!(released.len(), 2);
|
||||
assert_eq!(batch_marker(&released[0]), Some("before-response"));
|
||||
assert!(released[1].has_renderer_protocol_response());
|
||||
assert!(channel.output_is_suspended());
|
||||
assert!(channel.document_projection_is_pending());
|
||||
assert!(channel.take_released_output().is_empty());
|
||||
}
|
||||
|
||||
@@ -1016,11 +1282,19 @@ mod tests {
|
||||
let other_agent = RendererDevToolsAgentToken::allocate();
|
||||
let mut channel = DevToolsRendererChannel::default();
|
||||
channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.expect("first attach");
|
||||
let stale = channel.current().expect("first attachment");
|
||||
channel
|
||||
.attach_current(page.renderer_inspection_endpoint())
|
||||
.attach_current(
|
||||
DocumentId::allocate(),
|
||||
BrowserSequence::allocate(),
|
||||
page.renderer_inspection_endpoint(),
|
||||
)
|
||||
.expect("reattach");
|
||||
let current = channel.current().expect("current attachment");
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ use moli_page_types::{
|
||||
///
|
||||
/// Browser-side policy and renderer-inspector state live together here so
|
||||
/// attachment, replay, and disposal all address the same session object.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub(crate) struct DevToolsSessionState {
|
||||
pub(crate) dom_session_state: DevToolsDomSessionState,
|
||||
pub(crate) dom_debugger_event_listener_breakpoints:
|
||||
@@ -977,6 +977,30 @@ impl DevToolsSessionState {
|
||||
self.pending_inspector_awaits.remove(cdp_request_id)
|
||||
}
|
||||
|
||||
pub(crate) fn claim_pending_inspector_await(&mut self, cdp_request_id: u64) -> bool {
|
||||
self.pending_inspector_awaits
|
||||
.claim_scheduler_deferred_reply(cdp_request_id)
|
||||
}
|
||||
|
||||
pub(crate) fn take_claimed_pending_inspector_await(
|
||||
&mut self,
|
||||
cdp_request_id: u64,
|
||||
) -> Option<PendingInspectorAwait> {
|
||||
self.pending_inspector_awaits
|
||||
.take_claimed_scheduler_deferred_reply(cdp_request_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn has_claimed_pending_inspector_awaits(&self) -> bool {
|
||||
self.pending_inspector_awaits
|
||||
.has_claimed_scheduler_deferred_reply()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn has_unclaimed_pending_inspector_awaits(&self) -> bool {
|
||||
self.pending_inspector_awaits.has_unclaimed_await()
|
||||
}
|
||||
|
||||
pub(crate) fn has_pending_inspector_awaits(&self) -> bool {
|
||||
!self.pending_inspector_awaits.is_empty()
|
||||
}
|
||||
@@ -1638,7 +1662,7 @@ mod tests {
|
||||
sessions
|
||||
.set_timezone_override(&key, Some("Europe/Paris".to_owned()))
|
||||
.unwrap();
|
||||
let snapshot = sessions.primary().clone();
|
||||
let snapshot = sessions.primary().emulation_session_state.clone();
|
||||
drop(sessions);
|
||||
|
||||
let replacement = moli_core::ProcessEnvironmentOwner::default();
|
||||
|
||||
@@ -13,7 +13,7 @@ pub(in crate::conn) use browser_context::javascript_dialog;
|
||||
mod navigation_outcome;
|
||||
mod page_resource;
|
||||
pub(in crate::conn) use browser_context::page_slot;
|
||||
mod page_target_host;
|
||||
mod page_agent_host;
|
||||
mod pending_renderer_command;
|
||||
mod profiler;
|
||||
pub(in crate::conn) use browser_context::runtime_slot;
|
||||
@@ -51,7 +51,8 @@ pub(crate) use identity::{
|
||||
pub(crate) use moli_core::browser::{DocumentId, NavigationId, RendererPageResidenceIdentity};
|
||||
|
||||
pub(crate) use devtools_renderer_channel::{
|
||||
DevToolsRendererChannelError, RendererAgentAttachment, RendererAgentBinding,
|
||||
DevToolsRendererChannelError, DocumentProjectionFence, RendererAgentAttachment,
|
||||
RendererAgentBinding,
|
||||
};
|
||||
|
||||
pub(crate) use dedicated_worker_target::{
|
||||
@@ -78,7 +79,7 @@ pub(crate) use page_slot::{
|
||||
};
|
||||
pub use page_slot::{DocumentStartScript, IsolatedWorldDefinition, RuntimeBindingDefinition};
|
||||
|
||||
pub(crate) use runtime_slot::{FinishedRendererDocumentNavigation, TargetRuntimeSlot};
|
||||
pub(crate) use runtime_slot::{DocumentProjectionOutputRelease, TargetRuntimeSlot};
|
||||
|
||||
pub use fetch::TargetFetchConfig;
|
||||
pub(crate) use fetch::{TargetFetchOwner, TargetFetchSubresourceInterceptionSnapshot};
|
||||
@@ -91,9 +92,9 @@ pub(crate) use javascript_dialog::{
|
||||
TargetPreparedJavaScriptDialog, TargetPreparedJavaScriptDialogRoute,
|
||||
};
|
||||
pub(crate) use pending_renderer_command::{
|
||||
DuplicatePendingRendererCommand, PendingRendererCommandKey, PreparedRendererCallDispatch,
|
||||
PreparedRendererCallTermination, RegisterRendererCallError, RendererCommandCorrelation,
|
||||
RendererCommandDescriptor, RendererCommandReplay,
|
||||
DuplicatePendingRendererCommand, PreparedRendererCallDispatch, PreparedRendererCallTermination,
|
||||
RegisterRendererCallError, RendererCommandCorrelation, RendererCommandDescriptor,
|
||||
RendererCommandReplay,
|
||||
};
|
||||
pub(crate) use profiler::{ProfilerAction, ProfilerInspectorCommand};
|
||||
pub(crate) use service_worker_lifetime::{
|
||||
@@ -153,7 +154,7 @@ pub use emulation::{
|
||||
EmulatedMediaOverrides,
|
||||
};
|
||||
pub(crate) use emulation::{EmulatedNetworkConditions, EmulatedViewportSurface};
|
||||
pub use page_target_host::PageTargetHost;
|
||||
pub use page_agent_host::PageAgentHost;
|
||||
pub(crate) use target_state::{
|
||||
PendingBidiChannelListener, PendingInspectorAwait, TargetOwnerState,
|
||||
};
|
||||
|
||||
+15
-15
@@ -15,7 +15,7 @@ use crate::conn::cookie_manager_surface::BrowserContextCookieManagerSurface;
|
||||
/// DevTools projection of a stable Browser page and its main-frame slot.
|
||||
/// Browser ownership and foreground selection belong to the physical Context.
|
||||
#[derive(Debug)]
|
||||
pub struct PageTargetHost {
|
||||
pub struct PageAgentHost {
|
||||
target_id: String,
|
||||
web_contents_id: WebContentsId,
|
||||
main_frame_slot_id: MainFrameSlotId,
|
||||
@@ -36,7 +36,7 @@ pub struct PageTargetHost {
|
||||
pub(crate) owner_state: TargetOwnerState,
|
||||
}
|
||||
|
||||
impl PageTargetHost {
|
||||
impl PageAgentHost {
|
||||
pub(crate) fn new(
|
||||
target_id: String,
|
||||
primary_session_id: Option<String>,
|
||||
@@ -155,11 +155,11 @@ impl PageTargetHost {
|
||||
|
||||
/// DevTools page projections. The Browser collection owns selection and close.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct PageTargetRegistry {
|
||||
hosts: IndexMap<String, PageTargetHost>,
|
||||
pub(crate) struct PageAgentHostRegistry {
|
||||
hosts: IndexMap<String, PageAgentHost>,
|
||||
}
|
||||
|
||||
impl PageTargetRegistry {
|
||||
impl PageAgentHostRegistry {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.hosts.is_empty()
|
||||
@@ -169,14 +169,14 @@ impl PageTargetRegistry {
|
||||
self.hosts.len()
|
||||
}
|
||||
|
||||
pub(crate) fn active(&self, selected: Option<WebContentsId>) -> Option<&PageTargetHost> {
|
||||
pub(crate) fn active(&self, selected: Option<WebContentsId>) -> Option<&PageAgentHost> {
|
||||
self.get_for_web_contents(selected?)
|
||||
}
|
||||
|
||||
pub(crate) fn active_mut(
|
||||
&mut self,
|
||||
selected: Option<WebContentsId>,
|
||||
) -> Option<&mut PageTargetHost> {
|
||||
) -> Option<&mut PageAgentHost> {
|
||||
let id = selected?;
|
||||
self.iter_mut().find(|host| host.web_contents_id() == id)
|
||||
}
|
||||
@@ -184,19 +184,19 @@ impl PageTargetRegistry {
|
||||
pub(in crate::conn) fn get_for_web_contents(
|
||||
&self,
|
||||
id: WebContentsId,
|
||||
) -> Option<&PageTargetHost> {
|
||||
) -> Option<&PageAgentHost> {
|
||||
self.iter().find(|host| host.web_contents_id() == id)
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, target_id: &str) -> Option<&PageTargetHost> {
|
||||
pub(crate) fn get(&self, target_id: &str) -> Option<&PageAgentHost> {
|
||||
self.hosts.get(target_id)
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut(&mut self, target_id: &str) -> Option<&mut PageTargetHost> {
|
||||
pub(crate) fn get_mut(&mut self, target_id: &str) -> Option<&mut PageAgentHost> {
|
||||
self.hosts.get_mut(target_id)
|
||||
}
|
||||
|
||||
pub(crate) fn insert(&mut self, host: PageTargetHost) -> bool {
|
||||
pub(crate) fn insert(&mut self, host: PageAgentHost) -> bool {
|
||||
let target_id = host.target_id().to_owned();
|
||||
if self.hosts.contains_key(&target_id) {
|
||||
return false;
|
||||
@@ -205,7 +205,7 @@ impl PageTargetRegistry {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, target_id: &str) -> Option<PageTargetHost> {
|
||||
pub(crate) fn remove(&mut self, target_id: &str) -> Option<PageAgentHost> {
|
||||
self.hosts.shift_remove(target_id)
|
||||
}
|
||||
|
||||
@@ -223,18 +223,18 @@ impl PageTargetRegistry {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl DoubleEndedIterator<Item = &PageTargetHost> {
|
||||
pub(crate) fn iter(&self) -> impl DoubleEndedIterator<Item = &PageAgentHost> {
|
||||
self.hosts.values()
|
||||
}
|
||||
|
||||
pub(crate) fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut PageTargetHost> {
|
||||
pub(crate) fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut PageAgentHost> {
|
||||
self.hosts.values_mut()
|
||||
}
|
||||
|
||||
pub(crate) fn background(
|
||||
&self,
|
||||
active_id: Option<WebContentsId>,
|
||||
) -> impl DoubleEndedIterator<Item = &PageTargetHost> {
|
||||
) -> impl DoubleEndedIterator<Item = &PageAgentHost> {
|
||||
self.iter()
|
||||
.filter(move |host| Some(host.web_contents_id()) != active_id)
|
||||
}
|
||||
@@ -9,8 +9,7 @@ use moli_core::{
|
||||
RendererRuntimeInspectorResponseSender,
|
||||
};
|
||||
use moli_page_types::{
|
||||
DevToolsSessionKey, FrontendCommandId, RendererAgentAttachmentId, RendererCallId,
|
||||
RendererInspectorResponseDelivery,
|
||||
FrontendCommandId, RendererAgentAttachmentId, RendererCallId, RendererInspectorResponseDelivery,
|
||||
};
|
||||
use moli_protocol_cdp::{
|
||||
CdpRendererCommandPolicy, CdpRendererCommandReplacement, CdpRendererCommandReplayDispatch,
|
||||
@@ -134,21 +133,6 @@ impl fmt::Display for DuplicatePendingRendererCommand {
|
||||
|
||||
impl std::error::Error for DuplicatePendingRendererCommand {}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub(crate) struct PendingRendererCommandKey {
|
||||
session_key: DevToolsSessionKey,
|
||||
frontend_command_id: FrontendCommandId,
|
||||
}
|
||||
|
||||
impl PendingRendererCommandKey {
|
||||
pub(crate) fn new(session_id: Option<&str>, frontend_command_id: u64) -> Self {
|
||||
Self {
|
||||
session_key: DevToolsSessionKey::from_wire_session_id(session_id),
|
||||
frontend_command_id: FrontendCommandId::new(frontend_command_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct RendererCallIdExhausted;
|
||||
|
||||
|
||||
@@ -1105,8 +1105,43 @@ impl ServiceWorkerTargetState {
|
||||
cdp_request_id: u64,
|
||||
) -> Option<PendingInspectorAwait> {
|
||||
self.session_state_mut(owner_session_id)?
|
||||
.pending_inspector_awaits
|
||||
.remove(cdp_request_id)
|
||||
.remove_pending_inspector_await(cdp_request_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn has_claimed_pending_inspector_awaits_for_session(
|
||||
&self,
|
||||
session_id: &str,
|
||||
) -> bool {
|
||||
self.session_state(session_id)
|
||||
.is_some_and(DevToolsSessionState::has_claimed_pending_inspector_awaits)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn has_unclaimed_pending_inspector_awaits_for_session(
|
||||
&self,
|
||||
session_id: &str,
|
||||
) -> bool {
|
||||
self.session_state(session_id)
|
||||
.is_some_and(DevToolsSessionState::has_unclaimed_pending_inspector_awaits)
|
||||
}
|
||||
|
||||
pub(crate) fn claim_pending_inspector_await(
|
||||
&mut self,
|
||||
owner_session_id: &str,
|
||||
cdp_request_id: u64,
|
||||
) -> bool {
|
||||
self.session_state_mut(owner_session_id)
|
||||
.is_some_and(|state| state.claim_pending_inspector_await(cdp_request_id))
|
||||
}
|
||||
|
||||
pub(crate) fn take_claimed_pending_inspector_await(
|
||||
&mut self,
|
||||
owner_session_id: &str,
|
||||
cdp_request_id: u64,
|
||||
) -> Option<PendingInspectorAwait> {
|
||||
self.session_state_mut(owner_session_id)?
|
||||
.take_claimed_pending_inspector_await(cdp_request_id)
|
||||
}
|
||||
|
||||
fn current_run_identity(
|
||||
|
||||
@@ -700,8 +700,25 @@ impl SharedWorkerTargetState {
|
||||
cdp_request_id: u64,
|
||||
) -> Option<PendingInspectorAwait> {
|
||||
self.session_state_mut(owner_session_id)?
|
||||
.pending_inspector_awaits
|
||||
.remove(cdp_request_id)
|
||||
.remove_pending_inspector_await(cdp_request_id)
|
||||
}
|
||||
|
||||
pub(crate) fn claim_pending_inspector_await(
|
||||
&mut self,
|
||||
owner_session_id: &str,
|
||||
cdp_request_id: u64,
|
||||
) -> bool {
|
||||
self.session_state_mut(owner_session_id)
|
||||
.is_some_and(|state| state.claim_pending_inspector_await(cdp_request_id))
|
||||
}
|
||||
|
||||
pub(crate) fn take_claimed_pending_inspector_await(
|
||||
&mut self,
|
||||
owner_session_id: &str,
|
||||
cdp_request_id: u64,
|
||||
) -> Option<PendingInspectorAwait> {
|
||||
self.session_state_mut(owner_session_id)?
|
||||
.take_claimed_pending_inspector_await(cdp_request_id)
|
||||
}
|
||||
|
||||
pub(crate) fn has_pending_inspector_awaits(&self) -> bool {
|
||||
@@ -715,6 +732,24 @@ impl SharedWorkerTargetState {
|
||||
.is_some_and(|state| !state.pending_inspector_awaits.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn has_claimed_pending_inspector_awaits_for_session(
|
||||
&self,
|
||||
session_id: &str,
|
||||
) -> bool {
|
||||
self.session_state(session_id)
|
||||
.is_some_and(DevToolsSessionState::has_claimed_pending_inspector_awaits)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn has_unclaimed_pending_inspector_awaits_for_session(
|
||||
&self,
|
||||
session_id: &str,
|
||||
) -> bool {
|
||||
self.session_state(session_id)
|
||||
.is_some_and(DevToolsSessionState::has_unclaimed_pending_inspector_awaits)
|
||||
}
|
||||
|
||||
pub(crate) fn pending_inspector_await_count_all_sessions(&self) -> usize {
|
||||
self.sessions
|
||||
.values()
|
||||
|
||||
@@ -74,12 +74,13 @@ impl PendingBidiChannelListener {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct PendingInspectorAwait {
|
||||
session_id: Option<String>,
|
||||
object_group: Option<String>,
|
||||
bidi_channel_listener: Option<crate::conn::BidiChannelListenerResidence>,
|
||||
renderer_correlation: Option<RendererCommandCorrelation>,
|
||||
scheduler_deferred_reply_claimed: bool,
|
||||
}
|
||||
|
||||
impl PendingInspectorAwait {
|
||||
@@ -100,9 +101,13 @@ impl PendingInspectorAwait {
|
||||
pub(crate) fn renderer_correlation(&self) -> Option<RendererCommandCorrelation> {
|
||||
self.renderer_correlation
|
||||
}
|
||||
|
||||
pub(crate) const fn scheduler_deferred_reply_claimed(&self) -> bool {
|
||||
self.scheduler_deferred_reply_claimed
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct TargetPendingInspectorAwaitRegistry {
|
||||
entries: PendingRendererCommandRegistry<PendingInspectorAwait>,
|
||||
}
|
||||
@@ -127,6 +132,7 @@ impl TargetPendingInspectorAwaitRegistry {
|
||||
object_group: object_group.map(str::to_owned),
|
||||
bidi_channel_listener: None,
|
||||
renderer_correlation: None,
|
||||
scheduler_deferred_reply_claimed: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -147,6 +153,7 @@ impl TargetPendingInspectorAwaitRegistry {
|
||||
object_group: object_group.map(str::to_owned),
|
||||
bidi_channel_listener: Some(listener),
|
||||
renderer_correlation,
|
||||
scheduler_deferred_reply_claimed: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -265,8 +272,60 @@ impl TargetPendingInspectorAwaitRegistry {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
/// Claims an await for a scheduler-owned deferred reply without moving it
|
||||
/// out of its DevTools session. The session registry remains the sole
|
||||
/// lifetime authority, so detach can settle the command before a late
|
||||
/// scheduler continuation resumes.
|
||||
pub(crate) fn claim_scheduler_deferred_reply(&mut self, cdp_request_id: u64) -> bool {
|
||||
let Some(entry) = self.entries.get_mut(FrontendCommandId::new(cdp_request_id)) else {
|
||||
return false;
|
||||
};
|
||||
if entry.scheduler_deferred_reply_claimed {
|
||||
return false;
|
||||
}
|
||||
entry.scheduler_deferred_reply_claimed = true;
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn take_claimed_scheduler_deferred_reply(
|
||||
&mut self,
|
||||
cdp_request_id: u64,
|
||||
) -> Option<PendingInspectorAwait> {
|
||||
let frontend_command_id = FrontendCommandId::new(cdp_request_id);
|
||||
if !self
|
||||
.entries
|
||||
.get_mut(frontend_command_id)?
|
||||
.scheduler_deferred_reply_claimed
|
||||
{
|
||||
return None;
|
||||
}
|
||||
self.entries.remove(frontend_command_id)
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, cdp_request_id: u64) -> Option<PendingInspectorAwait> {
|
||||
self.entries.remove(FrontendCommandId::new(cdp_request_id))
|
||||
let frontend_command_id = FrontendCommandId::new(cdp_request_id);
|
||||
if self
|
||||
.entries
|
||||
.get_mut(frontend_command_id)?
|
||||
.scheduler_deferred_reply_claimed
|
||||
{
|
||||
return None;
|
||||
}
|
||||
self.entries.remove(frontend_command_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn has_claimed_scheduler_deferred_reply(&self) -> bool {
|
||||
self.entries
|
||||
.iter()
|
||||
.any(|(_, entry)| entry.scheduler_deferred_reply_claimed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn has_unclaimed_await(&self) -> bool {
|
||||
self.entries
|
||||
.iter()
|
||||
.any(|(_, entry)| !entry.scheduler_deferred_reply_claimed)
|
||||
}
|
||||
|
||||
pub(crate) fn drain_all(&mut self) -> Vec<(u64, PendingInspectorAwait)> {
|
||||
|
||||
@@ -47,7 +47,7 @@ fn test_navigation_dispatch_state(fetch_request_id: &str) -> NavigationDispatchS
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_target_host_owns_session_state_directly() {
|
||||
fn page_agent_host_owns_session_state_directly() {
|
||||
let mut context = BrowserContext::new("CTX-session-state".into());
|
||||
context.set_active_target_id("TID-state-test");
|
||||
let target = context.active_page_target_mut();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use moli_core::{
|
||||
browser::{
|
||||
DocumentId, DocumentLifecycle, MainFrameSlotId, RendererPageResidenceIdentity,
|
||||
WebContentsId,
|
||||
BrowserSequence, DocumentId, DocumentLifecycle, MainFrameSlotId,
|
||||
RendererPageResidenceIdentity, WebContentsId,
|
||||
},
|
||||
runtime::{BuiltDocumentPage, PendingPreparedDocumentPage, PreparedDocumentPagePolicy},
|
||||
};
|
||||
@@ -308,12 +308,14 @@ impl WebContents {
|
||||
{
|
||||
self.navigation.mark_initial_empty_document_exited();
|
||||
}
|
||||
let browser_sequence = BrowserSequence::allocate();
|
||||
completion.finish(Ok(()));
|
||||
Ok(CommittedInitialDocument {
|
||||
key,
|
||||
inspection_endpoint,
|
||||
lifecycle: CommittedDocumentLifecycle {
|
||||
document: key.document,
|
||||
browser_sequence,
|
||||
artifacts: page_creation_artifacts,
|
||||
},
|
||||
diagnostics: page_creation_diagnostics,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use moli_core::{
|
||||
browser::{
|
||||
DocumentId, DocumentLifecycle, MainFrameSlotId, NavigationId,
|
||||
BrowserSequence, DocumentId, DocumentLifecycle, MainFrameSlotId, NavigationId,
|
||||
RendererPageResidenceIdentity, WebContentsId,
|
||||
},
|
||||
page::{Page, RendererPageCommandPostResponseContinuation, RendererPageCreationArtifacts},
|
||||
@@ -177,6 +177,7 @@ pub(crate) struct RetiringDocument(Option<Page>);
|
||||
/// initialize or rewind native state from a frontend projection.
|
||||
pub(crate) struct CommittedDocumentLifecycle {
|
||||
pub(crate) document: DocumentId,
|
||||
pub(crate) browser_sequence: BrowserSequence,
|
||||
pub(crate) artifacts: RendererPageCreationArtifacts,
|
||||
}
|
||||
|
||||
@@ -328,6 +329,7 @@ impl WebContents {
|
||||
self.navigation
|
||||
.commit_pending_document_navigation_if_matches(&navigation)
|
||||
);
|
||||
let browser_sequence = BrowserSequence::allocate();
|
||||
Ok(CommittedDocumentNavigation {
|
||||
web_contents: self.id(),
|
||||
frame_slot: self.main_frame.id(),
|
||||
@@ -338,6 +340,7 @@ impl WebContents {
|
||||
inspection_endpoint,
|
||||
lifecycle: CommittedDocumentLifecycle {
|
||||
document: document_id,
|
||||
browser_sequence,
|
||||
artifacts: prepared.creation_artifacts,
|
||||
},
|
||||
info: prepared.info,
|
||||
|
||||
+46
-38
@@ -7,19 +7,20 @@ use crate::devtools_runtime::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
CommittedAttachSession, DetachedTargetSession, PreparedAttachSession, TargetClosureCleanupPlan,
|
||||
TargetClosurePlan, TargetEventPlan, TargetHandlerStore, TargetHostDelta, TargetSessionRegistry,
|
||||
CommittedAttachSession, DetachedTargetSession, DevToolsSessionHandlerSet,
|
||||
PreparedAttachSession, TargetClosureCleanupPlan, TargetClosurePlan, TargetEventPlan,
|
||||
TargetHandlerStore, TargetHostDelta, TargetSessionRegistry,
|
||||
};
|
||||
use crate::conn::{BackgroundProtocolEvent, CdpSessionRoute, CdpTargetFilter};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct TargetControlPlane {
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct DevToolsAgentHostRegistry {
|
||||
sessions: TargetSessionRegistry,
|
||||
graph: TargetGraph,
|
||||
handlers: TargetHandlerStore,
|
||||
}
|
||||
|
||||
impl TargetControlPlane {
|
||||
impl DevToolsAgentHostRegistry {
|
||||
pub(crate) fn register_tab(&mut self, tab_target_id: String, primary_page_target_id: String) {
|
||||
self.graph
|
||||
.register_tab(tab_target_id, primary_page_target_id);
|
||||
@@ -468,6 +469,13 @@ impl TargetControlPlane {
|
||||
self.sessions.attached_session_route(session_id)
|
||||
}
|
||||
|
||||
pub(crate) fn attached_session_handler_set(
|
||||
&self,
|
||||
session_id: &str,
|
||||
) -> Option<DevToolsSessionHandlerSet> {
|
||||
self.sessions.attached_session_handler_set(session_id)
|
||||
}
|
||||
|
||||
pub(crate) fn browser_session_count(&self) -> usize {
|
||||
self.sessions.browser_session_count()
|
||||
}
|
||||
@@ -567,7 +575,7 @@ fn target_detached_event(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::conn::{CdpSessionRoute, CdpTargetFilter, TargetControlPlane};
|
||||
use crate::conn::{CdpSessionRoute, CdpTargetFilter, DevToolsAgentHostRegistry};
|
||||
use crate::devtools_runtime::{DevToolsTargetId, DevToolsTargetInfo, DevToolsTargetKind};
|
||||
|
||||
fn target_info(target_id: &str, kind: DevToolsTargetKind) -> DevToolsTargetInfo {
|
||||
@@ -587,18 +595,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn target_crashed_events_only_reach_discovery_owners_that_reported_the_host() {
|
||||
let mut control = TargetControlPlane::default();
|
||||
control.set_discover_targets(None, CdpTargetFilter::default_target_discovery());
|
||||
control.set_discover_targets(
|
||||
let mut registry = DevToolsAgentHostRegistry::default();
|
||||
registry.set_discover_targets(None, CdpTargetFilter::default_target_discovery());
|
||||
registry.set_discover_targets(
|
||||
Some("SID-reported"),
|
||||
CdpTargetFilter::default_target_discovery(),
|
||||
);
|
||||
control.set_discover_targets(
|
||||
registry.set_discover_targets(
|
||||
Some("SID-unreported"),
|
||||
CdpTargetFilter::default_target_discovery(),
|
||||
);
|
||||
assert_eq!(
|
||||
control
|
||||
registry
|
||||
.initial_target_created_events_for_owner(
|
||||
None,
|
||||
vec![target_info("TID-page", DevToolsTargetKind::Page)],
|
||||
@@ -607,7 +615,7 @@ mod tests {
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
control
|
||||
registry
|
||||
.initial_target_created_events_for_owner(
|
||||
Some("SID-reported"),
|
||||
vec![target_info("TID-page", DevToolsTargetKind::Page)],
|
||||
@@ -617,7 +625,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let events =
|
||||
control.target_crashed_events_for_all_discovery_owners("TID-page", "crashed", 5);
|
||||
registry.target_crashed_events_for_all_discovery_owners("TID-page", "crashed", 5);
|
||||
assert_eq!(events.len(), 2);
|
||||
let messages = events
|
||||
.into_iter()
|
||||
@@ -633,7 +641,7 @@ mod tests {
|
||||
assert_eq!(messages[1]["sessionId"], "SID-reported");
|
||||
|
||||
assert!(
|
||||
control
|
||||
registry
|
||||
.target_crashed_events_for_all_discovery_owners("TID-never-reported", "crashed", 5,)
|
||||
.is_empty()
|
||||
);
|
||||
@@ -641,15 +649,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn commit_auto_attached_session_event_records_committed_session_plan() {
|
||||
let mut control = TargetControlPlane::default();
|
||||
let mut registry = DevToolsAgentHostRegistry::default();
|
||||
let route = CdpSessionRoute::PageTarget {
|
||||
browser_context_id: "BID-1".to_owned(),
|
||||
target_id: "TID-page".to_owned(),
|
||||
session_key: moli_page_types::DevToolsSessionKey::Primary,
|
||||
};
|
||||
|
||||
control.ensure_owner(Some("SID-tab"));
|
||||
let plan = control.commit_attached_session_event(
|
||||
registry.ensure_owner(Some("SID-tab"));
|
||||
let plan = registry.commit_attached_session_event(
|
||||
"SID-page".to_owned(),
|
||||
Some("SID-tab"),
|
||||
"TID-page",
|
||||
@@ -660,11 +668,11 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
control.attached_sessions_for_target("TID-page"),
|
||||
registry.attached_sessions_for_target("TID-page"),
|
||||
vec!["SID-page".to_owned()]
|
||||
);
|
||||
assert_eq!(
|
||||
control.auto_attached_sessions_for_owner(Some("SID-tab")),
|
||||
registry.auto_attached_sessions_for_owner(Some("SID-tab")),
|
||||
vec!["SID-page".to_owned()]
|
||||
);
|
||||
|
||||
@@ -683,13 +691,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn detach_attached_session_event_plan_records_detached_session() {
|
||||
let mut control = TargetControlPlane::default();
|
||||
let mut registry = DevToolsAgentHostRegistry::default();
|
||||
let route = CdpSessionRoute::PageTarget {
|
||||
browser_context_id: "BID-1".to_owned(),
|
||||
target_id: "TID-page".to_owned(),
|
||||
session_key: moli_page_types::DevToolsSessionKey::Primary,
|
||||
};
|
||||
control.commit_attached_session_event(
|
||||
registry.commit_attached_session_event(
|
||||
"SID-page".to_owned(),
|
||||
Some("SID-owner"),
|
||||
"TID-page",
|
||||
@@ -699,7 +707,7 @@ mod tests {
|
||||
target_info("TID-page", DevToolsTargetKind::Page),
|
||||
);
|
||||
|
||||
let plan = control
|
||||
let plan = registry
|
||||
.detach_attached_session_event_plan(
|
||||
"SID-page",
|
||||
Some("Target closed"),
|
||||
@@ -707,7 +715,7 @@ mod tests {
|
||||
)
|
||||
.expect("attached session should detach");
|
||||
|
||||
assert!(control.attached_sessions_for_target("TID-page").is_empty());
|
||||
assert!(registry.attached_sessions_for_target("TID-page").is_empty());
|
||||
let detached_sessions = plan.detached_sessions();
|
||||
assert_eq!(detached_sessions.len(), 1);
|
||||
let detached = &detached_sessions[0];
|
||||
@@ -727,9 +735,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn target_closure_attached_sessions_event_plan_detaches_all_target_sessions() {
|
||||
let mut control = TargetControlPlane::default();
|
||||
let mut registry = DevToolsAgentHostRegistry::default();
|
||||
for session_id in ["SID-a", "SID-b"] {
|
||||
control.commit_attached_session_event(
|
||||
registry.commit_attached_session_event(
|
||||
session_id.to_owned(),
|
||||
None,
|
||||
"TID-page",
|
||||
@@ -744,13 +752,13 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
let plan = control.detach_target_closure_attached_sessions_event_plan(
|
||||
let plan = registry.detach_target_closure_attached_sessions_event_plan(
|
||||
"TID-page",
|
||||
Some("Target closed"),
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(control.attached_sessions_for_target("TID-page").is_empty());
|
||||
assert!(registry.attached_sessions_for_target("TID-page").is_empty());
|
||||
assert_eq!(
|
||||
plan.detached_sessions()
|
||||
.iter()
|
||||
@@ -763,9 +771,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn target_closure_cleanup_event_plan_detaches_declared_sessions() {
|
||||
let mut control = TargetControlPlane::default();
|
||||
let mut registry = DevToolsAgentHostRegistry::default();
|
||||
for session_id in ["SID-primary", "SID-attached"] {
|
||||
control.commit_attached_session_event(
|
||||
registry.commit_attached_session_event(
|
||||
session_id.to_owned(),
|
||||
None,
|
||||
"TID-page",
|
||||
@@ -780,7 +788,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
let plan = control.detach_target_closure_cleanup_event_plan(
|
||||
let plan = registry.detach_target_closure_cleanup_event_plan(
|
||||
crate::conn::TargetClosureCleanupPlan::new(
|
||||
"TID-page",
|
||||
Some("Render process gone."),
|
||||
@@ -789,7 +797,7 @@ mod tests {
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(control.attached_sessions_for_target("TID-page").is_empty());
|
||||
assert!(registry.attached_sessions_for_target("TID-page").is_empty());
|
||||
assert_eq!(
|
||||
plan.detached_sessions()
|
||||
.iter()
|
||||
@@ -802,9 +810,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rollback_attached_session_without_event_clears_session_indexes() {
|
||||
let mut control = TargetControlPlane::default();
|
||||
control.ensure_owner(Some("SID-tab"));
|
||||
control.commit_attached_session_event(
|
||||
let mut registry = DevToolsAgentHostRegistry::default();
|
||||
registry.ensure_owner(Some("SID-tab"));
|
||||
registry.commit_attached_session_event(
|
||||
"SID-page".to_owned(),
|
||||
Some("SID-tab"),
|
||||
"TID-page",
|
||||
@@ -818,21 +826,21 @@ mod tests {
|
||||
target_info("TID-page", DevToolsTargetKind::Page),
|
||||
);
|
||||
|
||||
let rollback_plan = control.rollback_attached_session_without_event("SID-page");
|
||||
let rollback_plan = registry.rollback_attached_session_without_event("SID-page");
|
||||
assert_eq!(
|
||||
rollback_plan.rolled_back_session_ids(),
|
||||
&["SID-page".to_owned()]
|
||||
);
|
||||
assert!(rollback_plan.into_background_events().is_empty());
|
||||
|
||||
assert!(control.attached_sessions_for_target("TID-page").is_empty());
|
||||
assert!(registry.attached_sessions_for_target("TID-page").is_empty());
|
||||
assert!(
|
||||
control
|
||||
registry
|
||||
.auto_attached_sessions_for_owner(Some("SID-tab"))
|
||||
.is_empty()
|
||||
);
|
||||
assert!(
|
||||
control
|
||||
registry
|
||||
.rollback_attached_session_without_event("SID-page")
|
||||
.rolled_back_session_ids()
|
||||
.is_empty()
|
||||
@@ -45,7 +45,7 @@ impl CdpConnection {
|
||||
self.clear_service_worker_auto_attach_related_owner(session_id);
|
||||
let key = session_id.map(str::to_owned);
|
||||
if enabled {
|
||||
self.target_control.ensure_owner(session_id);
|
||||
self.agent_hosts.ensure_owner(session_id);
|
||||
self.auto_attach_owner_sessions.insert(
|
||||
key,
|
||||
AutoAttachOwnerPolicy {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod activation;
|
||||
mod agent_host_registry;
|
||||
mod auto_attach_owner;
|
||||
mod control;
|
||||
mod default_target;
|
||||
mod graph;
|
||||
mod observer;
|
||||
@@ -13,13 +13,14 @@ mod worker_auto_attach;
|
||||
mod worker_session;
|
||||
|
||||
pub(crate) use activation::TargetActivationTransition;
|
||||
pub(crate) use control::TargetControlPlane;
|
||||
pub(crate) use agent_host_registry::DevToolsAgentHostRegistry;
|
||||
pub(crate) use default_target::{DEFAULT_BROWSER_CONTEXT_ID, DefaultTargetLifecycle};
|
||||
pub(crate) use graph::{TargetClosurePlan, TargetHostDelta};
|
||||
pub(crate) use observer::{TargetHandlerStore, target_destroyed_automation_events};
|
||||
pub(crate) use route::{CdpSessionRoute, TargetHandlerAccessMode};
|
||||
pub(crate) use session::{
|
||||
CommittedAttachSession, DetachedTargetSession, PreparedAttachSession, TargetSessionRegistry,
|
||||
CommittedAttachSession, DetachedTargetSession, DevToolsSessionHandlerSet,
|
||||
PreparedAttachSession, TargetSessionRegistry,
|
||||
};
|
||||
pub(crate) use transaction::{
|
||||
PreparedTargetAttach, PreparedTargetHostClosure, PreparedTargetHostDelta, SessionDisposalPlan,
|
||||
|
||||
@@ -177,7 +177,7 @@ impl CdpConnection {
|
||||
.as_deref()
|
||||
== Some(target_id)
|
||||
|| self
|
||||
.target_control
|
||||
.agent_hosts
|
||||
.auto_attached_target_ids_for_owner(Some(session_id))
|
||||
.iter()
|
||||
.any(|attached_target_id| attached_target_id == target_id)
|
||||
@@ -185,9 +185,7 @@ impl CdpConnection {
|
||||
|
||||
pub(crate) fn session_route(&self, session_id: Option<&str>) -> Option<CdpSessionRoute> {
|
||||
let session_id = session_id?;
|
||||
self.target_control
|
||||
.attached_session_route(session_id)
|
||||
.cloned()
|
||||
self.agent_hosts.attached_session_route(session_id).cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn target_session_route_for_target_id(
|
||||
@@ -313,7 +311,7 @@ mod tests {
|
||||
target_id: "TID-a".to_owned(),
|
||||
session_key: DevToolsSessionKey::Primary,
|
||||
};
|
||||
connection.target_control.commit_attached_session(
|
||||
connection.agent_hosts.commit_attached_session(
|
||||
"SID-a".to_owned(),
|
||||
None,
|
||||
"TID-a",
|
||||
|
||||
@@ -2,12 +2,36 @@ use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::conn::CdpSessionRoute;
|
||||
|
||||
/// Browser-side domain handlers installed for one attached DevTools session.
|
||||
/// The attachment registry freezes this class at commit and disposal carries
|
||||
/// it forward instead of reconstructing ownership from teardown state.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum DevToolsSessionHandlerSet {
|
||||
Connection,
|
||||
Page,
|
||||
Worker,
|
||||
}
|
||||
|
||||
impl DevToolsSessionHandlerSet {
|
||||
pub(crate) fn for_route(route: &CdpSessionRoute) -> Option<Self> {
|
||||
match route {
|
||||
CdpSessionRoute::Browser | CdpSessionRoute::TabTarget { .. } => Some(Self::Connection),
|
||||
CdpSessionRoute::PageTarget { .. } => Some(Self::Page),
|
||||
CdpSessionRoute::SharedWorkerTarget { .. }
|
||||
| CdpSessionRoute::DedicatedWorkerTarget { .. }
|
||||
| CdpSessionRoute::ServiceWorkerTarget { .. } => Some(Self::Worker),
|
||||
CdpSessionRoute::BrowserContext { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct PreparedAttachSession {
|
||||
session_id: String,
|
||||
owner_session_id: Option<String>,
|
||||
target_id: String,
|
||||
route: CdpSessionRoute,
|
||||
handler_set: DevToolsSessionHandlerSet,
|
||||
auto_attached: bool,
|
||||
waiting_for_debugger: bool,
|
||||
}
|
||||
@@ -21,11 +45,14 @@ impl PreparedAttachSession {
|
||||
auto_attached: bool,
|
||||
waiting_for_debugger: bool,
|
||||
) -> Self {
|
||||
let handler_set = DevToolsSessionHandlerSet::for_route(&route)
|
||||
.expect("attached DevTools session must have a disposable route");
|
||||
Self {
|
||||
session_id,
|
||||
owner_session_id: owner_session_id.map(str::to_owned),
|
||||
target_id: target_id.to_owned(),
|
||||
route,
|
||||
handler_set,
|
||||
auto_attached,
|
||||
waiting_for_debugger,
|
||||
}
|
||||
@@ -38,6 +65,7 @@ pub(crate) struct CommittedAttachSession {
|
||||
owner_session_id: Option<String>,
|
||||
target_id: String,
|
||||
route: CdpSessionRoute,
|
||||
handler_set: DevToolsSessionHandlerSet,
|
||||
auto_attached: bool,
|
||||
waiting_for_debugger: bool,
|
||||
}
|
||||
@@ -154,11 +182,12 @@ struct AttachedTargetSession {
|
||||
owner_session_id: Option<String>,
|
||||
target_id: String,
|
||||
route: CdpSessionRoute,
|
||||
handler_set: DevToolsSessionHandlerSet,
|
||||
auto_attached: bool,
|
||||
waiting_for_debugger: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct TargetSessionRegistry {
|
||||
attached_sessions: HashMap<String, AttachedTargetSession>,
|
||||
attached_sessions_by_target: HashMap<String, HashSet<String>>,
|
||||
@@ -177,6 +206,7 @@ impl TargetSessionRegistry {
|
||||
let owner_session_id = prepared.owner_session_id;
|
||||
let target_id = prepared.target_id;
|
||||
let route = prepared.route;
|
||||
let handler_set = prepared.handler_set;
|
||||
let auto_attached = prepared.auto_attached;
|
||||
let waiting_for_debugger = prepared.waiting_for_debugger;
|
||||
|
||||
@@ -187,6 +217,7 @@ impl TargetSessionRegistry {
|
||||
owner_session_id: owner_session_id.clone(),
|
||||
target_id: target_id.clone(),
|
||||
route: route.clone(),
|
||||
handler_set,
|
||||
auto_attached,
|
||||
waiting_for_debugger,
|
||||
},
|
||||
@@ -213,6 +244,7 @@ impl TargetSessionRegistry {
|
||||
owner_session_id,
|
||||
target_id,
|
||||
route,
|
||||
handler_set,
|
||||
auto_attached,
|
||||
waiting_for_debugger,
|
||||
}
|
||||
@@ -343,6 +375,15 @@ impl TargetSessionRegistry {
|
||||
.map(|session| &session.route)
|
||||
}
|
||||
|
||||
pub(crate) fn attached_session_handler_set(
|
||||
&self,
|
||||
session_id: &str,
|
||||
) -> Option<DevToolsSessionHandlerSet> {
|
||||
self.attached_sessions
|
||||
.get(session_id)
|
||||
.map(|session| session.handler_set)
|
||||
}
|
||||
|
||||
pub(crate) fn browser_session_count(&self) -> usize {
|
||||
self.attached_sessions
|
||||
.values()
|
||||
@@ -562,8 +603,8 @@ fn test_route_for_target(session_id: &str, target_id: &str) -> CdpSessionRoute {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
PreparedAttachSession, PreparedAutoAttachSession, TargetSessionRegistry,
|
||||
test_route_for_target,
|
||||
DevToolsSessionHandlerSet, PreparedAttachSession, PreparedAutoAttachSession,
|
||||
TargetSessionRegistry, test_route_for_target,
|
||||
};
|
||||
use crate::conn::CdpSessionRoute;
|
||||
|
||||
@@ -731,11 +772,13 @@ mod tests {
|
||||
false,
|
||||
false,
|
||||
));
|
||||
let expected_handlers = DevToolsSessionHandlerSet::Connection;
|
||||
|
||||
assert_eq!(committed.session_id, "SID-tab");
|
||||
assert_eq!(committed.owner_session_id.as_deref(), Some("SID-browser"));
|
||||
assert_eq!(committed.target_id, "TAB-TID-page");
|
||||
assert_eq!(committed.route, route);
|
||||
assert_eq!(committed.handler_set, expected_handlers);
|
||||
assert!(!committed.auto_attached);
|
||||
assert!(!committed.waiting_for_debugger);
|
||||
assert_eq!(
|
||||
@@ -743,6 +786,10 @@ mod tests {
|
||||
vec!["SID-tab".to_owned()]
|
||||
);
|
||||
assert_eq!(registry.attached_session_route("SID-tab"), Some(&route));
|
||||
assert_eq!(
|
||||
registry.attached_session_handler_set("SID-tab"),
|
||||
Some(expected_handlers)
|
||||
);
|
||||
assert!(
|
||||
registry
|
||||
.auto_attached_sessions_for_owner(Some("SID-browser"))
|
||||
|
||||
@@ -10,8 +10,17 @@ use super::{
|
||||
};
|
||||
|
||||
impl CdpConnection {
|
||||
/// Freezes the exact AgentHost route and installed handler set before
|
||||
/// asynchronous cleanup. The registry entry stays live until the caller
|
||||
/// commits this plan's binding removal.
|
||||
pub(crate) fn session_disposal_plan(&self, session_id: &str) -> Option<SessionDisposalPlan> {
|
||||
let route = self.agent_hosts.attached_session_route(session_id)?;
|
||||
let handler_set = self.agent_hosts.attached_session_handler_set(session_id)?;
|
||||
SessionDisposalPlan::for_attached_session(session_id, route, handler_set)
|
||||
}
|
||||
|
||||
/// Commits target-side session declarations built by low-level test
|
||||
/// fixtures into the same control-plane registry used by production
|
||||
/// fixtures into the same AgentHost registry used by production
|
||||
/// attachment transactions.
|
||||
///
|
||||
/// Older tests construct `BrowserContext` and worker targets directly,
|
||||
@@ -91,13 +100,13 @@ impl CdpConnection {
|
||||
|
||||
for (session_id, target_id, route) in declared {
|
||||
if self
|
||||
.target_control
|
||||
.agent_hosts
|
||||
.attached_session_route(&session_id)
|
||||
.is_some()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.commit_attached_session(session_id, None, &target_id, route, false, false);
|
||||
}
|
||||
}
|
||||
@@ -199,12 +208,12 @@ impl CdpConnection {
|
||||
let Some(session_id) = session_id else {
|
||||
return false;
|
||||
};
|
||||
self.target_control.attached_session_route(session_id) == Some(&CdpSessionRoute::Browser)
|
||||
self.agent_hosts.attached_session_route(session_id) == Some(&CdpSessionRoute::Browser)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn register_browser_session(&mut self, session_id: String) {
|
||||
self.target_control.commit_attached_session(
|
||||
self.agent_hosts.commit_attached_session(
|
||||
session_id,
|
||||
None,
|
||||
"browser",
|
||||
@@ -236,19 +245,19 @@ impl CdpConnection {
|
||||
"InvalidSessionId"
|
||||
);
|
||||
let owner_session_id = self
|
||||
.target_control
|
||||
.agent_hosts
|
||||
.attached_session_owner_session_id(plan.session_id())
|
||||
.map(str::to_owned);
|
||||
let session_id = plan.session_id().to_owned();
|
||||
let event_plan = self
|
||||
.target_control
|
||||
.agent_hosts
|
||||
.detach_attached_session_event_plan(
|
||||
plan.session_id(),
|
||||
None,
|
||||
owner_session_id.as_deref(),
|
||||
)
|
||||
.ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?;
|
||||
self.remove_detached_session_control_owner(&session_id);
|
||||
self.remove_detached_session_handler_owner(&session_id);
|
||||
Ok(event_plan)
|
||||
}
|
||||
|
||||
@@ -257,7 +266,7 @@ impl CdpConnection {
|
||||
self.cancel_tracing_for_session_owner(None);
|
||||
self.clear_auto_attach_owner(None);
|
||||
self.clear_target_discovery_for_owner(None);
|
||||
self.target_control.remove_owner(None);
|
||||
self.agent_hosts.remove_owner(None);
|
||||
}
|
||||
|
||||
pub(crate) fn release_primary_target_session_binding_without_event(
|
||||
@@ -292,7 +301,7 @@ impl CdpConnection {
|
||||
let target_id = route.target_id().unwrap_or_else(|| {
|
||||
panic!("test auto-attached session route must identify a target: {route:?}")
|
||||
});
|
||||
self.target_control.commit_auto_attached_session_for_target(
|
||||
self.agent_hosts.commit_auto_attached_session_for_target(
|
||||
session_id,
|
||||
owner_session_id,
|
||||
target_id,
|
||||
@@ -320,7 +329,7 @@ impl CdpConnection {
|
||||
route: CdpSessionRoute,
|
||||
) {
|
||||
let target_id = route.target_id().unwrap_or("browser").to_owned();
|
||||
self.target_control.commit_attached_session(
|
||||
self.agent_hosts.commit_attached_session(
|
||||
session_id.to_owned(),
|
||||
None,
|
||||
&target_id,
|
||||
@@ -358,10 +367,9 @@ impl CdpConnection {
|
||||
let (session_id, owner_session_id, route, auto_attached, waiting_for_debugger) =
|
||||
session.into_parts();
|
||||
if auto_attached {
|
||||
self.target_control
|
||||
.ensure_owner(owner_session_id.as_deref());
|
||||
self.agent_hosts.ensure_owner(owner_session_id.as_deref());
|
||||
}
|
||||
plan.extend(self.target_control.commit_attached_session_event(
|
||||
plan.extend(self.agent_hosts.commit_attached_session_event(
|
||||
session_id,
|
||||
owner_session_id.as_deref(),
|
||||
&target_id,
|
||||
@@ -690,7 +698,7 @@ impl CdpConnection {
|
||||
target_id: &str,
|
||||
target_info: DevToolsTargetInfo,
|
||||
) -> TargetEventPlan {
|
||||
self.target_control.commit_attached_session_event(
|
||||
self.agent_hosts.commit_attached_session_event(
|
||||
session_id,
|
||||
owner_session_id,
|
||||
target_id,
|
||||
@@ -706,10 +714,10 @@ impl CdpConnection {
|
||||
session_id: &str,
|
||||
) -> TargetEventPlan {
|
||||
let plan = self
|
||||
.target_control
|
||||
.agent_hosts
|
||||
.rollback_attached_session_without_event(session_id);
|
||||
for session_id in plan.rolled_back_session_ids() {
|
||||
self.remove_detached_session_control_owner(session_id);
|
||||
self.remove_detached_session_handler_owner(session_id);
|
||||
}
|
||||
plan
|
||||
}
|
||||
@@ -740,7 +748,7 @@ impl CdpConnection {
|
||||
) -> TargetEventPlan {
|
||||
let attached_state_delta_plan = emit_attached_state_delta
|
||||
.then(|| self.exact_target_info_changed_event_plan_for_target_delta(target_id));
|
||||
let mut plan = self.target_control.detach_known_session_event_plan(
|
||||
let mut plan = self.agent_hosts.detach_known_session_event_plan(
|
||||
target_id,
|
||||
session_id,
|
||||
reason,
|
||||
@@ -750,7 +758,7 @@ impl CdpConnection {
|
||||
.detached_sessions()
|
||||
.iter()
|
||||
.any(|session| session.target_id() == target_id && session.was_waiting_for_debugger());
|
||||
self.remove_detached_session_control_owner(session_id);
|
||||
self.remove_detached_session_handler_owner(session_id);
|
||||
if let Some(attached_state_delta_plan) = attached_state_delta_plan {
|
||||
plan.extend(attached_state_delta_plan);
|
||||
}
|
||||
@@ -773,18 +781,10 @@ impl CdpConnection {
|
||||
.map(str::to_owned)
|
||||
.collect::<Vec<_>>();
|
||||
for session_id in session_ids {
|
||||
let Some(route) = self.session_route(Some(&session_id)) else {
|
||||
let Some(disposal_plan) = self.session_disposal_plan(&session_id) else {
|
||||
tracing::warn!(
|
||||
session_id,
|
||||
"closed target session no longer has an authoritative route"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let Some(disposal_plan) = SessionDisposalPlan::for_session_route(&session_id, &route)
|
||||
else {
|
||||
tracing::warn!(
|
||||
session_id,
|
||||
"closed target session does not support target disposal"
|
||||
"closed target session no longer has an authoritative disposal binding"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
@@ -800,10 +800,10 @@ impl CdpConnection {
|
||||
parent_session_id: Option<&str>,
|
||||
) -> TargetEventPlan {
|
||||
let plan = self
|
||||
.target_control
|
||||
.agent_hosts
|
||||
.detach_target_closure_cleanup_event_plan(cleanup_plan, parent_session_id);
|
||||
for session in plan.detached_sessions() {
|
||||
self.remove_detached_session_control_owner(session.session_id());
|
||||
self.remove_detached_session_handler_owner(session.session_id());
|
||||
}
|
||||
plan
|
||||
}
|
||||
@@ -907,7 +907,7 @@ impl CdpConnection {
|
||||
%error,
|
||||
"failed to clean prepared target binding during attach rollback"
|
||||
);
|
||||
// Keep both the domain binding and its control-plane route as
|
||||
// Keep both the domain binding and its AgentHost route as
|
||||
// retry authority. Dropping only the latter would make any
|
||||
// renderer-owned state unreachable.
|
||||
return TargetEventPlan::default();
|
||||
@@ -919,10 +919,10 @@ impl CdpConnection {
|
||||
&self,
|
||||
session_id: &str,
|
||||
) -> TargetAutoAttachedSessionDetachPlan {
|
||||
let route = self.session_route(Some(session_id)).unwrap_or_else(|| {
|
||||
panic!("committed auto-attached session {session_id} must retain its route")
|
||||
let disposal_plan = self.session_disposal_plan(session_id).unwrap_or_else(|| {
|
||||
panic!("committed auto-attached session {session_id} must retain its disposal binding")
|
||||
});
|
||||
TargetAutoAttachedSessionDetachPlan::from_session_route(session_id, route)
|
||||
TargetAutoAttachedSessionDetachPlan::from_session_disposal_plan(disposal_plan)
|
||||
}
|
||||
|
||||
pub(crate) fn rollback_auto_attached_session_detach_plan_without_event(
|
||||
@@ -1034,7 +1034,7 @@ impl CdpConnection {
|
||||
let parent_session_id = cleanup_plan
|
||||
.parent_session_id()
|
||||
.or_else(|| {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.attached_session_owner_session_id(&session_id)
|
||||
})
|
||||
.map(str::to_owned);
|
||||
@@ -1056,16 +1056,16 @@ impl CdpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_detached_session_control_owner(&mut self, session_id: &str) {
|
||||
self.target_control.remove_owner(Some(session_id));
|
||||
fn remove_detached_session_handler_owner(&mut self, session_id: &str) {
|
||||
self.agent_hosts.remove_owner(Some(session_id));
|
||||
}
|
||||
|
||||
pub(crate) fn attached_sessions_for_target(&self, target_id: &str) -> Vec<String> {
|
||||
self.target_control.attached_sessions_for_target(target_id)
|
||||
self.agent_hosts.attached_sessions_for_target(target_id)
|
||||
}
|
||||
|
||||
pub(crate) fn target_has_waiting_for_debugger_session(&self, target_id: &str) -> bool {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.target_has_waiting_for_debugger_session(target_id)
|
||||
}
|
||||
|
||||
@@ -1074,7 +1074,7 @@ impl CdpConnection {
|
||||
session_id: Option<&str>,
|
||||
) -> bool {
|
||||
session_id.is_some_and(|session_id| {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.release_waiting_for_debugger_session(session_id)
|
||||
})
|
||||
}
|
||||
@@ -1083,7 +1083,7 @@ impl CdpConnection {
|
||||
&self,
|
||||
owner_session_id: Option<&str>,
|
||||
) -> Vec<String> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.auto_attached_sessions_for_owner(owner_session_id)
|
||||
}
|
||||
|
||||
@@ -1091,12 +1091,12 @@ impl CdpConnection {
|
||||
&self,
|
||||
owner_session_id: Option<&str>,
|
||||
) -> Vec<String> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.attached_session_cascade_for_owner(owner_session_id)
|
||||
}
|
||||
|
||||
pub(crate) fn attached_session_cascade_for_root_frontend(&self) -> Vec<String> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.attached_session_cascade_for_root_frontend()
|
||||
}
|
||||
|
||||
@@ -1104,7 +1104,7 @@ impl CdpConnection {
|
||||
&self,
|
||||
owner_session_id: Option<&str>,
|
||||
) -> Vec<String> {
|
||||
self.target_control
|
||||
self.agent_hosts
|
||||
.auto_attached_session_cascade_for_owner(owner_session_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ use crate::conn::{BackgroundProtocolEvent, CdpSessionRoute};
|
||||
use crate::devtools_runtime::DevToolsTargetInfo;
|
||||
use moli_page_types::DevToolsSessionKey;
|
||||
|
||||
use super::{CommittedAttachSession, DetachedTargetSession, TargetHostDelta};
|
||||
use super::{
|
||||
CommittedAttachSession, DetachedTargetSession, DevToolsSessionHandlerSet, TargetHostDelta,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub(crate) struct TargetEventPlan {
|
||||
@@ -305,19 +307,35 @@ impl PreparedTargetAttach {
|
||||
pub(crate) struct SessionDisposalPlan {
|
||||
session_id: String,
|
||||
target: SessionDisposalTarget,
|
||||
handler_set: DevToolsSessionHandlerSet,
|
||||
}
|
||||
|
||||
impl SessionDisposalPlan {
|
||||
/// Freezes the exact binding that must remain alive while every domain
|
||||
/// handler disables its session-owned state.
|
||||
///
|
||||
/// Callers must execute the domain cleanup phase before committing this
|
||||
/// plan's binding removal. Keeping both phases tied to one value prevents
|
||||
/// detach paths from resolving the session again after asynchronous work.
|
||||
pub(crate) fn for_session_route(session_id: &str, route: &CdpSessionRoute) -> Option<Self> {
|
||||
/// Attach rollback has no committed registry entry, so it derives the
|
||||
/// otherwise immutable handler set from the prepared route.
|
||||
fn for_uncommitted_session_route(session_id: &str, route: &CdpSessionRoute) -> Option<Self> {
|
||||
let target = SessionDisposalTarget::from_route(route)?;
|
||||
Some(Self {
|
||||
session_id: session_id.to_owned(),
|
||||
target: SessionDisposalTarget::from_route(route)?,
|
||||
handler_set: DevToolsSessionHandlerSet::for_route(route)?,
|
||||
target,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn for_attached_session(
|
||||
session_id: &str,
|
||||
route: &CdpSessionRoute,
|
||||
handler_set: DevToolsSessionHandlerSet,
|
||||
) -> Option<Self> {
|
||||
let target = SessionDisposalTarget::from_route(route)?;
|
||||
debug_assert_eq!(
|
||||
Some(handler_set),
|
||||
DevToolsSessionHandlerSet::for_route(route)
|
||||
);
|
||||
Some(Self {
|
||||
session_id: session_id.to_owned(),
|
||||
target,
|
||||
handler_set,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -333,6 +351,10 @@ impl SessionDisposalPlan {
|
||||
&self.target
|
||||
}
|
||||
|
||||
pub(crate) fn handler_set(&self) -> DevToolsSessionHandlerSet {
|
||||
self.handler_set
|
||||
}
|
||||
|
||||
pub(crate) fn target_id(&self) -> Option<&str> {
|
||||
self.target.target_id()
|
||||
}
|
||||
@@ -451,7 +473,8 @@ pub(crate) struct TargetAttachRollbackPlan {
|
||||
impl TargetAttachRollbackPlan {
|
||||
pub(crate) fn from_prepared_attach_session(prepared: &TargetAttachSessionCommit) -> Self {
|
||||
let session_id = prepared.session_id().to_owned();
|
||||
let cleanup_plan = SessionDisposalPlan::for_session_route(&session_id, prepared.route());
|
||||
let cleanup_plan =
|
||||
SessionDisposalPlan::for_uncommitted_session_route(&session_id, prepared.route());
|
||||
Self {
|
||||
session_id,
|
||||
cleanup_plan,
|
||||
@@ -474,20 +497,27 @@ pub(crate) enum TargetAutoAttachedSessionDetachPlan {
|
||||
}
|
||||
|
||||
impl TargetAutoAttachedSessionDetachPlan {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn from_session_route(
|
||||
session_id: impl Into<String>,
|
||||
route: CdpSessionRoute,
|
||||
) -> Self {
|
||||
let session_id = session_id.into();
|
||||
if matches!(route, CdpSessionRoute::Browser) {
|
||||
return Self::Rollback { session_id };
|
||||
}
|
||||
match SessionDisposalPlan::for_session_route(&session_id, &route) {
|
||||
Some(cleanup_plan) => Self::Detach { cleanup_plan },
|
||||
match SessionDisposalPlan::for_uncommitted_session_route(&session_id, &route) {
|
||||
Some(cleanup_plan) => Self::from_session_disposal_plan(cleanup_plan),
|
||||
None => Self::Rollback { session_id },
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_session_disposal_plan(cleanup_plan: SessionDisposalPlan) -> Self {
|
||||
if matches!(cleanup_plan.target(), SessionDisposalTarget::Browser) {
|
||||
return Self::Rollback {
|
||||
session_id: cleanup_plan.session_id().to_owned(),
|
||||
};
|
||||
}
|
||||
Self::Detach { cleanup_plan }
|
||||
}
|
||||
|
||||
pub(crate) fn session_id(&self) -> &str {
|
||||
match self {
|
||||
Self::Rollback { session_id } => session_id,
|
||||
@@ -606,15 +636,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn target_binding_cleanup_plan_maps_route_to_cleanup_action() {
|
||||
let browser =
|
||||
SessionDisposalPlan::for_session_route("SID-browser", &CdpSessionRoute::Browser)
|
||||
.expect("Browser sessions require domain cleanup");
|
||||
let browser = SessionDisposalPlan::for_uncommitted_session_route(
|
||||
"SID-browser",
|
||||
&CdpSessionRoute::Browser,
|
||||
)
|
||||
.expect("Browser sessions require domain cleanup");
|
||||
assert_eq!(browser.target(), &SessionDisposalTarget::Browser);
|
||||
assert_eq!(browser.browser_context_id(), None);
|
||||
assert_eq!(browser.target_id(), None);
|
||||
|
||||
assert_eq!(
|
||||
SessionDisposalPlan::for_session_route(
|
||||
SessionDisposalPlan::for_uncommitted_session_route(
|
||||
"SID-active",
|
||||
&CdpSessionRoute::PageTarget {
|
||||
browser_context_id: "BID-1".to_owned(),
|
||||
@@ -632,7 +664,7 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SessionDisposalPlan::for_session_route(
|
||||
SessionDisposalPlan::for_uncommitted_session_route(
|
||||
"SID-bg",
|
||||
&CdpSessionRoute::PageTarget {
|
||||
browser_context_id: "BID-1".to_owned(),
|
||||
@@ -650,7 +682,7 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SessionDisposalPlan::for_session_route(
|
||||
SessionDisposalPlan::for_uncommitted_session_route(
|
||||
"SID-tab",
|
||||
&CdpSessionRoute::TabTarget {
|
||||
browser_context_id: "BID-1".to_owned(),
|
||||
|
||||
@@ -790,7 +790,7 @@ async fn memory_diagnostics_reports_page_vm_document_isolate_model() {
|
||||
assert_eq!(
|
||||
diagnostics["isolateScope"]["loadedDocumentRendererOwnerCount"],
|
||||
json!(2),
|
||||
"each loaded PageTargetHost owns an independently schedulable renderer owner"
|
||||
"each loaded PageAgentHost owns an independently schedulable renderer owner"
|
||||
);
|
||||
assert_eq!(
|
||||
diagnostics["isolateScope"]["estimatedDocumentIsolateCount"],
|
||||
@@ -978,7 +978,7 @@ async fn memory_diagnostics_excludes_empty_page_hosts_from_document_isolates() {
|
||||
let empty_page_engine = conn
|
||||
.browser_context_by_id("BID-empty-page")
|
||||
.and_then(|context| context.page_navigation_engine("TID-empty-page"))
|
||||
.expect("empty PageTargetHost must own its navigation engine");
|
||||
.expect("empty PageAgentHost must own its navigation engine");
|
||||
assert!(
|
||||
!empty_page_engine.shares_renderer_owner_with(conn.standalone_navigation_engine.ensure()),
|
||||
"test setup must keep a distinct renderer owner without a loaded document"
|
||||
@@ -1177,10 +1177,10 @@ fn memory_diagnostics_counts_isolated_page_engines_by_renderer_owner() {
|
||||
.browser_context
|
||||
.as_ref()
|
||||
.and_then(|context| context.page_navigation_engine("TID-shared-diagnostics-bg"))
|
||||
.expect("background PageTargetHost must own an engine");
|
||||
.expect("background PageAgentHost must own an engine");
|
||||
assert!(
|
||||
!background.shares_renderer_owner_with(active),
|
||||
"same-context PageTargetHost engines must remain independently schedulable"
|
||||
"same-context PageAgentHost engines must remain independently schedulable"
|
||||
);
|
||||
|
||||
let diagnostics = conn.moli_memory_diagnostics();
|
||||
@@ -1188,7 +1188,7 @@ fn memory_diagnostics_counts_isolated_page_engines_by_renderer_owner() {
|
||||
assert_eq!(
|
||||
diagnostics["connection"]["pageNavigationEngineCount"],
|
||||
json!(2),
|
||||
"each PageTargetHost must expose one resident NavigationEngine"
|
||||
"each PageAgentHost must expose one resident NavigationEngine"
|
||||
);
|
||||
assert_eq!(
|
||||
diagnostics["isolateScope"]["pageNavigationEngineRendererOwnerCount"],
|
||||
@@ -1243,7 +1243,7 @@ fn page_navigation_engines_remain_target_local_across_selection() {
|
||||
.browser_identity()
|
||||
.user_agent(),
|
||||
"Moli/Page-First",
|
||||
"selection must not replace or move a PageTargetHost engine"
|
||||
"selection must not replace or move a PageAgentHost engine"
|
||||
);
|
||||
assert_eq!(
|
||||
conn.browser_context
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::conn::{CdpConnection, Cmd, CommandOwnerScope};
|
||||
use crate::conn::{CdpConnection, Cmd, CommandOwnerScope, RendererDispatchLane};
|
||||
use crate::domains::actions::AccessibilityAction;
|
||||
use crate::domains::command_output::CommandOutputPlan;
|
||||
use moli_core::page::{
|
||||
@@ -29,6 +29,11 @@ pub(crate) enum AccessibilityCommandDispatchStep {
|
||||
Complete(CommandOutputPlan),
|
||||
}
|
||||
|
||||
pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool {
|
||||
cmd.parse_action::<AccessibilityAction>()
|
||||
.is_some_and(AccessibilityAction::queries_tree)
|
||||
}
|
||||
|
||||
enum PendingAccessibilityCommandKind {
|
||||
TopFrameFullTree {
|
||||
max_depth: Option<i32>,
|
||||
@@ -74,6 +79,12 @@ struct PendingAccessibilityCommandStartError {
|
||||
}
|
||||
|
||||
impl PendingAccessibilityCommandDispatch {
|
||||
pub(crate) fn renderer_dispatch_lane(&self) -> Option<RendererDispatchLane> {
|
||||
self.pending
|
||||
.renderer_agent_attachment_id()
|
||||
.map(|_| RendererDispatchLane::Main)
|
||||
}
|
||||
|
||||
fn from_command(
|
||||
conn: &CdpConnection,
|
||||
cmd: &Cmd<'_>,
|
||||
|
||||
@@ -1389,6 +1389,7 @@ mod tests {
|
||||
frame_id: "FRAME-1".to_owned(),
|
||||
loader_id: "LID-1".to_owned(),
|
||||
document_id: crate::conn::DocumentId::from_raw_for_test(1),
|
||||
browser_sequence: moli_core::browser::BrowserSequence::allocate(),
|
||||
document_open_replacement_epoch: None,
|
||||
};
|
||||
let renderer_lifecycle_events = vec![
|
||||
|
||||
@@ -5,7 +5,7 @@ mod output_payloads;
|
||||
mod output_slot;
|
||||
mod output_work;
|
||||
mod publication_route;
|
||||
mod runtime_command_barrier;
|
||||
mod renderer_command_response_order;
|
||||
mod scheduler_work;
|
||||
mod subresource;
|
||||
|
||||
@@ -26,9 +26,9 @@ pub(crate) use output_work::{
|
||||
ProtocolOutputWork, RootFrameStoppedLoadingSettlement, RootFrameStoppedLoadingSettlementError,
|
||||
};
|
||||
pub(crate) use publication_route::{RendererPublicationOwner, renderer_publication_owners};
|
||||
pub use runtime_command_barrier::{
|
||||
RuntimeCommandOutputBarrierCompletion, RuntimeCommandOutputBarrierPermit,
|
||||
RuntimeCommandOutputBarrierTerminal, RuntimeCommandOutputBarriers,
|
||||
pub use renderer_command_response_order::{
|
||||
RendererCommandResponseCompletion, RendererCommandResponseOrder, RendererCommandResponsePermit,
|
||||
RendererCommandResponseTerminal,
|
||||
};
|
||||
pub(crate) use scheduler_work::ReadyProtocolSchedulerWork;
|
||||
pub use scheduler_work::{
|
||||
|
||||
@@ -14,8 +14,8 @@ use crate::domains::page::{PagePreparedOutputSlot, SLOT_TOP_LEVEL_LOCATION_NAVIG
|
||||
///
|
||||
/// The batch owns typed payload exactly once and preserves the producer's
|
||||
/// explicit FIFO order. It cannot select renderer work or inspect current renderer state. Consumers
|
||||
/// must either project it, hold its remaining after-response slots behind an
|
||||
/// exact command barrier, or consume only its owner actions during stale
|
||||
/// must either project it, hold its remaining after-response slots for an
|
||||
/// exact command response permit, or consume only its owner actions during stale
|
||||
/// cleanup.
|
||||
#[derive(Debug)]
|
||||
#[must_use = "prepared protocol outputs must be projected, held, or cleaned up exactly once"]
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::collections::VecDeque;
|
||||
use super::super::publication_route::RendererPublicationOwner;
|
||||
use super::super::publication_route::RendererPublicationProjection;
|
||||
use super::super::publication_route::RendererPublicationRoute;
|
||||
use super::super::runtime_command_barrier::RuntimeCommandOutputBarriers;
|
||||
use super::super::renderer_command_response_order::RendererCommandResponseOrder;
|
||||
use super::prepared_outputs::PreparedProtocolOutputs;
|
||||
use crate::conn::{
|
||||
BackgroundProtocolEvent, CdpConnection, CommandDispatchContext, CommandOwnerScope,
|
||||
@@ -52,7 +52,7 @@ fn renderer_owner_action_owner(
|
||||
pub(crate) async fn ingest_renderer_output_transport_async(
|
||||
conn: &mut CdpConnection,
|
||||
publication: RendererOutputTransportMessage,
|
||||
barriers: &mut RuntimeCommandOutputBarriers,
|
||||
order: &mut RendererCommandResponseOrder,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) -> Vec<BackgroundProtocolEvent> {
|
||||
match publication {
|
||||
@@ -83,7 +83,7 @@ pub(crate) async fn ingest_renderer_output_transport_async(
|
||||
while let Some(output) = ready.pop_front() {
|
||||
let (output, owner) = output.into_parts();
|
||||
let cursor = output.cursor();
|
||||
ingest_renderer_output_publication(conn, output, owner, barriers, command_context)
|
||||
ingest_renderer_output_publication(conn, output, owner, order, command_context)
|
||||
.await;
|
||||
match conn.complete_renderer_output_projection(cursor) {
|
||||
super::RendererOutputIngressAdmission::Ready(next) => ready.extend(next),
|
||||
@@ -102,7 +102,7 @@ async fn ingest_renderer_output_publication(
|
||||
conn: &mut CdpConnection,
|
||||
publication: RendererOutputPublication,
|
||||
owner: RendererPublicationOwner,
|
||||
barriers: &mut RuntimeCommandOutputBarriers,
|
||||
order: &mut RendererCommandResponseOrder,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) {
|
||||
let cursor = publication.cursor();
|
||||
@@ -150,7 +150,7 @@ async fn ingest_renderer_output_publication(
|
||||
records,
|
||||
cursor,
|
||||
projection,
|
||||
barriers,
|
||||
order,
|
||||
command_context,
|
||||
)
|
||||
.await;
|
||||
@@ -166,7 +166,7 @@ async fn ingest_renderer_output_publication(
|
||||
records,
|
||||
cursor,
|
||||
projection,
|
||||
barriers,
|
||||
order,
|
||||
command_context,
|
||||
)
|
||||
.await;
|
||||
@@ -180,7 +180,7 @@ async fn project_renderer_output_records_for_owner(
|
||||
records: Vec<moli_core::RendererOutputRecord>,
|
||||
cursor: moli_core::RendererOutputCursor,
|
||||
projection: RendererPublicationProjection,
|
||||
barriers: &mut RuntimeCommandOutputBarriers,
|
||||
order: &mut RendererCommandResponseOrder,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) {
|
||||
for record in records {
|
||||
@@ -243,7 +243,7 @@ async fn project_renderer_output_records_for_owner(
|
||||
action,
|
||||
)
|
||||
.await;
|
||||
barriers
|
||||
order
|
||||
.route_publication_outputs(
|
||||
conn,
|
||||
&action_owner,
|
||||
@@ -289,7 +289,7 @@ async fn project_renderer_output_records_for_owner(
|
||||
&observation,
|
||||
)
|
||||
};
|
||||
barriers
|
||||
order
|
||||
.route_publication_outputs(
|
||||
conn,
|
||||
owner,
|
||||
@@ -410,7 +410,7 @@ mod tests {
|
||||
})
|
||||
.collect();
|
||||
let cursor = RendererOutputCursor::new_for_test(stream, 1);
|
||||
let mut barriers = RuntimeCommandOutputBarriers::default();
|
||||
let mut order = RendererCommandResponseOrder::default();
|
||||
let mut commands = CommandDispatchContext::default();
|
||||
if let Some(projection) = projection {
|
||||
project_renderer_output_records_for_owner(
|
||||
@@ -419,7 +419,7 @@ mod tests {
|
||||
records,
|
||||
cursor,
|
||||
projection,
|
||||
&mut barriers,
|
||||
&mut order,
|
||||
&mut commands,
|
||||
)
|
||||
.await;
|
||||
@@ -439,7 +439,7 @@ mod tests {
|
||||
&mut conn,
|
||||
RendererOutputPublication::new_for_test(cursor, records),
|
||||
missing_route,
|
||||
&mut barriers,
|
||||
&mut order,
|
||||
&mut commands,
|
||||
)
|
||||
.await;
|
||||
|
||||
+196
-219
@@ -8,40 +8,40 @@ use crate::conn::{
|
||||
TargetPageResidenceIdentity,
|
||||
};
|
||||
|
||||
/// The single-use authority for one Runtime command's causal output barrier.
|
||||
/// The single-use authority for one renderer command's response-order boundary.
|
||||
///
|
||||
/// The permit is deliberately move-only. Releasing or canceling it removes
|
||||
/// exactly one command predecessor from held output; cloning it would make the
|
||||
/// barrier terminal ambiguous.
|
||||
/// response-order terminal ambiguous.
|
||||
#[derive(Debug)]
|
||||
#[must_use = "a Runtime command output barrier must be released or canceled exactly once"]
|
||||
pub struct RuntimeCommandOutputBarrierPermit {
|
||||
id: RuntimeCommandOutputBarrierId,
|
||||
#[must_use = "a renderer command response permit must be released or canceled exactly once"]
|
||||
pub struct RendererCommandResponsePermit {
|
||||
id: RendererCommandResponsePermitId,
|
||||
command_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
|
||||
struct RuntimeCommandOutputBarrierId(u64);
|
||||
struct RendererCommandResponsePermitId(u64);
|
||||
|
||||
impl RuntimeCommandOutputBarrierId {
|
||||
impl RendererCommandResponsePermitId {
|
||||
fn checked_next(self) -> Self {
|
||||
Self(
|
||||
self.0
|
||||
.checked_add(1)
|
||||
.expect("Runtime command output barrier id overflow"),
|
||||
.expect("renderer command response ordering id overflow"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
struct RuntimeCommandOutputHoldId(u64);
|
||||
struct RendererCommandOutputHoldId(u64);
|
||||
|
||||
impl RuntimeCommandOutputHoldId {
|
||||
impl RendererCommandOutputHoldId {
|
||||
fn checked_next(self) -> Self {
|
||||
Self(
|
||||
self.0
|
||||
.checked_add(1)
|
||||
.expect("Runtime command held-output id overflow"),
|
||||
.expect("renderer command held-output id overflow"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -56,13 +56,13 @@ enum RuntimeCommandCausalOwner {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct RuntimeCommandOutputRoute {
|
||||
struct RendererCommandOutputRoute {
|
||||
delivery_scope: CommandOwnerScope,
|
||||
causal_owner: RuntimeCommandCausalOwner,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ActiveRuntimeCommandOutputBarrier {
|
||||
struct ActiveRendererCommandResponsePermit {
|
||||
command_id: u64,
|
||||
command_scope: CommandOwnerScope,
|
||||
causal_owner: RuntimeCommandCausalOwner,
|
||||
@@ -76,21 +76,21 @@ enum HeldOutputReleaseMode {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct HeldRuntimeCommandOutput {
|
||||
id: RuntimeCommandOutputHoldId,
|
||||
struct HeldRendererCommandOutput {
|
||||
id: RendererCommandOutputHoldId,
|
||||
renderer_output_cursor: Option<RendererOutputCursor>,
|
||||
predecessors: BTreeSet<RuntimeCommandOutputBarrierId>,
|
||||
predecessors: BTreeSet<RendererCommandResponsePermitId>,
|
||||
release_mode: HeldOutputReleaseMode,
|
||||
outputs: PreparedProtocolOutputs,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct HeldRuntimeCommandOutputRoute {
|
||||
route: RuntimeCommandOutputRoute,
|
||||
outputs: VecDeque<HeldRuntimeCommandOutput>,
|
||||
struct HeldRendererCommandOutputRoute {
|
||||
route: RendererCommandOutputRoute,
|
||||
outputs: VecDeque<HeldRendererCommandOutput>,
|
||||
}
|
||||
|
||||
/// Typed terminal for one command barrier.
|
||||
/// Typed terminal for one command permit.
|
||||
///
|
||||
/// `Released` means the exact command owner still existed when its response
|
||||
/// entered the protocol output sequence. `Superseded` means the command's
|
||||
@@ -99,28 +99,25 @@ struct HeldRuntimeCommandOutputRoute {
|
||||
/// canceled/superseded output performs only browser-owner cleanup and does not
|
||||
/// project protocol-only observations from the stale command.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RuntimeCommandOutputBarrierTerminal {
|
||||
pub enum RendererCommandResponseTerminal {
|
||||
Released,
|
||||
Canceled,
|
||||
Superseded,
|
||||
}
|
||||
|
||||
/// One consumed barrier terminal plus the protocol/scheduler output produced
|
||||
/// One consumed permit terminal plus the protocol/scheduler output produced
|
||||
/// while releasing its held concrete work.
|
||||
pub struct RuntimeCommandOutputBarrierCompletion {
|
||||
terminal: RuntimeCommandOutputBarrierTerminal,
|
||||
pub struct RendererCommandResponseCompletion {
|
||||
terminal: RendererCommandResponseTerminal,
|
||||
outcome: CdpTurnOutcome,
|
||||
}
|
||||
|
||||
impl RuntimeCommandOutputBarrierCompletion {
|
||||
pub(crate) fn new(
|
||||
terminal: RuntimeCommandOutputBarrierTerminal,
|
||||
outcome: CdpTurnOutcome,
|
||||
) -> Self {
|
||||
impl RendererCommandResponseCompletion {
|
||||
pub(crate) fn new(terminal: RendererCommandResponseTerminal, outcome: CdpTurnOutcome) -> Self {
|
||||
Self { terminal, outcome }
|
||||
}
|
||||
|
||||
pub fn terminal(&self) -> RuntimeCommandOutputBarrierTerminal {
|
||||
pub fn terminal(&self) -> RendererCommandResponseTerminal {
|
||||
self.terminal
|
||||
}
|
||||
|
||||
@@ -129,19 +126,19 @@ impl RuntimeCommandOutputBarrierCompletion {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable owner-local residence for concrete output held behind Runtime
|
||||
/// command responses.
|
||||
/// Stable owner-local residence for concrete output held until its renderer
|
||||
/// command response is ready for frontend publication.
|
||||
///
|
||||
/// A held batch is frozen once by the renderer/protocol owner and is never
|
||||
/// reconstructed from current state. Batches stay FIFO within their exact
|
||||
/// delivery route. Each batch names the exact Runtime command that produced
|
||||
/// delivery route. Each batch names the exact renderer command that produced
|
||||
/// it; another pending command on the same Page is not a predecessor.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RuntimeCommandOutputBarriers {
|
||||
next_barrier_id: RuntimeCommandOutputBarrierId,
|
||||
next_hold_id: RuntimeCommandOutputHoldId,
|
||||
active: BTreeMap<RuntimeCommandOutputBarrierId, ActiveRuntimeCommandOutputBarrier>,
|
||||
held_routes: Vec<HeldRuntimeCommandOutputRoute>,
|
||||
pub struct RendererCommandResponseOrder {
|
||||
next_permit_id: RendererCommandResponsePermitId,
|
||||
next_hold_id: RendererCommandOutputHoldId,
|
||||
active: BTreeMap<RendererCommandResponsePermitId, ActiveRendererCommandResponsePermit>,
|
||||
held_routes: Vec<HeldRendererCommandOutputRoute>,
|
||||
}
|
||||
|
||||
impl RuntimeCommandCausalOwner {
|
||||
@@ -168,7 +165,7 @@ impl RuntimeCommandCausalOwner {
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeCommandOutputRoute {
|
||||
impl RendererCommandOutputRoute {
|
||||
fn capture_for_scope(conn: &CdpConnection, delivery_scope: CommandOwnerScope) -> Option<Self> {
|
||||
Some(Self {
|
||||
causal_owner: RuntimeCommandCausalOwner::capture_for_scope(conn, &delivery_scope)?,
|
||||
@@ -177,13 +174,13 @@ impl RuntimeCommandOutputRoute {
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeCommandOutputBarriers {
|
||||
impl RendererCommandResponseOrder {
|
||||
pub fn admit(
|
||||
&mut self,
|
||||
conn: &CdpConnection,
|
||||
command_id: u64,
|
||||
session_id: Option<&str>,
|
||||
) -> Option<RuntimeCommandOutputBarrierPermit> {
|
||||
) -> Option<RendererCommandResponsePermit> {
|
||||
let command_scope = CommandOwnerScope::capture(conn, session_id);
|
||||
let causal_owner = RuntimeCommandCausalOwner::capture(conn, session_id)?;
|
||||
// V8 Inspector commands use a session-local renderer call id. It is
|
||||
@@ -193,16 +190,16 @@ impl RuntimeCommandOutputBarriers {
|
||||
let renderer_cause =
|
||||
conn.renderer_runtime_command_cause_for_frontend(session_id, command_id)?;
|
||||
assert!(
|
||||
!self.active.values().any(|barrier| {
|
||||
barrier.causal_owner == causal_owner && barrier.renderer_cause == renderer_cause
|
||||
!self.active.values().any(|permit| {
|
||||
permit.causal_owner == causal_owner && permit.renderer_cause == renderer_cause
|
||||
}),
|
||||
"an exact renderer Runtime command cause must have only one active response barrier"
|
||||
"an exact renderer command cause must have only one active response permit"
|
||||
);
|
||||
let id = self.next_barrier_id;
|
||||
self.next_barrier_id = self.next_barrier_id.checked_next();
|
||||
let id = self.next_permit_id;
|
||||
self.next_permit_id = self.next_permit_id.checked_next();
|
||||
let replaced = self.active.insert(
|
||||
id,
|
||||
ActiveRuntimeCommandOutputBarrier {
|
||||
ActiveRendererCommandResponsePermit {
|
||||
command_id,
|
||||
command_scope,
|
||||
causal_owner,
|
||||
@@ -211,9 +208,9 @@ impl RuntimeCommandOutputBarriers {
|
||||
);
|
||||
assert!(
|
||||
replaced.is_none(),
|
||||
"Runtime command barrier ids must not be reused while active"
|
||||
"renderer command response permit ids must not be reused while active"
|
||||
);
|
||||
Some(RuntimeCommandOutputBarrierPermit { id, command_id })
|
||||
Some(RendererCommandResponsePermit { id, command_id })
|
||||
}
|
||||
|
||||
pub(super) async fn route_publication_outputs(
|
||||
@@ -226,7 +223,7 @@ impl RuntimeCommandOutputBarriers {
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) {
|
||||
let Some(route) =
|
||||
RuntimeCommandOutputRoute::capture_for_scope(conn, delivery_scope.clone())
|
||||
RendererCommandOutputRoute::capture_for_scope(conn, delivery_scope.clone())
|
||||
else {
|
||||
outputs
|
||||
.project_async(conn, delivery_scope, command_context)
|
||||
@@ -245,16 +242,16 @@ impl RuntimeCommandOutputBarriers {
|
||||
let matching: Vec<_> = self
|
||||
.active
|
||||
.iter()
|
||||
.filter_map(|(id, barrier)| {
|
||||
(barrier.causal_owner == route.causal_owner
|
||||
&& &barrier.renderer_cause == renderer_cause)
|
||||
.filter_map(|(id, permit)| {
|
||||
(permit.causal_owner == route.causal_owner
|
||||
&& &permit.renderer_cause == renderer_cause)
|
||||
.then_some(*id)
|
||||
})
|
||||
.collect();
|
||||
let [barrier_id] = matching.as_slice() else {
|
||||
let [permit_id] = matching.as_slice() else {
|
||||
assert!(
|
||||
matching.is_empty(),
|
||||
"one renderer Runtime command cause cannot match multiple active barriers"
|
||||
"one renderer command cause cannot match multiple active response permits"
|
||||
);
|
||||
outputs
|
||||
.project_async(conn, delivery_scope, command_context)
|
||||
@@ -272,10 +269,10 @@ impl RuntimeCommandOutputBarriers {
|
||||
} else {
|
||||
outputs
|
||||
};
|
||||
self.hold_for_exact_barrier(
|
||||
self.hold_for_exact_response(
|
||||
conn,
|
||||
route,
|
||||
*barrier_id,
|
||||
*permit_id,
|
||||
renderer_output_cursor,
|
||||
causal_outputs,
|
||||
command_context,
|
||||
@@ -283,11 +280,11 @@ impl RuntimeCommandOutputBarriers {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn hold_for_exact_barrier(
|
||||
async fn hold_for_exact_response(
|
||||
&mut self,
|
||||
conn: &mut CdpConnection,
|
||||
route: RuntimeCommandOutputRoute,
|
||||
barrier_id: RuntimeCommandOutputBarrierId,
|
||||
route: RendererCommandOutputRoute,
|
||||
permit_id: RendererCommandResponsePermitId,
|
||||
renderer_output_cursor: Option<RendererOutputCursor>,
|
||||
outputs: PreparedProtocolOutputs,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
@@ -304,10 +301,10 @@ impl RuntimeCommandOutputBarriers {
|
||||
};
|
||||
let id = self.next_hold_id;
|
||||
self.next_hold_id = self.next_hold_id.checked_next();
|
||||
let held = HeldRuntimeCommandOutput {
|
||||
let held = HeldRendererCommandOutput {
|
||||
id,
|
||||
renderer_output_cursor,
|
||||
predecessors: BTreeSet::from([barrier_id]),
|
||||
predecessors: BTreeSet::from([permit_id]),
|
||||
release_mode: HeldOutputReleaseMode::All,
|
||||
outputs,
|
||||
};
|
||||
@@ -318,7 +315,7 @@ impl RuntimeCommandOutputBarriers {
|
||||
{
|
||||
existing.outputs.push_back(held);
|
||||
} else {
|
||||
self.held_routes.push(HeldRuntimeCommandOutputRoute {
|
||||
self.held_routes.push(HeldRendererCommandOutputRoute {
|
||||
route,
|
||||
outputs: VecDeque::from([held]),
|
||||
});
|
||||
@@ -328,16 +325,16 @@ impl RuntimeCommandOutputBarriers {
|
||||
pub(crate) async fn release(
|
||||
&mut self,
|
||||
conn: &mut CdpConnection,
|
||||
permit: RuntimeCommandOutputBarrierPermit,
|
||||
permit: RendererCommandResponsePermit,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) -> RuntimeCommandOutputBarrierTerminal {
|
||||
let (barrier_id, barrier) = self.consume_permit(permit);
|
||||
let terminal = if self.command_owner_is_current(conn, &barrier) {
|
||||
RuntimeCommandOutputBarrierTerminal::Released
|
||||
) -> RendererCommandResponseTerminal {
|
||||
let (permit_id, permit) = self.consume_permit(permit);
|
||||
let terminal = if self.command_owner_is_current(conn, &permit) {
|
||||
RendererCommandResponseTerminal::Released
|
||||
} else {
|
||||
RuntimeCommandOutputBarrierTerminal::Superseded
|
||||
RendererCommandResponseTerminal::Superseded
|
||||
};
|
||||
self.finish_predecessor(conn, barrier_id, barrier, terminal, command_context)
|
||||
self.finish_predecessor(conn, permit_id, permit, terminal, command_context)
|
||||
.await;
|
||||
terminal
|
||||
}
|
||||
@@ -345,58 +342,60 @@ impl RuntimeCommandOutputBarriers {
|
||||
pub(crate) async fn cancel(
|
||||
&mut self,
|
||||
conn: &mut CdpConnection,
|
||||
permit: RuntimeCommandOutputBarrierPermit,
|
||||
permit: RendererCommandResponsePermit,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) -> RuntimeCommandOutputBarrierTerminal {
|
||||
let (barrier_id, barrier) = self.consume_permit(permit);
|
||||
let terminal = RuntimeCommandOutputBarrierTerminal::Canceled;
|
||||
self.finish_predecessor(conn, barrier_id, barrier, terminal, command_context)
|
||||
) -> RendererCommandResponseTerminal {
|
||||
let (permit_id, permit) = self.consume_permit(permit);
|
||||
let terminal = RendererCommandResponseTerminal::Canceled;
|
||||
self.finish_predecessor(conn, permit_id, permit, terminal, command_context)
|
||||
.await;
|
||||
terminal
|
||||
}
|
||||
|
||||
fn consume_permit(
|
||||
&mut self,
|
||||
permit: RuntimeCommandOutputBarrierPermit,
|
||||
permit: RendererCommandResponsePermit,
|
||||
) -> (
|
||||
RuntimeCommandOutputBarrierId,
|
||||
ActiveRuntimeCommandOutputBarrier,
|
||||
RendererCommandResponsePermitId,
|
||||
ActiveRendererCommandResponsePermit,
|
||||
) {
|
||||
let barrier = self
|
||||
let permit_id = permit.id;
|
||||
let command_id = permit.command_id;
|
||||
let active = self
|
||||
.active
|
||||
.remove(&permit.id)
|
||||
.expect("Runtime command output barrier permit must name one active barrier");
|
||||
.remove(&permit_id)
|
||||
.expect("renderer command response ordering permit must name one active permit");
|
||||
assert_eq!(
|
||||
barrier.command_id, permit.command_id,
|
||||
"Runtime command output barrier permit must retain its exact command identity"
|
||||
active.command_id, command_id,
|
||||
"renderer command response ordering permit must retain its exact command identity"
|
||||
);
|
||||
(permit.id, barrier)
|
||||
(permit_id, active)
|
||||
}
|
||||
|
||||
fn command_owner_is_current(
|
||||
&self,
|
||||
conn: &CdpConnection,
|
||||
barrier: &ActiveRuntimeCommandOutputBarrier,
|
||||
permit: &ActiveRendererCommandResponsePermit,
|
||||
) -> bool {
|
||||
RuntimeCommandCausalOwner::capture_for_scope(conn, &barrier.command_scope).as_ref()
|
||||
== Some(&barrier.causal_owner)
|
||||
RuntimeCommandCausalOwner::capture_for_scope(conn, &permit.command_scope).as_ref()
|
||||
== Some(&permit.causal_owner)
|
||||
}
|
||||
|
||||
async fn finish_predecessor(
|
||||
&mut self,
|
||||
conn: &mut CdpConnection,
|
||||
barrier_id: RuntimeCommandOutputBarrierId,
|
||||
barrier: ActiveRuntimeCommandOutputBarrier,
|
||||
terminal: RuntimeCommandOutputBarrierTerminal,
|
||||
permit_id: RendererCommandResponsePermitId,
|
||||
permit: ActiveRendererCommandResponsePermit,
|
||||
terminal: RendererCommandResponseTerminal,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) {
|
||||
for route in &mut self.held_routes {
|
||||
if route.route.causal_owner != barrier.causal_owner {
|
||||
if route.route.causal_owner != permit.causal_owner {
|
||||
continue;
|
||||
}
|
||||
for held in &mut route.outputs {
|
||||
let was_predecessor = held.predecessors.remove(&barrier_id);
|
||||
if was_predecessor && terminal != RuntimeCommandOutputBarrierTerminal::Released {
|
||||
let was_predecessor = held.predecessors.remove(&permit_id);
|
||||
if was_predecessor && terminal != RendererCommandResponseTerminal::Released {
|
||||
held.release_mode = HeldOutputReleaseMode::OwnerActionsOnly;
|
||||
}
|
||||
}
|
||||
@@ -427,7 +426,7 @@ impl RuntimeCommandOutputBarriers {
|
||||
if moli_trace::cdp_runtime_trace_enabled() {
|
||||
tracing::info!(
|
||||
target: "moli_cdp_runtime",
|
||||
stage = "runtime_command_barrier_output_release",
|
||||
stage = "renderer_command_response_output_release",
|
||||
hold_id = held.id.0,
|
||||
renderer_output_stream_epoch = ?held.renderer_output_cursor.map(|cursor| cursor.stream().epoch().get()),
|
||||
renderer_output_sequence = ?held.renderer_output_cursor.map(RendererOutputCursor::sequence),
|
||||
@@ -494,20 +493,20 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::{
|
||||
RuntimeCommandOutputBarrierPermit, RuntimeCommandOutputBarrierTerminal,
|
||||
RuntimeCommandOutputBarriers,
|
||||
RendererCommandResponseOrder, RendererCommandResponsePermit,
|
||||
RendererCommandResponseTerminal,
|
||||
};
|
||||
|
||||
const SESSION_ID: &str = "SID-runtime-command-barrier";
|
||||
const SESSION_ID: &str = "SID-runtime-command-permit";
|
||||
|
||||
async fn connection_with_loaded_page() -> CdpConnection {
|
||||
let mut conn = CdpConnection::new();
|
||||
let page = conn
|
||||
.load_page_via_runtime_async("data:text/html,<title>runtime-command-barrier</title>")
|
||||
.load_page_via_runtime_async("data:text/html,<title>runtime-command-permit</title>")
|
||||
.await
|
||||
.expect("barrier test page should load");
|
||||
let mut browser_context = BrowserContext::new("BID-runtime-command-barrier".to_owned());
|
||||
browser_context.set_active_target_id("TID-runtime-command-barrier");
|
||||
.expect("permit test page should load");
|
||||
let mut browser_context = BrowserContext::new("BID-runtime-command-permit".to_owned());
|
||||
browser_context.set_active_target_id("TID-runtime-command-permit");
|
||||
browser_context.attach_active_session(SESSION_ID);
|
||||
browser_context.set_target_url(page.final_url().as_str().to_owned());
|
||||
let _ = browser_context.replace_active_page_for_test(Some(page));
|
||||
@@ -536,9 +535,9 @@ mod tests {
|
||||
|
||||
fn admit_registered_command(
|
||||
conn: &mut CdpConnection,
|
||||
barriers: &mut RuntimeCommandOutputBarriers,
|
||||
order: &mut RendererCommandResponseOrder,
|
||||
frontend_command_id: u64,
|
||||
) -> RuntimeCommandOutputBarrierPermit {
|
||||
) -> RendererCommandResponsePermit {
|
||||
let descriptor = RendererCommandDescriptor::from_synthesized_payload(
|
||||
serde_json::json!({
|
||||
"id": frontend_command_id,
|
||||
@@ -555,14 +554,14 @@ mod tests {
|
||||
descriptor,
|
||||
)
|
||||
.expect("test Runtime command should register its renderer call");
|
||||
barriers
|
||||
order
|
||||
.admit(conn, frontend_command_id, Some(SESSION_ID))
|
||||
.expect("registered Runtime command should admit an exact output barrier")
|
||||
.expect("registered Runtime command should admit an exact output permit")
|
||||
}
|
||||
|
||||
fn renderer_cause_for_permit(
|
||||
conn: &CdpConnection,
|
||||
permit: &RuntimeCommandOutputBarrierPermit,
|
||||
permit: &RendererCommandResponsePermit,
|
||||
) -> RendererRuntimeCommandCausalIdentity {
|
||||
conn.renderer_runtime_command_cause_for_frontend(Some(SESSION_ID), permit.command_id)
|
||||
.expect("active test command should retain its exact renderer call identity")
|
||||
@@ -570,8 +569,8 @@ mod tests {
|
||||
|
||||
async fn route_same_document_navigation(
|
||||
conn: &mut CdpConnection,
|
||||
barriers: &mut RuntimeCommandOutputBarriers,
|
||||
permit: Option<&super::RuntimeCommandOutputBarrierPermit>,
|
||||
order: &mut RendererCommandResponseOrder,
|
||||
permit: Option<&super::RendererCommandResponsePermit>,
|
||||
fragment: &str,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) {
|
||||
@@ -585,7 +584,7 @@ mod tests {
|
||||
RendererDocumentSourcedSameDocumentNavigation::new(
|
||||
source_document,
|
||||
RendererPendingSameDocumentNavigation {
|
||||
url: format!("data:text/html,runtime-command-barrier#{fragment}"),
|
||||
url: format!("data:text/html,runtime-command-permit#{fragment}"),
|
||||
navigation_type: "fragment".to_owned(),
|
||||
},
|
||||
),
|
||||
@@ -593,7 +592,7 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
let renderer_cause = permit.map(|permit| renderer_cause_for_permit(conn, permit));
|
||||
barriers
|
||||
order
|
||||
.route_publication_outputs(
|
||||
conn,
|
||||
&command_owner,
|
||||
@@ -607,8 +606,8 @@ mod tests {
|
||||
|
||||
async fn route_top_level_navigation_for_command(
|
||||
conn: &mut CdpConnection,
|
||||
barriers: &mut RuntimeCommandOutputBarriers,
|
||||
permit: &super::RuntimeCommandOutputBarrierPermit,
|
||||
order: &mut RendererCommandResponseOrder,
|
||||
permit: &super::RendererCommandResponsePermit,
|
||||
marker: &str,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) {
|
||||
@@ -628,7 +627,7 @@ mod tests {
|
||||
),
|
||||
);
|
||||
|
||||
barriers
|
||||
order
|
||||
.route_publication_outputs(conn, &command_owner, None, None, outputs, command_context)
|
||||
.await;
|
||||
}
|
||||
@@ -660,9 +659,9 @@ mod tests {
|
||||
let [event]: [crate::conn::CdpSchedulerEvent; 1] = conn
|
||||
.take_scheduler_events()
|
||||
.try_into()
|
||||
.expect("barrier release should publish one concrete navigation action");
|
||||
.expect("permit release should publish one concrete navigation action");
|
||||
let crate::conn::CdpSchedulerEvent::ProtocolWorkPublished { work } = event else {
|
||||
panic!("barrier release must publish concrete protocol work");
|
||||
panic!("permit release must publish concrete protocol work");
|
||||
};
|
||||
assert!(work.is_top_level_location_navigation_owner_action());
|
||||
let (events, nested_scheduler_events) = conn
|
||||
@@ -685,44 +684,42 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn same_document_navigation_projects_before_exact_runtime_command_response() {
|
||||
let mut conn = connection_with_loaded_page().await;
|
||||
let mut barriers = RuntimeCommandOutputBarriers::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut barriers, 1);
|
||||
let mut order = RendererCommandResponseOrder::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut order, 1);
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
|
||||
route_same_document_navigation(
|
||||
&mut conn,
|
||||
&mut barriers,
|
||||
&mut order,
|
||||
Some(&permit),
|
||||
"held",
|
||||
&mut command_context,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(barriers.active_count(), 1);
|
||||
assert_eq!(barriers.held_output_count(), 0);
|
||||
assert_eq!(order.active_count(), 1);
|
||||
assert_eq!(order.held_output_count(), 0);
|
||||
assert!(
|
||||
contains_same_document_navigation(&protocol_messages(&mut command_context)),
|
||||
"the completed history mutation must be reported before the exact command response"
|
||||
);
|
||||
|
||||
let terminal = barriers
|
||||
.release(&mut conn, permit, &mut command_context)
|
||||
.await;
|
||||
let terminal = order.release(&mut conn, permit, &mut command_context).await;
|
||||
|
||||
assert_eq!(terminal, RuntimeCommandOutputBarrierTerminal::Released);
|
||||
assert_eq!(barriers.active_count(), 0);
|
||||
assert_eq!(barriers.held_output_count(), 0);
|
||||
assert_eq!(terminal, RendererCommandResponseTerminal::Released);
|
||||
assert_eq!(order.active_count(), 0);
|
||||
assert_eq!(order.held_output_count(), 0);
|
||||
assert!(
|
||||
!contains_same_document_navigation(&protocol_messages(&mut command_context)),
|
||||
"releasing the barrier must not deliver the same navigation twice"
|
||||
"releasing the permit must not deliver the same navigation twice"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn debugger_transition_messages_wait_for_the_exact_command_response() {
|
||||
let mut conn = connection_with_loaded_page().await;
|
||||
let mut barriers = RuntimeCommandOutputBarriers::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut barriers, 2);
|
||||
let mut order = RendererCommandResponseOrder::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut order, 2);
|
||||
let cause = renderer_cause_for_permit(&conn, &permit);
|
||||
let attachment = conn
|
||||
.runtime_session_owner_slot(Some(SESSION_ID))
|
||||
@@ -768,7 +765,7 @@ mod tests {
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
let command_owner = CommandOwnerScope::for_session(SESSION_ID);
|
||||
|
||||
barriers
|
||||
order
|
||||
.route_publication_outputs(
|
||||
&mut conn,
|
||||
&command_owner,
|
||||
@@ -779,17 +776,15 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(barriers.held_output_count(), 1);
|
||||
assert_eq!(order.held_output_count(), 1);
|
||||
assert!(
|
||||
protocol_messages(&mut command_context).is_empty(),
|
||||
"Debugger transition output must not overtake the matching response"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
barriers
|
||||
.release(&mut conn, permit, &mut command_context)
|
||||
.await,
|
||||
RuntimeCommandOutputBarrierTerminal::Released
|
||||
order.release(&mut conn, permit, &mut command_context).await,
|
||||
RendererCommandResponseTerminal::Released
|
||||
);
|
||||
let messages = protocol_messages(&mut command_context);
|
||||
assert_eq!(
|
||||
@@ -804,45 +799,41 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn output_is_owned_by_one_exact_command_not_every_command_on_the_page() {
|
||||
let mut conn = connection_with_loaded_page().await;
|
||||
let mut barriers = RuntimeCommandOutputBarriers::default();
|
||||
let first = admit_registered_command(&mut conn, &mut barriers, 11);
|
||||
let second = admit_registered_command(&mut conn, &mut barriers, 12);
|
||||
let mut order = RendererCommandResponseOrder::default();
|
||||
let first = admit_registered_command(&mut conn, &mut order, 11);
|
||||
let second = admit_registered_command(&mut conn, &mut order, 12);
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
|
||||
route_top_level_navigation_for_command(
|
||||
&mut conn,
|
||||
&mut barriers,
|
||||
&mut order,
|
||||
&first,
|
||||
"exact-predecessor",
|
||||
&mut command_context,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(barriers.held_output_count(), 1);
|
||||
assert_eq!(order.held_output_count(), 1);
|
||||
|
||||
assert_eq!(
|
||||
barriers
|
||||
.release(&mut conn, second, &mut command_context)
|
||||
.await,
|
||||
RuntimeCommandOutputBarrierTerminal::Released
|
||||
order.release(&mut conn, second, &mut command_context).await,
|
||||
RendererCommandResponseTerminal::Released
|
||||
);
|
||||
assert_eq!(barriers.active_count(), 1);
|
||||
assert_eq!(barriers.held_output_count(), 1);
|
||||
assert_eq!(order.active_count(), 1);
|
||||
assert_eq!(order.held_output_count(), 1);
|
||||
assert!(
|
||||
!contains_top_level_navigation(&protocol_messages(&mut command_context)),
|
||||
"an unrelated command response must not release another command's output"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
barriers
|
||||
.release(&mut conn, first, &mut command_context)
|
||||
.await,
|
||||
RuntimeCommandOutputBarrierTerminal::Released
|
||||
order.release(&mut conn, first, &mut command_context).await,
|
||||
RendererCommandResponseTerminal::Released
|
||||
);
|
||||
assert_eq!(barriers.active_count(), 0);
|
||||
assert_eq!(barriers.held_output_count(), 0);
|
||||
assert_eq!(order.active_count(), 0);
|
||||
assert_eq!(order.held_output_count(), 0);
|
||||
assert!(
|
||||
!contains_top_level_navigation(&protocol_messages(&mut command_context)),
|
||||
"barrier release must publish, not execute, its concrete owner action"
|
||||
"permit release must publish, not execute, its concrete owner action"
|
||||
);
|
||||
complete_published_top_level_navigation(&mut conn, &mut command_context).await;
|
||||
assert!(
|
||||
@@ -854,45 +845,41 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn canceling_an_unrelated_command_does_not_downgrade_held_output() {
|
||||
let mut conn = connection_with_loaded_page().await;
|
||||
let mut barriers = RuntimeCommandOutputBarriers::default();
|
||||
let first = admit_registered_command(&mut conn, &mut barriers, 15);
|
||||
let second = admit_registered_command(&mut conn, &mut barriers, 16);
|
||||
let mut order = RendererCommandResponseOrder::default();
|
||||
let first = admit_registered_command(&mut conn, &mut order, 15);
|
||||
let second = admit_registered_command(&mut conn, &mut order, 16);
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
|
||||
route_top_level_navigation_for_command(
|
||||
&mut conn,
|
||||
&mut barriers,
|
||||
&mut order,
|
||||
&first,
|
||||
"exact-predecessor-survives-unrelated-cancel",
|
||||
&mut command_context,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(barriers.held_output_count(), 1);
|
||||
assert_eq!(order.held_output_count(), 1);
|
||||
|
||||
assert_eq!(
|
||||
barriers
|
||||
.cancel(&mut conn, second, &mut command_context)
|
||||
.await,
|
||||
RuntimeCommandOutputBarrierTerminal::Canceled
|
||||
order.cancel(&mut conn, second, &mut command_context).await,
|
||||
RendererCommandResponseTerminal::Canceled
|
||||
);
|
||||
assert_eq!(barriers.active_count(), 1);
|
||||
assert_eq!(barriers.held_output_count(), 1);
|
||||
assert_eq!(order.active_count(), 1);
|
||||
assert_eq!(order.held_output_count(), 1);
|
||||
assert!(
|
||||
!contains_top_level_navigation(&protocol_messages(&mut command_context)),
|
||||
"canceling an unrelated command must neither release nor downgrade another command's output"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
barriers
|
||||
.release(&mut conn, first, &mut command_context)
|
||||
.await,
|
||||
RuntimeCommandOutputBarrierTerminal::Released
|
||||
order.release(&mut conn, first, &mut command_context).await,
|
||||
RendererCommandResponseTerminal::Released
|
||||
);
|
||||
assert_eq!(barriers.active_count(), 0);
|
||||
assert_eq!(barriers.held_output_count(), 0);
|
||||
assert_eq!(order.active_count(), 0);
|
||||
assert_eq!(order.held_output_count(), 0);
|
||||
assert!(
|
||||
!contains_top_level_navigation(&protocol_messages(&mut command_context)),
|
||||
"barrier release must publish, not execute, the retained owner action"
|
||||
"permit release must publish, not execute, the retained owner action"
|
||||
);
|
||||
complete_published_top_level_navigation(&mut conn, &mut command_context).await;
|
||||
assert!(
|
||||
@@ -904,37 +891,35 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn unattributed_same_page_output_is_not_held_by_a_pending_command() {
|
||||
let mut conn = connection_with_loaded_page().await;
|
||||
let mut barriers = RuntimeCommandOutputBarriers::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut barriers, 13);
|
||||
let mut order = RendererCommandResponseOrder::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut order, 13);
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
|
||||
route_same_document_navigation(
|
||||
&mut conn,
|
||||
&mut barriers,
|
||||
&mut order,
|
||||
None,
|
||||
"independent-page-task",
|
||||
&mut command_context,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(barriers.held_output_count(), 0);
|
||||
assert_eq!(order.held_output_count(), 0);
|
||||
assert!(
|
||||
contains_same_document_navigation(&protocol_messages(&mut command_context)),
|
||||
"output without the exact command identity must remain visible while that command awaits"
|
||||
);
|
||||
assert_eq!(
|
||||
barriers
|
||||
.release(&mut conn, permit, &mut command_context)
|
||||
.await,
|
||||
RuntimeCommandOutputBarrierTerminal::Released
|
||||
order.release(&mut conn, permit, &mut command_context).await,
|
||||
RendererCommandResponseTerminal::Released
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn concrete_runtime_navigation_cause_survives_an_earlier_untagged_wake() {
|
||||
let mut conn = connection_with_loaded_page().await;
|
||||
let mut barriers = RuntimeCommandOutputBarriers::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut barriers, 14);
|
||||
let mut order = RendererCommandResponseOrder::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut order, 14);
|
||||
let cause = renderer_cause_for_permit(&conn, &permit);
|
||||
let owner = conn
|
||||
.target_page_residence_identity_for_session(Some(SESSION_ID))
|
||||
@@ -952,7 +937,7 @@ mod tests {
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
let command_owner = CommandOwnerScope::for_session(SESSION_ID);
|
||||
|
||||
barriers
|
||||
order
|
||||
.route_publication_outputs(
|
||||
&mut conn,
|
||||
&command_owner,
|
||||
@@ -963,21 +948,19 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(barriers.held_output_count(), 1);
|
||||
assert_eq!(order.held_output_count(), 1);
|
||||
assert!(
|
||||
!contains_top_level_navigation(&protocol_messages(&mut command_context)),
|
||||
"an older untagged wake must not release a concrete command-caused navigation"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
barriers
|
||||
.release(&mut conn, permit, &mut command_context)
|
||||
.await,
|
||||
RuntimeCommandOutputBarrierTerminal::Released
|
||||
order.release(&mut conn, permit, &mut command_context).await,
|
||||
RendererCommandResponseTerminal::Released
|
||||
);
|
||||
assert!(
|
||||
!contains_top_level_navigation(&protocol_messages(&mut command_context)),
|
||||
"barrier release must publish, not execute, the concrete navigation action"
|
||||
"permit release must publish, not execute, the concrete navigation action"
|
||||
);
|
||||
complete_published_top_level_navigation(&mut conn, &mut command_context).await;
|
||||
assert!(
|
||||
@@ -987,10 +970,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn replacement_supersedes_old_barrier_without_holding_new_page_output() {
|
||||
async fn replacement_supersedes_old_response_permit_without_holding_new_page_output() {
|
||||
let mut conn = connection_with_loaded_page().await;
|
||||
let mut barriers = RuntimeCommandOutputBarriers::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut barriers, 21);
|
||||
let mut order = RendererCommandResponseOrder::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut order, 21);
|
||||
conn.replace_document_fixture_for_owner_test(&crate::conn::CommandOwnerScope::capture(
|
||||
&conn,
|
||||
Some(SESSION_ID),
|
||||
@@ -999,7 +982,7 @@ mod tests {
|
||||
|
||||
route_same_document_navigation(
|
||||
&mut conn,
|
||||
&mut barriers,
|
||||
&mut order,
|
||||
Some(&permit),
|
||||
"replacement",
|
||||
&mut command_context,
|
||||
@@ -1007,9 +990,9 @@ mod tests {
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
barriers.held_output_count(),
|
||||
order.held_output_count(),
|
||||
0,
|
||||
"output from the replacement Page must not be held by an old Page barrier"
|
||||
"output from the replacement Page must not be held by an old Page permit"
|
||||
);
|
||||
assert!(
|
||||
contains_same_document_navigation(&protocol_messages(&mut command_context)),
|
||||
@@ -1017,42 +1000,38 @@ mod tests {
|
||||
recaptured through the old command"
|
||||
);
|
||||
assert_eq!(
|
||||
barriers
|
||||
.release(&mut conn, permit, &mut command_context)
|
||||
.await,
|
||||
RuntimeCommandOutputBarrierTerminal::Superseded
|
||||
order.release(&mut conn, permit, &mut command_context).await,
|
||||
RendererCommandResponseTerminal::Superseded
|
||||
);
|
||||
assert_eq!(barriers.active_count(), 0);
|
||||
assert_eq!(order.active_count(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn replacement_after_capture_retires_held_old_page_output() {
|
||||
let mut conn = connection_with_loaded_page().await;
|
||||
let mut barriers = RuntimeCommandOutputBarriers::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut barriers, 30);
|
||||
let mut order = RendererCommandResponseOrder::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut order, 30);
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
|
||||
route_top_level_navigation_for_command(
|
||||
&mut conn,
|
||||
&mut barriers,
|
||||
&mut order,
|
||||
&permit,
|
||||
"retired-source-page",
|
||||
&mut command_context,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(barriers.held_output_count(), 1);
|
||||
assert_eq!(order.held_output_count(), 1);
|
||||
|
||||
conn.replace_document_fixture_for_owner_test(&crate::conn::CommandOwnerScope::capture(
|
||||
&conn,
|
||||
Some(SESSION_ID),
|
||||
));
|
||||
assert_eq!(
|
||||
barriers
|
||||
.release(&mut conn, permit, &mut command_context)
|
||||
.await,
|
||||
RuntimeCommandOutputBarrierTerminal::Superseded
|
||||
order.release(&mut conn, permit, &mut command_context).await,
|
||||
RendererCommandResponseTerminal::Superseded
|
||||
);
|
||||
assert_eq!(barriers.held_output_count(), 0);
|
||||
assert_eq!(order.held_output_count(), 0);
|
||||
complete_published_top_level_navigation(&mut conn, &mut command_context).await;
|
||||
assert!(
|
||||
!contains_top_level_navigation(&protocol_messages(&mut command_context)),
|
||||
@@ -1061,29 +1040,27 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn explicit_cancel_consumes_the_barrier_once() {
|
||||
async fn explicit_cancel_consumes_the_response_permit_once() {
|
||||
let mut conn = connection_with_loaded_page().await;
|
||||
let mut barriers = RuntimeCommandOutputBarriers::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut barriers, 31);
|
||||
let mut order = RendererCommandResponseOrder::default();
|
||||
let permit = admit_registered_command(&mut conn, &mut order, 31);
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
|
||||
route_top_level_navigation_for_command(
|
||||
&mut conn,
|
||||
&mut barriers,
|
||||
&mut order,
|
||||
&permit,
|
||||
"canceled-protocol-command",
|
||||
&mut command_context,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(barriers.held_output_count(), 1);
|
||||
assert_eq!(order.held_output_count(), 1);
|
||||
assert_eq!(
|
||||
barriers
|
||||
.cancel(&mut conn, permit, &mut command_context)
|
||||
.await,
|
||||
RuntimeCommandOutputBarrierTerminal::Canceled
|
||||
order.cancel(&mut conn, permit, &mut command_context).await,
|
||||
RendererCommandResponseTerminal::Canceled
|
||||
);
|
||||
assert_eq!(barriers.active_count(), 0);
|
||||
assert_eq!(barriers.held_output_count(), 0);
|
||||
assert_eq!(order.active_count(), 0);
|
||||
assert_eq!(order.held_output_count(), 0);
|
||||
assert!(
|
||||
!contains_top_level_navigation(&protocol_messages(&mut command_context)),
|
||||
"cancel must publish, not execute, the already-produced owner action"
|
||||
@@ -4,6 +4,10 @@ use crate::domains::actions::ConsoleAction;
|
||||
use crate::domains::command_output::CommandOutputPlan;
|
||||
use crate::domains::runtime::{RuntimeCommandTaskStep, start_console_inspector_command_dispatch};
|
||||
|
||||
pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool {
|
||||
cmd.parse_action::<ConsoleAction>().is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn try_start_console_command_dispatch(
|
||||
conn: &mut CdpConnection,
|
||||
cmd: &Cmd<'_>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::conn::{CdpConnection, Cmd, CommandOwnerScope};
|
||||
use crate::conn::{CdpConnection, Cmd, CommandOwnerScope, RendererDispatchLane};
|
||||
use crate::domains::actions::CssAction;
|
||||
use crate::domains::command_output::CommandOutputPlan;
|
||||
use chromiumoxide_cdp::cdp::browser_protocol::css::{
|
||||
@@ -44,6 +44,10 @@ pub(crate) enum CssCommandDispatchStep {
|
||||
Complete(CommandOutputPlan),
|
||||
}
|
||||
|
||||
pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool {
|
||||
cmd.parse_action::<CssAction>().is_some()
|
||||
}
|
||||
|
||||
enum PendingCssCommandKind {
|
||||
Enable {
|
||||
frame_id: String,
|
||||
@@ -78,6 +82,12 @@ struct PendingCssCommandStartError {
|
||||
}
|
||||
|
||||
impl PendingCssCommandDispatch {
|
||||
pub(crate) fn renderer_dispatch_lane(&self) -> Option<RendererDispatchLane> {
|
||||
self.pending
|
||||
.renderer_agent_attachment_id()
|
||||
.map(|_| RendererDispatchLane::Main)
|
||||
}
|
||||
|
||||
fn from_command(
|
||||
conn: &CdpConnection,
|
||||
cmd: &Cmd<'_>,
|
||||
|
||||
@@ -3,6 +3,10 @@ use serde_json::{Map, Value};
|
||||
use crate::conn::{CdpConnection, Cmd};
|
||||
use crate::domains::runtime::{RuntimeCommandTaskStep, start_debugger_inspector_command_dispatch};
|
||||
|
||||
pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool {
|
||||
crate::domains::runtime::debugger_command_waits_for_document_projection(cmd)
|
||||
}
|
||||
|
||||
pub(crate) fn try_start_debugger_command_dispatch(
|
||||
conn: &mut CdpConnection,
|
||||
cmd: &Cmd<'_>,
|
||||
@@ -160,7 +164,7 @@ mod tests {
|
||||
.expect("start cross-Document navigation");
|
||||
assert!(
|
||||
ctx.conn
|
||||
.renderer_document_navigation_is_suspended_for_session_owner(None)
|
||||
.document_projection_is_pending_for_session_owner(None)
|
||||
);
|
||||
|
||||
let source = command(
|
||||
@@ -182,7 +186,10 @@ mod tests {
|
||||
|
||||
let _ = ctx
|
||||
.conn
|
||||
.finish_renderer_document_navigation_for_owner(&navigation_owner, &navigation);
|
||||
.finish_navigation_without_document_projection_for_owner(
|
||||
&navigation_owner,
|
||||
&navigation,
|
||||
);
|
||||
ctx.conn
|
||||
.clear_pending_document_navigation_for_owner_if_matches(&navigation_owner, &navigation);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@ pub(crate) enum DomCommandDispatchStep {
|
||||
Complete(CommandOutputPlan),
|
||||
}
|
||||
|
||||
pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool {
|
||||
cmd.parse_action::<DomAction>()
|
||||
.is_some_and(|action| action != DomAction::Enable)
|
||||
}
|
||||
|
||||
pub(crate) fn try_start_dom_command_dispatch(
|
||||
conn: &mut CdpConnection,
|
||||
cmd: &Cmd<'_>,
|
||||
|
||||
@@ -380,6 +380,12 @@ pub(super) enum PendingSetChildNodesAfter {
|
||||
}
|
||||
|
||||
impl PendingDomCommandDispatch {
|
||||
pub(crate) fn renderer_dispatch_lane(&self) -> Option<crate::conn::RendererDispatchLane> {
|
||||
self.pending
|
||||
.renderer_agent_attachment_id()
|
||||
.map(|_| crate::conn::RendererDispatchLane::Main)
|
||||
}
|
||||
|
||||
pub(crate) async fn wait(self) -> CompletedDomCommandDispatch {
|
||||
let completed = Box::pin(self.pending.wait())
|
||||
.await
|
||||
|
||||
@@ -6,7 +6,7 @@ use moli_core::page::{
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::conn::{CdpConnection, Cmd, CommandOwnerScope};
|
||||
use crate::conn::{CdpConnection, Cmd, CommandOwnerScope, RendererDispatchLane};
|
||||
use crate::domains::actions::DomDebuggerAction;
|
||||
use crate::domains::command_output::CommandOutputPlan;
|
||||
|
||||
@@ -43,6 +43,10 @@ pub(crate) enum DomDebuggerCommandTaskStep {
|
||||
Complete(CommandOutputPlan),
|
||||
}
|
||||
|
||||
pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool {
|
||||
cmd.parse_action::<DomDebuggerAction>().is_some()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GetEventListenersParams {
|
||||
@@ -78,6 +82,12 @@ fn default_depth() -> i32 {
|
||||
}
|
||||
|
||||
impl PendingDomDebuggerCommandDispatch {
|
||||
pub(crate) fn renderer_dispatch_lane(&self) -> Option<RendererDispatchLane> {
|
||||
self.pending
|
||||
.renderer_agent_attachment_id()
|
||||
.map(|_| RendererDispatchLane::Main)
|
||||
}
|
||||
|
||||
pub(crate) async fn wait(self) -> CompletedDomDebuggerCommandDispatch {
|
||||
CompletedDomDebuggerCommandDispatch {
|
||||
command_id: self.command_id,
|
||||
|
||||
@@ -9,7 +9,7 @@ use moli_core::page::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::conn::{CdpConnection, Cmd, CommandOwnerScope};
|
||||
use crate::conn::{CdpConnection, Cmd, CommandOwnerScope, RendererDispatchLane};
|
||||
use crate::domains::actions::DomSnapshotAction;
|
||||
use crate::domains::command_output::CommandOutputPlan;
|
||||
|
||||
@@ -30,7 +30,17 @@ pub(crate) enum DomSnapshotCommandDispatchStep {
|
||||
Complete(CommandOutputPlan),
|
||||
}
|
||||
|
||||
pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool {
|
||||
cmd.parse_action::<DomSnapshotAction>() == Some(DomSnapshotAction::CaptureSnapshot)
|
||||
}
|
||||
|
||||
impl PendingDomSnapshotCommandDispatch {
|
||||
pub(crate) fn renderer_dispatch_lane(&self) -> Option<RendererDispatchLane> {
|
||||
self.pending
|
||||
.renderer_agent_attachment_id()
|
||||
.map(|_| RendererDispatchLane::Main)
|
||||
}
|
||||
|
||||
pub async fn wait(self) -> CompletedDomSnapshotCommandDispatch {
|
||||
CompletedDomSnapshotCommandDispatch {
|
||||
command_id: self.command_id,
|
||||
|
||||
@@ -122,6 +122,16 @@ impl PendingEmulationPageOperation {
|
||||
}
|
||||
|
||||
impl PendingEmulationCommandDispatch {
|
||||
pub(crate) const fn renderer_dispatch_lane(&self) -> Option<crate::conn::RendererDispatchLane> {
|
||||
match self.pending {
|
||||
PendingEmulationRendererDispatch::IoAdapterReply(_)
|
||||
| PendingEmulationRendererDispatch::IoSessionOutput { .. } => {
|
||||
Some(crate::conn::RendererDispatchLane::Io)
|
||||
}
|
||||
PendingEmulationRendererDispatch::Pages(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn wait(self) -> CompletedEmulationCommandDispatch {
|
||||
let completed = match self.pending {
|
||||
PendingEmulationRendererDispatch::Pages(pending_pages) => {
|
||||
|
||||
@@ -6,6 +6,16 @@ use crate::domains::runtime::{
|
||||
start_moli_diagnostics_command_dispatch,
|
||||
};
|
||||
|
||||
pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool {
|
||||
cmd.parse_action::<HeapProfilerAction>()
|
||||
.is_some_and(|action| {
|
||||
!matches!(
|
||||
action,
|
||||
HeapProfilerAction::MoliDiagnostics | HeapProfilerAction::MoliResetIdleEngine
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn try_start_heap_profiler_command_dispatch(
|
||||
conn: &mut CdpConnection,
|
||||
cmd: &Cmd<'_>,
|
||||
|
||||
@@ -360,7 +360,44 @@ pub(crate) enum PageCommandTaskStep {
|
||||
Complete(CommandOutputPlan),
|
||||
}
|
||||
|
||||
pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool {
|
||||
matches!(
|
||||
cmd.parse_action::<PageAction>(),
|
||||
Some(PageAction::GetFrameTree | PageAction::GetResourceTree | PageAction::GetLayoutMetrics)
|
||||
)
|
||||
}
|
||||
|
||||
impl PendingPageCommandDispatch {
|
||||
pub(crate) fn renderer_dispatch_lane(&self) -> Option<crate::conn::RendererDispatchLane> {
|
||||
match self.kind.as_ref() {
|
||||
PendingPageCommandKind::GetFrameTree { .. }
|
||||
| PendingPageCommandKind::GetLayoutMetrics { .. } => {
|
||||
Some(crate::conn::RendererDispatchLane::Main)
|
||||
}
|
||||
PendingPageCommandKind::BringToFront { .. }
|
||||
| PendingPageCommandKind::AppendDefaultDocumentStartScript { .. }
|
||||
| PendingPageCommandKind::RemoveDocumentStartScript { .. }
|
||||
| PendingPageCommandKind::AddScriptToEvaluateOnNewDocument(_)
|
||||
| PendingPageCommandKind::SearchInResource(_)
|
||||
| PendingPageCommandKind::GetAppManifest(_)
|
||||
| PendingPageCommandKind::ResetNavigationHistory { .. }
|
||||
| PendingPageCommandKind::SetDocumentContent { .. }
|
||||
| PendingPageCommandKind::SetBypassContentSecurityPolicy { .. }
|
||||
| PendingPageCommandKind::SameDocumentNavigate(_)
|
||||
| PendingPageCommandKind::CaptureSnapshot { .. }
|
||||
| PendingPageCommandKind::CaptureScreenshot { .. }
|
||||
| PendingPageCommandKind::PrintToPdf { .. }
|
||||
| PendingPageCommandKind::Navigate(_)
|
||||
| PendingPageCommandKind::TraverseSameDocumentHistory(_)
|
||||
| PendingPageCommandKind::ChildFrameNavigate(_)
|
||||
| PendingPageCommandKind::ContinueNavigationWithoutRequestPause(_)
|
||||
| PendingPageCommandKind::StopLoading
|
||||
| PendingPageCommandKind::Crash
|
||||
| PendingPageCommandKind::Close
|
||||
| PendingPageCommandKind::CreateIsolatedWorld(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait(self) -> CompletedPageCommandDispatch {
|
||||
let kind = match *self.kind {
|
||||
PendingPageCommandKind::BringToFront {
|
||||
|
||||
@@ -3205,6 +3205,7 @@ async fn complete_materialized_navigation_into_buffer_inner_async(
|
||||
) {
|
||||
let navigation_owner = state.owner.clone();
|
||||
let navigation_session_id = navigation_owner.session_id().map(str::to_owned);
|
||||
let mut document_projection_release = None;
|
||||
match navigation {
|
||||
network::MaterializedNavigationLoadOutcome::ResponseCommitReady(navigation) => {
|
||||
match conn.start_response_document_materialization_for_owner(
|
||||
@@ -3219,7 +3220,7 @@ async fn complete_materialized_navigation_into_buffer_inner_async(
|
||||
network::materialize_loaded_navigation_progress(
|
||||
conn, &state, navigation,
|
||||
);
|
||||
commit_loaded_navigation_async(
|
||||
document_projection_release = commit_loaded_navigation_async(
|
||||
conn,
|
||||
out,
|
||||
&token,
|
||||
@@ -3254,8 +3255,10 @@ async fn complete_materialized_navigation_into_buffer_inner_async(
|
||||
let primary_protocol_session_id = conn
|
||||
.runtime_session_owner_primary_session_id_for_owner(&navigation_owner)
|
||||
.or_else(|| navigation_session_id.clone());
|
||||
let (routed_renderer_output, renderer_call_replacements) = conn
|
||||
.finish_renderer_document_navigation_for_owner(&navigation_owner, &token)
|
||||
let (routed_renderer_output, renderer_call_replacements) = document_projection_release
|
||||
.or_else(|| {
|
||||
conn.finish_navigation_without_document_projection_for_owner(&navigation_owner, &token)
|
||||
})
|
||||
.map(|finish| (finish.released_output, finish.renderer_call_replacements))
|
||||
.unwrap_or_default();
|
||||
if !routed_renderer_output.is_empty() {
|
||||
|
||||
@@ -19,7 +19,7 @@ pub(super) async fn commit_loaded_navigation_async(
|
||||
prepared: crate::conn::PreparedDocumentNavigation,
|
||||
navigation: MaterializedLoadedDocumentProgress,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) {
|
||||
) -> Option<crate::conn::DocumentProjectionOutputRelease> {
|
||||
let MaterializedLoadedDocumentProgress {
|
||||
pending_download,
|
||||
final_url,
|
||||
@@ -38,7 +38,7 @@ pub(super) async fn commit_loaded_navigation_async(
|
||||
navigation_activity =
|
||||
navigation_activity.with_network_error_page_result(error_page.error_text().to_owned());
|
||||
}
|
||||
let Some(lifecycle) = commit_and_project_loaded_navigation_async(
|
||||
let (lifecycle, projection_fence) = commit_and_project_loaded_navigation_async(
|
||||
conn,
|
||||
out,
|
||||
navigation_activity.state(),
|
||||
@@ -46,10 +46,7 @@ pub(super) async fn commit_loaded_navigation_async(
|
||||
initial_runtime_realms,
|
||||
command_context,
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
.await?;
|
||||
if !is_network_error_page {
|
||||
let _ = conn.commit_main_document_resource_for_owner(
|
||||
&navigation_activity.state().owner,
|
||||
@@ -69,6 +66,7 @@ pub(super) async fn commit_loaded_navigation_async(
|
||||
&navigation_activity.state().owner,
|
||||
crate::conn::CommittedDocumentLifecycle {
|
||||
document: lifecycle.document,
|
||||
browser_sequence: lifecycle.browser_sequence,
|
||||
artifacts,
|
||||
},
|
||||
Some(*token),
|
||||
@@ -95,21 +93,39 @@ pub(super) async fn commit_loaded_navigation_async(
|
||||
navigation_activity.defer_initial_renderer_document_lifecycle_events_until_load_boundary(
|
||||
deferred_initial_renderer_document_lifecycle_events,
|
||||
);
|
||||
let projection_owner = navigation_activity.state().owner.clone();
|
||||
let projected_document = renderer_document_binding.clone();
|
||||
// Keep the loaded commit tail boxed: the target/Patchright CDP test thread
|
||||
// has historically hit stack limits when this future is inlined.
|
||||
Box::pin(async move {
|
||||
navigation_activity
|
||||
.emit_loaded_navigation_commit_async(
|
||||
conn,
|
||||
out,
|
||||
pending_download,
|
||||
renderer_document_binding,
|
||||
initial_renderer_document_lifecycle_events,
|
||||
renderer_output_predecessor,
|
||||
)
|
||||
.await;
|
||||
})
|
||||
Box::pin(navigation_activity.emit_loaded_navigation_commit_async(
|
||||
&mut *conn,
|
||||
out,
|
||||
pending_download,
|
||||
renderer_document_binding,
|
||||
initial_renderer_document_lifecycle_events,
|
||||
renderer_output_predecessor,
|
||||
))
|
||||
.await;
|
||||
|
||||
match projection_fence {
|
||||
Some(fence) => {
|
||||
let projected_document = projected_document.as_ref().unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Browser sequence {} rebound renderer attachment {} without a committed frame projection",
|
||||
fence.browser_sequence().get(),
|
||||
fence.renderer_attachment().get(),
|
||||
)
|
||||
});
|
||||
Some(conn.publish_document_projection_fence_for_owner(
|
||||
&projection_owner,
|
||||
projected_document,
|
||||
fence,
|
||||
))
|
||||
}
|
||||
None => {
|
||||
conn.finish_navigation_without_document_projection_for_owner(&projection_owner, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split_renderer_page_creation_lifecycle_at_load_boundary(
|
||||
@@ -196,7 +212,10 @@ async fn commit_and_project_loaded_navigation_async(
|
||||
prepared: crate::conn::PreparedDocumentNavigation,
|
||||
initial_runtime_realms: Vec<RendererRuntimeRealmInfo>,
|
||||
command_context: &mut CommandDispatchContext,
|
||||
) -> Option<crate::conn::CommittedDocumentLifecycle> {
|
||||
) -> Option<(
|
||||
crate::conn::CommittedDocumentLifecycle,
|
||||
Option<crate::conn::DocumentProjectionFence>,
|
||||
)> {
|
||||
let commit = match conn.commit_loaded_navigation(prepared) {
|
||||
Ok(commit) => commit,
|
||||
Err(error) => {
|
||||
@@ -207,8 +226,8 @@ async fn commit_and_project_loaded_navigation_async(
|
||||
|
||||
// Everything below observes a committed Browser Document. Inspection failure
|
||||
// settles DevTools work; it must never turn this navigation into a rollback.
|
||||
let inspection_available = match commit.inspection_projection {
|
||||
Ok(()) => true,
|
||||
let (inspection_available, projection_fence) = match commit.inspection_projection {
|
||||
Ok(fence) => (true, Some(fence)),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, session_id = state.owner.session_id(),
|
||||
"inspection projection failed after Browser navigation committed");
|
||||
@@ -230,7 +249,7 @@ async fn commit_and_project_loaded_navigation_async(
|
||||
sessions,
|
||||
"Inspector rebind failed after navigation",
|
||||
);
|
||||
false
|
||||
(false, None)
|
||||
}
|
||||
};
|
||||
commit.previous_document_retirement.close().await;
|
||||
@@ -277,7 +296,7 @@ async fn commit_and_project_loaded_navigation_async(
|
||||
}
|
||||
out.extend_background_events_after_messages(events);
|
||||
}
|
||||
Some(commit.lifecycle)
|
||||
Some((commit.lifecycle, projection_fence))
|
||||
}
|
||||
|
||||
/// Rebind failures are session failures, never Browser crash or close commands.
|
||||
|
||||
@@ -34,31 +34,33 @@ async fn install_document_content_test_page(ctx: &mut TestContext, url: &str) {
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
let committed = browser_context
|
||||
browser_context
|
||||
.commit_loaded_navigation(prepared)
|
||||
.expect("document-content test Document should commit");
|
||||
assert!(committed.inspection_projection.is_ok());
|
||||
assert!(
|
||||
browser_context
|
||||
.active_page_target_mut()
|
||||
.runtime_slot
|
||||
.finish_renderer_document_navigation(&committed_document)
|
||||
.expect("document-content test renderer navigation should finish")
|
||||
.released_output
|
||||
.is_empty(),
|
||||
"the fixture should not leave buffered Inspector output behind"
|
||||
);
|
||||
committed
|
||||
.expect("document-content test Document should commit")
|
||||
};
|
||||
let projection_fence = committed
|
||||
.inspection_projection
|
||||
.expect("document-content test inspection projection should succeed");
|
||||
committed.previous_document_retirement.close().await;
|
||||
let (binding, _) = ctx.conn.bind_renderer_document_lifecycle_for_owner(
|
||||
let (binding, _) = ctx.conn.project_committed_document_lifecycle_for_owner(
|
||||
&crate::conn::CommandOwnerScope::for_session("SID-1"),
|
||||
artifacts,
|
||||
committed.lifecycle,
|
||||
Some(committed_document),
|
||||
"TID-1".to_owned(),
|
||||
LOADER_ID.to_owned(),
|
||||
);
|
||||
assert!(binding.is_some(), "renderer lifecycle should bind");
|
||||
let binding = binding.expect("renderer lifecycle should bind");
|
||||
assert!(
|
||||
ctx.conn
|
||||
.publish_document_projection_fence_for_owner(
|
||||
&crate::conn::CommandOwnerScope::for_session("SID-1"),
|
||||
&binding,
|
||||
projection_fence,
|
||||
)
|
||||
.released_output
|
||||
.is_empty(),
|
||||
"the fixture should not leave buffered Inspector output behind"
|
||||
);
|
||||
// The fixture commits an already-running renderer Page directly instead
|
||||
// of going through the production navigation command. Route that exact
|
||||
// Document's initial lifecycle publication before enabling observers.
|
||||
|
||||
@@ -64,6 +64,10 @@ pub(crate) enum PerformanceCommandTaskStep {
|
||||
}
|
||||
|
||||
impl PendingPerformanceCommandDispatch {
|
||||
pub(crate) const fn renderer_dispatch_lane(&self) -> Option<crate::conn::RendererDispatchLane> {
|
||||
Some(crate::conn::RendererDispatchLane::Io)
|
||||
}
|
||||
|
||||
pub async fn wait(self) -> CompletedPerformanceCommandDispatch {
|
||||
let completed = match *self.pending {
|
||||
PendingPerformanceRendererCommand::IoAdapterReply {
|
||||
|
||||
@@ -2,6 +2,10 @@ use crate::conn::{CdpConnection, Cmd, ProfilerAction, ProfilerInspectorCommand};
|
||||
use crate::domains::command_output::CommandOutputPlan;
|
||||
use crate::domains::runtime::{RuntimeCommandTaskStep, start_profiler_inspector_command_dispatch};
|
||||
|
||||
pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool {
|
||||
cmd.parse_action::<ProfilerAction>().is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn try_start_profiler_command_dispatch(
|
||||
conn: &mut CdpConnection,
|
||||
cmd: &Cmd<'_>,
|
||||
|
||||
@@ -151,8 +151,8 @@ pub(in crate::domains) use dispatcher::replay_shared_worker_runtime_bindings_for
|
||||
pub(crate) use dispatcher::{
|
||||
BidiPreloadFunctionDeclaration, CompletedRuntimeCommandDispatch, PendingRuntimeCommandDispatch,
|
||||
RuntimeCommandTaskStep, bidi_preload_function_declaration_source,
|
||||
complete_pending_runtime_command_at_response_boundary,
|
||||
devtools_deep_serialization_options_json,
|
||||
command_waits_for_document_projection, complete_pending_runtime_command_at_response_boundary,
|
||||
debugger_command_waits_for_document_projection, devtools_deep_serialization_options_json,
|
||||
execute_devtools_runtime_command_async_with_protocol_events,
|
||||
execute_runtime_listener_command_for_owner,
|
||||
start_bidi_preload_channel_listeners_for_execution_context_background_events_async,
|
||||
|
||||
@@ -24,8 +24,8 @@ use moli_page_types::RendererInspectorResponseDelivery;
|
||||
use crate::conn::{
|
||||
BackgroundCommandResponsePayload, BackgroundCommandResponsePayloadRef, BackgroundProtocolEvent,
|
||||
BidiChannelListenerResidence, BidiChannelOwnerAction, BidiChannelPageOwner, CdpConnection,
|
||||
CdpRendererCommandPolicy, CdpRendererDispatchLane, CdpSchedulerEvent, CdpSessionRoute,
|
||||
ClaimedPendingInspectorAwait, Cmd, CommandOwnerScope, CompletedMoliDiagnosticsDispatch,
|
||||
CdpRendererCommandPolicy, CdpSchedulerEvent, CdpSessionRoute, ClaimedPendingInspectorAwait,
|
||||
Cmd, CommandOwnerScope, CompletedMoliDiagnosticsDispatch,
|
||||
CompletedRuntimeBindingPageCommandDispatch, CompletedRuntimeChildDefaultContextLookupDispatch,
|
||||
CompletedRuntimeEnableEventsDispatch, CompletedRuntimeProtocolMessageDispatch,
|
||||
CompletedServiceWorkerRuntimeProtocolMessageDispatch,
|
||||
@@ -36,11 +36,11 @@ use crate::conn::{
|
||||
PendingRuntimeEnableEventsDispatch, PendingRuntimeProtocolMessageDispatch,
|
||||
PendingServiceWorkerRuntimeProtocolMessageDispatch,
|
||||
PendingSharedWorkerRuntimeProtocolMessageDispatch, ProfilerInspectorCommand,
|
||||
RendererCommandDescriptor, RuntimeBindingDefinition, RuntimeEnableReplayEvent,
|
||||
RuntimeInspectorAsyncCompletionReceiver, RuntimeInspectorResponseReady,
|
||||
ServiceWorkerRuntimeExceptionSnapshot, SessionOwnerRuntimeFrontendEnableResult,
|
||||
monotonic_timestamp_seconds, renderer_command_turn_frontend_protocol_response,
|
||||
runtime_remote_object_ids_in_map,
|
||||
RendererCommandDescriptor, RendererDispatchLane, RuntimeBindingDefinition,
|
||||
RuntimeEnableReplayEvent, RuntimeInspectorAsyncCompletionReceiver,
|
||||
RuntimeInspectorResponseReady, ServiceWorkerRuntimeExceptionSnapshot,
|
||||
SessionOwnerRuntimeFrontendEnableResult, monotonic_timestamp_seconds,
|
||||
renderer_command_turn_frontend_protocol_response, runtime_remote_object_ids_in_map,
|
||||
};
|
||||
use crate::domains::actions::{ConsoleAction, HeapProfilerAction, RuntimeAction};
|
||||
use crate::domains::command_output::{
|
||||
@@ -521,6 +521,26 @@ impl RuntimeCommandCompletionMeta {
|
||||
}
|
||||
|
||||
impl PendingRuntimeCommandDispatch {
|
||||
pub(crate) fn renderer_dispatch_lane(&self) -> Option<RendererDispatchLane> {
|
||||
match &self.pending {
|
||||
PendingRuntimeCommandKind::Inspector { pending }
|
||||
| PendingRuntimeCommandKind::BindingInspector { pending, .. } => {
|
||||
Some(renderer_dispatch_lane(pending.renderer_route()))
|
||||
}
|
||||
PendingRuntimeCommandKind::SharedWorkerInspector { .. }
|
||||
| PendingRuntimeCommandKind::ServiceWorkerInspector { .. } => {
|
||||
Some(RendererDispatchLane::Io)
|
||||
}
|
||||
PendingRuntimeCommandKind::Enable(_)
|
||||
| PendingRuntimeCommandKind::BindingContextLookup { .. } => {
|
||||
Some(RendererDispatchLane::Main)
|
||||
}
|
||||
PendingRuntimeCommandKind::InspectorDeferredReply { .. }
|
||||
| PendingRuntimeCommandKind::MoliDiagnostics(_)
|
||||
| PendingRuntimeCommandKind::BindingPage { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn command_id(&self) -> Option<u64> {
|
||||
self.command_id
|
||||
}
|
||||
@@ -1021,6 +1041,21 @@ pub(crate) fn try_start_runtime_command_dispatch(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool {
|
||||
!matches!(
|
||||
cmd.parse_action::<RuntimeAction>(),
|
||||
None | Some(
|
||||
RuntimeAction::AddBinding
|
||||
| RuntimeAction::RemoveBinding
|
||||
| RuntimeAction::TerminateExecution
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn debugger_command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool {
|
||||
debugger_renderer_dispatch_lane(cmd.action) == RendererDispatchLane::Main && can_dispatch(cmd)
|
||||
}
|
||||
|
||||
fn start_main_runtime_inspector_command(
|
||||
conn: &mut CdpConnection,
|
||||
cmd: &Cmd<'_>,
|
||||
@@ -1080,10 +1115,16 @@ fn start_main_runtime_inspector_command_for_owner(
|
||||
));
|
||||
}
|
||||
};
|
||||
let renderer_lane = if action == RuntimeAction::TerminateExecution {
|
||||
RendererDispatchLane::Io
|
||||
} else {
|
||||
RendererDispatchLane::Main
|
||||
};
|
||||
let pending = match start_pending_runtime_routable_inspector_dispatch(
|
||||
conn,
|
||||
cmd,
|
||||
&owner_scope,
|
||||
renderer_lane,
|
||||
inspector_json,
|
||||
cmd.terminal_response_delivery(),
|
||||
) {
|
||||
@@ -1281,26 +1322,20 @@ pub(crate) fn start_debugger_inspector_command_dispatch(
|
||||
}
|
||||
|
||||
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
|
||||
let pending = match cmd.renderer_policy().renderer_lane() {
|
||||
Some(CdpRendererDispatchLane::Main) => {
|
||||
start_pending_runtime_inspector_dispatch_with_delivery(
|
||||
conn,
|
||||
cmd,
|
||||
&owner_scope,
|
||||
inspector_json,
|
||||
cmd.terminal_response_delivery(),
|
||||
)
|
||||
}
|
||||
Some(CdpRendererDispatchLane::Io) => start_pending_runtime_io_inspector_dispatch(
|
||||
let pending = match debugger_renderer_dispatch_lane(cmd.action) {
|
||||
RendererDispatchLane::Main => start_pending_runtime_inspector_dispatch_with_delivery(
|
||||
conn,
|
||||
cmd,
|
||||
&owner_scope,
|
||||
inspector_json,
|
||||
cmd.terminal_response_delivery(),
|
||||
),
|
||||
None => Err(
|
||||
"a command without renderer fallthrough cannot enter the Debugger Inspector dispatcher"
|
||||
.to_owned(),
|
||||
RendererDispatchLane::Io => start_pending_runtime_io_inspector_dispatch(
|
||||
conn,
|
||||
cmd,
|
||||
&owner_scope,
|
||||
inspector_json,
|
||||
cmd.terminal_response_delivery(),
|
||||
),
|
||||
};
|
||||
let pending = match pending {
|
||||
@@ -1861,7 +1896,7 @@ fn try_start_pending_runtime_binding_command(
|
||||
|| should_persist
|
||||
&& conn
|
||||
.runtime_session_owner_slot_for_owner(&owner_scope)
|
||||
.is_ok_and(|slot| slot.renderer_document_navigation_is_suspended());
|
||||
.is_ok_and(|slot| slot.document_projection_is_pending());
|
||||
if live_page_update_unavailable {
|
||||
task.command_response = Some(RuntimeBindingCommandResponse::empty_success());
|
||||
let meta = RuntimeCommandCompletionMeta {
|
||||
@@ -1938,33 +1973,52 @@ fn start_pending_runtime_routable_inspector_dispatch(
|
||||
conn: &mut CdpConnection,
|
||||
cmd: &Cmd<'_>,
|
||||
owner: &CommandOwnerScope,
|
||||
lane: RendererDispatchLane,
|
||||
inspector_json: String,
|
||||
response_delivery: RendererInspectorResponseDelivery,
|
||||
) -> Result<PendingRuntimeProtocolMessageDispatch, String> {
|
||||
match cmd.renderer_policy().renderer_lane() {
|
||||
Some(CdpRendererDispatchLane::Main) => {
|
||||
start_pending_runtime_inspector_dispatch_with_delivery(
|
||||
conn,
|
||||
cmd,
|
||||
owner,
|
||||
inspector_json,
|
||||
response_delivery,
|
||||
)
|
||||
}
|
||||
Some(CdpRendererDispatchLane::Io) => start_pending_runtime_io_inspector_dispatch(
|
||||
match lane {
|
||||
RendererDispatchLane::Main => start_pending_runtime_inspector_dispatch_with_delivery(
|
||||
conn,
|
||||
cmd,
|
||||
owner,
|
||||
inspector_json,
|
||||
response_delivery,
|
||||
),
|
||||
None => Err(
|
||||
"a command without renderer fallthrough cannot enter the Runtime Inspector dispatcher"
|
||||
.to_owned(),
|
||||
RendererDispatchLane::Io => start_pending_runtime_io_inspector_dispatch(
|
||||
conn,
|
||||
cmd,
|
||||
owner,
|
||||
inspector_json,
|
||||
response_delivery,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn renderer_dispatch_lane(
|
||||
route: moli_core::page::RendererInspectorCommandRoute,
|
||||
) -> RendererDispatchLane {
|
||||
match route {
|
||||
moli_core::page::RendererInspectorCommandRoute::MainThread => RendererDispatchLane::Main,
|
||||
moli_core::page::RendererInspectorCommandRoute::Io => RendererDispatchLane::Io,
|
||||
}
|
||||
}
|
||||
|
||||
fn debugger_renderer_dispatch_lane(action: &str) -> RendererDispatchLane {
|
||||
match action {
|
||||
"getPossibleBreakpoints"
|
||||
| "getScriptSource"
|
||||
| "getStackTrace"
|
||||
| "pause"
|
||||
| "removeBreakpoint"
|
||||
| "resume"
|
||||
| "setBreakpoint"
|
||||
| "setBreakpointByUrl"
|
||||
| "setBreakpointsActive" => RendererDispatchLane::Io,
|
||||
_ => RendererDispatchLane::Main,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_pending_runtime_io_inspector_dispatch(
|
||||
conn: &mut CdpConnection,
|
||||
cmd: &Cmd<'_>,
|
||||
@@ -2262,7 +2316,7 @@ fn pre_register_runtime_await_if_needed(
|
||||
owner,
|
||||
object_group,
|
||||
)?;
|
||||
conn.register_runtime_await_job_for_owner(command_id, owner, object_group, action);
|
||||
conn.trace_runtime_await_started(command_id, owner, object_group, action);
|
||||
conn.trace_runtime_await_pending_registered(command_id, owner.session_id());
|
||||
Ok(Some(command_id))
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ async fn add_binding_during_document_navigation_persists_without_touching_retiri
|
||||
ctx.conn.commit_declared_session_fixtures_for_test();
|
||||
assert!(
|
||||
ctx.conn
|
||||
.renderer_document_navigation_is_suspended_for_session_owner(Some("SID-1"))
|
||||
.document_projection_is_pending_for_session_owner(Some("SID-1"))
|
||||
);
|
||||
|
||||
ctx.process_async(json!({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::conn::{
|
||||
BackgroundProtocolEvent, CdpConnection, SessionDisposalPlan, SessionDisposalTarget,
|
||||
BackgroundProtocolEvent, CdpConnection, DevToolsSessionHandlerSet, SessionDisposalPlan,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -44,22 +44,13 @@ const WORKER_HANDLERS: &[DevToolsSessionDomainHandler] = &[
|
||||
/// state or disable operation. Page Inspector resources have already retired
|
||||
/// through the renderer session lifecycle; these handlers revoke service
|
||||
/// contributions and reconcile Browser-owned policy without that Inspector.
|
||||
struct DevToolsSessionHandlers {
|
||||
handlers: &'static [DevToolsSessionDomainHandler],
|
||||
}
|
||||
|
||||
impl DevToolsSessionHandlers {
|
||||
fn for_target(target: &SessionDisposalTarget) -> Self {
|
||||
let handlers = match target {
|
||||
SessionDisposalTarget::PageTarget { .. } => PAGE_HANDLERS,
|
||||
SessionDisposalTarget::SharedWorkerTarget { .. }
|
||||
| SessionDisposalTarget::DedicatedWorkerTarget { .. }
|
||||
| SessionDisposalTarget::ServiceWorkerTarget { .. } => WORKER_HANDLERS,
|
||||
SessionDisposalTarget::Browser | SessionDisposalTarget::TabTarget { .. } => {
|
||||
CONNECTION_HANDLERS
|
||||
}
|
||||
};
|
||||
Self { handlers }
|
||||
impl DevToolsSessionHandlerSet {
|
||||
fn handlers(self) -> &'static [DevToolsSessionDomainHandler] {
|
||||
match self {
|
||||
Self::Connection => CONNECTION_HANDLERS,
|
||||
Self::Page => PAGE_HANDLERS,
|
||||
Self::Worker => WORKER_HANDLERS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +83,7 @@ pub(crate) async fn dispose_live_handlers_async(
|
||||
first_error: None,
|
||||
renderer_output_predecessor: None,
|
||||
};
|
||||
for handler in DevToolsSessionHandlers::for_target(plan.target()).handlers {
|
||||
for handler in plan.handler_set().handlers() {
|
||||
let result = handler
|
||||
.dispose_async(
|
||||
conn,
|
||||
@@ -211,30 +202,16 @@ impl DevToolsSessionDomainHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use moli_page_types::DevToolsSessionKey;
|
||||
|
||||
#[test]
|
||||
fn handler_sets_are_owned_by_session_target_kind() {
|
||||
let page = SessionDisposalTarget::PageTarget {
|
||||
browser_context_id: "BID".to_owned(),
|
||||
target_id: "TID".to_owned(),
|
||||
session_key: DevToolsSessionKey::Attached("SID".to_owned()),
|
||||
};
|
||||
assert_eq!(DevToolsSessionHandlerSet::Page.handlers(), PAGE_HANDLERS);
|
||||
assert_eq!(
|
||||
DevToolsSessionHandlers::for_target(&page).handlers,
|
||||
PAGE_HANDLERS
|
||||
);
|
||||
|
||||
let worker = SessionDisposalTarget::DedicatedWorkerTarget {
|
||||
browser_context_id: "BID".to_owned(),
|
||||
target_id: "WID".to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
DevToolsSessionHandlers::for_target(&worker).handlers,
|
||||
DevToolsSessionHandlerSet::Worker.handlers(),
|
||||
WORKER_HANDLERS
|
||||
);
|
||||
assert_eq!(
|
||||
DevToolsSessionHandlers::for_target(&SessionDisposalTarget::Browser).handlers,
|
||||
DevToolsSessionHandlerSet::Connection.handlers(),
|
||||
CONNECTION_HANDLERS
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,8 @@ fn prepare_session_disposal(
|
||||
detachment: TargetSessionDetachCleanupPlan,
|
||||
) -> anyhow::Result<(SessionDisposalPlan, TargetSessionDetachCleanupPlan)> {
|
||||
let session_id = detachment.session_id();
|
||||
let route = conn
|
||||
.session_route(Some(session_id))
|
||||
.ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?;
|
||||
let plan = SessionDisposalPlan::for_session_route(session_id, &route)
|
||||
let plan = conn
|
||||
.session_disposal_plan(session_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?;
|
||||
anyhow::ensure!(
|
||||
plan.target_id() == Some(detachment.target_id()),
|
||||
@@ -24,10 +22,8 @@ fn prepare_browser_session_disposal(
|
||||
conn: &CdpConnection,
|
||||
session_id: &str,
|
||||
) -> anyhow::Result<SessionDisposalPlan> {
|
||||
let route = conn
|
||||
.session_route(Some(session_id))
|
||||
.ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?;
|
||||
let plan = SessionDisposalPlan::for_session_route(session_id, &route)
|
||||
let plan = conn
|
||||
.session_disposal_plan(session_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?;
|
||||
anyhow::ensure!(
|
||||
matches!(plan.target(), SessionDisposalTarget::Browser),
|
||||
@@ -175,10 +171,8 @@ pub(super) async fn dispose_primary_page_session_preserving_frontend_async(
|
||||
protocol_events: &mut Vec<BackgroundProtocolEvent>,
|
||||
session_id: &str,
|
||||
) -> anyhow::Result<Option<moli_core::RendererOutputFence>> {
|
||||
let route = conn
|
||||
.session_route(Some(session_id))
|
||||
.ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?;
|
||||
let plan = SessionDisposalPlan::for_session_route(session_id, &route)
|
||||
let plan = conn
|
||||
.session_disposal_plan(session_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?;
|
||||
if !matches!(
|
||||
plan.target(),
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
use super::super::tests_cdp_smoke_fixture::SmokeFixtureServer;
|
||||
use super::super::*;
|
||||
use crate::{CdpCommandTaskStep, CommandDispatchContext, ParsedCdpCommand};
|
||||
use crate::{AgentHostDispatchResult, CommandDispatchContext, ParsedCdpCommand};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn event<'a>(messages: &'a [Value], method: &str) -> &'a Value {
|
||||
@@ -280,7 +280,7 @@ async fn create_isolated_world_restart_does_not_inherit_the_stale_renderer_strea
|
||||
}))
|
||||
.expect("createIsolatedWorld command should parse");
|
||||
let mut command_context = CommandDispatchContext::default();
|
||||
let CdpCommandTaskStep::Pending(first_pending) = ctx
|
||||
let AgentHostDispatchResult::PendingService(first_pending) = ctx
|
||||
.conn
|
||||
.start_parsed_command_dispatch_with_context(&command, &mut command_context)
|
||||
else {
|
||||
@@ -298,7 +298,7 @@ async fn create_isolated_world_restart_does_not_inherit_the_stale_renderer_strea
|
||||
assert_eq!(navigation["result"]["frameId"], json!(target_id));
|
||||
|
||||
let first_completed = first_pending.wait().await;
|
||||
let CdpCommandTaskStep::Pending(restarted) = ctx
|
||||
let AgentHostDispatchResult::PendingService(restarted) = ctx
|
||||
.conn
|
||||
.complete_pending_command_dispatch_with_context(first_completed, &mut command_context)
|
||||
.await
|
||||
@@ -311,7 +311,7 @@ async fn create_isolated_world_restart_does_not_inherit_the_stale_renderer_strea
|
||||
);
|
||||
|
||||
let replacement_completed = restarted.wait().await;
|
||||
let CdpCommandTaskStep::Complete(outcome) = ctx
|
||||
let AgentHostDispatchResult::Complete(outcome) = ctx
|
||||
.conn
|
||||
.complete_pending_command_dispatch_with_context(replacement_completed, &mut command_context)
|
||||
.await
|
||||
|
||||
@@ -2052,6 +2052,7 @@ fn remove_shared_worker_target_with_reason(
|
||||
);
|
||||
CdpConnection::fail_pending_inspector_awaits_from_shared_worker_target_session_background_events_into(
|
||||
&mut pending_await_direct_events,
|
||||
&mut pending_await_claimed_events,
|
||||
&mut target,
|
||||
session_id,
|
||||
reason,
|
||||
@@ -2220,11 +2221,14 @@ fn remove_service_worker_target_with_reason(
|
||||
);
|
||||
}
|
||||
let mut target_pending_await_events = Vec::new();
|
||||
let mut target_claimed_await_events = Vec::new();
|
||||
CdpConnection::fail_pending_inspector_awaits_from_service_worker_target_state_background_events_into(
|
||||
&mut target_pending_await_events,
|
||||
&mut target_claimed_await_events,
|
||||
&mut target,
|
||||
reason,
|
||||
);
|
||||
target_pending_await_events.extend(target_claimed_await_events);
|
||||
push_service_worker_version_events(&mut outputs, version.clone(), target_pending_await_events);
|
||||
if let Some(retirement) = run_retirement {
|
||||
outputs.push(WorkerTargetLifecycleOutput::ServiceWorkerRunRetired { retirement });
|
||||
|
||||
@@ -18,9 +18,9 @@ pub mod version;
|
||||
pub use devtools_runtime::*;
|
||||
|
||||
pub use conn::{
|
||||
BackgroundCommandResponsePayload, BackgroundProtocolEvent, CdpCommandTaskStep, CdpConnection,
|
||||
CdpInitialStoragePartition, CdpRendererCommandReplacement, CdpRendererCommandReplayDispatch,
|
||||
CdpRendererDispatchLane, CdpRendererOwnerTurnOutcome, CdpSchedulerEvent,
|
||||
AgentHostDispatchResult, BackgroundCommandResponsePayload, BackgroundProtocolEvent,
|
||||
CdpCommandTaskStep, CdpConnection, CdpInitialStoragePartition, CdpRendererCommandReplacement,
|
||||
CdpRendererCommandReplayDispatch, CdpRendererOwnerTurnOutcome, CdpSchedulerEvent,
|
||||
CdpTargetHostLifecycleDelta, CdpTargetHostLifecycleObserver, CdpTurnOutcome,
|
||||
CommandDispatchContext, CommandResponseFlushContext, CommandResponseFlushPermit,
|
||||
CompletedCdpCommandDispatch, CompletedDeferredMainDocumentLoadCompletion,
|
||||
@@ -31,12 +31,13 @@ pub use conn::{
|
||||
DevToolsDocumentLifecycleWaitKey, DevToolsDocumentLifecycleWaitState,
|
||||
DevToolsDocumentNavigationState, DevToolsPageResidenceIdentity, ParsedCdpCommand,
|
||||
PendingCdpCommandDispatch, PendingDeferredMainDocumentLoadCompletion,
|
||||
PendingRuntimeProtocolMessageDispatch,
|
||||
PendingRuntimeProtocolMessageDispatch, RendererDispatch, RendererDispatchBinding,
|
||||
RendererDispatchLane, RendererPageDispatchBinding,
|
||||
};
|
||||
pub use domains::activity::{
|
||||
ProtocolSchedulerWork, ProtocolSchedulerWorkKind, ProtocolWorkPublishSequence,
|
||||
RuntimeCommandOutputBarrierCompletion, RuntimeCommandOutputBarrierPermit,
|
||||
RuntimeCommandOutputBarrierTerminal, RuntimeCommandOutputBarriers,
|
||||
RendererCommandResponseCompletion, RendererCommandResponseOrder, RendererCommandResponsePermit,
|
||||
RendererCommandResponseTerminal,
|
||||
};
|
||||
pub use domains::page::{
|
||||
BackgroundNavigationCompletion, CompletedPageScreencastCapture,
|
||||
|
||||
@@ -14,8 +14,8 @@ use super::conn::{
|
||||
};
|
||||
use crate::devtools_runtime::{DevToolsCommand, DevToolsCommandResult, DevToolsError};
|
||||
use crate::domains::activity::{
|
||||
ProtocolSchedulerWork, ProtocolSchedulerWorkKind, RuntimeCommandOutputBarrierCompletion,
|
||||
RuntimeCommandOutputBarrierPermit, RuntimeCommandOutputBarriers,
|
||||
ProtocolSchedulerWork, ProtocolSchedulerWorkKind, RendererCommandResponseCompletion,
|
||||
RendererCommandResponseOrder, RendererCommandResponsePermit,
|
||||
};
|
||||
use moli_core::{
|
||||
LayoutPolicy, OptionalResourceFetchMask, RendererOutputFence, RendererOutputStreamControl,
|
||||
@@ -42,7 +42,7 @@ pub struct TestContext {
|
||||
pub sent: Vec<Value>,
|
||||
pending_runtime_deferred_replies: VecDeque<PendingTestRuntimeDeferredReply>,
|
||||
pending_protocol_scheduler_work: VecDeque<ProtocolSchedulerWork>,
|
||||
runtime_command_output_barriers: RuntimeCommandOutputBarriers,
|
||||
renderer_command_response_order: RendererCommandResponseOrder,
|
||||
runtime_inspector_response_ready_rx:
|
||||
tokio::sync::mpsc::UnboundedReceiver<RuntimeInspectorResponseReady>,
|
||||
renderer_publication_rx: moli_core::RendererOutputTransportReceiver,
|
||||
@@ -58,19 +58,19 @@ pub struct TestContext {
|
||||
struct PendingTestRuntimeDeferredReply {
|
||||
pending: PendingCdpCommandDispatch,
|
||||
command_context: CommandDispatchContext,
|
||||
runtime_output_barrier: Option<RuntimeCommandOutputBarrierPermit>,
|
||||
renderer_response_permit: Option<RendererCommandResponsePermit>,
|
||||
}
|
||||
|
||||
impl PendingTestRuntimeDeferredReply {
|
||||
fn new(
|
||||
pending: PendingCdpCommandDispatch,
|
||||
command_context: CommandDispatchContext,
|
||||
runtime_output_barrier: Option<RuntimeCommandOutputBarrierPermit>,
|
||||
renderer_response_permit: Option<RendererCommandResponsePermit>,
|
||||
) -> Self {
|
||||
Self {
|
||||
pending,
|
||||
command_context,
|
||||
runtime_output_barrier,
|
||||
renderer_response_permit,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,14 +86,14 @@ enum TestSchedulerWork {
|
||||
BackgroundNavigationCompletion(crate::domains::page::BackgroundNavigationCompletion),
|
||||
RuntimeDeferredReplyReady(RuntimeInspectorResponseReady),
|
||||
RendererPublication(RendererOutputTransportMessage),
|
||||
ReleaseRuntimeOutputBarrier(RuntimeCommandOutputBarrierPermit),
|
||||
CancelRuntimeOutputBarrier(RuntimeCommandOutputBarrierPermit),
|
||||
ReleaseRendererResponsePermit(RendererCommandResponsePermit),
|
||||
CancelRendererResponsePermit(RendererCommandResponsePermit),
|
||||
}
|
||||
|
||||
#[must_use = "the held test command response must be released exactly once"]
|
||||
pub(crate) struct TestCommandResponseFlushPermit {
|
||||
response_flush: CommandResponseFlushPermit,
|
||||
runtime_output_barrier: Option<RuntimeCommandOutputBarrierPermit>,
|
||||
renderer_response_permit: Option<RendererCommandResponsePermit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -215,7 +215,7 @@ impl TestContext {
|
||||
sent: Vec::new(),
|
||||
pending_runtime_deferred_replies: VecDeque::new(),
|
||||
pending_protocol_scheduler_work: VecDeque::new(),
|
||||
runtime_command_output_barriers: RuntimeCommandOutputBarriers::default(),
|
||||
renderer_command_response_order: RendererCommandResponseOrder::default(),
|
||||
runtime_inspector_response_ready_rx,
|
||||
renderer_publication_rx,
|
||||
background_event_tx,
|
||||
@@ -350,7 +350,9 @@ impl TestContext {
|
||||
.conn
|
||||
.commit_loaded_navigation(prepared)
|
||||
.expect("navigation fixture Page commit must succeed");
|
||||
assert!(page_commit.inspection_projection.is_ok());
|
||||
let projection_fence = page_commit
|
||||
.inspection_projection
|
||||
.expect("navigation fixture must rebind its renderer inspection endpoint");
|
||||
assert!(
|
||||
page_commit
|
||||
.committed_document_post_response_continuation
|
||||
@@ -358,18 +360,6 @@ impl TestContext {
|
||||
"lifecycle-target fixture must not retain a DocumentCommit response gate"
|
||||
);
|
||||
page_commit.previous_document_retirement.close().await;
|
||||
let finished = self
|
||||
.conn
|
||||
.finish_renderer_document_navigation_for_owner(&owner, &token)
|
||||
.expect("navigation fixture must finish its renderer suspension");
|
||||
assert!(
|
||||
finished.released_output.is_empty(),
|
||||
"fixture output is ingested after the Document binding"
|
||||
);
|
||||
assert!(
|
||||
finished.renderer_call_replacements.is_none(),
|
||||
"fixture must not replace in-flight renderer calls"
|
||||
);
|
||||
let (binding, _) = self.conn.project_committed_document_lifecycle_for_owner(
|
||||
&owner,
|
||||
page_commit.lifecycle,
|
||||
@@ -379,6 +369,19 @@ impl TestContext {
|
||||
);
|
||||
let binding =
|
||||
binding.expect("navigation fixture must install its exact renderer Document binding");
|
||||
let finished = self.conn.publish_document_projection_fence_for_owner(
|
||||
&owner,
|
||||
&binding,
|
||||
projection_fence,
|
||||
);
|
||||
assert!(
|
||||
finished.released_output.is_empty(),
|
||||
"fixture output is ingested after the Document binding"
|
||||
);
|
||||
assert!(
|
||||
finished.renderer_call_replacements.is_none(),
|
||||
"fixture must not replace in-flight renderer calls"
|
||||
);
|
||||
assert_eq!(
|
||||
self.conn
|
||||
.target_root_document_lifecycle_identity_for_owner(&owner,),
|
||||
@@ -573,8 +576,9 @@ impl TestContext {
|
||||
let mut command_context = CommandDispatchContext::new(response_flush_context);
|
||||
let step = self
|
||||
.conn
|
||||
.start_parsed_command_dispatch_with_context(&command, &mut command_context);
|
||||
let mut runtime_output_barrier = self.admit_runtime_output_barrier(&command);
|
||||
.start_parsed_command_dispatch_with_context(&command, &mut command_context)
|
||||
.into();
|
||||
let mut renderer_response_permit = self.admit_renderer_response_permit(&command);
|
||||
let mut protocol_events = Vec::new();
|
||||
let mut scheduler_events = Vec::new();
|
||||
let completed = Box::pin(self.complete_command_step_like_scheduler(
|
||||
@@ -582,7 +586,7 @@ impl TestContext {
|
||||
&mut command_context,
|
||||
&mut protocol_events,
|
||||
&mut scheduler_events,
|
||||
&mut runtime_output_barrier,
|
||||
&mut renderer_response_permit,
|
||||
))
|
||||
.await;
|
||||
assert!(
|
||||
@@ -596,7 +600,7 @@ impl TestContext {
|
||||
Box::pin(self.route_ready_test_command_response(command_id, response_start)).await;
|
||||
TestCommandResponseFlushPermit {
|
||||
response_flush: response_flush_permit,
|
||||
runtime_output_barrier,
|
||||
renderer_response_permit,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,9 +615,12 @@ impl TestContext {
|
||||
permit: TestCommandResponseFlushPermit,
|
||||
) {
|
||||
permit.response_flush.finish();
|
||||
if let Some(runtime_output_barrier) = permit.runtime_output_barrier {
|
||||
if let Some(renderer_response_permit) = permit.renderer_response_permit {
|
||||
Box::pin(
|
||||
self.release_runtime_output_barrier_like_scheduler(runtime_output_barrier, true),
|
||||
self.release_renderer_response_permit_like_scheduler(
|
||||
renderer_response_permit,
|
||||
true,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -631,13 +638,13 @@ impl TestContext {
|
||||
};
|
||||
let mut protocol_events = Vec::new();
|
||||
let mut scheduler_events = Vec::new();
|
||||
let mut runtime_output_barrier = None;
|
||||
let mut renderer_response_permit = None;
|
||||
let completed = Box::pin(self.complete_command_step_like_scheduler(
|
||||
step,
|
||||
&mut crate::conn::CommandDispatchContext::default(),
|
||||
&mut protocol_events,
|
||||
&mut scheduler_events,
|
||||
&mut runtime_output_barrier,
|
||||
&mut renderer_response_permit,
|
||||
))
|
||||
.await;
|
||||
if completed {
|
||||
@@ -741,14 +748,15 @@ impl TestContext {
|
||||
let mut command_context = crate::conn::CommandDispatchContext::default();
|
||||
let step = self
|
||||
.conn
|
||||
.start_parsed_command_dispatch_with_context(command, &mut command_context);
|
||||
let mut runtime_output_barrier = self.admit_runtime_output_barrier(command);
|
||||
.start_parsed_command_dispatch_with_context(command, &mut command_context)
|
||||
.into();
|
||||
let mut renderer_response_permit = self.admit_renderer_response_permit(command);
|
||||
let completed = Box::pin(self.complete_command_step_like_scheduler(
|
||||
step,
|
||||
&mut command_context,
|
||||
&mut protocol_events,
|
||||
&mut scheduler_events,
|
||||
&mut runtime_output_barrier,
|
||||
&mut renderer_response_permit,
|
||||
))
|
||||
.await;
|
||||
|
||||
@@ -768,17 +776,17 @@ impl TestContext {
|
||||
Box::pin(self.route_test_scheduler_causal_batch(protocol_events, scheduler_events))
|
||||
.await;
|
||||
if completed {
|
||||
if let Some(runtime_output_barrier) = runtime_output_barrier {
|
||||
Box::pin(self.release_runtime_output_barrier_like_scheduler(
|
||||
runtime_output_barrier,
|
||||
if let Some(renderer_response_permit) = renderer_response_permit {
|
||||
Box::pin(self.release_renderer_response_permit_like_scheduler(
|
||||
renderer_response_permit,
|
||||
true,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
assert!(
|
||||
runtime_output_barrier.is_none(),
|
||||
"a pending Runtime command must transfer its output barrier to the pending reply"
|
||||
renderer_response_permit.is_none(),
|
||||
"a pending renderer command must transfer its response permit to the pending reply"
|
||||
);
|
||||
}
|
||||
if !completed && !self.pending_runtime_deferred_replies.is_empty() {
|
||||
@@ -788,10 +796,10 @@ impl TestContext {
|
||||
} else {
|
||||
Box::pin(self.route_test_scheduler_causal_batch(protocol_events, Vec::new())).await;
|
||||
if completed {
|
||||
if let Some(runtime_output_barrier) = runtime_output_barrier {
|
||||
if let Some(renderer_response_permit) = renderer_response_permit {
|
||||
let mut release_scheduler_events =
|
||||
Box::pin(self.release_runtime_output_barrier_like_scheduler(
|
||||
runtime_output_barrier,
|
||||
Box::pin(self.release_renderer_response_permit_like_scheduler(
|
||||
renderer_response_permit,
|
||||
false,
|
||||
))
|
||||
.await;
|
||||
@@ -799,8 +807,8 @@ impl TestContext {
|
||||
}
|
||||
} else {
|
||||
assert!(
|
||||
runtime_output_barrier.is_none(),
|
||||
"a pending Runtime command must transfer its output barrier to the pending reply"
|
||||
renderer_response_permit.is_none(),
|
||||
"a pending renderer command must transfer its response permit to the pending reply"
|
||||
);
|
||||
}
|
||||
if !completed && !self.pending_runtime_deferred_replies.is_empty() {
|
||||
@@ -810,14 +818,14 @@ impl TestContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn admit_runtime_output_barrier(
|
||||
fn admit_renderer_response_permit(
|
||||
&mut self,
|
||||
command: &ParsedCdpCommand,
|
||||
) -> Option<RuntimeCommandOutputBarrierPermit> {
|
||||
) -> Option<RendererCommandResponsePermit> {
|
||||
command
|
||||
.runtime_command_executes_page_javascript()
|
||||
.then(|| {
|
||||
self.runtime_command_output_barriers.admit(
|
||||
self.renderer_command_response_order.admit(
|
||||
&self.conn,
|
||||
command.request().id(),
|
||||
command.command_output_session_id(),
|
||||
@@ -832,7 +840,7 @@ impl TestContext {
|
||||
command_context: &mut CommandDispatchContext,
|
||||
protocol_events: &mut Vec<BackgroundProtocolEvent>,
|
||||
scheduler_events: &mut Vec<CdpSchedulerEvent>,
|
||||
runtime_output_barrier: &mut Option<RuntimeCommandOutputBarrierPermit>,
|
||||
renderer_response_permit: &mut Option<RendererCommandResponsePermit>,
|
||||
) -> bool {
|
||||
loop {
|
||||
match step {
|
||||
@@ -905,7 +913,7 @@ impl TestContext {
|
||||
self.enqueue_pending_runtime_deferred_reply(
|
||||
*pending,
|
||||
std::mem::take(command_context),
|
||||
runtime_output_barrier.take(),
|
||||
renderer_response_permit.take(),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -920,7 +928,8 @@ impl TestContext {
|
||||
completed,
|
||||
command_context,
|
||||
))
|
||||
.await;
|
||||
.await
|
||||
.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1038,15 +1047,15 @@ impl TestContext {
|
||||
(self.sent.drain(sent_start..).collect(), scheduler_events)
|
||||
}
|
||||
|
||||
async fn release_runtime_output_barrier_like_scheduler(
|
||||
async fn release_renderer_response_permit_like_scheduler(
|
||||
&mut self,
|
||||
permit: RuntimeCommandOutputBarrierPermit,
|
||||
permit: RendererCommandResponsePermit,
|
||||
route_scheduler_events: bool,
|
||||
) -> Vec<CdpSchedulerEvent> {
|
||||
let completion = self
|
||||
.conn
|
||||
.release_runtime_command_output_barrier_turn_async(
|
||||
&mut self.runtime_command_output_barriers,
|
||||
.release_renderer_command_response_permit_turn_async(
|
||||
&mut self.renderer_command_response_order,
|
||||
permit,
|
||||
)
|
||||
.await;
|
||||
@@ -1062,9 +1071,9 @@ impl TestContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn enqueue_runtime_output_barrier_completion(
|
||||
fn enqueue_renderer_response_permit_completion(
|
||||
&mut self,
|
||||
completion: RuntimeCommandOutputBarrierCompletion,
|
||||
completion: RendererCommandResponseCompletion,
|
||||
work: &mut VecDeque<TestSchedulerWork>,
|
||||
) {
|
||||
let (protocol_events, scheduler_events) =
|
||||
@@ -1114,7 +1123,7 @@ impl TestContext {
|
||||
&mut self,
|
||||
mut pending: PendingCdpCommandDispatch,
|
||||
command_context: CommandDispatchContext,
|
||||
runtime_output_barrier: Option<RuntimeCommandOutputBarrierPermit>,
|
||||
renderer_response_permit: Option<RendererCommandResponsePermit>,
|
||||
) {
|
||||
if let Some(command_id) = pending.command_id()
|
||||
&& let Some(response_rx) = pending.take_scheduler_deferred_inspector_reply_receiver()
|
||||
@@ -1139,7 +1148,7 @@ impl TestContext {
|
||||
.push_back(PendingTestRuntimeDeferredReply::new(
|
||||
pending,
|
||||
command_context,
|
||||
runtime_output_barrier,
|
||||
renderer_response_permit,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1178,25 +1187,25 @@ impl TestContext {
|
||||
Box::pin(self.ingest_renderer_publication_like_scheduler(publication, work))
|
||||
.await;
|
||||
}
|
||||
TestSchedulerWork::ReleaseRuntimeOutputBarrier(permit) => {
|
||||
TestSchedulerWork::ReleaseRendererResponsePermit(permit) => {
|
||||
let completion = self
|
||||
.conn
|
||||
.release_runtime_command_output_barrier_turn_async(
|
||||
&mut self.runtime_command_output_barriers,
|
||||
.release_renderer_command_response_permit_turn_async(
|
||||
&mut self.renderer_command_response_order,
|
||||
permit,
|
||||
)
|
||||
.await;
|
||||
self.enqueue_runtime_output_barrier_completion(completion, work);
|
||||
self.enqueue_renderer_response_permit_completion(completion, work);
|
||||
}
|
||||
TestSchedulerWork::CancelRuntimeOutputBarrier(permit) => {
|
||||
TestSchedulerWork::CancelRendererResponsePermit(permit) => {
|
||||
let completion = self
|
||||
.conn
|
||||
.cancel_runtime_command_output_barrier_turn_async(
|
||||
&mut self.runtime_command_output_barriers,
|
||||
.cancel_renderer_command_response_permit_turn_async(
|
||||
&mut self.renderer_command_response_order,
|
||||
permit,
|
||||
)
|
||||
.await;
|
||||
self.enqueue_runtime_output_barrier_completion(completion, work);
|
||||
self.enqueue_renderer_response_permit_completion(completion, work);
|
||||
}
|
||||
}
|
||||
self.route_ready_protocol_scheduler_work_for_test_context(work)
|
||||
@@ -1519,9 +1528,9 @@ impl TestContext {
|
||||
"message": "RuntimeDeferredReplyLooseProtocolResponse",
|
||||
},
|
||||
}));
|
||||
if let Some(runtime_output_barrier) = pending.runtime_output_barrier.take() {
|
||||
work.push_front(TestSchedulerWork::CancelRuntimeOutputBarrier(
|
||||
runtime_output_barrier,
|
||||
if let Some(renderer_response_permit) = pending.renderer_response_permit.take() {
|
||||
work.push_front(TestSchedulerWork::CancelRendererResponsePermit(
|
||||
renderer_response_permit,
|
||||
));
|
||||
}
|
||||
if !events.is_empty() {
|
||||
@@ -1632,7 +1641,7 @@ impl TestContext {
|
||||
.conn
|
||||
.ingest_renderer_output_turn_async(
|
||||
publication,
|
||||
&mut self.runtime_command_output_barriers,
|
||||
&mut self.renderer_command_response_order,
|
||||
)
|
||||
.await;
|
||||
let (protocol_events, scheduler_events) = outcome.into_protocol_event_parts();
|
||||
@@ -1738,7 +1747,8 @@ impl TestContext {
|
||||
let step = self
|
||||
.conn
|
||||
.complete_pending_command_dispatch_with_context(completed, &mut pending.command_context)
|
||||
.await;
|
||||
.await
|
||||
.into();
|
||||
let mut protocol_events = Vec::new();
|
||||
let mut scheduler_events = Vec::new();
|
||||
let completed = Box::pin(self.complete_command_step_like_scheduler(
|
||||
@@ -1746,22 +1756,22 @@ impl TestContext {
|
||||
&mut pending.command_context,
|
||||
&mut protocol_events,
|
||||
&mut scheduler_events,
|
||||
&mut pending.runtime_output_barrier,
|
||||
&mut pending.renderer_response_permit,
|
||||
))
|
||||
.await;
|
||||
if !suffix_events.is_empty() {
|
||||
work.push_front(TestSchedulerWork::ProtocolEvents(suffix_events));
|
||||
}
|
||||
if completed {
|
||||
if let Some(runtime_output_barrier) = pending.runtime_output_barrier {
|
||||
work.push_front(TestSchedulerWork::ReleaseRuntimeOutputBarrier(
|
||||
runtime_output_barrier,
|
||||
if let Some(renderer_response_permit) = pending.renderer_response_permit {
|
||||
work.push_front(TestSchedulerWork::ReleaseRendererResponsePermit(
|
||||
renderer_response_permit,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
assert!(
|
||||
pending.runtime_output_barrier.is_none(),
|
||||
"a still-pending Runtime command must transfer its output barrier"
|
||||
pending.renderer_response_permit.is_none(),
|
||||
"a still-pending renderer command must transfer its response permit"
|
||||
);
|
||||
}
|
||||
if !scheduler_events.is_empty() {
|
||||
@@ -2209,7 +2219,7 @@ mod tests {
|
||||
sent: Vec::new(),
|
||||
pending_runtime_deferred_replies: VecDeque::new(),
|
||||
pending_protocol_scheduler_work: VecDeque::new(),
|
||||
runtime_command_output_barriers: RuntimeCommandOutputBarriers::default(),
|
||||
renderer_command_response_order: RendererCommandResponseOrder::default(),
|
||||
runtime_inspector_response_ready_rx: runtime_response_rx,
|
||||
renderer_publication_rx: publication_rx,
|
||||
background_event_tx,
|
||||
|
||||
Reference in New Issue
Block a user