blob: 875275797689413dcbc5d93970276b1c64919bde [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
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -0500181def _to_unicode(s):
182 """Upgrades a str instance to unicode. Pass unicode through as-is."""
183 if isinstance(s, unicode) or s is None:
184 return s
185 return s.decode('utf-8')
186
187
maruel03e11842016-07-14 10:50:16 -0700188def make_temp_dir(prefix, root_dir):
189 """Returns a new unique temporary directory."""
190 return unicode(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000191
192
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500193def change_tree_read_only(rootdir, read_only):
194 """Changes the tree read-only bits according to the read_only specification.
195
196 The flag can be 0, 1 or 2, which will affect the possibility to modify files
197 and create or delete files.
198 """
199 if read_only == 2:
200 # Files and directories (except on Windows) are marked read only. This
201 # inhibits modifying, creating or deleting files in the test directory,
202 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400203 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500204 elif read_only == 1:
205 # Files are marked read only but not the directories. This inhibits
206 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400207 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500208 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500209 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500210 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
211 # is not yet changed to verify the hash of the content of the files it is
212 # looking at, so that if a test modifies an input file, the file must be
213 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400214 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500215 else:
216 raise ValueError(
217 'change_tree_read_only(%s, %s): Unknown flag %s' %
218 (rootdir, read_only, read_only))
219
220
vadimsh9c54b2c2017-07-25 14:08:29 -0700221@contextlib.contextmanager
222def set_luci_context_account(account, tmp_dir):
223 """Sets LUCI_CONTEXT account to be used by the task.
224
225 If 'account' is None or '', does nothing at all. This happens when
226 run_isolated.py is called without '--switch-to-account' flag. In this case,
227 if run_isolated.py is running in some LUCI_CONTEXT environment, the task will
228 just inherit whatever account is already set. This may happen is users invoke
229 run_isolated.py explicitly from their code.
230
231 If the requested account is not defined in the context, switches to
232 non-authenticated access. This happens for Swarming tasks that don't use
233 'task' service accounts.
234
235 If not using LUCI_CONTEXT-based auth, does nothing.
236 If already running as requested account, does nothing.
237 """
238 if not account:
239 # Not actually switching.
240 yield
241 return
242
243 local_auth = luci_context.read('local_auth')
244 if not local_auth:
245 # Not using LUCI_CONTEXT auth at all.
246 yield
247 return
248
249 # See LUCI_CONTEXT.md for the format of 'local_auth'.
250 if local_auth.get('default_account_id') == account:
251 # Already set, no need to switch.
252 yield
253 return
254
255 available = {a['id'] for a in local_auth.get('accounts') or []}
256 if account in available:
257 logging.info('Switching default LUCI_CONTEXT account to %r', account)
258 local_auth['default_account_id'] = account
259 else:
260 logging.warning(
261 'Requested LUCI_CONTEXT account %r is not available (have only %r), '
262 'disabling authentication', account, sorted(available))
263 local_auth.pop('default_account_id', None)
264
265 with luci_context.write(_tmpdir=tmp_dir, local_auth=local_auth):
266 yield
267
268
nodir90bc8dc2016-06-15 13:35:21 -0700269def process_command(command, out_dir, bot_file):
nodirbe642ff2016-06-09 15:51:51 -0700270 """Replaces variables in a command line.
271
272 Raises:
273 ValueError if a parameter is requested in |command| but its value is not
274 provided.
275 """
maruela9cfd6f2015-09-15 11:03:15 -0700276 def fix(arg):
nodirbe642ff2016-06-09 15:51:51 -0700277 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
278 replace_slash = False
nodir55be77b2016-05-03 09:39:57 -0700279 if ISOLATED_OUTDIR_PARAMETER in arg:
nodirbe642ff2016-06-09 15:51:51 -0700280 if not out_dir:
maruel7f63a272016-07-12 12:40:36 -0700281 raise ValueError(
282 'output directory is requested in command, but not provided; '
283 'please specify one')
nodir55be77b2016-05-03 09:39:57 -0700284 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
nodirbe642ff2016-06-09 15:51:51 -0700285 replace_slash = True
nodir90bc8dc2016-06-15 13:35:21 -0700286 if SWARMING_BOT_FILE_PARAMETER in arg:
287 if bot_file:
288 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
289 replace_slash = True
290 else:
291 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command, but no '
292 'bot_file specified. Leaving parameter unchanged.')
nodirbe642ff2016-06-09 15:51:51 -0700293 if replace_slash:
294 # Replace slashes only if parameters are present
nodir55be77b2016-05-03 09:39:57 -0700295 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
296 arg = arg.replace('/', os.sep)
maruela9cfd6f2015-09-15 11:03:15 -0700297 return arg
298
299 return [fix(arg) for arg in command]
300
301
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500302def get_command_env(tmp_dir, cipd_info, cwd, env, env_prefixes):
vadimsh232f5a82017-01-20 19:23:44 -0800303 """Returns full OS environment to run a command in.
304
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800305 Sets up TEMP, puts directory with cipd binary in front of PATH, exposes
306 CIPD_CACHE_DIR env var, and installs all env_prefixes.
vadimsh232f5a82017-01-20 19:23:44 -0800307
308 Args:
309 tmp_dir: temp directory.
310 cipd_info: CipdInfo object is cipd client is used, None if not.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800311 cwd: The directory the command will run in
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500312 env: environment variables to use
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800313 env_prefixes: {"ENV_KEY": ['cwd', 'relative', 'paths', 'to', 'prepend']}
vadimsh232f5a82017-01-20 19:23:44 -0800314 """
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500315 out = os.environ.copy()
316 for k, v in env.iteritems():
317 if not v:
318 del out[k]
319 else:
320 out[k] = v
321
322 if cipd_info:
323 bin_dir = os.path.dirname(cipd_info.client.binary_path)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500324 out['PATH'] = '%s%s%s' % (_to_str(bin_dir), os.pathsep, out['PATH'])
325 out['CIPD_CACHE_DIR'] = _to_str(cipd_info.cache_dir)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500326
327 for key, paths in env_prefixes.iteritems():
328 paths = [os.path.normpath(os.path.join(cwd, p)) for p in paths]
329 cur = out.get(key)
330 if cur:
331 paths.append(cur)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500332 out[key] = _to_str(os.path.pathsep.join(paths))
vadimsh232f5a82017-01-20 19:23:44 -0800333
iannucciac0342c2017-02-24 05:47:01 -0800334 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
iannucci460def72017-02-24 10:49:48 -0800335 # * mktemp on linux respects $TMPDIR, not $TMP
336 # * mktemp on OS X SOMETIMES respects $TMPDIR
iannucciac0342c2017-02-24 05:47:01 -0800337 # * chromium's base utils respects $TMPDIR on linux, $TEMP on windows.
338 # Unfortunately at the time of writing it completely ignores all envvars
339 # on OS X.
iannucci460def72017-02-24 10:49:48 -0800340 # * python respects TMPDIR, TEMP, and TMP (regardless of platform)
341 # * golang respects TMPDIR on linux+mac, TEMP on windows.
iannucciac0342c2017-02-24 05:47:01 -0800342 key = {'win32': 'TEMP'}.get(sys.platform, 'TMPDIR')
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500343 out[key] = _to_str(tmp_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800344
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500345 return out
vadimsh232f5a82017-01-20 19:23:44 -0800346
347
348def run_command(command, cwd, env, hard_timeout, grace_period):
maruel6be7f9e2015-10-01 12:25:30 -0700349 """Runs the command.
350
351 Returns:
352 tuple(process exit code, bool if had a hard timeout)
353 """
maruela9cfd6f2015-09-15 11:03:15 -0700354 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700355
maruel6be7f9e2015-10-01 12:25:30 -0700356 exit_code = None
357 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700358 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700359 proc = None
360 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700361 try:
maruel6be7f9e2015-10-01 12:25:30 -0700362 # TODO(maruel): This code is imperfect. It doesn't handle well signals
363 # during the download phase and there's short windows were things can go
364 # wrong.
365 def handler(signum, _frame):
366 if proc and not had_signal:
367 logging.info('Received signal %d', signum)
368 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700369 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700370
371 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
372 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
373 try:
374 exit_code = proc.wait(hard_timeout or None)
375 except subprocess42.TimeoutExpired:
376 if not had_signal:
377 logging.warning('Hard timeout')
378 had_hard_timeout = True
379 logging.warning('Sending SIGTERM')
380 proc.terminate()
381
382 # Ignore signals in grace period. Forcibly give the grace period to the
383 # child process.
384 if exit_code is None:
385 ignore = lambda *_: None
386 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
387 try:
388 exit_code = proc.wait(grace_period or None)
389 except subprocess42.TimeoutExpired:
390 # Now kill for real. The user can distinguish between the
391 # following states:
392 # - signal but process exited within grace period,
393 # hard_timed_out will be set but the process exit code will be
394 # script provided.
395 # - processed exited late, exit code will be -9 on posix.
396 logging.warning('Grace exhausted; sending SIGKILL')
397 proc.kill()
martiniss5c8043e2017-08-01 17:09:43 -0700398 logging.info('Waiting for process exit')
maruel6be7f9e2015-10-01 12:25:30 -0700399 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700400 except OSError:
401 # This is not considered to be an internal error. The executable simply
402 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800403 sys.stderr.write(
404 '<The executable does not exist or a dependent library is missing>\n'
405 '<Check for missing .so/.dll in the .isolate or GN file>\n'
406 '<Command: %s>\n' % command)
407 if os.environ.get('SWARMING_TASK_ID'):
408 # Give an additional hint when running as a swarming task.
409 sys.stderr.write(
410 '<See the task\'s page for commands to help diagnose this issue '
411 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700412 exit_code = 1
413 logging.info(
414 'Command finished with exit code %d (%s)',
415 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700416 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700417
418
maruel4409e302016-07-19 14:25:51 -0700419def fetch_and_map(isolated_hash, storage, cache, outdir, use_symlinks):
420 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700421 start = time.time()
422 bundle = isolateserver.fetch_isolated(
423 isolated_hash=isolated_hash,
424 storage=storage,
425 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700426 outdir=outdir,
427 use_symlinks=use_symlinks)
nodir6f801882016-04-29 14:41:50 -0700428 return bundle, {
429 'duration': time.time() - start,
430 'initial_number_items': cache.initial_number_items,
431 'initial_size': cache.initial_size,
432 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
433 'items_hot': base64.b64encode(
tansell9e04a8d2016-07-28 09:31:59 -0700434 large.pack(sorted(set(cache.used) - set(cache.added)))),
nodir6f801882016-04-29 14:41:50 -0700435 }
436
437
aludwin0a8e17d2016-10-27 15:57:39 -0700438def link_outputs_to_outdir(run_dir, out_dir, outputs):
439 """Links any named outputs to out_dir so they can be uploaded.
440
441 Raises an error if the file already exists in that directory.
442 """
443 if not outputs:
444 return
445 isolateserver.create_directories(out_dir, outputs)
446 for o in outputs:
447 try:
aludwinf31ab802017-06-12 06:03:00 -0700448 infile = os.path.join(run_dir, o)
449 outfile = os.path.join(out_dir, o)
450 if fs.islink(infile):
451 # TODO(aludwin): handle directories
452 fs.copy2(infile, outfile)
453 else:
454 file_path.link_file(outfile, infile, file_path.HARDLINK_WITH_FALLBACK)
aludwin0a8e17d2016-10-27 15:57:39 -0700455 except OSError as e:
aludwin81178302016-11-30 17:18:49 -0800456 logging.info("Couldn't collect output file %s: %s", o, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700457
458
maruela9cfd6f2015-09-15 11:03:15 -0700459def delete_and_upload(storage, out_dir, leak_temp_dir):
460 """Deletes the temporary run directory and uploads results back.
461
462 Returns:
nodir6f801882016-04-29 14:41:50 -0700463 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700464 - outputs_ref: a dict referring to the results archived back to the isolated
465 server, if applicable.
466 - success: False if something occurred that means that the task must
467 forcibly be considered a failure, e.g. zombie processes were left
468 behind.
nodir6f801882016-04-29 14:41:50 -0700469 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700470 """
maruela9cfd6f2015-09-15 11:03:15 -0700471 # Upload out_dir and generate a .isolated file out of this directory. It is
472 # only done if files were written in the directory.
473 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700474 cold = []
475 hot = []
nodir6f801882016-04-29 14:41:50 -0700476 start = time.time()
477
maruel12e30012015-10-09 11:55:35 -0700478 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700479 with tools.Profiler('ArchiveOutput'):
480 try:
maruel064c0a32016-04-05 11:47:15 -0700481 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700482 storage, [out_dir], None)
483 outputs_ref = {
484 'isolated': results[0][0],
485 'isolatedserver': storage.location,
486 'namespace': storage.namespace,
487 }
maruel064c0a32016-04-05 11:47:15 -0700488 cold = sorted(i.size for i in f_cold)
489 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700490 except isolateserver.Aborted:
491 # This happens when a signal SIGTERM was received while uploading data.
492 # There is 2 causes:
493 # - The task was too slow and was about to be killed anyway due to
494 # exceeding the hard timeout.
495 # - The amount of data uploaded back is very large and took too much
496 # time to archive.
497 sys.stderr.write('Received SIGTERM while uploading')
498 # Re-raise, so it will be treated as an internal failure.
499 raise
nodir6f801882016-04-29 14:41:50 -0700500
501 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700502 try:
maruel12e30012015-10-09 11:55:35 -0700503 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700504 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700505 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700506 else:
507 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700508 except OSError as e:
509 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700510 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700511 stats = {
512 'duration': time.time() - start,
513 'items_cold': base64.b64encode(large.pack(cold)),
514 'items_hot': base64.b64encode(large.pack(hot)),
515 }
516 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700517
518
marueleb5fbee2015-09-17 13:01:36 -0700519def map_and_run(
nodir0ae98b32017-05-11 13:21:53 -0700520 command, isolated_hash, storage, isolate_cache, outputs,
521 install_named_caches, leak_temp_dir, root_dir, hard_timeout, grace_period,
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -0700522 bot_file, switch_to_account, install_packages_fn, use_symlinks, raw_cmd,
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500523 env, env_prefixes, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700524 """Runs a command with optional isolated input/output.
525
526 See run_tha_test for argument documentation.
527
528 Returns metadata about the result.
529 """
maruelabec63c2017-04-26 11:53:24 -0700530 assert isinstance(command, list), command
nodir56efa452016-10-12 12:17:39 -0700531 assert root_dir or root_dir is None
maruela9cfd6f2015-09-15 11:03:15 -0700532 result = {
maruel064c0a32016-04-05 11:47:15 -0700533 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700534 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700535 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700536 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700537 'stats': {
nodir55715712016-06-03 12:28:19 -0700538 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700539 # 'cipd': {
540 # 'duration': 0.,
541 # 'get_client_duration': 0.,
542 # },
nodir55715712016-06-03 12:28:19 -0700543 # 'download': {
544 # 'duration': 0.,
545 # 'initial_number_items': 0,
546 # 'initial_size': 0,
547 # 'items_cold': '<large.pack()>',
548 # 'items_hot': '<large.pack()>',
549 # },
550 # 'upload': {
551 # 'duration': 0.,
552 # 'items_cold': '<large.pack()>',
553 # 'items_hot': '<large.pack()>',
554 # },
maruel064c0a32016-04-05 11:47:15 -0700555 # },
556 },
iannucci96fcccc2016-08-30 15:52:22 -0700557 # 'cipd_pins': {
558 # 'packages': [
559 # {'package_name': ..., 'version': ..., 'path': ...},
560 # ...
561 # ],
562 # 'client_package': {'package_name': ..., 'version': ...},
563 # },
maruela9cfd6f2015-09-15 11:03:15 -0700564 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700565 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700566 }
nodirbe642ff2016-06-09 15:51:51 -0700567
marueleb5fbee2015-09-17 13:01:36 -0700568 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700569 file_path.ensure_tree(root_dir, 0700)
nodir56efa452016-10-12 12:17:39 -0700570 elif isolate_cache.cache_dir:
571 root_dir = os.path.dirname(isolate_cache.cache_dir)
maruele2f2cb82016-07-13 14:41:03 -0700572 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700573 # If root_dir is not specified, it is not constant.
574 # TODO(maruel): This is not obvious. Change this to become an error once we
575 # make the constant_run_path an exposed flag.
576 if constant_run_path and root_dir:
577 run_dir = os.path.join(root_dir, ISOLATED_RUN_DIR)
maruel5c4eed82017-05-26 05:33:40 -0700578 if os.path.isdir(run_dir):
579 file_path.rmtree(run_dir)
maruelcffa0542017-04-07 08:39:20 -0700580 os.mkdir(run_dir)
581 else:
582 run_dir = make_temp_dir(ISOLATED_RUN_DIR, root_dir)
maruel03e11842016-07-14 10:50:16 -0700583 # storage should be normally set but don't crash if it is not. This can happen
584 # as Swarming task can run without an isolate server.
maruele2f2cb82016-07-13 14:41:03 -0700585 out_dir = make_temp_dir(ISOLATED_OUT_DIR, root_dir) if storage else None
586 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, root_dir)
nodir55be77b2016-05-03 09:39:57 -0700587 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700588
nodir55be77b2016-05-03 09:39:57 -0700589 try:
vadimsh232f5a82017-01-20 19:23:44 -0800590 with install_packages_fn(run_dir) as cipd_info:
591 if cipd_info:
592 result['stats']['cipd'] = cipd_info.stats
593 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700594
vadimsh232f5a82017-01-20 19:23:44 -0800595 if isolated_hash:
596 isolated_stats = result['stats'].setdefault('isolated', {})
597 bundle, isolated_stats['download'] = fetch_and_map(
598 isolated_hash=isolated_hash,
599 storage=storage,
600 cache=isolate_cache,
601 outdir=run_dir,
602 use_symlinks=use_symlinks)
vadimsh232f5a82017-01-20 19:23:44 -0800603 change_tree_read_only(run_dir, bundle.read_only)
maruelabec63c2017-04-26 11:53:24 -0700604 # Inject the command
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -0700605 if not raw_cmd and bundle.command:
maruelabec63c2017-04-26 11:53:24 -0700606 command = bundle.command + command
Marc-Antoine Rueld704a1f2017-10-31 10:51:23 -0400607 # Only set the relative directory if the isolated file specified a
608 # command, and no raw command was specified.
609 if bundle.relative_cwd:
610 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
maruelabec63c2017-04-26 11:53:24 -0700611
612 if not command:
613 # Handle this as a task failure, not an internal failure.
614 sys.stderr.write(
615 '<No command was specified!>\n'
616 '<Please secify a command when triggering your Swarming task>\n')
617 result['exit_code'] = 1
618 return result
nodirbe642ff2016-06-09 15:51:51 -0700619
vadimsh232f5a82017-01-20 19:23:44 -0800620 # If we have an explicit list of files to return, make sure their
621 # directories exist now.
622 if storage and outputs:
623 isolateserver.create_directories(run_dir, outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700624
vadimsh232f5a82017-01-20 19:23:44 -0800625 command = tools.fix_python_path(command)
626 command = process_command(command, out_dir, bot_file)
627 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700628
nodir0ae98b32017-05-11 13:21:53 -0700629 with install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -0800630 sys.stdout.flush()
631 start = time.time()
632 try:
vadimsh9c54b2c2017-07-25 14:08:29 -0700633 # Need to switch the default account before 'get_command_env' call,
634 # so it can grab correct value of LUCI_CONTEXT env var.
635 with set_luci_context_account(switch_to_account, tmp_dir):
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500636 env = get_command_env(tmp_dir, cipd_info, cwd, env, env_prefixes)
vadimsh9c54b2c2017-07-25 14:08:29 -0700637 result['exit_code'], result['had_hard_timeout'] = run_command(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800638 command, cwd, env, hard_timeout, grace_period)
nodird6160682017-02-02 13:03:35 -0800639 finally:
640 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700641 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700642 # An internal error occurred. Report accordingly so the swarming task will
643 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700644 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700645 result['internal_failure'] = str(e)
646 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700647
648 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700649 finally:
650 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700651 # Try to link files to the output directory, if specified.
652 if out_dir:
653 link_outputs_to_outdir(run_dir, out_dir, outputs)
654
nodir32a1ec12016-10-26 18:34:07 -0700655 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700656 if leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700657 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700658 logging.warning(
659 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700660 else:
maruel84537cb2015-10-16 14:21:28 -0700661 # On Windows rmtree(run_dir) call above has a synchronization effect: it
662 # finishes only when all task child processes terminate (since a running
663 # process locks *.exe file). Examine out_dir only after that call
664 # completes (since child processes may write to out_dir too and we need
665 # to wait for them to finish).
666 if fs.isdir(run_dir):
667 try:
668 success = file_path.rmtree(run_dir)
669 except OSError as e:
670 logging.error('Failure with %s', e)
671 success = False
672 if not success:
marueld928c862017-06-08 08:20:04 -0700673 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('run', grace_period))
maruel84537cb2015-10-16 14:21:28 -0700674 if result['exit_code'] == 0:
675 result['exit_code'] = 1
676 if fs.isdir(tmp_dir):
677 try:
678 success = file_path.rmtree(tmp_dir)
679 except OSError as e:
680 logging.error('Failure with %s', e)
681 success = False
682 if not success:
maruelca2a38c2017-06-08 13:06:40 -0700683 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('temp', grace_period))
maruel84537cb2015-10-16 14:21:28 -0700684 if result['exit_code'] == 0:
685 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700686
marueleb5fbee2015-09-17 13:01:36 -0700687 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700688 if out_dir:
nodir55715712016-06-03 12:28:19 -0700689 isolated_stats = result['stats'].setdefault('isolated', {})
690 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700691 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700692 if not success and result['exit_code'] == 0:
693 result['exit_code'] = 1
694 except Exception as e:
695 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700696 if out_dir:
697 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700698 result['internal_failure'] = str(e)
699 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500700
701
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400702def run_tha_test(
nodir0ae98b32017-05-11 13:21:53 -0700703 command, isolated_hash, storage, isolate_cache, outputs,
704 install_named_caches, leak_temp_dir, result_json, root_dir, hard_timeout,
vadimsh9c54b2c2017-07-25 14:08:29 -0700705 grace_period, bot_file, switch_to_account, install_packages_fn,
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500706 use_symlinks, raw_cmd, env, env_prefixes):
nodir55be77b2016-05-03 09:39:57 -0700707 """Runs an executable and records execution metadata.
708
709 Either command or isolated_hash must be specified.
710
711 If isolated_hash is specified, downloads the dependencies in the cache,
712 hardlinks them into a temporary directory and runs the command specified in
713 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500714
715 A temporary directory is created to hold the output files. The content inside
716 this directory will be uploaded back to |storage| packaged as a .isolated
717 file.
718
719 Arguments:
maruelabec63c2017-04-26 11:53:24 -0700720 command: a list of string; the command to run OR optional arguments to add
721 to the command stated in the .isolated file if a command was
722 specified.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500723 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500724 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700725 The command specified in the .isolated is executed.
726 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500727 storage: an isolateserver.Storage object to retrieve remote objects. This
728 object has a reference to an isolateserver.StorageApi, which does
729 the actual I/O.
nodir6b945692016-10-19 19:09:06 -0700730 isolate_cache: an isolateserver.LocalCache to keep from retrieving the
731 same objects constantly by caching the objects retrieved.
732 Can be on-disk or in-memory.
vadimsh9c54b2c2017-07-25 14:08:29 -0700733 outputs: list of paths relative to root_dir to put into the output isolated
734 bundle upon task completion (see link_outputs_to_outdir).
nodir0ae98b32017-05-11 13:21:53 -0700735 install_named_caches: a function (run_dir) => context manager that installs
vadimsh9c54b2c2017-07-25 14:08:29 -0700736 named caches into |run_dir|.
Kenneth Russell61d42352014-09-15 11:41:16 -0700737 leak_temp_dir: if true, the temporary directory will be deliberately leaked
738 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700739 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700740 exit code is always 0 unless an internal error occurred.
nodir90bc8dc2016-06-15 13:35:21 -0700741 root_dir: path to the directory to use to create the temporary directory. If
marueleb5fbee2015-09-17 13:01:36 -0700742 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700743 hard_timeout: kills the process if it lasts more than this amount of
744 seconds.
745 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
vadimsh9c54b2c2017-07-25 14:08:29 -0700746 bot_file: path to a file with bot state, used in place of
747 ${SWARMING_BOT_FILE} task command line argument.
748 switch_to_account: a logical account to switch LUCI_CONTEXT into.
iannuccib58d10d2017-03-18 02:00:25 -0700749 install_packages_fn: context manager dir => CipdInfo, see
vadimsh9c54b2c2017-07-25 14:08:29 -0700750 install_client_and_packages.
maruel4409e302016-07-19 14:25:51 -0700751 use_symlinks: create tree with symlinks instead of hardlinks.
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -0700752 raw_cmd: ignore the command in the isolated file.
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500753 env: environment variables to set
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800754 env_prefixes: {"ENV_KEY": ['relative', 'paths', 'to', 'prepend']}
maruela9cfd6f2015-09-15 11:03:15 -0700755
756 Returns:
757 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000758 """
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -0500759 root_dir = _to_unicode(root_dir)
maruela76b9ee2015-12-15 06:18:08 -0800760 if result_json:
761 # Write a json output file right away in case we get killed.
762 result = {
763 'exit_code': None,
764 'had_hard_timeout': False,
765 'internal_failure': 'Was terminated before completion',
766 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700767 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800768 }
769 tools.write_json(result_json, result, dense=True)
770
maruela9cfd6f2015-09-15 11:03:15 -0700771 # run_isolated exit code. Depends on if result_json is used or not.
772 result = map_and_run(
nodir220308c2017-02-01 19:32:53 -0800773 command, isolated_hash, storage, isolate_cache, outputs,
nodir0ae98b32017-05-11 13:21:53 -0700774 install_named_caches, leak_temp_dir, root_dir, hard_timeout, grace_period,
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -0700775 bot_file, switch_to_account, install_packages_fn, use_symlinks, raw_cmd,
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500776 env, env_prefixes, True)
maruela9cfd6f2015-09-15 11:03:15 -0700777 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700778
maruela9cfd6f2015-09-15 11:03:15 -0700779 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700780 # We've found tests to delete 'work' when quitting, causing an exception
781 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700782 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700783 tools.write_json(result_json, result, dense=True)
784 # Only return 1 if there was an internal error.
785 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000786
maruela9cfd6f2015-09-15 11:03:15 -0700787 # Marshall into old-style inline output.
788 if result['outputs_ref']:
789 data = {
790 'hash': result['outputs_ref']['isolated'],
791 'namespace': result['outputs_ref']['namespace'],
792 'storage': result['outputs_ref']['isolatedserver'],
793 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500794 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700795 print(
796 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
797 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800798 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700799 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000800
801
iannuccib58d10d2017-03-18 02:00:25 -0700802# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800803CipdInfo = collections.namedtuple('CipdInfo', [
804 'client', # cipd.CipdClient object
805 'cache_dir', # absolute path to bot-global cipd tag and instance cache
806 'stats', # dict with stats to return to the server
807 'pins', # dict with installed cipd pins to return to the server
808])
809
810
811@contextlib.contextmanager
812def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700813 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800814 yield None
815
816
iannuccib58d10d2017-03-18 02:00:25 -0700817def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
818 """Calls 'cipd ensure' for packages.
819
820 Args:
821 run_dir (str): root of installation.
822 cipd_cache_dir (str): the directory to use for the cipd package cache.
823 client (CipdClient): the cipd client to use
824 packages: packages to install, list [(path, package_name, version), ...].
825 timeout: max duration in seconds that this function can take.
826
827 Returns: list of pinned packages. Looks like [
828 {
829 'path': 'subdirectory',
830 'package_name': 'resolved/package/name',
831 'version': 'deadbeef...',
832 },
833 ...
834 ]
835 """
836 package_pins = [None]*len(packages)
837 def insert_pin(path, name, version, idx):
838 package_pins[idx] = {
839 'package_name': name,
840 # swarming deals with 'root' as '.'
841 'path': path or '.',
842 'version': version,
843 }
844
845 by_path = collections.defaultdict(list)
846 for i, (path, name, version) in enumerate(packages):
847 # cipd deals with 'root' as ''
848 if path == '.':
849 path = ''
850 by_path[path].append((name, version, i))
851
852 pins = client.ensure(
853 run_dir,
854 {
855 subdir: [(name, vers) for name, vers, _ in pkgs]
856 for subdir, pkgs in by_path.iteritems()
857 },
858 cache_dir=cipd_cache_dir,
859 timeout=timeout,
860 )
861
862 for subdir, pin_list in sorted(pins.iteritems()):
863 this_subdir = by_path[subdir]
864 for i, (name, version) in enumerate(pin_list):
865 insert_pin(subdir, name, version, this_subdir[i][2])
866
867 assert None not in package_pins
868
869 return package_pins
870
871
vadimsh232f5a82017-01-20 19:23:44 -0800872@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700873def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700874 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800875 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800876 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700877
vadimsh232f5a82017-01-20 19:23:44 -0800878 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
879
880 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700881 [
882 {
883 "path": path, "package_name": package_name, "version": version,
884 },
885 ...
886 ]
vadimsh902948e2017-01-20 15:57:32 -0800887 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700888
889 such that they correspond 1:1 to all input package arguments from the command
890 line. These dictionaries make their all the way back to swarming, where they
891 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700892
vadimsh902948e2017-01-20 15:57:32 -0800893 If 'packages' list is empty, will bootstrap CIPD client, but won't install
894 any packages.
895
896 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800897 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800898
nodirbe642ff2016-06-09 15:51:51 -0700899 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700900 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800901 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700902 service_url (str): CIPD server url, e.g.
903 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700904 client_package_name (str): CIPD package name of CIPD client.
905 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700906 cache_dir (str): where to keep cache of cipd clients, packages and tags.
907 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700908 """
909 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700910
nodirbe642ff2016-06-09 15:51:51 -0700911 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700912 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700913
vadimsh902948e2017-01-20 15:57:32 -0800914 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800915 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700916 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800917 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700918
nodirbe642ff2016-06-09 15:51:51 -0700919 get_client_start = time.time()
920 client_manager = cipd.get_client(
921 service_url, client_package_name, client_version, cache_dir,
922 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700923
nodirbe642ff2016-06-09 15:51:51 -0700924 with client_manager as client:
925 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700926
iannuccib58d10d2017-03-18 02:00:25 -0700927 package_pins = []
928 if packages:
929 package_pins = _install_packages(
930 run_dir, cipd_cache_dir, client, packages, timeoutfn())
931
932 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700933
vadimsh232f5a82017-01-20 19:23:44 -0800934 total_duration = time.time() - start
935 logging.info(
936 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700937
vadimsh232f5a82017-01-20 19:23:44 -0800938 yield CipdInfo(
939 client=client,
940 cache_dir=cipd_cache_dir,
941 stats={
942 'duration': total_duration,
943 'get_client_duration': get_client_duration,
944 },
945 pins={
iannuccib58d10d2017-03-18 02:00:25 -0700946 'client_package': {
947 'package_name': client.package_name,
948 'version': client.instance_id,
949 },
vadimsh232f5a82017-01-20 19:23:44 -0800950 'packages': package_pins,
951 })
nodirbe642ff2016-06-09 15:51:51 -0700952
953
nodirf33b8d62016-10-26 22:34:58 -0700954def clean_caches(options, isolate_cache, named_cache_manager):
maruele6fc9382017-05-04 09:03:48 -0700955 """Trims isolated and named caches.
956
957 The goal here is to coherently trim both caches, deleting older items
958 independent of which container they belong to.
959 """
960 # TODO(maruel): Trim CIPD cache the same way.
961 total = 0
nodirf33b8d62016-10-26 22:34:58 -0700962 with named_cache_manager.open():
963 oldest_isolated = isolate_cache.get_oldest()
964 oldest_named = named_cache_manager.get_oldest()
965 trimmers = [
966 (
967 isolate_cache.trim,
968 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
969 ),
970 (
971 lambda: named_cache_manager.trim(options.min_free_space),
972 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
973 ),
974 ]
975 trimmers.sort(key=lambda (_, ts): ts)
maruele6fc9382017-05-04 09:03:48 -0700976 # TODO(maruel): This is incorrect, we want to trim 'items' that are strictly
977 # the oldest independent of in which cache they live in. Right now, the
978 # cache with the oldest item pays the price.
nodirf33b8d62016-10-26 22:34:58 -0700979 for trim, _ in trimmers:
maruele6fc9382017-05-04 09:03:48 -0700980 total += trim()
nodirf33b8d62016-10-26 22:34:58 -0700981 isolate_cache.cleanup()
maruele6fc9382017-05-04 09:03:48 -0700982 return total
nodirf33b8d62016-10-26 22:34:58 -0700983
984
nodirbe642ff2016-06-09 15:51:51 -0700985def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400986 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700987 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000988 version=__version__,
989 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700990 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700991 '--clean', action='store_true',
992 help='Cleans the cache, trimming it necessary and remove corrupted items '
993 'and returns without executing anything; use with -v to know what '
994 'was done')
995 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -0700996 '--no-clean', action='store_true',
997 help='Do not clean the cache automatically on startup. This is meant for '
998 'bots where a separate execution with --clean was done earlier so '
999 'doing it again is redundant')
1000 parser.add_option(
maruel4409e302016-07-19 14:25:51 -07001001 '--use-symlinks', action='store_true',
1002 help='Use symlinks instead of hardlinks')
1003 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -07001004 '--json',
1005 help='dump output metadata to json file. When used, run_isolated returns '
1006 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -07001007 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001008 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -07001009 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001010 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -07001011 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -07001012 parser.add_option(
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001013 '--raw-cmd', action='store_true',
1014 help='Ignore the isolated command, use the one supplied at the command '
1015 'line')
1016 parser.add_option(
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001017 '--env', default=[], action='append',
1018 help='Environment variables to set for the child process')
1019 parser.add_option(
1020 '--env-prefix', default=[], action='append',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001021 help='Specify a VAR=./path/fragment to put in the environment variable '
1022 'before executing the command. The path fragment must be relative '
1023 'to the isolated run directory, and must not contain a `..` token. '
1024 'The path will be made absolute and prepended to the indicated '
1025 '$VAR using the OS\'s path separator. Multiple items for the same '
1026 '$VAR will be prepended in order.')
1027 parser.add_option(
bpastene3ae09522016-06-10 17:12:59 -07001028 '--bot-file',
1029 help='Path to a file describing the state of the host. The content is '
1030 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -07001031 parser.add_option(
vadimsh9c54b2c2017-07-25 14:08:29 -07001032 '--switch-to-account',
1033 help='If given, switches LUCI_CONTEXT to given logical service account '
1034 '(e.g. "task" or "system") before launching the isolated process.')
1035 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -07001036 '--output', action='append',
1037 help='Specifies an output to return. If no outputs are specified, all '
1038 'files located in $(ISOLATED_OUTDIR) will be returned; '
1039 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
1040 'specified by --output option (there can be multiple) will be '
1041 'returned. Note that if a file in OUT_DIR has the same path '
1042 'as an --output option, the --output version will be returned.')
1043 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -07001044 '-a', '--argsfile',
1045 # This is actually handled in parse_args; it's included here purely so it
1046 # can make it into the help text.
1047 help='Specify a file containing a JSON array of arguments to this '
1048 'script. If --argsfile is provided, no other argument may be '
1049 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001050 data_group = optparse.OptionGroup(parser, 'Data source')
1051 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001052 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -07001053 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001054 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001055 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001056
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -04001057 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001058
1059 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -07001060 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001061
Kenneth Russell61d42352014-09-15 11:41:16 -07001062 debug_group = optparse.OptionGroup(parser, 'Debugging')
1063 debug_group.add_option(
1064 '--leak-temp-dir',
1065 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -07001066 help='Deliberately leak isolate\'s temp dir for later examination. '
1067 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -07001068 debug_group.add_option(
1069 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -07001070 parser.add_option_group(debug_group)
1071
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001072 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001073
nodirf33b8d62016-10-26 22:34:58 -07001074 parser.set_defaults(
1075 cache='cache',
1076 cipd_cache='cipd_cache',
1077 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -07001078 return parser
1079
1080
aludwin7556e0c2016-10-26 08:46:10 -07001081def parse_args(args):
1082 # Create a fake mini-parser just to get out the "-a" command. Note that
1083 # it's not documented here; instead, it's documented in create_option_parser
1084 # even though that parser will never actually get to parse it. This is
1085 # because --argsfile is exclusive with all other options and arguments.
1086 file_argparse = argparse.ArgumentParser(add_help=False)
1087 file_argparse.add_argument('-a', '--argsfile')
1088 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
1089 if file_args.argsfile:
1090 if nonfile_args:
1091 file_argparse.error('Can\'t specify --argsfile with'
1092 'any other arguments (%s)' % nonfile_args)
1093 try:
1094 with open(file_args.argsfile, 'r') as f:
1095 args = json.load(f)
1096 except (IOError, OSError, ValueError) as e:
1097 # We don't need to error out here - "args" is now empty,
1098 # so the call below to parser.parse_args(args) will fail
1099 # and print the full help text.
1100 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
1101
1102 # Even if we failed to read the args, just call the normal parser now since it
1103 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -07001104 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001105 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -07001106 return (parser, options, args)
1107
1108
1109def main(args):
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -05001110 # Warning: when --argsfile is used, the strings are unicode instances, when
1111 # parsed normally, the strings are str instances.
aludwin7556e0c2016-10-26 08:46:10 -07001112 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -07001113
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001114 if not file_path.enable_symlink():
1115 logging.error('Symlink support is not enabled')
1116
nodirf33b8d62016-10-26 22:34:58 -07001117 isolate_cache = isolateserver.process_cache_options(options, trim=False)
1118 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -07001119 if options.clean:
1120 if options.isolated:
1121 parser.error('Can\'t use --isolated with --clean.')
1122 if options.isolate_server:
1123 parser.error('Can\'t use --isolate-server with --clean.')
1124 if options.json:
1125 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -07001126 if options.named_caches:
1127 parser.error('Can\t use --named-cache with --clean.')
1128 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001129 return 0
nodirf33b8d62016-10-26 22:34:58 -07001130
maruel2e8d0f52016-07-16 07:51:29 -07001131 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -07001132 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001133
nodir55be77b2016-05-03 09:39:57 -07001134 if not options.isolated and not args:
1135 parser.error('--isolated or command to run is required.')
1136
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001137 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001138
1139 isolateserver.process_isolate_server_options(
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001140 parser, options, True, False)
nodir55be77b2016-05-03 09:39:57 -07001141 if not options.isolate_server:
1142 if options.isolated:
1143 parser.error('--isolated requires --isolate-server')
1144 if ISOLATED_OUTDIR_PARAMETER in args:
1145 parser.error(
1146 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001147
nodir90bc8dc2016-06-15 13:35:21 -07001148 if options.root_dir:
1149 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -07001150 if options.json:
1151 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001152
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001153 if any('=' not in i for i in options.env):
1154 parser.error(
1155 '--env required key=value form. value can be skipped to delete '
1156 'the variable')
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001157 options.env = dict(i.split('=', 1) for i in options.env)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001158
1159 prefixes = {}
1160 cwd = os.path.realpath(os.getcwd())
1161 for item in options.env_prefix:
1162 if '=' not in item:
1163 parser.error(
1164 '--env-prefix %r is malformed, must be in the form `VAR=./path`'
1165 % item)
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001166 key, opath = item.split('=', 1)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001167 if os.path.isabs(opath):
1168 parser.error('--env-prefix %r path is bad, must be relative.' % opath)
1169 opath = os.path.normpath(opath)
1170 if not os.path.realpath(os.path.join(cwd, opath)).startswith(cwd):
1171 parser.error(
1172 '--env-prefix %r path is bad, must be relative and not contain `..`.'
1173 % opath)
1174 prefixes.setdefault(key, []).append(opath)
1175 options.env_prefix = prefixes
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001176
nodirbe642ff2016-06-09 15:51:51 -07001177 cipd.validate_cipd_options(parser, options)
1178
vadimsh232f5a82017-01-20 19:23:44 -08001179 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -08001180 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -07001181 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -08001182 run_dir, cipd.parse_package_args(options.cipd_packages),
1183 options.cipd_server, options.cipd_client_package,
1184 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001185
nodird6160682017-02-02 13:03:35 -08001186 @contextlib.contextmanager
nodir0ae98b32017-05-11 13:21:53 -07001187 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001188 # WARNING: this function depends on "options" variable defined in the outer
1189 # function.
nodir0ae98b32017-05-11 13:21:53 -07001190 caches = [
1191 (os.path.join(run_dir, unicode(relpath)), name)
1192 for name, relpath in options.named_caches
1193 ]
nodirf33b8d62016-10-26 22:34:58 -07001194 with named_cache_manager.open():
nodir0ae98b32017-05-11 13:21:53 -07001195 for path, name in caches:
1196 named_cache_manager.install(path, name)
nodird6160682017-02-02 13:03:35 -08001197 try:
1198 yield
1199 finally:
dnje289d132017-07-07 11:16:44 -07001200 # Uninstall each named cache, returning it to the cache pool. If an
1201 # uninstall fails for a given cache, it will remain in the task's
1202 # temporary space, get cleaned up by the Swarming bot, and be lost.
1203 #
1204 # If the Swarming bot cannot clean up the cache, it will handle it like
1205 # any other bot file that could not be removed.
nodir0ae98b32017-05-11 13:21:53 -07001206 with named_cache_manager.open():
1207 for path, name in caches:
dnje289d132017-07-07 11:16:44 -07001208 try:
1209 named_cache_manager.uninstall(path, name)
1210 except named_cache.Error:
1211 logging.exception('Error while removing named cache %r at %r. '
1212 'The cache will be lost.', path, name)
nodirf33b8d62016-10-26 22:34:58 -07001213
nodirbe642ff2016-06-09 15:51:51 -07001214 try:
nodir90bc8dc2016-06-15 13:35:21 -07001215 if options.isolate_server:
1216 storage = isolateserver.get_storage(
1217 options.isolate_server, options.namespace)
1218 with storage:
nodirf33b8d62016-10-26 22:34:58 -07001219 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
1220 assert storage.hash_algo == isolate_cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -07001221 return run_tha_test(
maruelabec63c2017-04-26 11:53:24 -07001222 args,
nodirf33b8d62016-10-26 22:34:58 -07001223 options.isolated,
1224 storage,
1225 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001226 options.output,
nodir0ae98b32017-05-11 13:21:53 -07001227 install_named_caches,
nodirf33b8d62016-10-26 22:34:58 -07001228 options.leak_temp_dir,
1229 options.json, options.root_dir,
1230 options.hard_timeout,
1231 options.grace_period,
maruelabec63c2017-04-26 11:53:24 -07001232 options.bot_file,
vadimsh9c54b2c2017-07-25 14:08:29 -07001233 options.switch_to_account,
nodirf33b8d62016-10-26 22:34:58 -07001234 install_packages_fn,
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001235 options.use_symlinks,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001236 options.raw_cmd,
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001237 options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001238 options.env_prefix)
maruel4409e302016-07-19 14:25:51 -07001239 return run_tha_test(
maruelabec63c2017-04-26 11:53:24 -07001240 args,
nodirf33b8d62016-10-26 22:34:58 -07001241 options.isolated,
1242 None,
1243 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001244 options.output,
nodir0ae98b32017-05-11 13:21:53 -07001245 install_named_caches,
nodirf33b8d62016-10-26 22:34:58 -07001246 options.leak_temp_dir,
1247 options.json,
1248 options.root_dir,
1249 options.hard_timeout,
1250 options.grace_period,
maruelabec63c2017-04-26 11:53:24 -07001251 options.bot_file,
vadimsh9c54b2c2017-07-25 14:08:29 -07001252 options.switch_to_account,
nodirf33b8d62016-10-26 22:34:58 -07001253 install_packages_fn,
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001254 options.use_symlinks,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001255 options.raw_cmd,
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001256 options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001257 options.env_prefix)
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:]))