mirror of https://github.com/lukechilds/node.git
Browse Source
Both our team experiments and some embedder request indicate a potential in implementing alternative transport for inspector - e.g. IPC pipes or custom embedder APIs. This change moves all HTTP specific code into a separate class and is a first attempt at defining a boundary between the inspector agent and transport. This API will be refined as new transports are implemented. Note that even without considering alternative transports, this change enables better testing of the HTTP server (Valgrind made it possible to identify and fix some existing memory leaks). PR-URL: https://github.com/nodejs/node/pull/9630 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>v7.x
Eugene Ostroukhov
8 years ago
committed by
Evan Lucas
6 changed files with 1212 additions and 301 deletions
@ -0,0 +1,471 @@ |
|||
#include "inspector_socket_server.h" |
|||
|
|||
#include "node.h" |
|||
#include "uv.h" |
|||
#include "zlib.h" |
|||
|
|||
#include <algorithm> |
|||
#include <map> |
|||
#include <set> |
|||
#include <sstream> |
|||
|
|||
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<std::string, std::string>& 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<std::map<std::string, std::string>>& 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<std::string>& 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<std::string, std::string> 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<uint8_t*>(PROTOCOL_JSON + 3); |
|||
strm.avail_in = sizeof(PROTOCOL_JSON) - 3; |
|||
std::string data(kDecompressedSize, '\0'); |
|||
strm.next_out = reinterpret_cast<Byte*>(&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<InspectorSocketServer::ServerCallback> 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<uv_handle_t*>(&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<std::map<std::string, std::string>> response; |
|||
for (const std::string& id : delegate_->GetTargetIds()) { |
|||
response.push_back(std::map<std::string, std::string>()); |
|||
std::map<std::string, std::string>& 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<const struct sockaddr*>(&addr), 0); |
|||
if (err == 0) { |
|||
err = uv_listen(reinterpret_cast<uv_stream_t*>(&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<uv_handle_t*>(&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<uv_handle_t*>(&server_); |
|||
if (uv_is_active(handle)) { |
|||
closer_->IncreaseExpectedCount(); |
|||
uv_close(reinterpret_cast<uv_handle_t*>(&server_), ServerClosedCallback); |
|||
} |
|||
closer_->NotifyIfDone(); |
|||
} |
|||
|
|||
void InspectorSocketServer::TerminateConnections(ServerCallback cb) { |
|||
if (closer_ == nullptr) { |
|||
closer_ = new Closer(this); |
|||
} |
|||
closer_->AddCallback(cb); |
|||
std::map<int, SocketSession*> 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<std::string>& 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
|
@ -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 <map> |
|||
#include <string> |
|||
#include <vector> |
|||
|
|||
#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<std::string> 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<typename SomeUvStruct> |
|||
static InspectorSocketServer* From(SomeUvStruct* server) { |
|||
return node::ContainerOf(&InspectorSocketServer::server_, |
|||
reinterpret_cast<uv_tcp_t*>(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<int, SocketSession*> connected_sessions_; |
|||
int next_session_id_; |
|||
|
|||
friend class SocketSession; |
|||
friend class Closer; |
|||
}; |
|||
|
|||
} // namespace inspector
|
|||
} // namespace node
|
|||
|
|||
#endif // SRC_INSPECTOR_SOCKET_SERVER_H_
|
@ -0,0 +1,517 @@ |
|||
#include "inspector_socket_server.h" |
|||
|
|||
#include "node.h" |
|||
#include "gtest/gtest.h" |
|||
|
|||
#include <algorithm> |
|||
#include <sstream> |
|||
|
|||
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<uv_handle_t*>(&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<uv_timer_t*>(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<std::string> 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<std::string> targets_; |
|||
InspectorSocketServer* server_; |
|||
int session_id_; |
|||
std::vector<char> 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<const sockaddr*>(&addr), |
|||
Connected_); |
|||
ASSERT_EQ(0, err); |
|||
SPIN_WHILE(!connected_) |
|||
uv_read_start(reinterpret_cast<uv_stream_t*>(&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<const sockaddr*>(&addr), |
|||
ConnectionMustFail_); |
|||
ASSERT_EQ(0, err); |
|||
SPIN_WHILE(!connection_failed_) |
|||
uv_read_start(reinterpret_cast<uv_stream_t*>(&socket_), AllocCallback, |
|||
ReadCallback); |
|||
} |
|||
|
|||
void Close() { |
|||
uv_close(reinterpret_cast<uv_handle_t*>(&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<char*>(data.data()); |
|||
buf[0].len = data.length(); |
|||
sending_ = true; |
|||
int err = uv_write(&write_, reinterpret_cast<uv_stream_t*>(&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<uv_tcp_t*>(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<uv_tcp_t*>(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<char> contents_; |
|||
}; |
|||
|
|||
class ServerHolder { |
|||
public: |
|||
template <typename Delegate> |
|||
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<std::string> GetTargetIds() override { |
|||
return std::vector<std::string>(); |
|||
} |
|||
|
|||
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); |
|||
} |
Loading…
Reference in new issue