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