diff --git a/node.gyp b/node.gyp index f59037737c905a..e5f02d73086a09 100644 --- a/node.gyp +++ b/node.gyp @@ -318,8 +318,10 @@ 'sources': [ 'src/inspector_agent.cc', 'src/inspector_socket.cc', - 'src/inspector_socket.h', + 'src/inspector_socket_server.cc', 'src/inspector_agent.h', + 'src/inspector_socket.h', + 'src/inspector_socket_server.h', ], 'dependencies': [ 'deps/v8_inspector/src/inspector/inspector.gyp:standalone_inspector', @@ -868,7 +870,8 @@ 'dependencies': [ 'deps/gtest/gtest.gyp:gtest' ], 'include_dirs': [ 'src', - 'deps/v8/include' + 'deps/v8/include', + '<(SHARED_INTERMEDIATE_DIR)' ], 'defines': [ # gtest's ASSERT macros conflict with our own. @@ -886,9 +889,21 @@ 'conditions': [ ['v8_inspector=="true"', { + 'defines': [ + 'HAVE_INSPECTOR=1', + ], + 'dependencies': [ + 'deps/zlib/zlib.gyp:zlib', + 'v8_inspector_compress_protocol_json#host' + ], + 'include_dirs': [ + '<(SHARED_INTERMEDIATE_DIR)' + ], 'sources': [ 'src/inspector_socket.cc', - 'test/cctest/test_inspector_socket.cc' + 'src/inspector_socket_server.cc', + 'test/cctest/test_inspector_socket.cc', + 'test/cctest/test_inspector_socket_server.cc' ], 'conditions': [ [ 'node_shared_openssl=="false"', { diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index ec713942f50e7e..fc478c49a09d61 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -1,6 +1,6 @@ #include "inspector_agent.h" -#include "inspector_socket.h" +#include "inspector_socket_server.h" #include "env.h" #include "env-inl.h" #include "node.h" @@ -37,84 +37,6 @@ static const uint8_t PROTOCOL_JSON[] = { #include "v8_inspector_protocol_json.h" // NOLINT(build/include_order) }; -std::string GetWsUrl(int port, const std::string& id) { - char buf[1024]; - snprintf(buf, sizeof(buf), "127.0.0.1:%d/%s", port, id.c_str()); - return buf; -} - -void PrintDebuggerReadyMessage(int port, const std::string& id) { - fprintf(stderr, "Debugger listening on port %d.\n" - "Warning: This is an experimental feature and could change at any time.\n" - "To start debugging, open the following URL in Chrome:\n" - " chrome-devtools://devtools/bundled/inspector.html?" - "experiments=true&v8only=true&ws=%s\n", - port, GetWsUrl(port, id).c_str()); - fflush(stderr); -} - -std::string MapToString(const std::map object) { - std::ostringstream json; - json << "[ {\n"; - bool first = true; - for (const auto& name_value : object) { - if (!first) - json << ",\n"; - json << " \"" << name_value.first << "\": \""; - json << name_value.second << "\""; - first = false; - } - json << "\n} ]\n\n"; - return json.str(); -} - -void Escape(std::string* string) { - for (char& c : *string) { - c = (c == '\"' || c == '\\') ? '_' : c; - } -} - -void DisposeInspector(InspectorSocket* socket, int status) { - delete socket; -} - -void DisconnectAndDisposeIO(InspectorSocket* socket) { - if (socket) { - inspector_close(socket, DisposeInspector); - } -} - -void OnBufferAlloc(uv_handle_t* handle, size_t len, uv_buf_t* buf) { - buf->base = new char[len]; - buf->len = len; -} - -void SendHttpResponse(InspectorSocket* socket, const char* response, - size_t size) { - const char HEADERS[] = "HTTP/1.0 200 OK\r\n" - "Content-Type: application/json; charset=UTF-8\r\n" - "Cache-Control: no-cache\r\n" - "Content-Length: %zu\r\n" - "\r\n"; - char header[sizeof(HEADERS) + 20]; - int header_len = snprintf(header, sizeof(header), HEADERS, size); - inspector_write(socket, header, header_len); - inspector_write(socket, response, size); -} - -void SendHttpResponse(InspectorSocket* socket, const std::string& response) { - SendHttpResponse(socket, response.data(), response.size()); -} - -void SendVersionResponse(InspectorSocket* socket) { - static const char response[] = - "{\n" - " \"Browser\": \"node.js/" NODE_VERSION "\",\n" - " \"Protocol-Version\": \"1.1\"\n" - "}\n"; - SendHttpResponse(socket, response, sizeof(response) - 1); -} - std::string GetProcessTitle() { // uv_get_process_title will trim the title if it is too long. char title[2048]; @@ -126,36 +48,6 @@ std::string GetProcessTitle() { } } -void SendProtocolJson(InspectorSocket* socket) { - z_stream strm; - strm.zalloc = Z_NULL; - strm.zfree = Z_NULL; - strm.opaque = Z_NULL; - CHECK_EQ(Z_OK, inflateInit(&strm)); - static const size_t kDecompressedSize = - PROTOCOL_JSON[0] * 0x10000u + - PROTOCOL_JSON[1] * 0x100u + - PROTOCOL_JSON[2]; - strm.next_in = const_cast(PROTOCOL_JSON + 3); - strm.avail_in = sizeof(PROTOCOL_JSON) - 3; - std::string data(kDecompressedSize, '\0'); - strm.next_out = reinterpret_cast(&data[0]); - strm.avail_out = data.size(); - CHECK_EQ(Z_STREAM_END, inflate(&strm, Z_FINISH)); - CHECK_EQ(0, strm.avail_out); - CHECK_EQ(Z_OK, inflateEnd(&strm)); - SendHttpResponse(socket, data); -} - -const char* match_path_segment(const char* path, const char* expected) { - size_t len = strlen(expected); - if (StringEqualNoCaseN(path, expected, len)) { - if (path[len] == '/') return path + len + 1; - if (path[len] == '\0') return path + len; - } - return nullptr; -} - // UUID RFC: https://www.ietf.org/rfc/rfc4122.txt // Used ver 4 - with numbers std::string GenerateID() { @@ -201,17 +93,39 @@ std::string StringViewToUtf8(const StringView& view) { return result; } -std::unique_ptr Utf8ToStringView(const char* source, - size_t length) { - UnicodeString utf16 = UnicodeString::fromUTF8(StringPiece(source, length)); +std::unique_ptr Utf8ToStringView(const std::string& message) { + UnicodeString utf16 = + UnicodeString::fromUTF8(StringPiece(message.data(), message.length())); StringView view(reinterpret_cast(utf16.getBuffer()), utf16.length()); return StringBuffer::create(view); } + } // namespace class V8NodeInspector; +class InspectorAgentDelegate: public node::inspector::SocketServerDelegate { + public: + InspectorAgentDelegate(AgentImpl* agent, const std::string& script_path, + const std::string& script_name, bool wait); + bool StartSession(int session_id, const std::string& target_id) override; + void MessageReceived(int session_id, const std::string& message) override; + void EndSession(int session_id) override; + std::vector GetTargetIds() override; + std::string GetTargetTitle(const std::string& id) override; + std::string GetTargetUrl(const std::string& id) override; + bool IsConnected() { return connected_; } + private: + AgentImpl* agent_; + bool connected_; + int session_id_; + const std::string script_name_; + const std::string script_path_; + const std::string target_id_; + bool waiting_; +}; + class AgentImpl { public: explicit AgentImpl(node::Environment* env); @@ -223,42 +137,37 @@ class AgentImpl { void Stop(); bool IsStarted(); - bool IsConnected() { return state_ == State::kConnected; } + bool IsConnected(); void WaitForDisconnect(); void FatalException(v8::Local error, v8::Local message); + void PostIncomingMessage(int session_id, const std::string& message); + void ResumeStartup() { + uv_sem_post(&start_sem_); + } + private: using MessageQueue = std::vector>>; enum class State { kNew, kAccepting, kConnected, kDone, kError }; static void ThreadCbIO(void* agent); - static void OnSocketConnectionIO(uv_stream_t* server, int status); - static bool OnInspectorHandshakeIO(InspectorSocket* socket, - enum inspector_handshake_event state, - const std::string& path); static void WriteCbIO(uv_async_t* async); void InstallInspectorOnProcess(); void WorkerRunIO(); - void OnInspectorConnectionIO(InspectorSocket* socket); - void OnRemoteDataIO(InspectorSocket* stream, ssize_t read, - const uv_buf_t* b); void SetConnected(bool connected); void DispatchMessages(); void Write(int session_id, const StringView& message); bool AppendMessage(MessageQueue* vector, int session_id, std::unique_ptr buffer); void SwapBehindLock(MessageQueue* vector1, MessageQueue* vector2); - void PostIncomingMessage(const char* message, size_t len); void WaitForFrontendMessage(); void NotifyMessageReceived(); State ToState(State state); - void SendListResponse(InspectorSocket* socket); - bool RespondToGet(InspectorSocket* socket, const std::string& path); uv_sem_t start_sem_; ConditionVariable incoming_message_cond_; @@ -266,6 +175,8 @@ class AgentImpl { uv_thread_t thread_; uv_loop_t child_loop_; + InspectorAgentDelegate* delegate_; + int port_; bool wait_; bool shutting_down_; @@ -274,18 +185,15 @@ class AgentImpl { uv_async_t* data_written_; uv_async_t io_thread_req_; - InspectorSocket* client_socket_; V8NodeInspector* inspector_; v8::Platform* platform_; MessageQueue incoming_message_queue_; MessageQueue outgoing_message_queue_; bool dispatching_messages_; - int frontend_session_id_; - int backend_session_id_; + int session_id_; + InspectorSocketServer* server_; std::string script_name_; - std::string script_path_; - const std::string id_; friend class ChannelImpl; friend class DispatchOnInspectorBackendTask; @@ -300,11 +208,6 @@ void InterruptCallback(v8::Isolate*, void* agent) { static_cast(agent)->DispatchMessages(); } -void DataCallback(uv_stream_t* stream, ssize_t read, const uv_buf_t* buf) { - InspectorSocket* socket = inspector_from_stream(stream); - static_cast(socket->data)->OnRemoteDataIO(socket, read, buf); -} - class DispatchOnInspectorBackendTask : public v8::Task { public: explicit DispatchOnInspectorBackendTask(AgentImpl* agent) : agent_(agent) {} @@ -333,7 +236,7 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel { void flushProtocolNotifications() override { } void sendMessageToFrontend(const StringView& message) { - agent_->Write(agent_->frontend_session_id_, message); + agent_->Write(agent_->session_id_, message); } AgentImpl* const agent_; @@ -414,19 +317,18 @@ class V8NodeInspector : public v8_inspector::V8InspectorClient { std::unique_ptr session_; }; -AgentImpl::AgentImpl(Environment* env) : port_(0), +AgentImpl::AgentImpl(Environment* env) : delegate_(nullptr), + port_(0), wait_(false), shutting_down_(false), state_(State::kNew), parent_env_(env), data_written_(new uv_async_t()), - client_socket_(nullptr), inspector_(nullptr), platform_(nullptr), dispatching_messages_(false), - frontend_session_id_(0), - backend_session_id_(0), - id_(GenerateID()) { + session_id_(0), + server_(nullptr) { CHECK_EQ(0, uv_sem_init(&start_sem_, 0)); memset(&io_thread_req_, 0, sizeof(io_thread_req_)); CHECK_EQ(0, uv_async_init(env->event_loop(), data_written_, nullptr)); @@ -543,6 +445,10 @@ void AgentImpl::Stop() { delete inspector_; } +bool AgentImpl::IsConnected() { + return delegate_ != nullptr && delegate_->IsConnected(); +} + bool AgentImpl::IsStarted() { return !!platform_; } @@ -550,6 +456,9 @@ bool AgentImpl::IsStarted() { void AgentImpl::WaitForDisconnect() { if (state_ == State::kConnected) { shutting_down_ = true; + // Gives a signal to stop accepting new connections + // TODO(eugeneo): Introduce an API with explicit request names. + Write(0, StringView()); fprintf(stderr, "Waiting for the debugger to disconnect...\n"); fflush(stderr); inspector_->runMessageLoopOnPause(0); @@ -621,181 +530,59 @@ void AgentImpl::ThreadCbIO(void* agent) { static_cast(agent)->WorkerRunIO(); } -// static -void AgentImpl::OnSocketConnectionIO(uv_stream_t* server, int status) { - if (status == 0) { - InspectorSocket* socket = new InspectorSocket(); - socket->data = server->data; - if (inspector_accept(server, socket, - AgentImpl::OnInspectorHandshakeIO) != 0) { - delete socket; - } - } -} - -// static -bool AgentImpl::OnInspectorHandshakeIO(InspectorSocket* socket, - enum inspector_handshake_event state, - const std::string& path) { - AgentImpl* agent = static_cast(socket->data); - switch (state) { - case kInspectorHandshakeHttpGet: - return agent->RespondToGet(socket, path); - case kInspectorHandshakeUpgrading: - return path.length() == agent->id_.length() + 1 && - path.find(agent->id_) == 1; - case kInspectorHandshakeUpgraded: - agent->OnInspectorConnectionIO(socket); - return true; - case kInspectorHandshakeFailed: - delete socket; - return false; - default: - UNREACHABLE(); - return false; - } -} - -void AgentImpl::OnRemoteDataIO(InspectorSocket* socket, - ssize_t read, - const uv_buf_t* buf) { - if (read > 0) { - // TODO(pfeldman): Instead of blocking execution while debugger - // engages, node should wait for the run callback from the remote client - // and initiate its startup. This is a change to node.cc that should be - // upstreamed separately. - if (wait_) { - std::string message(buf->base, read); - if (message.find("\"Runtime.runIfWaitingForDebugger\"") != - std::string::npos) { - wait_ = false; - uv_sem_post(&start_sem_); - } - } - PostIncomingMessage(buf->base, read); - } else { - // EOF - if (client_socket_ == socket) { - client_socket_ = nullptr; - PostIncomingMessage(TAG_DISCONNECT, sizeof(TAG_DISCONNECT) - 1); - } - DisconnectAndDisposeIO(socket); - } - if (buf) { - delete[] buf->base; - } -} - -void AgentImpl::SendListResponse(InspectorSocket* socket) { - std::map response; - response["description"] = "node.js instance"; - response["faviconUrl"] = "https://nodejs.org/static/favicon.ico"; - response["id"] = id_; - response["title"] = script_name_.empty() ? GetProcessTitle() : script_name_; - Escape(&response["title"]); - response["type"] = "node"; - // This attribute value is a "best effort" URL that is passed as a JSON - // string. It is not guaranteed to resolve to a valid resource. - response["url"] = "file://" + script_path_; - Escape(&response["url"]); - - if (!client_socket_) { - std::string address = GetWsUrl(port_, id_); - - std::ostringstream frontend_url; - frontend_url << "chrome-devtools://devtools/bundled"; - frontend_url << "/inspector.html?experiments=true&v8only=true&ws="; - frontend_url << address; - - response["devtoolsFrontendUrl"] += frontend_url.str(); - response["webSocketDebuggerUrl"] = "ws://" + address; - } - SendHttpResponse(socket, MapToString(response)); -} - -bool AgentImpl::RespondToGet(InspectorSocket* socket, const std::string& path) { - const char* command = match_path_segment(path.c_str(), "/json"); - if (command == nullptr) - return false; - - if (match_path_segment(command, "list") || command[0] == '\0') { - SendListResponse(socket); - return true; - } else if (match_path_segment(command, "protocol")) { - SendProtocolJson(socket); - return true; - } else if (match_path_segment(command, "version")) { - SendVersionResponse(socket); - return true; - } else if (const char* pid = match_path_segment(command, "activate")) { - if (pid != id_) - return false; - SendHttpResponse(socket, "Target activated"); - return true; - } - return false; -} - // static void AgentImpl::WriteCbIO(uv_async_t* async) { AgentImpl* agent = static_cast(async->data); - InspectorSocket* socket = agent->client_socket_; - if (socket) { - MessageQueue outgoing_messages; - agent->SwapBehindLock(&agent->outgoing_message_queue_, &outgoing_messages); - for (const MessageQueue::value_type& outgoing : outgoing_messages) { - if (outgoing.first == agent->frontend_session_id_) { - StringView message = outgoing.second->string(); - std::string utf8Message = StringViewToUtf8(message); - inspector_write(socket, utf8Message.c_str(), utf8Message.length()); - } + MessageQueue outgoing_messages; + agent->SwapBehindLock(&agent->outgoing_message_queue_, &outgoing_messages); + for (const MessageQueue::value_type& outgoing : outgoing_messages) { + StringView view = outgoing.second->string(); + if (view.length() == 0) { + agent->server_->Stop(nullptr); + } else { + agent->server_->Send(outgoing.first, + StringViewToUtf8(outgoing.second->string())); } } } void AgentImpl::WorkerRunIO() { - sockaddr_in addr; - uv_tcp_t server; int err = uv_loop_init(&child_loop_); CHECK_EQ(err, 0); err = uv_async_init(&child_loop_, &io_thread_req_, AgentImpl::WriteCbIO); CHECK_EQ(err, 0); io_thread_req_.data = this; + std::string script_path; if (!script_name_.empty()) { uv_fs_t req; if (0 == uv_fs_realpath(&child_loop_, &req, script_name_.c_str(), nullptr)) - script_path_ = std::string(reinterpret_cast(req.ptr)); + script_path = std::string(reinterpret_cast(req.ptr)); uv_fs_req_cleanup(&req); } - uv_tcp_init(&child_loop_, &server); - uv_ip4_addr("0.0.0.0", port_, &addr); - server.data = this; - err = uv_tcp_bind(&server, - reinterpret_cast(&addr), 0); - if (err == 0) { - err = uv_listen(reinterpret_cast(&server), 1, - OnSocketConnectionIO); - } - if (err != 0) { + InspectorAgentDelegate delegate(this, script_path, script_name_, wait_); + delegate_ = &delegate; + InspectorSocketServer server(&delegate, port_); + if (!server.Start(&child_loop_)) { fprintf(stderr, "Unable to open devtools socket: %s\n", uv_strerror(err)); state_ = State::kError; // Safe, main thread is waiting on semaphore uv_close(reinterpret_cast(&io_thread_req_), nullptr); - uv_close(reinterpret_cast(&server), nullptr); uv_loop_close(&child_loop_); uv_sem_post(&start_sem_); return; } - PrintDebuggerReadyMessage(port_, id_); + server_ = &server; if (!wait_) { uv_sem_post(&start_sem_); } uv_run(&child_loop_, UV_RUN_DEFAULT); uv_close(reinterpret_cast(&io_thread_req_), nullptr); - uv_close(reinterpret_cast(&server), nullptr); - DisconnectAndDisposeIO(client_socket_); + server.Stop(nullptr); + server.TerminateConnections(nullptr); uv_run(&child_loop_, UV_RUN_NOWAIT); err = uv_loop_close(&child_loop_); CHECK_EQ(err, 0); + delegate_ = nullptr; + server_ = nullptr; } bool AgentImpl::AppendMessage(MessageQueue* queue, int session_id, @@ -811,9 +598,10 @@ void AgentImpl::SwapBehindLock(MessageQueue* vector1, MessageQueue* vector2) { vector1->swap(*vector2); } -void AgentImpl::PostIncomingMessage(const char* message, size_t len) { - if (AppendMessage(&incoming_message_queue_, frontend_session_id_, - Utf8ToStringView(message, len))) { +void AgentImpl::PostIncomingMessage(int session_id, + const std::string& message) { + if (AppendMessage(&incoming_message_queue_, session_id, + Utf8ToStringView(message))) { v8::Isolate* isolate = parent_env_->isolate(); platform_->CallOnForegroundThread(isolate, new DispatchOnInspectorBackendTask(this)); @@ -834,17 +622,6 @@ void AgentImpl::NotifyMessageReceived() { incoming_message_cond_.Broadcast(scoped_lock); } -void AgentImpl::OnInspectorConnectionIO(InspectorSocket* socket) { - if (client_socket_) { - DisconnectAndDisposeIO(socket); - return; - } - client_socket_ = socket; - inspector_read_start(socket, OnBufferAlloc, DataCallback); - frontend_session_id_++; - PostIncomingMessage(TAG_CONNECT, sizeof(TAG_CONNECT) - 1); -} - void AgentImpl::DispatchMessages() { // This function can be reentered if there was an incoming message while // V8 was processing another inspector request (e.g. if the user is @@ -867,7 +644,7 @@ void AgentImpl::DispatchMessages() { if (tag == TAG_CONNECT) { CHECK_EQ(State::kAccepting, state_); - backend_session_id_++; + session_id_ = pair.first; state_ = State::kConnected; fprintf(stderr, "Debugger attached.\n"); inspector_->connectFrontend(); @@ -876,7 +653,6 @@ void AgentImpl::DispatchMessages() { if (shutting_down_) { state_ = State::kDone; } else { - PrintDebuggerReadyMessage(port_, id_); state_ = State::kAccepting; } inspector_->quitMessageLoopOnPause(); @@ -930,6 +706,60 @@ void Agent::FatalException(v8::Local error, impl->FatalException(error, message); } +InspectorAgentDelegate::InspectorAgentDelegate(AgentImpl* agent, + const std::string& script_path, + const std::string& script_name, + bool wait) + : agent_(agent), + connected_(false), + session_id_(0), + script_name_(script_name), + script_path_(script_path), + target_id_(GenerateID()), + waiting_(wait) { } + + +bool InspectorAgentDelegate::StartSession(int session_id, + const std::string& target_id) { + if (connected_) + return false; + connected_ = true; + agent_->PostIncomingMessage(session_id, TAG_CONNECT); + return true; +} + +void InspectorAgentDelegate::MessageReceived(int session_id, + const std::string& message) { + // TODO(pfeldman): Instead of blocking execution while debugger + // engages, node should wait for the run callback from the remote client + // and initiate its startup. This is a change to node.cc that should be + // upstreamed separately. + if (waiting_) { + if (message.find("\"Runtime.runIfWaitingForDebugger\"") != + std::string::npos) { + waiting_ = false; + agent_->ResumeStartup(); + } + } + agent_->PostIncomingMessage(session_id, message); +} + +void InspectorAgentDelegate::EndSession(int session_id) { + connected_ = false; + agent_->PostIncomingMessage(session_id, TAG_DISCONNECT); +} + +std::vector InspectorAgentDelegate::GetTargetIds() { + return { target_id_ }; +} + +std::string InspectorAgentDelegate::GetTargetTitle(const std::string& id) { + return script_name_.empty() ? GetProcessTitle() : script_name_; +} + +std::string InspectorAgentDelegate::GetTargetUrl(const std::string& id) { + return "file://" + script_path_; +} } // namespace inspector } // namespace node diff --git a/src/inspector_agent.h b/src/inspector_agent.h index 3607cffba5d21f..b31c77496b3d70 100644 --- a/src/inspector_agent.h +++ b/src/inspector_agent.h @@ -1,6 +1,8 @@ #ifndef SRC_INSPECTOR_AGENT_H_ #define SRC_INSPECTOR_AGENT_H_ +#include + #if !HAVE_INSPECTOR #error("This header can only be used when inspector is enabled") #endif @@ -36,7 +38,6 @@ class Agent { bool IsStarted(); bool IsConnected(); void WaitForDisconnect(); - void FatalException(v8::Local error, v8::Local message); private: diff --git a/src/inspector_socket_server.cc b/src/inspector_socket_server.cc new file mode 100644 index 00000000000000..e05a0c577da7b1 --- /dev/null +++ b/src/inspector_socket_server.cc @@ -0,0 +1,471 @@ +#include "inspector_socket_server.h" + +#include "node.h" +#include "uv.h" +#include "zlib.h" + +#include +#include +#include +#include + +namespace node { +namespace inspector { + +namespace { + +static const uint8_t PROTOCOL_JSON[] = { + #include "v8_inspector_protocol_json.h" // NOLINT(build/include_order) +}; + +void Escape(std::string* string) { + for (char& c : *string) { + c = (c == '\"' || c == '\\') ? '_' : c; + } +} + +std::string GetWsUrl(int port, const std::string& id) { + char buf[1024]; + snprintf(buf, sizeof(buf), "127.0.0.1:%d/%s", port, id.c_str()); + return buf; +} + +std::string MapToString(const std::map& object) { + bool first = true; + std::ostringstream json; + json << "{\n"; + for (const auto& name_value : object) { + if (!first) + json << ",\n"; + first = false; + json << " \"" << name_value.first << "\": \""; + json << name_value.second << "\""; + } + json << "\n} "; + return json.str(); +} + +std::string MapsToString( + const std::vector>& array) { + bool first = true; + std::ostringstream json; + json << "[ "; + for (const auto& object : array) { + if (!first) + json << ", "; + first = false; + json << MapToString(object); + } + json << "]\n\n"; + return json.str(); +} + +const char* MatchPathSegment(const char* path, const char* expected) { + size_t len = strlen(expected); + if (StringEqualNoCaseN(path, expected, len)) { + if (path[len] == '/') return path + len + 1; + if (path[len] == '\0') return path + len; + } + return nullptr; +} + +void OnBufferAlloc(uv_handle_t* handle, size_t len, uv_buf_t* buf) { + buf->base = new char[len]; + buf->len = len; +} + +void PrintDebuggerReadyMessage(int port, const std::vector& ids) { + fprintf(stderr, + "Debugger listening on port %d.\n" + "Warning: This is an experimental feature " + "and could change at any time.\n", + port); + if (ids.size() == 1) + fprintf(stderr, "To start debugging, open the following URL in Chrome:\n"); + if (ids.size() > 1) + fprintf(stderr, "To start debugging, open the following URLs in Chrome:\n"); + for (const std::string& id : ids) { + fprintf(stderr, + " chrome-devtools://devtools/bundled/inspector.html?" + "experiments=true&v8only=true&ws=%s\n", GetWsUrl(port, id).c_str()); + } + fflush(stderr); +} + +void SendHttpResponse(InspectorSocket* socket, const std::string& response) { + const char HEADERS[] = "HTTP/1.0 200 OK\r\n" + "Content-Type: application/json; charset=UTF-8\r\n" + "Cache-Control: no-cache\r\n" + "Content-Length: %zu\r\n" + "\r\n"; + char header[sizeof(HEADERS) + 20]; + int header_len = snprintf(header, sizeof(header), HEADERS, response.size()); + inspector_write(socket, header, header_len); + inspector_write(socket, response.data(), response.size()); +} + +void SendVersionResponse(InspectorSocket* socket) { + std::map response; + response["Browser"] = "node.js/" NODE_VERSION; + response["Protocol-Version"] = "1.1"; + SendHttpResponse(socket, MapToString(response)); +} + +void SendProtocolJson(InspectorSocket* socket) { + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + CHECK_EQ(Z_OK, inflateInit(&strm)); + static const size_t kDecompressedSize = + PROTOCOL_JSON[0] * 0x10000u + + PROTOCOL_JSON[1] * 0x100u + + PROTOCOL_JSON[2]; + strm.next_in = const_cast(PROTOCOL_JSON + 3); + strm.avail_in = sizeof(PROTOCOL_JSON) - 3; + std::string data(kDecompressedSize, '\0'); + strm.next_out = reinterpret_cast(&data[0]); + strm.avail_out = data.size(); + CHECK_EQ(Z_STREAM_END, inflate(&strm, Z_FINISH)); + CHECK_EQ(0, strm.avail_out); + CHECK_EQ(Z_OK, inflateEnd(&strm)); + SendHttpResponse(socket, data); +} + +} // namespace + + +class Closer { + public: + explicit Closer(InspectorSocketServer* server) : server_(server), + close_count_(0) { } + + void AddCallback(InspectorSocketServer::ServerCallback callback) { + if (callback == nullptr) + return; + callbacks_.insert(callback); + } + + void DecreaseExpectedCount() { + --close_count_; + NotifyIfDone(); + } + + void IncreaseExpectedCount() { + ++close_count_; + } + + void NotifyIfDone() { + if (close_count_ == 0) { + for (auto callback : callbacks_) { + callback(server_); + } + InspectorSocketServer* server = server_; + delete server->closer_; + server->closer_ = nullptr; + } + } + + private: + InspectorSocketServer* server_; + std::set callbacks_; + int close_count_; +}; + +class SocketSession { + public: + SocketSession(InspectorSocketServer* server, int id); + void Close(bool socket_cleanup, Closer* closer); + void Declined() { state_ = State::kDeclined; } + static SocketSession* From(InspectorSocket* socket) { + return node::ContainerOf(&SocketSession::socket_, socket); + } + void FrontendConnected(); + InspectorSocketServer* GetServer() { return server_; } + int Id() { return id_; } + void Send(const std::string& message); + void SetTargetId(const std::string& target_id) { + CHECK(target_id_.empty()); + target_id_ = target_id; + } + InspectorSocket* Socket() { return &socket_; } + const std::string TargetId() { return target_id_; } + + private: + enum class State { kHttp, kWebSocket, kClosing, kEOF, kDeclined }; + static void CloseCallback_(InspectorSocket* socket, int code); + static void ReadCallback_(uv_stream_t* stream, ssize_t read, + const uv_buf_t* buf); + void OnRemoteDataIO(InspectorSocket* socket, ssize_t read, + const uv_buf_t* buf); + const int id_; + Closer* closer_; + InspectorSocket socket_; + InspectorSocketServer* server_; + std::string target_id_; + State state_; +}; + +InspectorSocketServer::InspectorSocketServer(SocketServerDelegate* delegate, + int port) : loop_(nullptr), + delegate_(delegate), + port_(port), + closer_(nullptr), + next_session_id_(0) { } + + +// static +bool InspectorSocketServer::HandshakeCallback(InspectorSocket* socket, + inspector_handshake_event event, + const std::string& path) { + InspectorSocketServer* server = SocketSession::From(socket)->GetServer(); + const std::string& id = path.empty() ? path : path.substr(1); + switch (event) { + case kInspectorHandshakeHttpGet: + return server->RespondToGet(socket, path); + case kInspectorHandshakeUpgrading: + return server->SessionStarted(SocketSession::From(socket), id); + case kInspectorHandshakeUpgraded: + SocketSession::From(socket)->FrontendConnected(); + return true; + case kInspectorHandshakeFailed: + SocketSession::From(socket)->Close(false, nullptr); + return false; + default: + UNREACHABLE(); + return false; + } +} + +bool InspectorSocketServer::SessionStarted(SocketSession* session, + const std::string& id) { + bool connected = false; + if (TargetExists(id)) { + connected = delegate_->StartSession(session->Id(), id); + } + if (connected) { + connected_sessions_[session->Id()] = session; + session->SetTargetId(id); + } else { + session->Declined(); + } + return connected; +} + +void InspectorSocketServer::SessionTerminated(int session_id) { + if (connected_sessions_.erase(session_id) == 0) { + return; + } + delegate_->EndSession(session_id); + if (connected_sessions_.empty() && + uv_is_active(reinterpret_cast(&server_))) { + PrintDebuggerReadyMessage(port_, delegate_->GetTargetIds()); + } +} + +bool InspectorSocketServer::RespondToGet(InspectorSocket* socket, + const std::string& path) { + const char* command = MatchPathSegment(path.c_str(), "/json"); + if (command == nullptr) + return false; + + if (MatchPathSegment(command, "list") || command[0] == '\0') { + SendListResponse(socket); + return true; + } else if (MatchPathSegment(command, "protocol")) { + SendProtocolJson(socket); + return true; + } else if (MatchPathSegment(command, "version")) { + SendVersionResponse(socket); + return true; + } else if (const char* target_id = MatchPathSegment(command, "activate")) { + if (TargetExists(target_id)) { + SendHttpResponse(socket, "Target activated"); + return true; + } + return false; + } + return false; +} + +void InspectorSocketServer::SendListResponse(InspectorSocket* socket) { + std::vector> response; + for (const std::string& id : delegate_->GetTargetIds()) { + response.push_back(std::map()); + std::map& target_map = response.back(); + target_map["description"] = "node.js instance"; + target_map["faviconUrl"] = "https://nodejs.org/static/favicon.ico"; + target_map["id"] = id; + target_map["title"] = delegate_->GetTargetTitle(id); + Escape(&target_map["title"]); + target_map["type"] = "node"; + // This attribute value is a "best effort" URL that is passed as a JSON + // string. It is not guaranteed to resolve to a valid resource. + target_map["url"] = delegate_->GetTargetUrl(id); + Escape(&target_map["url"]); + + bool connected = false; + for (const auto& session : connected_sessions_) { + if (session.second->TargetId() == id) { + connected = true; + break; + } + } + if (!connected) { + std::string address = GetWsUrl(port_, id); + std::ostringstream frontend_url; + frontend_url << "chrome-devtools://devtools/bundled"; + frontend_url << "/inspector.html?experiments=true&v8only=true&ws="; + frontend_url << address; + target_map["devtoolsFrontendUrl"] += frontend_url.str(); + target_map["webSocketDebuggerUrl"] = "ws://" + address; + } + } + SendHttpResponse(socket, MapsToString(response)); +} + +bool InspectorSocketServer::Start(uv_loop_t* loop) { + loop_ = loop; + sockaddr_in addr; + uv_tcp_init(loop_, &server_); + uv_ip4_addr("0.0.0.0", port_, &addr); + int err = uv_tcp_bind(&server_, + reinterpret_cast(&addr), 0); + if (err == 0) { + err = uv_listen(reinterpret_cast(&server_), 1, + SocketConnectedCallback); + } + if (err == 0 && connected_sessions_.empty()) { + PrintDebuggerReadyMessage(port_, delegate_->GetTargetIds()); + } + if (err != 0 && connected_sessions_.empty()) { + fprintf(stderr, "Unable to open devtools socket: %s\n", uv_strerror(err)); + uv_close(reinterpret_cast(&server_), nullptr); + return false; + } + return true; +} + +void InspectorSocketServer::Stop(ServerCallback cb) { + if (closer_ == nullptr) { + closer_ = new Closer(this); + } + closer_->AddCallback(cb); + + uv_handle_t* handle = reinterpret_cast(&server_); + if (uv_is_active(handle)) { + closer_->IncreaseExpectedCount(); + uv_close(reinterpret_cast(&server_), ServerClosedCallback); + } + closer_->NotifyIfDone(); +} + +void InspectorSocketServer::TerminateConnections(ServerCallback cb) { + if (closer_ == nullptr) { + closer_ = new Closer(this); + } + closer_->AddCallback(cb); + std::map sessions; + std::swap(sessions, connected_sessions_); + for (const auto& session : sessions) { + int id = session.second->Id(); + session.second->Close(true, closer_); + delegate_->EndSession(id); + } + closer_->NotifyIfDone(); +} + +bool InspectorSocketServer::TargetExists(const std::string& id) { + const std::vector& target_ids = delegate_->GetTargetIds(); + const auto& found = std::find(target_ids.begin(), target_ids.end(), id); + return found != target_ids.end(); +} + +void InspectorSocketServer::Send(int session_id, const std::string& message) { + auto session_iterator = connected_sessions_.find(session_id); + if (session_iterator != connected_sessions_.end()) { + session_iterator->second->Send(message); + } +} + +// static +void InspectorSocketServer::ServerClosedCallback(uv_handle_t* server) { + InspectorSocketServer* socket_server = InspectorSocketServer::From(server); + if (socket_server->closer_) + socket_server->closer_->DecreaseExpectedCount(); +} + +// static +void InspectorSocketServer::SocketConnectedCallback(uv_stream_t* server, + int status) { + if (status == 0) { + InspectorSocketServer* socket_server = InspectorSocketServer::From(server); + SocketSession* session = + new SocketSession(socket_server, socket_server->next_session_id_++); + if (inspector_accept(server, session->Socket(), HandshakeCallback) != 0) { + delete session; + } + } +} + +// InspectorSession tracking +SocketSession::SocketSession(InspectorSocketServer* server, int id) + : id_(id), closer_(nullptr), server_(server), + state_(State::kHttp) { } + +void SocketSession::Close(bool socket_cleanup, Closer* closer) { + CHECK_EQ(closer_, nullptr); + CHECK_NE(state_, State::kClosing); + server_->SessionTerminated(id_); + if (socket_cleanup) { + state_ = State::kClosing; + closer_ = closer; + if (closer_ != nullptr) + closer->IncreaseExpectedCount(); + inspector_close(&socket_, CloseCallback_); + } else { + delete this; + } +} + +// static +void SocketSession::CloseCallback_(InspectorSocket* socket, int code) { + SocketSession* session = SocketSession::From(socket); + CHECK_EQ(State::kClosing, session->state_); + Closer* closer = session->closer_; + if (closer != nullptr) + closer->DecreaseExpectedCount(); + delete session; +} + +void SocketSession::FrontendConnected() { + CHECK_EQ(State::kHttp, state_); + state_ = State::kWebSocket; + inspector_read_start(&socket_, OnBufferAlloc, ReadCallback_); +} + +// static +void SocketSession::ReadCallback_(uv_stream_t* stream, ssize_t read, + const uv_buf_t* buf) { + InspectorSocket* socket = inspector_from_stream(stream); + SocketSession::From(socket)->OnRemoteDataIO(socket, read, buf); +} + +void SocketSession::OnRemoteDataIO(InspectorSocket* socket, ssize_t read, + const uv_buf_t* buf) { + if (read > 0) { + server_->Delegate()->MessageReceived(id_, std::string(buf->base, read)); + } else { + server_->SessionTerminated(id_); + Close(true, nullptr); + } + if (buf != nullptr && buf->base != nullptr) + delete[] buf->base; +} + +void SocketSession::Send(const std::string& message) { + inspector_write(&socket_, message.data(), message.length()); +} + +} // namespace inspector +} // namespace node diff --git a/src/inspector_socket_server.h b/src/inspector_socket_server.h new file mode 100644 index 00000000000000..4c139e138f7547 --- /dev/null +++ b/src/inspector_socket_server.h @@ -0,0 +1,77 @@ +#ifndef SRC_INSPECTOR_SOCKET_SERVER_H_ +#define SRC_INSPECTOR_SOCKET_SERVER_H_ + +#include "inspector_agent.h" +#include "inspector_socket.h" +#include "uv.h" + +#include +#include +#include + +#if !HAVE_INSPECTOR +#error("This header can only be used when inspector is enabled") +#endif + +namespace node { +namespace inspector { + +class Closer; +class SocketSession; + +class SocketServerDelegate { + public: + virtual bool StartSession(int session_id, const std::string& target_id) = 0; + virtual void EndSession(int session_id) = 0; + virtual void MessageReceived(int session_id, const std::string& message) = 0; + virtual std::vector GetTargetIds() = 0; + virtual std::string GetTargetTitle(const std::string& id) = 0; + virtual std::string GetTargetUrl(const std::string& id) = 0; +}; + +class InspectorSocketServer { + public: + using ServerCallback = void (*)(InspectorSocketServer*); + InspectorSocketServer(SocketServerDelegate* delegate, int port); + bool Start(uv_loop_t* loop); + void Stop(ServerCallback callback); + void Send(int session_id, const std::string& message); + void TerminateConnections(ServerCallback callback); + + private: + static bool HandshakeCallback(InspectorSocket* socket, + enum inspector_handshake_event state, + const std::string& path); + static void SocketConnectedCallback(uv_stream_t* server, int status); + static void ServerClosedCallback(uv_handle_t* server); + template + static InspectorSocketServer* From(SomeUvStruct* server) { + return node::ContainerOf(&InspectorSocketServer::server_, + reinterpret_cast(server)); + } + bool RespondToGet(InspectorSocket* socket, const std::string& path); + void SendListResponse(InspectorSocket* socket); + void ReadCallback(InspectorSocket* socket, ssize_t read, const uv_buf_t* buf); + bool SessionStarted(SocketSession* session, const std::string& id); + void SessionTerminated(int id); + bool TargetExists(const std::string& id); + static void SocketSessionDeleter(SocketSession*); + SocketServerDelegate* Delegate() { return delegate_; } + + uv_loop_t* loop_; + SocketServerDelegate* const delegate_; + const int port_; + std::string path_; + uv_tcp_t server_; + Closer* closer_; + std::map connected_sessions_; + int next_session_id_; + + friend class SocketSession; + friend class Closer; +}; + +} // namespace inspector +} // namespace node + +#endif // SRC_INSPECTOR_SOCKET_SERVER_H_ diff --git a/test/cctest/test_inspector_socket_server.cc b/test/cctest/test_inspector_socket_server.cc new file mode 100644 index 00000000000000..d253df5dd9c593 --- /dev/null +++ b/test/cctest/test_inspector_socket_server.cc @@ -0,0 +1,517 @@ +#include "inspector_socket_server.h" + +#include "node.h" +#include "gtest/gtest.h" + +#include +#include + +static const int PORT = 9229; +static uv_loop_t loop; + +static const char CLIENT_CLOSE_FRAME[] = "\x88\x80\x2D\x0E\x1E\xFA"; +static const char SERVER_CLOSE_FRAME[] = "\x88\x00"; + +static const char MAIN_TARGET_ID[] = "main-target"; +static const char UNCONNECTABLE_TARGET_ID[] = "unconnectable-target"; + +static const char WS_HANDSHAKE_RESPONSE[] = + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: Dt87H1OULVZnSJo/KgMUYI7xPCg=\r\n\r\n"; + +#define SPIN_WHILE(condition) \ + { \ + Timeout timeout(&loop); \ + while ((condition) && !timeout.timed_out) { \ + uv_run(&loop, UV_RUN_NOWAIT); \ + } \ + ASSERT_FALSE((condition)); \ + } + +namespace { + +using InspectorSocketServer = node::inspector::InspectorSocketServer; +using SocketServerDelegate = node::inspector::SocketServerDelegate; + +class Timeout { + public: + explicit Timeout(uv_loop_t* loop) : timed_out(false), done_(false) { + uv_timer_init(loop, &timer_); + uv_timer_start(&timer_, Timeout::set_flag, 5000, 0); + } + + ~Timeout() { + uv_timer_stop(&timer_); + uv_close(reinterpret_cast(&timer_), mark_done); + while (!done_) { + uv_run(&loop, UV_RUN_NOWAIT); + } + } + bool timed_out; + + private: + static void set_flag(uv_timer_t* timer) { + Timeout* t = node::ContainerOf(&Timeout::timer_, timer); + t->timed_out = true; + } + + static void mark_done(uv_handle_t* timer) { + Timeout* t = node::ContainerOf(&Timeout::timer_, + reinterpret_cast(timer)); + t->done_ = true; + } + + bool done_; + uv_timer_t timer_; +}; + +class InspectorSocketServerTest : public ::testing::Test { + protected: + void SetUp() override { + uv_loop_init(&loop); + } + + void TearDown() override { + const int err = uv_loop_close(&loop); + if (err != 0) { + uv_print_all_handles(&loop, stderr); + } + EXPECT_EQ(0, err); + } +}; + +class TestInspectorServerDelegate : public SocketServerDelegate { + public: + TestInspectorServerDelegate() : connected(0), disconnected(0), + targets_({ MAIN_TARGET_ID, + UNCONNECTABLE_TARGET_ID }) {} + + void Connect(InspectorSocketServer* server) { + server_ = server; + } + + bool StartSession(int session_id, const std::string& target_id) override { + buffer_.clear(); + CHECK_NE(targets_.end(), + std::find(targets_.begin(), targets_.end(), target_id)); + if (target_id == UNCONNECTABLE_TARGET_ID) { + return false; + } + connected++; + session_id_ = session_id; + return true; + } + + void MessageReceived(int session_id, const std::string& message) override { + ASSERT_EQ(session_id_, session_id); + buffer_.insert(buffer_.end(), message.begin(), message.end()); + } + + void EndSession(int session_id) override { + ASSERT_EQ(session_id_, session_id); + disconnected++; + } + + std::vector GetTargetIds() override { + return targets_; + } + + std::string GetTargetTitle(const std::string& id) override { + return id + " Target Title"; + } + + std::string GetTargetUrl(const std::string& id) override { + return "file://" + id + "/script.js"; + } + + void Expect(const std::string& expects) { + SPIN_WHILE(buffer_.size() < expects.length()); + ASSERT_STREQ(std::string(buffer_.data(), expects.length()).c_str(), + expects.c_str()); + buffer_.erase(buffer_.begin(), buffer_.begin() + expects.length()); + } + + void Write(const std::string& message) { + server_->Send(session_id_, message); + } + + int connected; + int disconnected; + + private: + const std::vector targets_; + InspectorSocketServer* server_; + int session_id_; + std::vector buffer_; +}; + +class SocketWrapper { + public: + explicit SocketWrapper(uv_loop_t* loop) : closed_(false), + eof_(false), + loop_(loop), + connected_(false), + sending_(false) { } + + void Connect(std::string host, int port) { + closed_ = false; + connection_failed_ = false; + connected_ = false; + eof_ = false; + contents_.clear(); + uv_tcp_init(loop_, &socket_); + sockaddr_in addr; + uv_ip4_addr(host.c_str(), PORT, &addr); + int err = uv_tcp_connect(&connect_, &socket_, + reinterpret_cast(&addr), + Connected_); + ASSERT_EQ(0, err); + SPIN_WHILE(!connected_) + uv_read_start(reinterpret_cast(&socket_), AllocCallback, + ReadCallback); + } + + void ExpectFailureToConnect(std::string host, int port) { + connected_ = false; + connection_failed_ = false; + closed_ = false; + eof_ = false; + contents_.clear(); + uv_tcp_init(loop_, &socket_); + sockaddr_in addr; + uv_ip4_addr(host.c_str(), PORT, &addr); + int err = uv_tcp_connect(&connect_, &socket_, + reinterpret_cast(&addr), + ConnectionMustFail_); + ASSERT_EQ(0, err); + SPIN_WHILE(!connection_failed_) + uv_read_start(reinterpret_cast(&socket_), AllocCallback, + ReadCallback); + } + + void Close() { + uv_close(reinterpret_cast(&socket_), ClosedCallback); + SPIN_WHILE(!closed_); + } + + void Expect(const std::string& expects) { + SPIN_WHILE(contents_.size() < expects.length()); + ASSERT_STREQ(expects.c_str(), + std::string(contents_.data(), expects.length()).c_str()); + contents_.erase(contents_.begin(), contents_.begin() + expects.length()); + } + + void ExpectEOF() { + SPIN_WHILE(!eof_); + Close(); + } + + void TestHttpRequest(const std::string& path, + const std::string& expected_reply) { + std::ostringstream expectations; + expectations << "HTTP/1.0 200 OK\r\n" + "Content-Type: application/json; charset=UTF-8\r\n" + "Cache-Control: no-cache\r\n" + "Content-Length: "; + expectations << expected_reply.length() + 2; + expectations << "\r\n\r\n" << expected_reply << "\n\n"; + Write("GET " + path + " HTTP/1.1\r\n" + "Host: localhost:9229\r\n\r\n"); + Expect(expectations.str()); + } + + void Write(const std::string& data) { + ASSERT_FALSE(sending_); + uv_buf_t buf[1]; + buf[0].base = const_cast(data.data()); + buf[0].len = data.length(); + sending_ = true; + int err = uv_write(&write_, reinterpret_cast(&socket_), + buf, 1, WriteDone_); + ASSERT_EQ(err, 0); + SPIN_WHILE(sending_); + } + + private: + static void AllocCallback(uv_handle_t*, size_t size, uv_buf_t* buf) { + *buf = uv_buf_init(new char[size], size); + } + + static void ClosedCallback(uv_handle_t* handle) { + SocketWrapper* wrapper = + node::ContainerOf(&SocketWrapper::socket_, + reinterpret_cast(handle)); + ASSERT_FALSE(wrapper->closed_); + wrapper->closed_ = true; + } + + static void Connected_(uv_connect_t* connect, int status) { + EXPECT_EQ(0, status); + SocketWrapper* wrapper = + node::ContainerOf(&SocketWrapper::connect_, connect); + wrapper->connected_ = true; + } + + static void ConnectionMustFail_(uv_connect_t* connect, int status) { + EXPECT_EQ(UV_ECONNREFUSED, status); + SocketWrapper* wrapper = + node::ContainerOf(&SocketWrapper::connect_, connect); + wrapper->connection_failed_ = true; + } + + static void ReadCallback(uv_stream_t* stream, ssize_t read, + const uv_buf_t* buf) { + SocketWrapper* wrapper = + node::ContainerOf(&SocketWrapper::socket_, + reinterpret_cast(stream)); + if (read == UV_EOF) { + wrapper->eof_ = true; + } else { + wrapper->contents_.insert(wrapper->contents_.end(), buf->base, + buf->base + read); + } + delete[] buf->base; + } + static void WriteDone_(uv_write_t* req, int err) { + ASSERT_EQ(0, err); + SocketWrapper* wrapper = + node::ContainerOf(&SocketWrapper::write_, req); + ASSERT_TRUE(wrapper->sending_); + wrapper->sending_ = false; + } + bool IsConnected() { return connected_; } + + bool closed_; + bool eof_; + uv_loop_t* loop_; + uv_tcp_t socket_; + uv_connect_t connect_; + uv_write_t write_; + bool connected_; + bool connection_failed_; + bool sending_; + std::vector contents_; +}; + +class ServerHolder { + public: + template + ServerHolder(Delegate* delegate, int port) + : closed(false), paused(false), sessions_terminated(false), + server_(delegate, port) { + delegate->Connect(&server_); + } + + InspectorSocketServer* operator->() { + return &server_; + } + + static void CloseCallback(InspectorSocketServer* server) { + ServerHolder* holder = node::ContainerOf(&ServerHolder::server_, server); + holder->closed = true; + } + + static void ConnectionsTerminated(InspectorSocketServer* server) { + ServerHolder* holder = node::ContainerOf(&ServerHolder::server_, server); + holder->sessions_terminated = true; + } + + static void PausedCallback(InspectorSocketServer* server) { + ServerHolder* holder = node::ContainerOf(&ServerHolder::server_, server); + holder->paused = true; + } + + bool closed; + bool paused; + bool sessions_terminated; + + private: + InspectorSocketServer server_; +}; + +class ServerDelegateNoTargets : public SocketServerDelegate { + public: + void Connect(InspectorSocketServer* server) { } + void MessageReceived(int session_id, const std::string& message) override { } + void EndSession(int session_id) override { } + + bool StartSession(int session_id, const std::string& target_id) override { + return false; + } + + std::vector GetTargetIds() override { + return std::vector(); + } + + std::string GetTargetTitle(const std::string& id) override { + return ""; + } + + std::string GetTargetUrl(const std::string& id) override { + return ""; + } +}; + +static void TestHttpRequest(int port, const std::string& path, + const std::string& expected_body) { + SocketWrapper socket(&loop); + socket.Connect("0.0.0.0", port); + socket.TestHttpRequest(path, expected_body); + socket.Close(); +} + +static const std::string WsHandshakeRequest(const std::string& target_id) { + return "GET /" + target_id + " HTTP/1.1\r\n" + "Host: localhost:9229\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key: aaa==\r\n" + "Sec-WebSocket-Version: 13\r\n\r\n"; +} +} // namespace + + +TEST_F(InspectorSocketServerTest, InspectorSessions) { + TestInspectorServerDelegate delegate; + ServerHolder server(&delegate, PORT); + ASSERT_TRUE(server->Start(&loop)); + + SocketWrapper well_behaved_socket(&loop); + // Regular connection + well_behaved_socket.Connect("0.0.0.0", PORT); + well_behaved_socket.Write(WsHandshakeRequest(MAIN_TARGET_ID)); + well_behaved_socket.Expect(WS_HANDSHAKE_RESPONSE); + + + EXPECT_EQ(1, delegate.connected); + + well_behaved_socket.Write("\x81\x84\x7F\xC2\x66\x31\x4E\xF0\x55\x05"); + + delegate.Expect("1234"); + delegate.Write("5678"); + + well_behaved_socket.Expect("\x81\x4" "5678"); + + well_behaved_socket.Write(CLIENT_CLOSE_FRAME); + well_behaved_socket.Expect(SERVER_CLOSE_FRAME); + + EXPECT_EQ(1, delegate.disconnected); + + well_behaved_socket.Close(); + + // Declined connection + SocketWrapper declined_target_socket(&loop); + declined_target_socket.Connect("127.0.0.1", PORT); + declined_target_socket.Write(WsHandshakeRequest(UNCONNECTABLE_TARGET_ID)); + declined_target_socket.Expect("HTTP/1.0 400 Bad Request"); + declined_target_socket.ExpectEOF(); + EXPECT_EQ(1, delegate.connected); + EXPECT_EQ(1, delegate.disconnected); + + // Bogus target - start session callback should not even be invoked + SocketWrapper bogus_target_socket(&loop); + bogus_target_socket.Connect("127.0.0.1", PORT); + bogus_target_socket.Write(WsHandshakeRequest("bogus_target")); + bogus_target_socket.Expect("HTTP/1.0 400 Bad Request"); + bogus_target_socket.ExpectEOF(); + EXPECT_EQ(1, delegate.connected); + EXPECT_EQ(1, delegate.disconnected); + + // Drop connection (no proper close frames) + SocketWrapper dropped_connection_socket(&loop); + dropped_connection_socket.Connect("127.0.0.1", PORT); + dropped_connection_socket.Write(WsHandshakeRequest(MAIN_TARGET_ID)); + dropped_connection_socket.Expect(WS_HANDSHAKE_RESPONSE); + + EXPECT_EQ(2, delegate.connected); + + delegate.Write("5678"); + dropped_connection_socket.Expect("\x81\x4" "5678"); + + dropped_connection_socket.Close(); + SPIN_WHILE(delegate.disconnected < 2); + + // Reconnect regular connection + SocketWrapper stays_till_termination_socket(&loop); + stays_till_termination_socket.Connect("127.0.0.1", PORT); + stays_till_termination_socket.Write(WsHandshakeRequest(MAIN_TARGET_ID)); + stays_till_termination_socket.Expect(WS_HANDSHAKE_RESPONSE); + + EXPECT_EQ(3, delegate.connected); + + delegate.Write("5678"); + stays_till_termination_socket.Expect("\x81\x4" "5678"); + + stays_till_termination_socket + .Write("\x81\x84\x7F\xC2\x66\x31\x4E\xF0\x55\x05"); + delegate.Expect("1234"); + + server->Stop(ServerHolder::CloseCallback); + server->TerminateConnections(ServerHolder::ConnectionsTerminated); + + stays_till_termination_socket.Write(CLIENT_CLOSE_FRAME); + stays_till_termination_socket.Expect(SERVER_CLOSE_FRAME); + + EXPECT_EQ(3, delegate.disconnected); + + SPIN_WHILE(!server.closed); + stays_till_termination_socket.ExpectEOF(); +} + +TEST_F(InspectorSocketServerTest, ServerDoesNothing) { + TestInspectorServerDelegate delegate; + ServerHolder server(&delegate, PORT); + ASSERT_TRUE(server->Start(&loop)); + + server->Stop(ServerHolder::CloseCallback); + server->TerminateConnections(ServerHolder::ConnectionsTerminated); + SPIN_WHILE(!server.closed); +} + +TEST_F(InspectorSocketServerTest, ServerWithoutTargets) { + ServerDelegateNoTargets delegate; + ServerHolder server(&delegate, PORT); + ASSERT_TRUE(server->Start(&loop)); + TestHttpRequest(PORT, "/json/list", "[ ]"); + TestHttpRequest(PORT, "/json", "[ ]"); + + // Declined connection + SocketWrapper socket(&loop); + socket.Connect("0.0.0.0", PORT); + socket.Write(WsHandshakeRequest(UNCONNECTABLE_TARGET_ID)); + socket.Expect("HTTP/1.0 400 Bad Request"); + socket.ExpectEOF(); + server->Stop(ServerHolder::CloseCallback); + server->TerminateConnections(ServerHolder::ConnectionsTerminated); + SPIN_WHILE(!server.closed); +} + +TEST_F(InspectorSocketServerTest, ServerCannotStart) { + ServerDelegateNoTargets delegate1, delegate2; + ServerHolder server1(&delegate1, PORT); + ASSERT_TRUE(server1->Start(&loop)); + ServerHolder server2(&delegate2, PORT); + ASSERT_FALSE(server2->Start(&loop)); + server1->Stop(ServerHolder::CloseCallback); + server1->TerminateConnections(ServerHolder::ConnectionsTerminated); + server2->Stop(ServerHolder::CloseCallback); + server2->TerminateConnections(ServerHolder::ConnectionsTerminated); + SPIN_WHILE(!server1.closed); + SPIN_WHILE(!server2.closed); +} + +TEST_F(InspectorSocketServerTest, StoppingServerDoesNotKillConnections) { + ServerDelegateNoTargets delegate; + ServerHolder server(&delegate, PORT); + ASSERT_TRUE(server->Start(&loop)); + SocketWrapper socket1(&loop); + socket1.Connect("0.0.0.0", PORT); + socket1.TestHttpRequest("/json/list", "[ ]"); + server->Stop(ServerHolder::CloseCallback); + SPIN_WHILE(!server.closed); + socket1.TestHttpRequest("/json/list", "[ ]"); + socket1.Close(); + uv_run(&loop, UV_RUN_DEFAULT); +}