WorkerError.cpp (15738B)
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/. */ 6 7 #include "WorkerError.h" 8 9 #include <stdio.h> 10 11 #include <algorithm> 12 #include <utility> 13 14 #include "MainThreadUtils.h" 15 #include "WorkerPrivate.h" 16 #include "WorkerRunnable.h" 17 #include "WorkerScope.h" 18 #include "js/ComparisonOperators.h" 19 #include "js/UniquePtr.h" 20 #include "js/friend/ErrorMessages.h" 21 #include "jsapi.h" 22 #include "mozilla/ArrayAlgorithm.h" 23 #include "mozilla/ArrayIterator.h" 24 #include "mozilla/Assertions.h" 25 #include "mozilla/BasicEvents.h" 26 #include "mozilla/DOMEventTargetHelper.h" 27 #include "mozilla/ErrorResult.h" 28 #include "mozilla/EventDispatcher.h" 29 #include "mozilla/RefPtr.h" 30 #include "mozilla/dom/BindingDeclarations.h" 31 #include "mozilla/dom/BindingUtils.h" 32 #include "mozilla/dom/ErrorEvent.h" 33 #include "mozilla/dom/ErrorEventBinding.h" 34 #include "mozilla/dom/Event.h" 35 #include "mozilla/dom/EventBinding.h" 36 #include "mozilla/dom/EventTarget.h" 37 #include "mozilla/dom/RemoteWorkerChild.h" 38 #include "mozilla/dom/RemoteWorkerTypes.h" 39 #include "mozilla/dom/RootedDictionary.h" 40 #include "mozilla/dom/ServiceWorkerManager.h" 41 #include "mozilla/dom/ServiceWorkerUtils.h" 42 #include "mozilla/dom/SimpleGlobalObject.h" 43 #include "mozilla/dom/Worker.h" 44 #include "mozilla/dom/WorkerCommon.h" 45 #include "mozilla/dom/WorkerDebuggerGlobalScopeBinding.h" 46 #include "mozilla/dom/WorkerGlobalScopeBinding.h" 47 #include "mozilla/fallible.h" 48 #include "nsCOMPtr.h" 49 #include "nsDebug.h" 50 #include "nsGlobalWindowInner.h" 51 #include "nsIConsoleService.h" 52 #include "nsIScriptError.h" 53 #include "nsScriptError.h" 54 #include "nsServiceManagerUtils.h" 55 #include "nsString.h" 56 #include "nsWrapperCacheInlines.h" 57 #include "nscore.h" 58 #include "xpcpublic.h" 59 60 namespace mozilla::dom { 61 62 namespace { 63 64 class ReportErrorRunnable final : public WorkerParentDebuggeeRunnable { 65 UniquePtr<WorkerErrorReport> mReport; 66 67 public: 68 ReportErrorRunnable(WorkerPrivate* aWorkerPrivate, 69 UniquePtr<WorkerErrorReport> aReport) 70 : WorkerParentDebuggeeRunnable("ReportErrorRunnable"), 71 mReport(std::move(aReport)) {} 72 73 private: 74 virtual void PostDispatch(WorkerPrivate* aWorkerPrivate, 75 bool aDispatchResult) override { 76 aWorkerPrivate->AssertIsOnWorkerThread(); 77 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. 80 } 81 82 virtual bool WorkerRun(JSContext* aCx, 83 WorkerPrivate* aWorkerPrivate) override { 84 uint64_t innerWindowId; 85 bool fireAtScope = true; 86 87 bool workerIsAcceptingEvents = aWorkerPrivate->IsAcceptingEvents(); 88 89 WorkerPrivate* parent = aWorkerPrivate->GetParent(); 90 if (parent) { 91 innerWindowId = 0; 92 } else { 93 AssertIsOnMainThread(); 94 95 if (aWorkerPrivate->IsSharedWorker()) { 96 aWorkerPrivate->GetRemoteWorkerController() 97 ->ErrorPropagationOnMainThread(mReport.get(), 98 /* isErrorEvent */ true); 99 return true; 100 } 101 102 // Service workers do not have a main thread parent global, so normal 103 // worker error reporting will crash. Instead, pass the error to 104 // the ServiceWorkerManager to report on any controlled documents. 105 if (aWorkerPrivate->IsServiceWorker()) { 106 RefPtr<RemoteWorkerChild> actor( 107 aWorkerPrivate->GetRemoteWorkerController()); 108 109 (void)NS_WARN_IF(!actor); 110 111 if (actor) { 112 actor->ErrorPropagationOnMainThread(nullptr, false); 113 } 114 115 return true; 116 } 117 118 // The innerWindowId is only required if we are going to ReportError 119 // below, which is gated on this condition. The inner window correctness 120 // check is only going to succeed when the worker is accepting events. 121 if (workerIsAcceptingEvents) { 122 aWorkerPrivate->AssertInnerWindowIsCorrect(); 123 innerWindowId = aWorkerPrivate->WindowID(); 124 } 125 } 126 127 // Don't fire this event if the JS object has been disconnected from the 128 // private object. 129 if (!workerIsAcceptingEvents) { 130 return true; 131 } 132 133 WorkerErrorReport::ReportError(aCx, parent, fireAtScope, 134 aWorkerPrivate->ParentEventTargetRef(), 135 std::move(mReport), innerWindowId); 136 return true; 137 } 138 }; 139 140 class ReportGenericErrorRunnable final : public WorkerParentDebuggeeRunnable { 141 public: 142 static void CreateAndDispatch(WorkerPrivate* aWorkerPrivate) { 143 MOZ_ASSERT(aWorkerPrivate); 144 aWorkerPrivate->AssertIsOnWorkerThread(); 145 146 RefPtr<ReportGenericErrorRunnable> runnable = 147 new ReportGenericErrorRunnable(aWorkerPrivate); 148 runnable->Dispatch(aWorkerPrivate); 149 } 150 151 private: 152 explicit ReportGenericErrorRunnable(WorkerPrivate* aWorkerPrivate) 153 : WorkerParentDebuggeeRunnable("ReportGenericErrorRunnable") { 154 aWorkerPrivate->AssertIsOnWorkerThread(); 155 } 156 157 void PostDispatch(WorkerPrivate* aWorkerPrivate, 158 bool aDispatchResult) override { 159 aWorkerPrivate->AssertIsOnWorkerThread(); 160 161 // Dispatch may fail if the worker was canceled, no need to report that as 162 // an error, so don't call base class PostDispatch. 163 } 164 165 bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { 166 if (!aWorkerPrivate->IsAcceptingEvents()) { 167 return true; 168 } 169 170 if (aWorkerPrivate->IsSharedWorker()) { 171 aWorkerPrivate->GetRemoteWorkerController()->ErrorPropagationOnMainThread( 172 nullptr, false); 173 return true; 174 } 175 176 if (aWorkerPrivate->IsServiceWorker()) { 177 RefPtr<RemoteWorkerChild> actor( 178 aWorkerPrivate->GetRemoteWorkerController()); 179 180 (void)NS_WARN_IF(!actor); 181 182 if (actor) { 183 actor->ErrorPropagationOnMainThread(nullptr, false); 184 } 185 186 return true; 187 } 188 189 RefPtr<mozilla::dom::EventTarget> parentEventTarget = 190 aWorkerPrivate->ParentEventTargetRef(); 191 RefPtr<Event> event = 192 Event::Constructor(parentEventTarget, u"error"_ns, EventInit()); 193 event->SetTrusted(true); 194 195 parentEventTarget->DispatchEvent(*event); 196 return true; 197 } 198 }; 199 200 } // namespace 201 202 void WorkerErrorBase::AssignErrorBase(JSErrorBase* aReport) { 203 mFilename.Assign(aReport->filename.c_str()); 204 mLineNumber = aReport->lineno; 205 mColumnNumber = aReport->column.oneOriginValue(); 206 mErrorNumber = aReport->errorNumber; 207 } 208 209 void WorkerErrorNote::AssignErrorNote(JSErrorNotes::Note* aNote) { 210 WorkerErrorBase::AssignErrorBase(aNote); 211 xpc::ErrorNote::ErrorNoteToMessageString(aNote, mMessage); 212 } 213 214 WorkerErrorReport::WorkerErrorReport() 215 : mIsWarning(false), mExnType(JSEXN_ERR), mMutedError(false) {} 216 217 void WorkerErrorReport::AssignErrorReport(JSErrorReport* aReport) { 218 WorkerErrorBase::AssignErrorBase(aReport); 219 xpc::ErrorReport::ErrorReportToMessageString(aReport, mMessage); 220 mIsWarning = aReport->isWarning(); 221 MOZ_ASSERT(aReport->exnType >= JSEXN_FIRST && aReport->exnType < JSEXN_LIMIT); 222 mExnType = JSExnType(aReport->exnType); 223 mMutedError = aReport->isMuted; 224 225 if (aReport->notes) { 226 if (!mNotes.SetLength(aReport->notes->length(), fallible)) { 227 return; 228 } 229 230 size_t i = 0; 231 for (auto&& note : *aReport->notes) { 232 mNotes.ElementAt(i).AssignErrorNote(note.get()); 233 i++; 234 } 235 } 236 } 237 238 // aWorkerPrivate is the worker thread we're on (or the main thread, if null) 239 // aTarget is the worker object that we are going to fire an error at 240 // (if any). 241 /* static */ 242 void WorkerErrorReport::ReportError( 243 JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aFireAtScope, 244 DOMEventTargetHelper* aTarget, UniquePtr<WorkerErrorReport> aReport, 245 uint64_t aInnerWindowId, JS::Handle<JS::Value> aException) { 246 if (aWorkerPrivate) { 247 aWorkerPrivate->AssertIsOnWorkerThread(); 248 } else { 249 AssertIsOnMainThread(); 250 } 251 252 // We should not fire error events for warnings but instead make sure that 253 // they show up in the error console. 254 if (!aReport->mIsWarning) { 255 // First fire an ErrorEvent at the worker. 256 RootedDictionary<ErrorEventInit> init(aCx); 257 258 if (aReport->mMutedError) { 259 init.mMessage.AssignLiteral("Script error."); 260 } else { 261 init.mMessage = aReport->mMessage; 262 init.mFilename = aReport->mFilename; 263 init.mLineno = aReport->mLineNumber; 264 init.mColno = aReport->mColumnNumber; 265 init.mError = aException; 266 } 267 268 init.mCancelable = true; 269 init.mBubbles = false; 270 271 if (aTarget) { 272 RefPtr<ErrorEvent> event = 273 ErrorEvent::Constructor(aTarget, u"error"_ns, init); 274 event->SetTrusted(true); 275 276 bool defaultActionEnabled = 277 aTarget->DispatchEvent(*event, CallerType::System, IgnoreErrors()); 278 if (!defaultActionEnabled) { 279 return; 280 } 281 } 282 283 // Now fire an event at the global object, but don't do that if the error 284 // code is too much recursion and this is the same script threw the error. 285 // XXXbz the interaction of this with worker errors seems kinda broken. 286 // An overrecursion in the debugger or debugger sandbox will get turned 287 // into an error event on our parent worker! 288 // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks making this 289 // better. 290 if (aFireAtScope && 291 (aTarget || aReport->mErrorNumber != JSMSG_OVER_RECURSED)) { 292 JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx)); 293 NS_ASSERTION(global, "This should never be null!"); 294 295 nsEventStatus status = nsEventStatus_eIgnore; 296 297 if (aWorkerPrivate) { 298 RefPtr<WorkerGlobalScope> globalScope; 299 UNWRAP_OBJECT(WorkerGlobalScope, &global, globalScope); 300 301 if (!globalScope) { 302 WorkerDebuggerGlobalScope* globalScope = nullptr; 303 UNWRAP_OBJECT(WorkerDebuggerGlobalScope, &global, globalScope); 304 305 MOZ_ASSERT_IF(globalScope, 306 globalScope->GetWrapperPreserveColor() == global); 307 if (globalScope || IsWorkerDebuggerSandbox(global)) { 308 aWorkerPrivate->ReportErrorToDebugger( 309 aReport->mFilename, aReport->mLineNumber, aReport->mMessage); 310 return; 311 } 312 313 MOZ_ASSERT(SimpleGlobalObject::SimpleGlobalType(global) == 314 SimpleGlobalObject::GlobalType::BindingDetail); 315 // XXXbz We should really log this to console, but unwinding out of 316 // this stuff without ending up firing any events is ... hard. Just 317 // return for now. 318 // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks 319 // making this better. 320 return; 321 } 322 323 MOZ_ASSERT(globalScope->GetWrapperPreserveColor() == global); 324 325 RefPtr<ErrorEvent> event = ErrorEvent::Constructor( 326 aTarget ? aTarget : globalScope, u"error"_ns, init); 327 event->SetTrusted(true); 328 329 if (NS_FAILED(EventDispatcher::DispatchDOMEvent( 330 globalScope, nullptr, event, nullptr, &status))) { 331 NS_WARNING("Failed to dispatch worker thread error event!"); 332 status = nsEventStatus_eIgnore; 333 } 334 } else if (nsGlobalWindowInner* win = xpc::WindowOrNull(global)) { 335 MOZ_ASSERT(NS_IsMainThread()); 336 337 if (!win->HandleScriptError(init, &status)) { 338 NS_WARNING("Failed to dispatch main thread error event!"); 339 status = nsEventStatus_eIgnore; 340 } 341 } 342 343 // Was preventDefault() called? 344 if (status == nsEventStatus_eConsumeNoDefault) { 345 return; 346 } 347 } 348 } 349 350 // Now fire a runnable to do the same on the parent's thread if we can. 351 if (aWorkerPrivate) { 352 RefPtr<ReportErrorRunnable> runnable = 353 new ReportErrorRunnable(aWorkerPrivate, std::move(aReport)); 354 runnable->Dispatch(aWorkerPrivate); 355 return; 356 } 357 358 // Otherwise log an error to the error console. 359 WorkerErrorReport::LogErrorToConsole(aCx, *aReport, aInnerWindowId); 360 } 361 362 /* static */ 363 void WorkerErrorReport::LogErrorToConsole(JSContext* aCx, 364 WorkerErrorReport& aReport, 365 uint64_t aInnerWindowId) { 366 JS::Rooted<JSObject*> stack(aCx, aReport.ReadStack(aCx)); 367 JS::Rooted<JSObject*> stackGlobal(aCx, JS::CurrentGlobalOrNull(aCx)); 368 369 ErrorData errorData( 370 aReport.mIsWarning, aReport.mLineNumber, aReport.mColumnNumber, 371 aReport.mMessage, aReport.mFilename, 372 TransformIntoNewArray(aReport.mNotes, [](const WorkerErrorNote& note) { 373 return ErrorDataNote(note.mLineNumber, note.mColumnNumber, 374 note.mMessage, note.mFilename); 375 })); 376 LogErrorToConsole(errorData, aInnerWindowId, stack, stackGlobal); 377 } 378 379 /* static */ 380 void WorkerErrorReport::LogErrorToConsole(const ErrorData& aReport, 381 uint64_t aInnerWindowId, 382 JS::Handle<JSObject*> aStack, 383 JS::Handle<JSObject*> aStackGlobal) { 384 AssertIsOnMainThread(); 385 386 RefPtr<nsScriptErrorBase> scriptError = 387 CreateScriptError(nullptr, JS::NothingHandleValue, aStack, aStackGlobal); 388 389 NS_WARNING_ASSERTION(scriptError, "Failed to create script error!"); 390 391 if (scriptError) { 392 nsAutoCString category("Web Worker"); 393 uint32_t flags = aReport.isWarning() ? nsIScriptError::warningFlag 394 : nsIScriptError::errorFlag; 395 if (NS_FAILED(scriptError->nsIScriptError::InitWithWindowID( 396 aReport.message(), aReport.filename(), aReport.lineNumber(), 397 aReport.columnNumber(), flags, category, aInnerWindowId))) { 398 NS_WARNING("Failed to init script error!"); 399 scriptError = nullptr; 400 } 401 402 for (const ErrorDataNote& note : aReport.notes()) { 403 nsScriptErrorNote* noteObject = new nsScriptErrorNote(); 404 noteObject->Init(note.message(), note.filename(), 0, note.lineNumber(), 405 note.columnNumber()); 406 scriptError->AddNote(noteObject); 407 } 408 } 409 410 nsCOMPtr<nsIConsoleService> consoleService = 411 do_GetService(NS_CONSOLESERVICE_CONTRACTID); 412 NS_WARNING_ASSERTION(consoleService, "Failed to get console service!"); 413 414 if (consoleService) { 415 if (scriptError) { 416 if (NS_SUCCEEDED(consoleService->LogMessage(scriptError))) { 417 return; 418 } 419 NS_WARNING("LogMessage failed!"); 420 } else if (NS_SUCCEEDED(consoleService->LogStringMessage( 421 aReport.message().BeginReading()))) { 422 return; 423 } 424 NS_WARNING("LogStringMessage failed!"); 425 } 426 427 NS_ConvertUTF16toUTF8 msg(aReport.message()); 428 429 static const char kErrorString[] = "JS error in Web Worker: %s [%s:%u]"; 430 431 #ifdef ANDROID 432 __android_log_print(ANDROID_LOG_INFO, "Gecko", kErrorString, msg.get(), 433 aReport.filename().get(), aReport.lineNumber()); 434 #endif 435 436 fprintf(stderr, kErrorString, msg.get(), aReport.filename().get(), 437 aReport.lineNumber()); 438 fflush(stderr); 439 } 440 441 /* static */ 442 void WorkerErrorReport::LogErrorToConsole(const nsAString& aMessage) { 443 AssertIsOnMainThread(); 444 445 nsCOMPtr<nsIConsoleService> consoleService = 446 do_GetService(NS_CONSOLESERVICE_CONTRACTID); 447 NS_WARNING_ASSERTION(consoleService, "Failed to get console service!"); 448 449 consoleService->LogStringMessage(aMessage.BeginReading()); 450 } 451 452 /* static */ 453 void WorkerErrorReport::CreateAndDispatchGenericErrorRunnableToParent( 454 WorkerPrivate* aWorkerPrivate) { 455 ReportGenericErrorRunnable::CreateAndDispatch(aWorkerPrivate); 456 } 457 458 } // namespace mozilla::dom