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