Files
servo/components/devtools/actors/browsing_context.rs
Simon Wülker 53ffdda2a1 devtools: Properly handle opening and closing client connections (#42583)
Both the `DevtoolsInstance` and the `BrowsingContextActor` maintain a
list of live connections. When a new global is created, its
`BrowsingContextActor` copies the list of active connections from the
`DevtoolsInstance`. When a client handler thread exits, the
`BrowsingContextActor`s remove the connection from their list of
connections.

This has two implications:
* `BrowsingContextActor`s don't know about connections that are created
after they were constructed, causing them to possibly incorrectly tell
script that it doesn't need to send devtools updates anymore
* the `DevtoolsInstance` never notices when connections are closed and
panics when writing to them.

To fix this, I've removed the list of connections from the
`BrowsingContextActor` and adjusted the logic to notify script when
updates aren't needed anymore accordingly.

For some reason, our tests always open two connections and close the
first one immediately, which is how I found this bug in the first place.

Part of https://github.com/servo/servo/issues/42454 - previously
d5d400c7d6/components/script/dom/globalscope.rs (L240-L242)
was always false during our tests.

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
2026-02-13 14:39:47 +00:00

384 lines
13 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 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,
inspector: String,
reflow: String,
style_sheets: String,
pub thread: String,
_tab: String,
pub script_chan: 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, pipeline_id, script_sender.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());
let watcher = WatcherActor::new(
actors,
name.clone(),
SessionContext::new(SessionContextType::BrowserElement),
);
let target = BrowsingContextActor {
name,
script_chan: script_sender,
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 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()
}
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,
}
}
}