1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/test/chromedriver/chrome/devtools_client_impl.h"
8 #include "base/json/json_reader.h"
9 #include "base/json/json_writer.h"
10 #include "base/logging.h"
11 #include "base/strings/stringprintf.h"
12 #include "base/values.h"
13 #include "chrome/test/chromedriver/chrome/devtools_event_listener.h"
14 #include "chrome/test/chromedriver/chrome/log.h"
15 #include "chrome/test/chromedriver/chrome/status.h"
16 #include "chrome/test/chromedriver/chrome/util.h"
17 #include "chrome/test/chromedriver/net/sync_websocket.h"
18 #include "chrome/test/chromedriver/net/url_request_context_getter.h"
22 const char kInspectorContextError
[] =
23 "Execution context with given id not found.";
25 Status
ParseInspectorError(const std::string
& error_json
) {
26 scoped_ptr
<base::Value
> error
= base::JSONReader::Read(error_json
);
27 base::DictionaryValue
* error_dict
;
28 if (!error
|| !error
->GetAsDictionary(&error_dict
))
29 return Status(kUnknownError
, "inspector error with no error message");
30 std::string error_message
;
31 if (error_dict
->GetString("message", &error_message
) &&
32 error_message
== kInspectorContextError
) {
33 return Status(kNoSuchExecutionContext
);
35 return Status(kUnknownError
, "unhandled inspector error: " + error_json
);
38 class ScopedIncrementer
{
40 explicit ScopedIncrementer(int* count
) : count_(count
) {
43 ~ScopedIncrementer() {
51 Status
ConditionIsMet(bool* is_condition_met
) {
52 *is_condition_met
= true;
56 Status
FakeCloseFrontends() {
64 InspectorEvent::InspectorEvent() {}
66 InspectorEvent::~InspectorEvent() {}
68 InspectorCommandResponse::InspectorCommandResponse() {}
70 InspectorCommandResponse::~InspectorCommandResponse() {}
72 } // namespace internal
74 const char DevToolsClientImpl::kBrowserwideDevToolsClientId
[] = "browser";
76 DevToolsClientImpl::DevToolsClientImpl(
77 const SyncWebSocketFactory
& factory
,
78 const std::string
& url
,
79 const std::string
& id
)
80 : socket_(factory
.Run().Pass()),
84 frontend_closer_func_(base::Bind(&FakeCloseFrontends
)),
85 parser_func_(base::Bind(&internal::ParseInspectorMessage
)),
86 unnotified_event_(NULL
),
90 DevToolsClientImpl::DevToolsClientImpl(
91 const SyncWebSocketFactory
& factory
,
92 const std::string
& url
,
93 const std::string
& id
,
94 const FrontendCloserFunc
& frontend_closer_func
)
95 : socket_(factory
.Run().Pass()),
99 frontend_closer_func_(frontend_closer_func
),
100 parser_func_(base::Bind(&internal::ParseInspectorMessage
)),
101 unnotified_event_(NULL
),
105 DevToolsClientImpl::DevToolsClientImpl(
106 const SyncWebSocketFactory
& factory
,
107 const std::string
& url
,
108 const std::string
& id
,
109 const FrontendCloserFunc
& frontend_closer_func
,
110 const ParserFunc
& parser_func
)
111 : socket_(factory
.Run().Pass()),
115 frontend_closer_func_(frontend_closer_func
),
116 parser_func_(parser_func
),
117 unnotified_event_(NULL
),
121 DevToolsClientImpl::~DevToolsClientImpl() {}
123 void DevToolsClientImpl::SetParserFuncForTesting(
124 const ParserFunc
& parser_func
) {
125 parser_func_
= parser_func
;
128 const std::string
& DevToolsClientImpl::GetId() {
132 bool DevToolsClientImpl::WasCrashed() {
136 Status
DevToolsClientImpl::ConnectIfNecessary() {
138 return Status(kUnknownError
, "cannot connect when nested");
140 if (socket_
->IsConnected())
143 if (!socket_
->Connect(url_
)) {
144 // Try to close devtools frontend and then reconnect.
145 Status status
= frontend_closer_func_
.Run();
146 if (status
.IsError())
148 if (!socket_
->Connect(url_
))
149 return Status(kDisconnected
, "unable to connect to renderer");
152 unnotified_connect_listeners_
= listeners_
;
153 unnotified_event_listeners_
.clear();
154 response_info_map_
.clear();
156 // Notify all listeners of the new connection. Do this now so that any errors
157 // that occur are reported now instead of later during some unrelated call.
158 // Also gives listeners a chance to send commands before other clients.
159 return EnsureListenersNotifiedOfConnect();
162 Status
DevToolsClientImpl::SendCommand(
163 const std::string
& method
,
164 const base::DictionaryValue
& params
) {
165 scoped_ptr
<base::DictionaryValue
> result
;
166 return SendCommandInternal(method
, params
, &result
, true);
169 Status
DevToolsClientImpl::SendAsyncCommand(
170 const std::string
& method
,
171 const base::DictionaryValue
& params
) {
172 scoped_ptr
<base::DictionaryValue
> result
;
173 return SendCommandInternal(method
, params
, &result
, false);
176 Status
DevToolsClientImpl::SendCommandAndGetResult(
177 const std::string
& method
,
178 const base::DictionaryValue
& params
,
179 scoped_ptr
<base::DictionaryValue
>* result
) {
180 scoped_ptr
<base::DictionaryValue
> intermediate_result
;
181 Status status
= SendCommandInternal(
182 method
, params
, &intermediate_result
, true);
183 if (status
.IsError())
185 if (!intermediate_result
)
186 return Status(kUnknownError
, "inspector response missing result");
187 result
->reset(intermediate_result
.release());
191 void DevToolsClientImpl::AddListener(DevToolsEventListener
* listener
) {
193 listeners_
.push_back(listener
);
196 Status
DevToolsClientImpl::HandleReceivedEvents() {
197 return HandleEventsUntil(base::Bind(&ConditionIsMet
), base::TimeDelta());
200 Status
DevToolsClientImpl::HandleEventsUntil(
201 const ConditionalFunc
& conditional_func
, const base::TimeDelta
& timeout
) {
202 if (!socket_
->IsConnected())
203 return Status(kDisconnected
, "not connected to DevTools");
205 base::TimeTicks deadline
= base::TimeTicks::Now() + timeout
;
206 base::TimeDelta next_message_timeout
= timeout
;
208 if (!socket_
->HasNextMessage()) {
209 bool is_condition_met
= false;
210 Status status
= conditional_func
.Run(&is_condition_met
);
211 if (status
.IsError())
213 if (is_condition_met
)
217 Status status
= ProcessNextMessage(-1, next_message_timeout
);
218 if (status
.IsError())
220 next_message_timeout
= deadline
- base::TimeTicks::Now();
224 DevToolsClientImpl::ResponseInfo::ResponseInfo(const std::string
& method
)
225 : state(kWaiting
), method(method
) {}
227 DevToolsClientImpl::ResponseInfo::~ResponseInfo() {}
229 Status
DevToolsClientImpl::SendCommandInternal(
230 const std::string
& method
,
231 const base::DictionaryValue
& params
,
232 scoped_ptr
<base::DictionaryValue
>* result
,
233 bool wait_for_response
) {
234 if (!socket_
->IsConnected())
235 return Status(kDisconnected
, "not connected to DevTools");
237 int command_id
= next_id_
++;
238 base::DictionaryValue command
;
239 command
.SetInteger("id", command_id
);
240 command
.SetString("method", method
);
241 command
.Set("params", params
.DeepCopy());
242 std::string message
= SerializeValue(&command
);
244 VLOG(1) << "DEVTOOLS COMMAND " << method
<< " (id=" << command_id
<< ") "
245 << FormatValueForDisplay(params
);
247 if (!socket_
->Send(message
))
248 return Status(kDisconnected
, "unable to send message to renderer");
250 if (wait_for_response
) {
251 linked_ptr
<ResponseInfo
> response_info
=
252 make_linked_ptr(new ResponseInfo(method
));
253 response_info_map_
[command_id
] = response_info
;
254 while (response_info
->state
== kWaiting
) {
255 Status status
= ProcessNextMessage(
256 command_id
, base::TimeDelta::FromMinutes(10));
257 if (status
.IsError()) {
258 if (response_info
->state
== kReceived
)
259 response_info_map_
.erase(command_id
);
263 if (response_info
->state
== kBlocked
) {
264 response_info
->state
= kIgnored
;
265 return Status(kUnexpectedAlertOpen
);
267 CHECK_EQ(response_info
->state
, kReceived
);
268 internal::InspectorCommandResponse
& response
= response_info
->response
;
269 if (!response
.result
)
270 return ParseInspectorError(response
.error
);
271 *result
= response
.result
.Pass();
276 Status
DevToolsClientImpl::ProcessNextMessage(
278 const base::TimeDelta
& timeout
) {
279 ScopedIncrementer
increment_stack_count(&stack_count_
);
281 Status status
= EnsureListenersNotifiedOfConnect();
282 if (status
.IsError())
284 status
= EnsureListenersNotifiedOfEvent();
285 if (status
.IsError())
287 status
= EnsureListenersNotifiedOfCommandResponse();
288 if (status
.IsError())
291 // The command response may have already been received (in which case it will
292 // have been deleted from |response_info_map_|) or blocked while notifying
294 if (expected_id
!= -1) {
295 ResponseInfoMap::iterator iter
= response_info_map_
.find(expected_id
);
296 if (iter
== response_info_map_
.end() || iter
->second
->state
!= kWaiting
)
301 return Status(kTabCrashed
);
304 switch (socket_
->ReceiveNextMessage(&message
, timeout
)) {
305 case SyncWebSocket::kOk
:
307 case SyncWebSocket::kDisconnected
: {
308 std::string err
= "Unable to receive message from renderer";
310 return Status(kDisconnected
, err
);
312 case SyncWebSocket::kTimeout
: {
314 "Timed out receiving message from renderer: " +
315 base::StringPrintf("%.3lf", timeout
.InSecondsF());
317 return Status(kTimeout
, err
);
324 internal::InspectorMessageType type
;
325 internal::InspectorEvent event
;
326 internal::InspectorCommandResponse response
;
327 if (!parser_func_
.Run(message
, expected_id
, &type
, &event
, &response
)) {
328 LOG(ERROR
) << "Bad inspector message: " << message
;
329 return Status(kUnknownError
, "bad inspector message: " + message
);
332 if (type
== internal::kEventMessageType
)
333 return ProcessEvent(event
);
334 CHECK_EQ(type
, internal::kCommandResponseMessageType
);
335 return ProcessCommandResponse(response
);
338 Status
DevToolsClientImpl::ProcessEvent(const internal::InspectorEvent
& event
) {
340 VLOG(1) << "DEVTOOLS EVENT " << event
.method
<< " "
341 << FormatValueForDisplay(*event
.params
);
343 unnotified_event_listeners_
= listeners_
;
344 unnotified_event_
= &event
;
345 Status status
= EnsureListenersNotifiedOfEvent();
346 unnotified_event_
= NULL
;
347 if (status
.IsError())
349 if (event
.method
== "Inspector.detached")
350 return Status(kDisconnected
, "received Inspector.detached event");
351 if (event
.method
== "Inspector.targetCrashed") {
353 return Status(kTabCrashed
);
355 if (event
.method
== "Page.javascriptDialogOpening") {
356 // A command may have opened the dialog, which will block the response.
357 // To find out which one (if any), do a round trip with a simple command
358 // to the renderer and afterwards see if any of the commands still haven't
359 // received a response.
360 // This relies on the fact that DevTools commands are processed
361 // sequentially. This may break if any of the commands are asynchronous.
362 // If for some reason the round trip command fails, mark all the waiting
363 // commands as blocked and return the error. This is better than risking
365 int max_id
= next_id_
;
366 base::DictionaryValue enable_params
;
367 enable_params
.SetString("purpose", "detect if alert blocked any cmds");
368 Status enable_status
= SendCommand("Inspector.enable", enable_params
);
369 for (ResponseInfoMap::const_iterator iter
= response_info_map_
.begin();
370 iter
!= response_info_map_
.end(); ++iter
) {
371 if (iter
->first
> max_id
)
373 if (iter
->second
->state
== kWaiting
)
374 iter
->second
->state
= kBlocked
;
376 if (enable_status
.IsError())
382 Status
DevToolsClientImpl::ProcessCommandResponse(
383 const internal::InspectorCommandResponse
& response
) {
384 ResponseInfoMap::iterator iter
= response_info_map_
.find(response
.id
);
386 std::string method
, result
;
387 if (iter
!= response_info_map_
.end())
388 method
= iter
->second
->method
;
390 result
= FormatValueForDisplay(*response
.result
);
392 result
= response
.error
;
393 VLOG(1) << "DEVTOOLS RESPONSE " << method
<< " (id=" << response
.id
397 if (iter
== response_info_map_
.end())
398 return Status(kUnknownError
, "unexpected command response");
400 linked_ptr
<ResponseInfo
> response_info
= response_info_map_
[response
.id
];
401 response_info_map_
.erase(response
.id
);
403 if (response_info
->state
!= kIgnored
) {
404 response_info
->state
= kReceived
;
405 response_info
->response
.id
= response
.id
;
406 response_info
->response
.error
= response
.error
;
408 response_info
->response
.result
.reset(response
.result
->DeepCopy());
411 if (response
.result
) {
412 unnotified_cmd_response_listeners_
= listeners_
;
413 unnotified_cmd_response_info_
= response_info
;
414 Status status
= EnsureListenersNotifiedOfCommandResponse();
415 unnotified_cmd_response_info_
.reset();
416 if (status
.IsError())
422 Status
DevToolsClientImpl::EnsureListenersNotifiedOfConnect() {
423 while (unnotified_connect_listeners_
.size()) {
424 DevToolsEventListener
* listener
= unnotified_connect_listeners_
.front();
425 unnotified_connect_listeners_
.pop_front();
426 Status status
= listener
->OnConnected(this);
427 if (status
.IsError())
433 Status
DevToolsClientImpl::EnsureListenersNotifiedOfEvent() {
434 while (unnotified_event_listeners_
.size()) {
435 DevToolsEventListener
* listener
= unnotified_event_listeners_
.front();
436 unnotified_event_listeners_
.pop_front();
437 Status status
= listener
->OnEvent(
438 this, unnotified_event_
->method
, *unnotified_event_
->params
);
439 if (status
.IsError())
445 Status
DevToolsClientImpl::EnsureListenersNotifiedOfCommandResponse() {
446 while (unnotified_cmd_response_listeners_
.size()) {
447 DevToolsEventListener
* listener
=
448 unnotified_cmd_response_listeners_
.front();
449 unnotified_cmd_response_listeners_
.pop_front();
450 Status status
= listener
->OnCommandSuccess(
452 unnotified_cmd_response_info_
->method
,
453 *unnotified_cmd_response_info_
->response
.result
.get());
454 if (status
.IsError())
462 bool ParseInspectorMessage(
463 const std::string
& message
,
465 InspectorMessageType
* type
,
466 InspectorEvent
* event
,
467 InspectorCommandResponse
* command_response
) {
468 scoped_ptr
<base::Value
> message_value
= base::JSONReader::Read(message
);
469 base::DictionaryValue
* message_dict
;
470 if (!message_value
|| !message_value
->GetAsDictionary(&message_dict
))
474 if (!message_dict
->HasKey("id")) {
476 if (!message_dict
->GetString("method", &method
))
478 base::DictionaryValue
* params
= NULL
;
479 message_dict
->GetDictionary("params", ¶ms
);
481 *type
= kEventMessageType
;
482 event
->method
= method
;
484 event
->params
.reset(params
->DeepCopy());
486 event
->params
.reset(new base::DictionaryValue());
488 } else if (message_dict
->GetInteger("id", &id
)) {
489 base::DictionaryValue
* unscoped_error
= NULL
;
490 base::DictionaryValue
* unscoped_result
= NULL
;
491 *type
= kCommandResponseMessageType
;
492 command_response
->id
= id
;
493 // As per Chromium issue 392577, DevTools does not necessarily return a
494 // "result" dictionary for every valid response. In particular,
495 // Tracing.start and Tracing.end command responses do not contain one.
496 // So, if neither "error" nor "result" keys are present, just provide
497 // a blank result dictionary.
498 if (message_dict
->GetDictionary("result", &unscoped_result
))
499 command_response
->result
.reset(unscoped_result
->DeepCopy());
500 else if (message_dict
->GetDictionary("error", &unscoped_error
))
501 base::JSONWriter::Write(*unscoped_error
, &command_response
->error
);
503 command_response
->result
.reset(new base::DictionaryValue());
509 } // namespace internal