script/dom/
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
5use std::convert::TryFrom;
6use std::ptr::{self, NonNull};
7use std::slice;
8
9use devtools_traits::{
10    ConsoleMessage, ConsoleMessageArgument, ConsoleMessageBuilder, LogLevel,
11    ScriptToDevtoolsControlMsg, StackFrame,
12};
13use js::conversions::jsstr_to_string;
14use js::jsapi::{self, ESClass, PropertyDescriptor};
15use js::jsval::{Int32Value, UndefinedValue};
16use js::rust::wrappers::{
17    GetBuiltinClass, GetPropertyKeys, JS_GetOwnPropertyDescriptorById, JS_GetPropertyById,
18    JS_IdToValue, JS_Stringify, JS_ValueToSource,
19};
20use js::rust::{
21    CapturedJSStack, HandleObject, HandleValue, IdVector, ToString, describe_scripted_caller,
22};
23use script_bindings::conversions::get_dom_class;
24
25use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
26use crate::dom::bindings::inheritance::Castable;
27use crate::dom::bindings::str::DOMString;
28use crate::dom::globalscope::GlobalScope;
29use crate::dom::workerglobalscope::WorkerGlobalScope;
30use crate::script_runtime::JSContext;
31
32/// The maximum object depth logged by console methods.
33const MAX_LOG_DEPTH: usize = 10;
34/// The maximum elements in an object logged by console methods.
35const MAX_LOG_CHILDREN: usize = 15;
36
37/// <https://developer.mozilla.org/en-US/docs/Web/API/Console>
38#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
39pub(crate) struct Console;
40
41impl Console {
42    #[allow(unsafe_code)]
43    fn build_message(level: LogLevel) -> ConsoleMessageBuilder {
44        let cx = GlobalScope::get_cx();
45        let caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default();
46
47        ConsoleMessageBuilder::new(level, caller.filename, caller.line, caller.col)
48    }
49
50    /// Helper to send a message that only consists of a single string to log,
51    /// console and stdout
52    fn send_string_message(global: &GlobalScope, level: LogLevel, message: String) {
53        let s = DOMString::from(message.clone());
54        log!(level.clone().into(), "{}", &s);
55        console_message_to_stdout(global, &s);
56
57        let mut builder = Self::build_message(level);
58        builder.add_argument(message.into());
59        let log_message = builder.finish();
60
61        Self::send_to_devtools(global, log_message);
62    }
63
64    fn method(
65        global: &GlobalScope,
66        level: LogLevel,
67        messages: Vec<HandleValue>,
68        include_stacktrace: IncludeStackTrace,
69    ) {
70        let cx = GlobalScope::get_cx();
71
72        let mut log: ConsoleMessageBuilder = Console::build_message(level.clone());
73        for message in &messages {
74            log.add_argument(console_argument_from_handle_value(cx, *message));
75        }
76
77        if include_stacktrace == IncludeStackTrace::Yes {
78            log.attach_stack_trace(get_js_stack(*GlobalScope::get_cx()));
79        }
80
81        Console::send_to_devtools(global, log.finish());
82
83        let msgs = stringify_handle_values(&messages);
84        // Also log messages to stdout
85        console_message_to_stdout(global, &msgs);
86
87        // Also output to the logger which will be at script::dom::console
88        log!(level.into(), "{}", &msgs);
89    }
90
91    fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
92        if let Some(chan) = global.devtools_chan() {
93            let worker_id = global
94                .downcast::<WorkerGlobalScope>()
95                .map(|worker| worker.get_worker_id());
96            let devtools_message =
97                ScriptToDevtoolsControlMsg::ConsoleAPI(global.pipeline_id(), message, worker_id);
98            chan.send(devtools_message).unwrap();
99        }
100    }
101
102    // Directly logs a DOMString, without processing the message
103    pub(crate) fn internal_warn(global: &GlobalScope, message: DOMString) {
104        Console::send_string_message(global, LogLevel::Warn, String::from(message.clone()));
105    }
106}
107
108// In order to avoid interleaving the stdout output of the Console API methods
109// with stderr that could be in use on other threads, we lock stderr until
110// we're finished with stdout. Since the stderr lock is reentrant, there is
111// no risk of deadlock if the callback ends up trying to write to stderr for
112// any reason.
113#[cfg(not(any(target_os = "android", target_env = "ohos")))]
114fn with_stderr_lock<F>(f: F)
115where
116    F: FnOnce(),
117{
118    use std::io;
119    let stderr = io::stderr();
120    let _handle = stderr.lock();
121    f()
122}
123
124#[allow(unsafe_code)]
125unsafe fn handle_value_to_string(cx: *mut jsapi::JSContext, value: HandleValue) -> DOMString {
126    rooted!(in(cx) let mut js_string = std::ptr::null_mut::<jsapi::JSString>());
127    match std::ptr::NonNull::new(JS_ValueToSource(cx, value)) {
128        Some(js_str) => {
129            js_string.set(js_str.as_ptr());
130            DOMString::from_string(jsstr_to_string(cx, js_str))
131        },
132        None => "<error converting value to string>".into(),
133    }
134}
135
136#[allow(unsafe_code)]
137fn console_argument_from_handle_value(
138    cx: JSContext,
139    handle_value: HandleValue,
140) -> ConsoleMessageArgument {
141    if handle_value.is_string() {
142        let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
143        let dom_string = unsafe { jsstr_to_string(*cx, js_string) };
144        return ConsoleMessageArgument::String(dom_string);
145    }
146
147    if handle_value.is_int32() {
148        let integer = handle_value.to_int32();
149        return ConsoleMessageArgument::Integer(integer);
150    }
151
152    if handle_value.is_number() {
153        let number = handle_value.to_number();
154        return ConsoleMessageArgument::Number(number);
155    }
156
157    // FIXME: Handle more complex argument types here
158    let stringified_value = stringify_handle_value(handle_value);
159    ConsoleMessageArgument::String(stringified_value.into())
160}
161
162#[allow(unsafe_code)]
163fn stringify_handle_value(message: HandleValue) -> DOMString {
164    let cx = GlobalScope::get_cx();
165    unsafe {
166        if message.is_string() {
167            let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
168            return DOMString::from_string(jsstr_to_string(*cx, jsstr));
169        }
170        unsafe fn stringify_object_from_handle_value(
171            cx: *mut jsapi::JSContext,
172            value: HandleValue,
173            parents: Vec<u64>,
174        ) -> DOMString {
175            rooted!(in(cx) let mut obj = value.to_object());
176            let mut object_class = ESClass::Other;
177            if !GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) {
178                return DOMString::from("/* invalid */");
179            }
180            let mut ids = IdVector::new(cx);
181            if !GetPropertyKeys(
182                cx,
183                obj.handle(),
184                jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
185                ids.handle_mut(),
186            ) {
187                return DOMString::from("/* invalid */");
188            }
189            let truncate = ids.len() > MAX_LOG_CHILDREN;
190            if object_class != ESClass::Array && object_class != ESClass::Object {
191                if truncate {
192                    return DOMString::from("…");
193                } else {
194                    return handle_value_to_string(cx, value);
195                }
196            }
197
198            let mut explicit_keys = object_class == ESClass::Object;
199            let mut props = Vec::with_capacity(ids.len());
200            for id in ids.iter().take(MAX_LOG_CHILDREN) {
201                rooted!(in(cx) let id = *id);
202                rooted!(in(cx) let mut desc = PropertyDescriptor::default());
203
204                let mut is_none = false;
205                if !JS_GetOwnPropertyDescriptorById(
206                    cx,
207                    obj.handle(),
208                    id.handle(),
209                    desc.handle_mut(),
210                    &mut is_none,
211                ) {
212                    return DOMString::from("/* invalid */");
213                }
214
215                rooted!(in(cx) let mut property = UndefinedValue());
216                if !JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut()) {
217                    return DOMString::from("/* invalid */");
218                }
219
220                if !explicit_keys {
221                    if id.is_int() {
222                        if let Ok(id_int) = usize::try_from(id.to_int()) {
223                            explicit_keys = props.len() != id_int;
224                        } else {
225                            explicit_keys = false;
226                        }
227                    } else {
228                        explicit_keys = false;
229                    }
230                }
231                let value_string =
232                    stringify_inner(JSContext::from_ptr(cx), property.handle(), parents.clone());
233                if explicit_keys {
234                    let key = if id.is_string() || id.is_symbol() || id.is_int() {
235                        rooted!(in(cx) let mut key_value = UndefinedValue());
236                        let raw_id: jsapi::HandleId = id.handle().into();
237                        if !JS_IdToValue(cx, *raw_id.ptr, key_value.handle_mut()) {
238                            return DOMString::from("/* invalid */");
239                        }
240                        handle_value_to_string(cx, key_value.handle())
241                    } else {
242                        return DOMString::from("/* invalid */");
243                    };
244                    props.push(format!("{}: {}", key, value_string,));
245                } else {
246                    props.push(value_string.to_string());
247                }
248            }
249            if truncate {
250                props.push("…".to_string());
251            }
252            if object_class == ESClass::Array {
253                DOMString::from(format!("[{}]", itertools::join(props, ", ")))
254            } else {
255                DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
256            }
257        }
258        fn stringify_inner(cx: JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
259            if parents.len() >= MAX_LOG_DEPTH {
260                return DOMString::from("...");
261            }
262            let value_bits = value.asBits_;
263            if parents.contains(&value_bits) {
264                return DOMString::from("[circular]");
265            }
266            if value.is_undefined() {
267                // This produces a better value than "(void 0)" from JS_ValueToSource.
268                return DOMString::from("undefined");
269            } else if !value.is_object() {
270                return unsafe { handle_value_to_string(*cx, value) };
271            }
272            parents.push(value_bits);
273
274            if value.is_object() {
275                if let Some(repr) = maybe_stringify_dom_object(cx, value) {
276                    return repr;
277                }
278            }
279            unsafe { stringify_object_from_handle_value(*cx, value, parents) }
280        }
281        stringify_inner(cx, message, Vec::new())
282    }
283}
284
285#[allow(unsafe_code)]
286fn maybe_stringify_dom_object(cx: JSContext, value: HandleValue) -> Option<DOMString> {
287    // The standard object serialization is not effective for DOM objects,
288    // since their properties generally live on the prototype object.
289    // Instead, fall back to the output of JSON.stringify combined
290    // with the class name extracted from the output of toString().
291    rooted!(in(*cx) let obj = value.to_object());
292    let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
293    if !is_dom_class {
294        return None;
295    }
296    rooted!(in(*cx) let class_name = unsafe { ToString(*cx, value) });
297    let Some(class_name) = NonNull::new(class_name.get()) else {
298        return Some("<error converting DOM object to string>".into());
299    };
300    let class_name = unsafe {
301        jsstr_to_string(*cx, class_name)
302            .replace("[object ", "")
303            .replace("]", "")
304    };
305    let mut repr = format!("{} ", class_name);
306    rooted!(in(*cx) let mut value = value.get());
307
308    #[allow(unsafe_code)]
309    unsafe extern "C" fn stringified(
310        string: *const u16,
311        len: u32,
312        data: *mut std::ffi::c_void,
313    ) -> bool {
314        let s = data as *mut String;
315        let string_chars = slice::from_raw_parts(string, len as usize);
316        (*s).push_str(&String::from_utf16_lossy(string_chars));
317        true
318    }
319
320    rooted!(in(*cx) let space = Int32Value(2));
321    let stringify_result = unsafe {
322        JS_Stringify(
323            *cx,
324            value.handle_mut(),
325            HandleObject::null(),
326            space.handle(),
327            Some(stringified),
328            &mut repr as *mut String as *mut _,
329        )
330    };
331    if !stringify_result {
332        return Some("<error converting DOM object to string>".into());
333    }
334    Some(repr.into())
335}
336
337fn stringify_handle_values(messages: &[HandleValue]) -> DOMString {
338    DOMString::from(itertools::join(
339        messages.iter().copied().map(stringify_handle_value),
340        " ",
341    ))
342}
343
344/// On OHOS/ Android, stdout and stderr will be redirected to go
345/// to the logger. As `Console::method` and `Console::send_string_message`
346/// already forwards all messages to the logger with appropriate level
347/// this does not need to do anything for these targets.
348#[allow(unused_variables)]
349fn console_message_to_stdout(global: &GlobalScope, message: &DOMString) {
350    #[cfg(not(any(target_os = "android", target_env = "ohos")))]
351    {
352        let prefix = global.current_group_label().unwrap_or_default();
353        let formatted_message = format!("{}{}", prefix, message);
354        with_stderr_lock(move || {
355            println!("{}", formatted_message);
356        });
357    }
358}
359
360#[derive(Debug, Eq, PartialEq)]
361enum IncludeStackTrace {
362    Yes,
363    No,
364}
365
366impl consoleMethods<crate::DomTypeHolder> for Console {
367    // https://developer.mozilla.org/en-US/docs/Web/API/Console/log
368    fn Log(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
369        Console::method(global, LogLevel::Log, messages, IncludeStackTrace::No);
370    }
371
372    // https://developer.mozilla.org/en-US/docs/Web/API/Console/clear
373    fn Clear(global: &GlobalScope) {
374        let message = Console::build_message(LogLevel::Clear).finish();
375        Console::send_to_devtools(global, message);
376    }
377
378    // https://developer.mozilla.org/en-US/docs/Web/API/Console
379    fn Debug(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
380        Console::method(global, LogLevel::Debug, messages, IncludeStackTrace::No);
381    }
382
383    // https://developer.mozilla.org/en-US/docs/Web/API/Console/info
384    fn Info(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
385        Console::method(global, LogLevel::Info, messages, IncludeStackTrace::No);
386    }
387
388    // https://developer.mozilla.org/en-US/docs/Web/API/Console/warn
389    fn Warn(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
390        Console::method(global, LogLevel::Warn, messages, IncludeStackTrace::No);
391    }
392
393    // https://developer.mozilla.org/en-US/docs/Web/API/Console/error
394    fn Error(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
395        Console::method(global, LogLevel::Error, messages, IncludeStackTrace::No);
396    }
397
398    /// <https://console.spec.whatwg.org/#trace>
399    fn Trace(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
400        Console::method(global, LogLevel::Trace, messages, IncludeStackTrace::Yes);
401    }
402
403    // https://developer.mozilla.org/en-US/docs/Web/API/Console/assert
404    fn Assert(_cx: JSContext, global: &GlobalScope, condition: bool, messages: Vec<HandleValue>) {
405        if !condition {
406            let message = format!("Assertion failed: {}", stringify_handle_values(&messages));
407
408            Console::send_string_message(global, LogLevel::Log, message.clone());
409        }
410    }
411
412    // https://console.spec.whatwg.org/#time
413    fn Time(global: &GlobalScope, label: DOMString) {
414        if let Ok(()) = global.time(label.clone()) {
415            let message = format!("{label}: timer started");
416            Console::send_string_message(global, LogLevel::Log, message.clone());
417        }
418    }
419
420    // https://console.spec.whatwg.org/#timelog
421    fn TimeLog(_cx: JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
422        if let Ok(delta) = global.time_log(&label) {
423            let message = format!("{label}: {delta}ms {}", stringify_handle_values(&data));
424
425            Console::send_string_message(global, LogLevel::Log, message.clone());
426        }
427    }
428
429    // https://console.spec.whatwg.org/#timeend
430    fn TimeEnd(global: &GlobalScope, label: DOMString) {
431        if let Ok(delta) = global.time_end(&label) {
432            let message = format!("{label}: {delta}ms");
433
434            Console::send_string_message(global, LogLevel::Log, message.clone());
435        }
436    }
437
438    // https://console.spec.whatwg.org/#group
439    fn Group(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
440        global.push_console_group(stringify_handle_values(&messages));
441    }
442
443    // https://console.spec.whatwg.org/#groupcollapsed
444    fn GroupCollapsed(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
445        global.push_console_group(stringify_handle_values(&messages));
446    }
447
448    // https://console.spec.whatwg.org/#groupend
449    fn GroupEnd(global: &GlobalScope) {
450        global.pop_console_group();
451    }
452
453    /// <https://console.spec.whatwg.org/#count>
454    fn Count(global: &GlobalScope, label: DOMString) {
455        let count = global.increment_console_count(&label);
456        let message = format!("{label}: {count}");
457
458        Console::send_string_message(global, LogLevel::Log, message.clone());
459    }
460
461    /// <https://console.spec.whatwg.org/#countreset>
462    fn CountReset(global: &GlobalScope, label: DOMString) {
463        if global.reset_console_count(&label).is_err() {
464            Self::internal_warn(
465                global,
466                DOMString::from(format!("Counter “{label}” doesn’t exist.")),
467            )
468        }
469    }
470}
471
472#[allow(unsafe_code)]
473fn get_js_stack(cx: *mut jsapi::JSContext) -> Vec<StackFrame> {
474    const MAX_FRAME_COUNT: u32 = 128;
475
476    let mut frames = vec![];
477    rooted!(in(cx) let mut handle =  ptr::null_mut());
478    let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
479    let Some(captured_js_stack) = captured_js_stack else {
480        return frames;
481    };
482
483    captured_js_stack.for_each_stack_frame(|frame| {
484        rooted!(in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
485
486        // Get function name
487        unsafe {
488            jsapi::GetSavedFrameFunctionDisplayName(
489                cx,
490                ptr::null_mut(),
491                frame.into(),
492                result.handle_mut().into(),
493                jsapi::SavedFrameSelfHosted::Include,
494            );
495        }
496        let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
497            unsafe { jsstr_to_string(cx, nonnull_result) }
498        } else {
499            "<anonymous>".into()
500        };
501
502        // Get source file name
503        result.set(ptr::null_mut());
504        unsafe {
505            jsapi::GetSavedFrameSource(
506                cx,
507                ptr::null_mut(),
508                frame.into(),
509                result.handle_mut().into(),
510                jsapi::SavedFrameSelfHosted::Include,
511            );
512        }
513        let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
514            unsafe { jsstr_to_string(cx, nonnull_result) }
515        } else {
516            "<anonymous>".into()
517        };
518
519        // get line/column number
520        let mut line_number = 0;
521        unsafe {
522            jsapi::GetSavedFrameLine(
523                cx,
524                ptr::null_mut(),
525                frame.into(),
526                &mut line_number,
527                jsapi::SavedFrameSelfHosted::Include,
528            );
529        }
530
531        let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
532        unsafe {
533            jsapi::GetSavedFrameColumn(
534                cx,
535                ptr::null_mut(),
536                frame.into(),
537                &mut column_number,
538                jsapi::SavedFrameSelfHosted::Include,
539            );
540        }
541        let frame = StackFrame {
542            filename,
543            function_name,
544            line_number,
545            column_number: column_number.value_,
546        };
547
548        frames.push(frame);
549    });
550
551    frames
552}