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/. */
6
7 /*
8 * Maintains a circular buffer of recent messages, and notifies
9 * listeners when new messages are logged.
10 */
11
12 /* Threadsafe. */
13
14 #include "nsCOMArray.h"
15 #include "nsThreadUtils.h"
16
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 "mozilla/dom/WindowGlobalParent.h"
27 #include "mozilla/dom/ContentParent.h"
28 #include "mozilla/dom/BrowserParent.h"
29
30 #include "mozilla/SchedulerGroup.h"
31 #include "mozilla/Services.h"
32
33 #if defined(ANDROID)
34 # include <android/log.h>
35 # include "mozilla/dom/ContentChild.h"
36 # include "mozilla/StaticPrefs_consoleservice.h"
37 #endif
38 #ifdef XP_WIN
39 # include <windows.h>
40 #endif
41
42 using namespace mozilla;
43
44 NS_IMPL_ADDREF(nsConsoleService)
45 NS_IMPL_RELEASE(nsConsoleService)
46 NS_IMPL_CLASSINFO(nsConsoleService, nullptr,
47 nsIClassInfo::THREADSAFE | nsIClassInfo::SINGLETON,
48 NS_CONSOLESERVICE_CID)
49 NS_IMPL_QUERY_INTERFACE_CI(nsConsoleService, nsIConsoleService, nsIObserver)
50 NS_IMPL_CI_INTERFACE_GETTER(nsConsoleService, nsIConsoleService, nsIObserver)
51
52 static const bool gLoggingEnabled = true;
53 static const bool gLoggingBuffered = true;
54 #ifdef XP_WIN
55 static bool gLoggingToDebugger = true;
56 #endif // XP_WIN
57
58 nsConsoleService::MessageElement::~MessageElement() = default;
59
nsConsoleService()60 nsConsoleService::nsConsoleService()
61 : mCurrentSize(0),
62 mDeliveringMessage(false),
63 mLock("nsConsoleService.mLock") {
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
69 #ifdef XP_WIN
70 // This environment variable controls whether the console service
71 // should be prevented from putting output to the attached debugger.
72 // It only affects the Windows platform.
73 //
74 // To disable OutputDebugString, set:
75 // MOZ_CONSOLESERVICE_DISABLE_DEBUGGER_OUTPUT=1
76 //
77 const char* disableDebugLoggingVar =
78 getenv("MOZ_CONSOLESERVICE_DISABLE_DEBUGGER_OUTPUT");
79 gLoggingToDebugger =
80 !disableDebugLoggingVar || (disableDebugLoggingVar[0] == '0');
81 #endif // XP_WIN
82 }
83
ClearMessagesForWindowID(const uint64_t innerID)84 void nsConsoleService::ClearMessagesForWindowID(const uint64_t innerID) {
85 MOZ_RELEASE_ASSERT(NS_IsMainThread());
86 MutexAutoLock lock(mLock);
87
88 for (MessageElement* e = mMessages.getFirst(); e != nullptr;) {
89 // Only messages implementing nsIScriptError interface expose the
90 // inner window ID.
91 nsCOMPtr<nsIScriptError> scriptError = do_QueryInterface(e->Get());
92 if (!scriptError) {
93 e = e->getNext();
94 continue;
95 }
96 uint64_t innerWindowID;
97 nsresult rv = scriptError->GetInnerWindowID(&innerWindowID);
98 if (NS_FAILED(rv) || innerWindowID != innerID) {
99 e = e->getNext();
100 continue;
101 }
102
103 MessageElement* next = e->getNext();
104 e->remove();
105 delete e;
106 mCurrentSize--;
107 MOZ_ASSERT(mCurrentSize < mMaximumSize);
108
109 e = next;
110 }
111 }
112
ClearMessages()113 void nsConsoleService::ClearMessages() {
114 // NB: A lock is not required here as it's only called from |Reset| which
115 // locks for us and from the dtor.
116 while (!mMessages.isEmpty()) {
117 MessageElement* e = mMessages.popFirst();
118 delete e;
119 }
120 mCurrentSize = 0;
121 }
122
~nsConsoleService()123 nsConsoleService::~nsConsoleService() {
124 MOZ_RELEASE_ASSERT(NS_IsMainThread());
125
126 ClearMessages();
127 }
128
129 class AddConsolePrefWatchers : public Runnable {
130 public:
AddConsolePrefWatchers(nsConsoleService * aConsole)131 explicit AddConsolePrefWatchers(nsConsoleService* aConsole)
132 : mozilla::Runnable("AddConsolePrefWatchers"), mConsole(aConsole) {}
133
Run()134 NS_IMETHOD Run() override {
135 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
136 MOZ_ASSERT(obs);
137 obs->AddObserver(mConsole, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
138 obs->AddObserver(mConsole, "inner-window-destroyed", false);
139
140 if (!gLoggingBuffered) {
141 mConsole->Reset();
142 }
143 return NS_OK;
144 }
145
146 private:
147 RefPtr<nsConsoleService> mConsole;
148 };
149
Init()150 nsresult nsConsoleService::Init() {
151 NS_DispatchToMainThread(new AddConsolePrefWatchers(this));
152
153 return NS_OK;
154 }
155
MaybeForwardScriptError(nsIConsoleMessage * aMessage,bool * sent)156 nsresult nsConsoleService::MaybeForwardScriptError(nsIConsoleMessage* aMessage,
157 bool* sent) {
158 *sent = false;
159
160 nsCOMPtr<nsIScriptError> scriptError = do_QueryInterface(aMessage);
161 if (!scriptError) {
162 // Not an nsIScriptError
163 return NS_OK;
164 }
165
166 uint64_t windowID;
167 nsresult rv;
168 rv = scriptError->GetInnerWindowID(&windowID);
169 NS_ENSURE_SUCCESS(rv, rv);
170 if (!windowID) {
171 // Does not set window id
172 return NS_OK;
173 }
174
175 RefPtr<mozilla::dom::WindowGlobalParent> windowGlobalParent =
176 mozilla::dom::WindowGlobalParent::GetByInnerWindowId(windowID);
177 if (!windowGlobalParent) {
178 // Could not find parent window by id
179 return NS_OK;
180 }
181
182 RefPtr<mozilla::dom::BrowserParent> browserParent =
183 windowGlobalParent->GetBrowserParent();
184 if (!browserParent) {
185 return NS_OK;
186 }
187
188 mozilla::dom::ContentParent* contentParent = browserParent->Manager();
189 if (!contentParent) {
190 return NS_ERROR_FAILURE;
191 }
192
193 nsAutoString msg, sourceName, sourceLine;
194 nsCString category;
195 uint32_t lineNum, colNum, flags;
196 uint64_t innerWindowId;
197 bool fromPrivateWindow, fromChromeContext;
198
199 rv = scriptError->GetErrorMessage(msg);
200 NS_ENSURE_SUCCESS(rv, rv);
201 rv = scriptError->GetSourceName(sourceName);
202 NS_ENSURE_SUCCESS(rv, rv);
203 rv = scriptError->GetSourceLine(sourceLine);
204 NS_ENSURE_SUCCESS(rv, rv);
205
206 rv = scriptError->GetCategory(getter_Copies(category));
207 NS_ENSURE_SUCCESS(rv, rv);
208 rv = scriptError->GetLineNumber(&lineNum);
209 NS_ENSURE_SUCCESS(rv, rv);
210 rv = scriptError->GetColumnNumber(&colNum);
211 NS_ENSURE_SUCCESS(rv, rv);
212 rv = scriptError->GetFlags(&flags);
213 NS_ENSURE_SUCCESS(rv, rv);
214 rv = scriptError->GetIsFromPrivateWindow(&fromPrivateWindow);
215 NS_ENSURE_SUCCESS(rv, rv);
216 rv = scriptError->GetIsFromChromeContext(&fromChromeContext);
217 NS_ENSURE_SUCCESS(rv, rv);
218 rv = scriptError->GetInnerWindowID(&innerWindowId);
219 NS_ENSURE_SUCCESS(rv, rv);
220
221 *sent = contentParent->SendScriptError(
222 msg, sourceName, sourceLine, lineNum, colNum, flags, category,
223 fromPrivateWindow, innerWindowId, fromChromeContext);
224 return NS_OK;
225 }
226
227 namespace {
228
229 class LogMessageRunnable : public Runnable {
230 public:
LogMessageRunnable(nsIConsoleMessage * aMessage,nsConsoleService * aService)231 LogMessageRunnable(nsIConsoleMessage* aMessage, nsConsoleService* aService)
232 : mozilla::Runnable("LogMessageRunnable"),
233 mMessage(aMessage),
234 mService(aService) {}
235
236 NS_DECL_NSIRUNNABLE
237
238 private:
239 nsCOMPtr<nsIConsoleMessage> mMessage;
240 RefPtr<nsConsoleService> mService;
241 };
242
243 NS_IMETHODIMP
Run()244 LogMessageRunnable::Run() {
245 // Snapshot of listeners so that we don't reenter this hash during
246 // enumeration.
247 nsCOMArray<nsIConsoleListener> listeners;
248 mService->CollectCurrentListeners(listeners);
249
250 mService->SetIsDelivering();
251
252 for (int32_t i = 0; i < listeners.Count(); ++i) {
253 listeners[i]->Observe(mMessage);
254 }
255
256 mService->SetDoneDelivering();
257
258 return NS_OK;
259 }
260
261 } // namespace
262
263 // nsIConsoleService methods
264 NS_IMETHODIMP
LogMessage(nsIConsoleMessage * aMessage)265 nsConsoleService::LogMessage(nsIConsoleMessage* aMessage) {
266 return LogMessageWithMode(aMessage, OutputToLog);
267 }
268
269 // This can be called off the main thread.
LogMessageWithMode(nsIConsoleMessage * aMessage,nsConsoleService::OutputMode aOutputMode)270 nsresult nsConsoleService::LogMessageWithMode(
271 nsIConsoleMessage* aMessage, nsConsoleService::OutputMode aOutputMode) {
272 if (!aMessage) {
273 return NS_ERROR_INVALID_ARG;
274 }
275
276 if (!gLoggingEnabled) {
277 return NS_OK;
278 }
279
280 if (NS_IsMainThread() && mDeliveringMessage) {
281 nsCString msg;
282 aMessage->ToString(msg);
283 NS_WARNING(
284 nsPrintfCString(
285 "Reentrancy error: some client attempted to display a message to "
286 "the console while in a console listener. The following message "
287 "was discarded: \"%s\"",
288 msg.get())
289 .get());
290 return NS_ERROR_FAILURE;
291 }
292
293 if (XRE_IsParentProcess() && NS_IsMainThread()) {
294 // If mMessage is a scriptError with an innerWindowId set,
295 // forward it to the matching ContentParent
296 // This enables logging from parent to content process
297 bool sent;
298 nsresult rv = MaybeForwardScriptError(aMessage, &sent);
299 NS_ENSURE_SUCCESS(rv, rv);
300 if (sent) {
301 return NS_OK;
302 }
303 }
304
305 RefPtr<LogMessageRunnable> r;
306 nsCOMPtr<nsIConsoleMessage> retiredMessage;
307
308 /*
309 * Lock while updating buffer, and while taking snapshot of
310 * listeners array.
311 */
312 {
313 MutexAutoLock lock(mLock);
314
315 #if defined(ANDROID)
316 if (StaticPrefs::consoleservice_logcat() && aOutputMode == OutputToLog) {
317 nsCString msg;
318 aMessage->ToString(msg);
319
320 /** Attempt to use the process name as the log tag. */
321 mozilla::dom::ContentChild* child =
322 mozilla::dom::ContentChild::GetSingleton();
323 nsCString appName;
324 if (child) {
325 child->GetProcessName(appName);
326 } else {
327 appName = "GeckoConsole";
328 }
329
330 uint32_t logLevel = 0;
331 aMessage->GetLogLevel(&logLevel);
332
333 android_LogPriority logPriority = ANDROID_LOG_INFO;
334 switch (logLevel) {
335 case nsIConsoleMessage::debug:
336 logPriority = ANDROID_LOG_DEBUG;
337 break;
338 case nsIConsoleMessage::info:
339 logPriority = ANDROID_LOG_INFO;
340 break;
341 case nsIConsoleMessage::warn:
342 logPriority = ANDROID_LOG_WARN;
343 break;
344 case nsIConsoleMessage::error:
345 logPriority = ANDROID_LOG_ERROR;
346 break;
347 }
348
349 __android_log_print(logPriority, appName.get(), "%s", msg.get());
350 }
351 #endif
352 #ifdef XP_WIN
353 if (gLoggingToDebugger && IsDebuggerPresent()) {
354 nsString msg;
355 aMessage->GetMessageMoz(msg);
356 msg.Append('\n');
357 OutputDebugStringW(msg.get());
358 }
359 #endif
360
361 if (gLoggingBuffered) {
362 MessageElement* e = new MessageElement(aMessage);
363 mMessages.insertBack(e);
364 if (mCurrentSize != mMaximumSize) {
365 mCurrentSize++;
366 } else {
367 MessageElement* p = mMessages.popFirst();
368 MOZ_ASSERT(p);
369 p->swapMessage(retiredMessage);
370 delete p;
371 }
372 }
373
374 if (mListeners.Count() > 0) {
375 r = new LogMessageRunnable(aMessage, this);
376 }
377 }
378
379 if (retiredMessage) {
380 // Release |retiredMessage| on the main thread in case it is an instance of
381 // a mainthread-only class like nsScriptErrorWithStack and we're off the
382 // main thread.
383 NS_ReleaseOnMainThread("nsConsoleService::retiredMessage",
384 retiredMessage.forget());
385 }
386
387 if (r) {
388 // avoid failing in XPCShell tests
389 nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
390 if (mainThread) {
391 SchedulerGroup::Dispatch(TaskCategory::Other, r.forget());
392 }
393 }
394
395 return NS_OK;
396 }
397
CollectCurrentListeners(nsCOMArray<nsIConsoleListener> & aListeners)398 void nsConsoleService::CollectCurrentListeners(
399 nsCOMArray<nsIConsoleListener>& aListeners) {
400 MutexAutoLock lock(mLock);
401 // XXX When MakeBackInserter(nsCOMArray<T>&) is added, we can do:
402 // AppendToArray(aListeners, mListeners.Values());
403 for (const auto& listener : mListeners.Values()) {
404 aListeners.AppendObject(listener);
405 }
406 }
407
408 NS_IMETHODIMP
LogStringMessage(const char16_t * aMessage)409 nsConsoleService::LogStringMessage(const char16_t* aMessage) {
410 if (!gLoggingEnabled) {
411 return NS_OK;
412 }
413
414 RefPtr<nsConsoleMessage> msg(new nsConsoleMessage(aMessage));
415 return this->LogMessage(msg);
416 }
417
418 NS_IMETHODIMP
GetMessageArray(nsTArray<RefPtr<nsIConsoleMessage>> & aMessages)419 nsConsoleService::GetMessageArray(
420 nsTArray<RefPtr<nsIConsoleMessage>>& aMessages) {
421 MOZ_RELEASE_ASSERT(NS_IsMainThread());
422
423 MutexAutoLock lock(mLock);
424
425 if (mMessages.isEmpty()) {
426 return NS_OK;
427 }
428
429 MOZ_ASSERT(mCurrentSize <= mMaximumSize);
430 aMessages.SetCapacity(mCurrentSize);
431
432 for (MessageElement* e = mMessages.getFirst(); e != nullptr;
433 e = e->getNext()) {
434 aMessages.AppendElement(e->Get());
435 }
436
437 return NS_OK;
438 }
439
440 NS_IMETHODIMP
RegisterListener(nsIConsoleListener * aListener)441 nsConsoleService::RegisterListener(nsIConsoleListener* aListener) {
442 if (!NS_IsMainThread()) {
443 NS_ERROR("nsConsoleService::RegisterListener is main thread only.");
444 return NS_ERROR_NOT_SAME_THREAD;
445 }
446
447 nsCOMPtr<nsISupports> canonical = do_QueryInterface(aListener);
448 MOZ_ASSERT(canonical);
449
450 MutexAutoLock lock(mLock);
451 return mListeners.WithEntryHandle(canonical, [&](auto&& entry) {
452 if (entry) {
453 // Reregistering a listener isn't good
454 return NS_ERROR_FAILURE;
455 }
456 entry.Insert(aListener);
457 return NS_OK;
458 });
459 }
460
461 NS_IMETHODIMP
UnregisterListener(nsIConsoleListener * aListener)462 nsConsoleService::UnregisterListener(nsIConsoleListener* aListener) {
463 if (!NS_IsMainThread()) {
464 NS_ERROR("nsConsoleService::UnregisterListener is main thread only.");
465 return NS_ERROR_NOT_SAME_THREAD;
466 }
467
468 nsCOMPtr<nsISupports> canonical = do_QueryInterface(aListener);
469
470 MutexAutoLock lock(mLock);
471
472 return mListeners.Remove(canonical)
473 ? NS_OK
474 // Unregistering a listener that was never registered?
475 : NS_ERROR_FAILURE;
476 }
477
478 NS_IMETHODIMP
Reset()479 nsConsoleService::Reset() {
480 MOZ_RELEASE_ASSERT(NS_IsMainThread());
481
482 /*
483 * Make sure nobody trips into the buffer while it's being reset
484 */
485 MutexAutoLock lock(mLock);
486
487 ClearMessages();
488 return NS_OK;
489 }
490
491 NS_IMETHODIMP
ResetWindow(uint64_t windowInnerId)492 nsConsoleService::ResetWindow(uint64_t windowInnerId) {
493 MOZ_RELEASE_ASSERT(NS_IsMainThread());
494
495 ClearMessagesForWindowID(windowInnerId);
496 return NS_OK;
497 }
498
499 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)500 nsConsoleService::Observe(nsISupports* aSubject, const char* aTopic,
501 const char16_t* aData) {
502 if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
503 // Dump all our messages, in case any are cycle collected.
504 Reset();
505 // We could remove ourselves from the observer service, but it is about to
506 // drop all observers anyways, so why bother.
507 } else if (!strcmp(aTopic, "inner-window-destroyed")) {
508 nsCOMPtr<nsISupportsPRUint64> supportsInt = do_QueryInterface(aSubject);
509 MOZ_ASSERT(supportsInt);
510
511 uint64_t windowId;
512 MOZ_ALWAYS_SUCCEEDS(supportsInt->GetData(&windowId));
513
514 ClearMessagesForWindowID(windowId);
515 } else {
516 MOZ_CRASH();
517 }
518 return NS_OK;
519 }
520