Bug 1274922 part 6 - Rewrite the shell's error reporting to handle exceptions in the embedding. r=jorendorff
authorJan de Mooij <jdemooij@mozilla.com>
Tue, 31 May 2016 08:07:00 +0200
changeset 338641 7a5ff0cdea30f55b0972659ce300a3179633dc80
parent 338640 b2eb560ec7c8d35c8e0b65915e663e321bbc6b3a
child 338642 8b0c9204fec755257cdecd21e4c10423adb769af
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjorendorff
bugs1274922
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1274922 part 6 - Rewrite the shell's error reporting to handle exceptions in the embedding. r=jorendorff
js/src/shell/js.cpp
js/src/shell/jsshell.h
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -502,27 +502,24 @@ SkipUTF8BOM(FILE* file)
 }
 
 void
 EnvironmentPreparer::invoke(HandleObject scope, Closure& closure)
 {
     MOZ_ASSERT(!JS_IsExceptionPending(cx));
 
     AutoCompartment ac(cx, scope);
+    AutoReportException are(cx);
     if (!closure(cx))
-        JS_ReportPendingException(cx);
-
-    MOZ_ASSERT(!JS_IsExceptionPending(cx));
-}
-
-static void
+        return;
+}
+
+static MOZ_MUST_USE bool
 RunFile(JSContext* cx, const char* filename, FILE* file, bool compileOnly)
 {
-    ShellRuntime* sr = GetShellRuntime(cx);
-
     SkipUTF8BOM(file);
 
     // To support the UNIX #! shell hack, gobble the first line if it starts
     // with '#'.
     int ch = fgetc(file);
     if (ch == '#') {
         while ((ch = fgetc(file)) != EOF) {
             if (ch == '\n' || ch == '\r')
@@ -537,32 +534,33 @@ RunFile(JSContext* cx, const char* filen
     {
         CompileOptions options(cx);
         options.setIntroductionType("js shell file")
                .setUTF8(true)
                .setFileAndLine(filename, 1)
                .setIsRunOnce(true)
                .setNoScriptRval(true);
 
-        (void) JS::Compile(cx, options, file, &script);
+        if (!JS::Compile(cx, options, file, &script))
+            return false;
+        MOZ_ASSERT(script);
     }
 
     #ifdef DEBUG
         if (dumpEntrainedVariables)
             AnalyzeEntrainedVariables(cx, script);
     #endif
-    if (script && !compileOnly) {
-        if (!JS_ExecuteScript(cx, script)) {
-            if (!sr->quitting && sr->exitCode != EXITCODE_TIMEOUT)
-                sr->exitCode = EXITCODE_RUNTIME_ERROR;
-        }
+    if (!compileOnly) {
+        if (!JS_ExecuteScript(cx, script))
+            return false;
         int64_t t2 = PRMJ_Now() - t1;
         if (printTiming)
             printf("runtime = %.3f ms\n", double(t2) / PRMJ_USEC_PER_MSEC);
     }
+    return true;
 }
 
 static bool
 InitModuleLoader(JSContext* cx)
 {
     // Decompress and evaluate the embedded module loader source to initialize
     // the module loader for the current compartment.
 
@@ -618,38 +616,33 @@ GetImportMethod(JSContext* cx, HandleObj
     RootedObject object(cx, &value.toObject());
     if (!object->is<JSFunction>())
         return false;
 
     resultOut.set(&object->as<JSFunction>());
     return true;
 }
 
-static void
+static MOZ_MUST_USE bool
 RunModule(JSContext* cx, const char* filename, FILE* file, bool compileOnly)
 {
-    // Exectute a module by calling |Reflect.Loader.import(filename)|.
-
-    ShellRuntime* sr = GetShellRuntime(cx);
+    // Execute a module by calling |Reflect.Loader.import(filename)|.
 
     RootedObject loaderObj(cx);
     MOZ_ALWAYS_TRUE(GetLoaderObject(cx, &loaderObj));
 
     RootedFunction importFun(cx);
     MOZ_ALWAYS_TRUE(GetImportMethod(cx, loaderObj, &importFun));
 
     JS::AutoValueArray<2> args(cx);
     args[0].setString(JS_NewStringCopyZ(cx, filename));
     args[1].setUndefined();
 
     RootedValue value(cx);
-    if (!JS_CallFunction(cx, loaderObj, importFun, args, &value)) {
-        sr->exitCode = EXITCODE_RUNTIME_ERROR;
-        return;
-    }
+    return JS_CallFunction(cx, loaderObj, importFun, args, &value);
 }
 
 #ifdef SPIDERMONKEY_PROMISE
 static bool
 ShellEnqueuePromiseJobCallback(JSContext* cx, JS::HandleObject job, JS::HandleObject allocationSite,
                                void* data)
 {
     ShellRuntime* sr = GetShellRuntime(cx);
@@ -669,18 +662,20 @@ DrainJobQueue(JSContext* cx)
     JS::HandleValueArray args(JS::HandleValueArray::empty());
     RootedValue rval(cx);
     // Execute jobs in a loop until we've reached the end of the queue.
     // Since executing a job can trigger enqueuing of additional jobs,
     // it's crucial to re-check the queue length during each iteration.
     for (size_t i = 0; i < sr->jobQueue.length(); i++) {
         job = sr->jobQueue[i];
         AutoCompartment ac(cx, job);
-        if (!JS::Call(cx, UndefinedHandleValue, job, args, &rval))
-            JS_ReportPendingException(cx);
+        {
+            AutoReportException are(cx);
+            JS::Call(cx, UndefinedHandleValue, job, args, &rval);
+        }
         sr->jobQueue[i].set(nullptr);
     }
     sr->jobQueue.clear();
 #endif // SPIDERMONKEY_PROMISE
     return true;
 }
 
 static bool
@@ -766,17 +761,17 @@ EvalAndPrint(JSContext* cx, const char* 
         if (!utf8chars)
             return false;
         fprintf(gOutFile->fp, "%s\n", utf8chars);
         JS_free(cx, utf8chars);
     }
     return true;
 }
 
-static void
+static MOZ_MUST_USE bool
 ReadEvalPrintLoop(JSContext* cx, FILE* in, bool compileOnly)
 {
     ShellRuntime* sr = GetShellRuntime(cx);
     int lineno = 1;
     bool hitEOF = false;
 
     do {
         /*
@@ -793,39 +788,41 @@ ReadEvalPrintLoop(JSContext* cx, FILE* i
             ScheduleWatchdog(cx->runtime(), -1);
             sr->serviceInterrupt = false;
             errno = 0;
 
             char* line = GetLine(in, startline == lineno ? "js> " : "");
             if (!line) {
                 if (errno) {
                     JS_ReportError(cx, strerror(errno));
-                    return;
+                    return false;
                 }
                 hitEOF = true;
                 break;
             }
 
             if (!buffer.append(line, strlen(line)) || !buffer.append('\n'))
-                return;
+                return false;
 
             lineno++;
             if (!ScheduleWatchdog(cx->runtime(), sr->timeoutInterval)) {
                 hitEOF = true;
                 break;
             }
         } while (!JS_BufferIsCompilableUnit(cx, cx->global(), buffer.begin(), buffer.length()));
 
         if (hitEOF && buffer.empty())
             break;
 
-        if (!EvalAndPrint(cx, buffer.begin(), buffer.length(), startline, compileOnly)) {
-            // Catch the error, report it, and keep going.
-            JS_ReportPendingException(cx);
+        {
+            // Report exceptions but keep going.
+            AutoReportException are(cx);
+            (void) EvalAndPrint(cx, buffer.begin(), buffer.length(), startline, compileOnly);
         }
+
         // If a let or const fail to initialize they will remain in an unusable
         // without further intervention. This call cleans up the global scope,
         // setting uninitialized lexicals to undefined so that they may still
         // be used. This behavior is _only_ acceptable in the context of the repl.
         if (JS::ForceLexicalInitialization(cx, globalLexical) && gErrFile->isOpen()) {
             fputs("Warning: According to the standard, after the above exception,\n"
                   "Warning: the global bindings should be permanently uninitialized.\n"
                   "Warning: We have non-standard-ly initialized them to `undefined`"
@@ -834,51 +831,58 @@ ReadEvalPrintLoop(JSContext* cx, FILE* i
         }
 
         DrainJobQueue(cx);
 
     } while (!hitEOF && !sr->quitting);
 
     if (gOutFile->isOpen())
         fprintf(gOutFile->fp, "\n");
+
+    return true;
 }
 
 enum FileKind
 {
     FileScript,
     FileModule
 };
 
-static void
+static MOZ_MUST_USE bool
 Process(JSContext* cx, const char* filename, bool forceTTY, FileKind kind = FileScript)
 {
     FILE* file;
     if (forceTTY || !filename || strcmp(filename, "-") == 0) {
         file = stdin;
     } else {
         file = fopen(filename, "r");
         if (!file) {
             JS_ReportErrorNumber(cx, my_GetErrorMessage, nullptr,
                                  JSSMSG_CANT_OPEN, filename, strerror(errno));
-            return;
+            return false;
         }
     }
     AutoCloseFile autoClose(file);
 
     if (!forceTTY && !isatty(fileno(file))) {
         // It's not interactive - just execute it.
-        if (kind == FileScript)
-            RunFile(cx, filename, file, compileOnly);
-        else
-            RunModule(cx, filename, file, compileOnly);
+        if (kind == FileScript) {
+            if (!RunFile(cx, filename, file, compileOnly))
+                return false;
+        } else {
+            if (!RunModule(cx, filename, file, compileOnly))
+                return false;
+        }
     } else {
         // It's an interactive filehandle; drop into read-eval-print loop.
         MOZ_ASSERT(kind == FileScript);
-        ReadEvalPrintLoop(cx, file, compileOnly);
-    }
+        if (!ReadEvalPrintLoop(cx, file, compileOnly))
+            return false;
+    }
+    return true;
 }
 
 static bool
 Version(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     JSVersion origVersion = JS_GetVersion(cx);
     if (args.length() == 0 || args[0].isUndefined()) {
@@ -1229,17 +1233,16 @@ class AutoNewContext
     AutoNewContext() : oldcx(nullptr), newcx(nullptr) {}
 
     bool enter(JSContext* cx) {
         MOZ_ASSERT(!JS_IsExceptionPending(cx));
         oldcx = cx;
         newcx = NewContext(JS_GetRuntime(cx));
         if (!newcx)
             return false;
-        JS::ContextOptionsRef(newcx).setDontReportUncaught(true);
 
         newRequest.emplace(newcx);
         newCompartment.emplace(newcx, JS::CurrentGlobalOrNull(cx));
         return true;
     }
 
     JSContext* get() { return newcx; }
 
@@ -2942,17 +2945,17 @@ WorkerMain(void* arg)
         JS_DestroyRuntime(rt);
         js_delete(input);
         return;
     }
 
     sr->isWorker = true;
     JS_SetRuntimePrivate(rt, sr.get());
     JS_SetFutexCanWait(rt);
-    JS_SetErrorReporter(rt, my_ErrorReporter);
+    JS_SetErrorReporter(rt, WarningReporter);
     JS_InitDestroyPrincipalsCallback(rt, ShellPrincipals::destroy);
     SetWorkerRuntimeOptions(rt);
 
     JSContext* cx = NewContext(rt);
     if (!cx) {
         JS_DestroyRuntime(rt);
         js_delete(input);
         return;
@@ -2977,16 +2980,17 @@ WorkerMain(void* arg)
             break;
 
         JSAutoCompartment ac(cx, global);
 
         JS::CompileOptions options(cx);
         options.setFileAndLine("<string>", 1)
                .setIsRunOnce(true);
 
+        AutoReportException are(cx);
         RootedScript script(cx);
         if (!JS::Compile(cx, options, input->chars, input->length, &script))
             break;
         RootedValue result(cx);
         JS_ExecuteScript(cx, script, &result);
     } while (0);
 
     JS::SetLargeAllocationFailureCallback(rt, nullptr, nullptr);
@@ -3072,16 +3076,17 @@ EvalInWorker(JSContext* cx, unsigned arg
 
     AutoLockWorkerThreads alwt;
     if (!workerThreads.append(thread)) {
         ReportOutOfMemory(cx);
         PR_JoinThread(thread);
         return false;
     }
 
+    args.rval().setUndefined();
     return true;
 }
 
 static bool
 ShapeOf(JSContext* cx, unsigned argc, JS::Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     if (!args.get(0).isObject()) {
@@ -5925,50 +5930,75 @@ PrintStackTrace(JSContext* cx, HandleVal
 
     FILE* fp = ErrorFilePointer();
     fputs("Stack:\n", fp);
     fputs(stack.get(), fp);
 
     return true;
 }
 
+js::shell::AutoReportException::~AutoReportException()
+{
+    if (!JS_IsExceptionPending(cx))
+        return;
+
+    // Get exception object before printing and clearing exception.
+    RootedValue exn(cx);
+    (void) JS_GetPendingException(cx, &exn);
+
+    JS_ClearPendingException(cx);
+
+    ShellRuntime* sr = GetShellRuntime(cx);
+    js::ErrorReport report(cx);
+    if (!report.init(cx, exn, js::ErrorReport::WithSideEffects)) {
+        fprintf(stderr, "out of memory initializing ErrorReport\n");
+        JS_ClearPendingException(cx);
+        return;
+    }
+
+    MOZ_ASSERT(!JSREPORT_IS_WARNING(report.report()->flags));
+
+    FILE* fp = ErrorFilePointer();
+    PrintError(cx, fp, report.message(), report.report(), reportWarnings);
+
+    {
+        JS::AutoSaveExceptionState savedExc(cx);
+        if (!PrintStackTrace(cx, exn))
+            fputs("(Unable to print stack trace)\n", fp);
+        savedExc.restore();
+    }
+
+    if (report.report()->errorNumber == JSMSG_OUT_OF_MEMORY)
+        sr->exitCode = EXITCODE_OUT_OF_MEMORY;
+    else
+        sr->exitCode = EXITCODE_RUNTIME_ERROR;
+
+    JS_ClearPendingException(cx);
+}
+
 void
-js::shell::my_ErrorReporter(JSContext* cx, const char* message, JSErrorReport* report)
+js::shell::WarningReporter(JSContext* cx, const char* message, JSErrorReport* report)
 {
     ShellRuntime* sr = GetShellRuntime(cx);
     FILE* fp = ErrorFilePointer();
 
-    if (report && JSREPORT_IS_WARNING(report->flags) && sr->lastWarningEnabled) {
+    MOZ_ASSERT(report);
+    MOZ_ASSERT(JSREPORT_IS_WARNING(report->flags));
+
+    if (sr->lastWarningEnabled) {
         JS::AutoSaveExceptionState savedExc(cx);
         if (!CreateLastWarningObject(cx, report)) {
             fputs("Unhandled error happened while creating last warning object.\n", fp);
             fflush(fp);
         }
         savedExc.restore();
     }
 
-    // Get exception object before printing and clearing exception.
-    RootedValue exn(cx);
-    if (JS_IsExceptionPending(cx))
-        (void) JS_GetPendingException(cx, &exn);
-
-    (void) PrintError(cx, fp, message, report, reportWarnings);
-    if (!exn.isUndefined()) {
-        JS::AutoSaveExceptionState savedExc(cx);
-        if (!PrintStackTrace(cx, exn))
-            fputs("(Unable to print stack trace)\n", fp);
-        savedExc.restore();
-    }
-
-    if (!JSREPORT_IS_WARNING(report->flags)) {
-        if (report->errorNumber == JSMSG_OUT_OF_MEMORY)
-            sr->exitCode = EXITCODE_OUT_OF_MEMORY;
-        else
-            sr->exitCode = EXITCODE_RUNTIME_ERROR;
-    }
+    // Print the warning.
+    PrintError(cx, fp, message, report, reportWarnings);
 }
 
 static bool
 global_enumerate(JSContext* cx, HandleObject obj)
 {
 #ifdef LAZY_STANDARD_CLASSES
     return JS_EnumerateStandardClasses(cx, obj);
 #else
@@ -6467,16 +6497,19 @@ static const JS::AsmJSCacheOps asmJSCach
 
 static JSContext*
 NewContext(JSRuntime* rt)
 {
     JSContext* cx = JS_NewContext(rt, gStackChunkSize);
     if (!cx)
         return nullptr;
 
+    JS::ContextOptionsRef(cx).setDontReportUncaught(true);
+    JS::ContextOptionsRef(cx).setAutoJSAPIOwnsErrorReporting(true);
+
     JSShellContextData* data = NewContextData();
     if (!data) {
         DestroyContext(cx, false);
         return nullptr;
     }
 
     JS_SetContextPrivate(cx, data);
     return cx;
@@ -6580,16 +6613,18 @@ NewGlobalObject(JSContext* cx, JS::Compa
     JS_FireOnNewGlobalObject(cx, glob);
 
     return glob;
 }
 
 static bool
 BindScriptArgs(JSContext* cx, OptionParser* op)
 {
+    AutoReportException are(cx);
+
     MultiStringRange msr = op->getMultiStringArg("scriptArgs");
     RootedObject scriptArgs(cx);
     scriptArgs = JS_NewArrayObject(cx, 0);
     if (!scriptArgs)
         return false;
 
     if (!JS_DefineProperty(cx, cx->global(), "scriptArgs", scriptArgs, 0))
         return false;
@@ -6623,94 +6658,92 @@ BindScriptArgs(JSContext* cx, OptionPars
 
 static bool
 OptionFailure(const char* option, const char* str)
 {
     fprintf(stderr, "Unrecognized option for %s: %s\n", option, str);
     return false;
 }
 
-static int
+static MOZ_MUST_USE bool
 ProcessArgs(JSContext* cx, OptionParser* op)
 {
     ShellRuntime* sr = GetShellRuntime(cx);
 
     if (op->getBoolOption('s'))
         JS::RuntimeOptionsRef(cx).toggleExtraWarnings();
 
     /* |scriptArgs| gets bound on the global before any code is run. */
     if (!BindScriptArgs(cx, op))
-        return EXIT_FAILURE;
+        return false;
 
     MultiStringRange filePaths = op->getMultiStringOption('f');
     MultiStringRange codeChunks = op->getMultiStringOption('e');
     MultiStringRange modulePaths = op->getMultiStringOption('m');
 
     if (filePaths.empty() &&
         codeChunks.empty() &&
         modulePaths.empty() &&
         !op->getStringArg("script"))
     {
-        Process(cx, nullptr, true); /* Interactive. */
-        return sr->exitCode;
+        return Process(cx, nullptr, true); /* Interactive. */
     }
 
     if (const char* path = op->getStringOption("module-load-path"))
         moduleLoadPath = path;
 
     if (!modulePaths.empty() && !InitModuleLoader(cx))
-        return EXIT_FAILURE;
+        return false;
 
     while (!filePaths.empty() || !codeChunks.empty() || !modulePaths.empty()) {
         size_t fpArgno = filePaths.empty() ? SIZE_MAX : filePaths.argno();
         size_t ccArgno = codeChunks.empty() ? SIZE_MAX : codeChunks.argno();
         size_t mpArgno = modulePaths.empty() ? SIZE_MAX : modulePaths.argno();
 
         if (fpArgno < ccArgno && fpArgno < mpArgno) {
             char* path = filePaths.front();
-            Process(cx, path, false, FileScript);
-            if (sr->exitCode)
-                return sr->exitCode;
+            if (!Process(cx, path, false, FileScript))
+                return false;
             filePaths.popFront();
         } else if (ccArgno < fpArgno && ccArgno < mpArgno) {
             const char* code = codeChunks.front();
             RootedValue rval(cx);
             JS::CompileOptions opts(cx);
             opts.setFileAndLine("-e", 1);
             if (!JS::Evaluate(cx, opts, code, strlen(code), &rval))
-                return sr->exitCode ? sr->exitCode : EXITCODE_RUNTIME_ERROR;
+                return false;
             codeChunks.popFront();
             if (sr->quitting)
                 break;
         } else {
             MOZ_ASSERT(mpArgno < fpArgno && mpArgno < ccArgno);
             char* path = modulePaths.front();
-            Process(cx, path, false, FileModule);
-            if (sr->exitCode)
-                return sr->exitCode;
+            if (!Process(cx, path, false, FileModule))
+                return false;
             modulePaths.popFront();
         }
     }
 
     if (sr->quitting)
-        return sr->exitCode ? sr->exitCode : EXIT_SUCCESS;
+        return false;
 
     /* The |script| argument is processed after all options. */
     if (const char* path = op->getStringArg("script")) {
-        Process(cx, path, false);
-        if (sr->exitCode)
-            return sr->exitCode;
+        if (!Process(cx, path, false))
+            return false;
     }
 
     DrainJobQueue(cx);
 
-    if (op->getBoolOption('i'))
-        Process(cx, nullptr, true);
-
-    return sr->exitCode ? sr->exitCode : EXIT_SUCCESS;
+    if (op->getBoolOption('i')) {
+        if (!Process(cx, nullptr, true))
+            return false;
+    }
+
+    return true;
 }
 
 static bool
 SetRuntimeOptions(JSRuntime* rt, const OptionParser& op)
 {
     enableBaseline = !op.getBoolOption("no-baseline");
     enableIon = !op.getBoolOption("no-ion");
     enableAsmJS = !op.getBoolOption("no-asmjs");
@@ -7031,17 +7064,26 @@ Shell(JSContext* cx, OptionParser* op, c
     JS::CompartmentOptions options;
     SetStandardCompartmentOptions(options);
     RootedObject glob(cx, NewGlobalObject(cx, options, nullptr));
     if (!glob)
         return 1;
 
     JSAutoCompartment ac(cx, glob);
 
-    int result = ProcessArgs(cx, op);
+    ShellRuntime* sr = GetShellRuntime(cx);
+    int result = EXIT_SUCCESS;
+    {
+        AutoReportException are(cx);
+        if (!ProcessArgs(cx, op) && !sr->quitting)
+            result = EXITCODE_RUNTIME_ERROR;
+    }
+
+    if (sr->exitCode)
+        result = sr->exitCode;
 
     if (enableDisassemblyDumps)
         js::DumpCompartmentPCCounts(cx);
 
     if (!op->getBoolOption("no-js-cache-per-process")) {
         if (jsCacheAsmJSPath) {
             unlink(jsCacheAsmJSPath);
             JS_free(cx, const_cast<char*>(jsCacheAsmJSPath));
@@ -7363,17 +7405,17 @@ main(int argc, char** argv, char** envp)
 
     UniquePtr<ShellRuntime> sr = MakeUnique<ShellRuntime>(rt);
     if (!sr)
         return 1;
 
     JS_SetRuntimePrivate(rt, sr.get());
     // Waiting is allowed on the shell's main thread, for now.
     JS_SetFutexCanWait(rt);
-    JS_SetErrorReporter(rt, my_ErrorReporter);
+    JS_SetErrorReporter(rt, WarningReporter);
     if (!SetRuntimeOptions(rt, op))
         return 1;
 
     JS_SetGCParameter(rt, JSGC_MAX_BYTES, 0xffffffff);
 
     size_t availMem = op.getIntOption("available-memory");
     if (availMem > 0)
         JS_SetGCParametersBasedOnAvailableMemory(rt, availMem);
--- a/js/src/shell/jsshell.h
+++ b/js/src/shell/jsshell.h
@@ -19,17 +19,27 @@ enum JSShellErrNum {
 #undef MSG_DEF
     JSShellErr_Limit
 };
 
 const JSErrorFormatString*
 my_GetErrorMessage(void* userRef, const unsigned errorNumber);
 
 void
-my_ErrorReporter(JSContext* cx, const char* message, JSErrorReport* report);
+WarningReporter(JSContext* cx, const char* message, JSErrorReport* report);
+
+class MOZ_STACK_CLASS AutoReportException
+{
+    JSContext* cx;
+  public:
+    explicit AutoReportException(JSContext* cx)
+      : cx(cx)
+    {}
+    ~AutoReportException();
+};
 
 bool
 GenerateInterfaceHelp(JSContext* cx, JS::HandleObject obj, const char* name);
 
 JSString*
 FileAsString(JSContext* cx, const char* pathname);
 
 class AutoCloseFile