devtools/actors/
source.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
5use std::cell::RefCell;
6use std::collections::{BTreeMap, BTreeSet};
7
8use base::id::PipelineId;
9use devtools_traits::DevtoolScriptControlMsg;
10use ipc_channel::ipc::{IpcSender, channel};
11use serde::Serialize;
12use serde_json::{Map, Value};
13use servo_url::ServoUrl;
14
15use crate::StreamId;
16use crate::actor::{Actor, ActorError, ActorRegistry};
17use crate::protocol::ClientRequest;
18
19/// A `sourceForm` as used in responses to thread `sources` requests.
20///
21/// For now, we also use this for sources in watcher `resource-available-array` messages,
22/// but in Firefox those have extra fields.
23///
24/// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#loading-script-sources>
25#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
26#[serde(rename_all = "camelCase")]
27pub(crate) struct SourceForm {
28    pub actor: String,
29    /// URL of the script, or URL of the page for inline scripts.
30    pub url: String,
31    pub is_black_boxed: bool,
32    /// `introductionType` in SpiderMonkey `CompileOptionsWrapper`.
33    pub introduction_type: String,
34}
35
36#[derive(Serialize)]
37pub(crate) struct SourcesReply {
38    pub from: String,
39    pub sources: Vec<SourceForm>,
40}
41
42pub(crate) struct SourceManager {
43    source_actor_names: RefCell<BTreeSet<String>>,
44}
45
46#[derive(Clone, Debug)]
47pub struct SourceActor {
48    /// Actor name.
49    pub name: String,
50
51    /// URL of the script, or URL of the page for inline scripts.
52    pub url: ServoUrl,
53
54    /// The ‘black-boxed’ flag, which tells the debugger to avoid pausing inside this script.
55    /// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#black-boxing-sources>
56    pub is_black_boxed: bool,
57
58    pub content: Option<String>,
59    pub content_type: Option<String>,
60
61    // TODO: use it in #37667, then remove this allow
62    pub spidermonkey_id: u32,
63    /// `introductionType` in SpiderMonkey `CompileOptionsWrapper`.
64    pub introduction_type: String,
65
66    script_sender: IpcSender<DevtoolScriptControlMsg>,
67}
68
69#[derive(Serialize)]
70struct SourceContentReply {
71    from: String,
72    #[serde(rename = "contentType")]
73    content_type: Option<String>,
74    source: String,
75}
76
77#[derive(Serialize)]
78struct GetBreakableLinesReply {
79    from: String,
80    // Line numbers are one-based.
81    // <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
82    lines: BTreeSet<u32>,
83}
84
85#[derive(Serialize)]
86struct GetBreakpointPositionsCompressedReply {
87    from: String,
88    // Column numbers are in UTF-16 code units, not Unicode scalar values or grapheme clusters.
89    // Line number are one-based. Column numbers are zero-based.
90    // FIXME: the docs say column numbers are one-based, but this appears to be incorrect.
91    // <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
92    positions: BTreeMap<u32, BTreeSet<u32>>,
93}
94
95impl SourceManager {
96    pub fn new() -> Self {
97        Self {
98            source_actor_names: RefCell::new(BTreeSet::default()),
99        }
100    }
101
102    pub fn add_source(&self, actor_name: &str) {
103        self.source_actor_names
104            .borrow_mut()
105            .insert(actor_name.to_owned());
106    }
107
108    pub fn source_forms(&self, actors: &ActorRegistry) -> Vec<SourceForm> {
109        self.source_actor_names
110            .borrow()
111            .iter()
112            .map(|actor_name| actors.find::<SourceActor>(actor_name).source_form())
113            .collect()
114    }
115}
116
117impl SourceActor {
118    pub fn new(
119        name: String,
120        url: ServoUrl,
121        content: Option<String>,
122        content_type: Option<String>,
123        spidermonkey_id: u32,
124        introduction_type: String,
125        script_sender: IpcSender<DevtoolScriptControlMsg>,
126    ) -> SourceActor {
127        SourceActor {
128            name,
129            url,
130            content,
131            content_type,
132            is_black_boxed: false,
133            spidermonkey_id,
134            introduction_type,
135            script_sender,
136        }
137    }
138
139    #[allow(clippy::too_many_arguments)]
140    pub fn new_registered(
141        actors: &mut ActorRegistry,
142        pipeline_id: PipelineId,
143        url: ServoUrl,
144        content: Option<String>,
145        content_type: Option<String>,
146        spidermonkey_id: u32,
147        introduction_type: String,
148        script_sender: IpcSender<DevtoolScriptControlMsg>,
149    ) -> &SourceActor {
150        let source_actor_name = actors.new_name("source");
151
152        let source_actor = SourceActor::new(
153            source_actor_name.clone(),
154            url,
155            content,
156            content_type,
157            spidermonkey_id,
158            introduction_type,
159            script_sender,
160        );
161        actors.register(Box::new(source_actor));
162        actors.register_source_actor(pipeline_id, &source_actor_name);
163
164        actors.find(&source_actor_name)
165    }
166
167    pub fn source_form(&self) -> SourceForm {
168        SourceForm {
169            actor: self.name.clone(),
170            url: self.url.to_string(),
171            is_black_boxed: self.is_black_boxed,
172            introduction_type: self.introduction_type.clone(),
173        }
174    }
175}
176
177impl Actor for SourceActor {
178    fn name(&self) -> String {
179        self.name.clone()
180    }
181
182    fn handle_message(
183        &self,
184        request: ClientRequest,
185        _registry: &ActorRegistry,
186        msg_type: &str,
187        _msg: &Map<String, Value>,
188        _id: StreamId,
189    ) -> Result<(), ActorError> {
190        match msg_type {
191            // Client has requested contents of the source.
192            "source" => {
193                let reply = SourceContentReply {
194                    from: self.name(),
195                    content_type: self.content_type.clone(),
196                    // TODO: if needed, fetch the page again, in the same way as in the original request.
197                    // Fetch it from cache, even if the original request was non-idempotent (e.g. POST).
198                    // If we can’t fetch it from cache, we should probably give up, because with a real
199                    // fetch, the server could return a different response.
200                    // TODO: do we want to wait instead of giving up immediately, in cases where the content could
201                    // become available later (e.g. after a fetch)?
202                    source: self
203                        .content
204                        .as_deref()
205                        .unwrap_or("<!-- not available; please reload! -->")
206                        .to_owned(),
207                };
208                request.reply_final(&reply)?
209            },
210            // Client wants to know which lines can have breakpoints.
211            // Sent when opening a source in the Sources panel, and controls whether the line numbers can be clicked.
212            "getBreakableLines" => {
213                let (tx, rx) = channel().map_err(|_| ActorError::Internal)?;
214                self.script_sender
215                    .send(DevtoolScriptControlMsg::GetPossibleBreakpoints(
216                        self.spidermonkey_id,
217                        tx,
218                    ))
219                    .map_err(|_| ActorError::Internal)?;
220                let result = rx.recv().map_err(|_| ActorError::Internal)?;
221                let lines = result
222                    .into_iter()
223                    .map(|entry| entry.line_number)
224                    .collect::<BTreeSet<_>>();
225                let reply = GetBreakableLinesReply {
226                    from: self.name(),
227                    lines,
228                };
229                request.reply_final(&reply)?
230            },
231            // Client wants to know which columns in the line can have breakpoints.
232            // Sent when the user tries to set a breakpoint by clicking a line number in a source.
233            "getBreakpointPositionsCompressed" => {
234                let (tx, rx) = channel().map_err(|_| ActorError::Internal)?;
235                self.script_sender
236                    .send(DevtoolScriptControlMsg::GetPossibleBreakpoints(
237                        self.spidermonkey_id,
238                        tx,
239                    ))
240                    .map_err(|_| ActorError::Internal)?;
241                let result = rx.recv().map_err(|_| ActorError::Internal)?;
242                let mut positions: BTreeMap<u32, BTreeSet<u32>> = BTreeMap::default();
243                for entry in result {
244                    // Line number are one-based. Column numbers are zero-based.
245                    // FIXME: the docs say column numbers are one-based, but this appears to be incorrect.
246                    // <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
247                    positions
248                        .entry(entry.line_number)
249                        .or_default()
250                        .insert(entry.column_number - 1);
251                }
252                let reply = GetBreakpointPositionsCompressedReply {
253                    from: self.name(),
254                    positions,
255                };
256                request.reply_final(&reply)?
257            },
258            _ => return Err(ActorError::UnrecognizedPacketType),
259        };
260        Ok(())
261    }
262}