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 |