/* -*- 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 "WorkerDebugger.h" #include "mozilla/dom/MessageEvent.h" #include "mozilla/dom/MessageEventBinding.h" #include "mozilla/PerformanceUtils.h" #include "nsProxyRelease.h" #include "nsQueryObject.h" #include "nsThreadUtils.h" #include "ScriptLoader.h" #include "WorkerCommon.h" #include "WorkerError.h" #include "WorkerPrivate.h" #include "WorkerRunnable.h" #include "WorkerScope.h" #if defined(XP_WIN) # include // for GetCurrentProcessId() #else # include // for getpid() #endif // defined(XP_WIN) namespace mozilla { namespace dom { namespace { class DebuggerMessageEventRunnable : public WorkerDebuggerRunnable { nsString mMessage; public: DebuggerMessageEventRunnable(WorkerPrivate* aWorkerPrivate, const nsAString& aMessage) : WorkerDebuggerRunnable(aWorkerPrivate), mMessage(aMessage) {} private: virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { WorkerDebuggerGlobalScope* globalScope = aWorkerPrivate->DebuggerGlobalScope(); MOZ_ASSERT(globalScope); JS::Rooted message( aCx, JS_NewUCStringCopyN(aCx, mMessage.get(), mMessage.Length())); if (!message) { return false; } JS::Rooted data(aCx, JS::StringValue(message)); RefPtr event = new MessageEvent(globalScope, nullptr, nullptr); event->InitMessageEvent(nullptr, NS_LITERAL_STRING("message"), CanBubble::eNo, Cancelable::eYes, data, EmptyString(), EmptyString(), nullptr, Sequence>()); event->SetTrusted(true); globalScope->DispatchEvent(*event); return true; } }; class CompileDebuggerScriptRunnable final : public WorkerDebuggerRunnable { nsString mScriptURL; public: CompileDebuggerScriptRunnable(WorkerPrivate* aWorkerPrivate, const nsAString& aScriptURL) : WorkerDebuggerRunnable(aWorkerPrivate), mScriptURL(aScriptURL) {} private: virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { aWorkerPrivate->AssertIsOnWorkerThread(); WorkerDebuggerGlobalScope* globalScope = aWorkerPrivate->CreateDebuggerGlobalScope(aCx); if (!globalScope) { NS_WARNING("Failed to make global!"); return false; } if (NS_WARN_IF(!aWorkerPrivate->EnsureClientSource())) { return false; } if (NS_WARN_IF(!aWorkerPrivate->EnsureCSPEventListener())) { return false; } // Initialize performance state which might be used on the main thread, as // in CompileScriptRunnable. This runnable might execute first. aWorkerPrivate->EnsurePerformanceStorage(); aWorkerPrivate->EnsurePerformanceCounter(); JS::Rooted global(aCx, globalScope->GetWrapper()); ErrorResult rv; JSAutoRealm ar(aCx, global); workerinternals::LoadMainScript(aWorkerPrivate, nullptr, mScriptURL, DebuggerScript, rv); rv.WouldReportJSException(); // Explicitly ignore NS_BINDING_ABORTED on rv. Or more precisely, still // return false and don't SetWorkerScriptExecutedSuccessfully() in that // case, but don't throw anything on aCx. The idea is to not dispatch error // events if our load is canceled with that error code. if (rv.ErrorCodeIs(NS_BINDING_ABORTED)) { rv.SuppressException(); return false; } // Make sure to propagate exceptions from rv onto aCx, so that they will get // reported after we return. We do this for all failures on rv, because now // we're using rv to track all the state we care about. if (rv.MaybeSetPendingException(aCx)) { return false; } return true; } }; } // namespace class WorkerDebugger::PostDebuggerMessageRunnable final : public Runnable { WorkerDebugger* mDebugger; nsString mMessage; public: PostDebuggerMessageRunnable(WorkerDebugger* aDebugger, const nsAString& aMessage) : mozilla::Runnable("PostDebuggerMessageRunnable"), mDebugger(aDebugger), mMessage(aMessage) {} private: ~PostDebuggerMessageRunnable() {} NS_IMETHOD Run() override { mDebugger->PostMessageToDebuggerOnMainThread(mMessage); return NS_OK; } }; class WorkerDebugger::ReportDebuggerErrorRunnable final : public Runnable { WorkerDebugger* mDebugger; nsString mFilename; uint32_t mLineno; nsString mMessage; public: ReportDebuggerErrorRunnable(WorkerDebugger* aDebugger, const nsAString& aFilename, uint32_t aLineno, const nsAString& aMessage) : Runnable("ReportDebuggerErrorRunnable"), mDebugger(aDebugger), mFilename(aFilename), mLineno(aLineno), mMessage(aMessage) {} private: ~ReportDebuggerErrorRunnable() {} NS_IMETHOD Run() override { mDebugger->ReportErrorToDebuggerOnMainThread(mFilename, mLineno, mMessage); return NS_OK; } }; WorkerDebugger::WorkerDebugger(WorkerPrivate* aWorkerPrivate) : mWorkerPrivate(aWorkerPrivate), mIsInitialized(false) { AssertIsOnMainThread(); } WorkerDebugger::~WorkerDebugger() { MOZ_ASSERT(!mWorkerPrivate); if (!NS_IsMainThread()) { for (size_t index = 0; index < mListeners.Length(); ++index) { NS_ReleaseOnMainThreadSystemGroup("WorkerDebugger::mListeners", mListeners[index].forget()); } } } NS_IMPL_ISUPPORTS(WorkerDebugger, nsIWorkerDebugger) NS_IMETHODIMP WorkerDebugger::GetIsClosed(bool* aResult) { AssertIsOnMainThread(); *aResult = !mWorkerPrivate; return NS_OK; } NS_IMETHODIMP WorkerDebugger::GetIsChrome(bool* aResult) { AssertIsOnMainThread(); if (!mWorkerPrivate) { return NS_ERROR_UNEXPECTED; } *aResult = mWorkerPrivate->IsChromeWorker(); return NS_OK; } NS_IMETHODIMP WorkerDebugger::GetIsInitialized(bool* aResult) { AssertIsOnMainThread(); if (!mWorkerPrivate) { return NS_ERROR_UNEXPECTED; } *aResult = mIsInitialized; return NS_OK; } NS_IMETHODIMP WorkerDebugger::GetParent(nsIWorkerDebugger** aResult) { AssertIsOnMainThread(); if (!mWorkerPrivate) { return NS_ERROR_UNEXPECTED; } WorkerPrivate* parent = mWorkerPrivate->GetParent(); if (!parent) { *aResult = nullptr; return NS_OK; } MOZ_ASSERT(mWorkerPrivate->IsDedicatedWorker()); nsCOMPtr debugger = parent->Debugger(); debugger.forget(aResult); return NS_OK; } NS_IMETHODIMP WorkerDebugger::GetType(uint32_t* aResult) { AssertIsOnMainThread(); if (!mWorkerPrivate) { return NS_ERROR_UNEXPECTED; } *aResult = mWorkerPrivate->Type(); return NS_OK; } NS_IMETHODIMP WorkerDebugger::GetUrl(nsAString& aResult) { AssertIsOnMainThread(); if (!mWorkerPrivate) { return NS_ERROR_UNEXPECTED; } aResult = mWorkerPrivate->ScriptURL(); return NS_OK; } NS_IMETHODIMP WorkerDebugger::GetWindow(mozIDOMWindow** aResult) { AssertIsOnMainThread(); if (!mWorkerPrivate) { return NS_ERROR_UNEXPECTED; } if (mWorkerPrivate->GetParent() || !mWorkerPrivate->IsDedicatedWorker()) { *aResult = nullptr; return NS_OK; } nsCOMPtr window = mWorkerPrivate->GetWindow(); window.forget(aResult); return NS_OK; } NS_IMETHODIMP WorkerDebugger::GetPrincipal(nsIPrincipal** aResult) { AssertIsOnMainThread(); MOZ_ASSERT(aResult); if (!mWorkerPrivate) { return NS_ERROR_UNEXPECTED; } nsCOMPtr prin = mWorkerPrivate->GetPrincipal(); prin.forget(aResult); return NS_OK; } NS_IMETHODIMP WorkerDebugger::GetServiceWorkerID(uint32_t* aResult) { AssertIsOnMainThread(); MOZ_ASSERT(aResult); if (!mWorkerPrivate || !mWorkerPrivate->IsServiceWorker()) { return NS_ERROR_UNEXPECTED; } *aResult = mWorkerPrivate->ServiceWorkerID(); return NS_OK; } NS_IMETHODIMP WorkerDebugger::GetId(nsAString& aResult) { AssertIsOnMainThread(); if (!mWorkerPrivate) { return NS_ERROR_UNEXPECTED; } aResult = mWorkerPrivate->Id(); return NS_OK; } NS_IMETHODIMP WorkerDebugger::Initialize(const nsAString& aURL) { AssertIsOnMainThread(); if (!mWorkerPrivate) { return NS_ERROR_UNEXPECTED; } if (!mIsInitialized) { RefPtr runnable = new CompileDebuggerScriptRunnable(mWorkerPrivate, aURL); if (!runnable->Dispatch()) { return NS_ERROR_FAILURE; } mIsInitialized = true; } return NS_OK; } NS_IMETHODIMP WorkerDebugger::PostMessageMoz(const nsAString& aMessage) { AssertIsOnMainThread(); if (!mWorkerPrivate || !mIsInitialized) { return NS_ERROR_UNEXPECTED; } RefPtr runnable = new DebuggerMessageEventRunnable(mWorkerPrivate, aMessage); if (!runnable->Dispatch()) { return NS_ERROR_FAILURE; } return NS_OK; } NS_IMETHODIMP WorkerDebugger::AddListener(nsIWorkerDebuggerListener* aListener) { AssertIsOnMainThread(); if (mListeners.Contains(aListener)) { return NS_ERROR_INVALID_ARG; } mListeners.AppendElement(aListener); return NS_OK; } NS_IMETHODIMP WorkerDebugger::RemoveListener(nsIWorkerDebuggerListener* aListener) { AssertIsOnMainThread(); if (!mListeners.Contains(aListener)) { return NS_ERROR_INVALID_ARG; } mListeners.RemoveElement(aListener); return NS_OK; } NS_IMETHODIMP WorkerDebugger::SetDebuggerReady(bool aReady) { return mWorkerPrivate->SetIsDebuggerReady(aReady); } void WorkerDebugger::Close() { MOZ_ASSERT(mWorkerPrivate); mWorkerPrivate = nullptr; nsTArray> listeners(mListeners); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnClose(); } } void WorkerDebugger::PostMessageToDebugger(const nsAString& aMessage) { mWorkerPrivate->AssertIsOnWorkerThread(); RefPtr runnable = new PostDebuggerMessageRunnable(this, aMessage); if (NS_FAILED(mWorkerPrivate->DispatchToMainThreadForMessaging( runnable.forget()))) { NS_WARNING("Failed to post message to debugger on main thread!"); } } void WorkerDebugger::PostMessageToDebuggerOnMainThread( const nsAString& aMessage) { AssertIsOnMainThread(); nsTArray> listeners(mListeners); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnMessage(aMessage); } } void WorkerDebugger::ReportErrorToDebugger(const nsAString& aFilename, uint32_t aLineno, const nsAString& aMessage) { mWorkerPrivate->AssertIsOnWorkerThread(); RefPtr runnable = new ReportDebuggerErrorRunnable(this, aFilename, aLineno, aMessage); if (NS_FAILED(mWorkerPrivate->DispatchToMainThreadForMessaging( runnable.forget()))) { NS_WARNING("Failed to report error to debugger on main thread!"); } } void WorkerDebugger::ReportErrorToDebuggerOnMainThread( const nsAString& aFilename, uint32_t aLineno, const nsAString& aMessage) { AssertIsOnMainThread(); nsTArray> listeners(mListeners); for (size_t index = 0; index < listeners.Length(); ++index) { listeners[index]->OnError(aFilename, aLineno, aMessage); } // We need a JSContext to be able to read any stack associated with the error. // This will not run any scripts. AutoJSAPI jsapi; DebugOnly ok = jsapi.Init(xpc::UnprivilegedJunkScope()); MOZ_ASSERT(ok, "UnprivilegedJunkScope should exist"); WorkerErrorReport report; report.mMessage = aMessage; report.mFilename = aFilename; WorkerErrorReport::LogErrorToConsole(jsapi.cx(), report, 0); } RefPtr WorkerDebugger::ReportPerformanceInfo() { AssertIsOnMainThread(); nsCOMPtr top; RefPtr self = this; #if defined(XP_WIN) uint32_t pid = GetCurrentProcessId(); #else uint32_t pid = getpid(); #endif bool isTopLevel = false; uint64_t windowID = mWorkerPrivate->WindowID(); PerformanceMemoryInfo memoryInfo; // Walk up to our containing page and its window WorkerPrivate* wp = mWorkerPrivate; while (wp->GetParent()) { wp = wp->GetParent(); } nsPIDOMWindowInner* win = wp->GetWindow(); if (win) { nsPIDOMWindowOuter* outer = win->GetOuterWindow(); if (outer) { top = outer->GetInProcessTop(); if (top) { windowID = top->WindowID(); isTopLevel = outer->IsTopLevelWindow(); } } } // getting the worker URL RefPtr scriptURI = mWorkerPrivate->GetResolvedScriptURI(); if (NS_WARN_IF(!scriptURI)) { // This can happen at shutdown, let's stop here. return PerformanceInfoPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } nsCString url = scriptURI->GetSpecOrDefault(); // Workers only produce metrics for a single category - // DispatchCategory::Worker. We still return an array of CategoryDispatch so // the PerformanceInfo struct is common to all performance counters throughout // Firefox. FallibleTArray items; uint64_t duration = 0; uint16_t count = 0; uint64_t perfId = 0; RefPtr perf = mWorkerPrivate->GetPerformanceCounter(); if (perf) { perfId = perf->GetID(); count = perf->GetTotalDispatchCount(); duration = perf->GetExecutionDuration(); CategoryDispatch item = CategoryDispatch(DispatchCategory::Worker.GetValue(), count); if (!items.AppendElement(item, fallible)) { NS_ERROR("Could not complete the operation"); } } if (!isTopLevel) { return PerformanceInfoPromise::CreateAndResolve( PerformanceInfo(url, pid, windowID, duration, perfId, true, isTopLevel, memoryInfo, items), __func__); } // We need to keep a ref on workerPrivate, passed to the promise, // to make sure it's still aloive when collecting the info. RefPtr workerRef = mWorkerPrivate; RefPtr mainThread = SystemGroup::AbstractMainThreadFor(TaskCategory::Performance); return CollectMemoryInfo(top, mainThread) ->Then( mainThread, __func__, [workerRef, url, pid, perfId, windowID, duration, isTopLevel, items](const PerformanceMemoryInfo& aMemoryInfo) { return PerformanceInfoPromise::CreateAndResolve( PerformanceInfo(url, pid, windowID, duration, perfId, true, isTopLevel, aMemoryInfo, items), __func__); }, [workerRef]() { return PerformanceInfoPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); }); } } // namespace dom } // namespace mozilla