From 2bbbeb6f20b040e49d4ddbc8e739c6b0ce5fd9f2 Mon Sep 17 00:00:00 2001 From: Trevor Norris Date: Tue, 11 Apr 2017 16:29:39 -0600 Subject: [PATCH 01/12] tty_wrap: throw when uv_tty_init() returns error Also add checks in lib/tty.js and tests. --- lib/tty.js | 8 +++++- src/tty_wrap.cc | 14 +++++++--- src/tty_wrap.h | 3 ++- test/parallel/test-ttywrap-invalid-fd.js | 33 ++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 test/parallel/test-ttywrap-invalid-fd.js diff --git a/lib/tty.js b/lib/tty.js index 3812e9c56b1ddc..d467c827810491 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -37,6 +37,8 @@ exports.isatty = function(fd) { function ReadStream(fd, options) { if (!(this instanceof ReadStream)) return new ReadStream(fd, options); + if (fd >> 0 !== fd || fd < 0) + throw new RangeError('fd must be positive integer: ' + fd); options = util._extend({ highWaterMark: 0, @@ -62,7 +64,11 @@ ReadStream.prototype.setRawMode = function(flag) { function WriteStream(fd) { - if (!(this instanceof WriteStream)) return new WriteStream(fd); + if (!(this instanceof WriteStream)) + return new WriteStream(fd); + if (fd >> 0 !== fd || fd < 0) + throw new RangeError('fd must be positive integer: ' + fd); + net.Socket.call(this, { handle: new TTY(fd, false), readable: false, diff --git a/src/tty_wrap.cc b/src/tty_wrap.cc index 82476e755d285b..b6e3efcc100a34 100644 --- a/src/tty_wrap.cc +++ b/src/tty_wrap.cc @@ -150,17 +150,25 @@ void TTYWrap::New(const FunctionCallbackInfo& args) { int fd = args[0]->Int32Value(); CHECK_GE(fd, 0); - TTYWrap* wrap = new TTYWrap(env, args.This(), fd, args[1]->IsTrue()); + int err = 0; + TTYWrap* wrap = new TTYWrap(env, args.This(), fd, args[1]->IsTrue(), &err); + if (err != 0) + return env->ThrowUVException(err, "uv_tty_init"); + wrap->UpdateWriteQueueSize(); } -TTYWrap::TTYWrap(Environment* env, Local object, int fd, bool readable) +TTYWrap::TTYWrap(Environment* env, + Local object, + int fd, + bool readable, + int* init_err) : StreamWrap(env, object, reinterpret_cast(&handle_), AsyncWrap::PROVIDER_TTYWRAP) { - uv_tty_init(env->event_loop(), &handle_, fd, readable); + *init_err = uv_tty_init(env->event_loop(), &handle_, fd, readable); } } // namespace node diff --git a/src/tty_wrap.h b/src/tty_wrap.h index 8eadbf0a9fc937..7b7cb5ece80e77 100644 --- a/src/tty_wrap.h +++ b/src/tty_wrap.h @@ -44,7 +44,8 @@ class TTYWrap : public StreamWrap { TTYWrap(Environment* env, v8::Local object, int fd, - bool readable); + bool readable, + int* init_err); static void GuessHandleType(const v8::FunctionCallbackInfo& args); static void IsTTY(const v8::FunctionCallbackInfo& args); diff --git a/test/parallel/test-ttywrap-invalid-fd.js b/test/parallel/test-ttywrap-invalid-fd.js new file mode 100644 index 00000000000000..7b466fdbe2650e --- /dev/null +++ b/test/parallel/test-ttywrap-invalid-fd.js @@ -0,0 +1,33 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const tty = require('tty'); + + +assert.throws(() => { + new tty.WriteStream(-1); +}); + +assert.throws(() => { + let fd = 2; + // Get first known bad file descriptor. + try { + while (fs.fstatSync(++fd)); + } catch (e) { } + new tty.WriteStream(fd); +}); + +assert.throws(() => { + new tty.ReadStream(-1); +}); + +assert.throws(() => { + let fd = 2; + // Get first known bad file descriptor. + try { + while (fs.fstatSync(++fd)); + } catch (e) { } + new tty.ReadStream(fd); +}); From a5504e5ca600856c635a5610bbef4810dfb729cf Mon Sep 17 00:00:00 2001 From: Trevor Norris Date: Fri, 3 Mar 2017 13:09:19 -0700 Subject: [PATCH 02/12] test: remove unneeded tests AsyncWrap will be going through many changes, and the old API will no longer be used. So remove those tests that will no longer be useful. They may be added back later using the new API, once fully implemented. --- .../test-async-wrap-check-providers.js | 122 ------------------ ...st-async-wrap-disabled-propagate-parent.js | 50 ------- .../test-async-wrap-post-did-throw.js | 39 ------ .../test-async-wrap-propagate-parent.js | 49 ------- .../test-async-wrap-throw-from-callback.js | 73 ----------- .../parallel/test-async-wrap-throw-no-init.js | 25 ---- test/parallel/test-async-wrap-uid.js | 63 --------- test/parallel/test-stream-base-no-abort.js | 64 --------- 8 files changed, 485 deletions(-) delete mode 100644 test/parallel/test-async-wrap-check-providers.js delete mode 100644 test/parallel/test-async-wrap-disabled-propagate-parent.js delete mode 100644 test/parallel/test-async-wrap-post-did-throw.js delete mode 100644 test/parallel/test-async-wrap-propagate-parent.js delete mode 100644 test/parallel/test-async-wrap-throw-from-callback.js delete mode 100644 test/parallel/test-async-wrap-throw-no-init.js delete mode 100644 test/parallel/test-async-wrap-uid.js delete mode 100644 test/parallel/test-stream-base-no-abort.js diff --git a/test/parallel/test-async-wrap-check-providers.js b/test/parallel/test-async-wrap-check-providers.js deleted file mode 100644 index 2800dbb16c8c23..00000000000000 --- a/test/parallel/test-async-wrap-check-providers.js +++ /dev/null @@ -1,122 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) { - common.skip('missing crypto'); - return; -} - -const assert = require('assert'); -const crypto = require('crypto'); -const dgram = require('dgram'); -const dns = require('dns'); -const fs = require('fs'); -const net = require('net'); -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); - -let keyList = pkeys.slice(); -// Drop NONE -keyList.splice(0, 1); - -// fs-watch currently needs special configuration on AIX and we -// want to improve under https://github.com/nodejs/node/issues/5085. -// strip out fs watch related parts for now -if (common.isAix) { - for (let i = 0; i < keyList.length; i++) { - if ((keyList[i] === 'FSEVENTWRAP') || (keyList[i] === 'STATWATCHER')) { - keyList.splice(i, 1); - } - } -} - -function init(id, provider) { - keyList = keyList.filter((e) => e !== pkeys[provider]); -} - -async_wrap.setupHooks({ init }); - -async_wrap.enable(); - - -setTimeout(common.noop, 1); - -fs.stat(__filename, common.noop); - -if (!common.isAix) { - // fs-watch currently needs special configuration on AIX and we - // want to improve under https://github.com/nodejs/node/issues/5085. - // strip out fs watch related parts for now - fs.watchFile(__filename, common.noop); - fs.unwatchFile(__filename); - fs.watch(__filename).close(); -} - -dns.lookup('localhost', common.noop); -dns.lookupService('::', 0, common.noop); -dns.resolve('localhost', common.noop); - -new StreamWrap(new net.Socket()); - -new (process.binding('tty_wrap').TTY)(); - -crypto.randomBytes(1, common.noop); - -common.refreshTmpDir(); - -net.createServer(function(c) { - c.end(); - this.close(); -}).listen(common.PIPE, function() { - net.connect(common.PIPE, common.noop); -}); - -net.createServer(function(c) { - c.end(); - this.close(checkTLS); -}).listen(0, function() { - net.connect(this.address().port, common.noop); -}); - -dgram.createSocket('udp4').bind(0, function() { - this.send(Buffer.allocUnsafe(2), 0, 2, this.address().port, '::', () => { - this.close(); - }); -}); - -process.on('SIGINT', () => process.exit()); - -// Run from closed net server above. -function checkTLS() { - const options = { - key: fs.readFileSync(common.fixturesDir + '/keys/ec-key.pem'), - cert: fs.readFileSync(common.fixturesDir + '/keys/ec-cert.pem') - }; - const server = tls.createServer(options, common.noop) - .listen(0, function() { - const connectOpts = { rejectUnauthorized: false }; - tls.connect(this.address().port, connectOpts, function() { - this.destroy(); - server.close(); - }); - }); -} - -zlib.createGzip(); - -new ChildProcess(); - -new HTTPParser(HTTPParser.REQUEST); - -process.on('exit', function() { - if (keyList.length !== 0) { - process._rawDebug('Not all keys have been used:'); - process._rawDebug(keyList); - assert.strictEqual(keyList.length, 0); - } -}); diff --git a/test/parallel/test-async-wrap-disabled-propagate-parent.js b/test/parallel/test-async-wrap-disabled-propagate-parent.js deleted file mode 100644 index 65e5eaafa3553a..00000000000000 --- a/test/parallel/test-async-wrap-disabled-propagate-parent.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -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(uid, type, parentUid, parentHandle) { - this[uidSymbol] = uid; - - if (parentHandle) { - cntr++; - // Cannot assert in init callback or will abort. - process.nextTick(() => { - assert.strictEqual(providers[type], 'TCPWRAP'); - assert.strictEqual(parentUid, server._handle[uidSymbol], - 'server uid doesn\'t match parent uid'); - assert.strictEqual(parentHandle, server._handle, - 'server handle doesn\'t match parent handle'); - assert.strictEqual(this, client._handle, 'client doesn\'t match context'); - }); - } -} - -async_wrap.setupHooks({ init }); -async_wrap.enable(); - -const server = net.createServer(function(c) { - client = c; - // Allow init callback to run before closing. - setImmediate(() => { - c.end(); - this.close(); - }); -}).listen(0, function() { - net.connect(this.address().port, common.noop); -}); - -async_wrap.disable(); - -process.on('exit', function() { - // init should have only been called once with a parent. - assert.strictEqual(cntr, 1); -}); diff --git a/test/parallel/test-async-wrap-post-did-throw.js b/test/parallel/test-async-wrap-post-did-throw.js deleted file mode 100644 index 35dbfe1378464a..00000000000000 --- a/test/parallel/test-async-wrap-post-did-throw.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) { - common.skip('missing crypto'); - return; -} - -const assert = require('assert'); -const async_wrap = process.binding('async_wrap'); -let asyncThrows = 0; -let uncaughtExceptionCount = 0; - -process.on('uncaughtException', (e) => { - assert.strictEqual(e.message, 'oh noes!', 'error messages do not match'); -}); - -process.on('exit', () => { - process.removeAllListeners('uncaughtException'); - assert.strictEqual(uncaughtExceptionCount, 1); - assert.strictEqual(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 deleted file mode 100644 index dbb358ad6ae3c5..00000000000000 --- a/test/parallel/test-async-wrap-propagate-parent.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -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(uid, type, parentUid, parentHandle) { - this[uidSymbol] = uid; - - if (parentHandle) { - cntr++; - // Cannot assert in init callback or will abort. - process.nextTick(() => { - assert.strictEqual(providers[type], 'TCPWRAP'); - assert.strictEqual(parentUid, server._handle[uidSymbol], - 'server uid doesn\'t match parent uid'); - assert.strictEqual(parentHandle, server._handle, - 'server handle doesn\'t match parent handle'); - assert.strictEqual(this, client._handle, 'client doesn\'t match context'); - }); - } -} - -async_wrap.setupHooks({ init }); -async_wrap.enable(); - -const server = net.createServer(function(c) { - client = c; - // Allow init callback to run before closing. - setImmediate(() => { - c.end(); - this.close(); - }); -}).listen(0, function() { - net.connect(this.address().port, common.noop); -}); - - -process.on('exit', function() { - // init should have only been called once with a parent. - assert.strictEqual(cntr, 1); -}); diff --git a/test/parallel/test-async-wrap-throw-from-callback.js b/test/parallel/test-async-wrap-throw-from-callback.js deleted file mode 100644 index bb820a1b088cd6..00000000000000 --- a/test/parallel/test-async-wrap-throw-from-callback.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) { - common.skip('missing crypto'); - return; -} - -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]; -let msgCalled = 0; -let 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', common.mustNotCall()); - - const d = domain.create(); - d.on('error', common.mustNotCall()); - d.run(() => { - // Using randomBytes because timers are not yet supported. - crypto.randomBytes(0, common.noop); - }); - -} else { - - process.on('exit', (code) => { - assert.strictEqual(msgCalled, callbacks.length); - assert.strictEqual(msgCalled, msgReceived); - }); - - callbacks.forEach((item) => { - msgCalled++; - - const child = spawn(process.execPath, [__filename, item]); - let errstring = ''; - - child.stderr.on('data', (data) => { - errstring += data.toString(); - }); - - child.on('close', (code) => { - if (errstring.includes('Error: ' + item)) - msgReceived++; - - assert.strictEqual(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 deleted file mode 100644 index 05f453cbcde2c9..00000000000000 --- a/test/parallel/test-async-wrap-throw-no-init.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const common = require('../common'); -const assert = require('assert'); -const async_wrap = process.binding('async_wrap'); - -assert.throws(function() { - async_wrap.setupHooks(null); -}, /first argument must be an object/); - -assert.throws(function() { - async_wrap.setupHooks({}); -}, /init callback must be a function/); - -assert.throws(function() { - async_wrap.enable(); -}, /init callback is not assigned to a function/); - -// Should not throw -async_wrap.setupHooks({ init: common.noop }); -async_wrap.enable(); - -assert.throws(function() { - async_wrap.setupHooks(common.noop); -}, /hooks should not be set while also enabled/); diff --git a/test/parallel/test-async-wrap-uid.js b/test/parallel/test-async-wrap-uid.js deleted file mode 100644 index e28f8537d235c5..00000000000000 --- a/test/parallel/test-async-wrap-uid.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -const common = require('../common'); -const fs = require('fs'); -const assert = require('assert'); -const async_wrap = process.binding('async_wrap'); - -// Give the event loop time to clear out the final uv_close(). -let si_cntr = 3; -process.on('beforeExit', () => { - if (--si_cntr > 0) setImmediate(common.noop); -}); - -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-stream-base-no-abort.js b/test/parallel/test-stream-base-no-abort.js deleted file mode 100644 index f046a6f7aff8e8..00000000000000 --- a/test/parallel/test-stream-base-no-abort.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) { - common.skip('missing crypto'); - return; -} - -const async_wrap = process.binding('async_wrap'); -const uv = process.binding('uv'); -const assert = require('assert'); -const dgram = require('dgram'); -const fs = require('fs'); -const net = require('net'); -const tls = require('tls'); -const providers = Object.keys(async_wrap.Providers); -let flags = 0; - -// Make sure all asserts have run at least once. -process.on('exit', () => assert.strictEqual(flags, 0b111)); - -function init(id, provider) { - this._external; // Test will abort if nullptr isn't properly checked. - switch (providers[provider]) { - case 'TCPWRAP': - assert.strictEqual(this.fd, uv.UV_EINVAL); - flags |= 0b1; - break; - case 'TLSWRAP': - assert.strictEqual(this.fd, uv.UV_EINVAL); - flags |= 0b10; - break; - case 'UDPWRAP': - assert.strictEqual(this.fd, uv.UV_EBADF); - flags |= 0b100; - break; - } -} - -async_wrap.setupHooks({ init }); -async_wrap.enable(); - -const checkTLS = common.mustCall(function checkTLS() { - const options = { - key: fs.readFileSync(common.fixturesDir + '/keys/ec-key.pem'), - cert: fs.readFileSync(common.fixturesDir + '/keys/ec-cert.pem') - }; - const server = tls.createServer(options, common.noop) - .listen(0, function() { - const connectOpts = { rejectUnauthorized: false }; - tls.connect(this.address().port, connectOpts, function() { - this.destroy(); - server.close(); - }); - }); -}); - -const checkTCP = common.mustCall(function checkTCP() { - net.createServer(common.noop).listen(0, function() { - this.close(checkTLS); - }); -}); - -dgram.createSocket('udp4').close(checkTCP); From 484ecfd14ed7c12609b55ba52d35e3ac384a8fad Mon Sep 17 00:00:00 2001 From: Trevor Norris Date: Mon, 12 Dec 2016 23:24:02 -0700 Subject: [PATCH 03/12] async_wrap: use double, not int64_t, for async id The number of ids is limited to 2^53-1 regardless of whether an int64_t is used or not because JS is limited to a double. So to make conversion simpler, track ids internally as a double. This will also make life simpler when this is eventually exposed to JS via a Float64Array. Rename AsyncWrap::get_uid() to AsyncWrap::get_id(). --- src/async-wrap-inl.h | 4 ++-- src/async-wrap.cc | 12 ++++++------ src/async-wrap.h | 4 ++-- src/env-inl.h | 8 ++++---- src/env.h | 8 ++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/async-wrap-inl.h b/src/async-wrap-inl.h index 80419405ba78e8..8d7ada213d5611 100644 --- a/src/async-wrap-inl.h +++ b/src/async-wrap-inl.h @@ -46,8 +46,8 @@ inline AsyncWrap::ProviderType AsyncWrap::provider_type() const { } -inline int64_t AsyncWrap::get_uid() const { - return uid_; +inline double AsyncWrap::get_id() const { + return id_; } diff --git a/src/async-wrap.cc b/src/async-wrap.cc index bc3e049d262c81..71f09ba1603494 100644 --- a/src/async-wrap.cc +++ b/src/async-wrap.cc @@ -220,7 +220,7 @@ void AsyncWrap::DestroyIdsCb(uv_idle_t* handle) { TryCatch try_catch(env->isolate()); - std::vector destroy_ids_list; + std::vector destroy_ids_list; destroy_ids_list.swap(*env->destroy_ids_list()); for (auto current_id : destroy_ids_list) { // Want each callback to be cleaned up after itself, instead of cleaning @@ -255,7 +255,7 @@ AsyncWrap::AsyncWrap(Environment* env, ProviderType provider, AsyncWrap* parent) : BaseObject(env, object), bits_(static_cast(provider) << 1), - uid_(env->get_async_wrap_uid()) { + id_(env->get_async_wrap_uid()) { CHECK_NE(provider, PROVIDER_NONE); CHECK_GE(object->InternalFieldCount(), 1); @@ -277,14 +277,14 @@ AsyncWrap::AsyncWrap(Environment* env, HandleScope scope(env->isolate()); Local argv[] = { - Number::New(env->isolate(), get_uid()), + Number::New(env->isolate(), get_id()), Int32::New(env->isolate(), provider), Null(env->isolate()), Null(env->isolate()) }; if (parent != nullptr) { - argv[2] = Number::New(env->isolate(), parent->get_uid()); + argv[2] = Number::New(env->isolate(), parent->get_id()); argv[3] = parent->object(); } @@ -309,7 +309,7 @@ AsyncWrap::~AsyncWrap() { if (env()->destroy_ids_list()->empty()) uv_idle_start(env()->destroy_ids_idle_handle(), DestroyIdsCb); - env()->destroy_ids_list()->push_back(get_uid()); + env()->destroy_ids_list()->push_back(get_id()); } @@ -320,7 +320,7 @@ Local AsyncWrap::MakeCallback(const Local cb, 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 uid = Number::New(env()->isolate(), get_id()); Local context = object(); Local domain; bool has_domain = false; diff --git a/src/async-wrap.h b/src/async-wrap.h index a044a01863202d..8483685c2274b2 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -86,7 +86,7 @@ class AsyncWrap : public BaseObject { inline ProviderType provider_type() const; - inline int64_t get_uid() const; + inline double get_id() const; // Only call these within a valid HandleScope. v8::Local MakeCallback(const v8::Local cb, @@ -109,7 +109,7 @@ class AsyncWrap : public BaseObject { // expected the context object will receive a _asyncQueue object property // that will be used to call pre/post in MakeCallback. uint32_t bits_; - const int64_t uid_; + const double id_; }; void LoadAsyncWrapperInfo(Environment* env); diff --git a/src/env-inl.h b/src/env-inl.h index 978f8ca819dec1..ae2146a7dd760f 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -192,7 +192,7 @@ inline Environment::Environment(IsolateData* isolate_data, printed_error_(false), trace_sync_io_(false), makecallback_cntr_(0), - async_wrap_uid_(0), + async_wrap_id_(0), debugger_agent_(this), #if HAVE_INSPECTOR inspector_agent_(this), @@ -343,11 +343,11 @@ inline void Environment::set_trace_sync_io(bool value) { trace_sync_io_ = value; } -inline int64_t Environment::get_async_wrap_uid() { - return ++async_wrap_uid_; +inline double Environment::get_async_wrap_uid() { + return ++async_wrap_id_; } -inline std::vector* Environment::destroy_ids_list() { +inline std::vector* Environment::destroy_ids_list() { return &destroy_ids_list_; } diff --git a/src/env.h b/src/env.h index a719cda2d40c6e..70a129701ff131 100644 --- a/src/env.h +++ b/src/env.h @@ -483,10 +483,10 @@ class Environment { void PrintSyncTrace() const; inline void set_trace_sync_io(bool value); - inline int64_t get_async_wrap_uid(); + inline double get_async_wrap_uid(); // List of id's that have been destroyed and need the destroy() cb called. - inline std::vector* destroy_ids_list(); + inline std::vector* destroy_ids_list(); inline double* heap_statistics_buffer() const; inline void set_heap_statistics_buffer(double* pointer); @@ -593,8 +593,8 @@ class Environment { bool printed_error_; bool trace_sync_io_; size_t makecallback_cntr_; - int64_t async_wrap_uid_; - std::vector destroy_ids_list_; + double async_wrap_id_; + std::vector destroy_ids_list_; debugger::Agent debugger_agent_; #if HAVE_INSPECTOR inspector::Agent inspector_agent_; From 6d024ce86463c220c9e52a8af4be0178222a6429 Mon Sep 17 00:00:00 2001 From: Trevor Norris Date: Wed, 28 Sep 2016 15:40:01 -0600 Subject: [PATCH 04/12] crypto: use named FunctionTemplate RandomBytes and PBKDF2 were using the same "generic" ObjectTemplate for construction. Instead create one for each that is properly named. --- src/env-inl.h | 12 ------------ src/env.h | 5 ++--- src/node_crypto.cc | 19 +++++++++++++++++-- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/env-inl.h b/src/env-inl.h index ae2146a7dd760f..5243b66d58cdcc 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -208,12 +208,6 @@ inline Environment::Environment(IsolateData* isolate_data, set_binding_cache_object(v8::Object::New(isolate())); set_module_load_list_array(v8::Array::New(isolate())); - v8::Local fn = v8::FunctionTemplate::New(isolate()); - fn->SetClassName(FIXED_ONE_BYTE_STRING(isolate(), "InternalFieldObject")); - v8::Local obj = fn->InstanceTemplate(); - obj->SetInternalFieldCount(1); - set_generic_internal_field_template(obj); - RB_INIT(&cares_task_list_); AssignToContext(context); @@ -496,12 +490,6 @@ inline void Environment::SetTemplateMethod(v8::Local that, t->SetClassName(name_string); // NODE_SET_METHOD() compatibility. } -inline v8::Local Environment::NewInternalFieldObject() { - v8::MaybeLocal m_obj = - generic_internal_field_template()->NewInstance(context()); - return m_obj.ToLocalChecked(); -} - #define VP(PropertyName, StringValue) V(v8::Private, PropertyName) #define VS(PropertyName, StringValue) V(v8::String, PropertyName) #define V(TypeName, PropertyName) \ diff --git a/src/env.h b/src/env.h index 70a129701ff131..926e65755c848d 100644 --- a/src/env.h +++ b/src/env.h @@ -258,13 +258,14 @@ namespace node { V(context, v8::Context) \ V(domain_array, v8::Array) \ V(domains_stack_array, v8::Array) \ - V(generic_internal_field_template, v8::ObjectTemplate) \ V(jsstream_constructor_template, v8::FunctionTemplate) \ V(module_load_list_array, v8::Array) \ + V(pbkdf2_constructor_template, v8::ObjectTemplate) \ V(pipe_constructor_template, v8::FunctionTemplate) \ V(process_object, v8::Object) \ V(promise_reject_function, v8::Function) \ V(push_values_to_array_function, v8::Function) \ + V(randombytes_constructor_template, v8::ObjectTemplate) \ V(script_context_constructor_template, v8::FunctionTemplate) \ V(script_data_constructor_function, v8::Function) \ V(secure_context_constructor_template, v8::FunctionTemplate) \ @@ -529,8 +530,6 @@ class Environment { const char* name, v8::FunctionCallback callback); - inline v8::Local NewInternalFieldObject(); - void AtExit(void (*cb)(void* arg), void* arg); void RunAtExitCallbacks(); diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 0f9ed3434eca09..34379eec27f90a 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -105,6 +105,7 @@ using v8::Local; using v8::Maybe; using v8::Null; using v8::Object; +using v8::ObjectTemplate; using v8::Persistent; using v8::PropertyAttribute; using v8::PropertyCallbackInfo; @@ -5533,7 +5534,8 @@ void PBKDF2(const FunctionCallbackInfo& args) { digest = EVP_sha1(); } - obj = env->NewInternalFieldObject(); + obj = env->pbkdf2_constructor_template()-> + NewInstance(env->context()).ToLocalChecked(); req = new PBKDF2Request(env, obj, digest, @@ -5701,7 +5703,8 @@ void RandomBytes(const FunctionCallbackInfo& args) { if (size < 0 || size > Buffer::kMaxLength) return env->ThrowRangeError("size is not a valid Smi"); - Local obj = env->NewInternalFieldObject(); + Local obj = env->randombytes_constructor_template()-> + NewInstance(env->context()).ToLocalChecked(); RandomBytesRequest* req = new RandomBytesRequest(env, obj, size); if (args[1]->IsFunction()) { @@ -6169,6 +6172,18 @@ void InitCrypto(Local target, PublicKeyCipher::Cipher); + + Local pb = FunctionTemplate::New(env->isolate()); + pb->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "PBKDF2")); + Local pbt = pb->InstanceTemplate(); + pbt->SetInternalFieldCount(1); + env->set_pbkdf2_constructor_template(pbt); + + Local rb = FunctionTemplate::New(env->isolate()); + rb->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "RandomBytes")); + Local rbt = rb->InstanceTemplate(); + rbt->SetInternalFieldCount(1); + env->set_randombytes_constructor_template(rbt); } } // namespace crypto From f090fd2c2d8a1ea361ab3d49783ca83f401f72e7 Mon Sep 17 00:00:00 2001 From: Trevor Norris Date: Wed, 2 Nov 2016 14:53:42 -0600 Subject: [PATCH 05/12] async_wrap: use more specific providers Instead of wrapping several providers into PROVIDER_CRYPTO, have them all be named after their class. Rename other providers to also match their class names. With the exception of Parser. Which is actually HTTPParser. Add PROVIDER_LENGTH to make better checks in WrapperInfo(). --- src/async-wrap.cc | 4 +++- src/async-wrap.h | 11 +++++++---- src/node_crypto.cc | 4 ++-- src/node_crypto.h | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/async-wrap.cc b/src/async-wrap.cc index 71f09ba1603494..304e5d0c956490 100644 --- a/src/async-wrap.cc +++ b/src/async-wrap.cc @@ -109,7 +109,9 @@ intptr_t RetainedAsyncInfo::GetSizeInBytes() { RetainedObjectInfo* WrapperInfo(uint16_t class_id, Local wrapper) { // No class_id should be the provider type of NONE. - CHECK_NE(NODE_ASYNC_ID_OFFSET, class_id); + CHECK_GT(class_id, NODE_ASYNC_ID_OFFSET); + // And make sure the class_id doesn't extend past the last provider. + CHECK_LE(class_id - NODE_ASYNC_ID_OFFSET, AsyncWrap::PROVIDERS_LENGTH); CHECK(wrapper->IsObject()); CHECK(!wrapper.IsEmpty()); diff --git a/src/async-wrap.h b/src/async-wrap.h index 8483685c2274b2..ecf8db9fc5eea9 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -36,27 +36,29 @@ namespace node { #define NODE_ASYNC_PROVIDER_TYPES(V) \ V(NONE) \ - V(CRYPTO) \ + V(CONNECTION) \ V(FSEVENTWRAP) \ V(FSREQWRAP) \ V(GETADDRINFOREQWRAP) \ V(GETNAMEINFOREQWRAP) \ V(HTTPPARSER) \ V(JSSTREAM) \ - V(PIPEWRAP) \ + V(PBKDF2REQUEST) \ V(PIPECONNECTWRAP) \ + V(PIPEWRAP) \ V(PROCESSWRAP) \ V(QUERYWRAP) \ + V(RANDOMBYTESREQUEST) \ V(SHUTDOWNWRAP) \ V(SIGNALWRAP) \ V(STATWATCHER) \ - V(TCPWRAP) \ V(TCPCONNECTWRAP) \ + V(TCPWRAP) \ V(TIMERWRAP) \ V(TLSWRAP) \ V(TTYWRAP) \ - V(UDPWRAP) \ V(UDPSENDWRAP) \ + V(UDPWRAP) \ V(WRITEWRAP) \ V(ZLIB) @@ -69,6 +71,7 @@ class AsyncWrap : public BaseObject { PROVIDER_ ## PROVIDER, NODE_ASYNC_PROVIDER_TYPES(V) #undef V + PROVIDERS_LENGTH, }; AsyncWrap(Environment* env, diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 34379eec27f90a..c066bcd9b2cc3d 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -5315,7 +5315,7 @@ class PBKDF2Request : public AsyncWrap { char* salt, int iter, int keylen) - : AsyncWrap(env, object, AsyncWrap::PROVIDER_CRYPTO), + : AsyncWrap(env, object, AsyncWrap::PROVIDER_PBKDF2REQUEST), digest_(digest), error_(0), passlen_(passlen), @@ -5581,7 +5581,7 @@ void PBKDF2(const FunctionCallbackInfo& args) { class RandomBytesRequest : public AsyncWrap { public: RandomBytesRequest(Environment* env, Local object, size_t size) - : AsyncWrap(env, object, AsyncWrap::PROVIDER_CRYPTO), + : AsyncWrap(env, object, AsyncWrap::PROVIDER_RANDOMBYTESREQUEST), error_(0), size_(size), data_(node::Malloc(size)) { diff --git a/src/node_crypto.h b/src/node_crypto.h index ffb8444ce60145..90e268456a4f12 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -402,7 +402,7 @@ class Connection : public AsyncWrap, public SSLWrap { v8::Local wrap, SecureContext* sc, SSLWrap::Kind kind) - : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_CRYPTO), + : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_CONNECTION), SSLWrap(env, sc, kind), bio_read_(nullptr), bio_write_(nullptr), From 6d11989cbe822156dc1aaaec6f0b0328698c9946 Mon Sep 17 00:00:00 2001 From: Trevor Norris Date: Wed, 2 Nov 2016 13:55:00 -0600 Subject: [PATCH 06/12] async_wrap,src: add GetAsyncId() method Allow handles to retrieve their own uid's by adding a new method on the FunctionTemplates. Implementation of these into all other classes will come in a future commit. Add the method AsyncWrap::GetAsyncId() to all inheriting class objects so the uid of the handle can be retrieved from JS. In all applicable locations, run ClearWrap() on the object holding the pointer so that it never points to invalid memory and make sure Wrap() is always run so the class pointer is correctly attached to the object and can be retrieved so GetAsyncId() can be run. In many places a class instance was not removing its own pointer from object() in the destructor. This left an invalid pointer in the JS object that could cause the application to segfault under certain conditions. Remove ClearWrap() from ReqWrap for continuity. The ReqWrap constructor was not the one to call Wrap(), so it shouldn't be the one to call ClearWrap(). Wrap() has been added to all constructors that inherit from AsyncWrap. Normally it's the child most class. Except in the case of HandleWrap. Which must be the constructor that runs Wrap() because the class pointer is retrieved for certain calls and because other child classes have multiple inheritance to pointer to the HandleWrap needs to be stored. ClearWrap() has been placed in all FunctionTemplate constructors so that no random values are returned when running getAsyncId(). ClearWrap() has also been placed in all class destructors, except in those that use MakeWeak() because the destructor will run during GC. Making the object() inaccessible. It could be simplified to where AsyncWrap sets the internal pointer, then if an inheriting class needs one of it's own it could set it again. But the inverse would need to be true also, where AsyncWrap then also runs ClearWeak. Unforunately because some of the handles are cleaned up during GC that's impossible. Also in the case of ReqWrap it runs Reset() in the destructor, making the object() inaccessible. Meaning, ClearWrap() must be run by the class that runs Wrap(). There's currently no generalized way of taking care of this across all instances of AsyncWrap. I'd prefer that there be checks in there for these things, but haven't found a way to place them that wouldn't be just as unreliable. Add test that checks all resources that can run getAsyncId(). Would like a way to enforce that any new classes that can also run getAsyncId() are tested, but don't have one. --- src/async-wrap.cc | 8 + src/async-wrap.h | 2 + src/cares_wrap.cc | 19 ++ src/connect_wrap.cc | 5 + src/connect_wrap.h | 1 + src/fs_event_wrap.cc | 1 + src/js_stream.cc | 2 + src/node_crypto.cc | 3 + src/node_crypto.h | 1 + src/node_file.cc | 7 +- src/node_http_parser.cc | 1 + src/node_stat_watcher.cc | 2 + src/node_zlib.cc | 2 + src/pipe_wrap.cc | 4 + src/process_wrap.cc | 2 + src/req-wrap-inl.h | 1 - src/signal_wrap.cc | 1 + src/stream_base.h | 10 + src/stream_wrap.cc | 2 + src/tcp_wrap.cc | 3 + src/timer_wrap.cc | 2 + src/tls_wrap.cc | 1 + src/tty_wrap.cc | 2 + src/udp_wrap.cc | 10 + test/parallel/test-async-wrap-getasyncid.js | 208 ++++++++++++++++++++ 25 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-async-wrap-getasyncid.js diff --git a/src/async-wrap.cc b/src/async-wrap.cc index 304e5d0c956490..11ed67d24207c0 100644 --- a/src/async-wrap.cc +++ b/src/async-wrap.cc @@ -180,6 +180,14 @@ static void SetupHooks(const FunctionCallbackInfo& args) { } +void AsyncWrap::GetAsyncId(const FunctionCallbackInfo& args) { + AsyncWrap* wrap; + args.GetReturnValue().Set(-1); + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + args.GetReturnValue().Set(wrap->get_id()); +} + + void AsyncWrap::Initialize(Local target, Local unused, Local context) { diff --git a/src/async-wrap.h b/src/async-wrap.h index ecf8db9fc5eea9..7ccae02cced05b 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -85,6 +85,8 @@ class AsyncWrap : public BaseObject { v8::Local unused, v8::Local context); + static void GetAsyncId(const v8::FunctionCallbackInfo& args); + static void DestroyIdsCb(uv_idle_t* handle); inline ProviderType provider_type() const; diff --git a/src/cares_wrap.cc b/src/cares_wrap.cc index 9fe20f15903e1e..e2f9ca80be67f8 100644 --- a/src/cares_wrap.cc +++ b/src/cares_wrap.cc @@ -103,6 +103,7 @@ inline const char* ToErrorCodeString(int status) { class GetAddrInfoReqWrap : public ReqWrap { public: GetAddrInfoReqWrap(Environment* env, Local req_wrap_obj); + ~GetAddrInfoReqWrap(); size_t self_size() const override { return sizeof(*this); } }; @@ -114,14 +115,21 @@ GetAddrInfoReqWrap::GetAddrInfoReqWrap(Environment* env, } +GetAddrInfoReqWrap::~GetAddrInfoReqWrap() { + ClearWrap(object()); +} + + static void NewGetAddrInfoReqWrap(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); + ClearWrap(args.This()); } class GetNameInfoReqWrap : public ReqWrap { public: GetNameInfoReqWrap(Environment* env, Local req_wrap_obj); + ~GetNameInfoReqWrap(); size_t self_size() const override { return sizeof(*this); } }; @@ -133,13 +141,20 @@ GetNameInfoReqWrap::GetNameInfoReqWrap(Environment* env, } +GetNameInfoReqWrap::~GetNameInfoReqWrap() { + ClearWrap(object()); +} + + static void NewGetNameInfoReqWrap(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); + ClearWrap(args.This()); } static void NewQueryReqWrap(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); + ClearWrap(args.This()); } @@ -307,6 +322,7 @@ class QueryWrap : public AsyncWrap { : AsyncWrap(env, req_wrap_obj, AsyncWrap::PROVIDER_QUERYWRAP) { if (env->in_domain()) req_wrap_obj->Set(env->domain_string(), env->domain_array()->Get(0)); + Wrap(req_wrap_obj, this); } ~QueryWrap() override { @@ -1402,6 +1418,7 @@ static void Initialize(Local target, Local aiw = FunctionTemplate::New(env->isolate(), NewGetAddrInfoReqWrap); aiw->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(aiw, "getAsyncId", AsyncWrap::GetAsyncId); aiw->SetClassName( FIXED_ONE_BYTE_STRING(env->isolate(), "GetAddrInfoReqWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "GetAddrInfoReqWrap"), @@ -1410,6 +1427,7 @@ static void Initialize(Local target, Local niw = FunctionTemplate::New(env->isolate(), NewGetNameInfoReqWrap); niw->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(niw, "getAsyncId", AsyncWrap::GetAsyncId); niw->SetClassName( FIXED_ONE_BYTE_STRING(env->isolate(), "GetNameInfoReqWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "GetNameInfoReqWrap"), @@ -1418,6 +1436,7 @@ static void Initialize(Local target, Local qrw = FunctionTemplate::New(env->isolate(), NewQueryReqWrap); qrw->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(qrw, "getAsyncId", AsyncWrap::GetAsyncId); qrw->SetClassName( FIXED_ONE_BYTE_STRING(env->isolate(), "QueryReqWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "QueryReqWrap"), diff --git a/src/connect_wrap.cc b/src/connect_wrap.cc index df3f093e732972..e373b5a36e33e6 100644 --- a/src/connect_wrap.cc +++ b/src/connect_wrap.cc @@ -19,4 +19,9 @@ ConnectWrap::ConnectWrap(Environment* env, Wrap(req_wrap_obj, this); } + +ConnectWrap::~ConnectWrap() { + ClearWrap(object()); +} + } // namespace node diff --git a/src/connect_wrap.h b/src/connect_wrap.h index 28d4872d7ed416..7b16a5448745aa 100644 --- a/src/connect_wrap.h +++ b/src/connect_wrap.h @@ -15,6 +15,7 @@ class ConnectWrap : public ReqWrap { ConnectWrap(Environment* env, v8::Local req_wrap_obj, AsyncWrap::ProviderType provider); + ~ConnectWrap(); size_t self_size() const override { return sizeof(*this); } }; diff --git a/src/fs_event_wrap.cc b/src/fs_event_wrap.cc index ce272362c420ee..5b2cf4a9b750c9 100644 --- a/src/fs_event_wrap.cc +++ b/src/fs_event_wrap.cc @@ -91,6 +91,7 @@ void FSEventWrap::Initialize(Local target, t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(fsevent_string); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(t, "start", Start); env->SetProtoMethod(t, "close", Close); diff --git a/src/js_stream.cc b/src/js_stream.cc index e51c4ae9b35084..1d20e1c6d77dfb 100644 --- a/src/js_stream.cc +++ b/src/js_stream.cc @@ -221,6 +221,8 @@ void JSStream::Initialize(Local target, t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "JSStream")); t->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(t, "doAlloc", DoAlloc); env->SetProtoMethod(t, "doRead", DoRead); env->SetProtoMethod(t, "doAfterWrite", DoAfterWrite); diff --git a/src/node_crypto.cc b/src/node_crypto.cc index c066bcd9b2cc3d..4593fdf44ac7fc 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -2737,6 +2737,7 @@ void Connection::Initialize(Environment* env, Local target) { t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Connection")); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(t, "encIn", Connection::EncIn); env->SetProtoMethod(t, "clearOut", Connection::ClearOut); env->SetProtoMethod(t, "clearIn", Connection::ClearIn); @@ -6175,12 +6176,14 @@ void InitCrypto(Local target, Local pb = FunctionTemplate::New(env->isolate()); pb->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "PBKDF2")); + env->SetProtoMethod(pb, "getAsyncId", AsyncWrap::GetAsyncId); Local pbt = pb->InstanceTemplate(); pbt->SetInternalFieldCount(1); env->set_pbkdf2_constructor_template(pbt); Local rb = FunctionTemplate::New(env->isolate()); rb->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "RandomBytes")); + env->SetProtoMethod(rb, "getAsyncId", AsyncWrap::GetAsyncId); Local rbt = rb->InstanceTemplate(); rbt->SetInternalFieldCount(1); env->set_randombytes_constructor_template(rbt); diff --git a/src/node_crypto.h b/src/node_crypto.h index 90e268456a4f12..ad1b493596aa7a 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -408,6 +408,7 @@ class Connection : public AsyncWrap, public SSLWrap { bio_write_(nullptr), hello_offset_(0) { MakeWeak(this); + Wrap(wrap, this); hello_parser_.Start(SSLWrap::OnClientHello, OnClientHelloParseEnd, this); diff --git a/src/node_file.cc b/src/node_file.cc index 9c180833fb926f..dcf70830789f92 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -109,7 +109,10 @@ class FSReqWrap: public ReqWrap { Wrap(object(), this); } - ~FSReqWrap() { ReleaseEarly(); } + ~FSReqWrap() { + ReleaseEarly(); + ClearWrap(object()); + } void* operator new(size_t size) = delete; void* operator new(size_t size, char* storage) { return storage; } @@ -150,6 +153,7 @@ void FSReqWrap::Dispose() { static void NewFSReqWrap(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); + ClearWrap(args.This()); } @@ -1445,6 +1449,7 @@ void InitFs(Local target, Local fst = FunctionTemplate::New(env->isolate(), NewFSReqWrap); fst->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(fst, "getAsyncId", AsyncWrap::GetAsyncId); fst->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "FSReqWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "FSReqWrap"), fst->GetFunction()); diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index 38450148b706a8..f5aa43c7ce31e4 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -784,6 +784,7 @@ void InitHttpParser(Local target, #undef V target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "methods"), methods); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(t, "close", Parser::Close); env->SetProtoMethod(t, "execute", Parser::Execute); env->SetProtoMethod(t, "finish", Parser::Finish); diff --git a/src/node_stat_watcher.cc b/src/node_stat_watcher.cc index 9eeed77476be56..18bf2c54193d7b 100644 --- a/src/node_stat_watcher.cc +++ b/src/node_stat_watcher.cc @@ -49,6 +49,7 @@ void StatWatcher::Initialize(Environment* env, Local target) { t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "StatWatcher")); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(t, "start", StatWatcher::Start); env->SetProtoMethod(t, "stop", StatWatcher::Stop); @@ -66,6 +67,7 @@ StatWatcher::StatWatcher(Environment* env, Local wrap) : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_STATWATCHER), watcher_(new uv_fs_poll_t) { MakeWeak(this); + Wrap(wrap, this); uv_fs_poll_init(env->event_loop(), watcher_); watcher_->data = static_cast(this); } diff --git a/src/node_zlib.cc b/src/node_zlib.cc index 2214f1cd1ecf54..aa701abb7460dc 100644 --- a/src/node_zlib.cc +++ b/src/node_zlib.cc @@ -89,6 +89,7 @@ class ZCtx : public AsyncWrap { refs_(0), gzip_id_bytes_read_(0) { MakeWeak(this); + Wrap(wrap, this); } @@ -679,6 +680,7 @@ void InitZlib(Local target, z->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(z, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(z, "write", ZCtx::Write); env->SetProtoMethod(z, "writeSync", ZCtx::Write); env->SetProtoMethod(z, "init", ZCtx::Init); diff --git a/src/pipe_wrap.cc b/src/pipe_wrap.cc index 132b2662f516f3..8c251f1f741461 100644 --- a/src/pipe_wrap.cc +++ b/src/pipe_wrap.cc @@ -69,6 +69,8 @@ void PipeWrap::Initialize(Local target, t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Pipe")); t->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(t, "close", HandleWrap::Close); env->SetProtoMethod(t, "unref", HandleWrap::Unref); env->SetProtoMethod(t, "ref", HandleWrap::Ref); @@ -95,9 +97,11 @@ void PipeWrap::Initialize(Local target, // Create FunctionTemplate for PipeConnectWrap. auto constructor = [](const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); + ClearWrap(args.This()); }; auto cwt = FunctionTemplate::New(env->isolate(), constructor); cwt->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(cwt, "getAsyncId", AsyncWrap::GetAsyncId); cwt->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "PipeConnectWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "PipeConnectWrap"), cwt->GetFunction()); diff --git a/src/process_wrap.cc b/src/process_wrap.cc index c067d5c9d8a765..725977dac1d929 100644 --- a/src/process_wrap.cc +++ b/src/process_wrap.cc @@ -53,6 +53,8 @@ class ProcessWrap : public HandleWrap { constructor->InstanceTemplate()->SetInternalFieldCount(1); constructor->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Process")); + env->SetProtoMethod(constructor, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(constructor, "close", HandleWrap::Close); env->SetProtoMethod(constructor, "spawn", Spawn); diff --git a/src/req-wrap-inl.h b/src/req-wrap-inl.h index 84af22023dc3b9..e21fb1bdad9363 100644 --- a/src/req-wrap-inl.h +++ b/src/req-wrap-inl.h @@ -30,7 +30,6 @@ template ReqWrap::~ReqWrap() { CHECK_EQ(req_.data, this); // Assert that someone has called Dispatched(). CHECK_EQ(false, persistent().IsEmpty()); - ClearWrap(object()); persistent().Reset(); } diff --git a/src/signal_wrap.cc b/src/signal_wrap.cc index 55f1563438362f..6306fe0c77d374 100644 --- a/src/signal_wrap.cc +++ b/src/signal_wrap.cc @@ -49,6 +49,7 @@ class SignalWrap : public HandleWrap { constructor->InstanceTemplate()->SetInternalFieldCount(1); constructor->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Signal")); + env->SetProtoMethod(constructor, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(constructor, "close", HandleWrap::Close); env->SetProtoMethod(constructor, "ref", HandleWrap::Ref); env->SetProtoMethod(constructor, "unref", HandleWrap::Unref); diff --git a/src/stream_base.h b/src/stream_base.h index 35929750bfbc54..fcae542b7cdc7e 100644 --- a/src/stream_base.h +++ b/src/stream_base.h @@ -53,8 +53,13 @@ class ShutdownWrap : public ReqWrap, Wrap(req_wrap_obj, this); } + ~ShutdownWrap() { + ClearWrap(object()); + } + static void NewShutdownWrap(const v8::FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); + ClearWrap(args.This()); } static ShutdownWrap* from_req(uv_shutdown_t* req) { @@ -85,6 +90,7 @@ class WriteWrap: public ReqWrap, static void NewWriteWrap(const v8::FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); + ClearWrap(args.This()); } static WriteWrap* from_req(uv_write_t* req) { @@ -106,6 +112,10 @@ class WriteWrap: public ReqWrap, Wrap(obj, this); } + ~WriteWrap() { + ClearWrap(object()); + } + void* operator new(size_t size) = delete; void* operator new(size_t size, char* storage) { return storage; } diff --git a/src/stream_wrap.cc b/src/stream_wrap.cc index 099151fdb71c35..6b30ee72de6595 100644 --- a/src/stream_wrap.cc +++ b/src/stream_wrap.cc @@ -63,6 +63,7 @@ void StreamWrap::Initialize(Local target, FunctionTemplate::New(env->isolate(), ShutdownWrap::NewShutdownWrap); sw->InstanceTemplate()->SetInternalFieldCount(1); sw->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "ShutdownWrap")); + env->SetProtoMethod(sw, "getAsyncId", AsyncWrap::GetAsyncId); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "ShutdownWrap"), sw->GetFunction()); @@ -70,6 +71,7 @@ void StreamWrap::Initialize(Local target, FunctionTemplate::New(env->isolate(), WriteWrap::NewWriteWrap); ww->InstanceTemplate()->SetInternalFieldCount(1); ww->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "WriteWrap")); + env->SetProtoMethod(ww, "getAsyncId", AsyncWrap::GetAsyncId); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "WriteWrap"), ww->GetFunction()); env->set_write_wrap_constructor_function(ww->GetFunction()); diff --git a/src/tcp_wrap.cc b/src/tcp_wrap.cc index f2525b1fb1b22a..931b637751e610 100644 --- a/src/tcp_wrap.cc +++ b/src/tcp_wrap.cc @@ -84,6 +84,7 @@ void TCPWrap::Initialize(Local target, "onconnection"), Null(env->isolate())); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(t, "close", HandleWrap::Close); @@ -116,9 +117,11 @@ void TCPWrap::Initialize(Local target, // Create FunctionTemplate for TCPConnectWrap. auto constructor = [](const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); + ClearWrap(args.This()); }; auto cwt = FunctionTemplate::New(env->isolate(), constructor); cwt->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(cwt, "getAsyncId", AsyncWrap::GetAsyncId); cwt->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "TCPConnectWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "TCPConnectWrap"), cwt->GetFunction()); diff --git a/src/timer_wrap.cc b/src/timer_wrap.cc index 8ffe934a21e0fb..b9c75152aa1ca7 100644 --- a/src/timer_wrap.cc +++ b/src/timer_wrap.cc @@ -56,6 +56,8 @@ class TimerWrap : public HandleWrap { env->SetTemplateMethod(constructor, "now", Now); + env->SetProtoMethod(constructor, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(constructor, "close", HandleWrap::Close); env->SetProtoMethod(constructor, "ref", HandleWrap::Ref); env->SetProtoMethod(constructor, "unref", HandleWrap::Unref); diff --git a/src/tls_wrap.cc b/src/tls_wrap.cc index 6f2d0e4c16d576..05349b2f55230d 100644 --- a/src/tls_wrap.cc +++ b/src/tls_wrap.cc @@ -939,6 +939,7 @@ void TLSWrap::Initialize(Local target, t->InstanceTemplate()->SetInternalFieldCount(1); t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "TLSWrap")); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); env->SetProtoMethod(t, "receive", Receive); env->SetProtoMethod(t, "start", Start); env->SetProtoMethod(t, "setVerifyMode", SetVerifyMode); diff --git a/src/tty_wrap.cc b/src/tty_wrap.cc index b6e3efcc100a34..f3f1edfe5d3248 100644 --- a/src/tty_wrap.cc +++ b/src/tty_wrap.cc @@ -53,6 +53,8 @@ void TTYWrap::Initialize(Local target, t->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "TTY")); t->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(t, "close", HandleWrap::Close); env->SetProtoMethod(t, "unref", HandleWrap::Unref); env->SetProtoMethod(t, "ref", HandleWrap::Ref); diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc index 4f5388080e2bfe..fe2b10661fd7c3 100644 --- a/src/udp_wrap.cc +++ b/src/udp_wrap.cc @@ -54,6 +54,7 @@ using v8::Value; class SendWrap : public ReqWrap { public: SendWrap(Environment* env, Local req_wrap_obj, bool have_callback); + ~SendWrap(); inline bool have_callback() const; size_t msg_size; size_t self_size() const override { return sizeof(*this); } @@ -71,6 +72,11 @@ SendWrap::SendWrap(Environment* env, } +SendWrap::~SendWrap() { + ClearWrap(object()); +} + + inline bool SendWrap::have_callback() const { return have_callback_; } @@ -78,6 +84,7 @@ inline bool SendWrap::have_callback() const { static void NewSendWrap(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); + ClearWrap(args.This()); } @@ -129,6 +136,8 @@ void UDPWrap::Initialize(Local target, env->SetProtoMethod(t, "unref", HandleWrap::Unref); env->SetProtoMethod(t, "hasRef", HandleWrap::HasRef); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); + target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "UDP"), t->GetFunction()); env->set_udp_constructor_function(t->GetFunction()); @@ -136,6 +145,7 @@ void UDPWrap::Initialize(Local target, Local swt = FunctionTemplate::New(env->isolate(), NewSendWrap); swt->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(swt, "getAsyncId", AsyncWrap::GetAsyncId); swt->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "SendWrap")); target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "SendWrap"), swt->GetFunction()); diff --git a/test/parallel/test-async-wrap-getasyncid.js b/test/parallel/test-async-wrap-getasyncid.js new file mode 100644 index 00000000000000..ab5a3a6d8d8270 --- /dev/null +++ b/test/parallel/test-async-wrap-getasyncid.js @@ -0,0 +1,208 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); + +function testUninitialized(req, ctor_name) { + assert.strictEqual(typeof req.getAsyncId, 'function'); + assert.strictEqual(req.getAsyncId(), -1); + assert.strictEqual(req.constructor.name, ctor_name); +} + +function testInitialized(req, ctor_name) { + assert.strictEqual(typeof req.getAsyncId, 'function'); + assert(Number.isSafeInteger(req.getAsyncId())); + assert(req.getAsyncId() > 0); + assert.strictEqual(req.constructor.name, ctor_name); +} + + +{ + const cares = process.binding('cares_wrap'); + const dns = require('dns'); + + testUninitialized(new cares.GetAddrInfoReqWrap(), 'GetAddrInfoReqWrap'); + testUninitialized(new cares.GetNameInfoReqWrap(), 'GetNameInfoReqWrap'); + testUninitialized(new cares.QueryReqWrap(), 'QueryReqWrap'); + + testInitialized(dns.lookup('www.google.com', () => {}), 'GetAddrInfoReqWrap'); + testInitialized(dns.lookupService('::1', 22, () => {}), 'GetNameInfoReqWrap'); + testInitialized(dns.resolve6('::1', () => {}), 'QueryReqWrap'); +} + + +{ + const FSEvent = process.binding('fs_event_wrap').FSEvent; + testInitialized(new FSEvent(), 'FSEvent'); +} + + +{ + const JSStream = process.binding('js_stream').JSStream; + testInitialized(new JSStream(), 'JSStream'); +} + + +if (common.hasCrypto) { + const tls = require('tls'); + // SecurePair + testInitialized(tls.createSecurePair().ssl, 'Connection'); +} + + +if (common.hasCrypto) { + const crypto = require('crypto'); + + // The handle for PBKDF2 and RandomBytes isn't returned by the function call, + // so need to check it from the callback. + + const mc = common.mustCall(function pb() { + testInitialized(this, 'PBKDF2'); + }); + crypto.pbkdf2('password', 'salt', 1, 20, 'sha256', mc); + + crypto.randomBytes(1, common.mustCall(function rb() { + testInitialized(this, 'RandomBytes'); + })); +} + + +{ + const binding = process.binding('fs'); + const path = require('path'); + + const FSReqWrap = binding.FSReqWrap; + const req = new FSReqWrap(); + req.oncomplete = () => { }; + + testUninitialized(req, 'FSReqWrap'); + binding.access(path._makeLong('../'), fs.F_OK, req); + testInitialized(req, 'FSReqWrap'); + + const StatWatcher = binding.StatWatcher; + testInitialized(new StatWatcher(), 'StatWatcher'); +} + + +{ + const HTTPParser = process.binding('http_parser').HTTPParser; + testInitialized(new HTTPParser(), 'HTTPParser'); +} + + +{ + const Zlib = process.binding('zlib').Zlib; + const constants = process.binding('constants').zlib; + testInitialized(new Zlib(constants.GZIP), 'Zlib'); +} + + +{ + const binding = process.binding('pipe_wrap'); + const handle = new binding.Pipe(); + testInitialized(handle, 'Pipe'); + const req = new binding.PipeConnectWrap(); + testUninitialized(req, 'PipeConnectWrap'); + req.address = common.PIPE; + req.oncomplete = common.mustCall(() => handle.close()); + handle.connect(req, req.address, req.oncomplete); + testInitialized(req, 'PipeConnectWrap'); +} + + +{ + const Process = process.binding('process_wrap').Process; + testInitialized(new Process(), 'Process'); +} + + +{ + const Signal = process.binding('signal_wrap').Signal; + testInitialized(new Signal(), 'Signal'); +} + + +{ + const binding = process.binding('stream_wrap'); + testUninitialized(new binding.WriteWrap(), 'WriteWrap'); +} + + +{ + const stream_wrap = process.binding('stream_wrap'); + const tcp_wrap = process.binding('tcp_wrap'); + const handle = new tcp_wrap.TCP(); + const req = new tcp_wrap.TCPConnectWrap(); + const sreq = new stream_wrap.ShutdownWrap(); + const wreq = new stream_wrap.WriteWrap(); + testInitialized(handle, 'TCP'); + testUninitialized(req, 'TCPConnectWrap'); + testUninitialized(sreq, 'ShutdownWrap'); + + sreq.oncomplete = common.mustCall(() => handle.close()); + + wreq.handle = handle; + wreq.oncomplete = common.mustCall(() => { + handle.shutdown(sreq); + testInitialized(sreq, 'ShutdownWrap'); + }); + wreq.async = true; + + req.oncomplete = common.mustCall(() => { + const err = handle.writeLatin1String(wreq, 'hi'); + if (err) + throw new Error(`write failed: ${process.binding('uv').errname(err)}`); + testInitialized(wreq, 'WriteWrap'); + }); + req.address = '0.0.0.0'; + req.port = common.PORT; + handle.connect(req, req.address, req.port); + testInitialized(req, 'TCPConnectWrap'); +} + + +{ + const TimerWrap = process.binding('timer_wrap').Timer; + testInitialized(new TimerWrap(), 'Timer'); +} + + +if (common.hasCrypto) { + const TCP = process.binding('tcp_wrap').TCP; + const tcp = new TCP(); + + const ca = fs.readFileSync(common.fixturesDir + '/test_ca.pem', 'ascii'); + const cert = fs.readFileSync(common.fixturesDir + '/test_cert.pem', 'ascii'); + const key = fs.readFileSync(common.fixturesDir + '/test_key.pem', 'ascii'); + const credentials = require('tls').createSecureContext({ ca, cert, key }); + + // TLSWrap is exposed, but needs to be instantiated via tls_wrap.wrap(). + const tls_wrap = process.binding('tls_wrap'); + testInitialized( + tls_wrap.wrap(tcp._externalStream, credentials.context, true), 'TLSWrap'); +} + + +{ + const tty_wrap = process.binding('tty_wrap'); + if (tty_wrap.isTTY(0)) { + testInitialized(new tty_wrap.TTY(0, false), 'TTY'); + } +} + + +{ + const binding = process.binding('udp_wrap'); + const handle = new binding.UDP(); + const req = new binding.SendWrap(); + testInitialized(handle, 'UDP'); + testUninitialized(req, 'SendWrap'); + + handle.bind('0.0.0.0', common.PORT, undefined); + req.address = '127.0.0.1'; + req.port = common.PORT; + req.oncomplete = () => handle.close(); + handle.send(req, [Buffer.alloc(1)], 1, req.port, req.address, true); + testInitialized(req, 'SendWrap'); +} From a556aca2a64f9261a520c9d0d4ebfa3f33bd2f1d Mon Sep 17 00:00:00 2001 From: Trevor Norris Date: Tue, 7 Mar 2017 12:40:18 -0700 Subject: [PATCH 07/12] src: implement native changes for async_hooks Changes in the native code for the upcoming async_hooks module. These have been separated to help with review and testing. Changes include: * Introduce an async id stack that tracks recursive calls into async execution contexts. For performance reasons the id stack is held as a double* and assigned to a Float64Array. If the stack grows too large it is then placed in it's own stack and replaced with a new double*. This should accommodate arbitrarily large stacks. I'm not especially happy with the complexity involved with this async id stack, but it's also the fastest and most full proof way of handling it that I have found. * Add helper functions in Environment and AsyncWrap to work with the async id stack. * Add AsyncWrap::Reset() to allow AsyncWrap instances that have been placed in a resource pool, instead of being released, to be reinitialized. AsyncWrap::AsyncWrap() also now uses Reset() for initialization. * AsyncWrap* parent no longer needs to be passed via the constructor. * Introduce Environment::AsyncHooks class to contain the needed native functionality. This includes the pointer to the async id stack, and array of v8::Eternal's that hold the names of all providers, mechanisms for storing/retrieving the trigger id, etc. * Introduce Environment::AsyncHooks::ExecScope as a way to track the current id and trigger id of function execution via RAII. * If the user passes --abort-on-uncaught-exception then instead of throwing the application will print a stack trace and abort. --- src/async-wrap-inl.h | 12 +- src/async-wrap.cc | 288 ++++++++++++++++++++++++---------------- src/async-wrap.h | 31 ++--- src/connection_wrap.cc | 13 +- src/connection_wrap.h | 3 +- src/env-inl.h | 155 +++++++++++++++++++-- src/env.h | 112 +++++++++++++--- src/handle_wrap.cc | 5 +- src/handle_wrap.h | 3 +- src/js_stream.cc | 17 +-- src/js_stream.h | 2 +- src/node.cc | 72 +++++----- src/node_http_parser.cc | 2 + src/pipe_wrap.cc | 20 +-- src/pipe_wrap.h | 3 +- src/stream_base-inl.h | 3 + src/stream_base.cc | 16 +++ src/stream_wrap.cc | 6 +- src/stream_wrap.h | 3 +- src/tcp_wrap.cc | 25 ++-- src/tcp_wrap.h | 2 +- src/udp_wrap.cc | 23 ++-- src/udp_wrap.h | 2 +- 23 files changed, 526 insertions(+), 292 deletions(-) diff --git a/src/async-wrap-inl.h b/src/async-wrap-inl.h index 8d7ada213d5611..75306a3b0ddfc1 100644 --- a/src/async-wrap-inl.h +++ b/src/async-wrap-inl.h @@ -36,18 +36,18 @@ namespace node { -inline bool AsyncWrap::ran_init_callback() const { - return static_cast(bits_ & 1); +inline AsyncWrap::ProviderType AsyncWrap::provider_type() const { + return provider_type_; } -inline AsyncWrap::ProviderType AsyncWrap::provider_type() const { - return static_cast(bits_ >> 1); +inline double AsyncWrap::get_id() const { + return async_id_; } -inline double AsyncWrap::get_id() const { - return id_; +inline double AsyncWrap::get_trigger_id() const { + return trigger_id_; } diff --git a/src/async-wrap.cc b/src/async-wrap.cc index 11ed67d24207c0..ab3bf5aa74912e 100644 --- a/src/async-wrap.cc +++ b/src/async-wrap.cc @@ -30,13 +30,14 @@ #include "v8.h" #include "v8-profiler.h" -using v8::Boolean; +using v8::Array; +using v8::ArrayBuffer; using v8::Context; +using v8::Float64Array; using v8::Function; using v8::FunctionCallbackInfo; using v8::HandleScope; using v8::HeapProfiler; -using v8::Int32; using v8::Integer; using v8::Isolate; using v8::Local; @@ -45,8 +46,11 @@ using v8::Number; using v8::Object; using v8::RetainedObjectInfo; using v8::TryCatch; +using v8::Uint32Array; using v8::Value; +using AsyncHooks = node::Environment::AsyncHooks; + namespace node { static const char* const provider_names[] = { @@ -57,6 +61,8 @@ static const char* const provider_names[] = { }; +// Report correct information in a heapdump. + class RetainedAsyncInfo: public RetainedObjectInfo { public: explicit RetainedAsyncInfo(uint16_t class_id, AsyncWrap* wrap); @@ -128,55 +134,31 @@ RetainedObjectInfo* WrapperInfo(uint16_t class_id, Local wrapper) { // end RetainedAsyncInfo -static void EnableHooksJS(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Local init_fn = env->async_hooks_init_function(); - if (init_fn.IsEmpty() || !init_fn->IsFunction()) - return env->ThrowTypeError("init callback is not assigned to a function"); - env->async_hooks()->set_enable_callbacks(1); -} - - -static void DisableHooksJS(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - env->async_hooks()->set_enable_callbacks(0); -} - - static void SetupHooks(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - if (env->async_hooks()->callbacks_enabled()) - return env->ThrowError("hooks should not be set while also enabled"); if (!args[0]->IsObject()) return env->ThrowTypeError("first argument must be an object"); + // All of init, before, after, destroy are supplied by async_hooks + // internally, so this should every only be called once. At which time all + // the functions should be set. Detect this by checking if init !IsEmpty(). + CHECK(env->async_hooks_init_function().IsEmpty()); + 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(init_v.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()); +#define SET_HOOK_FN(name) \ + Local name##_v = fn_obj->Get( \ + env->context(), \ + FIXED_ONE_BYTE_STRING(env->isolate(), #name)).ToLocalChecked(); \ + CHECK(name##_v->IsFunction()); \ + env->set_async_hooks_##name##_function(name##_v.As()); + + SET_HOOK_FN(init); + SET_HOOK_FN(before); + SET_HOOK_FN(after); + SET_HOOK_FN(destroy); +#undef SET_HOOK_FN } @@ -196,20 +178,76 @@ void AsyncWrap::Initialize(Local target, HandleScope scope(isolate); env->SetMethod(target, "setupHooks", SetupHooks); - env->SetMethod(target, "disable", DisableHooksJS); - env->SetMethod(target, "enable", EnableHooksJS); + + v8::PropertyAttribute ReadOnlyDontDelete = + static_cast(v8::ReadOnly | v8::DontDelete); + +#define FORCE_SET_TARGET_FIELD(obj, str, field) \ + (obj)->ForceSet(context, \ + FIXED_ONE_BYTE_STRING(isolate, str), \ + field, \ + ReadOnlyDontDelete).FromJust() + + // Attach the uint32_t[] where each slot contains the count of the number of + // callbacks waiting to be called on a particular event. It can then be + // incremented/decremented from JS quickly to communicate to C++ if there are + // any callbacks waiting to be called. + uint32_t* fields_ptr = env->async_hooks()->fields(); + int fields_count = env->async_hooks()->fields_count(); + Local fields_ab = + ArrayBuffer::New(isolate, fields_ptr, fields_count * sizeof(*fields_ptr)); + FORCE_SET_TARGET_FIELD(target, + "async_hook_fields", + Uint32Array::New(fields_ab, 0, fields_count)); + + // The following v8::Float64Array has 5 fields. These fields are shared in + // this way to allow JS and C++ to read/write each value as quickly as + // possible. The fields are represented as follows: + // + // kAsyncUid: Maintains the state of the next unique id to be assigned. + // + // kInitTriggerId: Write the id of the resource responsible for a handle's + // creation just before calling the new handle's constructor. After the new + // handle is constructed kInitTriggerId is set back to 0. + double* uid_fields_ptr = env->async_hooks()->uid_fields(); + int uid_fields_count = env->async_hooks()->uid_fields_count(); + Local uid_fields_ab = ArrayBuffer::New( + isolate, + uid_fields_ptr, + uid_fields_count * sizeof(*uid_fields_ptr)); + FORCE_SET_TARGET_FIELD(target, + "async_uid_fields", + Float64Array::New(uid_fields_ab, 0, uid_fields_count)); + + Local constants = Object::New(isolate); +#define SET_HOOKS_CONSTANT(name) \ + FORCE_SET_TARGET_FIELD( \ + constants, #name, Integer::New(isolate, AsyncHooks::name)); + + SET_HOOKS_CONSTANT(kInit); + SET_HOOKS_CONSTANT(kBefore); + SET_HOOKS_CONSTANT(kAfter); + SET_HOOKS_CONSTANT(kDestroy); + SET_HOOKS_CONSTANT(kCurrentAsyncId); + SET_HOOKS_CONSTANT(kCurrentTriggerId); + SET_HOOKS_CONSTANT(kAsyncUidCntr); + SET_HOOKS_CONSTANT(kInitTriggerId); +#undef SET_HOOKS_CONSTANT + FORCE_SET_TARGET_FIELD(target, "constants", constants); Local async_providers = Object::New(isolate); -#define V(PROVIDER) \ - async_providers->Set(FIXED_ONE_BYTE_STRING(isolate, #PROVIDER), \ - Integer::New(isolate, AsyncWrap::PROVIDER_ ## PROVIDER)); +#define V(p) \ + FORCE_SET_TARGET_FIELD( \ + async_providers, #p, Integer::New(isolate, AsyncWrap::PROVIDER_ ## p)); NODE_ASYNC_PROVIDER_TYPES(V) #undef V - target->Set(FIXED_ONE_BYTE_STRING(isolate, "Providers"), async_providers); + FORCE_SET_TARGET_FIELD(target, "Providers", async_providers); + +#undef FORCE_SET_TARGET_FIELD env->set_async_hooks_init_function(Local()); - env->set_async_hooks_pre_function(Local()); - env->set_async_hooks_post_function(Local()); + env->set_async_hooks_before_function(Local()); + env->set_async_hooks_after_function(Local()); env->set_async_hooks_destroy_function(Local()); } @@ -218,16 +256,11 @@ void AsyncWrap::DestroyIdsCb(uv_idle_t* handle) { uv_idle_stop(handle); Environment* env = Environment::from_destroy_ids_idle_handle(handle); - // None of the V8 calls done outside the HandleScope leak a handle. If this - // changes in the future then the SealHandleScope wrapping the uv_run() - // will catch this can cause the process to abort. + HandleScope handle_scope(env->isolate()); Context::Scope context_scope(env->context()); Local fn = env->async_hooks_destroy_function(); - if (fn.IsEmpty()) - return env->destroy_ids_list()->clear(); - TryCatch try_catch(env->isolate()); std::vector destroy_ids_list; @@ -262,64 +295,63 @@ void LoadAsyncWrapperInfo(Environment* env) { AsyncWrap::AsyncWrap(Environment* env, Local object, - ProviderType provider, - AsyncWrap* parent) - : BaseObject(env, object), bits_(static_cast(provider) << 1), - id_(env->get_async_wrap_uid()) { + ProviderType provider) + : BaseObject(env, object), + provider_type_(provider) { CHECK_NE(provider, PROVIDER_NONE); CHECK_GE(object->InternalFieldCount(), 1); // Shift provider value over to prevent id collision. persistent().SetWrapperClassId(NODE_ASYNC_ID_OFFSET + provider); - Local init_fn = env->async_hooks_init_function(); + // Use AsyncReset() call to execute the init() callbacks. + AsyncReset(); +} - // No init callback exists, no reason to go on. - if (init_fn.IsEmpty()) - return; - // If async wrap callbacks are disabled and no parent was passed that has - // run the init callback then return. - if (!env->async_wrap_callbacks_enabled() && - (parent == nullptr || !parent->ran_init_callback())) +AsyncWrap::~AsyncWrap() { + if (env()->async_hooks()->fields()[AsyncHooks::kDestroy] == 0) { return; + } - HandleScope scope(env->isolate()); - - Local argv[] = { - Number::New(env->isolate(), get_id()), - Int32::New(env->isolate(), provider), - Null(env->isolate()), - Null(env->isolate()) - }; + if (env()->destroy_ids_list()->empty()) + uv_idle_start(env()->destroy_ids_idle_handle(), DestroyIdsCb); - if (parent != nullptr) { - argv[2] = Number::New(env->isolate(), parent->get_id()); - argv[3] = parent->object(); - } + env()->destroy_ids_list()->push_back(get_id()); +} - TryCatch try_catch(env->isolate()); - MaybeLocal ret = - init_fn->Call(env->context(), object, arraysize(argv), argv); +// Generalized call for both the constructor and for handles that are pooled +// and reused over their lifetime. This way a new uid can be assigned when +// the resource is pulled out of the pool and put back into use. +void AsyncWrap::Reset() { + AsyncHooks* async_hooks = env()->async_hooks(); + async_id_ = env()->new_async_id(); + trigger_id_ = env()->get_init_trigger_id(); - if (ret.IsEmpty()) { - ClearFatalExceptionHandlers(env); - FatalException(env->isolate(), try_catch); + // Nothing to execute, so can continue normally. + if (async_hooks->fields()[AsyncHooks::kInit] == 0) { + return; } - bits_ |= 1; // ran_init_callback() is true now. -} - + HandleScope scope(env()->isolate()); + Local init_fn = env()->async_hooks_init_function(); -AsyncWrap::~AsyncWrap() { - if (!ran_init_callback()) - return; + Local argv[] = { + Number::New(env()->isolate(), get_id()), + env()->async_hooks()->provider_string(provider_type()), + object(), + Number::New(env()->isolate(), get_trigger_id()), + }; - if (env()->destroy_ids_list()->empty()) - uv_idle_start(env()->destroy_ids_idle_handle(), DestroyIdsCb); + TryCatch try_catch(env()->isolate()); + MaybeLocal ret = init_fn->Call( + env()->context(), object(), arraysize(argv), argv); - env()->destroy_ids_list()->push_back(get_id()); + if (ret.IsEmpty()) { + ClearFatalExceptionHandlers(env()); + FatalException(env()->isolate(), try_catch); + } } @@ -328,11 +360,10 @@ Local AsyncWrap::MakeCallback(const Local cb, 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_id()); + AsyncHooks* async_hooks = env()->async_hooks(); Local context = object(); Local domain; + Local uid; bool has_domain = false; Environment::AsyncCallbackScope callback_scope(env()); @@ -357,9 +388,15 @@ Local AsyncWrap::MakeCallback(const Local cb, } } - if (ran_init_callback() && !pre_fn.IsEmpty()) { + // Want currentId() to return the correct value from the callbacks. + AsyncHooks::ExecScope exec_scope(env(), get_id(), get_trigger_id()); + + if (async_hooks->fields()[AsyncHooks::kBefore] > 0) { + uid = Number::New(env()->isolate(), get_id()); + Local fn = env()->async_hooks_before_function(); TryCatch try_catch(env()->isolate()); - MaybeLocal ar = pre_fn->Call(env()->context(), context, 1, &uid); + MaybeLocal ar = fn->Call( + env()->context(), Undefined(env()->isolate()), 1, &uid); if (ar.IsEmpty()) { ClearFatalExceptionHandlers(env()); FatalException(env()->isolate(), try_catch); @@ -367,14 +404,23 @@ Local AsyncWrap::MakeCallback(const Local cb, } } - Local ret = cb->Call(context, argc, argv); + // Finally... Get to running the user's callback. + MaybeLocal ret = cb->Call(env()->context(), context, argc, argv); + + Local ret_v; + if (!ret.ToLocal(&ret_v)) { + return Local(); + } - if (ran_init_callback() && !post_fn.IsEmpty()) { - Local did_throw = Boolean::New(env()->isolate(), ret.IsEmpty()); - Local vals[] = { uid, did_throw }; + // If the callback failed then the after() hooks will be called at the end + // of _fatalException(). + if (async_hooks->fields()[AsyncHooks::kAfter] > 0) { + if (uid.IsEmpty()) + uid = Number::New(env()->isolate(), get_id()); + Local fn = env()->async_hooks_after_function(); TryCatch try_catch(env()->isolate()); - MaybeLocal ar = - post_fn->Call(env()->context(), context, arraysize(vals), vals); + MaybeLocal ar = fn->Call( + env()->context(), Undefined(env()->isolate()), 1, &uid); if (ar.IsEmpty()) { ClearFatalExceptionHandlers(env()); FatalException(env()->isolate(), try_catch); @@ -382,9 +428,8 @@ Local AsyncWrap::MakeCallback(const Local cb, } } - if (ret.IsEmpty()) { - return ret; - } + // The execution scope of the id and trigger_id only go this far. + exec_scope.Dispose(); if (has_domain) { Local exit_v = domain->Get(env()->exit_string()); @@ -397,7 +442,7 @@ Local AsyncWrap::MakeCallback(const Local cb, } if (callback_scope.in_makecallback()) { - return ret; + return ret_v; } Environment::TickInfo* tick_info = env()->tick_info(); @@ -406,18 +451,29 @@ Local AsyncWrap::MakeCallback(const Local cb, env()->isolate()->RunMicrotasks(); } + // Make sure the stack unwound properly. If there are nested MakeCallback's + // then it should return early and not reach this code. + CHECK_EQ(env()->current_async_id(), 0); + CHECK_EQ(env()->trigger_id(), 0); + Local process = env()->process_object(); if (tick_info->length() == 0) { tick_info->set_index(0); - return ret; + return ret_v; } - if (env()->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) { - return Local(); - } + MaybeLocal rcheck = + env()->tick_callback_function()->Call(env()->context(), + process, + 0, + nullptr); + + // Make sure the stack unwound properly. + CHECK_EQ(env()->current_async_id(), 0); + CHECK_EQ(env()->trigger_id(), 0); - return ret; + return rcheck.IsEmpty() ? Local() : ret_v; } } // namespace node diff --git a/src/async-wrap.h b/src/async-wrap.h index 7ccae02cced05b..1fe0499468b485 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -76,8 +76,7 @@ class AsyncWrap : public BaseObject { AsyncWrap(Environment* env, v8::Local object, - ProviderType provider, - AsyncWrap* parent = nullptr); + ProviderType provider); virtual ~AsyncWrap(); @@ -93,28 +92,30 @@ class AsyncWrap : public BaseObject { inline double get_id() const; + inline double get_trigger_id() const; + + void Reset(); + // Only call these within a valid HandleScope. + // TODO(trevnorris): These should return a MaybeLocal. v8::Local MakeCallback(const v8::Local cb, - int argc, - v8::Local* argv); + int argc, + v8::Local* argv); inline v8::Local MakeCallback(const v8::Local symbol, - int argc, - v8::Local* argv); + int argc, + v8::Local* argv); inline v8::Local MakeCallback(uint32_t index, - int argc, - v8::Local* argv); + int argc, + v8::Local* argv); virtual size_t self_size() const = 0; private: inline AsyncWrap(); - inline bool ran_init_callback() const; - - // When the async hooks init JS function is called from the constructor it is - // expected the context object will receive a _asyncQueue object property - // that will be used to call pre/post in MakeCallback. - uint32_t bits_; - const double id_; + const ProviderType provider_type_; + // Because the values may be Reset(), cannot be made const. + double async_id_; + double trigger_id_; }; void LoadAsyncWrapperInfo(Environment* env); diff --git a/src/connection_wrap.cc b/src/connection_wrap.cc index 020fe8b4c9508c..da65c493160e93 100644 --- a/src/connection_wrap.cc +++ b/src/connection_wrap.cc @@ -23,13 +23,11 @@ using v8::Value; template ConnectionWrap::ConnectionWrap(Environment* env, Local object, - ProviderType provider, - AsyncWrap* parent) + ProviderType provider) : StreamWrap(env, object, reinterpret_cast(&handle_), - provider, - parent) {} + provider) {} template @@ -53,6 +51,7 @@ void ConnectionWrap::OnConnection(uv_stream_t* handle, }; if (status == 0) { + env->set_init_trigger_id(wrap_data->get_id()); // Instantiate the client javascript object and handle. Local client_obj = WrapType::Instantiate(env, wrap_data); @@ -115,14 +114,12 @@ void ConnectionWrap::AfterConnect(uv_connect_t* req, template ConnectionWrap::ConnectionWrap( Environment* env, Local object, - ProviderType provider, - AsyncWrap* parent); + ProviderType provider); template ConnectionWrap::ConnectionWrap( Environment* env, Local object, - ProviderType provider, - AsyncWrap* parent); + ProviderType provider); template void ConnectionWrap::OnConnection( uv_stream_t* handle, int status); diff --git a/src/connection_wrap.h b/src/connection_wrap.h index 7af97fd3f05e1b..99fe5697ed91fa 100644 --- a/src/connection_wrap.h +++ b/src/connection_wrap.h @@ -22,8 +22,7 @@ class ConnectionWrap : public StreamWrap { protected: ConnectionWrap(Environment* env, v8::Local object, - ProviderType provider, - AsyncWrap* parent); + ProviderType provider); ~ConnectionWrap() { } diff --git a/src/env-inl.h b/src/env-inl.h index 5243b66d58cdcc..c794e6219b7cab 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -80,8 +80,29 @@ inline uint32_t* IsolateData::zero_fill_field() const { return zero_fill_field_; } -inline Environment::AsyncHooks::AsyncHooks() { - for (int i = 0; i < kFieldsCount; i++) fields_[i] = 0; +inline Environment::AsyncHooks::AsyncHooks(v8::Isolate* isolate) + : isolate_(isolate), + fields_(), + uid_fields_() { + v8::HandleScope handle_scope(isolate_); + + // kAsyncUidCntr should start at 1 because that'll be the id the execution + // context during bootstrap (code that runs before entering uv_run()). + uid_fields_[AsyncHooks::kAsyncUidCntr] = 1; + + // Create all the provider strings that will be passed to JS. Place them in + // an array so the array index matches the PROVIDER id offset. This way the + // strings can be retrieved quickly. +#define V(Provider) \ + providers_[AsyncWrap::PROVIDER_ ## Provider].Set( \ + isolate_, \ + v8::String::NewFromOneByte( \ + isolate_, \ + reinterpret_cast(#Provider), \ + v8::NewStringType::kInternalized, \ + sizeof(#Provider) - 1).ToLocalChecked()); + NODE_ASYNC_PROVIDER_TYPES(V) +#undef V } inline uint32_t* Environment::AsyncHooks::fields() { @@ -92,12 +113,94 @@ inline int Environment::AsyncHooks::fields_count() const { return kFieldsCount; } -inline bool Environment::AsyncHooks::callbacks_enabled() { - return fields_[kEnableCallbacks] != 0; +inline double* Environment::AsyncHooks::uid_fields() { + return uid_fields_; +} + +inline int Environment::AsyncHooks::uid_fields_count() const { + return kUidFieldsCount; +} + +inline v8::Local Environment::AsyncHooks::provider_string(int idx) { + return providers_[idx].Get(isolate_); +} + +inline void Environment::AsyncHooks::push_ids(double async_id, + double trigger_id) { + CHECK_GE(async_id, 0); + CHECK_GE(trigger_id, 0); + + ids_stack_.push({ uid_fields_[kCurrentAsyncId], + uid_fields_[kCurrentTriggerId] }); + uid_fields_[kCurrentAsyncId] = async_id; + uid_fields_[kCurrentTriggerId] = trigger_id; +} + +inline bool Environment::AsyncHooks::pop_ids(double async_id) { + // In case of an exception then this may have already been reset, if the + // stack was multiple MakeCallback()'s deep. + if (ids_stack_.empty()) return false; + + // Ask for the async_id to be restored as a sanity check that the stack + // hasn't been corrupted. + if (uid_fields_[kCurrentAsyncId] != async_id) { + fprintf(stderr, + "Error: async hook stack has become corrupted (" + "actual: %'.f, expected: %'.f)\n", + uid_fields_[kCurrentAsyncId], + async_id); + Environment* env = Environment::GetCurrent(isolate_); + DumpBacktrace(stderr); + fflush(stderr); + if (!env->abort_on_uncaught_exception()) + exit(1); + fprintf(stderr, "\n"); + fflush(stderr); + ABORT_NO_BACKTRACE(); + } + + auto ids = ids_stack_.top(); + ids_stack_.pop(); + uid_fields_[kCurrentAsyncId] = ids.async_id; + uid_fields_[kCurrentTriggerId] = ids.trigger_id; + return !ids_stack_.empty(); +} + +inline void Environment::AsyncHooks::clear_id_stack() { + while (!ids_stack_.empty()) + ids_stack_.pop(); + uid_fields_[kCurrentAsyncId] = 0; + uid_fields_[kCurrentTriggerId] = 0; +} + +inline Environment::AsyncHooks::InitScope::InitScope( + Environment* env, double init_trigger_id) + : env_(env), + uid_fields_(env->async_hooks()->uid_fields()) { + env->async_hooks()->push_ids(uid_fields_[AsyncHooks::kCurrentAsyncId], + init_trigger_id); +} + +inline Environment::AsyncHooks::InitScope::~InitScope() { + env_->async_hooks()->pop_ids(uid_fields_[AsyncHooks::kCurrentAsyncId]); } -inline void Environment::AsyncHooks::set_enable_callbacks(uint32_t flag) { - fields_[kEnableCallbacks] = flag; +inline Environment::AsyncHooks::ExecScope::ExecScope( + Environment* env, double async_id, double trigger_id) + : env_(env), + async_id_(async_id), + disposed_(false) { + env->async_hooks()->push_ids(async_id, trigger_id); +} + +inline Environment::AsyncHooks::ExecScope::~ExecScope() { + if (disposed_) return; + Dispose(); +} + +inline void Environment::AsyncHooks::ExecScope::Dispose() { + disposed_ = true; + env_->async_hooks()->pop_ids(async_id_); } inline Environment::AsyncCallbackScope::AsyncCallbackScope(Environment* env) @@ -187,12 +290,13 @@ inline Environment::Environment(IsolateData* isolate_data, v8::Local context) : isolate_(context->GetIsolate()), isolate_data_(isolate_data), + async_hooks_(context->GetIsolate()), timer_base_(uv_now(isolate_data->event_loop())), using_domains_(false), printed_error_(false), trace_sync_io_(false), + abort_on_uncaught_exception_(false), makecallback_cntr_(0), - async_wrap_id_(0), debugger_agent_(this), #if HAVE_INSPECTOR inspector_agent_(this), @@ -254,11 +358,6 @@ inline v8::Isolate* Environment::isolate() const { return isolate_; } -inline bool Environment::async_wrap_callbacks_enabled() const { - // The const_cast is okay, it doesn't violate conceptual const-ness. - return const_cast(this)->async_hooks()->callbacks_enabled(); -} - inline bool Environment::in_domain() const { // The const_cast is okay, it doesn't violate conceptual const-ness. return using_domains() && @@ -337,14 +436,42 @@ inline void Environment::set_trace_sync_io(bool value) { trace_sync_io_ = value; } -inline double Environment::get_async_wrap_uid() { - return ++async_wrap_id_; +inline bool Environment::abort_on_uncaught_exception() const { + return abort_on_uncaught_exception_; +} + +inline void Environment::set_abort_on_uncaught_exception(bool value) { + abort_on_uncaught_exception_ = value; } inline std::vector* Environment::destroy_ids_list() { return &destroy_ids_list_; } +inline double Environment::new_async_id() { + return ++async_hooks()->uid_fields()[AsyncHooks::kAsyncUidCntr]; +} + +inline double Environment::current_async_id() { + return async_hooks()->uid_fields()[AsyncHooks::kCurrentAsyncId]; +} + +inline double Environment::trigger_id() { + return async_hooks()->uid_fields()[AsyncHooks::kCurrentTriggerId]; +} + +inline double Environment::get_init_trigger_id() { + double* uid_fields = async_hooks()->uid_fields(); + double tid = uid_fields[AsyncHooks::kInitTriggerId]; + uid_fields[AsyncHooks::kInitTriggerId] = 0; + if (tid <= 0) tid = current_async_id(); + return tid; +} + +inline void Environment::set_init_trigger_id(const double id) { + async_hooks()->uid_fields()[AsyncHooks::kInitTriggerId] = id; +} + inline double* Environment::heap_statistics_buffer() const { CHECK_NE(heap_statistics_buffer_, nullptr); return heap_statistics_buffer_; diff --git a/src/env.h b/src/env.h index 926e65755c848d..317d153deac90f 100644 --- a/src/env.h +++ b/src/env.h @@ -39,6 +39,7 @@ #include #include #include +#include // Caveat emptor: we're going slightly crazy with macros here but the end // hopefully justifies the means. We have a lot of per-context properties @@ -83,7 +84,6 @@ namespace node { V(address_string, "address") \ V(args_string, "args") \ V(async, "async") \ - V(async_queue_string, "_asyncQueue") \ V(buffer_string, "buffer") \ V(bytes_string, "bytes") \ V(bytes_parsed_string, "bytesParsed") \ @@ -250,8 +250,9 @@ namespace node { V(as_external, v8::External) \ V(async_hooks_destroy_function, v8::Function) \ V(async_hooks_init_function, v8::Function) \ - V(async_hooks_post_function, v8::Function) \ - V(async_hooks_pre_function, v8::Function) \ + V(async_hooks_before_function, v8::Function) \ + V(async_hooks_after_function, v8::Function) \ + V(async_hooks_fatal_error_function, v8::Function) \ V(binding_cache_object, v8::Object) \ V(buffer_constructor_function, v8::Function) \ V(buffer_prototype_object, v8::Object) \ @@ -287,6 +288,11 @@ struct node_ares_task { RB_ENTRY(node_ares_task) node; }; +struct node_async_ids { + double async_id; + double trigger_id; +}; + RB_HEAD(node_ares_task_list, node_ares_task); class IsolateData { @@ -327,31 +333,96 @@ class Environment { public: class AsyncHooks { public: + // Reason for both UidFields and Fields are that one is stored as a double* + // and the other as a uint32_t*. + enum Fields { + kInit, + kBefore, + kAfter, + kDestroy, + kFieldsCount, + }; + + enum UidFields { + kCurrentAsyncId, + kCurrentTriggerId, + kAsyncUidCntr, + kInitTriggerId, + kUidFieldsCount, + }; + + AsyncHooks() = delete; + inline uint32_t* fields(); inline int fields_count() const; - inline bool callbacks_enabled(); - inline void set_enable_callbacks(uint32_t flag); - - private: - friend class Environment; // So we can call the constructor. - inline AsyncHooks(); + inline double* uid_fields(); + inline int uid_fields_count() const; + inline v8::Local provider_string(int idx); + + inline void push_ids(double async_id, double trigger_id); + inline bool pop_ids(double async_id); + inline void clear_id_stack(); // Used in fatal exceptions. + + // Used to propagate the trigger_id to the constructor of any newly created + // resources using RAII. Instead of needing to pass the trigger_id along + // with other constructor arguments. + class InitScope { + public: + InitScope() = delete; + explicit InitScope(Environment* env, double init_trigger_id); + ~InitScope(); + + private: + Environment* env_; + double* uid_fields_; + + DISALLOW_COPY_AND_ASSIGN(InitScope); + }; - enum Fields { - // Set this to not zero if the init hook should be called. - kEnableCallbacks, - kFieldsCount + // Used to manage the stack of async and trigger ids as calls are made into + // JS. Mainly used in MakeCallback(). + class ExecScope { + public: + ExecScope() = delete; + explicit ExecScope(Environment* env, double async_id, double trigger_id); + ~ExecScope(); + void Dispose(); + + private: + Environment* env_; + double async_id_; + // Manually track if the destructor has run so it isn't accidentally run + // twice on RAII cleanup. + bool disposed_; + + DISALLOW_COPY_AND_ASSIGN(ExecScope); }; + private: + friend class Environment; // So we can call the constructor. + inline explicit AsyncHooks(v8::Isolate* isolate); + // Keep a list of all Persistent strings used for Provider types. + v8::Eternal providers_[AsyncWrap::PROVIDERS_LENGTH]; + // Used by provider_string(). + v8::Isolate* isolate_; + // Stores the ids of the current execution context stack. + std::stack ids_stack_; + // Used to communicate state between C++ and JS cheaply. Is placed in an + // Uint32Array() and attached to the async_wrap object. uint32_t fields_[kFieldsCount]; + // Used to communicate ids between C++ and JS cheaply. Placed in a + // Float64Array and attached to the async_wrap object. Using a double only + // gives us 2^53-1 unique ids, but that should be sufficient. + double uid_fields_[kUidFieldsCount]; DISALLOW_COPY_AND_ASSIGN(AsyncHooks); }; class AsyncCallbackScope { public: + AsyncCallbackScope() = delete; explicit AsyncCallbackScope(Environment* env); ~AsyncCallbackScope(); - inline bool in_makecallback(); private: @@ -447,7 +518,6 @@ class Environment { inline v8::Isolate* isolate() const; inline uv_loop_t* event_loop() const; - inline bool async_wrap_callbacks_enabled() const; inline bool in_domain() const; inline uint32_t watched_providers() const; @@ -484,7 +554,15 @@ class Environment { void PrintSyncTrace() const; inline void set_trace_sync_io(bool value); - inline double get_async_wrap_uid(); + inline bool abort_on_uncaught_exception() const; + inline void set_abort_on_uncaught_exception(bool value); + + // The necessary API for async_hooks. + inline double new_async_id(); + inline double current_async_id(); + inline double trigger_id(); + inline double get_init_trigger_id(); + inline void set_init_trigger_id(const double id); // List of id's that have been destroyed and need the destroy() cb called. inline std::vector* destroy_ids_list(); @@ -591,8 +669,8 @@ class Environment { bool using_domains_; bool printed_error_; bool trace_sync_io_; + bool abort_on_uncaught_exception_; size_t makecallback_cntr_; - double async_wrap_id_; std::vector destroy_ids_list_; debugger::Agent debugger_agent_; #if HAVE_INSPECTOR diff --git a/src/handle_wrap.cc b/src/handle_wrap.cc index da65586a7edbdf..7d0925e2fd6354 100644 --- a/src/handle_wrap.cc +++ b/src/handle_wrap.cc @@ -90,9 +90,8 @@ void HandleWrap::Close(const FunctionCallbackInfo& args) { HandleWrap::HandleWrap(Environment* env, Local object, uv_handle_t* handle, - AsyncWrap::ProviderType provider, - AsyncWrap* parent) - : AsyncWrap(env, object, provider, parent), + AsyncWrap::ProviderType provider) + : AsyncWrap(env, object, provider), state_(kInitialized), handle_(handle) { handle_->data = this; diff --git a/src/handle_wrap.h b/src/handle_wrap.h index 280d60815e3b52..f8be356e1a730c 100644 --- a/src/handle_wrap.h +++ b/src/handle_wrap.h @@ -74,8 +74,7 @@ class HandleWrap : public AsyncWrap { HandleWrap(Environment* env, v8::Local object, uv_handle_t* handle, - AsyncWrap::ProviderType provider, - AsyncWrap* parent = nullptr); + AsyncWrap::ProviderType provider); ~HandleWrap() override; private: diff --git a/src/js_stream.cc b/src/js_stream.cc index 1d20e1c6d77dfb..2644a6a451a00f 100644 --- a/src/js_stream.cc +++ b/src/js_stream.cc @@ -12,7 +12,6 @@ namespace node { using v8::Array; using v8::Context; -using v8::External; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::HandleScope; @@ -21,8 +20,8 @@ using v8::Object; using v8::Value; -JSStream::JSStream(Environment* env, Local obj, AsyncWrap* parent) - : AsyncWrap(env, obj, AsyncWrap::PROVIDER_JSSTREAM, parent), +JSStream::JSStream(Environment* env, Local obj) + : AsyncWrap(env, obj, AsyncWrap::PROVIDER_JSSTREAM), StreamBase(env) { node::Wrap(obj, this); MakeWeak(this); @@ -115,17 +114,7 @@ void JSStream::New(const FunctionCallbackInfo& args) { // normal function. CHECK(args.IsConstructCall()); Environment* env = Environment::GetCurrent(args); - JSStream* wrap; - - if (args.Length() == 0) { - wrap = new JSStream(env, args.This(), nullptr); - } else if (args[0]->IsExternal()) { - void* ptr = args[0].As()->Value(); - wrap = new JSStream(env, args.This(), static_cast(ptr)); - } else { - UNREACHABLE(); - } - CHECK(wrap); + new JSStream(env, args.This()); } diff --git a/src/js_stream.h b/src/js_stream.h index 5a1244bc463e36..fc0b7abe15a633 100644 --- a/src/js_stream.h +++ b/src/js_stream.h @@ -33,7 +33,7 @@ class JSStream : public AsyncWrap, public StreamBase { size_t self_size() const override { return sizeof(*this); } protected: - JSStream(Environment* env, v8::Local obj, AsyncWrap* parent); + JSStream(Environment* env, v8::Local obj); AsyncWrap* GetAsyncWrap() override; diff --git a/src/node.cc b/src/node.cc index c241f734b28901..12e6968e688f6d 100644 --- a/src/node.cc +++ b/src/node.cc @@ -153,6 +153,8 @@ using v8::Uint32Array; using v8::V8; using v8::Value; +using AsyncHooks = node::Environment::AsyncHooks; + static bool print_eval = false; static bool force_repl = false; static bool syntax_check_only = false; @@ -173,6 +175,7 @@ static node_module* modlist_linked; static node_module* modlist_addon; static bool trace_enabled = false; static std::string trace_enabled_categories; // NOLINT(runtime/string) +static bool abort_on_uncaught_exception = false; #if defined(NODE_HAVE_I18N_SUPPORT) // Path to ICU data (for i18n / Intl) @@ -1229,21 +1232,13 @@ Local MakeCallback(Environment* env, // If you hit this assertion, you forgot to enter the v8::Context first. CHECK_EQ(env->context(), env->isolate()->GetCurrentContext()); - Local pre_fn = env->async_hooks_pre_function(); - Local post_fn = env->async_hooks_post_function(); Local object, domain; - 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()) { object = recv.As(); - Local async_queue_v = object->Get(env->async_queue_string()); - if (async_queue_v->IsObject()) - ran_init_callback = true; } if (env->using_domains()) { @@ -1267,34 +1262,13 @@ Local MakeCallback(Environment* env, } } - if (ran_init_callback && !pre_fn.IsEmpty()) { - 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(); - } - } + // TODO(trevnorris): Correct this once node::MakeCallback() support id and + // triggerId. Consider completely removing it until then so the async id can + // propagate through to the fatalException after hook calls. + AsyncHooks::ExecScope exec_scope(env, 0, 0); Local ret = callback->Call(recv, argc, argv); - if (ran_init_callback && !post_fn.IsEmpty()) { - 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. @@ -1302,6 +1276,8 @@ Local MakeCallback(Environment* env, ret : Undefined(env->isolate()).As(); } + exec_scope.Dispose(); + if (has_domain) { Local exit_v = domain->Get(env->exit_string()); if (exit_v->IsFunction()) { @@ -1322,6 +1298,11 @@ Local MakeCallback(Environment* env, env->isolate()->RunMicrotasks(); } + // Make sure the stack unwound properly. If there are nested MakeCallback's + // then it should return early and not reach this code. + CHECK_EQ(env->current_async_id(), 0); + CHECK_EQ(env->trigger_id(), 0); + Local process = env->process_object(); if (tick_info->length() == 0) { @@ -1338,10 +1319,10 @@ Local MakeCallback(Environment* env, Local MakeCallback(Environment* env, - Local recv, - Local symbol, - int argc, - Local argv[]) { + Local recv, + Local symbol, + int argc, + Local argv[]) { Local cb_v = recv->Get(symbol); CHECK(cb_v->IsFunction()); return MakeCallback(env, recv.As(), cb_v.As(), argc, argv); @@ -1349,10 +1330,10 @@ Local MakeCallback(Environment* env, Local MakeCallback(Environment* env, - Local recv, - const char* method, - int argc, - Local argv[]) { + Local recv, + const char* method, + int argc, + Local argv[]) { Local method_string = OneByteString(env->isolate(), method); return MakeCallback(env, recv, method_string, argc, argv); } @@ -3788,6 +3769,12 @@ static void ParseArgs(int* argc, } else if (strcmp(arg, "--") == 0) { index += 1; break; + } else if (strcmp(arg, "--abort-on-uncaught-exception") || + strcmp(arg, "--abort_on_uncaught_exception")) { + abort_on_uncaught_exception = true; + // Also a V8 option. Pass through as-is. + new_v8_argv[new_v8_argc] = arg; + new_v8_argc += 1; } else { // V8 option. Pass through as-is. new_v8_argv[new_v8_argc] = arg; @@ -4358,8 +4345,11 @@ inline int Start(Isolate* isolate, IsolateData* isolate_data, if (debugger_enabled && !debugger_running) return 12; // Signal internal error. + env.set_abort_on_uncaught_exception(abort_on_uncaught_exception); + { Environment::AsyncCallbackScope callback_scope(&env); + Environment::AsyncHooks::ExecScope exec_scope(&env, 1, 0); LoadEnvironment(&env); } diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index f5aa43c7ce31e4..f7e8c18658c355 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -475,6 +475,8 @@ class Parser : public AsyncWrap { ASSIGN_OR_RETURN_UNWRAP(&parser, args.Holder()); // Should always be called from the same context. CHECK_EQ(env, parser->env()); + // The parser is being reused. Reset the uid and call init() callbacks. + parser->Reset(); parser->Init(type); } diff --git a/src/pipe_wrap.cc b/src/pipe_wrap.cc index 8c251f1f741461..2185580b0662e8 100644 --- a/src/pipe_wrap.cc +++ b/src/pipe_wrap.cc @@ -38,7 +38,6 @@ namespace node { using v8::Context; using v8::EscapableHandleScope; -using v8::External; using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; @@ -47,15 +46,17 @@ using v8::Local; using v8::Object; using v8::Value; +using AsyncHooks = Environment::AsyncHooks; + Local PipeWrap::Instantiate(Environment* env, AsyncWrap* parent) { EscapableHandleScope handle_scope(env->isolate()); + AsyncHooks::InitScope init_scope(env, parent->get_id()); CHECK_EQ(false, env->pipe_constructor_template().IsEmpty()); Local constructor = env->pipe_constructor_template()->GetFunction(); CHECK_EQ(false, constructor.IsEmpty()); - Local ptr = External::New(env->isolate(), parent); Local instance = - constructor->NewInstance(env->context(), 1, &ptr).ToLocalChecked(); + constructor->NewInstance(env->context()).ToLocalChecked(); return handle_scope.Escape(instance); } @@ -114,23 +115,16 @@ void PipeWrap::New(const FunctionCallbackInfo& args) { // normal function. CHECK(args.IsConstructCall()); Environment* env = Environment::GetCurrent(args); - if (args[0]->IsExternal()) { - void* ptr = args[0].As()->Value(); - new PipeWrap(env, args.This(), false, static_cast(ptr)); - } else { - new PipeWrap(env, args.This(), args[0]->IsTrue(), nullptr); - } + new PipeWrap(env, args.This(), args[0]->IsTrue()); } PipeWrap::PipeWrap(Environment* env, Local object, - bool ipc, - AsyncWrap* parent) + bool ipc) : ConnectionWrap(env, object, - AsyncWrap::PROVIDER_PIPEWRAP, - parent) { + AsyncWrap::PROVIDER_PIPEWRAP) { int r = uv_pipe_init(env->event_loop(), &handle_, ipc); CHECK_EQ(r, 0); // How do we proxy this error up to javascript? // Suggestion: uv_pipe_init() returns void. diff --git a/src/pipe_wrap.h b/src/pipe_wrap.h index 5ad6a9be1b2644..6db7f4561cb522 100644 --- a/src/pipe_wrap.h +++ b/src/pipe_wrap.h @@ -42,8 +42,7 @@ class PipeWrap : public ConnectionWrap { private: PipeWrap(Environment* env, v8::Local object, - bool ipc, - AsyncWrap* parent); + bool ipc); static void New(const v8::FunctionCallbackInfo& args); static void Bind(const v8::FunctionCallbackInfo& args); diff --git a/src/stream_base-inl.h b/src/stream_base-inl.h index da636909b695f3..8b5b15420703ef 100644 --- a/src/stream_base-inl.h +++ b/src/stream_base-inl.h @@ -23,6 +23,8 @@ using v8::PropertyCallbackInfo; using v8::String; using v8::Value; +using AsyncHooks = Environment::AsyncHooks; + template void StreamBase::AddMethods(Environment* env, Local t, @@ -134,6 +136,7 @@ void StreamBase::JSMethod(const FunctionCallbackInfo& args) { if (!wrap->IsAlive()) return args.GetReturnValue().Set(UV_EINVAL); + AsyncHooks::InitScope init_scope(handle->env(), handle->get_id()); args.GetReturnValue().Set((wrap->*Method)(args)); } diff --git a/src/stream_base.cc b/src/stream_base.cc index 3ed622d7ef35a2..e815dd84db33b4 100644 --- a/src/stream_base.cc +++ b/src/stream_base.cc @@ -53,6 +53,9 @@ int StreamBase::Shutdown(const FunctionCallbackInfo& args) { CHECK(args[0]->IsObject()); Local req_wrap_obj = args[0].As(); + AsyncWrap* wrap = GetAsyncWrap(); + if (wrap != nullptr) + env->set_init_trigger_id(wrap->get_id()); ShutdownWrap* req_wrap = new ShutdownWrap(env, req_wrap_obj, this, @@ -129,6 +132,11 @@ int StreamBase::Writev(const FunctionCallbackInfo& args) { if (storage_size > INT_MAX) return UV_ENOBUFS; + AsyncWrap* wrap = GetAsyncWrap(); + // NOTE: All tests show that GetAsyncWrap() never returns nullptr here. If it + // can then replace the CHECK_NE() with if (wrap != nullptr). + CHECK_NE(wrap, nullptr); + env->set_init_trigger_id(wrap->get_id()); WriteWrap* req_wrap = WriteWrap::New(env, req_wrap_obj, this, @@ -196,6 +204,7 @@ int StreamBase::WriteBuffer(const FunctionCallbackInfo& args) { const char* data = Buffer::Data(args[1]); size_t length = Buffer::Length(args[1]); + AsyncWrap* wrap; WriteWrap* req_wrap; uv_buf_t buf; buf.base = const_cast(data); @@ -211,6 +220,9 @@ int StreamBase::WriteBuffer(const FunctionCallbackInfo& args) { goto done; CHECK_EQ(count, 1); + wrap = GetAsyncWrap(); + if (wrap != nullptr) + env->set_init_trigger_id(wrap->get_id()); // Allocate, or write rest req_wrap = WriteWrap::New(env, req_wrap_obj, this, AfterWrite); @@ -242,6 +254,7 @@ int StreamBase::WriteString(const FunctionCallbackInfo& args) { Local req_wrap_obj = args[0].As(); Local string = args[1].As(); Local send_handle_obj; + AsyncWrap* wrap; if (args[2]->IsObject()) send_handle_obj = args[2].As(); @@ -292,6 +305,9 @@ int StreamBase::WriteString(const FunctionCallbackInfo& args) { CHECK_EQ(count, 1); } + wrap = GetAsyncWrap(); + if (wrap != nullptr) + env->set_init_trigger_id(wrap->get_id()); req_wrap = WriteWrap::New(env, req_wrap_obj, this, AfterWrite, storage_size); data = req_wrap->Extra(); diff --git a/src/stream_wrap.cc b/src/stream_wrap.cc index 6b30ee72de6595..8735e6a7eab06a 100644 --- a/src/stream_wrap.cc +++ b/src/stream_wrap.cc @@ -81,13 +81,11 @@ void StreamWrap::Initialize(Local target, StreamWrap::StreamWrap(Environment* env, Local object, uv_stream_t* stream, - AsyncWrap::ProviderType provider, - AsyncWrap* parent) + AsyncWrap::ProviderType provider) : HandleWrap(env, object, reinterpret_cast(stream), - provider, - parent), + provider), StreamBase(env), stream_(stream) { set_after_write_cb({ OnAfterWriteImpl, this }); diff --git a/src/stream_wrap.h b/src/stream_wrap.h index 14ff18e7f3930b..161bcd550f65f1 100644 --- a/src/stream_wrap.h +++ b/src/stream_wrap.h @@ -81,8 +81,7 @@ class StreamWrap : public HandleWrap, public StreamBase { StreamWrap(Environment* env, v8::Local object, uv_stream_t* stream, - AsyncWrap::ProviderType provider, - AsyncWrap* parent = nullptr); + AsyncWrap::ProviderType provider); ~StreamWrap() { } diff --git a/src/tcp_wrap.cc b/src/tcp_wrap.cc index 931b637751e610..d1bf4a952a1f7c 100644 --- a/src/tcp_wrap.cc +++ b/src/tcp_wrap.cc @@ -40,7 +40,6 @@ namespace node { using v8::Boolean; using v8::Context; using v8::EscapableHandleScope; -using v8::External; using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; @@ -51,15 +50,17 @@ using v8::Object; using v8::String; using v8::Value; +using AsyncHooks = Environment::AsyncHooks; + Local TCPWrap::Instantiate(Environment* env, AsyncWrap* parent) { EscapableHandleScope handle_scope(env->isolate()); + AsyncHooks::InitScope init_scope(env, parent->get_id()); CHECK_EQ(env->tcp_constructor_template().IsEmpty(), false); Local constructor = env->tcp_constructor_template()->GetFunction(); CHECK_EQ(constructor.IsEmpty(), false); - Local ptr = External::New(env->isolate(), parent); Local instance = - constructor->NewInstance(env->context(), 1, &ptr).ToLocalChecked(); + constructor->NewInstance(env->context()).ToLocalChecked(); return handle_scope.Escape(instance); } @@ -134,24 +135,14 @@ void TCPWrap::New(const FunctionCallbackInfo& args) { // normal function. CHECK(args.IsConstructCall()); Environment* env = Environment::GetCurrent(args); - TCPWrap* wrap; - if (args.Length() == 0) { - wrap = new TCPWrap(env, args.This(), nullptr); - } else if (args[0]->IsExternal()) { - void* ptr = args[0].As()->Value(); - wrap = new TCPWrap(env, args.This(), static_cast(ptr)); - } else { - UNREACHABLE(); - } - CHECK(wrap); + new TCPWrap(env, args.This()); } -TCPWrap::TCPWrap(Environment* env, Local object, AsyncWrap* parent) +TCPWrap::TCPWrap(Environment* env, Local object) : ConnectionWrap(env, object, - AsyncWrap::PROVIDER_TCPWRAP, - parent) { + AsyncWrap::PROVIDER_TCPWRAP) { int r = uv_tcp_init(env->event_loop(), &handle_); CHECK_EQ(r, 0); // How do we proxy this error up to javascript? // Suggestion: uv_tcp_init() returns void. @@ -279,6 +270,7 @@ void TCPWrap::Connect(const FunctionCallbackInfo& args) { int err = uv_ip4_addr(*ip_address, port, &addr); if (err == 0) { + env->set_init_trigger_id(wrap->get_id()); ConnectWrap* req_wrap = new ConnectWrap(env, req_wrap_obj, AsyncWrap::PROVIDER_TCPCONNECTWRAP); err = uv_tcp_connect(req_wrap->req(), @@ -314,6 +306,7 @@ void TCPWrap::Connect6(const FunctionCallbackInfo& args) { int err = uv_ip6_addr(*ip_address, port, &addr); if (err == 0) { + env->set_init_trigger_id(wrap->get_id()); ConnectWrap* req_wrap = new ConnectWrap(env, req_wrap_obj, AsyncWrap::PROVIDER_TCPCONNECTWRAP); err = uv_tcp_connect(req_wrap->req(), diff --git a/src/tcp_wrap.h b/src/tcp_wrap.h index a3cb2d524b82a9..95c0b1c1e5b99e 100644 --- a/src/tcp_wrap.h +++ b/src/tcp_wrap.h @@ -46,7 +46,7 @@ class TCPWrap : public ConnectionWrap { int (*F)(const typename T::HandleType*, sockaddr*, int*)> friend void GetSockOrPeerName(const v8::FunctionCallbackInfo&); - TCPWrap(Environment* env, v8::Local object, AsyncWrap* parent); + TCPWrap(Environment* env, v8::Local object); ~TCPWrap(); static void New(const v8::FunctionCallbackInfo& args); diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc index fe2b10661fd7c3..c192de6d628cec 100644 --- a/src/udp_wrap.cc +++ b/src/udp_wrap.cc @@ -37,7 +37,6 @@ namespace node { using v8::Array; using v8::Context; using v8::EscapableHandleScope; -using v8::External; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::HandleScope; @@ -50,6 +49,8 @@ using v8::String; using v8::Undefined; using v8::Value; +using AsyncHooks = Environment::AsyncHooks; + class SendWrap : public ReqWrap { public: @@ -88,7 +89,7 @@ static void NewSendWrap(const FunctionCallbackInfo& args) { } -UDPWrap::UDPWrap(Environment* env, Local object, AsyncWrap* parent) +UDPWrap::UDPWrap(Environment* env, Local object) : HandleWrap(env, object, reinterpret_cast(&handle_), @@ -155,15 +156,7 @@ void UDPWrap::Initialize(Local target, void UDPWrap::New(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); Environment* env = Environment::GetCurrent(args); - if (args.Length() == 0) { - new UDPWrap(env, args.This(), nullptr); - } else if (args[0]->IsExternal()) { - new UDPWrap(env, - args.This(), - static_cast(args[0].As()->Value())); - } else { - UNREACHABLE(); - } + new UDPWrap(env, args.This()); } @@ -303,6 +296,7 @@ void UDPWrap::DoSend(const FunctionCallbackInfo& args, int family) { node::Utf8Value address(env->isolate(), args[4]); const bool have_callback = args[5]->IsTrue(); + env->set_init_trigger_id(wrap->get_id()); SendWrap* req_wrap = new SendWrap(env, req_wrap_obj, have_callback); size_t msg_size = 0; @@ -450,11 +444,12 @@ void UDPWrap::OnRecv(uv_udp_t* handle, Local UDPWrap::Instantiate(Environment* env, AsyncWrap* parent) { EscapableHandleScope scope(env->isolate()); + AsyncHooks::InitScope init_scope(env, parent->get_id()); // If this assert fires then Initialize hasn't been called yet. CHECK_EQ(env->udp_constructor_function().IsEmpty(), false); - Local ptr = External::New(env->isolate(), parent); - return scope.Escape(env->udp_constructor_function() - ->NewInstance(env->context(), 1, &ptr).ToLocalChecked()); + Local instance = env->udp_constructor_function() + ->NewInstance(env->context()).ToLocalChecked(); + return scope.Escape(instance); } diff --git a/src/udp_wrap.h b/src/udp_wrap.h index 60bedace7410df..c8913d5da2e107 100644 --- a/src/udp_wrap.h +++ b/src/udp_wrap.h @@ -68,7 +68,7 @@ class UDPWrap: public HandleWrap { int (*F)(const typename T::HandleType*, sockaddr*, int*)> friend void GetSockOrPeerName(const v8::FunctionCallbackInfo&); - UDPWrap(Environment* env, v8::Local object, AsyncWrap* parent); + UDPWrap(Environment* env, v8::Local object); static void DoBind(const v8::FunctionCallbackInfo& args, int family); From 0228da61195d0c0bfc592d5dea0b2a5ae1fb1251 Mon Sep 17 00:00:00 2001 From: Trevor Norris Date: Thu, 9 Mar 2017 16:13:34 -0700 Subject: [PATCH 08/12] async_hooks: initial async_hooks implementation Fill this commit messsage with more details about the change once all changes are rebased. * Add lib/async_hooks.js * Add JS methods to AsyncWrap for handling the async id stack * Introduce AsyncReset() so that JS functions can reset the id and again trigger the init hooks, allow AsyncWrap::Reset() to be called from JS via asyncReset(). * Add env variable to test additional things in test/common.js --- lib/async_hooks.js | 488 ++++++++++++++++++++ lib/internal/module.js | 8 +- node.gyp | 1 + src/async-wrap.cc | 124 +++-- src/async-wrap.h | 9 +- src/node_http_parser.cc | 2 +- src/tcp_wrap.cc | 1 + test/common.js | 60 +++ test/parallel/test-async-wrap-destroyid.js | 37 ++ test/parallel/test-async-wrap-getasyncid.js | 42 +- 10 files changed, 721 insertions(+), 51 deletions(-) create mode 100644 lib/async_hooks.js create mode 100644 test/parallel/test-async-wrap-destroyid.js diff --git a/lib/async_hooks.js b/lib/async_hooks.js new file mode 100644 index 00000000000000..736b189097672c --- /dev/null +++ b/lib/async_hooks.js @@ -0,0 +1,488 @@ +'use strict'; + +const async_wrap = process.binding('async_wrap'); +/* Both these arrays are used to communicate between JS and C++ with as little + * overhead as possible. + * + * async_hook_fields is a Uint32Array() that communicates the number of each + * type of active hooks of each type and wraps the uin32_t array of + * node::Environment::AsyncHooks::fields_. + * + * async_uid_fields is a Float64Array() that contains the async/trigger ids for + * several operations. These fields are as follows: + * kCurrentAsyncId: The async id of the current execution stack. + * kCurrentTriggerId: The trigger id of the current execution stack. + * kAsyncUidCntr: Counter that tracks the unique ids given to new resources. + * kInitTriggerId: Written to just before creating a new resource, so the + * constructor knows what other resource is responsible for its init(). + * Used this way so the trigger id doesn't need to be passed to every + * resource's constructor. + */ +const { async_hook_fields, async_uid_fields } = async_wrap; +// Used to change the state of the async id stack. +const { pushAsyncIds, popAsyncIds } = async_wrap; +// Array of all AsyncHooks that will be iterated whenever an async event fires. +// Using var instead of (preferably const) in order to assign +// tmp_active_hooks_array if a hook is enabled/disabled during hook execution. +var active_hooks_array = []; +// Track whether a hook callback is currently being processed. Used to make +// sure active_hooks_array isn't altered in mid execution if another hook is +// added or removed. +var processing_hook = false; +// Use to temporarily store and updated active_hooks_array if the user enables +// or disables a hook while hooks are being processed. +var tmp_active_hooks_array = null; +// Keep track of the field counds held in tmp_active_hooks_array. +var tmp_async_hook_fields = null; + +// Each constant tracks how many callbacks there are for any given step of +// async execution. These are tracked so if the user didn't include callbacks +// for a given step, that step can bail out early. +const { kInit, kBefore, kAfter, kDestroy, kCurrentAsyncId, kCurrentTriggerId, + kAsyncUidCntr, kInitTriggerId } = async_wrap.constants; + +// Used in AsyncHook and AsyncEvent. +const async_id_symbol = Symbol('_asyncId'); +const trigger_id_symbol = Symbol('_triggerId'); +const init_symbol = Symbol('init'); +const before_symbol = Symbol('before'); +const after_symbol = Symbol('after'); +const destroy_symbol = Symbol('destroy'); + +// Setup the callbacks that node::AsyncWrap will call when there are hooks to +// process. They use the same functions as the JS embedder API. +async_wrap.setupHooks({ init, + before: emitBeforeN, + after: emitAfterN, + destroy: emitDestroyN }); + +// Used to fatally abort the process if a callback throws. +function fatalError(e) { + if (typeof e.stack === 'string') { + process._rawDebug(e.stack); + } else { + const o = { message: e }; + Error.captureStackTrace(o, fatalError); + process._rawDebug(o.stack); + } + if (process.execArgv.some( + (e) => /^--abort[_-]on[_-]uncaught[_-]exception$/.test(e))) { + process.abort(); + } + process.exit(1); +} + + +// Public API // + +class AsyncHook { + constructor({ init, before, after, destroy }) { + if (init && typeof init !== 'function') + throw new TypeError('init must be a function'); + if (before && typeof before !== 'function') + throw new TypeError('before must be a function'); + if (after && typeof after !== 'function') + throw new TypeError('after must be a function'); + if (destroy && typeof destroy !== 'function') + throw new TypeError('destroy must be a function'); + + this[init_symbol] = init; + this[before_symbol] = before; + this[after_symbol] = after; + this[destroy_symbol] = destroy; + } + + enable() { + // The set of callbacks for a hook should be the same regardless of whether + // enable()/disable() are run during their execution. The following + // references are reassigned to the tmp arrays if a hook is currently being + // processed. + const [hooks_array, hook_fields] = getHookArrays(); + + // Each hook is only allowed to be added once. + if (hooks_array.includes(this)) + return; + + // createHook() has already enforced that the callbacks are all functions, + // so here simply increment the count of whether each callbacks exists or + // not. + hook_fields[kInit] += +!!this[init_symbol]; + hook_fields[kBefore] += +!!this[before_symbol]; + hook_fields[kAfter] += +!!this[after_symbol]; + hook_fields[kDestroy] += +!!this[destroy_symbol]; + hooks_array.push(this); + return this; + } + + disable() { + const [hooks_array, hook_fields] = getHookArrays(); + + const index = hooks_array.indexOf(this); + if (index === -1) + return; + + hook_fields[kInit] -= +!!this[init_symbol]; + hook_fields[kBefore] -= +!!this[before_symbol]; + hook_fields[kAfter] -= +!!this[after_symbol]; + hook_fields[kDestroy] -= +!!this[destroy_symbol]; + hooks_array.splice(index, 1); + return this; + } +} + + +function getHookArrays() { + if (!processing_hook) + return [active_hooks_array, async_hook_fields]; + // If this hook is being enabled while in the middle of processing the array + // of currently active hooks then duplicate the current set of active hooks + // and store this there. This shouldn't fire until the next time hooks are + // processed. + if (tmp_active_hooks_array === null) + storeActiveHooks(); + return [tmp_active_hooks_array, tmp_async_hook_fields]; +} + + +function storeActiveHooks() { + tmp_active_hooks_array = active_hooks_array.slice(); + // Don't want to make the assumption that kInit to kDestroy are indexes 0 to + // 4. So do this the long way. + tmp_async_hook_fields = []; + tmp_async_hook_fields[kInit] = async_hook_fields[kInit]; + tmp_async_hook_fields[kBefore] = async_hook_fields[kBefore]; + tmp_async_hook_fields[kAfter] = async_hook_fields[kAfter]; + tmp_async_hook_fields[kDestroy] = async_hook_fields[kDestroy]; +} + + +// Then restore the correct hooks array in case any hooks were added/removed +// during hook callback execution. +function restoreTmpHooks() { + active_hooks_array = tmp_active_hooks_array; + async_hook_fields[kInit] = tmp_async_hook_fields[kInit]; + async_hook_fields[kBefore] = tmp_async_hook_fields[kBefore]; + async_hook_fields[kAfter] = tmp_async_hook_fields[kAfter]; + async_hook_fields[kDestroy] = tmp_async_hook_fields[kDestroy]; + + tmp_active_hooks_array = null; + tmp_async_hook_fields = null; +} + + +function createHook(fns) { + return new AsyncHook(fns); +} + + +function currentId() { + return async_uid_fields[kCurrentAsyncId]; +} + + +function triggerId() { + return async_uid_fields[kCurrentTriggerId]; +} + + +// Embedder API // + +class AsyncEvent { + constructor(type, triggerId) { + this[async_id_symbol] = ++async_uid_fields[kAsyncUidCntr]; + // Read and reset the current kInitTriggerId so that when the constructor + // finishes the kInitTriggerId field is always 0. + if (triggerId === undefined) { + triggerId = initTriggerId(); + // If a triggerId was passed, any kInitTriggerId still must be null'd. + } else { + async_uid_fields[kInitTriggerId] = 0; + } + this[trigger_id_symbol] = triggerId; + + // Return immediately if there's nothing to do. + if (async_hook_fields[kInit] === 0) + return; + + if (typeof type !== 'string' || type.length <= 0) + throw new TypeError('type must be a string with length > 0'); + if (!Number.isSafeInteger(triggerId) || triggerId < 0) + throw new RangeError('triggerId must be an unsigned integer'); + + processing_hook = true; + for (var i = 0; i < active_hooks_array.length; i++) { + if (typeof active_hooks_array[i][init_symbol] === 'function') { + runInitCallback(active_hooks_array[i][init_symbol], + this[async_id_symbol], + type, + triggerId, + this); + } + } + processing_hook = false; + } + + emitBefore() { + emitBeforeS(this[async_id_symbol], this[trigger_id_symbol]); + return this; + } + + emitAfter() { + emitAfterS(this[async_id_symbol]); + return this; + } + + emitDestroy() { + emitDestroyS(this[async_id_symbol]); + return this; + } + + asyncId() { + return this[async_id_symbol]; + } + + triggerId() { + return this[trigger_id_symbol]; + } +} + + +function runInAsyncIdScope(asyncId, cb) { + // Store the async id now to make sure the stack is still good when the ids + // are popped off the stack. + const prevId = currentId(); + pushAsyncIds(asyncId, prevId); + try { + cb(); + } finally { + popAsyncIds(asyncId); + } +} + + +// Sensitive Embedder API // + +// Increment the internal id counter and return the value. Important that the +// counter increment first. Since it's done the same way in +// Environment::new_async_uid() +function newUid() { + return ++async_uid_fields[kAsyncUidCntr]; +} + + +// Return the triggerId meant for the constructor calling it. It's up to the +// user to safeguard this call and make sure it's zero'd out when the +// constructor is complete. +function initTriggerId() { + var tId = async_uid_fields[kInitTriggerId]; + // Reset value after it's been called so the next constructor doesn't + // inherit it by accident. + async_uid_fields[kInitTriggerId] = 0; + if (tId <= 0) + tId = async_uid_fields[kCurrentAsyncId]; + return tId; +} + + +function setInitTriggerId(triggerId) { + // CHECK(Number.isSafeInteger(triggerId)) + // CHECK(triggerId > 0) + async_uid_fields[kInitTriggerId] = triggerId; +} + + +function emitInitS(asyncId, type, triggerId, resource) { + // Short circuit all checks for the common case. Which is that no hooks have + // been set. Do this to remove performance impact for embedders (and core). + // Even though it bypasses all the argument checks. The performance savings + // here is critical. + if (async_hook_fields[kInit] === 0) + return; + + // This can run after the early return check b/c running this function + // manually means that the embedder must have used initTriggerId(). + if (!Number.isSafeInteger(triggerId)) { + if (triggerId !== undefined) + resource = triggerId; + triggerId = initTriggerId(); + } + + // I'd prefer allowing these checks to not exist, or only throw in a debug + // build, in order to improve performance. + if (!Number.isSafeInteger(asyncId) || asyncId < 0) + throw new RangeError('asyncId must be an unsigned integer'); + if (typeof type !== 'string' || type.length <= 0) + throw new TypeError('type must be a string with length > 0'); + if (!Number.isSafeInteger(triggerId) || triggerId < 0) + throw new RangeError('triggerId must be an unsigned integer'); + + processing_hook = true; + for (var i = 0; i < active_hooks_array.length; i++) { + if (typeof active_hooks_array[i][init_symbol] === 'function') { + runInitCallback( + active_hooks_array[i][init_symbol], asyncId, type, triggerId, resource); + } + } + processing_hook = false; + + // Isn't null if hooks were added/removed while the hooks were running. + if (tmp_active_hooks_array !== null) { + restoreTmpHooks(); + } +} + + +function emitBeforeN(asyncId) { + processing_hook = true; + for (var i = 0; i < active_hooks_array.length; i++) { + if (typeof active_hooks_array[i][before_symbol] === 'function') { + runCallback(active_hooks_array[i][before_symbol], asyncId); + } + } + processing_hook = false; + + if (tmp_active_hooks_array !== null) { + restoreTmpHooks(); + } +} + + +// Usage: emitBeforeS(asyncId[, triggerId]). If triggerId is omitted then +// asyncId will be used instead. +function emitBeforeS(asyncId, triggerId = asyncId) { + // CHECK(Number.isSafeInteger(asyncId) && asyncId > 0) + // CHECK(Number.isSafeInteger(triggerId) && triggerId > 0) + + // Validate the ids. + if (asyncId < 0 || triggerId < 0) { + fatalError('before(): asyncId or triggerId is less than zero ' + + `(asyncId: ${asyncId}, triggerId: ${triggerId})`); + } + + pushAsyncIds(asyncId, triggerId); + + if (async_hook_fields[kBefore] === 0) { + return; + } + + emitBeforeN(asyncId); +} + + +// Called from native. The asyncId stack handling is taken care of there before +// this is called. +function emitAfterN(asyncId) { + if (async_hook_fields[kAfter] > 0) { + processing_hook = true; + for (var i = 0; i < active_hooks_array.length; i++) { + if (typeof active_hooks_array[i][after_symbol] === 'function') { + runCallback(active_hooks_array[i][after_symbol], asyncId); + } + } + processing_hook = false; + } + + if (tmp_active_hooks_array !== null) { + restoreTmpHooks(); + } +} + + +// TODO(trevnorris): Calling emitBefore/emitAfter from native can't adjust the +// kIdStackIndex. But what happens if the user doesn't have both before and +// after callbacks. +function emitAfterS(asyncId) { + emitAfterN(asyncId); + popAsyncIds(asyncId); +} + + +function emitDestroyS(asyncId) { + // Return early if there are no destroy callbacks, or on attempt to emit + // destroy on the void. + if (async_hook_fields[kDestroy] === 0 || asyncId === 0) + return; + async_wrap.addIdToDestroyList(asyncId); +} + + +function emitDestroyN(asyncId) { + processing_hook = true; + for (var i = 0; i < active_hooks_array.length; i++) { + if (typeof active_hooks_array[i][destroy_symbol] === 'function') { + runCallback(active_hooks_array[i][destroy_symbol], asyncId); + } + } + processing_hook = false; + + if (tmp_active_hooks_array !== null) { + restoreTmpHooks(); + } +} + + +// Emit callbacks for native calls. Since some state can be setup directly from +// C++ there's no need to perform all the work here. + +// This should only be called if hooks_array has kInit > 0. There are no global +// values to setup. Though hooks_array will be cloned if C++ needed to call +// init(). +// TODO(trevnorris): Perhaps have MakeCallback call a single JS function that +// does the before/callback/after calls to remove two additional calls to JS. +function init(asyncId, type, resource, triggerId) { + processing_hook = true; + for (var i = 0; i < active_hooks_array.length; i++) { + if (typeof active_hooks_array[i][init_symbol] === 'function') { + runInitCallback( + active_hooks_array[i][init_symbol], asyncId, type, triggerId, resource); + } + } + processing_hook = false; +} + + +// Generalized callers for all callbacks that handles error handling. + +// If either runInitCallback() or runCallback() throw then force the +// application to shutdown if one of the callbacks throws. This may change in +// the future depending on whether it can be determined if there's a slim +// chance of the application remaining stable after handling one of these +// exceptions. + +function runInitCallback(cb, asyncId, type, triggerId, resource) { + try { + cb(asyncId, type, triggerId, resource); + } catch (e) { + fatalError(e); + } +} + + +function runCallback(cb, asyncId) { + try { + cb(asyncId); + } catch (e) { + fatalError(e); + } +} + + +// Placing all exports down here because the exported classes won't export +// otherwise. +module.exports = { + // Public API + createHook, + currentId, + triggerId, + // Embedder API + AsyncEvent, + runInAsyncIdScope, + // Sensitive Embedder API + newUid, + initTriggerId, + setInitTriggerId, + emitInit: emitInitS, + emitBefore: emitBeforeS, + emitAfter: emitAfterS, + emitDestroy: emitDestroyS, +}; diff --git a/lib/internal/module.js b/lib/internal/module.js index 8fc8dfbf327e61..43eab35072a8f2 100644 --- a/lib/internal/module.js +++ b/lib/internal/module.js @@ -51,10 +51,10 @@ function stripBOM(content) { } exports.builtinLibs = [ - 'assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dgram', 'dns', - 'domain', 'events', 'fs', 'http', 'https', 'net', 'os', 'path', 'punycode', - 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'tls', 'tty', - 'url', 'util', 'v8', 'vm', 'zlib' + 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'crypto', + 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https', 'net', 'os', + 'path', 'punycode', 'querystring', 'readline', 'repl', 'stream', + 'string_decoder', 'tls', 'tty', 'url', 'util', 'v8', 'vm', 'zlib' ]; function addBuiltinLibsToObject(object) { diff --git a/node.gyp b/node.gyp index 5f480001daa24d..8539ac312253a9 100644 --- a/node.gyp +++ b/node.gyp @@ -24,6 +24,7 @@ 'lib/internal/bootstrap_node.js', 'lib/_debug_agent.js', 'lib/_debugger.js', + 'lib/async_hooks.js', 'lib/assert.js', 'lib/buffer.js', 'lib/child_process.js', diff --git a/src/async-wrap.cc b/src/async-wrap.cc index ab3bf5aa74912e..6ccccfcb65c201 100644 --- a/src/async-wrap.cc +++ b/src/async-wrap.cc @@ -134,6 +134,48 @@ RetainedObjectInfo* WrapperInfo(uint16_t class_id, Local wrapper) { // end RetainedAsyncInfo +static void DestroyIdsCb(uv_idle_t* handle) { + uv_idle_stop(handle); + + Environment* env = Environment::from_destroy_ids_idle_handle(handle); + + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + Local fn = env->async_hooks_destroy_function(); + + TryCatch try_catch(env->isolate()); + + std::vector destroy_ids_list; + destroy_ids_list.swap(*env->destroy_ids_list()); + for (auto current_id : destroy_ids_list) { + // Want each callback to be cleaned up after itself, instead of cleaning + // them all up after the while() loop completes. + HandleScope scope(env->isolate()); + Local argv = Number::New(env->isolate(), current_id); + MaybeLocal ret = fn->Call( + env->context(), Undefined(env->isolate()), 1, &argv); + + if (ret.IsEmpty()) { + ClearFatalExceptionHandlers(env); + FatalException(env->isolate(), try_catch); + } + } + + env->destroy_ids_list()->clear(); +} + + +static void PushBackDestroyId(Environment* env, double id) { + if (env->async_hooks()->fields()[AsyncHooks::kDestroy] == 0) + return; + + if (env->destroy_ids_list()->empty()) + uv_idle_start(env->destroy_ids_idle_handle(), DestroyIdsCb); + + env->destroy_ids_list()->push_back(id); +} + + static void SetupHooks(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -170,6 +212,42 @@ void AsyncWrap::GetAsyncId(const FunctionCallbackInfo& args) { } +void AsyncWrap::PushAsyncIds(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + // No need for CHECK(IsNumber()) on args because if FromJust() doesn't fail + // then the checks in push_ids() and pop_ids() will. + double async_id = args[0]->NumberValue(env->context()).FromJust(); + double trigger_id = args[1]->NumberValue(env->context()).FromJust(); + env->async_hooks()->push_ids(async_id, trigger_id); +} + + +void AsyncWrap::PopAsyncIds(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + double async_id = args[0]->NumberValue(env->context()).FromJust(); + args.GetReturnValue().Set(env->async_hooks()->pop_ids(async_id)); +} + + +void AsyncWrap::ClearIdStack(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + env->async_hooks()->clear_id_stack(); +} + + +void AsyncWrap::AsyncReset(const FunctionCallbackInfo& args) { + AsyncWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + wrap->AsyncReset(); +} + + +void AsyncWrap::QueueDestroyId(const FunctionCallbackInfo& args) { + CHECK(args[0]->IsNumber()); + PushBackDestroyId(Environment::GetCurrent(args), args[0]->NumberValue()); +} + + void AsyncWrap::Initialize(Local target, Local unused, Local context) { @@ -178,6 +256,10 @@ void AsyncWrap::Initialize(Local target, HandleScope scope(isolate); env->SetMethod(target, "setupHooks", SetupHooks); + env->SetMethod(target, "pushAsyncIds", PushAsyncIds); + env->SetMethod(target, "popAsyncIds", PopAsyncIds); + env->SetMethod(target, "clearIdStack", ClearIdStack); + env->SetMethod(target, "addIdToDestroyList", QueueDestroyId); v8::PropertyAttribute ReadOnlyDontDelete = static_cast(v8::ReadOnly | v8::DontDelete); @@ -252,37 +334,6 @@ void AsyncWrap::Initialize(Local target, } -void AsyncWrap::DestroyIdsCb(uv_idle_t* handle) { - uv_idle_stop(handle); - - Environment* env = Environment::from_destroy_ids_idle_handle(handle); - - HandleScope handle_scope(env->isolate()); - Context::Scope context_scope(env->context()); - Local fn = env->async_hooks_destroy_function(); - - TryCatch try_catch(env->isolate()); - - std::vector destroy_ids_list; - destroy_ids_list.swap(*env->destroy_ids_list()); - for (auto current_id : destroy_ids_list) { - // Want each callback to be cleaned up after itself, instead of cleaning - // them all up after the while() loop completes. - HandleScope scope(env->isolate()); - Local argv = Number::New(env->isolate(), current_id); - MaybeLocal ret = fn->Call( - env->context(), Undefined(env->isolate()), 1, &argv); - - if (ret.IsEmpty()) { - ClearFatalExceptionHandlers(env); - FatalException(env->isolate(), try_catch); - } - } - - env->destroy_ids_list()->clear(); -} - - void LoadAsyncWrapperInfo(Environment* env) { HeapProfiler* heap_profiler = env->isolate()->GetHeapProfiler(); #define V(PROVIDER) \ @@ -310,21 +361,14 @@ AsyncWrap::AsyncWrap(Environment* env, AsyncWrap::~AsyncWrap() { - if (env()->async_hooks()->fields()[AsyncHooks::kDestroy] == 0) { - return; - } - - if (env()->destroy_ids_list()->empty()) - uv_idle_start(env()->destroy_ids_idle_handle(), DestroyIdsCb); - - env()->destroy_ids_list()->push_back(get_id()); + PushBackDestroyId(env(), get_id()); } // Generalized call for both the constructor and for handles that are pooled // and reused over their lifetime. This way a new uid can be assigned when // the resource is pulled out of the pool and put back into use. -void AsyncWrap::Reset() { +void AsyncWrap::AsyncReset() { AsyncHooks* async_hooks = env()->async_hooks(); async_id_ = env()->new_async_id(); trigger_id_ = env()->get_init_trigger_id(); diff --git a/src/async-wrap.h b/src/async-wrap.h index 1fe0499468b485..8a93e838786297 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -85,8 +85,11 @@ class AsyncWrap : public BaseObject { v8::Local context); static void GetAsyncId(const v8::FunctionCallbackInfo& args); - - static void DestroyIdsCb(uv_idle_t* handle); + static void PushAsyncIds(const v8::FunctionCallbackInfo& args); + static void PopAsyncIds(const v8::FunctionCallbackInfo& args); + static void ClearIdStack(const v8::FunctionCallbackInfo& args); + static void AsyncReset(const v8::FunctionCallbackInfo& args); + static void QueueDestroyId(const v8::FunctionCallbackInfo& args); inline ProviderType provider_type() const; @@ -94,7 +97,7 @@ class AsyncWrap : public BaseObject { inline double get_trigger_id() const; - void Reset(); + void AsyncReset(); // Only call these within a valid HandleScope. // TODO(trevnorris): These should return a MaybeLocal. diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index f7e8c18658c355..4b0158c4208018 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -476,7 +476,7 @@ class Parser : public AsyncWrap { // Should always be called from the same context. CHECK_EQ(env, parser->env()); // The parser is being reused. Reset the uid and call init() callbacks. - parser->Reset(); + parser->AsyncReset(); parser->Init(type); } diff --git a/src/tcp_wrap.cc b/src/tcp_wrap.cc index d1bf4a952a1f7c..4967b407145c1c 100644 --- a/src/tcp_wrap.cc +++ b/src/tcp_wrap.cc @@ -86,6 +86,7 @@ void TCPWrap::Initialize(Local target, Null(env->isolate())); env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(t, "asyncReset", AsyncWrap::AsyncReset); env->SetProtoMethod(t, "close", HandleWrap::Close); diff --git a/test/common.js b/test/common.js index 6fe2d4520f6a08..2d644a0d497f49 100644 --- a/test/common.js +++ b/test/common.js @@ -38,6 +38,7 @@ const testRoot = process.env.NODE_TEST_DIR ? const noop = () => {}; exports.noop = noop; + exports.fixturesDir = path.join(__dirname, 'fixtures'); exports.tmpDirName = 'tmp'; // PORT should match the definition in test/testpy/__init__.py. @@ -65,6 +66,50 @@ exports.enoughTestCpu = Array.isArray(cpus) && exports.rootDir = exports.isWindows ? 'c:\\' : '/'; exports.buildType = process.config.target_defaults.default_configuration; +// If env var is set then enable async_hook hooks for all tests. +if (process.env.NODE_TEST_WITH_ASYNC_HOOKS) { + const destroydIdsList = {}; + const destroyListList = {}; + const initHandles = {}; + const async_wrap = process.binding('async_wrap'); + + process.on('exit', () => { + // itterate through handles to make sure nothing crashes + for (const k in initHandles) + util.inspect(initHandles[k]); + }); + + const _addIdToDestroyList = async_wrap.addIdToDestroyList; + async_wrap.addIdToDestroyList = function addIdToDestroyList(id) { + if (destroyListList[id] !== undefined) { + process._rawDebug(destroyListList[id]); + process._rawDebug(); + throw new Error(`same id added twice (${id})`); + } + destroyListList[id] = new Error().stack; + _addIdToDestroyList(id); + }; + + require('async_hooks').createHook({ + init(id, ty, tr, h) { + if (initHandles[id]) { + throw new Error(`init called twice for same id (${id})`); + } + initHandles[id] = h; + }, + before() { }, + after() { }, + destroy(id) { + if (destroydIdsList[id] !== undefined) { + process._rawDebug(destroydIdsList[id]); + process._rawDebug(); + throw new Error(`destroy called for same id (${id})`); + } + destroydIdsList[id] = new Error().stack; + }, + }).enable(); +} + function rimrafSync(p) { let st; try { @@ -675,3 +720,18 @@ exports.getArrayBufferViews = function getArrayBufferViews(buf) { } return out; }; + +exports.getTTYfd = function getTTYfd() { + const tty = require('tty'); + let tty_fd = 0; + if (!tty.isatty(tty_fd)) tty_fd++; + else if (!tty.isatty(tty_fd)) tty_fd++; + else if (!tty.isatty(tty_fd)) tty_fd++; + else try { + tty_fd = require('fs').openSync('/dev/tty'); + } catch (e) { + // There aren't any tty fd's available to use. + return -1; + } + return tty_fd; +}; diff --git a/test/parallel/test-async-wrap-destroyid.js b/test/parallel/test-async-wrap-destroyid.js new file mode 100644 index 00000000000000..75f8ed9e661fe3 --- /dev/null +++ b/test/parallel/test-async-wrap-destroyid.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); +const async_wrap = process.binding('async_wrap'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const RUNS = 5; +let test_id = null; +let run_cntr = 0; +let hooks = null; + +process.on('beforeExit', common.mustCall(() => { + process.removeAllListeners('uncaughtException'); + hooks.disable(); + assert.strictEqual(test_id, null); + assert.strictEqual(run_cntr, RUNS); +})); + + +hooks = async_hooks.createHook({ + destroy(id) { + if (id === test_id) { + run_cntr++; + test_id = null; + } + }, +}).enable(); + + +(function runner(n) { + assert.strictEqual(test_id, null); + if (n <= 0) return; + + test_id = (Math.random() * 1e9) >>> 0; + async_wrap.addIdToDestroyList(test_id); + setImmediate(common.mustCall(runner), n - 1); +})(RUNS); diff --git a/test/parallel/test-async-wrap-getasyncid.js b/test/parallel/test-async-wrap-getasyncid.js index ab5a3a6d8d8270..5ab6240ed25da8 100644 --- a/test/parallel/test-async-wrap-getasyncid.js +++ b/test/parallel/test-async-wrap-getasyncid.js @@ -3,6 +3,27 @@ const common = require('../common'); const assert = require('assert'); const fs = require('fs'); +const providers = Object.assign({}, process.binding('async_wrap').Providers); + +// Make sure that all Providers are tested. +{ + const hooks = require('async_hooks').createHook({ + init(id, type) { + if (type === 'NONE') + throw new Error('received a provider type of NONE'); + delete providers[type]; + }, + }).enable(); + process.on('beforeExit', common.mustCall(() => { + process.removeAllListeners('uncaughtException'); + hooks.disable(); + delete providers.NONE; // Should never be used. + const obj_keys = Object.keys(providers); + if (obj_keys.length > 0) + process._rawDebug(obj_keys); + assert.strictEqual(obj_keys.length, 0); + })); +} function testUninitialized(req, ctor_name) { assert.strictEqual(typeof req.getAsyncId, 'function'); @@ -185,9 +206,24 @@ if (common.hasCrypto) { { - const tty_wrap = process.binding('tty_wrap'); - if (tty_wrap.isTTY(0)) { - testInitialized(new tty_wrap.TTY(0, false), 'TTY'); + // Do our best to grab a tty fd. + const tty_fd = common.getTTYfd(); + if (tty_fd >= 0) { + const tty_wrap = process.binding('tty_wrap'); + // fd may still be invalid, so guard against it. + const handle = (() => { + try { + return new tty_wrap.TTY(tty_fd, false); + } catch (e) { + return null; + } + })(); + if (handle !== null) + testInitialized(handle, 'TTY'); + else + delete providers.TTYWRAP; + } else { + delete providers.TTYWRAP; } } From 3783c985e68631f498100bad85a8b7e677483025 Mon Sep 17 00:00:00 2001 From: Trevor Norris Date: Fri, 10 Mar 2017 06:17:42 -0700 Subject: [PATCH 09/12] lib: implement aysnc_hooks API in core Implement async_hooks support in the following: * fatalException handler * process.nextTick * Timers * net/dgram/http --- lib/_http_agent.js | 9 +- lib/_http_client.js | 6 +- lib/_http_common.js | 8 +- lib/_http_outgoing.js | 12 +- lib/async_hooks.js | 6 +- lib/dgram.js | 12 +- lib/internal/bootstrap_node.js | 24 ++- lib/internal/process/next_tick.js | 153 +++++++++++++++--- lib/net.js | 47 +++++- lib/timers.js | 84 ++++++++++ src/async-wrap.cc | 12 ++ test/message/timeout_throw.out | 2 +- .../unhandled_promise_trace_warnings.out | 2 +- .../test-async-wrap-uncaughtexception.js | 43 +++++ 14 files changed, 379 insertions(+), 41 deletions(-) create mode 100644 test/parallel/test-async-wrap-uncaughtexception.js diff --git a/lib/_http_agent.js b/lib/_http_agent.js index 351417a7ba6a74..aed18d72e7c7e1 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -25,6 +25,8 @@ const net = require('net'); const util = require('util'); const EventEmitter = require('events'); const debug = util.debuglog('http'); +const async_id_symbol = process.binding('async_wrap').async_id_symbol; +const nextTick = require('internal/process/next_tick').nextTick; // New Agent code. @@ -93,6 +95,7 @@ function Agent(options) { self.freeSockets[name] = freeSockets; socket.setKeepAlive(true, self.keepAliveMsecs); socket.unref(); + socket[async_id_symbol] = -1; socket._httpMessage = null; self.removeSocket(socket, options); freeSockets.push(socket); @@ -162,6 +165,8 @@ Agent.prototype.addRequest = function addRequest(req, options) { if (freeLen) { // we have a free socket, so use that. var socket = this.freeSockets[name].shift(); + // Assign the handle a new asyncId and run any init() hooks. + socket._handle.asyncReset(); debug('have free socket'); // don't leak @@ -176,7 +181,7 @@ Agent.prototype.addRequest = function addRequest(req, options) { // If we are under maxSockets create a new one. this.createSocket(req, options, function(err, newSocket) { if (err) { - process.nextTick(function() { + nextTick(newSocket._handle.getAsyncId(), function() { req.emit('error', err); }); return; @@ -289,7 +294,7 @@ Agent.prototype.removeSocket = function removeSocket(s, options) { // If we have pending requests and a socket gets closed make a new one this.createSocket(req, options, function(err, newSocket) { if (err) { - process.nextTick(function() { + nextTick(newSocket._handle.getAsyncId(), function() { req.emit('error', err); }); return; diff --git a/lib/_http_client.js b/lib/_http_client.js index 2205e89d2ae3d6..8e349b453d9776 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -36,6 +36,7 @@ const Agent = require('_http_agent'); const Buffer = require('buffer').Buffer; const urlToOptions = require('internal/url').urlToOptions; const outHeadersKey = require('internal/http').outHeadersKey; +const nextTick = require('internal/process/next_tick').nextTick; // The actual list of disallowed characters in regexp form is more like: // /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/ @@ -577,9 +578,12 @@ function responseKeepAlive(res, req) { socket.removeListener('close', socketCloseListener); socket.removeListener('error', socketErrorListener); socket.once('error', freeSocketErrorListener); + // There are cases where _handle === null. Avoid those. Passing null to + // nextTick() will call initTriggerId() to retrieve the id. + const asyncId = socket._handle ? socket._handle.getAsyncId() : null; // Mark this socket as available, AFTER user-added end // handlers have a chance to run. - process.nextTick(emitFreeNT, socket); + nextTick(asyncId, emitFreeNT, socket); } } diff --git a/lib/_http_common.js b/lib/_http_common.js index 52cabd87fdfcab..98adf744f4b56a 100644 --- a/lib/_http_common.js +++ b/lib/_http_common.js @@ -28,6 +28,7 @@ const HTTPParser = binding.HTTPParser; const FreeList = require('internal/freelist').FreeList; const ondrain = require('internal/http').ondrain; const incoming = require('_http_incoming'); +const emitDestroy = require('async_hooks').emitDestroy; const IncomingMessage = incoming.IncomingMessage; const readStart = incoming.readStart; const readStop = incoming.readStop; @@ -211,8 +212,13 @@ function freeParser(parser, req, socket) { parser.incoming = null; parser.outgoing = null; parser[kOnExecute] = null; - if (parsers.free(parser) === false) + if (parsers.free(parser) === false) { parser.close(); + } else { + // Since the Parser destructor isn't going to run the destroy() callbacks + // it needs to be triggered manually. + emitDestroy(parser.getAsyncId()); + } } if (req) { req.parser = null; diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index a8e04d543a9f56..4cc3fa9d06b30b 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -31,6 +31,8 @@ const common = require('_http_common'); const checkIsHttpToken = common._checkIsHttpToken; const checkInvalidHeaderChar = common._checkInvalidHeaderChar; const outHeadersKey = require('internal/http').outHeadersKey; +const async_id_symbol = process.binding('async_wrap').async_id_symbol; +const nextTick = require('internal/process/next_tick').nextTick; const CRLF = common.CRLF; const debug = common.debug; @@ -265,8 +267,9 @@ function _writeRaw(data, encoding, callback) { if (this.output.length) { this._flushOutput(conn); } else if (!data.length) { - if (typeof callback === 'function') - process.nextTick(callback); + if (typeof callback === 'function') { + nextTick(this.socket[async_id_symbol], callback); + } return true; } // Directly write to socket. @@ -624,7 +627,10 @@ const crlf_buf = Buffer.from('\r\n'); OutgoingMessage.prototype.write = function write(chunk, encoding, callback) { if (this.finished) { var err = new Error('write after end'); - process.nextTick(writeAfterEndNT.bind(this), err, callback); + nextTick(this.socket[async_id_symbol], + writeAfterEndNT.bind(this), + err, + callback); return true; } diff --git a/lib/async_hooks.js b/lib/async_hooks.js index 736b189097672c..867b5eb52da14d 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -32,7 +32,7 @@ var processing_hook = false; // Use to temporarily store and updated active_hooks_array if the user enables // or disables a hook while hooks are being processed. var tmp_active_hooks_array = null; -// Keep track of the field counds held in tmp_active_hooks_array. +// Keep track of the field counts held in tmp_active_hooks_array. var tmp_async_hook_fields = null; // Each constant tracks how many callbacks there are for any given step of @@ -41,9 +41,9 @@ var tmp_async_hook_fields = null; const { kInit, kBefore, kAfter, kDestroy, kCurrentAsyncId, kCurrentTriggerId, kAsyncUidCntr, kInitTriggerId } = async_wrap.constants; +const { async_id_symbol, trigger_id_symbol } = async_wrap; + // Used in AsyncHook and AsyncEvent. -const async_id_symbol = Symbol('_asyncId'); -const trigger_id_symbol = Symbol('_triggerId'); const init_symbol = Symbol('init'); const before_symbol = Symbol('before'); const after_symbol = Symbol('after'); diff --git a/lib/dgram.js b/lib/dgram.js index 0104d962b1b81d..8b41029b8e4b8d 100644 --- a/lib/dgram.js +++ b/lib/dgram.js @@ -25,7 +25,10 @@ const assert = require('assert'); const Buffer = require('buffer').Buffer; const util = require('util'); const EventEmitter = require('events'); +const setInitTriggerId = require('async_hooks').setInitTriggerId; const UV_UDP_REUSEADDR = process.binding('constants').os.UV_UDP_REUSEADDR; +const async_id_symbol = process.binding('async_wrap').async_id_symbol; +const nextTick = require('internal/process/next_tick').nextTick; const UDP = process.binding('udp_wrap').UDP; const SendWrap = process.binding('udp_wrap').SendWrap; @@ -111,6 +114,7 @@ function Socket(type, listener) { this._handle = handle; this._receiving = false; this._bindState = BIND_STATE_UNBOUND; + this[async_id_symbol] = this._handle.getAsyncId(); this.type = type; this.fd = null; // compatibility hack @@ -433,6 +437,10 @@ function doSend(ex, self, ip, list, address, port, callback) { req.callback = callback; req.oncomplete = afterSend; } + // node::SendWrap isn't instantiated and attached to the JS instance of + // SendWrap above until send() is called. So don't set the init trigger id + // until now. + setInitTriggerId(self[async_id_symbol]); var err = self._handle.send(req, list, list.length, @@ -442,7 +450,7 @@ function doSend(ex, self, ip, list, address, port, callback) { if (err && callback) { // don't emit as error, dgram_legacy.js compatibility const ex = exceptionWithHostPort(err, 'send', address, port); - process.nextTick(callback, ex); + nextTick(self[async_id_symbol], callback, ex); } } @@ -469,7 +477,7 @@ Socket.prototype.close = function(callback) { this._stopReceiving(); this._handle.close(); this._handle = null; - process.nextTick(socketCloseNT, this); + nextTick(this[async_id_symbol], socketCloseNT, this); return this; }; diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index cb6867125860e1..d3fc5fdc379228 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -305,10 +305,20 @@ } function setupProcessFatal() { + const async_wrap = process.binding('async_wrap'); + // Arrays containing hook flags and ids for async_hook calls. + const { async_hook_fields, async_uid_fields } = async_wrap; + // Internal functions needed to manipulate the stack. + const { clearIdStack, popAsyncIds } = async_wrap; + const { kAfter, kCurrentAsyncId, kInitTriggerId } = async_wrap.constants; process._fatalException = function(er) { var caught; + // It's possible that kInitTriggerId was set for a constructor call that + // threw and was never cleared. So clear it now. + async_uid_fields[kInitTriggerId] = 0; + if (process.domain && process.domain._errorHandler) caught = process.domain._errorHandler(er) || caught; @@ -327,9 +337,21 @@ // nothing to be done about it at this point. } - // if we handled an error, then make sure any ticks get processed } else { + // If we handled an error, then make sure any ticks get processed NativeModule.require('timers').setImmediate(process._tickCallback); + + // Emit the after() hooks now that the exception has been handled. + if (async_hook_fields[kAfter] > 0) { + do { + NativeModule.require('async_hooks').emitAfter( + async_uid_fields[kCurrentAsyncId]); + // popAsyncIds() returns true if there are more ids on the stack. + } while (popAsyncIds(async_uid_fields[kCurrentAsyncId])); + // Or completely empty the id stack. + } else { + clearIdStack(); + } } return caught; diff --git a/lib/internal/process/next_tick.js b/lib/internal/process/next_tick.js index ad635aaf494b33..eb6b59cdf54865 100644 --- a/lib/internal/process/next_tick.js +++ b/lib/internal/process/next_tick.js @@ -7,10 +7,25 @@ const kMaxCallbacksPerLoop = 1e4; exports.setup = setupNextTick; +// Will be overwritten when setupNextTick() is called. +exports.nextTick = null; function setupNextTick() { + const async_wrap = process.binding('async_wrap'); + const async_hooks = require('async_hooks'); const promises = require('internal/process/promises'); const emitPendingUnhandledRejections = promises.setup(scheduleMicrotasks); + const initTriggerId = async_hooks.initTriggerId; + // Two arrays that share state between C++ and JS. + const { async_hook_fields, async_uid_fields } = async_wrap; + // Used to change the state of the async id stack. + const { pushAsyncIds, popAsyncIds } = async_wrap; + // The needed emit*() functions. + const { emitInit, emitBefore, emitAfter, emitDestroy } = async_hooks; + // Grab the constants necessary for working with internal arrays. + const { kInit, kBefore, kAfter, kDestroy, kAsyncUidCntr, kInitTriggerId } = + async_wrap.constants; + const { async_id_symbol, trigger_id_symbol } = async_wrap; var nextTickQueue = []; var microtasksScheduled = false; @@ -26,6 +41,9 @@ function setupNextTick() { process._tickCallback = _tickCallback; process._tickDomainCallback = _tickDomainCallback; + // Set the nextTick() function for internal usage. + exports.nextTick = internalNextTick; + // This tickInfo thing is used so that the C++ code in src/node.cc // can have easy access to our nextTick state, and avoid unnecessary // calls into JS land. @@ -50,10 +68,13 @@ function setupNextTick() { if (microtasksScheduled) return; - nextTickQueue.push({ - callback: runMicrotasksCallback, - domain: null - }); + const tickObject = + new TickObject(runMicrotasksCallback, undefined, null); + // For the moment all microtasks come from the void until the PromiseHook + // API is implemented. + tickObject[async_id_symbol] = 0; + tickObject[trigger_id_symbol] = 0; + nextTickQueue.push(tickObject); tickInfo[kLength]++; microtasksScheduled = true; @@ -88,20 +109,58 @@ function setupNextTick() { } } + // TODO(trevnorris): Using std::stack of Environment::AsyncHooks::ids_stack_ + // is much slower here than was the Float64Array stack used in a previous + // implementation. Problem is the Float64Array stack was a bit brittle. + // Investigate how to harden that implementation and possibly reintroduce it. + function nextTickEmitBefore(asyncId, triggerId) { + if (async_hook_fields[kBefore] > 0) + emitBefore(asyncId, triggerId); + else + pushAsyncIds(asyncId, triggerId); + } + + function nextTickEmitAfter(asyncId) { + if (async_hook_fields[kAfter] > 0) + emitAfter(asyncId); + else + popAsyncIds(asyncId); + } + // Run callbacks that have no domain. // Using domains will cause this to be overridden. function _tickCallback() { - var callback, args, tock; - do { while (tickInfo[kIndex] < tickInfo[kLength]) { - tock = nextTickQueue[tickInfo[kIndex]++]; - callback = tock.callback; - args = tock.args; + const tock = nextTickQueue[tickInfo[kIndex]++]; + const callback = tock.callback; + const args = tock.args; + + // CHECK(Number.isSafeInteger(tock[async_id_symbol])) + // CHECK(tock[async_id_symbol] > 0) + // CHECK(Number.isSafeInteger(tock[trigger_id_symbol])) + // CHECK(tock[trigger_id_symbol] > 0) + + nextTickEmitBefore(tock[async_id_symbol], tock[trigger_id_symbol]); + // emitDestroy() places the async_id_symbol into an asynchronous queue + // that calls the destroy callback in the future. It's called before + // calling tock.callback so destroy will be called even if the callback + // throws an exception that is handles by 'uncaughtException' or a + // domain. + // TODO(trevnorris): This is a bit of a hack. It relies on the fact + // that nextTick() doesn't allow the event loop to proceed, but if + // any async hooks are enabled during the callback's execution then + // this tock's after hook will be called, but not its destroy hook. + if (async_hook_fields[kDestroy] > 0) + emitDestroy(tock[async_id_symbol]); + // Using separate callback execution functions allows direct // callback invocation with small numbers of arguments to avoid the // performance hit associated with using `fn.apply()` _combinedTickCallback(args, callback); + + nextTickEmitAfter(tock[async_id_symbol]); + if (kMaxCallbacksPerLoop < tickInfo[kIndex]) tickDone(); } @@ -112,20 +171,33 @@ function setupNextTick() { } function _tickDomainCallback() { - var callback, domain, args, tock; - do { while (tickInfo[kIndex] < tickInfo[kLength]) { - tock = nextTickQueue[tickInfo[kIndex]++]; - callback = tock.callback; - domain = tock.domain; - args = tock.args; + const tock = nextTickQueue[tickInfo[kIndex]++]; + const callback = tock.callback; + const domain = tock.domain; + const args = tock.args; if (domain) domain.enter(); + + // CHECK(Number.isSafeInteger(tock[async_id_symbol])) + // CHECK(tock[async_id_symbol] > 0) + // CHECK(Number.isSafeInteger(tock[trigger_id_symbol])) + // CHECK(tock[trigger_id_symbol] > 0) + + nextTickEmitBefore(tock[async_id_symbol], tock[trigger_id_symbol]); + // TODO(trevnorris): See comment in _tickCallback() as to why this + // isn't a good solution. + if (async_hook_fields[kDestroy] > 0) + emitDestroy(tock[async_id_symbol]); + // Using separate callback execution functions allows direct // callback invocation with small numbers of arguments to avoid the // performance hit associated with using `fn.apply()` _combinedTickCallback(args, callback); + + nextTickEmitAfter(tock[async_id_symbol]); + if (kMaxCallbacksPerLoop < tickInfo[kIndex]) tickDone(); if (domain) @@ -137,6 +209,25 @@ function setupNextTick() { } while (tickInfo[kLength] !== 0); } + function TickObject(callback, args, domain) { + this.callback = callback; + this.domain = domain; + this.args = args; + this[async_id_symbol] = -1; + this[trigger_id_symbol] = -1; + } + + function setupInit(tickObject, triggerId) { + tickObject[async_id_symbol] = ++async_uid_fields[kAsyncUidCntr]; + tickObject[trigger_id_symbol] = triggerId || initTriggerId(); + if (async_hook_fields[kInit] > 0) { + emitInit(tickObject[async_id_symbol], + 'TickObject', + tickObject[trigger_id_symbol], + tickObject); + } + } + function nextTick(callback) { if (typeof callback !== 'function') throw new TypeError('callback is not a function'); @@ -151,11 +242,33 @@ function setupNextTick() { args[i - 1] = arguments[i]; } - nextTickQueue.push({ - callback, - domain: process.domain || null, - args - }); + var obj = new TickObject(callback, args, process.domain || null); + setupInit(obj, null); + nextTickQueue.push(obj); + tickInfo[kLength]++; + } + + function internalNextTick(triggerId, callback) { + if (typeof callback !== 'function') + throw new TypeError('callback is not a function'); + // CHECK(Number.isSafeInteger(triggerId) || triggerId === null) + // CHECK(triggerId > 0 || triggerId === null) + + if (process._exiting) + return; + + var args; + if (arguments.length > 2) { + args = new Array(arguments.length - 2); + for (var i = 2; i < arguments.length; i++) + args[i - 2] = arguments[i]; + } + + var obj = new TickObject(callback, args, process.domain || null); + setupInit(obj, triggerId); + // The call to initTriggerId() was skipped, so clear kInitTriggerId. + async_uid_fields[kInitTriggerId] = 0; + nextTickQueue.push(obj); tickInfo[kLength]++; } } diff --git a/lib/net.js b/lib/net.js index fe497b1bd79992..fdf1061559ab34 100644 --- a/lib/net.js +++ b/lib/net.js @@ -39,6 +39,9 @@ const TCPConnectWrap = process.binding('tcp_wrap').TCPConnectWrap; const PipeConnectWrap = process.binding('pipe_wrap').PipeConnectWrap; const ShutdownWrap = process.binding('stream_wrap').ShutdownWrap; const WriteWrap = process.binding('stream_wrap').WriteWrap; +const async_id_symbol = process.binding('async_wrap').async_id_symbol; +const { newUid, setInitTriggerId } = require('async_hooks'); +const nextTick = require('internal/process/next_tick').nextTick; var cluster; @@ -56,6 +59,12 @@ function createHandle(fd) { } +function getNewAsyncId(handle) { + return (!handle || typeof handle.getAsyncId !== 'function') ? + newUid() : handle.getAsyncId(); +} + + const debug = util.debuglog('net'); function isPipeName(s) { @@ -147,6 +156,7 @@ function initSocketHandle(self) { if (self._handle) { self._handle.owner = self; self._handle.onread = onread; + self[async_id_symbol] = getNewAsyncId(self._handle); // If handle doesn't support writev - neither do we if (!self._handle.writev) @@ -162,6 +172,10 @@ function Socket(options) { if (!(this instanceof Socket)) return new Socket(options); this.connecting = false; + // Problem with this is that users can supply their own handle, that may not + // have _handle.getAsyncId(). In this case an[async_id_symbol] should + // probably be supplied by async_hooks. + this[async_id_symbol] = -1; this._hadError = false; this._handle = null; this._parent = null; @@ -176,9 +190,11 @@ function Socket(options) { if (options.handle) { this._handle = options.handle; // private + this[async_id_symbol] = getNewAsyncId(this._handle); } else if (options.fd !== undefined) { this._handle = createHandle(options.fd); this._handle.open(options.fd); + this[async_id_symbol] = this._handle.getAsyncId(); // options.fd can be string (since it user-defined), // so changing this to === would be semver-major // See: https://github.com/nodejs/node/pull/11513 @@ -263,6 +279,10 @@ function onSocketFinish() { var req = new ShutdownWrap(); req.oncomplete = afterShutdown; req.handle = this._handle; + // node::ShutdownWrap isn't instantiated and attached to the JS instance of + // ShutdownWrap above until shutdown() is called. So don't set the init + // trigger id until now. + setInitTriggerId(this[async_id_symbol]); var err = this._handle.shutdown(req); if (err) @@ -328,7 +348,7 @@ function writeAfterFIN(chunk, encoding, cb) { // TODO: defer error events consistently everywhere, not just the cb this.emit('error', er); if (typeof cb === 'function') { - process.nextTick(cb, er); + nextTick(this[async_id_symbol], cb, er); } } @@ -892,6 +912,10 @@ function internalConnect( req.localAddress = localAddress; req.localPort = localPort; + // node::TCPConnectWrap isn't instantiated and attached to the JS instance + // of TCPConnectWrap above until connect() is called. So don't set the init + // trigger id until now. + setInitTriggerId(self[async_id_symbol]); if (addressType === 4) err = self._handle.connect(req, address, port); else @@ -901,6 +925,10 @@ function internalConnect( const req = new PipeConnectWrap(); req.address = address; req.oncomplete = afterConnect; + // node::PipeConnectWrap isn't instantiated and attached to the JS instance + // of PipeConnectWrap above until connect() is called. So don't set the + // init trigger id until now. + setInitTriggerId(self[async_id_symbol]); err = self._handle.connect(req, address, afterConnect); } @@ -996,7 +1024,7 @@ function lookupAndConnect(self, options) { // If host is an IP, skip performing a lookup var addressType = cares.isIP(host); if (addressType) { - process.nextTick(function() { + nextTick(self[async_id_symbol], function() { if (self.connecting) internalConnect(self, host, port, addressType, localAddress, localPort); }); @@ -1019,6 +1047,7 @@ function lookupAndConnect(self, options) { debug('connect: dns options', dnsopts); self._host = host; var lookup = options.lookup || dns.lookup; + setInitTriggerId(self[async_id_symbol]); lookup(host, dnsopts, function emitLookup(err, ip, addressType) { self.emit('lookup', err, ip, addressType, host); @@ -1166,6 +1195,7 @@ function Server(options, connectionListener) { configurable: true, enumerable: false }); + this[async_id_symbol] = -1; this._handle = null; this._usingSlaves = false; this._slaves = []; @@ -1273,6 +1303,7 @@ function setupListenHandle(address, port, addressType, backlog, fd) { this._handle = rval; } + this[async_id_symbol] = getNewAsyncId(this._handle); this._handle.onconnection = onconnection; this._handle.owner = this; @@ -1285,7 +1316,7 @@ function setupListenHandle(address, port, addressType, backlog, fd) { var ex = exceptionWithHostPort(err, 'listen', address, port); this._handle.close(); this._handle = null; - process.nextTick(emitErrorNT, this, ex); + nextTick(this[async_id_symbol], emitErrorNT, this, ex); return; } @@ -1296,7 +1327,7 @@ function setupListenHandle(address, port, addressType, backlog, fd) { if (this._unref) this.unref(); - process.nextTick(emitListeningNT, this); + nextTick(this[async_id_symbol], emitListeningNT, this); } Server.prototype._listen2 = setupListenHandle; // legacy alias @@ -1390,6 +1421,7 @@ Server.prototype.listen = function() { // (handle[, backlog][, cb]) where handle is an object with a handle if (options instanceof TCP) { this._handle = options; + this[async_id_symbol] = this._handle.getAsyncId(); listenInCluster(this, null, -1, -1, backlogFromArgs); return this; } @@ -1510,8 +1542,10 @@ function onconnection(err, clientHandle) { Server.prototype.getConnections = function(cb) { + const self = this; + function end(err, connections) { - process.nextTick(cb, err, connections); + nextTick(self[async_id_symbol], cb, err, connections); } if (!this._usingSlaves) { @@ -1588,7 +1622,8 @@ Server.prototype._emitCloseIfDrained = function() { return; } - process.nextTick(emitCloseNT, this); + const asyncId = this._handle ? this[async_id_symbol] : null; + nextTick(asyncId, emitCloseNT, this); }; diff --git a/lib/timers.js b/lib/timers.js index 115c3c82963530..ae784bbb254341 100644 --- a/lib/timers.js +++ b/lib/timers.js @@ -21,12 +21,27 @@ 'use strict'; +const async_wrap = process.binding('async_wrap'); const TimerWrap = process.binding('timer_wrap').Timer; const L = require('internal/linkedlist'); +const async_hooks = require('async_hooks'); const assert = require('assert'); const util = require('util'); const debug = util.debuglog('timer'); const kOnTimeout = TimerWrap.kOnTimeout | 0; +const initTriggerId = async_hooks.initTriggerId; +// Two arrays that share state between C++ and JS. +const { async_hook_fields, async_uid_fields } = async_wrap; +// Used to change the state of the async id stack. +const { pushAsyncIds, popAsyncIds } = async_wrap; +// The needed emit*() functions. +const { emitInit, emitBefore, emitAfter, emitDestroy } = async_hooks; +// Grab the constants necessary for working with internal arrays. +const { kInit, kBefore, kAfter, kDestroy, kAsyncUidCntr } = + async_wrap.constants; +// Symbols for storing async id state. +const async_id_symbol = Symbol('asyncId'); +const trigger_id_symbol = Symbol('triggerId'); // Timeout values > TIMEOUT_MAX are set to 1. const TIMEOUT_MAX = 2147483647; // 2^31-1 @@ -132,6 +147,22 @@ exports._unrefActive = function(item) { }; +function timerEmitBefore(asyncId, triggerId) { + if (async_hook_fields[kBefore] > 0) + emitBefore(asyncId, triggerId); + else + pushAsyncIds(asyncId, triggerId); +} + + +function timerEmitAfter(asyncId) { + if (async_hook_fields[kAfter] > 0) + emitAfter(asyncId); + else + popAsyncIds(asyncId); +} + + // The underlying logic for scheduling or re-scheduling a timer. // // Appends a timer onto the end of an existing timers list, or creates a new @@ -266,11 +297,28 @@ function listOnTimeout() { // 4.7) what is in this smaller function. function tryOnTimeout(timer, list) { timer._called = true; + const timerAsyncId = (typeof timer[async_id_symbol] === 'number') ? + timer[async_id_symbol] : null; var threw = true; + if (timerAsyncId !== null) + timerEmitBefore(timerAsyncId, timer[trigger_id_symbol]); try { ontimeout(timer); threw = false; } finally { + if (timerAsyncId !== null) { + if (typeof timer._repeat !== 'number') + timer._onTimeout = null; + if (!threw) + timerEmitAfter(timerAsyncId); + if (async_hook_fields[kDestroy] > 0 && + typeof timer._onTimeout === 'function' && + !timer._destroyed) { + emitDestroy(timerAsyncId); + timer._destroyed = true; + } + } + if (!threw) return; // Postpone all later list events to next tick. We need to do this @@ -440,6 +488,15 @@ function rearm(timer) { const clearTimeout = exports.clearTimeout = function(timer) { + // Fewer checks may be possible, but these cover everything. + if (async_hook_fields[kDestroy] > 0 && + timer && + typeof timer[async_id_symbol] === 'number' && + !timer._destroyed) { + emitDestroy(timer[async_id_symbol]); + timer._destroyed = true; + } + if (timer && (timer[kOnTimeout] || timer._onTimeout)) { timer[kOnTimeout] = timer._onTimeout = null; if (timer instanceof Timeout) { @@ -504,6 +561,11 @@ function Timeout(after, callback, args) { this._onTimeout = callback; this._timerArgs = args; this._repeat = null; + this._destroyed = false; + this[async_id_symbol] = ++async_uid_fields[kAsyncUidCntr]; + this[trigger_id_symbol] = initTriggerId(); + if (async_hook_fields[kInit] > 0) + emitInit(this[async_id_symbol], 'Timeout', this[trigger_id_symbol], this); } @@ -661,11 +723,21 @@ function processImmediate() { // 4.7) what is in this smaller function. function tryOnImmediate(immediate, oldTail) { var threw = true; + timerEmitBefore(immediate[async_id_symbol], immediate[trigger_id_symbol]); try { // make the actual call outside the try/catch to allow it to be optimized runCallback(immediate); threw = false; } finally { + // clearImmediate checks _callback === null for kDestroy hooks. + immediate._callback = null; + if (!threw) + timerEmitAfter(immediate[async_id_symbol]); + if (async_hook_fields[kDestroy] > 0 && !immediate._destroyed) { + emitDestroy(immediate[async_id_symbol]); + immediate._destroyed = true; + } + if (threw && immediate._idleNext) { // Handle any remaining on next tick, assuming we're still alive to do so. const curHead = immediateQueue.head; @@ -712,7 +784,12 @@ function Immediate() { this._callback = null; this._argv = null; this._onImmediate = null; + this._destroyed = false; this.domain = process.domain; + this[async_id_symbol] = ++async_uid_fields[kAsyncUidCntr]; + this[trigger_id_symbol] = initTriggerId(); + if (async_hook_fields[kInit] > 0) + emitInit(this[async_id_symbol], 'Immediate', this[trigger_id_symbol], this); } exports.setImmediate = function(callback, arg1, arg2, arg3) { @@ -763,6 +840,13 @@ function createImmediate(args, callback) { exports.clearImmediate = function(immediate) { if (!immediate) return; + if (async_hook_fields[kDestroy] > 0 && + immediate._callback !== null && + !immediate._destroyed) { + emitDestroy(immediate[async_id_symbol]); + immediate._destroyed = true; + } + immediate._onImmediate = null; immediateQueue.remove(immediate); diff --git a/src/async-wrap.cc b/src/async-wrap.cc index 6ccccfcb65c201..06567d6f7ccf39 100644 --- a/src/async-wrap.cc +++ b/src/async-wrap.cc @@ -45,6 +45,7 @@ using v8::MaybeLocal; using v8::Number; using v8::Object; using v8::RetainedObjectInfo; +using v8::Symbol; using v8::TryCatch; using v8::Uint32Array; using v8::Value; @@ -325,6 +326,17 @@ void AsyncWrap::Initialize(Local target, #undef V FORCE_SET_TARGET_FIELD(target, "Providers", async_providers); + // These Symbols are used throughout node so the stored values on each object + // can be accessed easily across files. + FORCE_SET_TARGET_FIELD( + target, + "async_id_symbol", + Symbol::New(isolate, FIXED_ONE_BYTE_STRING(isolate, "asyncId"))); + FORCE_SET_TARGET_FIELD( + target, + "trigger_id_symbol", + Symbol::New(isolate, FIXED_ONE_BYTE_STRING(isolate, "triggerId"))); + #undef FORCE_SET_TARGET_FIELD env->set_async_hooks_init_function(Local()); diff --git a/test/message/timeout_throw.out b/test/message/timeout_throw.out index 9ef4f63e3d8b97..609767a4d15ba6 100644 --- a/test/message/timeout_throw.out +++ b/test/message/timeout_throw.out @@ -2,7 +2,7 @@ undefined_reference_error_maker; ^ ReferenceError: undefined_reference_error_maker is not defined - at Timeout._onTimeout (*test*message*timeout_throw.js:*:*) + at Timeout. (*test*message*timeout_throw.js:*:*) at ontimeout (timers.js:*:*) at tryOnTimeout (timers.js:*:*) at Timer.listOnTimeout (timers.js:*:*) diff --git a/test/message/unhandled_promise_trace_warnings.out b/test/message/unhandled_promise_trace_warnings.out index 603333e64a946f..f495a8612c009e 100644 --- a/test/message/unhandled_promise_trace_warnings.out +++ b/test/message/unhandled_promise_trace_warnings.out @@ -25,7 +25,7 @@ at * at Promise.then * at Promise.catch * - at Immediate.setImmediate (*test*message*unhandled_promise_trace_warnings.js:*) + at Immediate.setImmediate [as _onImmediate] (*test*message*unhandled_promise_trace_warnings.js:*) at * at * at * diff --git a/test/parallel/test-async-wrap-uncaughtexception.js b/test/parallel/test-async-wrap-uncaughtexception.js new file mode 100644 index 00000000000000..099bdb70dd97fe --- /dev/null +++ b/test/parallel/test-async-wrap-uncaughtexception.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const call_log = [0, 0, 0, 0]; // [before, callback, exception, after]; +let call_id = null; +let hooks = null; + + +process.on('beforeExit', common.mustCall(() => { + process.removeAllListeners('uncaughtException'); + hooks.disable(); + assert.strictEqual(typeof call_id, 'number'); + assert.deepStrictEqual(call_log, [1, 1, 1, 1]); +})); + + +hooks = async_hooks.createHook({ + init(id, type) { + if (type === 'RANDOMBYTESREQUEST') + call_id = id; + }, + before(id) { + if (id === call_id) call_log[0]++; + }, + after(id) { + if (id === call_id) call_log[3]++; + }, +}).enable(); + + +process.on('uncaughtException', common.mustCall(() => { + assert.strictEqual(call_id, async_hooks.currentId()); + call_log[2]++; +})); + + +require('crypto').randomBytes(1, common.mustCall(() => { + assert.strictEqual(call_id, async_hooks.currentId()); + call_log[1]++; + throw new Error('ah crap'); +})); From 09b78e0f91e8107ed74c5bc30a94c5c18507107c Mon Sep 17 00:00:00 2001 From: Thorsten Lorenz Date: Tue, 22 Nov 2016 13:13:44 -0300 Subject: [PATCH 10/12] async-hooks: adding tests for initHooks API Async wrap providers tested: - crypto.randomBytes - crypto.pbkdf2 - fs event wrap - fsreqwrap access - fsreqwrap readFile - getaddrinforeq wrap - getnameinforeq wrap - pipe connect wrap - query wrap - pipewrap - processwrap - shutdown wrap - tcpwrap - udpwrap - send wrap - detailed signal wrap - statwatcher - timerwrap via setTimeout - timerwrap via setInterval - for Immediate - http parser request - http parser response - connection via ssl server - tls wrap - write wrap - ttywrap via readstream - ttywrap via wriream - zctx via zlib binding deflate Embedder API: - async-event tests - one test looks at the happy paths - another ensures that in cases of events emitted in an order that doesn't make sense, the order is enforced by async hooks throwing a meaningful error - embedder enforcement tests are split up since async hook stack corruption now the process - therefore we launch a child and check for error output of the offending code Additional tests: - tests that show that we can enable/disable hooks inside their lifetime events - tests that verify the graph of resources triggering the creation of other resources Test Helpers: - init-hooks: - returns one collector instance - when created an async hook is created and the lifetime events are registered to call the appropriate collector functions - the collector also exposes `enable` and `disable` functions which call through to the async hook - hook checks: - checks invocations of life time hooks against the actual invocations that were collected - in some cases like `destroy` a min/max range of invocations can be supplied since in these cases the exact number is non-deterministic - verify graph: - verifies the triggerIds of specific async resources are as expected, i.e. the creation of resources was triggered by the resource we expect - includes a printGraph function to generate easily readable test input for verify graph - both functions prune TickObjects to create less brittle and easier to understand tests --- test/async-hooks/coverage.md | 32 ++ test/async-hooks/hook-checks.js | 54 ++++ test/async-hooks/init-hooks.js | 220 ++++++++++++++ test/async-hooks/test-connection.ssl.js | 90 ++++++ test/async-hooks/test-crypto-pbkdf2.js | 42 +++ test/async-hooks/test-crypto-randomBytes.js | 43 +++ ...dder.api.async-event.after-on-destroyed.js | 48 +++ ...der.api.async-event.before-on-destroyed.js | 48 +++ ...embedder.api.async-event.improper-order.js | 46 +++ ...mbedder.api.async-event.improper-unwind.js | 55 ++++ .../test-embedder.api.async-event.js | 85 ++++++ test/async-hooks/test-enable-disable.js | 274 ++++++++++++++++++ test/async-hooks/test-fseventwrap.js | 33 +++ test/async-hooks/test-fsreqwrap-access.js | 37 +++ test/async-hooks/test-fsreqwrap-readFile.js | 48 +++ test/async-hooks/test-getaddrinforeqwrap.js | 39 +++ test/async-hooks/test-getnameinforeqwrap.js | 40 +++ test/async-hooks/test-graph.connection.js | 56 ++++ test/async-hooks/test-graph.fsreq-readFile.js | 26 ++ test/async-hooks/test-graph.intervals.js | 37 +++ test/async-hooks/test-graph.pipe.js | 32 ++ test/async-hooks/test-graph.pipeconnect.js | 37 +++ test/async-hooks/test-graph.shutdown.js | 44 +++ test/async-hooks/test-graph.signal.js | 54 ++++ test/async-hooks/test-graph.statwatcher.js | 34 +++ test/async-hooks/test-graph.tcp.js | 46 +++ test/async-hooks/test-graph.timeouts.js | 35 +++ test/async-hooks/test-graph.tls-write.js | 74 +++++ test/async-hooks/test-httpparser.request.js | 58 ++++ test/async-hooks/test-httpparser.response.js | 68 +++++ test/async-hooks/test-immediate.js | 66 +++++ test/async-hooks/test-pipeconnectwrap.js | 95 ++++++ test/async-hooks/test-pipewrap.js | 79 +++++ test/async-hooks/test-querywrap.js | 40 +++ test/async-hooks/test-shutdownwrap.js | 69 +++++ test/async-hooks/test-signalwrap.js | 91 ++++++ test/async-hooks/test-statwatcher.js | 64 ++++ test/async-hooks/test-tcpwrap.js | 172 +++++++++++ .../async-hooks/test-timerwrap.setInterval.js | 56 ++++ test/async-hooks/test-timerwrap.setTimeout.js | 78 +++++ test/async-hooks/test-tlswrap.js | 133 +++++++++ test/async-hooks/test-ttywrap.readstream.js | 42 +++ test/async-hooks/test-ttywrap.writestream.js | 62 ++++ test/async-hooks/test-udpsendwrap.js | 58 ++++ test/async-hooks/test-udpwrap.js | 38 +++ test/async-hooks/test-writewrap.js | 98 +++++++ .../test-zlib.zlib-binding.deflate.js | 62 ++++ test/async-hooks/testcfg.py | 6 + test/async-hooks/tick.js | 13 + test/async-hooks/verify-graph.js | 114 ++++++++ test/testpy/__init__.py | 12 + tools/test.py | 1 + 52 files changed, 3284 insertions(+) create mode 100644 test/async-hooks/coverage.md create mode 100644 test/async-hooks/hook-checks.js create mode 100644 test/async-hooks/init-hooks.js create mode 100644 test/async-hooks/test-connection.ssl.js create mode 100644 test/async-hooks/test-crypto-pbkdf2.js create mode 100644 test/async-hooks/test-crypto-randomBytes.js create mode 100644 test/async-hooks/test-embedder.api.async-event.after-on-destroyed.js create mode 100644 test/async-hooks/test-embedder.api.async-event.before-on-destroyed.js create mode 100644 test/async-hooks/test-embedder.api.async-event.improper-order.js create mode 100644 test/async-hooks/test-embedder.api.async-event.improper-unwind.js create mode 100644 test/async-hooks/test-embedder.api.async-event.js create mode 100644 test/async-hooks/test-enable-disable.js create mode 100644 test/async-hooks/test-fseventwrap.js create mode 100644 test/async-hooks/test-fsreqwrap-access.js create mode 100644 test/async-hooks/test-fsreqwrap-readFile.js create mode 100644 test/async-hooks/test-getaddrinforeqwrap.js create mode 100644 test/async-hooks/test-getnameinforeqwrap.js create mode 100644 test/async-hooks/test-graph.connection.js create mode 100644 test/async-hooks/test-graph.fsreq-readFile.js create mode 100644 test/async-hooks/test-graph.intervals.js create mode 100644 test/async-hooks/test-graph.pipe.js create mode 100644 test/async-hooks/test-graph.pipeconnect.js create mode 100644 test/async-hooks/test-graph.shutdown.js create mode 100644 test/async-hooks/test-graph.signal.js create mode 100644 test/async-hooks/test-graph.statwatcher.js create mode 100644 test/async-hooks/test-graph.tcp.js create mode 100644 test/async-hooks/test-graph.timeouts.js create mode 100644 test/async-hooks/test-graph.tls-write.js create mode 100644 test/async-hooks/test-httpparser.request.js create mode 100644 test/async-hooks/test-httpparser.response.js create mode 100644 test/async-hooks/test-immediate.js create mode 100644 test/async-hooks/test-pipeconnectwrap.js create mode 100644 test/async-hooks/test-pipewrap.js create mode 100644 test/async-hooks/test-querywrap.js create mode 100644 test/async-hooks/test-shutdownwrap.js create mode 100644 test/async-hooks/test-signalwrap.js create mode 100644 test/async-hooks/test-statwatcher.js create mode 100644 test/async-hooks/test-tcpwrap.js create mode 100644 test/async-hooks/test-timerwrap.setInterval.js create mode 100644 test/async-hooks/test-timerwrap.setTimeout.js create mode 100644 test/async-hooks/test-tlswrap.js create mode 100644 test/async-hooks/test-ttywrap.readstream.js create mode 100644 test/async-hooks/test-ttywrap.writestream.js create mode 100644 test/async-hooks/test-udpsendwrap.js create mode 100644 test/async-hooks/test-udpwrap.js create mode 100644 test/async-hooks/test-writewrap.js create mode 100644 test/async-hooks/test-zlib.zlib-binding.deflate.js create mode 100644 test/async-hooks/testcfg.py create mode 100644 test/async-hooks/tick.js create mode 100644 test/async-hooks/verify-graph.js diff --git a/test/async-hooks/coverage.md b/test/async-hooks/coverage.md new file mode 100644 index 00000000000000..461d5137e594da --- /dev/null +++ b/test/async-hooks/coverage.md @@ -0,0 +1,32 @@ +## AsyncHooks Coverage Overview + +Showing which kind of async resource is covered by which test: + +| Resource Type | Test | +|----------------------|----------------------------------------| +| CONNECTION | test-connection.ssl.js | +| FSEVENTWRAP | test-fseventwrap.js | +| FSREQWRAP | test-fsreqwrap-{access,readFile}.js | +| GETADDRINFOREQWRAP | test-getaddrinforeqwrap.js | +| GETNAMEINFOREQWRAP | test-getnameinforeqwrap.js | +| HTTPPARSER | test-httpparser.{request,response}.js | +| Immediate | test-immediate.js | +| JSSTREAM | TODO (crashes when accessing directly) | +| PBKDF2REQUEST | test-crypto-pbkdf2.js | +| PIPECONNECTWRAP | test-pipeconnectwrap.js | +| PIPEWRAP | test-pipewrap.js | +| PROCESSWRAP | test-pipewrap.js | +| QUERYWRAP | test-querywrap.js | +| RANDOMBYTESREQUEST | test-crypto-randomBytes.js | +| SHUTDOWNWRAP | test-shutdownwrap.js | +| SIGNALWRAP | test-signalwrap.js | +| STATWATCHER | test-statwatcher.js | +| TCPCONNECTWRAP | test-tcpwrap.js | +| TCPWRAP | test-tcpwrap.js | +| TIMERWRAP | test-timerwrap.set{Timeout,Interval}.js| +| TLSWRAP | test-tlswrap.js | +| TTYWRAP | test-ttywrap.{read,write}stream.js | +| UDPSENDWRAP | test-udpsendwrap.js | +| UDPWRAP | test-udpwrap.js | +| WRITEWRAP | test-writewrap.js | +| ZLIB | test-zlib.zlib-binding.deflate.js | diff --git a/test/async-hooks/hook-checks.js b/test/async-hooks/hook-checks.js new file mode 100644 index 00000000000000..60f505a24a95de --- /dev/null +++ b/test/async-hooks/hook-checks.js @@ -0,0 +1,54 @@ +'use strict'; +const assert = require('assert'); +require('../common'); + +/** + * Checks the expected invocations against the invocations that actually + * occurred. + * + * @name checkInvocations + * @function + * @param {Object} activity including timestamps for each life time event, + * i.e. init, before ... + * @param {Object} hooks the expected life time event invocations with a count + * indicating how oftn they should have been invoked, + * i.e. `{ init: 1, before: 2, after: 2 }` + * @param {String} stage the name of the stage in the test at which we are + * checking the invocations + */ +exports.checkInvocations = function checkInvocations(activity, hooks, stage) { + const stageInfo = `Checking invocations at stage "${stage}":\n `; + + assert.ok(activity != null, + `${stageInfo} Trying to check invocation for an activity, ` + + 'but it was empty/undefined.' + ); + + // Check that actual invocations for all hooks match the expected invocations + [ 'init', 'before', 'after', 'destroy' ].forEach(checkHook); + + function checkHook(k) { + const val = hooks[k]; + // Not expected ... all good + if (val == null) return; + + if (val === 0) { + // Didn't expect any invocations, but it was actually invoked + const invocations = activity[k].length; + const msg = `${stageInfo} Called "${k}" ${invocations} time(s), ` + + 'but expected no invocations.'; + assert(activity[k] === null && activity[k] === undefined, msg); + } else { + // Expected some invocations, make sure that it was invoked at all + const msg1 = `${stageInfo} Never called "${k}", ` + + `but expected ${val} invocation(s).`; + assert(activity[k] !== null && activity[k] !== undefined, msg1); + + // Now make sure that the expected count and + // the actual invocation count match + const msg2 = `${stageInfo} Called "${k}" ${activity[k].length} ` + + `time(s), but expected ${val} invocation(s).`; + assert.strictEqual(activity[k].length, val, msg2); + } + } +}; diff --git a/test/async-hooks/init-hooks.js b/test/async-hooks/init-hooks.js new file mode 100644 index 00000000000000..ce0a24db7d387c --- /dev/null +++ b/test/async-hooks/init-hooks.js @@ -0,0 +1,220 @@ +'use strict'; +// Flags: --expose-gc + +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const util = require('util'); +const print = process._rawDebug; +require('../common'); + +if (typeof global.gc === 'function') { + (function exity(cntr) { + cntr = 1 + (cntr >>> 0); + process.once('beforeExit', () => { + global.gc(); + if (++cntr < 2) setImmediate(exity); + }); + })(); +} + +function noop() {} + +class ActivityCollector { + constructor(start, { + allowNoInit = false, + oninit, + onbefore, + onafter, + ondestroy, + logid = null, + logtype = null + } = {}) { + this._start = start; + this._allowNoInit = allowNoInit; + this._activities = new Map(); + this._logid = logid; + this._logtype = logtype; + + // register event handlers if provided + this.oninit = typeof oninit === 'function' ? oninit : noop; + this.onbefore = typeof onbefore === 'function' ? onbefore : noop; + this.onafter = typeof onafter === 'function' ? onafter : noop; + this.ondestroy = typeof ondestroy === 'function' ? ondestroy : noop; + + // create the hook with which we'll collect activity data + this._asyncHook = async_hooks.createHook({ + init: this._init.bind(this), + before: this._before.bind(this), + after: this._after.bind(this), + destroy: this._destroy.bind(this) + }); + } + + enable() { + this._asyncHook.enable(); + } + + disable() { + this._asyncHook.disable(); + } + + sanityCheck(types) { + if (types != null && !Array.isArray(types)) types = [ types ]; + + function activityString(a) { + return util.inspect(a, false, 5, true); + } + + const violations = []; + function v(msg) { violations.push(msg); } + for (const a of this._activities.values()) { + if (types != null && types.indexOf(a.type) < 0) continue; + + if (a.init && a.init.length > 1) { + v('Activity inited twice\n' + activityString(a) + + '\nExpected "init" to be called at most once'); + } + if (a.destroy && a.destroy.length > 1) { + v('Activity destroyed twice\n' + activityString(a) + + '\nExpected "destroy" to be called at most once'); + } + if (a.before && a.after) { + if (a.before.length < a.after.length) { + v('Activity called "after" without calling "before"\n' + + activityString(a) + + '\nExpected no "after" call without a "before"'); + } + if (a.before.some((x, idx) => x > a.after[idx])) { + v('Activity had an instance where "after" ' + + 'was invoked before "before"\n' + + activityString(a) + + '\nExpected "after" to be called after "before"'); + } + } + if (a.before && a.destroy) { + if (a.before.some((x, idx) => x > a.destroy[idx])) { + v('Activity had an instance where "destroy" ' + + 'was invoked before "before"\n' + + activityString(a) + + '\nExpected "destroy" to be called after "before"'); + } + } + if (a.after && a.destroy) { + if (a.after.some((x, idx) => x > a.destroy[idx])) { + v('Activity had an instance where "destroy" ' + + 'was invoked before "after"\n' + + activityString(a) + + '\nExpected "destroy" to be called after "after"'); + } + } + } + if (violations.length) { + console.error(violations.join('\n')); + assert.fail(violations.length, 0, 'Failed sanity check'); + } + } + + inspect(opts = {}) { + if (typeof opts === 'string') opts = { types: opts }; + const { types = null, depth = 5, stage = null } = opts; + const activities = types == null ? + Array.from(this._activities.values()) : + this.activitiesOfTypes(types); + + if (stage != null) console.log('\n%s', stage); + console.log(util.inspect(activities, false, depth, true)); + } + + activitiesOfTypes(types) { + if (!Array.isArray(types)) types = [ types ]; + return this.activities.filter((x) => types.indexOf(x.type) >= 0); + } + + get activities() { + return Array.from(this._activities.values()); + } + + _stamp(h, hook) { + if (h == null) return; + if (h[hook] == null) h[hook] = []; + const time = process.hrtime(this._start); + h[hook].push((time[0] * 1e9) + time[1]); + } + + _getActivity(uid, hook) { + const h = this._activities.get(uid); + if (!h) { + // if we allowed handles without init we ignore any further life time + // events this makes sense for a few tests in which we enable some hooks + // later + if (this._allowNoInit) { + const stub = { uid, type: 'Unknown' }; + this._activities.set(uid, stub); + return stub; + } else { + const err = new Error('Found a handle who\'s ' + hook + + ' hook was invoked but not it\'s init hook'); + // Don't throw if we see invocations due to an assertion in a test + // failing since we want to list the assertion failure instead + if (/process\._fatalException/.test(err.stack)) return null; + throw err; + } + } + return h; + } + + _init(uid, type, triggerId, handle) { + const activity = { uid, type, triggerId }; + this._stamp(activity, 'init'); + this._activities.set(uid, activity); + this._maybeLog(uid, type, 'init'); + this.oninit(uid, type, triggerId, handle); + } + + _before(uid) { + const h = this._getActivity(uid, 'before'); + this._stamp(h, 'before'); + this._maybeLog(uid, h && h.type, 'before'); + this.onbefore(uid); + } + + _after(uid) { + const h = this._getActivity(uid, 'after'); + this._stamp(h, 'after'); + this._maybeLog(uid, h && h.type, 'after'); + this.onafter(uid); + } + + _destroy(uid) { + const h = this._getActivity(uid, 'destroy'); + this._stamp(h, 'destroy'); + this._maybeLog(uid, h && h.type, 'destroy'); + this.ondestroy(uid); + } + + _maybeLog(uid, type, name) { + if (this._logid && + (type == null || this._logtype == null || this._logtype === type)) { + print(this._logid + '.' + name + '.uid-' + uid); + } + } +} + +exports = module.exports = function initHooks({ + oninit, + onbefore, + onafter, + ondestroy, + allowNoInit, + logid, + logtype } = {}) { + return new ActivityCollector(process.hrtime(), { + oninit, + onbefore, + onafter, + ondestroy, + allowNoInit, + logid, + logtype + }); +}; diff --git a/test/async-hooks/test-connection.ssl.js b/test/async-hooks/test-connection.ssl.js new file mode 100644 index 00000000000000..69e31d85df09fc --- /dev/null +++ b/test/async-hooks/test-connection.ssl.js @@ -0,0 +1,90 @@ +'use strict'; + +const initHooks = require('./init-hooks'); +const tick = require('./tick'); +const common = require('../common'); +const assert = require('assert'); +const { checkInvocations } = require('./hook-checks'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); + return; +} + +const tls = require('tls'); +const Connection = process.binding('crypto').Connection; +const hooks = initHooks(); +hooks.enable(); + +function createServerConnection( + onhandshakestart, + certificate = null, + isServer = true, + servername = 'some server', + rejectUnauthorized +) { + if (certificate == null) certificate = tls.createSecureContext(); + const ssl = new Connection( + certificate.context, isServer, servername, rejectUnauthorized + ); + if (isServer) { + ssl.onhandshakestart = onhandshakestart; + ssl.lastHandshakeTime = 0; + } + return ssl; +} + +// creating first server connection +const sc1 = createServerConnection(common.mustCall(onfirstHandShake)); + +let as = hooks.activitiesOfTypes('CONNECTION'); +assert.strictEqual(as.length, 1, + 'one CONNECTION after first connection created'); +const f1 = as[0]; +assert.strictEqual(f1.type, 'CONNECTION', 'connection'); +assert.strictEqual(typeof f1.uid, 'number', 'uid is a number'); +assert.strictEqual(typeof f1.triggerId, 'number', 'triggerId is a number'); +checkInvocations(f1, { init: 1 }, 'first connection, when first created'); + +// creating second server connection +const sc2 = createServerConnection(common.mustCall(onsecondHandShake)); + +as = hooks.activitiesOfTypes('CONNECTION'); +assert.strictEqual(as.length, 2, + 'two CONNECTIONs after second connection created'); +const f2 = as[1]; +assert.strictEqual(f2.type, 'CONNECTION', 'connection'); +assert.strictEqual(typeof f2.uid, 'number', 'uid is a number'); +assert.strictEqual(typeof f2.triggerId, 'number', 'triggerId is a number'); +checkInvocations(f1, { init: 1 }, 'first connection, when second created'); +checkInvocations(f2, { init: 1 }, 'second connection, when second created'); + +// starting the connections which results in handshake starts +sc1.start(); +sc2.start(); + +function onfirstHandShake() { + checkInvocations(f1, { init: 1, before: 1 }, + 'first connection, when first handshake'); + checkInvocations(f2, { init: 1 }, 'second connection, when first handshake'); +} + +function onsecondHandShake() { + checkInvocations(f1, { init: 1, before: 1, after: 1 }, + 'first connection, when second handshake'); + checkInvocations(f2, { init: 1, before: 1 }, + 'second connection, when second handshake'); + tick(1E4); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('CONNECTION'); + + checkInvocations(f1, { init: 1, before: 1, after: 1 }, + 'first connection, when process exits'); + checkInvocations(f2, { init: 1, before: 1, after: 1 }, + 'second connection, when process exits'); +} diff --git a/test/async-hooks/test-crypto-pbkdf2.js b/test/async-hooks/test-crypto-pbkdf2.js new file mode 100644 index 00000000000000..f2a0d208ed4ccb --- /dev/null +++ b/test/async-hooks/test-crypto-pbkdf2.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const crypto = require('crypto'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); + return; +} + +const hooks = initHooks(); + +hooks.enable(); + +crypto.pbkdf2('password', 'salt', 1, 20, 'sha256', common.mustCall(onpbkdf2)); + +function onpbkdf2() { + const as = hooks.activitiesOfTypes('PBKDF2REQUEST'); + const a = as[0]; + checkInvocations(a, { init: 1, before: 1 }, 'while in onpbkdf2 callback'); + tick(2); +} + +process.on('exit', onexit); +function onexit() { + hooks.disable(); + hooks.sanityCheck('PBKDF2REQUEST'); + + const as = hooks.activitiesOfTypes('PBKDF2REQUEST'); + assert.strictEqual(as.length, 1, 'one activity'); + + const a = as[0]; + assert.strictEqual(a.type, 'PBKDF2REQUEST', 'random byte request'); + assert.strictEqual(typeof a.uid, 'number', 'uid is a number'); + assert.strictEqual(a.triggerId, 1, 'parent uid 1'); + checkInvocations(a, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-crypto-randomBytes.js b/test/async-hooks/test-crypto-randomBytes.js new file mode 100644 index 00000000000000..0b89fcc09c7794 --- /dev/null +++ b/test/async-hooks/test-crypto-randomBytes.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const crypto = require('crypto'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); + return; +} + +const hooks = initHooks(); + +hooks.enable(); +crypto.randomBytes(1, common.mustCall(onrandomBytes)); + +function onrandomBytes() { + const as = hooks.activitiesOfTypes('RANDOMBYTESREQUEST'); + const a = as[0]; + checkInvocations(a, { init: 1, before: 1 }, + 'while in onrandomBytes callback'); + tick(2); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('RANDOMBYTESREQUEST'); + + const as = hooks.activitiesOfTypes('RANDOMBYTESREQUEST'); + assert.strictEqual(as.length, 1, 'one activity'); + + const a = as[0]; + assert.strictEqual(a.type, 'RANDOMBYTESREQUEST', 'random byte request'); + assert.strictEqual(typeof a.uid, 'number', 'uid is a number'); + assert.strictEqual(a.triggerId, 1, 'parent uid 1'); + checkInvocations(a, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-embedder.api.async-event.after-on-destroyed.js b/test/async-hooks/test-embedder.api.async-event.after-on-destroyed.js new file mode 100644 index 00000000000000..c5d52a362d0263 --- /dev/null +++ b/test/async-hooks/test-embedder.api.async-event.after-on-destroyed.js @@ -0,0 +1,48 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const { AsyncEvent } = async_hooks; +const { spawn } = require('child_process'); +const corruptedMsg = /async hook stack has become corrupted/; +const heartbeatMsg = /heartbeat: still alive/; + +const initHooks = require('./init-hooks'); + +if (process.argv[2] === 'child') { + const hooks = initHooks(); + hooks.enable(); + + // once 'destroy' has been emitted, we can no longer emit 'after' + + // Emitting 'before', 'after' and then 'destroy' + const event1 = new AsyncEvent('event1', async_hooks.currentId()); + event1.emitBefore(); + event1.emitAfter(); + event1.emitDestroy(); + + // Emitting 'after' after 'destroy' + const event2 = new AsyncEvent('event2', async_hooks.currentId()); + event2.emitDestroy(); + + console.log('heartbeat: still alive'); + event2.emitAfter(); + +} else { + const args = process.argv.slice(1).concat('child'); + let errData = Buffer.from(''); + let outData = Buffer.from(''); + + const child = spawn(process.execPath, args); + child.stderr.on('data', (d) => { errData = Buffer.concat([ errData, d ]); }); + child.stdout.on('data', (d) => { outData = Buffer.concat([ outData, d ]); }); + + child.on('close', common.mustCall((code) => { + assert.strictEqual(code, 1, 'exit code 1'); + assert.ok(heartbeatMsg.test(outData.toString()), + 'did not crash until we reached offending line of code'); + assert.ok(corruptedMsg.test(errData.toString()), + 'printed error contains corrupted message'); + })); +} diff --git a/test/async-hooks/test-embedder.api.async-event.before-on-destroyed.js b/test/async-hooks/test-embedder.api.async-event.before-on-destroyed.js new file mode 100644 index 00000000000000..4cd98b2561dc53 --- /dev/null +++ b/test/async-hooks/test-embedder.api.async-event.before-on-destroyed.js @@ -0,0 +1,48 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const { AsyncEvent } = async_hooks; +const { spawn } = require('child_process'); +const corruptedMsg = /async hook stack has become corrupted/; +const heartbeatMsg = /heartbeat: still alive/; + +const initHooks = require('./init-hooks'); + +if (process.argv[2] === 'child') { + const hooks = initHooks(); + hooks.enable(); + + // once 'destroy' has been emitted, we can no longer emit 'before' + + // Emitting 'before', 'after' and then 'destroy' + const event1 = new AsyncEvent('event1', async_hooks.currentId()); + event1.emitBefore(); + event1.emitAfter(); + event1.emitDestroy(); + + // Emitting 'before' after 'destroy' + const event2 = new AsyncEvent('event2', async_hooks.currentId()); + event2.emitDestroy(); + + console.log('heartbeat: still alive'); + event2.emitBefore(); + +} else { + const args = process.argv.slice(1).concat('child'); + let errData = Buffer.from(''); + let outData = Buffer.from(''); + + const child = spawn(process.execPath, args); + child.stderr.on('data', (d) => { errData = Buffer.concat([ errData, d ]); }); + child.stdout.on('data', (d) => { outData = Buffer.concat([ outData, d ]); }); + + child.on('close', common.mustCall((code) => { + assert.strictEqual(code, 1, 'exit code 1'); + assert.ok(heartbeatMsg.test(outData.toString()), + 'did not crash until we reached offending line of code'); + assert.ok(corruptedMsg.test(errData.toString()), + 'printed error contains corrupted message'); + })); +} diff --git a/test/async-hooks/test-embedder.api.async-event.improper-order.js b/test/async-hooks/test-embedder.api.async-event.improper-order.js new file mode 100644 index 00000000000000..841ca1d1363c4c --- /dev/null +++ b/test/async-hooks/test-embedder.api.async-event.improper-order.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const { AsyncEvent } = async_hooks; +const { spawn } = require('child_process'); +const corruptedMsg = /async hook stack has become corrupted/; +const heartbeatMsg = /heartbeat: still alive/; + +const initHooks = require('./init-hooks'); + +if (process.argv[2] === 'child') { + const hooks = initHooks(); + hooks.enable(); + + // async hooks enforce proper order of 'before' and 'after' invocations + + // Proper ordering + const event1 = new AsyncEvent('event1', async_hooks.currentId()); + event1.emitBefore(); + event1.emitAfter(); + + // Improper ordering + // Emitting 'after' without 'before' which is illegal + const event2 = new AsyncEvent('event2', async_hooks.currentId()); + + console.log('heartbeat: still alive'); + event2.emitAfter(); +} else { + const args = process.argv.slice(1).concat('child'); + let errData = Buffer.from(''); + let outData = Buffer.from(''); + + const child = spawn(process.execPath, args); + child.stderr.on('data', (d) => { errData = Buffer.concat([ errData, d ]); }); + child.stdout.on('data', (d) => { outData = Buffer.concat([ outData, d ]); }); + + child.on('close', common.mustCall((code) => { + assert.strictEqual(code, 1, 'exit code 1'); + assert.ok(heartbeatMsg.test(outData.toString()), + 'did not crash until we reached offending line of code'); + assert.ok(corruptedMsg.test(errData.toString()), + 'printed error contains corrupted message'); + })); +} diff --git a/test/async-hooks/test-embedder.api.async-event.improper-unwind.js b/test/async-hooks/test-embedder.api.async-event.improper-unwind.js new file mode 100644 index 00000000000000..fe58185c79e565 --- /dev/null +++ b/test/async-hooks/test-embedder.api.async-event.improper-unwind.js @@ -0,0 +1,55 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const { AsyncEvent } = async_hooks; +const { spawn } = require('child_process'); +const corruptedMsg = /async hook stack has become corrupted/; +const heartbeatMsg = /heartbeat: still alive/; + +const initHooks = require('./init-hooks'); + +if (process.argv[2] === 'child') { + const hooks = initHooks(); + hooks.enable(); + + // In both the below two cases 'before' of event2 is nested inside 'before' + // of event1. + // Therefore the 'after' of event2 needs to occur before the + // 'after' of event 1. + // The first test of the two below follows that rule, + // the second one doesnt. + + const event1 = new AsyncEvent('event1', async_hooks.currentId()); + const event2 = new AsyncEvent('event2', async_hooks.currentId()); + + // Proper unwind + event1.emitBefore(); + event2.emitBefore(); + event2.emitAfter(); + event1.emitAfter(); + + // Improper unwind + event1.emitBefore(); + event2.emitBefore(); + + console.log('heartbeat: still alive'); + event1.emitAfter(); +} else { + const args = process.argv.slice(1).concat('child'); + let errData = Buffer.from(''); + let outData = Buffer.from(''); + + const child = spawn(process.execPath, args); + child.stderr.on('data', (d) => { errData = Buffer.concat([ errData, d ]); }); + child.stdout.on('data', (d) => { outData = Buffer.concat([ outData, d ]); }); + + child.on('close', common.mustCall((code) => { + assert.strictEqual(code, 1, 'exit code 1'); + assert.ok(heartbeatMsg.test(outData.toString()), + 'did not crash until we reached offending line of code'); + assert.ok(corruptedMsg.test(errData.toString()), + 'printed error contains corrupted message'); + })); +} diff --git a/test/async-hooks/test-embedder.api.async-event.js b/test/async-hooks/test-embedder.api.async-event.js new file mode 100644 index 00000000000000..dd9a8a8191ee36 --- /dev/null +++ b/test/async-hooks/test-embedder.api.async-event.js @@ -0,0 +1,85 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const async_hooks = require('async_hooks'); +const { AsyncEvent } = async_hooks; + +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); + +const hooks = initHooks(); +hooks.enable(); + +// create first custom event 'alcazares' with triggerId derived +// from async_hooks currentId +const alcaTriggerId = async_hooks.currentId(); +const alcaEvent = new AsyncEvent('alcazares', alcaTriggerId); +const alcazaresActivities = hooks.activitiesOfTypes([ 'alcazares' ]); + +// alcazares event was constructed and thus only has an `init` call +assert.strictEqual(alcazaresActivities.length, 1, + 'one alcazares activity after one was constructed'); +const alcazares = alcazaresActivities[0]; +assert.strictEqual(alcazares.type, 'alcazares', 'alcazares'); +assert.strictEqual(typeof alcazares.uid, 'number', 'uid is a number'); +assert.strictEqual(alcazares.triggerId, alcaTriggerId, + 'triggerId is the one supplied'); +checkInvocations(alcazares, { init: 1 }, 'alcazares constructed'); + +alcaEvent.emitBefore(); +checkInvocations(alcazares, { init: 1, before: 1 }, + 'alcazares emitted before'); +alcaEvent.emitAfter(); +checkInvocations(alcazares, { init: 1, before: 1, after: 1 }, + 'alcazares emitted after'); +alcaEvent.emitBefore(); +checkInvocations(alcazares, { init: 1, before: 2, after: 1 }, + 'alcazares emitted before again'); +alcaEvent.emitAfter(); +checkInvocations(alcazares, { init: 1, before: 2, after: 2 }, + 'alcazares emitted after again'); +alcaEvent.emitDestroy(); +tick(1, common.mustCall(tick1)); + +function tick1() { + checkInvocations(alcazares, { init: 1, before: 2, after: 2, destroy: 1 }, + 'alcazares emitted destroy'); + + // The below shows that we can pass any number as a trigger id + const pobTriggerId = 111; + const pobEvent = new AsyncEvent('poblado', pobTriggerId); + const pobladoActivities = hooks.activitiesOfTypes([ 'poblado' ]); + const poblado = pobladoActivities[0]; + assert.strictEqual(poblado.type, 'poblado', 'poblado'); + assert.strictEqual(typeof poblado.uid, 'number', 'uid is a number'); + assert.strictEqual(poblado.triggerId, pobTriggerId, + 'triggerId is the one supplied'); + checkInvocations(poblado, { init: 1 }, 'poblado constructed'); + pobEvent.emitBefore(); + checkInvocations(poblado, { init: 1, before: 1 }, + 'poblado emitted before'); + + pobEvent.emitAfter(); + checkInvocations(poblado, { init: 1, before: 1, after: 1 }, + 'poblado emitted after'); + + // after we disable the hooks we shouldn't receive any events anymore + hooks.disable(); + alcaEvent.emitDestroy(); + tick(1, common.mustCall(tick2)); + + function tick2() { + checkInvocations( + alcazares, { init: 1, before: 2, after: 2, destroy: 1 }, + 'alcazares emitted destroy a second time after hooks disabled'); + pobEvent.emitDestroy(); + tick(1, common.mustCall(tick3)); + } + + function tick3() { + checkInvocations(poblado, { init: 1, before: 1, after: 1 }, + 'poblado emitted destroy after hooks disabled'); + } +} diff --git a/test/async-hooks/test-enable-disable.js b/test/async-hooks/test-enable-disable.js new file mode 100644 index 00000000000000..555baf3fcf82cb --- /dev/null +++ b/test/async-hooks/test-enable-disable.js @@ -0,0 +1,274 @@ +/* + * Test Steps Explained + * ==================== + * + * Initializing hooks: + * + * We initialize 3 hooks. For hook2 and hook3 we register a callback for the + * "before" and in case of hook3 also for the "after" invocations. + * + * Enabling hooks initially: + * + * We only enable hook1 and hook3 initially. + * + * Enabling hook2: + * + * When hook3's "before" invocation occurs we enable hook2. Since this + * happens right before calling `onfirstImmediate` hook2 will miss all hook + * invocations until then, including the "init" and "before" of the first + * Immediate. + * However afterwards it collects all invocations that follow on the first + * Immediate as well as all invocations on the second Immediate. + * + * This shows that a hook can enable another hook inside a life time event + * callback. + * + * + * Disabling hook1 + * + * Since we registered the "before" callback for hook2 it will execute it + * right before `onsecondImmediate` is called. + * At that point we disable hook1 which is why it will miss all invocations + * afterwards and thus won't include the second "after" as well as the + * "destroy" invocations + * + * This shows that a hook can disable another hook inside a life time event + * callback. + * + * Disabling hook3 + * + * When the second "after" invocation occurs (after onsecondImmediate), hook3 + * disables itself. + * As a result it will not receive the "destroy" invocation. + * + * This shows that a hook can disable itself inside a life time event callback. + * + * Sample Test Log + * =============== + * + * - setting up first Immediate + * hook1.init.uid-5 + * hook3.init.uid-5 + * - finished setting first Immediate + + * hook1.before.uid-5 + * hook3.before.uid-5 + * - enabled hook2 + * - entering onfirstImmediate + + * - setting up second Immediate + * hook1.init.uid-6 + * hook3.init.uid-6 + * hook2.init.uid-6 + * - finished setting second Immediate + + * - exiting onfirstImmediate + * hook1.after.uid-5 + * hook3.after.uid-5 + * hook2.after.uid-5 + * hook1.destroy.uid-5 + * hook3.destroy.uid-5 + * hook2.destroy.uid-5 + * hook1.before.uid-6 + * hook3.before.uid-6 + * hook2.before.uid-6 + * - disabled hook1 + * - entering onsecondImmediate + * - exiting onsecondImmediate + * hook3.after.uid-6 + * - disabled hook3 + * hook2.after.uid-6 + * hook2.destroy.uid-6 + */ + +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +// Include "Unknown"s because hook2 will not be able to identify +// the type of the first Immediate since it will miss its `init` invocation. +const types = [ 'Immediate', 'Unknown' ]; + +// +// Initializing hooks +// +const hook1 = initHooks(); +const hook2 = initHooks({ onbefore: onhook2Before, allowNoInit: true }); +const hook3 = initHooks({ onbefore: onhook3Before, onafter: onhook3After }); + +// +// Enabling hook1 and hook3 only, hook2 is still disabled +// +hook1.enable(); +hook3.enable(); + +// +// Enabling hook2 +// +let enabledHook2 = false; +function onhook3Before() { + if (enabledHook2) return; + hook2.enable(); + enabledHook2 = true; +} + +// +// Disabling hook1 +// +let disabledHook3 = false; +function onhook2Before() { + if (disabledHook3) return; + hook1.disable(); + disabledHook3 = true; +} + +// +// Disabling hook3 during the second "after" invocations it sees +// +let count = 2; +function onhook3After() { + if (!--count) { + hook3.disable(); + } +} + +setImmediate(common.mustCall(onfirstImmediate)); + +// +// onfirstImmediate is called after all "init" and "before" callbacks of the +// active hooks were invoked +// +function onfirstImmediate() { + const as1 = hook1.activitiesOfTypes(types); + const as2 = hook2.activitiesOfTypes(types); + const as3 = hook3.activitiesOfTypes(types); + assert.strictEqual(as1.length, 1, + 'hook1 captured one immediate on first callback'); + // hook2 was not enabled yet .. it is enabled after hook3's "before" completed + assert.strictEqual(as2.length, 0, + 'hook2 captured no immediate on first callback'); + assert.strictEqual(as3.length, 1, + 'hook3 captured one immediate on first callback'); + + // Check that hook1 and hook3 captured the same Immediate and that it is valid + const firstImmediate = as1[0]; + assert.strictEqual(as3[0].uid, as1[0].uid, + 'hook1 and hook3 captured same first immediate'); + assert.strictEqual(firstImmediate.type, 'Immediate', 'immediate'); + assert.strictEqual(typeof firstImmediate.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof firstImmediate.triggerId, + 'number', 'triggerId is a number'); + checkInvocations(as1[0], { init: 1, before: 1 }, + 'hook1[0]: on first immediate'); + checkInvocations(as3[0], { init: 1, before: 1 }, + 'hook3[0]: on first immediate'); + + // Setup the second Immediate, note that now hook2 is enabled and thus + // will capture all lifetime events of this Immediate + setImmediate(common.mustCall(onsecondImmediate)); +} + +// +// Once we exit onfirstImmediate the "after" callbacks of the active hooks are +// invoked +// + +let hook1First, hook2First, hook3First; +let hook1Second, hook2Second, hook3Second; + +// +// onsecondImmediate is called after all "before" callbacks of the active hooks +// are invoked again +// +function onsecondImmediate() { + const as1 = hook1.activitiesOfTypes(types); + const as2 = hook2.activitiesOfTypes(types); + const as3 = hook3.activitiesOfTypes(types); + assert.strictEqual( + as1.length, 2, + 'hook1 captured first and second immediate on second callback'); + assert.strictEqual( + as2.length, 2, + 'hook2 captured first and second immediate on second callback'); + assert.strictEqual( + as3.length, 2, + 'hook3 captured first and second immediate on second callback'); + + // Assign the info collected by each hook for each immediate for easier + // reference. + // hook2 saw the "init" of the second immediate before the + // "after" of the first which is why they are ordered the opposite way + hook1First = as1[0]; + hook1Second = as1[1]; + hook2First = as2[1]; + hook2Second = as2[0]; + hook3First = as3[0]; + hook3Second = as3[1]; + + // Check that all hooks captured the same Immediate and that it is valid + const secondImmediate = hook1Second; + assert.strictEqual(hook2Second.uid, hook3Second.uid, + 'hook2 and hook3 captured same second immediate'); + assert.strictEqual(hook1Second.uid, hook3Second.uid, + 'hook1 and hook3 captured same second immediate'); + assert.strictEqual(secondImmediate.type, 'Immediate', 'immediate'); + assert.strictEqual(typeof secondImmediate.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof secondImmediate.triggerId, 'number', + 'triggerId is a number'); + + checkInvocations(hook1First, { init: 1, before: 1, after: 1, destroy: 1 }, + 'hook1First: on second immediate'); + checkInvocations(hook1Second, { init: 1, before: 1 }, + 'hook1Second: on second immediate'); + // hook2 missed the "init" and "before" since it was enabled after they + // occurred + checkInvocations(hook2First, { after: 1, destroy: 1 }, + 'hook2First: on second immediate'); + checkInvocations(hook2Second, { init: 1, before: 1 }, + 'hook2Second: on second immediate'); + checkInvocations(hook3First, { init: 1, before: 1, after: 1, destroy: 1 }, + 'hook3First: on second immediate'); + checkInvocations(hook3Second, { init: 1, before: 1 }, + 'hook3Second: on second immediate'); + tick(1); +} + +// +// Once we exit onsecondImmediate the "after" callbacks of the active hooks are +// invoked again. +// During this second "after" invocation hook3 disables itself +// (see onhook3After). +// + +process.on('exit', onexit); + +function onexit() { + hook1.disable(); + hook2.disable(); + hook3.disable(); + hook1.sanityCheck(); + hook2.sanityCheck(); + hook3.sanityCheck(); + + checkInvocations(hook1First, { init: 1, before: 1, after: 1, destroy: 1 }, + 'hook1First: when process exits'); + // hook1 was disabled during hook2's "before" of the second immediate + // and thus did not see "after" and "destroy" + checkInvocations(hook1Second, { init: 1, before: 1 }, + 'hook1Second: when process exits'); + // hook2 missed the "init" and "before" since it was enabled after they + // occurred + checkInvocations(hook2First, { after: 1, destroy: 1 }, + 'hook2First: when process exits'); + checkInvocations(hook2Second, { init: 1, before: 1, after: 1, destroy: 1 }, + 'hook2Second: when process exits'); + checkInvocations(hook3First, { init: 1, before: 1, after: 1, destroy: 1 }, + 'hook3First: when process exits'); + // we don't see a "destroy" invocation here since hook3 disabled itself + // during its "after" invocation + checkInvocations(hook3Second, { init: 1, before: 1, after: 1 }, + 'hook3Second: when process exits'); +} diff --git a/test/async-hooks/test-fseventwrap.js b/test/async-hooks/test-fseventwrap.js new file mode 100644 index 00000000000000..39e055005729dc --- /dev/null +++ b/test/async-hooks/test-fseventwrap.js @@ -0,0 +1,33 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const initHooks = require('./init-hooks'); +const tick = require('./tick'); +const { checkInvocations } = require('./hook-checks'); +const fs = require('fs'); + +const hooks = initHooks(); + +hooks.enable(); +const watcher = fs.watch(__dirname, onwatcherChanged); +function onwatcherChanged() { } + +watcher.close(); +tick(2); + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('FSEVENTWRAP'); + + const as = hooks.activitiesOfTypes('FSEVENTWRAP'); + assert.strictEqual(as.length, 1, 'one activity'); + + const a = as[0]; + assert.strictEqual(a.type, 'FSEVENTWRAP', 'fs event wrap'); + assert.strictEqual(typeof a.uid, 'number', 'uid is a number'); + assert.strictEqual(a.triggerId, 1, 'parent uid 1'); + checkInvocations(a, { init: 1, destroy: 1 }, 'when process exits'); +} diff --git a/test/async-hooks/test-fsreqwrap-access.js b/test/async-hooks/test-fsreqwrap-access.js new file mode 100644 index 00000000000000..5e4c3806c2be8d --- /dev/null +++ b/test/async-hooks/test-fsreqwrap-access.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const fs = require('fs'); + +const hooks = initHooks(); + +hooks.enable(); +fs.access(__filename, common.mustCall(onaccess)); + +function onaccess() { + const as = hooks.activitiesOfTypes('FSREQWRAP'); + const a = as[0]; + checkInvocations(a, { init: 1, before: 1 }, + 'while in onaccess callback'); + tick(2); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('FSREQWRAP'); + + const as = hooks.activitiesOfTypes('FSREQWRAP'); + assert.strictEqual(as.length, 1, 'one activity'); + + const a = as[0]; + assert.strictEqual(a.type, 'FSREQWRAP', 'fs req wrap'); + assert.strictEqual(typeof a.uid, 'number', 'uid is a number'); + checkInvocations(a, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-fsreqwrap-readFile.js b/test/async-hooks/test-fsreqwrap-readFile.js new file mode 100644 index 00000000000000..653de1493bc96f --- /dev/null +++ b/test/async-hooks/test-fsreqwrap-readFile.js @@ -0,0 +1,48 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const fs = require('fs'); + +const hooks = initHooks(); + +hooks.enable(); +fs.readFile(__filename, common.mustCall(onread)); + +function onread() { + const as = hooks.activitiesOfTypes('FSREQWRAP'); + let lastParent = 1; + for (let i = 0; i < as.length; i++) { + const a = as[i]; + assert.strictEqual(a.type, 'FSREQWRAP', 'fs req wrap'); + assert.strictEqual(typeof a.uid, 'number', 'uid is a number'); + assert.strictEqual(a.triggerId, lastParent, 'parent uid 1'); + lastParent = a.uid; + } + checkInvocations(as[0], { init: 1, before: 1, after: 1, destroy: 1 }, + 'reqwrap[0]: while in onread callback'); + checkInvocations(as[1], { init: 1, before: 1, after: 1, destroy: 1 }, + 'reqwrap[1]: while in onread callback'); + checkInvocations(as[2], { init: 1, before: 1, after: 1, destroy: 1 }, + 'reqwrap[2]: while in onread callback'); + + // this callback is called from within the last fs req callback therefore + // the last req is still going and after/destroy haven't been called yet + checkInvocations(as[3], { init: 1, before: 1 }, + 'reqwrap[3]: while in onread callback'); + tick(2); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('FSREQWRAP'); + const as = hooks.activitiesOfTypes('FSREQWRAP'); + const a = as.pop(); + checkInvocations(a, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-getaddrinforeqwrap.js b/test/async-hooks/test-getaddrinforeqwrap.js new file mode 100644 index 00000000000000..0bbe89f3270a16 --- /dev/null +++ b/test/async-hooks/test-getaddrinforeqwrap.js @@ -0,0 +1,39 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const dns = require('dns'); + +const hooks = initHooks(); + +hooks.enable(); +dns.lookup('www.google.com', 4, common.mustCall(onlookup)); +function onlookup(err_, ip, family) { + // we don't care about the error here in order to allow + // tests to run offline (lookup will fail in that case and the err be set); + + const as = hooks.activitiesOfTypes('GETADDRINFOREQWRAP'); + assert.strictEqual(as.length, 1, 'one activity'); + + const a = as[0]; + assert.strictEqual(a.type, 'GETADDRINFOREQWRAP', 'getaddrinforeq wrap'); + assert.strictEqual(typeof a.uid, 'number', 'uid is a number'); + assert.strictEqual(a.triggerId, 1, 'parent uid 1'); + checkInvocations(a, { init: 1, before: 1 }, 'while in onlookup callback'); + tick(2); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('GETADDRINFOREQWRAP'); + + const as = hooks.activitiesOfTypes('GETADDRINFOREQWRAP'); + const a = as[0]; + checkInvocations(a, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-getnameinforeqwrap.js b/test/async-hooks/test-getnameinforeqwrap.js new file mode 100644 index 00000000000000..eca1e8457bcc16 --- /dev/null +++ b/test/async-hooks/test-getnameinforeqwrap.js @@ -0,0 +1,40 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const dns = require('dns'); + +const hooks = initHooks(); + +hooks.enable(); +dns.lookupService('127.0.0.1', 80, common.mustCall(onlookupService)); +function onlookupService(err_, ip, family) { + // we don't care about the error here in order to allow + // tests to run offline (lookup will fail in that case and the err be set) + + const as = hooks.activitiesOfTypes('GETNAMEINFOREQWRAP'); + assert.strictEqual(as.length, 1, 'one activity'); + + const a = as[0]; + assert.strictEqual(a.type, 'GETNAMEINFOREQWRAP', 'getnameinforeq wrap'); + assert.strictEqual(typeof a.uid, 'number', 'uid is a number'); + assert.strictEqual(a.triggerId, 1, 'parent uid 1'); + checkInvocations(a, { init: 1, before: 1 }, + 'while in onlookupService callback'); + tick(2); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('GETNAMEINFOREQWRAP'); + + const as = hooks.activitiesOfTypes('GETNAMEINFOREQWRAP'); + const a = as[0]; + checkInvocations(a, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-graph.connection.js b/test/async-hooks/test-graph.connection.js new file mode 100644 index 00000000000000..60bd19996f7df7 --- /dev/null +++ b/test/async-hooks/test-graph.connection.js @@ -0,0 +1,56 @@ +'use strict'; + +const initHooks = require('./init-hooks'); +const common = require('../common'); +const verifyGraph = require('./verify-graph'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); + return; +} + +const tls = require('tls'); +const Connection = process.binding('crypto').Connection; +const hooks = initHooks(); +hooks.enable(); + +function createServerConnection( + onhandshakestart, + certificate = null, + isServer = true, + servername = 'some server', + rejectUnauthorized +) { + if (certificate == null) certificate = tls.createSecureContext(); + const ssl = new Connection( + certificate.context, isServer, servername, rejectUnauthorized + ); + if (isServer) { + ssl.onhandshakestart = onhandshakestart; + ssl.lastHandshakeTime = 0; + } + return ssl; +} + +// creating first server connection and start it +const sc1 = createServerConnection(common.mustCall(onfirstHandShake)); +sc1.start(); + +function onfirstHandShake() { + // Create second connection inside handshake of first to show + // that the triggerId of the second will be set to id of the first + const sc2 = createServerConnection(common.mustCall(onsecondHandShake)); + sc2.start(); +} +function onsecondHandShake() { } + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + verifyGraph( + hooks, + [ { type: 'CONNECTION', id: 'connection:1', triggerId: null }, + { type: 'CONNECTION', id: 'connection:2', triggerId: 'connection:1' } ] + ); +} diff --git a/test/async-hooks/test-graph.fsreq-readFile.js b/test/async-hooks/test-graph.fsreq-readFile.js new file mode 100644 index 00000000000000..b3610c22febcd7 --- /dev/null +++ b/test/async-hooks/test-graph.fsreq-readFile.js @@ -0,0 +1,26 @@ +'use strict'; + +const common = require('../common'); +const initHooks = require('./init-hooks'); +const verifyGraph = require('./verify-graph'); +const fs = require('fs'); + +const hooks = initHooks(); + +hooks.enable(); +fs.readFile(__filename, common.mustCall(onread)); + +function onread() {} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + verifyGraph( + hooks, + [ { type: 'FSREQWRAP', id: 'fsreq:1', triggerId: null }, + { type: 'FSREQWRAP', id: 'fsreq:2', triggerId: 'fsreq:1' }, + { type: 'FSREQWRAP', id: 'fsreq:3', triggerId: 'fsreq:2' }, + { type: 'FSREQWRAP', id: 'fsreq:4', triggerId: 'fsreq:3' } ] + ); +} diff --git a/test/async-hooks/test-graph.intervals.js b/test/async-hooks/test-graph.intervals.js new file mode 100644 index 00000000000000..9cb3caf0587968 --- /dev/null +++ b/test/async-hooks/test-graph.intervals.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); +const initHooks = require('./init-hooks'); +const verifyGraph = require('./verify-graph'); +const TIMEOUT = 1; + +const hooks = initHooks(); +hooks.enable(); + +let count = 0; +const iv1 = setInterval(common.mustCall(onfirstInterval, 3), TIMEOUT); +let iv2; + +function onfirstInterval() { + if (++count === 3) { + clearInterval(iv1); + iv2 = setInterval(common.mustCall(onsecondInterval, 1), TIMEOUT + 1); + } +} + +function onsecondInterval() { + clearInterval(iv2); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + verifyGraph( + hooks, + [ { type: 'Timeout', id: 'timeout:1', triggerId: null }, + { type: 'TIMERWRAP', id: 'timer:1', triggerId: null }, + { type: 'Timeout', id: 'timeout:2', triggerId: 'timeout:1' }, + { type: 'TIMERWRAP', id: 'timer:2', triggerId: 'timeout:1' } ] + ); +} diff --git a/test/async-hooks/test-graph.pipe.js b/test/async-hooks/test-graph.pipe.js new file mode 100644 index 00000000000000..03a5751b1ab3d2 --- /dev/null +++ b/test/async-hooks/test-graph.pipe.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('../common'); +const initHooks = require('./init-hooks'); +const verifyGraph = require('./verify-graph'); +const spawn = require('child_process').spawn; + +const hooks = initHooks(); + +hooks.enable(); +const sleep = spawn('sleep', [ '0.1' ]); + +sleep + .on('exit', common.mustCall(onsleepExit)) + .on('close', common.mustCall(onsleepClose)); + +function onsleepExit(code) {} + +function onsleepClose() {} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + verifyGraph( + hooks, + [ { type: 'PROCESSWRAP', id: 'process:1', triggerId: null }, + { type: 'PIPEWRAP', id: 'pipe:1', triggerId: null }, + { type: 'PIPEWRAP', id: 'pipe:2', triggerId: null }, + { type: 'PIPEWRAP', id: 'pipe:3', triggerId: null } ] + ); +} diff --git a/test/async-hooks/test-graph.pipeconnect.js b/test/async-hooks/test-graph.pipeconnect.js new file mode 100644 index 00000000000000..96837ec384427f --- /dev/null +++ b/test/async-hooks/test-graph.pipeconnect.js @@ -0,0 +1,37 @@ +'use strict'; + +const common = require('../common'); +const initHooks = require('./init-hooks'); +const verifyGraph = require('./verify-graph'); + +const net = require('net'); + +common.refreshTmpDir(); + +const hooks = initHooks(); +hooks.enable(); + +net.createServer(function(c) { + c.end(); + this.close(); +}).listen(common.PIPE, common.mustCall(onlisten)); + +function onlisten() { + net.connect(common.PIPE, common.mustCall(onconnect)); +} + +function onconnect() {} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + verifyGraph( + hooks, + [ { type: 'PIPEWRAP', id: 'pipe:1', triggerId: null }, + { type: 'PIPEWRAP', id: 'pipe:2', triggerId: 'pipe:1' }, + { type: 'PIPECONNECTWRAP', id: 'pipeconnect:1', triggerId: 'pipe:2' }, + { type: 'PIPEWRAP', id: 'pipe:3', triggerId: 'pipe:1' }, + { type: 'SHUTDOWNWRAP', id: 'shutdown:1', triggerId: 'pipe:3' } ] + ); +} diff --git a/test/async-hooks/test-graph.shutdown.js b/test/async-hooks/test-graph.shutdown.js new file mode 100644 index 00000000000000..031a927f05e77b --- /dev/null +++ b/test/async-hooks/test-graph.shutdown.js @@ -0,0 +1,44 @@ +'use strict'; + +const common = require('../common'); +const initHooks = require('./init-hooks'); +const verifyGraph = require('./verify-graph'); + +const net = require('net'); + +const hooks = initHooks(); +hooks.enable(); + +const server = net + .createServer(onconnection) + .on('listening', common.mustCall(onlistening)); +server.listen(); +function onlistening() { + net.connect(server.address().port, common.mustCall(onconnected)); +} + +function onconnection(c) { + c.end(); + this.close(onserverClosed); +} + +function onconnected() {} + +function onserverClosed() {} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + verifyGraph( + hooks, + [ { type: 'TCPWRAP', id: 'tcp:1', triggerId: null }, + { type: 'TCPWRAP', id: 'tcp:2', triggerId: 'tcp:1' }, + { type: 'GETADDRINFOREQWRAP', + id: 'getaddrinforeq:1', triggerId: 'tcp:2' }, + { type: 'TCPCONNECTWRAP', + id: 'tcpconnect:1', triggerId: 'tcp:2' }, + { type: 'TCPWRAP', id: 'tcp:3', triggerId: 'tcp:1' }, + { type: 'SHUTDOWNWRAP', id: 'shutdown:1', triggerId: 'tcp:3' } ] + ); +} diff --git a/test/async-hooks/test-graph.signal.js b/test/async-hooks/test-graph.signal.js new file mode 100644 index 00000000000000..e38f1c19ab86d3 --- /dev/null +++ b/test/async-hooks/test-graph.signal.js @@ -0,0 +1,54 @@ +'use strict'; + +const common = require('../common'); +const initHooks = require('./init-hooks'); +const verifyGraph = require('./verify-graph'); +const exec = require('child_process').exec; + +const hooks = initHooks(); + +hooks.enable(); +process.on('SIGUSR2', common.mustCall(onsigusr2, 2)); + +let count = 0; +exec('kill -USR2 ' + process.pid); + +function onsigusr2() { + count++; + + if (count === 1) { + // trigger same signal handler again + exec('kill -USR2 ' + process.pid); + } else { + // install another signal handler + process.removeAllListeners('SIGUSR2'); + process.on('SIGUSR2', common.mustCall(onsigusr2Again)); + + exec('kill -USR2 ' + process.pid); + } +} + +function onsigusr2Again() {} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + verifyGraph( + hooks, + [ { type: 'SIGNALWRAP', id: 'signal:1', triggerId: null }, + { type: 'PROCESSWRAP', id: 'process:1', triggerId: null }, + { type: 'PIPEWRAP', id: 'pipe:1', triggerId: null }, + { type: 'PIPEWRAP', id: 'pipe:2', triggerId: null }, + { type: 'PIPEWRAP', id: 'pipe:3', triggerId: null }, + { type: 'PROCESSWRAP', id: 'process:2', triggerId: 'signal:1' }, + { type: 'PIPEWRAP', id: 'pipe:4', triggerId: 'signal:1' }, + { type: 'PIPEWRAP', id: 'pipe:5', triggerId: 'signal:1' }, + { type: 'PIPEWRAP', id: 'pipe:6', triggerId: 'signal:1' }, + { type: 'SIGNALWRAP', id: 'signal:2', triggerId: 'signal:1' }, + { type: 'PROCESSWRAP', id: 'process:3', triggerId: 'signal:1' }, + { type: 'PIPEWRAP', id: 'pipe:7', triggerId: 'signal:1' }, + { type: 'PIPEWRAP', id: 'pipe:8', triggerId: 'signal:1' }, + { type: 'PIPEWRAP', id: 'pipe:9', triggerId: 'signal:1' } ] + ); +} diff --git a/test/async-hooks/test-graph.statwatcher.js b/test/async-hooks/test-graph.statwatcher.js new file mode 100644 index 00000000000000..c4e0432c7cff87 --- /dev/null +++ b/test/async-hooks/test-graph.statwatcher.js @@ -0,0 +1,34 @@ +'use strict'; + +require('../common'); +const commonPath = require.resolve('../common'); +const initHooks = require('./init-hooks'); +const verifyGraph = require('./verify-graph'); +const fs = require('fs'); + +const hooks = initHooks(); +hooks.enable(); + +function onchange() { } +// install first file watcher +fs.watchFile(__filename, onchange); + +// install second file watcher +fs.watchFile(commonPath, onchange); + +// remove first file watcher +fs.unwatchFile(__filename); + +// remove second file watcher +fs.unwatchFile(commonPath); + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + verifyGraph( + hooks, + [ { type: 'STATWATCHER', id: 'statwatcher:1', triggerId: null }, + { type: 'STATWATCHER', id: 'statwatcher:2', triggerId: null } ] + ); +} diff --git a/test/async-hooks/test-graph.tcp.js b/test/async-hooks/test-graph.tcp.js new file mode 100644 index 00000000000000..c215a7a7973597 --- /dev/null +++ b/test/async-hooks/test-graph.tcp.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +const initHooks = require('./init-hooks'); +const verifyGraph = require('./verify-graph'); + +const net = require('net'); + +const hooks = initHooks(); +hooks.enable(); + +const server = net + .createServer(common.mustCall(onconnection)) + .on('listening', common.mustCall(onlistening)); + +server.listen(common.PORT); + +net.connect({ port: server.address().port, host: server.address().address }, + common.mustCall(onconnected)); + +function onlistening() {} + +function onconnected() {} + +function onconnection(c) { + c.end(); + this.close(common.mustCall(onserverClosed)); +} + +function onserverClosed() {} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + + verifyGraph( + hooks, + [ { type: 'TCPWRAP', id: 'tcp:1', triggerId: null }, + { type: 'TCPWRAP', id: 'tcp:2', triggerId: null }, + { type: 'TCPCONNECTWRAP', + id: 'tcpconnect:1', triggerId: 'tcp:2' }, + { type: 'TCPWRAP', id: 'tcp:3', triggerId: 'tcp:1' }, + { type: 'SHUTDOWNWRAP', id: 'shutdown:1', triggerId: 'tcp:3' } ] + ); +} diff --git a/test/async-hooks/test-graph.timeouts.js b/test/async-hooks/test-graph.timeouts.js new file mode 100644 index 00000000000000..eebf320472efe9 --- /dev/null +++ b/test/async-hooks/test-graph.timeouts.js @@ -0,0 +1,35 @@ +'use strict'; + +const common = require('../common'); +const initHooks = require('./init-hooks'); +const verifyGraph = require('./verify-graph'); +const TIMEOUT = 1; + +const hooks = initHooks(); +hooks.enable(); + +setTimeout(common.mustCall(ontimeout), TIMEOUT); +function ontimeout() { + setTimeout(onsecondTimeout, TIMEOUT + 1); +} + +function onsecondTimeout() { + setTimeout(onthirdTimeout, TIMEOUT + 2); +} + +function onthirdTimeout() {} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + verifyGraph( + hooks, + [ { type: 'Timeout', id: 'timeout:1', triggerId: null }, + { type: 'TIMERWRAP', id: 'timer:1', triggerId: null }, + { type: 'Timeout', id: 'timeout:2', triggerId: 'timeout:1' }, + { type: 'TIMERWRAP', id: 'timer:2', triggerId: 'timeout:1' }, + { type: 'Timeout', id: 'timeout:3', triggerId: 'timeout:2' }, + { type: 'TIMERWRAP', id: 'timer:3', triggerId: 'timeout:2' } ] + ); +} diff --git a/test/async-hooks/test-graph.tls-write.js b/test/async-hooks/test-graph.tls-write.js new file mode 100644 index 00000000000000..62c9c502143da4 --- /dev/null +++ b/test/async-hooks/test-graph.tls-write.js @@ -0,0 +1,74 @@ +'use strict'; + +const common = require('../common'); +const initHooks = require('./init-hooks'); +const verifyGraph = require('./verify-graph'); +const fs = require('fs'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); + return; +} + +const tls = require('tls'); +const hooks = initHooks(); +hooks.enable(); + +// +// Creating server and listening on port +// +const server = tls + .createServer({ + cert: fs.readFileSync(common.fixturesDir + '/test_cert.pem'), + key: fs.readFileSync(common.fixturesDir + '/test_key.pem') + }) + .on('listening', common.mustCall(onlistening)) + .on('secureConnection', common.mustCall(onsecureConnection)) + .listen(common.PORT); + +function onlistening() { + // + // Creating client and connecting it to server + // + tls + .connect(common.PORT, { rejectUnauthorized: false }) + .on('secureConnect', common.mustCall(onsecureConnect)); +} + +function onsecureConnection() {} + +function onsecureConnect() { + // Destroying client socket + this.destroy(); + + // Closing server + server.close(common.mustCall(onserverClosed)); +} + +function onserverClosed() {} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + + verifyGraph( + hooks, + [ { type: 'TCPWRAP', id: 'tcp:1', triggerId: null }, + { type: 'TCPWRAP', id: 'tcp:2', triggerId: 'tcp:1' }, + { type: 'TLSWRAP', id: 'tls:1', triggerId: 'tcp:1' }, + { type: 'GETADDRINFOREQWRAP', + id: 'getaddrinforeq:1', triggerId: 'tls:1' }, + { type: 'TCPCONNECTWRAP', + id: 'tcpconnect:1', triggerId: 'tcp:2' }, + { type: 'WRITEWRAP', id: 'write:1', triggerId: 'tcpconnect:1' }, + { type: 'TCPWRAP', id: 'tcp:3', triggerId: 'tcp:1' }, + { type: 'TLSWRAP', id: 'tls:2', triggerId: 'tcp:1' }, + { type: 'TIMERWRAP', id: 'timer:1', triggerId: 'tcp:1' }, + { type: 'WRITEWRAP', id: 'write:2', triggerId: null }, + { type: 'WRITEWRAP', id: 'write:3', triggerId: null }, + { type: 'WRITEWRAP', id: 'write:4', triggerId: null }, + { type: 'Immediate', id: 'immediate:1', triggerId: 'tcp:2' }, + { type: 'Immediate', id: 'immediate:2', triggerId: 'tcp:3' } ] + ); +} diff --git a/test/async-hooks/test-httpparser.request.js b/test/async-hooks/test-httpparser.request.js new file mode 100644 index 00000000000000..b6f594a1597e3d --- /dev/null +++ b/test/async-hooks/test-httpparser.request.js @@ -0,0 +1,58 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); + +const binding = process.binding('http_parser'); +const HTTPParser = binding.HTTPParser; + +const CRLF = '\r\n'; +const REQUEST = HTTPParser.REQUEST; + +const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0; + +const hooks = initHooks(); + +hooks.enable(); + +const request = Buffer.from( + 'GET /hello HTTP/1.1' + CRLF + CRLF +); + +const parser = new HTTPParser(REQUEST); +const as = hooks.activitiesOfTypes('HTTPPARSER'); +const httpparser = as[0]; + +assert.strictEqual( + as.length, 1, + '1 httpparser created synchronously when creating new httpparser'); +assert.strictEqual(typeof httpparser.uid, 'number', 'uid is a number'); +assert.strictEqual(typeof httpparser.triggerId, + 'number', 'triggerId is a number'); +checkInvocations(httpparser, { init: 1 }, 'when created new Httphttpparser'); + +parser[kOnHeadersComplete] = common.mustCall(onheadersComplete); +parser.execute(request, 0, request.length); + +function onheadersComplete() { + checkInvocations(httpparser, { init: 1, before: 1 }, + 'when onheadersComplete called'); + tick(1, common.mustCall(tick1)); +} + +function tick1() { + parser.close(); + tick(1); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('HTTPPARSER'); + checkInvocations(httpparser, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-httpparser.response.js b/test/async-hooks/test-httpparser.response.js new file mode 100644 index 00000000000000..e7930ea2bcfc5c --- /dev/null +++ b/test/async-hooks/test-httpparser.response.js @@ -0,0 +1,68 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); + +const binding = process.binding('http_parser'); +const HTTPParser = binding.HTTPParser; + +const CRLF = '\r\n'; +const RESPONSE = HTTPParser.RESPONSE; +const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0; +const kOnBody = HTTPParser.kOnBody | 0; + +const hooks = initHooks(); + +hooks.enable(); + +const request = Buffer.from( + 'HTTP/1.1 200 OK' + CRLF + + 'Content-types: text/plain' + CRLF + + 'Content-Length: 4' + CRLF + + CRLF + + 'pong' +); + +const parser = new HTTPParser(RESPONSE); +const as = hooks.activitiesOfTypes('HTTPPARSER'); +const httpparser = as[0]; + +assert.strictEqual( + as.length, 1, + '1 httpparser created synchronously when creating new httpparser'); +assert.strictEqual(typeof httpparser.uid, 'number', 'uid is a number'); +assert.strictEqual(typeof httpparser.triggerId, + 'number', 'triggerId is a number'); +checkInvocations(httpparser, { init: 1 }, 'when created new Httphttpparser'); + +parser[kOnHeadersComplete] = common.mustCall(onheadersComplete); +parser[kOnBody] = common.mustCall(onbody); +parser.execute(request, 0, request.length); + +function onheadersComplete() { + checkInvocations(httpparser, { init: 1, before: 1 }, + 'when onheadersComplete called'); +} + +function onbody(buf, start, len) { + checkInvocations(httpparser, { init: 1, before: 2, after: 1 }, + 'when onbody called'); + tick(1, common.mustCall(tick1)); +} + +function tick1() { + parser.close(); + tick(1); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('HTTPPARSER'); + checkInvocations(httpparser, { init: 1, before: 2, after: 2, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-immediate.js b/test/async-hooks/test-immediate.js new file mode 100644 index 00000000000000..2434d98003bef1 --- /dev/null +++ b/test/async-hooks/test-immediate.js @@ -0,0 +1,66 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); + +const hooks = initHooks(); +hooks.enable(); + +// install first immediate +setImmediate(common.mustCall(onimmediate)); + +const as = hooks.activitiesOfTypes('Immediate'); +assert.strictEqual(as.length, 1, + 'one immediate when first set immediate installed'); +const imd1 = as[0]; +assert.strictEqual(imd1.type, 'Immediate', 'immediate'); +assert.strictEqual(typeof imd1.uid, 'number', 'uid is a number'); +assert.strictEqual(typeof imd1.triggerId, 'number', 'triggerId is a number'); +checkInvocations(imd1, { init: 1 }, + 'imd1: when first set immediate installed'); + +let imd2; + +function onimmediate() { + let as = hooks.activitiesOfTypes('Immediate'); + assert.strictEqual(as.length, 1, + 'one immediate when first set immediate triggered'); + checkInvocations(imd1, { init: 1, before: 1 }, + 'imd1: when first set immediate triggered'); + + // install second immediate + setImmediate(common.mustCall(onimmediateTwo)); + as = hooks.activitiesOfTypes('Immediate'); + assert.strictEqual(as.length, 2, + 'two immediates when second set immediate installed'); + imd2 = as[1]; + assert.strictEqual(imd2.type, 'Immediate', 'immediate'); + assert.strictEqual(typeof imd2.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof imd2.triggerId, 'number', 'triggerId is a number'); + checkInvocations(imd1, { init: 1, before: 1 }, + 'imd1: when second set immediate installed'); + checkInvocations(imd2, { init: 1 }, + 'imd2: when second set immediate installed'); +} + +function onimmediateTwo() { + checkInvocations(imd1, { init: 1, before: 1, after: 1, destroy: 1 }, + 'imd1: when second set immediate triggered'); + checkInvocations(imd2, { init: 1, before: 1 }, + 'imd2: when second set immediate triggered'); + tick(1); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('Immediate'); + checkInvocations(imd1, { init: 1, before: 1, after: 1, destroy: 1 }, + 'imd1: when process exits'); + checkInvocations(imd2, { init: 1, before: 1, after: 1, destroy: 1 }, + 'imd2: when process exits'); +} diff --git a/test/async-hooks/test-pipeconnectwrap.js b/test/async-hooks/test-pipeconnectwrap.js new file mode 100644 index 00000000000000..f42143a9558c9b --- /dev/null +++ b/test/async-hooks/test-pipeconnectwrap.js @@ -0,0 +1,95 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); + +const net = require('net'); + +common.refreshTmpDir(); + +const hooks = initHooks(); +hooks.enable(); +let pipe1, pipe2, pipe3; +let pipeconnect; + +net.createServer(function(c) { + c.end(); + this.close(); +}).listen(common.PIPE, common.mustCall(onlisten)); + +function onlisten() { + let pipes = hooks.activitiesOfTypes('PIPEWRAP'); + let pipeconnects = hooks.activitiesOfTypes('PIPECONNECTWRAP'); + assert.strictEqual( + pipes.length, 1, + 'one pipe wrap created when net server is listening'); + assert.strictEqual( + pipeconnects.length, 0, + 'no pipeconnect wrap created when net server is listening'); + + net.connect(common.PIPE, common.mustCall(onconnect)); + + pipes = hooks.activitiesOfTypes('PIPEWRAP'); + pipeconnects = hooks.activitiesOfTypes('PIPECONNECTWRAP'); + assert.strictEqual(pipes.length, 2, + '2 pipe wraps created when connecting client'); + assert.strictEqual(pipeconnects.length, 1, + '1 connectwrap created when connecting client'); + + pipe1 = pipes[0]; + pipe2 = pipes[1]; + pipeconnect = pipeconnects[0]; + + assert.strictEqual(pipe1.type, 'PIPEWRAP', 'first is pipe wrap'); + assert.strictEqual(pipe2.type, 'PIPEWRAP', 'second is pipe wrap'); + assert.strictEqual(pipeconnect.type, 'PIPECONNECTWRAP', + 'third is pipeconnect wrap'); + [ pipe1, pipe2, pipeconnect ].forEach(check); + + function check(a) { + assert.strictEqual(typeof a.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof a.triggerId, 'number', 'triggerId is a number'); + checkInvocations(a, { init: 1 }, 'after net.connect'); + } +} + +function onconnect() { + const pipes = hooks.activitiesOfTypes('PIPEWRAP'); + const pipeconnects = hooks.activitiesOfTypes('PIPECONNECTWRAP'); + + assert.strictEqual(pipes.length, 3, + '3 pipe wraps created when client connected'); + assert.strictEqual(pipeconnects.length, 1, + '1 connectwrap created when client connected'); + pipe3 = pipes[2]; + assert.strictEqual(typeof pipe3.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof pipe3.triggerId, 'number', 'triggerId is a number'); + + checkInvocations(pipe1, { init: 1, before: 1, after: 1 }, + 'pipe1, client connected'); + checkInvocations(pipe2, { init: 1 }, 'pipe2, client connected'); + checkInvocations(pipeconnect, { init: 1, before: 1 }, + 'pipeconnect, client connected'); + checkInvocations(pipe3, { init: 1 }, 'pipe3, client connected'); + tick(5); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('PIPEWRAP'); + hooks.sanityCheck('PIPECONNECTWRAP'); + // TODO(thlorenz) why have some of those 'before' and 'after' called twice + checkInvocations(pipe1, { init: 1, before: 1, after: 1, destroy: 1 }, + 'pipe1, process exiting'); + checkInvocations(pipe2, { init: 1, before: 2, after: 2, destroy: 1 }, + 'pipe2, process exiting'); + checkInvocations(pipeconnect, { init: 1, before: 1, after: 1, destroy: 1 }, + 'pipeconnect, process exiting'); + checkInvocations(pipe3, { init: 1, before: 2, after: 2, destroy: 1 }, + 'pipe3, process exiting'); +} diff --git a/test/async-hooks/test-pipewrap.js b/test/async-hooks/test-pipewrap.js new file mode 100644 index 00000000000000..1a7173283632d4 --- /dev/null +++ b/test/async-hooks/test-pipewrap.js @@ -0,0 +1,79 @@ +// NOTE: this also covers process wrap as one is created along with the pipes +// when we launch the sleep process +'use strict'; +// Flags: --expose-gc + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const spawn = require('child_process').spawn; + +const hooks = initHooks(); + +hooks.enable(); +const sleep = spawn('sleep', [ '0.1' ]); + +sleep + .on('exit', common.mustCall(onsleepExit)) + .on('close', common.mustCall(onsleepClose)); + +// a process wrap and 3 pipe wraps for std{in,out,err} are initialized +// synchronously +const processes = hooks.activitiesOfTypes('PROCESSWRAP'); +const pipes = hooks.activitiesOfTypes('PIPEWRAP'); +assert.strictEqual(processes.length, 1, + '1 processwrap created when process created'); +assert.strictEqual(pipes.length, 3, + '3 pipe wraps created when process created'); + +const processwrap = processes[0]; +const pipe1 = pipes[0]; +const pipe2 = pipes[1]; +const pipe3 = pipes[2]; + +assert.strictEqual(processwrap.type, 'PROCESSWRAP', 'process wrap type'); +assert.strictEqual(processwrap.triggerId, 1, 'processwrap triggerId is 1'); +checkInvocations(processwrap, { init: 1 }, + 'processwrap when sleep.spawn was called'); + +[ pipe1, pipe2, pipe3 ].forEach((x) => { + assert(x.type, 'PIPEWRAP', 'pipe wrap type'); + assert.strictEqual(x.triggerId, 1, 'pipe wrap triggerId is 1'); + checkInvocations(x, { init: 1 }, 'pipe wrap when sleep.spawn was called'); +}); + +function onsleepExit(code) { + checkInvocations(processwrap, { init: 1, before: 1 }, + 'processwrap while in onsleepExit callback'); +} + +function onsleepClose() { + tick(1, () => + checkInvocations( + processwrap, + { init: 1, before: 1, after: 1 }, + 'processwrap while in onsleepClose callback') + ); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('PROCESSWRAP'); + hooks.sanityCheck('PIPEWRAP'); + + checkInvocations( + processwrap, + { init: 1, before: 1, after: 1 }, + 'processwrap while in onsleepClose callback'); + + [ pipe1, pipe2, pipe3 ].forEach((x) => { + assert(x.type, 'PIPEWRAP', 'pipe wrap type'); + assert.strictEqual(x.triggerId, 1, 'pipe wrap triggerId is 1'); + checkInvocations(x, { init: 1, before: 2, after: 2 }, + 'pipe wrap when sleep.spawn was called'); + }); +} diff --git a/test/async-hooks/test-querywrap.js b/test/async-hooks/test-querywrap.js new file mode 100644 index 00000000000000..9db44bdfd238f0 --- /dev/null +++ b/test/async-hooks/test-querywrap.js @@ -0,0 +1,40 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const dns = require('dns'); + +const hooks = initHooks(); + +hooks.enable(); +// uses cares for queryA which in turn uses QUERYWRAP +dns.resolve('localhost', common.mustCall(onresolved)); + +function onresolved() { + const as = hooks.activitiesOfTypes('QUERYWRAP'); + const a = as[0]; + assert.strictEqual(as.length, 1, 'one activity in onresolved callback'); + checkInvocations(a, { init: 1, before: 1 }, 'while in onresolved callback'); +} + +tick(1E4); +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('QUERYWRAP'); + + const as = hooks.activitiesOfTypes('QUERYWRAP'); + assert.strictEqual(as.length, 1, 'one activity on process exit'); + const a = as[0]; + + assert.strictEqual(a.type, 'QUERYWRAP', 'query wrap'); + assert.strictEqual(typeof a.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof a.triggerId, 'number', 'triggerId is a number'); + checkInvocations(a, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-shutdownwrap.js b/test/async-hooks/test-shutdownwrap.js new file mode 100644 index 00000000000000..8ce2aae27514ef --- /dev/null +++ b/test/async-hooks/test-shutdownwrap.js @@ -0,0 +1,69 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); + +const net = require('net'); + +const hooks = initHooks(); +hooks.enable(); + +const server = net + .createServer(onconnection) + .on('listening', common.mustCall(onlistening)); +server.listen(); +function onlistening() { + net.connect(server.address().port, common.mustCall(onconnected)); +} + +// It is non-deterministic in which order onconnection and onconnected fire. +// Therefore we track here if we ended the connection already or not. +let endedConnection = false; +function onconnection(c) { + assert.strictEqual(hooks.activitiesOfTypes('SHUTDOWNWRAP').length, 0, + 'no shutdown wrap before ending the client connection'); + c.end(); + endedConnection = true; + const as = hooks.activitiesOfTypes('SHUTDOWNWRAP'); + assert.strictEqual( + as.length, 1, + 'one shutdown wrap created sync after ending the client connection'); + checkInvocations(as[0], { init: 1 }, 'after ending client connection'); + this.close(onserverClosed); +} + +function onconnected() { + if (endedConnection) { + assert.strictEqual( + hooks.activitiesOfTypes('SHUTDOWNWRAP').length, 1, + 'one shutdown wrap when client connected but server ended connection'); + + } else { + assert.strictEqual( + hooks.activitiesOfTypes('SHUTDOWNWRAP').length, 0, + 'no shutdown wrap when client connected and server did not end connection' + ); + } +} + +function onserverClosed() { + const as = hooks.activitiesOfTypes('SHUTDOWNWRAP'); + checkInvocations(as[0], { init: 1, before: 1, after: 1, destroy: 1 }, + 'when server closed'); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('SHUTDOWNWRAP'); + const as = hooks.activitiesOfTypes('SHUTDOWNWRAP'); + const a = as[0]; + assert.strictEqual(a.type, 'SHUTDOWNWRAP', 'shutdown wrap'); + assert.strictEqual(typeof a.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof a.triggerId, 'number', 'triggerId is a number'); + checkInvocations(as[0], { init: 1, before: 1, after: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-signalwrap.js b/test/async-hooks/test-signalwrap.js new file mode 100644 index 00000000000000..1065616032663e --- /dev/null +++ b/test/async-hooks/test-signalwrap.js @@ -0,0 +1,91 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const exec = require('child_process').exec; + +const hooks = initHooks(); + +hooks.enable(); +process.on('SIGUSR2', common.mustCall(onsigusr2, 2)); + +const as = hooks.activitiesOfTypes('SIGNALWRAP'); +assert.strictEqual(as.length, 1, + 'one signal wrap when SIGUSR2 handler is set up'); +const signal1 = as[0]; +assert.strictEqual(signal1.type, 'SIGNALWRAP', 'signal wrap'); +assert.strictEqual(typeof signal1.uid, 'number', 'uid is a number'); +assert.strictEqual(typeof signal1.triggerId, 'number', 'triggerId is a number'); +checkInvocations(signal1, { init: 1 }, 'when SIGUSR2 handler is set up'); + +let count = 0; +exec('kill -USR2 ' + process.pid); + +let signal2; + +function onsigusr2() { + count++; + + if (count === 1) { + // first invocation + checkInvocations( + signal1, { init: 1, before: 1 }, + ' signal1: when first SIGUSR2 handler is called for the first time'); + + // trigger same signal handler again + exec('kill -USR2 ' + process.pid); + } else { + // second invocation + checkInvocations( + signal1, { init: 1, before: 2, after: 1 }, + 'signal1: when first SIGUSR2 handler is called for the second time'); + + // install another signal handler + process.removeAllListeners('SIGUSR2'); + process.on('SIGUSR2', common.mustCall(onsigusr2Again)); + + const as = hooks.activitiesOfTypes('SIGNALWRAP'); + assert.strictEqual( + as.length, 2, + 'two signal wraps when second SIGUSR2 handler is set up'); + signal2 = as[1]; + assert.strictEqual(signal2.type, 'SIGNALWRAP', 'signal wrap'); + assert.strictEqual(typeof signal2.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof signal2.triggerId, 'number', + 'triggerId is a number'); + + checkInvocations( + signal1, { init: 1, before: 2, after: 1 }, + 'signal1: when second SIGUSR2 handler is set up'); + checkInvocations( + signal2, { init: 1 }, + 'signal2: when second SIGUSR2 handler is setup'); + + exec('kill -USR2 ' + process.pid); + } +} + +function onsigusr2Again() { + checkInvocations( + signal1, { init: 1, before: 2, after: 2, destroy: 1 }, + 'signal1: when second SIGUSR2 handler is called'); + checkInvocations( + signal2, { init: 1, before: 1 }, + 'signal2: when second SIGUSR2 handler is called'); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('SIGNALWRAP'); + checkInvocations( + signal1, { init: 1, before: 2, after: 2, destroy: 1 }, + 'signal1: when second SIGUSR2 process exits'); + // second signal not destroyed yet since its event listener is still active + checkInvocations( + signal2, { init: 1, before: 1, after: 1 }, + 'signal2: when second SIGUSR2 process exits'); +} diff --git a/test/async-hooks/test-statwatcher.js b/test/async-hooks/test-statwatcher.js new file mode 100644 index 00000000000000..fe2a97c06f43bf --- /dev/null +++ b/test/async-hooks/test-statwatcher.js @@ -0,0 +1,64 @@ +'use strict'; + +require('../common'); +const commonPath = require.resolve('../common'); +const assert = require('assert'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const fs = require('fs'); + +const hooks = initHooks(); +hooks.enable(); + +function onchange() {} +// install first file watcher +fs.watchFile(__filename, onchange); + +let as = hooks.activitiesOfTypes('STATWATCHER'); +assert.strictEqual(as.length, 1, 'one stat watcher when watching one file'); + +const statwatcher1 = as[0]; +assert.strictEqual(statwatcher1.type, 'STATWATCHER', 'stat watcher'); +assert.strictEqual(typeof statwatcher1.uid, 'number', 'uid is a number'); +assert.strictEqual(statwatcher1.triggerId, 1, 'parent uid 1'); +checkInvocations(statwatcher1, { init: 1 }, + 'watcher1: when started to watch file'); + +// install second file watcher +fs.watchFile(commonPath, onchange); +as = hooks.activitiesOfTypes('STATWATCHER'); +assert.strictEqual(as.length, 2, 'two stat watchers when watching two files'); + +const statwatcher2 = as[1]; +assert.strictEqual(statwatcher2.type, 'STATWATCHER', 'stat watcher'); +assert.strictEqual(typeof statwatcher2.uid, 'number', 'uid is a number'); +assert.strictEqual(statwatcher2.triggerId, 1, 'parent uid 1'); +checkInvocations(statwatcher1, { init: 1 }, + 'watcher1: when started to watch second file'); +checkInvocations(statwatcher2, { init: 1 }, + 'watcher2: when started to watch second file'); + +// remove first file watcher +fs.unwatchFile(__filename); +checkInvocations(statwatcher1, { init: 1, before: 1, after: 1 }, + 'watcher:1 when unwatched first file'); +checkInvocations(statwatcher2, { init: 1 }, + 'watcher2: when unwatched first file'); + +// remove second file watcher +fs.unwatchFile(commonPath); +checkInvocations(statwatcher1, { init: 1, before: 1, after: 1 }, + 'watcher:1 when unwatched second file'); +checkInvocations(statwatcher2, { init: 1, before: 1, after: 1 }, + 'watcher2: when unwatched second file'); + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('STATWATCHER'); + checkInvocations(statwatcher1, { init: 1, before: 1, after: 1 }, + 'watcher:1 when process exits'); + checkInvocations(statwatcher2, { init: 1, before: 1, after: 1 }, + 'watcher2: when process exits'); +} diff --git a/test/async-hooks/test-tcpwrap.js b/test/async-hooks/test-tcpwrap.js new file mode 100644 index 00000000000000..8442ac9e38034a --- /dev/null +++ b/test/async-hooks/test-tcpwrap.js @@ -0,0 +1,172 @@ +// Covers TCPWRAP and related TCPCONNECTWRAP +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); + +const net = require('net'); + +let tcp1, tcp2, tcp3; +let tcpconnect; + +const hooks = initHooks(); +hooks.enable(); + +const server = net + .createServer(common.mustCall(onconnection)) + .on('listening', common.mustCall(onlistening)); + +// Calling server.listen creates a TCPWRAP synchronously +{ + server.listen(common.PORT); + const tcps = hooks.activitiesOfTypes('TCPWRAP'); + const tcpconnects = hooks.activitiesOfTypes('TCPCONNECTWRAP'); + assert.strictEqual( + tcps.length, 1, + 'one TCPWRAP created synchronously when calling server.listen'); + assert.strictEqual( + tcpconnects.length, 0, + 'no TCPCONNECTWRAP created synchronously when calling server.listen'); + tcp1 = tcps[0]; + assert.strictEqual(tcp1.type, 'TCPWRAP', 'tcp wrap'); + assert.strictEqual(typeof tcp1.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof tcp1.triggerId, 'number', 'triggerId is a number'); + checkInvocations(tcp1, { init: 1 }, 'when calling server.listen'); +} + +// Calling net.connect creates another TCPWRAP synchronously +{ + net.connect( + { port: server.address().port, host: server.address().address }, + common.mustCall(onconnected)); + const tcps = hooks.activitiesOfTypes('TCPWRAP'); + const tcpconnects = hooks.activitiesOfTypes('TCPCONNECTWRAP'); + assert.strictEqual( + tcps.length, 2, + '2 TCPWRAPs present when client is connecting'); + assert.strictEqual( + tcpconnects.length, 0, + 'no TCPCONNECTWRAP present when client is connecting'); + tcp2 = tcps[1]; + assert.strictEqual(tcps.length, 2, + '2 TCPWRAP present when client is connecting'); + assert.strictEqual(tcp2.type, 'TCPWRAP', 'tcp wrap'); + assert.strictEqual(typeof tcp2.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof tcp2.triggerId, 'number', 'triggerId is a number'); + + checkInvocations(tcp1, { init: 1 }, 'tcp1 when client is connecting'); + checkInvocations(tcp2, { init: 1 }, 'tcp2 when client is connecting'); +} + +function onlistening() { + assert.strictEqual(hooks.activitiesOfTypes('TCPWRAP').length, 2, + 'two TCPWRAPs when server is listening'); +} + +// Depending on timing we see client: onconnected or server: onconnection first +// Therefore we can't depend on any ordering, but when we see a connection for +// the first time we assign the tcpconnectwrap. +function ontcpConnection(serverConnection) { + if (tcpconnect != null) { + // When client receives connection first ('onconnected') and the server + // second then we see an 'after' here, otherwise not + const expected = serverConnection ? + { init: 1, before: 1, after: 1 } : + { init: 1, before: 1 }; + checkInvocations( + tcpconnect, expected, + 'tcpconnect: when both client and server received connection'); + return; + } + + // only focusing on TCPCONNECTWRAP here + const tcpconnects = hooks.activitiesOfTypes('TCPCONNECTWRAP'); + assert.strictEqual( + tcpconnects.length, 1, + 'one TCPCONNECTWRAP present on tcp connection'); + tcpconnect = tcpconnects[0]; + assert.strictEqual(tcpconnect.type, 'TCPCONNECTWRAP', 'tcpconnect wrap'); + assert.strictEqual(typeof tcpconnect.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof tcpconnect.triggerId, + 'number', 'triggerId is a number'); + // When client receives connection first ('onconnected'), we 'before' has + // been invoked at this point already, otherwise it only was 'init'ed + const expected = serverConnection ? { init: 1 } : { init: 1, before: 1 }; + checkInvocations(tcpconnect, expected, + 'tcpconnect: when tcp connection is established'); +} + +let serverConnected = false; +function onconnected() { + ontcpConnection(false); + // In the case that the client connects before the server TCPWRAP 'before' + // and 'after' weren't invoked yet. Also @see ontcpConnection. + const expected = serverConnected ? + { init: 1, before: 1, after: 1 } : + { init: 1 }; + checkInvocations(tcp1, expected, 'tcp1 when client connects'); + checkInvocations(tcp2, { init: 1 }, 'tcp2 when client connects'); +} + +function onconnection(c) { + serverConnected = true; + ontcpConnection(true); + + const tcps = hooks.activitiesOfTypes([ 'TCPWRAP' ]); + const tcpconnects = hooks.activitiesOfTypes('TCPCONNECTWRAP'); + assert.strictEqual( + tcps.length, 3, + '3 TCPWRAPs present when server receives connection'); + assert.strictEqual( + tcpconnects.length, 1, + 'one TCPCONNECTWRAP present when server receives connection'); + tcp3 = tcps[2]; + assert.strictEqual(tcp3.type, 'TCPWRAP', 'tcp wrap'); + assert.strictEqual(typeof tcp3.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof tcp3.triggerId, 'number', 'triggerId is a number'); + + checkInvocations(tcp1, { init: 1, before: 1 }, + 'tcp1 when server receives connection'); + checkInvocations(tcp2, { init: 1 }, 'tcp2 when server receives connection'); + checkInvocations(tcp3, { init: 1 }, 'tcp3 when server receives connection'); + + c.end(); + this.close(common.mustCall(onserverClosed)); +} + +function onserverClosed() { + checkInvocations(tcp1, { init: 1, before: 1, after: 1, destroy: 1 }, + 'tcp1 when server is closed'); + checkInvocations(tcp2, { init: 1, before: 2, after: 2, destroy: 1 }, + 'tcp2 when server is closed'); + checkInvocations(tcp3, { init: 1, before: 1, after: 1 }, + 'tcp3 synchronously when server is closed'); + tick(2, () => { + checkInvocations(tcp3, { init: 1, before: 2, after: 2, destroy: 1 }, + 'tcp3 when server is closed'); + checkInvocations(tcpconnect, { init: 1, before: 1, after: 1, destroy: 1 }, + 'tcpconnect when server is closed'); + }); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck([ 'TCPWRAP', 'TCPCONNECTWRAP' ]); + + checkInvocations(tcp1, { init: 1, before: 1, after: 1, destroy: 1 }, + 'tcp1 when process exits'); + checkInvocations( + tcp2, { init: 1, before: 2, after: 2, destroy: 1 }, + 'tcp2 when process exits'); + checkInvocations( + tcp3, { init: 1, before: 2, after: 2, destroy: 1 }, + 'tcp3 when process exits'); + checkInvocations( + tcpconnect, { init: 1, before: 1, after: 1, destroy: 1 }, + 'tcpconnect when process exits'); +} diff --git a/test/async-hooks/test-timerwrap.setInterval.js b/test/async-hooks/test-timerwrap.setInterval.js new file mode 100644 index 00000000000000..8e8b11a7e76bdb --- /dev/null +++ b/test/async-hooks/test-timerwrap.setInterval.js @@ -0,0 +1,56 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const TIMEOUT = 1; + +const hooks = initHooks(); +hooks.enable(); + +let count = 0; +const iv = setInterval(common.mustCall(oninterval, 3), TIMEOUT); + +const as = hooks.activitiesOfTypes('TIMERWRAP'); +assert.strictEqual(as.length, 1, 'one timer wrap when interval installed'); +const t = as[0]; +assert.strictEqual(t.type, 'TIMERWRAP', 'timer wrap'); +assert.strictEqual(typeof t.uid, 'number', 'uid is a number'); +assert.strictEqual(typeof t.triggerId, 'number', 'triggerId is a number'); +checkInvocations(t, { init: 1 }, 't: when first timer installed'); + +function oninterval() { + count++; + assert.strictEqual(as.length, 1, 'one timer wrap when timer is triggered'); + switch (count) { + case 1: { + checkInvocations(t, { init: 1, before: 1 }, + 't: when first timer triggered first time'); + break; + } + case 2: { + checkInvocations(t, { init: 1, before: 2, after: 1 }, + 't: when first timer triggered second time'); + break; + } + case 3: { + clearInterval(iv); + checkInvocations(t, { init: 1, before: 3, after: 2 }, + 't: when first timer triggered third time'); + tick(2); + break; + } + } +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('TIMERWRAP'); + + checkInvocations(t, { init: 1, before: 3, after: 3, destroy: 1 }, + 't: when process exits'); +} diff --git a/test/async-hooks/test-timerwrap.setTimeout.js b/test/async-hooks/test-timerwrap.setTimeout.js new file mode 100644 index 00000000000000..76913fb4bdc513 --- /dev/null +++ b/test/async-hooks/test-timerwrap.setTimeout.js @@ -0,0 +1,78 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const TIMEOUT = 1; + +const hooks = initHooks(); +hooks.enable(); + +// install first timeout +setTimeout(common.mustCall(ontimeout), TIMEOUT); +const as = hooks.activitiesOfTypes('TIMERWRAP'); +assert.strictEqual(as.length, 1, 'one timer wrap when first timeout installed'); +const t1 = as[0]; +assert.strictEqual(t1.type, 'TIMERWRAP', 'timer wrap'); +assert.strictEqual(typeof t1.uid, 'number', 'uid is a number'); +assert.strictEqual(typeof t1.triggerId, 'number', 'triggerId is a number'); +checkInvocations(t1, { init: 1 }, 't1: when first timer installed'); + +function ontimeout() { + checkInvocations(t1, { init: 1, before: 1 }, 't1: when first timer fired'); + + // install second timeout with same TIMEOUT to see timer wrap being reused + setTimeout(onsecondTimeout, TIMEOUT); + const as = hooks.activitiesOfTypes('TIMERWRAP'); + assert.strictEqual(as.length, 1, + 'one timer wrap when second timer installed'); + checkInvocations(t1, { init: 1, before: 1 }, + 't1: when second timer installed'); +} + +// even though we install 3 timers we only have two timerwrap resources created +// as one is reused for the two timers with the same timeout +let t2; + +function onsecondTimeout() { + let as = hooks.activitiesOfTypes('TIMERWRAP'); + assert.strictEqual(as.length, 1, 'one timer wrap when second timer fired'); + checkInvocations(t1, { init: 1, before: 2, after: 1 }, + 't1: when second timer fired'); + + // install third timeout with different TIMEOUT + setTimeout(onthirdTimeout, TIMEOUT + 1); + as = hooks.activitiesOfTypes('TIMERWRAP'); + assert.strictEqual(as.length, 2, + 'two timer wraps when third timer installed'); + t2 = as[1]; + assert.strictEqual(t2.type, 'TIMERWRAP', 'timer wrap'); + assert.strictEqual(typeof t2.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof t2.triggerId, 'number', 'triggerId is a number'); + checkInvocations(t1, { init: 1, before: 2, after: 1 }, + 't1: when third timer installed'); + checkInvocations(t2, { init: 1 }, + 't2: when third timer installed'); +} + +function onthirdTimeout() { + checkInvocations(t1, { init: 1, before: 2, after: 2, destroy: 1 }, + 't1: when third timer fired'); + checkInvocations(t2, { init: 1, before: 1 }, + 't2: when third timer fired'); + tick(2); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('TIMERWRAP'); + + checkInvocations(t1, { init: 1, before: 2, after: 2, destroy: 1 }, + 't1: when process exits'); + checkInvocations(t2, { init: 1, before: 1, after: 1, destroy: 1 }, + 't2: when process exits'); +} diff --git a/test/async-hooks/test-tlswrap.js b/test/async-hooks/test-tlswrap.js new file mode 100644 index 00000000000000..39e2cf100a623c --- /dev/null +++ b/test/async-hooks/test-tlswrap.js @@ -0,0 +1,133 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const fs = require('fs'); +const { checkInvocations } = require('./hook-checks'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); + return; +} + +const tls = require('tls'); +const hooks = initHooks(); +hooks.enable(); + +// +// Creating server and listening on port +// +const server = tls + .createServer({ + cert: fs.readFileSync(common.fixturesDir + '/test_cert.pem'), + key: fs.readFileSync(common.fixturesDir + '/test_key.pem') + }) + .on('listening', common.mustCall(onlistening)) + .on('secureConnection', common.mustCall(onsecureConnection)) + .listen(common.PORT); + +let svr, client; +function onlistening() { + // + // Creating client and connecting it to server + // + tls + .connect(common.PORT, { rejectUnauthorized: false }) + .on('secureConnect', common.mustCall(onsecureConnect)); + + const as = hooks.activitiesOfTypes('TLSWRAP'); + assert.strictEqual(as.length, 1, 'one TLSWRAP when client connecting'); + svr = as[0]; + + assert.strictEqual(svr.type, 'TLSWRAP', 'tls wrap'); + assert.strictEqual(typeof svr.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof svr.triggerId, 'number', 'triggerId is a number'); + checkInvocations(svr, { init: 1 }, 'server: when client connecting'); +} + +function onsecureConnection() { + // + // Server received client connection + // + const as = hooks.activitiesOfTypes('TLSWRAP'); + assert.strictEqual(as.length, 2, + 'two TLSWRAPs when server has secure connection'); + client = as[1]; + assert.strictEqual(client.type, 'TLSWRAP', 'tls wrap'); + assert.strictEqual(typeof client.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof client.triggerId, 'number', + 'triggerId is a number'); + + // TODO(thlorenz) which callback did the server wrap execute that already + // finished as well? + checkInvocations(svr, { init: 1, before: 1, after: 1 }, + 'server: when server has secure connection'); + + checkInvocations(client, { init: 1, before: 2, after: 1 }, + 'client: when server has secure connection'); +} + +function onsecureConnect() { + // + // Client connected to server + // + checkInvocations(svr, { init: 1, before: 2, after: 1 }, + 'server: when client connected'); + checkInvocations(client, { init: 1, before: 2, after: 2 }, + 'client: when client connected'); + + // + // Destroying client socket + // + this.destroy(); + checkInvocations(svr, { init: 1, before: 2, after: 1 }, + 'server: when destroying client'); + checkInvocations(client, { init: 1, before: 2, after: 2 }, + 'client: when destroying client'); + + tick(5, tick1); + function tick1() { + checkInvocations(svr, { init: 1, before: 2, after: 2 }, + 'server: when client destroyed'); + // TODO: why is client not destroyed here even after 5 ticks? + // or could it be that it isn't actually destroyed until + // the server is closed? + checkInvocations(client, { init: 1, before: 3, after: 3 }, + 'client: when client destroyed'); + // + // Closing server + // + server.close(common.mustCall(onserverClosed)); + // No changes to invocations until server actually closed below + checkInvocations(svr, { init: 1, before: 2, after: 2 }, + 'server: when closing server'); + checkInvocations(client, { init: 1, before: 3, after: 3 }, + 'client: when closing server'); + } +} + +function onserverClosed() { + // + // Server closed + // + tick(1E4, common.mustCall(() => { + checkInvocations(svr, { init: 1, before: 2, after: 2 }, + 'server: when server closed'); + checkInvocations(client, { init: 1, before: 3, after: 3 }, + 'client: when server closed'); + })); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('TLSWRAP'); + + checkInvocations(svr, { init: 1, before: 2, after: 2 }, + 'server: when process exits'); + checkInvocations(client, { init: 1, before: 3, after: 3 }, + 'client: when process exits'); +} diff --git a/test/async-hooks/test-ttywrap.readstream.js b/test/async-hooks/test-ttywrap.readstream.js new file mode 100644 index 00000000000000..017fb3142a7324 --- /dev/null +++ b/test/async-hooks/test-ttywrap.readstream.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); + +const hooks = initHooks(); +hooks.enable(); + +const ReadStream = require('tty').ReadStream; +const ttyStream = new ReadStream(0); + +const as = hooks.activitiesOfTypes('TTYWRAP'); +assert.strictEqual(as.length, 1, 'one TTYWRAP when tty created'); +const tty = as[0]; +assert.strictEqual(tty.type, 'TTYWRAP', 'tty wrap'); +assert.strictEqual(typeof tty.uid, 'number', 'uid is a number'); +assert.strictEqual(typeof tty.triggerId, 'number', 'triggerId is a number'); +checkInvocations(tty, { init: 1 }, 'when tty created'); + +ttyStream.end(common.mustCall(onend)); + +checkInvocations(tty, { init: 1 }, 'when tty.end() was invoked '); + +function onend() { + tick(2, common.mustCall(() => + checkInvocations( + tty, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when tty ended ') + )); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('TTYWRAP'); + checkInvocations(tty, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-ttywrap.writestream.js b/test/async-hooks/test-ttywrap.writestream.js new file mode 100644 index 00000000000000..c6dd6e5f145361 --- /dev/null +++ b/test/async-hooks/test-ttywrap.writestream.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const tty_fd = common.getTTYfd(); + +if (tty_fd < 0) + return common.skip('no valid TTY fd available'); +const ttyStream = (() => { + try { + return new (require('tty').WriteStream)(tty_fd); + } catch (e) { + return null; + } +})(); +if (ttyStream === null) + return common.skip('no valid TTY fd available'); + +const hooks = initHooks(); +hooks.enable(); + +const as = hooks.activitiesOfTypes('TTYWRAP'); +assert.strictEqual(as.length, 1, 'one TTYWRAP when tty created'); +const tty = as[0]; +assert.strictEqual(tty.type, 'TTYWRAP', 'tty wrap'); +assert.strictEqual(typeof tty.uid, 'number', 'uid is a number'); +assert.strictEqual(typeof tty.triggerId, 'number', 'triggerId is a number'); +checkInvocations(tty, { init: 1 }, 'when tty created'); + +ttyStream + .on('finish', common.mustCall(onfinish)) + .end(common.mustCall(onend)); + +checkInvocations(tty, { init: 1}, 'when tty.end() was invoked '); + +function onend() { + tick(2, common.mustCall(() => + checkInvocations( + tty, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when tty ended ') + )); +} + +function onfinish() { + tick(2, common.mustCall(() => + checkInvocations( + tty, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when tty ended ') + )); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('TTYWRAP'); + checkInvocations(tty, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-udpsendwrap.js b/test/async-hooks/test-udpsendwrap.js new file mode 100644 index 00000000000000..72b12c1e217cc1 --- /dev/null +++ b/test/async-hooks/test-udpsendwrap.js @@ -0,0 +1,58 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const dgram = require('dgram'); + +const hooks = initHooks(); + +hooks.enable(); +let send; + +const sock = dgram + .createSocket('udp4') + .on('listening', common.mustCall(onlistening)) + .bind(); + +function onlistening() { + sock.send( + new Buffer(2), 0, 2, sock.address().port, + undefined, common.mustCall(onsent)); + + // init not called synchronously because dns lookup alwasy wraps + // callback in a next tick even if no lookup is needed + // TODO (trevnorris) submit patch to fix creation of tick objects and instead + // create the send wrap synchronously. + assert.strictEqual( + hooks.activitiesOfTypes('UDPSENDWRAP').length, 0, + 'no udpsendwrap after sock connected and sock.send called'); +} + +function onsent() { + const as = hooks.activitiesOfTypes('UDPSENDWRAP'); + send = as[0]; + + assert.strictEqual(as.length, 1, + 'one UDPSENDWRAP created synchronously when message sent'); + assert.strictEqual(send.type, 'UDPSENDWRAP', 'send wrap'); + assert.strictEqual(typeof send.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof send.triggerId, 'number', 'triggerId is a number'); + checkInvocations(send, { init: 1, before: 1 }, 'when message sent'); + + sock.close(common.mustCall(onsockClosed)); +} + +function onsockClosed() { + checkInvocations(send, { init: 1, before: 1, after: 1 }, 'when sock closed'); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('UDPSENDWRAP'); + checkInvocations(send, { init: 1, before: 1, after: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-udpwrap.js b/test/async-hooks/test-udpwrap.js new file mode 100644 index 00000000000000..db81db8339eb3e --- /dev/null +++ b/test/async-hooks/test-udpwrap.js @@ -0,0 +1,38 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tick = require('./tick'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); +const dgram = require('dgram'); + +const hooks = initHooks(); + +hooks.enable(); +const sock = dgram.createSocket('udp4'); + +const as = hooks.activitiesOfTypes('UDPWRAP'); +const udpwrap = as[0]; +assert.strictEqual(as.length, 1, + 'one UDPWRAP handle after dgram.createSocket call'); +assert.strictEqual(udpwrap.type, 'UDPWRAP', 'udp wrap'); +assert.strictEqual(typeof udpwrap.uid, 'number', 'uid is a number'); +assert.strictEqual(typeof udpwrap.triggerId, 'number', 'triggerId is a number'); +checkInvocations(udpwrap, { init: 1 }, 'after dgram.createSocket call'); + +sock.close(common.mustCall(onsockClosed)); + +function onsockClosed() { + checkInvocations(udpwrap, { init: 1 }, 'when socket is closed'); + tick(2); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('UDPWRAP'); + checkInvocations(udpwrap, { init: 1, destroy: 1 }, + 'when process exits'); +} diff --git a/test/async-hooks/test-writewrap.js b/test/async-hooks/test-writewrap.js new file mode 100644 index 00000000000000..fecceaf13c5cad --- /dev/null +++ b/test/async-hooks/test-writewrap.js @@ -0,0 +1,98 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const initHooks = require('./init-hooks'); +const fs = require('fs'); +const { checkInvocations } = require('./hook-checks'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); + return; +} + +const tls = require('tls'); +const hooks = initHooks(); +hooks.enable(); + +// +// Creating server and listening on port +// +const server = tls + .createServer({ + cert: fs.readFileSync(common.fixturesDir + '/test_cert.pem'), + key: fs.readFileSync(common.fixturesDir + '/test_key.pem') + }) + .on('listening', common.mustCall(onlistening)) + .on('secureConnection', common.mustCall(onsecureConnection)) + .listen(common.PORT); + +assert.strictEqual(hooks.activitiesOfTypes('WRITEWRAP').length, 0, + 'no WRITEWRAP when server created'); + +function onlistening() { + assert.strictEqual(hooks.activitiesOfTypes('WRITEWRAP').length, 0, + 'no WRITEWRAP when server is listening'); + // + // Creating client and connecting it to server + // + tls + .connect(common.PORT, { rejectUnauthorized: false }) + .on('secureConnect', common.mustCall(onsecureConnect)); + + assert.strictEqual(hooks.activitiesOfTypes('WRITEWRAP').length, 0, + 'no WRITEWRAP when client created'); +} + +function checkDestroyedWriteWraps(n, stage) { + const as = hooks.activitiesOfTypes('WRITEWRAP'); + assert.strictEqual(as.length, n, n + ' WRITEWRAPs when ' + stage); + + function checkValidWriteWrap(w) { + assert.strictEqual(w.type, 'WRITEWRAP', 'write wrap'); + assert.strictEqual(typeof w.uid, 'number', 'uid is a number'); + assert.strictEqual(typeof w.triggerId, 'number', 'triggerId is a number'); + + checkInvocations(w, { init: 1, destroy: 1 }, 'when ' + stage); + } + as.forEach(checkValidWriteWrap); +} + +function onsecureConnection() { + // + // Server received client connection + // + checkDestroyedWriteWraps(3, 'server got secure connection'); +} + +function onsecureConnect() { + // + // Client connected to server + // + checkDestroyedWriteWraps(4, 'client connected'); + + // + // Destroying client socket + // + this.destroy(); + + checkDestroyedWriteWraps(4, 'client destroyed'); + + // + // Closing server + // + server.close(common.mustCall(onserverClosed)); + checkDestroyedWriteWraps(4, 'server closing'); +} + +function onserverClosed() { + checkDestroyedWriteWraps(4, 'server closed'); +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('WRITEWRAP'); + checkDestroyedWriteWraps(4, 'process exits'); +} diff --git a/test/async-hooks/test-zlib.zlib-binding.deflate.js b/test/async-hooks/test-zlib.zlib-binding.deflate.js new file mode 100644 index 00000000000000..715d1652b94444 --- /dev/null +++ b/test/async-hooks/test-zlib.zlib-binding.deflate.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); + +const hooks = initHooks(); + +hooks.enable(); +const Zlib = process.binding('zlib').Zlib; +const constants = process.binding('constants').zlib; + +const handle = new Zlib(constants.DEFLATE); + +const as = hooks.activitiesOfTypes('ZLIB'); +assert.strictEqual(as.length, 1, 'one zlib on when created handle'); +const hdl = as[0]; +assert.strictEqual(hdl.type, 'ZLIB', 'zlib'); +assert.strictEqual(typeof hdl.uid, 'number', 'uid is a number'); +assert.strictEqual(typeof hdl.triggerId, 'number', 'triggerId is a number'); +checkInvocations(hdl, { init: 1 }, 'when created handle'); + +handle.init( + constants.Z_DEFAULT_WINDOWBITS, + constants.Z_MIN_LEVEL, + constants.Z_DEFAULT_MEMLEVEL, + constants.Z_DEFAULT_STRATEGY, + Buffer.from('') +); +checkInvocations(hdl, { init: 1 }, 'when initialized handle'); + +const inBuf = Buffer.from('x'); +const outBuf = Buffer.allocUnsafe(1); + +let count = 2; +handle.callback = common.mustCall(onwritten, 2); +handle.write(true, inBuf, 0, 1, outBuf, 0, 1); +checkInvocations(hdl, { init: 1 }, 'when invoked write() on handle'); + +function onwritten() { + if (--count) { + // first write + checkInvocations(hdl, { init: 1, before: 1 }, + 'when wrote to handle the first time'); + handle.write(true, inBuf, 0, 1, outBuf, 0, 1); + } else { + // second write + checkInvocations(hdl, { init: 1, before: 2, after: 1 }, + 'when wrote to handle the second time'); + } +} + +process.on('exit', onexit); + +function onexit() { + hooks.disable(); + hooks.sanityCheck('ZLIB'); + // TODO: destroy never called here even with large amounts of ticks + // is that correct? + checkInvocations(hdl, { init: 1, before: 2, after: 2 }, 'when process exits'); +} diff --git a/test/async-hooks/testcfg.py b/test/async-hooks/testcfg.py new file mode 100644 index 00000000000000..9f75273938ee23 --- /dev/null +++ b/test/async-hooks/testcfg.py @@ -0,0 +1,6 @@ +import sys, os +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +import testpy + +def GetConfiguration(context, root): + return testpy.AsyncHooksTestConfiguration(context, root, 'async-hooks') diff --git a/test/async-hooks/tick.js b/test/async-hooks/tick.js new file mode 100644 index 00000000000000..b02315b10ca96f --- /dev/null +++ b/test/async-hooks/tick.js @@ -0,0 +1,13 @@ +'use strict'; +require('../common'); + +module.exports = function tick(x, cb) { + function ontick() { + if (--x === 0) { + if (typeof cb === 'function') cb(); + } else { + setImmediate(ontick); + } + } + setImmediate(ontick); +}; diff --git a/test/async-hooks/verify-graph.js b/test/async-hooks/verify-graph.js new file mode 100644 index 00000000000000..e87dd5596c31cb --- /dev/null +++ b/test/async-hooks/verify-graph.js @@ -0,0 +1,114 @@ +'use strict'; + +const assert = require('assert'); +require('../common'); + +function findInGraph(graph, type, n) { + let found = 0; + for (let i = 0; i < graph.length; i++) { + const node = graph[i]; + if (node.type === type) found++; + if (found === n) return node; + } +} + +function pruneTickObjects(activities) { + // remove one TickObject on each pass until none is left anymore + // not super efficient, but simplest especially to handle + // multiple TickObjects in a row + let foundTickObject = true; + + while (foundTickObject) { + foundTickObject = false; + let tickObjectIdx = -1; + for (let i = 0; i < activities.length; i++) { + if (activities[i].type !== 'TickObject') continue; + tickObjectIdx = i; + break; + } + + if (tickObjectIdx >= 0) { + foundTickObject = true; + + // point all triggerIds that point to the tickObject + // to its triggerId and findally remove it from the activities + const tickObject = activities[tickObjectIdx]; + const newTriggerId = tickObject.triggerId; + const oldTriggerId = tickObject.uid; + activities.forEach(function repointTriggerId(x) { + if (x.triggerId === oldTriggerId) x.triggerId = newTriggerId; + }); + activities.splice(tickObjectIdx, 1); + } + } + return activities; +} + +module.exports = function verifyGraph(hooks, graph) { + pruneTickObjects(hooks); + + // map actual ids to standin ids defined in the graph + const idtouid = {}; + const uidtoid = {}; + const typeSeen = {}; + const errors = []; + + const activities = pruneTickObjects(hooks.activities); + activities.forEach(processActivity); + + function processActivity(x) { + if (!typeSeen[x.type]) typeSeen[x.type] = 0; + typeSeen[x.type]++; + + const node = findInGraph(graph, x.type, typeSeen[x.type]); + if (node == null) return; + + idtouid[node.id] = x.uid; + uidtoid[x.uid] = node.id; + if (node.triggerId == null) return; + + const tid = idtouid[node.triggerId]; + if (x.triggerId === tid) return; + + errors.push({ + id: node.id, + expectedTid: node.triggerId, + actualTid: uidtoid[x.triggerId] + }); + } + + if (errors.length) { + errors.forEach((x) => + console.error( + `'${x.id}' expected to be triggered by '${x.expectedTid}', ` + + `but was triggered by '${x.actualTid}' instead.` + ) + ); + } + assert.strictEqual(errors.length, 0, 'Found errors while verifying graph.'); +}; + +// +// Helper to generate the input to the verifyGraph tests +// +function inspect(obj, depth) { + console.error(require('util').inspect(obj, false, depth || 5, true)); +} + +module.exports.printGraph = function printGraph(hooks) { + const ids = {}; + const uidtoid = {}; + const activities = pruneTickObjects(hooks.activities); + const graph = []; + activities.forEach(procesNode); + + function procesNode(x) { + const key = x.type.replace(/WRAP/, '').toLowerCase(); + if (!ids[key]) ids[key] = 1; + const id = key + ':' + ids[key]++; + uidtoid[x.uid] = id; + const triggerId = uidtoid[x.triggerId] || null; + graph.push({ type: x.type, id, triggerId }); + } + inspect(graph); +}; diff --git a/test/testpy/__init__.py b/test/testpy/__init__.py index f999b6a6baafaf..f542d19c708adb 100644 --- a/test/testpy/__init__.py +++ b/test/testpy/__init__.py @@ -164,3 +164,15 @@ def ListTests(self, current_path, path, arch, mode): result.append( SimpleTestCase(test, file_path, arch, mode, self.context, self, self.additional_flags)) return result + +class AsyncHooksTestConfiguration(SimpleTestConfiguration): + def __init__(self, context, root, section, additional=None): + super(AsyncHooksTestConfiguration, self).__init__(context, root, section, + additional) + + def ListTests(self, current_path, path, arch, mode): + result = super(AsyncHooksTestConfiguration, self).ListTests( + current_path, path, arch, mode) + for test in result: + test.parallel = True + return result diff --git a/tools/test.py b/tools/test.py index 7efc2574025057..10285591008712 100755 --- a/tools/test.py +++ b/tools/test.py @@ -1531,6 +1531,7 @@ def ExpandCommand(args): 'debugger', 'doctool', 'inspector', + 'async-hooks', ] From 3bca39df1b16af236c1ed8e00b13a38be8b509ec Mon Sep 17 00:00:00 2001 From: Trevor Norris Date: Wed, 5 Apr 2017 04:04:24 -0600 Subject: [PATCH 11/12] build: add test-async-hooks --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index cfbe9424d0949a..f896a0ced1626a 100644 --- a/Makefile +++ b/Makefile @@ -415,6 +415,9 @@ test-timers: test-timers-clean: $(MAKE) --directory=tools clean +test-async-hooks: + $(PYTHON) tools/test.py --mode=release async-hooks + ifneq ("","$(wildcard deps/v8/tools/run-tests.py)") test-v8: v8 From 75f2d2a1bf6e062c54c7a08374fcf22e13b911c3 Mon Sep 17 00:00:00 2001 From: Trevor Norris Date: Wed, 12 Apr 2017 00:02:21 -0600 Subject: [PATCH 12/12] makefile: add async-hooks to test and test-ci --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f896a0ced1626a..4a6a66ac286543 100644 --- a/Makefile +++ b/Makefile @@ -196,7 +196,7 @@ test: all $(MAKE) build-addons-napi $(MAKE) cctest $(PYTHON) tools/test.py --mode=release -J \ - addons addons-napi doctool inspector known_issues message pseudo-tty parallel sequential + addons addons-napi doctool inspector known_issues message pseudo-tty parallel sequential async-hooks $(MAKE) lint test-parallel: all @@ -323,7 +323,7 @@ test-all-valgrind: test-build $(PYTHON) tools/test.py --mode=debug,release --valgrind CI_NATIVE_SUITES := addons addons-napi -CI_JS_SUITES := doctool inspector known_issues message parallel pseudo-tty sequential +CI_JS_SUITES := doctool inspector known_issues message parallel pseudo-tty sequential async-hooks # Build and test addons without building anything else test-ci-native: LOGLEVEL := info