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