src,lib: make ^C print a JS stack trace

If terminating the process with ctrl-c / SIGINT, prints a JS stacktrace
leading up to the currently executing code.

The feature would be enabled under option `--trace-sigint`.

Conditions of no stacktrace on sigint:

- has (an) active sigint listener(s);
- main thread is idle (i.e. uv polling), a message instead of stacktrace
  would be printed.

PR-URL: https://github.com/nodejs/node/pull/29207
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Christopher Hiller <boneskull@boneskull.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
This commit is contained in:
legendecas 2019-08-19 21:03:08 +08:00
parent 78743f8e39
commit 7b7e7bd185
No known key found for this signature in database
GPG key ID: 321332BDA88D6AA3
20 changed files with 405 additions and 15 deletions

View file

@ -779,6 +779,13 @@ added: v13.5.0
Prints a stack trace whenever an environment is exited proactively,
i.e. invoking `process.exit()`.
### `--trace-sigint`
<!-- YAML
added: REPLACEME
-->
Prints a stack trace on SIGINT.
### `--trace-sync-io`
<!-- YAML
added: v2.1.0
@ -1122,6 +1129,7 @@ Node.js options that are allowed are:
* `--trace-event-file-pattern`
* `--trace-events-enabled`
* `--trace-exit`
* `--trace-sigint`
* `--trace-sync-io`
* `--trace-tls`
* `--trace-uncaught`

View file

@ -367,6 +367,8 @@ Enable the collection of trace event tracing information.
.It Fl -trace-exit
Prints a stack trace whenever an environment is exited proactively,
i.e. invoking `process.exit()`.
.It Fl -trace-sigint
Prints a stack trace on SIGINT.
.
.It Fl -trace-sync-io
Print a stack trace whenever synchronous I/O is detected after the first turn of the event loop.

View file

@ -37,6 +37,9 @@ function prepareMainThreadExecution(expandArgv1 = false) {
setupDebugEnv();
// Print stack trace on `SIGINT` if option `--trace-sigint` presents.
setupStacktracePrinterOnSigint();
// Process initial diagnostic reporting configuration, if present.
initializeReport();
initializeReportSignalHandlers(); // Main-thread-only.
@ -149,6 +152,16 @@ function setupCoverageHooks(dir) {
return coverageDirectory;
}
function setupStacktracePrinterOnSigint() {
if (!getOptionValue('--trace-sigint')) {
return;
}
const { SigintWatchdog } = require('internal/watchdog');
const watchdog = new SigintWatchdog();
watchdog.start();
}
function initializeReport() {
if (!getOptionValue('--experimental-report')) {
return;

59
lib/internal/watchdog.js Normal file
View file

@ -0,0 +1,59 @@
'use strict';
const {
TraceSigintWatchdog
} = internalBinding('watchdog');
class SigintWatchdog extends TraceSigintWatchdog {
_started = false;
_effective = false;
_onNewListener = (eve) => {
if (eve === 'SIGINT' && this._effective) {
super.stop();
this._effective = false;
}
};
_onRemoveListener = (eve) => {
if (eve === 'SIGINT' && process.listenerCount('SIGINT') === 0 &&
!this._effective) {
super.start();
this._effective = true;
}
}
start() {
if (this._started) {
return;
}
this._started = true;
// Prepend sigint newListener to remove stop watchdog before signal wrap
// been activated. Also make sigint removeListener been ran after signal
// wrap been stopped.
process.prependListener('newListener', this._onNewListener);
process.addListener('removeListener', this._onRemoveListener);
if (process.listenerCount('SIGINT') === 0) {
super.start();
this._effective = true;
}
}
stop() {
if (!this._started) {
return;
}
this._started = false;
process.removeListener('newListener', this._onNewListener);
process.removeListener('removeListener', this._onRemoveListener);
if (this._effective) {
super.stop();
this._effective = false;
}
}
}
module.exports = {
SigintWatchdog
};

View file

@ -210,6 +210,7 @@
'lib/internal/vm/module.js',
'lib/internal/worker.js',
'lib/internal/worker/io.js',
'lib/internal/watchdog.js',
'lib/internal/streams/lazy_transform.js',
'lib/internal/streams/async_iterator.js',
'lib/internal/streams/buffer_list.js',

View file

@ -68,6 +68,7 @@ namespace node {
V(TTYWRAP) \
V(UDPSENDWRAP) \
V(UDPWRAP) \
V(SIGINTWATCHDOG) \
V(WORKER) \
V(WRITEWRAP) \
V(ZLIB)

View file

@ -81,6 +81,14 @@ void MemoryTracker::TrackFieldWithSize(const char* edge_name,
if (size > 0) AddNode(GetNodeName(node_name, edge_name), size, edge_name);
}
void MemoryTracker::TrackInlineFieldWithSize(const char* edge_name,
size_t size,
const char* node_name) {
if (size > 0) AddNode(GetNodeName(node_name, edge_name), size, edge_name);
CHECK(CurrentNode());
CurrentNode()->size_ -= size;
}
void MemoryTracker::TrackField(const char* edge_name,
const MemoryRetainer& value,
const char* node_name) {
@ -248,6 +256,12 @@ void MemoryTracker::TrackField(const char* name,
TrackFieldWithSize(name, sizeof(value), "uv_async_t");
}
void MemoryTracker::TrackInlineField(const char* name,
const uv_async_t& value,
const char* node_name) {
TrackInlineFieldWithSize(name, sizeof(value), "uv_async_t");
}
template <class NativeT, class V8T>
void MemoryTracker::TrackField(const char* name,
const AliasedBufferBase<NativeT, V8T>& value,

View file

@ -135,6 +135,10 @@ class MemoryTracker {
inline void TrackFieldWithSize(const char* edge_name,
size_t size,
const char* node_name = nullptr);
inline void TrackInlineFieldWithSize(const char* edge_name,
size_t size,
const char* node_name = nullptr);
// Shortcut to extract the underlying object out of the smart pointer
template <typename T>
inline void TrackField(const char* edge_name,
@ -228,6 +232,9 @@ class MemoryTracker {
inline void TrackField(const char* edge_name,
const uv_async_t& value,
const char* node_name = nullptr);
inline void TrackInlineField(const char* edge_name,
const uv_async_t& value,
const char* node_name = nullptr);
template <class NativeT, class V8T>
inline void TrackField(const char* edge_name,
const AliasedBufferBase<NativeT, V8T>& value,

View file

@ -87,6 +87,7 @@
V(v8) \
V(wasi) \
V(worker) \
V(watchdog) \
V(zlib)
#define NODE_BUILTIN_MODULES(V) \

View file

@ -758,6 +758,11 @@ PerProcessOptionsParser::PerProcessOptionsParser(
&PerProcessOptions::use_largepages,
kAllowedInEnvironment);
AddOption("--trace-sigint",
"enable printing JavaScript stacktrace on SIGINT",
&PerProcessOptions::trace_sigint,
kAllowedInEnvironment);
Insert(iop, &PerProcessOptions::get_per_isolate_options);
}

View file

@ -237,6 +237,7 @@ class PerProcessOptions : public Options {
#endif
#endif
std::string use_largepages = "off";
bool trace_sigint = false;
#ifdef NODE_REPORT
std::vector<std::string> cmdline;

View file

@ -21,6 +21,7 @@
#include <algorithm>
#include "async_wrap-inl.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "node_errors.h"
@ -30,6 +31,12 @@
namespace node {
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Object;
using v8::Value;
Watchdog::Watchdog(v8::Isolate* isolate, uint64_t ms, bool* timed_out)
: isolate_(isolate), timed_out_(timed_out) {
@ -106,10 +113,116 @@ SigintWatchdog::~SigintWatchdog() {
SigintWatchdogHelper::GetInstance()->Stop();
}
void SigintWatchdog::HandleSigint() {
SignalPropagation SigintWatchdog::HandleSigint() {
*received_signal_ = true;
isolate_->TerminateExecution();
return SignalPropagation::kStopPropagation;
}
void TraceSigintWatchdog::Init(Environment* env, Local<Object> target) {
Local<FunctionTemplate> constructor = env->NewFunctionTemplate(New);
constructor->InstanceTemplate()->SetInternalFieldCount(1);
Local<v8::String> js_sigint_watch_dog =
FIXED_ONE_BYTE_STRING(env->isolate(), "TraceSigintWatchdog");
constructor->SetClassName(js_sigint_watch_dog);
constructor->Inherit(HandleWrap::GetConstructorTemplate(env));
env->SetProtoMethod(constructor, "start", Start);
env->SetProtoMethod(constructor, "stop", Stop);
target
->Set(env->context(),
js_sigint_watch_dog,
constructor->GetFunction(env->context()).ToLocalChecked())
.Check();
}
void TraceSigintWatchdog::New(const FunctionCallbackInfo<Value>& args) {
// This constructor should not be exposed to public javascript.
// Therefore we assert that we are not trying to call this as a
// normal function.
CHECK(args.IsConstructCall());
Environment* env = Environment::GetCurrent(args);
new TraceSigintWatchdog(env, args.This());
}
void TraceSigintWatchdog::Start(const FunctionCallbackInfo<Value>& args) {
TraceSigintWatchdog* watchdog;
ASSIGN_OR_RETURN_UNWRAP(&watchdog, args.Holder());
// Register this watchdog with the global SIGINT/Ctrl+C listener.
SigintWatchdogHelper::GetInstance()->Register(watchdog);
// Start the helper thread, if that has not already happened.
int r = SigintWatchdogHelper::GetInstance()->Start();
CHECK_EQ(r, 0);
}
void TraceSigintWatchdog::Stop(const FunctionCallbackInfo<Value>& args) {
TraceSigintWatchdog* watchdog;
ASSIGN_OR_RETURN_UNWRAP(&watchdog, args.Holder());
SigintWatchdogHelper::GetInstance()->Unregister(watchdog);
SigintWatchdogHelper::GetInstance()->Stop();
}
TraceSigintWatchdog::TraceSigintWatchdog(Environment* env, Local<Object> object)
: HandleWrap(env,
object,
reinterpret_cast<uv_handle_t*>(&handle_),
AsyncWrap::PROVIDER_SIGINTWATCHDOG) {
int r = uv_async_init(env->event_loop(), &handle_, [](uv_async_t* handle) {
TraceSigintWatchdog* watchdog =
ContainerOf(&TraceSigintWatchdog::handle_, handle);
watchdog->signal_flag_ = SignalFlags::FromIdle;
watchdog->HandleInterrupt();
});
CHECK_EQ(r, 0);
uv_unref(reinterpret_cast<uv_handle_t*>(&handle_));
}
SignalPropagation TraceSigintWatchdog::HandleSigint() {
/**
* In case of uv loop polling, i.e. no JS currently running, activate the
* loop to run a piece of JS code to trigger interruption.
*/
CHECK_EQ(uv_async_send(&handle_), 0);
env()->isolate()->RequestInterrupt(
[](v8::Isolate* isolate, void* data) {
TraceSigintWatchdog* self = static_cast<TraceSigintWatchdog*>(data);
if (self->signal_flag_ == SignalFlags::None) {
self->signal_flag_ = SignalFlags::FromInterrupt;
}
self->HandleInterrupt();
},
this);
return SignalPropagation::kContinuePropagation;
}
void TraceSigintWatchdog::HandleInterrupt() {
// Do not nest interrupts.
if (interrupting) {
return;
}
interrupting = true;
if (signal_flag_ == SignalFlags::None) {
return;
}
Environment* env_ = env();
// FIXME: Before
// https://github.com/nodejs/node/pull/29207#issuecomment-527667993 get
// fixed, additional JavaScript code evaluation shall be prevented from
// running during interruption.
FPrintF(stderr,
"KEYBOARD_INTERRUPT: Script execution was interrupted by `SIGINT`\n");
if (signal_flag_ == SignalFlags::FromInterrupt) {
PrintStackTrace(env_->isolate(),
v8::StackTrace::CurrentStackTrace(
env_->isolate(), 10, v8::StackTrace::kDetailed));
}
signal_flag_ = SignalFlags::None;
interrupting = false;
SigintWatchdogHelper::GetInstance()->Unregister(this);
SigintWatchdogHelper::GetInstance()->Stop();
raise(SIGINT);
}
#ifdef __POSIX__
@ -163,8 +276,13 @@ bool SigintWatchdogHelper::InformWatchdogsAboutSignal() {
instance.has_pending_signal_ = true;
}
for (auto it : instance.watchdogs_)
it->HandleSigint();
for (auto it = instance.watchdogs_.rbegin(); it != instance.watchdogs_.rend();
it++) {
SignalPropagation wp = (*it)->HandleSigint();
if (wp == SignalPropagation::kStopPropagation) {
break;
}
}
return is_stopping;
}
@ -260,15 +378,13 @@ bool SigintWatchdogHelper::HasPendingSignal() {
return has_pending_signal_;
}
void SigintWatchdogHelper::Register(SigintWatchdog* wd) {
void SigintWatchdogHelper::Register(SigintWatchdogBase* wd) {
Mutex::ScopedLock lock(list_mutex_);
watchdogs_.push_back(wd);
}
void SigintWatchdogHelper::Unregister(SigintWatchdog* wd) {
void SigintWatchdogHelper::Unregister(SigintWatchdogBase* wd) {
Mutex::ScopedLock lock(list_mutex_);
auto it = std::find(watchdogs_.begin(), watchdogs_.end(), wd);
@ -303,4 +419,16 @@ SigintWatchdogHelper::~SigintWatchdogHelper() {
SigintWatchdogHelper SigintWatchdogHelper::instance;
namespace watchdog {
static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
TraceSigintWatchdog::Init(env, target);
}
} // namespace watchdog
} // namespace node
NODE_MODULE_CONTEXT_AWARE_INTERNAL(watchdog, node::watchdog::Initialize);

View file

@ -24,9 +24,12 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "uv.h"
#include "node_mutex.h"
#include <vector>
#include "handle_wrap.h"
#include "memory_tracker-inl.h"
#include "node_mutex.h"
#include "uv.h"
#include "v8.h"
#ifdef __POSIX__
#include <pthread.h>
@ -34,6 +37,11 @@
namespace node {
enum class SignalPropagation {
kContinuePropagation,
kStopPropagation,
};
class Watchdog {
public:
explicit Watchdog(v8::Isolate* isolate,
@ -54,24 +62,56 @@ class Watchdog {
bool* timed_out_;
};
class SigintWatchdog {
class SigintWatchdogBase {
public:
virtual ~SigintWatchdogBase() = default;
virtual SignalPropagation HandleSigint() = 0;
};
class SigintWatchdog : public SigintWatchdogBase {
public:
explicit SigintWatchdog(v8::Isolate* isolate,
bool* received_signal = nullptr);
~SigintWatchdog();
v8::Isolate* isolate() { return isolate_; }
void HandleSigint();
SignalPropagation HandleSigint() override;
private:
v8::Isolate* isolate_;
bool* received_signal_;
};
class TraceSigintWatchdog : public HandleWrap, public SigintWatchdogBase {
public:
static void Init(Environment* env, Local<v8::Object> target);
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Start(const v8::FunctionCallbackInfo<Value>& args);
static void Stop(const v8::FunctionCallbackInfo<Value>& args);
SignalPropagation HandleSigint() override;
inline void MemoryInfo(node::MemoryTracker* tracker) const override {
tracker->TrackInlineField("handle_", handle_);
}
SET_MEMORY_INFO_NAME(TraceSigintWatchdog)
SET_SELF_SIZE(TraceSigintWatchdog)
private:
enum class SignalFlags { None, FromIdle, FromInterrupt };
TraceSigintWatchdog(Environment* env, Local<v8::Object> object);
void HandleInterrupt();
bool interrupting = false;
uv_async_t handle_;
SignalFlags signal_flag_ = SignalFlags::None;
};
class SigintWatchdogHelper {
public:
static SigintWatchdogHelper* GetInstance() { return &instance; }
void Register(SigintWatchdog* watchdog);
void Unregister(SigintWatchdog* watchdog);
void Register(SigintWatchdogBase* watchdog);
void Unregister(SigintWatchdogBase* watchdog);
bool HasPendingSignal();
int Start();
@ -88,7 +128,7 @@ class SigintWatchdogHelper {
Mutex mutex_;
Mutex list_mutex_;
std::vector<SigintWatchdog*> watchdogs_;
std::vector<SigintWatchdogBase*> watchdogs_;
bool has_pending_signal_;
#ifdef __POSIX__

View file

@ -0,0 +1,39 @@
'use strict';
const { mustCall } = require('../common');
const childProcess = require('child_process');
const assert = require('assert');
if (process.env.CHILD === 'true') {
main();
} else {
// Use inherited stdio child process to prevent test tools from determining
// the case as crashed from SIGINT
const cp = childProcess.spawn(
process.execPath,
['--trace-sigint', __filename],
{
env: { ...process.env, CHILD: 'true' },
stdio: 'inherit'
});
cp.on('exit', mustCall((code, signal) => {
assert.strictEqual(signal, null);
assert.strictEqual(code, 0);
}));
}
function main() {
// Deactivate colors even if the tty does support colors.
process.env.NODE_DISABLE_COLORS = '1';
const noop = mustCall(() => {
process.exit(0);
});
process.on('SIGINT', noop);
// Try testing re-add 'SIGINT' listeners
process.removeListener('SIGINT', noop);
process.on('SIGINT', noop);
process.kill(process.pid, 'SIGINT');
setTimeout(() => { assert.fail('unreachable path'); }, 10 * 1000);
}

View file

@ -0,0 +1,30 @@
'use strict';
const { mustCall } = require('../common');
const childProcess = require('child_process');
const assert = require('assert');
if (process.env.CHILD === 'true') {
main();
} else {
// Use inherited stdio child process to prevent test tools from determining
// the case as crashed from SIGINT
const cp = childProcess.spawn(
process.execPath,
['--trace-sigint', __filename],
{
env: { ...process.env, CHILD: 'true' },
stdio: 'inherit'
});
setTimeout(() => cp.kill('SIGINT'), 1 * 1000);
cp.on('exit', mustCall((code, signal) => {
assert.strictEqual(signal, 'SIGINT');
assert.strictEqual(code, null);
}));
}
function main() {
// Deactivate colors even if the tty does support colors.
process.env.NODE_DISABLE_COLORS = '1';
setTimeout(() => {}, 10 * 1000);
}

View file

@ -0,0 +1 @@
KEYBOARD_INTERRUPT: Script execution was interrupted by `SIGINT`

View file

@ -0,0 +1,30 @@
'use strict';
const { mustCall } = require('../common');
const childProcess = require('child_process');
const assert = require('assert');
if (process.env.CHILD === 'true') {
main();
} else {
// Use inherited stdio child process to prevent test tools from determining
// the case as crashed from SIGINT
const cp = childProcess.spawn(
process.execPath,
['--trace-sigint', __filename],
{
env: { ...process.env, CHILD: 'true' },
stdio: 'inherit'
});
cp.on('exit', mustCall((code, signal) => {
assert.strictEqual(signal, 'SIGINT');
assert.strictEqual(code, null);
}));
}
function main() {
// Deactivate colors even if the tty does support colors.
process.env.NODE_DISABLE_COLORS = '1';
process.kill(process.pid, 'SIGINT');
while (true) {}
}

View file

@ -0,0 +1,9 @@
KEYBOARD_INTERRUPT: Script execution was interrupted by `SIGINT`
at main (*/test-trace-sigint.js:*)
at */test-trace-sigint.js:*
at *
at *
at *
at *
at *
at *

View file

@ -51,6 +51,7 @@ const { getSystemErrorName } = require('util');
delete providers.HTTPCLIENTREQUEST;
delete providers.HTTPINCOMINGMESSAGE;
delete providers.ELDHISTOGRAM;
delete providers.SIGINTWATCHDOG;
const objKeys = Object.keys(providers);
if (objKeys.length > 0)