Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/env python | |
| 2 # Copyright 2017 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 # A tool for manipulating the user retention experiment state for a Chrome | |
| 7 # install. | |
| 8 # | |
| 9 # usage: experiment_tool_win.py [-h] --operation {clean,prep} | |
| 10 # [--channel {stable,beta,dev,canary}] | |
| 11 # [--system-level] | |
| 12 # | |
| 13 # A helper for working with state associated with the M60 Chrome on Windows 10 | |
| 14 # retention experiment. | |
|
Nico
2017/06/15 14:47:37
I love this descriptive comment up here. And the w
grt (UTC plus 2)
2017/06/15 15:07:26
Nice!
| |
| 15 # | |
| 16 # optional arguments: | |
| 17 # -h, --help show this help message and exit | |
| 18 # --operation {clean,prep} | |
| 19 # The operation to be performed. | |
| 20 # --channel {stable,beta,dev,canary} | |
| 21 # The install on which to operate (stable by default). | |
| 22 # --system-level Specify to operate on a system-level install (user- | |
| 23 # level by default). | |
| 24 | |
| 25 import argparse | |
| 26 import math | |
| 27 import sys | |
| 28 from datetime import datetime, timedelta | |
| 29 import win32api | |
| 30 import win32security | |
| 31 import _winreg | |
| 32 | |
| 33 | |
| 34 def GetUserSidString(): | |
| 35 """Returns the current user's SID string.""" | |
| 36 token_handle = win32security.OpenProcessToken(win32api.GetCurrentProcess(), | |
| 37 win32security.TOKEN_QUERY) | |
| 38 user_sid, _ = win32security.GetTokenInformation(token_handle, | |
| 39 win32security.TokenUser) | |
| 40 return win32security.ConvertSidToStringSid(user_sid) | |
| 41 | |
| 42 | |
| 43 def InternalTimeFromPyTime(pytime): | |
| 44 """Returns a Chromium internal time value representing a Python datetime.""" | |
| 45 # Microseconds since 1601-01-01 00:00:00 UTC | |
| 46 delta = pytime - datetime(1601, 1, 1) | |
| 47 return math.trunc(delta.total_seconds()) * 1000000 + delta.microseconds | |
| 48 | |
| 49 | |
| 50 class ChromeState: | |
| 51 """An object that provides mutations on Chrome's state relating to the | |
| 52 user experiment. | |
| 53 """ | |
| 54 _CHANNEL_CONFIGS = { | |
| 55 'stable': { | |
| 56 'guid': '{8A69D345-D564-463c-AFF1-A69D9E530F96}' | |
| 57 }, | |
| 58 'beta': { | |
| 59 'guid': '{8237E44A-0054-442C-B6B6-EA0509993955}' | |
| 60 }, | |
| 61 'dev': { | |
| 62 'guid': '{401C381F-E0DE-4B85-8BD8-3F3F14FBDA57}' | |
| 63 }, | |
| 64 'canary': { | |
| 65 'guid': '{4ea16ac7-fd5a-47c3-875b-dbf4a2008c20}' | |
| 66 }, | |
| 67 } | |
| 68 _GOOGLE_UPDATE_PATH = 'Software\\Google\\Update' | |
| 69 _ACTIVE_SETUP_PATH = 'Software\\Microsoft\\Active Setup\\Installed ' + \ | |
| 70 'Components\\' | |
| 71 | |
| 72 def __init__(self, channel_name, system_level): | |
| 73 self._config = ChromeState._CHANNEL_CONFIGS[channel_name] | |
| 74 self._system_level = system_level | |
| 75 self._registry_root = _winreg.HKEY_LOCAL_MACHINE if self._system_level \ | |
| 76 else _winreg.HKEY_CURRENT_USER | |
| 77 | |
| 78 def SetRetentionStudyValue(self, study): | |
| 79 """Sets the RetentionStudy value for the install.""" | |
| 80 path = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState\\' + \ | |
| 81 self._config['guid'] | |
| 82 with _winreg.OpenKey(self._registry_root, path, 0, | |
| 83 _winreg.KEY_WOW64_32KEY | | |
| 84 _winreg.KEY_SET_VALUE) as key: | |
| 85 _winreg.SetValueEx(key, 'RetentionStudy', 0, _winreg.REG_DWORD, study) | |
| 86 | |
| 87 def DeleteRetentionStudyValue(self): | |
| 88 """Deletes the RetentionStudy value for the install.""" | |
| 89 path = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState\\' + \ | |
| 90 self._config['guid'] | |
| 91 try: | |
| 92 with _winreg.OpenKey(self._registry_root, path, 0, | |
| 93 _winreg.KEY_WOW64_32KEY | | |
| 94 _winreg.KEY_SET_VALUE) as key: | |
| 95 _winreg.DeleteValue(key, 'RetentionStudy') | |
| 96 except WindowsError as error: | |
| 97 if error.winerror != 2: | |
| 98 raise | |
| 99 | |
| 100 def DeleteExperimentLabelsValue(self): | |
| 101 """Deletes the experiment_labels for the install.""" | |
| 102 medium = 'Medium' if self._system_level else '' | |
| 103 path = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState' + medium + '\\' + \ | |
| 104 self._config['guid'] | |
| 105 try: | |
| 106 with _winreg.OpenKey(self._registry_root, path, 0, | |
| 107 _winreg.KEY_WOW64_32KEY | | |
| 108 _winreg.KEY_SET_VALUE) as key: | |
| 109 _winreg.DeleteValue(key, 'experiment_labels') | |
| 110 except WindowsError as error: | |
| 111 if error.winerror != 2: | |
| 112 raise | |
| 113 | |
| 114 def DeleteRentionKey(self): | |
| 115 """Deletes the Retention key for the current user.""" | |
| 116 medium = 'Medium' if self._system_level else '' | |
| 117 path = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState' + medium + '\\' + \ | |
| 118 self._config['guid'] + '\\Retention' | |
| 119 try: | |
| 120 if self._system_level: | |
| 121 _winreg.DeleteKeyEx(self._registry_root, | |
| 122 path + '\\' + GetUserSidString(), | |
| 123 _winreg.KEY_WOW64_32KEY) | |
| 124 _winreg.DeleteKeyEx(self._registry_root, path, _winreg.KEY_WOW64_32KEY) | |
| 125 except WindowsError as error: | |
| 126 if error.winerror != 2: | |
| 127 raise | |
| 128 | |
| 129 def SetLastRunTime(self, delta): | |
| 130 """Sets Chrome's lastrun time for the current user.""" | |
| 131 path = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState\\' + \ | |
| 132 self._config['guid'] | |
| 133 lastrun = InternalTimeFromPyTime(datetime.utcnow() - delta) | |
| 134 with _winreg.CreateKeyEx(_winreg.HKEY_CURRENT_USER, path, 0, | |
| 135 _winreg.KEY_WOW64_32KEY | | |
| 136 _winreg.KEY_SET_VALUE) as key: | |
| 137 _winreg.SetValueEx(key, 'lastrun', 0, _winreg.REG_SZ, str(lastrun)) | |
| 138 | |
| 139 def AdjustActiveSetupCommand(self): | |
| 140 """Adds --experiment-enterprise-bypass to system-level Chrome's Active Setup | |
| 141 command.""" | |
| 142 if not self._system_level: | |
| 143 return | |
| 144 enable_switch = '--experiment-enable-for-testing' | |
| 145 bypass_switch = '--experiment-enterprise-bypass' | |
| 146 for flag in [_winreg.KEY_WOW64_32KEY, _winreg.KEY_WOW64_64KEY]: | |
| 147 try: | |
| 148 with _winreg.OpenKey(self._registry_root, | |
| 149 ChromeState._ACTIVE_SETUP_PATH + | |
| 150 self._config['guid'], 0, | |
| 151 _winreg.KEY_SET_VALUE | _winreg.KEY_QUERY_VALUE | | |
| 152 flag) as key: | |
| 153 command, _ = _winreg.QueryValueEx(key, 'StubPath') | |
| 154 if not bypass_switch in command: | |
| 155 command += ' ' + bypass_switch | |
| 156 if not enable_switch in command: | |
| 157 command += ' ' + enable_switch | |
| 158 _winreg.SetValueEx(key, 'StubPath', 0, _winreg.REG_SZ, command) | |
| 159 except WindowsError as error: | |
| 160 if error.winerror != 2: | |
| 161 raise | |
| 162 | |
| 163 def ClearUserActiveSetup(self): | |
| 164 """Clears per-user state associated with Active Setup so that it will run | |
| 165 again on next login.""" | |
| 166 if not self._system_level: | |
| 167 return | |
| 168 paths = [ChromeState._ACTIVE_SETUP_PATH, | |
| 169 ChromeState._ACTIVE_SETUP_PATH.replace('Software\\', | |
| 170 'Software\\Wow6432Node\\')] | |
| 171 for path in paths: | |
| 172 try: | |
| 173 _winreg.DeleteKeyEx(_winreg.HKEY_CURRENT_USER, | |
| 174 path + self._config['guid'], 0) | |
| 175 except WindowsError as error: | |
| 176 if error.winerror != 2: | |
| 177 raise | |
| 178 | |
| 179 | |
| 180 def DoClean(chrome_state): | |
| 181 """Deletes all state associated with the user experiment.""" | |
| 182 chrome_state.DeleteRetentionStudyValue() | |
| 183 chrome_state.DeleteExperimentLabelsValue() | |
| 184 chrome_state.DeleteRentionKey() | |
| 185 return 0 | |
| 186 | |
| 187 | |
| 188 def DoPrep(chrome_state): | |
| 189 """Prepares an install for participation in the experiment.""" | |
| 190 # Clear old state. | |
| 191 DoClean(chrome_state) | |
| 192 # Make Chrome appear to have been last run 30 days ago. | |
| 193 chrome_state.SetLastRunTime(timedelta(30)) | |
| 194 # Add the enterprise bypass switch to the Active Setup command. | |
| 195 chrome_state.AdjustActiveSetupCommand() | |
| 196 # Cause Active Setup to be run for the current user on next logon. | |
| 197 chrome_state.ClearUserActiveSetup() | |
| 198 # Put the machine into the first study. | |
| 199 chrome_state.SetRetentionStudyValue(1) | |
| 200 return 0 | |
| 201 | |
| 202 | |
| 203 def main(options): | |
| 204 chrome_state = ChromeState(options.channel, options.system_level) | |
| 205 if options.operation == 'clean': | |
| 206 return DoClean(chrome_state) | |
| 207 if options.operation == 'prep': | |
| 208 return DoPrep(chrome_state) | |
| 209 return 1 | |
| 210 | |
| 211 | |
| 212 if __name__ == '__main__': | |
| 213 parser = argparse.ArgumentParser( | |
| 214 description='A helper for working with state associated with the M60 ' | |
| 215 'Chrome on Windows 10 retention experiment.') | |
| 216 parser.add_argument('--operation', required=True, choices=['clean', 'prep'], | |
| 217 help='The operation to be performed.') | |
| 218 parser.add_argument('--channel', default='stable', | |
| 219 choices=['stable', 'beta', 'dev', 'canary'], | |
| 220 help='The install on which to operate (stable by ' \ | |
| 221 'default).') | |
| 222 parser.add_argument('--system-level', action='store_true', | |
| 223 help='Specify to operate on a system-level install ' \ | |
| 224 '(user-level by default).') | |
| 225 sys.exit(main(parser.parse_args())) | |
| OLD | NEW |