• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/device_metrics.h"
17 #include "chrome/test/chromedriver/chrome/devtools_client_impl.h"
18 #include "chrome/test/chromedriver/chrome/log.h"
19 #include "chrome/test/chromedriver/chrome/status.h"
20 #include "chrome/test/chromedriver/chrome/version.h"
21 #include "chrome/test/chromedriver/chrome/web_view_impl.h"
22 #include "chrome/test/chromedriver/net/net_util.h"
23 #include "chrome/test/chromedriver/net/url_request_context_getter.h"
24 
25 namespace {
26 
FakeCloseFrontends()27 Status FakeCloseFrontends() {
28   return Status(kOk);
29 }
30 
31 }  // namespace
32 
WebViewInfo(const std::string & id,const std::string & debugger_url,const std::string & url,Type type)33 WebViewInfo::WebViewInfo(const std::string& id,
34                          const std::string& debugger_url,
35                          const std::string& url,
36                          Type type)
37     : id(id), debugger_url(debugger_url), url(url), type(type) {}
38 
~WebViewInfo()39 WebViewInfo::~WebViewInfo() {}
40 
IsFrontend() const41 bool WebViewInfo::IsFrontend() const {
42   return url.find("chrome-devtools://") == 0u;
43 }
44 
WebViewsInfo()45 WebViewsInfo::WebViewsInfo() {}
46 
WebViewsInfo(const std::vector<WebViewInfo> & info)47 WebViewsInfo::WebViewsInfo(const std::vector<WebViewInfo>& info)
48     : views_info(info) {}
49 
~WebViewsInfo()50 WebViewsInfo::~WebViewsInfo() {}
51 
Get(int index) const52 const WebViewInfo& WebViewsInfo::Get(int index) const {
53   return views_info[index];
54 }
55 
GetSize() const56 size_t WebViewsInfo::GetSize() const {
57   return views_info.size();
58 }
59 
GetForId(const std::string & id) const60 const WebViewInfo* WebViewsInfo::GetForId(const std::string& id) const {
61   for (size_t i = 0; i < views_info.size(); ++i) {
62     if (views_info[i].id == id)
63       return &views_info[i];
64   }
65   return NULL;
66 }
67 
DevToolsHttpClient(const NetAddress & address,scoped_refptr<URLRequestContextGetter> context_getter,const SyncWebSocketFactory & socket_factory,scoped_ptr<DeviceMetrics> device_metrics)68 DevToolsHttpClient::DevToolsHttpClient(
69     const NetAddress& address,
70     scoped_refptr<URLRequestContextGetter> context_getter,
71     const SyncWebSocketFactory& socket_factory,
72     scoped_ptr<DeviceMetrics> device_metrics)
73     : context_getter_(context_getter),
74       socket_factory_(socket_factory),
75       server_url_("http://" + address.ToString()),
76       web_socket_url_prefix_(base::StringPrintf(
77           "ws://%s/devtools/page/", address.ToString().c_str())),
78       device_metrics_(device_metrics.Pass()) {}
79 
~DevToolsHttpClient()80 DevToolsHttpClient::~DevToolsHttpClient() {}
81 
Init(const base::TimeDelta & timeout)82 Status DevToolsHttpClient::Init(const base::TimeDelta& timeout) {
83   base::TimeTicks deadline = base::TimeTicks::Now() + timeout;
84   std::string browser_version;
85   std::string blink_version;
86 
87   while (true) {
88     Status status = GetVersion(&browser_version, &blink_version);
89     if (status.IsOk())
90       break;
91     if (status.code() != kChromeNotReachable ||
92         base::TimeTicks::Now() > deadline) {
93       return status;
94     }
95     base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
96   }
97 
98   // |blink_version| is should look something like "537.36 (@159105)", and for
99   // this example |blink_revision| should be 159105
100   size_t before = blink_version.find('@');
101   size_t after = blink_version.find(')');
102   if (before == std::string::npos || after == std::string::npos) {
103     return Status(kUnknownError,
104                   "unrecognized Blink version: " + blink_version);
105   }
106 
107   std::string blink_revision_string = blink_version.substr(before + 1,
108                                                            after - before - 1);
109   int blink_revision_int;
110   if (!base::StringToInt(blink_revision_string, &blink_revision_int)) {
111     return Status(kUnknownError,
112                   "unrecognized Blink revision: " + blink_revision_string);
113   }
114 
115   browser_info_.blink_revision = blink_revision_int;
116 
117   if (browser_version.empty()) {
118     browser_info_.browser_name = "content shell";
119     return Status(kOk);
120   }
121   if (browser_version.find("Version/") == 0u) {
122     browser_info_.browser_name = "webview";
123     return Status(kOk);
124   }
125   std::string prefix = "Chrome/";
126   if (browser_version.find(prefix) != 0u) {
127     return Status(kUnknownError,
128                   "unrecognized Chrome version: " + browser_version);
129   }
130 
131   std::string stripped_version = browser_version.substr(prefix.length());
132   int temp_build_no;
133   std::vector<std::string> version_parts;
134   base::SplitString(stripped_version, '.', &version_parts);
135   if (version_parts.size() != 4 ||
136       !base::StringToInt(version_parts[2], &temp_build_no)) {
137     return Status(kUnknownError,
138                   "unrecognized Chrome version: " + browser_version);
139   }
140 
141   browser_info_.browser_name = "chrome";
142   browser_info_.browser_version = stripped_version;
143   browser_info_.build_no = temp_build_no;
144 
145   return Status(kOk);
146 }
147 
GetWebViewsInfo(WebViewsInfo * views_info)148 Status DevToolsHttpClient::GetWebViewsInfo(WebViewsInfo* views_info) {
149   std::string data;
150   if (!FetchUrlAndLog(server_url_ + "/json", context_getter_.get(), &data))
151     return Status(kChromeNotReachable);
152 
153   return internal::ParseWebViewsInfo(data, views_info);
154 }
155 
CreateClient(const std::string & id)156 scoped_ptr<DevToolsClient> DevToolsHttpClient::CreateClient(
157     const std::string& id) {
158   return scoped_ptr<DevToolsClient>(new DevToolsClientImpl(
159       socket_factory_,
160       web_socket_url_prefix_ + id,
161       id,
162       base::Bind(
163           &DevToolsHttpClient::CloseFrontends, base::Unretained(this), id)));
164 }
165 
CloseWebView(const std::string & id)166 Status DevToolsHttpClient::CloseWebView(const std::string& id) {
167   std::string data;
168   if (!FetchUrlAndLog(
169           server_url_ + "/json/close/" + id, context_getter_.get(), &data)) {
170     return Status(kOk);  // Closing the last web view leads chrome to quit.
171   }
172 
173   // Wait for the target window to be completely closed.
174   base::TimeTicks deadline =
175       base::TimeTicks::Now() + base::TimeDelta::FromSeconds(20);
176   while (base::TimeTicks::Now() < deadline) {
177     WebViewsInfo views_info;
178     Status status = GetWebViewsInfo(&views_info);
179     if (status.code() == kChromeNotReachable)
180       return Status(kOk);
181     if (status.IsError())
182       return status;
183     if (!views_info.GetForId(id))
184       return Status(kOk);
185     base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
186   }
187   return Status(kUnknownError, "failed to close window in 20 seconds");
188 }
189 
ActivateWebView(const std::string & id)190 Status DevToolsHttpClient::ActivateWebView(const std::string& id) {
191   std::string data;
192   if (!FetchUrlAndLog(
193           server_url_ + "/json/activate/" + id, context_getter_.get(), &data))
194     return Status(kUnknownError, "cannot activate web view");
195   return Status(kOk);
196 }
197 
browser_info()198 const BrowserInfo* DevToolsHttpClient::browser_info() {
199   return &browser_info_;
200 }
201 
device_metrics()202 const DeviceMetrics* DevToolsHttpClient::device_metrics() {
203   return device_metrics_.get();
204 }
205 
GetVersion(std::string * browser_version,std::string * blink_version)206 Status DevToolsHttpClient::GetVersion(std::string* browser_version,
207                                       std::string* blink_version) {
208   std::string data;
209   if (!FetchUrlAndLog(
210           server_url_ + "/json/version", context_getter_.get(), &data))
211     return Status(kChromeNotReachable);
212 
213   return internal::ParseVersionInfo(data, browser_version, blink_version);
214 }
215 
CloseFrontends(const std::string & for_client_id)216 Status DevToolsHttpClient::CloseFrontends(const std::string& for_client_id) {
217   WebViewsInfo views_info;
218   Status status = GetWebViewsInfo(&views_info);
219   if (status.IsError())
220     return status;
221 
222   // Close frontends. Usually frontends are docked in the same page, although
223   // some may be in tabs (undocked, chrome://inspect, the DevTools
224   // discovery page, etc.). Tabs can be closed via the DevTools HTTP close
225   // URL, but docked frontends can only be closed, by design, by connecting
226   // to them and clicking the close button. Close the tab frontends first
227   // in case one of them is debugging a docked frontend, which would prevent
228   // the code from being able to connect to the docked one.
229   std::list<std::string> tab_frontend_ids;
230   std::list<std::string> docked_frontend_ids;
231   for (size_t i = 0; i < views_info.GetSize(); ++i) {
232     const WebViewInfo& view_info = views_info.Get(i);
233     if (view_info.IsFrontend()) {
234       if (view_info.type == WebViewInfo::kPage)
235         tab_frontend_ids.push_back(view_info.id);
236       else if (view_info.type == WebViewInfo::kOther)
237         docked_frontend_ids.push_back(view_info.id);
238       else
239         return Status(kUnknownError, "unknown type of DevTools frontend");
240     }
241   }
242 
243   for (std::list<std::string>::const_iterator it = tab_frontend_ids.begin();
244        it != tab_frontend_ids.end(); ++it) {
245     status = CloseWebView(*it);
246     if (status.IsError())
247       return status;
248   }
249 
250   for (std::list<std::string>::const_iterator it = docked_frontend_ids.begin();
251        it != docked_frontend_ids.end(); ++it) {
252     scoped_ptr<DevToolsClient> client(new DevToolsClientImpl(
253         socket_factory_,
254         web_socket_url_prefix_ + *it,
255         *it,
256         base::Bind(&FakeCloseFrontends)));
257     scoped_ptr<WebViewImpl> web_view(
258         new WebViewImpl(*it, &browser_info_, client.Pass(), NULL));
259 
260     status = web_view->ConnectIfNecessary();
261     // Ignore disconnected error, because the debugger might have closed when
262     // its container page was closed above.
263     if (status.IsError() && status.code() != kDisconnected)
264       return status;
265 
266     scoped_ptr<base::Value> result;
267     status = web_view->EvaluateScript(
268         std::string(),
269         "document.querySelector('*[id^=\"close-button-\"]').click();",
270         &result);
271     // Ignore disconnected error, because it may be closed already.
272     if (status.IsError() && status.code() != kDisconnected)
273       return status;
274   }
275 
276   // Wait until DevTools UI disconnects from the given web view.
277   base::TimeTicks deadline =
278       base::TimeTicks::Now() + base::TimeDelta::FromSeconds(20);
279   while (base::TimeTicks::Now() < deadline) {
280     status = GetWebViewsInfo(&views_info);
281     if (status.IsError())
282       return status;
283 
284     const WebViewInfo* view_info = views_info.GetForId(for_client_id);
285     if (!view_info)
286       return Status(kNoSuchWindow, "window was already closed");
287     if (view_info->debugger_url.size())
288       return Status(kOk);
289 
290     base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50));
291   }
292   return Status(kUnknownError, "failed to close UI debuggers");
293 }
294 
FetchUrlAndLog(const std::string & url,URLRequestContextGetter * getter,std::string * response)295 bool DevToolsHttpClient::FetchUrlAndLog(const std::string& url,
296                                         URLRequestContextGetter* getter,
297                                         std::string* response) {
298   VLOG(1) << "DevTools request: " << url;
299   bool ok = FetchUrl(url, getter, response);
300   if (ok) {
301     VLOG(1) << "DevTools response: " << *response;
302   } else {
303     VLOG(1) << "DevTools request failed";
304   }
305   return ok;
306 }
307 
308 namespace internal {
309 
ParseWebViewsInfo(const std::string & data,WebViewsInfo * views_info)310 Status ParseWebViewsInfo(const std::string& data,
311                          WebViewsInfo* views_info) {
312   scoped_ptr<base::Value> value(base::JSONReader::Read(data));
313   if (!value.get())
314     return Status(kUnknownError, "DevTools returned invalid JSON");
315   base::ListValue* list;
316   if (!value->GetAsList(&list))
317     return Status(kUnknownError, "DevTools did not return list");
318 
319   std::vector<WebViewInfo> temp_views_info;
320   for (size_t i = 0; i < list->GetSize(); ++i) {
321     base::DictionaryValue* info;
322     if (!list->GetDictionary(i, &info))
323       return Status(kUnknownError, "DevTools contains non-dictionary item");
324     std::string id;
325     if (!info->GetString("id", &id))
326       return Status(kUnknownError, "DevTools did not include id");
327     std::string type_as_string;
328     if (!info->GetString("type", &type_as_string))
329       return Status(kUnknownError, "DevTools did not include type");
330     std::string url;
331     if (!info->GetString("url", &url))
332       return Status(kUnknownError, "DevTools did not include url");
333     std::string debugger_url;
334     info->GetString("webSocketDebuggerUrl", &debugger_url);
335     WebViewInfo::Type type;
336     if (type_as_string == "app")
337       type = WebViewInfo::kApp;
338     else if (type_as_string == "background_page")
339       type = WebViewInfo::kBackgroundPage;
340     else if (type_as_string == "page")
341       type = WebViewInfo::kPage;
342     else if (type_as_string == "worker")
343       type = WebViewInfo::kWorker;
344     else if (type_as_string == "other")
345       type = WebViewInfo::kOther;
346     else
347       return Status(kUnknownError,
348                     "DevTools returned unknown type:" + type_as_string);
349     temp_views_info.push_back(WebViewInfo(id, debugger_url, url, type));
350   }
351   *views_info = WebViewsInfo(temp_views_info);
352   return Status(kOk);
353 }
354 
ParseVersionInfo(const std::string & data,std::string * browser_version,std::string * blink_version)355 Status ParseVersionInfo(const std::string& data,
356                         std::string* browser_version,
357                         std::string* blink_version) {
358   scoped_ptr<base::Value> value(base::JSONReader::Read(data));
359   if (!value.get())
360     return Status(kUnknownError, "version info not in JSON");
361   base::DictionaryValue* dict;
362   if (!value->GetAsDictionary(&dict))
363     return Status(kUnknownError, "version info not a dictionary");
364   if (!dict->GetString("Browser", browser_version)) {
365     return Status(
366         kUnknownError,
367         "Chrome version must be >= " + GetMinimumSupportedChromeVersion(),
368         Status(kUnknownError, "version info doesn't include string 'Browser'"));
369   }
370   if (!dict->GetString("WebKit-Version", blink_version)) {
371     return Status(kUnknownError,
372                   "version info doesn't include string 'WebKit-Version'");
373   }
374   return Status(kOk);
375 }
376 
377 }  // namespace internal
378