mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-20 16:55:40 +00:00
321347c110
Backed out changeset b2b7b46c8ad0 (bug 1419771) Backed out changeset 0206657d2ea2 (bug 1419771) Backed out changeset f0f4b98a07b6 (bug 1419771) Backed out changeset 4b52904694f4 (bug 1419771) Backed out changeset 9f40cc12d6c6 (bug 1419771) Backed out changeset f500a61f564c (bug 1419771) Backed out changeset 0bd9d964df8a (bug 1419771) Backed out changeset 1397a6bbb446 (bug 1419771) Backed out changeset 7e77a00fa8b5 (bug 1419771) Backed out changeset 5f6df771459a (bug 1419771) Backed out changeset 7624e70b2965 (bug 1419771) Backed out changeset b9d674bdc723 (bug 1419771) Backed out changeset 5e44aeda4196 (bug 1419771) Backed out changeset 601b49f51b41 (bug 1419771) Backed out changeset d12dc5557982 (bug 1419771) Backed out changeset 6c863ab2e986 (bug 1419771) Backed out changeset 0866d79873ab (bug 1419771) Backed out changeset 8ecc91474621 (bug 1419771) Backed out changeset 19b14deed8fe (bug 1419771) Backed out changeset 000c8d5fbc03 (bug 1419771) Backed out changeset 2e263a2519c5 (bug 1419771) Backed out changeset b628d9298be8 (bug 1419771)
2497 lines
66 KiB
C++
2497 lines
66 KiB
C++
/* -*- 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/. */
|
|
|
|
#include "mozilla/dom/Console.h"
|
|
#include "mozilla/dom/ConsoleBinding.h"
|
|
|
|
#include "mozilla/dom/BlobBinding.h"
|
|
#include "mozilla/dom/Exceptions.h"
|
|
#include "mozilla/dom/File.h"
|
|
#include "mozilla/dom/FunctionBinding.h"
|
|
#include "mozilla/dom/Performance.h"
|
|
#include "mozilla/dom/ScriptSettings.h"
|
|
#include "mozilla/dom/StructuredCloneHolder.h"
|
|
#include "mozilla/dom/ToJSValue.h"
|
|
#include "mozilla/dom/WorkletGlobalScope.h"
|
|
#include "mozilla/Maybe.h"
|
|
#include "nsCycleCollectionParticipant.h"
|
|
#include "nsDocument.h"
|
|
#include "nsDOMNavigationTiming.h"
|
|
#include "nsGlobalWindow.h"
|
|
#include "nsJSUtils.h"
|
|
#include "nsNetUtil.h"
|
|
#include "WorkerPrivate.h"
|
|
#include "WorkerRunnable.h"
|
|
#include "WorkerScope.h"
|
|
#include "xpcpublic.h"
|
|
#include "nsContentUtils.h"
|
|
#include "nsDocShell.h"
|
|
#include "nsProxyRelease.h"
|
|
#include "mozilla/ConsoleTimelineMarker.h"
|
|
#include "mozilla/TimestampTimelineMarker.h"
|
|
|
|
#include "nsIConsoleAPIStorage.h"
|
|
#include "nsIDOMWindowUtils.h"
|
|
#include "nsIInterfaceRequestorUtils.h"
|
|
#include "nsILoadContext.h"
|
|
#include "nsISensitiveInfoHiddenURI.h"
|
|
#include "nsIServiceManager.h"
|
|
#include "nsISupportsPrimitives.h"
|
|
#include "nsIWebNavigation.h"
|
|
#include "nsIXPConnect.h"
|
|
|
|
// The maximum allowed number of concurrent timers per page.
|
|
#define MAX_PAGE_TIMERS 10000
|
|
|
|
// The maximum allowed number of concurrent counters per page.
|
|
#define MAX_PAGE_COUNTERS 10000
|
|
|
|
// The maximum stacktrace depth when populating the stacktrace array used for
|
|
// console.trace().
|
|
#define DEFAULT_MAX_STACKTRACE_DEPTH 200
|
|
|
|
// This tags are used in the Structured Clone Algorithm to move js values from
|
|
// worker thread to main thread
|
|
#define CONSOLE_TAG_BLOB JS_SCTAG_USER_MIN
|
|
|
|
// This value is taken from ConsoleAPIStorage.js
|
|
#define STORAGE_MAX_EVENTS 1000
|
|
|
|
using namespace mozilla::dom::exceptions;
|
|
using namespace mozilla::dom::workers;
|
|
|
|
namespace mozilla {
|
|
namespace dom {
|
|
|
|
struct
|
|
ConsoleStructuredCloneData
|
|
{
|
|
nsCOMPtr<nsISupports> mParent;
|
|
nsTArray<RefPtr<BlobImpl>> mBlobs;
|
|
};
|
|
|
|
/**
|
|
* Console API in workers uses the Structured Clone Algorithm to move any value
|
|
* from the worker thread to the main-thread. Some object cannot be moved and,
|
|
* in these cases, we convert them to strings.
|
|
* It's not the best, but at least we are able to show something.
|
|
*/
|
|
|
|
class ConsoleCallData final
|
|
{
|
|
public:
|
|
NS_INLINE_DECL_REFCOUNTING(ConsoleCallData)
|
|
|
|
ConsoleCallData()
|
|
: mMethodName(Console::MethodLog)
|
|
, mTimeStamp(JS_Now() / PR_USEC_PER_MSEC)
|
|
, mStartTimerValue(0)
|
|
, mStartTimerStatus(Console::eTimerUnknown)
|
|
, mStopTimerDuration(0)
|
|
, mStopTimerStatus(Console::eTimerUnknown)
|
|
, mCountValue(MAX_PAGE_COUNTERS)
|
|
, mIDType(eUnknown)
|
|
, mOuterIDNumber(0)
|
|
, mInnerIDNumber(0)
|
|
, mStatus(eUnused)
|
|
{}
|
|
|
|
bool
|
|
Initialize(JSContext* aCx, Console::MethodName aName,
|
|
const nsAString& aString,
|
|
const Sequence<JS::Value>& aArguments,
|
|
Console* aConsole)
|
|
{
|
|
AssertIsOnOwningThread();
|
|
MOZ_ASSERT(aConsole);
|
|
|
|
// We must be registered before doing any JS operation otherwise it can
|
|
// happen that mCopiedArguments are not correctly traced.
|
|
aConsole->StoreCallData(this);
|
|
|
|
mMethodName = aName;
|
|
mMethodString = aString;
|
|
|
|
mGlobal = JS::CurrentGlobalOrNull(aCx);
|
|
|
|
for (uint32_t i = 0; i < aArguments.Length(); ++i) {
|
|
if (NS_WARN_IF(!mCopiedArguments.AppendElement(aArguments[i]))) {
|
|
aConsole->UnstoreCallData(this);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
SetIDs(uint64_t aOuterID, uint64_t aInnerID)
|
|
{
|
|
MOZ_ASSERT(mIDType == eUnknown);
|
|
|
|
mOuterIDNumber = aOuterID;
|
|
mInnerIDNumber = aInnerID;
|
|
mIDType = eNumber;
|
|
}
|
|
|
|
void
|
|
SetIDs(const nsAString& aOuterID, const nsAString& aInnerID)
|
|
{
|
|
MOZ_ASSERT(mIDType == eUnknown);
|
|
|
|
mOuterIDString = aOuterID;
|
|
mInnerIDString = aInnerID;
|
|
mIDType = eString;
|
|
}
|
|
|
|
void
|
|
SetOriginAttributes(const OriginAttributes& aOriginAttributes)
|
|
{
|
|
mOriginAttributes = aOriginAttributes;
|
|
}
|
|
|
|
void
|
|
SetAddonId(nsIPrincipal* aPrincipal)
|
|
{
|
|
nsAutoString addonId;
|
|
aPrincipal->GetAddonId(addonId);
|
|
|
|
mAddonId = addonId;
|
|
}
|
|
|
|
bool
|
|
PopulateArgumentsSequence(Sequence<JS::Value>& aSequence) const
|
|
{
|
|
AssertIsOnOwningThread();
|
|
|
|
for (uint32_t i = 0; i < mCopiedArguments.Length(); ++i) {
|
|
if (NS_WARN_IF(!aSequence.AppendElement(mCopiedArguments[i],
|
|
fallible))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
Trace(const TraceCallbacks& aCallbacks, void* aClosure)
|
|
{
|
|
ConsoleCallData* tmp = this;
|
|
for (uint32_t i = 0; i < mCopiedArguments.Length(); ++i) {
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCopiedArguments[i])
|
|
}
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mGlobal);
|
|
}
|
|
|
|
void
|
|
AssertIsOnOwningThread() const
|
|
{
|
|
NS_ASSERT_OWNINGTHREAD(ConsoleCallData);
|
|
}
|
|
|
|
JS::Heap<JSObject*> mGlobal;
|
|
|
|
// This is a copy of the arguments we received from the DOM bindings. Console
|
|
// object traces them because this ConsoleCallData calls
|
|
// RegisterConsoleCallData() in the Initialize().
|
|
nsTArray<JS::Heap<JS::Value>> mCopiedArguments;
|
|
|
|
Console::MethodName mMethodName;
|
|
int64_t mTimeStamp;
|
|
|
|
// These values are set in the owning thread and they contain the timestamp of
|
|
// when the new timer has started, the name of it and the status of the
|
|
// creation of it. If status is false, something went wrong. User
|
|
// DOMHighResTimeStamp instead mozilla::TimeStamp because we use
|
|
// monotonicTimer from Performance.now();
|
|
// They will be set on the owning thread and never touched again on that
|
|
// thread. They will be used in order to create a ConsoleTimerStart dictionary
|
|
// when console.time() is used.
|
|
DOMHighResTimeStamp mStartTimerValue;
|
|
nsString mStartTimerLabel;
|
|
Console::TimerStatus mStartTimerStatus;
|
|
|
|
// These values are set in the owning thread and they contain the duration,
|
|
// the name and the status of the StopTimer method. If status is false,
|
|
// something went wrong. They will be set on the owning thread and never
|
|
// touched again on that thread. They will be used in order to create a
|
|
// ConsoleTimerEnd dictionary. This members are set when
|
|
// console.timeEnd() is called.
|
|
double mStopTimerDuration;
|
|
nsString mStopTimerLabel;
|
|
Console::TimerStatus mStopTimerStatus;
|
|
|
|
// These 2 values are set by IncreaseCounter on the owning thread and they are
|
|
// used CreateCounterValue. These members are set when console.count() is
|
|
// called.
|
|
nsString mCountLabel;
|
|
uint32_t mCountValue;
|
|
|
|
// The concept of outerID and innerID is misleading because when a
|
|
// ConsoleCallData is created from a window, these are the window IDs, but
|
|
// when the object is created from a SharedWorker, a ServiceWorker or a
|
|
// subworker of a ChromeWorker these IDs are the type of worker and the
|
|
// filename of the callee.
|
|
// In Console.jsm the ID is 'jsm'.
|
|
enum {
|
|
eString,
|
|
eNumber,
|
|
eUnknown
|
|
} mIDType;
|
|
|
|
uint64_t mOuterIDNumber;
|
|
nsString mOuterIDString;
|
|
|
|
uint64_t mInnerIDNumber;
|
|
nsString mInnerIDString;
|
|
|
|
OriginAttributes mOriginAttributes;
|
|
|
|
nsString mAddonId;
|
|
|
|
nsString mMethodString;
|
|
|
|
// Stack management is complicated, because we want to do it as
|
|
// lazily as possible. Therefore, we have the following behavior:
|
|
// 1) mTopStackFrame is initialized whenever we have any JS on the stack
|
|
// 2) mReifiedStack is initialized if we're created in a worker.
|
|
// 3) mStack is set (possibly to null if there is no JS on the stack) if
|
|
// we're created on main thread.
|
|
Maybe<ConsoleStackEntry> mTopStackFrame;
|
|
Maybe<nsTArray<ConsoleStackEntry>> mReifiedStack;
|
|
nsCOMPtr<nsIStackFrame> mStack;
|
|
|
|
// mStatus is about the lifetime of this object. Console must take care of
|
|
// keep it alive or not following this enumeration.
|
|
enum {
|
|
// If the object is created but it is owned by some runnable, this is its
|
|
// status. It can be deleted at any time.
|
|
eUnused,
|
|
|
|
// When a runnable takes ownership of a ConsoleCallData and send it to
|
|
// different thread, this is its status. Console cannot delete it at this
|
|
// time.
|
|
eInUse,
|
|
|
|
// When a runnable owns this ConsoleCallData, we can't delete it directly.
|
|
// instead, we mark it with this new status and we move it in
|
|
// mCallDataStoragePending list in order to keep it alive an trace it
|
|
// correctly. Once the runnable finishs its task, it will delete this object
|
|
// calling ReleaseCallData().
|
|
eToBeDeleted
|
|
} mStatus;
|
|
|
|
private:
|
|
~ConsoleCallData()
|
|
{
|
|
AssertIsOnOwningThread();
|
|
MOZ_ASSERT(mStatus != eInUse);
|
|
}
|
|
};
|
|
|
|
// This class is used to clear any exception at the end of this method.
|
|
class ClearException
|
|
{
|
|
public:
|
|
explicit ClearException(JSContext* aCx)
|
|
: mCx(aCx)
|
|
{
|
|
}
|
|
|
|
~ClearException()
|
|
{
|
|
JS_ClearPendingException(mCx);
|
|
}
|
|
|
|
private:
|
|
JSContext* mCx;
|
|
};
|
|
|
|
class ConsoleRunnable : public WorkerProxyToMainThreadRunnable
|
|
, public StructuredCloneHolderBase
|
|
{
|
|
public:
|
|
explicit ConsoleRunnable(Console* aConsole)
|
|
: WorkerProxyToMainThreadRunnable(GetCurrentThreadWorkerPrivate())
|
|
, mConsole(aConsole)
|
|
{}
|
|
|
|
virtual
|
|
~ConsoleRunnable()
|
|
{
|
|
// Clear the StructuredCloneHolderBase class.
|
|
Clear();
|
|
}
|
|
|
|
bool
|
|
Dispatch(JSContext* aCx)
|
|
{
|
|
mWorkerPrivate->AssertIsOnWorkerThread();
|
|
|
|
if (NS_WARN_IF(!PreDispatch(aCx))) {
|
|
RunBackOnWorkerThreadForCleanup();
|
|
return false;
|
|
}
|
|
|
|
if (NS_WARN_IF(!WorkerProxyToMainThreadRunnable::Dispatch())) {
|
|
// RunBackOnWorkerThreadForCleanup() will be called by
|
|
// WorkerProxyToMainThreadRunnable::Dispatch().
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected:
|
|
void
|
|
RunOnMainThread() override
|
|
{
|
|
AssertIsOnMainThread();
|
|
|
|
// Walk up to our containing page
|
|
WorkerPrivate* wp = mWorkerPrivate;
|
|
while (wp->GetParent()) {
|
|
wp = wp->GetParent();
|
|
}
|
|
|
|
nsPIDOMWindowInner* window = wp->GetWindow();
|
|
if (!window) {
|
|
RunWindowless();
|
|
} else {
|
|
RunWithWindow(window);
|
|
}
|
|
}
|
|
|
|
void
|
|
RunWithWindow(nsPIDOMWindowInner* aWindow)
|
|
{
|
|
AssertIsOnMainThread();
|
|
|
|
AutoJSAPI jsapi;
|
|
MOZ_ASSERT(aWindow);
|
|
|
|
RefPtr<nsGlobalWindowInner> win = nsGlobalWindowInner::Cast(aWindow);
|
|
if (NS_WARN_IF(!jsapi.Init(win))) {
|
|
return;
|
|
}
|
|
|
|
nsPIDOMWindowOuter* outerWindow = aWindow->GetOuterWindow();
|
|
if (NS_WARN_IF(!outerWindow)) {
|
|
return;
|
|
}
|
|
|
|
RunConsole(jsapi.cx(), outerWindow, aWindow);
|
|
}
|
|
|
|
void
|
|
RunWindowless()
|
|
{
|
|
AssertIsOnMainThread();
|
|
|
|
WorkerPrivate* wp = mWorkerPrivate;
|
|
while (wp->GetParent()) {
|
|
wp = wp->GetParent();
|
|
}
|
|
|
|
MOZ_ASSERT(!wp->GetWindow());
|
|
|
|
AutoJSAPI jsapi;
|
|
jsapi.Init();
|
|
|
|
JSContext* cx = jsapi.cx();
|
|
|
|
JS::Rooted<JSObject*> global(cx, mConsole->GetOrCreateSandbox(cx, wp->GetPrincipal()));
|
|
if (NS_WARN_IF(!global)) {
|
|
return;
|
|
}
|
|
|
|
// The GetOrCreateSandbox call returns a proxy to the actual sandbox object.
|
|
// We don't need a proxy here.
|
|
global = js::UncheckedUnwrap(global);
|
|
|
|
JSAutoCompartment ac(cx, global);
|
|
|
|
RunConsole(cx, nullptr, nullptr);
|
|
}
|
|
|
|
void
|
|
RunBackOnWorkerThreadForCleanup() override
|
|
{
|
|
mWorkerPrivate->AssertIsOnWorkerThread();
|
|
ReleaseData();
|
|
mConsole = nullptr;
|
|
}
|
|
|
|
// This method is called in the owning thread of the Console object.
|
|
virtual bool
|
|
PreDispatch(JSContext* aCx) = 0;
|
|
|
|
// This method is called in the main-thread.
|
|
virtual void
|
|
RunConsole(JSContext* aCx, nsPIDOMWindowOuter* aOuterWindow,
|
|
nsPIDOMWindowInner* aInnerWindow) = 0;
|
|
|
|
// This method is called in the owning thread of the Console object.
|
|
virtual void
|
|
ReleaseData() = 0;
|
|
|
|
virtual JSObject* CustomReadHandler(JSContext* aCx,
|
|
JSStructuredCloneReader* aReader,
|
|
uint32_t aTag,
|
|
uint32_t aIndex) override
|
|
{
|
|
AssertIsOnMainThread();
|
|
|
|
if (aTag == CONSOLE_TAG_BLOB) {
|
|
MOZ_ASSERT(mClonedData.mBlobs.Length() > aIndex);
|
|
|
|
JS::Rooted<JS::Value> val(aCx);
|
|
{
|
|
RefPtr<Blob> blob =
|
|
Blob::Create(mClonedData.mParent, mClonedData.mBlobs.ElementAt(aIndex));
|
|
if (!ToJSValue(aCx, blob, &val)) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return &val.toObject();
|
|
}
|
|
|
|
MOZ_CRASH("No other tags are supported.");
|
|
return nullptr;
|
|
}
|
|
|
|
virtual bool CustomWriteHandler(JSContext* aCx,
|
|
JSStructuredCloneWriter* aWriter,
|
|
JS::Handle<JSObject*> aObj) override
|
|
{
|
|
RefPtr<Blob> blob;
|
|
if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, aObj, blob)) &&
|
|
blob->Impl()->MayBeClonedToOtherThreads()) {
|
|
if (NS_WARN_IF(!JS_WriteUint32Pair(aWriter, CONSOLE_TAG_BLOB,
|
|
mClonedData.mBlobs.Length()))) {
|
|
return false;
|
|
}
|
|
|
|
mClonedData.mBlobs.AppendElement(blob->Impl());
|
|
return true;
|
|
}
|
|
|
|
if (!JS_ObjectNotWritten(aWriter, aObj)) {
|
|
return false;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> value(aCx, JS::ObjectOrNullValue(aObj));
|
|
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value));
|
|
if (NS_WARN_IF(!jsString)) {
|
|
return false;
|
|
}
|
|
|
|
if (NS_WARN_IF(!JS_WriteString(aWriter, jsString))) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// This must be released on the worker thread.
|
|
RefPtr<Console> mConsole;
|
|
|
|
ConsoleStructuredCloneData mClonedData;
|
|
};
|
|
|
|
// This runnable appends a CallData object into the Console queue running on
|
|
// the main-thread.
|
|
class ConsoleCallDataRunnable final : public ConsoleRunnable
|
|
{
|
|
public:
|
|
ConsoleCallDataRunnable(Console* aConsole,
|
|
ConsoleCallData* aCallData)
|
|
: ConsoleRunnable(aConsole)
|
|
, mCallData(aCallData)
|
|
{
|
|
MOZ_ASSERT(aCallData);
|
|
mWorkerPrivate->AssertIsOnWorkerThread();
|
|
mCallData->AssertIsOnOwningThread();
|
|
}
|
|
|
|
private:
|
|
~ConsoleCallDataRunnable()
|
|
{
|
|
MOZ_ASSERT(!mCallData);
|
|
}
|
|
|
|
bool
|
|
PreDispatch(JSContext* aCx) override
|
|
{
|
|
mWorkerPrivate->AssertIsOnWorkerThread();
|
|
mCallData->AssertIsOnOwningThread();
|
|
|
|
ClearException ce(aCx);
|
|
|
|
JS::Rooted<JSObject*> arguments(aCx,
|
|
JS_NewArrayObject(aCx, mCallData->mCopiedArguments.Length()));
|
|
if (NS_WARN_IF(!arguments)) {
|
|
return false;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> arg(aCx);
|
|
for (uint32_t i = 0; i < mCallData->mCopiedArguments.Length(); ++i) {
|
|
arg = mCallData->mCopiedArguments[i];
|
|
if (NS_WARN_IF(!JS_DefineElement(aCx, arguments, i, arg,
|
|
JSPROP_ENUMERATE))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
JS::Rooted<JS::Value> value(aCx, JS::ObjectValue(*arguments));
|
|
|
|
if (NS_WARN_IF(!Write(aCx, value))) {
|
|
return false;
|
|
}
|
|
|
|
mCallData->mStatus = ConsoleCallData::eInUse;
|
|
return true;
|
|
}
|
|
|
|
void
|
|
RunConsole(JSContext* aCx, nsPIDOMWindowOuter* aOuterWindow,
|
|
nsPIDOMWindowInner* aInnerWindow) override
|
|
{
|
|
AssertIsOnMainThread();
|
|
|
|
// The windows have to run in parallel.
|
|
MOZ_ASSERT(!!aOuterWindow == !!aInnerWindow);
|
|
|
|
if (aOuterWindow) {
|
|
mCallData->SetIDs(aOuterWindow->WindowID(), aInnerWindow->WindowID());
|
|
} else {
|
|
ConsoleStackEntry frame;
|
|
if (mCallData->mTopStackFrame) {
|
|
frame = *mCallData->mTopStackFrame;
|
|
}
|
|
|
|
nsString id = frame.mFilename;
|
|
nsString innerID;
|
|
if (mWorkerPrivate->IsSharedWorker()) {
|
|
innerID = NS_LITERAL_STRING("SharedWorker");
|
|
} else if (mWorkerPrivate->IsServiceWorker()) {
|
|
innerID = NS_LITERAL_STRING("ServiceWorker");
|
|
// Use scope as ID so the webconsole can decide if the message should
|
|
// show up per tab
|
|
CopyASCIItoUTF16(mWorkerPrivate->ServiceWorkerScope(), id);
|
|
} else {
|
|
innerID = NS_LITERAL_STRING("Worker");
|
|
}
|
|
|
|
mCallData->SetIDs(id, innerID);
|
|
}
|
|
|
|
// Now we could have the correct window (if we are not window-less).
|
|
mClonedData.mParent = aInnerWindow;
|
|
|
|
ProcessCallData(aCx);
|
|
|
|
mClonedData.mParent = nullptr;
|
|
}
|
|
|
|
virtual void
|
|
ReleaseData() override
|
|
{
|
|
mConsole->AssertIsOnOwningThread();
|
|
|
|
if (mCallData->mStatus == ConsoleCallData::eToBeDeleted) {
|
|
mConsole->ReleaseCallData(mCallData);
|
|
} else {
|
|
MOZ_ASSERT(mCallData->mStatus == ConsoleCallData::eInUse);
|
|
mCallData->mStatus = ConsoleCallData::eUnused;
|
|
}
|
|
|
|
mCallData = nullptr;
|
|
}
|
|
|
|
void
|
|
ProcessCallData(JSContext* aCx)
|
|
{
|
|
AssertIsOnMainThread();
|
|
|
|
ClearException ce(aCx);
|
|
|
|
JS::Rooted<JS::Value> argumentsValue(aCx);
|
|
if (!Read(aCx, &argumentsValue)) {
|
|
return;
|
|
}
|
|
|
|
MOZ_ASSERT(argumentsValue.isObject());
|
|
|
|
JS::Rooted<JSObject*> argumentsObj(aCx, &argumentsValue.toObject());
|
|
|
|
uint32_t length;
|
|
if (!JS_GetArrayLength(aCx, argumentsObj, &length)) {
|
|
return;
|
|
}
|
|
|
|
Sequence<JS::Value> values;
|
|
SequenceRooter<JS::Value> arguments(aCx, &values);
|
|
|
|
for (uint32_t i = 0; i < length; ++i) {
|
|
JS::Rooted<JS::Value> value(aCx);
|
|
|
|
if (!JS_GetElement(aCx, argumentsObj, i, &value)) {
|
|
return;
|
|
}
|
|
|
|
if (!values.AppendElement(value, fallible)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
MOZ_ASSERT(values.Length() == length);
|
|
|
|
mConsole->ProcessCallData(aCx, mCallData, values);
|
|
}
|
|
|
|
RefPtr<ConsoleCallData> mCallData;
|
|
};
|
|
|
|
// This runnable calls ProfileMethod() on the console on the main-thread.
|
|
class ConsoleProfileRunnable final : public ConsoleRunnable
|
|
{
|
|
public:
|
|
ConsoleProfileRunnable(Console* aConsole, const nsAString& aAction,
|
|
const Sequence<JS::Value>& aArguments)
|
|
: ConsoleRunnable(aConsole)
|
|
, mAction(aAction)
|
|
, mArguments(aArguments)
|
|
{
|
|
MOZ_ASSERT(aConsole);
|
|
}
|
|
|
|
private:
|
|
bool
|
|
PreDispatch(JSContext* aCx) override
|
|
{
|
|
ClearException ce(aCx);
|
|
|
|
JS::Rooted<JSObject*> arguments(aCx,
|
|
JS_NewArrayObject(aCx, mArguments.Length()));
|
|
if (NS_WARN_IF(!arguments)) {
|
|
return false;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> arg(aCx);
|
|
for (uint32_t i = 0; i < mArguments.Length(); ++i) {
|
|
arg = mArguments[i];
|
|
if (NS_WARN_IF(!JS_DefineElement(aCx, arguments, i, arg,
|
|
JSPROP_ENUMERATE))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
JS::Rooted<JS::Value> value(aCx, JS::ObjectValue(*arguments));
|
|
|
|
if (NS_WARN_IF(!Write(aCx, value))) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
RunConsole(JSContext* aCx, nsPIDOMWindowOuter* aOuterWindow,
|
|
nsPIDOMWindowInner* aInnerWindow) override
|
|
{
|
|
AssertIsOnMainThread();
|
|
|
|
ClearException ce(aCx);
|
|
|
|
// Now we could have the correct window (if we are not window-less).
|
|
mClonedData.mParent = aInnerWindow;
|
|
|
|
JS::Rooted<JS::Value> argumentsValue(aCx);
|
|
bool ok = Read(aCx, &argumentsValue);
|
|
mClonedData.mParent = nullptr;
|
|
|
|
if (!ok) {
|
|
return;
|
|
}
|
|
|
|
MOZ_ASSERT(argumentsValue.isObject());
|
|
JS::Rooted<JSObject*> argumentsObj(aCx, &argumentsValue.toObject());
|
|
if (NS_WARN_IF(!argumentsObj)) {
|
|
return;
|
|
}
|
|
|
|
uint32_t length;
|
|
if (!JS_GetArrayLength(aCx, argumentsObj, &length)) {
|
|
return;
|
|
}
|
|
|
|
Sequence<JS::Value> arguments;
|
|
|
|
for (uint32_t i = 0; i < length; ++i) {
|
|
JS::Rooted<JS::Value> value(aCx);
|
|
|
|
if (!JS_GetElement(aCx, argumentsObj, i, &value)) {
|
|
return;
|
|
}
|
|
|
|
if (!arguments.AppendElement(value, fallible)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
mConsole->ProfileMethodInternal(aCx, mAction, arguments);
|
|
}
|
|
|
|
virtual void
|
|
ReleaseData() override
|
|
{}
|
|
|
|
nsString mAction;
|
|
|
|
// This is a reference of the sequence of arguments we receive from the DOM
|
|
// bindings and it's rooted by them. It's only used on the owning thread in
|
|
// PreDispatch().
|
|
const Sequence<JS::Value>& mArguments;
|
|
};
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_CLASS(Console)
|
|
|
|
// We don't need to traverse/unlink mStorage and mSandbox because they are not
|
|
// CCed objects and they are only used on the main thread, even when this
|
|
// Console object is used on workers.
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Console)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsoleEventNotifier)
|
|
tmp->Shutdown();
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Console)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsoleEventNotifier)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Console)
|
|
for (uint32_t i = 0; i < tmp->mCallDataStorage.Length(); ++i) {
|
|
tmp->mCallDataStorage[i]->Trace(aCallbacks, aClosure);
|
|
}
|
|
|
|
for (uint32_t i = 0; i < tmp->mCallDataStoragePending.Length(); ++i) {
|
|
tmp->mCallDataStoragePending[i]->Trace(aCallbacks, aClosure);
|
|
}
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTING_ADDREF(Console)
|
|
NS_IMPL_CYCLE_COLLECTING_RELEASE(Console)
|
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Console)
|
|
NS_INTERFACE_MAP_ENTRY(nsIObserver)
|
|
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
|
|
NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
|
|
NS_INTERFACE_MAP_END
|
|
|
|
/* static */ already_AddRefed<Console>
|
|
Console::Create(nsPIDOMWindowInner* aWindow, ErrorResult& aRv)
|
|
{
|
|
MOZ_ASSERT_IF(NS_IsMainThread(), aWindow);
|
|
|
|
RefPtr<Console> console = new Console(aWindow);
|
|
console->Initialize(aRv);
|
|
if (NS_WARN_IF(aRv.Failed())) {
|
|
return nullptr;
|
|
}
|
|
|
|
return console.forget();
|
|
}
|
|
|
|
Console::Console(nsPIDOMWindowInner* aWindow)
|
|
: mWindow(aWindow)
|
|
, mOuterID(0)
|
|
, mInnerID(0)
|
|
, mStatus(eUnknown)
|
|
{
|
|
MOZ_ASSERT_IF(NS_IsMainThread(), aWindow);
|
|
|
|
if (mWindow) {
|
|
mInnerID = mWindow->WindowID();
|
|
|
|
// Without outerwindow any console message coming from this object will not
|
|
// shown in the devtools webconsole. But this should be fine because
|
|
// probably we are shutting down, or the window is CCed/GCed.
|
|
nsPIDOMWindowOuter* outerWindow = mWindow->GetOuterWindow();
|
|
if (outerWindow) {
|
|
mOuterID = outerWindow->WindowID();
|
|
}
|
|
}
|
|
|
|
mozilla::HoldJSObjects(this);
|
|
}
|
|
|
|
Console::~Console()
|
|
{
|
|
AssertIsOnOwningThread();
|
|
Shutdown();
|
|
mozilla::DropJSObjects(this);
|
|
}
|
|
|
|
void
|
|
Console::Initialize(ErrorResult& aRv)
|
|
{
|
|
AssertIsOnOwningThread();
|
|
MOZ_ASSERT(mStatus == eUnknown);
|
|
|
|
if (NS_IsMainThread()) {
|
|
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
|
|
if (NS_WARN_IF(!obs)) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
aRv = obs->AddObserver(this, "inner-window-destroyed", true);
|
|
if (NS_WARN_IF(aRv.Failed())) {
|
|
return;
|
|
}
|
|
|
|
aRv = obs->AddObserver(this, "memory-pressure", true);
|
|
if (NS_WARN_IF(aRv.Failed())) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
mStatus = eInitialized;
|
|
}
|
|
|
|
void
|
|
Console::Shutdown()
|
|
{
|
|
AssertIsOnOwningThread();
|
|
|
|
if (mStatus == eUnknown || mStatus == eShuttingDown) {
|
|
return;
|
|
}
|
|
|
|
if (NS_IsMainThread()) {
|
|
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
|
|
if (obs) {
|
|
obs->RemoveObserver(this, "inner-window-destroyed");
|
|
obs->RemoveObserver(this, "memory-pressure");
|
|
}
|
|
}
|
|
|
|
NS_ReleaseOnMainThreadSystemGroup("Console::mStorage", mStorage.forget());
|
|
NS_ReleaseOnMainThreadSystemGroup("Console::mSandbox", mSandbox.forget());
|
|
|
|
mTimerRegistry.Clear();
|
|
mCounterRegistry.Clear();
|
|
|
|
mCallDataStorage.Clear();
|
|
mCallDataStoragePending.Clear();
|
|
|
|
mStatus = eShuttingDown;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
Console::Observe(nsISupports* aSubject, const char* aTopic,
|
|
const char16_t* aData)
|
|
{
|
|
AssertIsOnMainThread();
|
|
|
|
if (!strcmp(aTopic, "inner-window-destroyed")) {
|
|
nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject);
|
|
NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE);
|
|
|
|
uint64_t innerID;
|
|
nsresult rv = wrapper->GetData(&innerID);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
if (innerID == mInnerID) {
|
|
Shutdown();
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!strcmp(aTopic, "memory-pressure")) {
|
|
ClearStorage();
|
|
return NS_OK;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
Console::ClearStorage()
|
|
{
|
|
mCallDataStorage.Clear();
|
|
}
|
|
|
|
#define METHOD(name, string) \
|
|
/* static */ void \
|
|
Console::name(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData) \
|
|
{ \
|
|
Method(aGlobal, Method##name, NS_LITERAL_STRING(string), aData); \
|
|
}
|
|
|
|
METHOD(Log, "log")
|
|
METHOD(Info, "info")
|
|
METHOD(Warn, "warn")
|
|
METHOD(Error, "error")
|
|
METHOD(Exception, "exception")
|
|
METHOD(Debug, "debug")
|
|
METHOD(Table, "table")
|
|
METHOD(Trace, "trace")
|
|
|
|
/* static */ void
|
|
Console::Clear(const GlobalObject& aGlobal)
|
|
{
|
|
const Sequence<JS::Value> data;
|
|
Method(aGlobal, MethodClear, NS_LITERAL_STRING("clear"), data);
|
|
}
|
|
|
|
// Displays an interactive listing of all the properties of an object.
|
|
METHOD(Dir, "dir");
|
|
METHOD(Dirxml, "dirxml");
|
|
|
|
METHOD(Group, "group")
|
|
METHOD(GroupCollapsed, "groupCollapsed")
|
|
|
|
/* static */ void
|
|
Console::GroupEnd(const GlobalObject& aGlobal)
|
|
{
|
|
const Sequence<JS::Value> data;
|
|
Method(aGlobal, MethodGroupEnd, NS_LITERAL_STRING("groupEnd"), data);
|
|
}
|
|
|
|
/* static */ void
|
|
Console::Time(const GlobalObject& aGlobal, const nsAString& aLabel)
|
|
{
|
|
StringMethod(aGlobal, aLabel, MethodTime, NS_LITERAL_STRING("time"));
|
|
}
|
|
|
|
/* static */ void
|
|
Console::TimeEnd(const GlobalObject& aGlobal, const nsAString& aLabel)
|
|
{
|
|
StringMethod(aGlobal, aLabel, MethodTimeEnd, NS_LITERAL_STRING("timeEnd"));
|
|
}
|
|
|
|
/* static */ void
|
|
Console::StringMethod(const GlobalObject& aGlobal, const nsAString& aLabel,
|
|
MethodName aMethodName, const nsAString& aMethodString)
|
|
{
|
|
JSContext* cx = aGlobal.Context();
|
|
|
|
ClearException ce(cx);
|
|
|
|
Sequence<JS::Value> data;
|
|
SequenceRooter<JS::Value> rooter(cx, &data);
|
|
|
|
JS::Rooted<JS::Value> value(cx);
|
|
if (!dom::ToJSValue(cx, aLabel, &value)) {
|
|
return;
|
|
}
|
|
|
|
if (!data.AppendElement(value, fallible)) {
|
|
return;
|
|
}
|
|
|
|
Method(aGlobal, aMethodName, aMethodString, data);
|
|
}
|
|
|
|
/* static */ void
|
|
Console::TimeStamp(const GlobalObject& aGlobal,
|
|
const JS::Handle<JS::Value> aData)
|
|
{
|
|
JSContext* cx = aGlobal.Context();
|
|
|
|
ClearException ce(cx);
|
|
|
|
Sequence<JS::Value> data;
|
|
SequenceRooter<JS::Value> rooter(cx, &data);
|
|
|
|
if (aData.isString() && !data.AppendElement(aData, fallible)) {
|
|
return;
|
|
}
|
|
|
|
Method(aGlobal, MethodTimeStamp, NS_LITERAL_STRING("timeStamp"), data);
|
|
}
|
|
|
|
/* static */ void
|
|
Console::Profile(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData)
|
|
{
|
|
ProfileMethod(aGlobal, NS_LITERAL_STRING("profile"), aData);
|
|
}
|
|
|
|
/* static */ void
|
|
Console::ProfileEnd(const GlobalObject& aGlobal,
|
|
const Sequence<JS::Value>& aData)
|
|
{
|
|
ProfileMethod(aGlobal, NS_LITERAL_STRING("profileEnd"), aData);
|
|
}
|
|
|
|
/* static */ void
|
|
Console::ProfileMethod(const GlobalObject& aGlobal, const nsAString& aAction,
|
|
const Sequence<JS::Value>& aData)
|
|
{
|
|
RefPtr<Console> console = GetConsole(aGlobal);
|
|
if (!console) {
|
|
return;
|
|
}
|
|
|
|
JSContext* cx = aGlobal.Context();
|
|
console->ProfileMethodInternal(cx, aAction, aData);
|
|
}
|
|
|
|
void
|
|
Console::ProfileMethodInternal(JSContext* aCx, const nsAString& aAction,
|
|
const Sequence<JS::Value>& aData)
|
|
{
|
|
// Make all Console API no-op if DevTools aren't enabled.
|
|
if (!nsContentUtils::DevToolsEnabled(aCx)) {
|
|
return;
|
|
}
|
|
|
|
if (!NS_IsMainThread()) {
|
|
// Here we are in a worker thread.
|
|
RefPtr<ConsoleProfileRunnable> runnable =
|
|
new ConsoleProfileRunnable(this, aAction, aData);
|
|
|
|
runnable->Dispatch(aCx);
|
|
return;
|
|
}
|
|
|
|
ClearException ce(aCx);
|
|
|
|
RootedDictionary<ConsoleProfileEvent> event(aCx);
|
|
event.mAction = aAction;
|
|
|
|
event.mArguments.Construct();
|
|
Sequence<JS::Value>& sequence = event.mArguments.Value();
|
|
|
|
for (uint32_t i = 0; i < aData.Length(); ++i) {
|
|
if (!sequence.AppendElement(aData[i], fallible)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
JS::Rooted<JS::Value> eventValue(aCx);
|
|
if (!ToJSValue(aCx, event, &eventValue)) {
|
|
return;
|
|
}
|
|
|
|
JS::Rooted<JSObject*> eventObj(aCx, &eventValue.toObject());
|
|
MOZ_ASSERT(eventObj);
|
|
|
|
if (!JS_DefineProperty(aCx, eventObj, "wrappedJSObject", eventValue,
|
|
JSPROP_ENUMERATE)) {
|
|
return;
|
|
}
|
|
|
|
nsIXPConnect* xpc = nsContentUtils::XPConnect();
|
|
nsCOMPtr<nsISupports> wrapper;
|
|
const nsIID& iid = NS_GET_IID(nsISupports);
|
|
|
|
if (NS_FAILED(xpc->WrapJS(aCx, eventObj, iid, getter_AddRefs(wrapper)))) {
|
|
return;
|
|
}
|
|
|
|
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
|
|
if (obs) {
|
|
obs->NotifyObservers(wrapper, "console-api-profiler", nullptr);
|
|
}
|
|
}
|
|
|
|
/* static */ void
|
|
Console::Assert(const GlobalObject& aGlobal, bool aCondition,
|
|
const Sequence<JS::Value>& aData)
|
|
{
|
|
if (!aCondition) {
|
|
Method(aGlobal, MethodAssert, NS_LITERAL_STRING("assert"), aData);
|
|
}
|
|
}
|
|
|
|
/* static */ void
|
|
Console::Count(const GlobalObject& aGlobal, const nsAString& aLabel)
|
|
{
|
|
StringMethod(aGlobal, aLabel, MethodCount, NS_LITERAL_STRING("count"));
|
|
}
|
|
|
|
namespace {
|
|
|
|
nsresult
|
|
StackFrameToStackEntry(JSContext* aCx, nsIStackFrame* aStackFrame,
|
|
ConsoleStackEntry& aStackEntry)
|
|
{
|
|
MOZ_ASSERT(aStackFrame);
|
|
|
|
nsresult rv = aStackFrame->GetFilename(aCx, aStackEntry.mFilename);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
int32_t lineNumber;
|
|
rv = aStackFrame->GetLineNumber(aCx, &lineNumber);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
aStackEntry.mLineNumber = lineNumber;
|
|
|
|
int32_t columnNumber;
|
|
rv = aStackFrame->GetColumnNumber(aCx, &columnNumber);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
aStackEntry.mColumnNumber = columnNumber;
|
|
|
|
rv = aStackFrame->GetName(aCx, aStackEntry.mFunctionName);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsString cause;
|
|
rv = aStackFrame->GetAsyncCause(aCx, cause);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
if (!cause.IsEmpty()) {
|
|
aStackEntry.mAsyncCause.Construct(cause);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
ReifyStack(JSContext* aCx, nsIStackFrame* aStack,
|
|
nsTArray<ConsoleStackEntry>& aRefiedStack)
|
|
{
|
|
nsCOMPtr<nsIStackFrame> stack(aStack);
|
|
|
|
while (stack) {
|
|
ConsoleStackEntry& data = *aRefiedStack.AppendElement();
|
|
nsresult rv = StackFrameToStackEntry(aCx, stack, data);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsCOMPtr<nsIStackFrame> caller;
|
|
rv = stack->GetCaller(aCx, getter_AddRefs(caller));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
if (!caller) {
|
|
rv = stack->GetAsyncCaller(aCx, getter_AddRefs(caller));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
stack.swap(caller);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
// Queue a call to a console method. See the CALL_DELAY constant.
|
|
/* static */ void
|
|
Console::Method(const GlobalObject& aGlobal, MethodName aMethodName,
|
|
const nsAString& aMethodString,
|
|
const Sequence<JS::Value>& aData)
|
|
{
|
|
RefPtr<Console> console = GetConsole(aGlobal);
|
|
if (!console) {
|
|
return;
|
|
}
|
|
|
|
console->MethodInternal(aGlobal.Context(), aMethodName, aMethodString,
|
|
aData);
|
|
}
|
|
|
|
void
|
|
Console::MethodInternal(JSContext* aCx, MethodName aMethodName,
|
|
const nsAString& aMethodString,
|
|
const Sequence<JS::Value>& aData)
|
|
{
|
|
// Make all Console API no-op if DevTools aren't enabled.
|
|
if (!nsContentUtils::DevToolsEnabled(aCx)) {
|
|
return;
|
|
}
|
|
AssertIsOnOwningThread();
|
|
|
|
RefPtr<ConsoleCallData> callData(new ConsoleCallData());
|
|
|
|
ClearException ce(aCx);
|
|
|
|
if (NS_WARN_IF(!callData->Initialize(aCx, aMethodName, aMethodString,
|
|
aData, this))) {
|
|
return;
|
|
}
|
|
|
|
OriginAttributes oa;
|
|
|
|
if (mWindow) {
|
|
// Save the principal's OriginAttributes in the console event data
|
|
// so that we will be able to filter messages by origin attributes.
|
|
nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(mWindow);
|
|
if (NS_WARN_IF(!sop)) {
|
|
return;
|
|
}
|
|
|
|
nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
|
|
if (NS_WARN_IF(!principal)) {
|
|
return;
|
|
}
|
|
|
|
oa = principal->OriginAttributesRef();
|
|
callData->SetAddonId(principal);
|
|
|
|
#ifdef DEBUG
|
|
if (!nsContentUtils::IsSystemPrincipal(principal)) {
|
|
nsCOMPtr<nsIWebNavigation> webNav = do_GetInterface(mWindow);
|
|
if (webNav) {
|
|
nsCOMPtr<nsILoadContext> loadContext = do_QueryInterface(webNav);
|
|
MOZ_ASSERT(loadContext);
|
|
|
|
bool pb;
|
|
if (NS_SUCCEEDED(loadContext->GetUsePrivateBrowsing(&pb))) {
|
|
MOZ_ASSERT(pb == !!oa.mPrivateBrowsingId);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
} else {
|
|
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
|
|
MOZ_ASSERT(workerPrivate);
|
|
oa = workerPrivate->GetOriginAttributes();
|
|
}
|
|
|
|
callData->SetOriginAttributes(oa);
|
|
|
|
JS::StackCapture captureMode = ShouldIncludeStackTrace(aMethodName) ?
|
|
JS::StackCapture(JS::MaxFrames(DEFAULT_MAX_STACKTRACE_DEPTH)) :
|
|
JS::StackCapture(JS::FirstSubsumedFrame(aCx));
|
|
nsCOMPtr<nsIStackFrame> stack = CreateStack(aCx, mozilla::Move(captureMode));
|
|
|
|
if (stack) {
|
|
callData->mTopStackFrame.emplace();
|
|
nsresult rv = StackFrameToStackEntry(aCx, stack,
|
|
*callData->mTopStackFrame);
|
|
if (NS_FAILED(rv)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (NS_IsMainThread()) {
|
|
callData->mStack = stack;
|
|
} else {
|
|
// nsIStackFrame is not threadsafe, so we need to snapshot it now,
|
|
// before we post our runnable to the main thread.
|
|
callData->mReifiedStack.emplace();
|
|
nsresult rv = ReifyStack(aCx, stack, *callData->mReifiedStack);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
DOMHighResTimeStamp monotonicTimer;
|
|
|
|
// Monotonic timer for 'time' and 'timeEnd'
|
|
if (aMethodName == MethodTime ||
|
|
aMethodName == MethodTimeEnd ||
|
|
aMethodName == MethodTimeStamp) {
|
|
if (mWindow) {
|
|
nsGlobalWindowInner *win = nsGlobalWindowInner::Cast(mWindow);
|
|
MOZ_ASSERT(win);
|
|
|
|
RefPtr<Performance> performance = win->GetPerformance();
|
|
if (!performance) {
|
|
return;
|
|
}
|
|
|
|
monotonicTimer = performance->Now();
|
|
|
|
nsDocShell* docShell = static_cast<nsDocShell*>(mWindow->GetDocShell());
|
|
RefPtr<TimelineConsumers> timelines = TimelineConsumers::Get();
|
|
bool isTimelineRecording = timelines && timelines->HasConsumer(docShell);
|
|
|
|
// The 'timeStamp' recordings do not need an argument; use empty string
|
|
// if no arguments passed in.
|
|
if (isTimelineRecording && aMethodName == MethodTimeStamp) {
|
|
JS::Rooted<JS::Value> value(aCx, aData.Length() == 0
|
|
? JS_GetEmptyStringValue(aCx)
|
|
: aData[0]);
|
|
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value));
|
|
if (!jsString) {
|
|
return;
|
|
}
|
|
|
|
nsAutoJSString key;
|
|
if (!key.init(aCx, jsString)) {
|
|
return;
|
|
}
|
|
|
|
timelines->AddMarkerForDocShell(docShell, Move(
|
|
MakeUnique<TimestampTimelineMarker>(key)));
|
|
}
|
|
// For `console.time(foo)` and `console.timeEnd(foo)`.
|
|
else if (isTimelineRecording && aData.Length() == 1) {
|
|
JS::Rooted<JS::Value> value(aCx, aData[0]);
|
|
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value));
|
|
if (!jsString) {
|
|
return;
|
|
}
|
|
|
|
nsAutoJSString key;
|
|
if (!key.init(aCx, jsString)) {
|
|
return;
|
|
}
|
|
|
|
timelines->AddMarkerForDocShell(docShell, Move(
|
|
MakeUnique<ConsoleTimelineMarker>(
|
|
key, aMethodName == MethodTime ? MarkerTracingType::START
|
|
: MarkerTracingType::END)));
|
|
}
|
|
} else {
|
|
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
|
|
MOZ_ASSERT(workerPrivate);
|
|
|
|
monotonicTimer = workerPrivate->TimeStampToDOMHighRes(TimeStamp::Now());
|
|
}
|
|
}
|
|
|
|
if (aMethodName == MethodTime && !aData.IsEmpty()) {
|
|
callData->mStartTimerStatus = StartTimer(aCx, aData[0],
|
|
monotonicTimer,
|
|
callData->mStartTimerLabel,
|
|
&callData->mStartTimerValue);
|
|
}
|
|
|
|
else if (aMethodName == MethodTimeEnd && !aData.IsEmpty()) {
|
|
callData->mStopTimerStatus = StopTimer(aCx, aData[0],
|
|
monotonicTimer,
|
|
callData->mStopTimerLabel,
|
|
&callData->mStopTimerDuration);
|
|
}
|
|
|
|
else if (aMethodName == MethodCount) {
|
|
callData->mCountValue = IncreaseCounter(aCx, aData, callData->mCountLabel);
|
|
if (!callData->mCountValue) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (NS_IsMainThread()) {
|
|
callData->SetIDs(mOuterID, mInnerID);
|
|
ProcessCallData(aCx, callData, aData);
|
|
|
|
// Just because we don't want to expose
|
|
// retrieveConsoleEvents/setConsoleEventHandler to main-thread, we can
|
|
// cleanup the mCallDataStorage:
|
|
UnstoreCallData(callData);
|
|
return;
|
|
}
|
|
|
|
// We do this only in workers for now.
|
|
NotifyHandler(aCx, aData, callData);
|
|
|
|
RefPtr<ConsoleCallDataRunnable> runnable =
|
|
new ConsoleCallDataRunnable(this, callData);
|
|
Unused << NS_WARN_IF(!runnable->Dispatch(aCx));
|
|
}
|
|
|
|
// We store information to lazily compute the stack in the reserved slots of
|
|
// LazyStackGetter. The first slot always stores a JS object: it's either the
|
|
// JS wrapper of the nsIStackFrame or the actual reified stack representation.
|
|
// The second slot is a PrivateValue() holding an nsIStackFrame* when we haven't
|
|
// reified the stack yet, or an UndefinedValue() otherwise.
|
|
enum {
|
|
SLOT_STACKOBJ,
|
|
SLOT_RAW_STACK
|
|
};
|
|
|
|
bool
|
|
LazyStackGetter(JSContext* aCx, unsigned aArgc, JS::Value* aVp)
|
|
{
|
|
JS::CallArgs args = CallArgsFromVp(aArgc, aVp);
|
|
JS::Rooted<JSObject*> callee(aCx, &args.callee());
|
|
|
|
JS::Value v = js::GetFunctionNativeReserved(&args.callee(), SLOT_RAW_STACK);
|
|
if (v.isUndefined()) {
|
|
// Already reified.
|
|
args.rval().set(js::GetFunctionNativeReserved(callee, SLOT_STACKOBJ));
|
|
return true;
|
|
}
|
|
|
|
nsIStackFrame* stack = reinterpret_cast<nsIStackFrame*>(v.toPrivate());
|
|
nsTArray<ConsoleStackEntry> reifiedStack;
|
|
nsresult rv = ReifyStack(aCx, stack, reifiedStack);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
Throw(aCx, rv);
|
|
return false;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> stackVal(aCx);
|
|
if (NS_WARN_IF(!ToJSValue(aCx, reifiedStack, &stackVal))) {
|
|
return false;
|
|
}
|
|
|
|
MOZ_ASSERT(stackVal.isObject());
|
|
|
|
js::SetFunctionNativeReserved(callee, SLOT_STACKOBJ, stackVal);
|
|
js::SetFunctionNativeReserved(callee, SLOT_RAW_STACK, JS::UndefinedValue());
|
|
|
|
args.rval().set(stackVal);
|
|
return true;
|
|
}
|
|
|
|
void
|
|
Console::ProcessCallData(JSContext* aCx, ConsoleCallData* aData,
|
|
const Sequence<JS::Value>& aArguments)
|
|
{
|
|
AssertIsOnMainThread();
|
|
MOZ_ASSERT(aData);
|
|
|
|
JS::Rooted<JS::Value> eventValue(aCx);
|
|
|
|
// We want to create a console event object and pass it to our
|
|
// nsIConsoleAPIStorage implementation. We want to define some accessor
|
|
// properties on this object, and those will need to keep an nsIStackFrame
|
|
// alive. But nsIStackFrame cannot be wrapped in an untrusted scope. And
|
|
// further, passing untrusted objects to system code is likely to run afoul of
|
|
// Object Xrays. So we want to wrap in a system-principal scope here. But
|
|
// which one? We could cheat and try to get the underlying JSObject* of
|
|
// mStorage, but that's a bit fragile. Instead, we just use the junk scope,
|
|
// with explicit permission from the XPConnect module owner. If you're
|
|
// tempted to do that anywhere else, talk to said module owner first.
|
|
|
|
// aCx and aArguments are in the same compartment.
|
|
if (NS_WARN_IF(!PopulateConsoleNotificationInTheTargetScope(aCx, aArguments,
|
|
xpc::PrivilegedJunkScope(),
|
|
&eventValue, aData))) {
|
|
return;
|
|
}
|
|
|
|
if (!mStorage) {
|
|
mStorage = do_GetService("@mozilla.org/consoleAPI-storage;1");
|
|
}
|
|
|
|
if (!mStorage) {
|
|
NS_WARNING("Failed to get the ConsoleAPIStorage service.");
|
|
return;
|
|
}
|
|
|
|
nsAutoString innerID, outerID;
|
|
|
|
MOZ_ASSERT(aData->mIDType != ConsoleCallData::eUnknown);
|
|
if (aData->mIDType == ConsoleCallData::eString) {
|
|
outerID = aData->mOuterIDString;
|
|
innerID = aData->mInnerIDString;
|
|
} else {
|
|
MOZ_ASSERT(aData->mIDType == ConsoleCallData::eNumber);
|
|
outerID.AppendInt(aData->mOuterIDNumber);
|
|
innerID.AppendInt(aData->mInnerIDNumber);
|
|
}
|
|
|
|
if (aData->mMethodName == MethodClear) {
|
|
DebugOnly<nsresult> rv = mStorage->ClearEvents(innerID);
|
|
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "ClearEvents failed");
|
|
}
|
|
|
|
if (NS_FAILED(mStorage->RecordEvent(innerID, outerID, eventValue))) {
|
|
NS_WARNING("Failed to record a console event.");
|
|
}
|
|
}
|
|
|
|
bool
|
|
Console::PopulateConsoleNotificationInTheTargetScope(JSContext* aCx,
|
|
const Sequence<JS::Value>& aArguments,
|
|
JSObject* aTargetScope,
|
|
JS::MutableHandle<JS::Value> aEventValue,
|
|
ConsoleCallData* aData)
|
|
{
|
|
MOZ_ASSERT(aCx);
|
|
MOZ_ASSERT(aData);
|
|
MOZ_ASSERT(aTargetScope);
|
|
|
|
JS::Rooted<JSObject*> targetScope(aCx, aTargetScope);
|
|
|
|
ConsoleStackEntry frame;
|
|
if (aData->mTopStackFrame) {
|
|
frame = *aData->mTopStackFrame;
|
|
}
|
|
|
|
ClearException ce(aCx);
|
|
RootedDictionary<ConsoleEvent> event(aCx);
|
|
|
|
event.mAddonId = aData->mAddonId;
|
|
|
|
event.mID.Construct();
|
|
event.mInnerID.Construct();
|
|
|
|
if (aData->mIDType == ConsoleCallData::eString) {
|
|
event.mID.Value().SetAsString() = aData->mOuterIDString;
|
|
event.mInnerID.Value().SetAsString() = aData->mInnerIDString;
|
|
} else if (aData->mIDType == ConsoleCallData::eNumber) {
|
|
event.mID.Value().SetAsUnsignedLongLong() = aData->mOuterIDNumber;
|
|
event.mInnerID.Value().SetAsUnsignedLongLong() = aData->mInnerIDNumber;
|
|
} else {
|
|
// aData->mIDType can be eUnknown when we dispatch notifications via
|
|
// mConsoleEventNotifier.
|
|
event.mID.Value().SetAsUnsignedLongLong() = 0;
|
|
event.mInnerID.Value().SetAsUnsignedLongLong() = 0;
|
|
}
|
|
|
|
event.mLevel = aData->mMethodString;
|
|
event.mFilename = frame.mFilename;
|
|
|
|
nsCOMPtr<nsIURI> filenameURI;
|
|
nsAutoCString pass;
|
|
if (NS_IsMainThread() &&
|
|
NS_SUCCEEDED(NS_NewURI(getter_AddRefs(filenameURI), frame.mFilename)) &&
|
|
NS_SUCCEEDED(filenameURI->GetPassword(pass)) && !pass.IsEmpty()) {
|
|
nsCOMPtr<nsISensitiveInfoHiddenURI> safeURI = do_QueryInterface(filenameURI);
|
|
nsAutoCString spec;
|
|
if (safeURI &&
|
|
NS_SUCCEEDED(safeURI->GetSensitiveInfoHiddenSpec(spec))) {
|
|
CopyUTF8toUTF16(spec, event.mFilename);
|
|
}
|
|
}
|
|
|
|
event.mLineNumber = frame.mLineNumber;
|
|
event.mColumnNumber = frame.mColumnNumber;
|
|
event.mFunctionName = frame.mFunctionName;
|
|
event.mTimeStamp = aData->mTimeStamp;
|
|
event.mPrivate = !!aData->mOriginAttributes.mPrivateBrowsingId;
|
|
|
|
switch (aData->mMethodName) {
|
|
case MethodLog:
|
|
case MethodInfo:
|
|
case MethodWarn:
|
|
case MethodError:
|
|
case MethodException:
|
|
case MethodDebug:
|
|
case MethodAssert:
|
|
case MethodGroup:
|
|
case MethodGroupCollapsed:
|
|
event.mArguments.Construct();
|
|
event.mStyles.Construct();
|
|
if (NS_WARN_IF(!ProcessArguments(aCx, aArguments,
|
|
event.mArguments.Value(),
|
|
event.mStyles.Value()))) {
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
event.mArguments.Construct();
|
|
if (NS_WARN_IF(!ArgumentsToValueList(aArguments,
|
|
event.mArguments.Value()))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (aData->mMethodName == MethodGroup ||
|
|
aData->mMethodName == MethodGroupCollapsed) {
|
|
ComposeAndStoreGroupName(aCx, event.mArguments.Value(), event.mGroupName);
|
|
}
|
|
|
|
else if (aData->mMethodName == MethodGroupEnd) {
|
|
if (!UnstoreGroupName(event.mGroupName)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
else if (aData->mMethodName == MethodTime && !aArguments.IsEmpty()) {
|
|
event.mTimer = CreateStartTimerValue(aCx, aData->mStartTimerLabel,
|
|
aData->mStartTimerStatus);
|
|
}
|
|
|
|
else if (aData->mMethodName == MethodTimeEnd && !aArguments.IsEmpty()) {
|
|
event.mTimer = CreateStopTimerValue(aCx, aData->mStopTimerLabel,
|
|
aData->mStopTimerDuration,
|
|
aData->mStopTimerStatus);
|
|
}
|
|
|
|
else if (aData->mMethodName == MethodCount) {
|
|
event.mCounter = CreateCounterValue(aCx, aData->mCountLabel,
|
|
aData->mCountValue);
|
|
}
|
|
|
|
JSAutoCompartment ac2(aCx, targetScope);
|
|
|
|
if (NS_WARN_IF(!ToJSValue(aCx, event, aEventValue))) {
|
|
return false;
|
|
}
|
|
|
|
JS::Rooted<JSObject*> eventObj(aCx, &aEventValue.toObject());
|
|
if (NS_WARN_IF(!JS_DefineProperty(aCx, eventObj, "wrappedJSObject", eventObj,
|
|
JSPROP_ENUMERATE))) {
|
|
return false;
|
|
}
|
|
|
|
if (ShouldIncludeStackTrace(aData->mMethodName)) {
|
|
// Now define the "stacktrace" property on eventObj. There are two cases
|
|
// here. Either we came from a worker and have a reified stack, or we want
|
|
// to define a getter that will lazily reify the stack.
|
|
if (aData->mReifiedStack) {
|
|
JS::Rooted<JS::Value> stacktrace(aCx);
|
|
if (NS_WARN_IF(!ToJSValue(aCx, *aData->mReifiedStack, &stacktrace)) ||
|
|
NS_WARN_IF(!JS_DefineProperty(aCx, eventObj, "stacktrace", stacktrace,
|
|
JSPROP_ENUMERATE))) {
|
|
return false;
|
|
}
|
|
} else {
|
|
JSFunction* fun = js::NewFunctionWithReserved(aCx, LazyStackGetter, 0, 0,
|
|
"stacktrace");
|
|
if (NS_WARN_IF(!fun)) {
|
|
return false;
|
|
}
|
|
|
|
JS::Rooted<JSObject*> funObj(aCx, JS_GetFunctionObject(fun));
|
|
|
|
// We want to store our stack in the function and have it stay alive. But
|
|
// we also need sane access to the C++ nsIStackFrame. So store both a JS
|
|
// wrapper and the raw pointer: the former will keep the latter alive.
|
|
JS::Rooted<JS::Value> stackVal(aCx);
|
|
nsresult rv = nsContentUtils::WrapNative(aCx, aData->mStack,
|
|
&stackVal);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return false;
|
|
}
|
|
|
|
js::SetFunctionNativeReserved(funObj, SLOT_STACKOBJ, stackVal);
|
|
js::SetFunctionNativeReserved(funObj, SLOT_RAW_STACK,
|
|
JS::PrivateValue(aData->mStack.get()));
|
|
|
|
if (NS_WARN_IF(!JS_DefineProperty(aCx, eventObj, "stacktrace",
|
|
JS_DATA_TO_FUNC_PTR(JSNative, funObj.get()),
|
|
nullptr,
|
|
JSPROP_ENUMERATE |
|
|
JSPROP_GETTER | JSPROP_SETTER))) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
|
|
// Helper method for ProcessArguments. Flushes output, if non-empty, to aSequence.
|
|
bool
|
|
FlushOutput(JSContext* aCx, Sequence<JS::Value>& aSequence, nsString &aOutput)
|
|
{
|
|
if (!aOutput.IsEmpty()) {
|
|
JS::Rooted<JSString*> str(aCx, JS_NewUCStringCopyN(aCx,
|
|
aOutput.get(),
|
|
aOutput.Length()));
|
|
if (NS_WARN_IF(!str)) {
|
|
return false;
|
|
}
|
|
|
|
if (NS_WARN_IF(!aSequence.AppendElement(JS::StringValue(str), fallible))) {
|
|
return false;
|
|
}
|
|
|
|
aOutput.Truncate();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool
|
|
Console::ProcessArguments(JSContext* aCx,
|
|
const Sequence<JS::Value>& aData,
|
|
Sequence<JS::Value>& aSequence,
|
|
Sequence<nsString>& aStyles) const
|
|
{
|
|
if (aData.IsEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
if (aData.Length() == 1 || !aData[0].isString()) {
|
|
return ArgumentsToValueList(aData, aSequence);
|
|
}
|
|
|
|
JS::Rooted<JS::Value> format(aCx, aData[0]);
|
|
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, format));
|
|
if (NS_WARN_IF(!jsString)) {
|
|
return false;
|
|
}
|
|
|
|
nsAutoJSString string;
|
|
if (NS_WARN_IF(!string.init(aCx, jsString))) {
|
|
return false;
|
|
}
|
|
|
|
nsString::const_iterator start, end;
|
|
string.BeginReading(start);
|
|
string.EndReading(end);
|
|
|
|
nsString output;
|
|
uint32_t index = 1;
|
|
|
|
while (start != end) {
|
|
if (*start != '%') {
|
|
output.Append(*start);
|
|
++start;
|
|
continue;
|
|
}
|
|
|
|
++start;
|
|
if (start == end) {
|
|
output.Append('%');
|
|
break;
|
|
}
|
|
|
|
if (*start == '%') {
|
|
output.Append(*start);
|
|
++start;
|
|
continue;
|
|
}
|
|
|
|
nsAutoString tmp;
|
|
tmp.Append('%');
|
|
|
|
int32_t integer = -1;
|
|
int32_t mantissa = -1;
|
|
|
|
// Let's parse %<number>.<number> for %d and %f
|
|
if (*start >= '0' && *start <= '9') {
|
|
integer = 0;
|
|
|
|
do {
|
|
integer = integer * 10 + *start - '0';
|
|
tmp.Append(*start);
|
|
++start;
|
|
} while (*start >= '0' && *start <= '9' && start != end);
|
|
}
|
|
|
|
if (start == end) {
|
|
output.Append(tmp);
|
|
break;
|
|
}
|
|
|
|
if (*start == '.') {
|
|
tmp.Append(*start);
|
|
++start;
|
|
|
|
if (start == end) {
|
|
output.Append(tmp);
|
|
break;
|
|
}
|
|
|
|
// '.' must be followed by a number.
|
|
if (*start < '0' || *start > '9') {
|
|
output.Append(tmp);
|
|
continue;
|
|
}
|
|
|
|
mantissa = 0;
|
|
|
|
do {
|
|
mantissa = mantissa * 10 + *start - '0';
|
|
tmp.Append(*start);
|
|
++start;
|
|
} while (*start >= '0' && *start <= '9' && start != end);
|
|
|
|
if (start == end) {
|
|
output.Append(tmp);
|
|
break;
|
|
}
|
|
}
|
|
|
|
char ch = *start;
|
|
tmp.Append(ch);
|
|
++start;
|
|
|
|
switch (ch) {
|
|
case 'o':
|
|
case 'O':
|
|
{
|
|
if (NS_WARN_IF(!FlushOutput(aCx, aSequence, output))) {
|
|
return false;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> v(aCx);
|
|
if (index < aData.Length()) {
|
|
v = aData[index++];
|
|
}
|
|
|
|
if (NS_WARN_IF(!aSequence.AppendElement(v, fallible))) {
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case 'c':
|
|
{
|
|
// If there isn't any output but there's already a style, then
|
|
// discard the previous style and use the next one instead.
|
|
if (output.IsEmpty() && !aStyles.IsEmpty()) {
|
|
aStyles.TruncateLength(aStyles.Length() - 1);
|
|
}
|
|
|
|
if (NS_WARN_IF(!FlushOutput(aCx, aSequence, output))) {
|
|
return false;
|
|
}
|
|
|
|
if (index < aData.Length()) {
|
|
JS::Rooted<JS::Value> v(aCx, aData[index++]);
|
|
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, v));
|
|
if (NS_WARN_IF(!jsString)) {
|
|
return false;
|
|
}
|
|
|
|
int32_t diff = aSequence.Length() - aStyles.Length();
|
|
if (diff > 0) {
|
|
for (int32_t i = 0; i < diff; i++) {
|
|
if (NS_WARN_IF(!aStyles.AppendElement(VoidString(), fallible))) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
nsAutoJSString string;
|
|
if (NS_WARN_IF(!string.init(aCx, jsString))) {
|
|
return false;
|
|
}
|
|
|
|
if (NS_WARN_IF(!aStyles.AppendElement(string, fallible))) {
|
|
return false;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 's':
|
|
if (index < aData.Length()) {
|
|
JS::Rooted<JS::Value> value(aCx, aData[index++]);
|
|
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value));
|
|
if (NS_WARN_IF(!jsString)) {
|
|
return false;
|
|
}
|
|
|
|
nsAutoJSString v;
|
|
if (NS_WARN_IF(!v.init(aCx, jsString))) {
|
|
return false;
|
|
}
|
|
|
|
output.Append(v);
|
|
}
|
|
break;
|
|
|
|
case 'd':
|
|
case 'i':
|
|
if (index < aData.Length()) {
|
|
JS::Rooted<JS::Value> value(aCx, aData[index++]);
|
|
|
|
int32_t v;
|
|
if (NS_WARN_IF(!JS::ToInt32(aCx, value, &v))) {
|
|
return false;
|
|
}
|
|
|
|
nsCString format;
|
|
MakeFormatString(format, integer, mantissa, 'd');
|
|
output.AppendPrintf(format.get(), v);
|
|
}
|
|
break;
|
|
|
|
case 'f':
|
|
if (index < aData.Length()) {
|
|
JS::Rooted<JS::Value> value(aCx, aData[index++]);
|
|
|
|
double v;
|
|
if (NS_WARN_IF(!JS::ToNumber(aCx, value, &v))) {
|
|
return false;
|
|
}
|
|
|
|
// nspr returns "nan", but we want to expose it as "NaN"
|
|
if (std::isnan(v)) {
|
|
output.AppendFloat(v);
|
|
} else {
|
|
nsCString format;
|
|
MakeFormatString(format, integer, mantissa, 'f');
|
|
output.AppendPrintf(format.get(), v);
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
output.Append(tmp);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (NS_WARN_IF(!FlushOutput(aCx, aSequence, output))) {
|
|
return false;
|
|
}
|
|
|
|
// Discard trailing style element if there is no output to apply it to.
|
|
if (aStyles.Length() > aSequence.Length()) {
|
|
aStyles.TruncateLength(aSequence.Length());
|
|
}
|
|
|
|
// The rest of the array, if unused by the format string.
|
|
for (; index < aData.Length(); ++index) {
|
|
if (NS_WARN_IF(!aSequence.AppendElement(aData[index], fallible))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
Console::MakeFormatString(nsCString& aFormat, int32_t aInteger,
|
|
int32_t aMantissa, char aCh) const
|
|
{
|
|
aFormat.Append('%');
|
|
if (aInteger >= 0) {
|
|
aFormat.AppendInt(aInteger);
|
|
}
|
|
|
|
if (aMantissa >= 0) {
|
|
aFormat.Append('.');
|
|
aFormat.AppendInt(aMantissa);
|
|
}
|
|
|
|
aFormat.Append(aCh);
|
|
}
|
|
|
|
void
|
|
Console::ComposeAndStoreGroupName(JSContext* aCx,
|
|
const Sequence<JS::Value>& aData,
|
|
nsAString& aName)
|
|
{
|
|
for (uint32_t i = 0; i < aData.Length(); ++i) {
|
|
if (i != 0) {
|
|
aName.AppendASCII(" ");
|
|
}
|
|
|
|
JS::Rooted<JS::Value> value(aCx, aData[i]);
|
|
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value));
|
|
if (!jsString) {
|
|
return;
|
|
}
|
|
|
|
nsAutoJSString string;
|
|
if (!string.init(aCx, jsString)) {
|
|
return;
|
|
}
|
|
|
|
aName.Append(string);
|
|
}
|
|
|
|
mGroupStack.AppendElement(aName);
|
|
}
|
|
|
|
bool
|
|
Console::UnstoreGroupName(nsAString& aName)
|
|
{
|
|
if (mGroupStack.IsEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
uint32_t pos = mGroupStack.Length() - 1;
|
|
aName = mGroupStack[pos];
|
|
mGroupStack.RemoveElementAt(pos);
|
|
return true;
|
|
}
|
|
|
|
Console::TimerStatus
|
|
Console::StartTimer(JSContext* aCx, const JS::Value& aName,
|
|
DOMHighResTimeStamp aTimestamp,
|
|
nsAString& aTimerLabel,
|
|
DOMHighResTimeStamp* aTimerValue)
|
|
{
|
|
AssertIsOnOwningThread();
|
|
MOZ_ASSERT(aTimerValue);
|
|
|
|
*aTimerValue = 0;
|
|
|
|
if (NS_WARN_IF(mTimerRegistry.Count() >= MAX_PAGE_TIMERS)) {
|
|
return eTimerMaxReached;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> name(aCx, aName);
|
|
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, name));
|
|
if (NS_WARN_IF(!jsString)) {
|
|
return eTimerJSException;
|
|
}
|
|
|
|
nsAutoJSString label;
|
|
if (NS_WARN_IF(!label.init(aCx, jsString))) {
|
|
return eTimerJSException;
|
|
}
|
|
|
|
aTimerLabel = label;
|
|
|
|
auto entry = mTimerRegistry.LookupForAdd(label);
|
|
if (entry) {
|
|
return eTimerAlreadyExists;
|
|
}
|
|
entry.OrInsert([&aTimestamp](){ return aTimestamp; });
|
|
|
|
*aTimerValue = aTimestamp;
|
|
return eTimerDone;
|
|
}
|
|
|
|
JS::Value
|
|
Console::CreateStartTimerValue(JSContext* aCx, const nsAString& aTimerLabel,
|
|
TimerStatus aTimerStatus) const
|
|
{
|
|
MOZ_ASSERT(aTimerStatus != eTimerUnknown);
|
|
|
|
if (aTimerStatus != eTimerDone) {
|
|
return CreateTimerError(aCx, aTimerLabel, aTimerStatus);
|
|
}
|
|
|
|
RootedDictionary<ConsoleTimerStart> timer(aCx);
|
|
|
|
timer.mName = aTimerLabel;
|
|
|
|
JS::Rooted<JS::Value> value(aCx);
|
|
if (!ToJSValue(aCx, timer, &value)) {
|
|
return JS::UndefinedValue();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
Console::TimerStatus
|
|
Console::StopTimer(JSContext* aCx, const JS::Value& aName,
|
|
DOMHighResTimeStamp aTimestamp,
|
|
nsAString& aTimerLabel,
|
|
double* aTimerDuration)
|
|
{
|
|
AssertIsOnOwningThread();
|
|
MOZ_ASSERT(aTimerDuration);
|
|
|
|
*aTimerDuration = 0;
|
|
|
|
JS::Rooted<JS::Value> name(aCx, aName);
|
|
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, name));
|
|
if (NS_WARN_IF(!jsString)) {
|
|
return eTimerJSException;
|
|
}
|
|
|
|
nsAutoJSString key;
|
|
if (NS_WARN_IF(!key.init(aCx, jsString))) {
|
|
return eTimerJSException;
|
|
}
|
|
|
|
aTimerLabel = key;
|
|
|
|
DOMHighResTimeStamp value = 0;
|
|
if (!mTimerRegistry.Remove(key, &value)) {
|
|
NS_WARNING("mTimerRegistry entry not found");
|
|
return eTimerDoesntExist;
|
|
}
|
|
|
|
*aTimerDuration = aTimestamp - value;
|
|
return eTimerDone;
|
|
}
|
|
|
|
JS::Value
|
|
Console::CreateStopTimerValue(JSContext* aCx, const nsAString& aLabel,
|
|
double aDuration, TimerStatus aStatus) const
|
|
{
|
|
if (aStatus != eTimerDone) {
|
|
return CreateTimerError(aCx, aLabel, aStatus);
|
|
}
|
|
|
|
RootedDictionary<ConsoleTimerEnd> timer(aCx);
|
|
timer.mName = aLabel;
|
|
timer.mDuration = aDuration;
|
|
|
|
JS::Rooted<JS::Value> value(aCx);
|
|
if (!ToJSValue(aCx, timer, &value)) {
|
|
return JS::UndefinedValue();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
JS::Value
|
|
Console::CreateTimerError(JSContext* aCx, const nsAString& aLabel,
|
|
TimerStatus aStatus) const
|
|
{
|
|
MOZ_ASSERT(aStatus != eTimerUnknown && aStatus != eTimerDone);
|
|
|
|
RootedDictionary<ConsoleTimerError> error(aCx);
|
|
|
|
error.mName = aLabel;
|
|
|
|
switch (aStatus) {
|
|
case eTimerAlreadyExists:
|
|
error.mError.AssignLiteral("timerAlreadyExists");
|
|
break;
|
|
|
|
case eTimerDoesntExist:
|
|
error.mError.AssignLiteral("timerDoesntExist");
|
|
break;
|
|
|
|
case eTimerJSException:
|
|
error.mError.AssignLiteral("timerJSError");
|
|
break;
|
|
|
|
case eTimerMaxReached:
|
|
error.mError.AssignLiteral("maxTimersExceeded");
|
|
break;
|
|
|
|
default:
|
|
MOZ_CRASH("Unsupported status");
|
|
break;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> value(aCx);
|
|
if (!ToJSValue(aCx, error, &value)) {
|
|
return JS::UndefinedValue();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
bool
|
|
Console::ArgumentsToValueList(const Sequence<JS::Value>& aData,
|
|
Sequence<JS::Value>& aSequence) const
|
|
{
|
|
for (uint32_t i = 0; i < aData.Length(); ++i) {
|
|
if (NS_WARN_IF(!aSequence.AppendElement(aData[i], fallible))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
uint32_t
|
|
Console::IncreaseCounter(JSContext* aCx, const Sequence<JS::Value>& aArguments,
|
|
nsAString& aCountLabel)
|
|
{
|
|
AssertIsOnOwningThread();
|
|
|
|
ClearException ce(aCx);
|
|
|
|
MOZ_ASSERT(!aArguments.IsEmpty());
|
|
|
|
JS::Rooted<JS::Value> labelValue(aCx, aArguments[0]);
|
|
JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, labelValue));
|
|
if (!jsString) {
|
|
return 0; // We cannot continue.
|
|
}
|
|
|
|
nsAutoJSString string;
|
|
if (!string.init(aCx, jsString)) {
|
|
return 0; // We cannot continue.
|
|
}
|
|
|
|
aCountLabel = string;
|
|
|
|
const bool maxCountersReached = mCounterRegistry.Count() >= MAX_PAGE_COUNTERS;
|
|
auto entry = mCounterRegistry.LookupForAdd(aCountLabel);
|
|
if (entry) {
|
|
++entry.Data();
|
|
} else {
|
|
entry.OrInsert([](){ return 1; });
|
|
if (maxCountersReached) {
|
|
// oops, we speculatively added an entry even though we shouldn't
|
|
mCounterRegistry.Remove(aCountLabel);
|
|
return MAX_PAGE_COUNTERS;
|
|
}
|
|
}
|
|
return entry.Data();
|
|
}
|
|
|
|
JS::Value
|
|
Console::CreateCounterValue(JSContext* aCx, const nsAString& aCountLabel,
|
|
uint32_t aCountValue) const
|
|
{
|
|
ClearException ce(aCx);
|
|
|
|
if (aCountValue == MAX_PAGE_COUNTERS) {
|
|
RootedDictionary<ConsoleCounterError> error(aCx);
|
|
|
|
JS::Rooted<JS::Value> value(aCx);
|
|
if (!ToJSValue(aCx, error, &value)) {
|
|
return JS::UndefinedValue();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
RootedDictionary<ConsoleCounter> data(aCx);
|
|
data.mLabel = aCountLabel;
|
|
data.mCount = aCountValue;
|
|
|
|
JS::Rooted<JS::Value> value(aCx);
|
|
if (!ToJSValue(aCx, data, &value)) {
|
|
return JS::UndefinedValue();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
bool
|
|
Console::ShouldIncludeStackTrace(MethodName aMethodName) const
|
|
{
|
|
switch (aMethodName) {
|
|
case MethodError:
|
|
case MethodException:
|
|
case MethodAssert:
|
|
case MethodTrace:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
JSObject*
|
|
Console::GetOrCreateSandbox(JSContext* aCx, nsIPrincipal* aPrincipal)
|
|
{
|
|
AssertIsOnMainThread();
|
|
|
|
if (!mSandbox) {
|
|
nsIXPConnect* xpc = nsContentUtils::XPConnect();
|
|
MOZ_ASSERT(xpc, "This should never be null!");
|
|
|
|
JS::Rooted<JSObject*> sandbox(aCx);
|
|
nsresult rv = xpc->CreateSandbox(aCx, aPrincipal, sandbox.address());
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return nullptr;
|
|
}
|
|
|
|
mSandbox = new JSObjectHolder(aCx, sandbox);
|
|
}
|
|
|
|
return mSandbox->GetJSObject();
|
|
}
|
|
|
|
void
|
|
Console::StoreCallData(ConsoleCallData* aCallData)
|
|
{
|
|
AssertIsOnOwningThread();
|
|
|
|
MOZ_ASSERT(aCallData);
|
|
MOZ_ASSERT(!mCallDataStorage.Contains(aCallData));
|
|
MOZ_ASSERT(!mCallDataStoragePending.Contains(aCallData));
|
|
|
|
mCallDataStorage.AppendElement(aCallData);
|
|
|
|
if (mCallDataStorage.Length() > STORAGE_MAX_EVENTS) {
|
|
RefPtr<ConsoleCallData> callData = mCallDataStorage[0];
|
|
mCallDataStorage.RemoveElementAt(0);
|
|
|
|
MOZ_ASSERT(callData->mStatus != ConsoleCallData::eToBeDeleted);
|
|
|
|
// We cannot delete this object now because we have to trace its JSValues
|
|
// until the pending operation (ConsoleCallDataRunnable) is completed.
|
|
if (callData->mStatus == ConsoleCallData::eInUse) {
|
|
callData->mStatus = ConsoleCallData::eToBeDeleted;
|
|
mCallDataStoragePending.AppendElement(callData);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
Console::UnstoreCallData(ConsoleCallData* aCallData)
|
|
{
|
|
AssertIsOnOwningThread();
|
|
|
|
MOZ_ASSERT(aCallData);
|
|
|
|
MOZ_ASSERT(!mCallDataStoragePending.Contains(aCallData));
|
|
|
|
// It can be that mCallDataStorage has been already cleaned in case the
|
|
// processing of the argument of some Console methods triggers the
|
|
// window.close().
|
|
|
|
mCallDataStorage.RemoveElement(aCallData);
|
|
}
|
|
|
|
void
|
|
Console::ReleaseCallData(ConsoleCallData* aCallData)
|
|
{
|
|
AssertIsOnOwningThread();
|
|
MOZ_ASSERT(aCallData);
|
|
MOZ_ASSERT(aCallData->mStatus == ConsoleCallData::eToBeDeleted);
|
|
MOZ_ASSERT(mCallDataStoragePending.Contains(aCallData));
|
|
|
|
mCallDataStoragePending.RemoveElement(aCallData);
|
|
}
|
|
|
|
void
|
|
Console::NotifyHandler(JSContext* aCx, const Sequence<JS::Value>& aArguments,
|
|
ConsoleCallData* aCallData)
|
|
{
|
|
AssertIsOnOwningThread();
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
MOZ_ASSERT(aCallData);
|
|
|
|
if (!mConsoleEventNotifier) {
|
|
return;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> value(aCx);
|
|
|
|
JS::Rooted<JSObject*> callable(aCx, mConsoleEventNotifier->CallableOrNull());
|
|
if (NS_WARN_IF(!callable)) {
|
|
return;
|
|
}
|
|
|
|
// aCx and aArguments are in the same compartment because this method is
|
|
// called directly when a Console.something() runs.
|
|
// mConsoleEventNotifier->Callable() is the scope where value will be sent to.
|
|
if (NS_WARN_IF(!PopulateConsoleNotificationInTheTargetScope(aCx, aArguments,
|
|
callable,
|
|
&value,
|
|
aCallData))) {
|
|
return;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> ignored(aCx);
|
|
mConsoleEventNotifier->Call(value, &ignored);
|
|
}
|
|
|
|
void
|
|
Console::RetrieveConsoleEvents(JSContext* aCx, nsTArray<JS::Value>& aEvents,
|
|
ErrorResult& aRv)
|
|
{
|
|
AssertIsOnOwningThread();
|
|
|
|
// We don't want to expose this functionality to main-thread yet.
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
|
|
JS::Rooted<JSObject*> targetScope(aCx, JS::CurrentGlobalOrNull(aCx));
|
|
|
|
for (uint32_t i = 0; i < mCallDataStorage.Length(); ++i) {
|
|
JS::Rooted<JS::Value> value(aCx);
|
|
|
|
JS::Rooted<JSObject*> sequenceScope(aCx, mCallDataStorage[i]->mGlobal);
|
|
JSAutoCompartment ac(aCx, sequenceScope);
|
|
|
|
Sequence<JS::Value> sequence;
|
|
SequenceRooter<JS::Value> arguments(aCx, &sequence);
|
|
|
|
if (!mCallDataStorage[i]->PopulateArgumentsSequence(sequence)) {
|
|
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
return;
|
|
}
|
|
|
|
// Here we have aCx and sequence in the same compartment.
|
|
// targetScope is the destination scope and value will be populated in its
|
|
// compartment.
|
|
if (NS_WARN_IF(!PopulateConsoleNotificationInTheTargetScope(aCx, sequence,
|
|
targetScope,
|
|
&value,
|
|
mCallDataStorage[i]))) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
aEvents.AppendElement(value);
|
|
}
|
|
}
|
|
|
|
void
|
|
Console::SetConsoleEventHandler(AnyCallback* aHandler)
|
|
{
|
|
AssertIsOnOwningThread();
|
|
|
|
// We don't want to expose this functionality to main-thread yet.
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
|
|
mConsoleEventNotifier = aHandler;
|
|
}
|
|
|
|
void
|
|
Console::AssertIsOnOwningThread() const
|
|
{
|
|
NS_ASSERT_OWNINGTHREAD(Console);
|
|
}
|
|
|
|
bool
|
|
Console::IsShuttingDown() const
|
|
{
|
|
MOZ_ASSERT(mStatus != eUnknown);
|
|
return mStatus == eShuttingDown;
|
|
}
|
|
|
|
/* static */ already_AddRefed<Console>
|
|
Console::GetConsole(const GlobalObject& aGlobal)
|
|
{
|
|
ErrorResult rv;
|
|
RefPtr<Console> console = GetConsoleInternal(aGlobal, rv);
|
|
if (NS_WARN_IF(rv.Failed()) || !console) {
|
|
rv.SuppressException();
|
|
return nullptr;
|
|
}
|
|
|
|
console->AssertIsOnOwningThread();
|
|
|
|
if (console->IsShuttingDown()) {
|
|
return nullptr;
|
|
}
|
|
|
|
return console.forget();
|
|
}
|
|
|
|
/* static */ Console*
|
|
Console::GetConsoleInternal(const GlobalObject& aGlobal, ErrorResult& aRv)
|
|
{
|
|
// Worklet
|
|
if (NS_IsMainThread()) {
|
|
nsCOMPtr<WorkletGlobalScope> workletScope =
|
|
do_QueryInterface(aGlobal.GetAsSupports());
|
|
if (workletScope) {
|
|
return workletScope->GetConsole(aRv);
|
|
}
|
|
}
|
|
|
|
// Window
|
|
if (NS_IsMainThread()) {
|
|
nsCOMPtr<nsPIDOMWindowInner> innerWindow =
|
|
do_QueryInterface(aGlobal.GetAsSupports());
|
|
if (NS_WARN_IF(!innerWindow)) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsGlobalWindowInner* window = nsGlobalWindowInner::Cast(innerWindow);
|
|
return window->GetConsole(aRv);
|
|
}
|
|
|
|
// Workers
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
|
|
JSContext* cx = aGlobal.Context();
|
|
WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
|
|
MOZ_ASSERT(workerPrivate);
|
|
|
|
nsCOMPtr<nsIGlobalObject> global =
|
|
do_QueryInterface(aGlobal.GetAsSupports());
|
|
if (NS_WARN_IF(!global)) {
|
|
return nullptr;
|
|
}
|
|
|
|
WorkerGlobalScope* scope = workerPrivate->GlobalScope();
|
|
MOZ_ASSERT(scope);
|
|
|
|
// Normal worker scope.
|
|
if (scope == global) {
|
|
return scope->GetConsole(aRv);
|
|
}
|
|
|
|
// Debugger worker scope
|
|
else {
|
|
WorkerDebuggerGlobalScope* debuggerScope =
|
|
workerPrivate->DebuggerGlobalScope();
|
|
MOZ_ASSERT(debuggerScope);
|
|
MOZ_ASSERT(debuggerScope == global, "Which kind of global do we have?");
|
|
|
|
return debuggerScope->GetConsole(aRv);
|
|
}
|
|
}
|
|
|
|
} // namespace dom
|
|
} // namespace mozilla
|