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