devtools/actors/
watcher.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//! The watcher is the main entry point when debugging an element. Right now only web views are supported.
6//! It talks to the devtools remote and lists the capabilities of the inspected target, and it serves
7//! as a bridge for messages between actors.
8//!
9//! Liberally derived from the [Firefox JS implementation].
10//!
11//! [Firefox JS implementation]: https://searchfox.org/mozilla-central/source/devtools/server/actors/descriptors/watcher.js
12
13use std::collections::HashMap;
14use std::net::TcpStream;
15use std::time::{SystemTime, UNIX_EPOCH};
16
17use base::id::BrowsingContextId;
18use log::warn;
19use serde::Serialize;
20use serde_json::{Map, Value};
21use servo_url::ServoUrl;
22
23use self::network_parent::{NetworkParentActor, NetworkParentActorMsg};
24use super::breakpoint::BreakpointListActor;
25use super::thread::ThreadActor;
26use super::worker::WorkerMsg;
27use crate::actor::{Actor, ActorError, ActorRegistry};
28use crate::actors::breakpoint::BreakpointListActorMsg;
29use crate::actors::browsing_context::{BrowsingContextActor, BrowsingContextActorMsg};
30use crate::actors::root::RootActor;
31use crate::actors::watcher::target_configuration::{
32    TargetConfigurationActor, TargetConfigurationActorMsg,
33};
34use crate::actors::watcher::thread_configuration::{
35    ThreadConfigurationActor, ThreadConfigurationActorMsg,
36};
37use crate::protocol::{ClientRequest, JsonPacketStream};
38use crate::resource::{ResourceArrayType, ResourceAvailable};
39use crate::{EmptyReplyMsg, IdMap, StreamId, WorkerActor};
40
41pub mod network_parent;
42pub mod target_configuration;
43pub mod thread_configuration;
44
45/// Describes the debugged context. It informs the server of which objects can be debugged.
46/// <https://searchfox.org/mozilla-central/source/devtools/server/actors/watcher/session-context.js>
47#[derive(Serialize)]
48#[serde(rename_all = "camelCase")]
49pub struct SessionContext {
50    is_server_target_switching_enabled: bool,
51    supported_targets: HashMap<&'static str, bool>,
52    supported_resources: HashMap<&'static str, bool>,
53    context_type: SessionContextType,
54}
55
56impl SessionContext {
57    pub fn new(context_type: SessionContextType) -> Self {
58        Self {
59            is_server_target_switching_enabled: false,
60            // Right now we only support debugging web views (frames)
61            supported_targets: HashMap::from([
62                ("frame", true),
63                ("process", false),
64                ("worker", true),
65                ("service_worker", false),
66                ("shared_worker", false),
67            ]),
68            // At the moment we are blocking most resources to avoid errors
69            // Support for them will be enabled gradually once the corresponding actors start
70            // working propperly
71            supported_resources: HashMap::from([
72                ("console-message", true),
73                ("css-change", true),
74                ("css-message", false),
75                ("css-registered-properties", false),
76                ("document-event", false),
77                ("Cache", false),
78                ("cookies", false),
79                ("error-message", true),
80                ("extension-storage", false),
81                ("indexed-db", false),
82                ("local-storage", false),
83                ("session-storage", false),
84                ("platform-message", false),
85                ("network-event", true),
86                ("network-event-stacktrace", false),
87                ("reflow", false),
88                ("stylesheet", false),
89                ("source", true),
90                ("thread-state", false),
91                ("server-sent-event", false),
92                ("websocket", false),
93                ("jstracer-trace", false),
94                ("jstracer-state", false),
95                ("last-private-context-exit", false),
96            ]),
97            context_type,
98        }
99    }
100}
101
102#[derive(Serialize)]
103pub enum SessionContextType {
104    BrowserElement,
105    _ContextProcess,
106    _WebExtension,
107    _Worker,
108    _All,
109}
110
111#[derive(Serialize)]
112#[serde(untagged)]
113enum TargetActorMsg {
114    BrowsingContext(BrowsingContextActorMsg),
115    Worker(WorkerMsg),
116}
117
118#[derive(Serialize)]
119struct WatchTargetsReply {
120    from: String,
121    #[serde(rename = "type")]
122    type_: String,
123    target: TargetActorMsg,
124}
125
126#[derive(Serialize)]
127struct GetParentBrowsingContextIDReply {
128    from: String,
129    #[serde(rename = "browsingContextID")]
130    browsing_context_id: u32,
131}
132
133#[derive(Serialize)]
134struct GetNetworkParentActorReply {
135    from: String,
136    network: NetworkParentActorMsg,
137}
138
139#[derive(Serialize)]
140struct GetTargetConfigurationActorReply {
141    from: String,
142    configuration: TargetConfigurationActorMsg,
143}
144
145#[derive(Serialize)]
146struct GetThreadConfigurationActorReply {
147    from: String,
148    configuration: ThreadConfigurationActorMsg,
149}
150
151#[derive(Serialize)]
152#[serde(rename_all = "camelCase")]
153struct GetBreakpointListActorReply {
154    from: String,
155    breakpoint_list: BreakpointListActorMsg,
156}
157
158#[derive(Serialize)]
159#[serde(rename_all = "camelCase")]
160struct DocumentEvent {
161    #[serde(rename = "hasNativeConsoleAPI")]
162    has_native_console_api: Option<bool>,
163    name: String,
164    #[serde(rename = "newURI")]
165    new_uri: Option<String>,
166    time: u64,
167    title: Option<String>,
168    url: Option<String>,
169}
170
171#[derive(Serialize)]
172struct WatcherTraits {
173    resources: HashMap<&'static str, bool>,
174    #[serde(flatten)]
175    targets: HashMap<&'static str, bool>,
176}
177
178#[derive(Serialize)]
179pub struct WatcherActorMsg {
180    actor: String,
181    traits: WatcherTraits,
182}
183
184pub struct WatcherActor {
185    name: String,
186    browsing_context_actor: String,
187    network_parent: String,
188    target_configuration: String,
189    thread_configuration: String,
190    breakpoint_list: String,
191    session_context: SessionContext,
192}
193
194#[derive(Clone, Serialize)]
195#[serde(rename_all = "camelCase")]
196pub struct WillNavigateMessage {
197    #[serde(rename = "browsingContextID")]
198    browsing_context_id: u32,
199    inner_window_id: u32,
200    name: String,
201    time: u128,
202    is_frame_switching: bool,
203    #[serde(rename = "newURI")]
204    new_uri: ServoUrl,
205}
206
207impl Actor for WatcherActor {
208    fn name(&self) -> String {
209        self.name.clone()
210    }
211
212    /// The watcher actor can handle the following messages:
213    ///
214    /// - `watchTargets`: Returns a list of objects to debug. Since we only support web views, it
215    ///   returns the associated `BrowsingContextActor`. Every target sent creates a
216    ///   `target-available-form` event.
217    ///
218    /// - `watchResources`: Start watching certain resource types. This sends
219    ///   `resources-available-array` events.
220    ///
221    /// - `getNetworkParentActor`: Returns the network parent actor. It doesn't seem to do much at
222    ///   the moment.
223    ///
224    /// - `getTargetConfigurationActor`: Returns the configuration actor for a specific target, so
225    ///   that the server can update its settings.
226    ///
227    /// - `getThreadConfigurationActor`: The same but with the configuration actor for the thread
228    fn handle_message(
229        &self,
230        mut request: ClientRequest,
231        registry: &ActorRegistry,
232        msg_type: &str,
233        msg: &Map<String, Value>,
234        _id: StreamId,
235    ) -> Result<(), ActorError> {
236        let target = registry.find::<BrowsingContextActor>(&self.browsing_context_actor);
237        let root = registry.find::<RootActor>("root");
238        match msg_type {
239            "watchTargets" => {
240                // As per logs we either get targetType as "frame" or "worker"
241                let target_type = msg
242                    .get("targetType")
243                    .and_then(Value::as_str)
244                    .unwrap_or("frame"); // default to "frame"
245
246                if target_type == "frame" {
247                    let msg = WatchTargetsReply {
248                        from: self.name(),
249                        type_: "target-available-form".into(),
250                        target: TargetActorMsg::BrowsingContext(target.encodable()),
251                    };
252                    let _ = request.write_json_packet(&msg);
253
254                    target.frame_update(&mut request);
255                } else if target_type == "worker" {
256                    for worker_name in &root.workers {
257                        let worker = registry.find::<WorkerActor>(worker_name);
258                        let worker_msg = WatchTargetsReply {
259                            from: self.name(),
260                            type_: "target-available-form".into(),
261                            target: TargetActorMsg::Worker(worker.encodable()),
262                        };
263                        let _ = request.write_json_packet(&worker_msg);
264                    }
265                } else {
266                    warn!("Unexpected target_type: {}", target_type);
267                }
268
269                // Messages that contain a `type` field are used to send event callbacks, but they
270                // don't count as a reply. Since every message needs to be responded, we send an
271                // extra empty packet to the devtools host to inform that we successfully received
272                // and processed the message so that it can continue
273                let msg = EmptyReplyMsg { from: self.name() };
274                request.reply_final(&msg)?
275            },
276            "watchResources" => {
277                let Some(resource_types) = msg.get("resourceTypes") else {
278                    return Err(ActorError::MissingParameter);
279                };
280                let Some(resource_types) = resource_types.as_array() else {
281                    return Err(ActorError::BadParameterType);
282                };
283
284                for resource in resource_types {
285                    let Some(resource) = resource.as_str() else {
286                        continue;
287                    };
288                    match resource {
289                        "document-event" => {
290                            // TODO: This is a hacky way of sending the 3 messages
291                            //       Figure out if there needs work to be done here, ensure the page is loaded
292                            for &name in ["dom-loading", "dom-interactive", "dom-complete"].iter() {
293                                let event = DocumentEvent {
294                                    has_native_console_api: Some(true),
295                                    name: name.into(),
296                                    new_uri: None,
297                                    time: SystemTime::now()
298                                        .duration_since(UNIX_EPOCH)
299                                        .unwrap_or_default()
300                                        .as_millis()
301                                        as u64,
302                                    title: Some(target.title.borrow().clone()),
303                                    url: Some(target.url.borrow().clone()),
304                                };
305                                target.resource_array(
306                                    event,
307                                    "document-event".into(),
308                                    ResourceArrayType::Available,
309                                    &mut request,
310                                );
311                            }
312                        },
313                        "source" => {
314                            let thread_actor = registry.find::<ThreadActor>(&target.thread);
315                            target.resources_array(
316                                thread_actor.source_manager.source_forms(registry),
317                                "source".into(),
318                                ResourceArrayType::Available,
319                                &mut request,
320                            );
321
322                            for worker_name in &root.workers {
323                                let worker = registry.find::<WorkerActor>(worker_name);
324                                let thread = registry.find::<ThreadActor>(&worker.thread);
325
326                                worker.resources_array(
327                                    thread.source_manager.source_forms(registry),
328                                    "source".into(),
329                                    ResourceArrayType::Available,
330                                    &mut request,
331                                );
332                            }
333                        },
334                        "console-message" | "error-message" => {},
335                        "network-event" => {},
336                        _ => warn!("resource {} not handled yet", resource),
337                    }
338                }
339                let msg = EmptyReplyMsg { from: self.name() };
340                request.reply_final(&msg)?
341            },
342            "getParentBrowsingContextID" => {
343                let msg = GetParentBrowsingContextIDReply {
344                    from: self.name(),
345                    browsing_context_id: target.browsing_context_id.value(),
346                };
347                request.reply_final(&msg)?
348            },
349            "getNetworkParentActor" => {
350                let network_parent = registry.find::<NetworkParentActor>(&self.network_parent);
351                let msg = GetNetworkParentActorReply {
352                    from: self.name(),
353                    network: network_parent.encodable(),
354                };
355                request.reply_final(&msg)?
356            },
357            "getTargetConfigurationActor" => {
358                let target_configuration =
359                    registry.find::<TargetConfigurationActor>(&self.target_configuration);
360                let msg = GetTargetConfigurationActorReply {
361                    from: self.name(),
362                    configuration: target_configuration.encodable(),
363                };
364                request.reply_final(&msg)?
365            },
366            "getThreadConfigurationActor" => {
367                let thread_configuration =
368                    registry.find::<ThreadConfigurationActor>(&self.thread_configuration);
369                let msg = GetThreadConfigurationActorReply {
370                    from: self.name(),
371                    configuration: thread_configuration.encodable(),
372                };
373                request.reply_final(&msg)?
374            },
375            "getBreakpointListActor" => {
376                let breakpoint_list = registry.find::<BreakpointListActor>(&self.breakpoint_list);
377                request.reply_final(&GetBreakpointListActorReply {
378                    from: self.name(),
379                    breakpoint_list: breakpoint_list.encodable(),
380                })?
381            },
382            _ => return Err(ActorError::UnrecognizedPacketType),
383        };
384        Ok(())
385    }
386}
387
388impl ResourceAvailable for WatcherActor {
389    fn actor_name(&self) -> String {
390        self.name.clone()
391    }
392}
393
394impl WatcherActor {
395    pub fn new(
396        actors: &mut ActorRegistry,
397        browsing_context_actor: String,
398        session_context: SessionContext,
399    ) -> Self {
400        let network_parent = NetworkParentActor::new(actors.new_name("network-parent"));
401        let target_configuration =
402            TargetConfigurationActor::new(actors.new_name("target-configuration"));
403        let thread_configuration =
404            ThreadConfigurationActor::new(actors.new_name("thread-configuration"));
405        let breakpoint_list = BreakpointListActor::new(actors.new_name("breakpoint-list"));
406
407        let watcher = Self {
408            name: actors.new_name("watcher"),
409            browsing_context_actor,
410            network_parent: network_parent.name(),
411            target_configuration: target_configuration.name(),
412            thread_configuration: thread_configuration.name(),
413            breakpoint_list: breakpoint_list.name(),
414            session_context,
415        };
416
417        actors.register(Box::new(network_parent));
418        actors.register(Box::new(target_configuration));
419        actors.register(Box::new(thread_configuration));
420        actors.register(Box::new(breakpoint_list));
421
422        watcher
423    }
424
425    pub fn encodable(&self) -> WatcherActorMsg {
426        WatcherActorMsg {
427            actor: self.name(),
428            traits: WatcherTraits {
429                resources: self.session_context.supported_resources.clone(),
430                targets: self.session_context.supported_targets.clone(),
431            },
432        }
433    }
434
435    pub fn emit_will_navigate(
436        &self,
437        browsing_context_id: BrowsingContextId,
438        url: ServoUrl,
439        connections: &mut Vec<TcpStream>,
440        id_map: &mut IdMap,
441    ) {
442        let msg = WillNavigateMessage {
443            browsing_context_id: id_map.browsing_context_id(browsing_context_id).value(),
444            inner_window_id: 0, // TODO: set this to the correct value
445            name: "will-navigate".to_string(),
446            time: SystemTime::now()
447                .duration_since(UNIX_EPOCH)
448                .unwrap_or_default()
449                .as_millis(),
450            is_frame_switching: false, // TODO: Implement frame switching
451            new_uri: url,
452        };
453
454        for stream in connections {
455            self.resource_array(
456                msg.clone(),
457                "document-event".to_string(),
458                ResourceArrayType::Available,
459                stream,
460            );
461        }
462    }
463}