diff --git a/README.md b/README.md index a64ea7fd..4d6f2578 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,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/llnode.gyp.json b/llnode.gyp.json index 3c2a0bfe..6c0dd6b2 100644 --- a/llnode.gyp.json +++ b/llnode.gyp.json @@ -22,6 +22,7 @@ "src/llv8.cc", "src/llv8-constants.cc", "src/llscan.cc", + "src/node.cc", "src/node-constants.cc", ], diff --git a/src/llnode.cc b/src/llnode.cc index 5e4acf1e..00aa15b2 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 { @@ -20,6 +23,7 @@ using lldb::SBDebugger; using lldb::SBError; using lldb::SBExpressionOptions; using lldb::SBFrame; +using lldb::SBProcess; using lldb::SBStream; using lldb::SBSymbol; using lldb::SBTarget; @@ -300,6 +304,112 @@ bool ListCmd::DoExecute(SBDebugger d, char** cmd, return true; } +bool WorkqueueCmd::DoExecute(SBDebugger d, char** cmd, + SBCommandReturnObject& result) { + SBTarget target = d.GetSelectedTarget(); + SBProcess process = target.GetProcess(); + SBThread thread = process.GetSelectedThread(); + std::string result_message; + Error err; + + llv8_->Load(target); + node_->Load(target); + + if (!thread.IsValid()) { + result.SetError("No valid process, please start something\n"); + return false; + } + + 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_addr = w.persistent_addr(err); + if (err.Fail()) { + break; + } + if (persistent_addr == 0) { + continue; + } + + addr_t v8_object_addr = w.v8_object_addr(err); + if (err.Fail()) { + break; + } + v8::JSObject v8_object(llv8(), v8_object_addr); + std::string res = v8_object.Inspect(&inspect_options, err); + if (err.Fail()) { + Error::PrintInDebugMode("Failed to load object at address %" PRIx64, + v8_object_addr); + 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_handles = 0; + v8::Value::InspectOptions inspect_options; + inspect_options.detailed = true; + std::ostringstream result_message; + + for (auto w : env->req_wrap_queue()) { + addr_t persistent_addr = w.persistent_addr(err); + if (err.Fail()) { + break; + } + if (persistent_addr == 0) { + continue; + } + + addr_t v8_object_addr = w.v8_object_addr(err); + if (err.Fail()) { + break; + } + v8::JSObject v8_object(llv8(), v8_object_addr); + std::string res = v8_object.Inspect(&inspect_options, err); + if (err.Fail()) { + Error::PrintInDebugMode("Failed to load object at address %" PRIx64, + v8_object_addr); + break; + } + + active_handles++; + result_message << res.c_str() << std::endl; + } + + result_message << "Total: " << active_handles << std::endl; + return result_message.str(); +} + void InitDebugMode() { bool is_debug_mode = false; @@ -319,6 +429,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(); @@ -404,6 +515,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 5340571c..6880b7f5 100644 --- a/src/llnode.h +++ b/src/llnode.h @@ -6,6 +6,7 @@ #include #include "src/llv8.h" +#include "src/node.h" namespace llnode { @@ -52,6 +53,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-inl.h b/src/node-inl.h new file mode 100644 index 00000000..b1bc47eb --- /dev/null +++ b/src/node-inl.h @@ -0,0 +1,42 @@ +#include "node.h" + +namespace llnode { +namespace node { + +template +T Queue::Iterator::operator*() const { + return T::FromListNode(node_, current_); +} + +template +const typename Queue::Iterator Queue::Iterator::operator++() { + lldb::SBError sberr; + + addr_t current = current_ + constants_->kNextOffset; + current = node_->process().ReadPointerFromMemory(current, sberr); + current_ = current; + 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 currentNode = raw_ + constants_->kHeadOffset; + + currentNode = currentNode + constants_->kNextOffset; + currentNode = node_->process().ReadPointerFromMemory(currentNode, sberr); + + return Iterator(node_, currentNode, constants_); +} + +template +typename Queue::Iterator Queue::end() const { + return Iterator(node_, raw_ + constants_->kHeadOffset, constants_); +} +} // namespace node +} // namespace llnode diff --git a/src/node.cc b/src/node.cc new file mode 100644 index 00000000..2cef038e --- /dev/null +++ b/src/node.cc @@ -0,0 +1,77 @@ +#include "node.h" + +namespace llnode { +namespace node { + +addr_t BaseObject::persistent_addr(Error& err) { + lldb::SBError sberr; + + addr_t persistentHandlePtr = + raw_ + node_->base_object()->kPersistentHandleOffset; + addr_t persistentHandle = + node_->process().ReadPointerFromMemory(persistentHandlePtr, sberr); + if (sberr.Fail()) { + err = Error::Failure("Failed to load persistent handle"); + return 0; + } + return persistentHandle; +} + +addr_t BaseObject::v8_object_addr(Error& err) { + lldb::SBError sberr; + + addr_t persistentHandle = persistent_addr(err); + addr_t obj = node_->process().ReadPointerFromMemory(persistentHandle, sberr); + if (sberr.Fail()) { + err = Error::Failure("Failed to load object from persistent handle"); + return 0; + } + return obj; +} + +HandleWrap HandleWrap::FromListNode(Node* node, addr_t list_node_addr) { + return HandleWrap(node, + list_node_addr - node->handle_wrap()->kListNodeOffset); +} + +ReqWrap ReqWrap::FromListNode(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..887de58c --- /dev/null +++ b/src/node.h @@ -0,0 +1,134 @@ +#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_; +}; + +typedef Queue HandleWrapQueue; +typedef Queue ReqWrapQueue; + +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_addr(Error& err); + + addr_t v8_object_addr(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 FromListNode(Node* node, addr_t list_node_addr); +}; + +class ReqWrap : public AsyncWrap { + public: + ReqWrap(Node* node, addr_t raw) : AsyncWrap(node, raw){}; + + static ReqWrap FromListNode(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: + addr_t raw_; + C* constants_; +}; +} // namespace node +} // namespace llnode + +#endif diff --git a/test/common.js b/test/common.js index e0dbed91..6e861997 100644 --- a/test/common.js +++ b/test/common.js @@ -89,12 +89,14 @@ SessionOutput.prototype.timeoutAfter = function timeoutAfter(timeout) { this.timeout = timeout; }; -SessionOutput.prototype.wait = function wait(regexp, callback, allLines) { +SessionOutput.prototype.wait = function wait(regexp, callback, allLines, + outputOnTimeout) { if (!this._queueWait(() => { this.wait(regexp, callback, allLines); })) return; const self = this; const lines = []; + outputOnTimeout = outputOnTimeout == undefined ? true : outputOnTimeout; function onLine(line) { lines.push(line); @@ -117,9 +119,11 @@ SessionOutput.prototype.wait = function wait(regexp, callback, allLines) { self.removeListener('line', onLine); self._unqueueWait(); - console.error(`${'='.repeat(10)} lldb output ${'='.repeat(10)}`); - console.error(lines.join('\n')); - console.error('='.repeat(33)); + if (outputOnTimeout) { + console.error(`${'='.repeat(10)} lldb output ${'='.repeat(10)}`); + console.error(lines.join('\n')); + console.error('='.repeat(33)); + } const message = `Test timeout in ${this.timeout} ` + `waiting for ${regexp}`; callback(new Error(message)); @@ -193,6 +197,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 +233,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..b4f68487 --- /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); +});