diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index a576eeb5e579f6..492e3a21190e2e 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -45,6 +45,8 @@ if (global.__coverage__) NativeModule.require('internal/process/write-coverage').setup(); + NativeModule.require('internal/inspector_async_hook').setup(); + // Do not initialize channel in debugger agent, it deletes env variable // and the main thread won't see it. if (process.argv[1] !== '--debug-agent') diff --git a/lib/internal/inspector_async_hook.js b/lib/internal/inspector_async_hook.js new file mode 100644 index 00000000000000..e32a026cd69155 --- /dev/null +++ b/lib/internal/inspector_async_hook.js @@ -0,0 +1,64 @@ +'use strict'; + +const { createHook } = require('async_hooks'); +const inspector = process.binding('inspector'); +const config = process.binding('config'); + +if (!inspector || !inspector.asyncTaskScheduled) { + exports.setup = function() {}; + return; +} + +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + // It's difficult to tell which tasks will be recurring and which won't, + // therefore we mark all tasks as recurring. Based on the discussion + // in https://github.com/nodejs/node/pull/13870#discussion_r124515293, + // this should be fine as long as we call asyncTaskCanceled() too. + const recurring = true; + inspector.asyncTaskScheduled(type, asyncId, recurring); + }, + + before(asyncId) { + inspector.asyncTaskStarted(asyncId); + }, + + after(asyncId) { + inspector.asyncTaskFinished(asyncId); + }, + + destroy(asyncId) { + inspector.asyncTaskCanceled(asyncId); + }, +}); + +function enable() { + if (config.bits < 64) { + // V8 Inspector stores task ids as (void*) pointers. + // async_hooks store ids as 64bit numbers. + // As a result, we cannot reliably translate async_hook ids to V8 async_task + // ids on 32bit platforms. + process.emitWarning( + 'Warning: Async stack traces in debugger are not available ' + + `on ${config.bits}bit platforms. The feature is disabled.`, + { + code: 'INSPECTOR_ASYNC_STACK_TRACES_NOT_AVAILABLE', + }); + } else { + hook.enable(); + } +} + +function disable() { + hook.disable(); +} + +exports.setup = function() { + inspector.registerAsyncHook(enable, disable); + + if (inspector.isEnabled()) { + // If the inspector was already enabled via --inspect or --inspect-brk, + // the we need to enable the async hook immediately at startup. + enable(); + } +}; diff --git a/node.gyp b/node.gyp index 14eb9886785a7f..5ef250d5dd7023 100644 --- a/node.gyp +++ b/node.gyp @@ -88,6 +88,7 @@ 'lib/internal/freelist.js', 'lib/internal/fs.js', 'lib/internal/http.js', + 'lib/internal/inspector_async_hook.js', 'lib/internal/linkedlist.js', 'lib/internal/net.js', 'lib/internal/module.js', diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index 0f9caa32f2a22e..2520fbdd533bdc 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -23,20 +23,27 @@ namespace node { namespace inspector { namespace { + +using node::FatalError; + using v8::Array; +using v8::Boolean; using v8::Context; using v8::External; using v8::Function; using v8::FunctionCallbackInfo; using v8::HandleScope; +using v8::Integer; using v8::Isolate; using v8::Local; using v8::Maybe; using v8::MaybeLocal; +using v8::Name; using v8::NewStringType; using v8::Object; using v8::Persistent; using v8::String; +using v8::Undefined; using v8::Value; using v8_inspector::StringBuffer; @@ -616,6 +623,28 @@ class NodeInspectorClient : public V8InspectorClient { timers_.erase(data); } + // Async stack traces instrumentation. + void AsyncTaskScheduled(const StringView& task_name, void* task, + bool recurring) { + client_->asyncTaskScheduled(task_name, task, recurring); + } + + void AsyncTaskCanceled(void* task) { + client_->asyncTaskCanceled(task); + } + + void AsyncTaskStarted(void* task) { + client_->asyncTaskStarted(task); + } + + void AsyncTaskFinished(void* task) { + client_->asyncTaskFinished(task); + } + + void AllAsyncTasksCanceled() { + client_->allAsyncTasksCanceled(); + } + private: node::Environment* env_; v8::Platform* platform_; @@ -676,9 +705,21 @@ bool Agent::StartIoThread(bool wait_for_connect) { } v8::Isolate* isolate = parent_env_->isolate(); + HandleScope handle_scope(isolate); + + // Enable tracking of async stack traces + if (!enable_async_hook_function_.IsEmpty()) { + Local enable_fn = enable_async_hook_function_.Get(isolate); + auto context = parent_env_->context(); + auto result = enable_fn->Call(context, Undefined(isolate), 0, nullptr); + if (result.IsEmpty()) { + FatalError( + "node::InspectorAgent::StartIoThread", + "Cannot enable Inspector's AsyncHook, please report this."); + } + } // Send message to enable debug in workers - HandleScope handle_scope(isolate); Local process_object = parent_env_->process_object(); Local emit_fn = process_object->Get(FIXED_ONE_BYTE_STRING(isolate, "emit")); @@ -717,10 +758,40 @@ void Agent::Stop() { if (io_ != nullptr) { io_->Stop(); io_.reset(); + enabled_ = false; + } + + v8::Isolate* isolate = parent_env_->isolate(); + HandleScope handle_scope(isolate); + + // Disable tracking of async stack traces + if (!disable_async_hook_function_.IsEmpty()) { + Local disable_fn = disable_async_hook_function_.Get(isolate); + auto result = disable_fn->Call(parent_env_->context(), + Undefined(parent_env_->isolate()), 0, nullptr); + if (result.IsEmpty()) { + FatalError( + "node::InspectorAgent::Stop", + "Cannot disable Inspector's AsyncHook, please report this."); + } } } void Agent::Connect(InspectorSessionDelegate* delegate) { + if (!enabled_) { + // Enable tracking of async stack traces + v8::Isolate* isolate = parent_env_->isolate(); + HandleScope handle_scope(isolate); + auto context = parent_env_->context(); + Local enable_fn = enable_async_hook_function_.Get(isolate); + auto result = enable_fn->Call(context, Undefined(isolate), 0, nullptr); + if (result.IsEmpty()) { + FatalError( + "node::InspectorAgent::Connect", + "Cannot enable Inspector's AsyncHook, please report this."); + } + } + enabled_ = true; client_->connectFrontend(delegate); } @@ -773,6 +844,34 @@ void Agent::PauseOnNextJavascriptStatement(const std::string& reason) { channel->schedulePauseOnNextStatement(reason); } +void Agent::RegisterAsyncHook(Isolate* isolate, + v8::Local enable_function, + v8::Local disable_function) { + enable_async_hook_function_.Reset(isolate, enable_function); + disable_async_hook_function_.Reset(isolate, disable_function); +} + +void Agent::AsyncTaskScheduled(const StringView& task_name, void* task, + bool recurring) { + client_->AsyncTaskScheduled(task_name, task, recurring); +} + +void Agent::AsyncTaskCanceled(void* task) { + client_->AsyncTaskCanceled(task); +} + +void Agent::AsyncTaskStarted(void* task) { + client_->AsyncTaskStarted(task); +} + +void Agent::AsyncTaskFinished(void* task) { + client_->AsyncTaskFinished(task); +} + +void Agent::AllAsyncTasksCanceled() { + client_->AllAsyncTasksCanceled(); +} + void Open(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); inspector::Agent* agent = env->inspector_agent(); @@ -810,6 +909,59 @@ void Url(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(OneByteString(env->isolate(), url.c_str())); } +static void* GetAsyncTask(int64_t asyncId) { + // The inspector assumes that when other clients use its asyncTask* API, + // they use real pointers, or at least something aligned like real pointer. + // In general it means that our task_id should always be even. + // + // On 32bit platforms, the 64bit asyncId would get truncated when converted + // to a 32bit pointer. However, the javascript part will never enable + // the async_hook on 32bit platforms, therefore the truncation will never + // happen in practice. + return reinterpret_cast(asyncId << 1); +} + +template +static void InvokeAsyncTaskFnWithId(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsNumber()); + int64_t task_id = args[0]->IntegerValue(env->context()).FromJust(); + (env->inspector_agent()->*asyncTaskFn)(GetAsyncTask(task_id)); +} + +static void AsyncTaskScheduledWrapper(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CHECK(args[0]->IsString()); + Local task_name = args[0].As(); + String::Value task_name_value(task_name); + StringView task_name_view(*task_name_value, task_name_value.length()); + + CHECK(args[1]->IsNumber()); + int64_t task_id = args[1]->IntegerValue(env->context()).FromJust(); + void* task = GetAsyncTask(task_id); + + CHECK(args[2]->IsBoolean()); + bool recurring = args[2]->BooleanValue(env->context()).FromJust(); + + env->inspector_agent()->AsyncTaskScheduled(task_name_view, task, recurring); +} + +static void RegisterAsyncHookWrapper(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + CHECK(args[0]->IsFunction()); + v8::Local enable_function = args[0].As(); + CHECK(args[1]->IsFunction()); + v8::Local disable_function = args[1].As(); + env->inspector_agent()->RegisterAsyncHook(env->isolate(), + enable_function, disable_function); +} + +static void IsEnabled(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + args.GetReturnValue().Set(env->inspector_agent()->enabled()); +} // static void Agent::InitInspector(Local target, Local unused, @@ -830,6 +982,17 @@ void Agent::InitInspector(Local target, Local unused, env->SetMethod(target, "connect", ConnectJSBindingsSession); env->SetMethod(target, "open", Open); env->SetMethod(target, "url", Url); + + env->SetMethod(target, "asyncTaskScheduled", AsyncTaskScheduledWrapper); + env->SetMethod(target, "asyncTaskCanceled", + InvokeAsyncTaskFnWithId<&Agent::AsyncTaskCanceled>); + env->SetMethod(target, "asyncTaskStarted", + InvokeAsyncTaskFnWithId<&Agent::AsyncTaskStarted>); + env->SetMethod(target, "asyncTaskFinished", + InvokeAsyncTaskFnWithId<&Agent::AsyncTaskFinished>); + + env->SetMethod(target, "registerAsyncHook", RegisterAsyncHookWrapper); + env->SetMethod(target, "isEnabled", IsEnabled); } void Agent::RequestIoThreadStart() { diff --git a/src/inspector_agent.h b/src/inspector_agent.h index cf9a8bff8645ec..6ec1bc28dc2e22 100644 --- a/src/inspector_agent.h +++ b/src/inspector_agent.h @@ -16,17 +16,7 @@ namespace node { class Environment; } // namespace node -namespace v8 { -class Context; -template -class FunctionCallbackInfo; -template -class Local; -class Message; -class Object; -class Platform; -class Value; -} // namespace v8 +#include "v8.h" namespace v8_inspector { class StringView; @@ -67,6 +57,18 @@ class Agent { void FatalException(v8::Local error, v8::Local message); + // Async stack traces instrumentation. + void AsyncTaskScheduled(const v8_inspector::StringView& taskName, void* task, + bool recurring); + void AsyncTaskCanceled(void* task); + void AsyncTaskStarted(void* task); + void AsyncTaskFinished(void* task); + void AllAsyncTasksCanceled(); + + void RegisterAsyncHook(v8::Isolate* isolate, + v8::Local enable_function, + v8::Local disable_function); + // These methods are called by the WS protocol and JS binding to create // inspector sessions. The inspector responds by using the delegate to send // messages back. @@ -107,6 +109,9 @@ class Agent { std::string path_; DebugOptions debug_options_; int next_context_number_; + + v8::Persistent enable_async_hook_function_; + v8::Persistent disable_async_hook_function_; }; } // namespace inspector diff --git a/src/node_config.cc b/src/node_config.cc index 041e18f6b76ff9..d4fb991c5818a2 100644 --- a/src/node_config.cc +++ b/src/node_config.cc @@ -13,6 +13,7 @@ using v8::Boolean; using v8::Context; using v8::Integer; using v8::Local; +using v8::Number; using v8::Object; using v8::ReadOnly; using v8::String; @@ -30,6 +31,15 @@ using v8::Value; True(env->isolate()), ReadOnly).FromJust(); \ } while (0) +#define READONLY_PROPERTY(obj, name, value) \ + do { \ + obj->DefineOwnProperty(env->context(), \ + OneByteString(env->isolate(), name), \ + value, \ + ReadOnly).FromJust(); \ + } while (0) + + static void InitConfig(Local target, Local unused, Local context) { @@ -91,6 +101,10 @@ static void InitConfig(Local target, if (config_expose_http2) READONLY_BOOLEAN_PROPERTY("exposeHTTP2"); + + READONLY_PROPERTY(target, + "bits", + Number::New(env->isolate(), 8 * sizeof(intptr_t))); } // InitConfig } // namespace node diff --git a/test/common/README.md b/test/common/README.md index 59b02cf52a9a48..b8d9af2fcf70f5 100644 --- a/test/common/README.md +++ b/test/common/README.md @@ -325,6 +325,16 @@ Path to the 'root' directory. either `/` or `c:\\` (windows) Logs '1..0 # Skipped: ' + `msg` and exits with exit code `0`. +### skipIfInspectorDisabled() + +Skip the rest of the tests in the current file when the Inspector +was disabled at compile time. + +### skipIf32Bits() + +Skip the rest of the tests in the current file when the Node.js executable +was compiled with a pointer size smaller than 64 bits. + ### spawnPwd(options) * `options` [<Object>] * return [<Object>] diff --git a/test/common/index.js b/test/common/index.js index 2564b227fe3efd..8175474818b9dc 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -746,6 +746,12 @@ exports.skipIfInspectorDisabled = function skipIfInspectorDisabled() { } }; +exports.skipIf32Bits = function skipIf32Bits() { + if (process.binding('config').bits < 64) { + exports.skip('The tested feature is not available in 32bit builds'); + } +}; + const arrayBufferViews = [ Int8Array, Uint8Array, diff --git a/test/inspector/inspector-helper.js b/test/inspector/inspector-helper.js index e9464c3679a00c..9c1cca3a771293 100644 --- a/test/inspector/inspector-helper.js +++ b/test/inspector/inspector-helper.js @@ -9,7 +9,7 @@ const url = require('url'); const _MAINSCRIPT = path.join(common.fixturesDir, 'loop.js'); const DEBUG = false; -const TIMEOUT = 15 * 1000; +const TIMEOUT = common.platformTimeout(15 * 1000); function spawnChildProcess(inspectorFlags, scriptContents, scriptFile) { const args = [].concat(inspectorFlags); @@ -253,9 +253,7 @@ class InspectorSession { .waitForNotification( (notification) => this._isBreakOnLineNotification(notification, line, url), - `break on ${url}:${line}`) - .then((notification) => - notification.params.callFrames[0].scopeChain[0].object.objectId); + `break on ${url}:${line}`); } _matchesConsoleOutputNotification(notification, type, values) { @@ -321,13 +319,24 @@ class NodeInstance { }); } + static async startViaSignal(scriptContents) { + const instance = new NodeInstance( + [], `${scriptContents}\nprocess._rawDebug('started');`, undefined); + const msg = 'Timed out waiting for process to start'; + while (await common.fires(instance.nextStderrString(), msg, TIMEOUT) !== + 'started') {} + process._debugProcess(instance._process.pid); + return instance; + } + onStderrLine(line) { console.log('[err]', line); if (this._portCallback) { const matches = line.match(/Debugger listening on ws:\/\/.+:(\d+)\/.+/); - if (matches) + if (matches) { this._portCallback(matches[1]); - this._portCallback = null; + this._portCallback = null; + } } if (this._stderrLineCallback) { this._stderrLineCallback(line); diff --git a/test/inspector/test-async-hook-setup-at-inspect-brk.js b/test/inspector/test-async-hook-setup-at-inspect-brk.js new file mode 100644 index 00000000000000..70887ff63d9d4e --- /dev/null +++ b/test/inspector/test-async-hook-setup-at-inspect-brk.js @@ -0,0 +1,45 @@ +'use strict'; +const common = require('../common'); +common.skipIfInspectorDisabled(); +common.skipIf32Bits(); +common.crashOnUnhandledRejection(); +const { NodeInstance } = require('./inspector-helper.js'); +const assert = require('assert'); + +const script = ` +setTimeout(() => { + debugger; + process.exitCode = 55; +}, 50); +`; + +async function checkAsyncStackTrace(session) { + console.error('[test]', 'Verify basic properties of asyncStackTrace'); + const paused = await session.waitForBreakOnLine(2, '[eval]'); + assert(paused.params.asyncStackTrace, + `${Object.keys(paused.params)} contains "asyncStackTrace" property`); + assert(paused.params.asyncStackTrace.description, 'Timeout'); + assert(paused.params.asyncStackTrace.callFrames + .some((frame) => frame.functionName === 'Module._compile')); +} + +async function runTests() { + const instance = new NodeInstance(undefined, script); + const session = await instance.connectInspectorSession(); + await session.send([ + { 'method': 'Runtime.enable' }, + { 'method': 'Debugger.enable' }, + { 'method': 'Debugger.setAsyncCallStackDepth', + 'params': { 'maxDepth': 10 } }, + { 'method': 'Debugger.setBlackboxPatterns', + 'params': { 'patterns': [] } }, + { 'method': 'Runtime.runIfWaitingForDebugger' } + ]); + + await checkAsyncStackTrace(session); + + await session.runToCompletion(); + assert.strictEqual(55, (await instance.expectShutdown()).exitCode); +} + +runTests(); diff --git a/test/inspector/test-async-hook-setup-at-inspect.js b/test/inspector/test-async-hook-setup-at-inspect.js new file mode 100644 index 00000000000000..bbf418a858838c --- /dev/null +++ b/test/inspector/test-async-hook-setup-at-inspect.js @@ -0,0 +1,70 @@ +'use strict'; +const common = require('../common'); +common.skipIfInspectorDisabled(); +common.skipIf32Bits(); +common.crashOnUnhandledRejection(); +const { NodeInstance } = require('../inspector/inspector-helper.js'); +const assert = require('assert'); + +// Even with --inspect, the default async call stack depth is 0. We need a +// chance to call Debugger.setAsyncCallStackDepth *before* activating the timer +// for async stack traces to work. +const script = ` +process._rawDebug('Waiting until the inspector is activated...'); +const waiting = setInterval(() => { debugger; }, 50); + +// This function is called by the inspector client (session) +function setupTimeoutWithBreak() { + clearInterval(waiting); + process._rawDebug('Debugger ready, setting up timeout with a break'); + setTimeout(() => { debugger; }, 50); +} +`; + +async function waitForInitialSetup(session) { + console.error('[test]', 'Waiting for initial setup'); + await session.waitForBreakOnLine(2, '[eval]'); +} + +async function setupTimeoutForStackTrace(session) { + console.error('[test]', 'Setting up timeout for async stack trace'); + await session.send([ + { 'method': 'Runtime.evaluate', + 'params': { expression: 'setupTimeoutWithBreak()' } }, + { 'method': 'Debugger.resume' } + ]); +} + +async function checkAsyncStackTrace(session) { + console.error('[test]', 'Verify basic properties of asyncStackTrace'); + const paused = await session.waitForBreakOnLine(8, '[eval]'); + assert(paused.params.asyncStackTrace, + `${Object.keys(paused.params)} contains "asyncStackTrace" property`); + assert(paused.params.asyncStackTrace.description, 'Timeout'); + assert(paused.params.asyncStackTrace.callFrames + .some((frame) => frame.functionName === 'setupTimeoutWithBreak')); +} + +async function runTests() { + const instance = new NodeInstance(['--inspect=0'], script); + const session = await instance.connectInspectorSession(); + await session.send([ + { 'method': 'Runtime.enable' }, + { 'method': 'Debugger.enable' }, + { 'method': 'Debugger.setAsyncCallStackDepth', + 'params': { 'maxDepth': 10 } }, + { 'method': 'Debugger.setBlackboxPatterns', + 'params': { 'patterns': [] } }, + { 'method': 'Runtime.runIfWaitingForDebugger' } + ]); + + await waitForInitialSetup(session); + await setupTimeoutForStackTrace(session); + await checkAsyncStackTrace(session); + + console.error('[test]', 'Stopping child instance'); + session.disconnect(); + instance.kill(); +} + +runTests(); diff --git a/test/inspector/test-async-hook-setup-at-signal.js b/test/inspector/test-async-hook-setup-at-signal.js new file mode 100644 index 00000000000000..f0150bc7acc0a9 --- /dev/null +++ b/test/inspector/test-async-hook-setup-at-signal.js @@ -0,0 +1,81 @@ +'use strict'; +const common = require('../common'); +common.skipIfInspectorDisabled(); +common.skipIf32Bits(); +common.crashOnUnhandledRejection(); +const { NodeInstance } = require('../inspector/inspector-helper.js'); +const assert = require('assert'); + +const script = ` +process._rawDebug('Waiting until a signal enables the inspector...'); +let waiting = setInterval(waitUntilDebugged, 50); + +function waitUntilDebugged() { + if (!process.binding('inspector').isEnabled()) return; + clearInterval(waiting); + // At this point, even though the Inspector is enabled, the default async + // call stack depth is 0. We need a chance to call + // Debugger.setAsyncCallStackDepth *before* activating the actual timer for + // async stack traces to work. Directly using a debugger statement would be + // too brittle, and using a longer timeout would unnecesarily slow down the + // test on most machines. Triggering a debugger break through an interval is + // a faster and more reliable way. + process._rawDebug('Signal received, waiting for debugger setup'); + waiting = setInterval(() => { debugger; }, 50); +} + +// This function is called by the inspector client (session) +function setupTimeoutWithBreak() { + clearInterval(waiting); + process._rawDebug('Debugger ready, setting up timeout with a break'); + setTimeout(() => { debugger; }, 50); +} +`; + +async function waitForInitialSetup(session) { + console.error('[test]', 'Waiting for initial setup'); + await session.waitForBreakOnLine(15, '[eval]'); +} + +async function setupTimeoutForStackTrace(session) { + console.error('[test]', 'Setting up timeout for async stack trace'); + await session.send([ + { 'method': 'Runtime.evaluate', + 'params': { expression: 'setupTimeoutWithBreak()' } }, + { 'method': 'Debugger.resume' } + ]); +} + +async function checkAsyncStackTrace(session) { + console.error('[test]', 'Verify basic properties of asyncStackTrace'); + const paused = await session.waitForBreakOnLine(22, '[eval]'); + assert(paused.params.asyncStackTrace, + `${Object.keys(paused.params)} contains "asyncStackTrace" property`); + assert(paused.params.asyncStackTrace.description, 'Timeout'); + assert(paused.params.asyncStackTrace.callFrames + .some((frame) => frame.functionName === 'setupTimeoutWithBreak')); +} + +async function runTests() { + const instance = await NodeInstance.startViaSignal(script); + const session = await instance.connectInspectorSession(); + await session.send([ + { 'method': 'Runtime.enable' }, + { 'method': 'Debugger.enable' }, + { 'method': 'Debugger.setAsyncCallStackDepth', + 'params': { 'maxDepth': 10 } }, + { 'method': 'Debugger.setBlackboxPatterns', + 'params': { 'patterns': [] } }, + { 'method': 'Runtime.runIfWaitingForDebugger' } + ]); + + await waitForInitialSetup(session); + await setupTimeoutForStackTrace(session); + await checkAsyncStackTrace(session); + + console.error('[test]', 'Stopping child instance'); + session.disconnect(); + instance.kill(); +} + +runTests(); diff --git a/test/inspector/test-async-hook-teardown-at-debug-end.js b/test/inspector/test-async-hook-teardown-at-debug-end.js new file mode 100644 index 00000000000000..9084efdd412bcf --- /dev/null +++ b/test/inspector/test-async-hook-teardown-at-debug-end.js @@ -0,0 +1,33 @@ +'use strict'; +const common = require('../common'); +common.skipIfInspectorDisabled(); +common.skipIf32Bits(); + +const spawn = require('child_process').spawn; + +const script = ` +const assert = require('assert'); + +// Verify that inspector-async-hook is registered +// by checking that emitInit with invalid arguments +// throw an error. +// See test/async-hooks/test-emit-init.js +assert.throws( + () => async_hooks.emitInit(), + 'inspector async hook should have been enabled initially'); + +process._debugEnd(); + +// Verify that inspector-async-hook is no longer registered, +// thus emitInit() ignores invalid arguments +// See test/async-hooks/test-emit-init.js +assert.doesNotThrow( + () => async_hooks.emitInit(), + 'inspector async hook should have beend disabled by _debugEnd()'); +`; + +const args = ['--inspect', '-e', script]; +const child = spawn(process.execPath, args, { stdio: 'inherit' }); +child.on('exit', (code, signal) => { + process.exit(code || signal); +}); diff --git a/test/inspector/test-async-stack-traces-promise-then.js b/test/inspector/test-async-stack-traces-promise-then.js new file mode 100644 index 00000000000000..68584b0a3c5dad --- /dev/null +++ b/test/inspector/test-async-stack-traces-promise-then.js @@ -0,0 +1,69 @@ +'use strict'; +const common = require('../common'); +common.skipIfInspectorDisabled(); +common.skipIf32Bits(); +common.crashOnUnhandledRejection(); +const { NodeInstance } = require('./inspector-helper'); +const assert = require('assert'); + +const script = `runTest(); +function runTest() { + const p = Promise.resolve(); + p.then(function break1() { // lineNumber 3 + debugger; + }); + p.then(function break2() { // lineNumber 6 + debugger; + }); +} +`; + +async function runTests() { + const instance = new NodeInstance(undefined, script); + const session = await instance.connectInspectorSession(); + await session.send([ + { 'method': 'Runtime.enable' }, + { 'method': 'Debugger.enable' }, + { 'method': 'Debugger.setAsyncCallStackDepth', + 'params': { 'maxDepth': 10 } }, + { 'method': 'Debugger.setBlackboxPatterns', + 'params': { 'patterns': [] } }, + { 'method': 'Runtime.runIfWaitingForDebugger' } + ]); + + console.error('[test] Waiting for break1'); + debuggerPausedAt(await session.waitForBreakOnLine(4, '[eval]'), + 'break1', 'runTest:3'); + + await session.send({ 'method': 'Debugger.resume' }); + + console.error('[test] Waiting for break2'); + debuggerPausedAt(await session.waitForBreakOnLine(7, '[eval]'), + 'break2', 'runTest:6'); + + await session.runToCompletion(); + assert.strictEqual(0, (await instance.expectShutdown()).exitCode); +} + +function debuggerPausedAt(msg, functionName, previousTickLocation) { + assert( + !!msg.params.asyncStackTrace, + `${Object.keys(msg.params)} contains "asyncStackTrace" property`); + + assert.strictEqual(msg.params.callFrames[0].functionName, functionName); + assert.strictEqual(msg.params.asyncStackTrace.description, 'PROMISE'); + + const frameLocations = msg.params.asyncStackTrace.callFrames.map( + (frame) => `${frame.functionName}:${frame.lineNumber}`); + assertArrayIncludes(frameLocations, previousTickLocation); +} + +function assertArrayIncludes(actual, expected) { + const expectedString = JSON.stringify(expected); + const actualString = JSON.stringify(actual); + assert( + actual.includes(expected), + `Expected ${actualString} to contain ${expectedString}.`); +} + +runTests(); diff --git a/test/inspector/test-async-stack-traces-set-interval.js b/test/inspector/test-async-stack-traces-set-interval.js new file mode 100644 index 00000000000000..bc96df9588fc6a --- /dev/null +++ b/test/inspector/test-async-stack-traces-set-interval.js @@ -0,0 +1,41 @@ +'use strict'; +const common = require('../common'); +common.skipIfInspectorDisabled(); +common.skipIf32Bits(); +common.crashOnUnhandledRejection(); +const { NodeInstance } = require('./inspector-helper'); +const assert = require('assert'); + +const script = 'setInterval(() => { debugger; }, 50);'; + +async function checkAsyncStackTrace(session) { + console.error('[test]', 'Verify basic properties of asyncStackTrace'); + const paused = await session.waitForBreakOnLine(0, '[eval]'); + assert(paused.params.asyncStackTrace, + `${Object.keys(paused.params)} contains "asyncStackTrace" property`); + assert(paused.params.asyncStackTrace.description, 'Timeout'); + assert(paused.params.asyncStackTrace.callFrames + .some((frame) => frame.functionName === 'Module._compile')); +} + +async function runTests() { + const instance = new NodeInstance(undefined, script); + const session = await instance.connectInspectorSession(); + await session.send([ + { 'method': 'Runtime.enable' }, + { 'method': 'Debugger.enable' }, + { 'method': 'Debugger.setAsyncCallStackDepth', + 'params': { 'maxDepth': 10 } }, + { 'method': 'Debugger.setBlackboxPatterns', + 'params': { 'patterns': [] } }, + { 'method': 'Runtime.runIfWaitingForDebugger' } + ]); + + await checkAsyncStackTrace(session); + + console.error('[test]', 'Stopping child instance'); + session.disconnect(); + instance.kill(); +} + +runTests(); diff --git a/test/inspector/test-inspector-enabled.js b/test/inspector/test-inspector-enabled.js new file mode 100644 index 00000000000000..a7a0832793283c --- /dev/null +++ b/test/inspector/test-inspector-enabled.js @@ -0,0 +1,26 @@ +'use strict'; +const common = require('../common'); +common.skipIfInspectorDisabled(); + +const spawn = require('child_process').spawn; + +const script = ` +const assert = require('assert'); +const inspector = process.binding('inspector'); + +assert( + !!inspector.isEnabled(), + 'inspector.isEnabled() should be true when run with --inspect'); + +process._debugEnd(); + +assert( + !inspector.isEnabled(), + 'inspector.isEnabled() should be false after _debugEnd()'); +`; + +const args = ['--inspect', '-e', script]; +const child = spawn(process.execPath, args, { stdio: 'inherit' }); +child.on('exit', (code, signal) => { + process.exit(code || signal); +}); diff --git a/test/inspector/test-inspector.js b/test/inspector/test-inspector.js index 3139940451a515..6a5dbe60030a64 100644 --- a/test/inspector/test-inspector.js +++ b/test/inspector/test-inspector.js @@ -98,7 +98,8 @@ async function testBreakpoint(session) { `Script source is wrong: ${scriptSource}`); await session.waitForConsoleOutput('log', ['A message', 5]); - const scopeId = await session.waitForBreakOnLine(5, mainScriptPath); + const paused = await session.waitForBreakOnLine(5, mainScriptPath); + const scopeId = paused.params.callFrames[0].scopeChain[0].object.objectId; console.log('[test]', 'Verify we can read current application state'); const response = await session.send({