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::cell::RefCell;
10use std::collections::HashMap;
11use std::net::TcpStream;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use base::id::TEST_PIPELINE_ID;
15use devtools_traits::EvaluateJSReply::{
16    ActorValue, BooleanValue, NullValue, NumberValue, StringValue, VoidValue,
17};
18use devtools_traits::{
19    CachedConsoleMessage, CachedConsoleMessageTypes, ConsoleLog, ConsoleMessage,
20    DevtoolScriptControlMsg, PageError,
21};
22use ipc_channel::ipc::{self, IpcSender};
23use log::debug;
24use serde::Serialize;
25use serde_json::{self, Map, Number, Value};
26use uuid::Uuid;
27
28use crate::actor::{Actor, ActorError, ActorRegistry};
29use crate::actors::browsing_context::BrowsingContextActor;
30use crate::actors::object::ObjectActor;
31use crate::actors::worker::WorkerActor;
32use crate::protocol::{ClientRequest, JsonPacketStream};
33use crate::resource::{ResourceArrayType, ResourceAvailable};
34use crate::{StreamId, UniqueId};
35
36trait EncodableConsoleMessage {
37    fn encode(&self) -> serde_json::Result<String>;
38}
39
40impl EncodableConsoleMessage for CachedConsoleMessage {
41    fn encode(&self) -> serde_json::Result<String> {
42        match *self {
43            CachedConsoleMessage::PageError(ref a) => serde_json::to_string(a),
44            CachedConsoleMessage::ConsoleLog(ref a) => serde_json::to_string(a),
45        }
46    }
47}
48
49#[derive(Serialize)]
50struct StartedListenersTraits;
51
52#[derive(Serialize)]
53#[serde(rename_all = "camelCase")]
54struct StartedListenersReply {
55    from: String,
56    native_console_api: bool,
57    started_listeners: Vec<String>,
58    traits: StartedListenersTraits,
59}
60
61#[derive(Serialize)]
62struct GetCachedMessagesReply {
63    from: String,
64    messages: Vec<Map<String, Value>>,
65}
66
67#[derive(Serialize)]
68#[serde(rename_all = "camelCase")]
69struct StopListenersReply {
70    from: String,
71    stopped_listeners: Vec<String>,
72}
73
74#[derive(Serialize)]
75#[serde(rename_all = "camelCase")]
76struct AutocompleteReply {
77    from: String,
78    matches: Vec<String>,
79    match_prop: String,
80}
81
82#[derive(Serialize)]
83#[serde(rename_all = "camelCase")]
84struct EvaluateJSReply {
85    from: String,
86    input: String,
87    result: Value,
88    timestamp: u64,
89    exception: Value,
90    exception_message: Value,
91    helper_result: Value,
92}
93
94#[derive(Serialize)]
95#[serde(rename_all = "camelCase")]
96struct EvaluateJSEvent {
97    from: String,
98    #[serde(rename = "type")]
99    type_: String,
100    input: String,
101    result: Value,
102    timestamp: u64,
103    #[serde(rename = "resultID")]
104    result_id: String,
105    exception: Value,
106    exception_message: Value,
107    helper_result: Value,
108}
109
110#[derive(Serialize)]
111struct EvaluateJSAsyncReply {
112    from: String,
113    #[serde(rename = "resultID")]
114    result_id: String,
115}
116
117#[derive(Serialize)]
118struct SetPreferencesReply {
119    from: String,
120    updated: Vec<String>,
121}
122
123#[derive(Serialize)]
124#[serde(rename_all = "camelCase")]
125struct PageErrorWrapper {
126    page_error: PageError,
127}
128
129pub(crate) enum Root {
130    BrowsingContext(String),
131    DedicatedWorker(String),
132}
133
134pub(crate) struct ConsoleActor {
135    pub name: String,
136    pub root: Root,
137    pub cached_events: RefCell<HashMap<UniqueId, Vec<CachedConsoleMessage>>>,
138}
139
140impl ConsoleActor {
141    fn script_chan<'a>(
142        &self,
143        registry: &'a ActorRegistry,
144    ) -> &'a IpcSender<DevtoolScriptControlMsg> {
145        match &self.root {
146            Root::BrowsingContext(bc) => &registry.find::<BrowsingContextActor>(bc).script_chan,
147            Root::DedicatedWorker(worker) => &registry.find::<WorkerActor>(worker).script_chan,
148        }
149    }
150
151    fn current_unique_id(&self, registry: &ActorRegistry) -> UniqueId {
152        match &self.root {
153            Root::BrowsingContext(bc) => UniqueId::Pipeline(
154                registry
155                    .find::<BrowsingContextActor>(bc)
156                    .active_pipeline_id
157                    .get(),
158            ),
159            Root::DedicatedWorker(w) => UniqueId::Worker(registry.find::<WorkerActor>(w).worker_id),
160        }
161    }
162
163    fn evaluate_js(
164        &self,
165        registry: &ActorRegistry,
166        msg: &Map<String, Value>,
167    ) -> Result<EvaluateJSReply, ()> {
168        let input = msg.get("text").unwrap().as_str().unwrap().to_owned();
169        let (chan, port) = ipc::channel().unwrap();
170        // FIXME: Redesign messages so we don't have to fake pipeline ids when
171        //        communicating with workers.
172        let pipeline = match self.current_unique_id(registry) {
173            UniqueId::Pipeline(p) => p,
174            UniqueId::Worker(_) => TEST_PIPELINE_ID,
175        };
176        self.script_chan(registry)
177            .send(DevtoolScriptControlMsg::EvaluateJS(
178                pipeline,
179                input.clone(),
180                chan,
181            ))
182            .unwrap();
183
184        // TODO: Extract conversion into protocol module or some other useful place
185        let result = match port.recv().map_err(|_| ())? {
186            VoidValue => {
187                let mut m = Map::new();
188                m.insert("type".to_owned(), Value::String("undefined".to_owned()));
189                Value::Object(m)
190            },
191            NullValue => {
192                let mut m = Map::new();
193                m.insert("type".to_owned(), Value::String("null".to_owned()));
194                Value::Object(m)
195            },
196            BooleanValue(val) => Value::Bool(val),
197            NumberValue(val) => {
198                if val.is_nan() {
199                    let mut m = Map::new();
200                    m.insert("type".to_owned(), Value::String("NaN".to_owned()));
201                    Value::Object(m)
202                } else if val.is_infinite() {
203                    let mut m = Map::new();
204                    if val < 0. {
205                        m.insert("type".to_owned(), Value::String("-Infinity".to_owned()));
206                    } else {
207                        m.insert("type".to_owned(), Value::String("Infinity".to_owned()));
208                    }
209                    Value::Object(m)
210                } else if val == 0. && val.is_sign_negative() {
211                    let mut m = Map::new();
212                    m.insert("type".to_owned(), Value::String("-0".to_owned()));
213                    Value::Object(m)
214                } else {
215                    Value::Number(Number::from_f64(val).unwrap())
216                }
217            },
218            StringValue(s) => Value::String(s),
219            ActorValue { class, uuid } => {
220                // TODO: Make initial ActorValue message include these properties?
221                let mut m = Map::new();
222                let actor = ObjectActor::register(registry, uuid);
223
224                m.insert("type".to_owned(), Value::String("object".to_owned()));
225                m.insert("class".to_owned(), Value::String(class));
226                m.insert("actor".to_owned(), Value::String(actor));
227                m.insert("extensible".to_owned(), Value::Bool(true));
228                m.insert("frozen".to_owned(), Value::Bool(false));
229                m.insert("sealed".to_owned(), Value::Bool(false));
230                Value::Object(m)
231            },
232        };
233
234        // TODO: Catch and return exception values from JS evaluation
235        let reply = EvaluateJSReply {
236            from: self.name(),
237            input,
238            result,
239            timestamp: SystemTime::now()
240                .duration_since(UNIX_EPOCH)
241                .unwrap_or_default()
242                .as_millis() as u64,
243            exception: Value::Null,
244            exception_message: Value::Null,
245            helper_result: Value::Null,
246        };
247        std::result::Result::Ok(reply)
248    }
249
250    pub(crate) fn handle_page_error(
251        &self,
252        page_error: PageError,
253        id: UniqueId,
254        registry: &ActorRegistry,
255        stream: &mut TcpStream,
256    ) {
257        self.cached_events
258            .borrow_mut()
259            .entry(id.clone())
260            .or_default()
261            .push(CachedConsoleMessage::PageError(page_error.clone()));
262        if id == self.current_unique_id(registry) {
263            if let Root::BrowsingContext(bc) = &self.root {
264                registry.find::<BrowsingContextActor>(bc).resource_array(
265                    PageErrorWrapper { page_error },
266                    "error-message".into(),
267                    ResourceArrayType::Available,
268                    stream,
269                )
270            };
271        }
272    }
273
274    pub(crate) fn handle_console_api(
275        &self,
276        console_message: ConsoleMessage,
277        id: UniqueId,
278        registry: &ActorRegistry,
279        stream: &mut TcpStream,
280    ) {
281        let log_message: ConsoleLog = console_message.into();
282        self.cached_events
283            .borrow_mut()
284            .entry(id.clone())
285            .or_default()
286            .push(CachedConsoleMessage::ConsoleLog(log_message.clone()));
287        if id == self.current_unique_id(registry) {
288            if let Root::BrowsingContext(bc) = &self.root {
289                registry.find::<BrowsingContextActor>(bc).resource_array(
290                    log_message,
291                    "console-message".into(),
292                    ResourceArrayType::Available,
293                    stream,
294                )
295            };
296        }
297    }
298}
299
300impl Actor for ConsoleActor {
301    fn name(&self) -> String {
302        self.name.clone()
303    }
304
305    fn handle_message(
306        &self,
307        request: ClientRequest,
308        registry: &ActorRegistry,
309        msg_type: &str,
310        msg: &Map<String, Value>,
311        _id: StreamId,
312    ) -> Result<(), ActorError> {
313        match msg_type {
314            "clearMessagesCache" => {
315                self.cached_events
316                    .borrow_mut()
317                    .remove(&self.current_unique_id(registry));
318                // FIXME: need to send a reply here!
319                return Err(ActorError::UnrecognizedPacketType);
320            },
321
322            "getCachedMessages" => {
323                let str_types = msg
324                    .get("messageTypes")
325                    .unwrap()
326                    .as_array()
327                    .unwrap()
328                    .iter()
329                    .map(|json_type| json_type.as_str().unwrap());
330                let mut message_types = CachedConsoleMessageTypes::empty();
331                for str_type in str_types {
332                    match str_type {
333                        "PageError" => message_types.insert(CachedConsoleMessageTypes::PAGE_ERROR),
334                        "ConsoleAPI" => {
335                            message_types.insert(CachedConsoleMessageTypes::CONSOLE_API)
336                        },
337                        s => debug!("unrecognized message type requested: \"{}\"", s),
338                    };
339                }
340                let mut messages = vec![];
341                for event in self
342                    .cached_events
343                    .borrow()
344                    .get(&self.current_unique_id(registry))
345                    .unwrap_or(&vec![])
346                    .iter()
347                {
348                    let include = match event {
349                        CachedConsoleMessage::PageError(_)
350                            if message_types.contains(CachedConsoleMessageTypes::PAGE_ERROR) =>
351                        {
352                            true
353                        },
354                        CachedConsoleMessage::ConsoleLog(_)
355                            if message_types.contains(CachedConsoleMessageTypes::CONSOLE_API) =>
356                        {
357                            true
358                        },
359                        _ => false,
360                    };
361                    if include {
362                        let json_string = event.encode().unwrap();
363                        let json = serde_json::from_str::<Value>(&json_string).unwrap();
364                        messages.push(json.as_object().unwrap().to_owned())
365                    }
366                }
367
368                let msg = GetCachedMessagesReply {
369                    from: self.name(),
370                    messages,
371                };
372                request.reply_final(&msg)?
373            },
374
375            "startListeners" => {
376                // TODO: actually implement listener filters that support starting/stopping
377                let listeners = msg.get("listeners").unwrap().as_array().unwrap().to_owned();
378                let msg = StartedListenersReply {
379                    from: self.name(),
380                    native_console_api: true,
381                    started_listeners: listeners
382                        .into_iter()
383                        .map(|s| s.as_str().unwrap().to_owned())
384                        .collect(),
385                    traits: StartedListenersTraits,
386                };
387                request.reply_final(&msg)?
388            },
389
390            "stopListeners" => {
391                // TODO: actually implement listener filters that support starting/stopping
392                let msg = StopListenersReply {
393                    from: self.name(),
394                    stopped_listeners: msg
395                        .get("listeners")
396                        .unwrap()
397                        .as_array()
398                        .unwrap_or(&vec![])
399                        .iter()
400                        .map(|listener| listener.as_str().unwrap().to_owned())
401                        .collect(),
402                };
403                request.reply_final(&msg)?
404            },
405
406            // TODO: implement autocompletion like onAutocomplete in
407            //      http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/webconsole.js
408            "autocomplete" => {
409                let msg = AutocompleteReply {
410                    from: self.name(),
411                    matches: vec![],
412                    match_prop: "".to_owned(),
413                };
414                request.reply_final(&msg)?
415            },
416
417            "evaluateJS" => {
418                let msg = self.evaluate_js(registry, msg);
419                request.reply_final(&msg)?
420            },
421
422            "evaluateJSAsync" => {
423                let result_id = Uuid::new_v4().to_string();
424                let early_reply = EvaluateJSAsyncReply {
425                    from: self.name(),
426                    result_id: result_id.clone(),
427                };
428                // Emit an eager reply so that the client starts listening
429                // for an async event with the resultID
430                let stream = request.reply(&early_reply)?;
431
432                if msg.get("eager").and_then(|v| v.as_bool()).unwrap_or(false) {
433                    // We don't support the side-effect free evaluation that eager evalaution
434                    // really needs.
435                    return Ok(());
436                }
437
438                let reply = self.evaluate_js(registry, msg).unwrap();
439                let msg = EvaluateJSEvent {
440                    from: self.name(),
441                    type_: "evaluationResult".to_owned(),
442                    input: reply.input,
443                    result: reply.result,
444                    timestamp: reply.timestamp,
445                    result_id,
446                    exception: reply.exception,
447                    exception_message: reply.exception_message,
448                    helper_result: reply.helper_result,
449                };
450                // Send the data from evaluateJS along with a resultID
451                stream.write_json_packet(&msg)?
452            },
453
454            "setPreferences" => {
455                let msg = SetPreferencesReply {
456                    from: self.name(),
457                    updated: vec![],
458                };
459                request.reply_final(&msg)?
460            },
461
462            _ => return Err(ActorError::UnrecognizedPacketType),
463        };
464        Ok(())
465    }
466}