text: // Copyright (c) 2012 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include "chrome/test/chromedriver/chrome/devtools_client_impl.h"
  
  #include "base/bind.h"
  #include "base/json/json_reader.h"
  #include "base/json/json_writer.h"
  #include "base/logging.h"
  #include "base/strings/stringprintf.h"
  #include "base/values.h"
  #include "chrome/test/chromedriver/chrome/devtools_event_listener.h"
  #include "chrome/test/chromedriver/chrome/log.h"
  #include "chrome/test/chromedriver/chrome/status.h"
  #include "chrome/test/chromedriver/chrome/util.h"
  #include "chrome/test/chromedriver/net/sync_websocket.h"
  #include "chrome/test/chromedriver/net/url_request_context_getter.h"
  
  namespace {
  
  const char kInspectorContextError[] =
      "Execution context with given id not found.";
  
  Status ParseInspectorError(const std::string& error_json) {
    scoped_ptr<base::Value> error(base::JSONReader::Read(error_json));
    base::DictionaryValue* error_dict;
    if (!error || !error->GetAsDictionary(&error_dict))
      return Status(kUnknownError, "inspector error with no error message");
    std::string error_message;
    if (error_dict->GetString("message", &error_message) &&
        error_message == kInspectorContextError) {
      return Status(kNoSuchExecutionContext);
    }
    return Status(kUnknownError, "unhandled inspector error: " + error_json);
  }
  
  class ScopedIncrementer {
   public:
    explicit ScopedIncrementer(int* count) : count_(count) {
      (*count_)++;
    }
    ~ScopedIncrementer() {
      (*count_)--;
    }
  
   private:
    int* count_;
  };
  
  Status ConditionIsMet(bool* is_condition_met) {
    *is_condition_met = true;
    return Status(kOk);
  }
  
  Status FakeCloseFrontends() {
    return Status(kOk);
  }
  
  }  // namespace
  
  namespace internal {
  
  InspectorEvent::InspectorEvent() {}
  
  InspectorEvent::~InspectorEvent() {}
  
  InspectorCommandResponse::InspectorCommandResponse() {}
  
  InspectorCommandResponse::~InspectorCommandResponse() {}
  
  }  // namespace internal
  
  const char DevToolsClientImpl::kBrowserwideDevToolsClientId[] = "browser";
  
  DevToolsClientImpl::DevToolsClientImpl(
      const SyncWebSocketFactory& factory,
      const std::string& url,
      const std::string& id)
      : socket_(factory.Run().Pass()),
        url_(url),
        crashed_(false),
        id_(id),
        frontend_closer_func_(base::Bind(&FakeCloseFrontends)),
        parser_func_(base::Bind(&internal::ParseInspectorMessage)),
        unnotified_event_(NULL),
        next_id_(1),
        stack_count_(0) {}
  
  DevToolsClientImpl::DevToolsClientImpl(
      const SyncWebSocketFactory& factory,
      const std::string& url,
      const std::string& id,
      const FrontendCloserFunc& frontend_closer_func)
      : socket_(factory.Run().Pass()),
        url_(url),
        crashed_(false),
        id_(id),
        frontend_closer_func_(frontend_closer_func),
        parser_func_(base::Bind(&internal::ParseInspectorMessage)),
        unnotified_event_(NULL),
        next_id_(1),
        stack_count_(0) {}
  
  DevToolsClientImpl::DevToolsClientImpl(
      const SyncWebSocketFactory& factory,
      const std::string& url,
      const std::string& id,
      const FrontendCloserFunc& frontend_closer_func,
      const ParserFunc& parser_func)
      : socket_(factory.Run().Pass()),
        url_(url),
        crashed_(false),
        id_(id),
        frontend_closer_func_(frontend_closer_func),
        parser_func_(parser_func),
        unnotified_event_(NULL),
        next_id_(1),
        stack_count_(0) {}
  
  DevToolsClientImpl::~DevToolsClientImpl() {}
  
  void DevToolsClientImpl::SetParserFuncForTesting(
      const ParserFunc& parser_func) {
    parser_func_ = parser_func;
  }
  
  const std::string& DevToolsClientImpl::GetId() {
    return id_;
  }
  
  bool DevToolsClientImpl::WasCrashed() {
    return crashed_;
  }
  
  Status DevToolsClientImpl::ConnectIfNecessary() {
    if (stack_count_)
      return Status(kUnknownError, "cannot connect when nested");
  
    if (socket_->IsConnected())
      return Status(kOk);
  
    if (!socket_->Connect(url_)) {
      // Try to close devtools frontend and then reconnect.
      Status status = frontend_closer_func_.Run();
      if (status.IsError())
        return status;
      if (!socket_->Connect(url_))
        return Status(kDisconnected, "unable to connect to renderer");
    }
  
    unnotified_connect_listeners_ = listeners_;
    unnotified_event_listeners_.clear();
    response_info_map_.clear();
  
    // Notify all listeners of the new connection. Do this now so that any errors
    // that occur are reported now instead of later during some unrelated call.
    // Also gives listeners a chance to send commands before other clients.
    return EnsureListenersNotifiedOfConnect();
  }
  
  Status DevToolsClientImpl::SendCommand(
      const std::string& method,
      const base::DictionaryValue& params) {
    scoped_ptr<base::DictionaryValue> result;
    return SendCommandInternal(method, params, &result);
  }
  
  Status DevToolsClientImpl::SendCommandAndGetResult(
      const std::string& method,
      const base::DictionaryValue& params,
      scoped_ptr<base::DictionaryValue>* result) {
    scoped_ptr<base::DictionaryValue> intermediate_result;
    Status status = SendCommandInternal(method, params, &intermediate_result);
    if (status.IsError())
      return status;
    if (!intermediate_result)
      return Status(kUnknownError, "inspector response missing result");
    result->reset(intermediate_result.release());
    return Status(kOk);
  }
  
  void DevToolsClientImpl::AddListener(DevToolsEventListener* listener) {
    CHECK(listener);
    listeners_.push_back(listener);
  }
  
  Status DevToolsClientImpl::HandleReceivedEvents() {
    return HandleEventsUntil(base::Bind(&ConditionIsMet), base::TimeDelta());
  }
  
  Status DevToolsClientImpl::HandleEventsUntil(
      const ConditionalFunc& conditional_func, const base::TimeDelta& timeout) {
    if (!socket_->IsConnected())
      return Status(kDisconnected, "not connected to DevTools");
  
    base::TimeTicks deadline = base::TimeTicks::Now() + timeout;
    base::TimeDelta next_message_timeout = timeout;
    while (true) {
      if (!socket_->HasNextMessage()) {
        bool is_condition_met = false;
        Status status = conditional_func.Run(&is_condition_met);
        if (status.IsError())
          return status;
        if (is_condition_met)
          return Status(kOk);
      }
  
      Status status = ProcessNextMessage(-1, next_message_timeout);
      if (status.IsError())
        return status;
      next_message_timeout = deadline - base::TimeTicks::Now();
    }
  }
  
  DevToolsClientImpl::ResponseInfo::ResponseInfo(const std::string& method)
      : state(kWaiting), method(method) {}
  
  DevToolsClientImpl::ResponseInfo::~ResponseInfo() {}
  
  Status DevToolsClientImpl::SendCommandInternal(
      const std::string& method,
      const base::DictionaryValue& params,
      scoped_ptr<base::DictionaryValue>* result) {
    if (!socket_->IsConnected())
      return Status(kDisconnected, "not connected to DevTools");
  
    int command_id = next_id_++;
    base::DictionaryValue command;
    command.SetInteger("id", command_id);
    command.SetString("method", method);
    command.Set("params", params.DeepCopy());
    std::string message = SerializeValue(&command);
    if (IsVLogOn(1)) {
      VLOG(1) << "DEVTOOLS COMMAND " << method << " (id=" << command_id << ") "
              << FormatValueForDisplay(params);
    }
    if (!socket_->Send(message))
      return Status(kDisconnected, "unable to send message to renderer");
  
    linked_ptr<ResponseInfo> response_info =
        make_linked_ptr(new ResponseInfo(method));
    response_info_map_[command_id] = response_info;
    while (response_info->state == kWaiting) {
      Status status = ProcessNextMessage(
          command_id, base::TimeDelta::FromMinutes(10));
      if (status.IsError()) {
        if (response_info->state == kReceived)
          response_info_map_.erase(command_id);
        return status;
      }
    }
    if (response_info->state == kBlocked) {
      response_info->state = kIgnored;
      return Status(kUnexpectedAlertOpen);
    }
    CHECK_EQ(response_info->state, kReceived);
    internal::InspectorCommandResponse& response = response_info->response;
    if (!response.result)
      return ParseInspectorError(response.error);
    *result = response.result.Pass();
    return Status(kOk);
  }
  
  Status DevToolsClientImpl::ProcessNextMessage(
      int expected_id,
      const base::TimeDelta& timeout) {
    ScopedIncrementer increment_stack_count(&stack_count_);
  
    Status status = EnsureListenersNotifiedOfConnect();
    if (status.IsError())
      return status;
    status = EnsureListenersNotifiedOfEvent();
    if (status.IsError())
      return status;
    status = EnsureListenersNotifiedOfCommandResponse();
    if (status.IsError())
      return status;
  
    // The command response may have already been received (in which case it will
    // have been deleted from |response_info_map_|) or blocked while notifying
    // listeners.
    if (expected_id != -1) {
      ResponseInfoMap::iterator iter = response_info_map_.find(expected_id);
      if (iter == response_info_map_.end() || iter->second->state != kWaiting)
        return Status(kOk);
    }
  
    if (crashed_)
      return Status(kTabCrashed);
  
    std::string message;
    switch (socket_->ReceiveNextMessage(&message, timeout)) {
      case SyncWebSocket::kOk:
        break;
      case SyncWebSocket::kDisconnected: {
        std::string err = "Unable to receive message from renderer";
        LOG(ERROR) << err;
        return Status(kDisconnected, err);
      }
      case SyncWebSocket::kTimeout: {
        std::string err =
            "Timed out receiving message from renderer: " +
            base::StringPrintf("%.3lf", timeout.InSecondsF());
        LOG(ERROR) << err;
        return Status(kTimeout, err);
      }
      default:
        NOTREACHED();
        break;
    }
  
    internal::InspectorMessageType type;
    internal::InspectorEvent event;
    internal::InspectorCommandResponse response;
    if (!parser_func_.Run(message, expected_id, &type, &event, &response)) {
      LOG(ERROR) << "Bad inspector message: " << message;
      return Status(kUnknownError, "bad inspector message: " + message);
    }
  
    if (type == internal::kEventMessageType)
      return ProcessEvent(event);
    CHECK_EQ(type, internal::kCommandResponseMessageType);
    return ProcessCommandResponse(response);
  }
  
  Status DevToolsClientImpl::ProcessEvent(const internal::InspectorEvent& event) {
    if (IsVLogOn(1)) {
      VLOG(1) << "DEVTOOLS EVENT " << event.method << " "
              << FormatValueForDisplay(*event.params);
    }
    unnotified_event_listeners_ = listeners_;
    unnotified_event_ = &event;
    Status status = EnsureListenersNotifiedOfEvent();
    unnotified_event_ = NULL;
    if (status.IsError())
      return status;
    if (event.method == "Inspector.detached")
      return Status(kDisconnected, "received Inspector.detached event");
    if (event.method == "Inspector.targetCrashed") {
      crashed_ = true;
      return Status(kTabCrashed);
    }
    if (event.method == "Page.javascriptDialogOpening") {
      // A command may have opened the dialog, which will block the response.
      // To find out which one (if any), do a round trip with a simple command
      // to the renderer and afterwards see if any of the commands still haven't
      // received a response.
      // This relies on the fact that DevTools commands are processed
      // sequentially. This may break if any of the commands are asynchronous.
      // If for some reason the round trip command fails, mark all the waiting
      // commands as blocked and return the error. This is better than risking
      // a hang.
      int max_id = next_id_;
      base::DictionaryValue enable_params;
      enable_params.SetString("purpose", "detect if alert blocked any cmds");
      Status enable_status = SendCommand("Inspector.enable", enable_params);
      for (ResponseInfoMap::const_iterator iter = response_info_map_.begin();
           iter != response_info_map_.end(); ++iter) {
        if (iter->first > max_id)
          continue;
        if (iter->second->state == kWaiting)
          iter->second->state = kBlocked;
      }
      if (enable_status.IsError())
        return status;
    }
    return Status(kOk);
  }
  
  Status DevToolsClientImpl::ProcessCommandResponse(
      const internal::InspectorCommandResponse& response) {
    ResponseInfoMap::iterator iter = response_info_map_.find(response.id);
    if (IsVLogOn(1)) {
      std::string method, result;
      if (iter != response_info_map_.end())
        method = iter->second->method;
      if (response.result)
        result = FormatValueForDisplay(*response.result);
      else
        result = response.error;
      VLOG(1) << "DEVTOOLS RESPONSE " << method << " (id=" << response.id
              << ") " << result;
    }
  
    if (iter == response_info_map_.end())
      return Status(kUnknownError, "unexpected command response");
  
    linked_ptr<ResponseInfo> response_info = response_info_map_[response.id];
    response_info_map_.erase(response.id);
  
    if (response_info->state != kIgnored) {
      response_info->state = kReceived;
      response_info->response.id = response.id;
      response_info->response.error = response.error;
      if (response.result)
        response_info->response.result.reset(response.result->DeepCopy());
    }
  
    if (response.result) {
      unnotified_cmd_response_listeners_ = listeners_;
      unnotified_cmd_response_info_ = response_info;
      Status status = EnsureListenersNotifiedOfCommandResponse();
      unnotified_cmd_response_info_.reset();
      if (status.IsError())
        return status;
    }
    return Status(kOk);
  }
  
  Status DevToolsClientImpl::EnsureListenersNotifiedOfConnect() {
    while (unnotified_connect_listeners_.size()) {
      DevToolsEventListener* listener = unnotified_connect_listeners_.front();
      unnotified_connect_listeners_.pop_front();
      Status status = listener->OnConnected(this);
      if (status.IsError())
        return status;
    }
    return Status(kOk);
  }
  
  Status DevToolsClientImpl::EnsureListenersNotifiedOfEvent() {
    while (unnotified_event_listeners_.size()) {
      DevToolsEventListener* listener = unnotified_event_listeners_.front();
      unnotified_event_listeners_.pop_front();
      Status status = listener->OnEvent(
          this, unnotified_event_->method, *unnotified_event_->params);
      if (status.IsError())
        return status;
    }
    return Status(kOk);
  }
  
  Status DevToolsClientImpl::EnsureListenersNotifiedOfCommandResponse() {
    while (unnotified_cmd_response_listeners_.size()) {
      DevToolsEventListener* listener =
          unnotified_cmd_response_listeners_.front();
      unnotified_cmd_response_listeners_.pop_front();
      Status status =
          listener->OnCommandSuccess(this, unnotified_cmd_response_info_->method);
      if (status.IsError())
        return status;
    }
    return Status(kOk);
  }
  
  namespace internal {
  
  bool ParseInspectorMessage(
      const std::string& message,
      int expected_id,
      InspectorMessageType* type,
      InspectorEvent* event,
      InspectorCommandResponse* command_response) {
    scoped_ptr<base::Value> message_value(base::JSONReader::Read(message));
    base::DictionaryValue* message_dict;
    if (!message_value || !message_value->GetAsDictionary(&message_dict))
      return false;
  
    int id;
    if (!message_dict->HasKey("id")) {
      std::string method;
      if (!message_dict->GetString("method", &method))
        return false;
      base::DictionaryValue* params = NULL;
      message_dict->GetDictionary("params", &params);
  
      *type = kEventMessageType;
      event->method = method;
      if (params)
        event->params.reset(params->DeepCopy());
      else
        event->params.reset(new base::DictionaryValue());
      return true;
    } else if (message_dict->GetInteger("id", &id)) {
      base::DictionaryValue* unscoped_error = NULL;
      base::DictionaryValue* unscoped_result = NULL;
      *type = kCommandResponseMessageType;
      command_response->id = id;
      // As per Chromium issue 392577, DevTools does not necessarily return a
      // "result" dictionary for every valid response. In particular,
      // Tracing.start and Tracing.end command responses do not contain one.
      // So, if neither "error" nor "result" keys are present, just provide
      // a blank result dictionary.
      if (message_dict->GetDictionary("result", &unscoped_result))
        command_response->result.reset(unscoped_result->DeepCopy());
      else if (message_dict->GetDictionary("error", &unscoped_error))
        base::JSONWriter::Write(unscoped_error, &command_response->error);
      else
        command_response->result.reset(new base::DictionaryValue());
      return true;
    }
    return false;
  }
  
  }  // namespace internal
  