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