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     int port,
     69     scoped_refptr<URLRequestContextGetter> context_getter,
     70     const SyncWebSocketFactory& socket_factory,
     71     Log* log)
     72     : context_getter_(context_getter),
     73       socket_factory_(socket_factory),
     74       log_(log),
     75       server_url_(base::StringPrintf("http://127.0.0.1:%d", port)),
     76       web_socket_url_prefix_(
     77           base::StringPrintf("ws://127.0.0.1:%d/devtools/page/", port)) {}
     78 
     79 DevToolsHttpClient::~DevToolsHttpClient() {}
     80 
     81 Status DevToolsHttpClient::Init(const base::TimeDelta& timeout) {
     82   base::Time deadline = base::Time::Now() + timeout;
     83   std::string devtools_version;
     84   while (true) {
     85     Status status = GetVersion(&devtools_version);
     86     if (status.IsOk())
     87       break;
     88     if (status.code() != kChromeNotReachable || base::Time::Now() > deadline)
     89       return status;
     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       log_));
    144 }
    145 
    146 Status DevToolsHttpClient::CloseWebView(const std::string& id) {
    147   std::string data;
    148   if (!FetchUrlAndLog(
    149           server_url_ + "/json/close/" + id, context_getter_.get(), &data)) {
    150     return Status(kOk);  // Closing the last web view leads chrome to quit.
    151   }
    152 
    153   // Wait for the target window to be completely closed.
    154   base::Time deadline = base::Time::Now() + base::TimeDelta::FromSeconds(20);
    155   while (base::Time::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 const std::string& DevToolsHttpClient::version() const {
    170   return version_;
    171 }
    172 
    173 int DevToolsHttpClient::build_no() const {
    174   return build_no_;
    175 }
    176 
    177 Status DevToolsHttpClient::GetVersion(std::string* version) {
    178   std::string data;
    179   if (!FetchUrlAndLog(
    180           server_url_ + "/json/version", context_getter_.get(), &data))
    181     return Status(kChromeNotReachable);
    182 
    183   return internal::ParseVersionInfo(data, version);
    184 }
    185 
    186 Status DevToolsHttpClient::CloseFrontends(const std::string& for_client_id) {
    187   WebViewsInfo views_info;
    188   Status status = GetWebViewsInfo(&views_info);
    189   if (status.IsError())
    190     return status;
    191 
    192   // Close frontends. Usually frontends are docked in the same page, although
    193   // some may be in tabs (undocked, chrome://inspect, the DevTools
    194   // discovery page, etc.). Tabs can be closed via the DevTools HTTP close
    195   // URL, but docked frontends can only be closed, by design, by connecting
    196   // to them and clicking the close button. Close the tab frontends first
    197   // in case one of them is debugging a docked frontend, which would prevent
    198   // the code from being able to connect to the docked one.
    199   std::list<std::string> tab_frontend_ids;
    200   std::list<std::string> docked_frontend_ids;
    201   for (size_t i = 0; i < views_info.GetSize(); ++i) {
    202     const WebViewInfo& view_info = views_info.Get(i);
    203     if (view_info.IsFrontend()) {
    204       if (view_info.type == WebViewInfo::kPage)
    205         tab_frontend_ids.push_back(view_info.id);
    206       else if (view_info.type == WebViewInfo::kOther)
    207         docked_frontend_ids.push_back(view_info.id);
    208       else
    209         return Status(kUnknownError, "unknown type of DevTools frontend");
    210     }
    211   }
    212 
    213   for (std::list<std::string>::const_iterator it = tab_frontend_ids.begin();
    214        it != tab_frontend_ids.end(); ++it) {
    215     status = CloseWebView(*it);
    216     if (status.IsError())
    217       return status;
    218   }
    219 
    220   for (std::list<std::string>::const_iterator it = docked_frontend_ids.begin();
    221        it != docked_frontend_ids.end(); ++it) {
    222     scoped_ptr<DevToolsClient> client(new DevToolsClientImpl(
    223         socket_factory_,
    224         web_socket_url_prefix_ + *it,
    225         *it,
    226         base::Bind(&FakeCloseFrontends),
    227         log_));
    228     scoped_ptr<WebViewImpl> web_view(
    229         new WebViewImpl(*it, build_no_, client.Pass(), log_));
    230 
    231     status = web_view->ConnectIfNecessary();
    232     // Ignore disconnected error, because the debugger might have closed when
    233     // its container page was closed above.
    234     if (status.IsError() && status.code() != kDisconnected)
    235       return status;
    236 
    237     scoped_ptr<base::Value> result;
    238     status = web_view->EvaluateScript(
    239         std::string(),
    240         "document.querySelector('*[id^=\"close-button-\"]').click();",
    241         &result);
    242     // Ignore disconnected error, because it may be closed already.
    243     if (status.IsError() && status.code() != kDisconnected)
    244       return status;
    245   }
    246 
    247   // Wait until DevTools UI disconnects from the given web view.
    248   base::Time deadline = base::Time::Now() + base::TimeDelta::FromSeconds(20);
    249   while (base::Time::Now() < deadline) {
    250     status = GetWebViewsInfo(&views_info);
    251     if (status.IsError())
    252       return status;
    253 
    254     const WebViewInfo* view_info = views_info.GetForId(for_client_id);
    255     if (!view_info) {
    256       return Status(kDisconnected,
    257                     "DevTools client closed during closing UI debuggers");
    258     }
    259     if (view_info->debugger_url.size())
    260       return Status(kOk);
    261 
    262     base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
    263   }
    264   return Status(kUnknownError, "failed to close UI debuggers");
    265 }
    266 
    267 bool DevToolsHttpClient::FetchUrlAndLog(const std::string& url,
    268                                         URLRequestContextGetter* getter,
    269                                         std::string* response) {
    270   log_->AddEntry(Log::kDebug, "devtools request: " + url);
    271   bool ok = FetchUrl(url, getter, response);
    272   if (ok)
    273     log_->AddEntry(Log::kDebug, "devtools response: " + *response);
    274   else
    275     log_->AddEntry(Log::kDebug, "devtools request failed");
    276   return ok;
    277 }
    278 
    279 namespace internal {
    280 
    281 Status ParseWebViewsInfo(const std::string& data,
    282                          WebViewsInfo* views_info) {
    283   scoped_ptr<base::Value> value(base::JSONReader::Read(data));
    284   if (!value.get())
    285     return Status(kUnknownError, "DevTools returned invalid JSON");
    286   base::ListValue* list;
    287   if (!value->GetAsList(&list))
    288     return Status(kUnknownError, "DevTools did not return list");
    289 
    290   std::vector<WebViewInfo> temp_views_info;
    291   for (size_t i = 0; i < list->GetSize(); ++i) {
    292     base::DictionaryValue* info;
    293     if (!list->GetDictionary(i, &info))
    294       return Status(kUnknownError, "DevTools contains non-dictionary item");
    295     std::string id;
    296     if (!info->GetString("id", &id))
    297       return Status(kUnknownError, "DevTools did not include id");
    298     std::string type;
    299     if (!info->GetString("type", &type))
    300       return Status(kUnknownError, "DevTools did not include type");
    301     std::string url;
    302     if (!info->GetString("url", &url))
    303       return Status(kUnknownError, "DevTools did not include url");
    304     std::string debugger_url;
    305     info->GetString("webSocketDebuggerUrl", &debugger_url);
    306     if (type == "page")
    307       temp_views_info.push_back(
    308           WebViewInfo(id, debugger_url, url, WebViewInfo::kPage));
    309     else if (type == "other")
    310       temp_views_info.push_back(
    311           WebViewInfo(id, debugger_url, url, WebViewInfo::kOther));
    312     else
    313       return Status(kUnknownError, "DevTools returned unknown type:" + type);
    314   }
    315   *views_info = WebViewsInfo(temp_views_info);
    316   return Status(kOk);
    317 }
    318 
    319 Status ParseVersionInfo(const std::string& data,
    320                         std::string* version) {
    321   scoped_ptr<base::Value> value(base::JSONReader::Read(data));
    322   if (!value.get())
    323     return Status(kUnknownError, "version info not in JSON");
    324   base::DictionaryValue* dict;
    325   if (!value->GetAsDictionary(&dict))
    326     return Status(kUnknownError, "version info not a dictionary");
    327   if (!dict->GetString("Browser", version)) {
    328     return Status(
    329         kUnknownError,
    330         "Chrome version must be >= " + GetMinimumSupportedChromeVersion(),
    331         Status(kUnknownError, "version info doesn't include string 'Browser'"));
    332   }
    333   return Status(kOk);
    334 }
    335 
    336 }  // namespace internal
    337