OLD | NEW |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "components/devtools_service/devtools_http_server.h" | 5 #include "components/devtools_service/devtools_http_server.h" |
6 | 6 |
| 7 #include <string.h> |
| 8 |
| 9 #include <string> |
| 10 |
| 11 #include "base/bind.h" |
| 12 #include "base/json/json_writer.h" |
7 #include "base/logging.h" | 13 #include "base/logging.h" |
8 #include "base/stl_util.h" | 14 #include "base/stl_util.h" |
9 #include "base/strings/stringprintf.h" | 15 #include "base/strings/stringprintf.h" |
| 16 #include "base/values.h" |
| 17 #include "components/devtools_service/devtools_agent_host.h" |
| 18 #include "components/devtools_service/devtools_registry_impl.h" |
10 #include "components/devtools_service/devtools_service.h" | 19 #include "components/devtools_service/devtools_service.h" |
11 #include "mojo/application/public/cpp/application_impl.h" | 20 #include "mojo/application/public/cpp/application_impl.h" |
12 #include "mojo/services/network/public/interfaces/http_message.mojom.h" | 21 #include "mojo/services/network/public/cpp/web_socket_read_queue.h" |
| 22 #include "mojo/services/network/public/cpp/web_socket_write_queue.h" |
13 #include "mojo/services/network/public/interfaces/net_address.mojom.h" | 23 #include "mojo/services/network/public/interfaces/net_address.mojom.h" |
14 #include "mojo/services/network/public/interfaces/network_service.mojom.h" | 24 #include "mojo/services/network/public/interfaces/network_service.mojom.h" |
| 25 #include "mojo/services/network/public/interfaces/web_socket.mojom.h" |
| 26 #include "third_party/mojo/src/mojo/public/cpp/system/data_pipe.h" |
15 | 27 |
16 namespace devtools_service { | 28 namespace devtools_service { |
17 | 29 |
| 30 namespace { |
| 31 |
| 32 const char kPageUrlPrefix[] = "/devtools/page/"; |
| 33 const char kBrowserUrlPrefix[] = "/devtools/browser"; |
| 34 const char kJsonRequestUrlPrefix[] = "/json"; |
| 35 |
| 36 const char kActivateCommand[] = "activate"; |
| 37 const char kCloseCommand[] = "close"; |
| 38 const char kListCommand[] = "list"; |
| 39 const char kNewCommand[] = "new"; |
| 40 const char kVersionCommand[] = "version"; |
| 41 |
| 42 const char kTargetIdField[] = "id"; |
| 43 const char kTargetTypeField[] = "type"; |
| 44 const char kTargetTitleField[] = "title"; |
| 45 const char kTargetDescriptionField[] = "description"; |
| 46 const char kTargetUrlField[] = "url"; |
| 47 const char kTargetWebSocketDebuggerUrlField[] = "webSocketDebuggerUrl"; |
| 48 const char kTargetDevtoolsFrontendUrlField[] = "devtoolsFrontendUrl"; |
| 49 |
| 50 bool ParseJsonPath(const std::string& path, |
| 51 std::string* command, |
| 52 std::string* target_id) { |
| 53 // Fall back to list in case of empty query. |
| 54 if (path.empty()) { |
| 55 *command = kListCommand; |
| 56 return true; |
| 57 } |
| 58 |
| 59 if (path.find("/") != 0) { |
| 60 // Malformed command. |
| 61 return false; |
| 62 } |
| 63 *command = path.substr(1); |
| 64 |
| 65 size_t separator_pos = command->find("/"); |
| 66 if (separator_pos != std::string::npos) { |
| 67 *target_id = command->substr(separator_pos + 1); |
| 68 *command = command->substr(0, separator_pos); |
| 69 } |
| 70 return true; |
| 71 } |
| 72 |
| 73 mojo::HttpResponsePtr MakeResponse(uint32_t status_code, |
| 74 const std::string& content_type, |
| 75 const std::string& body) { |
| 76 mojo::HttpResponsePtr response(mojo::HttpResponse::New()); |
| 77 response->headers.resize(2); |
| 78 response->headers[0] = mojo::HttpHeader::New(); |
| 79 response->headers[0]->name = "Content-Length"; |
| 80 response->headers[0]->value = |
| 81 base::StringPrintf("%lu", static_cast<unsigned long>(body.size())); |
| 82 response->headers[1] = mojo::HttpHeader::New(); |
| 83 response->headers[1]->name = "Content-Type"; |
| 84 response->headers[1]->value = content_type; |
| 85 |
| 86 if (!body.empty()) { |
| 87 uint32_t num_bytes = static_cast<uint32_t>(body.size()); |
| 88 MojoCreateDataPipeOptions options; |
| 89 options.struct_size = sizeof(MojoCreateDataPipeOptions); |
| 90 options.flags = MOJO_CREATE_DATA_PIPE_OPTIONS_FLAG_NONE; |
| 91 options.element_num_bytes = 1; |
| 92 options.capacity_num_bytes = num_bytes; |
| 93 mojo::DataPipe data_pipe(options); |
| 94 response->body = data_pipe.consumer_handle.Pass(); |
| 95 MojoResult result = |
| 96 WriteDataRaw(data_pipe.producer_handle.get(), body.data(), &num_bytes, |
| 97 MOJO_WRITE_DATA_FLAG_ALL_OR_NONE); |
| 98 CHECK_EQ(MOJO_RESULT_OK, result); |
| 99 } |
| 100 return response.Pass(); |
| 101 } |
| 102 |
| 103 mojo::HttpResponsePtr MakeJsonResponse(uint32_t status_code, |
| 104 base::Value* value, |
| 105 const std::string& message) { |
| 106 // Serialize value and message. |
| 107 std::string json_value; |
| 108 if (value) { |
| 109 base::JSONWriter::WriteWithOptions( |
| 110 *value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json_value); |
| 111 } |
| 112 |
| 113 return MakeResponse(status_code, "application/json; charset=UTF-8", |
| 114 json_value + message); |
| 115 } |
| 116 |
| 117 class WebSocketRelayer : public DevToolsAgentHost::Delegate, |
| 118 public mojo::WebSocketClient, |
| 119 public mojo::ErrorHandler { |
| 120 public: |
| 121 // Creates a WebSocketRelayer instance and sets it as the delegate of |
| 122 // |agent_host|. |
| 123 // |
| 124 // The object destroys itself when either of the following happens: |
| 125 // - |agent_host| is dead and the object finishes all pending sends (if any) |
| 126 // to the Web socket; or |
| 127 // - the underlying pipe of |web_socket| is closed and the object finishes all |
| 128 // pending receives (if any) from the Web socket. |
| 129 static mojo::WebSocketClientPtr SetUp( |
| 130 DevToolsAgentHost* agent_host, |
| 131 mojo::WebSocketPtr web_socket, |
| 132 mojo::ScopedDataPipeProducerHandle send_stream) { |
| 133 DCHECK(agent_host); |
| 134 DCHECK(web_socket); |
| 135 DCHECK(send_stream.is_valid()); |
| 136 |
| 137 mojo::WebSocketClientPtr web_socket_client; |
| 138 new WebSocketRelayer(agent_host, web_socket.Pass(), send_stream.Pass(), |
| 139 &web_socket_client); |
| 140 return web_socket_client.Pass(); |
| 141 } |
| 142 |
| 143 private: |
| 144 WebSocketRelayer(DevToolsAgentHost* agent_host, |
| 145 mojo::WebSocketPtr web_socket, |
| 146 mojo::ScopedDataPipeProducerHandle send_stream, |
| 147 mojo::WebSocketClientPtr* web_socket_client) |
| 148 : agent_host_(agent_host), |
| 149 binding_(this, web_socket_client), |
| 150 web_socket_(web_socket.Pass()), |
| 151 send_stream_(send_stream.Pass()), |
| 152 write_send_stream_(new mojo::WebSocketWriteQueue(send_stream_.get())), |
| 153 pending_send_count_(0), |
| 154 pending_receive_count_(0) { |
| 155 web_socket_.set_error_handler(this); |
| 156 agent_host->SetDelegate(this); |
| 157 } |
| 158 |
| 159 ~WebSocketRelayer() override { |
| 160 if (agent_host_) |
| 161 agent_host_->SetDelegate(nullptr); |
| 162 } |
| 163 |
| 164 // DevToolsAgentHost::Delegate implementation. |
| 165 void DispatchProtocolMessage(DevToolsAgentHost* agent_host, |
| 166 const std::string& message) override { |
| 167 if (!web_socket_) |
| 168 return; |
| 169 |
| 170 // TODO(yzshen): It shouldn't be an issue to pass an empty message. However, |
| 171 // WebSocket{Read,Write}Queue doesn't handle that correctly. |
| 172 if (message.empty()) |
| 173 return; |
| 174 |
| 175 pending_send_count_++; |
| 176 uint32_t size = static_cast<uint32_t>(message.size()); |
| 177 write_send_stream_->Write( |
| 178 &message[0], size, |
| 179 base::Bind(&WebSocketRelayer::OnFinishedWritingSendStream, |
| 180 base::Unretained(this), size)); |
| 181 } |
| 182 |
| 183 void OnAgentHostClosed(DevToolsAgentHost* agent_host) override { |
| 184 DispatchProtocolMessage(agent_host_, |
| 185 "{ \"method\": \"Inspector.detached\", " |
| 186 "\"params\": { \"reason\": \"target_closed\" } }"); |
| 187 |
| 188 // No need to call SetDelegate(nullptr) on |agent_host_| because it is going |
| 189 // away. |
| 190 agent_host_ = nullptr; |
| 191 |
| 192 if (ShouldSelfDestruct()) |
| 193 delete this; |
| 194 } |
| 195 |
| 196 // WebSocketClient implementation. |
| 197 void DidConnect(const mojo::String& selected_subprotocol, |
| 198 const mojo::String& extensions, |
| 199 mojo::ScopedDataPipeConsumerHandle receive_stream) override { |
| 200 receive_stream_ = receive_stream.Pass(); |
| 201 read_receive_stream_.reset( |
| 202 new mojo::WebSocketReadQueue(receive_stream_.get())); |
| 203 } |
| 204 |
| 205 void DidReceiveData(bool fin, |
| 206 mojo::WebSocket::MessageType type, |
| 207 uint32_t num_bytes) override { |
| 208 if (!agent_host_) |
| 209 return; |
| 210 |
| 211 // TODO(yzshen): It shouldn't be an issue to pass an empty message. However, |
| 212 // WebSocket{Read,Write}Queue doesn't handle that correctly. |
| 213 if (num_bytes == 0) |
| 214 return; |
| 215 |
| 216 pending_receive_count_++; |
| 217 read_receive_stream_->Read( |
| 218 num_bytes, base::Bind(&WebSocketRelayer::OnFinishedReadingReceiveStream, |
| 219 base::Unretained(this), num_bytes)); |
| 220 } |
| 221 |
| 222 void DidReceiveFlowControl(int64_t quota) override {} |
| 223 |
| 224 void DidFail(const mojo::String& message) override {} |
| 225 |
| 226 void DidClose(bool was_clean, |
| 227 uint16_t code, |
| 228 const mojo::String& reason) override {} |
| 229 |
| 230 // mojo::ErrorHandler implementation. |
| 231 void OnConnectionError() override { |
| 232 web_socket_ = nullptr; |
| 233 binding_.Close(); |
| 234 |
| 235 if (ShouldSelfDestruct()) |
| 236 delete this; |
| 237 } |
| 238 |
| 239 void OnFinishedWritingSendStream(uint32_t num_bytes, const char* buffer) { |
| 240 DCHECK_GT(pending_send_count_, 0u); |
| 241 pending_send_count_--; |
| 242 |
| 243 if (web_socket_ && buffer) |
| 244 web_socket_->Send(true, mojo::WebSocket::MESSAGE_TYPE_TEXT, num_bytes); |
| 245 |
| 246 if (ShouldSelfDestruct()) |
| 247 delete this; |
| 248 } |
| 249 |
| 250 void OnFinishedReadingReceiveStream(uint32_t num_bytes, const char* data) { |
| 251 DCHECK_GT(pending_receive_count_, 0u); |
| 252 pending_receive_count_--; |
| 253 |
| 254 if (agent_host_ && data) |
| 255 agent_host_->SendProtocolMessageToAgent(std::string(data, num_bytes)); |
| 256 |
| 257 if (ShouldSelfDestruct()) |
| 258 delete this; |
| 259 } |
| 260 |
| 261 bool ShouldSelfDestruct() const { |
| 262 return (!agent_host_ && pending_send_count_ == 0) || |
| 263 (!web_socket_ && pending_receive_count_ == 0); |
| 264 } |
| 265 |
| 266 DevToolsAgentHost* agent_host_; |
| 267 mojo::Binding<WebSocketClient> binding_; |
| 268 mojo::WebSocketPtr web_socket_; |
| 269 |
| 270 mojo::ScopedDataPipeProducerHandle send_stream_; |
| 271 scoped_ptr<mojo::WebSocketWriteQueue> write_send_stream_; |
| 272 size_t pending_send_count_; |
| 273 |
| 274 mojo::ScopedDataPipeConsumerHandle receive_stream_; |
| 275 scoped_ptr<mojo::WebSocketReadQueue> read_receive_stream_; |
| 276 size_t pending_receive_count_; |
| 277 |
| 278 DISALLOW_COPY_AND_ASSIGN(WebSocketRelayer); |
| 279 }; |
| 280 |
| 281 } // namespace |
| 282 |
18 class DevToolsHttpServer::HttpConnectionDelegateImpl | 283 class DevToolsHttpServer::HttpConnectionDelegateImpl |
19 : public mojo::HttpConnectionDelegate, | 284 : public mojo::HttpConnectionDelegate, |
20 public mojo::ErrorHandler { | 285 public mojo::ErrorHandler { |
21 public: | 286 public: |
22 HttpConnectionDelegateImpl( | 287 HttpConnectionDelegateImpl( |
23 DevToolsHttpServer* owner, | 288 DevToolsHttpServer* owner, |
24 mojo::HttpConnectionPtr connection, | 289 mojo::HttpConnectionPtr connection, |
25 mojo::InterfaceRequest<HttpConnectionDelegate> delegate_request) | 290 mojo::InterfaceRequest<HttpConnectionDelegate> delegate_request) |
26 : owner_(owner), | 291 : owner_(owner), |
27 connection_(connection.Pass()), | 292 connection_(connection.Pass()), |
(...skipping 26 matching lines...) Expand all Loading... |
54 | 319 |
55 DevToolsHttpServer* const owner_; | 320 DevToolsHttpServer* const owner_; |
56 mojo::HttpConnectionPtr connection_; | 321 mojo::HttpConnectionPtr connection_; |
57 mojo::Binding<HttpConnectionDelegate> binding_; | 322 mojo::Binding<HttpConnectionDelegate> binding_; |
58 | 323 |
59 DISALLOW_COPY_AND_ASSIGN(HttpConnectionDelegateImpl); | 324 DISALLOW_COPY_AND_ASSIGN(HttpConnectionDelegateImpl); |
60 }; | 325 }; |
61 | 326 |
62 DevToolsHttpServer::DevToolsHttpServer(DevToolsService* service, | 327 DevToolsHttpServer::DevToolsHttpServer(DevToolsService* service, |
63 uint16_t remote_debugging_port) | 328 uint16_t remote_debugging_port) |
64 : service_(service) { | 329 : service_(service), remote_debugging_port_(remote_debugging_port) { |
65 VLOG(1) << "Remote debugging HTTP server is started on port " | 330 VLOG(1) << "Remote debugging HTTP server is started on port " |
66 << remote_debugging_port << "."; | 331 << remote_debugging_port << "."; |
67 mojo::NetworkServicePtr network_service; | 332 mojo::NetworkServicePtr network_service; |
68 mojo::URLRequestPtr request(mojo::URLRequest::New()); | 333 mojo::URLRequestPtr request(mojo::URLRequest::New()); |
69 request->url = "mojo:network_service"; | 334 request->url = "mojo:network_service"; |
70 service_->application()->ConnectToService(request.Pass(), &network_service); | 335 service_->application()->ConnectToService(request.Pass(), &network_service); |
71 | 336 |
72 mojo::NetAddressPtr local_address(mojo::NetAddress::New()); | 337 mojo::NetAddressPtr local_address(mojo::NetAddress::New()); |
73 local_address->family = mojo::NET_ADDRESS_FAMILY_IPV4; | 338 local_address->family = mojo::NET_ADDRESS_FAMILY_IPV4; |
74 local_address->ipv4 = mojo::NetAddressIPv4::New(); | 339 local_address->ipv4 = mojo::NetAddressIPv4::New(); |
(...skipping 22 matching lines...) Expand all Loading... |
97 connections_.insert( | 362 connections_.insert( |
98 new HttpConnectionDelegateImpl(this, connection.Pass(), delegate.Pass())); | 363 new HttpConnectionDelegateImpl(this, connection.Pass(), delegate.Pass())); |
99 } | 364 } |
100 | 365 |
101 void DevToolsHttpServer::OnReceivedRequest( | 366 void DevToolsHttpServer::OnReceivedRequest( |
102 HttpConnectionDelegateImpl* connection, | 367 HttpConnectionDelegateImpl* connection, |
103 mojo::HttpRequestPtr request, | 368 mojo::HttpRequestPtr request, |
104 const OnReceivedRequestCallback& callback) { | 369 const OnReceivedRequestCallback& callback) { |
105 DCHECK(connections_.find(connection) != connections_.end()); | 370 DCHECK(connections_.find(connection) != connections_.end()); |
106 | 371 |
107 // TODO(yzshen): Implement it. | 372 if (request->url.get().find(kJsonRequestUrlPrefix) == 0) { |
108 static const char kNotImplemented[] = "Not implemented yet!"; | 373 callback.Run(ProcessJsonRequest(request.Pass())); |
109 mojo::HttpResponsePtr response(mojo::HttpResponse::New()); | 374 } else { |
110 response->headers.resize(2); | 375 // TODO(yzshen): Implement it. |
111 response->headers[0] = mojo::HttpHeader::New(); | 376 NOTIMPLEMENTED(); |
112 response->headers[0]->name = "Content-Length"; | 377 callback.Run(MakeResponse(404, "text/html", "Not implemented yet!")); |
113 response->headers[0]->value = base::StringPrintf( | 378 } |
114 "%lu", static_cast<unsigned long>(sizeof(kNotImplemented))); | |
115 response->headers[1] = mojo::HttpHeader::New(); | |
116 response->headers[1]->name = "Content-Type"; | |
117 response->headers[1]->value = "text/html"; | |
118 | |
119 uint32_t num_bytes = sizeof(kNotImplemented); | |
120 MojoCreateDataPipeOptions options; | |
121 options.struct_size = sizeof(MojoCreateDataPipeOptions); | |
122 options.flags = MOJO_CREATE_DATA_PIPE_OPTIONS_FLAG_NONE; | |
123 options.element_num_bytes = 1; | |
124 options.capacity_num_bytes = num_bytes; | |
125 mojo::DataPipe data_pipe(options); | |
126 response->body = data_pipe.consumer_handle.Pass(); | |
127 WriteDataRaw(data_pipe.producer_handle.get(), kNotImplemented, &num_bytes, | |
128 MOJO_WRITE_DATA_FLAG_ALL_OR_NONE); | |
129 | |
130 callback.Run(response.Pass()); | |
131 } | 379 } |
132 | 380 |
133 void DevToolsHttpServer::OnReceivedWebSocketRequest( | 381 void DevToolsHttpServer::OnReceivedWebSocketRequest( |
134 HttpConnectionDelegateImpl* connection, | 382 HttpConnectionDelegateImpl* connection, |
135 mojo::HttpRequestPtr request, | 383 mojo::HttpRequestPtr request, |
136 const OnReceivedWebSocketRequestCallback& callback) { | 384 const OnReceivedWebSocketRequestCallback& callback) { |
137 DCHECK(connections_.find(connection) != connections_.end()); | 385 DCHECK(connections_.find(connection) != connections_.end()); |
138 | 386 |
139 // TODO(yzshen): Implement it. | 387 std::string path = request->url; |
140 NOTIMPLEMENTED(); | 388 size_t browser_pos = path.find(kBrowserUrlPrefix); |
| 389 if (browser_pos == 0) { |
| 390 // TODO(yzshen): Implement it. |
| 391 NOTIMPLEMENTED(); |
| 392 callback.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr); |
| 393 return; |
| 394 } |
| 395 |
| 396 size_t pos = path.find(kPageUrlPrefix); |
| 397 if (pos != 0) { |
| 398 callback.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr); |
| 399 return; |
| 400 } |
| 401 |
| 402 std::string target_id = path.substr(strlen(kPageUrlPrefix)); |
| 403 DevToolsAgentHost* agent = service_->registry()->GetAgentById(target_id); |
| 404 if (!agent || agent->IsAttached()) { |
| 405 callback.Run(nullptr, mojo::ScopedDataPipeConsumerHandle(), nullptr); |
| 406 return; |
| 407 } |
| 408 |
| 409 mojo::WebSocketPtr web_socket; |
| 410 mojo::InterfaceRequest<mojo::WebSocket> web_socket_request = |
| 411 mojo::GetProxy(&web_socket); |
| 412 mojo::DataPipe data_pipe; |
| 413 mojo::WebSocketClientPtr web_socket_client = WebSocketRelayer::SetUp( |
| 414 agent, web_socket.Pass(), data_pipe.producer_handle.Pass()); |
| 415 callback.Run(web_socket_request.Pass(), data_pipe.consumer_handle.Pass(), |
| 416 web_socket_client.Pass()); |
141 } | 417 } |
142 | 418 |
143 void DevToolsHttpServer::OnConnectionClosed( | 419 void DevToolsHttpServer::OnConnectionClosed( |
144 HttpConnectionDelegateImpl* connection) { | 420 HttpConnectionDelegateImpl* connection) { |
145 DCHECK(connections_.find(connection) != connections_.end()); | 421 DCHECK(connections_.find(connection) != connections_.end()); |
146 | 422 |
147 delete connection; | 423 delete connection; |
148 connections_.erase(connection); | 424 connections_.erase(connection); |
149 } | 425 } |
150 | 426 |
| 427 mojo::HttpResponsePtr DevToolsHttpServer::ProcessJsonRequest( |
| 428 mojo::HttpRequestPtr request) { |
| 429 // Trim "/json". |
| 430 std::string path = request->url.get().substr(strlen(kJsonRequestUrlPrefix)); |
| 431 |
| 432 // Trim query. |
| 433 size_t query_pos = path.find("?"); |
| 434 if (query_pos != std::string::npos) |
| 435 path = path.substr(0, query_pos); |
| 436 |
| 437 // Trim fragment. |
| 438 size_t fragment_pos = path.find("#"); |
| 439 if (fragment_pos != std::string::npos) |
| 440 path = path.substr(0, fragment_pos); |
| 441 |
| 442 std::string command; |
| 443 std::string target_id; |
| 444 if (!ParseJsonPath(path, &command, &target_id)) |
| 445 return MakeJsonResponse(404, nullptr, |
| 446 "Malformed query: " + request->url.get()); |
| 447 |
| 448 if (command == kVersionCommand || command == kNewCommand || |
| 449 command == kActivateCommand || command == kCloseCommand) { |
| 450 NOTIMPLEMENTED(); |
| 451 return MakeJsonResponse(404, nullptr, |
| 452 "Not implemented yet: " + request->url.get()); |
| 453 } |
| 454 |
| 455 if (command == kListCommand) { |
| 456 base::ListValue list_value; |
| 457 for (DevToolsRegistryImpl::Iterator iter(service_->registry()); |
| 458 !iter.IsAtEnd(); iter.Advance()) { |
| 459 scoped_ptr<base::DictionaryValue> dict_value(new base::DictionaryValue()); |
| 460 |
| 461 // TODO(yzshen): Add more information. |
| 462 dict_value->SetString(kTargetDescriptionField, std::string()); |
| 463 dict_value->SetString(kTargetDevtoolsFrontendUrlField, std::string()); |
| 464 dict_value->SetString(kTargetIdField, iter.value()->id()); |
| 465 dict_value->SetString(kTargetTitleField, std::string()); |
| 466 dict_value->SetString(kTargetTypeField, "page"); |
| 467 dict_value->SetString(kTargetUrlField, std::string()); |
| 468 dict_value->SetString( |
| 469 kTargetWebSocketDebuggerUrlField, |
| 470 base::StringPrintf("ws://127.0.0.1:%u%s%s", |
| 471 static_cast<unsigned>(remote_debugging_port_), |
| 472 kPageUrlPrefix, iter.value()->id().c_str())); |
| 473 list_value.Append(dict_value.Pass()); |
| 474 } |
| 475 return MakeJsonResponse(200, &list_value, std::string()); |
| 476 } |
| 477 |
| 478 return MakeJsonResponse(404, nullptr, "Unknown command: " + command); |
| 479 } |
| 480 |
151 } // namespace devtools_service | 481 } // namespace devtools_service |
OLD | NEW |