Skip to main content

devtools/
actor.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::any::{Any, type_name};
6use std::borrow::Borrow;
7use std::collections::{HashMap, HashSet};
8use std::hash::{Hash, Hasher};
9use std::marker::PhantomData;
10use std::ops::{Deref, DerefMut};
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU32, Ordering};
13
14use log::{debug, warn};
15use malloc_size_of::MallocSizeOf;
16use malloc_size_of_derive::MallocSizeOf;
17use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
18use serde::Serialize;
19use serde_json::{Map, Value, json};
20use servo_base::id::PipelineId;
21
22use crate::StreamId;
23use crate::protocol::{ClientRequest, DevtoolsConnection, JsonPacketStream};
24
25/// Error replies.
26///
27/// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#error-packets>
28#[derive(Debug)]
29pub enum ActorError {
30    MissingParameter,
31    BadParameterType,
32    UnrecognizedPacketType,
33    /// Custom errors, not defined in the protocol docs.
34    /// This includes send errors, and errors that prevent Servo from sending a reply.
35    Internal,
36}
37
38impl ActorError {
39    pub fn name(&self) -> &'static str {
40        match self {
41            ActorError::MissingParameter => "missingParameter",
42            ActorError::BadParameterType => "badParameterType",
43            ActorError::UnrecognizedPacketType => "unrecognizedPacketType",
44            // The devtools frontend always checks for specific protocol errors by catching a JS exception `e` whose
45            // message contains the error name, and checking `e.message.includes("someErrorName")`. As a result, the
46            // only error name we can safely use for custom errors is the empty string, because any other error name we
47            // use may be a substring of some upstream error name.
48            ActorError::Internal => "",
49        }
50    }
51}
52
53/// Create a name prefix for each actor type, without any counter suffix.
54pub(crate) fn base_name<T: ?Sized>() -> &'static str {
55    let prefix = type_name::<T>();
56    prefix.split("::").last().unwrap_or(prefix)
57}
58
59/// Create a unique actor name based on the type and a monotonically increasing suffix.
60pub(crate) fn new_actor_name<T: ?Sized>() -> String {
61    static COUNTER: AtomicU32 = AtomicU32::new(0);
62    let suffix = COUNTER.fetch_add(1, Ordering::Relaxed);
63    let base = base_name::<T>();
64
65    // Firefox DevTools client requires "/workerTarget" in actor name to recognize workers
66    // <https://searchfox.org/firefox-main/source/devtools/client/fronts/watcher.js#65>
67    if base.contains("WorkerTarget") {
68        format!("/workerTarget{suffix}")
69    } else {
70        format!("{base}{suffix}")
71    }
72}
73
74/// A common trait for all devtools actors that encompasses an immutable name
75/// and the ability to process messages that are directed to particular actors.
76pub(crate) trait Actor: Any + ActorAsAny + Send + Sync + MallocSizeOf {
77    fn handle_message(
78        &self,
79        request: ClientRequest,
80        registry: &ActorRegistry,
81        msg_type: &str,
82        msg: &Map<String, Value>,
83        stream_id: StreamId,
84    ) -> Result<(), ActorError> {
85        let _ = (request, registry, msg_type, msg, stream_id);
86        Err(ActorError::UnrecognizedPacketType)
87    }
88    fn name(&self) -> &str;
89    fn cleanup(&self, _id: StreamId) {}
90}
91
92pub(crate) trait ActorAsAny {
93    fn actor_as_any(&self) -> &dyn Any;
94}
95
96impl<T: Actor> ActorAsAny for T {
97    fn actor_as_any(&self) -> &dyn Any {
98        self
99    }
100}
101
102pub(crate) trait ActorEncode<T: Serialize>: Actor {
103    fn encode(&self, registry: &ActorRegistry) -> T;
104}
105
106/// Return value of `ActorRegistry::find` that allows seamless downcasting
107/// from `dyn Actor` to the concrete actor type.
108pub(crate) struct DowncastableActorArc<T> {
109    actor: Arc<dyn Actor>,
110    _phantom: PhantomData<T>,
111}
112
113impl<T: Actor> From<Arc<T>> for DowncastableActorArc<T> {
114    fn from(actor: Arc<T>) -> Self {
115        Self {
116            actor,
117            _phantom: PhantomData,
118        }
119    }
120}
121
122impl<T: 'static> std::ops::Deref for DowncastableActorArc<T> {
123    type Target = T;
124    fn deref(&self) -> &Self::Target {
125        self.actor
126            .actor_as_any()
127            .downcast_ref::<T>()
128            .unwrap_or_else(|| {
129                panic!(
130                    "Failed to downcast {} to type {}",
131                    self.actor.name(),
132                    type_name::<T>()
133                )
134            })
135    }
136}
137
138#[derive(Clone)]
139struct RegisteredActor(Arc<dyn Actor>);
140
141impl PartialEq for RegisteredActor {
142    fn eq(&self, other: &Self) -> bool {
143        self.0.name() == other.0.name()
144    }
145}
146
147impl Eq for RegisteredActor {}
148
149impl Hash for RegisteredActor {
150    fn hash<H: Hasher>(&self, state: &mut H) {
151        self.0.name().hash(state);
152    }
153}
154
155impl Borrow<str> for RegisteredActor {
156    fn borrow(&self) -> &str {
157        self.0.name()
158    }
159}
160
161impl MallocSizeOf for RegisteredActor {
162    fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
163        self.0.size_of(ops)
164    }
165}
166
167// Debug-only guard to prevent deadlocks. Panics if `ActorRegistry::write` is called
168// reentrantly on the same thread before the previous write guard is dropped.
169#[cfg(debug_assertions)]
170thread_local! {
171    static REENTRANCY_GUARD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
172}
173
174struct WriteGuard<'a>(RwLockWriteGuard<'a, ActorRegistryInner>);
175
176impl<'a> Deref for WriteGuard<'a> {
177    type Target = ActorRegistryInner;
178    fn deref(&self) -> &Self::Target {
179        &self.0
180    }
181}
182
183impl<'a> DerefMut for WriteGuard<'a> {
184    fn deref_mut(&mut self) -> &mut Self::Target {
185        &mut self.0
186    }
187}
188
189impl<'a> Drop for WriteGuard<'a> {
190    fn drop(&mut self) {
191        #[cfg(debug_assertions)]
192        REENTRANCY_GUARD.with(|cell| cell.set(false));
193    }
194}
195
196#[derive(Default, MallocSizeOf)]
197struct ActorRegistryInner {
198    actors: HashSet<RegisteredActor>,
199    script_to_actor: HashMap<String, String>,
200    actor_to_script: HashMap<String, String>,
201    source_actor_names: HashMap<PipelineId, Vec<String>>,
202    inline_source_content: HashMap<PipelineId, String>,
203}
204
205#[derive(Default, MallocSizeOf)]
206pub(crate) struct ActorRegistry(RwLock<ActorRegistryInner>);
207
208impl ActorRegistry {
209    fn read(&self) -> RwLockReadGuard<'_, ActorRegistryInner> {
210        self.0.read()
211    }
212
213    fn write(&self) -> WriteGuard<'_> {
214        #[cfg(debug_assertions)]
215        REENTRANCY_GUARD.with(|cell| {
216            assert!(
217                !cell.get(),
218                "Reentrant write operation detected on the same thread for ActorRegistry"
219            );
220            cell.set(true);
221        });
222        WriteGuard(self.0.write())
223    }
224}
225
226impl ActorRegistry {
227    /// Add an actor to the registry of known actors that can receive messages.
228    pub(crate) fn register<T: Actor>(&self, actor: T) -> Arc<T> {
229        let actor = Arc::new(actor);
230        self.write().actors.insert(RegisteredActor(actor.clone()));
231        actor
232    }
233
234    /// Find an actor by registered name
235    pub(crate) fn find<T: Actor>(&self, name: &str) -> DowncastableActorArc<T> {
236        let actor = self
237            .read()
238            .actors
239            .get(name)
240            .expect("Should never look for a nonexistent actor")
241            .0
242            .clone();
243        DowncastableActorArc {
244            actor,
245            _phantom: PhantomData,
246        }
247    }
248
249    /// Find an actor by registered name and return its serialization
250    pub(crate) fn encode<T: ActorEncode<S>, S: Serialize>(&self, name: &str) -> S {
251        self.find::<T>(name).encode(self)
252    }
253
254    /// Attempt to process a message as directed by its `to` property. If the actor is not found, does not support the
255    /// message, or failed to handle the message, send an error reply instead.
256    pub(crate) fn handle_message(
257        &self,
258        msg: &Map<String, Value>,
259        stream: &mut DevtoolsConnection,
260        stream_id: StreamId,
261    ) -> Result<(), ()> {
262        let Some(actor_name) = msg.get("to").and_then(|value| value.as_str()) else {
263            warn!("Received unexpected message: {msg:?}");
264            return Err(());
265        };
266
267        let actor = self
268            .read()
269            .actors
270            .get(actor_name)
271            .map(|registered_actor| registered_actor.0.clone());
272        match actor {
273            None => {
274                // <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#packets>
275                let _ = stream
276                    .write_json_packet(&json!({ "from": actor_name, "error": "noSuchActor" }));
277            },
278            Some(actor) => {
279                let Some(msg_type) = msg.get("type").and_then(|value| value.as_str()) else {
280                    let _ = stream.write_json_packet(
281                        &json!({ "from": actor_name, "error": "missingParameter" }),
282                    );
283                    return Ok(());
284                };
285                if let Err(error) = ClientRequest::handle(stream.clone(), actor_name, |req| {
286                    actor.handle_message(req, self, msg_type, msg, stream_id)
287                }) {
288                    // <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#error-packets>
289                    let error = json!({
290                        "from": actor.name(), "error": error.name()
291                    });
292                    warn!("Sending devtools protocol error: error={error:?} request={msg:?}");
293                    let _ = stream.write_json_packet(&error);
294                }
295            },
296        }
297        Ok(())
298    }
299
300    pub(crate) fn register_script_actor(&self, script_id: String, actor: String) {
301        debug!("Registering {actor} ({script_id})");
302        let mut lock = self.write();
303        lock.script_to_actor
304            .insert(script_id.clone(), actor.clone());
305        lock.actor_to_script.insert(actor, script_id);
306    }
307
308    pub(crate) fn script_to_actor(&self, script_id: &str) -> String {
309        if script_id.is_empty() {
310            return String::new();
311        }
312        self.read()
313            .script_to_actor
314            .get(script_id)
315            .unwrap_or_else(|| panic!("No actor for script id {script_id}"))
316            .clone()
317    }
318
319    pub(crate) fn script_actor_registered(&self, script_id: &str) -> bool {
320        self.read().script_to_actor.contains_key(script_id)
321    }
322
323    pub(crate) fn actor_to_script(&self, actor: String) -> String {
324        self.read()
325            .actor_to_script
326            .get(&actor)
327            .unwrap_or_else(|| panic!("No script id for actor {actor}"))
328            .clone()
329    }
330
331    pub(crate) fn register_source_actor(&self, pipeline_id: PipelineId, actor_name: &str) {
332        self.write()
333            .source_actor_names
334            .entry(pipeline_id)
335            .or_default()
336            .push(actor_name.to_owned());
337    }
338
339    pub(crate) fn source_actor_names_for_pipeline(&self, pipeline_id: PipelineId) -> Vec<String> {
340        self.read()
341            .source_actor_names
342            .get(&pipeline_id)
343            .cloned()
344            .unwrap_or_default()
345    }
346
347    pub(crate) fn set_inline_source_content(&self, pipeline_id: PipelineId, content: String) {
348        assert!(
349            self.write()
350                .inline_source_content
351                .insert(pipeline_id, content)
352                .is_none()
353        );
354    }
355
356    pub(crate) fn inline_source_content(&self, pipeline_id: PipelineId) -> Option<String> {
357        self.read().inline_source_content.get(&pipeline_id).cloned()
358    }
359
360    /// This is a no-op, we don't currently handle removals from the actor registry
361    pub(crate) fn remove(&self, _name: String) {}
362
363    pub(crate) fn cleanup(&self, stream_id: StreamId) {
364        let actors = self.read().actors.clone();
365        for actor in actors {
366            actor.0.cleanup(stream_id);
367        }
368    }
369}