Bug 1787199 [wpt PR 35620] - Add tests for `VisibilityStateEntry`, a=testonly
[gecko.git] / dom / workers / WorkerError.cpp
blob254eb81bc7717674c0b5d6586754c793c115604f
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "WorkerError.h"
9 #include <stdio.h>
10 #include <algorithm>
11 #include <utility>
12 #include "MainThreadUtils.h"
13 #include "WorkerPrivate.h"
14 #include "WorkerRunnable.h"
15 #include "WorkerScope.h"
16 #include "js/ComparisonOperators.h"
17 #include "js/UniquePtr.h"
18 #include "js/friend/ErrorMessages.h"
19 #include "jsapi.h"
20 #include "mozilla/ArrayAlgorithm.h"
21 #include "mozilla/ArrayIterator.h"
22 #include "mozilla/Assertions.h"
23 #include "mozilla/BasicEvents.h"
24 #include "mozilla/DOMEventTargetHelper.h"
25 #include "mozilla/ErrorResult.h"
26 #include "mozilla/EventDispatcher.h"
27 #include "mozilla/RefPtr.h"
28 #include "mozilla/Span.h"
29 #include "mozilla/ThreadSafeWeakPtr.h"
30 #include "mozilla/Unused.h"
31 #include "mozilla/dom/BindingDeclarations.h"
32 #include "mozilla/dom/BindingUtils.h"
33 #include "mozilla/dom/ErrorEvent.h"
34 #include "mozilla/dom/ErrorEventBinding.h"
35 #include "mozilla/dom/Event.h"
36 #include "mozilla/dom/EventBinding.h"
37 #include "mozilla/dom/EventTarget.h"
38 #include "mozilla/dom/RemoteWorkerChild.h"
39 #include "mozilla/dom/RemoteWorkerTypes.h"
40 #include "mozilla/dom/RootedDictionary.h"
41 #include "mozilla/dom/ServiceWorkerManager.h"
42 #include "mozilla/dom/ServiceWorkerUtils.h"
43 #include "mozilla/dom/SimpleGlobalObject.h"
44 #include "mozilla/dom/Worker.h"
45 #include "mozilla/dom/WorkerCommon.h"
46 #include "mozilla/dom/WorkerDebuggerGlobalScopeBinding.h"
47 #include "mozilla/dom/WorkerGlobalScopeBinding.h"
48 #include "mozilla/fallible.h"
49 #include "nsCOMPtr.h"
50 #include "nsDebug.h"
51 #include "nsGlobalWindowOuter.h"
52 #include "nsIConsoleService.h"
53 #include "nsIScriptError.h"
54 #include "nsScriptError.h"
55 #include "nsServiceManagerUtils.h"
56 #include "nsString.h"
57 #include "nsWrapperCacheInlines.h"
58 #include "nscore.h"
59 #include "xpcpublic.h"
61 namespace mozilla::dom {
63 namespace {
65 class ReportErrorRunnable final : public WorkerDebuggeeRunnable {
66 UniquePtr<WorkerErrorReport> mReport;
68 public:
69 ReportErrorRunnable(WorkerPrivate* aWorkerPrivate,
70 UniquePtr<WorkerErrorReport> aReport)
71 : WorkerDebuggeeRunnable(aWorkerPrivate), mReport(std::move(aReport)) {}
73 private:
74 virtual void PostDispatch(WorkerPrivate* aWorkerPrivate,
75 bool aDispatchResult) override {
76 aWorkerPrivate->AssertIsOnWorkerThread();
78 // Dispatch may fail if the worker was canceled, no need to report that as
79 // an error, so don't call base class PostDispatch.
82 virtual bool WorkerRun(JSContext* aCx,
83 WorkerPrivate* aWorkerPrivate) override {
84 uint64_t innerWindowId;
85 bool fireAtScope = true;
87 bool workerIsAcceptingEvents = aWorkerPrivate->IsAcceptingEvents();
89 WorkerPrivate* parent = aWorkerPrivate->GetParent();
90 if (parent) {
91 innerWindowId = 0;
92 } else {
93 AssertIsOnMainThread();
95 // Once a window has frozen its workers, their
96 // mMainThreadDebuggeeEventTargets should be paused, and their
97 // WorkerDebuggeeRunnables should not be being executed. The same goes for
98 // WorkerDebuggeeRunnables sent from child to parent workers, but since a
99 // frozen parent worker runs only control runnables anyway, that is taken
100 // care of naturally.
101 MOZ_ASSERT(!aWorkerPrivate->IsFrozen());
103 // Similarly for paused windows; all its workers should have been
104 // informed. (Subworkers are unaffected by paused windows.)
105 MOZ_ASSERT(!aWorkerPrivate->IsParentWindowPaused());
107 if (aWorkerPrivate->IsSharedWorker()) {
108 aWorkerPrivate->GetRemoteWorkerController()
109 ->ErrorPropagationOnMainThread(mReport.get(),
110 /* isErrorEvent */ true);
111 return true;
114 // Service workers do not have a main thread parent global, so normal
115 // worker error reporting will crash. Instead, pass the error to
116 // the ServiceWorkerManager to report on any controlled documents.
117 if (aWorkerPrivate->IsServiceWorker()) {
118 RefPtr<RemoteWorkerChild> actor(
119 aWorkerPrivate->GetRemoteWorkerControllerWeakRef());
121 Unused << NS_WARN_IF(!actor);
123 if (actor) {
124 actor->ErrorPropagationOnMainThread(nullptr, false);
127 return true;
130 // The innerWindowId is only required if we are going to ReportError
131 // below, which is gated on this condition. The inner window correctness
132 // check is only going to succeed when the worker is accepting events.
133 if (workerIsAcceptingEvents) {
134 aWorkerPrivate->AssertInnerWindowIsCorrect();
135 innerWindowId = aWorkerPrivate->WindowID();
139 // Don't fire this event if the JS object has been disconnected from the
140 // private object.
141 if (!workerIsAcceptingEvents) {
142 return true;
145 WorkerErrorReport::ReportError(aCx, parent, fireAtScope,
146 aWorkerPrivate->ParentEventTargetRef(),
147 std::move(mReport), innerWindowId);
148 return true;
152 class ReportGenericErrorRunnable final : public WorkerDebuggeeRunnable {
153 public:
154 static void CreateAndDispatch(WorkerPrivate* aWorkerPrivate) {
155 MOZ_ASSERT(aWorkerPrivate);
156 aWorkerPrivate->AssertIsOnWorkerThread();
158 RefPtr<ReportGenericErrorRunnable> runnable =
159 new ReportGenericErrorRunnable(aWorkerPrivate);
160 runnable->Dispatch();
163 private:
164 explicit ReportGenericErrorRunnable(WorkerPrivate* aWorkerPrivate)
165 : WorkerDebuggeeRunnable(aWorkerPrivate) {
166 aWorkerPrivate->AssertIsOnWorkerThread();
169 void PostDispatch(WorkerPrivate* aWorkerPrivate,
170 bool aDispatchResult) override {
171 aWorkerPrivate->AssertIsOnWorkerThread();
173 // Dispatch may fail if the worker was canceled, no need to report that as
174 // an error, so don't call base class PostDispatch.
177 bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
178 // Once a window has frozen its workers, their
179 // mMainThreadDebuggeeEventTargets should be paused, and their
180 // WorkerDebuggeeRunnables should not be being executed. The same goes for
181 // WorkerDebuggeeRunnables sent from child to parent workers, but since a
182 // frozen parent worker runs only control runnables anyway, that is taken
183 // care of naturally.
184 MOZ_ASSERT(!aWorkerPrivate->IsFrozen());
186 // Similarly for paused windows; all its workers should have been informed.
187 // (Subworkers are unaffected by paused windows.)
188 MOZ_ASSERT(!aWorkerPrivate->IsParentWindowPaused());
190 if (aWorkerPrivate->IsSharedWorker()) {
191 aWorkerPrivate->GetRemoteWorkerController()->ErrorPropagationOnMainThread(
192 nullptr, false);
193 return true;
196 if (aWorkerPrivate->IsServiceWorker()) {
197 RefPtr<RemoteWorkerChild> actor(
198 aWorkerPrivate->GetRemoteWorkerControllerWeakRef());
200 Unused << NS_WARN_IF(!actor);
202 if (actor) {
203 actor->ErrorPropagationOnMainThread(nullptr, false);
206 return true;
209 if (!aWorkerPrivate->IsAcceptingEvents()) {
210 return true;
213 RefPtr<mozilla::dom::EventTarget> parentEventTarget =
214 aWorkerPrivate->ParentEventTargetRef();
215 RefPtr<Event> event =
216 Event::Constructor(parentEventTarget, u"error"_ns, EventInit());
217 event->SetTrusted(true);
219 parentEventTarget->DispatchEvent(*event);
220 return true;
224 } // namespace
226 void WorkerErrorBase::AssignErrorBase(JSErrorBase* aReport) {
227 CopyUTF8toUTF16(MakeStringSpan(aReport->filename), mFilename);
228 mLineNumber = aReport->lineno;
229 mColumnNumber = aReport->column;
230 mErrorNumber = aReport->errorNumber;
233 void WorkerErrorNote::AssignErrorNote(JSErrorNotes::Note* aNote) {
234 WorkerErrorBase::AssignErrorBase(aNote);
235 xpc::ErrorNote::ErrorNoteToMessageString(aNote, mMessage);
238 WorkerErrorReport::WorkerErrorReport()
239 : mIsWarning(false), mExnType(JSEXN_ERR), mMutedError(false) {}
241 void WorkerErrorReport::AssignErrorReport(JSErrorReport* aReport) {
242 WorkerErrorBase::AssignErrorBase(aReport);
243 xpc::ErrorReport::ErrorReportToMessageString(aReport, mMessage);
245 mLine.Assign(aReport->linebuf(), aReport->linebufLength());
246 mIsWarning = aReport->isWarning();
247 MOZ_ASSERT(aReport->exnType >= JSEXN_FIRST && aReport->exnType < JSEXN_LIMIT);
248 mExnType = JSExnType(aReport->exnType);
249 mMutedError = aReport->isMuted;
251 if (aReport->notes) {
252 if (!mNotes.SetLength(aReport->notes->length(), fallible)) {
253 return;
256 size_t i = 0;
257 for (auto&& note : *aReport->notes) {
258 mNotes.ElementAt(i).AssignErrorNote(note.get());
259 i++;
264 // aWorkerPrivate is the worker thread we're on (or the main thread, if null)
265 // aTarget is the worker object that we are going to fire an error at
266 // (if any).
267 /* static */
268 void WorkerErrorReport::ReportError(
269 JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aFireAtScope,
270 DOMEventTargetHelper* aTarget, UniquePtr<WorkerErrorReport> aReport,
271 uint64_t aInnerWindowId, JS::Handle<JS::Value> aException) {
272 if (aWorkerPrivate) {
273 aWorkerPrivate->AssertIsOnWorkerThread();
274 } else {
275 AssertIsOnMainThread();
278 // We should not fire error events for warnings but instead make sure that
279 // they show up in the error console.
280 if (!aReport->mIsWarning) {
281 // First fire an ErrorEvent at the worker.
282 RootedDictionary<ErrorEventInit> init(aCx);
284 if (aReport->mMutedError) {
285 init.mMessage.AssignLiteral("Script error.");
286 } else {
287 init.mMessage = aReport->mMessage;
288 init.mFilename = aReport->mFilename;
289 init.mLineno = aReport->mLineNumber;
290 init.mColno = aReport->mColumnNumber;
291 init.mError = aException;
294 init.mCancelable = true;
295 init.mBubbles = false;
297 if (aTarget) {
298 RefPtr<ErrorEvent> event =
299 ErrorEvent::Constructor(aTarget, u"error"_ns, init);
300 event->SetTrusted(true);
302 bool defaultActionEnabled =
303 aTarget->DispatchEvent(*event, CallerType::System, IgnoreErrors());
304 if (!defaultActionEnabled) {
305 return;
309 // Now fire an event at the global object, but don't do that if the error
310 // code is too much recursion and this is the same script threw the error.
311 // XXXbz the interaction of this with worker errors seems kinda broken.
312 // An overrecursion in the debugger or debugger sandbox will get turned
313 // into an error event on our parent worker!
314 // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks making this
315 // better.
316 if (aFireAtScope &&
317 (aTarget || aReport->mErrorNumber != JSMSG_OVER_RECURSED)) {
318 JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx));
319 NS_ASSERTION(global, "This should never be null!");
321 nsEventStatus status = nsEventStatus_eIgnore;
323 if (aWorkerPrivate) {
324 RefPtr<WorkerGlobalScope> globalScope;
325 UNWRAP_OBJECT(WorkerGlobalScope, &global, globalScope);
327 if (!globalScope) {
328 WorkerDebuggerGlobalScope* globalScope = nullptr;
329 UNWRAP_OBJECT(WorkerDebuggerGlobalScope, &global, globalScope);
331 MOZ_ASSERT_IF(globalScope,
332 globalScope->GetWrapperPreserveColor() == global);
333 if (globalScope || IsWorkerDebuggerSandbox(global)) {
334 aWorkerPrivate->ReportErrorToDebugger(
335 aReport->mFilename, aReport->mLineNumber, aReport->mMessage);
336 return;
339 MOZ_ASSERT(SimpleGlobalObject::SimpleGlobalType(global) ==
340 SimpleGlobalObject::GlobalType::BindingDetail);
341 // XXXbz We should really log this to console, but unwinding out of
342 // this stuff without ending up firing any events is ... hard. Just
343 // return for now.
344 // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks
345 // making this better.
346 return;
349 MOZ_ASSERT(globalScope->GetWrapperPreserveColor() == global);
351 RefPtr<ErrorEvent> event =
352 ErrorEvent::Constructor(aTarget, u"error"_ns, init);
353 event->SetTrusted(true);
355 // TODO: Bug 1506441
356 if (NS_FAILED(EventDispatcher::DispatchDOMEvent(
357 MOZ_KnownLive(ToSupports(globalScope)), nullptr, event, nullptr,
358 &status))) {
359 NS_WARNING("Failed to dispatch worker thread error event!");
360 status = nsEventStatus_eIgnore;
362 } else if (nsGlobalWindowInner* win = xpc::WindowOrNull(global)) {
363 MOZ_ASSERT(NS_IsMainThread());
365 if (!win->HandleScriptError(init, &status)) {
366 NS_WARNING("Failed to dispatch main thread error event!");
367 status = nsEventStatus_eIgnore;
371 // Was preventDefault() called?
372 if (status == nsEventStatus_eConsumeNoDefault) {
373 return;
378 // Now fire a runnable to do the same on the parent's thread if we can.
379 if (aWorkerPrivate) {
380 RefPtr<ReportErrorRunnable> runnable =
381 new ReportErrorRunnable(aWorkerPrivate, std::move(aReport));
382 runnable->Dispatch();
383 return;
386 // Otherwise log an error to the error console.
387 WorkerErrorReport::LogErrorToConsole(aCx, *aReport, aInnerWindowId);
390 /* static */
391 void WorkerErrorReport::LogErrorToConsole(JSContext* aCx,
392 WorkerErrorReport& aReport,
393 uint64_t aInnerWindowId) {
394 JS::Rooted<JSObject*> stack(aCx, aReport.ReadStack(aCx));
395 JS::Rooted<JSObject*> stackGlobal(aCx, JS::CurrentGlobalOrNull(aCx));
397 ErrorData errorData(
398 aReport.mIsWarning, aReport.mLineNumber, aReport.mColumnNumber,
399 aReport.mMessage, aReport.mFilename, aReport.mLine,
400 TransformIntoNewArray(aReport.mNotes, [](const WorkerErrorNote& note) {
401 return ErrorDataNote(note.mLineNumber, note.mColumnNumber,
402 note.mMessage, note.mFilename);
403 }));
404 LogErrorToConsole(errorData, aInnerWindowId, stack, stackGlobal);
407 /* static */
408 void WorkerErrorReport::LogErrorToConsole(const ErrorData& aReport,
409 uint64_t aInnerWindowId,
410 JS::Handle<JSObject*> aStack,
411 JS::Handle<JSObject*> aStackGlobal) {
412 AssertIsOnMainThread();
414 RefPtr<nsScriptErrorBase> scriptError =
415 CreateScriptError(nullptr, JS::NothingHandleValue, aStack, aStackGlobal);
417 NS_WARNING_ASSERTION(scriptError, "Failed to create script error!");
419 if (scriptError) {
420 nsAutoCString category("Web Worker");
421 uint32_t flags = aReport.isWarning() ? nsIScriptError::warningFlag
422 : nsIScriptError::errorFlag;
423 if (NS_FAILED(scriptError->nsIScriptError::InitWithWindowID(
424 aReport.message(), aReport.filename(), aReport.line(),
425 aReport.lineNumber(), aReport.columnNumber(), flags, category,
426 aInnerWindowId))) {
427 NS_WARNING("Failed to init script error!");
428 scriptError = nullptr;
431 for (const ErrorDataNote& note : aReport.notes()) {
432 nsScriptErrorNote* noteObject = new nsScriptErrorNote();
433 noteObject->Init(note.message(), note.filename(), 0, note.lineNumber(),
434 note.columnNumber());
435 scriptError->AddNote(noteObject);
439 nsCOMPtr<nsIConsoleService> consoleService =
440 do_GetService(NS_CONSOLESERVICE_CONTRACTID);
441 NS_WARNING_ASSERTION(consoleService, "Failed to get console service!");
443 if (consoleService) {
444 if (scriptError) {
445 if (NS_SUCCEEDED(consoleService->LogMessage(scriptError))) {
446 return;
448 NS_WARNING("LogMessage failed!");
449 } else if (NS_SUCCEEDED(consoleService->LogStringMessage(
450 aReport.message().BeginReading()))) {
451 return;
453 NS_WARNING("LogStringMessage failed!");
456 NS_ConvertUTF16toUTF8 msg(aReport.message());
457 NS_ConvertUTF16toUTF8 filename(aReport.filename());
459 static const char kErrorString[] = "JS error in Web Worker: %s [%s:%u]";
461 #ifdef ANDROID
462 __android_log_print(ANDROID_LOG_INFO, "Gecko", kErrorString, msg.get(),
463 filename.get(), aReport.lineNumber());
464 #endif
466 fprintf(stderr, kErrorString, msg.get(), filename.get(),
467 aReport.lineNumber());
468 fflush(stderr);
471 /* static */
472 void WorkerErrorReport::CreateAndDispatchGenericErrorRunnableToParent(
473 WorkerPrivate* aWorkerPrivate) {
474 ReportGenericErrorRunnable::CreateAndDispatch(aWorkerPrivate);
477 } // namespace mozilla::dom