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.
5 #include "chrome/test/chromedriver/chrome_launcher.h"
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"
37 Status
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");
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
)
47 return Status(kUnknownError
, "failed to write automation extension zip");
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");
54 *automation_extension
= extension_dir
;
58 Status
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()));
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,");
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());
92 if (!extension_dir
->CreateUniqueTempDir()) {
93 return Status(kUnknownError
,
94 "cannot create temp dir for unpacking extensions");
96 Status status
= internal::ProcessExtensions(
97 capabilities
.extensions
, extension_dir
->path(), true, &command
);
101 *prepared_command
= command
;
105 Status
ParseAndCheckVersion(const std::string
& devtools_version
,
106 std::string
* version
,
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";
115 std::string prefix
= "Chrome/";
116 if (devtools_version
.find(prefix
) != 0u) {
117 return Status(kUnknownError
,
118 "unrecognized Chrome version: " + devtools_version
);
121 std::string stripped_version
= devtools_version
.substr(prefix
.length());
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
);
131 if (temp_build_no
< kMinimumSupportedChromeBuildNo
) {
132 return Status(kUnknownError
, "Chrome version must be >= " +
133 GetMinimumSupportedChromeVersion());
135 *version
= stripped_version
;
136 *build_no
= temp_build_no
;
140 Status
WaitForDevToolsAndCheckVersion(
142 URLRequestContextGetter
* context_getter
,
143 const SyncWebSocketFactory
& socket_factory
,
144 scoped_ptr
<DevToolsHttpClient
>* user_client
,
145 std::string
* version
,
147 scoped_ptr
<DevToolsHttpClient
> client(new DevToolsHttpClient(
148 port
, context_getter
, socket_factory
));
150 base::Time deadline
= base::Time::Now() + base::TimeDelta::FromSeconds(20);
151 std::string devtools_version
;
153 while (base::Time::Now() < deadline
) {
154 status
= client
->GetVersion(&devtools_version
);
157 if (status
.code() != kChromeNotReachable
)
159 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
161 if (status
.IsError())
163 status
= ParseAndCheckVersion(devtools_version
, version
, build_no
);
164 if (status
.IsError())
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();
174 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
176 return Status(kUnknownError
, "unable to discover open pages");
179 Status
LaunchDesktopChrome(
180 URLRequestContextGetter
* context_getter
,
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
;
195 base::EnvironmentVector environ
;
196 if (!capabilities
.log_path
.empty()) {
198 base::EnvironmentVector::value_type("CHROME_LOG_FILE",
199 capabilities
.log_path
));
200 options
.environ
= &environ
;
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");
209 scoped_ptr
<DevToolsHttpClient
> devtools_client
;
212 Status status
= WaitForDevToolsAndCheckVersion(
213 port
, context_getter
, socket_factory
, &devtools_client
, &version
,
216 if (status
.IsError()) {
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";
226 case base::TERMINATION_STATUS_ABNORMAL_TERMINATION
:
227 termination_reason
= "exited abnormally";
229 case base::TERMINATION_STATUS_PROCESS_WAS_KILLED
:
230 termination_reason
= "was killed";
232 case base::TERMINATION_STATUS_PROCESS_CRASHED
:
233 termination_reason
= "crashed";
236 termination_reason
= "unknown";
239 return Status(kUnknownError
,
240 "Chrome failed to start: " + termination_reason
);
242 if (!base::KillProcess(process
, 0, true)) {
244 if (base::GetTerminationStatus(process
, &exit_code
) ==
245 base::TERMINATION_STATUS_STILL_RUNNING
)
246 return Status(kUnknownError
, "cannot kill Chrome", status
);
250 chrome
->reset(new ChromeDesktopImpl(
251 devtools_client
.Pass(), version
, build_no
, devtools_event_listeners
,
252 process
, &user_data_dir
, &extension_dir
));
256 Status
LaunchAndroidChrome(
257 URLRequestContextGetter
* context_getter
,
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
));
272 if (!base::GetAppOutput(command
, &output
)) {
276 "failed to run adb_commands.py. Make sure it is set in PATH.");
278 return Status(kUnknownError
, "android app failed to start.\n" + output
);
281 scoped_ptr
<DevToolsHttpClient
> devtools_client
;
284 Status status
= WaitForDevToolsAndCheckVersion(
285 port
, context_getter
, socket_factory
, &devtools_client
, &version
,
287 if (status
.IsError())
290 chrome
->reset(new ChromeAndroidImpl(
291 devtools_client
.Pass(), version
, build_no
, devtools_event_listeners
));
298 URLRequestContextGetter
* context_getter
,
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
);
309 return LaunchDesktopChrome(
310 context_getter
, port
, socket_factory
, capabilities
,
311 devtools_event_listeners
, chrome
);
317 Status
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
;
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
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");
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
)
344 return Status(kUnknownError
, "failed to write extension file");
347 // Unzips the temporary .crx file.
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());
356 if (include_automation_extension
) {
357 base::FilePath automation_extension
;
358 Status status
= UnpackAutomationExtension(temp_dir
, &automation_extension
);
359 if (status
.IsError())
361 extension_paths
.push_back(automation_extension
.value());
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
);
372 Status
WritePrefsFile(
373 const std::string
& template_string
,
374 const base::DictionaryValue
* custom_prefs
,
375 const base::FilePath
& path
) {
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
);
387 for (base::DictionaryValue::Iterator
it(*custom_prefs
); !it
.IsAtEnd();
389 prefs
->Set(it
.key(), it
.value().DeepCopy());
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");
402 Status
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");
410 Status status
= WritePrefsFile(
413 default_dir
.AppendASCII("Preferences"));
414 if (status
.IsError())
417 status
= WritePrefsFile(
420 user_data_dir
.AppendASCII("Local State"));
421 if (status
.IsError())
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");
433 } // namespace internal