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