From baf4cae7f2da6de13daa31e1b57168cecf92771f Mon Sep 17 00:00:00 2001 From: Matheus Marchini Date: Fri, 16 Mar 2018 15:29:15 -0300 Subject: [PATCH] src: add commands to inspect the workqueue Add two new commands: `v8 getactivehandles` and `v8 getactiverequests`. These comamnds will print all pending handles and requests. The result should be similar to running process._getActiveHandles() and process._getActiveRequests() on the living process. PR-URL: https://github.com/nodejs/llnode/pull/210 Fixes: https://github.com/nodejs/llnode/issues/100 Reviewed-By: Joyee Cheung --- README.md | 6 ++ binding.gyp | 1 + src/llnode.cc | 108 ++++++++++++++++++++++ src/llnode.h | 41 ++++++++- src/node-constants.cc | 3 +- src/node-inl.h | 38 ++++++++ src/node.cc | 76 +++++++++++++++ src/node.h | 137 ++++++++++++++++++++++++++++ test/common.js | 3 +- test/fixtures/workqueue-scenario.js | 15 +++ test/workqueue-test.js | 58 ++++++++++++ 11 files changed, 482 insertions(+), 4 deletions(-) create mode 100644 src/node-inl.h create mode 100644 src/node.cc create mode 100644 src/node.h create mode 100644 test/fixtures/workqueue-scenario.js create mode 100644 test/workqueue-test.js diff --git a/README.md b/README.md index 92698218..02f3abd7 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,12 @@ The following subcommands are supported: * -n, --name name - all properties with the specified name * -s, --string string - all properties that refer to the specified JavaScript string value + getactivehandles -- Print all pending handles in the queue. Equivalent to running process._getActiveHandles() on + the living process. + + getactiverequests -- Print all pending handles in the queue. Equivalent to running process._getActiveHandles() on + the living process. + inspect -- Print detailed description and contents of the JavaScript value. Possible flags (all optional): diff --git a/binding.gyp b/binding.gyp index 9a632e9b..8da0c457 100644 --- a/binding.gyp +++ b/binding.gyp @@ -77,6 +77,7 @@ "src/llv8.cc", "src/llv8-constants.cc", "src/llscan.cc", + "src/node.cc", "src/node-constants.cc", ], "conditions": [ diff --git a/src/llnode.cc b/src/llnode.cc index d37bf8f1..31722844 100644 --- a/src/llnode.cc +++ b/src/llnode.cc @@ -4,6 +4,8 @@ #include #include +#include +#include #include @@ -11,6 +13,7 @@ #include "src/llnode.h" #include "src/llscan.h" #include "src/llv8.h" +#include "src/node-inl.h" namespace llnode { @@ -245,6 +248,100 @@ bool ListCmd::DoExecute(SBDebugger d, char** cmd, return true; } +bool WorkqueueCmd::DoExecute(SBDebugger d, char** cmd, + SBCommandReturnObject& result) { + SBTarget target = d.GetSelectedTarget(); + SBThread thread = target.GetProcess().GetSelectedThread(); + if (!thread.IsValid()) { + result.SetError("No valid process, please start something\n"); + return false; + } + + std::string result_message; + Error err; + + llv8_->Load(target); + node_->Load(target); + + node::Environment env = node::Environment::GetCurrent(node_, err); + if (err.Fail()) { + result.SetError(err.GetMessage()); + return false; + } + + result_message = GetResultMessage(&env, err); + if (err.Fail()) { + result.SetError(err.GetMessage()); + return false; + } + + result.Printf("%s", result_message.c_str()); + return true; +} + +std::string GetActiveHandlesCmd::GetResultMessage(node::Environment* env, + Error& err) { + int active_handles = 0; + v8::Value::InspectOptions inspect_options; + inspect_options.detailed = true; + std::ostringstream result_message; + + for (auto w : env->handle_wrap_queue()) { + addr_t persistent = w.Persistent(err); + if (err.Fail()) break; + if (persistent == 0) continue; + + addr_t raw_object = w.Object(err); + if (err.Fail()) break; + + v8::JSObject v8_object(llv8(), raw_object); + std::string res = v8_object.Inspect(&inspect_options, err); + if (err.Fail()) { + Error::PrintInDebugMode("Failed to load object at address %" PRIx64, + raw_object); + break; + } + + active_handles++; + result_message << res.c_str() << std::endl; + } + + result_message << "Total: " << active_handles << std::endl; + return result_message.str(); +} + + +std::string GetActiveRequestsCmd::GetResultMessage(node::Environment* env, + Error& err) { + int active_requests = 0; + v8::Value::InspectOptions inspect_options; + inspect_options.detailed = true; + std::ostringstream result_message; + + for (auto w : env->req_wrap_queue()) { + addr_t persistent = w.Persistent(err); + if (err.Fail()) break; + if (persistent == 0) continue; + + addr_t raw_object = w.Object(err); + if (err.Fail()) break; + + v8::JSObject v8_object(llv8(), raw_object); + std::string res = v8_object.Inspect(&inspect_options, err); + if (err.Fail()) { + Error::PrintInDebugMode("Failed to load object at address %" PRIx64, + raw_object); + break; + } + + active_requests++; + result_message << res.c_str() << std::endl; + } + + result_message << "Total: " << active_requests << std::endl; + return result_message.str(); +} + void InitDebugMode() { bool is_debug_mode = false; @@ -264,6 +361,7 @@ bool PluginInitialize(SBDebugger d) { llnode::InitDebugMode(); static llnode::v8::LLV8 llv8; + static llnode::node::Node node(&llv8); static llnode::LLScan llscan = llnode::LLScan(&llv8); SBCommandInterpreter interpreter = d.GetCommandInterpreter(); @@ -349,6 +447,16 @@ bool PluginInitialize(SBDebugger d) { "JavaScript string value\n" "\n"); + v8.AddCommand("getactivehandles", + new llnode::GetActiveHandlesCmd(&llv8, &node), + "Print all pending handles in the queue. Equivalent to running " + "process._getActiveHandles() on the living process.\n"); + + v8.AddCommand( + "getactiverequests", new llnode::GetActiveRequestsCmd(&llv8, &node), + "Print all pending requests in the queue. Equivalent to " + "running process._getActiveRequests() on the living process.\n"); + return true; } diff --git a/src/llnode.h b/src/llnode.h index 13ff6859..539cd66a 100644 --- a/src/llnode.h +++ b/src/llnode.h @@ -6,11 +6,11 @@ #include #include "src/llv8.h" +#include "src/node.h" namespace llnode { -class CommandBase : public lldb::SBCommandPluginInterface { -}; +class CommandBase : public lldb::SBCommandPluginInterface {}; class BacktraceCmd : public CommandBase { public: @@ -50,6 +50,43 @@ class ListCmd : public CommandBase { v8::LLV8* llv8_; }; +class WorkqueueCmd : public CommandBase { + public: + WorkqueueCmd(v8::LLV8* llv8, node::Node* node) : llv8_(llv8), node_(node) {} + ~WorkqueueCmd() override {} + + inline v8::LLV8* llv8() { return llv8_; }; + inline node::Node* node() { return node_; }; + + bool DoExecute(lldb::SBDebugger d, char** cmd, + lldb::SBCommandReturnObject& result) override; + + virtual std::string GetResultMessage(node::Environment* env, Error& err) { + return std::string(); + }; + + private: + v8::LLV8* llv8_; + node::Node* node_; +}; + +class GetActiveHandlesCmd : public WorkqueueCmd { + public: + GetActiveHandlesCmd(v8::LLV8* llv8, node::Node* node) + : WorkqueueCmd(llv8, node) {} + + std::string GetResultMessage(node::Environment* env, Error& err) override; +}; + +class GetActiveRequestsCmd : public WorkqueueCmd { + public: + GetActiveRequestsCmd(v8::LLV8* llv8, node::Node* node) + : WorkqueueCmd(llv8, node) {} + + std::string GetResultMessage(node::Environment* env, Error& err) override; +}; + + } // namespace llnode #endif // SRC_LLNODE_H_ diff --git a/src/node-constants.cc b/src/node-constants.cc index b3af3806..d0701c91 100644 --- a/src/node-constants.cc +++ b/src/node-constants.cc @@ -19,7 +19,8 @@ void Environment::Load() { kHandleWrapQueueOffset = LoadConstant( "offset_Environment__handle_wrap_queue___Environment_HandleWrapQueue"); kEnvContextEmbedderDataIndex = - LoadConstant("const_Environment__kContextEmbedderDataIndex__int"); + LoadConstant("const_Environment__kContextEmbedderDataIndex__int", + "const_ContextEmbedderIndex__kEnvironment__int"); Error err; kCurrentEnvironment = LoadCurrentEnvironment(err); diff --git a/src/node-inl.h b/src/node-inl.h new file mode 100644 index 00000000..a124a774 --- /dev/null +++ b/src/node-inl.h @@ -0,0 +1,38 @@ +#include "node.h" + +namespace llnode { +namespace node { + +template +T Queue::Iterator::operator*() const { + return T::GetItemFromList(node_, current_); +} + +template +const typename Queue::Iterator Queue::Iterator::operator++() { + lldb::SBError sberr; + + current_ = node_->process().ReadPointerFromMemory( + current_ + constants_->kNextOffset, sberr); + return Iterator(node_, current_, constants_); +} + +template +bool Queue::Iterator::operator!=(const Iterator& that) const { + return current_ != that.current_; +} + +template +typename Queue::Iterator Queue::begin() const { + lldb::SBError sberr; + + addr_t first = node_->process().ReadPointerFromMemory(next(head()), sberr); + return Iterator(node_, first, constants_); +} + +template +typename Queue::Iterator Queue::end() const { + return Iterator(node_, head(), constants_); +} +} // namespace node +} // namespace llnode diff --git a/src/node.cc b/src/node.cc new file mode 100644 index 00000000..dc5fb990 --- /dev/null +++ b/src/node.cc @@ -0,0 +1,76 @@ +#include "node.h" + +namespace llnode { +namespace node { + +addr_t BaseObject::Persistent(Error& err) { + lldb::SBError sberr; + + addr_t persistent_ptr = raw_ + node_->base_object()->kPersistentHandleOffset; + addr_t persistent = + node_->process().ReadPointerFromMemory(persistent_ptr, sberr); + if (sberr.Fail()) { + err = Error::Failure("Failed to load persistent handle"); + return 0; + } + return persistent; +} + +addr_t BaseObject::Object(Error& err) { + lldb::SBError sberr; + + addr_t persistent = Persistent(err); + addr_t obj = node_->process().ReadPointerFromMemory(persistent, sberr); + if (sberr.Fail()) { + err = Error::Failure("Failed to load object from persistent handle"); + return 0; + } + return obj; +} + +HandleWrap HandleWrap::GetItemFromList(Node* node, addr_t list_node_addr) { + return HandleWrap(node, + list_node_addr - node->handle_wrap()->kListNodeOffset); +} + +ReqWrap ReqWrap::GetItemFromList(Node* node, addr_t list_node_addr) { + return ReqWrap(node, list_node_addr - node->req_wrap()->kListNodeOffset); +} + +Environment Environment::GetCurrent(Node* node, Error& err) { + addr_t envAddr = node->env()->kCurrentEnvironment; + if (envAddr == 0) { + err = Error::Failure("Couldn't get node's Environment"); + } + + return Environment(node, envAddr); +} + +HandleWrapQueue Environment::handle_wrap_queue() const { + return HandleWrapQueue(node_, raw_ + node_->env()->kHandleWrapQueueOffset, + node_->handle_wrap_queue()); +} + +ReqWrapQueue Environment::req_wrap_queue() const { + return ReqWrapQueue(node_, raw_ + node_->env()->kReqWrapQueueOffset, + node_->req_wrap_queue()); +} + +void Node::Load(SBTarget target) { + // Reload process anyway + process_ = target.GetProcess(); + + // No need to reload + if (target_ == target) return; + + target_ = target; + + env.Assign(target); + req_wrap_queue.Assign(target); + req_wrap.Assign(target); + handle_wrap_queue.Assign(target); + handle_wrap.Assign(target); + base_object.Assign(target); +} +} // namespace node +} // namespace llnode diff --git a/src/node.h b/src/node.h new file mode 100644 index 00000000..00e5ab0d --- /dev/null +++ b/src/node.h @@ -0,0 +1,137 @@ +#ifndef SRC_NODE_H_ +#define SRC_NODE_H_ + +#include +#include + +#include "node-constants.h" + +#define CONSTANTS_LIST(V) \ + V(Environment, env) \ + V(ReqWrapQueue, req_wrap_queue) \ + V(ReqWrap, req_wrap) \ + V(HandleWrapQueue, handle_wrap_queue) \ + V(HandleWrap, handle_wrap) \ + V(BaseObject, base_object) + +namespace llnode { +namespace node { + +class Node; +class HandleWrap; +class ReqWrap; +template +class Queue; + +class BaseNode { + public: + BaseNode(Node* node) : node_(node){}; + + protected: + Node* node_; +}; + +using HandleWrapQueue = Queue; +using ReqWrapQueue = Queue; + +class Environment : public BaseNode { + public: + Environment(Node* node, addr_t raw) : BaseNode(node), raw_(raw){}; + inline addr_t raw() { return raw_; }; + + static Environment GetCurrent(Node* node, Error& err); + + HandleWrapQueue handle_wrap_queue() const; + ReqWrapQueue req_wrap_queue() const; + + private: + addr_t raw_; +}; + +class BaseObject : public BaseNode { + public: + BaseObject(Node* node, addr_t raw) : BaseNode(node), raw_(raw){}; + inline addr_t raw() { return raw_; }; + + addr_t Persistent(Error& err); + addr_t Object(Error& err); + + private: + addr_t raw_; +}; + +class AsyncWrap : public BaseObject { + public: + AsyncWrap(Node* node, addr_t raw) : BaseObject(node, raw){}; +}; + +class HandleWrap : public AsyncWrap { + public: + HandleWrap(Node* node, addr_t raw) : AsyncWrap(node, raw){}; + + static HandleWrap GetItemFromList(Node* node, addr_t list_node_addr); +}; + +class ReqWrap : public AsyncWrap { + public: + ReqWrap(Node* node, addr_t raw) : AsyncWrap(node, raw){}; + + static ReqWrap GetItemFromList(Node* node, addr_t list_node_addr); +}; + +class Node { + public: +#define V(Class, Attribute) Attribute(constants::Class(llv8)), + Node(v8::LLV8* llv8) : CONSTANTS_LIST(V) target_(lldb::SBTarget()) {} +#undef V + + inline lldb::SBProcess process() { return process_; }; + + void Load(lldb::SBTarget target); + +#define V(Class, Attribute) constants::Class Attribute; + CONSTANTS_LIST(V) +#undef V + + private: + lldb::SBTarget target_; + lldb::SBProcess process_; +}; + +template +class Queue : public BaseNode { + class Iterator : public BaseNode { + public: + inline T operator*() const; + inline const Iterator operator++(); + inline bool operator!=(const Iterator& that) const; + + + inline Iterator(Node* node, addr_t current, C* constants) + : BaseNode(node), current_(current), constants_(constants){}; + + public: + addr_t current_; + C* constants_; + }; + + public: + inline Queue(Node* node, addr_t raw, C* constants) + : BaseNode(node), raw_(raw), constants_(constants){}; + + inline Iterator begin() const; + inline Iterator end() const; + + private: + inline addr_t head() const { return raw_ + constants_->kHeadOffset; } + inline addr_t next(addr_t item) const { + return item + constants_->kNextOffset; + } + + addr_t raw_; + C* constants_; +}; +} // namespace node +} // namespace llnode + +#endif diff --git a/test/common.js b/test/common.js index c11b5a6c..7959b9f6 100644 --- a/test/common.js +++ b/test/common.js @@ -193,6 +193,7 @@ function Session(options) { // Map these methods to stdout for compatibility with legacy tests. this.wait = SessionOutput.prototype.wait.bind(this.stdout); + this.waitError = SessionOutput.prototype.wait.bind(this.stderr); this.waitBreak = SessionOutput.prototype.waitBreak.bind(this.stdout); this.linesUntil = SessionOutput.prototype.linesUntil.bind(this.stdout); this.timeoutAfter = SessionOutput.prototype.timeoutAfter.bind(this.stdout); @@ -228,7 +229,7 @@ exports.saveCore = function saveCore(options, cb) { cb(null); } else { const ranges = core + '.ranges'; - exports.generateRanges(core, ranges, cb); + exports.generateRanges(core, ranges, cb); } }); } diff --git a/test/fixtures/workqueue-scenario.js b/test/fixtures/workqueue-scenario.js new file mode 100644 index 00000000..add58d6b --- /dev/null +++ b/test/fixtures/workqueue-scenario.js @@ -0,0 +1,15 @@ +'use strict'; +const http = require('http') +var fs = require('fs'); + +// Creates a Timer to be inspected with getactivehandles +setInterval(() => {}, 500); + +// Creates a TCP to be inspected with getactivehandles +const server = http.createServer((req, res) => { res.end('test'); }); +server.listen(4321, (err) => {}); + +// Creates a FSReqWrap to be inspected with getactivehandles +fs.readFile('invalidFile', (err, data) => {}); + +uncaughtException(); diff --git a/test/workqueue-test.js b/test/workqueue-test.js new file mode 100644 index 00000000..9d74d683 --- /dev/null +++ b/test/workqueue-test.js @@ -0,0 +1,58 @@ +'use strict'; + +const tape = require('tape'); + +const common = require('./common'); + +function testWorkqueueCommands(t, sess) { + sess.send('v8 getactivehandles'); + + sess.wait(/TCP/, (err, line) => { + t.error(err); + let match = line.match(/ { + t.error(err); + let match = line.match(/ { + t.error(err); + let match = line.match(/ { + t.timeoutAfter(15000); + + const sess = common.Session.create('workqueue-scenario.js'); + sess.timeoutAfter + + sess.waitBreak((err) => { + t.error(err); + sess.send('v8 getactivehandles'); + }); + + // Check if current node version support these commands. + sess.waitError(/error: Couldn't get node's Environment/, (err, line) => { + if (err) { + testWorkqueueCommands(t, sess); + } else { + // TODO (mmarchini): print node's version from core. + t.skip(`workqueue commands can't be tested with this version of node`); + sess.quit(); + t.end(); + } + }, false, false); +});