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    #[allow(unused)]
63    pub spidermonkey_id: u32,
64    /// `introductionType` in SpiderMonkey `CompileOptionsWrapper`.
65    pub introduction_type: String,
66
67    script_sender: IpcSender<DevtoolScriptControlMsg>,
68}
69
70#[derive(Serialize)]
71struct SourceContentReply {
72    from: String,
73    #[serde(rename = "contentType")]
74    content_type: Option<String>,
75    source: String,
76}
77
78#[derive(Serialize)]
79struct GetBreakableLinesReply {
80    from: String,
81    // Line numbers are one-based.
82    // <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
83    lines: BTreeSet<u32>,
84}
85
86#[derive(Serialize)]
87struct GetBreakpointPositionsCompressedReply {
88    from: String,
89    // Column numbers are in UTF-16 code units, not Unicode scalar values or grapheme clusters.
90    // Line number are one-based. Column numbers are zero-based.
91    // FIXME: the docs say column numbers are one-based, but this appears to be incorrect.
92    // <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
93    positions: BTreeMap<u32, BTreeSet<u32>>,
94}
95
96impl SourceManager {
97    pub fn new() -> Self {
98        Self {
99            source_actor_names: RefCell::new(BTreeSet::default()),
100        }
101    }
102
103    pub fn add_source(&self, actor_name: &str) {
104        self.source_actor_names
105            .borrow_mut()
106            .insert(actor_name.to_owned());
107    }
108
109    pub fn source_forms(&self, actors: &ActorRegistry) -> Vec<SourceForm> {
110        self.source_actor_names
111            .borrow()
112            .iter()
113            .map(|actor_name| actors.find::<SourceActor>(actor_name).source_form())
114            .collect()
115    }
116}
117
118impl SourceActor {
119    pub fn new(
120        name: String,
121        url: ServoUrl,
122        content: Option<String>,
123        content_type: Option<String>,
124        spidermonkey_id: u32,
125        introduction_type: String,
126        script_sender: IpcSender<DevtoolScriptControlMsg>,
127    ) -> SourceActor {
128        SourceActor {
129            name,
130            url,
131            content,
132            content_type,
133            is_black_boxed: false,
134            spidermonkey_id,
135            introduction_type,
136            script_sender,
137        }
138    }
139
140    #[allow(clippy::too_many_arguments)]
141    pub fn new_registered(
142        actors: &mut ActorRegistry,
143        pipeline_id: PipelineId,
144        url: ServoUrl,
145        content: Option<String>,
146        content_type: Option<String>,
147        spidermonkey_id: u32,
148        introduction_type: String,
149        script_sender: IpcSender<DevtoolScriptControlMsg>,
150    ) -> &SourceActor {
151        let source_actor_name = actors.new_name("source");
152
153        let source_actor = SourceActor::new(
154            source_actor_name.clone(),
155            url,
156            content,
157            content_type,
158            spidermonkey_id,
159            introduction_type,
160            script_sender,
161        );
162        actors.register(Box::new(source_actor));
163        actors.register_source_actor(pipeline_id, &source_actor_name);
164
165        actors.find(&source_actor_name)
166    }
167
168    pub fn source_form(&self) -> SourceForm {
169        SourceForm {
170            actor: self.name.clone(),
171            url: self.url.to_string(),
172            is_black_boxed: self.is_black_boxed,
173            introduction_type: self.introduction_type.clone(),
174        }
175    }
176}
177
178impl Actor for SourceActor {
179    fn name(&self) -> String {
180        self.name.clone()
181    }
182
183    fn handle_message(
184        &self,
185        request: ClientRequest,
186        _registry: &ActorRegistry,
187        msg_type: &str,
188        _msg: &Map<String, Value>,
189        _id: StreamId,
190    ) -> Result<(), ActorError> {
191        match msg_type {
192            // Client has requested contents of the source.
193            "source" => {
194                let reply = SourceContentReply {
195                    from: self.name(),
196                    content_type: self.content_type.clone(),
197                    // TODO: if needed, fetch the page again, in the same way as in the original request.
198                    // Fetch it from cache, even if the original request was non-idempotent (e.g. POST).
199                    // If we can’t fetch it from cache, we should probably give up, because with a real
200                    // fetch, the server could return a different response.
201                    // TODO: do we want to wait instead of giving up immediately, in cases where the content could
202                    // become available later (e.g. after a fetch)?
203                    source: self
204                        .content
205                        .as_deref()
206                        .unwrap_or("<!-- not available; please reload! -->")
207                        .to_owned(),
208                };
209                request.reply_final(&reply)?
210            },
211            // Client wants to know which lines can have breakpoints.
212            // Sent when opening a source in the Sources panel, and controls whether the line numbers can be clicked.
213            "getBreakableLines" => {
214                let (tx, rx) = channel().map_err(|_| ActorError::Internal)?;
215                self.script_sender
216                    .send(DevtoolScriptControlMsg::GetPossibleBreakpoints(
217                        self.spidermonkey_id,
218                        tx,
219                    ))
220                    .map_err(|_| ActorError::Internal)?;
221                let result = rx.recv().map_err(|_| ActorError::Internal)?;
222                let lines = result
223                    .into_iter()
224                    .map(|entry| entry.line_number)
225                    .collect::<BTreeSet<_>>();
226                let reply = GetBreakableLinesReply {
227                    from: self.name(),
228                    lines,
229                };
230                request.reply_final(&reply)?
231            },
232            // Client wants to know which columns in the line can have breakpoints.
233            // Sent when the user tries to set a breakpoint by clicking a line number in a source.
234            "getBreakpointPositionsCompressed" => {
235                let (tx, rx) = channel().map_err(|_| ActorError::Internal)?;
236                self.script_sender
237                    .send(DevtoolScriptControlMsg::GetPossibleBreakpoints(
238                        self.spidermonkey_id,
239                        tx,
240                    ))
241                    .map_err(|_| ActorError::Internal)?;
242                let result = rx.recv().map_err(|_| ActorError::Internal)?;
243                let mut positions: BTreeMap<u32, BTreeSet<u32>> = BTreeMap::default();
244                for entry in result {
245                    // Line number are one-based. Column numbers are zero-based.
246                    // FIXME: the docs say column numbers are one-based, but this appears to be incorrect.
247                    // <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
248                    positions
249                        .entry(entry.line_number)
250                        .or_default()
251                        .insert(entry.column_number - 1);
252                }
253                let reply = GetBreakpointPositionsCompressedReply {
254                    from: self.name(),
255                    positions,
256                };
257                request.reply_final(&reply)?
258            },
259            _ => return Err(ActorError::UnrecognizedPacketType),
260        };
261        Ok(())
262    }
263}