summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/internal/bootstrap_node.js2
-rw-r--r--lib/internal/inspector_async_hook.js64
-rw-r--r--node.gyp1
-rw-r--r--src/inspector_agent.cc165
-rw-r--r--src/inspector_agent.h27
-rw-r--r--src/node_config.cc14
-rw-r--r--test/common/README.md10
-rw-r--r--test/common/index.js6
-rw-r--r--test/inspector/inspector-helper.js16
-rw-r--r--test/inspector/test-async-hook-setup-at-inspect-brk.js45
-rw-r--r--test/inspector/test-async-hook-setup-at-inspect.js70
-rw-r--r--test/inspector/test-async-hook-setup-at-signal.js81
-rw-r--r--test/inspector/test-async-hook-teardown-at-debug-end.js33
-rw-r--r--test/inspector/test-async-stack-traces-promise-then.js69
-rw-r--r--test/inspector/test-async-stack-traces-set-interval.js41
-rw-r--r--test/inspector/test-inspector-enabled.js26
-rw-r--r--test/inspector/test-inspector.js3
17 files changed, 656 insertions, 17 deletions
diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js
index a576eeb5e5..492e3a2119 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 0000000000..e32a026cd6
--- /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 4d38a906ec..5998424db4 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 0f9caa32f2..2520fbdd53 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<Function> 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<Object> process_object = parent_env_->process_object();
Local<Value> 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<Function> 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<Function> 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<v8::Function> enable_function,
+ v8::Local<v8::Function> 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<Value>& args) {
Environment* env = Environment::GetCurrent(args);
inspector::Agent* agent = env->inspector_agent();
@@ -810,6 +909,59 @@ void Url(const FunctionCallbackInfo<Value>& 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<void*>(asyncId << 1);
+}
+
+template<void (Agent::*asyncTaskFn)(void*)>
+static void InvokeAsyncTaskFnWithId(const FunctionCallbackInfo<Value>& 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<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+
+ CHECK(args[0]->IsString());
+ Local<String> task_name = args[0].As<String>();
+ 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<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+
+ CHECK(args[0]->IsFunction());
+ v8::Local<v8::Function> enable_function = args[0].As<Function>();
+ CHECK(args[1]->IsFunction());
+ v8::Local<v8::Function> disable_function = args[1].As<Function>();
+ env->inspector_agent()->RegisterAsyncHook(env->isolate(),
+ enable_function, disable_function);
+}
+
+static void IsEnabled(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ args.GetReturnValue().Set(env->inspector_agent()->enabled());
+}
// static
void Agent::InitInspector(Local<Object> target, Local<Value> unused,
@@ -830,6 +982,17 @@ void Agent::InitInspector(Local<Object> target, Local<Value> 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 cf9a8bff86..6ec1bc28dc 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 <typename V>
-class FunctionCallbackInfo;
-template<typename T>
-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<v8::Value> error,
v8::Local<v8::Message> 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<v8::Function> enable_function,
+ v8::Local<v8::Function> 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<v8::Function> enable_async_hook_function_;
+ v8::Persistent<v8::Function> disable_async_hook_function_;
};
} // namespace inspector
diff --git a/src/node_config.cc b/src/node_config.cc
index 041e18f6b7..d4fb991c58 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<Object> target,
Local<Value> unused,
Local<Context> context) {
@@ -91,6 +101,10 @@ static void InitConfig(Local<Object> 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 59b02cf52a..b8d9af2fcf 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` [&lt;Object>]
* return [&lt;Object>]
diff --git a/test/common/index.js b/test/common/index.js
index 2564b227fe..8175474818 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 11e8639540..9c1cca3a77 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,6 +319,16 @@ 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) {
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 0000000000..70887ff63d
--- /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 0000000000..bbf418a858
--- /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 0000000000..f0150bc7ac
--- /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 0000000000..9084efdd41
--- /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 0000000000..68584b0a3c
--- /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 0000000000..bc96df9588
--- /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 0000000000..a7a0832793
--- /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 3139940451..6a5dbe6003 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({