blob: 4cff1eb38866fddf2cf0f49a643ba5d49e2904ce [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 Ruelee6ca622017-11-29 11:19:16 -050030__version__ = '0.10.2'
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
vadimsh9c54b2c2017-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
marueld928c862017-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
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000145def get_as_zip_package(executable=True):
146 """Returns ZipPackage with this module and all its dependencies.
147
148 If |executable| is True will store run_isolated.py as __main__.py so that
149 zip package is directly executable be python.
150 """
151 # Building a zip package when running from another zip package is
152 # unsupported and probably unneeded.
153 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +0000154 assert THIS_FILE_PATH
155 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000156 package = zip_package.ZipPackage(root=BASE_DIR)
157 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
aludwin81178302016-11-30 17:18:49 -0800158 package.add_python_file(os.path.join(BASE_DIR, 'isolate_storage.py'))
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400159 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000160 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800161 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
nodirbe642ff2016-06-09 15:51:51 -0700162 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
nodirf33b8d62016-10-26 22:34:58 -0700163 package.add_python_file(os.path.join(BASE_DIR, 'named_cache.py'))
tanselle4288c32016-07-28 09:45:40 -0700164 package.add_directory(os.path.join(BASE_DIR, 'libs'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000165 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
166 package.add_directory(os.path.join(BASE_DIR, 'utils'))
167 return package
168
169
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500170def _to_str(s):
171 """Downgrades a unicode instance to str. Pass str through as-is."""
172 if isinstance(s, str):
173 return s
174 # This is technically incorrect, especially on Windows. In theory
175 # sys.getfilesystemencoding() should be used to use the right 'ANSI code
176 # page' on Windows, but that causes other problems, as the character set
177 # is very limited.
178 return s.encode('utf-8')
179
180
maruel03e11842016-07-14 10:50:16 -0700181def make_temp_dir(prefix, root_dir):
182 """Returns a new unique temporary directory."""
183 return unicode(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000184
185
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500186def change_tree_read_only(rootdir, read_only):
187 """Changes the tree read-only bits according to the read_only specification.
188
189 The flag can be 0, 1 or 2, which will affect the possibility to modify files
190 and create or delete files.
191 """
192 if read_only == 2:
193 # Files and directories (except on Windows) are marked read only. This
194 # inhibits modifying, creating or deleting files in the test directory,
195 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400196 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500197 elif read_only == 1:
198 # Files are marked read only but not the directories. This inhibits
199 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400200 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500201 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500202 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500203 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
204 # is not yet changed to verify the hash of the content of the files it is
205 # looking at, so that if a test modifies an input file, the file must be
206 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400207 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500208 else:
209 raise ValueError(
210 'change_tree_read_only(%s, %s): Unknown flag %s' %
211 (rootdir, read_only, read_only))
212
213
vadimsh9c54b2c2017-07-25 14:08:29 -0700214@contextlib.contextmanager
215def set_luci_context_account(account, tmp_dir):
216 """Sets LUCI_CONTEXT account to be used by the task.
217
218 If 'account' is None or '', does nothing at all. This happens when
219 run_isolated.py is called without '--switch-to-account' flag. In this case,
220 if run_isolated.py is running in some LUCI_CONTEXT environment, the task will
221 just inherit whatever account is already set. This may happen is users invoke
222 run_isolated.py explicitly from their code.
223
224 If the requested account is not defined in the context, switches to
225 non-authenticated access. This happens for Swarming tasks that don't use
226 'task' service accounts.
227
228 If not using LUCI_CONTEXT-based auth, does nothing.
229 If already running as requested account, does nothing.
230 """
231 if not account:
232 # Not actually switching.
233 yield
234 return
235
236 local_auth = luci_context.read('local_auth')
237 if not local_auth:
238 # Not using LUCI_CONTEXT auth at all.
239 yield
240 return
241
242 # See LUCI_CONTEXT.md for the format of 'local_auth'.
243 if local_auth.get('default_account_id') == account:
244 # Already set, no need to switch.
245 yield
246 return
247
248 available = {a['id'] for a in local_auth.get('accounts') or []}
249 if account in available:
250 logging.info('Switching default LUCI_CONTEXT account to %r', account)
251 local_auth['default_account_id'] = account
252 else:
253 logging.warning(
254 'Requested LUCI_CONTEXT account %r is not available (have only %r), '
255 'disabling authentication', account, sorted(available))
256 local_auth.pop('default_account_id', None)
257
258 with luci_context.write(_tmpdir=tmp_dir, local_auth=local_auth):
259 yield
260
261
nodir90bc8dc2016-06-15 13:35:21 -0700262def process_command(command, out_dir, bot_file):
nodirbe642ff2016-06-09 15:51:51 -0700263 """Replaces variables in a command line.
264
265 Raises:
266 ValueError if a parameter is requested in |command| but its value is not
267 provided.
268 """
maruela9cfd6f2015-09-15 11:03:15 -0700269 def fix(arg):
nodirbe642ff2016-06-09 15:51:51 -0700270 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
271 replace_slash = False
nodir55be77b2016-05-03 09:39:57 -0700272 if ISOLATED_OUTDIR_PARAMETER in arg:
nodirbe642ff2016-06-09 15:51:51 -0700273 if not out_dir:
maruel7f63a272016-07-12 12:40:36 -0700274 raise ValueError(
275 'output directory is requested in command, but not provided; '
276 'please specify one')
nodir55be77b2016-05-03 09:39:57 -0700277 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
nodirbe642ff2016-06-09 15:51:51 -0700278 replace_slash = True
nodir90bc8dc2016-06-15 13:35:21 -0700279 if SWARMING_BOT_FILE_PARAMETER in arg:
280 if bot_file:
281 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
282 replace_slash = True
283 else:
284 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command, but no '
285 'bot_file specified. Leaving parameter unchanged.')
nodirbe642ff2016-06-09 15:51:51 -0700286 if replace_slash:
287 # Replace slashes only if parameters are present
nodir55be77b2016-05-03 09:39:57 -0700288 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
289 arg = arg.replace('/', os.sep)
maruela9cfd6f2015-09-15 11:03:15 -0700290 return arg
291
292 return [fix(arg) for arg in command]
293
294
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500295def get_command_env(tmp_dir, cipd_info, cwd, env, env_prefixes):
vadimsh232f5a82017-01-20 19:23:44 -0800296 """Returns full OS environment to run a command in.
297
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800298 Sets up TEMP, puts directory with cipd binary in front of PATH, exposes
299 CIPD_CACHE_DIR env var, and installs all env_prefixes.
vadimsh232f5a82017-01-20 19:23:44 -0800300
301 Args:
302 tmp_dir: temp directory.
303 cipd_info: CipdInfo object is cipd client is used, None if not.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800304 cwd: The directory the command will run in
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500305 env: environment variables to use
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800306 env_prefixes: {"ENV_KEY": ['cwd', 'relative', 'paths', 'to', 'prepend']}
vadimsh232f5a82017-01-20 19:23:44 -0800307 """
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500308 out = os.environ.copy()
309 for k, v in env.iteritems():
310 if not v:
311 del out[k]
312 else:
313 out[k] = v
314
315 if cipd_info:
316 bin_dir = os.path.dirname(cipd_info.client.binary_path)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500317 out['PATH'] = '%s%s%s' % (_to_str(bin_dir), os.pathsep, out['PATH'])
318 out['CIPD_CACHE_DIR'] = _to_str(cipd_info.cache_dir)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500319
320 for key, paths in env_prefixes.iteritems():
321 paths = [os.path.normpath(os.path.join(cwd, p)) for p in paths]
322 cur = out.get(key)
323 if cur:
324 paths.append(cur)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500325 out[key] = _to_str(os.path.pathsep.join(paths))
vadimsh232f5a82017-01-20 19:23:44 -0800326
iannucciac0342c2017-02-24 05:47:01 -0800327 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
iannucci460def72017-02-24 10:49:48 -0800328 # * mktemp on linux respects $TMPDIR, not $TMP
329 # * mktemp on OS X SOMETIMES respects $TMPDIR
iannucciac0342c2017-02-24 05:47:01 -0800330 # * chromium's base utils respects $TMPDIR on linux, $TEMP on windows.
331 # Unfortunately at the time of writing it completely ignores all envvars
332 # on OS X.
iannucci460def72017-02-24 10:49:48 -0800333 # * python respects TMPDIR, TEMP, and TMP (regardless of platform)
334 # * golang respects TMPDIR on linux+mac, TEMP on windows.
iannucciac0342c2017-02-24 05:47:01 -0800335 key = {'win32': 'TEMP'}.get(sys.platform, 'TMPDIR')
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500336 out[key] = _to_str(tmp_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800337
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500338 return out
vadimsh232f5a82017-01-20 19:23:44 -0800339
340
341def run_command(command, cwd, env, hard_timeout, grace_period):
maruel6be7f9e2015-10-01 12:25:30 -0700342 """Runs the command.
343
344 Returns:
345 tuple(process exit code, bool if had a hard timeout)
346 """
maruela9cfd6f2015-09-15 11:03:15 -0700347 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700348
maruel6be7f9e2015-10-01 12:25:30 -0700349 exit_code = None
350 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700351 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700352 proc = None
353 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700354 try:
maruel6be7f9e2015-10-01 12:25:30 -0700355 # TODO(maruel): This code is imperfect. It doesn't handle well signals
356 # during the download phase and there's short windows were things can go
357 # wrong.
358 def handler(signum, _frame):
359 if proc and not had_signal:
360 logging.info('Received signal %d', signum)
361 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700362 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700363
364 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
365 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
366 try:
367 exit_code = proc.wait(hard_timeout or None)
368 except subprocess42.TimeoutExpired:
369 if not had_signal:
370 logging.warning('Hard timeout')
371 had_hard_timeout = True
372 logging.warning('Sending SIGTERM')
373 proc.terminate()
374
375 # Ignore signals in grace period. Forcibly give the grace period to the
376 # child process.
377 if exit_code is None:
378 ignore = lambda *_: None
379 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
380 try:
381 exit_code = proc.wait(grace_period or None)
382 except subprocess42.TimeoutExpired:
383 # Now kill for real. The user can distinguish between the
384 # following states:
385 # - signal but process exited within grace period,
386 # hard_timed_out will be set but the process exit code will be
387 # script provided.
388 # - processed exited late, exit code will be -9 on posix.
389 logging.warning('Grace exhausted; sending SIGKILL')
390 proc.kill()
martiniss5c8043e2017-08-01 17:09:43 -0700391 logging.info('Waiting for process exit')
maruel6be7f9e2015-10-01 12:25:30 -0700392 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700393 except OSError:
394 # This is not considered to be an internal error. The executable simply
395 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800396 sys.stderr.write(
397 '<The executable does not exist or a dependent library is missing>\n'
398 '<Check for missing .so/.dll in the .isolate or GN file>\n'
399 '<Command: %s>\n' % command)
400 if os.environ.get('SWARMING_TASK_ID'):
401 # Give an additional hint when running as a swarming task.
402 sys.stderr.write(
403 '<See the task\'s page for commands to help diagnose this issue '
404 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700405 exit_code = 1
406 logging.info(
407 'Command finished with exit code %d (%s)',
408 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700409 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700410
411
maruel4409e302016-07-19 14:25:51 -0700412def fetch_and_map(isolated_hash, storage, cache, outdir, use_symlinks):
413 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700414 start = time.time()
415 bundle = isolateserver.fetch_isolated(
416 isolated_hash=isolated_hash,
417 storage=storage,
418 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700419 outdir=outdir,
420 use_symlinks=use_symlinks)
nodir6f801882016-04-29 14:41:50 -0700421 return bundle, {
422 'duration': time.time() - start,
423 'initial_number_items': cache.initial_number_items,
424 'initial_size': cache.initial_size,
425 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
426 'items_hot': base64.b64encode(
tansell9e04a8d2016-07-28 09:31:59 -0700427 large.pack(sorted(set(cache.used) - set(cache.added)))),
nodir6f801882016-04-29 14:41:50 -0700428 }
429
430
aludwin0a8e17d2016-10-27 15:57:39 -0700431def link_outputs_to_outdir(run_dir, out_dir, outputs):
432 """Links any named outputs to out_dir so they can be uploaded.
433
434 Raises an error if the file already exists in that directory.
435 """
436 if not outputs:
437 return
438 isolateserver.create_directories(out_dir, outputs)
439 for o in outputs:
440 try:
aludwinf31ab802017-06-12 06:03:00 -0700441 infile = os.path.join(run_dir, o)
442 outfile = os.path.join(out_dir, o)
443 if fs.islink(infile):
444 # TODO(aludwin): handle directories
445 fs.copy2(infile, outfile)
446 else:
447 file_path.link_file(outfile, infile, file_path.HARDLINK_WITH_FALLBACK)
aludwin0a8e17d2016-10-27 15:57:39 -0700448 except OSError as e:
aludwin81178302016-11-30 17:18:49 -0800449 logging.info("Couldn't collect output file %s: %s", o, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700450
451
maruela9cfd6f2015-09-15 11:03:15 -0700452def delete_and_upload(storage, out_dir, leak_temp_dir):
453 """Deletes the temporary run directory and uploads results back.
454
455 Returns:
nodir6f801882016-04-29 14:41:50 -0700456 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700457 - outputs_ref: a dict referring to the results archived back to the isolated
458 server, if applicable.
459 - success: False if something occurred that means that the task must
460 forcibly be considered a failure, e.g. zombie processes were left
461 behind.
nodir6f801882016-04-29 14:41:50 -0700462 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700463 """
maruela9cfd6f2015-09-15 11:03:15 -0700464 # Upload out_dir and generate a .isolated file out of this directory. It is
465 # only done if files were written in the directory.
466 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700467 cold = []
468 hot = []
nodir6f801882016-04-29 14:41:50 -0700469 start = time.time()
470
maruel12e30012015-10-09 11:55:35 -0700471 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700472 with tools.Profiler('ArchiveOutput'):
473 try:
maruel064c0a32016-04-05 11:47:15 -0700474 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700475 storage, [out_dir], None)
476 outputs_ref = {
477 'isolated': results[0][0],
478 'isolatedserver': storage.location,
479 'namespace': storage.namespace,
480 }
maruel064c0a32016-04-05 11:47:15 -0700481 cold = sorted(i.size for i in f_cold)
482 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700483 except isolateserver.Aborted:
484 # This happens when a signal SIGTERM was received while uploading data.
485 # There is 2 causes:
486 # - The task was too slow and was about to be killed anyway due to
487 # exceeding the hard timeout.
488 # - The amount of data uploaded back is very large and took too much
489 # time to archive.
490 sys.stderr.write('Received SIGTERM while uploading')
491 # Re-raise, so it will be treated as an internal failure.
492 raise
nodir6f801882016-04-29 14:41:50 -0700493
494 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700495 try:
maruel12e30012015-10-09 11:55:35 -0700496 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700497 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700498 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700499 else:
500 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700501 except OSError as e:
502 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700503 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700504 stats = {
505 'duration': time.time() - start,
506 'items_cold': base64.b64encode(large.pack(cold)),
507 'items_hot': base64.b64encode(large.pack(hot)),
508 }
509 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700510
511
marueleb5fbee2015-09-17 13:01:36 -0700512def map_and_run(
nodir0ae98b32017-05-11 13:21:53 -0700513 command, isolated_hash, storage, isolate_cache, outputs,
514 install_named_caches, leak_temp_dir, root_dir, hard_timeout, grace_period,
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -0700515 bot_file, switch_to_account, install_packages_fn, use_symlinks, raw_cmd,
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500516 env, env_prefixes, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700517 """Runs a command with optional isolated input/output.
518
519 See run_tha_test for argument documentation.
520
521 Returns metadata about the result.
522 """
maruelabec63c2017-04-26 11:53:24 -0700523 assert isinstance(command, list), command
nodir56efa452016-10-12 12:17:39 -0700524 assert root_dir or root_dir is None
maruela9cfd6f2015-09-15 11:03:15 -0700525 result = {
maruel064c0a32016-04-05 11:47:15 -0700526 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700527 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700528 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700529 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700530 'stats': {
nodir55715712016-06-03 12:28:19 -0700531 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700532 # 'cipd': {
533 # 'duration': 0.,
534 # 'get_client_duration': 0.,
535 # },
nodir55715712016-06-03 12:28:19 -0700536 # 'download': {
537 # 'duration': 0.,
538 # 'initial_number_items': 0,
539 # 'initial_size': 0,
540 # 'items_cold': '<large.pack()>',
541 # 'items_hot': '<large.pack()>',
542 # },
543 # 'upload': {
544 # 'duration': 0.,
545 # 'items_cold': '<large.pack()>',
546 # 'items_hot': '<large.pack()>',
547 # },
maruel064c0a32016-04-05 11:47:15 -0700548 # },
549 },
iannucci96fcccc2016-08-30 15:52:22 -0700550 # 'cipd_pins': {
551 # 'packages': [
552 # {'package_name': ..., 'version': ..., 'path': ...},
553 # ...
554 # ],
555 # 'client_package': {'package_name': ..., 'version': ...},
556 # },
maruela9cfd6f2015-09-15 11:03:15 -0700557 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700558 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700559 }
nodirbe642ff2016-06-09 15:51:51 -0700560
marueleb5fbee2015-09-17 13:01:36 -0700561 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700562 file_path.ensure_tree(root_dir, 0700)
nodir56efa452016-10-12 12:17:39 -0700563 elif isolate_cache.cache_dir:
564 root_dir = os.path.dirname(isolate_cache.cache_dir)
maruele2f2cb82016-07-13 14:41:03 -0700565 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700566 # If root_dir is not specified, it is not constant.
567 # TODO(maruel): This is not obvious. Change this to become an error once we
568 # make the constant_run_path an exposed flag.
569 if constant_run_path and root_dir:
570 run_dir = os.path.join(root_dir, ISOLATED_RUN_DIR)
maruel5c4eed82017-05-26 05:33:40 -0700571 if os.path.isdir(run_dir):
572 file_path.rmtree(run_dir)
maruelcffa0542017-04-07 08:39:20 -0700573 os.mkdir(run_dir)
574 else:
575 run_dir = make_temp_dir(ISOLATED_RUN_DIR, root_dir)
maruel03e11842016-07-14 10:50:16 -0700576 # storage should be normally set but don't crash if it is not. This can happen
577 # as Swarming task can run without an isolate server.
maruele2f2cb82016-07-13 14:41:03 -0700578 out_dir = make_temp_dir(ISOLATED_OUT_DIR, root_dir) if storage else None
579 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, root_dir)
nodir55be77b2016-05-03 09:39:57 -0700580 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700581
nodir55be77b2016-05-03 09:39:57 -0700582 try:
vadimsh232f5a82017-01-20 19:23:44 -0800583 with install_packages_fn(run_dir) as cipd_info:
584 if cipd_info:
585 result['stats']['cipd'] = cipd_info.stats
586 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700587
vadimsh232f5a82017-01-20 19:23:44 -0800588 if isolated_hash:
589 isolated_stats = result['stats'].setdefault('isolated', {})
590 bundle, isolated_stats['download'] = fetch_and_map(
591 isolated_hash=isolated_hash,
592 storage=storage,
593 cache=isolate_cache,
594 outdir=run_dir,
595 use_symlinks=use_symlinks)
vadimsh232f5a82017-01-20 19:23:44 -0800596 change_tree_read_only(run_dir, bundle.read_only)
maruelabec63c2017-04-26 11:53:24 -0700597 # Inject the command
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -0700598 if not raw_cmd and bundle.command:
maruelabec63c2017-04-26 11:53:24 -0700599 command = bundle.command + command
Marc-Antoine Rueld704a1f2017-10-31 10:51:23 -0400600 # Only set the relative directory if the isolated file specified a
601 # command, and no raw command was specified.
602 if bundle.relative_cwd:
603 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
maruelabec63c2017-04-26 11:53:24 -0700604
605 if not command:
606 # Handle this as a task failure, not an internal failure.
607 sys.stderr.write(
608 '<No command was specified!>\n'
609 '<Please secify a command when triggering your Swarming task>\n')
610 result['exit_code'] = 1
611 return result
nodirbe642ff2016-06-09 15:51:51 -0700612
vadimsh232f5a82017-01-20 19:23:44 -0800613 # If we have an explicit list of files to return, make sure their
614 # directories exist now.
615 if storage and outputs:
616 isolateserver.create_directories(run_dir, outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700617
vadimsh232f5a82017-01-20 19:23:44 -0800618 command = tools.fix_python_path(command)
619 command = process_command(command, out_dir, bot_file)
620 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700621
nodir0ae98b32017-05-11 13:21:53 -0700622 with install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -0800623 sys.stdout.flush()
624 start = time.time()
625 try:
vadimsh9c54b2c2017-07-25 14:08:29 -0700626 # Need to switch the default account before 'get_command_env' call,
627 # so it can grab correct value of LUCI_CONTEXT env var.
628 with set_luci_context_account(switch_to_account, tmp_dir):
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500629 env = get_command_env(tmp_dir, cipd_info, cwd, env, env_prefixes)
vadimsh9c54b2c2017-07-25 14:08:29 -0700630 result['exit_code'], result['had_hard_timeout'] = run_command(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800631 command, cwd, env, hard_timeout, grace_period)
nodird6160682017-02-02 13:03:35 -0800632 finally:
633 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700634 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700635 # An internal error occurred. Report accordingly so the swarming task will
636 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700637 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700638 result['internal_failure'] = str(e)
639 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700640
641 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700642 finally:
643 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700644 # Try to link files to the output directory, if specified.
645 if out_dir:
646 link_outputs_to_outdir(run_dir, out_dir, outputs)
647
nodir32a1ec12016-10-26 18:34:07 -0700648 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700649 if leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700650 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700651 logging.warning(
652 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700653 else:
maruel84537cb2015-10-16 14:21:28 -0700654 # On Windows rmtree(run_dir) call above has a synchronization effect: it
655 # finishes only when all task child processes terminate (since a running
656 # process locks *.exe file). Examine out_dir only after that call
657 # completes (since child processes may write to out_dir too and we need
658 # to wait for them to finish).
659 if fs.isdir(run_dir):
660 try:
661 success = file_path.rmtree(run_dir)
662 except OSError as e:
663 logging.error('Failure with %s', e)
664 success = False
665 if not success:
marueld928c862017-06-08 08:20:04 -0700666 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('run', grace_period))
maruel84537cb2015-10-16 14:21:28 -0700667 if result['exit_code'] == 0:
668 result['exit_code'] = 1
669 if fs.isdir(tmp_dir):
670 try:
671 success = file_path.rmtree(tmp_dir)
672 except OSError as e:
673 logging.error('Failure with %s', e)
674 success = False
675 if not success:
maruelca2a38c2017-06-08 13:06:40 -0700676 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('temp', grace_period))
maruel84537cb2015-10-16 14:21:28 -0700677 if result['exit_code'] == 0:
678 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700679
marueleb5fbee2015-09-17 13:01:36 -0700680 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700681 if out_dir:
nodir55715712016-06-03 12:28:19 -0700682 isolated_stats = result['stats'].setdefault('isolated', {})
683 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700684 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700685 if not success and result['exit_code'] == 0:
686 result['exit_code'] = 1
687 except Exception as e:
688 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700689 if out_dir:
690 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700691 result['internal_failure'] = str(e)
692 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500693
694
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400695def run_tha_test(
nodir0ae98b32017-05-11 13:21:53 -0700696 command, isolated_hash, storage, isolate_cache, outputs,
697 install_named_caches, leak_temp_dir, result_json, root_dir, hard_timeout,
vadimsh9c54b2c2017-07-25 14:08:29 -0700698 grace_period, bot_file, switch_to_account, install_packages_fn,
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500699 use_symlinks, raw_cmd, env, env_prefixes):
nodir55be77b2016-05-03 09:39:57 -0700700 """Runs an executable and records execution metadata.
701
702 Either command or isolated_hash must be specified.
703
704 If isolated_hash is specified, downloads the dependencies in the cache,
705 hardlinks them into a temporary directory and runs the command specified in
706 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500707
708 A temporary directory is created to hold the output files. The content inside
709 this directory will be uploaded back to |storage| packaged as a .isolated
710 file.
711
712 Arguments:
maruelabec63c2017-04-26 11:53:24 -0700713 command: a list of string; the command to run OR optional arguments to add
714 to the command stated in the .isolated file if a command was
715 specified.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500716 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500717 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700718 The command specified in the .isolated is executed.
719 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500720 storage: an isolateserver.Storage object to retrieve remote objects. This
721 object has a reference to an isolateserver.StorageApi, which does
722 the actual I/O.
nodir6b945692016-10-19 19:09:06 -0700723 isolate_cache: an isolateserver.LocalCache to keep from retrieving the
724 same objects constantly by caching the objects retrieved.
725 Can be on-disk or in-memory.
vadimsh9c54b2c2017-07-25 14:08:29 -0700726 outputs: list of paths relative to root_dir to put into the output isolated
727 bundle upon task completion (see link_outputs_to_outdir).
nodir0ae98b32017-05-11 13:21:53 -0700728 install_named_caches: a function (run_dir) => context manager that installs
vadimsh9c54b2c2017-07-25 14:08:29 -0700729 named caches into |run_dir|.
Kenneth Russell61d42352014-09-15 11:41:16 -0700730 leak_temp_dir: if true, the temporary directory will be deliberately leaked
731 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700732 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700733 exit code is always 0 unless an internal error occurred.
nodir90bc8dc2016-06-15 13:35:21 -0700734 root_dir: path to the directory to use to create the temporary directory. If
marueleb5fbee2015-09-17 13:01:36 -0700735 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700736 hard_timeout: kills the process if it lasts more than this amount of
737 seconds.
738 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
vadimsh9c54b2c2017-07-25 14:08:29 -0700739 bot_file: path to a file with bot state, used in place of
740 ${SWARMING_BOT_FILE} task command line argument.
741 switch_to_account: a logical account to switch LUCI_CONTEXT into.
iannuccib58d10d2017-03-18 02:00:25 -0700742 install_packages_fn: context manager dir => CipdInfo, see
vadimsh9c54b2c2017-07-25 14:08:29 -0700743 install_client_and_packages.
maruel4409e302016-07-19 14:25:51 -0700744 use_symlinks: create tree with symlinks instead of hardlinks.
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -0700745 raw_cmd: ignore the command in the isolated file.
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500746 env: environment variables to set
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800747 env_prefixes: {"ENV_KEY": ['relative', 'paths', 'to', 'prepend']}
maruela9cfd6f2015-09-15 11:03:15 -0700748
749 Returns:
750 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000751 """
maruela76b9ee2015-12-15 06:18:08 -0800752 if result_json:
753 # Write a json output file right away in case we get killed.
754 result = {
755 'exit_code': None,
756 'had_hard_timeout': False,
757 'internal_failure': 'Was terminated before completion',
758 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700759 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800760 }
761 tools.write_json(result_json, result, dense=True)
762
maruela9cfd6f2015-09-15 11:03:15 -0700763 # run_isolated exit code. Depends on if result_json is used or not.
764 result = map_and_run(
nodir220308c2017-02-01 19:32:53 -0800765 command, isolated_hash, storage, isolate_cache, outputs,
nodir0ae98b32017-05-11 13:21:53 -0700766 install_named_caches, leak_temp_dir, root_dir, hard_timeout, grace_period,
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -0700767 bot_file, switch_to_account, install_packages_fn, use_symlinks, raw_cmd,
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500768 env, env_prefixes, True)
maruela9cfd6f2015-09-15 11:03:15 -0700769 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700770
maruela9cfd6f2015-09-15 11:03:15 -0700771 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700772 # We've found tests to delete 'work' when quitting, causing an exception
773 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700774 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700775 tools.write_json(result_json, result, dense=True)
776 # Only return 1 if there was an internal error.
777 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000778
maruela9cfd6f2015-09-15 11:03:15 -0700779 # Marshall into old-style inline output.
780 if result['outputs_ref']:
781 data = {
782 'hash': result['outputs_ref']['isolated'],
783 'namespace': result['outputs_ref']['namespace'],
784 'storage': result['outputs_ref']['isolatedserver'],
785 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500786 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700787 print(
788 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
789 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800790 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700791 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000792
793
iannuccib58d10d2017-03-18 02:00:25 -0700794# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800795CipdInfo = collections.namedtuple('CipdInfo', [
796 'client', # cipd.CipdClient object
797 'cache_dir', # absolute path to bot-global cipd tag and instance cache
798 'stats', # dict with stats to return to the server
799 'pins', # dict with installed cipd pins to return to the server
800])
801
802
803@contextlib.contextmanager
804def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700805 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800806 yield None
807
808
iannuccib58d10d2017-03-18 02:00:25 -0700809def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
810 """Calls 'cipd ensure' for packages.
811
812 Args:
813 run_dir (str): root of installation.
814 cipd_cache_dir (str): the directory to use for the cipd package cache.
815 client (CipdClient): the cipd client to use
816 packages: packages to install, list [(path, package_name, version), ...].
817 timeout: max duration in seconds that this function can take.
818
819 Returns: list of pinned packages. Looks like [
820 {
821 'path': 'subdirectory',
822 'package_name': 'resolved/package/name',
823 'version': 'deadbeef...',
824 },
825 ...
826 ]
827 """
828 package_pins = [None]*len(packages)
829 def insert_pin(path, name, version, idx):
830 package_pins[idx] = {
831 'package_name': name,
832 # swarming deals with 'root' as '.'
833 'path': path or '.',
834 'version': version,
835 }
836
837 by_path = collections.defaultdict(list)
838 for i, (path, name, version) in enumerate(packages):
839 # cipd deals with 'root' as ''
840 if path == '.':
841 path = ''
842 by_path[path].append((name, version, i))
843
844 pins = client.ensure(
845 run_dir,
846 {
847 subdir: [(name, vers) for name, vers, _ in pkgs]
848 for subdir, pkgs in by_path.iteritems()
849 },
850 cache_dir=cipd_cache_dir,
851 timeout=timeout,
852 )
853
854 for subdir, pin_list in sorted(pins.iteritems()):
855 this_subdir = by_path[subdir]
856 for i, (name, version) in enumerate(pin_list):
857 insert_pin(subdir, name, version, this_subdir[i][2])
858
859 assert None not in package_pins
860
861 return package_pins
862
863
vadimsh232f5a82017-01-20 19:23:44 -0800864@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700865def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700866 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800867 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800868 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700869
vadimsh232f5a82017-01-20 19:23:44 -0800870 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
871
872 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700873 [
874 {
875 "path": path, "package_name": package_name, "version": version,
876 },
877 ...
878 ]
vadimsh902948e2017-01-20 15:57:32 -0800879 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700880
881 such that they correspond 1:1 to all input package arguments from the command
882 line. These dictionaries make their all the way back to swarming, where they
883 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700884
vadimsh902948e2017-01-20 15:57:32 -0800885 If 'packages' list is empty, will bootstrap CIPD client, but won't install
886 any packages.
887
888 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800889 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800890
nodirbe642ff2016-06-09 15:51:51 -0700891 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700892 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800893 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700894 service_url (str): CIPD server url, e.g.
895 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700896 client_package_name (str): CIPD package name of CIPD client.
897 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700898 cache_dir (str): where to keep cache of cipd clients, packages and tags.
899 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700900 """
901 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700902
nodirbe642ff2016-06-09 15:51:51 -0700903 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700904 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700905
vadimsh902948e2017-01-20 15:57:32 -0800906 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800907 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700908 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800909 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700910
nodirbe642ff2016-06-09 15:51:51 -0700911 get_client_start = time.time()
912 client_manager = cipd.get_client(
913 service_url, client_package_name, client_version, cache_dir,
914 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700915
nodirbe642ff2016-06-09 15:51:51 -0700916 with client_manager as client:
917 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700918
iannuccib58d10d2017-03-18 02:00:25 -0700919 package_pins = []
920 if packages:
921 package_pins = _install_packages(
922 run_dir, cipd_cache_dir, client, packages, timeoutfn())
923
924 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700925
vadimsh232f5a82017-01-20 19:23:44 -0800926 total_duration = time.time() - start
927 logging.info(
928 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700929
vadimsh232f5a82017-01-20 19:23:44 -0800930 yield CipdInfo(
931 client=client,
932 cache_dir=cipd_cache_dir,
933 stats={
934 'duration': total_duration,
935 'get_client_duration': get_client_duration,
936 },
937 pins={
iannuccib58d10d2017-03-18 02:00:25 -0700938 'client_package': {
939 'package_name': client.package_name,
940 'version': client.instance_id,
941 },
vadimsh232f5a82017-01-20 19:23:44 -0800942 'packages': package_pins,
943 })
nodirbe642ff2016-06-09 15:51:51 -0700944
945
nodirf33b8d62016-10-26 22:34:58 -0700946def clean_caches(options, isolate_cache, named_cache_manager):
maruele6fc9382017-05-04 09:03:48 -0700947 """Trims isolated and named caches.
948
949 The goal here is to coherently trim both caches, deleting older items
950 independent of which container they belong to.
951 """
952 # TODO(maruel): Trim CIPD cache the same way.
953 total = 0
nodirf33b8d62016-10-26 22:34:58 -0700954 with named_cache_manager.open():
955 oldest_isolated = isolate_cache.get_oldest()
956 oldest_named = named_cache_manager.get_oldest()
957 trimmers = [
958 (
959 isolate_cache.trim,
960 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
961 ),
962 (
963 lambda: named_cache_manager.trim(options.min_free_space),
964 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
965 ),
966 ]
967 trimmers.sort(key=lambda (_, ts): ts)
maruele6fc9382017-05-04 09:03:48 -0700968 # TODO(maruel): This is incorrect, we want to trim 'items' that are strictly
969 # the oldest independent of in which cache they live in. Right now, the
970 # cache with the oldest item pays the price.
nodirf33b8d62016-10-26 22:34:58 -0700971 for trim, _ in trimmers:
maruele6fc9382017-05-04 09:03:48 -0700972 total += trim()
nodirf33b8d62016-10-26 22:34:58 -0700973 isolate_cache.cleanup()
maruele6fc9382017-05-04 09:03:48 -0700974 return total
nodirf33b8d62016-10-26 22:34:58 -0700975
976
nodirbe642ff2016-06-09 15:51:51 -0700977def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400978 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700979 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000980 version=__version__,
981 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700982 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700983 '--clean', action='store_true',
984 help='Cleans the cache, trimming it necessary and remove corrupted items '
985 'and returns without executing anything; use with -v to know what '
986 'was done')
987 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -0700988 '--no-clean', action='store_true',
989 help='Do not clean the cache automatically on startup. This is meant for '
990 'bots where a separate execution with --clean was done earlier so '
991 'doing it again is redundant')
992 parser.add_option(
maruel4409e302016-07-19 14:25:51 -0700993 '--use-symlinks', action='store_true',
994 help='Use symlinks instead of hardlinks')
995 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700996 '--json',
997 help='dump output metadata to json file. When used, run_isolated returns '
998 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700999 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001000 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -07001001 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001002 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -07001003 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -07001004 parser.add_option(
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001005 '--raw-cmd', action='store_true',
1006 help='Ignore the isolated command, use the one supplied at the command '
1007 'line')
1008 parser.add_option(
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001009 '--env', default=[], action='append',
1010 help='Environment variables to set for the child process')
1011 parser.add_option(
1012 '--env-prefix', default=[], action='append',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001013 help='Specify a VAR=./path/fragment to put in the environment variable '
1014 'before executing the command. The path fragment must be relative '
1015 'to the isolated run directory, and must not contain a `..` token. '
1016 'The path will be made absolute and prepended to the indicated '
1017 '$VAR using the OS\'s path separator. Multiple items for the same '
1018 '$VAR will be prepended in order.')
1019 parser.add_option(
bpastene3ae09522016-06-10 17:12:59 -07001020 '--bot-file',
1021 help='Path to a file describing the state of the host. The content is '
1022 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -07001023 parser.add_option(
vadimsh9c54b2c2017-07-25 14:08:29 -07001024 '--switch-to-account',
1025 help='If given, switches LUCI_CONTEXT to given logical service account '
1026 '(e.g. "task" or "system") before launching the isolated process.')
1027 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -07001028 '--output', action='append',
1029 help='Specifies an output to return. If no outputs are specified, all '
1030 'files located in $(ISOLATED_OUTDIR) will be returned; '
1031 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
1032 'specified by --output option (there can be multiple) will be '
1033 'returned. Note that if a file in OUT_DIR has the same path '
1034 'as an --output option, the --output version will be returned.')
1035 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -07001036 '-a', '--argsfile',
1037 # This is actually handled in parse_args; it's included here purely so it
1038 # can make it into the help text.
1039 help='Specify a file containing a JSON array of arguments to this '
1040 'script. If --argsfile is provided, no other argument may be '
1041 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001042 data_group = optparse.OptionGroup(parser, 'Data source')
1043 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001044 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -07001045 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001046 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001047 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001048
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -04001049 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001050
1051 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -07001052 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001053
Kenneth Russell61d42352014-09-15 11:41:16 -07001054 debug_group = optparse.OptionGroup(parser, 'Debugging')
1055 debug_group.add_option(
1056 '--leak-temp-dir',
1057 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -07001058 help='Deliberately leak isolate\'s temp dir for later examination. '
1059 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -07001060 debug_group.add_option(
1061 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -07001062 parser.add_option_group(debug_group)
1063
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001064 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001065
nodirf33b8d62016-10-26 22:34:58 -07001066 parser.set_defaults(
1067 cache='cache',
1068 cipd_cache='cipd_cache',
1069 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -07001070 return parser
1071
1072
aludwin7556e0c2016-10-26 08:46:10 -07001073def parse_args(args):
1074 # Create a fake mini-parser just to get out the "-a" command. Note that
1075 # it's not documented here; instead, it's documented in create_option_parser
1076 # even though that parser will never actually get to parse it. This is
1077 # because --argsfile is exclusive with all other options and arguments.
1078 file_argparse = argparse.ArgumentParser(add_help=False)
1079 file_argparse.add_argument('-a', '--argsfile')
1080 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
1081 if file_args.argsfile:
1082 if nonfile_args:
1083 file_argparse.error('Can\'t specify --argsfile with'
1084 'any other arguments (%s)' % nonfile_args)
1085 try:
1086 with open(file_args.argsfile, 'r') as f:
1087 args = json.load(f)
1088 except (IOError, OSError, ValueError) as e:
1089 # We don't need to error out here - "args" is now empty,
1090 # so the call below to parser.parse_args(args) will fail
1091 # and print the full help text.
1092 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
1093
1094 # Even if we failed to read the args, just call the normal parser now since it
1095 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -07001096 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001097 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -07001098 return (parser, options, args)
1099
1100
1101def main(args):
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -05001102 # Warning: when --argsfile is used, the strings are unicode instances, when
1103 # parsed normally, the strings are str instances.
aludwin7556e0c2016-10-26 08:46:10 -07001104 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -07001105
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001106 if not file_path.enable_symlink():
1107 logging.error('Symlink support is not enabled')
1108
nodirf33b8d62016-10-26 22:34:58 -07001109 isolate_cache = isolateserver.process_cache_options(options, trim=False)
1110 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -07001111 if options.clean:
1112 if options.isolated:
1113 parser.error('Can\'t use --isolated with --clean.')
1114 if options.isolate_server:
1115 parser.error('Can\'t use --isolate-server with --clean.')
1116 if options.json:
1117 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -07001118 if options.named_caches:
1119 parser.error('Can\t use --named-cache with --clean.')
1120 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001121 return 0
nodirf33b8d62016-10-26 22:34:58 -07001122
maruel2e8d0f52016-07-16 07:51:29 -07001123 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -07001124 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001125
nodir55be77b2016-05-03 09:39:57 -07001126 if not options.isolated and not args:
1127 parser.error('--isolated or command to run is required.')
1128
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001129 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001130
1131 isolateserver.process_isolate_server_options(
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001132 parser, options, True, False)
nodir55be77b2016-05-03 09:39:57 -07001133 if not options.isolate_server:
1134 if options.isolated:
1135 parser.error('--isolated requires --isolate-server')
1136 if ISOLATED_OUTDIR_PARAMETER in args:
1137 parser.error(
1138 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001139
nodir90bc8dc2016-06-15 13:35:21 -07001140 if options.root_dir:
1141 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -07001142 if options.json:
1143 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001144
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001145 if any('=' not in i for i in options.env):
1146 parser.error(
1147 '--env required key=value form. value can be skipped to delete '
1148 'the variable')
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -05001149 # Take the occasion to assert that everything is converted to str instances,
1150 # subprocess.Popen doesn't like unicode.
1151 options.env = {
1152 _to_str(k): _to_str(v) for k, v in (i.split('=', 1) for i in options.env)
1153 }
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001154
1155 prefixes = {}
1156 cwd = os.path.realpath(os.getcwd())
1157 for item in options.env_prefix:
1158 if '=' not in item:
1159 parser.error(
1160 '--env-prefix %r is malformed, must be in the form `VAR=./path`'
1161 % item)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -05001162 # Downgrade to str as for env above.
1163 key, opath = _to_str(item).split('=', 1)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001164 if os.path.isabs(opath):
1165 parser.error('--env-prefix %r path is bad, must be relative.' % opath)
1166 opath = os.path.normpath(opath)
1167 if not os.path.realpath(os.path.join(cwd, opath)).startswith(cwd):
1168 parser.error(
1169 '--env-prefix %r path is bad, must be relative and not contain `..`.'
1170 % opath)
1171 prefixes.setdefault(key, []).append(opath)
1172 options.env_prefix = prefixes
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001173
nodirbe642ff2016-06-09 15:51:51 -07001174 cipd.validate_cipd_options(parser, options)
1175
vadimsh232f5a82017-01-20 19:23:44 -08001176 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -08001177 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -07001178 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -08001179 run_dir, cipd.parse_package_args(options.cipd_packages),
1180 options.cipd_server, options.cipd_client_package,
1181 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001182
nodird6160682017-02-02 13:03:35 -08001183 @contextlib.contextmanager
nodir0ae98b32017-05-11 13:21:53 -07001184 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001185 # WARNING: this function depends on "options" variable defined in the outer
1186 # function.
nodir0ae98b32017-05-11 13:21:53 -07001187 caches = [
1188 (os.path.join(run_dir, unicode(relpath)), name)
1189 for name, relpath in options.named_caches
1190 ]
nodirf33b8d62016-10-26 22:34:58 -07001191 with named_cache_manager.open():
nodir0ae98b32017-05-11 13:21:53 -07001192 for path, name in caches:
1193 named_cache_manager.install(path, name)
nodird6160682017-02-02 13:03:35 -08001194 try:
1195 yield
1196 finally:
dnje289d132017-07-07 11:16:44 -07001197 # Uninstall each named cache, returning it to the cache pool. If an
1198 # uninstall fails for a given cache, it will remain in the task's
1199 # temporary space, get cleaned up by the Swarming bot, and be lost.
1200 #
1201 # If the Swarming bot cannot clean up the cache, it will handle it like
1202 # any other bot file that could not be removed.
nodir0ae98b32017-05-11 13:21:53 -07001203 with named_cache_manager.open():
1204 for path, name in caches:
dnje289d132017-07-07 11:16:44 -07001205 try:
1206 named_cache_manager.uninstall(path, name)
1207 except named_cache.Error:
1208 logging.exception('Error while removing named cache %r at %r. '
1209 'The cache will be lost.', path, name)
nodirf33b8d62016-10-26 22:34:58 -07001210
nodirbe642ff2016-06-09 15:51:51 -07001211 try:
nodir90bc8dc2016-06-15 13:35:21 -07001212 if options.isolate_server:
1213 storage = isolateserver.get_storage(
1214 options.isolate_server, options.namespace)
1215 with storage:
nodirf33b8d62016-10-26 22:34:58 -07001216 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
1217 assert storage.hash_algo == isolate_cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -07001218 return run_tha_test(
maruelabec63c2017-04-26 11:53:24 -07001219 args,
nodirf33b8d62016-10-26 22:34:58 -07001220 options.isolated,
1221 storage,
1222 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001223 options.output,
nodir0ae98b32017-05-11 13:21:53 -07001224 install_named_caches,
nodirf33b8d62016-10-26 22:34:58 -07001225 options.leak_temp_dir,
1226 options.json, options.root_dir,
1227 options.hard_timeout,
1228 options.grace_period,
maruelabec63c2017-04-26 11:53:24 -07001229 options.bot_file,
vadimsh9c54b2c2017-07-25 14:08:29 -07001230 options.switch_to_account,
nodirf33b8d62016-10-26 22:34:58 -07001231 install_packages_fn,
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001232 options.use_symlinks,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001233 options.raw_cmd,
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001234 options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001235 options.env_prefix)
maruel4409e302016-07-19 14:25:51 -07001236 return run_tha_test(
maruelabec63c2017-04-26 11:53:24 -07001237 args,
nodirf33b8d62016-10-26 22:34:58 -07001238 options.isolated,
1239 None,
1240 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001241 options.output,
nodir0ae98b32017-05-11 13:21:53 -07001242 install_named_caches,
nodirf33b8d62016-10-26 22:34:58 -07001243 options.leak_temp_dir,
1244 options.json,
1245 options.root_dir,
1246 options.hard_timeout,
1247 options.grace_period,
maruelabec63c2017-04-26 11:53:24 -07001248 options.bot_file,
vadimsh9c54b2c2017-07-25 14:08:29 -07001249 options.switch_to_account,
nodirf33b8d62016-10-26 22:34:58 -07001250 install_packages_fn,
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001251 options.use_symlinks,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001252 options.raw_cmd,
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001253 options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001254 options.env_prefix)
nodirf33b8d62016-10-26 22:34:58 -07001255 except (cipd.Error, named_cache.Error) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001256 print >> sys.stderr, ex.message
1257 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001258
1259
1260if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001261 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001262 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001263 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001264 sys.exit(main(sys.argv[1:]))