mirror of
https://github.com/servo/servo
synced 2026-05-09 16:42:16 +02:00
The `InspectorActor` and its children now query the `BrowsingContextActor` for the active pipeline ID and script sender instead of storing their own copies, which become outdated on navigation. Previously, the inspector remained permanently stuck to the pipeline that was active upon launch. Now, connecting a fresh devtools client session after navigation allows inspection of each `BrowsingContext`'s active DOM. Navigation within a continuous remote debugging session still breaks the Firefox inspector panel. This is due to multiple compatibility issues with the protocol implementation, which will be addressed in separate PRs. This PR does not touch `TimelineActor` or `FramerateActor`. These are theoretically affected by the same issue as the `InspectorActor`, but in practice neither is ever instantiated. It seems both have been effectively unreachable code since1aab10f2and will require more extensive work and testing that is beyond the scope of this change. Testing: Updates to the `WalkerActor` and `BrowsingContextActor` are unit tested via the new `test_walker_observes_new_dom_after_nav` case in `devtools_tests.py`. There are no existing unit tests to reference for the highlighter and style actors; these have been tested manually but this PR does not introduce further automated tests. This PR also improves handling of `tabNavigated` messages to simplify the unit test written for4c69d85. --------- Signed-off-by: Brent Schroeter <contact@brentsch.com>
417 lines
14 KiB
Rust
417 lines
14 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
//! Liberally derived from the [Firefox JS implementation](https://searchfox.org/mozilla-central/source/devtools/server/actors/webbrowser.js).
|
|
//! Connection point for remote devtools that wish to investigate a particular Browsing Context's contents.
|
|
//! Supports dynamic attaching and detaching which control notifications of navigation, etc.
|
|
|
|
use std::net::TcpStream;
|
|
|
|
use atomic_refcell::AtomicRefCell;
|
|
use base::generic_channel::{self, GenericSender, SendError};
|
|
use base::id::PipelineId;
|
|
use devtools_traits::DevtoolScriptControlMsg::{self, GetCssDatabase, SimulateColorScheme};
|
|
use devtools_traits::{DevtoolsPageInfo, NavigationState};
|
|
use embedder_traits::Theme;
|
|
use malloc_size_of_derive::MallocSizeOf;
|
|
use rustc_hash::FxHashMap;
|
|
use serde::Serialize;
|
|
use serde_json::{Map, Value};
|
|
|
|
use crate::actor::{Actor, ActorEncode, ActorError, ActorRegistry};
|
|
use crate::actors::inspector::InspectorActor;
|
|
use crate::actors::inspector::accessibility::AccessibilityActor;
|
|
use crate::actors::inspector::css_properties::CssPropertiesActor;
|
|
use crate::actors::reflow::ReflowActor;
|
|
use crate::actors::stylesheets::StyleSheetsActor;
|
|
use crate::actors::tab::TabDescriptorActor;
|
|
use crate::actors::thread::ThreadActor;
|
|
use crate::actors::watcher::{SessionContext, SessionContextType, WatcherActor};
|
|
use crate::id::{DevtoolsBrowserId, DevtoolsBrowsingContextId, DevtoolsOuterWindowId, IdMap};
|
|
use crate::protocol::{ClientRequest, JsonPacketStream};
|
|
use crate::resource::ResourceAvailable;
|
|
use crate::{EmptyReplyMsg, StreamId};
|
|
|
|
#[derive(Serialize)]
|
|
struct ListWorkersReply {
|
|
from: String,
|
|
workers: Vec<()>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct FrameUpdateReply {
|
|
from: String,
|
|
#[serde(rename = "type")]
|
|
type_: String,
|
|
frames: Vec<FrameUpdateMsg>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct FrameUpdateMsg {
|
|
id: u32,
|
|
is_top_level: bool,
|
|
url: String,
|
|
title: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct TabNavigated {
|
|
from: String,
|
|
#[serde(rename = "type")]
|
|
type_: String,
|
|
url: String,
|
|
title: Option<String>,
|
|
#[serde(rename = "nativeConsoleAPI")]
|
|
native_console_api: bool,
|
|
state: String,
|
|
is_frame_switching: bool,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct BrowsingContextTraits {
|
|
frames: bool,
|
|
is_browsing_context: bool,
|
|
log_in_page: bool,
|
|
navigation: bool,
|
|
supports_top_level_target_flag: bool,
|
|
watchpoints: bool,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
enum TargetType {
|
|
Frame,
|
|
// Other target types not implemented yet.
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct BrowsingContextActorMsg {
|
|
actor: String,
|
|
title: String,
|
|
url: String,
|
|
/// This correspond to webview_id
|
|
#[serde(rename = "browserId")]
|
|
browser_id: u32,
|
|
#[serde(rename = "outerWindowID")]
|
|
outer_window_id: u32,
|
|
#[serde(rename = "browsingContextID")]
|
|
browsing_context_id: u32,
|
|
is_top_level_target: bool,
|
|
traits: BrowsingContextTraits,
|
|
// Implemented actors
|
|
accessibility_actor: String,
|
|
console_actor: String,
|
|
css_properties_actor: String,
|
|
inspector_actor: String,
|
|
reflow_actor: String,
|
|
style_sheets_actor: String,
|
|
thread_actor: String,
|
|
target_type: TargetType,
|
|
// Part of the official protocol, but not yet implemented.
|
|
// animations_actor: String,
|
|
// changes_actor: String,
|
|
// framerate_actor: String,
|
|
// manifest_actor: String,
|
|
// memory_actor: String,
|
|
// network_content_actor: String,
|
|
// objects_manager: String,
|
|
// performance_actor: String,
|
|
// resonsive_actor: String,
|
|
// storage_actor: String,
|
|
// tracer_actor: String,
|
|
// web_extension_inspected_window_actor: String,
|
|
// web_socket_actor: String,
|
|
}
|
|
|
|
/// The browsing context actor encompasses all of the other supporting actors when debugging a web
|
|
/// view. To this extent, it contains a watcher actor that helps when communicating with the host,
|
|
/// as well as resource actors that each perform one debugging function.
|
|
#[derive(MallocSizeOf)]
|
|
pub(crate) struct BrowsingContextActor {
|
|
name: String,
|
|
pub title: AtomicRefCell<String>,
|
|
pub url: AtomicRefCell<String>,
|
|
/// This corresponds to webview_id
|
|
pub browser_id: DevtoolsBrowserId,
|
|
// TODO: Should these ids be atomic?
|
|
active_pipeline_id: AtomicRefCell<PipelineId>,
|
|
active_outer_window_id: AtomicRefCell<DevtoolsOuterWindowId>,
|
|
pub browsing_context_id: DevtoolsBrowsingContextId,
|
|
accessibility: String,
|
|
pub console: String,
|
|
css_properties: String,
|
|
pub(crate) inspector: String,
|
|
reflow: String,
|
|
style_sheets: String,
|
|
pub thread: String,
|
|
_tab: String,
|
|
// Different pipelines may run on different script threads.
|
|
// These should be kept around even when the active pipeline is updated,
|
|
// in case the browsing context revisits a pipeline via history navigation.
|
|
// TODO: Each entry is stored forever; ideally there should be a way to
|
|
// detect when `ScriptThread`s are destroyed and remove the associated
|
|
// entries.
|
|
script_chans: AtomicRefCell<FxHashMap<PipelineId, GenericSender<DevtoolScriptControlMsg>>>,
|
|
pub watcher: String,
|
|
}
|
|
|
|
impl ResourceAvailable for BrowsingContextActor {
|
|
fn actor_name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
}
|
|
|
|
impl Actor for BrowsingContextActor {
|
|
fn name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
|
|
fn handle_message(
|
|
&self,
|
|
request: ClientRequest,
|
|
_registry: &ActorRegistry,
|
|
msg_type: &str,
|
|
_msg: &Map<String, Value>,
|
|
_id: StreamId,
|
|
) -> Result<(), ActorError> {
|
|
match msg_type {
|
|
"listFrames" => {
|
|
// TODO: Find out what needs to be listed here
|
|
let msg = EmptyReplyMsg { from: self.name() };
|
|
request.reply_final(&msg)?
|
|
},
|
|
"listWorkers" => {
|
|
request.reply_final(&ListWorkersReply {
|
|
from: self.name(),
|
|
// TODO: Find out what needs to be listed here
|
|
workers: vec![],
|
|
})?
|
|
},
|
|
_ => return Err(ActorError::UnrecognizedPacketType),
|
|
};
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl BrowsingContextActor {
|
|
#[expect(clippy::too_many_arguments)]
|
|
pub(crate) fn new(
|
|
console: String,
|
|
browser_id: DevtoolsBrowserId,
|
|
browsing_context_id: DevtoolsBrowsingContextId,
|
|
page_info: DevtoolsPageInfo,
|
|
pipeline_id: PipelineId,
|
|
outer_window_id: DevtoolsOuterWindowId,
|
|
script_sender: GenericSender<DevtoolScriptControlMsg>,
|
|
actors: &ActorRegistry,
|
|
) -> BrowsingContextActor {
|
|
let name = actors.new_name::<BrowsingContextActor>();
|
|
let DevtoolsPageInfo {
|
|
title,
|
|
url,
|
|
is_top_level_global,
|
|
} = page_info;
|
|
|
|
let accessibility = AccessibilityActor::new(actors.new_name::<AccessibilityActor>());
|
|
|
|
let properties = (|| {
|
|
let (properties_sender, properties_receiver) = generic_channel::channel()?;
|
|
script_sender.send(GetCssDatabase(properties_sender)).ok()?;
|
|
properties_receiver.recv().ok()
|
|
})()
|
|
.unwrap_or_default();
|
|
let css_properties =
|
|
CssPropertiesActor::new(actors.new_name::<CssPropertiesActor>(), properties);
|
|
|
|
let inspector = InspectorActor::register(actors, name.clone());
|
|
|
|
let reflow = ReflowActor::new(actors.new_name::<ReflowActor>());
|
|
|
|
let style_sheets = StyleSheetsActor::new(actors.new_name::<StyleSheetsActor>());
|
|
|
|
let tabdesc = TabDescriptorActor::new(actors, name.clone(), is_top_level_global);
|
|
|
|
let thread = ThreadActor::new(
|
|
actors.new_name::<ThreadActor>(),
|
|
script_sender.clone(),
|
|
Some(name.clone()),
|
|
);
|
|
|
|
let watcher = WatcherActor::new(
|
|
actors,
|
|
name.clone(),
|
|
SessionContext::new(SessionContextType::BrowserElement),
|
|
);
|
|
|
|
let mut script_chans = FxHashMap::default();
|
|
script_chans.insert(pipeline_id, script_sender);
|
|
|
|
let target = BrowsingContextActor {
|
|
name,
|
|
script_chans: AtomicRefCell::new(script_chans),
|
|
title: AtomicRefCell::new(title),
|
|
url: AtomicRefCell::new(url.into_string()),
|
|
active_pipeline_id: AtomicRefCell::new(pipeline_id),
|
|
active_outer_window_id: AtomicRefCell::new(outer_window_id),
|
|
browser_id,
|
|
browsing_context_id,
|
|
accessibility: accessibility.name(),
|
|
console,
|
|
css_properties: css_properties.name(),
|
|
inspector,
|
|
reflow: reflow.name(),
|
|
style_sheets: style_sheets.name(),
|
|
_tab: tabdesc.name(),
|
|
thread: thread.name(),
|
|
watcher: watcher.name(),
|
|
};
|
|
|
|
actors.register(accessibility);
|
|
actors.register(css_properties);
|
|
actors.register(reflow);
|
|
actors.register(style_sheets);
|
|
actors.register(tabdesc);
|
|
actors.register(thread);
|
|
actors.register(watcher);
|
|
|
|
target
|
|
}
|
|
|
|
pub(crate) fn handle_new_global(
|
|
&self,
|
|
pipeline: PipelineId,
|
|
script_sender: GenericSender<DevtoolScriptControlMsg>,
|
|
) {
|
|
self.script_chans
|
|
.borrow_mut()
|
|
.insert(pipeline, script_sender);
|
|
}
|
|
|
|
pub(crate) fn handle_navigate<'a>(
|
|
&self,
|
|
state: NavigationState,
|
|
id_map: &mut IdMap,
|
|
connections: impl Iterator<Item = &'a mut TcpStream>,
|
|
) {
|
|
let (pipeline_id, title, url, state) = match state {
|
|
NavigationState::Start(url) => (None, None, url, "start"),
|
|
NavigationState::Stop(pipeline, info) => {
|
|
(Some(pipeline), Some(info.title), info.url, "stop")
|
|
},
|
|
};
|
|
if let Some(pipeline_id) = pipeline_id {
|
|
let outer_window_id = id_map.outer_window_id(pipeline_id);
|
|
*self.active_outer_window_id.borrow_mut() = outer_window_id;
|
|
*self.active_pipeline_id.borrow_mut() = pipeline_id;
|
|
}
|
|
url.as_str().clone_into(&mut self.url.borrow_mut());
|
|
if let Some(ref t) = title {
|
|
self.title.borrow_mut().clone_from(t);
|
|
}
|
|
|
|
let msg = TabNavigated {
|
|
from: self.name(),
|
|
type_: "tabNavigated".to_owned(),
|
|
url: url.as_str().to_owned(),
|
|
title,
|
|
native_console_api: true,
|
|
state: state.to_owned(),
|
|
is_frame_switching: false,
|
|
};
|
|
|
|
for stream in connections {
|
|
let _ = stream.write_json_packet(&msg);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn title_changed(&self, pipeline_id: PipelineId, title: String) {
|
|
if pipeline_id != self.pipeline_id() {
|
|
return;
|
|
}
|
|
*self.title.borrow_mut() = title;
|
|
}
|
|
|
|
pub(crate) fn frame_update(&self, request: &mut ClientRequest) {
|
|
let _ = request.write_json_packet(&FrameUpdateReply {
|
|
from: self.name(),
|
|
type_: "frameUpdate".into(),
|
|
frames: vec![FrameUpdateMsg {
|
|
id: self.browsing_context_id.value(),
|
|
is_top_level: true,
|
|
title: self.title.borrow().clone(),
|
|
url: self.url.borrow().clone(),
|
|
}],
|
|
});
|
|
}
|
|
|
|
pub fn simulate_color_scheme(&self, theme: Theme) -> Result<(), ()> {
|
|
self.script_chan()
|
|
.send(SimulateColorScheme(self.pipeline_id(), theme))
|
|
.map_err(|_| ())
|
|
}
|
|
|
|
pub(crate) fn pipeline_id(&self) -> PipelineId {
|
|
*self.active_pipeline_id.borrow()
|
|
}
|
|
|
|
pub(crate) fn outer_window_id(&self) -> DevtoolsOuterWindowId {
|
|
*self.active_outer_window_id.borrow()
|
|
}
|
|
|
|
/// Returns the script sender for the active pipeline.
|
|
pub(crate) fn script_chan(&self) -> GenericSender<DevtoolScriptControlMsg> {
|
|
self.script_chans
|
|
.borrow()
|
|
.get(&self.pipeline_id())
|
|
.unwrap()
|
|
.clone()
|
|
}
|
|
|
|
pub(crate) fn instruct_script_to_send_live_updates(&self, should_send_updates: bool) {
|
|
let result = self
|
|
.script_chan()
|
|
.send(DevtoolScriptControlMsg::WantsLiveNotifications(
|
|
self.pipeline_id(),
|
|
should_send_updates,
|
|
));
|
|
|
|
// Notifying the script thread may fail with a "Disconnected" error if servo
|
|
// as a whole is being shut down.
|
|
debug_assert!(matches!(result, Ok(_) | Err(SendError::Disconnected)));
|
|
}
|
|
}
|
|
|
|
impl ActorEncode<BrowsingContextActorMsg> for BrowsingContextActor {
|
|
fn encode(&self, _: &ActorRegistry) -> BrowsingContextActorMsg {
|
|
BrowsingContextActorMsg {
|
|
actor: self.name(),
|
|
traits: BrowsingContextTraits {
|
|
is_browsing_context: true,
|
|
frames: true,
|
|
log_in_page: false,
|
|
navigation: true,
|
|
supports_top_level_target_flag: true,
|
|
watchpoints: true,
|
|
},
|
|
title: self.title.borrow().clone(),
|
|
url: self.url.borrow().clone(),
|
|
browser_id: self.browser_id.value(),
|
|
browsing_context_id: self.browsing_context_id.value(),
|
|
outer_window_id: self.outer_window_id().value(),
|
|
is_top_level_target: true,
|
|
accessibility_actor: self.accessibility.clone(),
|
|
console_actor: self.console.clone(),
|
|
css_properties_actor: self.css_properties.clone(),
|
|
inspector_actor: self.inspector.clone(),
|
|
reflow_actor: self.reflow.clone(),
|
|
style_sheets_actor: self.style_sheets.clone(),
|
|
thread_actor: self.thread.clone(),
|
|
target_type: TargetType::Frame,
|
|
}
|
|
}
|
|
}
|