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