Line data Source code
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 "nsMemory.h"
15 : #include "nsCOMArray.h"
16 : #include "nsThreadUtils.h"
17 :
18 : #include "nsConsoleService.h"
19 : #include "nsConsoleMessage.h"
20 : #include "nsIClassInfoImpl.h"
21 : #include "nsIConsoleListener.h"
22 : #include "nsIObserverService.h"
23 : #include "nsPrintfCString.h"
24 : #include "nsProxyRelease.h"
25 : #include "nsIScriptError.h"
26 : #include "nsISupportsPrimitives.h"
27 :
28 : #include "mozilla/Preferences.h"
29 : #include "mozilla/Services.h"
30 : #include "mozilla/SystemGroup.h"
31 :
32 : #if defined(ANDROID)
33 : #include <android/log.h>
34 : #include "mozilla/dom/ContentChild.h"
35 : #endif
36 : #ifdef XP_WIN
37 : #include <windows.h>
38 : #endif
39 :
40 : #ifdef MOZ_TASK_TRACER
41 : #include "GeckoTaskTracer.h"
42 : using namespace mozilla::tasktracer;
43 : #endif
44 :
45 : using namespace mozilla;
46 :
47 30 : NS_IMPL_ADDREF(nsConsoleService)
48 19 : NS_IMPL_RELEASE(nsConsoleService)
49 3 : NS_IMPL_CLASSINFO(nsConsoleService, nullptr,
50 : nsIClassInfo::THREADSAFE | nsIClassInfo::SINGLETON,
51 : NS_CONSOLESERVICE_CID)
52 9 : NS_IMPL_QUERY_INTERFACE_CI(nsConsoleService, nsIConsoleService, nsIObserver)
53 0 : NS_IMPL_CI_INTERFACE_GETTER(nsConsoleService, nsIConsoleService, nsIObserver)
54 :
55 : static bool sLoggingEnabled = true;
56 : static bool sLoggingBuffered = true;
57 : #if defined(ANDROID)
58 : static bool sLoggingLogcat = true;
59 : #endif // defined(ANDROID)
60 :
61 0 : nsConsoleService::MessageElement::~MessageElement()
62 : {
63 0 : }
64 :
65 3 : nsConsoleService::nsConsoleService()
66 : : mCurrentSize(0)
67 : , mDeliveringMessage(false)
68 3 : , mLock("nsConsoleService.mLock")
69 : {
70 : // XXX grab this from a pref!
71 : // hm, but worry about circularity, bc we want to be able to report
72 : // prefs errs...
73 3 : mMaximumSize = 250;
74 3 : }
75 :
76 :
77 : void
78 3 : nsConsoleService::ClearMessagesForWindowID(const uint64_t innerID)
79 : {
80 3 : MOZ_RELEASE_ASSERT(NS_IsMainThread());
81 6 : MutexAutoLock lock(mLock);
82 :
83 6 : for (MessageElement* e = mMessages.getFirst(); e != nullptr; ) {
84 : // Only messages implementing nsIScriptError interface expose the
85 : // inner window ID.
86 3 : nsCOMPtr<nsIScriptError> scriptError = do_QueryInterface(e->Get());
87 3 : if (!scriptError) {
88 2 : e = e->getNext();
89 2 : continue;
90 : }
91 : uint64_t innerWindowID;
92 1 : nsresult rv = scriptError->GetInnerWindowID(&innerWindowID);
93 1 : if (NS_FAILED(rv) || innerWindowID != innerID) {
94 1 : e = e->getNext();
95 1 : continue;
96 : }
97 :
98 0 : MessageElement* next = e->getNext();
99 0 : e->remove();
100 0 : delete e;
101 0 : mCurrentSize--;
102 0 : MOZ_ASSERT(mCurrentSize < mMaximumSize);
103 :
104 0 : e = next;
105 : }
106 3 : }
107 :
108 : void
109 0 : nsConsoleService::ClearMessages()
110 : {
111 : // NB: A lock is not required here as it's only called from |Reset| which
112 : // locks for us and from the dtor.
113 0 : while (!mMessages.isEmpty()) {
114 0 : MessageElement* e = mMessages.popFirst();
115 0 : delete e;
116 : }
117 0 : mCurrentSize = 0;
118 0 : }
119 :
120 0 : nsConsoleService::~nsConsoleService()
121 : {
122 0 : MOZ_RELEASE_ASSERT(NS_IsMainThread());
123 :
124 0 : ClearMessages();
125 0 : }
126 :
127 9 : class AddConsolePrefWatchers : public Runnable
128 : {
129 : public:
130 3 : explicit AddConsolePrefWatchers(nsConsoleService* aConsole)
131 3 : : mozilla::Runnable("AddConsolePrefWatchers")
132 3 : , mConsole(aConsole)
133 : {
134 3 : }
135 :
136 3 : NS_IMETHOD Run() override
137 : {
138 3 : Preferences::AddBoolVarCache(&sLoggingEnabled, "consoleservice.enabled", true);
139 3 : Preferences::AddBoolVarCache(&sLoggingBuffered, "consoleservice.buffered", true);
140 : #if defined(ANDROID)
141 : Preferences::AddBoolVarCache(&sLoggingLogcat, "consoleservice.logcat", true);
142 : #endif // defined(ANDROID)
143 :
144 6 : nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
145 3 : MOZ_ASSERT(obs);
146 3 : obs->AddObserver(mConsole, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
147 3 : obs->AddObserver(mConsole, "inner-window-destroyed", false);
148 :
149 3 : if (!sLoggingBuffered) {
150 0 : mConsole->Reset();
151 : }
152 6 : return NS_OK;
153 : }
154 :
155 : private:
156 : RefPtr<nsConsoleService> mConsole;
157 : };
158 :
159 : nsresult
160 3 : nsConsoleService::Init()
161 : {
162 3 : NS_DispatchToMainThread(new AddConsolePrefWatchers(this));
163 :
164 3 : return NS_OK;
165 : }
166 :
167 : namespace {
168 :
169 0 : class LogMessageRunnable : public Runnable
170 : {
171 : public:
172 0 : LogMessageRunnable(nsIConsoleMessage* aMessage, nsConsoleService* aService)
173 0 : : mozilla::Runnable("LogMessageRunnable")
174 : , mMessage(aMessage)
175 0 : , mService(aService)
176 0 : { }
177 :
178 : NS_DECL_NSIRUNNABLE
179 :
180 : private:
181 : nsCOMPtr<nsIConsoleMessage> mMessage;
182 : RefPtr<nsConsoleService> mService;
183 : };
184 :
185 : NS_IMETHODIMP
186 0 : LogMessageRunnable::Run()
187 : {
188 0 : MOZ_ASSERT(NS_IsMainThread());
189 :
190 : // Snapshot of listeners so that we don't reenter this hash during
191 : // enumeration.
192 0 : nsCOMArray<nsIConsoleListener> listeners;
193 0 : mService->CollectCurrentListeners(listeners);
194 :
195 0 : mService->SetIsDelivering();
196 :
197 0 : for (int32_t i = 0; i < listeners.Count(); ++i) {
198 0 : listeners[i]->Observe(mMessage);
199 : }
200 :
201 0 : mService->SetDoneDelivering();
202 :
203 0 : return NS_OK;
204 : }
205 :
206 : } // namespace
207 :
208 : // nsIConsoleService methods
209 : NS_IMETHODIMP
210 3 : nsConsoleService::LogMessage(nsIConsoleMessage* aMessage)
211 : {
212 3 : return LogMessageWithMode(aMessage, OutputToLog);
213 : }
214 :
215 : // This can be called off the main thread.
216 : nsresult
217 3 : nsConsoleService::LogMessageWithMode(nsIConsoleMessage* aMessage,
218 : nsConsoleService::OutputMode aOutputMode)
219 : {
220 3 : if (!aMessage) {
221 0 : return NS_ERROR_INVALID_ARG;
222 : }
223 :
224 3 : if (!sLoggingEnabled) {
225 0 : return NS_OK;
226 : }
227 :
228 3 : if (NS_IsMainThread() && mDeliveringMessage) {
229 0 : nsCString msg;
230 0 : aMessage->ToString(msg);
231 0 : NS_WARNING(nsPrintfCString("Reentrancy error: some client attempted "
232 : "to display a message to the console while in a console listener. "
233 0 : "The following message was discarded: \"%s\"", msg.get()).get());
234 0 : return NS_ERROR_FAILURE;
235 : }
236 :
237 6 : RefPtr<LogMessageRunnable> r;
238 6 : nsCOMPtr<nsIConsoleMessage> retiredMessage;
239 :
240 : /*
241 : * Lock while updating buffer, and while taking snapshot of
242 : * listeners array.
243 : */
244 : {
245 6 : MutexAutoLock lock(mLock);
246 :
247 : #if defined(ANDROID)
248 : if (sLoggingLogcat && aOutputMode == OutputToLog) {
249 : nsCString msg;
250 : aMessage->ToString(msg);
251 :
252 : /** Attempt to use the process name as the log tag. */
253 : mozilla::dom::ContentChild* child =
254 : mozilla::dom::ContentChild::GetSingleton();
255 : nsCString appName;
256 : if (child) {
257 : child->GetProcessName(appName);
258 : } else {
259 : appName = "GeckoConsole";
260 : }
261 :
262 : uint32_t logLevel = 0;
263 : aMessage->GetLogLevel(&logLevel);
264 :
265 : android_LogPriority logPriority = ANDROID_LOG_INFO;
266 : switch (logLevel) {
267 : case nsIConsoleMessage::debug:
268 : logPriority = ANDROID_LOG_DEBUG;
269 : break;
270 : case nsIConsoleMessage::info:
271 : logPriority = ANDROID_LOG_INFO;
272 : break;
273 : case nsIConsoleMessage::warn:
274 : logPriority = ANDROID_LOG_WARN;
275 : break;
276 : case nsIConsoleMessage::error:
277 : logPriority = ANDROID_LOG_ERROR;
278 : break;
279 : }
280 :
281 : __android_log_print(logPriority, appName.get(), "%s", msg.get());
282 : }
283 : #endif
284 : #ifdef XP_WIN
285 : if (IsDebuggerPresent()) {
286 : nsString msg;
287 : aMessage->GetMessageMoz(getter_Copies(msg));
288 : msg.Append('\n');
289 : OutputDebugStringW(msg.get());
290 : }
291 : #endif
292 : #ifdef MOZ_TASK_TRACER
293 : if (IsStartLogging()) {
294 : nsCString msg;
295 : aMessage->ToString(msg);
296 : int prefixPos = msg.Find(GetJSLabelPrefix());
297 : if (prefixPos >= 0) {
298 : nsDependentCSubstring submsg(msg, prefixPos);
299 : AddLabel("%s", submsg.BeginReading());
300 : }
301 : }
302 : #endif
303 :
304 3 : if (sLoggingBuffered) {
305 3 : MessageElement* e = new MessageElement(aMessage);
306 3 : mMessages.insertBack(e);
307 3 : if (mCurrentSize != mMaximumSize) {
308 3 : mCurrentSize++;
309 : } else {
310 0 : MessageElement* p = mMessages.popFirst();
311 0 : MOZ_ASSERT(p);
312 0 : p->swapMessage(retiredMessage);
313 0 : delete p;
314 : }
315 : }
316 :
317 3 : if (mListeners.Count() > 0) {
318 0 : r = new LogMessageRunnable(aMessage, this);
319 : }
320 : }
321 :
322 3 : if (retiredMessage) {
323 : // Release |retiredMessage| on the main thread in case it is an instance of
324 : // a mainthread-only class like nsScriptErrorWithStack and we're off the
325 : // main thread.
326 : NS_ReleaseOnMainThread(
327 0 : "nsConsoleService::retiredMessage", retiredMessage.forget());
328 : }
329 :
330 3 : if (r) {
331 : // avoid failing in XPCShell tests
332 0 : nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
333 0 : if (mainThread) {
334 0 : SystemGroup::Dispatch("LogMessageRunnable", TaskCategory::Other, r.forget());
335 : }
336 : }
337 :
338 3 : return NS_OK;
339 : }
340 :
341 : void
342 0 : nsConsoleService::CollectCurrentListeners(
343 : nsCOMArray<nsIConsoleListener>& aListeners)
344 : {
345 0 : MutexAutoLock lock(mLock);
346 0 : for (auto iter = mListeners.Iter(); !iter.Done(); iter.Next()) {
347 0 : nsIConsoleListener* value = iter.UserData();
348 0 : aListeners.AppendObject(value);
349 : }
350 0 : }
351 :
352 : NS_IMETHODIMP
353 0 : nsConsoleService::LogStringMessage(const char16_t* aMessage)
354 : {
355 0 : if (!sLoggingEnabled) {
356 0 : return NS_OK;
357 : }
358 :
359 0 : RefPtr<nsConsoleMessage> msg(new nsConsoleMessage(aMessage));
360 0 : return this->LogMessage(msg);
361 : }
362 :
363 : NS_IMETHODIMP
364 0 : nsConsoleService::GetMessageArray(uint32_t* aCount,
365 : nsIConsoleMessage*** aMessages)
366 : {
367 0 : MOZ_RELEASE_ASSERT(NS_IsMainThread());
368 :
369 0 : MutexAutoLock lock(mLock);
370 :
371 0 : if (mMessages.isEmpty()) {
372 : /*
373 : * Make a 1-length output array so that nobody gets confused,
374 : * and return a count of 0. This should result in a 0-length
375 : * array object when called from script.
376 : */
377 : nsIConsoleMessage** messageArray = (nsIConsoleMessage**)
378 0 : moz_xmalloc(sizeof(nsIConsoleMessage*));
379 0 : *messageArray = nullptr;
380 0 : *aMessages = messageArray;
381 0 : *aCount = 0;
382 :
383 0 : return NS_OK;
384 : }
385 :
386 0 : MOZ_ASSERT(mCurrentSize <= mMaximumSize);
387 : nsIConsoleMessage** messageArray =
388 0 : static_cast<nsIConsoleMessage**>(moz_xmalloc(sizeof(nsIConsoleMessage*)
389 0 : * mCurrentSize));
390 :
391 0 : uint32_t i = 0;
392 0 : for (MessageElement* e = mMessages.getFirst(); e != nullptr; e = e->getNext()) {
393 0 : nsCOMPtr<nsIConsoleMessage> m = e->Get();
394 0 : m.forget(&messageArray[i]);
395 0 : i++;
396 : }
397 :
398 0 : MOZ_ASSERT(i == mCurrentSize);
399 :
400 0 : *aCount = i;
401 0 : *aMessages = messageArray;
402 :
403 0 : return NS_OK;
404 : }
405 :
406 : NS_IMETHODIMP
407 2 : nsConsoleService::RegisterListener(nsIConsoleListener* aListener)
408 : {
409 2 : if (!NS_IsMainThread()) {
410 0 : NS_ERROR("nsConsoleService::RegisterListener is main thread only.");
411 0 : return NS_ERROR_NOT_SAME_THREAD;
412 : }
413 :
414 4 : nsCOMPtr<nsISupports> canonical = do_QueryInterface(aListener);
415 :
416 4 : MutexAutoLock lock(mLock);
417 2 : if (mListeners.GetWeak(canonical)) {
418 : // Reregistering a listener isn't good
419 0 : return NS_ERROR_FAILURE;
420 : }
421 2 : mListeners.Put(canonical, aListener);
422 2 : return NS_OK;
423 : }
424 :
425 : NS_IMETHODIMP
426 0 : nsConsoleService::UnregisterListener(nsIConsoleListener* aListener)
427 : {
428 0 : if (!NS_IsMainThread()) {
429 0 : NS_ERROR("nsConsoleService::UnregisterListener is main thread only.");
430 0 : return NS_ERROR_NOT_SAME_THREAD;
431 : }
432 :
433 0 : nsCOMPtr<nsISupports> canonical = do_QueryInterface(aListener);
434 :
435 0 : MutexAutoLock lock(mLock);
436 :
437 0 : if (!mListeners.GetWeak(canonical)) {
438 : // Unregistering a listener that was never registered?
439 0 : return NS_ERROR_FAILURE;
440 : }
441 0 : mListeners.Remove(canonical);
442 0 : return NS_OK;
443 : }
444 :
445 : NS_IMETHODIMP
446 0 : nsConsoleService::Reset()
447 : {
448 0 : MOZ_RELEASE_ASSERT(NS_IsMainThread());
449 :
450 : /*
451 : * Make sure nobody trips into the buffer while it's being reset
452 : */
453 0 : MutexAutoLock lock(mLock);
454 :
455 0 : ClearMessages();
456 0 : return NS_OK;
457 : }
458 :
459 : NS_IMETHODIMP
460 3 : nsConsoleService::Observe(nsISupports* aSubject, const char* aTopic,
461 : const char16_t* aData)
462 : {
463 3 : if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
464 : // Dump all our messages, in case any are cycle collected.
465 0 : Reset();
466 : // We could remove ourselves from the observer service, but it is about to
467 : // drop all observers anyways, so why bother.
468 3 : } else if (!strcmp(aTopic, "inner-window-destroyed")) {
469 6 : nsCOMPtr<nsISupportsPRUint64> supportsInt = do_QueryInterface(aSubject);
470 3 : MOZ_ASSERT(supportsInt);
471 :
472 : uint64_t windowId;
473 3 : MOZ_ALWAYS_SUCCEEDS(supportsInt->GetData(&windowId));
474 :
475 3 : ClearMessagesForWindowID(windowId);
476 : } else {
477 0 : MOZ_CRASH();
478 : }
479 3 : return NS_OK;
480 : }
|