blob: 563155f97c30a4690ec34bfbaa736ffbae84fbfb [file] [log] [blame]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2012 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# 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.org9c72d4e2012-09-28 19:20:25 +00005
nodir55be77b2016-05-03 09:39:57 -07006"""Runs a command with optional isolated input/output.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00007
nodir55be77b2016-05-03 09:39:57 -07008Despite name "run_isolated", can run a generic non-isolated command specified as
9args.
10
11If input isolated hash is provided, fetches it, creates a tree of hard links,
12appends args to the command in the fetched isolated and runs it.
13To improve performance, keeps a local cache.
14The local cache can safely be deleted.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -050015
nodirbe642ff2016-06-09 15:51:51 -070016Any ${EXECUTABLE_SUFFIX} on the command line will be replaced with ".exe" string
17on Windows and "" on other platforms.
18
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -050019Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
20temporary directory upon execution of the command specified in the .isolated
21file. All content written to this directory will be uploaded upon termination
22and the .isolated file describing this directory will be printed to stdout.
bpastene447c1992016-06-20 15:21:47 -070023
24Any ${SWARMING_BOT_FILE} on the command line will be replaced by the value of
25the --bot-file parameter. This file is used by a swarming bot to communicate
26state of the host to tasks. It is written to by the swarming bot's
27on_before_task() hook in the swarming server's custom bot_config.py.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000028"""
29
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -050030__version__ = '0.10.3'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000031
aludwin7556e0c2016-10-26 08:46:10 -070032import argparse
maruel064c0a32016-04-05 11:47:15 -070033import base64
iannucci96fcccc2016-08-30 15:52:22 -070034import collections
vadimsh232f5a82017-01-20 19:23:44 -080035import contextlib
aludwin7556e0c2016-10-26 08:46:10 -070036import json
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000037import logging
38import optparse
39import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000040import sys
41import tempfile
maruel064c0a32016-04-05 11:47:15 -070042import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000043
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000044from third_party.depot_tools import fix_encoding
45
Vadim Shtayura6b555c12014-07-23 16:22:18 -070046from utils import file_path
maruel12e30012015-10-09 11:55:35 -070047from utils import fs
maruel064c0a32016-04-05 11:47:15 -070048from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040049from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040050from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050051from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000052from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000053from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000054
vadimsh8ec66822017-07-25 14:08:29 -070055from libs import luci_context
56
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080057import auth
nodirbe642ff2016-06-09 15:51:51 -070058import cipd
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000059import isolateserver
nodirf33b8d62016-10-26 22:34:58 -070060import named_cache
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000061
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000062
vadimsh@chromium.org85071062013-08-21 23:37:45 +000063# Absolute path to this file (can be None if running from zip on Mac).
tansella4949442016-06-23 22:34:32 -070064THIS_FILE_PATH = os.path.abspath(
65 __file__.decode(sys.getfilesystemencoding())) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000066
67# Directory that contains this file (might be inside zip package).
tansella4949442016-06-23 22:34:32 -070068BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__.decode(
69 sys.getfilesystemencoding()) else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000070
71# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000072if zip_package.get_main_script_path():
73 MAIN_DIR = os.path.dirname(
74 os.path.abspath(zip_package.get_main_script_path()))
75else:
76 # This happens when 'import run_isolated' is executed at the python
77 # interactive prompt, in that case __file__ is undefined.
78 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000079
maruele2f2cb82016-07-13 14:41:03 -070080
81# Magic variables that can be found in the isolate task command line.
82ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
83EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
84SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
85
86
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000087# The name of the log file to use.
88RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
89
maruele2f2cb82016-07-13 14:41:03 -070090
csharp@chromium.orge217f302012-11-22 16:51:53 +000091# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000092RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000093
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000094
maruele2f2cb82016-07-13 14:41:03 -070095# Use short names for temporary directories. This is driven by Windows, which
96# imposes a relatively short maximum path length of 260 characters, often
97# referred to as MAX_PATH. It is relatively easy to create files with longer
98# path length. A use case is with recursive depedency treesV like npm packages.
99#
100# It is recommended to start the script with a `root_dir` as short as
101# possible.
102# - ir stands for isolated_run
103# - io stands for isolated_out
104# - it stands for isolated_tmp
105ISOLATED_RUN_DIR = u'ir'
106ISOLATED_OUT_DIR = u'io'
107ISOLATED_TMP_DIR = u'it'
108
109
maruel7eb6b562017-06-08 08:20:04 -0700110OUTLIVING_ZOMBIE_MSG = """\
111*** Swarming tried multiple times to delete the %s directory and failed ***
112*** Hard failing the task ***
113
114Swarming detected that your testing script ran an executable, which may have
115started a child executable, and the main script returned early, leaving the
116children executables playing around unguided.
117
118You don't want to leave children processes outliving the task on the Swarming
119bot, do you? The Swarming bot doesn't.
120
121How to fix?
122- For any process that starts children processes, make sure all children
123 processes terminated properly before each parent process exits. This is
124 especially important in very deep process trees.
125 - This must be done properly both in normal successful task and in case of
126 task failure. Cleanup is very important.
127- The Swarming bot sends a SIGTERM in case of timeout.
128 - You have %s seconds to comply after the signal was sent to the process
129 before the process is forcibly killed.
130- To achieve not leaking children processes in case of signals on timeout, you
131 MUST handle signals in each executable / python script and propagate them to
132 children processes.
133 - When your test script (python or binary) receives a signal like SIGTERM or
134 CTRL_BREAK_EVENT on Windows), send it to all children processes and wait for
135 them to terminate before quitting.
136
137See
138https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Bot.md#graceful-termination-aka-the-sigterm-and-sigkill-dance
139for more information.
140
141*** May the SIGKILL force be with you ***
142"""
143
144
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500145TaskData = collections.namedtuple(
146 'TaskData', [
147 # List of strings; the command line to use, independent of what was
148 # specified in the isolated file.
149 'command',
150 # List of strings; the arguments to add to the command specified in the
151 # isolated file.
152 'extra_args',
153 # Hash of the .isolated file that must be retrieved to recreate the tree
154 # of files to run the target executable. The command specified in the
155 # .isolated is executed. Mutually exclusive with command argument.
156 'isolated_hash',
157 # isolateserver.Storage instance to retrieve remote objects. This object
158 # has a reference to an isolateserver.StorageApi, which does the actual
159 # I/O.
160 'storage',
161 # isolateserver.LocalCache instance to keep from retrieving the same
162 # objects constantly by caching the objects retrieved. Can be on-disk or
163 # in-memory.
164 'isolate_cache',
165 # List of paths relative to root_dir to put into the output isolated
166 # bundle upon task completion (see link_outputs_to_outdir).
167 'outputs',
168 # Function (run_dir) => context manager that installs named caches into
169 # |run_dir|.
170 'install_named_caches',
171 # If True, the temporary directory will be deliberately leaked for later
172 # examination.
173 'leak_temp_dir',
174 # Path to the directory to use to create the temporary directory. If not
175 # specified, a random temporary directory is created.
176 'root_dir',
177 # Kills the process if it lasts more than this amount of seconds.
178 'hard_timeout',
179 # Number of seconds to wait between SIGTERM and SIGKILL.
180 'grace_period',
181 # Path to a file with bot state, used in place of ${SWARMING_BOT_FILE}
182 # task command line argument.
183 'bot_file',
184 # Logical account to switch LUCI_CONTEXT into.
185 'switch_to_account',
186 # Context manager dir => CipdInfo, see install_client_and_packages.
187 'install_packages_fn',
188 # Create tree with symlinks instead of hardlinks.
189 'use_symlinks',
190 # Environment variables to set.
191 'env',
192 # Environment variables to mutate with relative directories.
193 # Example: {"ENV_KEY": ['relative', 'paths', 'to', 'prepend']}
194 'env_prefix'])
195
196
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000197def get_as_zip_package(executable=True):
198 """Returns ZipPackage with this module and all its dependencies.
199
200 If |executable| is True will store run_isolated.py as __main__.py so that
201 zip package is directly executable be python.
202 """
203 # Building a zip package when running from another zip package is
204 # unsupported and probably unneeded.
205 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +0000206 assert THIS_FILE_PATH
207 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000208 package = zip_package.ZipPackage(root=BASE_DIR)
209 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
aludwin81178302016-11-30 17:18:49 -0800210 package.add_python_file(os.path.join(BASE_DIR, 'isolate_storage.py'))
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400211 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000212 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800213 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
nodirbe642ff2016-06-09 15:51:51 -0700214 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
nodirf33b8d62016-10-26 22:34:58 -0700215 package.add_python_file(os.path.join(BASE_DIR, 'named_cache.py'))
tanselle4288c32016-07-28 09:45:40 -0700216 package.add_directory(os.path.join(BASE_DIR, 'libs'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000217 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
218 package.add_directory(os.path.join(BASE_DIR, 'utils'))
219 return package
220
221
Marc-Antoine Rueldc527d12017-11-29 11:19:16 -0500222def _to_str(s):
223 """Downgrades a unicode instance to str. Pass str through as-is."""
224 if isinstance(s, str):
225 return s
226 # This is technically incorrect, especially on Windows. In theory
227 # sys.getfilesystemencoding() should be used to use the right 'ANSI code
228 # page' on Windows, but that causes other problems, as the character set
229 # is very limited.
230 return s.encode('utf-8')
231
232
Marc-Antoine Ruelb651cad2017-12-01 18:45:18 -0500233def _to_unicode(s):
234 """Upgrades a str instance to unicode. Pass unicode through as-is."""
235 if isinstance(s, unicode) or s is None:
236 return s
237 return s.decode('utf-8')
238
239
maruel03e11842016-07-14 10:50:16 -0700240def make_temp_dir(prefix, root_dir):
241 """Returns a new unique temporary directory."""
242 return unicode(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000243
244
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500245def change_tree_read_only(rootdir, read_only):
246 """Changes the tree read-only bits according to the read_only specification.
247
248 The flag can be 0, 1 or 2, which will affect the possibility to modify files
249 and create or delete files.
250 """
251 if read_only == 2:
252 # Files and directories (except on Windows) are marked read only. This
253 # inhibits modifying, creating or deleting files in the test directory,
254 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400255 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500256 elif read_only == 1:
257 # Files are marked read only but not the directories. This inhibits
258 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400259 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500260 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500261 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500262 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
263 # is not yet changed to verify the hash of the content of the files it is
264 # looking at, so that if a test modifies an input file, the file must be
265 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400266 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500267 else:
268 raise ValueError(
269 'change_tree_read_only(%s, %s): Unknown flag %s' %
270 (rootdir, read_only, read_only))
271
272
vadimsh8ec66822017-07-25 14:08:29 -0700273@contextlib.contextmanager
274def set_luci_context_account(account, tmp_dir):
275 """Sets LUCI_CONTEXT account to be used by the task.
276
277 If 'account' is None or '', does nothing at all. This happens when
278 run_isolated.py is called without '--switch-to-account' flag. In this case,
279 if run_isolated.py is running in some LUCI_CONTEXT environment, the task will
280 just inherit whatever account is already set. This may happen is users invoke
281 run_isolated.py explicitly from their code.
282
283 If the requested account is not defined in the context, switches to
284 non-authenticated access. This happens for Swarming tasks that don't use
285 'task' service accounts.
286
287 If not using LUCI_CONTEXT-based auth, does nothing.
288 If already running as requested account, does nothing.
289 """
290 if not account:
291 # Not actually switching.
292 yield
293 return
294
295 local_auth = luci_context.read('local_auth')
296 if not local_auth:
297 # Not using LUCI_CONTEXT auth at all.
298 yield
299 return
300
301 # See LUCI_CONTEXT.md for the format of 'local_auth'.
302 if local_auth.get('default_account_id') == account:
303 # Already set, no need to switch.
304 yield
305 return
306
307 available = {a['id'] for a in local_auth.get('accounts') or []}
308 if account in available:
309 logging.info('Switching default LUCI_CONTEXT account to %r', account)
310 local_auth['default_account_id'] = account
311 else:
312 logging.warning(
313 'Requested LUCI_CONTEXT account %r is not available (have only %r), '
314 'disabling authentication', account, sorted(available))
315 local_auth.pop('default_account_id', None)
316
317 with luci_context.write(_tmpdir=tmp_dir, local_auth=local_auth):
318 yield
319
320
nodir90bc8dc2016-06-15 13:35:21 -0700321def process_command(command, out_dir, bot_file):
nodirbe642ff2016-06-09 15:51:51 -0700322 """Replaces variables in a command line.
323
324 Raises:
325 ValueError if a parameter is requested in |command| but its value is not
326 provided.
327 """
maruela9cfd6f2015-09-15 11:03:15 -0700328 def fix(arg):
nodirbe642ff2016-06-09 15:51:51 -0700329 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
330 replace_slash = False
nodir55be77b2016-05-03 09:39:57 -0700331 if ISOLATED_OUTDIR_PARAMETER in arg:
nodirbe642ff2016-06-09 15:51:51 -0700332 if not out_dir:
maruel7f63a272016-07-12 12:40:36 -0700333 raise ValueError(
334 'output directory is requested in command, but not provided; '
335 'please specify one')
nodir55be77b2016-05-03 09:39:57 -0700336 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
nodirbe642ff2016-06-09 15:51:51 -0700337 replace_slash = True
nodir90bc8dc2016-06-15 13:35:21 -0700338 if SWARMING_BOT_FILE_PARAMETER in arg:
339 if bot_file:
340 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
341 replace_slash = True
342 else:
343 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command, but no '
344 'bot_file specified. Leaving parameter unchanged.')
nodirbe642ff2016-06-09 15:51:51 -0700345 if replace_slash:
346 # Replace slashes only if parameters are present
nodir55be77b2016-05-03 09:39:57 -0700347 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
348 arg = arg.replace('/', os.sep)
maruela9cfd6f2015-09-15 11:03:15 -0700349 return arg
350
351 return [fix(arg) for arg in command]
352
353
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -0500354def get_command_env(tmp_dir, cipd_info, cwd, env, env_prefixes):
vadimsh232f5a82017-01-20 19:23:44 -0800355 """Returns full OS environment to run a command in.
356
Robert Iannuccibe66ce72017-11-22 12:56:50 -0800357 Sets up TEMP, puts directory with cipd binary in front of PATH, exposes
358 CIPD_CACHE_DIR env var, and installs all env_prefixes.
vadimsh232f5a82017-01-20 19:23:44 -0800359
360 Args:
361 tmp_dir: temp directory.
362 cipd_info: CipdInfo object is cipd client is used, None if not.
Robert Iannuccibe66ce72017-11-22 12:56:50 -0800363 cwd: The directory the command will run in
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -0500364 env: environment variables to use
Robert Iannuccibe66ce72017-11-22 12:56:50 -0800365 env_prefixes: {"ENV_KEY": ['cwd', 'relative', 'paths', 'to', 'prepend']}
vadimsh232f5a82017-01-20 19:23:44 -0800366 """
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -0500367 out = os.environ.copy()
368 for k, v in env.iteritems():
369 if not v:
370 del out[k]
371 else:
372 out[k] = v
373
374 if cipd_info:
375 bin_dir = os.path.dirname(cipd_info.client.binary_path)
Marc-Antoine Rueldc527d12017-11-29 11:19:16 -0500376 out['PATH'] = '%s%s%s' % (_to_str(bin_dir), os.pathsep, out['PATH'])
377 out['CIPD_CACHE_DIR'] = _to_str(cipd_info.cache_dir)
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -0500378
379 for key, paths in env_prefixes.iteritems():
380 paths = [os.path.normpath(os.path.join(cwd, p)) for p in paths]
381 cur = out.get(key)
382 if cur:
383 paths.append(cur)
Marc-Antoine Rueldc527d12017-11-29 11:19:16 -0500384 out[key] = _to_str(os.path.pathsep.join(paths))
vadimsh232f5a82017-01-20 19:23:44 -0800385
iannucciac0342c2017-02-24 05:47:01 -0800386 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
iannucci460def72017-02-24 10:49:48 -0800387 # * mktemp on linux respects $TMPDIR, not $TMP
388 # * mktemp on OS X SOMETIMES respects $TMPDIR
iannucciac0342c2017-02-24 05:47:01 -0800389 # * chromium's base utils respects $TMPDIR on linux, $TEMP on windows.
390 # Unfortunately at the time of writing it completely ignores all envvars
391 # on OS X.
iannucci460def72017-02-24 10:49:48 -0800392 # * python respects TMPDIR, TEMP, and TMP (regardless of platform)
393 # * golang respects TMPDIR on linux+mac, TEMP on windows.
iannucciac0342c2017-02-24 05:47:01 -0800394 key = {'win32': 'TEMP'}.get(sys.platform, 'TMPDIR')
Marc-Antoine Rueldc527d12017-11-29 11:19:16 -0500395 out[key] = _to_str(tmp_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800396
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -0500397 return out
vadimsh232f5a82017-01-20 19:23:44 -0800398
399
400def run_command(command, cwd, env, hard_timeout, grace_period):
maruel6be7f9e2015-10-01 12:25:30 -0700401 """Runs the command.
402
403 Returns:
404 tuple(process exit code, bool if had a hard timeout)
405 """
maruela9cfd6f2015-09-15 11:03:15 -0700406 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700407
maruel6be7f9e2015-10-01 12:25:30 -0700408 exit_code = None
409 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700410 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700411 proc = None
412 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700413 try:
maruel6be7f9e2015-10-01 12:25:30 -0700414 # TODO(maruel): This code is imperfect. It doesn't handle well signals
415 # during the download phase and there's short windows were things can go
416 # wrong.
417 def handler(signum, _frame):
418 if proc and not had_signal:
419 logging.info('Received signal %d', signum)
420 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700421 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700422
423 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
424 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
425 try:
426 exit_code = proc.wait(hard_timeout or None)
427 except subprocess42.TimeoutExpired:
428 if not had_signal:
429 logging.warning('Hard timeout')
430 had_hard_timeout = True
431 logging.warning('Sending SIGTERM')
432 proc.terminate()
433
434 # Ignore signals in grace period. Forcibly give the grace period to the
435 # child process.
436 if exit_code is None:
437 ignore = lambda *_: None
438 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
439 try:
440 exit_code = proc.wait(grace_period or None)
441 except subprocess42.TimeoutExpired:
442 # Now kill for real. The user can distinguish between the
443 # following states:
444 # - signal but process exited within grace period,
445 # hard_timed_out will be set but the process exit code will be
446 # script provided.
447 # - processed exited late, exit code will be -9 on posix.
448 logging.warning('Grace exhausted; sending SIGKILL')
449 proc.kill()
martiniss3343ec02017-08-01 17:09:43 -0700450 logging.info('Waiting for process exit')
maruel6be7f9e2015-10-01 12:25:30 -0700451 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700452 except OSError:
453 # This is not considered to be an internal error. The executable simply
454 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800455 sys.stderr.write(
456 '<The executable does not exist or a dependent library is missing>\n'
457 '<Check for missing .so/.dll in the .isolate or GN file>\n'
458 '<Command: %s>\n' % command)
459 if os.environ.get('SWARMING_TASK_ID'):
460 # Give an additional hint when running as a swarming task.
461 sys.stderr.write(
462 '<See the task\'s page for commands to help diagnose this issue '
463 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700464 exit_code = 1
465 logging.info(
466 'Command finished with exit code %d (%s)',
467 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700468 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700469
470
maruel4409e302016-07-19 14:25:51 -0700471def fetch_and_map(isolated_hash, storage, cache, outdir, use_symlinks):
472 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700473 start = time.time()
474 bundle = isolateserver.fetch_isolated(
475 isolated_hash=isolated_hash,
476 storage=storage,
477 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700478 outdir=outdir,
479 use_symlinks=use_symlinks)
nodir6f801882016-04-29 14:41:50 -0700480 return bundle, {
481 'duration': time.time() - start,
482 'initial_number_items': cache.initial_number_items,
483 'initial_size': cache.initial_size,
484 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
485 'items_hot': base64.b64encode(
tansell9e04a8d2016-07-28 09:31:59 -0700486 large.pack(sorted(set(cache.used) - set(cache.added)))),
nodir6f801882016-04-29 14:41:50 -0700487 }
488
489
aludwin0a8e17d2016-10-27 15:57:39 -0700490def link_outputs_to_outdir(run_dir, out_dir, outputs):
491 """Links any named outputs to out_dir so they can be uploaded.
492
493 Raises an error if the file already exists in that directory.
494 """
495 if not outputs:
496 return
497 isolateserver.create_directories(out_dir, outputs)
498 for o in outputs:
499 try:
aludwinb35146d2017-06-12 06:03:00 -0700500 infile = os.path.join(run_dir, o)
501 outfile = os.path.join(out_dir, o)
502 if fs.islink(infile):
503 # TODO(aludwin): handle directories
504 fs.copy2(infile, outfile)
505 else:
506 file_path.link_file(outfile, infile, file_path.HARDLINK_WITH_FALLBACK)
aludwin0a8e17d2016-10-27 15:57:39 -0700507 except OSError as e:
aludwin81178302016-11-30 17:18:49 -0800508 logging.info("Couldn't collect output file %s: %s", o, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700509
510
maruela9cfd6f2015-09-15 11:03:15 -0700511def delete_and_upload(storage, out_dir, leak_temp_dir):
512 """Deletes the temporary run directory and uploads results back.
513
514 Returns:
nodir6f801882016-04-29 14:41:50 -0700515 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700516 - outputs_ref: a dict referring to the results archived back to the isolated
517 server, if applicable.
518 - success: False if something occurred that means that the task must
519 forcibly be considered a failure, e.g. zombie processes were left
520 behind.
nodir6f801882016-04-29 14:41:50 -0700521 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700522 """
maruela9cfd6f2015-09-15 11:03:15 -0700523 # Upload out_dir and generate a .isolated file out of this directory. It is
524 # only done if files were written in the directory.
525 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700526 cold = []
527 hot = []
nodir6f801882016-04-29 14:41:50 -0700528 start = time.time()
529
maruel12e30012015-10-09 11:55:35 -0700530 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700531 with tools.Profiler('ArchiveOutput'):
532 try:
maruel064c0a32016-04-05 11:47:15 -0700533 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700534 storage, [out_dir], None)
535 outputs_ref = {
536 'isolated': results[0][0],
537 'isolatedserver': storage.location,
538 'namespace': storage.namespace,
539 }
maruel064c0a32016-04-05 11:47:15 -0700540 cold = sorted(i.size for i in f_cold)
541 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700542 except isolateserver.Aborted:
543 # This happens when a signal SIGTERM was received while uploading data.
544 # There is 2 causes:
545 # - The task was too slow and was about to be killed anyway due to
546 # exceeding the hard timeout.
547 # - The amount of data uploaded back is very large and took too much
548 # time to archive.
549 sys.stderr.write('Received SIGTERM while uploading')
550 # Re-raise, so it will be treated as an internal failure.
551 raise
nodir6f801882016-04-29 14:41:50 -0700552
553 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700554 try:
maruel12e30012015-10-09 11:55:35 -0700555 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700556 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700557 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700558 else:
559 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700560 except OSError as e:
561 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700562 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700563 stats = {
564 'duration': time.time() - start,
565 'items_cold': base64.b64encode(large.pack(cold)),
566 'items_hot': base64.b64encode(large.pack(hot)),
567 }
568 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700569
570
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500571def map_and_run(data, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700572 """Runs a command with optional isolated input/output.
573
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500574 Arguments:
575 - data: TaskData instance.
576 - constant_run_path: TODO
nodir55be77b2016-05-03 09:39:57 -0700577
578 Returns metadata about the result.
579 """
maruela9cfd6f2015-09-15 11:03:15 -0700580 result = {
maruel064c0a32016-04-05 11:47:15 -0700581 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700582 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700583 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700584 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700585 'stats': {
nodir55715712016-06-03 12:28:19 -0700586 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700587 # 'cipd': {
588 # 'duration': 0.,
589 # 'get_client_duration': 0.,
590 # },
nodir55715712016-06-03 12:28:19 -0700591 # 'download': {
592 # 'duration': 0.,
593 # 'initial_number_items': 0,
594 # 'initial_size': 0,
595 # 'items_cold': '<large.pack()>',
596 # 'items_hot': '<large.pack()>',
597 # },
598 # 'upload': {
599 # 'duration': 0.,
600 # 'items_cold': '<large.pack()>',
601 # 'items_hot': '<large.pack()>',
602 # },
maruel064c0a32016-04-05 11:47:15 -0700603 # },
604 },
iannucci96fcccc2016-08-30 15:52:22 -0700605 # 'cipd_pins': {
606 # 'packages': [
607 # {'package_name': ..., 'version': ..., 'path': ...},
608 # ...
609 # ],
610 # 'client_package': {'package_name': ..., 'version': ...},
611 # },
maruela9cfd6f2015-09-15 11:03:15 -0700612 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700613 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700614 }
nodirbe642ff2016-06-09 15:51:51 -0700615
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500616 if data.root_dir:
617 file_path.ensure_tree(data.root_dir, 0700)
618 elif data.isolate_cache.cache_dir:
619 data = data._replace(
620 root_dir=os.path.dirname(data.isolate_cache.cache_dir))
maruele2f2cb82016-07-13 14:41:03 -0700621 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700622 # If root_dir is not specified, it is not constant.
623 # TODO(maruel): This is not obvious. Change this to become an error once we
624 # make the constant_run_path an exposed flag.
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500625 if constant_run_path and data.root_dir:
626 run_dir = os.path.join(data.root_dir, ISOLATED_RUN_DIR)
maruel13437a72017-05-26 05:33:40 -0700627 if os.path.isdir(run_dir):
628 file_path.rmtree(run_dir)
maruelcffa0542017-04-07 08:39:20 -0700629 os.mkdir(run_dir)
630 else:
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500631 run_dir = make_temp_dir(ISOLATED_RUN_DIR, data.root_dir)
maruel03e11842016-07-14 10:50:16 -0700632 # storage should be normally set but don't crash if it is not. This can happen
633 # as Swarming task can run without an isolate server.
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500634 out_dir = make_temp_dir(
635 ISOLATED_OUT_DIR, data.root_dir) if data.storage else None
636 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, data.root_dir)
nodir55be77b2016-05-03 09:39:57 -0700637 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700638
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500639 command = data.command
nodir55be77b2016-05-03 09:39:57 -0700640 try:
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500641 with data.install_packages_fn(run_dir) as cipd_info:
vadimsh232f5a82017-01-20 19:23:44 -0800642 if cipd_info:
643 result['stats']['cipd'] = cipd_info.stats
644 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700645
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500646 if data.isolated_hash:
vadimsh232f5a82017-01-20 19:23:44 -0800647 isolated_stats = result['stats'].setdefault('isolated', {})
648 bundle, isolated_stats['download'] = fetch_and_map(
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500649 isolated_hash=data.isolated_hash,
650 storage=data.storage,
651 cache=data.isolate_cache,
vadimsh232f5a82017-01-20 19:23:44 -0800652 outdir=run_dir,
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500653 use_symlinks=data.use_symlinks)
vadimsh232f5a82017-01-20 19:23:44 -0800654 change_tree_read_only(run_dir, bundle.read_only)
maruelabec63c2017-04-26 11:53:24 -0700655 # Inject the command
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500656 if not command and bundle.command:
657 command = bundle.command + data.extra_args
Marc-Antoine Ruelb2cef0f2017-10-31 10:51:23 -0400658 # Only set the relative directory if the isolated file specified a
659 # command, and no raw command was specified.
660 if bundle.relative_cwd:
661 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
maruelabec63c2017-04-26 11:53:24 -0700662
663 if not command:
664 # Handle this as a task failure, not an internal failure.
665 sys.stderr.write(
666 '<No command was specified!>\n'
667 '<Please secify a command when triggering your Swarming task>\n')
668 result['exit_code'] = 1
669 return result
nodirbe642ff2016-06-09 15:51:51 -0700670
vadimsh232f5a82017-01-20 19:23:44 -0800671 # If we have an explicit list of files to return, make sure their
672 # directories exist now.
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500673 if data.storage and data.outputs:
674 isolateserver.create_directories(run_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700675
vadimsh232f5a82017-01-20 19:23:44 -0800676 command = tools.fix_python_path(command)
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500677 command = process_command(command, out_dir, data.bot_file)
vadimsh232f5a82017-01-20 19:23:44 -0800678 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700679
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500680 with data.install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -0800681 sys.stdout.flush()
682 start = time.time()
683 try:
vadimsh8ec66822017-07-25 14:08:29 -0700684 # Need to switch the default account before 'get_command_env' call,
685 # so it can grab correct value of LUCI_CONTEXT env var.
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500686 with set_luci_context_account(data.switch_to_account, tmp_dir):
687 env = get_command_env(
688 tmp_dir, cipd_info, cwd, data.env, data.env_prefix)
vadimsh8ec66822017-07-25 14:08:29 -0700689 result['exit_code'], result['had_hard_timeout'] = run_command(
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500690 command, cwd, env, data.hard_timeout, data.grace_period)
nodird6160682017-02-02 13:03:35 -0800691 finally:
692 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700693 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700694 # An internal error occurred. Report accordingly so the swarming task will
695 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700696 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700697 result['internal_failure'] = str(e)
698 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700699
700 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700701 finally:
702 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700703 # Try to link files to the output directory, if specified.
704 if out_dir:
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500705 link_outputs_to_outdir(run_dir, out_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700706
nodir32a1ec12016-10-26 18:34:07 -0700707 success = False
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500708 if data.leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700709 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700710 logging.warning(
711 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700712 else:
maruel84537cb2015-10-16 14:21:28 -0700713 # On Windows rmtree(run_dir) call above has a synchronization effect: it
714 # finishes only when all task child processes terminate (since a running
715 # process locks *.exe file). Examine out_dir only after that call
716 # completes (since child processes may write to out_dir too and we need
717 # to wait for them to finish).
718 if fs.isdir(run_dir):
719 try:
720 success = file_path.rmtree(run_dir)
721 except OSError as e:
722 logging.error('Failure with %s', e)
723 success = False
724 if not success:
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500725 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('run', data.grace_period))
maruel84537cb2015-10-16 14:21:28 -0700726 if result['exit_code'] == 0:
727 result['exit_code'] = 1
728 if fs.isdir(tmp_dir):
729 try:
730 success = file_path.rmtree(tmp_dir)
731 except OSError as e:
732 logging.error('Failure with %s', e)
733 success = False
734 if not success:
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500735 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('temp', data.grace_period))
maruel84537cb2015-10-16 14:21:28 -0700736 if result['exit_code'] == 0:
737 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700738
marueleb5fbee2015-09-17 13:01:36 -0700739 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700740 if out_dir:
nodir55715712016-06-03 12:28:19 -0700741 isolated_stats = result['stats'].setdefault('isolated', {})
742 result['outputs_ref'], success, isolated_stats['upload'] = (
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500743 delete_and_upload(data.storage, out_dir, data.leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700744 if not success and result['exit_code'] == 0:
745 result['exit_code'] = 1
746 except Exception as e:
747 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700748 if out_dir:
749 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700750 result['internal_failure'] = str(e)
751 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500752
753
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500754def run_tha_test(data, result_json):
nodir55be77b2016-05-03 09:39:57 -0700755 """Runs an executable and records execution metadata.
756
nodir55be77b2016-05-03 09:39:57 -0700757 If isolated_hash is specified, downloads the dependencies in the cache,
758 hardlinks them into a temporary directory and runs the command specified in
759 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500760
761 A temporary directory is created to hold the output files. The content inside
762 this directory will be uploaded back to |storage| packaged as a .isolated
763 file.
764
765 Arguments:
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500766 - data: TaskData instance.
767 - result_json: File path to dump result metadata into. If set, the process
768 exit code is always 0 unless an internal error occurred.
maruela9cfd6f2015-09-15 11:03:15 -0700769
770 Returns:
771 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000772 """
maruela76b9ee2015-12-15 06:18:08 -0800773 if result_json:
774 # Write a json output file right away in case we get killed.
775 result = {
776 'exit_code': None,
777 'had_hard_timeout': False,
778 'internal_failure': 'Was terminated before completion',
779 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700780 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800781 }
782 tools.write_json(result_json, result, dense=True)
783
maruela9cfd6f2015-09-15 11:03:15 -0700784 # run_isolated exit code. Depends on if result_json is used or not.
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500785 result = map_and_run(data, True)
maruela9cfd6f2015-09-15 11:03:15 -0700786 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700787
maruela9cfd6f2015-09-15 11:03:15 -0700788 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700789 # We've found tests to delete 'work' when quitting, causing an exception
790 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700791 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700792 tools.write_json(result_json, result, dense=True)
793 # Only return 1 if there was an internal error.
794 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000795
maruela9cfd6f2015-09-15 11:03:15 -0700796 # Marshall into old-style inline output.
797 if result['outputs_ref']:
798 data = {
799 'hash': result['outputs_ref']['isolated'],
800 'namespace': result['outputs_ref']['namespace'],
801 'storage': result['outputs_ref']['isolatedserver'],
802 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500803 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700804 print(
805 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
806 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800807 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700808 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000809
810
iannuccib58d10d2017-03-18 02:00:25 -0700811# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800812CipdInfo = collections.namedtuple('CipdInfo', [
813 'client', # cipd.CipdClient object
814 'cache_dir', # absolute path to bot-global cipd tag and instance cache
815 'stats', # dict with stats to return to the server
816 'pins', # dict with installed cipd pins to return to the server
817])
818
819
820@contextlib.contextmanager
821def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700822 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800823 yield None
824
825
iannuccib58d10d2017-03-18 02:00:25 -0700826def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
827 """Calls 'cipd ensure' for packages.
828
829 Args:
830 run_dir (str): root of installation.
831 cipd_cache_dir (str): the directory to use for the cipd package cache.
832 client (CipdClient): the cipd client to use
833 packages: packages to install, list [(path, package_name, version), ...].
834 timeout: max duration in seconds that this function can take.
835
836 Returns: list of pinned packages. Looks like [
837 {
838 'path': 'subdirectory',
839 'package_name': 'resolved/package/name',
840 'version': 'deadbeef...',
841 },
842 ...
843 ]
844 """
845 package_pins = [None]*len(packages)
846 def insert_pin(path, name, version, idx):
847 package_pins[idx] = {
848 'package_name': name,
849 # swarming deals with 'root' as '.'
850 'path': path or '.',
851 'version': version,
852 }
853
854 by_path = collections.defaultdict(list)
855 for i, (path, name, version) in enumerate(packages):
856 # cipd deals with 'root' as ''
857 if path == '.':
858 path = ''
859 by_path[path].append((name, version, i))
860
861 pins = client.ensure(
862 run_dir,
863 {
864 subdir: [(name, vers) for name, vers, _ in pkgs]
865 for subdir, pkgs in by_path.iteritems()
866 },
867 cache_dir=cipd_cache_dir,
868 timeout=timeout,
869 )
870
871 for subdir, pin_list in sorted(pins.iteritems()):
872 this_subdir = by_path[subdir]
873 for i, (name, version) in enumerate(pin_list):
874 insert_pin(subdir, name, version, this_subdir[i][2])
875
876 assert None not in package_pins
877
878 return package_pins
879
880
vadimsh232f5a82017-01-20 19:23:44 -0800881@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700882def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700883 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800884 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800885 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700886
vadimsh232f5a82017-01-20 19:23:44 -0800887 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
888
889 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700890 [
891 {
892 "path": path, "package_name": package_name, "version": version,
893 },
894 ...
895 ]
vadimsh902948e2017-01-20 15:57:32 -0800896 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700897
898 such that they correspond 1:1 to all input package arguments from the command
899 line. These dictionaries make their all the way back to swarming, where they
900 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700901
vadimsh902948e2017-01-20 15:57:32 -0800902 If 'packages' list is empty, will bootstrap CIPD client, but won't install
903 any packages.
904
905 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800906 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800907
nodirbe642ff2016-06-09 15:51:51 -0700908 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700909 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800910 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700911 service_url (str): CIPD server url, e.g.
912 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700913 client_package_name (str): CIPD package name of CIPD client.
914 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700915 cache_dir (str): where to keep cache of cipd clients, packages and tags.
916 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700917 """
918 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700919
nodirbe642ff2016-06-09 15:51:51 -0700920 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700921 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700922
vadimsh902948e2017-01-20 15:57:32 -0800923 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800924 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700925 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800926 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700927
nodirbe642ff2016-06-09 15:51:51 -0700928 get_client_start = time.time()
929 client_manager = cipd.get_client(
930 service_url, client_package_name, client_version, cache_dir,
931 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700932
nodirbe642ff2016-06-09 15:51:51 -0700933 with client_manager as client:
934 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700935
iannuccib58d10d2017-03-18 02:00:25 -0700936 package_pins = []
937 if packages:
938 package_pins = _install_packages(
939 run_dir, cipd_cache_dir, client, packages, timeoutfn())
940
941 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700942
vadimsh232f5a82017-01-20 19:23:44 -0800943 total_duration = time.time() - start
944 logging.info(
945 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700946
vadimsh232f5a82017-01-20 19:23:44 -0800947 yield CipdInfo(
948 client=client,
949 cache_dir=cipd_cache_dir,
950 stats={
951 'duration': total_duration,
952 'get_client_duration': get_client_duration,
953 },
954 pins={
iannuccib58d10d2017-03-18 02:00:25 -0700955 'client_package': {
956 'package_name': client.package_name,
957 'version': client.instance_id,
958 },
vadimsh232f5a82017-01-20 19:23:44 -0800959 'packages': package_pins,
960 })
nodirbe642ff2016-06-09 15:51:51 -0700961
962
nodirf33b8d62016-10-26 22:34:58 -0700963def clean_caches(options, isolate_cache, named_cache_manager):
maruele6fc9382017-05-04 09:03:48 -0700964 """Trims isolated and named caches.
965
966 The goal here is to coherently trim both caches, deleting older items
967 independent of which container they belong to.
968 """
969 # TODO(maruel): Trim CIPD cache the same way.
970 total = 0
nodirf33b8d62016-10-26 22:34:58 -0700971 with named_cache_manager.open():
972 oldest_isolated = isolate_cache.get_oldest()
973 oldest_named = named_cache_manager.get_oldest()
974 trimmers = [
975 (
976 isolate_cache.trim,
977 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
978 ),
979 (
980 lambda: named_cache_manager.trim(options.min_free_space),
981 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
982 ),
983 ]
984 trimmers.sort(key=lambda (_, ts): ts)
maruele6fc9382017-05-04 09:03:48 -0700985 # TODO(maruel): This is incorrect, we want to trim 'items' that are strictly
986 # the oldest independent of in which cache they live in. Right now, the
987 # cache with the oldest item pays the price.
nodirf33b8d62016-10-26 22:34:58 -0700988 for trim, _ in trimmers:
maruele6fc9382017-05-04 09:03:48 -0700989 total += trim()
nodirf33b8d62016-10-26 22:34:58 -0700990 isolate_cache.cleanup()
maruele6fc9382017-05-04 09:03:48 -0700991 return total
nodirf33b8d62016-10-26 22:34:58 -0700992
993
nodirbe642ff2016-06-09 15:51:51 -0700994def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400995 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700996 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000997 version=__version__,
998 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700999 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -07001000 '--clean', action='store_true',
1001 help='Cleans the cache, trimming it necessary and remove corrupted items '
1002 'and returns without executing anything; use with -v to know what '
1003 'was done')
1004 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -07001005 '--no-clean', action='store_true',
1006 help='Do not clean the cache automatically on startup. This is meant for '
1007 'bots where a separate execution with --clean was done earlier so '
1008 'doing it again is redundant')
1009 parser.add_option(
maruel4409e302016-07-19 14:25:51 -07001010 '--use-symlinks', action='store_true',
1011 help='Use symlinks instead of hardlinks')
1012 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -07001013 '--json',
1014 help='dump output metadata to json file. When used, run_isolated returns '
1015 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -07001016 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001017 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -07001018 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001019 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -07001020 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -07001021 parser.add_option(
Marc-Antoine Ruel7d179af2017-10-24 16:52:02 -07001022 '--raw-cmd', action='store_true',
1023 help='Ignore the isolated command, use the one supplied at the command '
1024 'line')
1025 parser.add_option(
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -05001026 '--env', default=[], action='append',
1027 help='Environment variables to set for the child process')
1028 parser.add_option(
1029 '--env-prefix', default=[], action='append',
Robert Iannuccibe66ce72017-11-22 12:56:50 -08001030 help='Specify a VAR=./path/fragment to put in the environment variable '
1031 'before executing the command. The path fragment must be relative '
1032 'to the isolated run directory, and must not contain a `..` token. '
1033 'The path will be made absolute and prepended to the indicated '
1034 '$VAR using the OS\'s path separator. Multiple items for the same '
1035 '$VAR will be prepended in order.')
1036 parser.add_option(
bpastene3ae09522016-06-10 17:12:59 -07001037 '--bot-file',
1038 help='Path to a file describing the state of the host. The content is '
1039 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -07001040 parser.add_option(
vadimsh8ec66822017-07-25 14:08:29 -07001041 '--switch-to-account',
1042 help='If given, switches LUCI_CONTEXT to given logical service account '
1043 '(e.g. "task" or "system") before launching the isolated process.')
1044 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -07001045 '--output', action='append',
1046 help='Specifies an output to return. If no outputs are specified, all '
1047 'files located in $(ISOLATED_OUTDIR) will be returned; '
1048 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
1049 'specified by --output option (there can be multiple) will be '
1050 'returned. Note that if a file in OUT_DIR has the same path '
1051 'as an --output option, the --output version will be returned.')
1052 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -07001053 '-a', '--argsfile',
1054 # This is actually handled in parse_args; it's included here purely so it
1055 # can make it into the help text.
1056 help='Specify a file containing a JSON array of arguments to this '
1057 'script. If --argsfile is provided, no other argument may be '
1058 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001059 data_group = optparse.OptionGroup(parser, 'Data source')
1060 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001061 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -07001062 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001063 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001064 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001065
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -04001066 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001067
1068 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -07001069 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001070
Kenneth Russell61d42352014-09-15 11:41:16 -07001071 debug_group = optparse.OptionGroup(parser, 'Debugging')
1072 debug_group.add_option(
1073 '--leak-temp-dir',
1074 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -07001075 help='Deliberately leak isolate\'s temp dir for later examination. '
1076 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -07001077 debug_group.add_option(
1078 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -07001079 parser.add_option_group(debug_group)
1080
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001081 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001082
nodirf33b8d62016-10-26 22:34:58 -07001083 parser.set_defaults(
1084 cache='cache',
1085 cipd_cache='cipd_cache',
1086 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -07001087 return parser
1088
1089
aludwin7556e0c2016-10-26 08:46:10 -07001090def parse_args(args):
1091 # Create a fake mini-parser just to get out the "-a" command. Note that
1092 # it's not documented here; instead, it's documented in create_option_parser
1093 # even though that parser will never actually get to parse it. This is
1094 # because --argsfile is exclusive with all other options and arguments.
1095 file_argparse = argparse.ArgumentParser(add_help=False)
1096 file_argparse.add_argument('-a', '--argsfile')
1097 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
1098 if file_args.argsfile:
1099 if nonfile_args:
1100 file_argparse.error('Can\'t specify --argsfile with'
1101 'any other arguments (%s)' % nonfile_args)
1102 try:
1103 with open(file_args.argsfile, 'r') as f:
1104 args = json.load(f)
1105 except (IOError, OSError, ValueError) as e:
1106 # We don't need to error out here - "args" is now empty,
1107 # so the call below to parser.parse_args(args) will fail
1108 # and print the full help text.
1109 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
1110
1111 # Even if we failed to read the args, just call the normal parser now since it
1112 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -07001113 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001114 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -07001115 return (parser, options, args)
1116
1117
1118def main(args):
Marc-Antoine Rueldc527d12017-11-29 11:19:16 -05001119 # Warning: when --argsfile is used, the strings are unicode instances, when
1120 # parsed normally, the strings are str instances.
aludwin7556e0c2016-10-26 08:46:10 -07001121 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -07001122
Marc-Antoine Ruelc7a332f2017-08-25 17:37:51 -04001123 if not file_path.enable_symlink():
1124 logging.error('Symlink support is not enabled')
1125
nodirf33b8d62016-10-26 22:34:58 -07001126 isolate_cache = isolateserver.process_cache_options(options, trim=False)
1127 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -07001128 if options.clean:
1129 if options.isolated:
1130 parser.error('Can\'t use --isolated with --clean.')
1131 if options.isolate_server:
1132 parser.error('Can\'t use --isolate-server with --clean.')
1133 if options.json:
1134 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -07001135 if options.named_caches:
1136 parser.error('Can\t use --named-cache with --clean.')
1137 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001138 return 0
nodirf33b8d62016-10-26 22:34:58 -07001139
maruel2e8d0f52016-07-16 07:51:29 -07001140 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -07001141 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001142
nodir55be77b2016-05-03 09:39:57 -07001143 if not options.isolated and not args:
1144 parser.error('--isolated or command to run is required.')
1145
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001146 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001147
1148 isolateserver.process_isolate_server_options(
Marc-Antoine Ruelc7a332f2017-08-25 17:37:51 -04001149 parser, options, True, False)
nodir55be77b2016-05-03 09:39:57 -07001150 if not options.isolate_server:
1151 if options.isolated:
1152 parser.error('--isolated requires --isolate-server')
1153 if ISOLATED_OUTDIR_PARAMETER in args:
1154 parser.error(
1155 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001156
nodir90bc8dc2016-06-15 13:35:21 -07001157 if options.root_dir:
1158 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -07001159 if options.json:
1160 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001161
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -05001162 if any('=' not in i for i in options.env):
1163 parser.error(
1164 '--env required key=value form. value can be skipped to delete '
1165 'the variable')
Marc-Antoine Ruelb651cad2017-12-01 18:45:18 -05001166 options.env = dict(i.split('=', 1) for i in options.env)
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -05001167
1168 prefixes = {}
1169 cwd = os.path.realpath(os.getcwd())
1170 for item in options.env_prefix:
1171 if '=' not in item:
1172 parser.error(
1173 '--env-prefix %r is malformed, must be in the form `VAR=./path`'
1174 % item)
Marc-Antoine Ruelb651cad2017-12-01 18:45:18 -05001175 key, opath = item.split('=', 1)
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -05001176 if os.path.isabs(opath):
1177 parser.error('--env-prefix %r path is bad, must be relative.' % opath)
1178 opath = os.path.normpath(opath)
1179 if not os.path.realpath(os.path.join(cwd, opath)).startswith(cwd):
1180 parser.error(
1181 '--env-prefix %r path is bad, must be relative and not contain `..`.'
1182 % opath)
1183 prefixes.setdefault(key, []).append(opath)
1184 options.env_prefix = prefixes
Robert Iannuccibe66ce72017-11-22 12:56:50 -08001185
nodirbe642ff2016-06-09 15:51:51 -07001186 cipd.validate_cipd_options(parser, options)
1187
vadimsh232f5a82017-01-20 19:23:44 -08001188 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -08001189 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -07001190 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -08001191 run_dir, cipd.parse_package_args(options.cipd_packages),
1192 options.cipd_server, options.cipd_client_package,
1193 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001194
nodird6160682017-02-02 13:03:35 -08001195 @contextlib.contextmanager
nodir26251c42017-05-11 13:21:53 -07001196 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001197 # WARNING: this function depends on "options" variable defined in the outer
1198 # function.
nodir26251c42017-05-11 13:21:53 -07001199 caches = [
1200 (os.path.join(run_dir, unicode(relpath)), name)
1201 for name, relpath in options.named_caches
1202 ]
nodirf33b8d62016-10-26 22:34:58 -07001203 with named_cache_manager.open():
nodir26251c42017-05-11 13:21:53 -07001204 for path, name in caches:
1205 named_cache_manager.install(path, name)
nodird6160682017-02-02 13:03:35 -08001206 try:
1207 yield
1208 finally:
dnjd5e4ecc2017-07-07 11:16:44 -07001209 # Uninstall each named cache, returning it to the cache pool. If an
1210 # uninstall fails for a given cache, it will remain in the task's
1211 # temporary space, get cleaned up by the Swarming bot, and be lost.
1212 #
1213 # If the Swarming bot cannot clean up the cache, it will handle it like
1214 # any other bot file that could not be removed.
nodir26251c42017-05-11 13:21:53 -07001215 with named_cache_manager.open():
1216 for path, name in caches:
dnjd5e4ecc2017-07-07 11:16:44 -07001217 try:
1218 named_cache_manager.uninstall(path, name)
1219 except named_cache.Error:
1220 logging.exception('Error while removing named cache %r at %r. '
1221 'The cache will be lost.', path, name)
nodirf33b8d62016-10-26 22:34:58 -07001222
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -05001223 extra_args = []
1224 command = []
1225 if options.raw_cmd:
1226 command = args
1227 else:
1228 extra_args = args
1229
1230 data = TaskData(
1231 command=command,
1232 extra_args=extra_args,
1233 isolated_hash=options.isolated,
1234 storage=None,
1235 isolate_cache=isolate_cache,
1236 outputs=options.output,
1237 install_named_caches=install_named_caches,
1238 leak_temp_dir=options.leak_temp_dir,
1239 root_dir=_to_unicode(options.root_dir),
1240 hard_timeout=options.hard_timeout,
1241 grace_period=options.grace_period,
1242 bot_file=options.bot_file,
1243 switch_to_account=options.switch_to_account,
1244 install_packages_fn=install_packages_fn,
1245 use_symlinks=options.use_symlinks,
1246 env=options.env,
1247 env_prefix=options.env_prefix)
nodirbe642ff2016-06-09 15:51:51 -07001248 try:
nodir90bc8dc2016-06-15 13:35:21 -07001249 if options.isolate_server:
1250 storage = isolateserver.get_storage(
1251 options.isolate_server, options.namespace)
1252 with storage:
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -05001253 data = data._replace(storage=storage)
nodirf33b8d62016-10-26 22:34:58 -07001254 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
1255 assert storage.hash_algo == isolate_cache.hash_algo
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -05001256 return run_tha_test(data, options.json)
1257 return run_tha_test(data, options.json)
nodirf33b8d62016-10-26 22:34:58 -07001258 except (cipd.Error, named_cache.Error) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001259 print >> sys.stderr, ex.message
1260 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001261
1262
1263if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001264 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001265 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001266 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001267 sys.exit(main(sys.argv[1:]))