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