From 5850220229cdd8b62f6d5022779af7c232231d64 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sat, 3 Nov 2018 22:45:40 +0800 Subject: src: move error handling code into node_errors.cc Move the following code into a new node_errors.cc file and declare them in node_errors.h for clarity and make it possible to include them with node_errors.h. - AppendExceptionLine() - DecorateErrorStack() - FatalError() - OnFatalError() - PrintErrorString() - FatalException() - ReportException() - FatalTryCatch And move the following definitions (declared elsewhere than node_errors.h) to node_errors.cc: - Abort() (in util.h) - Assert() (in util.h) PR-URL: https://github.com/nodejs/node/pull/24058 Reviewed-By: Refael Ackermann Reviewed-By: Daniel Bevenius --- src/node_errors.cc | 427 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 src/node_errors.cc (limited to 'src/node_errors.cc') diff --git a/src/node_errors.cc b/src/node_errors.cc new file mode 100644 index 0000000000..cc8cff0f0e --- /dev/null +++ b/src/node_errors.cc @@ -0,0 +1,427 @@ +#include +#include "node_errors.h" +#include "node_internals.h" + +namespace node { +using v8::Context; +using v8::Exception; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Int32; +using v8::Isolate; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Message; +using v8::NewStringType; +using v8::Number; +using v8::Object; +using v8::ScriptOrigin; +using v8::String; +using v8::TryCatch; +using v8::Undefined; +using v8::Value; + +bool IsExceptionDecorated(Environment* env, Local er) { + if (!er.IsEmpty() && er->IsObject()) { + Local err_obj = er.As(); + auto maybe_value = + err_obj->GetPrivate(env->context(), env->decorated_private_symbol()); + Local decorated; + return maybe_value.ToLocal(&decorated) && decorated->IsTrue(); + } + return false; +} + +void AppendExceptionLine(Environment* env, + Local er, + Local message, + enum ErrorHandlingMode mode) { + if (message.IsEmpty()) return; + + HandleScope scope(env->isolate()); + Local err_obj; + if (!er.IsEmpty() && er->IsObject()) { + err_obj = er.As(); + } + + // Print (filename):(line number): (message). + ScriptOrigin origin = message->GetScriptOrigin(); + node::Utf8Value filename(env->isolate(), message->GetScriptResourceName()); + const char* filename_string = *filename; + int linenum = message->GetLineNumber(env->context()).FromJust(); + // Print line of source code. + MaybeLocal source_line_maybe = message->GetSourceLine(env->context()); + node::Utf8Value sourceline(env->isolate(), + source_line_maybe.ToLocalChecked()); + const char* sourceline_string = *sourceline; + if (strstr(sourceline_string, "node-do-not-add-exception-line") != nullptr) + return; + + // Because of how node modules work, all scripts are wrapped with a + // "function (module, exports, __filename, ...) {" + // to provide script local variables. + // + // When reporting errors on the first line of a script, this wrapper + // function is leaked to the user. There used to be a hack here to + // truncate off the first 62 characters, but it caused numerous other + // problems when vm.runIn*Context() methods were used for non-module + // code. + // + // If we ever decide to re-instate such a hack, the following steps + // must be taken: + // + // 1. Pass a flag around to say "this code was wrapped" + // 2. Update the stack frame output so that it is also correct. + // + // It would probably be simpler to add a line rather than add some + // number of characters to the first line, since V8 truncates the + // sourceline to 78 characters, and we end up not providing very much + // useful debugging info to the user if we remove 62 characters. + + int script_start = (linenum - origin.ResourceLineOffset()->Value()) == 1 + ? origin.ResourceColumnOffset()->Value() + : 0; + int start = message->GetStartColumn(env->context()).FromMaybe(0); + int end = message->GetEndColumn(env->context()).FromMaybe(0); + if (start >= script_start) { + CHECK_GE(end, start); + start -= script_start; + end -= script_start; + } + + char arrow[1024]; + int max_off = sizeof(arrow) - 2; + + int off = snprintf(arrow, + sizeof(arrow), + "%s:%i\n%s\n", + filename_string, + linenum, + sourceline_string); + CHECK_GE(off, 0); + if (off > max_off) { + off = max_off; + } + + // Print wavy underline (GetUnderline is deprecated). + for (int i = 0; i < start; i++) { + if (sourceline_string[i] == '\0' || off >= max_off) { + break; + } + CHECK_LT(off, max_off); + arrow[off++] = (sourceline_string[i] == '\t') ? '\t' : ' '; + } + for (int i = start; i < end; i++) { + if (sourceline_string[i] == '\0' || off >= max_off) { + break; + } + CHECK_LT(off, max_off); + arrow[off++] = '^'; + } + CHECK_LE(off, max_off); + arrow[off] = '\n'; + arrow[off + 1] = '\0'; + + Local arrow_str = + String::NewFromUtf8(env->isolate(), arrow, NewStringType::kNormal) + .ToLocalChecked(); + + const bool can_set_arrow = !arrow_str.IsEmpty() && !err_obj.IsEmpty(); + // If allocating arrow_str failed, print it out. There's not much else to do. + // If it's not an error, but something needs to be printed out because + // it's a fatal exception, also print it out from here. + // Otherwise, the arrow property will be attached to the object and handled + // by the caller. + if (!can_set_arrow || (mode == FATAL_ERROR && !err_obj->IsNativeError())) { + if (env->printed_error()) return; + Mutex::ScopedLock lock(process_mutex); + env->set_printed_error(true); + + uv_tty_reset_mode(); + PrintErrorString("\n%s", arrow); + return; + } + + CHECK(err_obj + ->SetPrivate( + env->context(), env->arrow_message_private_symbol(), arrow_str) + .FromMaybe(false)); +} + +[[noreturn]] void Abort() { + DumpBacktrace(stderr); + fflush(stderr); + ABORT_NO_BACKTRACE(); +} + +[[noreturn]] void Assert(const char* const (*args)[4]) { + auto filename = (*args)[0]; + auto linenum = (*args)[1]; + auto message = (*args)[2]; + auto function = (*args)[3]; + + char name[1024]; + GetHumanReadableProcessName(&name); + + fprintf(stderr, + "%s: %s:%s:%s%s Assertion `%s' failed.\n", + name, + filename, + linenum, + function, + *function ? ":" : "", + message); + fflush(stderr); + + Abort(); +} + +void ReportException(Environment* env, + Local er, + Local message) { + CHECK(!er.IsEmpty()); + HandleScope scope(env->isolate()); + + if (message.IsEmpty()) message = Exception::CreateMessage(env->isolate(), er); + + AppendExceptionLine(env, er, message, FATAL_ERROR); + + Local trace_value; + Local arrow; + const bool decorated = IsExceptionDecorated(env, er); + + if (er->IsUndefined() || er->IsNull()) { + trace_value = Undefined(env->isolate()); + } else { + Local err_obj = er->ToObject(env->context()).ToLocalChecked(); + + trace_value = err_obj->Get(env->stack_string()); + arrow = + err_obj->GetPrivate(env->context(), env->arrow_message_private_symbol()) + .ToLocalChecked(); + } + + node::Utf8Value trace(env->isolate(), trace_value); + + // range errors have a trace member set to undefined + if (trace.length() > 0 && !trace_value->IsUndefined()) { + if (arrow.IsEmpty() || !arrow->IsString() || decorated) { + PrintErrorString("%s\n", *trace); + } else { + node::Utf8Value arrow_string(env->isolate(), arrow); + PrintErrorString("%s\n%s\n", *arrow_string, *trace); + } + } else { + // this really only happens for RangeErrors, since they're the only + // kind that won't have all this info in the trace, or when non-Error + // objects are thrown manually. + Local message; + Local name; + + if (er->IsObject()) { + Local err_obj = er.As(); + message = err_obj->Get(env->message_string()); + name = err_obj->Get(FIXED_ONE_BYTE_STRING(env->isolate(), "name")); + } + + if (message.IsEmpty() || message->IsUndefined() || name.IsEmpty() || + name->IsUndefined()) { + // Not an error object. Just print as-is. + String::Utf8Value message(env->isolate(), er); + + PrintErrorString("%s\n", + *message ? *message : ""); + } else { + node::Utf8Value name_string(env->isolate(), name); + node::Utf8Value message_string(env->isolate(), message); + + if (arrow.IsEmpty() || !arrow->IsString() || decorated) { + PrintErrorString("%s: %s\n", *name_string, *message_string); + } else { + node::Utf8Value arrow_string(env->isolate(), arrow); + PrintErrorString( + "%s\n%s: %s\n", *arrow_string, *name_string, *message_string); + } + } + } + + fflush(stderr); + +#if HAVE_INSPECTOR + env->inspector_agent()->FatalException(er, message); +#endif +} + +void ReportException(Environment* env, const TryCatch& try_catch) { + ReportException(env, try_catch.Exception(), try_catch.Message()); +} + +void DecorateErrorStack(Environment* env, const TryCatch& try_catch) { + Local exception = try_catch.Exception(); + + if (!exception->IsObject()) return; + + Local err_obj = exception.As(); + + if (IsExceptionDecorated(env, err_obj)) return; + + AppendExceptionLine(env, exception, try_catch.Message(), CONTEXTIFY_ERROR); + Local stack = err_obj->Get(env->stack_string()); + MaybeLocal maybe_value = + err_obj->GetPrivate(env->context(), env->arrow_message_private_symbol()); + + Local arrow; + if (!(maybe_value.ToLocal(&arrow) && arrow->IsString())) { + return; + } + + if (stack.IsEmpty() || !stack->IsString()) { + return; + } + + Local decorated_stack = String::Concat( + env->isolate(), + String::Concat(env->isolate(), + arrow.As(), + FIXED_ONE_BYTE_STRING(env->isolate(), "\n")), + stack.As()); + err_obj->Set(env->stack_string(), decorated_stack); + err_obj->SetPrivate( + env->context(), env->decorated_private_symbol(), True(env->isolate())); +} + +void PrintErrorString(const char* format, ...) { + va_list ap; + va_start(ap, format); +#ifdef _WIN32 + HANDLE stderr_handle = GetStdHandle(STD_ERROR_HANDLE); + + // Check if stderr is something other than a tty/console + if (stderr_handle == INVALID_HANDLE_VALUE || stderr_handle == nullptr || + uv_guess_handle(_fileno(stderr)) != UV_TTY) { + vfprintf(stderr, format, ap); + va_end(ap); + return; + } + + // Fill in any placeholders + int n = _vscprintf(format, ap); + std::vector out(n + 1); + vsprintf(out.data(), format, ap); + + // Get required wide buffer size + n = MultiByteToWideChar(CP_UTF8, 0, out.data(), -1, nullptr, 0); + + std::vector wbuf(n); + MultiByteToWideChar(CP_UTF8, 0, out.data(), -1, wbuf.data(), n); + + // Don't include the null character in the output + CHECK_GT(n, 0); + WriteConsoleW(stderr_handle, wbuf.data(), n - 1, nullptr, nullptr); +#else + vfprintf(stderr, format, ap); +#endif + va_end(ap); +} + +[[noreturn]] void FatalError(const char* location, const char* message) { + OnFatalError(location, message); + // to suppress compiler warning + ABORT(); +} + +void OnFatalError(const char* location, const char* message) { + if (location) { + PrintErrorString("FATAL ERROR: %s %s\n", location, message); + } else { + PrintErrorString("FATAL ERROR: %s\n", message); + } + fflush(stderr); + ABORT(); +} + +FatalTryCatch::~FatalTryCatch() { + if (HasCaught()) { + HandleScope scope(env_->isolate()); + ReportException(env_, *this); + exit(7); + } +} + +void FatalException(Isolate* isolate, + Local error, + Local message) { + HandleScope scope(isolate); + + Environment* env = Environment::GetCurrent(isolate); + CHECK_NOT_NULL(env); // TODO(addaleax): Handle nullptr here. + Local process_object = env->process_object(); + Local fatal_exception_string = env->fatal_exception_string(); + Local fatal_exception_function = + process_object->Get(fatal_exception_string); + + if (!fatal_exception_function->IsFunction()) { + // Failed before the process._fatalException function was added! + // this is probably pretty bad. Nothing to do but report and exit. + ReportException(env, error, message); + exit(6); + } else { + TryCatch fatal_try_catch(isolate); + + // Do not call FatalException when _fatalException handler throws + fatal_try_catch.SetVerbose(false); + + // This will return true if the JS layer handled it, false otherwise + MaybeLocal caught = fatal_exception_function.As()->Call( + env->context(), process_object, 1, &error); + + if (fatal_try_catch.HasTerminated()) return; + + if (fatal_try_catch.HasCaught()) { + // The fatal exception function threw, so we must exit + ReportException(env, fatal_try_catch); + exit(7); + } else if (caught.ToLocalChecked()->IsFalse()) { + ReportException(env, error, message); + + // fatal_exception_function call before may have set a new exit code -> + // read it again, otherwise use default for uncaughtException 1 + Local exit_code = env->exit_code_string(); + Local code; + if (!process_object->Get(env->context(), exit_code).ToLocal(&code) || + !code->IsInt32()) { + exit(1); + } + exit(code.As()->Value()); + } + } +} + +void FatalException(Isolate* isolate, const TryCatch& try_catch) { + // If we try to print out a termination exception, we'd just get 'null', + // so just crashing here with that information seems like a better idea, + // and in particular it seems like we should handle terminations at the call + // site for this function rather than by printing them out somewhere. + CHECK(!try_catch.HasTerminated()); + + HandleScope scope(isolate); + if (!try_catch.IsVerbose()) { + FatalException(isolate, try_catch.Exception(), try_catch.Message()); + } +} + +void FatalException(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Environment* env = Environment::GetCurrent(isolate); + if (env != nullptr && env->abort_on_uncaught_exception()) { + Abort(); + } + Local exception = args[0]; + Local message = Exception::CreateMessage(isolate, exception); + FatalException(isolate, exception, message); +} + +} // namespace node -- cgit v1.2.3