devtools/actors/
browsing_context.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//! Liberally derived from the [Firefox JS implementation](https://searchfox.org/mozilla-central/source/devtools/server/actors/webbrowser.js).
6//! Connection point for remote devtools that wish to investigate a particular Browsing Context's contents.
7//! Supports dynamic attaching and detaching which control notifications of navigation, etc.
8
9use atomic_refcell::AtomicRefCell;
10use devtools_traits::DevtoolScriptControlMsg::{self, GetCssDatabase, SimulateColorScheme};
11use devtools_traits::{DevtoolsPageInfo, NavigationState};
12use embedder_traits::Theme;
13use malloc_size_of_derive::MallocSizeOf;
14use rustc_hash::FxHashMap;
15use serde::Serialize;
16use serde_json::{Map, Value};
17use servo_base::generic_channel::{self, GenericSender, SendError};
18use servo_base::id::PipelineId;
19
20use crate::actor::{Actor, ActorEncode, ActorError, ActorRegistry};
21use crate::actors::inspector::InspectorActor;
22use crate::actors::inspector::accessibility::AccessibilityActor;
23use crate::actors::inspector::css_properties::CssPropertiesActor;
24use crate::actors::reflow::ReflowActor;
25use crate::actors::stylesheets::StyleSheetsActor;
26use crate::actors::tab::TabDescriptorActor;
27use crate::actors::thread::ThreadActor;
28use crate::actors::watcher::{SessionContext, SessionContextType, WatcherActor};
29use crate::id::{DevtoolsBrowserId, DevtoolsBrowsingContextId, DevtoolsOuterWindowId, IdMap};
30use crate::protocol::{ClientRequest, DevtoolsConnection, JsonPacketStream};
31use crate::resource::ResourceAvailable;
32use crate::{EmptyReplyMsg, StreamId};
33
34#[derive(Serialize)]
35struct ListWorkersReply {
36    from: String,
37    workers: Vec<()>,
38}
39
40#[derive(Serialize)]
41struct FrameUpdateReply {
42    from: String,
43    #[serde(rename = "type")]
44    type_: String,
45    frames: Vec<FrameUpdateMsg>,
46}
47
48#[derive(Serialize)]
49#[serde(rename_all = "camelCase")]
50struct FrameUpdateMsg {
51    id: u32,
52    is_top_level: bool,
53    url: String,
54    title: String,
55}
56
57#[derive(Serialize)]
58struct TabNavigated {
59    from: String,
60    #[serde(rename = "type")]
61    type_: String,
62    url: String,
63    title: Option<String>,
64    #[serde(rename = "nativeConsoleAPI")]
65    native_console_api: bool,
66    state: String,
67    is_frame_switching: bool,
68}
69
70#[derive(Serialize)]
71#[serde(rename_all = "camelCase")]
72struct BrowsingContextTraits {
73    frames: bool,
74    is_browsing_context: bool,
75    log_in_page: bool,
76    navigation: bool,
77    supports_top_level_target_flag: bool,
78    watchpoints: bool,
79}
80
81#[derive(Serialize)]
82#[serde(rename_all = "lowercase")]
83enum TargetType {
84    Frame,
85    // Other target types not implemented yet.
86}
87
88#[derive(Serialize)]
89#[serde(rename_all = "camelCase")]
90pub(crate) struct BrowsingContextActorMsg {
91    actor: String,
92    title: String,
93    url: String,
94    /// This correspond to webview_id
95    #[serde(rename = "browserId")]
96    browser_id: u32,
97    #[serde(rename = "outerWindowID")]
98    outer_window_id: u32,
99    #[serde(rename = "browsingContextID")]
100    browsing_context_id: u32,
101    is_top_level_target: bool,
102    traits: BrowsingContextTraits,
103    // Implemented actors
104    accessibility_actor: String,
105    console_actor: String,
106    css_properties_actor: String,
107    inspector_actor: String,
108    reflow_actor: String,
109    style_sheets_actor: String,
110    thread_actor: String,
111    target_type: TargetType,
112    // Part of the official protocol, but not yet implemented.
113    // animations_actor: String,
114    // changes_actor: String,
115    // framerate_actor: String,
116    // manifest_actor: String,
117    // memory_actor: String,
118    // network_content_actor: String,
119    // objects_manager: String,
120    // performance_actor: String,
121    // resonsive_actor: String,
122    // storage_actor: String,
123    // tracer_actor: String,
124    // web_extension_inspected_window_actor: String,
125    // web_socket_actor: String,
126}
127
128/// The browsing context actor encompasses all of the other supporting actors when debugging a web
129/// view. To this extent, it contains a watcher actor that helps when communicating with the host,
130/// as well as resource actors that each perform one debugging function.
131#[derive(MallocSizeOf)]
132pub(crate) struct BrowsingContextActor {
133    name: String,
134    pub title: AtomicRefCell<String>,
135    pub url: AtomicRefCell<String>,
136    /// This corresponds to webview_id
137    pub browser_id: DevtoolsBrowserId,
138    // TODO: Should these ids be atomic?
139    active_pipeline_id: AtomicRefCell<PipelineId>,
140    active_outer_window_id: AtomicRefCell<DevtoolsOuterWindowId>,
141    pub browsing_context_id: DevtoolsBrowsingContextId,
142    accessibility_name: String,
143    pub console_name: String,
144    css_properties_name: String,
145    pub(crate) inspector_name: String,
146    reflow_name: String,
147    style_sheets_name: String,
148    pub thread_name: String,
149    _tab: String,
150    // Different pipelines may run on different script threads.
151    // These should be kept around even when the active pipeline is updated,
152    // in case the browsing context revisits a pipeline via history navigation.
153    // TODO: Each entry is stored forever; ideally there should be a way to
154    //       detect when `ScriptThread`s are destroyed and remove the associated
155    //       entries.
156    script_chans: AtomicRefCell<FxHashMap<PipelineId, GenericSender<DevtoolScriptControlMsg>>>,
157    pub watcher_name: String,
158}
159
160impl ResourceAvailable for BrowsingContextActor {
161    fn actor_name(&self) -> String {
162        self.name.clone()
163    }
164}
165
166impl Actor for BrowsingContextActor {
167    fn name(&self) -> String {
168        self.name.clone()
169    }
170
171    fn handle_message(
172        &self,
173        request: ClientRequest,
174        _registry: &ActorRegistry,
175        msg_type: &str,
176        _msg: &Map<String, Value>,
177        _id: StreamId,
178    ) -> Result<(), ActorError> {
179        match msg_type {
180            "listFrames" => {
181                // TODO: Find out what needs to be listed here
182                let msg = EmptyReplyMsg { from: self.name() };
183                request.reply_final(&msg)?
184            },
185            "listWorkers" => {
186                request.reply_final(&ListWorkersReply {
187                    from: self.name(),
188                    // TODO: Find out what needs to be listed here
189                    workers: vec![],
190                })?
191            },
192            _ => return Err(ActorError::UnrecognizedPacketType),
193        };
194        Ok(())
195    }
196}
197
198impl BrowsingContextActor {
199    #[expect(clippy::too_many_arguments)]
200    pub(crate) fn register(
201        registry: &ActorRegistry,
202        console_name: String,
203        browser_id: DevtoolsBrowserId,
204        browsing_context_id: DevtoolsBrowsingContextId,
205        page_info: DevtoolsPageInfo,
206        pipeline_id: PipelineId,
207        outer_window_id: DevtoolsOuterWindowId,
208        script_sender: GenericSender<DevtoolScriptControlMsg>,
209    ) -> String {
210        let name = registry.new_name::<BrowsingContextActor>();
211        let DevtoolsPageInfo {
212            title,
213            url,
214            is_top_level_global,
215            ..
216        } = page_info;
217
218        let accessibility_name = AccessibilityActor::register(registry);
219
220        let properties = (|| {
221            let (properties_sender, properties_receiver) = generic_channel::channel()?;
222            script_sender.send(GetCssDatabase(properties_sender)).ok()?;
223            properties_receiver.recv().ok()
224        })()
225        .unwrap_or_default();
226        let css_properties_name = CssPropertiesActor::register(registry, properties);
227
228        let inspector_name = InspectorActor::register(registry, name.clone());
229
230        let reflow_name = ReflowActor::register(registry);
231
232        let style_sheets_name = StyleSheetsActor::register(registry);
233
234        let tab_descriptor_actor =
235            TabDescriptorActor::new(registry, name.clone(), is_top_level_global);
236
237        let thread_name =
238            ThreadActor::register(registry, script_sender.clone(), Some(name.clone()));
239
240        let watcher_actor = WatcherActor::new(
241            registry,
242            name.clone(),
243            SessionContext::new(SessionContextType::BrowserElement),
244        );
245
246        let mut script_chans = FxHashMap::default();
247        script_chans.insert(pipeline_id, script_sender);
248
249        let actor = BrowsingContextActor {
250            name: name.clone(),
251            script_chans: AtomicRefCell::new(script_chans),
252            title: AtomicRefCell::new(title),
253            url: AtomicRefCell::new(url.into_string()),
254            active_pipeline_id: AtomicRefCell::new(pipeline_id),
255            active_outer_window_id: AtomicRefCell::new(outer_window_id),
256            browser_id,
257            browsing_context_id,
258            accessibility_name,
259            console_name,
260            css_properties_name,
261            inspector_name,
262            reflow_name,
263            style_sheets_name,
264            _tab: tab_descriptor_actor.name(),
265            thread_name,
266            watcher_name: watcher_actor.name(),
267        };
268
269        registry.register(tab_descriptor_actor);
270        registry.register(watcher_actor);
271        registry.register::<Self>(actor);
272
273        name
274    }
275
276    pub(crate) fn handle_new_global(
277        &self,
278        pipeline: PipelineId,
279        script_sender: GenericSender<DevtoolScriptControlMsg>,
280    ) {
281        self.script_chans
282            .borrow_mut()
283            .insert(pipeline, script_sender);
284    }
285
286    pub(crate) fn handle_navigate<'a>(
287        &self,
288        state: NavigationState,
289        id_map: &mut IdMap,
290        connections: impl Iterator<Item = &'a mut DevtoolsConnection>,
291    ) {
292        let (pipeline_id, title, url, state) = match state {
293            NavigationState::Start(url) => (None, None, url, "start"),
294            NavigationState::Stop(pipeline, info) => {
295                (Some(pipeline), Some(info.title), info.url, "stop")
296            },
297        };
298        if let Some(pipeline_id) = pipeline_id {
299            let outer_window_id = id_map.outer_window_id(pipeline_id);
300            *self.active_outer_window_id.borrow_mut() = outer_window_id;
301            *self.active_pipeline_id.borrow_mut() = pipeline_id;
302        }
303        url.as_str().clone_into(&mut self.url.borrow_mut());
304        if let Some(ref t) = title {
305            self.title.borrow_mut().clone_from(t);
306        }
307
308        let msg = TabNavigated {
309            from: self.name(),
310            type_: "tabNavigated".to_owned(),
311            url: url.as_str().to_owned(),
312            title,
313            native_console_api: true,
314            state: state.to_owned(),
315            is_frame_switching: false,
316        };
317
318        for stream in connections {
319            let _ = stream.write_json_packet(&msg);
320        }
321    }
322
323    pub(crate) fn title_changed(&self, pipeline_id: PipelineId, title: String) {
324        if pipeline_id != self.pipeline_id() {
325            return;
326        }
327        *self.title.borrow_mut() = title;
328    }
329
330    pub(crate) fn frame_update(&self, request: &mut ClientRequest) {
331        let _ = request.write_json_packet(&FrameUpdateReply {
332            from: self.name(),
333            type_: "frameUpdate".into(),
334            frames: vec![FrameUpdateMsg {
335                id: self.browsing_context_id.value(),
336                is_top_level: true,
337                title: self.title.borrow().clone(),
338                url: self.url.borrow().clone(),
339            }],
340        });
341    }
342
343    pub fn simulate_color_scheme(&self, theme: Theme) -> Result<(), ()> {
344        self.script_chan()
345            .send(SimulateColorScheme(self.pipeline_id(), theme))
346            .map_err(|_| ())
347    }
348
349    pub(crate) fn pipeline_id(&self) -> PipelineId {
350        *self.active_pipeline_id.borrow()
351    }
352
353    pub(crate) fn outer_window_id(&self) -> DevtoolsOuterWindowId {
354        *self.active_outer_window_id.borrow()
355    }
356
357    /// Returns the script sender for the active pipeline.
358    pub(crate) fn script_chan(&self) -> GenericSender<DevtoolScriptControlMsg> {
359        self.script_chans
360            .borrow()
361            .get(&self.pipeline_id())
362            .unwrap()
363            .clone()
364    }
365
366    pub(crate) fn instruct_script_to_send_live_updates(&self, should_send_updates: bool) {
367        let result = self
368            .script_chan()
369            .send(DevtoolScriptControlMsg::WantsLiveNotifications(
370                self.pipeline_id(),
371                should_send_updates,
372            ));
373
374        // Notifying the script thread may fail with a "Disconnected" error if servo
375        // as a whole is being shut down.
376        debug_assert!(matches!(result, Ok(_) | Err(SendError::Disconnected)));
377    }
378}
379
380impl ActorEncode<BrowsingContextActorMsg> for BrowsingContextActor {
381    fn encode(&self, _: &ActorRegistry) -> BrowsingContextActorMsg {
382        BrowsingContextActorMsg {
383            actor: self.name(),
384            traits: BrowsingContextTraits {
385                is_browsing_context: true,
386                frames: true,
387                log_in_page: false,
388                navigation: true,
389                supports_top_level_target_flag: true,
390                watchpoints: true,
391            },
392            title: self.title.borrow().clone(),
393            url: self.url.borrow().clone(),
394            browser_id: self.browser_id.value(),
395            browsing_context_id: self.browsing_context_id.value(),
396            outer_window_id: self.outer_window_id().value(),
397            is_top_level_target: true,
398            accessibility_actor: self.accessibility_name.clone(),
399            console_actor: self.console_name.clone(),
400            css_properties_actor: self.css_properties_name.clone(),
401            inspector_actor: self.inspector_name.clone(),
402            reflow_actor: self.reflow_name.clone(),
403            style_sheets_actor: self.style_sheets_name.clone(),
404            thread_actor: self.thread_name.clone(),
405            target_type: TargetType::Frame,
406        }
407    }
408}