servo/components/devtools/actors/tab.rs
atbrakhi 71d97bd935
Devtools: send error replies instead of ignoring messages (#37686)
Client messages, which are always requests, are dispatched to Actor
instances one at a time via Actor::handle_message. Each request must be
paired with exactly one reply from the same actor the request was sent
to, where a reply is a message with no type (if a message from the
server has a type, it’s a notification, not a reply).

Failing to reply to a request will almost always permanently break that
actor, because either the client gets stuck waiting for a reply, or the
client receives the reply for a subsequent request as if it was the
reply for the current request. If an actor fails to reply to a request,
we want the dispatcher (ActorRegistry::handle_message) to send an error
of type `unrecognizedPacketType`, to keep the conversation for that
actor in sync. Since replies come in all shapes and sizes, we want to
allow Actor types to send replies without having to return them to the
dispatcher.

This patch adds a wrapper type around a client stream that guarantees
request/reply invariants. It allows the dispatcher to check if a valid
reply was sent, and guarantees that if the actor tries to send a reply,
it’s actually a valid reply (see ClientRequest::is_valid_reply). It does
not currently guarantee anything about messages sent via the TcpStream
released via ClientRequest::try_clone_stream or the return value of
ClientRequest::reply. We also send `unrecognizedPacketType`,
`missingParameter`, `badParameterType`, and `noSuchActor` messages per
the
[protocol](https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#error-packets)
[docs](https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#packets).

Testing: automated tests all pass, and manual testing looks ok
Fixes: #37683 and at least six bugs, plus one with a different root
cause, plus three with zero impact

---------

Signed-off-by: atbrakhi <atbrakhi@igalia.com>
Signed-off-by: Delan Azabani <dazabani@igalia.com>
Co-authored-by: delan azabani <dazabani@igalia.com>
Co-authored-by: Simon Wülker <simon.wuelker@arcor.de>
Co-authored-by: the6p4c <me@doggirl.gay>
2025-07-07 12:40:44 +00:00

172 lines
5.1 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 serde::Serialize;
use serde_json::{Map, Value};
use crate::StreamId;
use crate::actor::{Actor, 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;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub 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,
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,
}
pub 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.
fn handle_message(
&self,
request: ClientRequest,
registry: &ActorRegistry,
msg_type: &str,
_msg: &Map<String, Value>,
_id: StreamId,
) -> Result<(), ActorError> {
match msg_type {
"getTarget" => {
let frame = registry
.find::<BrowsingContextActor>(&self.browsing_context_actor)
.encodable();
request.reply_final(&GetTargetReply {
from: self.name(),
frame,
})?
},
"getFavicon" => {
// TODO: Return a favicon when available
request.reply_final(&GetFaviconReply {
from: self.name(),
favicon: String::new(),
})?
},
"getWatcher" => {
let ctx_actor = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
let watcher = registry.find::<WatcherActor>(&ctx_actor.watcher);
request.reply_final(&GetWatcherReply {
from: self.name(),
watcher: watcher.encodable(),
})?
},
_ => return Err(ActorError::UnrecognizedPacketType),
};
Ok(())
}
}
impl TabDescriptorActor {
pub(crate) fn new(
actors: &mut ActorRegistry,
browsing_context_actor: String,
is_top_level_global: bool,
) -> TabDescriptorActor {
let name = actors.new_name("tab-description");
let root = actors.find_mut::<RootActor>("root");
root.tabs.push(name.clone());
TabDescriptorActor {
name,
browsing_context_actor,
is_top_level_global,
}
}
pub fn encodable(&self, registry: &ActorRegistry, selected: bool) -> 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.active_outer_window_id.get().value(),
selected,
title,
traits: DescriptorTraits {
watcher: true,
supports_reload_descriptor: true,
},
url,
}
}
pub(crate) fn is_top_level_global(&self) -> bool {
self.is_top_level_global
}
#[allow(dead_code)]
pub fn browsing_context(&self) -> String {
self.browsing_context_actor.clone()
}
}