Home | History | Annotate | Download | only in chrome
      1 // Copyright (c) 2013 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.
      4 
      5 #include "chrome/test/chromedriver/chrome/devtools_http_client.h"
      6 
      7 #include "base/bind.h"
      8 #include "base/bind_helpers.h"
      9 #include "base/json/json_reader.h"
     10 #include "base/strings/string_number_conversions.h"
     11 #include "base/strings/string_split.h"
     12 #include "base/strings/stringprintf.h"
     13 #include "base/threading/platform_thread.h"
     14 #include "base/time/time.h"
     15 #include "base/values.h"
     16 #include "chrome/test/chromedriver/chrome/devtools_client_impl.h"
     17 #include "chrome/test/chromedriver/chrome/log.h"
     18 #include "chrome/test/chromedriver/chrome/status.h"
     19 #include "chrome/test/chromedriver/chrome/version.h"
     20 #include "chrome/test/chromedriver/chrome/web_view_impl.h"
     21 #include "chrome/test/chromedriver/net/net_util.h"
     22 #include "chrome/test/chromedriver/net/url_request_context_getter.h"
     23 
     24 namespace {
     25 
     26 Status FakeCloseFrontends() {
     27   return Status(kOk);
     28 }
     29 
     30 }  // namespace
     31 
     32 WebViewInfo::WebViewInfo(const std::string& id,
     33                          const std::string& debugger_url,
     34                          const std::string& url,
     35                          Type type)
     36     : id(id), debugger_url(debugger_url), url(url), type(type) {}
     37 
     38 WebViewInfo::~WebViewInfo() {}
     39 
     40 bool WebViewInfo::IsFrontend() const {
     41   return url.find("chrome-devtools://") == 0u;
     42 }
     43 
     44 WebViewsInfo::WebViewsInfo() {}
     45 
     46 WebViewsInfo::WebViewsInfo(const std::vector<WebViewInfo>& info)
     47     : views_info(info) {}
     48 
     49 WebViewsInfo::~WebViewsInfo() {}
     50 
     51 const WebViewInfo& WebViewsInfo::Get(int index) const {
     52   return views_info[index];
     53 }
     54 
     55 size_t WebViewsInfo::GetSize() const {
     56   return views_info.size();
     57 }
     58 
     59 const WebViewInfo* WebViewsInfo::GetForId(const std::string& id) const {
     60   for (size_t i = 0; i < views_info.size(); ++i) {
     61     if (views_info[i].id == id)
     62       return &views_info[i];
     63   }
     64   return NULL;
     65 }
     66 
     67 DevToolsHttpClient::DevToolsHttpClient(
     68     const NetAddress& address,
     69     scoped_refptr<URLRequestContextGetter> context_getter,
     70     const SyncWebSocketFactory& socket_factory)
     71     : context_getter_(context_getter),
     72       socket_factory_(socket_factory),
     73       server_url_("http://" + address.ToString()),
     74       web_socket_url_prefix_(base::StringPrintf(
     75           "ws://%s/devtools/page/", address.ToString().c_str())) {}
     76 
     77 DevToolsHttpClient::~DevToolsHttpClient() {}
     78 
     79 Status DevToolsHttpClient::Init(const base::TimeDelta& timeout) {
     80   base::TimeTicks deadline = base::TimeTicks::Now() + timeout;
     81   std::string devtools_version;
     82   while (true) {
     83     Status status = GetVersion(&devtools_version);
     84     if (status.IsOk())
     85       break;
     86     if (status.code() != kChromeNotReachable ||
     87         base::TimeTicks::Now() > deadline) {
     88       return status;
     89     }
     90     base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
     91   }
     92 
     93   int kToTBuildNo = 9999;
     94   if (devtools_version.empty()) {
     95     // Content Shell has an empty product version and a fake user agent.
     96     // There's no way to detect the actual version, so assume it is tip of tree.
     97     version_ = "content shell";
     98     build_no_ = kToTBuildNo;
     99     return Status(kOk);
    100   }
    101   if (devtools_version.find("Version/") == 0u) {
    102     version_ = "webview";
    103     build_no_ = kToTBuildNo;
    104     return Status(kOk);
    105   }
    106   std::string prefix = "Chrome/";
    107   if (devtools_version.find(prefix) != 0u) {
    108     return Status(kUnknownError,
    109                   "unrecognized Chrome version: " + devtools_version);
    110   }
    111 
    112   std::string stripped_version = devtools_version.substr(prefix.length());
    113   int temp_build_no;
    114   std::vector<std::string> version_parts;
    115   base::SplitString(stripped_version, '.', &version_parts);
    116   if (version_parts.size() != 4 ||
    117       !base::StringToInt(version_parts[2], &temp_build_no)) {
    118     return Status(kUnknownError,
    119                   "unrecognized Chrome version: " + devtools_version);
    120   }
    121 
    122   version_ = stripped_version;
    123   build_no_ = temp_build_no;
    124   return Status(kOk);
    125 }
    126 
    127 Status DevToolsHttpClient::GetWebViewsInfo(WebViewsInfo* views_info) {
    128   std::string data;
    129   if (!FetchUrlAndLog(server_url_ + "/json", context_getter_.get(), &data))
    130     return Status(kChromeNotReachable);
    131 
    132   return internal::ParseWebViewsInfo(data, views_info);
    133 }
    134 
    135 scoped_ptr<DevToolsClient> DevToolsHttpClient::CreateClient(
    136     const std::string& id) {
    137   return scoped_ptr<DevToolsClient>(new DevToolsClientImpl(
    138       socket_factory_,
    139       web_socket_url_prefix_ + id,
    140       id,
    141       base::Bind(
    142           &DevToolsHttpClient::CloseFrontends, base::Unretained(this), id)));
    143 }
    144 
    145 Status DevToolsHttpClient::CloseWebView(const std::string& id) {
    146   std::string data;
    147   if (!FetchUrlAndLog(
    148           server_url_ + "/json/close/" + id, context_getter_.get(), &data)) {
    149     return Status(kOk);  // Closing the last web view leads chrome to quit.
    150   }
    151 
    152   // Wait for the target window to be completely closed.
    153   base::TimeTicks deadline =
    154       base::TimeTicks::Now() + base::TimeDelta::FromSeconds(20);
    155   while (base::TimeTicks::Now() < deadline) {
    156     WebViewsInfo views_info;
    157     Status status = GetWebViewsInfo(&views_info);
    158     if (status.code() == kChromeNotReachable)
    159       return Status(kOk);
    160     if (status.IsError())
    161       return status;
    162     if (!views_info.GetForId(id))
    163       return Status(kOk);
    164     base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
    165   }
    166   return Status(kUnknownError, "failed to close window in 20 seconds");
    167 }
    168 
    169 Status DevToolsHttpClient::ActivateWebView(const std::string& id) {
    170   std::string data;
    171   if (!FetchUrlAndLog(
    172           server_url_ + "/json/activate/" + id, context_getter_.get(), &data))
    173     return Status(kUnknownError, "cannot activate web view");
    174   return Status(kOk);
    175 }
    176 
    177 const std::string& DevToolsHttpClient::version() const {
    178   return version_;
    179 }
    180 
    181 int DevToolsHttpClient::build_no() const {
    182   return build_no_;
    183 }
    184 
    185 Status DevToolsHttpClient::GetVersion(std::string* version) {
    186   std::string data;
    187   if (!FetchUrlAndLog(
    188           server_url_ + "/json/version", context_getter_.get(), &data))
    189     return Status(kChromeNotReachable);
    190 
    191   return internal::ParseVersionInfo(data, version);
    192 }
    193 
    194 Status DevToolsHttpClient::CloseFrontends(const std::string& for_client_id) {
    195   WebViewsInfo views_info;
    196   Status status = GetWebViewsInfo(&views_info);
    197   if (status.IsError())
    198     return status;
    199 
    200   // Close frontends. Usually frontends are docked in the same page, although
    201   // some may be in tabs (undocked, chrome://inspect, the DevTools
    202   // discovery page, etc.). Tabs can be closed via the DevTools HTTP close
    203   // URL, but docked frontends can only be closed, by design, by connecting
    204   // to them and clicking the close button. Close the tab frontends first
    205   // in case one of them is debugging a docked frontend, which would prevent
    206   // the code from being able to connect to the docked one.
    207   std::list<std::string> tab_frontend_ids;
    208   std::list<std::string> docked_frontend_ids;
    209   for (size_t i = 0; i < views_info.GetSize(); ++i) {
    210     const WebViewInfo& view_info = views_info.Get(i);
    211     if (view_info.IsFrontend()) {
    212       if (view_info.type == WebViewInfo::kPage)
    213         tab_frontend_ids.push_back(view_info.id);
    214       else if (view_info.type == WebViewInfo::kOther)
    215         docked_frontend_ids.push_back(view_info.id);
    216       else
    217         return Status(kUnknownError, "unknown type of DevTools frontend");
    218     }
    219   }
    220 
    221   for (std::list<std::string>::const_iterator it = tab_frontend_ids.begin();
    222        it != tab_frontend_ids.end(); ++it) {
    223     status = CloseWebView(*it);
    224     if (status.IsError())
    225       return status;
    226   }
    227 
    228   for (std::list<std::string>::const_iterator it = docked_frontend_ids.begin();
    229        it != docked_frontend_ids.end(); ++it) {
    230     scoped_ptr<DevToolsClient> client(new DevToolsClientImpl(
    231         socket_factory_,
    232         web_socket_url_prefix_ + *it,
    233         *it,
    234         base::Bind(&FakeCloseFrontends)));
    235     scoped_ptr<WebViewImpl> web_view(
    236         new WebViewImpl(*it, build_no_, client.Pass()));
    237 
    238     status = web_view->ConnectIfNecessary();
    239     // Ignore disconnected error, because the debugger might have closed when
    240     // its container page was closed above.
    241     if (status.IsError() && status.code() != kDisconnected)
    242       return status;
    243 
    244     scoped_ptr<base::Value> result;
    245     status = web_view->EvaluateScript(
    246         std::string(),
    247         "document.querySelector('*[id^=\"close-button-\"]').click();",
    248         &result);
    249     // Ignore disconnected error, because it may be closed already.
    250     if (status.IsError() && status.code() != kDisconnected)
    251       return status;
    252   }
    253 
    254   // Wait until DevTools UI disconnects from the given web view.
    255   base::TimeTicks deadline =
    256       base::TimeTicks::Now() + base::TimeDelta::FromSeconds(20);
    257   while (base::TimeTicks::Now() < deadline) {
    258     status = GetWebViewsInfo(&views_info);
    259     if (status.IsError())
    260       return status;
    261 
    262     const WebViewInfo* view_info = views_info.GetForId(for_client_id);
    263     if (!view_info)
    264       return Status(kNoSuchWindow, "window was already closed");
    265     if (view_info->debugger_url.size())
    266       return Status(kOk);
    267 
    268     base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
    269   }
    270   return Status(kUnknownError, "failed to close UI debuggers");
    271 }
    272 
    273 bool DevToolsHttpClient::FetchUrlAndLog(const std::string& url,
    274                                         URLRequestContextGetter* getter,
    275                                         std::string* response) {
    276   VLOG(1) << "DevTools request: " << url;
    277   bool ok = FetchUrl(url, getter, response);
    278   if (ok) {
    279     VLOG(1) << "DevTools response: " << *response;
    280   } else {
    281     VLOG(1) << "DevTools request failed";
    282   }
    283   return ok;
    284 }
    285 
    286 namespace internal {
    287 
    288 Status ParseWebViewsInfo(const std::string& data,
    289                          WebViewsInfo* views_info) {
    290   scoped_ptr<base::Value> value(base::JSONReader::Read(data));
    291   if (!value.get())
    292     return Status(kUnknownError, "DevTools returned invalid JSON");
    293   base::ListValue* list;
    294   if (!value->GetAsList(&list))
    295     return Status(kUnknownError, "DevTools did not return list");
    296 
    297   std::vector<WebViewInfo> temp_views_info;
    298   for (size_t i = 0; i < list->GetSize(); ++i) {
    299     base::DictionaryValue* info;
    300     if (!list->GetDictionary(i, &info))
    301       return Status(kUnknownError, "DevTools contains non-dictionary item");
    302     std::string id;
    303     if (!info->GetString("id", &id))
    304       return Status(kUnknownError, "DevTools did not include id");
    305     std::string type_as_string;
    306     if (!info->GetString("type", &type_as_string))
    307       return Status(kUnknownError, "DevTools did not include type");
    308     std::string url;
    309     if (!info->GetString("url", &url))
    310       return Status(kUnknownError, "DevTools did not include url");
    311     std::string debugger_url;
    312     info->GetString("webSocketDebuggerUrl", &debugger_url);
    313     WebViewInfo::Type type;
    314     if (type_as_string == "app")
    315       type = WebViewInfo::kApp;
    316     else if (type_as_string == "background_page")
    317       type = WebViewInfo::kBackgroundPage;
    318     else if (type_as_string == "page")
    319       type = WebViewInfo::kPage;
    320     else if (type_as_string == "worker")
    321       type = WebViewInfo::kWorker;
    322     else if (type_as_string == "other")
    323       type = WebViewInfo::kOther;
    324     else
    325       return Status(kUnknownError,
    326                     "DevTools returned unknown type:" + type_as_string);
    327     temp_views_info.push_back(WebViewInfo(id, debugger_url, url, type));
    328   }
    329   *views_info = WebViewsInfo(temp_views_info);
    330   return Status(kOk);
    331 }
    332 
    333 Status ParseVersionInfo(const std::string& data,
    334                         std::string* version) {
    335   scoped_ptr<base::Value> value(base::JSONReader::Read(data));
    336   if (!value.get())
    337     return Status(kUnknownError, "version info not in JSON");
    338   base::DictionaryValue* dict;
    339   if (!value->GetAsDictionary(&dict))
    340     return Status(kUnknownError, "version info not a dictionary");
    341   if (!dict->GetString("Browser", version)) {
    342     return Status(
    343         kUnknownError,
    344         "Chrome version must be >= " + GetMinimumSupportedChromeVersion(),
    345         Status(kUnknownError, "version info doesn't include string 'Browser'"));
    346   }
    347   return Status(kOk);
    348 }
    349 
    350 }  // namespace internal
    351