Index: chrome/tools/test/experiment_tool_win.py |
diff --git a/chrome/tools/test/experiment_tool_win.py b/chrome/tools/test/experiment_tool_win.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..2d942197c21898458efc2de7ad65518da0806572 |
--- /dev/null |
+++ b/chrome/tools/test/experiment_tool_win.py |
@@ -0,0 +1,212 @@ |
+#!/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 M60 Chrome on Windows 10 |
+retention experiment. |
+ |
+For example: |
+experiment_tool_win.py --channel beta --system-level --operation prep |
+""" |
+ |
+import argparse |
+import math |
+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\\' |
+ |
+ 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 = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState\\' + \ |
+ 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 = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState\\' + \ |
+ 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 DeleteExperimentLabelsValue(self): |
+ """Deletes the experiment_labels for the install.""" |
+ medium = 'Medium' if self._system_level else '' |
+ path = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState' + 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 DeleteRentionKey(self): |
+ """Deletes the Retention key for the current user.""" |
+ medium = 'Medium' if self._system_level else '' |
+ path = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState' + medium + '\\' + \ |
+ self._config['guid'] + '\\Retention' |
+ 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 = ChromeState._GOOGLE_UPDATE_PATH + '\\ClientState\\' + \ |
+ 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 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) |
+ return 1 |
+ |
+ |
+if __name__ == '__main__': |
+ parser = argparse.ArgumentParser( |
+ description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) |
+ parser.add_argument('--operation', required=True, choices=['clean', 'prep'], |
+ 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())) |