Skip to main content

devtools/actors/
console.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/firefox-main/source/devtools/server/actors/webconsole.js).
6//! Mediates interaction between the remote web console and equivalent functionality (object
7//! inspection, JS evaluation, autocompletion) in Servo.
8
9use std::collections::HashMap;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, Ordering};
12
13use atomic_refcell::AtomicRefCell;
14use devtools_traits::{
15    ConsoleMessage, ConsoleMessageFields, DevtoolScriptControlMsg, GetEnvironmentRequest,
16    PageError, StackFrame, get_time_stamp,
17};
18use malloc_size_of_derive::MallocSizeOf;
19use serde::Serialize;
20use serde_json::{self, Map, Value};
21use servo_base::generic_channel::{self, GenericSender, channel};
22use servo_base::id::{PipelineId, TEST_PIPELINE_ID};
23use uuid::Uuid;
24
25use crate::actor::{Actor, ActorError, ActorRegistry};
26use crate::actors::browsing_context::BrowsingContextActor;
27use crate::actors::environment::EnvironmentActor;
28use crate::actors::worker::WorkerTargetActor;
29use crate::protocol::{ClientRequest, DevtoolsConnection, JsonPacketStream};
30use crate::resource::{ResourceArrayType, ResourceAvailable};
31use crate::{EmptyReplyMsg, StreamId, UniqueId, debugger_value_to_json};
32
33#[derive(Clone, Serialize, MallocSizeOf)]
34#[serde(rename_all = "camelCase")]
35pub(crate) struct DevtoolsConsoleMessage {
36    #[serde(flatten)]
37    fields: ConsoleMessageFields,
38    #[ignore_malloc_size_of = "Currently no way to have serde_json::Value"]
39    arguments: Vec<Value>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    stacktrace: Option<Vec<StackFrame>>,
42    // Not implemented in Servo
43    // inner_window_id
44    // source_id
45}
46
47impl DevtoolsConsoleMessage {
48    pub(crate) fn new(message: ConsoleMessage, registry: &ActorRegistry) -> Self {
49        Self {
50            fields: message.fields,
51            arguments: message
52                .arguments
53                .into_iter()
54                .map(|argument| debugger_value_to_json(registry, argument))
55                .collect(),
56            stacktrace: message.stacktrace,
57        }
58    }
59}
60
61#[derive(Clone, Serialize, MallocSizeOf)]
62#[serde(rename_all = "camelCase")]
63struct DevtoolsPageError {
64    #[serde(flatten)]
65    page_error: PageError,
66    category: String,
67    error: bool,
68    warning: bool,
69    info: bool,
70    private: bool,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    stacktrace: Option<Vec<StackFrame>>,
73    // Not implemented in Servo
74    // inner_window_id
75    // source_id
76    // has_exception
77    // exception
78}
79
80impl From<PageError> for DevtoolsPageError {
81    fn from(page_error: PageError) -> Self {
82        Self {
83            page_error,
84            category: "script".to_string(),
85            error: true,
86            warning: false,
87            info: false,
88            private: false,
89            stacktrace: None,
90        }
91    }
92}
93#[derive(Clone, Serialize, MallocSizeOf)]
94#[serde(rename_all = "camelCase")]
95pub(crate) struct PageErrorWrapper {
96    page_error: DevtoolsPageError,
97}
98
99impl From<PageError> for PageErrorWrapper {
100    fn from(page_error: PageError) -> Self {
101        Self {
102            page_error: page_error.into(),
103        }
104    }
105}
106
107#[derive(Clone, Serialize, MallocSizeOf)]
108#[serde(untagged)]
109pub(crate) enum ConsoleResource {
110    ConsoleMessage(DevtoolsConsoleMessage),
111    PageError(PageErrorWrapper),
112}
113
114impl ConsoleResource {
115    pub fn resource_type(&self) -> String {
116        match self {
117            ConsoleResource::ConsoleMessage(_) => "console-message".into(),
118            ConsoleResource::PageError(_) => "error-message".into(),
119        }
120    }
121}
122
123#[derive(Serialize)]
124pub struct ConsoleClearMessage {
125    pub level: String,
126}
127
128#[derive(Serialize)]
129#[serde(rename_all = "camelCase")]
130struct AutocompleteReply {
131    from: String,
132    matches: Vec<String>,
133    match_prop: String,
134    is_element_access: bool,
135}
136
137#[derive(Serialize)]
138#[serde(rename_all = "camelCase")]
139struct EvaluateJSReply {
140    from: String,
141    input: String,
142    result: Value,
143    timestamp: u64,
144    exception: Value,
145    exception_message: Option<String>,
146    has_exception: bool,
147    helper_result: Value,
148}
149
150#[derive(Serialize)]
151#[serde(rename_all = "camelCase")]
152struct EvaluateJSEvent {
153    from: String,
154    #[serde(rename = "type")]
155    type_: String,
156    input: String,
157    result: Value,
158    timestamp: u64,
159    #[serde(rename = "resultID")]
160    result_id: String,
161    exception: Value,
162    exception_message: Option<String>,
163    has_exception: bool,
164    helper_result: Value,
165}
166
167#[derive(Serialize)]
168struct EvaluateJSAsyncReply {
169    from: String,
170    #[serde(rename = "resultID")]
171    result_id: String,
172}
173
174#[derive(Serialize)]
175struct SetPreferencesReply {
176    from: String,
177    updated: Vec<String>,
178}
179
180#[derive(MallocSizeOf)]
181pub(crate) enum Root {
182    BrowsingContext(String),
183    DedicatedWorker(String),
184}
185
186#[derive(MallocSizeOf)]
187pub(crate) struct ConsoleActor {
188    name: String,
189    root: Root,
190    cached_events: AtomicRefCell<HashMap<UniqueId, Vec<ConsoleResource>>>,
191    /// Used to control whether to send resource array messages from
192    /// `handle_console_resource`. It starts being false, and it only gets
193    /// activated after the client requests `console-message` or `error-message`
194    /// resources for the first time. Otherwise we would be sending messages
195    /// before the client is ready to receive them.
196    client_ready_to_receive_messages: AtomicBool,
197}
198
199impl ConsoleActor {
200    pub fn register(registry: &ActorRegistry, name: String, root: Root) -> Arc<Self> {
201        let actor = Self {
202            name,
203            root,
204            cached_events: Default::default(),
205            client_ready_to_receive_messages: false.into(),
206        };
207        registry.register::<Self>(actor)
208    }
209
210    fn script_chan(&self, registry: &ActorRegistry) -> GenericSender<DevtoolScriptControlMsg> {
211        match &self.root {
212            Root::BrowsingContext(browsing_context_name) => registry
213                .find::<BrowsingContextActor>(browsing_context_name)
214                .script_chan(),
215            Root::DedicatedWorker(worker_name) => registry
216                .find::<WorkerTargetActor>(worker_name)
217                .script_sender
218                .clone(),
219        }
220    }
221
222    fn current_unique_id(&self, registry: &ActorRegistry) -> UniqueId {
223        match &self.root {
224            Root::BrowsingContext(browsing_context_name) => UniqueId::Pipeline(
225                registry
226                    .find::<BrowsingContextActor>(browsing_context_name)
227                    .pipeline_id(),
228            ),
229            Root::DedicatedWorker(worker_name) => {
230                UniqueId::Worker(registry.find::<WorkerTargetActor>(worker_name).worker_id)
231            },
232        }
233    }
234
235    fn pipeline_id(&self, registry: &ActorRegistry) -> PipelineId {
236        // FIXME: Redesign messages so we don't have to fake pipeline ids when communicating with workers.
237        match self.current_unique_id(registry) {
238            UniqueId::Pipeline(p) => p,
239            UniqueId::Worker(_) => TEST_PIPELINE_ID,
240        }
241    }
242
243    fn evaluate_js(
244        &self,
245        registry: &ActorRegistry,
246        msg: &Map<String, Value>,
247    ) -> Result<EvaluateJSReply, ()> {
248        let input = msg.get("text").unwrap().as_str().unwrap().to_owned();
249        let eager = msg.get("eager").and_then(|v| v.as_bool()).unwrap_or(false);
250        let frame_actor_id = msg
251            .get("frameActor")
252            .and_then(|v| v.as_str())
253            .map(String::from);
254
255        let (chan, port) = generic_channel::channel().unwrap();
256        self.script_chan(registry)
257            .send(DevtoolScriptControlMsg::Eval(
258                input.clone(),
259                self.pipeline_id(registry),
260                frame_actor_id,
261                eager,
262                chan,
263            ))
264            .unwrap();
265
266        let eval_result = port.recv().map_err(|_| ())?;
267        let has_exception = eval_result.has_exception;
268        let (result, exception) = if has_exception {
269            (
270                Value::Null,
271                debugger_value_to_json(registry, eval_result.value),
272            )
273        } else {
274            (
275                debugger_value_to_json(registry, eval_result.value),
276                Value::Null,
277            )
278        };
279        let reply = EvaluateJSReply {
280            from: self.name().into(),
281            input,
282            result,
283            timestamp: get_time_stamp(),
284            exception,
285            exception_message: eval_result.exception_message,
286            has_exception,
287            helper_result: Value::Null,
288        };
289        Ok(reply)
290    }
291
292    pub(crate) fn handle_console_resource(
293        &self,
294        resource: ConsoleResource,
295        id: UniqueId,
296        registry: &ActorRegistry,
297        stream: &mut DevtoolsConnection,
298    ) {
299        self.cached_events
300            .borrow_mut()
301            .entry(id.clone())
302            .or_default()
303            .push(resource.clone());
304        if !self
305            .client_ready_to_receive_messages
306            .load(Ordering::Relaxed)
307        {
308            return;
309        }
310        let resource_type = resource.resource_type();
311        if id == self.current_unique_id(registry) &&
312            let Root::BrowsingContext(browsing_context_name) = &self.root
313        {
314            registry
315                .find::<BrowsingContextActor>(browsing_context_name)
316                .resource_array(
317                    resource,
318                    resource_type,
319                    ResourceArrayType::Available,
320                    stream,
321                )
322        };
323    }
324
325    pub(crate) fn send_clear_message(
326        &self,
327        id: UniqueId,
328        registry: &ActorRegistry,
329        stream: &mut DevtoolsConnection,
330    ) {
331        if id == self.current_unique_id(registry) &&
332            let Root::BrowsingContext(browsing_context_name) = &self.root
333        {
334            registry
335                .find::<BrowsingContextActor>(browsing_context_name)
336                .resource_array(
337                    ConsoleClearMessage {
338                        level: "clear".to_owned(),
339                    },
340                    "console-message".into(),
341                    ResourceArrayType::Available,
342                    stream,
343                )
344        };
345    }
346
347    pub(crate) fn get_cached_messages(
348        &self,
349        registry: &ActorRegistry,
350        resource: &str,
351    ) -> Vec<ConsoleResource> {
352        let id = self.current_unique_id(registry);
353        let cached_events = self.cached_events.borrow();
354        let Some(events) = cached_events.get(&id) else {
355            return vec![];
356        };
357        events
358            .iter()
359            .filter(|event| event.resource_type() == resource)
360            .cloned()
361            .collect()
362    }
363
364    pub(crate) fn received_first_message_from_client(&self) {
365        self.client_ready_to_receive_messages
366            .store(true, Ordering::Relaxed);
367    }
368
369    /// Matching function with conditional case-sensitivity as per:
370    /// <https://searchfox.org/firefox-main/rev/7fa9b602777418732e08b26d1ef6c9945c4bbd72/devtools/shared/webconsole/js-property-provider.js#617-622>
371    pub(crate) fn autocomplete_match(prefix: &str, identifier: &str) -> bool {
372        let is_insensitive = prefix.chars().next().is_some_and(|c| c.is_lowercase());
373        if is_insensitive {
374            identifier
375                .to_lowercase()
376                .starts_with(&prefix.to_lowercase())
377        } else {
378            identifier.starts_with(prefix)
379        }
380    }
381}
382
383impl Actor for ConsoleActor {
384    fn name(&self) -> &str {
385        &self.name
386    }
387
388    fn handle_message(
389        &self,
390        request: ClientRequest,
391        registry: &ActorRegistry,
392        msg_type: &str,
393        msg: &Map<String, Value>,
394        _id: StreamId,
395    ) -> Result<(), ActorError> {
396        match msg_type {
397            "clearMessagesCacheAsync" => {
398                self.cached_events
399                    .borrow_mut()
400                    .remove(&self.current_unique_id(registry));
401                let msg = EmptyReplyMsg {
402                    from: self.name().into(),
403                };
404                request.reply_final(&msg)?
405            },
406
407            "autocomplete" => {
408                let Some((tx, rx)) = channel() else {
409                    return Err(ActorError::Internal);
410                };
411
412                let env_request = if let Some(frame_actor) = msg
413                    .get("frameActor")
414                    .and_then(|v| v.as_str())
415                    .map(String::from)
416                {
417                    GetEnvironmentRequest::Frame(frame_actor)
418                } else {
419                    GetEnvironmentRequest::Global(self.pipeline_id(registry))
420                };
421
422                self.script_chan(registry)
423                    .send(DevtoolScriptControlMsg::GetEnvironment(env_request, tx))
424                    .map_err(|_| ActorError::Internal)?;
425
426                let environment_name = rx.recv().map_err(|_| ActorError::Internal)?;
427                let environment_actor = registry.find::<EnvironmentActor>(&environment_name);
428
429                let prompt = msg
430                    .get("text")
431                    .and_then(|v| v.as_str())
432                    .map(String::from)
433                    .ok_or(ActorError::Internal)?;
434
435                let identifiers = environment_actor.search_identifiers_recursive(registry, &prompt);
436
437                let matches: Vec<String> = identifiers
438                    .into_iter()
439                    .filter(|name| ConsoleActor::autocomplete_match(&prompt, name))
440                    .collect();
441
442                let msg = if matches.is_empty() {
443                    AutocompleteReply {
444                        from: self.name().into(),
445                        matches: vec![],
446                        match_prop: String::new(),
447                        is_element_access: false,
448                    }
449                } else {
450                    AutocompleteReply {
451                        from: self.name().into(),
452                        matches,
453                        match_prop: prompt,
454                        is_element_access: false,
455                    }
456                };
457                request.reply_final(&msg)?
458            },
459
460            "evaluateJS" => {
461                let msg = self.evaluate_js(registry, msg);
462                request.reply_final(&msg)?
463            },
464
465            "evaluateJSAsync" => {
466                let result_id = Uuid::new_v4().to_string();
467                let early_reply = EvaluateJSAsyncReply {
468                    from: self.name().into(),
469                    result_id: result_id.clone(),
470                };
471                // Emit an eager reply so that the client starts listening
472                // for an async event with the resultID
473                let mut stream = request.reply(&early_reply)?;
474
475                let reply = self.evaluate_js(registry, msg).unwrap();
476                let msg = EvaluateJSEvent {
477                    from: self.name().into(),
478                    type_: "evaluationResult".to_owned(),
479                    input: reply.input,
480                    result: reply.result,
481                    timestamp: reply.timestamp,
482                    result_id,
483                    exception: reply.exception,
484                    exception_message: reply.exception_message,
485                    has_exception: reply.has_exception,
486                    helper_result: reply.helper_result,
487                };
488                // Send the data from evaluateJS along with a resultID
489                stream.write_json_packet(&msg)?
490            },
491
492            "setPreferences" => {
493                let msg = SetPreferencesReply {
494                    from: self.name().into(),
495                    updated: vec![],
496                };
497                request.reply_final(&msg)?
498            },
499
500            // NOTE: Do not handle `startListeners`, it is a legacy API.
501            // Instead, enable the resource in `WatcherActor::supported_resources`
502            // and handle the messages there.
503            _ => return Err(ActorError::UnrecognizedPacketType),
504        };
505        Ok(())
506    }
507}