michael@0: /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /* michael@0: * Maintains a circular buffer of recent messages, and notifies michael@0: * listeners when new messages are logged. michael@0: */ michael@0: michael@0: /* Threadsafe. */ michael@0: michael@0: #include "nsMemory.h" michael@0: #include "nsCOMArray.h" michael@0: #include "nsThreadUtils.h" michael@0: michael@0: #include "nsConsoleService.h" michael@0: #include "nsConsoleMessage.h" michael@0: #include "nsIClassInfoImpl.h" michael@0: #include "nsIConsoleListener.h" michael@0: #include "nsPrintfCString.h" michael@0: michael@0: #include "mozilla/Preferences.h" michael@0: michael@0: #if defined(ANDROID) michael@0: #include michael@0: #endif michael@0: #ifdef XP_WIN michael@0: #include michael@0: #endif michael@0: michael@0: using namespace mozilla; michael@0: michael@0: NS_IMPL_ADDREF(nsConsoleService) michael@0: NS_IMPL_RELEASE(nsConsoleService) michael@0: NS_IMPL_CLASSINFO(nsConsoleService, nullptr, nsIClassInfo::THREADSAFE | nsIClassInfo::SINGLETON, NS_CONSOLESERVICE_CID) michael@0: NS_IMPL_QUERY_INTERFACE_CI(nsConsoleService, nsIConsoleService) michael@0: NS_IMPL_CI_INTERFACE_GETTER(nsConsoleService, nsIConsoleService) michael@0: michael@0: static bool sLoggingEnabled = true; michael@0: static bool sLoggingBuffered = true; michael@0: michael@0: nsConsoleService::nsConsoleService() michael@0: : mMessages(nullptr) michael@0: , mCurrent(0) michael@0: , mFull(false) michael@0: , mDeliveringMessage(false) michael@0: , mLock("nsConsoleService.mLock") michael@0: { michael@0: // XXX grab this from a pref! michael@0: // hm, but worry about circularity, bc we want to be able to report michael@0: // prefs errs... michael@0: mBufferSize = 250; michael@0: } michael@0: michael@0: nsConsoleService::~nsConsoleService() michael@0: { michael@0: uint32_t i = 0; michael@0: while (i < mBufferSize && mMessages[i] != nullptr) { michael@0: NS_RELEASE(mMessages[i]); michael@0: i++; michael@0: } michael@0: michael@0: if (mMessages) michael@0: nsMemory::Free(mMessages); michael@0: } michael@0: michael@0: class AddConsolePrefWatchers : public nsRunnable michael@0: { michael@0: public: michael@0: AddConsolePrefWatchers(nsConsoleService* aConsole) : mConsole(aConsole) {} michael@0: michael@0: NS_IMETHOD Run() michael@0: { michael@0: Preferences::AddBoolVarCache(&sLoggingEnabled, "consoleservice.enabled", true); michael@0: Preferences::AddBoolVarCache(&sLoggingBuffered, "consoleservice.buffered", true); michael@0: if (!sLoggingBuffered) { michael@0: mConsole->Reset(); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: private: michael@0: nsRefPtr mConsole; michael@0: }; michael@0: michael@0: nsresult michael@0: nsConsoleService::Init() michael@0: { michael@0: mMessages = (nsIConsoleMessage **) michael@0: nsMemory::Alloc(mBufferSize * sizeof(nsIConsoleMessage *)); michael@0: if (!mMessages) michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: michael@0: // Array elements should be 0 initially for circular buffer algorithm. michael@0: memset(mMessages, 0, mBufferSize * sizeof(nsIConsoleMessage *)); michael@0: michael@0: NS_DispatchToMainThread(new AddConsolePrefWatchers(this)); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: namespace { michael@0: michael@0: class LogMessageRunnable : public nsRunnable michael@0: { michael@0: public: michael@0: LogMessageRunnable(nsIConsoleMessage* message, nsConsoleService* service) michael@0: : mMessage(message) michael@0: , mService(service) michael@0: { } michael@0: michael@0: NS_DECL_NSIRUNNABLE michael@0: michael@0: private: michael@0: nsCOMPtr mMessage; michael@0: nsRefPtr mService; michael@0: }; michael@0: michael@0: typedef nsCOMArray ListenerArrayType; michael@0: michael@0: PLDHashOperator michael@0: CollectCurrentListeners(nsISupports* aKey, nsIConsoleListener* aValue, michael@0: void* closure) michael@0: { michael@0: ListenerArrayType* listeners = static_cast(closure); michael@0: listeners->AppendObject(aValue); michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: LogMessageRunnable::Run() michael@0: { michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: michael@0: // Snapshot of listeners so that we don't reenter this hash during michael@0: // enumeration. michael@0: nsCOMArray listeners; michael@0: mService->EnumerateListeners(CollectCurrentListeners, &listeners); michael@0: michael@0: mService->SetIsDelivering(); michael@0: michael@0: for (int32_t i = 0; i < listeners.Count(); ++i) michael@0: listeners[i]->Observe(mMessage); michael@0: michael@0: mService->SetDoneDelivering(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: } // anonymous namespace michael@0: michael@0: // nsIConsoleService methods michael@0: NS_IMETHODIMP michael@0: nsConsoleService::LogMessage(nsIConsoleMessage *message) michael@0: { michael@0: return LogMessageWithMode(message, OutputToLog); michael@0: } michael@0: michael@0: nsresult michael@0: nsConsoleService::LogMessageWithMode(nsIConsoleMessage *message, nsConsoleService::OutputMode outputMode) michael@0: { michael@0: if (message == nullptr) michael@0: return NS_ERROR_INVALID_ARG; michael@0: michael@0: if (!sLoggingEnabled) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: if (NS_IsMainThread() && mDeliveringMessage) { michael@0: nsCString msg; michael@0: message->ToString(msg); michael@0: NS_WARNING(nsPrintfCString("Reentrancy error: some client attempted " michael@0: "to display a message to the console while in a console listener. " michael@0: "The following message was discarded: \"%s\"", msg.get()).get()); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: nsRefPtr r; michael@0: nsIConsoleMessage *retiredMessage; michael@0: michael@0: if (sLoggingBuffered) { michael@0: NS_ADDREF(message); // early, in case it's same as replaced below. michael@0: } michael@0: michael@0: /* michael@0: * Lock while updating buffer, and while taking snapshot of michael@0: * listeners array. michael@0: */ michael@0: { michael@0: MutexAutoLock lock(mLock); michael@0: michael@0: #if defined(ANDROID) michael@0: if (outputMode == OutputToLog) michael@0: { michael@0: nsCString msg; michael@0: message->ToString(msg); michael@0: __android_log_print(ANDROID_LOG_ERROR, "GeckoConsole", michael@0: "%s", msg.get()); michael@0: } michael@0: #endif michael@0: #ifdef XP_WIN michael@0: if (IsDebuggerPresent()) { michael@0: nsString msg; michael@0: message->GetMessageMoz(getter_Copies(msg)); michael@0: msg.AppendLiteral("\n"); michael@0: OutputDebugStringW(msg.get()); michael@0: } michael@0: #endif michael@0: michael@0: /* michael@0: * If there's already a message in the slot we're about to replace, michael@0: * we've wrapped around, and we need to release the old message. We michael@0: * save a pointer to it, so we can release below outside the lock. michael@0: */ michael@0: retiredMessage = mMessages[mCurrent]; michael@0: michael@0: if (sLoggingBuffered) { michael@0: mMessages[mCurrent++] = message; michael@0: if (mCurrent == mBufferSize) { michael@0: mCurrent = 0; // wrap around. michael@0: mFull = true; michael@0: } michael@0: } michael@0: michael@0: if (mListeners.Count() > 0) { michael@0: r = new LogMessageRunnable(message, this); michael@0: } michael@0: } michael@0: michael@0: if (retiredMessage != nullptr) michael@0: NS_RELEASE(retiredMessage); michael@0: michael@0: if (r) michael@0: NS_DispatchToMainThread(r); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: nsConsoleService::EnumerateListeners(ListenerHash::EnumReadFunction aFunction, michael@0: void* aClosure) michael@0: { michael@0: MutexAutoLock lock(mLock); michael@0: mListeners.EnumerateRead(aFunction, aClosure); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsConsoleService::LogStringMessage(const char16_t *message) michael@0: { michael@0: if (!sLoggingEnabled) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsRefPtr msg(new nsConsoleMessage(message)); michael@0: return this->LogMessage(msg); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsConsoleService::GetMessageArray(uint32_t *count, nsIConsoleMessage ***messages) michael@0: { michael@0: nsIConsoleMessage **messageArray; michael@0: michael@0: /* michael@0: * Lock the whole method, as we don't want anyone mucking with mCurrent or michael@0: * mFull while we're copying out the buffer. michael@0: */ michael@0: MutexAutoLock lock(mLock); michael@0: michael@0: if (mCurrent == 0 && !mFull) { michael@0: /* michael@0: * Make a 1-length output array so that nobody gets confused, michael@0: * and return a count of 0. This should result in a 0-length michael@0: * array object when called from script. michael@0: */ michael@0: messageArray = (nsIConsoleMessage **) michael@0: nsMemory::Alloc(sizeof (nsIConsoleMessage *)); michael@0: *messageArray = nullptr; michael@0: *messages = messageArray; michael@0: *count = 0; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: uint32_t resultSize = mFull ? mBufferSize : mCurrent; michael@0: messageArray = michael@0: (nsIConsoleMessage **)nsMemory::Alloc((sizeof (nsIConsoleMessage *)) michael@0: * resultSize); michael@0: michael@0: if (messageArray == nullptr) { michael@0: *messages = nullptr; michael@0: *count = 0; michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: uint32_t i; michael@0: if (mFull) { michael@0: for (i = 0; i < mBufferSize; i++) { michael@0: // if full, fill the buffer starting from mCurrent (which'll be michael@0: // oldest) wrapping around the buffer to the most recent. michael@0: messageArray[i] = mMessages[(mCurrent + i) % mBufferSize]; michael@0: NS_ADDREF(messageArray[i]); michael@0: } michael@0: } else { michael@0: for (i = 0; i < mCurrent; i++) { michael@0: messageArray[i] = mMessages[i]; michael@0: NS_ADDREF(messageArray[i]); michael@0: } michael@0: } michael@0: *count = resultSize; michael@0: *messages = messageArray; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsConsoleService::RegisterListener(nsIConsoleListener *listener) michael@0: { michael@0: if (!NS_IsMainThread()) { michael@0: NS_ERROR("nsConsoleService::RegisterListener is main thread only."); michael@0: return NS_ERROR_NOT_SAME_THREAD; michael@0: } michael@0: michael@0: nsCOMPtr canonical = do_QueryInterface(listener); michael@0: michael@0: MutexAutoLock lock(mLock); michael@0: if (mListeners.GetWeak(canonical)) { michael@0: // Reregistering a listener isn't good michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: mListeners.Put(canonical, listener); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsConsoleService::UnregisterListener(nsIConsoleListener *listener) michael@0: { michael@0: if (!NS_IsMainThread()) { michael@0: NS_ERROR("nsConsoleService::UnregisterListener is main thread only."); michael@0: return NS_ERROR_NOT_SAME_THREAD; michael@0: } michael@0: michael@0: nsCOMPtr canonical = do_QueryInterface(listener); michael@0: michael@0: MutexAutoLock lock(mLock); michael@0: michael@0: if (!mListeners.GetWeak(canonical)) { michael@0: // Unregistering a listener that was never registered? michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: mListeners.Remove(canonical); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsConsoleService::Reset() michael@0: { michael@0: /* michael@0: * Make sure nobody trips into the buffer while it's being reset michael@0: */ michael@0: MutexAutoLock lock(mLock); michael@0: michael@0: mCurrent = 0; michael@0: mFull = false; michael@0: michael@0: /* michael@0: * Free all messages stored so far (cf. destructor) michael@0: */ michael@0: for (uint32_t i = 0; i < mBufferSize && mMessages[i] != nullptr; i++) michael@0: NS_RELEASE(mMessages[i]); michael@0: michael@0: return NS_OK; michael@0: }