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/stringprintf.h" 11#include "base/threading/platform_thread.h" 12#include "base/time/time.h" 13#include "base/values.h" 14#include "chrome/test/chromedriver/chrome/device_metrics.h" 15#include "chrome/test/chromedriver/chrome/devtools_client_impl.h" 16#include "chrome/test/chromedriver/chrome/log.h" 17#include "chrome/test/chromedriver/chrome/status.h" 18#include "chrome/test/chromedriver/chrome/web_view_impl.h" 19#include "chrome/test/chromedriver/net/net_util.h" 20#include "chrome/test/chromedriver/net/url_request_context_getter.h" 21 22WebViewInfo::WebViewInfo(const std::string& id, 23 const std::string& debugger_url, 24 const std::string& url, 25 Type type) 26 : id(id), debugger_url(debugger_url), url(url), type(type) {} 27 28WebViewInfo::~WebViewInfo() {} 29 30bool WebViewInfo::IsFrontend() const { 31 return url.find("chrome-devtools://") == 0u; 32} 33 34WebViewsInfo::WebViewsInfo() {} 35 36WebViewsInfo::WebViewsInfo(const std::vector<WebViewInfo>& info) 37 : views_info(info) {} 38 39WebViewsInfo::~WebViewsInfo() {} 40 41const WebViewInfo& WebViewsInfo::Get(int index) const { 42 return views_info[index]; 43} 44 45size_t WebViewsInfo::GetSize() const { 46 return views_info.size(); 47} 48 49const WebViewInfo* WebViewsInfo::GetForId(const std::string& id) const { 50 for (size_t i = 0; i < views_info.size(); ++i) { 51 if (views_info[i].id == id) 52 return &views_info[i]; 53 } 54 return NULL; 55} 56 57DevToolsHttpClient::DevToolsHttpClient( 58 const NetAddress& address, 59 scoped_refptr<URLRequestContextGetter> context_getter, 60 const SyncWebSocketFactory& socket_factory, 61 scoped_ptr<DeviceMetrics> device_metrics) 62 : context_getter_(context_getter), 63 socket_factory_(socket_factory), 64 server_url_("http://" + address.ToString()), 65 web_socket_url_prefix_(base::StringPrintf( 66 "ws://%s/devtools/page/", address.ToString().c_str())), 67 device_metrics_(device_metrics.Pass()) {} 68 69DevToolsHttpClient::~DevToolsHttpClient() {} 70 71Status DevToolsHttpClient::Init(const base::TimeDelta& timeout) { 72 base::TimeTicks deadline = base::TimeTicks::Now() + timeout; 73 std::string version_url = server_url_ + "/json/version"; 74 std::string data; 75 76 while (!FetchUrlAndLog(version_url, context_getter_.get(), &data) 77 || data.empty()) { 78 if (base::TimeTicks::Now() > deadline) 79 return Status(kChromeNotReachable); 80 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50)); 81 } 82 83 return ParseBrowserInfo(data, &browser_info_); 84} 85 86Status DevToolsHttpClient::GetWebViewsInfo(WebViewsInfo* views_info) { 87 std::string data; 88 if (!FetchUrlAndLog(server_url_ + "/json", context_getter_.get(), &data)) 89 return Status(kChromeNotReachable); 90 91 return internal::ParseWebViewsInfo(data, views_info); 92} 93 94scoped_ptr<DevToolsClient> DevToolsHttpClient::CreateClient( 95 const std::string& id) { 96 return scoped_ptr<DevToolsClient>(new DevToolsClientImpl( 97 socket_factory_, 98 web_socket_url_prefix_ + id, 99 id, 100 base::Bind( 101 &DevToolsHttpClient::CloseFrontends, base::Unretained(this), id))); 102} 103 104Status DevToolsHttpClient::CloseWebView(const std::string& id) { 105 std::string data; 106 if (!FetchUrlAndLog( 107 server_url_ + "/json/close/" + id, context_getter_.get(), &data)) { 108 return Status(kOk); // Closing the last web view leads chrome to quit. 109 } 110 111 // Wait for the target window to be completely closed. 112 base::TimeTicks deadline = 113 base::TimeTicks::Now() + base::TimeDelta::FromSeconds(20); 114 while (base::TimeTicks::Now() < deadline) { 115 WebViewsInfo views_info; 116 Status status = GetWebViewsInfo(&views_info); 117 if (status.code() == kChromeNotReachable) 118 return Status(kOk); 119 if (status.IsError()) 120 return status; 121 if (!views_info.GetForId(id)) 122 return Status(kOk); 123 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50)); 124 } 125 return Status(kUnknownError, "failed to close window in 20 seconds"); 126} 127 128Status DevToolsHttpClient::ActivateWebView(const std::string& id) { 129 std::string data; 130 if (!FetchUrlAndLog( 131 server_url_ + "/json/activate/" + id, context_getter_.get(), &data)) 132 return Status(kUnknownError, "cannot activate web view"); 133 return Status(kOk); 134} 135 136const BrowserInfo* DevToolsHttpClient::browser_info() { 137 return &browser_info_; 138} 139 140const DeviceMetrics* DevToolsHttpClient::device_metrics() { 141 return device_metrics_.get(); 142} 143 144Status DevToolsHttpClient::CloseFrontends(const std::string& for_client_id) { 145 WebViewsInfo views_info; 146 Status status = GetWebViewsInfo(&views_info); 147 if (status.IsError()) 148 return status; 149 150 // Close frontends. Usually frontends are docked in the same page, although 151 // some may be in tabs (undocked, chrome://inspect, the DevTools 152 // discovery page, etc.). Tabs can be closed via the DevTools HTTP close 153 // URL, but docked frontends can only be closed, by design, by connecting 154 // to them and clicking the close button. Close the tab frontends first 155 // in case one of them is debugging a docked frontend, which would prevent 156 // the code from being able to connect to the docked one. 157 std::list<std::string> tab_frontend_ids; 158 std::list<std::string> docked_frontend_ids; 159 for (size_t i = 0; i < views_info.GetSize(); ++i) { 160 const WebViewInfo& view_info = views_info.Get(i); 161 if (view_info.IsFrontend()) { 162 if (view_info.type == WebViewInfo::kPage) 163 tab_frontend_ids.push_back(view_info.id); 164 else if (view_info.type == WebViewInfo::kOther) 165 docked_frontend_ids.push_back(view_info.id); 166 else 167 return Status(kUnknownError, "unknown type of DevTools frontend"); 168 } 169 } 170 171 for (std::list<std::string>::const_iterator it = tab_frontend_ids.begin(); 172 it != tab_frontend_ids.end(); ++it) { 173 status = CloseWebView(*it); 174 if (status.IsError()) 175 return status; 176 } 177 178 for (std::list<std::string>::const_iterator it = docked_frontend_ids.begin(); 179 it != docked_frontend_ids.end(); ++it) { 180 scoped_ptr<DevToolsClient> client(new DevToolsClientImpl( 181 socket_factory_, 182 web_socket_url_prefix_ + *it, 183 *it)); 184 scoped_ptr<WebViewImpl> web_view( 185 new WebViewImpl(*it, &browser_info_, client.Pass(), NULL)); 186 187 status = web_view->ConnectIfNecessary(); 188 // Ignore disconnected error, because the debugger might have closed when 189 // its container page was closed above. 190 if (status.IsError() && status.code() != kDisconnected) 191 return status; 192 193 scoped_ptr<base::Value> result; 194 status = web_view->EvaluateScript( 195 std::string(), 196 "document.querySelector('*[id^=\"close-button-\"]').click();", 197 &result); 198 // Ignore disconnected error, because it may be closed already. 199 if (status.IsError() && status.code() != kDisconnected) 200 return status; 201 } 202 203 // Wait until DevTools UI disconnects from the given web view. 204 base::TimeTicks deadline = 205 base::TimeTicks::Now() + base::TimeDelta::FromSeconds(20); 206 while (base::TimeTicks::Now() < deadline) { 207 status = GetWebViewsInfo(&views_info); 208 if (status.IsError()) 209 return status; 210 211 const WebViewInfo* view_info = views_info.GetForId(for_client_id); 212 if (!view_info) 213 return Status(kNoSuchWindow, "window was already closed"); 214 if (view_info->debugger_url.size()) 215 return Status(kOk); 216 217 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50)); 218 } 219 return Status(kUnknownError, "failed to close UI debuggers"); 220} 221 222bool DevToolsHttpClient::FetchUrlAndLog(const std::string& url, 223 URLRequestContextGetter* getter, 224 std::string* response) { 225 VLOG(1) << "DevTools request: " << url; 226 bool ok = FetchUrl(url, getter, response); 227 if (ok) { 228 VLOG(1) << "DevTools response: " << *response; 229 } else { 230 VLOG(1) << "DevTools request failed"; 231 } 232 return ok; 233} 234 235namespace internal { 236 237Status ParseWebViewsInfo(const std::string& data, 238 WebViewsInfo* views_info) { 239 scoped_ptr<base::Value> value(base::JSONReader::Read(data)); 240 if (!value.get()) 241 return Status(kUnknownError, "DevTools returned invalid JSON"); 242 base::ListValue* list; 243 if (!value->GetAsList(&list)) 244 return Status(kUnknownError, "DevTools did not return list"); 245 246 std::vector<WebViewInfo> temp_views_info; 247 for (size_t i = 0; i < list->GetSize(); ++i) { 248 base::DictionaryValue* info; 249 if (!list->GetDictionary(i, &info)) 250 return Status(kUnknownError, "DevTools contains non-dictionary item"); 251 std::string id; 252 if (!info->GetString("id", &id)) 253 return Status(kUnknownError, "DevTools did not include id"); 254 std::string type_as_string; 255 if (!info->GetString("type", &type_as_string)) 256 return Status(kUnknownError, "DevTools did not include type"); 257 std::string url; 258 if (!info->GetString("url", &url)) 259 return Status(kUnknownError, "DevTools did not include url"); 260 std::string debugger_url; 261 info->GetString("webSocketDebuggerUrl", &debugger_url); 262 WebViewInfo::Type type; 263 if (type_as_string == "app") 264 type = WebViewInfo::kApp; 265 else if (type_as_string == "background_page") 266 type = WebViewInfo::kBackgroundPage; 267 else if (type_as_string == "page") 268 type = WebViewInfo::kPage; 269 else if (type_as_string == "worker") 270 type = WebViewInfo::kWorker; 271 else if (type_as_string == "other") 272 type = WebViewInfo::kOther; 273 else 274 return Status(kUnknownError, 275 "DevTools returned unknown type:" + type_as_string); 276 temp_views_info.push_back(WebViewInfo(id, debugger_url, url, type)); 277 } 278 *views_info = WebViewsInfo(temp_views_info); 279 return Status(kOk); 280} 281 282} // namespace internal 283