Files
servo/components/devtools/actors/tab.rs
Brent Schroeter 890ef58052 devtools: Use active pipeline and script channel in inspector actors (#43153)
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 since 1aab10f2 and 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 for 4c69d85.

---------

Signed-off-by: Brent Schroeter <contact@brentsch.com>
2026-03-13 10:13:55 +00:00

216 lines
7.0 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/. */
//! Descriptor actor that represents a web view. It can link a tab to the corresponding watcher
//! actor to enable inspection.
//!
//! Liberally derived from the [Firefox JS implementation].
//!
//! [Firefox JS implementation]: https://searchfox.org/mozilla-central/source/devtools/server/actors/descriptors/tab.js
use devtools_traits::DevtoolScriptControlMsg;
use malloc_size_of_derive::MallocSizeOf;
use serde::Serialize;
use serde_json::{Map, Value};
use servo_url::ServoUrl;
use crate::actor::{Actor, ActorEncode, ActorError, ActorRegistry};
use crate::actors::browsing_context::{BrowsingContextActor, BrowsingContextActorMsg};
use crate::actors::root::{DescriptorTraits, RootActor};
use crate::actors::watcher::{WatcherActor, WatcherActorMsg};
use crate::protocol::ClientRequest;
use crate::{EmptyReplyMsg, StreamId};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct TabDescriptorActorMsg {
actor: String,
/// This correspond to webview_id
#[serde(rename = "browserId")]
browser_id: u32,
#[serde(rename = "browsingContextID")]
browsing_context_id: u32,
is_zombie_tab: bool,
#[serde(rename = "outerWindowID")]
outer_window_id: u32,
pub selected: bool,
title: String,
traits: DescriptorTraits,
url: String,
}
impl TabDescriptorActorMsg {
pub fn browser_id(&self) -> u32 {
self.browser_id
}
pub fn actor(&self) -> String {
self.actor.clone()
}
}
#[derive(Serialize)]
struct GetTargetReply {
from: String,
frame: BrowsingContextActorMsg,
}
#[derive(Serialize)]
struct GetFaviconReply {
from: String,
favicon: String,
}
#[derive(Serialize)]
struct GetWatcherReply {
from: String,
#[serde(flatten)]
watcher: WatcherActorMsg,
}
#[derive(MallocSizeOf)]
pub(crate) struct TabDescriptorActor {
name: String,
browsing_context_actor: String,
is_top_level_global: bool,
}
impl Actor for TabDescriptorActor {
fn name(&self) -> String {
self.name.clone()
}
/// The tab actor can handle the following messages:
///
/// - `getTarget`: Returns the surrounding `BrowsingContextActor`.
///
/// - `getFavicon`: Should return the tab favicon, but it is not yet supported.
///
/// - `getWatcher`: Returns a `WatcherActor` linked to the tab's `BrowsingContext`. It is used
/// to describe the debugging capabilities of this tab.
///
/// - `reloadDescriptor`: Causes the page to reload.
fn handle_message(
&self,
request: ClientRequest,
registry: &ActorRegistry,
msg_type: &str,
msg: &Map<String, Value>,
_id: StreamId,
) -> Result<(), ActorError> {
let ctx_actor = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
let pipeline = ctx_actor.pipeline_id();
match msg_type {
"getTarget" => request.reply_final(&GetTargetReply {
from: self.name(),
frame: registry.encode::<BrowsingContextActor, _>(&self.browsing_context_actor),
})?,
"getFavicon" => {
// TODO: Return a favicon when available
request.reply_final(&GetFaviconReply {
from: self.name(),
favicon: String::new(),
})?
},
"getWatcher" => request.reply_final(&GetWatcherReply {
from: self.name(),
watcher: registry.encode::<WatcherActor, _>(&ctx_actor.watcher),
})?,
"goBack" => {
ctx_actor
.script_chan()
.send(DevtoolScriptControlMsg::GoBack(pipeline))
.map_err(|_| ActorError::Internal)?;
request.reply_final(&EmptyReplyMsg { from: self.name() })?
},
"goForward" => {
ctx_actor
.script_chan()
.send(DevtoolScriptControlMsg::GoForward(pipeline))
.map_err(|_| ActorError::Internal)?;
request.reply_final(&EmptyReplyMsg { from: self.name() })?
},
"navigateTo" => {
if msg.get("waitForLoad").unwrap_or(&Value::Bool(false)) != &Value::Bool(false) {
log::warn!("waitForLoad option for devtools navigation is not supported.");
}
let url = msg
.get("url")
.and_then(|value| value.as_str())
.map(ServoUrl::parse)
.ok_or(ActorError::Internal)?
.map_err(|_| ActorError::Internal)?;
ctx_actor
.script_chan()
.send(DevtoolScriptControlMsg::NavigateTo(pipeline, url))
.map_err(|_| ActorError::Internal)?;
request.reply_final(&EmptyReplyMsg { from: self.name() })?
},
"reloadDescriptor" => {
// There is an extra bypassCache parameter that we don't currently use.
ctx_actor
.script_chan()
.send(DevtoolScriptControlMsg::Reload(pipeline))
.map_err(|_| ActorError::Internal)?;
request.reply_final(&EmptyReplyMsg { from: self.name() })?
},
_ => return Err(ActorError::UnrecognizedPacketType),
};
Ok(())
}
}
impl TabDescriptorActor {
pub(crate) fn new(
actors: &ActorRegistry,
browsing_context_actor: String,
is_top_level_global: bool,
) -> TabDescriptorActor {
let name = actors.new_name::<Self>();
let root = actors.find::<RootActor>("root");
root.tabs.borrow_mut().push(name.clone());
TabDescriptorActor {
name,
browsing_context_actor,
is_top_level_global,
}
}
pub(crate) fn is_top_level_global(&self) -> bool {
self.is_top_level_global
}
pub fn browsing_context(&self) -> String {
self.browsing_context_actor.clone()
}
}
impl ActorEncode<TabDescriptorActorMsg> for TabDescriptorActor {
fn encode(&self, registry: &ActorRegistry) -> TabDescriptorActorMsg {
let ctx_actor = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
let title = ctx_actor.title.borrow().clone();
let url = ctx_actor.url.borrow().clone();
TabDescriptorActorMsg {
actor: self.name(),
browser_id: ctx_actor.browser_id.value(),
browsing_context_id: ctx_actor.browsing_context_id.value(),
is_zombie_tab: false,
outer_window_id: ctx_actor.outer_window_id().value(),
selected: false,
title,
traits: DescriptorTraits {
watcher: true,
supports_navigation: true,
supports_reload_descriptor: true,
},
url,
}
}
}