Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* Maintains a circular buffer of recent messages, and notifies
* listeners when new messages are logged.
*/
/* Threadsafe. */
#include "nsCOMArray.h"
#include "nsThreadUtils.h"
#include "nsConsoleService.h"
#include "nsConsoleMessage.h"
#include "nsIClassInfoImpl.h"
#include "nsIConsoleListener.h"
#include "nsIObserverService.h"
#include "nsPrintfCString.h"
#include "nsProxyRelease.h"
#include "nsIScriptError.h"
#include "nsISupportsPrimitives.h"
#include "js/friend/ErrorMessages.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/BrowserParent.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/SchedulerGroup.h"
#include "mozilla/Services.h"
#if defined(ANDROID)
# include <android/log.h>
# include "mozilla/dom/ContentChild.h"
# include "mozilla/StaticPrefs_consoleservice.h"
#endif
#ifdef XP_WIN
# include <windows.h>
#endif
using namespace mozilla;
NS_IMPL_ADDREF(nsConsoleService)
NS_IMPL_RELEASE(nsConsoleService)
NS_IMPL_CLASSINFO(nsConsoleService, nullptr,
nsIClassInfo::THREADSAFE | nsIClassInfo::SINGLETON,
NS_CONSOLESERVICE_CID)
NS_IMPL_QUERY_INTERFACE_CI(nsConsoleService, nsIConsoleService, nsIObserver)
NS_IMPL_CI_INTERFACE_GETTER(nsConsoleService, nsIConsoleService, nsIObserver)
static const bool gLoggingEnabled = true;
static const bool gLoggingBuffered = true;
#ifdef XP_WIN
static bool gLoggingToDebugger = true;
#endif // XP_WIN
nsConsoleService::MessageElement::~MessageElement() = default;
nsConsoleService::nsConsoleService()
: mCurrentSize(0),
// XXX grab this from a pref!
// hm, but worry about circularity, bc we want to be able to report
// prefs errs...
mMaximumSize(250),
mDeliveringMessage(false),
mLock("nsConsoleService.mLock") {
#ifdef XP_WIN
// This environment variable controls whether the console service
// should be prevented from putting output to the attached debugger.
// It only affects the Windows platform.
//
// To disable OutputDebugString, set:
// MOZ_CONSOLESERVICE_DISABLE_DEBUGGER_OUTPUT=1
//
const char* disableDebugLoggingVar =
getenv("MOZ_CONSOLESERVICE_DISABLE_DEBUGGER_OUTPUT");
gLoggingToDebugger =
!disableDebugLoggingVar || (disableDebugLoggingVar[0] == '0');
#endif // XP_WIN
}
void nsConsoleService::ClearMessagesForWindowID(const uint64_t innerID) {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
MutexAutoLock lock(mLock);
for (MessageElement* e = mMessages.getFirst(); e != nullptr;) {
// Only messages implementing nsIScriptError interface expose the
// inner window ID.
nsCOMPtr<nsIScriptError> scriptError = do_QueryInterface(e->Get());
if (!scriptError) {
e = e->getNext();
continue;
}
uint64_t innerWindowID;
nsresult rv = scriptError->GetInnerWindowID(&innerWindowID);
if (NS_FAILED(rv) || innerWindowID != innerID) {
e = e->getNext();
continue;
}
MessageElement* next = e->getNext();
e->remove();
delete e;
mCurrentSize--;
MOZ_ASSERT(mCurrentSize < mMaximumSize);
e = next;
}
}
void nsConsoleService::ClearMessages() {
// NB: A lock is not required here as it's only called from |Reset| which
// locks for us and from the dtor.
while (!mMessages.isEmpty()) {
MessageElement* e = mMessages.popFirst();
delete e;
}
mCurrentSize = 0;
}
nsConsoleService::~nsConsoleService() {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
ClearMessages();
}
class AddConsolePrefWatchers : public Runnable {
public:
explicit AddConsolePrefWatchers(nsConsoleService* aConsole)
: mozilla::Runnable("AddConsolePrefWatchers"), mConsole(aConsole) {}
NS_IMETHOD Run() override {
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
MOZ_ASSERT(obs);
obs->AddObserver(mConsole, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
obs->AddObserver(mConsole, "inner-window-destroyed", false);
if (!gLoggingBuffered) {
mConsole->Reset();
}
return NS_OK;
}
private:
RefPtr<nsConsoleService> mConsole;
};
nsresult nsConsoleService::Init() {
NS_DispatchToMainThread(new AddConsolePrefWatchers(this));
return NS_OK;
}
nsresult nsConsoleService::MaybeForwardScriptError(nsIConsoleMessage* aMessage,
bool* sent) {
*sent = false;
nsCOMPtr<nsIScriptError> scriptError = do_QueryInterface(aMessage);
if (!scriptError) {
// Not an nsIScriptError
return NS_OK;
}
uint64_t windowID;
nsresult rv;
rv = scriptError->GetInnerWindowID(&windowID);
NS_ENSURE_SUCCESS(rv, rv);
if (!windowID) {
// Does not set window id
return NS_OK;
}
RefPtr<mozilla::dom::WindowGlobalParent> windowGlobalParent =
mozilla::dom::WindowGlobalParent::GetByInnerWindowId(windowID);
if (!windowGlobalParent) {
// Could not find parent window by id
return NS_OK;
}
RefPtr<mozilla::dom::BrowserParent> browserParent =
windowGlobalParent->GetBrowserParent();
if (!browserParent) {
return NS_OK;
}
mozilla::dom::ContentParent* contentParent = browserParent->Manager();
if (!contentParent) {
return NS_ERROR_FAILURE;
}
nsAutoString msg;
nsAutoCString sourceName;
nsCString category;
uint32_t lineNum, colNum, flags;
uint64_t innerWindowId;
bool fromPrivateWindow, fromChromeContext;
rv = scriptError->GetErrorMessage(msg);
NS_ENSURE_SUCCESS(rv, rv);
rv = scriptError->GetSourceName(sourceName);
NS_ENSURE_SUCCESS(rv, rv);
rv = scriptError->GetCategory(getter_Copies(category));
NS_ENSURE_SUCCESS(rv, rv);
rv = scriptError->GetLineNumber(&lineNum);
NS_ENSURE_SUCCESS(rv, rv);
rv = scriptError->GetColumnNumber(&colNum);
NS_ENSURE_SUCCESS(rv, rv);
rv = scriptError->GetFlags(&flags);
NS_ENSURE_SUCCESS(rv, rv);
rv = scriptError->GetIsFromPrivateWindow(&fromPrivateWindow);
NS_ENSURE_SUCCESS(rv, rv);
rv = scriptError->GetIsFromChromeContext(&fromChromeContext);
NS_ENSURE_SUCCESS(rv, rv);
rv = scriptError->GetInnerWindowID(&innerWindowId);
NS_ENSURE_SUCCESS(rv, rv);
*sent = contentParent->SendScriptError(msg, sourceName, lineNum, colNum,
flags, category, fromPrivateWindow,
innerWindowId, fromChromeContext);
return NS_OK;
}
namespace {
class LogMessageRunnable : public Runnable {
public:
LogMessageRunnable(nsIConsoleMessage* aMessage, nsConsoleService* aService)
: mozilla::Runnable("LogMessageRunnable"),
mMessage(aMessage),
mService(aService) {}
NS_DECL_NSIRUNNABLE
private:
nsCOMPtr<nsIConsoleMessage> mMessage;
RefPtr<nsConsoleService> mService;
};
NS_IMETHODIMP
LogMessageRunnable::Run() {
// Snapshot of listeners so that we don't reenter this hash during
// enumeration.
nsCOMArray<nsIConsoleListener> listeners;
mService->CollectCurrentListeners(listeners);
mService->SetIsDelivering();
for (int32_t i = 0; i < listeners.Count(); ++i) {
listeners[i]->Observe(mMessage);
}
mService->SetDoneDelivering();
return NS_OK;
}
} // namespace
// nsIConsoleService methods
NS_IMETHODIMP
nsConsoleService::LogMessage(nsIConsoleMessage* aMessage) {
return LogMessageWithMode(aMessage, nsIConsoleService::OutputToLog);
}
// This can be called off the main thread.
nsresult nsConsoleService::LogMessageWithMode(
nsIConsoleMessage* aMessage, nsIConsoleService::OutputMode aOutputMode) {
if (!aMessage) {
return NS_ERROR_INVALID_ARG;
}
if (!gLoggingEnabled) {
return NS_OK;
}
if (NS_IsMainThread() && mDeliveringMessage) {
nsCString msg;
aMessage->ToString(msg);
NS_WARNING(
nsPrintfCString(
"Reentrancy error: some client attempted to display a message to "
"the console while in a console listener. The following message "
"was discarded: \"%s\"",
msg.get())
.get());
return NS_ERROR_FAILURE;
}
if (XRE_IsParentProcess() && NS_IsMainThread()) {
// If mMessage is a scriptError with an innerWindowId set,
// forward it to the matching ContentParent
// This enables logging from parent to content process
bool sent;
nsresult rv = MaybeForwardScriptError(aMessage, &sent);
NS_ENSURE_SUCCESS(rv, rv);
if (sent) {
return NS_OK;
}
}
RefPtr<LogMessageRunnable> r;
nsCOMPtr<nsIConsoleMessage> retiredMessage;
/*
* Lock while updating buffer, and while taking snapshot of
* listeners array.
*/
{
MutexAutoLock lock(mLock);
#if defined(ANDROID)
if (StaticPrefs::consoleservice_logcat() && aOutputMode == OutputToLog) {
nsCString msg;
aMessage->ToString(msg);
/** Attempt to use the process name as the log tag. */
mozilla::dom::ContentChild* child =
mozilla::dom::ContentChild::GetSingleton();
nsCString appName;
if (child) {
child->GetProcessName(appName);
} else {
appName = "GeckoConsole";
}
uint32_t logLevel = 0;
aMessage->GetLogLevel(&logLevel);
android_LogPriority logPriority = ANDROID_LOG_INFO;
switch (logLevel) {
case nsIConsoleMessage::debug:
logPriority = ANDROID_LOG_DEBUG;
break;
case nsIConsoleMessage::info:
logPriority = ANDROID_LOG_INFO;
break;
case nsIConsoleMessage::warn:
logPriority = ANDROID_LOG_WARN;
break;
case nsIConsoleMessage::error:
logPriority = ANDROID_LOG_ERROR;
break;
}
__android_log_print(logPriority, appName.get(), "%s", msg.get());
}
#endif
#ifdef XP_WIN
if (gLoggingToDebugger && IsDebuggerPresent()) {
nsString msg;
aMessage->GetMessageMoz(msg);
msg.Append('\n');
OutputDebugStringW(msg.get());
}
#endif
if (gLoggingBuffered) {
MessageElement* e = new MessageElement(aMessage);
mMessages.insertBack(e);
if (mCurrentSize != mMaximumSize) {
mCurrentSize++;
} else {
MessageElement* p = mMessages.popFirst();
MOZ_ASSERT(p);
p->swapMessage(retiredMessage);
delete p;
}
}
if (mListeners.Count() > 0) {
r = new LogMessageRunnable(aMessage, this);
}
}
if (retiredMessage) {
// Release |retiredMessage| on the main thread in case it is an instance of
// a mainthread-only class like nsScriptErrorWithStack and we're off the
// main thread.
NS_ReleaseOnMainThread("nsConsoleService::retiredMessage",
retiredMessage.forget());
}
if (r) {
// avoid failing in XPCShell tests
nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
if (mainThread) {
SchedulerGroup::Dispatch(r.forget());
}
}
return NS_OK;
}
// See nsIConsoleService.idl for more info about this method
NS_IMETHODIMP
nsConsoleService::CallFunctionAndLogException(
JS::Handle<JS::Value> targetGlobal, JS::HandleValue function, JSContext* cx,
JS::MutableHandleValue retval) {
if (!targetGlobal.isObject() || !function.isObject()) {
return NS_ERROR_INVALID_ARG;
}
JS::Rooted<JS::Realm*> contextRealm(cx, JS::GetCurrentRealmOrNull(cx));
if (!contextRealm) {
return NS_ERROR_INVALID_ARG;
}
JS::Rooted<JSObject*> global(
cx, js::CheckedUnwrapDynamic(&targetGlobal.toObject(), cx));
if (!global) {
return NS_ERROR_INVALID_ARG;
}
// Use AutoJSAPI in order to trigger AutoJSAPI::ReportException
// which will do most of the work required for this function.
//
// We only have to pick the right global for which we want to flag
// the exception against.
dom::AutoJSAPI jsapi;
if (!jsapi.Init(global)) {
return NS_ERROR_UNEXPECTED;
}
JSContext* ccx = jsapi.cx();
// AutoJSAPI picks `targetGlobal` as execution compartment
// whereas we expect to run `function` from the callsites compartment.
JSAutoRealm ar(ccx, JS::GetRealmGlobalOrNull(contextRealm));
JS::RootedValue funVal(ccx, function);
if (!JS_WrapValue(ccx, &funVal)) {
return NS_ERROR_FAILURE;
}
if (!JS_CallFunctionValue(ccx, nullptr, funVal, JS::HandleValueArray::empty(),
retval)) {
return NS_ERROR_XPC_JAVASCRIPT_ERROR;
}
return NS_OK;
}
void nsConsoleService::CollectCurrentListeners(
nsCOMArray<nsIConsoleListener>& aListeners) {
MutexAutoLock lock(mLock);
// XXX When MakeBackInserter(nsCOMArray<T>&) is added, we can do:
// AppendToArray(aListeners, mListeners.Values());
for (const auto& listener : mListeners.Values()) {
aListeners.AppendObject(listener);
}
}
NS_IMETHODIMP
nsConsoleService::LogStringMessage(const char16_t* aMessage) {
if (!gLoggingEnabled) {
return NS_OK;
}
RefPtr<nsConsoleMessage> msg(new nsConsoleMessage(
aMessage ? nsDependentString(aMessage) : EmptyString()));
return LogMessage(msg);
}
NS_IMETHODIMP
nsConsoleService::GetMessageArray(
nsTArray<RefPtr<nsIConsoleMessage>>& aMessages) {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
MutexAutoLock lock(mLock);
if (mMessages.isEmpty()) {
return NS_OK;
}
MOZ_ASSERT(mCurrentSize <= mMaximumSize);
aMessages.SetCapacity(mCurrentSize);
for (MessageElement* e = mMessages.getFirst(); e != nullptr;
e = e->getNext()) {
aMessages.AppendElement(e->Get());
}
return NS_OK;
}
NS_IMETHODIMP
nsConsoleService::RegisterListener(nsIConsoleListener* aListener) {
if (!NS_IsMainThread()) {
NS_ERROR("nsConsoleService::RegisterListener is main thread only.");
return NS_ERROR_NOT_SAME_THREAD;
}
nsCOMPtr<nsISupports> canonical = do_QueryInterface(aListener);
MOZ_ASSERT(canonical);
MutexAutoLock lock(mLock);
return mListeners.WithEntryHandle(canonical, [&](auto&& entry) {
if (entry) {
// Reregistering a listener isn't good
return NS_ERROR_FAILURE;
}
entry.Insert(aListener);
return NS_OK;
});
}
NS_IMETHODIMP
nsConsoleService::UnregisterListener(nsIConsoleListener* aListener) {
if (!NS_IsMainThread()) {
NS_ERROR("nsConsoleService::UnregisterListener is main thread only.");
return NS_ERROR_NOT_SAME_THREAD;
}
nsCOMPtr<nsISupports> canonical = do_QueryInterface(aListener);
MutexAutoLock lock(mLock);
return mListeners.Remove(canonical)
? NS_OK
// Unregistering a listener that was never registered?
: NS_ERROR_FAILURE;
}
NS_IMETHODIMP
nsConsoleService::Reset() {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
/*
* Make sure nobody trips into the buffer while it's being reset
*/
MutexAutoLock lock(mLock);
ClearMessages();
return NS_OK;
}
NS_IMETHODIMP
nsConsoleService::ResetWindow(uint64_t windowInnerId) {
MOZ_RELEASE_ASSERT(NS_IsMainThread());
ClearMessagesForWindowID(windowInnerId);
return NS_OK;
}
NS_IMETHODIMP
nsConsoleService::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
// Dump all our messages, in case any are cycle collected.
Reset();
// We could remove ourselves from the observer service, but it is about to
// drop all observers anyways, so why bother.
} else if (!strcmp(aTopic, "inner-window-destroyed")) {
nsCOMPtr<nsISupportsPRUint64> supportsInt = do_QueryInterface(aSubject);
MOZ_ASSERT(supportsInt);
uint64_t windowId;
MOZ_ALWAYS_SUCCEEDS(supportsInt->GetData(&windowId));
ClearMessagesForWindowID(windowId);
} else {
MOZ_CRASH();
}
return NS_OK;
}