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