1 // Copyright 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/installer/util/user_experiment.h"
12 #include "base/command_line.h"
13 #include "base/files/file_path.h"
14 #include "base/process/launch.h"
15 #include "base/rand_util.h"
16 #include "base/strings/string_number_conversions.h"
17 #include "base/strings/string_split.h"
18 #include "base/strings/string_util.h"
19 #include "base/strings/utf_string_conversions.h"
20 #include "base/win/scoped_handle.h"
21 #include "base/win/windows_version.h"
22 #include "chrome/common/attrition_experiments.h"
23 #include "chrome/common/chrome_result_codes.h"
24 #include "chrome/common/chrome_switches.h"
25 #include "chrome/installer/util/browser_distribution.h"
26 #include "chrome/installer/util/google_update_constants.h"
27 #include "chrome/installer/util/google_update_settings.h"
28 #include "chrome/installer/util/helper.h"
29 #include "chrome/installer/util/install_util.h"
30 #include "chrome/installer/util/product.h"
31 #include "content/public/common/result_codes.h"
33 #pragma comment(lib, "wtsapi32.lib")
39 // The following strings are the possible outcomes of the toast experiment
40 // as recorded in the |client| field.
41 const wchar_t kToastExpControlGroup
[] = L
"01";
42 const wchar_t kToastExpCancelGroup
[] = L
"02";
43 const wchar_t kToastExpUninstallGroup
[] = L
"04";
44 const wchar_t kToastExpTriesOkGroup
[] = L
"18";
45 const wchar_t kToastExpTriesErrorGroup
[] = L
"28";
46 const wchar_t kToastActiveGroup
[] = L
"40";
47 const wchar_t kToastUDDirFailure
[] = L
"40";
48 const wchar_t kToastExpBaseGroup
[] = L
"80";
50 // Substitute the locale parameter in uninstall URL with whatever
51 // Google Update tells us is the locale. In case we fail to find
52 // the locale, we use US English.
53 base::string16
LocalizeUrl(const wchar_t* url
) {
54 base::string16 language
;
55 if (!GoogleUpdateSettings::GetLanguage(&language
))
56 language
= L
"en-US"; // Default to US English.
57 return ReplaceStringPlaceholders(url
, language
.c_str(), NULL
);
60 base::string16
GetWelcomeBackUrl() {
61 const wchar_t kWelcomeUrl
[] = L
"http://www.google.com/chrome/intl/$1/"
62 L
"welcomeback-new.html";
63 return LocalizeUrl(kWelcomeUrl
);
66 // Converts FILETIME to hours. FILETIME times are absolute times in
67 // 100 nanosecond units. For example 5:30 pm of June 15, 2009 is 3580464.
68 int FileTimeToHours(const FILETIME
& time
) {
69 const ULONGLONG k100sNanoSecsToHours
= 10000000LL * 60 * 60;
70 ULARGE_INTEGER uli
= {time
.dwLowDateTime
, time
.dwHighDateTime
};
71 return static_cast<int>(uli
.QuadPart
/ k100sNanoSecsToHours
);
74 // Returns the directory last write time in hours since January 1, 1601.
75 // Returns -1 if there was an error retrieving the directory time.
76 int GetDirectoryWriteTimeInHours(const wchar_t* path
) {
77 // To open a directory you need to pass FILE_FLAG_BACKUP_SEMANTICS.
78 DWORD share
= FILE_SHARE_READ
| FILE_SHARE_WRITE
| FILE_SHARE_DELETE
;
79 base::win::ScopedHandle
file(::CreateFileW(path
, 0, share
, NULL
,
80 OPEN_EXISTING
, FILE_FLAG_BACKUP_SEMANTICS
, NULL
));
85 return ::GetFileTime(file
, NULL
, NULL
, &time
) ? FileTimeToHours(time
) : -1;
88 // Returns the directory last-write time age in hours, relative to current
89 // time, so if it returns 14 it means that the directory was last written 14
90 // hours ago. Returns -1 if there was an error retrieving the directory.
91 int GetDirectoryWriteAgeInHours(const wchar_t* path
) {
92 int dir_time
= GetDirectoryWriteTimeInHours(path
);
96 GetSystemTimeAsFileTime(&time
);
97 int now_time
= FileTimeToHours(time
);
98 if (dir_time
>= now_time
)
100 return (now_time
- dir_time
);
103 // Launches setup.exe (located at |setup_path|) with |cmd_line|.
104 // If system_level_toast is true, appends --system-level-toast.
105 // If handle to experiment result key was given at startup, re-add it.
106 // Does not wait for the process to terminate.
107 // |cmd_line| may be modified as a result of this call.
108 bool LaunchSetup(CommandLine
* cmd_line
, bool system_level_toast
) {
109 const CommandLine
& current_cmd_line
= *CommandLine::ForCurrentProcess();
111 // Propagate --verbose-logging to the invoked setup.exe.
112 if (current_cmd_line
.HasSwitch(switches::kVerboseLogging
))
113 cmd_line
->AppendSwitch(switches::kVerboseLogging
);
115 // Re-add the system level toast flag.
116 if (system_level_toast
) {
117 cmd_line
->AppendSwitch(switches::kSystemLevel
);
118 cmd_line
->AppendSwitch(switches::kSystemLevelToast
);
120 // Re-add the toast result key. We need to do this because Setup running as
121 // system passes the key to Setup running as user, but that child process
122 // does not perform the actual toasting, it launches another Setup (as user)
123 // to do so. That is the process that needs the key.
124 std::string
key(switches::kToastResultsKey
);
125 std::string toast_key
= current_cmd_line
.GetSwitchValueASCII(key
);
126 if (!toast_key
.empty()) {
127 cmd_line
->AppendSwitchASCII(key
, toast_key
);
129 // Use handle inheritance to make sure the duplicated toast results key
130 // gets inherited by the child process.
131 base::LaunchOptions options
;
132 options
.inherit_handles
= true;
133 return base::LaunchProcess(*cmd_line
, options
, NULL
);
137 return base::LaunchProcess(*cmd_line
, base::LaunchOptions(), NULL
);
140 // For System level installs, setup.exe lives in the system temp, which
141 // is normally c:\windows\temp. In many cases files inside this folder
142 // are not accessible for execution by regular user accounts.
143 // This function changes the permissions so that any authenticated user
144 // can launch |exe| later on. This function should only be called if the
145 // code is running at the system level.
146 bool FixDACLsForExecute(const base::FilePath
& exe
) {
147 // The general strategy to is to add an ACE to the exe DACL the quick
148 // and dirty way: a) read the DACL b) convert it to sddl string c) add the
149 // new ACE to the string d) convert sddl string back to DACL and finally
150 // e) write new dacl.
152 DWORD len
= sizeof(buff
);
153 PSECURITY_DESCRIPTOR sd
= reinterpret_cast<PSECURITY_DESCRIPTOR
>(buff
);
154 if (!::GetFileSecurityW(exe
.value().c_str(), DACL_SECURITY_INFORMATION
,
159 if (!::ConvertSecurityDescriptorToStringSecurityDescriptorW(sd
,
160 SDDL_REVISION_1
, DACL_SECURITY_INFORMATION
, &sddl
, NULL
))
162 base::string16
new_sddl(sddl
);
165 // See MSDN for the security descriptor definition language (SDDL) syntax,
166 // in our case we add "A;" generic read 'GR' and generic execute 'GX' for
167 // the nt\authenticated_users 'AU' group, that becomes:
168 const wchar_t kAllowACE
[] = L
"(A;;GRGX;;;AU)";
169 // We should check that there are no special ACES for the group we
170 // are interested, which is nt\authenticated_users.
171 if (base::string16::npos
!= new_sddl
.find(L
";AU)"))
173 // Specific ACEs (not inherited) need to go to the front. It is ok if we
174 // are the very first one.
175 size_t pos_insert
= new_sddl
.find(L
"(");
176 if (base::string16::npos
== pos_insert
)
178 // All good, time to change the dacl.
179 new_sddl
.insert(pos_insert
, kAllowACE
);
180 if (!::ConvertStringSecurityDescriptorToSecurityDescriptorW(new_sddl
.c_str(),
181 SDDL_REVISION_1
, &sd
, NULL
))
183 bool rv
= ::SetFileSecurityW(exe
.value().c_str(), DACL_SECURITY_INFORMATION
,
189 // This function launches setup as the currently logged-in interactive
190 // user that is the user whose logon session is attached to winsta0\default.
191 // It assumes that currently we are running as SYSTEM in a non-interactive
193 // The function fails if there is no interactive session active, basically
194 // the computer is on but nobody has logged in locally.
195 // Remote Desktop sessions do not count as interactive sessions; running this
196 // method as a user logged in via remote desktop will do nothing.
197 bool LaunchSetupAsConsoleUser(CommandLine
* cmd_line
) {
198 // Convey to the invoked setup.exe that it's operating on a system-level
200 cmd_line
->AppendSwitch(switches::kSystemLevel
);
202 // Propagate --verbose-logging to the invoked setup.exe.
203 if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kVerboseLogging
))
204 cmd_line
->AppendSwitch(switches::kVerboseLogging
);
206 // Get the Google Update results key, and pass it on the command line to
207 // the child process.
208 int key
= GoogleUpdateSettings::DuplicateGoogleUpdateSystemClientKey();
209 cmd_line
->AppendSwitchASCII(switches::kToastResultsKey
,
210 base::IntToString(key
));
212 if (base::win::GetVersion() > base::win::VERSION_XP
) {
213 // Make sure that in Vista and Above we have the proper DACLs so
214 // the interactive user can launch it.
215 if (!FixDACLsForExecute(cmd_line
->GetProgram()))
219 DWORD console_id
= ::WTSGetActiveConsoleSessionId();
220 if (console_id
== 0xFFFFFFFF) {
221 PLOG(ERROR
) << __FUNCTION__
<< " failed to get active session id";
225 if (!::WTSQueryUserToken(console_id
, &user_token
)) {
226 PLOG(ERROR
) << __FUNCTION__
<< " failed to get user token for console_id "
230 // Note: Handle inheritance must be true in order for the child process to be
231 // able to use the duplicated handle above (Google Update results).
232 base::LaunchOptions options
;
233 options
.as_user
= user_token
;
234 options
.inherit_handles
= true;
235 options
.empty_desktop_name
= true;
236 VLOG(1) << __FUNCTION__
<< " launching " << cmd_line
->GetCommandLineString();
237 bool launched
= base::LaunchProcess(*cmd_line
, options
, NULL
);
238 ::CloseHandle(user_token
);
239 VLOG(1) << __FUNCTION__
<< " result: " << launched
;
243 // A helper function that writes to HKLM if the handle was passed through the
244 // command line, but HKCU otherwise. |experiment_group| is the value to write
245 // and |last_write| is used when writing to HKLM to determine whether to close
246 // the handle when done.
247 void SetClient(const base::string16
& experiment_group
, bool last_write
) {
248 static int reg_key_handle
= -1;
249 if (reg_key_handle
== -1) {
250 // If a specific Toast Results key handle (presumably to our HKLM key) was
251 // passed in to the command line (such as for system level installs), we use
252 // it. Otherwise, we write to the key under HKCU.
253 const CommandLine
& cmd_line
= *CommandLine::ForCurrentProcess();
254 if (cmd_line
.HasSwitch(switches::kToastResultsKey
)) {
255 // Get the handle to the key under HKLM.
257 cmd_line
.GetSwitchValueNative(switches::kToastResultsKey
),
264 if (reg_key_handle
) {
265 // Use it to write the experiment results.
266 GoogleUpdateSettings::WriteGoogleUpdateSystemClientKey(
267 reg_key_handle
, google_update::kRegClientField
, experiment_group
);
269 CloseHandle((HANDLE
) reg_key_handle
);
272 GoogleUpdateSettings::SetClient(experiment_group
);
278 bool CreateExperimentDetails(int flavor
, ExperimentDetails
* experiment
) {
279 struct FlavorDetails
{
283 // Maximum number of experiment flavors we support.
284 static const int kMax
= 4;
285 // This struct determines which experiment flavors we show for each locale and
288 // Plugin infobar experiment:
289 // The experiment in 2011 used PIxx codes.
291 // Inactive user toast experiment:
292 // The experiment in Dec 2009 used TGxx and THxx.
293 // The experiment in Feb 2010 used TKxx and TLxx.
294 // The experiment in Apr 2010 used TMxx and TNxx.
295 // The experiment in Oct 2010 used TVxx TWxx TXxx TYxx.
296 // The experiment in Feb 2011 used SJxx SKxx SLxx SMxx.
297 // The experiment in Mar 2012 used ZAxx ZBxx ZCxx.
298 // The experiment in Jan 2013 uses DAxx.
299 using namespace attrition_experiments
;
301 static const struct UserExperimentSpecs
{
302 const wchar_t* locale
; // Locale to show this experiment for (* for all).
303 const wchar_t* brands
; // Brand codes show this experiment for (* for all).
304 int control_group
; // Size of the control group, in percentages.
305 const wchar_t* prefix
; // The two letter experiment code. The second letter
306 // will be incremented with the flavor.
307 FlavorDetails flavors
[kMax
];
309 // The first match from top to bottom is used so this list should be ordered
310 // most-specific rule first.
311 { L
"*", L
"GGRV", // All locales, GGRV is enterprise.
312 0, // 0 percent control group.
313 L
"EA", // Experiment is EAxx, EBxx, etc.
314 // No flavors means no experiment.
321 { L
"*", L
"*", // All locales, all brands.
322 5, // 5 percent control group.
323 L
"DA", // Experiment is DAxx.
324 // One single flavor.
325 { { IDS_TRY_TOAST_HEADING3
, kToastUiMakeDefault
},
333 base::string16 locale
;
334 GoogleUpdateSettings::GetLanguage(&locale
);
335 if (locale
.empty() || (locale
== base::ASCIIToWide("en")))
336 locale
= base::ASCIIToWide("en-US");
338 base::string16 brand
;
339 if (!GoogleUpdateSettings::GetBrand(&brand
))
340 brand
= base::ASCIIToWide(""); // Could still be viable for catch-all rules
342 for (int i
= 0; i
< arraysize(kExperiments
); ++i
) {
343 if (kExperiments
[i
].locale
!= locale
&&
344 kExperiments
[i
].locale
!= base::ASCIIToWide("*"))
347 std::vector
<base::string16
> brand_codes
;
348 base::SplitString(kExperiments
[i
].brands
, L
',', &brand_codes
);
349 if (brand_codes
.empty())
351 for (std::vector
<base::string16
>::iterator it
= brand_codes
.begin();
352 it
!= brand_codes
.end(); ++it
) {
353 if (*it
!= brand
&& *it
!= L
"*")
355 // We have found our match.
356 const UserExperimentSpecs
& match
= kExperiments
[i
];
357 // Find out how many flavors we have. Zero means no experiment.
359 while (match
.flavors
[num_flavors
].heading_id
) { ++num_flavors
; }
364 flavor
= base::RandInt(0, num_flavors
- 1);
365 experiment
->flavor
= flavor
;
366 experiment
->heading
= match
.flavors
[flavor
].heading_id
;
367 experiment
->control_group
= match
.control_group
;
368 const wchar_t prefix
[] = { match
.prefix
[0], match
.prefix
[1] + flavor
, 0 };
369 experiment
->prefix
= prefix
;
370 experiment
->flags
= match
.flavors
[flavor
].flags
;
378 // Currently we only have one experiment: the inactive user toast. Which only
379 // applies for users doing upgrades.
381 // There are three scenarios when this function is called:
382 // 1- Is a per-user-install and it updated: perform the experiment
383 // 2- Is a system-install and it updated : relaunch as the interactive user
384 // 3- It has been re-launched from the #2 case. In this case we enter
385 // this function with |system_install| true and a REENTRY_SYS_UPDATE status.
386 void LaunchBrowserUserExperiment(const CommandLine
& base_cmd_line
,
387 InstallStatus status
,
390 if (NEW_VERSION_UPDATED
== status
) {
391 CommandLine
cmd_line(base_cmd_line
);
392 cmd_line
.AppendSwitch(switches::kSystemLevelToast
);
393 // We need to relaunch as the interactive user.
394 LaunchSetupAsConsoleUser(&cmd_line
);
398 if (status
!= NEW_VERSION_UPDATED
&& status
!= REENTRY_SYS_UPDATE
) {
399 // We are not updating or in re-launch. Exit.
404 // The |flavor| value ends up being processed by TryChromeDialogView to show
405 // different experiments.
406 ExperimentDetails experiment
;
407 if (!CreateExperimentDetails(-1, &experiment
)) {
408 VLOG(1) << "Failed to get experiment details.";
411 int flavor
= experiment
.flavor
;
412 base::string16 base_group
= experiment
.prefix
;
414 base::string16 brand
;
415 if (GoogleUpdateSettings::GetBrand(&brand
) && (brand
== L
"CHXX")) {
416 // Testing only: the user automatically qualifies for the experiment.
417 VLOG(1) << "Experiment qualification bypass";
419 // Check that the user was not already drafted in this experiment.
420 base::string16 client
;
421 GoogleUpdateSettings::GetClient(&client
);
422 if (client
.size() > 2) {
423 if (base_group
== client
.substr(0, 2)) {
424 VLOG(1) << "User already participated in this experiment";
428 // Check browser usage inactivity by the age of the last-write time of the
429 // most recently-used chrome user data directory.
430 std::vector
<base::FilePath
> user_data_dirs
;
431 BrowserDistribution
* dist
= BrowserDistribution::GetSpecificDistribution(
432 BrowserDistribution::CHROME_BROWSER
);
433 GetChromeUserDataPaths(dist
, &user_data_dirs
);
434 int dir_age_hours
= -1;
435 for (size_t i
= 0; i
< user_data_dirs
.size(); ++i
) {
436 int this_age
= GetDirectoryWriteAgeInHours(
437 user_data_dirs
[i
].value().c_str());
438 if (this_age
>= 0 && (dir_age_hours
< 0 || this_age
< dir_age_hours
))
439 dir_age_hours
= this_age
;
442 const bool experiment_enabled
= false;
443 const int kThirtyDays
= 30 * 24;
445 if (!experiment_enabled
) {
446 VLOG(1) << "Toast experiment is disabled.";
448 } else if (dir_age_hours
< 0) {
449 // This means that we failed to find the user data dir. The most likely
450 // cause is that this user has not ever used chrome at all which can
451 // happen in a system-level install.
452 SetClient(base_group
+ kToastUDDirFailure
, true);
454 } else if (dir_age_hours
< kThirtyDays
) {
455 // An active user, so it does not qualify.
456 VLOG(1) << "Chrome used in last " << dir_age_hours
<< " hours";
457 SetClient(base_group
+ kToastActiveGroup
, true);
460 // Check to see if this user belongs to the control group.
461 double control_group
= 1.0 * (100 - experiment
.control_group
) / 100;
462 if (base::RandDouble() > control_group
) {
463 SetClient(base_group
+ kToastExpControlGroup
, true);
464 VLOG(1) << "User is control group";
469 VLOG(1) << "User drafted for toast experiment " << flavor
;
470 SetClient(base_group
+ kToastExpBaseGroup
, false);
471 // User level: The experiment needs to be performed in a different process
472 // because google_update expects the upgrade process to be quick and nimble.
473 // System level: We have already been relaunched, so we don't need to be
474 // quick, but we relaunch to follow the exact same codepath.
475 CommandLine
cmd_line(base_cmd_line
);
476 cmd_line
.AppendSwitchASCII(switches::kInactiveUserToast
,
477 base::IntToString(flavor
));
478 cmd_line
.AppendSwitchASCII(switches::kExperimentGroup
,
479 WideToASCII(base_group
));
480 LaunchSetup(&cmd_line
, system_level
);
483 // User qualifies for the experiment. To test, use --try-chrome-again=|flavor|
484 // as a parameter to chrome.exe.
485 void InactiveUserToastExperiment(int flavor
,
486 const base::string16
& experiment_group
,
487 const Product
& product
,
488 const base::FilePath
& application_path
) {
489 // Add the 'welcome back' url for chrome to show.
490 CommandLine
options(CommandLine::NO_PROGRAM
);
491 options
.AppendSwitchNative(::switches::kTryChromeAgain
,
492 base::IntToString16(flavor
));
493 // Prepend the url with a space.
494 base::string16
url(GetWelcomeBackUrl());
495 options
.AppendArg("--");
496 options
.AppendArgNative(url
);
497 // The command line should now have the url added as:
498 // "chrome.exe -- <url>"
499 DCHECK_NE(base::string16::npos
,
500 options
.GetCommandLineString().find(L
" -- " + url
));
502 // Launch chrome now. It will show the toast UI.
504 if (!product
.LaunchChromeAndWait(application_path
, options
, &exit_code
))
507 // The chrome process has exited, figure out what happened.
508 const wchar_t* outcome
= NULL
;
510 case content::RESULT_CODE_NORMAL_EXIT
:
511 outcome
= kToastExpTriesOkGroup
;
513 case chrome::RESULT_CODE_NORMAL_EXIT_CANCEL
:
514 outcome
= kToastExpCancelGroup
;
516 case chrome::RESULT_CODE_NORMAL_EXIT_EXP2
:
517 outcome
= kToastExpUninstallGroup
;
520 outcome
= kToastExpTriesErrorGroup
;
522 // Write to the |client| key for the last time.
523 SetClient(experiment_group
+ outcome
, true);
525 if (outcome
!= kToastExpUninstallGroup
)
527 // The user wants to uninstall. This is a best effort operation. Note that
528 // we waited for chrome to exit so the uninstall would not detect chrome
530 bool system_level_toast
= CommandLine::ForCurrentProcess()->HasSwitch(
531 switches::kSystemLevelToast
);
533 CommandLine
cmd(InstallUtil::GetChromeUninstallCmd(
534 system_level_toast
, product
.distribution()->GetType()));
535 base::LaunchProcess(cmd
, base::LaunchOptions(), NULL
);
538 } // namespace installer