chrome_launcher.cc revision a93a17c8d99d686bd4a1511e5504e5e6cc9fcadf
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_launcher.h"
6
7#include "base/base64.h"
8#include "base/command_line.h"
9#include "base/file_util.h"
10#include "base/files/file_path.h"
11#include "base/format_macros.h"
12#include "base/json/json_reader.h"
13#include "base/json/json_writer.h"
14#include "base/process.h"
15#include "base/process_util.h"
16#include "base/string_util.h"
17#include "base/stringprintf.h"
18#include "base/strings/string_number_conversions.h"
19#include "base/strings/string_split.h"
20#include "base/threading/platform_thread.h"
21#include "base/time.h"
22#include "base/utf_string_conversions.h"
23#include "base/values.h"
24#include "chrome/test/chromedriver/chrome/chrome_android_impl.h"
25#include "chrome/test/chromedriver/chrome/chrome_desktop_impl.h"
26#include "chrome/test/chromedriver/chrome/chrome_finder.h"
27#include "chrome/test/chromedriver/chrome/devtools_http_client.h"
28#include "chrome/test/chromedriver/chrome/embedded_automation_extension.h"
29#include "chrome/test/chromedriver/chrome/status.h"
30#include "chrome/test/chromedriver/chrome/user_data_dir.h"
31#include "chrome/test/chromedriver/chrome/version.h"
32#include "chrome/test/chromedriver/chrome/zip.h"
33#include "chrome/test/chromedriver/net/url_request_context_getter.h"
34
35namespace {
36
37Status UnpackAutomationExtension(const base::FilePath& temp_dir,
38                                 base::FilePath* automation_extension) {
39  std::string decoded_extension;
40  if (!base::Base64Decode(kAutomationExtension, &decoded_extension))
41    return Status(kUnknownError, "failed to base64decode automation extension");
42
43  base::FilePath extension_zip = temp_dir.AppendASCII("internal.zip");
44  int size = static_cast<int>(decoded_extension.length());
45  if (file_util::WriteFile(extension_zip, decoded_extension.c_str(), size)
46      != size) {
47    return Status(kUnknownError, "failed to write automation extension zip");
48  }
49
50  base::FilePath extension_dir = temp_dir.AppendASCII("internal");
51  if (!zip::Unzip(extension_zip, extension_dir))
52    return Status(kUnknownError, "failed to unzip automation extension");
53
54  *automation_extension = extension_dir;
55  return Status(kOk);
56}
57
58Status PrepareCommandLine(int port,
59                          const Capabilities& capabilities,
60                          CommandLine* prepared_command,
61                          base::ScopedTempDir* user_data_dir,
62                          base::ScopedTempDir* extension_dir) {
63  CommandLine command = capabilities.command;
64  base::FilePath program = command.GetProgram();
65  if (program.empty()) {
66    if (!FindChrome(&program))
67      return Status(kUnknownError, "cannot find Chrome binary");
68    command.SetProgram(program);
69  } else if (!file_util::PathExists(program)) {
70    return Status(kUnknownError,
71                  base::StringPrintf("no chrome binary at %" PRFilePath,
72                                     program.value().c_str()));
73  }
74
75  command.AppendSwitchASCII("remote-debugging-port", base::IntToString(port));
76  command.AppendSwitch("no-first-run");
77  command.AppendSwitch("enable-logging");
78  command.AppendSwitchASCII("logging-level", "1");
79  command.AppendArg("data:text/html;charset=utf-8,");
80
81  if (!command.HasSwitch("user-data-dir")) {
82    if (!user_data_dir->CreateUniqueTempDir())
83      return Status(kUnknownError, "cannot create temp dir for user data dir");
84    command.AppendSwitchPath("user-data-dir", user_data_dir->path());
85    Status status = internal::PrepareUserDataDir(
86        user_data_dir->path(), capabilities.prefs.get(),
87        capabilities.local_state.get());
88    if (status.IsError())
89      return status;
90  }
91
92  if (!extension_dir->CreateUniqueTempDir()) {
93    return Status(kUnknownError,
94                  "cannot create temp dir for unpacking extensions");
95  }
96  Status status = internal::ProcessExtensions(
97      capabilities.extensions, extension_dir->path(), true, &command);
98  if (status.IsError())
99    return status;
100
101  *prepared_command = command;
102  return Status(kOk);
103}
104
105Status ParseAndCheckVersion(const std::string& devtools_version,
106                            std::string* version,
107                            int* build_no) {
108  if (devtools_version.empty()) {
109    // Content Shell has an empty product version and a fake user agent.
110    // There's no way to detect the actual version, so assume it is tip of tree.
111    *version = "content shell";
112    *build_no = 9999;
113    return Status(kOk);
114  }
115  std::string prefix = "Chrome/";
116  if (devtools_version.find(prefix) != 0u) {
117    return Status(kUnknownError,
118                  "unrecognized Chrome version: " + devtools_version);
119  }
120
121  std::string stripped_version = devtools_version.substr(prefix.length());
122  int temp_build_no;
123  std::vector<std::string> version_parts;
124  base::SplitString(stripped_version, '.', &version_parts);
125  if (version_parts.size() != 4 ||
126      !base::StringToInt(version_parts[2], &temp_build_no)) {
127    return Status(kUnknownError,
128                  "unrecognized Chrome version: " + devtools_version);
129  }
130
131  if (temp_build_no < kMinimumSupportedChromeBuildNo) {
132    return Status(kUnknownError, "Chrome version must be >= " +
133        GetMinimumSupportedChromeVersion());
134  }
135  *version = stripped_version;
136  *build_no = temp_build_no;
137  return Status(kOk);
138}
139
140Status WaitForDevToolsAndCheckVersion(
141    int port,
142    URLRequestContextGetter* context_getter,
143    const SyncWebSocketFactory& socket_factory,
144    scoped_ptr<DevToolsHttpClient>* user_client,
145    std::string* version,
146    int* build_no) {
147  scoped_ptr<DevToolsHttpClient> client(new DevToolsHttpClient(
148      port, context_getter, socket_factory));
149
150  base::Time deadline = base::Time::Now() + base::TimeDelta::FromSeconds(20);
151  std::string devtools_version;
152  Status status(kOk);
153  while (base::Time::Now() < deadline) {
154    status = client->GetVersion(&devtools_version);
155    if (status.IsOk())
156      break;
157    if (status.code() != kChromeNotReachable)
158      return status;
159    base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
160  }
161  if (status.IsError())
162    return status;
163  status = ParseAndCheckVersion(devtools_version, version, build_no);
164  if (status.IsError())
165    return status;
166
167  while (base::Time::Now() < deadline) {
168    WebViewsInfo views_info;
169    client->GetWebViewsInfo(&views_info);
170    if (views_info.GetSize()) {
171      *user_client = client.Pass();
172      return Status(kOk);
173    }
174    base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
175  }
176  return Status(kUnknownError, "unable to discover open pages");
177}
178
179Status LaunchDesktopChrome(
180    URLRequestContextGetter* context_getter,
181    int port,
182    const SyncWebSocketFactory& socket_factory,
183    const Capabilities& capabilities,
184    ScopedVector<DevToolsEventListener>& devtools_event_listeners,
185    scoped_ptr<Chrome>* chrome) {
186  CommandLine command(CommandLine::NO_PROGRAM);
187  base::ScopedTempDir user_data_dir;
188  base::ScopedTempDir extension_dir;
189  PrepareCommandLine(port, capabilities,
190                     &command, &user_data_dir, &extension_dir);
191  command.AppendSwitch("ignore-certificate-errors");
192  base::LaunchOptions options;
193
194#if !defined(OS_WIN)
195  base::EnvironmentVector environ;
196  if (!capabilities.log_path.empty()) {
197    environ.push_back(
198        base::EnvironmentVector::value_type("CHROME_LOG_FILE",
199                                            capabilities.log_path));
200    options.environ = &environ;
201  }
202#endif
203
204  LOG(INFO) << "Launching chrome: " << command.GetCommandLineString();
205  base::ProcessHandle process;
206  if (!base::LaunchProcess(command, options, &process))
207    return Status(kUnknownError, "chrome failed to start");
208
209  scoped_ptr<DevToolsHttpClient> devtools_client;
210  std::string version;
211  int build_no;
212  Status status = WaitForDevToolsAndCheckVersion(
213      port, context_getter, socket_factory, &devtools_client, &version,
214      &build_no);
215
216  if (status.IsError()) {
217    int exit_code;
218    base::TerminationStatus chrome_status =
219        base::GetTerminationStatus(process, &exit_code);
220    if (chrome_status != base::TERMINATION_STATUS_STILL_RUNNING) {
221      std::string termination_reason;
222      switch (chrome_status) {
223        case base::TERMINATION_STATUS_NORMAL_TERMINATION:
224          termination_reason = "exited normally";
225          break;
226        case base::TERMINATION_STATUS_ABNORMAL_TERMINATION:
227          termination_reason = "exited abnormally";
228          break;
229        case base::TERMINATION_STATUS_PROCESS_WAS_KILLED:
230          termination_reason = "was killed";
231          break;
232        case base::TERMINATION_STATUS_PROCESS_CRASHED:
233          termination_reason = "crashed";
234          break;
235        default:
236          termination_reason = "unknown";
237          break;
238      }
239      return Status(kUnknownError,
240                    "Chrome failed to start: " + termination_reason);
241    }
242    if (!base::KillProcess(process, 0, true)) {
243      int exit_code;
244      if (base::GetTerminationStatus(process, &exit_code) ==
245          base::TERMINATION_STATUS_STILL_RUNNING)
246        return Status(kUnknownError, "cannot kill Chrome", status);
247    }
248    return status;
249  }
250  chrome->reset(new ChromeDesktopImpl(
251      devtools_client.Pass(), version, build_no, devtools_event_listeners,
252      process, &user_data_dir, &extension_dir));
253  return Status(kOk);
254}
255
256Status LaunchAndroidChrome(
257    URLRequestContextGetter* context_getter,
258    int port,
259    const SyncWebSocketFactory& socket_factory,
260    const Capabilities& capabilities,
261    ScopedVector<DevToolsEventListener>& devtools_event_listeners,
262    scoped_ptr<Chrome>* chrome) {
263  // TODO(frankf): Figure out how this should be installed to
264  // make this work for all platforms.
265  base::FilePath adb_commands(FILE_PATH_LITERAL("adb_commands.py"));
266  CommandLine command(adb_commands);
267  command.AppendSwitchASCII("package", capabilities.android_package);
268  command.AppendSwitch("launch");
269  command.AppendSwitchASCII("port", base::IntToString(port));
270
271  std::string output;
272  if (!base::GetAppOutput(command, &output)) {
273    if (output.empty())
274      return Status(
275          kUnknownError,
276          "failed to run adb_commands.py. Make sure it is set in PATH.");
277    else
278      return Status(kUnknownError, "android app failed to start.\n" + output);
279  }
280
281  scoped_ptr<DevToolsHttpClient> devtools_client;
282  std::string version;
283  int build_no;
284  Status status = WaitForDevToolsAndCheckVersion(
285      port, context_getter, socket_factory, &devtools_client, &version,
286      &build_no);
287  if (status.IsError())
288    return status;
289
290  chrome->reset(new ChromeAndroidImpl(
291      devtools_client.Pass(), version, build_no, devtools_event_listeners));
292  return Status(kOk);
293}
294
295}  // namespace
296
297Status LaunchChrome(
298    URLRequestContextGetter* context_getter,
299    int port,
300    const SyncWebSocketFactory& socket_factory,
301    const Capabilities& capabilities,
302    ScopedVector<DevToolsEventListener>& devtools_event_listeners,
303    scoped_ptr<Chrome>* chrome) {
304  if (capabilities.IsAndroid()) {
305    return LaunchAndroidChrome(
306        context_getter, port, socket_factory, capabilities,
307        devtools_event_listeners, chrome);
308  } else {
309    return LaunchDesktopChrome(
310        context_getter, port, socket_factory, capabilities,
311        devtools_event_listeners, chrome);
312  }
313}
314
315namespace internal {
316
317Status ProcessExtensions(const std::vector<std::string>& extensions,
318                         const base::FilePath& temp_dir,
319                         bool include_automation_extension,
320                         CommandLine* command) {
321  std::vector<base::FilePath::StringType> extension_paths;
322  size_t count = 0;
323  for (std::vector<std::string>::const_iterator it = extensions.begin();
324       it != extensions.end(); ++it) {
325    std::string extension_base64;
326    // Decodes extension string.
327    // Some WebDriver client base64 encoders follow RFC 1521, which require that
328    // 'encoded lines be no more than 76 characters long'. Just remove any
329    // newlines.
330    RemoveChars(*it, "\n", &extension_base64);
331    std::string decoded_extension;
332    if (!base::Base64Decode(extension_base64, &decoded_extension))
333      return Status(kUnknownError, "failed to base64 decode extension");
334
335    // Writes decoded extension into a temporary .crx file.
336    base::ScopedTempDir temp_crx_dir;
337    if (!temp_crx_dir.CreateUniqueTempDir())
338      return Status(kUnknownError,
339                    "cannot create temp dir for writing extension CRX file");
340    base::FilePath extension_crx = temp_crx_dir.path().AppendASCII("temp.crx");
341    int size = static_cast<int>(decoded_extension.length());
342    if (file_util::WriteFile(extension_crx, decoded_extension.c_str(), size)
343        != size) {
344      return Status(kUnknownError, "failed to write extension file");
345    }
346
347    // Unzips the temporary .crx file.
348    count++;
349    base::FilePath extension_dir = temp_dir.AppendASCII(
350        base::StringPrintf("extension%" PRIuS, count));
351    if (!zip::Unzip(extension_crx, extension_dir))
352      return Status(kUnknownError, "failed to unzip the extension CRX file");
353    extension_paths.push_back(extension_dir.value());
354  }
355
356  if (include_automation_extension) {
357    base::FilePath automation_extension;
358    Status status = UnpackAutomationExtension(temp_dir, &automation_extension);
359    if (status.IsError())
360      return status;
361    extension_paths.push_back(automation_extension.value());
362  }
363
364  if (extension_paths.size()) {
365    base::FilePath::StringType extension_paths_value = JoinString(
366        extension_paths, FILE_PATH_LITERAL(','));
367    command->AppendSwitchNative("load-extension", extension_paths_value);
368  }
369  return Status(kOk);
370}
371
372Status WritePrefsFile(
373    const std::string& template_string,
374    const base::DictionaryValue* custom_prefs,
375    const base::FilePath& path) {
376  int code;
377  std::string error_msg;
378  scoped_ptr<base::Value> template_value(base::JSONReader::ReadAndReturnError(
379          template_string, 0, &code, &error_msg));
380  base::DictionaryValue* prefs;
381  if (!template_value || !template_value->GetAsDictionary(&prefs)) {
382    return Status(kUnknownError,
383                  "cannot parse internal JSON template: " + error_msg);
384  }
385
386  if (custom_prefs) {
387    for (base::DictionaryValue::Iterator it(*custom_prefs); !it.IsAtEnd();
388         it.Advance()) {
389      prefs->Set(it.key(), it.value().DeepCopy());
390    }
391  }
392
393  std::string prefs_str;
394  base::JSONWriter::Write(prefs, &prefs_str);
395  if (static_cast<int>(prefs_str.length()) != file_util::WriteFile(
396          path, prefs_str.c_str(), prefs_str.length())) {
397    return Status(kUnknownError, "failed to write prefs file");
398  }
399  return Status(kOk);
400}
401
402Status PrepareUserDataDir(
403    const base::FilePath& user_data_dir,
404    const base::DictionaryValue* custom_prefs,
405    const base::DictionaryValue* custom_local_state) {
406  base::FilePath default_dir = user_data_dir.AppendASCII("Default");
407  if (!file_util::CreateDirectory(default_dir))
408    return Status(kUnknownError, "cannot create default profile directory");
409
410  Status status = WritePrefsFile(
411      kPreferences,
412      custom_prefs,
413      default_dir.AppendASCII("Preferences"));
414  if (status.IsError())
415    return status;
416
417  status = WritePrefsFile(
418      kLocalState,
419      custom_local_state,
420      user_data_dir.AppendASCII("Local State"));
421  if (status.IsError())
422    return status;
423
424  // Write empty "First Run" file, otherwise Chrome will wipe the default
425  // profile that was written.
426  if (file_util::WriteFile(
427          user_data_dir.AppendASCII("First Run"), "", 0) != 0) {
428    return Status(kUnknownError, "failed to write first run file");
429  }
430  return Status(kOk);
431}
432
433}  // namespace internal
434