mirror of
https://github.com/servo/servo.git
synced 2025-07-22 23:03:42 +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>
351 lines
11 KiB
Rust
351 lines
11 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 walker actor is responsible for traversing the DOM tree in various ways to create new nodes
|
|
|
|
use std::cell::RefCell;
|
|
|
|
use base::id::PipelineId;
|
|
use devtools_traits::DevtoolScriptControlMsg::{GetChildren, GetDocumentElement};
|
|
use devtools_traits::{AttrModification, DevtoolScriptControlMsg};
|
|
use ipc_channel::ipc::{self, IpcSender};
|
|
use serde::Serialize;
|
|
use serde_json::{self, Map, Value};
|
|
|
|
use crate::actor::{Actor, ActorError, ActorRegistry};
|
|
use crate::actors::inspector::layout::{LayoutInspectorActor, LayoutInspectorActorMsg};
|
|
use crate::actors::inspector::node::{NodeActorMsg, NodeInfoToProtocol};
|
|
use crate::protocol::{ClientRequest, JsonPacketStream};
|
|
use crate::{EmptyReplyMsg, StreamId};
|
|
|
|
#[derive(Serialize)]
|
|
pub struct WalkerMsg {
|
|
pub actor: String,
|
|
pub root: NodeActorMsg,
|
|
}
|
|
|
|
pub struct WalkerActor {
|
|
pub name: String,
|
|
pub script_chan: IpcSender<DevtoolScriptControlMsg>,
|
|
pub pipeline: PipelineId,
|
|
pub root_node: NodeActorMsg,
|
|
pub mutations: RefCell<Vec<(AttrModification, String)>>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct QuerySelectorReply {
|
|
from: String,
|
|
node: NodeActorMsg,
|
|
new_parents: Vec<NodeActorMsg>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct DocumentElementReply {
|
|
from: String,
|
|
node: NodeActorMsg,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct ChildrenReply {
|
|
has_first: bool,
|
|
has_last: bool,
|
|
nodes: Vec<NodeActorMsg>,
|
|
from: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct GetLayoutInspectorReply {
|
|
actor: LayoutInspectorActorMsg,
|
|
from: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct WatchRootNodeNotification {
|
|
#[serde(rename = "type")]
|
|
type_: String,
|
|
from: String,
|
|
node: NodeActorMsg,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct MutationMsg {
|
|
attribute_name: String,
|
|
new_value: Option<String>,
|
|
target: String,
|
|
#[serde(rename = "type")]
|
|
type_: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct GetMutationsReply {
|
|
from: String,
|
|
mutations: Vec<MutationMsg>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct GetOffsetParentReply {
|
|
from: String,
|
|
node: Option<()>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct NewMutationsNotification {
|
|
from: String,
|
|
#[serde(rename = "type")]
|
|
type_: String,
|
|
}
|
|
|
|
impl Actor for WalkerActor {
|
|
fn name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
|
|
/// The walker actor can handle the following messages:
|
|
///
|
|
/// - `children`: Returns a list of children nodes of the specified node
|
|
///
|
|
/// - `clearPseudoClassLocks`: Placeholder
|
|
///
|
|
/// - `documentElement`: Returns the base document element node
|
|
///
|
|
/// - `getLayoutInspector`: Returns the Layout inspector actor, placeholder
|
|
///
|
|
/// - `getMutations`: Returns the list of attribute changes since it was last called
|
|
///
|
|
/// - `getOffsetParent`: Placeholder
|
|
///
|
|
/// - `querySelector`: Recursively looks for the specified selector in the tree, reutrning the
|
|
/// node and its ascendents
|
|
fn handle_message(
|
|
&self,
|
|
mut request: ClientRequest,
|
|
registry: &ActorRegistry,
|
|
msg_type: &str,
|
|
msg: &Map<String, Value>,
|
|
_id: StreamId,
|
|
) -> Result<(), ActorError> {
|
|
match msg_type {
|
|
"children" => {
|
|
let target = msg
|
|
.get("node")
|
|
.ok_or(ActorError::MissingParameter)?
|
|
.as_str()
|
|
.ok_or(ActorError::BadParameterType)?;
|
|
let (tx, rx) = ipc::channel().map_err(|_| ActorError::Internal)?;
|
|
self.script_chan
|
|
.send(GetChildren(
|
|
self.pipeline,
|
|
registry.actor_to_script(target.into()),
|
|
tx,
|
|
))
|
|
.map_err(|_| ActorError::Internal)?;
|
|
let children = rx
|
|
.recv()
|
|
.map_err(|_| ActorError::Internal)?
|
|
.ok_or(ActorError::Internal)?;
|
|
|
|
let msg = ChildrenReply {
|
|
has_first: true,
|
|
has_last: true,
|
|
nodes: children
|
|
.into_iter()
|
|
.map(|child| {
|
|
child.encode(
|
|
registry,
|
|
true,
|
|
self.script_chan.clone(),
|
|
self.pipeline,
|
|
self.name(),
|
|
)
|
|
})
|
|
.collect(),
|
|
from: self.name(),
|
|
};
|
|
request.reply_final(&msg)?
|
|
},
|
|
"clearPseudoClassLocks" => {
|
|
let msg = EmptyReplyMsg { from: self.name() };
|
|
request.reply_final(&msg)?
|
|
},
|
|
"documentElement" => {
|
|
let (tx, rx) = ipc::channel().map_err(|_| ActorError::Internal)?;
|
|
self.script_chan
|
|
.send(GetDocumentElement(self.pipeline, tx))
|
|
.map_err(|_| ActorError::Internal)?;
|
|
let doc_elem_info = rx
|
|
.recv()
|
|
.map_err(|_| ActorError::Internal)?
|
|
.ok_or(ActorError::Internal)?;
|
|
let node = doc_elem_info.encode(
|
|
registry,
|
|
true,
|
|
self.script_chan.clone(),
|
|
self.pipeline,
|
|
self.name(),
|
|
);
|
|
|
|
let msg = DocumentElementReply {
|
|
from: self.name(),
|
|
node,
|
|
};
|
|
request.reply_final(&msg)?
|
|
},
|
|
"getLayoutInspector" => {
|
|
// TODO: Create actual layout inspector actor
|
|
let layout = LayoutInspectorActor::new(registry.new_name("layout"));
|
|
let actor = layout.encodable();
|
|
registry.register_later(Box::new(layout));
|
|
|
|
let msg = GetLayoutInspectorReply {
|
|
from: self.name(),
|
|
actor,
|
|
};
|
|
request.reply_final(&msg)?
|
|
},
|
|
"getMutations" => {
|
|
let msg = GetMutationsReply {
|
|
from: self.name(),
|
|
mutations: self
|
|
.mutations
|
|
.borrow_mut()
|
|
.drain(..)
|
|
.map(|(mutation, target)| MutationMsg {
|
|
attribute_name: mutation.attribute_name,
|
|
new_value: mutation.new_value,
|
|
target,
|
|
type_: "attributes".into(),
|
|
})
|
|
.collect(),
|
|
};
|
|
request.reply_final(&msg)?
|
|
},
|
|
"getOffsetParent" => {
|
|
let msg = GetOffsetParentReply {
|
|
from: self.name(),
|
|
node: None,
|
|
};
|
|
request.reply_final(&msg)?
|
|
},
|
|
"querySelector" => {
|
|
let selector = msg
|
|
.get("selector")
|
|
.ok_or(ActorError::MissingParameter)?
|
|
.as_str()
|
|
.ok_or(ActorError::BadParameterType)?;
|
|
let node = msg
|
|
.get("node")
|
|
.ok_or(ActorError::MissingParameter)?
|
|
.as_str()
|
|
.ok_or(ActorError::BadParameterType)?;
|
|
let mut hierarchy = find_child(
|
|
&self.script_chan,
|
|
self.pipeline,
|
|
&self.name,
|
|
registry,
|
|
node,
|
|
vec![],
|
|
|msg| msg.display_name == selector,
|
|
)
|
|
.map_err(|_| ActorError::Internal)?;
|
|
hierarchy.reverse();
|
|
let node = hierarchy.pop().ok_or(ActorError::Internal)?;
|
|
|
|
let msg = QuerySelectorReply {
|
|
from: self.name(),
|
|
node,
|
|
new_parents: hierarchy,
|
|
};
|
|
request.reply_final(&msg)?
|
|
},
|
|
"watchRootNode" => {
|
|
let msg = WatchRootNodeNotification {
|
|
type_: "root-available".into(),
|
|
from: self.name(),
|
|
node: self.root_node.clone(),
|
|
};
|
|
let _ = request.write_json_packet(&msg);
|
|
|
|
let msg = EmptyReplyMsg { from: self.name() };
|
|
request.reply_final(&msg)?
|
|
},
|
|
_ => return Err(ActorError::UnrecognizedPacketType),
|
|
};
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl WalkerActor {
|
|
pub(crate) fn new_mutations(
|
|
&self,
|
|
request: &mut ClientRequest,
|
|
target: &str,
|
|
modifications: &[AttrModification],
|
|
) {
|
|
{
|
|
let mut mutations = self.mutations.borrow_mut();
|
|
mutations.extend(modifications.iter().cloned().map(|m| (m, target.into())));
|
|
}
|
|
let _ = request.write_json_packet(&NewMutationsNotification {
|
|
from: self.name(),
|
|
type_: "newMutations".into(),
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Recursively searches for a child with the specified selector
|
|
/// If it is found, returns a list with the child and all of its ancestors.
|
|
/// TODO: Investigate how to cache this to some extent.
|
|
pub fn find_child(
|
|
script_chan: &IpcSender<DevtoolScriptControlMsg>,
|
|
pipeline: PipelineId,
|
|
name: &str,
|
|
registry: &ActorRegistry,
|
|
node: &str,
|
|
mut hierarchy: Vec<NodeActorMsg>,
|
|
compare_fn: impl Fn(&NodeActorMsg) -> bool + Clone,
|
|
) -> Result<Vec<NodeActorMsg>, Vec<NodeActorMsg>> {
|
|
let (tx, rx) = ipc::channel().unwrap();
|
|
script_chan
|
|
.send(GetChildren(
|
|
pipeline,
|
|
registry.actor_to_script(node.into()),
|
|
tx,
|
|
))
|
|
.unwrap();
|
|
let children = rx.recv().unwrap().ok_or(vec![])?;
|
|
|
|
for child in children {
|
|
let msg = child.encode(registry, true, script_chan.clone(), pipeline, name.into());
|
|
if compare_fn(&msg) {
|
|
hierarchy.push(msg);
|
|
return Ok(hierarchy);
|
|
};
|
|
|
|
if msg.num_children == 0 {
|
|
continue;
|
|
}
|
|
|
|
match find_child(
|
|
script_chan,
|
|
pipeline,
|
|
name,
|
|
registry,
|
|
&msg.actor,
|
|
hierarchy,
|
|
compare_fn.clone(),
|
|
) {
|
|
Ok(mut hierarchy) => {
|
|
hierarchy.push(msg);
|
|
return Ok(hierarchy);
|
|
},
|
|
Err(e) => {
|
|
hierarchy = e;
|
|
},
|
|
}
|
|
}
|
|
Err(hierarchy)
|
|
}
|