Avi Drissman | dfd88085 | 2022-09-15 20:11:09 | [diff] [blame] | 1 | # Copyright 2020 The Chromium Authors |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | """ |
| 5 | This script runs Chrome and automatically navigates through the given list of |
| 6 | URLs the specified number of times. |
| 7 | |
Jesse McKenna | cb760eaa | 2021-05-13 22:17:45 | [diff] [blame] | 8 | Usage: vpython3 auto-nav.py <chrome dir> <number of navigations> <url> <url> ... |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 9 | |
| 10 | Optional flags: |
| 11 | * --interval <seconds>, -i <seconds>: specify a number of seconds to wait |
| 12 | between navigations, e.g., -i=5 |
Jesse McKenna | 599aa52 | 2021-02-23 02:05:38 | [diff] [blame] | 13 | * --start_prompt, -s: start Chrome, then wait for the user to press Enter before |
| 14 | starting auto-navigation |
| 15 | * --exit-prompt, -e: after auto-navigation, wait for the user to press Enter |
| 16 | before shutting down chrome.exe |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 17 | * --idlewakeups_dir: Windows only; specify the directory containing |
| 18 | idlewakeups.exe to print measurements taken by IdleWakeups, |
| 19 | e.g., --idlewakeups_dir=tools/win/IdleWakeups/x64/Debug |
Jesse McKenna | 6e21060 | 2020-10-29 22:42:48 | [diff] [blame] | 20 | |
| 21 | Optional flags to chrome.exe, example: |
| 22 | -- --user-data-dir=temp --disable-features=SomeFeature |
| 23 | Note: must be at end of command, following options terminator "--". The options |
| 24 | terminator stops command-line options from being interpreted as options for this |
| 25 | script, which would cause an unrecognized-argument error. |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 26 | """ |
| 27 | |
| 28 | # [VPYTHON:BEGIN] |
Jesse McKenna | cb760eaa | 2021-05-13 22:17:45 | [diff] [blame] | 29 | # python_version: "3.8" |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 30 | # wheel: < |
| 31 | # name: "infra/python/wheels/selenium-py2_py3" |
| 32 | # version: "version:3.14.0" |
| 33 | # > |
| 34 | # wheel: < |
| 35 | # name: "infra/python/wheels/urllib3-py2_py3" |
| 36 | # version: "version:1.24.3" |
| 37 | # > |
| 38 | # wheel: < |
| 39 | # name: "infra/python/wheels/psutil/${vpython_platform}" |
Jesse McKenna | cb760eaa | 2021-05-13 22:17:45 | [diff] [blame] | 40 | # version: "version:5.7.2" |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 41 | # > |
| 42 | # [VPYTHON:END] |
| 43 | |
| 44 | import argparse |
| 45 | import os |
| 46 | import subprocess |
| 47 | import sys |
| 48 | import time |
Jesse McKenna | 1324fe65 | 2021-05-13 23:05:20 | [diff] [blame] | 49 | import urllib |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 50 | |
| 51 | try: |
| 52 | import psutil |
| 53 | from seleniumimport webdriver |
| 54 | exceptImportError: |
Jesse McKenna | 2a8c00b | 2022-03-17 20:25:05 | [diff] [blame] | 55 | print('Error importing required modules. Run with vpython3 instead of ' |
| 56 | 'python.') |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 57 | sys.exit(1) |
| 58 | |
| 59 | DEFAULT_INTERVAL=1 |
Jesse McKenna | 1324fe65 | 2021-05-13 23:05:20 | [diff] [blame] | 60 | EXIT_CODE_ERROR=1 |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 61 | |
Jesse McKenna | 6e21060 | 2020-10-29 22:42:48 | [diff] [blame] | 62 | # Splits list |positional_args| into two lists: |urls| and |chrome_args|, where |
| 63 | # arguments starting with '-' are treated as chrome args, and the rest as URLs. |
| 64 | defParsePositionalArgs(positional_args): |
| 65 | urls, chrome_args=[],[] |
| 66 | for argin positional_args: |
| 67 | if arg.startswith('-'): |
| 68 | chrome_args.append(arg) |
| 69 | else: |
| 70 | urls.append(arg) |
| 71 | return[urls, chrome_args] |
| 72 | |
| 73 | |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 74 | # Returns an object containing the arguments parsed from this script's command |
| 75 | # line. |
| 76 | defParseArgs(): |
Jesse McKenna | 6e21060 | 2020-10-29 22:42:48 | [diff] [blame] | 77 | # Customize usage and help to include options to be passed to chrome.exe. |
Jesse McKenna | 2a8c00b | 2022-03-17 20:25:05 | [diff] [blame] | 78 | usage_text='''%(prog)s [-h] [--interval INTERVAL] [--start_prompt] |
| 79 | [--exit_prompt] [--idlewakeups_dir IDLEWAKEUPS_DIR] |
Jesse McKenna | 6e21060 | 2020-10-29 22:42:48 | [diff] [blame] | 80 | chrome_dir num_navigations url [url ...] |
| 81 | [-- --chrome_option ...]''' |
| 82 | additional_help_text='''optional arguments to chrome.exe, example: |
| 83 | -- --enable-features=MyFeature --browser-startup-dialog |
| 84 | Must be at end of command, following the options |
| 85 | terminator "--"''' |
| 86 | parser= argparse.ArgumentParser( |
| 87 | epilog=additional_help_text, |
| 88 | usage=usage_text, |
| 89 | formatter_class=argparse.RawDescriptionHelpFormatter) |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 90 | parser.add_argument( |
| 91 | 'chrome_dir', help='Directory containing chrome.exe and chromedriver.exe') |
| 92 | parser.add_argument('num_navigations', |
| 93 | type=int, |
| 94 | help='Number of times to navigate through list of URLs') |
| 95 | parser.add_argument('--interval', |
| 96 | '-i', |
| 97 | type=int, |
| 98 | help='Seconds to wait between navigations; default is 1') |
Jesse McKenna | 599aa52 | 2021-02-23 02:05:38 | [diff] [blame] | 99 | parser.add_argument('--start_prompt', |
| 100 | '-s', |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 101 | action='store_true', |
Jesse McKenna | 599aa52 | 2021-02-23 02:05:38 | [diff] [blame] | 102 | help='Wait for confirmation before starting navigation') |
| 103 | parser.add_argument('--exit_prompt', |
| 104 | '-e', |
| 105 | action='store_true', |
| 106 | help='Wait for confirmation before exiting chrome.exe') |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 107 | parser.add_argument( |
| 108 | '--idlewakeups_dir', |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 109 | help='Windows only; directory containing idlewakeups.exe, if using') |
| 110 | parser.add_argument( |
| 111 | 'url', |
| 112 | nargs='+', |
| 113 | help='URL(s) to navigate, separated by spaces; must include scheme, ' |
| 114 | 'e.g., "https://"') |
Jesse McKenna | 6e21060 | 2020-10-29 22:42:48 | [diff] [blame] | 115 | args= parser.parse_args() |
| 116 | args.url, chrome_args=ParsePositionalArgs(args.url) |
| 117 | ifnot args.url: |
| 118 | parser.print_usage() |
| 119 | print(os.path.basename(__file__)+': error: missing URL argument') |
Jesse McKenna | 1324fe65 | 2021-05-13 23:05:20 | [diff] [blame] | 120 | exit(EXIT_CODE_ERROR) |
| 121 | for urlin args.url: |
| 122 | ifnot urllib.parse.urlparse(url).scheme: |
| 123 | print(os.path.basename(__file__)+ |
| 124 | ': error: URL is missing required scheme (e.g., "https://"): '+ url) |
| 125 | exit(EXIT_CODE_ERROR) |
Jesse McKenna | 6e21060 | 2020-10-29 22:42:48 | [diff] [blame] | 126 | return[args, chrome_args] |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 127 | |
| 128 | |
| 129 | # If |path| does not exist, prints a generic error plus optional |error_message| |
| 130 | # and exits. |
| 131 | defExitIfNotFound(path, error_message=None): |
| 132 | ifnot os.path.exists(path): |
| 133 | print('File not found: {}.'.format(path)) |
| 134 | if error_message: |
| 135 | print(error_message) |
Jesse McKenna | 1324fe65 | 2021-05-13 23:05:20 | [diff] [blame] | 136 | exit(EXIT_CODE_ERROR) |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 137 | |
| 138 | |
| 139 | def main(): |
| 140 | # Parse arguments and check that file paths received are valid. |
Jesse McKenna | 6e21060 | 2020-10-29 22:42:48 | [diff] [blame] | 141 | args, chrome_args=ParseArgs() |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 142 | ExitIfNotFound(os.path.join(args.chrome_dir,'chrome.exe'), |
| 143 | 'Build target "chrome" to generate it first.') |
| 144 | chromedriver_exe= os.path.join(args.chrome_dir,'chromedriver.exe') |
| 145 | ExitIfNotFound(chromedriver_exe, |
| 146 | 'Build target "chromedriver" to generate it first.') |
| 147 | if args.idlewakeups_dir: |
| 148 | idlewakeups_exe= os.path.join(args.idlewakeups_dir,'idlewakeups.exe') |
| 149 | ExitIfNotFound(idlewakeups_exe) |
| 150 | |
| 151 | # Start chrome.exe. Disable chrome.exe's extensive logging to make reading |
| 152 | # this script's output easier. |
| 153 | chrome_options= webdriver.ChromeOptions() |
| 154 | chrome_options.add_experimental_option('excludeSwitches',['enable-logging']) |
Jesse McKenna | 6e21060 | 2020-10-29 22:42:48 | [diff] [blame] | 155 | for argin chrome_args: |
| 156 | chrome_options.add_argument(arg) |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 157 | driver= webdriver.Chrome(os.path.abspath(chromedriver_exe), |
| 158 | options=chrome_options) |
| 159 | |
Jesse McKenna | 599aa52 | 2021-02-23 02:05:38 | [diff] [blame] | 160 | if args.start_prompt: |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 161 | driver.get(args.url[0]) |
Jesse McKenna | cb760eaa | 2021-05-13 22:17:45 | [diff] [blame] | 162 | input('Press Enter to begin navigation...') |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 163 | |
| 164 | # Start IdleWakeups, if using, passing the browser process's ID as its target. |
| 165 | # IdleWakeups will monitor the browser process and its children. Other running |
| 166 | # chrome.exe processes (i.e., those not launched by this script) are excluded. |
| 167 | if args.idlewakeups_dir: |
| 168 | launched_processes= psutil.Process( |
| 169 | driver.service.process.pid).children(recursive=False) |
| 170 | ifnot launched_processes: |
| 171 | print('Error getting browser process ID for IdleWakeups.') |
| 172 | exit() |
| 173 | # Assume the first child process created by |driver| is the browser process. |
| 174 | idlewakeups= subprocess.Popen([ |
| 175 | idlewakeups_exe, |
| 176 | str(launched_processes[0].pid),'--stop-on-exit','--tabbed' |
| 177 | ], |
| 178 | stdout=subprocess.PIPE) |
| 179 | |
| 180 | # Navigate through |args.url| list |args.num_navigations| times, then close |
| 181 | # chrome.exe. |
| 182 | interval= args.intervalif args.intervalelse DEFAULT_INTERVAL |
| 183 | for _in range(args.num_navigations): |
| 184 | for urlin args.url: |
| 185 | driver.get(url) |
| 186 | time.sleep(interval) |
Jesse McKenna | 599aa52 | 2021-02-23 02:05:38 | [diff] [blame] | 187 | |
| 188 | if args.exit_prompt: |
Jesse McKenna | cb760eaa | 2021-05-13 22:17:45 | [diff] [blame] | 189 | input('Press Enter to exit...') |
Jesse McKenna | 5ab05eb | 2020-10-23 20:32:30 | [diff] [blame] | 190 | driver.quit() |
| 191 | |
| 192 | # Print IdleWakeups' output, if using. |
| 193 | if args.idlewakeups_dir: |
| 194 | print(idlewakeups.communicate()[0]) |
| 195 | |
| 196 | |
| 197 | if __name__=='__main__': |
| 198 | sys.exit(main()) |