mirror of
https://github.com/servo/servo.git
synced 2025-07-16 03:43:38 +01:00
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>
468 lines
17 KiB
Rust
468 lines
17 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/. */
|
|
|
|
//! The watcher is the main entry point when debugging an element. Right now only web views are supported.
|
|
//! It talks to the devtools remote and lists the capabilities of the inspected target, and it serves
|
|
//! as a bridge for messages between actors.
|
|
//!
|
|
//! Liberally derived from the [Firefox JS implementation].
|
|
//!
|
|
//! [Firefox JS implementation]: https://searchfox.org/mozilla-central/source/devtools/server/actors/descriptors/watcher.js
|
|
|
|
use std::collections::HashMap;
|
|
use std::net::TcpStream;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use base::id::BrowsingContextId;
|
|
use log::warn;
|
|
use serde::Serialize;
|
|
use serde_json::{Map, Value};
|
|
use servo_url::ServoUrl;
|
|
|
|
use self::network_parent::{NetworkParentActor, NetworkParentActorMsg};
|
|
use super::breakpoint::BreakpointListActor;
|
|
use super::thread::ThreadActor;
|
|
use super::worker::WorkerMsg;
|
|
use crate::actor::{Actor, ActorError, ActorRegistry};
|
|
use crate::actors::browsing_context::{BrowsingContextActor, BrowsingContextActorMsg};
|
|
use crate::actors::root::RootActor;
|
|
use crate::actors::watcher::target_configuration::{
|
|
TargetConfigurationActor, TargetConfigurationActorMsg,
|
|
};
|
|
use crate::actors::watcher::thread_configuration::{
|
|
ThreadConfigurationActor, ThreadConfigurationActorMsg,
|
|
};
|
|
use crate::protocol::{ClientRequest, JsonPacketStream};
|
|
use crate::resource::{ResourceArrayType, ResourceAvailable};
|
|
use crate::{EmptyReplyMsg, IdMap, StreamId, WorkerActor};
|
|
|
|
pub mod network_parent;
|
|
pub mod target_configuration;
|
|
pub mod thread_configuration;
|
|
|
|
/// Describes the debugged context. It informs the server of which objects can be debugged.
|
|
/// <https://searchfox.org/mozilla-central/source/devtools/server/actors/watcher/session-context.js>
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct SessionContext {
|
|
is_server_target_switching_enabled: bool,
|
|
supported_targets: HashMap<&'static str, bool>,
|
|
supported_resources: HashMap<&'static str, bool>,
|
|
context_type: SessionContextType,
|
|
}
|
|
|
|
impl SessionContext {
|
|
pub fn new(context_type: SessionContextType) -> Self {
|
|
Self {
|
|
is_server_target_switching_enabled: false,
|
|
// Right now we only support debugging web views (frames)
|
|
supported_targets: HashMap::from([
|
|
("frame", true),
|
|
("process", false),
|
|
("worker", true),
|
|
("service_worker", false),
|
|
("shared_worker", false),
|
|
]),
|
|
// At the moment we are blocking most resources to avoid errors
|
|
// Support for them will be enabled gradually once the corresponding actors start
|
|
// working propperly
|
|
supported_resources: HashMap::from([
|
|
("console-message", true),
|
|
("css-change", true),
|
|
("css-message", false),
|
|
("css-registered-properties", false),
|
|
("document-event", false),
|
|
("Cache", false),
|
|
("cookies", false),
|
|
("error-message", true),
|
|
("extension-storage", false),
|
|
("indexed-db", false),
|
|
("local-storage", false),
|
|
("session-storage", false),
|
|
("platform-message", false),
|
|
("network-event", true),
|
|
("network-event-stacktrace", false),
|
|
("reflow", false),
|
|
("stylesheet", false),
|
|
("source", true),
|
|
("thread-state", false),
|
|
("server-sent-event", false),
|
|
("websocket", false),
|
|
("jstracer-trace", false),
|
|
("jstracer-state", false),
|
|
("last-private-context-exit", false),
|
|
]),
|
|
context_type,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub enum SessionContextType {
|
|
BrowserElement,
|
|
_ContextProcess,
|
|
_WebExtension,
|
|
_Worker,
|
|
_All,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(untagged)]
|
|
enum TargetActorMsg {
|
|
BrowsingContext(BrowsingContextActorMsg),
|
|
Worker(WorkerMsg),
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct WatchTargetsReply {
|
|
from: String,
|
|
#[serde(rename = "type")]
|
|
type_: String,
|
|
target: TargetActorMsg,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct GetParentBrowsingContextIDReply {
|
|
from: String,
|
|
#[serde(rename = "browsingContextID")]
|
|
browsing_context_id: u32,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct GetNetworkParentActorReply {
|
|
from: String,
|
|
network: NetworkParentActorMsg,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct GetTargetConfigurationActorReply {
|
|
from: String,
|
|
configuration: TargetConfigurationActorMsg,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct GetThreadConfigurationActorReply {
|
|
from: String,
|
|
configuration: ThreadConfigurationActorMsg,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct GetBreakpointListActorReply {
|
|
from: String,
|
|
breakpoint_list: GetBreakpointListActorReplyInner,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct GetBreakpointListActorReplyInner {
|
|
actor: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct DocumentEvent {
|
|
#[serde(rename = "hasNativeConsoleAPI")]
|
|
has_native_console_api: Option<bool>,
|
|
name: String,
|
|
#[serde(rename = "newURI")]
|
|
new_uri: Option<String>,
|
|
time: u64,
|
|
title: Option<String>,
|
|
url: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct WatcherTraits {
|
|
resources: HashMap<&'static str, bool>,
|
|
#[serde(flatten)]
|
|
targets: HashMap<&'static str, bool>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct WatcherActorMsg {
|
|
actor: String,
|
|
traits: WatcherTraits,
|
|
}
|
|
|
|
pub struct WatcherActor {
|
|
name: String,
|
|
browsing_context_actor: String,
|
|
network_parent: String,
|
|
target_configuration: String,
|
|
thread_configuration: String,
|
|
session_context: SessionContext,
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WillNavigateMessage {
|
|
#[serde(rename = "browsingContextID")]
|
|
browsing_context_id: u32,
|
|
inner_window_id: u32,
|
|
name: String,
|
|
time: u128,
|
|
is_frame_switching: bool,
|
|
#[serde(rename = "newURI")]
|
|
new_uri: ServoUrl,
|
|
}
|
|
|
|
impl Actor for WatcherActor {
|
|
fn name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
|
|
/// The watcher actor can handle the following messages:
|
|
///
|
|
/// - `watchTargets`: Returns a list of objects to debug. Since we only support web views, it
|
|
/// returns the associated `BrowsingContextActor`. Every target sent creates a
|
|
/// `target-available-form` event.
|
|
///
|
|
/// - `watchResources`: Start watching certain resource types. This sends
|
|
/// `resources-available-array` events.
|
|
///
|
|
/// - `getNetworkParentActor`: Returns the network parent actor. It doesn't seem to do much at
|
|
/// the moment.
|
|
///
|
|
/// - `getTargetConfigurationActor`: Returns the configuration actor for a specific target, so
|
|
/// that the server can update its settings.
|
|
///
|
|
/// - `getThreadConfigurationActor`: The same but with the configuration actor for the thread
|
|
fn handle_message(
|
|
&self,
|
|
mut request: ClientRequest,
|
|
registry: &ActorRegistry,
|
|
msg_type: &str,
|
|
msg: &Map<String, Value>,
|
|
_id: StreamId,
|
|
) -> Result<(), ActorError> {
|
|
let target = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
|
|
let root = registry.find::<RootActor>("root");
|
|
match msg_type {
|
|
"watchTargets" => {
|
|
// As per logs we either get targetType as "frame" or "worker"
|
|
let target_type = msg
|
|
.get("targetType")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or("frame"); // default to "frame"
|
|
|
|
if target_type == "frame" {
|
|
let msg = WatchTargetsReply {
|
|
from: self.name(),
|
|
type_: "target-available-form".into(),
|
|
target: TargetActorMsg::BrowsingContext(target.encodable()),
|
|
};
|
|
let _ = request.write_json_packet(&msg);
|
|
|
|
target.frame_update(&mut request);
|
|
} else if target_type == "worker" {
|
|
for worker_name in &root.workers {
|
|
let worker = registry.find::<WorkerActor>(worker_name);
|
|
let worker_msg = WatchTargetsReply {
|
|
from: self.name(),
|
|
type_: "target-available-form".into(),
|
|
target: TargetActorMsg::Worker(worker.encodable()),
|
|
};
|
|
let _ = request.write_json_packet(&worker_msg);
|
|
}
|
|
} else {
|
|
warn!("Unexpected target_type: {}", target_type);
|
|
}
|
|
|
|
// Messages that contain a `type` field are used to send event callbacks, but they
|
|
// don't count as a reply. Since every message needs to be responded, we send an
|
|
// extra empty packet to the devtools host to inform that we successfully received
|
|
// and processed the message so that it can continue
|
|
let msg = EmptyReplyMsg { from: self.name() };
|
|
request.reply_final(&msg)?
|
|
},
|
|
"watchResources" => {
|
|
let Some(resource_types) = msg.get("resourceTypes") else {
|
|
return Err(ActorError::MissingParameter);
|
|
};
|
|
let Some(resource_types) = resource_types.as_array() else {
|
|
return Err(ActorError::BadParameterType);
|
|
};
|
|
|
|
for resource in resource_types {
|
|
let Some(resource) = resource.as_str() else {
|
|
continue;
|
|
};
|
|
match resource {
|
|
"document-event" => {
|
|
// TODO: This is a hacky way of sending the 3 messages
|
|
// Figure out if there needs work to be done here, ensure the page is loaded
|
|
for &name in ["dom-loading", "dom-interactive", "dom-complete"].iter() {
|
|
let event = DocumentEvent {
|
|
has_native_console_api: Some(true),
|
|
name: name.into(),
|
|
new_uri: None,
|
|
time: SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_millis()
|
|
as u64,
|
|
title: Some(target.title.borrow().clone()),
|
|
url: Some(target.url.borrow().clone()),
|
|
};
|
|
target.resource_array(
|
|
event,
|
|
"document-event".into(),
|
|
ResourceArrayType::Available,
|
|
&mut request,
|
|
);
|
|
}
|
|
},
|
|
"source" => {
|
|
let thread_actor = registry.find::<ThreadActor>(&target.thread);
|
|
target.resources_array(
|
|
thread_actor.source_manager.source_forms(registry),
|
|
"source".into(),
|
|
ResourceArrayType::Available,
|
|
&mut request,
|
|
);
|
|
|
|
for worker_name in &root.workers {
|
|
let worker = registry.find::<WorkerActor>(worker_name);
|
|
let thread = registry.find::<ThreadActor>(&worker.thread);
|
|
|
|
worker.resources_array(
|
|
thread.source_manager.source_forms(registry),
|
|
"source".into(),
|
|
ResourceArrayType::Available,
|
|
&mut request,
|
|
);
|
|
}
|
|
},
|
|
"console-message" | "error-message" => {},
|
|
"network-event" => {},
|
|
_ => warn!("resource {} not handled yet", resource),
|
|
}
|
|
}
|
|
let msg = EmptyReplyMsg { from: self.name() };
|
|
request.reply_final(&msg)?
|
|
},
|
|
"getParentBrowsingContextID" => {
|
|
let msg = GetParentBrowsingContextIDReply {
|
|
from: self.name(),
|
|
browsing_context_id: target.browsing_context_id.value(),
|
|
};
|
|
request.reply_final(&msg)?
|
|
},
|
|
"getNetworkParentActor" => {
|
|
let network_parent = registry.find::<NetworkParentActor>(&self.network_parent);
|
|
let msg = GetNetworkParentActorReply {
|
|
from: self.name(),
|
|
network: network_parent.encodable(),
|
|
};
|
|
request.reply_final(&msg)?
|
|
},
|
|
"getTargetConfigurationActor" => {
|
|
let target_configuration =
|
|
registry.find::<TargetConfigurationActor>(&self.target_configuration);
|
|
let msg = GetTargetConfigurationActorReply {
|
|
from: self.name(),
|
|
configuration: target_configuration.encodable(),
|
|
};
|
|
request.reply_final(&msg)?
|
|
},
|
|
"getThreadConfigurationActor" => {
|
|
let thread_configuration =
|
|
registry.find::<ThreadConfigurationActor>(&self.thread_configuration);
|
|
let msg = GetThreadConfigurationActorReply {
|
|
from: self.name(),
|
|
configuration: thread_configuration.encodable(),
|
|
};
|
|
request.reply_final(&msg)?
|
|
},
|
|
"getBreakpointListActor" => {
|
|
let breakpoint_list_name = registry.new_name("breakpoint-list");
|
|
let breakpoint_list = BreakpointListActor::new(breakpoint_list_name.clone());
|
|
registry.register_later(Box::new(breakpoint_list));
|
|
|
|
request.reply_final(&GetBreakpointListActorReply {
|
|
from: self.name(),
|
|
breakpoint_list: GetBreakpointListActorReplyInner {
|
|
actor: breakpoint_list_name,
|
|
},
|
|
})?
|
|
},
|
|
_ => return Err(ActorError::UnrecognizedPacketType),
|
|
};
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl ResourceAvailable for WatcherActor {
|
|
fn actor_name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
}
|
|
|
|
impl WatcherActor {
|
|
pub fn new(
|
|
actors: &mut ActorRegistry,
|
|
browsing_context_actor: String,
|
|
session_context: SessionContext,
|
|
) -> Self {
|
|
let network_parent = NetworkParentActor::new(actors.new_name("network-parent"));
|
|
let target_configuration =
|
|
TargetConfigurationActor::new(actors.new_name("target-configuration"));
|
|
let thread_configuration =
|
|
ThreadConfigurationActor::new(actors.new_name("thread-configuration"));
|
|
|
|
let watcher = Self {
|
|
name: actors.new_name("watcher"),
|
|
browsing_context_actor,
|
|
network_parent: network_parent.name(),
|
|
target_configuration: target_configuration.name(),
|
|
thread_configuration: thread_configuration.name(),
|
|
session_context,
|
|
};
|
|
|
|
actors.register(Box::new(network_parent));
|
|
actors.register(Box::new(target_configuration));
|
|
actors.register(Box::new(thread_configuration));
|
|
|
|
watcher
|
|
}
|
|
|
|
pub fn encodable(&self) -> WatcherActorMsg {
|
|
WatcherActorMsg {
|
|
actor: self.name(),
|
|
traits: WatcherTraits {
|
|
resources: self.session_context.supported_resources.clone(),
|
|
targets: self.session_context.supported_targets.clone(),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn emit_will_navigate(
|
|
&self,
|
|
browsing_context_id: BrowsingContextId,
|
|
url: ServoUrl,
|
|
connections: &mut Vec<TcpStream>,
|
|
id_map: &mut IdMap,
|
|
) {
|
|
let msg = WillNavigateMessage {
|
|
browsing_context_id: id_map.browsing_context_id(browsing_context_id).value(),
|
|
inner_window_id: 0, // TODO: set this to the correct value
|
|
name: "will-navigate".to_string(),
|
|
time: SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_millis(),
|
|
is_frame_switching: false, // TODO: Implement frame switching
|
|
new_uri: url,
|
|
};
|
|
|
|
for stream in connections {
|
|
self.resource_array(
|
|
msg.clone(),
|
|
"document-event".to_string(),
|
|
ResourceArrayType::Available,
|
|
stream,
|
|
);
|
|
}
|
|
}
|
|
}
|