root/chrome/test/chromedriver/chrome/devtools_http_client.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. FakeCloseFrontends
  2. type
  3. IsFrontend
  4. Get
  5. GetSize
  6. GetForId
  7. web_socket_url_prefix_
  8. Init
  9. GetWebViewsInfo
  10. CreateClient
  11. CloseWebView
  12. ActivateWebView
  13. version
  14. build_no
  15. GetVersion
  16. CloseFrontends
  17. FetchUrlAndLog
  18. ParseWebViewsInfo
  19. ParseVersionInfo

// Copyright (c) 2013 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_http_client.h"

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/json/json_reader.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/test/chromedriver/chrome/devtools_client_impl.h"
#include "chrome/test/chromedriver/chrome/log.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/version.h"
#include "chrome/test/chromedriver/chrome/web_view_impl.h"
#include "chrome/test/chromedriver/net/net_util.h"
#include "chrome/test/chromedriver/net/url_request_context_getter.h"

namespace {

Status FakeCloseFrontends() {
  return Status(kOk);
}

}  // namespace

WebViewInfo::WebViewInfo(const std::string& id,
                         const std::string& debugger_url,
                         const std::string& url,
                         Type type)
    : id(id), debugger_url(debugger_url), url(url), type(type) {}

WebViewInfo::~WebViewInfo() {}

bool WebViewInfo::IsFrontend() const {
  return url.find("chrome-devtools://") == 0u;
}

WebViewsInfo::WebViewsInfo() {}

WebViewsInfo::WebViewsInfo(const std::vector<WebViewInfo>& info)
    : views_info(info) {}

WebViewsInfo::~WebViewsInfo() {}

const WebViewInfo& WebViewsInfo::Get(int index) const {
  return views_info[index];
}

size_t WebViewsInfo::GetSize() const {
  return views_info.size();
}

const WebViewInfo* WebViewsInfo::GetForId(const std::string& id) const {
  for (size_t i = 0; i < views_info.size(); ++i) {
    if (views_info[i].id == id)
      return &views_info[i];
  }
  return NULL;
}

DevToolsHttpClient::DevToolsHttpClient(
    const NetAddress& address,
    scoped_refptr<URLRequestContextGetter> context_getter,
    const SyncWebSocketFactory& socket_factory)
    : context_getter_(context_getter),
      socket_factory_(socket_factory),
      server_url_("http://" + address.ToString()),
      web_socket_url_prefix_(base::StringPrintf(
          "ws://%s/devtools/page/", address.ToString().c_str())) {}

DevToolsHttpClient::~DevToolsHttpClient() {}

Status DevToolsHttpClient::Init(const base::TimeDelta& timeout) {
  base::TimeTicks deadline = base::TimeTicks::Now() + timeout;
  std::string devtools_version;
  while (true) {
    Status status = GetVersion(&devtools_version);
    if (status.IsOk())
      break;
    if (status.code() != kChromeNotReachable ||
        base::TimeTicks::Now() > deadline) {
      return status;
    }
    base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
  }

  int kToTBuildNo = 9999;
  if (devtools_version.empty()) {
    // Content Shell has an empty product version and a fake user agent.
    // There's no way to detect the actual version, so assume it is tip of tree.
    version_ = "content shell";
    build_no_ = kToTBuildNo;
    return Status(kOk);
  }
  if (devtools_version.find("Version/") == 0u) {
    version_ = "webview";
    build_no_ = kToTBuildNo;
    return Status(kOk);
  }
  std::string prefix = "Chrome/";
  if (devtools_version.find(prefix) != 0u) {
    return Status(kUnknownError,
                  "unrecognized Chrome version: " + devtools_version);
  }

  std::string stripped_version = devtools_version.substr(prefix.length());
  int temp_build_no;
  std::vector<std::string> version_parts;
  base::SplitString(stripped_version, '.', &version_parts);
  if (version_parts.size() != 4 ||
      !base::StringToInt(version_parts[2], &temp_build_no)) {
    return Status(kUnknownError,
                  "unrecognized Chrome version: " + devtools_version);
  }

  version_ = stripped_version;
  build_no_ = temp_build_no;
  return Status(kOk);
}

Status DevToolsHttpClient::GetWebViewsInfo(WebViewsInfo* views_info) {
  std::string data;
  if (!FetchUrlAndLog(server_url_ + "/json", context_getter_.get(), &data))
    return Status(kChromeNotReachable);

  return internal::ParseWebViewsInfo(data, views_info);
}

scoped_ptr<DevToolsClient> DevToolsHttpClient::CreateClient(
    const std::string& id) {
  return scoped_ptr<DevToolsClient>(new DevToolsClientImpl(
      socket_factory_,
      web_socket_url_prefix_ + id,
      id,
      base::Bind(
          &DevToolsHttpClient::CloseFrontends, base::Unretained(this), id)));
}

Status DevToolsHttpClient::CloseWebView(const std::string& id) {
  std::string data;
  if (!FetchUrlAndLog(
          server_url_ + "/json/close/" + id, context_getter_.get(), &data)) {
    return Status(kOk);  // Closing the last web view leads chrome to quit.
  }

  // Wait for the target window to be completely closed.
  base::TimeTicks deadline =
      base::TimeTicks::Now() + base::TimeDelta::FromSeconds(20);
  while (base::TimeTicks::Now() < deadline) {
    WebViewsInfo views_info;
    Status status = GetWebViewsInfo(&views_info);
    if (status.code() == kChromeNotReachable)
      return Status(kOk);
    if (status.IsError())
      return status;
    if (!views_info.GetForId(id))
      return Status(kOk);
    base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
  }
  return Status(kUnknownError, "failed to close window in 20 seconds");
}

Status DevToolsHttpClient::ActivateWebView(const std::string& id) {
  std::string data;
  if (!FetchUrlAndLog(
          server_url_ + "/json/activate/" + id, context_getter_.get(), &data))
    return Status(kUnknownError, "cannot activate web view");
  return Status(kOk);
}

const std::string& DevToolsHttpClient::version() const {
  return version_;
}

int DevToolsHttpClient::build_no() const {
  return build_no_;
}

Status DevToolsHttpClient::GetVersion(std::string* version) {
  std::string data;
  if (!FetchUrlAndLog(
          server_url_ + "/json/version", context_getter_.get(), &data))
    return Status(kChromeNotReachable);

  return internal::ParseVersionInfo(data, version);
}

Status DevToolsHttpClient::CloseFrontends(const std::string& for_client_id) {
  WebViewsInfo views_info;
  Status status = GetWebViewsInfo(&views_info);
  if (status.IsError())
    return status;

  // Close frontends. Usually frontends are docked in the same page, although
  // some may be in tabs (undocked, chrome://inspect, the DevTools
  // discovery page, etc.). Tabs can be closed via the DevTools HTTP close
  // URL, but docked frontends can only be closed, by design, by connecting
  // to them and clicking the close button. Close the tab frontends first
  // in case one of them is debugging a docked frontend, which would prevent
  // the code from being able to connect to the docked one.
  std::list<std::string> tab_frontend_ids;
  std::list<std::string> docked_frontend_ids;
  for (size_t i = 0; i < views_info.GetSize(); ++i) {
    const WebViewInfo& view_info = views_info.Get(i);
    if (view_info.IsFrontend()) {
      if (view_info.type == WebViewInfo::kPage)
        tab_frontend_ids.push_back(view_info.id);
      else if (view_info.type == WebViewInfo::kOther)
        docked_frontend_ids.push_back(view_info.id);
      else
        return Status(kUnknownError, "unknown type of DevTools frontend");
    }
  }

  for (std::list<std::string>::const_iterator it = tab_frontend_ids.begin();
       it != tab_frontend_ids.end(); ++it) {
    status = CloseWebView(*it);
    if (status.IsError())
      return status;
  }

  for (std::list<std::string>::const_iterator it = docked_frontend_ids.begin();
       it != docked_frontend_ids.end(); ++it) {
    scoped_ptr<DevToolsClient> client(new DevToolsClientImpl(
        socket_factory_,
        web_socket_url_prefix_ + *it,
        *it,
        base::Bind(&FakeCloseFrontends)));
    scoped_ptr<WebViewImpl> web_view(
        new WebViewImpl(*it, build_no_, client.Pass()));

    status = web_view->ConnectIfNecessary();
    // Ignore disconnected error, because the debugger might have closed when
    // its container page was closed above.
    if (status.IsError() && status.code() != kDisconnected)
      return status;

    scoped_ptr<base::Value> result;
    status = web_view->EvaluateScript(
        std::string(),
        "document.querySelector('*[id^=\"close-button-\"]').click();",
        &result);
    // Ignore disconnected error, because it may be closed already.
    if (status.IsError() && status.code() != kDisconnected)
      return status;
  }

  // Wait until DevTools UI disconnects from the given web view.
  base::TimeTicks deadline =
      base::TimeTicks::Now() + base::TimeDelta::FromSeconds(20);
  while (base::TimeTicks::Now() < deadline) {
    status = GetWebViewsInfo(&views_info);
    if (status.IsError())
      return status;

    const WebViewInfo* view_info = views_info.GetForId(for_client_id);
    if (!view_info)
      return Status(kNoSuchWindow, "window was already closed");
    if (view_info->debugger_url.size())
      return Status(kOk);

    base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
  }
  return Status(kUnknownError, "failed to close UI debuggers");
}

bool DevToolsHttpClient::FetchUrlAndLog(const std::string& url,
                                        URLRequestContextGetter* getter,
                                        std::string* response) {
  VLOG(1) << "DevTools request: " << url;
  bool ok = FetchUrl(url, getter, response);
  if (ok) {
    VLOG(1) << "DevTools response: " << *response;
  } else {
    VLOG(1) << "DevTools request failed";
  }
  return ok;
}

namespace internal {

Status ParseWebViewsInfo(const std::string& data,
                         WebViewsInfo* views_info) {
  scoped_ptr<base::Value> value(base::JSONReader::Read(data));
  if (!value.get())
    return Status(kUnknownError, "DevTools returned invalid JSON");
  base::ListValue* list;
  if (!value->GetAsList(&list))
    return Status(kUnknownError, "DevTools did not return list");

  std::vector<WebViewInfo> temp_views_info;
  for (size_t i = 0; i < list->GetSize(); ++i) {
    base::DictionaryValue* info;
    if (!list->GetDictionary(i, &info))
      return Status(kUnknownError, "DevTools contains non-dictionary item");
    std::string id;
    if (!info->GetString("id", &id))
      return Status(kUnknownError, "DevTools did not include id");
    std::string type_as_string;
    if (!info->GetString("type", &type_as_string))
      return Status(kUnknownError, "DevTools did not include type");
    std::string url;
    if (!info->GetString("url", &url))
      return Status(kUnknownError, "DevTools did not include url");
    std::string debugger_url;
    info->GetString("webSocketDebuggerUrl", &debugger_url);
    WebViewInfo::Type type;
    if (type_as_string == "app")
      type = WebViewInfo::kApp;
    else if (type_as_string == "background_page")
      type = WebViewInfo::kBackgroundPage;
    else if (type_as_string == "page")
      type = WebViewInfo::kPage;
    else if (type_as_string == "worker")
      type = WebViewInfo::kWorker;
    else if (type_as_string == "other")
      type = WebViewInfo::kOther;
    else
      return Status(kUnknownError,
                    "DevTools returned unknown type:" + type_as_string);
    temp_views_info.push_back(WebViewInfo(id, debugger_url, url, type));
  }
  *views_info = WebViewsInfo(temp_views_info);
  return Status(kOk);
}

Status ParseVersionInfo(const std::string& data,
                        std::string* version) {
  scoped_ptr<base::Value> value(base::JSONReader::Read(data));
  if (!value.get())
    return Status(kUnknownError, "version info not in JSON");
  base::DictionaryValue* dict;
  if (!value->GetAsDictionary(&dict))
    return Status(kUnknownError, "version info not a dictionary");
  if (!dict->GetString("Browser", version)) {
    return Status(
        kUnknownError,
        "Chrome version must be >= " + GetMinimumSupportedChromeVersion(),
        Status(kUnknownError, "version info doesn't include string 'Browser'"));
  }
  return Status(kOk);
}

}  // namespace internal

/* [<][>][^][v][top][bottom][index][help] */