Takuto Ikuta | 8070a9e | 2022-08-26 01:23:13 +0000 | [diff] [blame] | 1 | #!/usr/bin/env vpython3 |
maruel | ea586f3 | 2016-04-05 11:11:33 -0700 | [diff] [blame] | 2 | # Copyright 2013 The LUCI Authors. All rights reserved. |
maruel | f1f5e2a | 2016-05-25 17:10:39 -0700 | [diff] [blame] | 3 | # Use of this source code is governed under the Apache License, Version 2.0 |
| 4 | # that can be found in the LICENSE file. |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 5 | """Client tool to trigger tasks or retrieve results from a Swarming server.""" |
| 6 | |
Takuto Ikuta | 8070a9e | 2022-08-26 01:23:13 +0000 | [diff] [blame] | 7 | # This spec is for the case swarming.py is used via |
| 8 | # https://chromium.googlesource.com/infra/luci/client-py |
| 9 | # |
Takuto Ikuta | d93070f | 2022-08-26 06:08:23 +0000 | [diff] [blame] | 10 | # [VPYTHON:BEGIN] |
Takuto Ikuta | 8070a9e | 2022-08-26 01:23:13 +0000 | [diff] [blame] | 11 | # wheel: < |
| 12 | # name: "infra/python/wheels/pyobjc/${vpython_platform}" |
| 13 | # version: "version:7.3.chromium.1" |
| 14 | # match_tag: < |
| 15 | # platform: "macosx_10_10_intel" |
| 16 | # > |
| 17 | # match_tag: < |
| 18 | # platform: "macosx_11_0_arm64" |
| 19 | # > |
| 20 | # > |
| 21 | # [VPYTHON:END] |
| 22 | |
Lei Lei | fe202df | 2019-06-11 17:33:34 +0000 | [diff] [blame] | 23 | __version__ = '1.0' |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 24 | |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 25 | import json |
| 26 | import logging |
Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame] | 27 | import optparse |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 28 | import os |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 29 | import sys |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 30 | import textwrap |
Junji Watanabe | 7a677e9 | 2022-01-13 06:07:31 +0000 | [diff] [blame] | 31 | import urllib.parse |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 32 | |
vadimsh@chromium.org | 6b70621 | 2013-08-28 15:03:46 +0000 | [diff] [blame] | 33 | from utils import tools |
Marc-Antoine Ruel | 016c760 | 2019-04-02 18:31:13 +0000 | [diff] [blame] | 34 | tools.force_local_third_party() |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 35 | |
Marc-Antoine Ruel | 016c760 | 2019-04-02 18:31:13 +0000 | [diff] [blame] | 36 | # third_party/ |
| 37 | import colorama |
Marc-Antoine Ruel | 016c760 | 2019-04-02 18:31:13 +0000 | [diff] [blame] | 38 | from depot_tools import fix_encoding |
| 39 | from depot_tools import subcommand |
| 40 | |
| 41 | # pylint: disable=ungrouped-imports |
Vadim Shtayura | e34e13a | 2014-02-02 11:23:26 -0800 | [diff] [blame] | 42 | import auth |
Marc-Antoine Ruel | 016c760 | 2019-04-02 18:31:13 +0000 | [diff] [blame] | 43 | from utils import fs |
| 44 | from utils import logging_utils |
| 45 | from utils import net |
Marc-Antoine Ruel | 016c760 | 2019-04-02 18:31:13 +0000 | [diff] [blame] | 46 | from utils import subprocess42 |
Marc-Antoine Ruel | efdc528 | 2014-12-12 19:31:00 -0500 | [diff] [blame] | 47 | |
| 48 | |
| 49 | class Failure(Exception): |
| 50 | """Generic failure.""" |
Marc-Antoine Ruel | efdc528 | 2014-12-12 19:31:00 -0500 | [diff] [blame] | 51 | |
| 52 | |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 53 | ### API management. |
| 54 | |
| 55 | |
| 56 | class APIError(Exception): |
| 57 | pass |
| 58 | |
| 59 | |
| 60 | def endpoints_api_discovery_apis(host): |
| 61 | """Uses Cloud Endpoints' API Discovery Service to returns metadata about all |
| 62 | the APIs exposed by a host. |
| 63 | |
| 64 | https://developers.google.com/discovery/v1/reference/apis/list |
| 65 | """ |
maruel | 380e326 | 2016-08-31 16:10:06 -0700 | [diff] [blame] | 66 | # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud |
| 67 | # Endpoints version is turned down. |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 68 | data = net.url_read_json(host + '/_ah/api/discovery/v1/apis') |
| 69 | if data is None: |
| 70 | raise APIError('Failed to discover APIs on %s' % host) |
| 71 | out = {} |
| 72 | for api in data['items']: |
| 73 | if api['id'] == 'discovery:v1': |
| 74 | continue |
| 75 | # URL is of the following form: |
| 76 | # url = host + ( |
| 77 | # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version']) |
| 78 | api_data = net.url_read_json(api['discoveryRestUrl']) |
| 79 | if api_data is None: |
| 80 | raise APIError('Failed to discover %s on %s' % (api['id'], host)) |
| 81 | out[api['id']] = api_data |
| 82 | return out |
| 83 | |
| 84 | |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 85 | def get_yielder(base_url, limit): |
| 86 | """Returns the first query and a function that yields following items.""" |
| 87 | CHUNK_SIZE = 250 |
| 88 | |
| 89 | url = base_url |
| 90 | if limit: |
| 91 | url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit)) |
| 92 | data = net.url_read_json(url) |
| 93 | if data is None: |
| 94 | # TODO(maruel): Do basic diagnostic. |
| 95 | raise Failure('Failed to access %s' % url) |
| 96 | org_cursor = data.pop('cursor', None) |
| 97 | org_total = len(data.get('items') or []) |
| 98 | logging.info('get_yielder(%s) returning %d items', base_url, org_total) |
| 99 | if not org_cursor or not org_total: |
| 100 | # This is not an iterable resource. |
| 101 | return data, lambda: [] |
| 102 | |
| 103 | def yielder(): |
| 104 | cursor = org_cursor |
| 105 | total = org_total |
| 106 | # Some items support cursors. Try to get automatically if cursors are needed |
| 107 | # by looking at the 'cursor' items. |
| 108 | while cursor and (not limit or total < limit): |
| 109 | merge_char = '&' if '?' in base_url else '?' |
Marc-Antoine Ruel | ad8cabe | 2019-10-10 23:24:26 +0000 | [diff] [blame] | 110 | url = base_url + '%scursor=%s' % (merge_char, urllib.parse.quote(cursor)) |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 111 | if limit: |
| 112 | url += '&limit=%d' % min(CHUNK_SIZE, limit - total) |
| 113 | new = net.url_read_json(url) |
| 114 | if new is None: |
| 115 | raise Failure('Failed to access %s' % url) |
| 116 | cursor = new.get('cursor') |
| 117 | new_items = new.get('items') |
| 118 | nb_items = len(new_items or []) |
| 119 | total += nb_items |
| 120 | logging.info('get_yielder(%s) yielding %d items', base_url, nb_items) |
| 121 | yield new_items |
| 122 | |
| 123 | return data, yielder |
| 124 | |
| 125 | |
Marc-Antoine Ruel | efdc528 | 2014-12-12 19:31:00 -0500 | [diff] [blame] | 126 | ### Commands. |
| 127 | |
| 128 | |
Marc-Antoine Ruel | 833f5eb | 2018-04-25 16:49:40 -0400 | [diff] [blame] | 129 | @subcommand.usage('[method name]') |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 130 | def CMDquery(parser, args): |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 131 | """Returns raw JSON information via an URL endpoint. Use 'query-list' to |
| 132 | gather the list of API methods from the server. |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 133 | |
| 134 | Examples: |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 135 | Raw task request and results: |
| 136 | swarming.py query -S server-url.com task/123456/request |
| 137 | swarming.py query -S server-url.com task/123456/result |
| 138 | |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 139 | Listing all bots: |
maruel | 84e77aa | 2015-10-21 06:37:24 -0700 | [diff] [blame] | 140 | swarming.py query -S server-url.com bots/list |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 141 | |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 142 | Listing last 10 tasks on a specific bot named 'bot1': |
| 143 | swarming.py query -S server-url.com --limit 10 bot/bot1/tasks |
maruel | 84e77aa | 2015-10-21 06:37:24 -0700 | [diff] [blame] | 144 | |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 145 | Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that |
maruel | 84e77aa | 2015-10-21 06:37:24 -0700 | [diff] [blame] | 146 | quoting is important!: |
| 147 | swarming.py query -S server-url.com --limit 10 \\ |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 148 | 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome' |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 149 | """ |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 150 | parser.add_option( |
Junji Watanabe | cb05404 | 2020-07-21 08:43:26 +0000 | [diff] [blame] | 151 | '-L', |
| 152 | '--limit', |
| 153 | type='int', |
| 154 | default=200, |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 155 | help='Limit to enforce on limitless items (like number of tasks); ' |
Junji Watanabe | cb05404 | 2020-07-21 08:43:26 +0000 | [diff] [blame] | 156 | 'default=%default') |
Paweł Hajdan, Jr | 53ef013 | 2015-03-20 17:49:18 +0100 | [diff] [blame] | 157 | parser.add_option( |
| 158 | '--json', help='Path to JSON output file (otherwise prints to stdout)') |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 159 | parser.add_option( |
Junji Watanabe | cb05404 | 2020-07-21 08:43:26 +0000 | [diff] [blame] | 160 | '--progress', |
| 161 | action='store_true', |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 162 | help='Prints a dot at each request to show progress') |
| 163 | options, args = parser.parse_args(args) |
maruel | d8aba22 | 2015-09-03 12:21:19 -0700 | [diff] [blame] | 164 | if len(args) != 1: |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 165 | parser.error( |
| 166 | 'Must specify only method name and optionally query args properly ' |
| 167 | 'escaped.') |
smut | 281c390 | 2018-05-30 17:50:05 -0700 | [diff] [blame] | 168 | base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0] |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 169 | try: |
| 170 | data, yielder = get_yielder(base_url, options.limit) |
| 171 | for items in yielder(): |
| 172 | if items: |
| 173 | data['items'].extend(items) |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 174 | if options.progress: |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 175 | sys.stderr.write('.') |
| 176 | sys.stderr.flush() |
| 177 | except Failure as e: |
| 178 | sys.stderr.write('\n%s\n' % e) |
| 179 | return 1 |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 180 | if options.progress: |
maruel | af6b06c | 2017-06-08 06:26:53 -0700 | [diff] [blame] | 181 | sys.stderr.write('\n') |
| 182 | sys.stderr.flush() |
Paweł Hajdan, Jr | 53ef013 | 2015-03-20 17:49:18 +0100 | [diff] [blame] | 183 | if options.json: |
Junji Watanabe | 7a677e9 | 2022-01-13 06:07:31 +0000 | [diff] [blame] | 184 | options.json = os.path.abspath(options.json) |
maruel | 1ceb387 | 2015-10-14 06:10:44 -0700 | [diff] [blame] | 185 | tools.write_json(options.json, data, True) |
Paweł Hajdan, Jr | 53ef013 | 2015-03-20 17:49:18 +0100 | [diff] [blame] | 186 | else: |
Marc-Antoine Ruel | cda90ee | 2015-03-23 15:13:20 -0400 | [diff] [blame] | 187 | try: |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 188 | tools.write_json(sys.stdout, data, False) |
Marc-Antoine Ruel | cda90ee | 2015-03-23 15:13:20 -0400 | [diff] [blame] | 189 | sys.stdout.write('\n') |
| 190 | except IOError: |
| 191 | pass |
Marc-Antoine Ruel | 79940ae | 2014-09-23 17:55:41 -0400 | [diff] [blame] | 192 | return 0 |
| 193 | |
| 194 | |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 195 | def CMDquery_list(parser, args): |
| 196 | """Returns list of all the Swarming APIs that can be used with command |
| 197 | 'query'. |
| 198 | """ |
| 199 | parser.add_option( |
| 200 | '--json', help='Path to JSON output file (otherwise prints to stdout)') |
| 201 | options, args = parser.parse_args(args) |
| 202 | if args: |
| 203 | parser.error('No argument allowed.') |
| 204 | |
| 205 | try: |
| 206 | apis = endpoints_api_discovery_apis(options.swarming) |
| 207 | except APIError as e: |
| 208 | parser.error(str(e)) |
| 209 | if options.json: |
Junji Watanabe | 7a677e9 | 2022-01-13 06:07:31 +0000 | [diff] [blame] | 210 | options.json = os.path.abspath(options.json) |
maruel | 1ceb387 | 2015-10-14 06:10:44 -0700 | [diff] [blame] | 211 | with fs.open(options.json, 'wb') as f: |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 212 | json.dump(apis, f) |
| 213 | else: |
| 214 | help_url = ( |
Junji Watanabe | cb05404 | 2020-07-21 08:43:26 +0000 | [diff] [blame] | 215 | 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' % |
| 216 | options.swarming) |
Marc-Antoine Ruel | 04903a3 | 2019-10-09 21:09:25 +0000 | [diff] [blame] | 217 | for i, (api_id, api) in enumerate(sorted(apis.items())): |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 218 | if i: |
| 219 | print('') |
Lei Lei | fe202df | 2019-06-11 17:33:34 +0000 | [diff] [blame] | 220 | print(api_id) |
| 221 | print(' ' + api['description'].strip()) |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 222 | if 'resources' in api: |
| 223 | # Old. |
Marc-Antoine Ruel | 793bff3 | 2019-04-18 17:50:48 +0000 | [diff] [blame] | 224 | # TODO(maruel): Remove. |
| 225 | # pylint: disable=too-many-nested-blocks |
Junji Watanabe | cb05404 | 2020-07-21 08:43:26 +0000 | [diff] [blame] | 226 | for j, (resource_name, |
| 227 | resource) in enumerate(sorted(api['resources'].items())): |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 228 | if j: |
| 229 | print('') |
Marc-Antoine Ruel | 04903a3 | 2019-10-09 21:09:25 +0000 | [diff] [blame] | 230 | for method_name, method in sorted(resource['methods'].items()): |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 231 | # Only list the GET ones. |
| 232 | if method['httpMethod'] != 'GET': |
| 233 | continue |
Junji Watanabe | cb05404 | 2020-07-21 08:43:26 +0000 | [diff] [blame] | 234 | print('- %s.%s: %s' % (resource_name, method_name, method['path'])) |
| 235 | print('\n'.join(' ' + l for l in textwrap.wrap( |
| 236 | method.get('description', 'No description'), 78))) |
Lei Lei | fe202df | 2019-06-11 17:33:34 +0000 | [diff] [blame] | 237 | print(' %s%s%s' % (help_url, api['servicePath'], method['id'])) |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 238 | else: |
| 239 | # New. |
Marc-Antoine Ruel | 04903a3 | 2019-10-09 21:09:25 +0000 | [diff] [blame] | 240 | for method_name, method in sorted(api['methods'].items()): |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 241 | # Only list the GET ones. |
| 242 | if method['httpMethod'] != 'GET': |
| 243 | continue |
Lei Lei | fe202df | 2019-06-11 17:33:34 +0000 | [diff] [blame] | 244 | print('- %s: %s' % (method['id'], method['path'])) |
maruel | 11e31af | 2017-02-15 07:30:50 -0800 | [diff] [blame] | 245 | print('\n'.join( |
| 246 | ' ' + l for l in textwrap.wrap(method['description'], 78))) |
Lei Lei | fe202df | 2019-06-11 17:33:34 +0000 | [diff] [blame] | 247 | print(' %s%s%s' % (help_url, api['servicePath'], method['id'])) |
maruel | 77f720b | 2015-09-15 12:35:22 -0700 | [diff] [blame] | 248 | return 0 |
| 249 | |
| 250 | |
Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame] | 251 | class OptionParserSwarming(logging_utils.OptionParserWithLogging): |
Junji Watanabe | 38b28b0 | 2020-04-23 10:23:30 +0000 | [diff] [blame] | 252 | |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 253 | def __init__(self, **kwargs): |
Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame] | 254 | logging_utils.OptionParserWithLogging.__init__( |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 255 | self, prog='swarming.py', **kwargs) |
Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame] | 256 | self.server_group = optparse.OptionGroup(self, 'Server') |
Marc-Antoine Ruel | 5471e3d | 2013-11-11 19:10:32 -0500 | [diff] [blame] | 257 | self.server_group.add_option( |
Junji Watanabe | 38b28b0 | 2020-04-23 10:23:30 +0000 | [diff] [blame] | 258 | '-S', |
| 259 | '--swarming', |
| 260 | metavar='URL', |
| 261 | default=os.environ.get('SWARMING_SERVER', ''), |
maruel@chromium.org | e9403ab | 2013-09-20 18:03:49 +0000 | [diff] [blame] | 262 | help='Swarming server to use') |
Marc-Antoine Ruel | 5471e3d | 2013-11-11 19:10:32 -0500 | [diff] [blame] | 263 | self.add_option_group(self.server_group) |
Vadim Shtayura | e34e13a | 2014-02-02 11:23:26 -0800 | [diff] [blame] | 264 | auth.add_auth_options(self) |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 265 | |
| 266 | def parse_args(self, *args, **kwargs): |
Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame] | 267 | options, args = logging_utils.OptionParserWithLogging.parse_args( |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 268 | self, *args, **kwargs) |
Marc-Antoine Ruel | 012067b | 2014-12-10 15:45:42 -0500 | [diff] [blame] | 269 | auth.process_auth_options(self, options) |
| 270 | user = self._process_swarming(options) |
| 271 | if hasattr(options, 'user') and not options.user: |
| 272 | options.user = user |
| 273 | return options, args |
| 274 | |
| 275 | def _process_swarming(self, options): |
| 276 | """Processes the --swarming option and aborts if not specified. |
| 277 | |
| 278 | Returns the identity as determined by the server. |
| 279 | """ |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 280 | if not options.swarming: |
| 281 | self.error('--swarming is required.') |
Marc-Antoine Ruel | 012067b | 2014-12-10 15:45:42 -0500 | [diff] [blame] | 282 | try: |
| 283 | options.swarming = net.fix_url(options.swarming) |
| 284 | except ValueError as e: |
| 285 | self.error('--swarming %s' % e) |
Takuto Ikuta | ae767b3 | 2020-05-11 01:22:19 +0000 | [diff] [blame] | 286 | |
Marc-Antoine Ruel | f7d737d | 2014-12-10 15:36:29 -0500 | [diff] [blame] | 287 | try: |
| 288 | user = auth.ensure_logged_in(options.swarming) |
| 289 | except ValueError as e: |
| 290 | self.error(str(e)) |
Marc-Antoine Ruel | 012067b | 2014-12-10 15:45:42 -0500 | [diff] [blame] | 291 | return user |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 292 | |
| 293 | |
| 294 | def main(args): |
| 295 | dispatcher = subcommand.CommandDispatcher(__name__) |
Marc-Antoine Ruel | cfb6085 | 2014-07-02 15:22:00 -0400 | [diff] [blame] | 296 | return dispatcher.execute(OptionParserSwarming(version=__version__), args) |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 297 | |
| 298 | |
| 299 | if __name__ == '__main__': |
maruel | 8e4e40c | 2016-05-30 06:21:07 -0700 | [diff] [blame] | 300 | subprocess42.inhibit_os_error_reporting() |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 301 | fix_encoding.fix_encoding() |
| 302 | tools.disable_buffering() |
| 303 | colorama.init() |
Takuto Ikuta | 7c843c8 | 2020-04-15 05:42:54 +0000 | [diff] [blame] | 304 | net.set_user_agent('swarming.py/' + __version__) |
maruel@chromium.org | 0437a73 | 2013-08-27 16:05:52 +0000 | [diff] [blame] | 305 | sys.exit(main(sys.argv[1:])) |