Skip to main content

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