no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / xpcom / base / nsConsoleService.cpp
blobe6b17a6571bad10770211ebefbe948a59d0d3427
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /*
8 * Maintains a circular buffer of recent messages, and notifies
9 * listeners when new messages are logged.
12 /* Threadsafe. */
14 #include "nsCOMArray.h"
15 #include "nsThreadUtils.h"
17 #include "nsConsoleService.h"
18 #include "nsConsoleMessage.h"
19 #include "nsIClassInfoImpl.h"
20 #include "nsIConsoleListener.h"
21 #include "nsIObserverService.h"
22 #include "nsPrintfCString.h"
23 #include "nsProxyRelease.h"
24 #include "nsIScriptError.h"
25 #include "nsISupportsPrimitives.h"
26 #include "js/friend/ErrorMessages.h"
27 #include "mozilla/dom/WindowGlobalParent.h"
28 #include "mozilla/dom/ContentParent.h"
29 #include "mozilla/dom/BrowserParent.h"
30 #include "mozilla/dom/ScriptSettings.h"
32 #include "mozilla/SchedulerGroup.h"
33 #include "mozilla/Services.h"
35 #if defined(ANDROID)
36 # include <android/log.h>
37 # include "mozilla/dom/ContentChild.h"
38 # include "mozilla/StaticPrefs_consoleservice.h"
39 #endif
40 #ifdef XP_WIN
41 # include <windows.h>
42 #endif
44 using namespace mozilla;
46 NS_IMPL_ADDREF(nsConsoleService)
47 NS_IMPL_RELEASE(nsConsoleService)
48 NS_IMPL_CLASSINFO(nsConsoleService, nullptr,
49 nsIClassInfo::THREADSAFE | nsIClassInfo::SINGLETON,
50 NS_CONSOLESERVICE_CID)
51 NS_IMPL_QUERY_INTERFACE_CI(nsConsoleService, nsIConsoleService, nsIObserver)
52 NS_IMPL_CI_INTERFACE_GETTER(nsConsoleService, nsIConsoleService, nsIObserver)
54 static const bool gLoggingEnabled = true;
55 static const bool gLoggingBuffered = true;
56 #ifdef XP_WIN
57 static bool gLoggingToDebugger = true;
58 #endif // XP_WIN
60 nsConsoleService::MessageElement::~MessageElement() = default;
62 nsConsoleService::nsConsoleService()
63 : mCurrentSize(0),
64 // XXX grab this from a pref!
65 // hm, but worry about circularity, bc we want to be able to report
66 // prefs errs...
67 mMaximumSize(250),
68 mDeliveringMessage(false),
69 mLock("nsConsoleService.mLock") {
70 #ifdef XP_WIN
71 // This environment variable controls whether the console service
72 // should be prevented from putting output to the attached debugger.
73 // It only affects the Windows platform.
75 // To disable OutputDebugString, set:
76 // MOZ_CONSOLESERVICE_DISABLE_DEBUGGER_OUTPUT=1
78 const char* disableDebugLoggingVar =
79 getenv("MOZ_CONSOLESERVICE_DISABLE_DEBUGGER_OUTPUT");
80 gLoggingToDebugger =
81 !disableDebugLoggingVar || (disableDebugLoggingVar[0] == '0');
82 #endif // XP_WIN
85 void nsConsoleService::ClearMessagesForWindowID(const uint64_t innerID) {
86 MOZ_RELEASE_ASSERT(NS_IsMainThread());
87 MutexAutoLock lock(mLock);
89 for (MessageElement* e = mMessages.getFirst(); e != nullptr;) {
90 // Only messages implementing nsIScriptError interface expose the
91 // inner window ID.
92 nsCOMPtr<nsIScriptError> scriptError = do_QueryInterface(e->Get());
93 if (!scriptError) {
94 e = e->getNext();
95 continue;
97 uint64_t innerWindowID;
98 nsresult rv = scriptError->GetInnerWindowID(&innerWindowID);
99 if (NS_FAILED(rv) || innerWindowID != innerID) {
100 e = e->getNext();
101 continue;
104 MessageElement* next = e->getNext();
105 e->remove();
106 delete e;
107 mCurrentSize--;
108 MOZ_ASSERT(mCurrentSize < mMaximumSize);
110 e = next;
114 void nsConsoleService::ClearMessages() {
115 // NB: A lock is not required here as it's only called from |Reset| which
116 // locks for us and from the dtor.
117 while (!mMessages.isEmpty()) {
118 MessageElement* e = mMessages.popFirst();
119 delete e;
121 mCurrentSize = 0;
124 nsConsoleService::~nsConsoleService() {
125 MOZ_RELEASE_ASSERT(NS_IsMainThread());
127 ClearMessages();
130 class AddConsolePrefWatchers : public Runnable {
131 public:
132 explicit AddConsolePrefWatchers(nsConsoleService* aConsole)
133 : mozilla::Runnable("AddConsolePrefWatchers"), mConsole(aConsole) {}
135 NS_IMETHOD Run() override {
136 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
137 MOZ_ASSERT(obs);
138 obs->AddObserver(mConsole, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
139 obs->AddObserver(mConsole, "inner-window-destroyed", false);
141 if (!gLoggingBuffered) {
142 mConsole->Reset();
144 return NS_OK;
147 private:
148 RefPtr<nsConsoleService> mConsole;
151 nsresult nsConsoleService::Init() {
152 NS_DispatchToMainThread(new AddConsolePrefWatchers(this));
154 return NS_OK;
157 nsresult nsConsoleService::MaybeForwardScriptError(nsIConsoleMessage* aMessage,
158 bool* sent) {
159 *sent = false;
161 nsCOMPtr<nsIScriptError> scriptError = do_QueryInterface(aMessage);
162 if (!scriptError) {
163 // Not an nsIScriptError
164 return NS_OK;
167 uint64_t windowID;
168 nsresult rv;
169 rv = scriptError->GetInnerWindowID(&windowID);
170 NS_ENSURE_SUCCESS(rv, rv);
171 if (!windowID) {
172 // Does not set window id
173 return NS_OK;
176 RefPtr<mozilla::dom::WindowGlobalParent> windowGlobalParent =
177 mozilla::dom::WindowGlobalParent::GetByInnerWindowId(windowID);
178 if (!windowGlobalParent) {
179 // Could not find parent window by id
180 return NS_OK;
183 RefPtr<mozilla::dom::BrowserParent> browserParent =
184 windowGlobalParent->GetBrowserParent();
185 if (!browserParent) {
186 return NS_OK;
189 mozilla::dom::ContentParent* contentParent = browserParent->Manager();
190 if (!contentParent) {
191 return NS_ERROR_FAILURE;
194 nsAutoString msg, sourceName, sourceLine;
195 nsCString category;
196 uint32_t lineNum, colNum, flags;
197 uint64_t innerWindowId;
198 bool fromPrivateWindow, fromChromeContext;
200 rv = scriptError->GetErrorMessage(msg);
201 NS_ENSURE_SUCCESS(rv, rv);
202 rv = scriptError->GetSourceName(sourceName);
203 NS_ENSURE_SUCCESS(rv, rv);
204 rv = scriptError->GetSourceLine(sourceLine);
205 NS_ENSURE_SUCCESS(rv, rv);
207 rv = scriptError->GetCategory(getter_Copies(category));
208 NS_ENSURE_SUCCESS(rv, rv);
209 rv = scriptError->GetLineNumber(&lineNum);
210 NS_ENSURE_SUCCESS(rv, rv);
211 rv = scriptError->GetColumnNumber(&colNum);
212 NS_ENSURE_SUCCESS(rv, rv);
213 rv = scriptError->GetFlags(&flags);
214 NS_ENSURE_SUCCESS(rv, rv);
215 rv = scriptError->GetIsFromPrivateWindow(&fromPrivateWindow);
216 NS_ENSURE_SUCCESS(rv, rv);
217 rv = scriptError->GetIsFromChromeContext(&fromChromeContext);
218 NS_ENSURE_SUCCESS(rv, rv);
219 rv = scriptError->GetInnerWindowID(&innerWindowId);
220 NS_ENSURE_SUCCESS(rv, rv);
222 *sent = contentParent->SendScriptError(
223 msg, sourceName, sourceLine, lineNum, colNum, flags, category,
224 fromPrivateWindow, innerWindowId, fromChromeContext);
225 return NS_OK;
228 namespace {
230 class LogMessageRunnable : public Runnable {
231 public:
232 LogMessageRunnable(nsIConsoleMessage* aMessage, nsConsoleService* aService)
233 : mozilla::Runnable("LogMessageRunnable"),
234 mMessage(aMessage),
235 mService(aService) {}
237 NS_DECL_NSIRUNNABLE
239 private:
240 nsCOMPtr<nsIConsoleMessage> mMessage;
241 RefPtr<nsConsoleService> mService;
244 NS_IMETHODIMP
245 LogMessageRunnable::Run() {
246 // Snapshot of listeners so that we don't reenter this hash during
247 // enumeration.
248 nsCOMArray<nsIConsoleListener> listeners;
249 mService->CollectCurrentListeners(listeners);
251 mService->SetIsDelivering();
253 for (int32_t i = 0; i < listeners.Count(); ++i) {
254 listeners[i]->Observe(mMessage);
257 mService->SetDoneDelivering();
259 return NS_OK;
262 } // namespace
264 // nsIConsoleService methods
265 NS_IMETHODIMP
266 nsConsoleService::LogMessage(nsIConsoleMessage* aMessage) {
267 return LogMessageWithMode(aMessage, nsIConsoleService::OutputToLog);
270 // This can be called off the main thread.
271 nsresult nsConsoleService::LogMessageWithMode(
272 nsIConsoleMessage* aMessage, nsIConsoleService::OutputMode aOutputMode) {
273 if (!aMessage) {
274 return NS_ERROR_INVALID_ARG;
277 if (!gLoggingEnabled) {
278 return NS_OK;
281 if (NS_IsMainThread() && mDeliveringMessage) {
282 nsCString msg;
283 aMessage->ToString(msg);
284 NS_WARNING(
285 nsPrintfCString(
286 "Reentrancy error: some client attempted to display a message to "
287 "the console while in a console listener. The following message "
288 "was discarded: \"%s\"",
289 msg.get())
290 .get());
291 return NS_ERROR_FAILURE;
294 if (XRE_IsParentProcess() && NS_IsMainThread()) {
295 // If mMessage is a scriptError with an innerWindowId set,
296 // forward it to the matching ContentParent
297 // This enables logging from parent to content process
298 bool sent;
299 nsresult rv = MaybeForwardScriptError(aMessage, &sent);
300 NS_ENSURE_SUCCESS(rv, rv);
301 if (sent) {
302 return NS_OK;
306 RefPtr<LogMessageRunnable> r;
307 nsCOMPtr<nsIConsoleMessage> retiredMessage;
310 * Lock while updating buffer, and while taking snapshot of
311 * listeners array.
314 MutexAutoLock lock(mLock);
316 #if defined(ANDROID)
317 if (StaticPrefs::consoleservice_logcat() && aOutputMode == OutputToLog) {
318 nsCString msg;
319 aMessage->ToString(msg);
321 /** Attempt to use the process name as the log tag. */
322 mozilla::dom::ContentChild* child =
323 mozilla::dom::ContentChild::GetSingleton();
324 nsCString appName;
325 if (child) {
326 child->GetProcessName(appName);
327 } else {
328 appName = "GeckoConsole";
331 uint32_t logLevel = 0;
332 aMessage->GetLogLevel(&logLevel);
334 android_LogPriority logPriority = ANDROID_LOG_INFO;
335 switch (logLevel) {
336 case nsIConsoleMessage::debug:
337 logPriority = ANDROID_LOG_DEBUG;
338 break;
339 case nsIConsoleMessage::info:
340 logPriority = ANDROID_LOG_INFO;
341 break;
342 case nsIConsoleMessage::warn:
343 logPriority = ANDROID_LOG_WARN;
344 break;
345 case nsIConsoleMessage::error:
346 logPriority = ANDROID_LOG_ERROR;
347 break;
350 __android_log_print(logPriority, appName.get(), "%s", msg.get());
352 #endif
353 #ifdef XP_WIN
354 if (gLoggingToDebugger && IsDebuggerPresent()) {
355 nsString msg;
356 aMessage->GetMessageMoz(msg);
357 msg.Append('\n');
358 OutputDebugStringW(msg.get());
360 #endif
362 if (gLoggingBuffered) {
363 MessageElement* e = new MessageElement(aMessage);
364 mMessages.insertBack(e);
365 if (mCurrentSize != mMaximumSize) {
366 mCurrentSize++;
367 } else {
368 MessageElement* p = mMessages.popFirst();
369 MOZ_ASSERT(p);
370 p->swapMessage(retiredMessage);
371 delete p;
375 if (mListeners.Count() > 0) {
376 r = new LogMessageRunnable(aMessage, this);
380 if (retiredMessage) {
381 // Release |retiredMessage| on the main thread in case it is an instance of
382 // a mainthread-only class like nsScriptErrorWithStack and we're off the
383 // main thread.
384 NS_ReleaseOnMainThread("nsConsoleService::retiredMessage",
385 retiredMessage.forget());
388 if (r) {
389 // avoid failing in XPCShell tests
390 nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
391 if (mainThread) {
392 SchedulerGroup::Dispatch(r.forget());
396 return NS_OK;
399 // See nsIConsoleService.idl for more info about this method
400 NS_IMETHODIMP
401 nsConsoleService::CallFunctionAndLogException(
402 JS::Handle<JS::Value> targetGlobal, JS::HandleValue function, JSContext* cx,
403 JS::MutableHandleValue retval) {
404 if (!targetGlobal.isObject() || !function.isObject()) {
405 return NS_ERROR_INVALID_ARG;
408 JS::Rooted<JS::Realm*> contextRealm(cx, JS::GetCurrentRealmOrNull(cx));
409 if (!contextRealm) {
410 return NS_ERROR_INVALID_ARG;
413 JS::Rooted<JSObject*> global(
414 cx, js::CheckedUnwrapDynamic(&targetGlobal.toObject(), cx));
415 if (!global) {
416 return NS_ERROR_INVALID_ARG;
419 // Use AutoJSAPI in order to trigger AutoJSAPI::ReportException
420 // which will do most of the work required for this function.
422 // We only have to pick the right global for which we want to flag
423 // the exception against.
424 dom::AutoJSAPI jsapi;
425 if (!jsapi.Init(global)) {
426 return NS_ERROR_UNEXPECTED;
428 JSContext* ccx = jsapi.cx();
430 // AutoJSAPI picks `targetGlobal` as execution compartment
431 // whereas we expect to run `function` from the callsites compartment.
432 JSAutoRealm ar(ccx, JS::GetRealmGlobalOrNull(contextRealm));
434 JS::RootedValue funVal(ccx, function);
435 if (!JS_WrapValue(ccx, &funVal)) {
436 return NS_ERROR_FAILURE;
438 if (!JS_CallFunctionValue(ccx, nullptr, funVal, JS::HandleValueArray::empty(),
439 retval)) {
440 return NS_ERROR_XPC_JAVASCRIPT_ERROR;
443 return NS_OK;
446 void nsConsoleService::CollectCurrentListeners(
447 nsCOMArray<nsIConsoleListener>& aListeners) {
448 MutexAutoLock lock(mLock);
449 // XXX When MakeBackInserter(nsCOMArray<T>&) is added, we can do:
450 // AppendToArray(aListeners, mListeners.Values());
451 for (const auto& listener : mListeners.Values()) {
452 aListeners.AppendObject(listener);
456 NS_IMETHODIMP
457 nsConsoleService::LogStringMessage(const char16_t* aMessage) {
458 if (!gLoggingEnabled) {
459 return NS_OK;
462 RefPtr<nsConsoleMessage> msg(new nsConsoleMessage(
463 aMessage ? nsDependentString(aMessage) : EmptyString()));
464 return LogMessage(msg);
467 NS_IMETHODIMP
468 nsConsoleService::GetMessageArray(
469 nsTArray<RefPtr<nsIConsoleMessage>>& aMessages) {
470 MOZ_RELEASE_ASSERT(NS_IsMainThread());
472 MutexAutoLock lock(mLock);
474 if (mMessages.isEmpty()) {
475 return NS_OK;
478 MOZ_ASSERT(mCurrentSize <= mMaximumSize);
479 aMessages.SetCapacity(mCurrentSize);
481 for (MessageElement* e = mMessages.getFirst(); e != nullptr;
482 e = e->getNext()) {
483 aMessages.AppendElement(e->Get());
486 return NS_OK;
489 NS_IMETHODIMP
490 nsConsoleService::RegisterListener(nsIConsoleListener* aListener) {
491 if (!NS_IsMainThread()) {
492 NS_ERROR("nsConsoleService::RegisterListener is main thread only.");
493 return NS_ERROR_NOT_SAME_THREAD;
496 nsCOMPtr<nsISupports> canonical = do_QueryInterface(aListener);
497 MOZ_ASSERT(canonical);
499 MutexAutoLock lock(mLock);
500 return mListeners.WithEntryHandle(canonical, [&](auto&& entry) {
501 if (entry) {
502 // Reregistering a listener isn't good
503 return NS_ERROR_FAILURE;
505 entry.Insert(aListener);
506 return NS_OK;
510 NS_IMETHODIMP
511 nsConsoleService::UnregisterListener(nsIConsoleListener* aListener) {
512 if (!NS_IsMainThread()) {
513 NS_ERROR("nsConsoleService::UnregisterListener is main thread only.");
514 return NS_ERROR_NOT_SAME_THREAD;
517 nsCOMPtr<nsISupports> canonical = do_QueryInterface(aListener);
519 MutexAutoLock lock(mLock);
521 return mListeners.Remove(canonical)
522 ? NS_OK
523 // Unregistering a listener that was never registered?
524 : NS_ERROR_FAILURE;
527 NS_IMETHODIMP
528 nsConsoleService::Reset() {
529 MOZ_RELEASE_ASSERT(NS_IsMainThread());
532 * Make sure nobody trips into the buffer while it's being reset
534 MutexAutoLock lock(mLock);
536 ClearMessages();
537 return NS_OK;
540 NS_IMETHODIMP
541 nsConsoleService::ResetWindow(uint64_t windowInnerId) {
542 MOZ_RELEASE_ASSERT(NS_IsMainThread());
544 ClearMessagesForWindowID(windowInnerId);
545 return NS_OK;
548 NS_IMETHODIMP
549 nsConsoleService::Observe(nsISupports* aSubject, const char* aTopic,
550 const char16_t* aData) {
551 if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
552 // Dump all our messages, in case any are cycle collected.
553 Reset();
554 // We could remove ourselves from the observer service, but it is about to
555 // drop all observers anyways, so why bother.
556 } else if (!strcmp(aTopic, "inner-window-destroyed")) {
557 nsCOMPtr<nsISupportsPRUint64> supportsInt = do_QueryInterface(aSubject);
558 MOZ_ASSERT(supportsInt);
560 uint64_t windowId;
561 MOZ_ALWAYS_SUCCEEDS(supportsInt->GetData(&windowId));
563 ClearMessagesForWindowID(windowId);
564 } else {
565 MOZ_CRASH();
567 return NS_OK;