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