Skip to main content

devtools/actors/
root.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
5//! Connection point for all new remote devtools interactions, providing lists of know actors
6//! that perform more specific actions (targets, addons, browser chrome, etc.)
7//!
8//! Liberally derived from the [Firefox JS implementation].
9//!
10//! [Firefox JS implementation]: https://searchfox.org/mozilla-central/source/devtools/server/actors/root.js
11
12use std::collections::HashMap;
13
14use atomic_refcell::AtomicRefCell;
15use malloc_size_of_derive::MallocSizeOf;
16use serde::Serialize;
17use serde_json::{Map, Value};
18
19use crate::actor::{Actor, ActorEncode, ActorError, ActorRegistry};
20use crate::actors::browsing_context::BrowsingContextActor;
21use crate::actors::device::DeviceActor;
22use crate::actors::performance::PerformanceActor;
23use crate::actors::preference::PreferenceActor;
24use crate::actors::process::{ProcessActor, ProcessActorMsg};
25use crate::actors::tab::{TabDescriptorActor, TabDescriptorActorMsg};
26use crate::actors::worker::{WorkerTargetActor, WorkerTargetActorMsg};
27use crate::protocol::{ActorDescription, ClientRequest};
28use crate::{EmptyReplyMsg, StreamId};
29
30#[derive(Serialize)]
31#[serde(rename_all = "camelCase")]
32struct ServiceWorkerInfo {
33    actor: String,
34    url: String,
35    state: u32,
36    state_text: String,
37    id: String,
38    fetch: bool,
39    traits: HashMap<&'static str, bool>,
40}
41
42#[derive(Serialize)]
43#[serde(rename_all = "camelCase")]
44struct ServiceWorkerRegistrationMsg {
45    actor: String,
46    scope: String,
47    url: String,
48    registration_state: String,
49    last_update_time: u64,
50    traits: HashMap<&'static str, bool>,
51    // Firefox DevTools (LegacyServiceWorkersWatcher) matches workers via these
52    // four named fields, not via a `workers` array.
53    evaluating_worker: Option<ServiceWorkerInfo>,
54    installing_worker: Option<ServiceWorkerInfo>,
55    waiting_worker: Option<ServiceWorkerInfo>,
56    active_worker: Option<ServiceWorkerInfo>,
57}
58
59#[derive(Serialize)]
60#[serde(rename_all = "camelCase")]
61struct RootTraits {
62    sources: bool,
63    highlightable: bool,
64    custom_highlighters: bool,
65    network_monitor: bool,
66    resources: HashMap<&'static str, bool>,
67}
68
69#[derive(Serialize)]
70struct ListAddonsReply {
71    from: String,
72    addons: Vec<AddonMsg>,
73}
74
75#[derive(Serialize)]
76enum AddonMsg {}
77
78#[derive(Clone, Default, Serialize, MallocSizeOf)]
79#[serde(rename_all = "camelCase")]
80struct GlobalActors {
81    device_actor: String,
82    perf_actor: String,
83    preference_actor: String,
84    // Not implemented in Servo
85    // addons_actor
86    // heap_snapshot_file_actor
87    // parent_accessibility_actor
88    // screenshot_actor
89}
90
91#[derive(Serialize)]
92struct GetRootReply {
93    from: String,
94    #[serde(flatten)]
95    global_actors: GlobalActors,
96}
97
98#[derive(Serialize)]
99struct ListTabsReply {
100    from: String,
101    tabs: Vec<TabDescriptorActorMsg>,
102}
103
104#[derive(Serialize)]
105struct GetTabReply {
106    from: String,
107    tab: TabDescriptorActorMsg,
108}
109
110#[derive(Serialize)]
111#[serde(rename_all = "camelCase")]
112pub(crate) struct RootActorMsg {
113    from: String,
114    application_type: String,
115    traits: RootTraits,
116}
117
118#[derive(Serialize)]
119pub(crate) struct ProtocolDescriptionReply {
120    from: String,
121    types: Types,
122}
123
124#[derive(Serialize)]
125struct ListWorkersReply {
126    from: String,
127    workers: Vec<WorkerTargetActorMsg>,
128}
129
130#[derive(Serialize)]
131struct ListServiceWorkerRegistrationsReply {
132    from: String,
133    registrations: Vec<ServiceWorkerRegistrationMsg>,
134}
135
136#[derive(Serialize)]
137pub(crate) struct Types {
138    performance: ActorDescription,
139    device: ActorDescription,
140}
141
142#[derive(Serialize)]
143struct ListProcessesResponse {
144    from: String,
145    processes: Vec<ProcessActorMsg>,
146}
147
148#[derive(Default, Serialize)]
149#[serde(rename_all = "camelCase")]
150pub(crate) struct DescriptorTraits {
151    pub(crate) watcher: bool,
152    pub(crate) supports_reload_descriptor: bool,
153    pub(crate) supports_navigation: bool,
154}
155
156#[derive(Serialize)]
157#[serde(rename_all = "camelCase")]
158struct GetProcessResponse {
159    from: String,
160    process_descriptor: ProcessActorMsg,
161}
162
163#[derive(MallocSizeOf)]
164pub(crate) struct RootActor {
165    name: String,
166    active_tab: AtomicRefCell<Option<String>>,
167    global_actors: GlobalActors,
168    process_name: String,
169    pub tabs: AtomicRefCell<Vec<String>>,
170    pub workers: AtomicRefCell<Vec<String>>,
171    pub service_workers: AtomicRefCell<Vec<String>>,
172}
173
174impl Actor for RootActor {
175    fn name(&self) -> &str {
176        &self.name
177    }
178
179    fn handle_message(
180        &self,
181        request: ClientRequest,
182        registry: &ActorRegistry,
183        msg_type: &str,
184        msg: &Map<String, Value>,
185        _id: StreamId,
186    ) -> Result<(), ActorError> {
187        match msg_type {
188            "connect" => {
189                let message = EmptyReplyMsg {
190                    from: self.name().into(),
191                };
192                request.reply_final(&message)?
193            },
194
195            // TODO: Unexpected message getTarget for process (when inspecting)
196            "getProcess" => {
197                let process_descriptor = registry.encode::<ProcessActor, _>(&self.process_name);
198                let reply = GetProcessResponse {
199                    from: self.name().into(),
200                    process_descriptor,
201                };
202                request.reply_final(&reply)?
203            },
204
205            "getRoot" => {
206                let reply = GetRootReply {
207                    from: self.name().into(),
208                    global_actors: self.global_actors.clone(),
209                };
210                request.reply_final(&reply)?
211            },
212
213            "getTab" => {
214                let browser_id = msg
215                    .get("browserId")
216                    .ok_or(ActorError::MissingParameter)?
217                    .as_u64()
218                    .ok_or(ActorError::BadParameterType)?;
219                let Some(tab) = self.get_tab_msg_by_browser_id(registry, browser_id as u32) else {
220                    return Err(ActorError::Internal);
221                };
222
223                let reply = GetTabReply {
224                    from: self.name().into(),
225                    tab,
226                };
227                request.reply_final(&reply)?
228            },
229
230            "listAddons" => {
231                let reply = ListAddonsReply {
232                    from: "root".to_owned(),
233                    addons: vec![],
234                };
235                request.reply_final(&reply)?
236            },
237
238            "listProcesses" => {
239                let process_descriptor = registry.encode::<ProcessActor, _>(&self.process_name);
240                let reply = ListProcessesResponse {
241                    from: self.name().into(),
242                    processes: vec![process_descriptor],
243                };
244                request.reply_final(&reply)?
245            },
246
247            "listServiceWorkerRegistrations" => {
248                let registrations = self
249                    .service_workers
250                    .borrow()
251                    .iter()
252                    .map(|worker_name| {
253                        let worker_actor = registry.find::<WorkerTargetActor>(worker_name);
254                        let url = worker_actor.url.to_string();
255                        // Find correct scope url in the service worker
256                        let scope = url.clone();
257                        ServiceWorkerRegistrationMsg {
258                            actor: worker_actor.name().into(),
259                            scope,
260                            url: url.clone(),
261                            registration_state: String::new(),
262                            last_update_time: 0,
263                            traits: HashMap::new(),
264                            evaluating_worker: None,
265                            installing_worker: None,
266                            waiting_worker: None,
267                            active_worker: Some(ServiceWorkerInfo {
268                                actor: worker_actor.name().into(),
269                                url,
270                                state: 4, // activated
271                                state_text: "activated".to_string(),
272                                id: worker_actor.worker_id.to_string(),
273                                fetch: false,
274                                traits: HashMap::new(),
275                            }),
276                        }
277                    })
278                    .collect();
279                let reply = ListServiceWorkerRegistrationsReply {
280                    from: self.name().into(),
281                    registrations,
282                };
283                request.reply_final(&reply)?
284            },
285
286            "listTabs" => {
287                let top_level_tabs: Vec<_> = self
288                    .tabs
289                    .borrow()
290                    .iter()
291                    .filter(|&tab_name| {
292                        registry
293                            .find::<TabDescriptorActor>(tab_name)
294                            .is_top_level_global(registry)
295                    })
296                    .cloned()
297                    .collect();
298
299                // Ensure a tab is always selected
300                let active_tab_name = self.active_tab.borrow().clone();
301                let some_active_tab = active_tab_name
302                    .as_ref()
303                    .is_some_and(|name| top_level_tabs.contains(name));
304                if let Some(first_tab) = top_level_tabs.first() &&
305                    !some_active_tab
306                {
307                    *self.active_tab.borrow_mut() = Some(first_tab.clone());
308                }
309
310                let reply = ListTabsReply {
311                    from: self.name().into(),
312                    tabs: top_level_tabs
313                        .iter()
314                        .map(|tab_name| registry.encode::<TabDescriptorActor, _>(tab_name))
315                        .collect(),
316                };
317                request.reply_final(&reply)?
318            },
319
320            "listWorkers" => {
321                let reply = ListWorkersReply {
322                    from: self.name().into(),
323                    workers: self
324                        .workers
325                        .borrow()
326                        .iter()
327                        .map(|worker_name| registry.encode::<WorkerTargetActor, _>(worker_name))
328                        .collect(),
329                };
330                request.reply_final(&reply)?
331            },
332
333            "protocolDescription" => {
334                let msg = ProtocolDescriptionReply {
335                    from: self.name().into(),
336                    types: Types {
337                        performance: PerformanceActor::description(),
338                        device: DeviceActor::description(),
339                    },
340                };
341                request.reply_final(&msg)?
342            },
343
344            "watchResources" => {
345                // TODO: Respond to watch resource requests
346                request.reply_final(&EmptyReplyMsg {
347                    from: self.name().into(),
348                })?
349            },
350
351            "unwatchResources" => {
352                // TODO: Respond to unwatch resource requests
353                request.reply_final(&EmptyReplyMsg {
354                    from: self.name().into(),
355                })?
356            },
357
358            _ => return Err(ActorError::UnrecognizedPacketType),
359        };
360        Ok(())
361    }
362}
363
364impl RootActor {
365    /// Registers the root actor and its global actors (those not associated with a specific target).
366    pub fn register(registry: &mut ActorRegistry) {
367        // Global actors
368        let device_actor = DeviceActor::register(registry);
369        let performance_actor = PerformanceActor::register(registry);
370        let preference_actor = PreferenceActor::register(registry);
371
372        // Process descriptor
373        let process_actor = ProcessActor::register(registry);
374
375        // Root actor
376        let root_actor = Self {
377            // TODO: Create an actual name for the root actor
378            name: "root".into(),
379            global_actors: GlobalActors {
380                device_actor: device_actor.name().into(),
381                perf_actor: performance_actor.name().into(),
382                preference_actor: preference_actor.name().into(),
383            },
384            process_name: process_actor.name().into(),
385            active_tab: AtomicRefCell::new(None),
386            tabs: AtomicRefCell::new(vec![]),
387            workers: AtomicRefCell::new(vec![]),
388            service_workers: AtomicRefCell::new(vec![]),
389        };
390
391        registry.register(root_actor);
392    }
393
394    fn get_tab_msg_by_browser_id(
395        &self,
396        registry: &ActorRegistry,
397        browser_id: u32,
398    ) -> Option<TabDescriptorActorMsg> {
399        let tab_descriptor_actor = self
400            .tabs
401            .borrow()
402            .iter()
403            .map(|tab_descriptor_name| registry.find::<TabDescriptorActor>(tab_descriptor_name))
404            .find(|tab_descriptor_actor| {
405                let browsing_context = registry
406                    .find::<BrowsingContextActor>(&tab_descriptor_actor.browsing_context_name);
407                browsing_context.browser_id.value() == browser_id
408            });
409
410        if let Some(tab_descriptor_actor) = tab_descriptor_actor {
411            *self.active_tab.borrow_mut() = Some(tab_descriptor_actor.name().to_owned());
412            Some(registry.encode::<TabDescriptorActor, _>(tab_descriptor_actor.name()))
413        } else {
414            None
415        }
416    }
417
418    pub fn active_tab(&self) -> Option<String> {
419        self.active_tab.borrow().clone()
420    }
421}
422
423impl ActorEncode<RootActorMsg> for RootActor {
424    fn encode(&self, _: &ActorRegistry) -> RootActorMsg {
425        RootActorMsg {
426            from: self.name().into(),
427            application_type: "browser".to_owned(),
428            traits: RootTraits {
429                sources: false,
430                highlightable: true,
431                custom_highlighters: true,
432                network_monitor: true,
433                resources: HashMap::from([("extensions-backgroundscript-status", true)]),
434            },
435        }
436    }
437}