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