blob: 6ba0b929ae14e0d55733a9e49cf79630e6c29910 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""A tool for working with state associated with the M62 Chrome on Windows 10
retention experiment.
For example:
experiment_tool_win.py --channel beta --system-level --operation prep
An operation must be specified on the command line. The supported operations
are:
clean: Deletes all Windows registry state relating to the experiment.
prep: Prepares the install and user context to participate in the study.
Specifically, this operation:
- Makes it appear as if the user last ran Chrome 30 days ago.
- Adds test switches to Chrome's Active Setup registration so that
the next run of Chrome's installer via Active Setup runs the
experiment.
- Clears the user's Active Setup state so that the next user logon
for a system-level install results in running the experiment via
Active Setup.
- Puts the install into retention study group 1.
prelaunch: Writes experiment state to the registry in preparation for manual
testing of the UX.
"""
import argparse
import datetime
import math
import struct
import sys
from datetime import datetime, timedelta
import win32api
import win32security
import _winreg
def GetUserSidString():
"""Returns the current user's SID string."""
token_handle = win32security.OpenProcessToken(win32api.GetCurrentProcess(),
win32security.TOKEN_QUERY)
user_sid, _ = win32security.GetTokenInformation(token_handle,
win32security.TokenUser)
return win32security.ConvertSidToStringSid(user_sid)
def InternalTimeFromPyTime(pytime):
"""Returns a Chromium internal time value representing a Python datetime."""
# Microseconds since 1601-01-01 00:00:00 UTC
delta = pytime - datetime(1601, 1, 1)
return math.trunc(delta.total_seconds()) * 1000000 + delta.microseconds
class ChromeState:
"""An object that provides mutations on Chrome's state relating to the
user experiment.
"""
_CHANNEL_CONFIGS = {
'stable': {
'guid': '{8A69D345-D564-463c-AFF1-A69D9E530F96}'
},
'beta': {
'guid': '{8237E44A-0054-442C-B6B6-EA0509993955}'
},
'dev': {
'guid': '{401C381F-E0DE-4B85-8BD8-3F3F14FBDA57}'
},
'canary': {
'guid': '{4ea16ac7-fd5a-47c3-875b-dbf4a2008c20}'
},
}
_GOOGLE_UPDATE_PATH = 'Software\\Google\\Update'
_ACTIVE_SETUP_PATH = 'Software\\Microsoft\\Active Setup\\Installed ' + \
'Components\\'
_REG_QWORD = 11 # From winnt.h
def __init__(self, channel_name, system_level):
self._config = ChromeState._CHANNEL_CONFIGS[channel_name]
self._system_level = system_level
self._registry_root = _winreg.HKEY_LOCAL_MACHINE if self._system_level \
else _winreg.HKEY_CURRENT_USER
def SetRetentionStudyValue(self, study):
"""Sets the RetentionStudy value for the install."""
path = r'%s\ClientState\%s' % (ChromeState._GOOGLE_UPDATE_PATH,
self._config['guid'])
with _winreg.OpenKey(self._registry_root, path, 0,
_winreg.KEY_WOW64_32KEY |
_winreg.KEY_SET_VALUE) as key:
_winreg.SetValueEx(key, 'RetentionStudy', 0, _winreg.REG_DWORD, study)
def DeleteRetentionStudyValue(self):
"""Deletes the RetentionStudy value for the install."""
path = r'%s\ClientState\%s' % (ChromeState._GOOGLE_UPDATE_PATH,
self._config['guid'])
try:
with _winreg.OpenKey(self._registry_root, path, 0,
_winreg.KEY_WOW64_32KEY |
_winreg.KEY_SET_VALUE) as key:
_winreg.DeleteValue(key, 'RetentionStudy')
except WindowsError as error:
if error.winerror != 2:
raise
def SetExperimentLabelsValue(self, experiment_labels):
"""Sets the experiment_labels value for the install."""
medium = 'Medium' if self._system_level else ''
path = r'%s\ClientState%s\%s' % (ChromeState._GOOGLE_UPDATE_PATH, medium,
self._config['guid'])
with _winreg.OpenKey(self._registry_root, path, 0,
_winreg.KEY_WOW64_32KEY |
_winreg.KEY_SET_VALUE) as key:
_winreg.SetValueEx(key, 'experiment_labels', 0, _winreg.REG_SZ,
experiment_labels)
def DeleteExperimentLabelsValue(self):
"""Deletes the experiment_labels for the install."""
medium = 'Medium' if self._system_level else ''
path = r'%s\ClientState%s\%s' % (ChromeState._GOOGLE_UPDATE_PATH, medium,
self._config['guid'])
try:
with _winreg.OpenKey(self._registry_root, path, 0,
_winreg.KEY_WOW64_32KEY |
_winreg.KEY_SET_VALUE) as key:
_winreg.DeleteValue(key, 'experiment_labels')
except WindowsError as error:
if error.winerror != 2:
raise
def SetRetentionExperimentState(self, state):
"""Creates experiment state in the Retention key for |state|."""
medium = 'Medium' if self._system_level else ''
path = r'%s\ClientState%s\%s\Retention' % (ChromeState._GOOGLE_UPDATE_PATH,
medium, self._config['guid'])
if self._system_level:
path += '\\' + GetUserSidString()
# _winreg doesn't have support for REG_QWORD, so do it manually.
qword_zero = struct.pack('<q', 0)
with _winreg.CreateKeyEx(self._registry_root, path, 0,
_winreg.KEY_WOW64_32KEY |
_winreg.KEY_SET_VALUE) as key:
_winreg.SetValueEx(key, 'State', 0, _winreg.REG_DWORD, state)
_winreg.SetValueEx(key, 'Group', 0, _winreg.REG_DWORD, 0)
_winreg.SetValueEx(key, 'ToastLocation', 0, _winreg.REG_DWORD, 0)
_winreg.SetValueEx(key, 'InactiveDays', 0, _winreg.REG_DWORD, 0)
_winreg.SetValueEx(key, 'ToastCount', 0, _winreg.REG_DWORD, 0)
_winreg.SetValueEx(key, 'FirstDisplayTime', 0, ChromeState._REG_QWORD,
qword_zero)
_winreg.SetValueEx(key, 'LatestDisplayTime', 0, ChromeState._REG_QWORD,
qword_zero)
_winreg.SetValueEx(key, 'UserSessionUptime', 0, ChromeState._REG_QWORD,
qword_zero)
_winreg.SetValueEx(key, 'ActionDelay', 0, ChromeState._REG_QWORD,
qword_zero)
def DeleteRentionKey(self):
"""Deletes the Retention key for the current user."""
medium = 'Medium' if self._system_level else ''
path = r'%s\ClientState%s\%s\Retention' % (ChromeState._GOOGLE_UPDATE_PATH,
medium, self._config['guid'])
try:
if self._system_level:
_winreg.DeleteKeyEx(self._registry_root,
path + '\\' + GetUserSidString(),
_winreg.KEY_WOW64_32KEY)
_winreg.DeleteKeyEx(self._registry_root, path, _winreg.KEY_WOW64_32KEY)
except WindowsError as error:
if error.winerror != 2:
raise
def SetLastRunTime(self, delta):
"""Sets Chrome's lastrun time for the current user."""
path = r'%s\ClientState\%s' % (ChromeState._GOOGLE_UPDATE_PATH,
self._config['guid'])
lastrun = InternalTimeFromPyTime(datetime.utcnow() - delta)
with _winreg.CreateKeyEx(_winreg.HKEY_CURRENT_USER, path, 0,
_winreg.KEY_WOW64_32KEY |
_winreg.KEY_SET_VALUE) as key:
_winreg.SetValueEx(key, 'lastrun', 0, _winreg.REG_SZ, str(lastrun))
def AdjustActiveSetupCommand(self):
"""Adds --experiment-enterprise-bypass to system-level Chrome's Active Setup
command."""
if not self._system_level:
return
enable_switch = '--experiment-enable-for-testing'
bypass_switch = '--experiment-enterprise-bypass'
for flag in [_winreg.KEY_WOW64_32KEY, _winreg.KEY_WOW64_64KEY]:
try:
with _winreg.OpenKey(self._registry_root,
ChromeState._ACTIVE_SETUP_PATH +
self._config['guid'], 0,
_winreg.KEY_SET_VALUE | _winreg.KEY_QUERY_VALUE |
flag) as key:
command, _ = _winreg.QueryValueEx(key, 'StubPath')
if not bypass_switch in command:
command += ' ' + bypass_switch
if not enable_switch in command:
command += ' ' + enable_switch
_winreg.SetValueEx(key, 'StubPath', 0, _winreg.REG_SZ, command)
except WindowsError as error:
if error.winerror != 2:
raise
def ClearUserActiveSetup(self):
"""Clears per-user state associated with Active Setup so that it will run
again on next login."""
if not self._system_level:
return
paths = [ChromeState._ACTIVE_SETUP_PATH,
ChromeState._ACTIVE_SETUP_PATH.replace('Software\\',
'Software\\Wow6432Node\\')]
for path in paths:
try:
_winreg.DeleteKeyEx(_winreg.HKEY_CURRENT_USER,
path + self._config['guid'], 0)
except WindowsError as error:
if error.winerror != 2:
raise
def DoClean(chrome_state):
"""Deletes all state associated with the user experiment."""
chrome_state.DeleteRetentionStudyValue()
chrome_state.DeleteExperimentLabelsValue()
chrome_state.DeleteRentionKey()
return 0
def DoPrep(chrome_state):
"""Prepares an install for participation in the experiment."""
# Clear old state.
DoClean(chrome_state)
# Make Chrome appear to have been last run 30 days ago.
chrome_state.SetLastRunTime(timedelta(30))
# Add the enterprise bypass switch to the Active Setup command.
chrome_state.AdjustActiveSetupCommand()
# Cause Active Setup to be run for the current user on next logon.
chrome_state.ClearUserActiveSetup()
# Put the machine into the first study.
chrome_state.SetRetentionStudyValue(1)
return 0
def DoPrelaunch(chrome_state):
"""Writes experiment state to the registry in preparation for manual testing
of the UX.
"""
DoPrep(chrome_state)
# Write study data for group 0 in state 10 (kLaunchingChrome). The magic
# values here correspond to those used by Chrome:
# - State 10 is installer::ExperimentMetrics::kLaunchingChrome.
# - 'CrExp60' is the name of the experiment label used to report metrics.
# - 'AAAAAEAB' is the encoding of an installer::ExperimentMetrics instance
# for state 10.
# - 182 sets the experiment label to expire six months from now.
# - The format string matches that required by Omaha.
# See chrome/installer/util/experiment_storage.cc for reference.
chrome_state.SetExperimentLabelsValue('CrExp60=AAAAAEAB|' +
(datetime.utcnow() +
timedelta(182)).strftime(
'%a, %d %b %Y %H:%M:%S GMT'))
chrome_state.SetRetentionExperimentState(10)
def main(options):
chrome_state = ChromeState(options.channel, options.system_level)
if options.operation == 'clean':
return DoClean(chrome_state)
if options.operation == 'prep':
return DoPrep(chrome_state)
if options.operation == 'prelaunch':
return DoPrelaunch(chrome_state)
return 1
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('--operation', required=True,
choices=['clean', 'prep', 'prelaunch'],
help='The operation to be performed.')
parser.add_argument('--channel', default='stable',
choices=['stable', 'beta', 'dev', 'canary'],
help='The install on which to operate (stable by ' \
'default).')
parser.add_argument('--system-level', action='store_true',
help='Specify to operate on a system-level install ' \
'(user-level by default).')
sys.exit(main(parser.parse_args()))