diff options
author | Fedor Indutny <fedor@indutny.com> | 2014-10-04 18:44:39 +0400 |
---|---|---|
committer | Fedor Indutny <fedor@indutny.com> | 2014-10-08 15:44:40 +0400 |
commit | 7a0cfe9b116c6b5d56f7412d6ea71f5a8d8440e2 (patch) | |
tree | 65f1811b365ca5e994d7621ff807b44a2854ef6d /deps | |
parent | 8efcc7f456f863cb04724c7b35fb935a8c441fe6 (diff) | |
download | node-7a0cfe9b116c6b5d56f7412d6ea71f5a8d8440e2.tar.gz |
deps: re-implement debugger-agent
Reviewed-By: Trevor Norris <trevnorris@gmail.com>
PR-URL: https://github.com/joyent/node/pull/8476
Diffstat (limited to 'deps')
-rw-r--r-- | deps/debugger-agent/debugger-agent.gyp | 24 | ||||
-rw-r--r-- | deps/debugger-agent/include/debugger-agent.h | 109 | ||||
-rw-r--r-- | deps/debugger-agent/lib/_debugger_agent.js | 191 | ||||
-rw-r--r-- | deps/debugger-agent/src/agent.cc | 347 | ||||
-rw-r--r-- | deps/debugger-agent/src/agent.h | 64 |
5 files changed, 735 insertions, 0 deletions
diff --git a/deps/debugger-agent/debugger-agent.gyp b/deps/debugger-agent/debugger-agent.gyp new file mode 100644 index 000000000..e98206849 --- /dev/null +++ b/deps/debugger-agent/debugger-agent.gyp @@ -0,0 +1,24 @@ +{ + "targets": [{ + "target_name": "debugger-agent", + "type": "<(library)", + "include_dirs": [ + "src", + "include", + "../v8/include", + "../uv/include", + + # Private node.js folder and stuff needed to include from it + "../../src", + "../cares/include", + ], + "direct_dependent_settings": { + "include_dirs": [ + "include", + ], + }, + "sources": [ + "src/agent.cc", + ], + }], +} diff --git a/deps/debugger-agent/include/debugger-agent.h b/deps/debugger-agent/include/debugger-agent.h new file mode 100644 index 000000000..762a687a0 --- /dev/null +++ b/deps/debugger-agent/include/debugger-agent.h @@ -0,0 +1,109 @@ +// Copyright Fedor Indutny and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +#ifndef DEPS_DEBUGGER_AGENT_INCLUDE_DEBUGGER_AGENT_H_ +#define DEPS_DEBUGGER_AGENT_INCLUDE_DEBUGGER_AGENT_H_ + +#include "uv.h" +#include "v8.h" +#include "v8-debug.h" + +namespace node { + +// Forward declaration +class Environment; + +namespace debugger { + +// Forward declaration +class AgentMessage; + +class Agent { + public: + explicit Agent(node::Environment* env); + ~Agent(); + + typedef void (*DispatchHandler)(node::Environment* env); + + // Start the debugger agent thread + bool Start(int port, bool wait); + // Listen for debug events + void Enable(); + // Stop the debugger agent + void Stop(); + + inline void set_dispatch_handler(DispatchHandler handler) { + dispatch_handler_ = handler; + } + + inline node::Environment* parent_env() const { return parent_env_; } + inline node::Environment* child_env() const { return child_env_; } + + protected: + void InitAdaptor(Environment* env); + + // Worker body + void WorkerRun(); + + static void ThreadCb(Agent* agent); + static void ParentSignalCb(uv_async_t* signal); + static void ChildSignalCb(uv_async_t* signal); + static void MessageHandler(const v8::Debug::Message& message); + + // V8 API + static Agent* Unwrap(const v8::FunctionCallbackInfo<v8::Value>& args); + static void NotifyListen(const v8::FunctionCallbackInfo<v8::Value>& args); + static void NotifyWait(const v8::FunctionCallbackInfo<v8::Value>& args); + static void SendCommand(const v8::FunctionCallbackInfo<v8::Value>& args); + + void EnqueueMessage(AgentMessage* message); + + enum State { + kNone, + kRunning + }; + + // TODO(indutny): Verify that there are no races + State state_; + + int port_; + bool wait_; + + uv_sem_t start_sem_; + uv_mutex_t message_mutex_; + uv_async_t child_signal_; + + uv_thread_t thread_; + node::Environment* parent_env_; + node::Environment* child_env_; + uv_loop_t child_loop_; + v8::Persistent<v8::Object> api_; + + // QUEUE + void* messages_[2]; + + DispatchHandler dispatch_handler_; +}; + +} // namespace debugger +} // namespace node + +#endif // DEPS_DEBUGGER_AGENT_INCLUDE_DEBUGGER_AGENT_H_ diff --git a/deps/debugger-agent/lib/_debugger_agent.js b/deps/debugger-agent/lib/_debugger_agent.js new file mode 100644 index 000000000..680c5e95c --- /dev/null +++ b/deps/debugger-agent/lib/_debugger_agent.js @@ -0,0 +1,191 @@ +var assert = require('assert'); +var net = require('net'); +var util = require('util'); +var Buffer = require('buffer').Buffer; + +var Transform = require('stream').Transform; + +exports.start = function start() { + var agent = new Agent(); + + // Do not let `agent.listen()` request listening from cluster master + var cluster = require('cluster'); + cluster.isWorker = false; + cluster.isMaster = true; + + agent.on('error', function(err) { + process._rawDebug(err.stack || err); + }); + + agent.listen(process._debugAPI.port, function() { + var addr = this.address(); + process._rawDebug('Debugger listening on port %d', addr.port); + process._debugAPI.notifyListen(); + }); + + // Just to spin-off events + // TODO(indutny): Figure out why node.cc isn't doing this + setImmediate(function() { + }); + + process._debugAPI.onclose = function() { + // We don't care about it, but it prevents loop from cleaning up gently + // NOTE: removeAllListeners won't work, as it doesn't call `removeListener` + process.listeners('SIGWINCH').forEach(function(fn) { + process.removeListener('SIGWINCH', fn); + }); + + agent.close(); + }; + + // Not used now, but anyway + return agent; +}; + +function Agent() { + net.Server.call(this, this.onConnection); + + this.first = true; + this.binding = process._debugAPI; + + var self = this; + this.binding.onmessage = function(msg) { + self.clients.forEach(function(client) { + client.send({}, msg); + }); + }; + + this.clients = []; + assert(this.binding, 'Debugger agent running without bindings!'); +} +util.inherits(Agent, net.Server); + +Agent.prototype.onConnection = function onConnection(socket) { + var c = new Client(this, socket); + + c.start(); + this.clients.push(c); + + var self = this; + c.once('close', function() { + var index = self.clients.indexOf(c); + assert(index !== -1); + self.clients.splice(index, 1); + }); +}; + +Agent.prototype.notifyWait = function notifyWait() { + if (this.first) + this.binding.notifyWait(); + this.first = false; +}; + +function Client(agent, socket) { + Transform.call(this); + this._readableState.objectMode = true; + + this.agent = agent; + this.binding = this.agent.binding; + this.socket = socket; + + // Parse incoming data + this.state = 'headers'; + this.headers = {}; + this.buffer = ''; + socket.pipe(this); + + this.on('data', this.onCommand); + + var self = this; + this.socket.on('close', function() { + self.destroy(); + }); +} +util.inherits(Client, Transform); + +Client.prototype.destroy = function destroy(msg) { + this.socket.destroy(); + + this.emit('close'); +}; + +Client.prototype._transform = function _transform(data, enc, cb) { + cb(); + + this.buffer += data; + + while (true) { + if (this.state === 'headers') { + // Not enough data + if (!/\r\n/.test(this.buffer)) + break; + + if (/^\r\n/.test(this.buffer)) { + this.buffer = this.buffer.slice(2); + this.state = 'body'; + continue; + } + + // Match: + // Header-name: header-value\r\n + var match = this.buffer.match(/^([^:\s\r\n]+)\s*:\s*([^\s\r\n]+)\r\n/); + if (!match) + return this.destroy('Expected header, but failed to parse it'); + + this.headers[match[1].toLowerCase()] = match[2]; + + this.buffer = this.buffer.slice(match[0].length); + } else { + var len = this.headers['content-length']; + if (len === undefined) + return this.destroy('Expected content-length'); + + len = len | 0; + if (Buffer.byteLength(this.buffer) < len) + break; + + this.push(new Command(this.headers, this.buffer.slice(0, len))); + this.state = 'headers'; + this.buffer = this.buffer.slice(len); + this.headers = {}; + } + } +}; + +Client.prototype.send = function send(headers, data) { + if (!data) + data = ''; + + var out = []; + Object.keys(headers).forEach(function(key) { + out.push(key + ': ' + headers[key]); + }); + out.push('Content-Length: ' + Buffer.byteLength(data), ''); + + this.socket.cork(); + this.socket.write(out.join('\r\n') + '\r\n'); + + if (data.length > 0) + this.socket.write(data); + this.socket.uncork(); +}; + +Client.prototype.start = function start() { + this.send({ + Type: 'connect', + 'V8-Version': process.versions.v8, + 'Protocol-Version': 1, + 'Embedding-Host': 'node ' + process.version + }); +}; + +Client.prototype.onCommand = function onCommand(cmd) { + this.binding.sendCommand(cmd.body); + + this.agent.notifyWait(); +}; + +function Command(headers, body) { + this.headers = headers; + this.body = body; +} diff --git a/deps/debugger-agent/src/agent.cc b/deps/debugger-agent/src/agent.cc new file mode 100644 index 000000000..335737ffe --- /dev/null +++ b/deps/debugger-agent/src/agent.cc @@ -0,0 +1,347 @@ +// Copyright Fedor Indutny and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "agent.h" +#include "debugger-agent.h" + +#include "node.h" +#include "node_internals.h" // ARRAY_SIZE +#include "env.h" +#include "env-inl.h" +#include "v8.h" +#include "v8-debug.h" +#include "util.h" +#include "util-inl.h" +#include "queue.h" + +#include <string.h> + +namespace node { +namespace debugger { + +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Handle; +using v8::HandleScope; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::Locker; +using v8::Object; +using v8::String; +using v8::Value; + + +Agent::Agent(Environment* env) : state_(kNone), + port_(5858), + wait_(false), + parent_env_(env), + child_env_(NULL), + dispatch_handler_(NULL) { + int err; + + err = uv_sem_init(&start_sem_, 0); + CHECK_EQ(err, 0); + + err = uv_mutex_init(&message_mutex_); + CHECK_EQ(err, 0); + + QUEUE_INIT(&messages_); +} + + +Agent::~Agent() { + Stop(); + + uv_sem_destroy(&start_sem_); + uv_mutex_destroy(&message_mutex_); + + // Clean-up messages + while (!QUEUE_EMPTY(&messages_)) { + QUEUE* q = QUEUE_HEAD(&messages_); + QUEUE_REMOVE(q); + AgentMessage* msg = ContainerOf(&AgentMessage::member, q); + delete msg; + } +} + + +bool Agent::Start(int port, bool wait) { + int err; + + if (state_ == kRunning) + return false; + + err = uv_loop_init(&child_loop_); + if (err != 0) + goto loop_init_failed; + + // Interruption signal handler + err = uv_async_init(&child_loop_, &child_signal_, ChildSignalCb); + if (err != 0) + goto async_init_failed; + uv_unref(reinterpret_cast<uv_handle_t*>(&child_signal_)); + + port_ = port; + wait_ = wait; + + err = uv_thread_create(&thread_, + reinterpret_cast<uv_thread_cb>(ThreadCb), + this); + if (err != 0) + goto thread_create_failed; + + uv_sem_wait(&start_sem_); + + state_ = kRunning; + + return true; + + thread_create_failed: + uv_close(reinterpret_cast<uv_handle_t*>(&child_signal_), NULL); + + async_init_failed: + err = uv_loop_close(&child_loop_); + CHECK_EQ(err, 0); + + loop_init_failed: + return false; +} + + +void Agent::Enable() { + v8::Debug::SetMessageHandler(MessageHandler); + + // Assign environment to the debugger's context + // NOTE: The debugger context is created after `SetMessageHandler()` call + parent_env()->AssignToContext(v8::Debug::GetDebugContext()); +} + + +void Agent::Stop() { + int err; + + if (state_ != kRunning) { + return; + } + + v8::Debug::SetMessageHandler(NULL); + + // Send empty message to terminate things + EnqueueMessage(new AgentMessage(NULL, 0)); + + // Signal worker thread to make it stop + err = uv_async_send(&child_signal_); + CHECK_EQ(err, 0); + + err = uv_thread_join(&thread_); + CHECK_EQ(err, 0); + + uv_close(reinterpret_cast<uv_handle_t*>(&child_signal_), NULL); + uv_run(&child_loop_, UV_RUN_NOWAIT); + + err = uv_loop_close(&child_loop_); + CHECK_EQ(err, 0); + + state_ = kNone; +} + + +void Agent::WorkerRun() { + static const char* argv[] = { "node", "--debug-agent" }; + Isolate* isolate = Isolate::New(); + { + Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + + HandleScope handle_scope(isolate); + Local<Context> context = Context::New(isolate); + + Context::Scope context_scope(context); + Environment* env = CreateEnvironment( + isolate, + &child_loop_, + context, + ARRAY_SIZE(argv), + argv, + ARRAY_SIZE(argv), + argv); + + child_env_ = env; + + // Expose API + InitAdaptor(env); + LoadEnvironment(env); + + CHECK_EQ(&child_loop_, env->event_loop()); + uv_run(&child_loop_, UV_RUN_DEFAULT); + + // Clean-up peristent + api_.Reset(); + + // Clean-up all running handles + env->CleanupHandles(); + + env->Dispose(); + env = NULL; + } + isolate->Dispose(); +} + + +void Agent::InitAdaptor(Environment* env) { + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + + // Create API adaptor + Local<FunctionTemplate> t = FunctionTemplate::New(isolate); + t->InstanceTemplate()->SetInternalFieldCount(1); + t->SetClassName(String::NewFromUtf8(isolate, "DebugAPI")); + + NODE_SET_PROTOTYPE_METHOD(t, "notifyListen", NotifyListen); + NODE_SET_PROTOTYPE_METHOD(t, "notifyWait", NotifyWait); + NODE_SET_PROTOTYPE_METHOD(t, "sendCommand", SendCommand); + + Local<Object> api = t->GetFunction()->NewInstance(); + api->SetAlignedPointerInInternalField(0, this); + + api->Set(String::NewFromUtf8(isolate, "port"), Integer::New(isolate, port_)); + + env->process_object()->Set(String::NewFromUtf8(isolate, "_debugAPI"), api); + api_.Reset(env->isolate(), api); +} + + +Agent* Agent::Unwrap(const v8::FunctionCallbackInfo<v8::Value>& args) { + void* ptr = args.Holder()->GetAlignedPointerFromInternalField(0); + return reinterpret_cast<Agent*>(ptr); +} + + +void Agent::NotifyListen(const FunctionCallbackInfo<Value>& args) { + Agent* a = Unwrap(args); + + // Notify other thread that we are ready to process events + uv_sem_post(&a->start_sem_); +} + + +void Agent::NotifyWait(const FunctionCallbackInfo<Value>& args) { + Agent* a = Unwrap(args); + + a->wait_ = false; + + int err = uv_async_send(&a->child_signal_); + CHECK_EQ(err, 0); +} + + +void Agent::SendCommand(const FunctionCallbackInfo<Value>& args) { + Agent* a = Unwrap(args); + Environment* env = a->child_env(); + HandleScope scope(env->isolate()); + + String::Value v(args[0]); + + v8::Debug::SendCommand(a->parent_env()->isolate(), *v, v.length()); + if (a->dispatch_handler_ != NULL) + a->dispatch_handler_(a->parent_env()); +} + + +void Agent::ThreadCb(Agent* agent) { + agent->WorkerRun(); +} + + +void Agent::ChildSignalCb(uv_async_t* signal) { + Agent* a = ContainerOf(&Agent::child_signal_, signal); + Isolate* isolate = a->child_env()->isolate(); + + HandleScope scope(isolate); + Local<Object> api = PersistentToLocal(isolate, a->api_); + + uv_mutex_lock(&a->message_mutex_); + while (!QUEUE_EMPTY(&a->messages_)) { + QUEUE* q = QUEUE_HEAD(&a->messages_); + AgentMessage* msg = ContainerOf(&AgentMessage::member, q); + + // Time to close everything + if (msg->data() == NULL) { + QUEUE_REMOVE(q); + delete msg; + + MakeCallback(isolate, api, "onclose", 0, NULL); + break; + } + + // Waiting for client, do not send anything just yet + // TODO(indutny): move this to js-land + if (a->wait_) + break; + + QUEUE_REMOVE(q); + Local<Value> argv[] = { + String::NewFromTwoByte(isolate, + msg->data(), + String::kNormalString, + msg->length()) + }; + + // Emit message + MakeCallback(isolate, + api, + "onmessage", + ARRAY_SIZE(argv), + argv); + delete msg; + } + uv_mutex_unlock(&a->message_mutex_); +} + + +void Agent::EnqueueMessage(AgentMessage* message) { + uv_mutex_lock(&message_mutex_); + QUEUE_INSERT_TAIL(&messages_, &message->member); + uv_mutex_unlock(&message_mutex_); + uv_async_send(&child_signal_); +} + + +void Agent::MessageHandler(const v8::Debug::Message& message) { + Isolate* isolate = message.GetIsolate(); + Environment* env = Environment::GetCurrent(isolate); + Agent* a = env->debugger_agent(); + CHECK_NE(a, NULL); + CHECK_EQ(isolate, a->parent_env()->isolate()); + + HandleScope scope(isolate); + Local<String> json = message.GetJSON(); + String::Value v(json); + + AgentMessage* msg = new AgentMessage(*v, v.length()); + a->EnqueueMessage(msg); +} + +} // namespace debugger +} // namespace node diff --git a/deps/debugger-agent/src/agent.h b/deps/debugger-agent/src/agent.h new file mode 100644 index 000000000..82db5e5e1 --- /dev/null +++ b/deps/debugger-agent/src/agent.h @@ -0,0 +1,64 @@ +// Copyright Fedor Indutny and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +#ifndef DEPS_DEBUGGER_AGENT_SRC_AGENT_H_ +#define DEPS_DEBUGGER_AGENT_SRC_AGENT_H_ + +#include "v8.h" +#include "v8-debug.h" +#include "queue.h" + +#include <assert.h> +#include <string.h> + +namespace node { +namespace debugger { + +class AgentMessage { + public: + AgentMessage(uint16_t* val, int length) : length_(length) { + if (val == NULL) { + data_ = val; + } else { + data_ = new uint16_t[length]; + memcpy(data_, val, length * sizeof(*data_)); + } + } + + ~AgentMessage() { + delete[] data_; + data_ = NULL; + } + + inline const uint16_t* data() const { return data_; } + inline int length() const { return length_; } + + QUEUE member; + + private: + uint16_t* data_; + int length_; +}; + +} // namespace debugger +} // namespace node + +#endif // DEPS_DEBUGGER_AGENT_SRC_AGENT_H_ |