diff --git a/src/async-wrap-inl.h b/src/async-wrap-inl.h index f2d2c3ecf1c7b4..9f520de4429622 100644 --- a/src/async-wrap-inl.h +++ b/src/async-wrap-inl.h @@ -40,19 +40,26 @@ inline AsyncWrap::AsyncWrap(Environment* env, v8::HandleScope scope(env->isolate()); v8::Local argv[] = { + v8::Number::New(env->isolate(), get_uid()), v8::Int32::New(env->isolate(), provider), - v8::Integer::New(env->isolate(), get_uid()), + Null(env->isolate()), Null(env->isolate()) }; - if (parent != nullptr) - argv[2] = parent->object(); + if (parent != nullptr) { + argv[2] = v8::Number::New(env->isolate(), parent->get_uid()); + argv[3] = parent->object(); + } + + v8::TryCatch try_catch(env->isolate()); v8::MaybeLocal ret = init_fn->Call(env->context(), object, arraysize(argv), argv); - if (ret.IsEmpty()) - FatalError("node::AsyncWrap::AsyncWrap", "init hook threw"); + if (ret.IsEmpty()) { + ClearFatalExceptionHandlers(env); + FatalException(env->isolate(), try_catch); + } bits_ |= 1; // ran_init_callback() is true now. } @@ -65,11 +72,14 @@ inline AsyncWrap::~AsyncWrap() { v8::Local fn = env()->async_hooks_destroy_function(); if (!fn.IsEmpty()) { v8::HandleScope scope(env()->isolate()); - v8::Local uid = v8::Integer::New(env()->isolate(), get_uid()); + v8::Local uid = v8::Number::New(env()->isolate(), get_uid()); + v8::TryCatch try_catch(env()->isolate()); v8::MaybeLocal ret = fn->Call(env()->context(), v8::Null(env()->isolate()), 1, &uid); - if (ret.IsEmpty()) - FatalError("node::AsyncWrap::~AsyncWrap", "destroy hook threw"); + if (ret.IsEmpty()) { + ClearFatalExceptionHandlers(env()); + FatalException(env()->isolate(), try_catch); + } } } diff --git a/src/async-wrap.cc b/src/async-wrap.cc index c9f5caad1e4ea8..2b6839c05ee461 100644 --- a/src/async-wrap.cc +++ b/src/async-wrap.cc @@ -9,6 +9,7 @@ #include "v8-profiler.h" using v8::Array; +using v8::Boolean; using v8::Context; using v8::Function; using v8::FunctionCallbackInfo; @@ -17,6 +18,8 @@ using v8::HeapProfiler; using v8::Integer; using v8::Isolate; using v8::Local; +using v8::MaybeLocal; +using v8::Number; using v8::Object; using v8::RetainedObjectInfo; using v8::TryCatch; @@ -121,18 +124,35 @@ static void SetupHooks(const FunctionCallbackInfo& args) { if (env->async_hooks()->callbacks_enabled()) return env->ThrowError("hooks should not be set while also enabled"); - - if (!args[0]->IsFunction()) + if (!args[0]->IsObject()) + return env->ThrowTypeError("first argument must be an object"); + + Local fn_obj = args[0].As(); + + Local init_v = fn_obj->Get( + env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "init")).ToLocalChecked(); + Local pre_v = fn_obj->Get( + env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "pre")).ToLocalChecked(); + Local post_v = fn_obj->Get( + env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "post")).ToLocalChecked(); + Local destroy_v = fn_obj->Get( + env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "destroy")).ToLocalChecked(); + + if (!init_v->IsFunction()) return env->ThrowTypeError("init callback must be a function"); - env->set_async_hooks_init_function(args[0].As()); + env->set_async_hooks_init_function(init_v.As()); - if (args[1]->IsFunction()) - env->set_async_hooks_pre_function(args[1].As()); - if (args[2]->IsFunction()) - env->set_async_hooks_post_function(args[2].As()); - if (args[3]->IsFunction()) - env->set_async_hooks_destroy_function(args[3].As()); + if (pre_v->IsFunction()) + env->set_async_hooks_pre_function(pre_v.As()); + if (post_v->IsFunction()) + env->set_async_hooks_post_function(post_v.As()); + if (destroy_v->IsFunction()) + env->set_async_hooks_destroy_function(destroy_v.As()); } @@ -173,94 +193,97 @@ void LoadAsyncWrapperInfo(Environment* env) { Local AsyncWrap::MakeCallback(const Local cb, - int argc, - Local* argv) { + int argc, + Local* argv) { CHECK(env()->context() == env()->isolate()->GetCurrentContext()); Local pre_fn = env()->async_hooks_pre_function(); Local post_fn = env()->async_hooks_post_function(); + Local uid = Number::New(env()->isolate(), get_uid()); Local context = object(); - Local process = env()->process_object(); Local domain; bool has_domain = false; + Environment::AsyncCallbackScope callback_scope(env()); + if (env()->using_domains()) { Local domain_v = context->Get(env()->domain_string()); has_domain = domain_v->IsObject(); if (has_domain) { domain = domain_v.As(); if (domain->Get(env()->disposed_string())->IsTrue()) - return Undefined(env()->isolate()); + return Local(); } } - TryCatch try_catch(env()->isolate()); - try_catch.SetVerbose(true); - if (has_domain) { Local enter_v = domain->Get(env()->enter_string()); if (enter_v->IsFunction()) { - enter_v.As()->Call(domain, 0, nullptr); - if (try_catch.HasCaught()) - return Undefined(env()->isolate()); + if (enter_v.As()->Call(domain, 0, nullptr).IsEmpty()) { + FatalError("node::AsyncWrap::MakeCallback", + "domain enter callback threw, please report this"); + } } } if (ran_init_callback() && !pre_fn.IsEmpty()) { - try_catch.SetVerbose(false); - pre_fn->Call(context, 0, nullptr); - if (try_catch.HasCaught()) - FatalError("node::AsyncWrap::MakeCallback", "pre hook threw"); - try_catch.SetVerbose(true); + TryCatch try_catch(env()->isolate()); + MaybeLocal ar = pre_fn->Call(env()->context(), context, 1, &uid); + if (ar.IsEmpty()) { + ClearFatalExceptionHandlers(env()); + FatalException(env()->isolate(), try_catch); + return Local(); + } } Local ret = cb->Call(context, argc, argv); - if (try_catch.HasCaught()) { - return Undefined(env()->isolate()); + if (ran_init_callback() && !post_fn.IsEmpty()) { + Local did_throw = Boolean::New(env()->isolate(), ret.IsEmpty()); + Local vals[] = { uid, did_throw }; + TryCatch try_catch(env()->isolate()); + MaybeLocal ar = + post_fn->Call(env()->context(), context, arraysize(vals), vals); + if (ar.IsEmpty()) { + ClearFatalExceptionHandlers(env()); + FatalException(env()->isolate(), try_catch); + return Local(); + } } - if (ran_init_callback() && !post_fn.IsEmpty()) { - try_catch.SetVerbose(false); - post_fn->Call(context, 0, nullptr); - if (try_catch.HasCaught()) - FatalError("node::AsyncWrap::MakeCallback", "post hook threw"); - try_catch.SetVerbose(true); + if (ret.IsEmpty()) { + return ret; } if (has_domain) { Local exit_v = domain->Get(env()->exit_string()); if (exit_v->IsFunction()) { - exit_v.As()->Call(domain, 0, nullptr); - if (try_catch.HasCaught()) - return Undefined(env()->isolate()); + if (exit_v.As()->Call(domain, 0, nullptr).IsEmpty()) { + FatalError("node::AsyncWrap::MakeCallback", + "domain exit callback threw, please report this"); + } } } - Environment::TickInfo* tick_info = env()->tick_info(); - - if (tick_info->in_tick()) { + if (callback_scope.in_makecallback()) { return ret; } + Environment::TickInfo* tick_info = env()->tick_info(); + if (tick_info->length() == 0) { env()->isolate()->RunMicrotasks(); } + Local process = env()->process_object(); + if (tick_info->length() == 0) { tick_info->set_index(0); return ret; } - tick_info->set_in_tick(true); - - env()->tick_callback_function()->Call(process, 0, nullptr); - - tick_info->set_in_tick(false); - - if (try_catch.HasCaught()) { - tick_info->set_last_threw(true); - return Undefined(env()->isolate()); + if (env()->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) { + return Local(); } return ret; diff --git a/src/async-wrap.h b/src/async-wrap.h index 5db29600bcd180..cb0c9e211a8923 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -17,6 +17,7 @@ namespace node { V(FSREQWRAP) \ V(GETADDRINFOREQWRAP) \ V(GETNAMEINFOREQWRAP) \ + V(HTTPPARSER) \ V(JSSTREAM) \ V(PIPEWRAP) \ V(PIPECONNECTWRAP) \ diff --git a/src/env-inl.h b/src/env-inl.h index f73e9c6ba2000a..6f19ff50cb536f 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -88,6 +88,19 @@ inline void Environment::AsyncHooks::set_enable_callbacks(uint32_t flag) { fields_[kEnableCallbacks] = flag; } +inline Environment::AsyncCallbackScope::AsyncCallbackScope(Environment* env) + : env_(env) { + env_->makecallback_cntr_++; +} + +inline Environment::AsyncCallbackScope::~AsyncCallbackScope() { + env_->makecallback_cntr_--; +} + +inline bool Environment::AsyncCallbackScope::in_makecallback() { + return env_->makecallback_cntr_ > 1; +} + inline Environment::DomainFlag::DomainFlag() { for (int i = 0; i < kFieldsCount; ++i) fields_[i] = 0; } @@ -104,7 +117,7 @@ inline uint32_t Environment::DomainFlag::count() const { return fields_[kCount]; } -inline Environment::TickInfo::TickInfo() : in_tick_(false), last_threw_(false) { +inline Environment::TickInfo::TickInfo() { for (int i = 0; i < kFieldsCount; ++i) fields_[i] = 0; } @@ -117,34 +130,18 @@ inline int Environment::TickInfo::fields_count() const { return kFieldsCount; } -inline bool Environment::TickInfo::in_tick() const { - return in_tick_; -} - inline uint32_t Environment::TickInfo::index() const { return fields_[kIndex]; } -inline bool Environment::TickInfo::last_threw() const { - return last_threw_; -} - inline uint32_t Environment::TickInfo::length() const { return fields_[kLength]; } -inline void Environment::TickInfo::set_in_tick(bool value) { - in_tick_ = value; -} - inline void Environment::TickInfo::set_index(uint32_t value) { fields_[kIndex] = value; } -inline void Environment::TickInfo::set_last_threw(bool value) { - last_threw_ = value; -} - inline Environment::ArrayBufferAllocatorInfo::ArrayBufferAllocatorInfo() { for (int i = 0; i < kFieldsCount; ++i) fields_[i] = 0; @@ -210,6 +207,7 @@ inline Environment::Environment(v8::Local context, using_domains_(false), printed_error_(false), trace_sync_io_(false), + makecallback_cntr_(0), async_wrap_uid_(0), debugger_agent_(this), http_parser_buffer_(nullptr), diff --git a/src/env.cc b/src/env.cc index e28866efd06894..2509a8fd44b6a2 100644 --- a/src/env.cc +++ b/src/env.cc @@ -11,6 +11,7 @@ using v8::Message; using v8::StackFrame; using v8::StackTrace; using v8::TryCatch; +using v8::Value; void Environment::PrintSyncTrace() const { if (!trace_sync_io_) @@ -56,38 +57,4 @@ void Environment::PrintSyncTrace() const { fflush(stderr); } - -bool Environment::KickNextTick() { - TickInfo* info = tick_info(); - - if (info->in_tick()) { - return true; - } - - if (info->length() == 0) { - isolate()->RunMicrotasks(); - } - - if (info->length() == 0) { - info->set_index(0); - return true; - } - - info->set_in_tick(true); - - // process nextTicks after call - TryCatch try_catch; - try_catch.SetVerbose(true); - tick_callback_function()->Call(process_object(), 0, nullptr); - - info->set_in_tick(false); - - if (try_catch.HasCaught()) { - info->set_last_threw(true); - return false; - } - - return true; -} - } // namespace node diff --git a/src/env.h b/src/env.h index 1531d9911e310b..7b6ffc8b87c8ee 100644 --- a/src/env.h +++ b/src/env.h @@ -294,6 +294,19 @@ class Environment { DISALLOW_COPY_AND_ASSIGN(AsyncHooks); }; + class AsyncCallbackScope { + public: + explicit AsyncCallbackScope(Environment* env); + ~AsyncCallbackScope(); + + inline bool in_makecallback(); + + private: + Environment* env_; + + DISALLOW_COPY_AND_ASSIGN(AsyncCallbackScope); + }; + class DomainFlag { public: inline uint32_t* fields(); @@ -318,13 +331,9 @@ class Environment { public: inline uint32_t* fields(); inline int fields_count() const; - inline bool in_tick() const; - inline bool last_threw() const; inline uint32_t index() const; inline uint32_t length() const; - inline void set_in_tick(bool value); inline void set_index(uint32_t value); - inline void set_last_threw(bool value); private: friend class Environment; // So we can call the constructor. @@ -337,8 +346,6 @@ class Environment { }; uint32_t fields_[kFieldsCount]; - bool in_tick_; - bool last_threw_; DISALLOW_COPY_AND_ASSIGN(TickInfo); }; @@ -446,8 +453,6 @@ class Environment { inline int64_t get_async_wrap_uid(); - bool KickNextTick(); - inline uint32_t* heap_statistics_buffer() const; inline void set_heap_statistics_buffer(uint32_t* pointer); @@ -541,6 +546,7 @@ class Environment { bool using_domains_; bool printed_error_; bool trace_sync_io_; + size_t makecallback_cntr_; int64_t async_wrap_uid_; debugger::Agent debugger_agent_; diff --git a/src/node.cc b/src/node.cc index 8264bfe8bba8a3..2f08a1da378eaf 100644 --- a/src/node.cc +++ b/src/node.cc @@ -115,6 +115,7 @@ using v8::Integer; using v8::Isolate; using v8::Local; using v8::Locker; +using v8::MaybeLocal; using v8::Message; using v8::Number; using v8::Object; @@ -1119,10 +1120,10 @@ void SetupPromises(const FunctionCallbackInfo& args) { Local MakeCallback(Environment* env, - Local recv, - const Local callback, - int argc, - Local argv[]) { + Local recv, + const Local callback, + int argc, + Local argv[]) { // If you hit this assertion, you forgot to enter the v8::Context first. CHECK_EQ(env->context(), env->isolate()->GetCurrentContext()); @@ -1132,6 +1133,8 @@ Local MakeCallback(Environment* env, bool ran_init_callback = false; bool has_domain = false; + Environment::AsyncCallbackScope callback_scope(env); + // TODO(trevnorris): Adding "_asyncQueue" to the "this" in the init callback // is a horrible way to detect usage. Rethink how detection should happen. if (recv->IsObject()) { @@ -1152,51 +1155,80 @@ Local MakeCallback(Environment* env, } } - TryCatch try_catch; - try_catch.SetVerbose(true); - if (has_domain) { Local enter_v = domain->Get(env->enter_string()); if (enter_v->IsFunction()) { - enter_v.As()->Call(domain, 0, nullptr); - if (try_catch.HasCaught()) - return Undefined(env->isolate()); + if (enter_v.As()->Call(domain, 0, nullptr).IsEmpty()) { + FatalError("node::MakeCallback", + "domain enter callback threw, please report this"); + } } } if (ran_init_callback && !pre_fn.IsEmpty()) { - try_catch.SetVerbose(false); - pre_fn->Call(object, 0, nullptr); - if (try_catch.HasCaught()) - FatalError("node::MakeCallback", "pre hook threw"); - try_catch.SetVerbose(true); + TryCatch try_catch(env->isolate()); + MaybeLocal ar = pre_fn->Call(env->context(), object, 0, nullptr); + if (ar.IsEmpty()) { + ClearFatalExceptionHandlers(env); + FatalException(env->isolate(), try_catch); + return Local(); + } } Local ret = callback->Call(recv, argc, argv); if (ran_init_callback && !post_fn.IsEmpty()) { - try_catch.SetVerbose(false); - post_fn->Call(object, 0, nullptr); - if (try_catch.HasCaught()) - FatalError("node::MakeCallback", "post hook threw"); - try_catch.SetVerbose(true); + Local did_throw = Boolean::New(env->isolate(), ret.IsEmpty()); + // Currently there's no way to retrieve an uid from node::MakeCallback(). + // This needs to be fixed. + Local vals[] = + { Undefined(env->isolate()).As(), did_throw }; + TryCatch try_catch(env->isolate()); + MaybeLocal ar = + post_fn->Call(env->context(), object, arraysize(vals), vals); + if (ar.IsEmpty()) { + ClearFatalExceptionHandlers(env); + FatalException(env->isolate(), try_catch); + return Local(); + } + } + + if (ret.IsEmpty()) { + // NOTE: For backwards compatibility with public API we return Undefined() + // if the top level call threw. + return callback_scope.in_makecallback() ? + ret : Undefined(env->isolate()).As(); } if (has_domain) { Local exit_v = domain->Get(env->exit_string()); if (exit_v->IsFunction()) { - exit_v.As()->Call(domain, 0, nullptr); - if (try_catch.HasCaught()) - return Undefined(env->isolate()); + if (exit_v.As()->Call(domain, 0, nullptr).IsEmpty()) { + FatalError("node::MakeCallback", + "domain exit callback threw, please report this"); + } } } - if (try_catch.HasCaught()) { - return Undefined(env->isolate()); + if (callback_scope.in_makecallback()) { + return ret; + } + + Environment::TickInfo* tick_info = env->tick_info(); + + if (tick_info->length() == 0) { + env->isolate()->RunMicrotasks(); + } + + Local process = env->process_object(); + + if (tick_info->length() == 0) { + tick_info->set_index(0); } - if (!env->KickNextTick()) + if (env->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) { return Undefined(env->isolate()); + } return ret; } @@ -2332,6 +2364,25 @@ void OnMessage(Local message, Local error) { } +void ClearFatalExceptionHandlers(Environment* env) { + Local process = env->process_object(); + Local events = + process->Get(env->context(), env->events_string()).ToLocalChecked(); + + if (events->IsObject()) { + events.As()->Set( + env->context(), + OneByteString(env->isolate(), "uncaughtException"), + Undefined(env->isolate())).FromJust(); + } + + process->Set( + env->context(), + env->domain_string(), + Undefined(env->isolate())).FromJust(); +} + + static void Binding(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -4103,7 +4154,10 @@ static void StartNodeInstance(void* arg) { if (instance_data->use_debug_agent()) StartDebug(env, debug_wait_connect); - LoadEnvironment(env); + { + Environment::AsyncCallbackScope callback_scope(env); + LoadEnvironment(env); + } env->set_trace_sync_io(trace_sync_io); diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index 8c976b2e9f0b4c..6b15aa8c72d531 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -3,8 +3,8 @@ #include "node_http_parser.h" #include "node_revert.h" -#include "base-object.h" -#include "base-object-inl.h" +#include "async-wrap.h" +#include "async-wrap-inl.h" #include "env.h" #include "env-inl.h" #include "stream_base.h" @@ -148,10 +148,10 @@ struct StringPtr { }; -class Parser : public BaseObject { +class Parser : public AsyncWrap { public: Parser(Environment* env, Local wrap, enum http_parser_type type) - : BaseObject(env, wrap), + : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_HTTPPARSER), current_buffer_len_(0), current_buffer_data_(nullptr) { Wrap(object(), this); @@ -165,6 +165,11 @@ class Parser : public BaseObject { } + size_t self_size() const override { + return sizeof(*this); + } + + HTTP_CB(on_message_begin) { num_fields_ = num_values_ = 0; url_.Reset(); @@ -286,8 +291,10 @@ class Parser : public BaseObject { argv[A_UPGRADE] = Boolean::New(env()->isolate(), parser_.upgrade); + Environment::AsyncCallbackScope callback_scope(env()); + Local head_response = - cb.As()->Call(obj, arraysize(argv), argv); + MakeCallback(cb.As(), arraysize(argv), argv); if (head_response.IsEmpty()) { got_exception_ = true; @@ -322,7 +329,7 @@ class Parser : public BaseObject { Integer::NewFromUnsigned(env()->isolate(), length) }; - Local r = cb.As()->Call(obj, arraysize(argv), argv); + Local r = MakeCallback(cb.As(), arraysize(argv), argv); if (r.IsEmpty()) { got_exception_ = true; @@ -345,7 +352,9 @@ class Parser : public BaseObject { if (!cb->IsFunction()) return 0; - Local r = cb.As()->Call(obj, 0, nullptr); + Environment::AsyncCallbackScope callback_scope(env()); + + Local r = MakeCallback(cb.As(), 0, nullptr); if (r.IsEmpty()) { got_exception_ = true; @@ -583,12 +592,10 @@ class Parser : public BaseObject { parser->current_buffer_len_ = nread; parser->current_buffer_data_ = buf->base; - cb.As()->Call(obj, 1, &ret); + parser->MakeCallback(cb.As(), 1, &ret); parser->current_buffer_len_ = 0; parser->current_buffer_data_ = nullptr; - - parser->env()->KickNextTick(); } @@ -657,7 +664,7 @@ class Parser : public BaseObject { url_.ToString(env()) }; - Local r = cb.As()->Call(obj, arraysize(argv), argv); + Local r = MakeCallback(cb.As(), arraysize(argv), argv); if (r.IsEmpty()) got_exception_ = true; diff --git a/src/node_internals.h b/src/node_internals.h index 3df7676d2a9d51..33ae25d6062938 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -69,8 +69,6 @@ v8::Local MakeCallback(Environment* env, int argc = 0, v8::Local* argv = nullptr); -bool KickNextTick(); - // Convert a struct sockaddr to a { address: '1.2.3.4', port: 1234 } JS object. // Sets address and port properties on the info object and returns it. // If |info| is omitted, a new object is returned. @@ -239,6 +237,11 @@ class ArrayBufferAllocator : public v8::ArrayBuffer::Allocator { Environment* env_; }; +// Clear any domain and/or uncaughtException handlers to force the error's +// propagation and shutdown the process. Use this to force the process to exit +// by clearing all callbacks that could handle the error. +void ClearFatalExceptionHandlers(Environment* env); + enum NodeInstanceType { MAIN, WORKER }; class NodeInstanceData { diff --git a/test/addons/make-callback-recurse/binding.cc b/test/addons/make-callback-recurse/binding.cc new file mode 100644 index 00000000000000..1c575910ef66f5 --- /dev/null +++ b/test/addons/make-callback-recurse/binding.cc @@ -0,0 +1,31 @@ +#include "node.h" +#include "v8.h" + +#include "../../../src/util.h" + +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::Value; + +namespace { + +void MakeCallback(const FunctionCallbackInfo& args) { + CHECK(args[0]->IsObject()); + CHECK(args[1]->IsFunction()); + Isolate* isolate = args.GetIsolate(); + Local recv = args[0].As(); + Local method = args[1].As(); + + node::MakeCallback(isolate, recv, method, 0, nullptr); +} + +void Initialize(Local target) { + NODE_SET_METHOD(target, "makeCallback", MakeCallback); +} + +} // namespace anonymous + +NODE_MODULE(binding, Initialize) diff --git a/test/addons/make-callback-recurse/binding.gyp b/test/addons/make-callback-recurse/binding.gyp new file mode 100644 index 00000000000000..3bfb84493f3e87 --- /dev/null +++ b/test/addons/make-callback-recurse/binding.gyp @@ -0,0 +1,8 @@ +{ + 'targets': [ + { + 'target_name': 'binding', + 'sources': [ 'binding.cc' ] + } + ] +} diff --git a/test/addons/make-callback-recurse/test.js b/test/addons/make-callback-recurse/test.js new file mode 100644 index 00000000000000..f924ac61916ba5 --- /dev/null +++ b/test/addons/make-callback-recurse/test.js @@ -0,0 +1,170 @@ +'use strict'; + +const common = require('../../common'); +const assert = require('assert'); +const domain = require('domain'); +const binding = require('./build/Release/binding'); +const makeCallback = binding.makeCallback; + +// Make sure this is run in the future. +const mustCallCheckDomains = common.mustCall(checkDomains); + + +// Make sure that using MakeCallback allows the error to propagate. +assert.throws(function() { + makeCallback({}, function() { + throw new Error('hi from domain error'); + }); +}); + + +// Check the execution order of the nextTickQueue and MicrotaskQueue in +// relation to running multiple MakeCallback's from bootstrap, +// node::MakeCallback() and node::AsyncWrap::MakeCallback(). +// TODO(trevnorris): Is there a way to verify this is being run during +// bootstrap? +(function verifyExecutionOrder(arg) { + const results_arr = []; + + // Processing of the MicrotaskQueue is manually handled by node. They are not + // processed until after the nextTickQueue has been processed. + Promise.resolve(1).then(common.mustCall(function() { + results_arr.push(7); + })); + + // The nextTick should run after all immediately invoked calls. + process.nextTick(common.mustCall(function() { + results_arr.push(3); + + // Run same test again but while processing the nextTickQueue to make sure + // the following MakeCallback call breaks in the middle of processing the + // queue and allows the script to run normally. + process.nextTick(common.mustCall(function() { + results_arr.push(6); + })); + + makeCallback({}, common.mustCall(function() { + results_arr.push(4); + })); + + results_arr.push(5); + })); + + results_arr.push(0); + + // MakeCallback is calling the function immediately, but should then detect + // that a script is already in the middle of execution and return before + // either the nextTickQueue or MicrotaskQueue are processed. + makeCallback({}, common.mustCall(function() { + results_arr.push(1); + })); + + // This should run before either the nextTickQueue or MicrotaskQueue are + // processed. Previously MakeCallback would not detect this circumstance + // and process them immediately. + results_arr.push(2); + + setImmediate(common.mustCall(function() { + for (var i = 0; i < results_arr.length; i++) { + assert.equal(results_arr[i], + i, + `verifyExecutionOrder(${arg}) results: ${results_arr}`); + } + if (arg === 1) { + // The tests are first run on bootstrap during LoadEnvironment() in + // src/node.cc. Now run the tests through node::MakeCallback(). + setImmediate(function() { + makeCallback({}, common.mustCall(function() { + verifyExecutionOrder(2); + })); + }); + } else if (arg === 2) { + // setTimeout runs via the TimerWrap, which runs through + // AsyncWrap::MakeCallback(). Make sure there are no conflicts using + // node::MakeCallback() within it. + setTimeout(common.mustCall(function() { + verifyExecutionOrder(3); + }), 10); + } else if (arg === 3) { + mustCallCheckDomains(); + } else { + throw new Error('UNREACHABLE'); + } + })); +}(1)); + + +function checkDomains() { + // Check that domains are properly entered/exited when called in multiple + // levels from both node::MakeCallback() and AsyncWrap::MakeCallback + setImmediate(common.mustCall(function() { + const d1 = domain.create(); + const d2 = domain.create(); + const d3 = domain.create(); + + makeCallback({ domain: d1 }, common.mustCall(function() { + assert.equal(d1, process.domain); + makeCallback({ domain: d2 }, common.mustCall(function() { + assert.equal(d2, process.domain); + makeCallback({ domain: d3 }, common.mustCall(function() { + assert.equal(d3, process.domain); + })); + assert.equal(d2, process.domain); + })); + assert.equal(d1, process.domain); + })); + })); + + setTimeout(common.mustCall(function() { + const d1 = domain.create(); + const d2 = domain.create(); + const d3 = domain.create(); + + makeCallback({ domain: d1 }, common.mustCall(function() { + assert.equal(d1, process.domain); + makeCallback({ domain: d2 }, common.mustCall(function() { + assert.equal(d2, process.domain); + makeCallback({ domain: d3 }, common.mustCall(function() { + assert.equal(d3, process.domain); + })); + assert.equal(d2, process.domain); + })); + assert.equal(d1, process.domain); + })); + }), 1); + + // Make sure nextTick, setImmediate and setTimeout can all recover properly + // after a thrown makeCallback call. + process.nextTick(common.mustCall(function() { + const d = domain.create(); + d.on('error', common.mustCall(function(e) { + assert.equal(e.message, 'throw from domain 3'); + })); + makeCallback({ domain: d }, function() { + throw new Error('throw from domain 3'); + }); + throw new Error('UNREACHABLE'); + })); + + setImmediate(common.mustCall(function() { + const d = domain.create(); + d.on('error', common.mustCall(function(e) { + assert.equal(e.message, 'throw from domain 2'); + })); + makeCallback({ domain: d }, function() { + throw new Error('throw from domain 2'); + }); + throw new Error('UNREACHABLE'); + })); + + setTimeout(common.mustCall(function() { + const d = domain.create(); + d.on('error', common.mustCall(function(e) { + assert.equal(e.message, 'throw from domain 1'); + })); + makeCallback({ domain: d }, function() { + throw new Error('throw from domain 1'); + }); + throw new Error('UNREACHABLE'); + })); +} diff --git a/test/parallel/test-async-wrap-check-providers.js b/test/parallel/test-async-wrap-check-providers.js index 907b9fa3159926..5ae8654d337303 100644 --- a/test/parallel/test-async-wrap-check-providers.js +++ b/test/parallel/test-async-wrap-check-providers.js @@ -11,6 +11,7 @@ const tls = require('tls'); const zlib = require('zlib'); const ChildProcess = require('child_process').ChildProcess; const StreamWrap = require('_stream_wrap').StreamWrap; +const HTTPParser = process.binding('http_parser').HTTPParser; const async_wrap = process.binding('async_wrap'); const pkeys = Object.keys(async_wrap.Providers); @@ -29,13 +30,13 @@ if (common.isAix) { } } -function init(id) { - keyList = keyList.filter((e) => e != pkeys[id]); +function init(id, provider) { + keyList = keyList.filter((e) => e != pkeys[provider]); } function noop() { } -async_wrap.setupHooks(init, noop, noop); +async_wrap.setupHooks({ init }); async_wrap.enable(); @@ -106,6 +107,8 @@ zlib.createGzip(); new ChildProcess(); +new HTTPParser(HTTPParser.REQUEST); + process.on('exit', function() { if (keyList.length !== 0) { process._rawDebug('Not all keys have been used:'); diff --git a/test/parallel/test-async-wrap-disabled-propagate-parent.js b/test/parallel/test-async-wrap-disabled-propagate-parent.js index 55d2e59efb9ee7..f0daaeb06d0031 100644 --- a/test/parallel/test-async-wrap-disabled-propagate-parent.js +++ b/test/parallel/test-async-wrap-disabled-propagate-parent.js @@ -6,16 +6,23 @@ const net = require('net'); const async_wrap = process.binding('async_wrap'); const providers = Object.keys(async_wrap.Providers); +const uidSymbol = Symbol('uid'); + let cntr = 0; let client; -function init(type, id, parent) { - if (parent) { +function init(uid, type, parentUid, parentHandle) { + this[uidSymbol] = uid; + + if (parentHandle) { cntr++; // Cannot assert in init callback or will abort. process.nextTick(() => { assert.equal(providers[type], 'TCPWRAP'); - assert.equal(parent, server._handle, 'server doesn\'t match parent'); + assert.equal(parentUid, server._handle[uidSymbol], + 'server uid doesn\'t match parent uid'); + assert.equal(parentHandle, server._handle, + 'server handle doesn\'t match parent handle'); assert.equal(this, client._handle, 'client doesn\'t match context'); }); } @@ -23,7 +30,7 @@ function init(type, id, parent) { function noop() { } -async_wrap.setupHooks(init, noop, noop); +async_wrap.setupHooks({ init }); async_wrap.enable(); const server = net.createServer(function(c) { diff --git a/test/parallel/test-async-wrap-post-did-throw.js b/test/parallel/test-async-wrap-post-did-throw.js new file mode 100644 index 00000000000000..9781983f58987e --- /dev/null +++ b/test/parallel/test-async-wrap-post-did-throw.js @@ -0,0 +1,34 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const async_wrap = process.binding('async_wrap'); +var asyncThrows = 0; +var uncaughtExceptionCount = 0; + +process.on('uncaughtException', (e) => { + assert.equal(e.message, 'oh noes!', 'error messages do not match'); +}); + +process.on('exit', () => { + process.removeAllListeners('uncaughtException'); + assert.equal(uncaughtExceptionCount, 1); + assert.equal(uncaughtExceptionCount, asyncThrows); +}); + +function init() { } +function post(id, threw) { + if (threw) + uncaughtExceptionCount++; +} + +async_wrap.setupHooks({ init, post }); +async_wrap.enable(); + +// Timers still aren't supported, so use crypto API. +// It's also important that the callback not happen in a nextTick, like many +// error events in core. +require('crypto').randomBytes(0, () => { + asyncThrows++; + throw new Error('oh noes!'); +}); diff --git a/test/parallel/test-async-wrap-propagate-parent.js b/test/parallel/test-async-wrap-propagate-parent.js index 6001539c624678..34a00c22eeeafd 100644 --- a/test/parallel/test-async-wrap-propagate-parent.js +++ b/test/parallel/test-async-wrap-propagate-parent.js @@ -4,16 +4,25 @@ const common = require('../common'); const assert = require('assert'); const net = require('net'); const async_wrap = process.binding('async_wrap'); +const providers = Object.keys(async_wrap.Providers); + +const uidSymbol = Symbol('uid'); let cntr = 0; let client; -function init(type, id, parent) { - if (parent) { +function init(uid, type, parentUid, parentHandle) { + this[uidSymbol] = uid; + + if (parentHandle) { cntr++; // Cannot assert in init callback or will abort. process.nextTick(() => { - assert.equal(parent, server._handle, 'server doesn\'t match parent'); + assert.equal(providers[type], 'TCPWRAP'); + assert.equal(parentUid, server._handle[uidSymbol], + 'server uid doesn\'t match parent uid'); + assert.equal(parentHandle, server._handle, + 'server handle doesn\'t match parent handle'); assert.equal(this, client._handle, 'client doesn\'t match context'); }); } @@ -21,7 +30,7 @@ function init(type, id, parent) { function noop() { } -async_wrap.setupHooks(init, noop, noop); +async_wrap.setupHooks({ init }); async_wrap.enable(); const server = net.createServer(function(c) { diff --git a/test/parallel/test-async-wrap-throw-from-callback.js b/test/parallel/test-async-wrap-throw-from-callback.js new file mode 100644 index 00000000000000..bfbe32c38b021a --- /dev/null +++ b/test/parallel/test-async-wrap-throw-from-callback.js @@ -0,0 +1,68 @@ +'use strict'; + +require('../common'); +const async_wrap = process.binding('async_wrap'); +const assert = require('assert'); +const crypto = require('crypto'); +const domain = require('domain'); +const spawn = require('child_process').spawn; +const callbacks = [ 'init', 'pre', 'post', 'destroy' ]; +const toCall = process.argv[2]; +var msgCalled = 0; +var msgReceived = 0; + +function init() { + if (toCall === 'init') + throw new Error('init'); +} +function pre() { + if (toCall === 'pre') + throw new Error('pre'); +} +function post() { + if (toCall === 'post') + throw new Error('post'); +} +function destroy() { + if (toCall === 'destroy') + throw new Error('destroy'); +} + +if (typeof process.argv[2] === 'string') { + async_wrap.setupHooks({ init, pre, post, destroy }); + async_wrap.enable(); + + process.on('uncaughtException', () => assert.ok(0, 'UNREACHABLE')); + + const d = domain.create(); + d.on('error', () => assert.ok(0, 'UNREACHABLE')); + d.run(() => { + // Using randomBytes because timers are not yet supported. + crypto.randomBytes(0, () => { }); + }); + +} else { + + process.on('exit', (code) => { + assert.equal(msgCalled, callbacks.length); + assert.equal(msgCalled, msgReceived); + }); + + callbacks.forEach((item) => { + msgCalled++; + + const child = spawn(process.execPath, [__filename, item]); + var errstring = ''; + + child.stderr.on('data', (data) => { + errstring += data.toString(); + }); + + child.on('close', (code) => { + if (errstring.includes('Error: ' + item)) + msgReceived++; + + assert.equal(code, 1, `${item} closed with code ${code}`); + }); + }); +} diff --git a/test/parallel/test-async-wrap-throw-no-init.js b/test/parallel/test-async-wrap-throw-no-init.js index 768e38e8eff389..ccf77f66dc7232 100644 --- a/test/parallel/test-async-wrap-throw-no-init.js +++ b/test/parallel/test-async-wrap-throw-no-init.js @@ -7,14 +7,14 @@ const async_wrap = process.binding('async_wrap'); assert.throws(function() { async_wrap.setupHooks(null); -}, /init callback must be a function/); +}, /first argument must be an object/); assert.throws(function() { async_wrap.enable(); }, /init callback is not assigned to a function/); // Should not throw -async_wrap.setupHooks(() => {}); +async_wrap.setupHooks({ init: () => {} }); async_wrap.enable(); assert.throws(function() { diff --git a/test/parallel/test-async-wrap-uid.js b/test/parallel/test-async-wrap-uid.js new file mode 100644 index 00000000000000..5bf3a8856e0e3f --- /dev/null +++ b/test/parallel/test-async-wrap-uid.js @@ -0,0 +1,57 @@ +'use strict'; + +require('../common'); +const fs = require('fs'); +const assert = require('assert'); +const async_wrap = process.binding('async_wrap'); + +const storage = new Map(); +async_wrap.setupHooks({ init, pre, post, destroy }); +async_wrap.enable(); + +function init(uid) { + storage.set(uid, { + init: true, + pre: false, + post: false, + destroy: false + }); +} + +function pre(uid) { + storage.get(uid).pre = true; +} + +function post(uid) { + storage.get(uid).post = true; +} + +function destroy(uid) { + storage.get(uid).destroy = true; +} + +fs.access(__filename, function(err) { + assert.ifError(err); +}); + +fs.access(__filename, function(err) { + assert.ifError(err); +}); + +async_wrap.disable(); + +process.once('exit', function() { + assert.strictEqual(storage.size, 2); + + for (const item of storage) { + const uid = item[0]; + const value = item[1]; + assert.strictEqual(typeof uid, 'number'); + assert.deepStrictEqual(value, { + init: true, + pre: true, + post: true, + destroy: true + }); + } +}); diff --git a/test/parallel/test-http-catch-uncaughtexception.js b/test/parallel/test-http-catch-uncaughtexception.js new file mode 100644 index 00000000000000..c4054aafbfb699 --- /dev/null +++ b/test/parallel/test-http-catch-uncaughtexception.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +const uncaughtCallback = common.mustCall(function(er) { + assert.equal(er.message, 'get did fail'); +}); + +process.on('uncaughtException', uncaughtCallback); + +const server = http.createServer(function(req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('bye'); +}).listen(common.PORT, function() { + http.get({ port: common.PORT }, function(res) { + res.resume(); + throw new Error('get did fail'); + }).on('close', function() { + server.close(); + }); +});