Index: components/devtools_service/devtools_http_server.cc |
diff --git a/components/devtools_service/devtools_http_server.cc b/components/devtools_service/devtools_http_server.cc |
index 7199f2bc50238991904f48325ff258f458c6651a..ef17bcd4009afee9fb1bf8f5dd5f821a6f6ea2d2 100644 |
--- a/components/devtools_service/devtools_http_server.cc |
+++ b/components/devtools_service/devtools_http_server.cc |
@@ -4,17 +4,282 @@ |
#include "components/devtools_service/devtools_http_server.h" |
+#include <string.h> |
+ |
+#include <string> |
+ |
+#include "base/bind.h" |
+#include "base/json/json_writer.h" |
#include "base/logging.h" |
#include "base/stl_util.h" |
#include "base/strings/stringprintf.h" |
+#include "base/values.h" |
+#include "components/devtools_service/devtools_agent_host.h" |
+#include "components/devtools_service/devtools_registry_impl.h" |
#include "components/devtools_service/devtools_service.h" |
#include "mojo/application/public/cpp/application_impl.h" |
-#include "mojo/services/network/public/interfaces/http_message.mojom.h" |
+#include "mojo/services/network/public/cpp/web_socket_read_queue.h" |
+#include "mojo/services/network/public/cpp/web_socket_write_queue.h" |
#include "mojo/services/network/public/interfaces/net_address.mojom.h" |
#include "mojo/services/network/public/interfaces/network_service.mojom.h" |
+#include "mojo/services/network/public/interfaces/web_socket.mojom.h" |
+#include "third_party/mojo/src/mojo/public/cpp/system/data_pipe.h" |
namespace devtools_service { |
+namespace { |
+ |
+const char kPageUrlPrefix[] = "/devtools/page/"; |
+const char kBrowserUrlPrefix[] = "/devtools/browser"; |
+const char kJsonRequestUrlPrefix[] = "/json"; |
+ |
+const char kActivateCommand[] = "activate"; |
+const char kCloseCommand[] = "close"; |
+const char kListCommand[] = "list"; |
+const char kNewCommand[] = "new"; |
+const char kVersionCommand[] = "version"; |
+ |
+const char kTargetIdField[] = "id"; |
+const char kTargetTypeField[] = "type"; |
+const char kTargetTitleField[] = "title"; |
+const char kTargetDescriptionField[] = "description"; |
+const char kTargetUrlField[] = "url"; |
+const char kTargetWebSocketDebuggerUrlField[] = "webSocketDebuggerUrl"; |
+const char kTargetDevtoolsFrontendUrlField[] = "devtoolsFrontendUrl"; |
+ |
+bool ParseJsonPath(const std::string& path, |
+ std::string* command, |
+ std::string* target_id) { |
+ // Fall back to list in case of empty query. |
+ if (path.empty()) { |
+ *command = kListCommand; |
+ return true; |
+ } |
+ |
+ if (path.find("/") != 0) { |
+ // Malformed command. |
+ return false; |
+ } |
+ *command = path.substr(1); |
+ |
+ size_t separator_pos = command->find("/"); |
+ if (separator_pos != std::string::npos) { |
+ *target_id = command->substr(separator_pos + 1); |
+ *command = command->substr(0, separator_pos); |
+ } |
+ return true; |
+} |
+ |
+mojo::HttpResponsePtr MakeResponse(uint32_t status_code, |
+ const std::string& content_type, |
+ const std::string& body) { |
+ mojo::HttpResponsePtr response(mojo::HttpResponse::New()); |
+ response->headers.resize(2); |
+ response->headers[0] = mojo::HttpHeader::New(); |
+ response->headers[0]->name = "Content-Length"; |
+ response->headers[0]->value = |
+ base::StringPrintf("%lu", static_cast<unsigned long>(body.size())); |
+ response->headers[1] = mojo::HttpHeader::New(); |
+ response->headers[1]->name = "Content-Type"; |
+ response->headers[1]->value = content_type; |
+ |
+ if (!body.empty()) { |
+ uint32_t num_bytes = static_cast<uint32_t>(body.size()); |
+ MojoCreateDataPipeOptions options; |
+ options.struct_size = sizeof(MojoCreateDataPipeOptions); |
+ options.flags = MOJO_CREATE_DATA_PIPE_OPTIONS_FLAG_NONE; |
+ options.element_num_bytes = 1; |
+ options.capacity_num_bytes = num_bytes; |
+ mojo::DataPipe data_pipe(options); |
+ response->body = data_pipe.consumer_handle.Pass(); |
+ MojoResult result = |
+ WriteDataRaw(data_pipe.producer_handle.get(), body.data(), &num_bytes, |
+ MOJO_WRITE_DATA_FLAG_ALL_OR_NONE); |
+ CHECK_EQ(MOJO_RESULT_OK, result); |
+ } |
+ return response.Pass(); |
+} |
+ |
+mojo::HttpResponsePtr MakeJsonResponse(uint32_t status_code, |
+ base::Value* value, |
+ const std::string& message) { |
+ // Serialize value and message. |
+ std::string json_value; |
+ if (value) { |
+ base::JSONWriter::WriteWithOptions( |
+ *value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json_value); |
+ } |
+ |
+ return MakeResponse(status_code, "application/json; charset=UTF-8", |
+ json_value + message); |
+} |
+ |
+class WebSocketRelayer : public DevToolsAgentHost::Delegate, |
+ public mojo::WebSocketClient, |
+ public mojo::ErrorHandler { |
+ public: |
+ // Creates a WebSocketRelayer instance and sets it as the delegate of |
+ // |agent_host|. |
+ // |
+ // The object destroys itself when either of the following happens: |
+ // - |agent_host| is dead and the object finishes all pending sends (if any) |
+ // to the Web socket; or |
+ // - the underlying pipe of |web_socket| is closed and the object finishes all |
+ // pending receives (if any) from the Web socket. |
+ static mojo::WebSocketClientPtr SetUp( |
+ DevToolsAgentHost* agent_host, |
+ mojo::WebSocketPtr web_socket, |
+ mojo::ScopedDataPipeProducerHandle send_stream) { |
+ DCHECK(agent_host); |
+ DCHECK(web_socket); |
+ DCHECK(send_stream.is_valid()); |
+ |
+ mojo::WebSocketClientPtr web_socket_client; |
+ new WebSocketRelayer(agent_host, web_socket.Pass(), send_stream.Pass(), |
+ &web_socket_client); |
+ return web_socket_client.Pass(); |
+ } |
+ |
+ private: |
+ WebSocketRelayer(DevToolsAgentHost* agent_host, |
+ mojo::WebSocketPtr web_socket, |
+ mojo::ScopedDataPipeProducerHandle send_stream, |
+ mojo::WebSocketClientPtr* web_socket_client) |
+ : agent_host_(agent_host), |
+ binding_(this, web_socket_client), |
+ web_socket_(web_socket.Pass()), |
+ send_stream_(send_stream.Pass()), |
+ write_send_stream_(new mojo::WebSocketWriteQueue(send_stream_.get())), |
+ pending_send_count_(0), |
+ pending_receive_count_(0) { |
+ web_socket_.set_error_handler(this); |
+ agent_host->SetDelegate(this); |
+ } |
+ |
+ ~WebSocketRelayer() override { |
+ if (agent_host_) |
+ agent_host_->SetDelegate(nullptr); |
+ } |
+ |
+ // DevToolsAgentHost::Delegate implementation. |
+ void DispatchProtocolMessage(DevToolsAgentHost* agent_host, |
+ const std::string& message) override { |
+ if (!web_socket_) |
+ return; |
+ |
+ // TODO(yzshen): It shouldn't be an issue to pass an empty message. However, |
+ // WebSocket{Read,Write}Queue doesn't handle that correctly. |
+ if (message.empty()) |
+ return; |
+ |
+ pending_send_count_++; |
+ uint32_t size = static_cast<uint32_t>(message.size()); |
+ write_send_stream_->Write( |
+ &message[0], size, |
+ base::Bind(&WebSocketRelayer::OnFinishedWritingSendStream, |
+ base::Unretained(this), size)); |
+ } |
+ |
+ void OnAgentHostClosed(DevToolsAgentHost* agent_host) override { |
+ DispatchProtocolMessage(agent_host_, |
+ "{ \"method\": \"Inspector.detached\", " |
+ "\"params\": { \"reason\": \"target_closed\" } }"); |
+ |
+ // No need to call SetDelegate(nullptr) on |agent_host_| because it is going |
+ // away. |
+ agent_host_ = nullptr; |
+ |
+ if (ShouldSelfDestruct()) |
+ delete this; |
+ } |
+ |
+ // WebSocketClient implementation. |
+ void DidConnect(const mojo::String& selected_subprotocol, |
+ const mojo::String& extensions, |
+ mojo::ScopedDataPipeConsumerHandle receive_stream) override { |
+ receive_stream_ = receive_stream.Pass(); |
+ read_receive_stream_.reset( |
+ new mojo::WebSocketReadQueue(receive_stream_.get())); |
+ } |
+ |
+ void DidReceiveData(bool fin, |
+ mojo::WebSocket::MessageType type, |
+ uint32_t num_bytes) override { |
+ if (!agent_host_) |
+ return; |
+ |
+ // TODO(yzshen): It shouldn't be an issue to pass an empty message. However, |
+ // WebSocket{Read,Write}Queue doesn't handle that correctly. |
+ if (num_bytes == 0) |
+ return; |
+ |
+ pending_receive_count_++; |
+ read_receive_stream_->Read( |
+ num_bytes, base::Bind(&WebSocketRelayer::OnFinishedReadingReceiveStream, |
+ base::Unretained(this), num_bytes)); |
+ } |
+ |
+ void DidReceiveFlowControl(int64_t quota) override {} |
+ |
+ void DidFail(const mojo::String& message) override {} |
+ |
+ void DidClose(bool was_clean, |
+ uint16_t code, |
+ const mojo::String& reason) override {} |
+ |
+ // mojo::ErrorHandler implementation. |
+ void OnConnectionError() override { |
+ web_socket_ = nullptr; |
+ binding_.Close(); |
+ |
+ if (ShouldSelfDestruct()) |
+ delete this; |
+ } |
+ |
+ void OnFinishedWritingSendStream(uint32_t num_bytes, const char* buffer) { |
+ DCHECK_GT(pending_send_count_, 0u); |
+ pending_send_count_--; |
+ |
+ if (web_socket_ && buffer) |
+ web_socket_->Send(true, mojo::WebSocket::MESSAGE_TYPE_TEXT, num_bytes); |
+ |
+ if (ShouldSelfDestruct()) |
+ delete this; |
+ } |
+ |
+ void OnFinishedReadingReceiveStream(uint32_t num_bytes, const char* data) { |
+ DCHECK_GT(pending_receive_count_, 0u); |
+ pending_receive_count_--; |
+ |
+ if (agent_host_ && data) |
+ agent_host_->SendProtocolMessageToAgent(std::string(data, num_bytes)); |
+ |
+ if (ShouldSelfDestruct()) |
+ delete this; |
+ } |
+ |
+ bool ShouldSelfDestruct() const { |
+ return (!agent_host_ && pending_send_count_ == 0) || |
+ (!web_socket_ && pending_receive_count_ == 0); |
+ } |
+ |
+ DevToolsAgentHost* agent_host_; |
+ mojo::Binding<WebSocketClient> binding_; |
+ mojo::WebSocketPtr web_socket_; |
+ |
+ mojo::ScopedDataPipeProducerHandle send_stream_; |
+ scoped_ptr<mojo::WebSocketWriteQueue> write_send_stream_; |
+ size_t pending_send_count_; |
+ |
+ mojo::ScopedDataPipeConsumerHandle receive_stream_; |
+ scoped_ptr<mojo::WebSocketReadQueue> read_receive_stream_; |
+ size_t pending_receive_count_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(WebSocketRelayer); |
+}; |
+ |
+} // namespace |
+ |
class DevToolsHttpServer::HttpConnectionDelegateImpl |
: public mojo::HttpConnectionDelegate, |
public mojo::ErrorHandler { |
@@ -61,7 +326,7 @@ class DevToolsHttpServer::HttpConnectionDelegateImpl |
DevToolsHttpServer::DevToolsHttpServer(DevToolsService* service, |
uint16_t remote_debugging_port) |
- : service_(service) { |
+ : service_(service), remote_debugging_port_(remote_debugging_port) { |
VLOG(1) << "Remote debugging HTTP server is started on port " |
<< remote_debugging_port << "."; |
mojo::NetworkServicePtr network_service; |
@@ -104,30 +369,13 @@ void DevToolsHttpServer::OnReceivedRequest( |
const OnReceivedRequestCallback& callback) { |
DCHECK(connections_.find(connection) != connections_.end()); |
- // TODO(yzshen): Implement it. |
- static const char kNotImplemented[] = "Not implemented yet!"; |
- mojo::HttpResponsePtr response(mojo::HttpResponse::New()); |
- response->headers.resize(2); |
- response->headers[0] = mojo::HttpHeader::New(); |
- response->headers[0]->name = "Content-Length"; |
- response->headers[0]->value = base::StringPrintf( |
- "%lu", static_cast<unsigned long>(sizeof(kNotImplemented))); |
- response->headers[1] = mojo::HttpHeader::New(); |
- response->headers[1]->name = "Content-Type"; |
- response->headers[1]->value = "text/html"; |
- |
- uint32_t num_bytes = sizeof(kNotImplemented); |
- MojoCreateDataPipeOptions options; |
- options.struct_size = sizeof(MojoCreateDataPipeOptions); |
- options.flags = MOJO_CREATE_DATA_PIPE_OPTIONS_FLAG_NONE; |
- options.element_num_bytes = 1; |
- options.capacity_num_bytes = num_bytes; |
- mojo::DataPipe data_pipe(options); |
- response->body = data_pipe.consumer_handle.Pass(); |
- WriteDataRaw(data_pipe.producer_handle.get(), kNotImplemented, &num_bytes, |
- MOJO_WRITE_DATA_FLAG_ALL_OR_NONE); |
- |
- callback.Run(response.Pass()); |
+ if (request->url.get().find(kJsonRequestUrlPrefix) == 0) { |
+ callback.Run(ProcessJsonRequest(request.Pass())); |
+ } else { |
+ // TODO(yzshen): Implement it. |
+ NOTIMPLEMENTED(); |
+ callback.Run(MakeResponse(404, "text/html", "Not implemented yet!")); |
+ } |
} |
void DevToolsHttpServer::OnReceivedWebSocketRequest( |
@@ -136,8 +384,36 @@ void DevToolsHttpServer::OnReceivedWebSocketRequest( |
const OnReceivedWebSocketRequestCallback& callback) { |
DCHECK(connections_.find(connection) != connections_.end()); |
- // TODO(yzshen): Implement it. |
- NOTIMPLEMENTED(); |
+ std::string path = request->url; |
+ size_t browser_pos = path.find(kBrowserUrlPrefix); |
+ if (browser_pos == 0) { |
+ // TODO(yzshen): Implement it. |
+ NOTIMPLEMENTED(); |
+ callback.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr); |
+ return; |
+ } |
+ |
+ size_t pos = path.find(kPageUrlPrefix); |
+ if (pos != 0) { |
+ callback.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr); |
+ return; |
+ } |
+ |
+ std::string target_id = path.substr(strlen(kPageUrlPrefix)); |
+ DevToolsAgentHost* agent = service_->registry()->GetAgentById(target_id); |
+ if (!agent || agent->IsAttached()) { |
+ callback.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr); |
+ return; |
+ } |
+ |
+ mojo::WebSocketPtr web_socket; |
+ mojo::InterfaceRequest<mojo::WebSocket> web_socket_request = |
+ mojo::GetProxy(&web_socket); |
+ mojo::DataPipe data_pipe; |
+ mojo::WebSocketClientPtr web_socket_client = WebSocketRelayer::SetUp( |
+ agent, web_socket.Pass(), data_pipe.producer_handle.Pass()); |
+ callback.Run(web_socket_request.Pass(), data_pipe.consumer_handle.Pass(), |
+ web_socket_client.Pass()); |
} |
void DevToolsHttpServer::OnConnectionClosed( |
@@ -148,4 +424,58 @@ void DevToolsHttpServer::OnConnectionClosed( |
connections_.erase(connection); |
} |
+mojo::HttpResponsePtr DevToolsHttpServer::ProcessJsonRequest( |
+ mojo::HttpRequestPtr request) { |
+ // Trim "/json". |
+ std::string path = request->url.get().substr(strlen(kJsonRequestUrlPrefix)); |
+ |
+ // Trim query. |
+ size_t query_pos = path.find("?"); |
+ if (query_pos != std::string::npos) |
+ path = path.substr(0, query_pos); |
+ |
+ // Trim fragment. |
+ size_t fragment_pos = path.find("#"); |
+ if (fragment_pos != std::string::npos) |
+ path = path.substr(0, fragment_pos); |
+ |
+ std::string command; |
+ std::string target_id; |
+ if (!ParseJsonPath(path, &command, &target_id)) |
+ return MakeJsonResponse(404, nullptr, |
+ "Malformed query: " + request->url.get()); |
+ |
+ if (command == kVersionCommand || command == kNewCommand || |
+ command == kActivateCommand || command == kCloseCommand) { |
+ NOTIMPLEMENTED(); |
+ return MakeJsonResponse(404, nullptr, |
+ "Not implemented yet: " + request->url.get()); |
+ } |
+ |
+ if (command == kListCommand) { |
+ base::ListValue list_value; |
+ for (DevToolsRegistryImpl::Iterator iter(service_->registry()); |
+ !iter.IsAtEnd(); iter.Advance()) { |
+ scoped_ptr<base::DictionaryValue> dict_value(new base::DictionaryValue()); |
+ |
+ // TODO(yzshen): Add more information. |
+ dict_value->SetString(kTargetDescriptionField, std::string()); |
+ dict_value->SetString(kTargetDevtoolsFrontendUrlField, std::string()); |
+ dict_value->SetString(kTargetIdField, iter.value()->id()); |
+ dict_value->SetString(kTargetTitleField, std::string()); |
+ dict_value->SetString(kTargetTypeField, "page"); |
+ dict_value->SetString(kTargetUrlField, std::string()); |
+ dict_value->SetString( |
+ kTargetWebSocketDebuggerUrlField, |
+ base::StringPrintf("ws://127.0.0.1:%u%s%s", |
+ static_cast<unsigned>(remote_debugging_port_), |
+ kPageUrlPrefix, iter.value()->id().c_str())); |
+ list_value.Append(dict_value.Pass()); |
+ } |
+ return MakeJsonResponse(200, &list_value, std::string()); |
+ } |
+ |
+ return MakeJsonResponse(404, nullptr, "Unknown command: " + command); |
+} |
+ |
} // namespace devtools_service |