diff --git a/Makefile b/Makefile index cfbe9424d0949a..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 @@ -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 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 new file mode 100644 index 00000000000000..867b5eb52da14d --- /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 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 +// 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; + +const { async_id_symbol, trigger_id_symbol } = async_wrap; + +// Used in AsyncHook and AsyncEvent. +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/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/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/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/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/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-inl.h b/src/async-wrap-inl.h index 80419405ba78e8..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 int64_t AsyncWrap::get_uid() const { - return uid_; +inline double AsyncWrap::get_trigger_id() const { + return trigger_id_; } diff --git a/src/async-wrap.cc b/src/async-wrap.cc index bc3e049d262c81..06567d6f7ccf39 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; @@ -44,9 +45,13 @@ using v8::MaybeLocal; using v8::Number; using v8::Object; using v8::RetainedObjectInfo; +using v8::Symbol; using v8::TryCatch; +using v8::Uint32Array; using v8::Value; +using AsyncHooks = node::Environment::AsyncHooks; + namespace node { static const char* const provider_names[] = { @@ -57,6 +62,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); @@ -109,7 +116,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()); @@ -126,55 +135,117 @@ 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 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 DisableHooksJS(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - env->async_hooks()->set_enable_callbacks(0); +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); - 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 +} + + +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::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()); } @@ -186,57 +257,92 @@ void AsyncWrap::Initialize(Local target, HandleScope scope(isolate); env->SetMethod(target, "setupHooks", SetupHooks); - env->SetMethod(target, "disable", DisableHooksJS); - env->SetMethod(target, "enable", EnableHooksJS); + 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); + +#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); - 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_destroy_function(Local()); -} + // 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 -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; - 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(); + env->set_async_hooks_init_function(Local()); + env->set_async_hooks_before_function(Local()); + env->set_async_hooks_after_function(Local()); + env->set_async_hooks_destroy_function(Local()); } @@ -252,64 +358,56 @@ void LoadAsyncWrapperInfo(Environment* env) { AsyncWrap::AsyncWrap(Environment* env, Local object, - ProviderType provider, - AsyncWrap* parent) - : BaseObject(env, object), bits_(static_cast(provider) << 1), - uid_(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; +AsyncWrap::~AsyncWrap() { + PushBackDestroyId(env(), get_id()); +} - // 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())) + +// 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::AsyncReset() { + AsyncHooks* async_hooks = env()->async_hooks(); + async_id_ = env()->new_async_id(); + trigger_id_ = env()->get_init_trigger_id(); + + // Nothing to execute, so can continue normally. + if (async_hooks->fields()[AsyncHooks::kInit] == 0) { return; + } - HandleScope scope(env->isolate()); + HandleScope scope(env()->isolate()); + Local init_fn = env()->async_hooks_init_function(); Local argv[] = { - Number::New(env->isolate(), get_uid()), - Int32::New(env->isolate(), provider), - Null(env->isolate()), - Null(env->isolate()) + Number::New(env()->isolate(), get_id()), + env()->async_hooks()->provider_string(provider_type()), + object(), + Number::New(env()->isolate(), get_trigger_id()), }; - if (parent != nullptr) { - argv[2] = Number::New(env->isolate(), parent->get_uid()); - argv[3] = parent->object(); - } - - TryCatch try_catch(env->isolate()); - - MaybeLocal ret = - init_fn->Call(env->context(), object, arraysize(argv), argv); + TryCatch try_catch(env()->isolate()); + MaybeLocal ret = init_fn->Call( + env()->context(), object(), arraysize(argv), argv); if (ret.IsEmpty()) { - ClearFatalExceptionHandlers(env); - FatalException(env->isolate(), try_catch); + ClearFatalExceptionHandlers(env()); + FatalException(env()->isolate(), try_catch); } - - bits_ |= 1; // ran_init_callback() is true now. -} - - -AsyncWrap::~AsyncWrap() { - if (!ran_init_callback()) - return; - - if (env()->destroy_ids_list()->empty()) - uv_idle_start(env()->destroy_ids_idle_handle(), DestroyIdsCb); - - env()->destroy_ids_list()->push_back(get_uid()); } @@ -318,11 +416,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_uid()); + AsyncHooks* async_hooks = env()->async_hooks(); Local context = object(); Local domain; + Local uid; bool has_domain = false; Environment::AsyncCallbackScope callback_scope(env()); @@ -347,9 +444,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); @@ -357,14 +460,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); @@ -372,9 +484,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()); @@ -387,7 +498,7 @@ Local AsyncWrap::MakeCallback(const Local cb, } if (callback_scope.in_makecallback()) { - return ret; + return ret_v; } Environment::TickInfo* tick_info = env()->tick_info(); @@ -396,18 +507,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 a044a01863202d..8a93e838786297 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,12 +71,12 @@ class AsyncWrap : public BaseObject { PROVIDER_ ## PROVIDER, NODE_ASYNC_PROVIDER_TYPES(V) #undef V + PROVIDERS_LENGTH, }; AsyncWrap(Environment* env, v8::Local object, - ProviderType provider, - AsyncWrap* parent = nullptr); + ProviderType provider); virtual ~AsyncWrap(); @@ -82,34 +84,41 @@ class AsyncWrap : public BaseObject { v8::Local unused, v8::Local context); - static void DestroyIdsCb(uv_idle_t* handle); + static void GetAsyncId(const v8::FunctionCallbackInfo& args); + 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; - inline int64_t get_uid() const; + inline double get_id() const; + + inline double get_trigger_id() const; + + void AsyncReset(); // 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 int64_t uid_; + 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/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/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 978f8ca819dec1..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 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 void Environment::AsyncHooks::set_enable_callbacks(uint32_t flag) { - fields_[kEnableCallbacks] = flag; +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_uid_(0), debugger_agent_(this), #if HAVE_INSPECTOR inspector_agent_(this), @@ -208,12 +312,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); @@ -260,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() && @@ -343,14 +436,42 @@ 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 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() { +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_; @@ -496,12 +617,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 a719cda2d40c6e..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,21 +250,23 @@ 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) \ 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) \ @@ -286,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 { @@ -326,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: @@ -446,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; @@ -483,10 +554,18 @@ class Environment { void PrintSyncTrace() const; inline void set_trace_sync_io(bool value); - inline int64_t 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(); + inline std::vector* destroy_ids_list(); inline double* heap_statistics_buffer() const; inline void set_heap_statistics_buffer(double* pointer); @@ -529,8 +608,6 @@ class Environment { const char* name, v8::FunctionCallback callback); - inline v8::Local NewInternalFieldObject(); - void AtExit(void (*cb)(void* arg), void* arg); void RunAtExitCallbacks(); @@ -592,9 +669,9 @@ class Environment { bool using_domains_; bool printed_error_; bool trace_sync_io_; + bool abort_on_uncaught_exception_; size_t makecallback_cntr_; - int64_t async_wrap_uid_; - std::vector destroy_ids_list_; + std::vector destroy_ids_list_; debugger::Agent debugger_agent_; #if HAVE_INSPECTOR inspector::Agent inspector_agent_; 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/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 e51c4ae9b35084..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()); } @@ -221,6 +210,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/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_crypto.cc b/src/node_crypto.cc index 0f9ed3434eca09..4593fdf44ac7fc 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; @@ -2736,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); @@ -5314,7 +5316,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), @@ -5533,7 +5535,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, @@ -5579,7 +5582,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)) { @@ -5701,7 +5704,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 +6173,20 @@ void InitCrypto(Local target, PublicKeyCipher::Cipher); + + 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); } } // namespace crypto diff --git a/src/node_crypto.h b/src/node_crypto.h index ffb8444ce60145..ad1b493596aa7a 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -402,12 +402,13 @@ 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), 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..4b0158c4208018 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->AsyncReset(); parser->Init(type); } @@ -784,6 +786,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..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); } @@ -69,6 +70,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 +98,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()); @@ -110,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/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-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_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..8735e6a7eab06a 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()); @@ -79,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 f2525b1fb1b22a..4967b407145c1c 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); } @@ -84,6 +85,8 @@ void TCPWrap::Initialize(Local target, "onconnection"), Null(env->isolate())); + env->SetProtoMethod(t, "getAsyncId", AsyncWrap::GetAsyncId); + env->SetProtoMethod(t, "asyncReset", AsyncWrap::AsyncReset); env->SetProtoMethod(t, "close", HandleWrap::Close); @@ -116,9 +119,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()); @@ -131,24 +136,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. @@ -276,6 +271,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(), @@ -311,6 +307,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/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 82476e755d285b..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); @@ -150,17 +152,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/src/udp_wrap.cc b/src/udp_wrap.cc index 4f5388080e2bfe..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,10 +49,13 @@ using v8::String; using v8::Undefined; using v8::Value; +using AsyncHooks = Environment::AsyncHooks; + 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 +73,11 @@ SendWrap::SendWrap(Environment* env, } +SendWrap::~SendWrap() { + ClearWrap(object()); +} + + inline bool SendWrap::have_callback() const { return have_callback_; } @@ -78,10 +85,11 @@ inline bool SendWrap::have_callback() const { static void NewSendWrap(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); + ClearWrap(args.This()); } -UDPWrap::UDPWrap(Environment* env, Local object, AsyncWrap* parent) +UDPWrap::UDPWrap(Environment* env, Local object) : HandleWrap(env, object, reinterpret_cast(&handle_), @@ -129,6 +137,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 +146,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()); @@ -145,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()); } @@ -293,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; @@ -440,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); 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/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/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-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-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-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-getasyncid.js b/test/parallel/test-async-wrap-getasyncid.js new file mode 100644 index 00000000000000..5ab6240ed25da8 --- /dev/null +++ b/test/parallel/test-async-wrap-getasyncid.js @@ -0,0 +1,244 @@ +'use strict'; + +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'); + 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'); +} + + +{ + // 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; + } +} + + +{ + 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'); +} 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-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'); +})); 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); 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); +}); 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', ]