google_chrome_distribution.cc revision 5821806d5e7f356e8fa4b058a389a808ea183019
1// Copyright (c) 2012 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// This file defines specific implementation of BrowserDistribution class for 6// Google Chrome. 7 8#include "chrome/installer/util/google_chrome_distribution.h" 9 10#include <vector> 11#include <windows.h> 12#include <wtsapi32.h> 13#include <msi.h> 14#include <sddl.h> 15 16#include "base/command_line.h" 17#include "base/file_path.h" 18#include "base/json/json_file_value_serializer.h" 19#include "base/memory/scoped_ptr.h" 20#include "base/path_service.h" 21#include "base/process_util.h" 22#include "base/rand_util.h" 23#include "base/string_number_conversions.h" 24#include "base/string_split.h" 25#include "base/string_util.h" 26#include "base/stringprintf.h" 27#include "base/utf_string_conversions.h" 28#include "base/win/registry.h" 29#include "base/win/windows_version.h" 30#include "chrome/common/attrition_experiments.h" 31#include "chrome/common/chrome_result_codes.h" 32#include "chrome/common/chrome_switches.h" 33#include "chrome/common/net/test_server_locations.h" 34#include "chrome/common/pref_names.h" 35#include "chrome/installer/util/channel_info.h" 36#include "chrome/installer/util/product.h" 37#include "chrome/installer/util/install_util.h" 38#include "chrome/installer/util/l10n_string_util.h" 39#include "chrome/installer/util/google_update_constants.h" 40#include "chrome/installer/util/google_update_settings.h" 41#include "chrome/installer/util/helper.h" 42#include "chrome/installer/util/util_constants.h" 43#include "chrome/installer/util/wmi.h" 44 45#include "installer_util_strings.h" // NOLINT 46 47#pragma comment(lib, "wtsapi32.lib") 48 49namespace { 50 51const wchar_t kChromeGuid[] = L"{8A69D345-D564-463c-AFF1-A69D9E530F96}"; 52const wchar_t kBrowserAppId[] = L"Chrome"; 53const wchar_t kCommandExecuteImplUuid[] = 54 L"{5C65F4B0-3651-4514-B207-D10CB699B14B}"; 55 56// The following strings are the possible outcomes of the toast experiment 57// as recorded in the |client| field. 58const wchar_t kToastExpControlGroup[] = L"01"; 59const wchar_t kToastExpCancelGroup[] = L"02"; 60const wchar_t kToastExpUninstallGroup[] = L"04"; 61const wchar_t kToastExpTriesOkGroup[] = L"18"; 62const wchar_t kToastExpTriesErrorGroup[] = L"28"; 63const wchar_t kToastExpTriesOkDefaultGroup[] = L"48"; 64const wchar_t kToastActiveGroup[] = L"40"; 65const wchar_t kToastUDDirFailure[] = L"40"; 66const wchar_t kToastExpBaseGroup[] = L"80"; 67 68// Substitute the locale parameter in uninstall URL with whatever 69// Google Update tells us is the locale. In case we fail to find 70// the locale, we use US English. 71string16 LocalizeUrl(const wchar_t* url) { 72 string16 language; 73 if (!GoogleUpdateSettings::GetLanguage(&language)) 74 language = L"en-US"; // Default to US English. 75 return ReplaceStringPlaceholders(url, language.c_str(), NULL); 76} 77 78string16 GetUninstallSurveyUrl() { 79 const wchar_t kSurveyUrl[] = L"http://www.google.com/support/chrome/bin/" 80 L"request.py?hl=$1&contact_type=uninstall"; 81 return LocalizeUrl(kSurveyUrl); 82} 83 84string16 GetWelcomeBackUrl() { 85 const wchar_t kWelcomeUrl[] = L"http://www.google.com/chrome/intl/$1/" 86 L"welcomeback-new.html"; 87 return LocalizeUrl(kWelcomeUrl); 88} 89 90// Converts FILETIME to hours. FILETIME times are absolute times in 91// 100 nanosecond units. For example 5:30 pm of June 15, 2009 is 3580464. 92int FileTimeToHours(const FILETIME& time) { 93 const ULONGLONG k100sNanoSecsToHours = 10000000LL * 60 * 60; 94 ULARGE_INTEGER uli = {time.dwLowDateTime, time.dwHighDateTime}; 95 return static_cast<int>(uli.QuadPart / k100sNanoSecsToHours); 96} 97 98// Returns the directory last write time in hours since January 1, 1601. 99// Returns -1 if there was an error retrieving the directory time. 100int GetDirectoryWriteTimeInHours(const wchar_t* path) { 101 // To open a directory you need to pass FILE_FLAG_BACKUP_SEMANTICS. 102 DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; 103 HANDLE file = ::CreateFileW(path, 0, share, NULL, OPEN_EXISTING, 104 FILE_FLAG_BACKUP_SEMANTICS, NULL); 105 if (INVALID_HANDLE_VALUE == file) 106 return -1; 107 FILETIME time; 108 if (!::GetFileTime(file, NULL, NULL, &time)) { 109 ::CloseHandle(file); 110 return -1; 111 } 112 113 ::CloseHandle(file); 114 return FileTimeToHours(time); 115} 116 117// Returns the directory last-write time age in hours, relative to current 118// time, so if it returns 14 it means that the directory was last written 14 119// hours ago. Returns -1 if there was an error retrieving the directory. 120int GetDirectoryWriteAgeInHours(const wchar_t* path) { 121 int dir_time = GetDirectoryWriteTimeInHours(path); 122 if (dir_time < 0) 123 return dir_time; 124 FILETIME time; 125 GetSystemTimeAsFileTime(&time); 126 int now_time = FileTimeToHours(time); 127 if (dir_time >= now_time) 128 return 0; 129 return (now_time - dir_time); 130} 131 132// Launches setup.exe (located at |setup_path|) with |cmd_line|. 133// If system_level_toast is true, appends --system-level-toast. 134// If handle to experiment result key was given at startup, re-add it. 135// Does not wait for the process to terminate. 136// |cmd_line| may be modified as a result of this call. 137bool LaunchSetup(CommandLine* cmd_line, 138 const installer::Product& product, 139 bool system_level_toast) { 140 const CommandLine& current_cmd_line = *CommandLine::ForCurrentProcess(); 141 142 // Propagate --verbose-logging to the invoked setup.exe. 143 if (current_cmd_line.HasSwitch(installer::switches::kVerboseLogging)) 144 cmd_line->AppendSwitch(installer::switches::kVerboseLogging); 145 146 // Pass along product-specific options. 147 product.AppendProductFlags(cmd_line); 148 149 // Re-add the system level toast flag. 150 if (system_level_toast) { 151 cmd_line->AppendSwitch(installer::switches::kSystemLevel); 152 cmd_line->AppendSwitch(installer::switches::kSystemLevelToast); 153 154 // Re-add the toast result key. We need to do this because Setup running as 155 // system passes the key to Setup running as user, but that child process 156 // does not perform the actual toasting, it launches another Setup (as user) 157 // to do so. That is the process that needs the key. 158 std::string key(installer::switches::kToastResultsKey); 159 std::string toast_key = current_cmd_line.GetSwitchValueASCII(key); 160 if (!toast_key.empty()) { 161 cmd_line->AppendSwitchASCII(key, toast_key); 162 163 // Use handle inheritance to make sure the duplicated toast results key 164 // gets inherited by the child process. 165 base::LaunchOptions options; 166 options.inherit_handles = true; 167 return base::LaunchProcess(*cmd_line, options, NULL); 168 } 169 } 170 171 return base::LaunchProcess(*cmd_line, base::LaunchOptions(), NULL); 172} 173 174// For System level installs, setup.exe lives in the system temp, which 175// is normally c:\windows\temp. In many cases files inside this folder 176// are not accessible for execution by regular user accounts. 177// This function changes the permissions so that any authenticated user 178// can launch |exe| later on. This function should only be called if the 179// code is running at the system level. 180bool FixDACLsForExecute(const FilePath& exe) { 181 // The general strategy to is to add an ACE to the exe DACL the quick 182 // and dirty way: a) read the DACL b) convert it to sddl string c) add the 183 // new ACE to the string d) convert sddl string back to DACL and finally 184 // e) write new dacl. 185 char buff[1024]; 186 DWORD len = sizeof(buff); 187 PSECURITY_DESCRIPTOR sd = reinterpret_cast<PSECURITY_DESCRIPTOR>(buff); 188 if (!::GetFileSecurityW(exe.value().c_str(), DACL_SECURITY_INFORMATION, 189 sd, len, &len)) { 190 return false; 191 } 192 wchar_t* sddl = 0; 193 if (!::ConvertSecurityDescriptorToStringSecurityDescriptorW(sd, 194 SDDL_REVISION_1, DACL_SECURITY_INFORMATION, &sddl, NULL)) 195 return false; 196 string16 new_sddl(sddl); 197 ::LocalFree(sddl); 198 sd = NULL; 199 // See MSDN for the security descriptor definition language (SDDL) syntax, 200 // in our case we add "A;" generic read 'GR' and generic execute 'GX' for 201 // the nt\authenticated_users 'AU' group, that becomes: 202 const wchar_t kAllowACE[] = L"(A;;GRGX;;;AU)"; 203 // We should check that there are no special ACES for the group we 204 // are interested, which is nt\authenticated_users. 205 if (string16::npos != new_sddl.find(L";AU)")) 206 return false; 207 // Specific ACEs (not inherited) need to go to the front. It is ok if we 208 // are the very first one. 209 size_t pos_insert = new_sddl.find(L"("); 210 if (string16::npos == pos_insert) 211 return false; 212 // All good, time to change the dacl. 213 new_sddl.insert(pos_insert, kAllowACE); 214 if (!::ConvertStringSecurityDescriptorToSecurityDescriptorW(new_sddl.c_str(), 215 SDDL_REVISION_1, &sd, NULL)) 216 return false; 217 bool rv = ::SetFileSecurityW(exe.value().c_str(), DACL_SECURITY_INFORMATION, 218 sd) == TRUE; 219 ::LocalFree(sd); 220 return rv; 221} 222 223// This function launches setup as the currently logged-in interactive 224// user that is the user whose logon session is attached to winsta0\default. 225// It assumes that currently we are running as SYSTEM in a non-interactive 226// windowstation. 227// The function fails if there is no interactive session active, basically 228// the computer is on but nobody has logged in locally. 229// Remote Desktop sessions do not count as interactive sessions; running this 230// method as a user logged in via remote desktop will do nothing. 231bool LaunchSetupAsConsoleUser(const FilePath& setup_path, 232 const installer::Product& product, 233 const std::string& flag) { 234 CommandLine cmd_line(setup_path); 235 cmd_line.AppendSwitch(flag); 236 237 // Pass along product-specific options. 238 product.AppendProductFlags(&cmd_line); 239 240 // Convey to the invoked setup.exe that it's operating on a system-level 241 // installation. 242 cmd_line.AppendSwitch(installer::switches::kSystemLevel); 243 244 // Propagate --verbose-logging to the invoked setup.exe. 245 if (CommandLine::ForCurrentProcess()->HasSwitch( 246 installer::switches::kVerboseLogging)) { 247 cmd_line.AppendSwitch(installer::switches::kVerboseLogging); 248 } 249 250 // Get the Google Update results key, and pass it on the command line to 251 // the child process. 252 int key = GoogleUpdateSettings::DuplicateGoogleUpdateSystemClientKey(); 253 cmd_line.AppendSwitchASCII(installer::switches::kToastResultsKey, 254 base::IntToString(key)); 255 256 if (base::win::GetVersion() > base::win::VERSION_XP) { 257 // Make sure that in Vista and Above we have the proper DACLs so 258 // the interactive user can launch it. 259 if (!FixDACLsForExecute(setup_path)) 260 NOTREACHED(); 261 } 262 263 DWORD console_id = ::WTSGetActiveConsoleSessionId(); 264 if (console_id == 0xFFFFFFFF) { 265 PLOG(ERROR) << __FUNCTION__ << " failed to get active session id"; 266 return false; 267 } 268 HANDLE user_token; 269 if (!::WTSQueryUserToken(console_id, &user_token)) { 270 PLOG(ERROR) << __FUNCTION__ << " failed to get user token for console_id " 271 << console_id; 272 return false; 273 } 274 // Note: Handle inheritance must be true in order for the child process to be 275 // able to use the duplicated handle above (Google Update results). 276 base::LaunchOptions options; 277 options.as_user = user_token; 278 options.inherit_handles = true; 279 options.empty_desktop_name = true; 280 VLOG(1) << __FUNCTION__ << " launching " << cmd_line.GetCommandLineString(); 281 bool launched = base::LaunchProcess(cmd_line, options, NULL); 282 ::CloseHandle(user_token); 283 VLOG(1) << __FUNCTION__ << " result: " << launched; 284 return launched; 285} 286 287} // namespace 288 289GoogleChromeDistribution::GoogleChromeDistribution() 290 : BrowserDistribution(CHROME_BROWSER), 291 product_guid_(kChromeGuid) { 292} 293 294// The functions below are not used by the 64-bit Windows binary - 295// see the comment in google_chrome_distribution_dummy.cc 296#ifndef _WIN64 297bool GoogleChromeDistribution::BuildUninstallMetricsString( 298 const DictionaryValue* uninstall_metrics_dict, string16* metrics) { 299 DCHECK(NULL != metrics); 300 bool has_values = false; 301 302 for (DictionaryValue::key_iterator iter(uninstall_metrics_dict->begin_keys()); 303 iter != uninstall_metrics_dict->end_keys(); ++iter) { 304 has_values = true; 305 metrics->append(L"&"); 306 metrics->append(UTF8ToWide(*iter)); 307 metrics->append(L"="); 308 309 std::string value; 310 uninstall_metrics_dict->GetStringWithoutPathExpansion(*iter, &value); 311 metrics->append(UTF8ToWide(value)); 312 } 313 314 return has_values; 315} 316 317bool GoogleChromeDistribution::ExtractUninstallMetricsFromFile( 318 const FilePath& file_path, 319 string16* uninstall_metrics_string) { 320 JSONFileValueSerializer json_serializer(file_path); 321 322 std::string json_error_string; 323 scoped_ptr<Value> root(json_serializer.Deserialize(NULL, NULL)); 324 if (!root.get()) 325 return false; 326 327 // Preferences should always have a dictionary root. 328 if (!root->IsType(Value::TYPE_DICTIONARY)) 329 return false; 330 331 return ExtractUninstallMetrics(*static_cast<DictionaryValue*>(root.get()), 332 uninstall_metrics_string); 333} 334 335bool GoogleChromeDistribution::ExtractUninstallMetrics( 336 const DictionaryValue& root, 337 string16* uninstall_metrics_string) { 338 // Make sure that the user wants us reporting metrics. If not, don't 339 // add our uninstall metrics. 340 bool metrics_reporting_enabled = false; 341 if (!root.GetBoolean(prefs::kMetricsReportingEnabled, 342 &metrics_reporting_enabled) || 343 !metrics_reporting_enabled) { 344 return false; 345 } 346 347 const DictionaryValue* uninstall_metrics_dict = NULL; 348 if (!root.HasKey(installer::kUninstallMetricsName) || 349 !root.GetDictionary(installer::kUninstallMetricsName, 350 &uninstall_metrics_dict)) { 351 return false; 352 } 353 354 if (!BuildUninstallMetricsString(uninstall_metrics_dict, 355 uninstall_metrics_string)) { 356 return false; 357 } 358 359 return true; 360} 361#endif 362 363void GoogleChromeDistribution::DoPostUninstallOperations( 364 const Version& version, 365 const FilePath& local_data_path, 366 const string16& distribution_data) { 367 // Send the Chrome version and OS version as params to the form. 368 // It would be nice to send the locale, too, but I don't see an 369 // easy way to get that in the existing code. It's something we 370 // can add later, if needed. 371 // We depend on installed_version.GetString() not having spaces or other 372 // characters that need escaping: 0.2.13.4. Should that change, we will 373 // need to escape the string before using it in a URL. 374 const string16 kVersionParam = L"crversion"; 375 const string16 kOSParam = L"os"; 376 base::win::OSInfo::VersionNumber version_number = 377 base::win::OSInfo::GetInstance()->version_number(); 378 string16 os_version = base::StringPrintf(L"%d.%d.%d", 379 version_number.major, version_number.minor, version_number.build); 380 381 FilePath iexplore; 382 if (!PathService::Get(base::DIR_PROGRAM_FILES, &iexplore)) 383 return; 384 385 iexplore = iexplore.AppendASCII("Internet Explorer"); 386 iexplore = iexplore.AppendASCII("iexplore.exe"); 387 388 string16 command = iexplore.value() + L" " + GetUninstallSurveyUrl() + 389 L"&" + kVersionParam + L"=" + UTF8ToWide(version.GetString()) + L"&" + 390 kOSParam + L"=" + os_version; 391 392 string16 uninstall_metrics; 393 if (ExtractUninstallMetricsFromFile(local_data_path, &uninstall_metrics)) { 394 // The user has opted into anonymous usage data collection, so append 395 // metrics and distribution data. 396 command += uninstall_metrics; 397 if (!distribution_data.empty()) { 398 command += L"&"; 399 command += distribution_data; 400 } 401 } 402 403 int pid = 0; 404 // The reason we use WMI to launch the process is because the uninstall 405 // process runs inside a Job object controlled by the shell. As long as there 406 // are processes running, the shell will not close the uninstall applet. WMI 407 // allows us to escape from the Job object so the applet will close. 408 installer::WMIProcess::Launch(command, &pid); 409} 410 411string16 GoogleChromeDistribution::GetAppGuid() { 412 return product_guid(); 413} 414 415string16 GoogleChromeDistribution::GetBaseAppName() { 416 // I'd really like to return L ## PRODUCT_FULLNAME_STRING; but that's no good 417 // since it'd be "Chromium" in a non-Chrome build, which isn't at all what I 418 // want. Sigh. 419 return L"Google Chrome"; 420} 421 422string16 GoogleChromeDistribution::GetAppShortCutName() { 423 const string16& app_shortcut_name = 424 installer::GetLocalizedString(IDS_PRODUCT_NAME_BASE); 425 return app_shortcut_name; 426} 427 428string16 GoogleChromeDistribution::GetAlternateApplicationName() { 429 const string16& alt_product_name = 430 installer::GetLocalizedString(IDS_OEM_MAIN_SHORTCUT_NAME_BASE); 431 return alt_product_name; 432} 433 434string16 GoogleChromeDistribution::GetBaseAppId() { 435 return kBrowserAppId; 436} 437 438string16 GoogleChromeDistribution::GetInstallSubDir() { 439 string16 sub_dir(installer::kGoogleChromeInstallSubDir1); 440 sub_dir.append(L"\\"); 441 sub_dir.append(installer::kGoogleChromeInstallSubDir2); 442 return sub_dir; 443} 444 445string16 GoogleChromeDistribution::GetPublisherName() { 446 const string16& publisher_name = 447 installer::GetLocalizedString(IDS_ABOUT_VERSION_COMPANY_NAME_BASE); 448 return publisher_name; 449} 450 451string16 GoogleChromeDistribution::GetAppDescription() { 452 const string16& app_description = 453 installer::GetLocalizedString(IDS_SHORTCUT_TOOLTIP_BASE); 454 return app_description; 455} 456 457std::string GoogleChromeDistribution::GetSafeBrowsingName() { 458 return "googlechrome"; 459} 460 461string16 GoogleChromeDistribution::GetStateKey() { 462 string16 key(google_update::kRegPathClientState); 463 key.append(L"\\"); 464 key.append(product_guid()); 465 return key; 466} 467 468string16 GoogleChromeDistribution::GetStateMediumKey() { 469 string16 key(google_update::kRegPathClientStateMedium); 470 key.append(L"\\"); 471 key.append(product_guid()); 472 return key; 473} 474 475string16 GoogleChromeDistribution::GetStatsServerURL() { 476 return L"https://clients4.google.com/firefox/metrics/collect"; 477} 478 479std::string GoogleChromeDistribution::GetNetworkStatsServer() const { 480 return chrome_common_net::kEchoTestServerLocation; 481} 482 483std::string GoogleChromeDistribution::GetHttpPipeliningTestServer() const { 484 return chrome_common_net::kPipelineTestServerBaseUrl; 485} 486 487string16 GoogleChromeDistribution::GetDistributionData(HKEY root_key) { 488 string16 sub_key(google_update::kRegPathClientState); 489 sub_key.append(L"\\"); 490 sub_key.append(product_guid()); 491 492 base::win::RegKey client_state_key(root_key, sub_key.c_str(), KEY_READ); 493 string16 result; 494 string16 brand_value; 495 if (client_state_key.ReadValue(google_update::kRegRLZBrandField, 496 &brand_value) == ERROR_SUCCESS) { 497 result = google_update::kRegRLZBrandField; 498 result.append(L"="); 499 result.append(brand_value); 500 result.append(L"&"); 501 } 502 503 string16 client_value; 504 if (client_state_key.ReadValue(google_update::kRegClientField, 505 &client_value) == ERROR_SUCCESS) { 506 result.append(google_update::kRegClientField); 507 result.append(L"="); 508 result.append(client_value); 509 result.append(L"&"); 510 } 511 512 string16 ap_value; 513 // If we fail to read the ap key, send up "&ap=" anyway to indicate 514 // that this was probably a stable channel release. 515 client_state_key.ReadValue(google_update::kRegApField, &ap_value); 516 result.append(google_update::kRegApField); 517 result.append(L"="); 518 result.append(ap_value); 519 520 return result; 521} 522 523string16 GoogleChromeDistribution::GetUninstallLinkName() { 524 const string16& link_name = 525 installer::GetLocalizedString(IDS_UNINSTALL_CHROME_BASE); 526 return link_name; 527} 528 529string16 GoogleChromeDistribution::GetUninstallRegPath() { 530 return L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" 531 L"Google Chrome"; 532} 533 534string16 GoogleChromeDistribution::GetVersionKey() { 535 string16 key(google_update::kRegPathClients); 536 key.append(L"\\"); 537 key.append(product_guid()); 538 return key; 539} 540 541bool GoogleChromeDistribution::GetCommandExecuteImplClsid( 542 string16* handler_class_uuid) { 543 if (handler_class_uuid) 544 *handler_class_uuid = kCommandExecuteImplUuid; 545 return true; 546} 547 548// This method checks if we need to change "ap" key in Google Update to try 549// full installer as fall back method in case incremental installer fails. 550// - If incremental installer fails we append a magic string ("-full"), if 551// it is not present already, so that Google Update server next time will send 552// full installer to update Chrome on the local machine 553// - If we are currently running full installer, we remove this magic 554// string (if it is present) regardless of whether installer failed or not. 555// There is no fall-back for full installer :) 556void GoogleChromeDistribution::UpdateInstallStatus(bool system_install, 557 installer::ArchiveType archive_type, 558 installer::InstallStatus install_status) { 559 GoogleUpdateSettings::UpdateInstallStatus(system_install, 560 archive_type, InstallUtil::GetInstallReturnCode(install_status), 561 product_guid()); 562} 563 564// The functions below are not used by the 64-bit Windows binary - 565// see the comment in google_chrome_distribution_dummy.cc 566#ifndef _WIN64 567// A helper function that writes to HKLM if the handle was passed through the 568// command line, but HKCU otherwise. |experiment_group| is the value to write 569// and |last_write| is used when writing to HKLM to determine whether to close 570// the handle when done. 571void SetClient(const string16& experiment_group, bool last_write) { 572 static int reg_key_handle = -1; 573 if (reg_key_handle == -1) { 574 // If a specific Toast Results key handle (presumably to our HKLM key) was 575 // passed in to the command line (such as for system level installs), we use 576 // it. Otherwise, we write to the key under HKCU. 577 const CommandLine& cmd_line = *CommandLine::ForCurrentProcess(); 578 if (cmd_line.HasSwitch(installer::switches::kToastResultsKey)) { 579 // Get the handle to the key under HKLM. 580 base::StringToInt(cmd_line.GetSwitchValueASCII( 581 installer::switches::kToastResultsKey).c_str(), 582 ®_key_handle); 583 } else { 584 reg_key_handle = 0; 585 } 586 } 587 588 if (reg_key_handle) { 589 // Use it to write the experiment results. 590 GoogleUpdateSettings::WriteGoogleUpdateSystemClientKey( 591 reg_key_handle, google_update::kRegClientField, experiment_group); 592 if (last_write) 593 CloseHandle((HANDLE) reg_key_handle); 594 } else { 595 // Write to HKCU. 596 GoogleUpdateSettings::SetClient(experiment_group); 597 } 598} 599 600bool GoogleChromeDistribution::GetExperimentDetails( 601 UserExperiment* experiment, int flavor) { 602 struct FlavorDetails { 603 int heading_id; 604 int flags; 605 }; 606 // Maximum number of experiment flavors we support. 607 static const int kMax = 4; 608 // This struct determines which experiment flavors we show for each locale and 609 // brand. 610 // 611 // The big experiment in Dec 2009 used TGxx and THxx. 612 // The big experiment in Feb 2010 used TKxx and TLxx. 613 // The big experiment in Apr 2010 used TMxx and TNxx. 614 // The big experiment in Oct 2010 used TVxx TWxx TXxx TYxx. 615 // The big experiment in Feb 2011 used SJxx SKxx SLxx SMxx. 616 // Note: the plugin infobar experiment uses PIxx codes. 617 using namespace attrition_experiments; 618 619 static const struct UserExperimentDetails { 620 const wchar_t* locale; // Locale to show this experiment for (* for all). 621 const wchar_t* brands; // Brand codes show this experiment for (* for all). 622 int control_group; // Size of the control group, in percentages. 623 const wchar_t* prefix; // The two letter experiment code. The second letter 624 // will be incremented with the flavor. 625 FlavorDetails flavors[kMax]; 626 } kExperiments[] = { 627 // The first match from top to bottom is used so this list should be ordered 628 // most-specific rule first. 629 { L"*", L"CHMA", // All locales, CHMA brand. 630 25, // 25 percent control group. 631 L"ZA", // Experiment is ZAxx, ZBxx, ZCxx, ZDxx etc. 632 // Three flavors. 633 { { IDS_TRY_TOAST_HEADING3, kDontBugMeAsButton | kUninstall | kWhyLink }, 634 { IDS_TRY_TOAST_HEADING3, 0 }, 635 { IDS_TRY_TOAST_HEADING3, kMakeDefault }, 636 { 0, 0 }, 637 } 638 }, 639 { L"*", L"GGRV", // All locales, GGRV is enterprise. 640 0, // 0 percent control group. 641 L"EA", // Experiment is EAxx, EBxx, etc. 642 // No flavors means no experiment. 643 { { 0, 0 }, 644 { 0, 0 }, 645 { 0, 0 }, 646 { 0, 0 } 647 } 648 } 649 }; 650 651 string16 locale; 652 GoogleUpdateSettings::GetLanguage(&locale); 653 if (locale.empty() || (locale == ASCIIToWide("en"))) 654 locale = ASCIIToWide("en-US"); 655 656 string16 brand; 657 if (!GoogleUpdateSettings::GetBrand(&brand)) 658 brand = ASCIIToWide(""); // Could still be viable for catch-all rules. 659 660 for (int i = 0; i < arraysize(kExperiments); ++i) { 661 if (kExperiments[i].locale != locale && 662 kExperiments[i].locale != ASCIIToWide("*")) 663 continue; 664 665 std::vector<string16> brand_codes; 666 base::SplitString(kExperiments[i].brands, L',', &brand_codes); 667 if (brand_codes.empty()) 668 return false; 669 for (std::vector<string16>::iterator it = brand_codes.begin(); 670 it != brand_codes.end(); ++it) { 671 if (*it != brand && *it != L"*") 672 continue; 673 // We have found our match. 674 const UserExperimentDetails& match = kExperiments[i]; 675 // Find out how many flavors we have. Zero means no experiment. 676 int num_flavors = 0; 677 while (match.flavors[num_flavors].heading_id) { ++num_flavors; } 678 if (!num_flavors) 679 return false; 680 681 if (flavor < 0) 682 flavor = base::RandInt(0, num_flavors - 1); 683 experiment->flavor = flavor; 684 experiment->heading = match.flavors[flavor].heading_id; 685 experiment->control_group = match.control_group; 686 const wchar_t prefix[] = { match.prefix[0], match.prefix[1] + flavor, 0 }; 687 experiment->prefix = prefix; 688 experiment->flags = match.flavors[flavor].flags; 689 return true; 690 } 691 } 692 693 return false; 694} 695 696// Currently we only have one experiment: the inactive user toast. Which only 697// applies for users doing upgrades. 698 699// 700// There are three scenarios when this function is called: 701// 1- Is a per-user-install and it updated: perform the experiment 702// 2- Is a system-install and it updated : relaunch as the interactive user 703// 3- It has been re-launched from the #2 case. In this case we enter 704// this function with |system_install| true and a REENTRY_SYS_UPDATE status. 705void GoogleChromeDistribution::LaunchUserExperiment( 706 const FilePath& setup_path, installer::InstallStatus status, 707 const Version& version, const installer::Product& product, 708 bool system_level) { 709 VLOG(1) << "LaunchUserExperiment status: " << status << " product: " 710 << product.distribution()->GetAppShortCutName() 711 << " system_level: " << system_level; 712 713 if (system_level) { 714 if (installer::NEW_VERSION_UPDATED == status) { 715 // We need to relaunch as the interactive user. 716 LaunchSetupAsConsoleUser(setup_path, product, 717 installer::switches::kSystemLevelToast); 718 return; 719 } 720 } else { 721 if ((installer::NEW_VERSION_UPDATED != status) && 722 (installer::REENTRY_SYS_UPDATE != status)) { 723 // We are not updating or in re-launch. Exit. 724 return; 725 } 726 } 727 728 // The |flavor| value ends up being processed by TryChromeDialogView to show 729 // different experiments. 730 UserExperiment experiment; 731 if (!GetExperimentDetails(&experiment, -1)) { 732 VLOG(1) << "Failed to get experiment details."; 733 return; 734 } 735 int flavor = experiment.flavor; 736 string16 base_group = experiment.prefix; 737 738 string16 brand; 739 if (GoogleUpdateSettings::GetBrand(&brand) && (brand == L"CHXX")) { 740 // Testing only: the user automatically qualifies for the experiment. 741 VLOG(1) << "Experiment qualification bypass"; 742 } else { 743 // Check that the user was not already drafted in this experiment. 744 string16 client; 745 GoogleUpdateSettings::GetClient(&client); 746 if (client.size() > 2) { 747 if (base_group == client.substr(0, 2)) { 748 VLOG(1) << "User already participated in this experiment"; 749 return; 750 } 751 } 752 // Check browser usage inactivity by the age of the last-write time of the 753 // most recently-used chrome user data directory. 754 std::vector<FilePath> user_data_dirs; 755 product.GetUserDataPaths(&user_data_dirs); 756 int dir_age_hours = -1; 757 for (size_t i = 0; i < user_data_dirs.size(); ++i) { 758 int this_age = GetDirectoryWriteAgeInHours( 759 user_data_dirs[i].value().c_str()); 760 if (this_age >= 0 && (dir_age_hours < 0 || this_age < dir_age_hours)) 761 dir_age_hours = this_age; 762 } 763 764 const bool experiment_enabled = false; 765 const int kThirtyDays = 30 * 24; 766 767 if (!experiment_enabled) { 768 VLOG(1) << "Toast experiment is disabled."; 769 return; 770 } else if (dir_age_hours < 0) { 771 // This means that we failed to find the user data dir. The most likely 772 // cause is that this user has not ever used chrome at all which can 773 // happen in a system-level install. 774 SetClient(base_group + kToastUDDirFailure, true); 775 return; 776 } else if (dir_age_hours < kThirtyDays) { 777 // An active user, so it does not qualify. 778 VLOG(1) << "Chrome used in last " << dir_age_hours << " hours"; 779 SetClient(base_group + kToastActiveGroup, true); 780 return; 781 } 782 // Check to see if this user belongs to the control group. 783 double control_group = 1.0 * (100 - experiment.control_group) / 100; 784 if (base::RandDouble() > control_group) { 785 SetClient(base_group + kToastExpControlGroup, true); 786 VLOG(1) << "User is control group"; 787 return; 788 } 789 } 790 791 VLOG(1) << "User drafted for toast experiment " << flavor; 792 SetClient(base_group + kToastExpBaseGroup, false); 793 // User level: The experiment needs to be performed in a different process 794 // because google_update expects the upgrade process to be quick and nimble. 795 // System level: We have already been relaunched, so we don't need to be 796 // quick, but we relaunch to follow the exact same codepath. 797 CommandLine cmd_line(setup_path); 798 cmd_line.AppendSwitchASCII(installer::switches::kInactiveUserToast, 799 base::IntToString(flavor)); 800 cmd_line.AppendSwitchASCII(installer::switches::kExperimentGroup, 801 WideToASCII(base_group)); 802 LaunchSetup(&cmd_line, product, system_level); 803} 804 805// User qualifies for the experiment. To test, use --try-chrome-again=|flavor| 806// as a parameter to chrome.exe. 807void GoogleChromeDistribution::InactiveUserToastExperiment(int flavor, 808 const string16& experiment_group, 809 const installer::Product& installation, 810 const FilePath& application_path) { 811 // Add the 'welcome back' url for chrome to show. 812 CommandLine options(CommandLine::NO_PROGRAM); 813 options.AppendSwitchNative(switches::kTryChromeAgain, 814 base::IntToString16(flavor)); 815 // Prepend the url with a space. 816 string16 url(GetWelcomeBackUrl()); 817 options.AppendArg("--"); 818 options.AppendArgNative(url); 819 // The command line should now have the url added as: 820 // "chrome.exe -- <url>" 821 DCHECK_NE(string16::npos, 822 options.GetCommandLineString().find(L" -- " + url)); 823 824 // Launch chrome now. It will show the toast UI. 825 int32 exit_code = 0; 826 if (!installation.LaunchChromeAndWait(application_path, options, &exit_code)) 827 return; 828 829 // The chrome process has exited, figure out what happened. 830 const wchar_t* outcome = NULL; 831 switch (exit_code) { 832 case content::RESULT_CODE_NORMAL_EXIT: 833 outcome = kToastExpTriesOkGroup; 834 break; 835 case chrome::RESULT_CODE_NORMAL_EXIT_CANCEL: 836 outcome = kToastExpCancelGroup; 837 break; 838 case chrome::RESULT_CODE_NORMAL_EXIT_EXP2: 839 outcome = kToastExpUninstallGroup; 840 break; 841 default: 842 outcome = kToastExpTriesErrorGroup; 843 }; 844 845 if (outcome == kToastExpTriesOkGroup) { 846 // User tried chrome, but if it had the default group button it belongs 847 // to a different outcome group. 848 UserExperiment experiment; 849 if (GetExperimentDetails(&experiment, flavor)) { 850 outcome = experiment.flags & kMakeDefault ? kToastExpTriesOkDefaultGroup : 851 kToastExpTriesOkGroup; 852 } 853 } 854 855 // Write to the |client| key for the last time. 856 SetClient(experiment_group + outcome, true); 857 858 if (outcome != kToastExpUninstallGroup) 859 return; 860 861 // The user wants to uninstall. This is a best effort operation. Note that 862 // we waited for chrome to exit so the uninstall would not detect chrome 863 // running. 864 bool system_level_toast = CommandLine::ForCurrentProcess()->HasSwitch( 865 installer::switches::kSystemLevelToast); 866 867 CommandLine cmd(InstallUtil::GetChromeUninstallCmd(system_level_toast, 868 GetType())); 869 base::LaunchProcess(cmd, base::LaunchOptions(), NULL); 870} 871#endif 872