summaryrefslogtreecommitdiffstats
path: root/dom/workers/WorkerError.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/workers/WorkerError.cpp')
-rw-r--r--dom/workers/WorkerError.cpp477
1 files changed, 477 insertions, 0 deletions
diff --git a/dom/workers/WorkerError.cpp b/dom/workers/WorkerError.cpp
new file mode 100644
index 0000000000..43f039614a
--- /dev/null
+++ b/dom/workers/WorkerError.cpp
@@ -0,0 +1,477 @@
+/* -*- 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 "WorkerError.h"
+
+#include <stdio.h>
+#include <algorithm>
+#include <utility>
+#include "MainThreadUtils.h"
+#include "WorkerPrivate.h"
+#include "WorkerRunnable.h"
+#include "WorkerScope.h"
+#include "js/ComparisonOperators.h"
+#include "js/UniquePtr.h"
+#include "js/friend/ErrorMessages.h"
+#include "jsapi.h"
+#include "mozilla/ArrayAlgorithm.h"
+#include "mozilla/ArrayIterator.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Span.h"
+#include "mozilla/ThreadSafeWeakPtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/ErrorEvent.h"
+#include "mozilla/dom/ErrorEventBinding.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/EventBinding.h"
+#include "mozilla/dom/EventTarget.h"
+#include "mozilla/dom/RemoteWorkerChild.h"
+#include "mozilla/dom/RemoteWorkerTypes.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/ServiceWorkerManager.h"
+#include "mozilla/dom/ServiceWorkerUtils.h"
+#include "mozilla/dom/SimpleGlobalObject.h"
+#include "mozilla/dom/Worker.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerDebuggerGlobalScopeBinding.h"
+#include "mozilla/dom/WorkerGlobalScopeBinding.h"
+#include "mozilla/fallible.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsGlobalWindowOuter.h"
+#include "nsIConsoleService.h"
+#include "nsIScriptError.h"
+#include "nsScriptError.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsWrapperCacheInlines.h"
+#include "nscore.h"
+#include "xpcpublic.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+class ReportErrorRunnable final : public WorkerDebuggeeRunnable {
+ UniquePtr<WorkerErrorReport> mReport;
+
+ public:
+ ReportErrorRunnable(WorkerPrivate* aWorkerPrivate,
+ UniquePtr<WorkerErrorReport> aReport)
+ : WorkerDebuggeeRunnable(aWorkerPrivate), mReport(std::move(aReport)) {}
+
+ private:
+ virtual void PostDispatch(WorkerPrivate* aWorkerPrivate,
+ bool aDispatchResult) override {
+ aWorkerPrivate->AssertIsOnWorkerThread();
+
+ // Dispatch may fail if the worker was canceled, no need to report that as
+ // an error, so don't call base class PostDispatch.
+ }
+
+ virtual bool WorkerRun(JSContext* aCx,
+ WorkerPrivate* aWorkerPrivate) override {
+ uint64_t innerWindowId;
+ bool fireAtScope = true;
+
+ bool workerIsAcceptingEvents = aWorkerPrivate->IsAcceptingEvents();
+
+ WorkerPrivate* parent = aWorkerPrivate->GetParent();
+ if (parent) {
+ innerWindowId = 0;
+ } else {
+ AssertIsOnMainThread();
+
+ // Once a window has frozen its workers, their
+ // mMainThreadDebuggeeEventTargets should be paused, and their
+ // WorkerDebuggeeRunnables should not be being executed. The same goes for
+ // WorkerDebuggeeRunnables sent from child to parent workers, but since a
+ // frozen parent worker runs only control runnables anyway, that is taken
+ // care of naturally.
+ MOZ_ASSERT(!aWorkerPrivate->IsFrozen());
+
+ // Similarly for paused windows; all its workers should have been
+ // informed. (Subworkers are unaffected by paused windows.)
+ MOZ_ASSERT(!aWorkerPrivate->IsParentWindowPaused());
+
+ if (aWorkerPrivate->IsSharedWorker()) {
+ aWorkerPrivate->GetRemoteWorkerController()
+ ->ErrorPropagationOnMainThread(mReport.get(),
+ /* isErrorEvent */ true);
+ return true;
+ }
+
+ // Service workers do not have a main thread parent global, so normal
+ // worker error reporting will crash. Instead, pass the error to
+ // the ServiceWorkerManager to report on any controlled documents.
+ if (aWorkerPrivate->IsServiceWorker()) {
+ RefPtr<RemoteWorkerChild> actor(
+ aWorkerPrivate->GetRemoteWorkerController());
+
+ Unused << NS_WARN_IF(!actor);
+
+ if (actor) {
+ actor->ErrorPropagationOnMainThread(nullptr, false);
+ }
+
+ return true;
+ }
+
+ // The innerWindowId is only required if we are going to ReportError
+ // below, which is gated on this condition. The inner window correctness
+ // check is only going to succeed when the worker is accepting events.
+ if (workerIsAcceptingEvents) {
+ aWorkerPrivate->AssertInnerWindowIsCorrect();
+ innerWindowId = aWorkerPrivate->WindowID();
+ }
+ }
+
+ // Don't fire this event if the JS object has been disconnected from the
+ // private object.
+ if (!workerIsAcceptingEvents) {
+ return true;
+ }
+
+ WorkerErrorReport::ReportError(aCx, parent, fireAtScope,
+ aWorkerPrivate->ParentEventTargetRef(),
+ std::move(mReport), innerWindowId);
+ return true;
+ }
+};
+
+class ReportGenericErrorRunnable final : public WorkerDebuggeeRunnable {
+ public:
+ static void CreateAndDispatch(WorkerPrivate* aWorkerPrivate) {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+
+ RefPtr<ReportGenericErrorRunnable> runnable =
+ new ReportGenericErrorRunnable(aWorkerPrivate);
+ runnable->Dispatch();
+ }
+
+ private:
+ explicit ReportGenericErrorRunnable(WorkerPrivate* aWorkerPrivate)
+ : WorkerDebuggeeRunnable(aWorkerPrivate) {
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ }
+
+ void PostDispatch(WorkerPrivate* aWorkerPrivate,
+ bool aDispatchResult) override {
+ aWorkerPrivate->AssertIsOnWorkerThread();
+
+ // Dispatch may fail if the worker was canceled, no need to report that as
+ // an error, so don't call base class PostDispatch.
+ }
+
+ bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ // Once a window has frozen its workers, their
+ // mMainThreadDebuggeeEventTargets should be paused, and their
+ // WorkerDebuggeeRunnables should not be being executed. The same goes for
+ // WorkerDebuggeeRunnables sent from child to parent workers, but since a
+ // frozen parent worker runs only control runnables anyway, that is taken
+ // care of naturally.
+ MOZ_ASSERT(!aWorkerPrivate->IsFrozen());
+
+ // Similarly for paused windows; all its workers should have been informed.
+ // (Subworkers are unaffected by paused windows.)
+ MOZ_ASSERT(!aWorkerPrivate->IsParentWindowPaused());
+
+ if (aWorkerPrivate->IsSharedWorker()) {
+ aWorkerPrivate->GetRemoteWorkerController()->ErrorPropagationOnMainThread(
+ nullptr, false);
+ return true;
+ }
+
+ if (aWorkerPrivate->IsServiceWorker()) {
+ RefPtr<RemoteWorkerChild> actor(
+ aWorkerPrivate->GetRemoteWorkerController());
+
+ Unused << NS_WARN_IF(!actor);
+
+ if (actor) {
+ actor->ErrorPropagationOnMainThread(nullptr, false);
+ }
+
+ return true;
+ }
+
+ if (!aWorkerPrivate->IsAcceptingEvents()) {
+ return true;
+ }
+
+ RefPtr<mozilla::dom::EventTarget> parentEventTarget =
+ aWorkerPrivate->ParentEventTargetRef();
+ RefPtr<Event> event =
+ Event::Constructor(parentEventTarget, u"error"_ns, EventInit());
+ event->SetTrusted(true);
+
+ parentEventTarget->DispatchEvent(*event);
+ return true;
+ }
+};
+
+} // namespace
+
+void WorkerErrorBase::AssignErrorBase(JSErrorBase* aReport) {
+ CopyUTF8toUTF16(MakeStringSpan(aReport->filename), mFilename);
+ mLineNumber = aReport->lineno;
+ mColumnNumber = aReport->column;
+ mErrorNumber = aReport->errorNumber;
+}
+
+void WorkerErrorNote::AssignErrorNote(JSErrorNotes::Note* aNote) {
+ WorkerErrorBase::AssignErrorBase(aNote);
+ xpc::ErrorNote::ErrorNoteToMessageString(aNote, mMessage);
+}
+
+WorkerErrorReport::WorkerErrorReport()
+ : mIsWarning(false), mExnType(JSEXN_ERR), mMutedError(false) {}
+
+void WorkerErrorReport::AssignErrorReport(JSErrorReport* aReport) {
+ WorkerErrorBase::AssignErrorBase(aReport);
+ xpc::ErrorReport::ErrorReportToMessageString(aReport, mMessage);
+
+ mLine.Assign(aReport->linebuf(), aReport->linebufLength());
+ mIsWarning = aReport->isWarning();
+ MOZ_ASSERT(aReport->exnType >= JSEXN_FIRST && aReport->exnType < JSEXN_LIMIT);
+ mExnType = JSExnType(aReport->exnType);
+ mMutedError = aReport->isMuted;
+
+ if (aReport->notes) {
+ if (!mNotes.SetLength(aReport->notes->length(), fallible)) {
+ return;
+ }
+
+ size_t i = 0;
+ for (auto&& note : *aReport->notes) {
+ mNotes.ElementAt(i).AssignErrorNote(note.get());
+ i++;
+ }
+ }
+}
+
+// aWorkerPrivate is the worker thread we're on (or the main thread, if null)
+// aTarget is the worker object that we are going to fire an error at
+// (if any).
+/* static */
+void WorkerErrorReport::ReportError(
+ JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aFireAtScope,
+ DOMEventTargetHelper* aTarget, UniquePtr<WorkerErrorReport> aReport,
+ uint64_t aInnerWindowId, JS::Handle<JS::Value> aException) {
+ if (aWorkerPrivate) {
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ } else {
+ AssertIsOnMainThread();
+ }
+
+ // We should not fire error events for warnings but instead make sure that
+ // they show up in the error console.
+ if (!aReport->mIsWarning) {
+ // First fire an ErrorEvent at the worker.
+ RootedDictionary<ErrorEventInit> init(aCx);
+
+ if (aReport->mMutedError) {
+ init.mMessage.AssignLiteral("Script error.");
+ } else {
+ init.mMessage = aReport->mMessage;
+ init.mFilename = aReport->mFilename;
+ init.mLineno = aReport->mLineNumber;
+ init.mColno = aReport->mColumnNumber;
+ init.mError = aException;
+ }
+
+ init.mCancelable = true;
+ init.mBubbles = false;
+
+ if (aTarget) {
+ RefPtr<ErrorEvent> event =
+ ErrorEvent::Constructor(aTarget, u"error"_ns, init);
+ event->SetTrusted(true);
+
+ bool defaultActionEnabled =
+ aTarget->DispatchEvent(*event, CallerType::System, IgnoreErrors());
+ if (!defaultActionEnabled) {
+ return;
+ }
+ }
+
+ // Now fire an event at the global object, but don't do that if the error
+ // code is too much recursion and this is the same script threw the error.
+ // XXXbz the interaction of this with worker errors seems kinda broken.
+ // An overrecursion in the debugger or debugger sandbox will get turned
+ // into an error event on our parent worker!
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks making this
+ // better.
+ if (aFireAtScope &&
+ (aTarget || aReport->mErrorNumber != JSMSG_OVER_RECURSED)) {
+ JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx));
+ NS_ASSERTION(global, "This should never be null!");
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+
+ if (aWorkerPrivate) {
+ RefPtr<WorkerGlobalScope> globalScope;
+ UNWRAP_OBJECT(WorkerGlobalScope, &global, globalScope);
+
+ if (!globalScope) {
+ WorkerDebuggerGlobalScope* globalScope = nullptr;
+ UNWRAP_OBJECT(WorkerDebuggerGlobalScope, &global, globalScope);
+
+ MOZ_ASSERT_IF(globalScope,
+ globalScope->GetWrapperPreserveColor() == global);
+ if (globalScope || IsWorkerDebuggerSandbox(global)) {
+ aWorkerPrivate->ReportErrorToDebugger(
+ aReport->mFilename, aReport->mLineNumber, aReport->mMessage);
+ return;
+ }
+
+ MOZ_ASSERT(SimpleGlobalObject::SimpleGlobalType(global) ==
+ SimpleGlobalObject::GlobalType::BindingDetail);
+ // XXXbz We should really log this to console, but unwinding out of
+ // this stuff without ending up firing any events is ... hard. Just
+ // return for now.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks
+ // making this better.
+ return;
+ }
+
+ MOZ_ASSERT(globalScope->GetWrapperPreserveColor() == global);
+
+ RefPtr<ErrorEvent> event =
+ ErrorEvent::Constructor(aTarget, u"error"_ns, init);
+ event->SetTrusted(true);
+
+ // TODO: Bug 1506441
+ if (NS_FAILED(EventDispatcher::DispatchDOMEvent(
+ MOZ_KnownLive(ToSupports(globalScope)), nullptr, event, nullptr,
+ &status))) {
+ NS_WARNING("Failed to dispatch worker thread error event!");
+ status = nsEventStatus_eIgnore;
+ }
+ } else if (nsGlobalWindowInner* win = xpc::WindowOrNull(global)) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!win->HandleScriptError(init, &status)) {
+ NS_WARNING("Failed to dispatch main thread error event!");
+ status = nsEventStatus_eIgnore;
+ }
+ }
+
+ // Was preventDefault() called?
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ return;
+ }
+ }
+ }
+
+ // Now fire a runnable to do the same on the parent's thread if we can.
+ if (aWorkerPrivate) {
+ RefPtr<ReportErrorRunnable> runnable =
+ new ReportErrorRunnable(aWorkerPrivate, std::move(aReport));
+ runnable->Dispatch();
+ return;
+ }
+
+ // Otherwise log an error to the error console.
+ WorkerErrorReport::LogErrorToConsole(aCx, *aReport, aInnerWindowId);
+}
+
+/* static */
+void WorkerErrorReport::LogErrorToConsole(JSContext* aCx,
+ WorkerErrorReport& aReport,
+ uint64_t aInnerWindowId) {
+ JS::Rooted<JSObject*> stack(aCx, aReport.ReadStack(aCx));
+ JS::Rooted<JSObject*> stackGlobal(aCx, JS::CurrentGlobalOrNull(aCx));
+
+ ErrorData errorData(
+ aReport.mIsWarning, aReport.mLineNumber, aReport.mColumnNumber,
+ aReport.mMessage, aReport.mFilename, aReport.mLine,
+ TransformIntoNewArray(aReport.mNotes, [](const WorkerErrorNote& note) {
+ return ErrorDataNote(note.mLineNumber, note.mColumnNumber,
+ note.mMessage, note.mFilename);
+ }));
+ LogErrorToConsole(errorData, aInnerWindowId, stack, stackGlobal);
+}
+
+/* static */
+void WorkerErrorReport::LogErrorToConsole(const ErrorData& aReport,
+ uint64_t aInnerWindowId,
+ JS::Handle<JSObject*> aStack,
+ JS::Handle<JSObject*> aStackGlobal) {
+ AssertIsOnMainThread();
+
+ RefPtr<nsScriptErrorBase> scriptError =
+ CreateScriptError(nullptr, JS::NothingHandleValue, aStack, aStackGlobal);
+
+ NS_WARNING_ASSERTION(scriptError, "Failed to create script error!");
+
+ if (scriptError) {
+ nsAutoCString category("Web Worker");
+ uint32_t flags = aReport.isWarning() ? nsIScriptError::warningFlag
+ : nsIScriptError::errorFlag;
+ if (NS_FAILED(scriptError->nsIScriptError::InitWithWindowID(
+ aReport.message(), aReport.filename(), aReport.line(),
+ aReport.lineNumber(), aReport.columnNumber(), flags, category,
+ aInnerWindowId))) {
+ NS_WARNING("Failed to init script error!");
+ scriptError = nullptr;
+ }
+
+ for (const ErrorDataNote& note : aReport.notes()) {
+ nsScriptErrorNote* noteObject = new nsScriptErrorNote();
+ noteObject->Init(note.message(), note.filename(), 0, note.lineNumber(),
+ note.columnNumber());
+ scriptError->AddNote(noteObject);
+ }
+ }
+
+ nsCOMPtr<nsIConsoleService> consoleService =
+ do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+ NS_WARNING_ASSERTION(consoleService, "Failed to get console service!");
+
+ if (consoleService) {
+ if (scriptError) {
+ if (NS_SUCCEEDED(consoleService->LogMessage(scriptError))) {
+ return;
+ }
+ NS_WARNING("LogMessage failed!");
+ } else if (NS_SUCCEEDED(consoleService->LogStringMessage(
+ aReport.message().BeginReading()))) {
+ return;
+ }
+ NS_WARNING("LogStringMessage failed!");
+ }
+
+ NS_ConvertUTF16toUTF8 msg(aReport.message());
+ NS_ConvertUTF16toUTF8 filename(aReport.filename());
+
+ static const char kErrorString[] = "JS error in Web Worker: %s [%s:%u]";
+
+#ifdef ANDROID
+ __android_log_print(ANDROID_LOG_INFO, "Gecko", kErrorString, msg.get(),
+ filename.get(), aReport.lineNumber());
+#endif
+
+ fprintf(stderr, kErrorString, msg.get(), filename.get(),
+ aReport.lineNumber());
+ fflush(stderr);
+}
+
+/* static */
+void WorkerErrorReport::CreateAndDispatchGenericErrorRunnableToParent(
+ WorkerPrivate* aWorkerPrivate) {
+ ReportGenericErrorRunnable::CreateAndDispatch(aWorkerPrivate);
+}
+
+} // namespace mozilla::dom