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. |
| 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 DeleteRetentionStudyValue(self): |
| 79 """Deletes the RetentionStudy value for the install.""" |
| 80 path = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState\\' + \ |
| 81 self._config['guid'] |
| 82 try: |
| 83 with _winreg.OpenKey(self._registry_root, path, 0, |
| 84 _winreg.KEY_WOW64_32KEY | |
| 85 _winreg.KEY_SET_VALUE) as key: |
| 86 _winreg.DeleteValue(key, 'RetentionStudy') |
| 87 except WindowsError as error: |
| 88 if error.winerror != 2: |
| 89 raise |
| 90 |
| 91 def DeleteExperimentLabelsValue(self): |
| 92 """Deletes the experiment_labels for the install.""" |
| 93 medium = 'Medium' if self._system_level else '' |
| 94 path = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState' + medium + '\\' + \ |
| 95 self._config['guid'] |
| 96 try: |
| 97 with _winreg.OpenKey(self._registry_root, path, 0, |
| 98 _winreg.KEY_WOW64_32KEY | |
| 99 _winreg.KEY_SET_VALUE) as key: |
| 100 _winreg.DeleteValue(key, 'experiment_labels') |
| 101 except WindowsError as error: |
| 102 if error.winerror != 2: |
| 103 raise |
| 104 |
| 105 def DeleteRentionKey(self): |
| 106 """Deletes the Retention key for the current user.""" |
| 107 medium = 'Medium' if self._system_level else '' |
| 108 path = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState' + medium + '\\' + \ |
| 109 self._config['guid'] + '\\Retention' |
| 110 try: |
| 111 if self._system_level: |
| 112 _winreg.DeleteKeyEx(self._registry_root, |
| 113 path + '\\' + GetUserSidString(), |
| 114 _winreg.KEY_WOW64_32KEY) |
| 115 _winreg.DeleteKeyEx(self._registry_root, path, _winreg.KEY_WOW64_32KEY) |
| 116 except WindowsError as error: |
| 117 if error.winerror != 2: |
| 118 raise |
| 119 |
| 120 def SetLastRunTime(self, delta): |
| 121 """Sets Chrome's lastrun time for the current user.""" |
| 122 path = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState\\' + \ |
| 123 self._config['guid'] |
| 124 lastrun = InternalTimeFromPyTime(datetime.utcnow() - delta) |
| 125 with _winreg.CreateKeyEx(_winreg.HKEY_CURRENT_USER, path, 0, |
| 126 _winreg.KEY_WOW64_32KEY | |
| 127 _winreg.KEY_SET_VALUE) as key: |
| 128 _winreg.SetValueEx(key, 'lastrun', 0, _winreg.REG_SZ, str(lastrun)) |
| 129 |
| 130 def AdjustActiveSetupCommand(self): |
| 131 """Adds --experiment-enterprise-bypass to system-level Chrome's Active Setup |
| 132 command.""" |
| 133 if not self._system_level: |
| 134 return |
| 135 bypass_switch = '--experiment-enterprise-bypass' |
| 136 for flag in [_winreg.KEY_WOW64_32KEY, _winreg.KEY_WOW64_64KEY]: |
| 137 try: |
| 138 with _winreg.OpenKey(self._registry_root, |
| 139 ChromeState._ACTIVE_SETUP_PATH + |
| 140 self._config['guid'], 0, |
| 141 _winreg.KEY_SET_VALUE | _winreg.KEY_QUERY_VALUE | |
| 142 flag) as key: |
| 143 command, _ = _winreg.QueryValueEx(key, 'StubPath') |
| 144 if bypass_switch in command: |
| 145 continue |
| 146 command += ' ' + bypass_switch |
| 147 _winreg.SetValueEx(key, 'StubPath', 0, _winreg.REG_SZ, command) |
| 148 except WindowsError as error: |
| 149 if error.winerror != 2: |
| 150 raise |
| 151 |
| 152 def ClearUserActiveSetup(self): |
| 153 """Clears per-user state associated with Active Setup so that it will run |
| 154 again on next login.""" |
| 155 if not self._system_level: |
| 156 return |
| 157 paths = [ChromeState._ACTIVE_SETUP_PATH, |
| 158 ChromeState._ACTIVE_SETUP_PATH.replace('Software\\', |
| 159 'Software\\Wow6432Node\\')] |
| 160 for path in paths: |
| 161 try: |
| 162 _winreg.DeleteKeyEx(_winreg.HKEY_CURRENT_USER, |
| 163 path + self._config['guid'], 0) |
| 164 except WindowsError as error: |
| 165 if error.winerror != 2: |
| 166 raise |
| 167 |
| 168 |
| 169 def DoClean(chrome_state): |
| 170 """Deletes all state associated with the user experiment.""" |
| 171 chrome_state.DeleteRetentionStudyValue() |
| 172 chrome_state.DeleteExperimentLabelsValue() |
| 173 chrome_state.DeleteRentionKey() |
| 174 return 0 |
| 175 |
| 176 |
| 177 def DoPrep(chrome_state): |
| 178 """Prepares an install for participation in the experiment.""" |
| 179 # Clear old state. |
| 180 DoClean(chrome_state) |
| 181 # Make Chrome appear to have been last run 30 days ago. |
| 182 chrome_state.SetLastRunTime(timedelta(30)) |
| 183 # Add the enterprise bypass switch to the Active Setup command. |
| 184 chrome_state.AdjustActiveSetupCommand() |
| 185 # Cause Active Setup to be run for the current user on next logon. |
| 186 chrome_state.ClearUserActiveSetup() |
| 187 return 0 |
| 188 |
| 189 |
| 190 def main(options): |
| 191 chrome_state = ChromeState(options.channel, options.system_level) |
| 192 if options.operation == 'clean': |
| 193 return DoClean(chrome_state) |
| 194 if options.operation == 'prep': |
| 195 return DoPrep(chrome_state) |
| 196 return 1 |
| 197 |
| 198 |
| 199 if __name__ == '__main__': |
| 200 parser = argparse.ArgumentParser( |
| 201 description='A helper for working with state associated with the M60 ' |
| 202 'Chrome on Windows 10 retention experiment.') |
| 203 parser.add_argument('--operation', required=True, choices=['clean', 'prep'], |
| 204 help='The operation to be performed.') |
| 205 parser.add_argument('--channel', default='stable', |
| 206 choices=['stable', 'beta', 'dev', 'canary'], |
| 207 help='The install on which to operate (stable by ' \ |
| 208 'default).') |
| 209 parser.add_argument('--system-level', action='store_true', |
| 210 help='Specify to operate on a system-level install ' \ |
| 211 '(user-level by default).') |
| 212 sys.exit(main(parser.parse_args())) |
OLD | NEW |