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
FakeCloseFrontends()26 Status FakeCloseFrontends() {
27 return Status(kOk);
28 }
29
30 } // namespace
31
WebViewInfo(const std::string & id,const std::string & debugger_url,const std::string & url,Type type)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
~WebViewInfo()38 WebViewInfo::~WebViewInfo() {}
39
IsFrontend() const40 bool WebViewInfo::IsFrontend() const {
41 return url.find("chrome-devtools://") == 0u;
42 }
43
WebViewsInfo()44 WebViewsInfo::WebViewsInfo() {}
45
WebViewsInfo(const std::vector<WebViewInfo> & info)46 WebViewsInfo::WebViewsInfo(const std::vector<WebViewInfo>& info)
47 : views_info(info) {}
48
~WebViewsInfo()49 WebViewsInfo::~WebViewsInfo() {}
50
Get(int index) const51 const WebViewInfo& WebViewsInfo::Get(int index) const {
52 return views_info[index];
53 }
54
GetSize() const55 size_t WebViewsInfo::GetSize() const {
56 return views_info.size();
57 }
58
GetForId(const std::string & id) const59 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
DevToolsHttpClient(const NetAddress & address,scoped_refptr<URLRequestContextGetter> context_getter,const SyncWebSocketFactory & socket_factory)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
~DevToolsHttpClient()77 DevToolsHttpClient::~DevToolsHttpClient() {}
78
Init(const base::TimeDelta & timeout)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
GetWebViewsInfo(WebViewsInfo * views_info)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
CreateClient(const std::string & id)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
CloseWebView(const std::string & id)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
ActivateWebView(const std::string & id)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
version() const177 const std::string& DevToolsHttpClient::version() const {
178 return version_;
179 }
180
build_no() const181 int DevToolsHttpClient::build_no() const {
182 return build_no_;
183 }
184
GetVersion(std::string * version)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
CloseFrontends(const std::string & for_client_id)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
FetchUrlAndLog(const std::string & url,URLRequestContextGetter * getter,std::string * response)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
ParseWebViewsInfo(const std::string & data,WebViewsInfo * views_info)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
ParseVersionInfo(const std::string & data,std::string * version)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