blob: 7c3f063793c8f1356e4ceeb5697043fed212788a [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
iannucci96fcccc2016-08-30 15:52:22 -070030__version__ = '0.8.5'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000031
maruel064c0a32016-04-05 11:47:15 -070032import base64
iannucci96fcccc2016-08-30 15:52:22 -070033import collections
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000034import logging
35import optparse
36import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000037import sys
38import tempfile
maruel064c0a32016-04-05 11:47:15 -070039import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000040
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000041from third_party.depot_tools import fix_encoding
42
Vadim Shtayura6b555c12014-07-23 16:22:18 -070043from utils import file_path
maruel12e30012015-10-09 11:55:35 -070044from utils import fs
maruel064c0a32016-04-05 11:47:15 -070045from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040046from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040047from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050048from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000049from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000050from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000051
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080052import auth
nodirbe642ff2016-06-09 15:51:51 -070053import cipd
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000054import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000055
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000056
vadimsh@chromium.org85071062013-08-21 23:37:45 +000057# Absolute path to this file (can be None if running from zip on Mac).
tansella4949442016-06-23 22:34:32 -070058THIS_FILE_PATH = os.path.abspath(
59 __file__.decode(sys.getfilesystemencoding())) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000060
61# Directory that contains this file (might be inside zip package).
tansella4949442016-06-23 22:34:32 -070062BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__.decode(
63 sys.getfilesystemencoding()) else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000064
65# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000066if zip_package.get_main_script_path():
67 MAIN_DIR = os.path.dirname(
68 os.path.abspath(zip_package.get_main_script_path()))
69else:
70 # This happens when 'import run_isolated' is executed at the python
71 # interactive prompt, in that case __file__ is undefined.
72 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000073
maruele2f2cb82016-07-13 14:41:03 -070074
75# Magic variables that can be found in the isolate task command line.
76ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
77EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
78SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
79
80
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000081# The name of the log file to use.
82RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
83
maruele2f2cb82016-07-13 14:41:03 -070084
csharp@chromium.orge217f302012-11-22 16:51:53 +000085# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000086RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000087
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000088
maruele2f2cb82016-07-13 14:41:03 -070089# Use short names for temporary directories. This is driven by Windows, which
90# imposes a relatively short maximum path length of 260 characters, often
91# referred to as MAX_PATH. It is relatively easy to create files with longer
92# path length. A use case is with recursive depedency treesV like npm packages.
93#
94# It is recommended to start the script with a `root_dir` as short as
95# possible.
96# - ir stands for isolated_run
97# - io stands for isolated_out
98# - it stands for isolated_tmp
99ISOLATED_RUN_DIR = u'ir'
100ISOLATED_OUT_DIR = u'io'
101ISOLATED_TMP_DIR = u'it'
102
103
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000104def get_as_zip_package(executable=True):
105 """Returns ZipPackage with this module and all its dependencies.
106
107 If |executable| is True will store run_isolated.py as __main__.py so that
108 zip package is directly executable be python.
109 """
110 # Building a zip package when running from another zip package is
111 # unsupported and probably unneeded.
112 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +0000113 assert THIS_FILE_PATH
114 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000115 package = zip_package.ZipPackage(root=BASE_DIR)
116 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400117 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000118 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800119 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
nodirbe642ff2016-06-09 15:51:51 -0700120 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
tanselle4288c32016-07-28 09:45:40 -0700121 package.add_directory(os.path.join(BASE_DIR, 'libs'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000122 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
123 package.add_directory(os.path.join(BASE_DIR, 'utils'))
124 return package
125
126
maruel03e11842016-07-14 10:50:16 -0700127def make_temp_dir(prefix, root_dir):
128 """Returns a new unique temporary directory."""
129 return unicode(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000130
131
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500132def change_tree_read_only(rootdir, read_only):
133 """Changes the tree read-only bits according to the read_only specification.
134
135 The flag can be 0, 1 or 2, which will affect the possibility to modify files
136 and create or delete files.
137 """
138 if read_only == 2:
139 # Files and directories (except on Windows) are marked read only. This
140 # inhibits modifying, creating or deleting files in the test directory,
141 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400142 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500143 elif read_only == 1:
144 # Files are marked read only but not the directories. This inhibits
145 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400146 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500147 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500148 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500149 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
150 # is not yet changed to verify the hash of the content of the files it is
151 # looking at, so that if a test modifies an input file, the file must be
152 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400153 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500154 else:
155 raise ValueError(
156 'change_tree_read_only(%s, %s): Unknown flag %s' %
157 (rootdir, read_only, read_only))
158
159
nodir90bc8dc2016-06-15 13:35:21 -0700160def process_command(command, out_dir, bot_file):
nodirbe642ff2016-06-09 15:51:51 -0700161 """Replaces variables in a command line.
162
163 Raises:
164 ValueError if a parameter is requested in |command| but its value is not
165 provided.
166 """
maruela9cfd6f2015-09-15 11:03:15 -0700167 def fix(arg):
nodirbe642ff2016-06-09 15:51:51 -0700168 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
169 replace_slash = False
nodir55be77b2016-05-03 09:39:57 -0700170 if ISOLATED_OUTDIR_PARAMETER in arg:
nodirbe642ff2016-06-09 15:51:51 -0700171 if not out_dir:
maruel7f63a272016-07-12 12:40:36 -0700172 raise ValueError(
173 'output directory is requested in command, but not provided; '
174 'please specify one')
nodir55be77b2016-05-03 09:39:57 -0700175 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
nodirbe642ff2016-06-09 15:51:51 -0700176 replace_slash = True
nodir90bc8dc2016-06-15 13:35:21 -0700177 if SWARMING_BOT_FILE_PARAMETER in arg:
178 if bot_file:
179 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
180 replace_slash = True
181 else:
182 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command, but no '
183 'bot_file specified. Leaving parameter unchanged.')
nodirbe642ff2016-06-09 15:51:51 -0700184 if replace_slash:
185 # Replace slashes only if parameters are present
nodir55be77b2016-05-03 09:39:57 -0700186 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
187 arg = arg.replace('/', os.sep)
maruela9cfd6f2015-09-15 11:03:15 -0700188 return arg
189
190 return [fix(arg) for arg in command]
191
192
maruel6be7f9e2015-10-01 12:25:30 -0700193def run_command(command, cwd, tmp_dir, hard_timeout, grace_period):
194 """Runs the command.
195
196 Returns:
197 tuple(process exit code, bool if had a hard timeout)
198 """
maruela9cfd6f2015-09-15 11:03:15 -0700199 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700200
201 env = os.environ.copy()
202 if sys.platform == 'darwin':
tansella4949442016-06-23 22:34:32 -0700203 env['TMPDIR'] = tmp_dir.encode(sys.getfilesystemencoding())
marueleb5fbee2015-09-17 13:01:36 -0700204 elif sys.platform == 'win32':
tansella4949442016-06-23 22:34:32 -0700205 env['TEMP'] = tmp_dir.encode(sys.getfilesystemencoding())
marueleb5fbee2015-09-17 13:01:36 -0700206 else:
tansella4949442016-06-23 22:34:32 -0700207 env['TMP'] = tmp_dir.encode(sys.getfilesystemencoding())
maruel6be7f9e2015-10-01 12:25:30 -0700208 exit_code = None
209 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700210 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700211 proc = None
212 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700213 try:
maruel6be7f9e2015-10-01 12:25:30 -0700214 # TODO(maruel): This code is imperfect. It doesn't handle well signals
215 # during the download phase and there's short windows were things can go
216 # wrong.
217 def handler(signum, _frame):
218 if proc and not had_signal:
219 logging.info('Received signal %d', signum)
220 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700221 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700222
223 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
224 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
225 try:
226 exit_code = proc.wait(hard_timeout or None)
227 except subprocess42.TimeoutExpired:
228 if not had_signal:
229 logging.warning('Hard timeout')
230 had_hard_timeout = True
231 logging.warning('Sending SIGTERM')
232 proc.terminate()
233
234 # Ignore signals in grace period. Forcibly give the grace period to the
235 # child process.
236 if exit_code is None:
237 ignore = lambda *_: None
238 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
239 try:
240 exit_code = proc.wait(grace_period or None)
241 except subprocess42.TimeoutExpired:
242 # Now kill for real. The user can distinguish between the
243 # following states:
244 # - signal but process exited within grace period,
245 # hard_timed_out will be set but the process exit code will be
246 # script provided.
247 # - processed exited late, exit code will be -9 on posix.
248 logging.warning('Grace exhausted; sending SIGKILL')
249 proc.kill()
250 logging.info('Waiting for proces exit')
251 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700252 except OSError:
253 # This is not considered to be an internal error. The executable simply
254 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800255 sys.stderr.write(
256 '<The executable does not exist or a dependent library is missing>\n'
257 '<Check for missing .so/.dll in the .isolate or GN file>\n'
258 '<Command: %s>\n' % command)
259 if os.environ.get('SWARMING_TASK_ID'):
260 # Give an additional hint when running as a swarming task.
261 sys.stderr.write(
262 '<See the task\'s page for commands to help diagnose this issue '
263 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700264 exit_code = 1
265 logging.info(
266 'Command finished with exit code %d (%s)',
267 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700268 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700269
270
maruel4409e302016-07-19 14:25:51 -0700271def fetch_and_map(isolated_hash, storage, cache, outdir, use_symlinks):
272 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700273 start = time.time()
274 bundle = isolateserver.fetch_isolated(
275 isolated_hash=isolated_hash,
276 storage=storage,
277 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700278 outdir=outdir,
279 use_symlinks=use_symlinks)
nodir6f801882016-04-29 14:41:50 -0700280 return bundle, {
281 'duration': time.time() - start,
282 'initial_number_items': cache.initial_number_items,
283 'initial_size': cache.initial_size,
284 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
285 'items_hot': base64.b64encode(
tansell9e04a8d2016-07-28 09:31:59 -0700286 large.pack(sorted(set(cache.used) - set(cache.added)))),
nodir6f801882016-04-29 14:41:50 -0700287 }
288
289
maruela9cfd6f2015-09-15 11:03:15 -0700290def delete_and_upload(storage, out_dir, leak_temp_dir):
291 """Deletes the temporary run directory and uploads results back.
292
293 Returns:
nodir6f801882016-04-29 14:41:50 -0700294 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700295 - outputs_ref: a dict referring to the results archived back to the isolated
296 server, if applicable.
297 - success: False if something occurred that means that the task must
298 forcibly be considered a failure, e.g. zombie processes were left
299 behind.
nodir6f801882016-04-29 14:41:50 -0700300 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700301 """
302
303 # Upload out_dir and generate a .isolated file out of this directory. It is
304 # only done if files were written in the directory.
305 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700306 cold = []
307 hot = []
nodir6f801882016-04-29 14:41:50 -0700308 start = time.time()
309
maruel12e30012015-10-09 11:55:35 -0700310 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700311 with tools.Profiler('ArchiveOutput'):
312 try:
maruel064c0a32016-04-05 11:47:15 -0700313 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700314 storage, [out_dir], None)
315 outputs_ref = {
316 'isolated': results[0][0],
317 'isolatedserver': storage.location,
318 'namespace': storage.namespace,
319 }
maruel064c0a32016-04-05 11:47:15 -0700320 cold = sorted(i.size for i in f_cold)
321 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700322 except isolateserver.Aborted:
323 # This happens when a signal SIGTERM was received while uploading data.
324 # There is 2 causes:
325 # - The task was too slow and was about to be killed anyway due to
326 # exceeding the hard timeout.
327 # - The amount of data uploaded back is very large and took too much
328 # time to archive.
329 sys.stderr.write('Received SIGTERM while uploading')
330 # Re-raise, so it will be treated as an internal failure.
331 raise
nodir6f801882016-04-29 14:41:50 -0700332
333 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700334 try:
maruel12e30012015-10-09 11:55:35 -0700335 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700336 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700337 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700338 else:
339 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700340 except OSError as e:
341 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700342 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700343 stats = {
344 'duration': time.time() - start,
345 'items_cold': base64.b64encode(large.pack(cold)),
346 'items_hot': base64.b64encode(large.pack(hot)),
347 }
348 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700349
350
marueleb5fbee2015-09-17 13:01:36 -0700351def map_and_run(
nodir56efa452016-10-12 12:17:39 -0700352 command, isolated_hash, storage, isolate_cache, leak_temp_dir, root_dir,
maruel4409e302016-07-19 14:25:51 -0700353 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn,
354 use_symlinks):
nodir55be77b2016-05-03 09:39:57 -0700355 """Runs a command with optional isolated input/output.
356
357 See run_tha_test for argument documentation.
358
359 Returns metadata about the result.
360 """
nodir56efa452016-10-12 12:17:39 -0700361 assert root_dir or root_dir is None
nodir55be77b2016-05-03 09:39:57 -0700362 assert bool(command) ^ bool(isolated_hash)
maruela9cfd6f2015-09-15 11:03:15 -0700363 result = {
maruel064c0a32016-04-05 11:47:15 -0700364 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700365 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700366 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700367 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700368 'stats': {
nodir55715712016-06-03 12:28:19 -0700369 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700370 # 'cipd': {
371 # 'duration': 0.,
372 # 'get_client_duration': 0.,
373 # },
nodir55715712016-06-03 12:28:19 -0700374 # 'download': {
375 # 'duration': 0.,
376 # 'initial_number_items': 0,
377 # 'initial_size': 0,
378 # 'items_cold': '<large.pack()>',
379 # 'items_hot': '<large.pack()>',
380 # },
381 # 'upload': {
382 # 'duration': 0.,
383 # 'items_cold': '<large.pack()>',
384 # 'items_hot': '<large.pack()>',
385 # },
maruel064c0a32016-04-05 11:47:15 -0700386 # },
387 },
iannucci96fcccc2016-08-30 15:52:22 -0700388 # 'cipd_pins': {
389 # 'packages': [
390 # {'package_name': ..., 'version': ..., 'path': ...},
391 # ...
392 # ],
393 # 'client_package': {'package_name': ..., 'version': ...},
394 # },
maruela9cfd6f2015-09-15 11:03:15 -0700395 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700396 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700397 }
nodirbe642ff2016-06-09 15:51:51 -0700398
marueleb5fbee2015-09-17 13:01:36 -0700399 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700400 file_path.ensure_tree(root_dir, 0700)
nodir56efa452016-10-12 12:17:39 -0700401 elif isolate_cache.cache_dir:
402 root_dir = os.path.dirname(isolate_cache.cache_dir)
maruele2f2cb82016-07-13 14:41:03 -0700403 # See comment for these constants.
404 run_dir = make_temp_dir(ISOLATED_RUN_DIR, root_dir)
maruel03e11842016-07-14 10:50:16 -0700405 # storage should be normally set but don't crash if it is not. This can happen
406 # as Swarming task can run without an isolate server.
maruele2f2cb82016-07-13 14:41:03 -0700407 out_dir = make_temp_dir(ISOLATED_OUT_DIR, root_dir) if storage else None
408 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, root_dir)
nodir55be77b2016-05-03 09:39:57 -0700409 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700410
nodir55be77b2016-05-03 09:39:57 -0700411 try:
iannucci96fcccc2016-08-30 15:52:22 -0700412 cipd_info = install_packages_fn(run_dir)
413 if cipd_info:
414 result['stats']['cipd'] = cipd_info['stats']
415 result['cipd_pins'] = cipd_info['cipd_pins']
nodir90bc8dc2016-06-15 13:35:21 -0700416
nodir55be77b2016-05-03 09:39:57 -0700417 if isolated_hash:
nodir55715712016-06-03 12:28:19 -0700418 isolated_stats = result['stats'].setdefault('isolated', {})
maruel4409e302016-07-19 14:25:51 -0700419 bundle, isolated_stats['download'] = fetch_and_map(
nodir55be77b2016-05-03 09:39:57 -0700420 isolated_hash=isolated_hash,
421 storage=storage,
nodir56efa452016-10-12 12:17:39 -0700422 cache=isolate_cache,
maruel4409e302016-07-19 14:25:51 -0700423 outdir=run_dir,
424 use_symlinks=use_symlinks)
nodir55be77b2016-05-03 09:39:57 -0700425 if not bundle.command:
426 # Handle this as a task failure, not an internal failure.
427 sys.stderr.write(
428 '<The .isolated doesn\'t declare any command to run!>\n'
429 '<Check your .isolate for missing \'command\' variable>\n')
430 if os.environ.get('SWARMING_TASK_ID'):
431 # Give an additional hint when running as a swarming task.
432 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
433 result['exit_code'] = 1
434 return result
435
436 change_tree_read_only(run_dir, bundle.read_only)
437 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
438 command = bundle.command + extra_args
nodirbe642ff2016-06-09 15:51:51 -0700439
nodir34d673c2016-05-24 09:30:48 -0700440 command = tools.fix_python_path(command)
nodir90bc8dc2016-06-15 13:35:21 -0700441 command = process_command(command, out_dir, bot_file)
maruela9cfd6f2015-09-15 11:03:15 -0700442 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700443
maruel064c0a32016-04-05 11:47:15 -0700444 sys.stdout.flush()
445 start = time.time()
446 try:
447 result['exit_code'], result['had_hard_timeout'] = run_command(
nodirbe642ff2016-06-09 15:51:51 -0700448 command, cwd, tmp_dir, hard_timeout, grace_period)
maruel064c0a32016-04-05 11:47:15 -0700449 finally:
450 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700451 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700452 # An internal error occurred. Report accordingly so the swarming task will
453 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700454 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700455 result['internal_failure'] = str(e)
456 on_error.report(None)
457 finally:
458 try:
459 if leak_temp_dir:
460 logging.warning(
461 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700462 else:
maruel84537cb2015-10-16 14:21:28 -0700463 # On Windows rmtree(run_dir) call above has a synchronization effect: it
464 # finishes only when all task child processes terminate (since a running
465 # process locks *.exe file). Examine out_dir only after that call
466 # completes (since child processes may write to out_dir too and we need
467 # to wait for them to finish).
468 if fs.isdir(run_dir):
469 try:
470 success = file_path.rmtree(run_dir)
471 except OSError as e:
472 logging.error('Failure with %s', e)
473 success = False
474 if not success:
475 print >> sys.stderr, (
476 'Failed to delete the run directory, forcibly failing\n'
477 'the task because of it. No zombie process can outlive a\n'
478 'successful task run and still be marked as successful.\n'
479 'Fix your stuff.')
480 if result['exit_code'] == 0:
481 result['exit_code'] = 1
482 if fs.isdir(tmp_dir):
483 try:
484 success = file_path.rmtree(tmp_dir)
485 except OSError as e:
486 logging.error('Failure with %s', e)
487 success = False
488 if not success:
489 print >> sys.stderr, (
490 'Failed to delete the temporary directory, forcibly failing\n'
491 'the task because of it. No zombie process can outlive a\n'
492 'successful task run and still be marked as successful.\n'
493 'Fix your stuff.')
494 if result['exit_code'] == 0:
495 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700496
marueleb5fbee2015-09-17 13:01:36 -0700497 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700498 if out_dir:
nodir55715712016-06-03 12:28:19 -0700499 isolated_stats = result['stats'].setdefault('isolated', {})
500 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700501 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700502 if not success and result['exit_code'] == 0:
503 result['exit_code'] = 1
504 except Exception as e:
505 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700506 if out_dir:
507 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700508 result['internal_failure'] = str(e)
509 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500510
511
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400512def run_tha_test(
nodir6b945692016-10-19 19:09:06 -0700513 command, isolated_hash, storage, isolate_cache, leak_temp_dir, result_json,
bpastene3ae09522016-06-10 17:12:59 -0700514 root_dir, hard_timeout, grace_period, bot_file, extra_args,
maruel4409e302016-07-19 14:25:51 -0700515 install_packages_fn, use_symlinks):
nodir55be77b2016-05-03 09:39:57 -0700516 """Runs an executable and records execution metadata.
517
518 Either command or isolated_hash must be specified.
519
520 If isolated_hash is specified, downloads the dependencies in the cache,
521 hardlinks them into a temporary directory and runs the command specified in
522 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500523
524 A temporary directory is created to hold the output files. The content inside
525 this directory will be uploaded back to |storage| packaged as a .isolated
526 file.
527
528 Arguments:
nodir55be77b2016-05-03 09:39:57 -0700529 command: the command to run, a list of strings. Mutually exclusive with
530 isolated_hash.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500531 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500532 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700533 The command specified in the .isolated is executed.
534 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500535 storage: an isolateserver.Storage object to retrieve remote objects. This
536 object has a reference to an isolateserver.StorageApi, which does
537 the actual I/O.
nodir6b945692016-10-19 19:09:06 -0700538 isolate_cache: an isolateserver.LocalCache to keep from retrieving the
539 same objects constantly by caching the objects retrieved.
540 Can be on-disk or in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700541 leak_temp_dir: if true, the temporary directory will be deliberately leaked
542 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700543 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700544 exit code is always 0 unless an internal error occurred.
nodir90bc8dc2016-06-15 13:35:21 -0700545 root_dir: path to the directory to use to create the temporary directory. If
marueleb5fbee2015-09-17 13:01:36 -0700546 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700547 hard_timeout: kills the process if it lasts more than this amount of
548 seconds.
549 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500550 extra_args: optional arguments to add to the command stated in the .isolate
nodir55be77b2016-05-03 09:39:57 -0700551 file. Ignored if isolate_hash is empty.
iannucci96fcccc2016-08-30 15:52:22 -0700552 install_packages_fn: function (dir) => {"stats": cipd_stats, "pins":
553 cipd_pins}. Installs packages.
maruel4409e302016-07-19 14:25:51 -0700554 use_symlinks: create tree with symlinks instead of hardlinks.
maruela9cfd6f2015-09-15 11:03:15 -0700555
556 Returns:
557 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000558 """
nodir55be77b2016-05-03 09:39:57 -0700559 assert bool(command) ^ bool(isolated_hash)
560 extra_args = extra_args or []
nodirbe642ff2016-06-09 15:51:51 -0700561
nodir55be77b2016-05-03 09:39:57 -0700562 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
563 assert storage is not None, 'storage is None although outdir is specified'
564
maruela76b9ee2015-12-15 06:18:08 -0800565 if result_json:
566 # Write a json output file right away in case we get killed.
567 result = {
568 'exit_code': None,
569 'had_hard_timeout': False,
570 'internal_failure': 'Was terminated before completion',
571 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700572 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800573 }
574 tools.write_json(result_json, result, dense=True)
575
maruela9cfd6f2015-09-15 11:03:15 -0700576 # run_isolated exit code. Depends on if result_json is used or not.
577 result = map_and_run(
nodir6b945692016-10-19 19:09:06 -0700578 command, isolated_hash, storage, isolate_cache, leak_temp_dir, root_dir,
maruel4409e302016-07-19 14:25:51 -0700579 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn,
580 use_symlinks)
maruela9cfd6f2015-09-15 11:03:15 -0700581 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700582
maruela9cfd6f2015-09-15 11:03:15 -0700583 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700584 # We've found tests to delete 'work' when quitting, causing an exception
585 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700586 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700587 tools.write_json(result_json, result, dense=True)
588 # Only return 1 if there was an internal error.
589 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000590
maruela9cfd6f2015-09-15 11:03:15 -0700591 # Marshall into old-style inline output.
592 if result['outputs_ref']:
593 data = {
594 'hash': result['outputs_ref']['isolated'],
595 'namespace': result['outputs_ref']['namespace'],
596 'storage': result['outputs_ref']['isolatedserver'],
597 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500598 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700599 print(
600 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
601 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800602 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700603 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000604
605
nodir90bc8dc2016-06-15 13:35:21 -0700606def install_packages(
nodirff531b42016-06-23 13:05:06 -0700607 run_dir, packages, service_url, client_package_name,
nodir90bc8dc2016-06-15 13:35:21 -0700608 client_version, cache_dir=None, timeout=None):
iannucci96fcccc2016-08-30 15:52:22 -0700609 """Installs packages. Returns stats, cipd client info and pins.
610
611 pins and the cipd client info are in the form of:
612 [
613 {
614 "path": path, "package_name": package_name, "version": version,
615 },
616 ...
617 ]
618 (the cipd client info is a single dictionary instead of a list)
619
620 such that they correspond 1:1 to all input package arguments from the command
621 line. These dictionaries make their all the way back to swarming, where they
622 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700623
624 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700625 run_dir (str): root of installation.
iannucci96fcccc2016-08-30 15:52:22 -0700626 packages: packages to install, list [(path, package_name, version), ...]
nodirbe642ff2016-06-09 15:51:51 -0700627 service_url (str): CIPD server url, e.g.
628 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700629 client_package_name (str): CIPD package name of CIPD client.
630 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700631 cache_dir (str): where to keep cache of cipd clients, packages and tags.
632 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700633 """
634 assert cache_dir
nodirff531b42016-06-23 13:05:06 -0700635 if not packages:
nodir90bc8dc2016-06-15 13:35:21 -0700636 return None
637
nodirbe642ff2016-06-09 15:51:51 -0700638 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700639 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700640 cache_dir = os.path.abspath(cache_dir)
641
nodir90bc8dc2016-06-15 13:35:21 -0700642 run_dir = os.path.abspath(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700643
iannucci96fcccc2016-08-30 15:52:22 -0700644 package_pins = [None]*len(packages)
645 def insert_pin(path, name, version, idx):
646 path = path.replace(os.path.sep, '/')
647 package_pins[idx] = {
648 'package_name': name,
649 'path': path,
650 'version': version,
651 }
652
nodirbe642ff2016-06-09 15:51:51 -0700653 get_client_start = time.time()
654 client_manager = cipd.get_client(
655 service_url, client_package_name, client_version, cache_dir,
656 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700657
658 by_path = collections.defaultdict(list)
659 for i, (path, name, version) in enumerate(packages):
660 path = path.replace('/', os.path.sep)
661 by_path[path].append((name, version, i))
662
nodirbe642ff2016-06-09 15:51:51 -0700663 with client_manager as client:
iannucci96fcccc2016-08-30 15:52:22 -0700664 client_package = {
665 'package_name': client.package_name,
666 'version': client.instance_id,
667 }
nodirbe642ff2016-06-09 15:51:51 -0700668 get_client_duration = time.time() - get_client_start
iannucci96fcccc2016-08-30 15:52:22 -0700669 for path, pkgs in sorted(by_path.iteritems()):
nodir90bc8dc2016-06-15 13:35:21 -0700670 site_root = os.path.abspath(os.path.join(run_dir, path))
671 if not site_root.startswith(run_dir):
672 raise cipd.Error('Invalid CIPD package path "%s"' % path)
673
674 # Do not clean site_root before installation because it may contain other
675 # site roots.
676 file_path.ensure_tree(site_root, 0770)
iannucci96fcccc2016-08-30 15:52:22 -0700677 pins = client.ensure(
678 site_root, [(name, vers) for name, vers, _ in pkgs],
nodirbe642ff2016-06-09 15:51:51 -0700679 cache_dir=os.path.join(cache_dir, 'cipd_internal'),
680 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700681 for i, pin in enumerate(pins):
682 insert_pin(path, pin[0], pin[1], pkgs[i][2])
nodirbe642ff2016-06-09 15:51:51 -0700683 file_path.make_tree_files_read_only(site_root)
nodir90bc8dc2016-06-15 13:35:21 -0700684
685 total_duration = time.time() - start
686 logging.info(
687 'Installing CIPD client and packages took %d seconds', total_duration)
688
iannucci96fcccc2016-08-30 15:52:22 -0700689 assert None not in package_pins
690
nodir90bc8dc2016-06-15 13:35:21 -0700691 return {
iannucci96fcccc2016-08-30 15:52:22 -0700692 'stats': {
693 'duration': total_duration,
694 'get_client_duration': get_client_duration,
695 },
696 'cipd_pins': {
697 'client_package': client_package,
698 'packages': package_pins,
699 }
nodir90bc8dc2016-06-15 13:35:21 -0700700 }
nodirbe642ff2016-06-09 15:51:51 -0700701
702
703def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400704 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700705 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000706 version=__version__,
707 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700708 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700709 '--clean', action='store_true',
710 help='Cleans the cache, trimming it necessary and remove corrupted items '
711 'and returns without executing anything; use with -v to know what '
712 'was done')
713 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -0700714 '--no-clean', action='store_true',
715 help='Do not clean the cache automatically on startup. This is meant for '
716 'bots where a separate execution with --clean was done earlier so '
717 'doing it again is redundant')
718 parser.add_option(
maruel4409e302016-07-19 14:25:51 -0700719 '--use-symlinks', action='store_true',
720 help='Use symlinks instead of hardlinks')
721 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700722 '--json',
723 help='dump output metadata to json file. When used, run_isolated returns '
724 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700725 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800726 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700727 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800728 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700729 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700730 parser.add_option(
731 '--bot-file',
732 help='Path to a file describing the state of the host. The content is '
733 'defined by on_before_task() in bot_config.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500734 data_group = optparse.OptionGroup(parser, 'Data source')
735 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500736 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700737 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500738 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500739 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000740
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400741 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700742
743 cipd.add_cipd_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000744
Kenneth Russell61d42352014-09-15 11:41:16 -0700745 debug_group = optparse.OptionGroup(parser, 'Debugging')
746 debug_group.add_option(
747 '--leak-temp-dir',
748 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700749 help='Deliberately leak isolate\'s temp dir for later examination. '
750 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700751 debug_group.add_option(
752 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700753 parser.add_option_group(debug_group)
754
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800755 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700756
757 parser.set_defaults(cache='cache', cipd_cache='cipd_cache')
758 return parser
759
760
761def main(args):
762 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500763 options, args = parser.parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700764
nodir56efa452016-10-12 12:17:39 -0700765 isolated_cache = isolateserver.process_cache_options(options)
maruel36a963d2016-04-08 17:15:49 -0700766 if options.clean:
767 if options.isolated:
768 parser.error('Can\'t use --isolated with --clean.')
769 if options.isolate_server:
770 parser.error('Can\'t use --isolate-server with --clean.')
771 if options.json:
772 parser.error('Can\'t use --json with --clean.')
nodir56efa452016-10-12 12:17:39 -0700773 isolated_cache.cleanup()
maruel36a963d2016-04-08 17:15:49 -0700774 return 0
maruel2e8d0f52016-07-16 07:51:29 -0700775 if not options.no_clean:
nodir56efa452016-10-12 12:17:39 -0700776 isolated_cache.cleanup()
maruel36a963d2016-04-08 17:15:49 -0700777
nodir55be77b2016-05-03 09:39:57 -0700778 if not options.isolated and not args:
779 parser.error('--isolated or command to run is required.')
780
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800781 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700782
783 isolateserver.process_isolate_server_options(
784 parser, options, True, False)
785 if not options.isolate_server:
786 if options.isolated:
787 parser.error('--isolated requires --isolate-server')
788 if ISOLATED_OUTDIR_PARAMETER in args:
789 parser.error(
790 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000791
nodir90bc8dc2016-06-15 13:35:21 -0700792 if options.root_dir:
793 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -0700794 if options.json:
795 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700796
nodirbe642ff2016-06-09 15:51:51 -0700797 cipd.validate_cipd_options(parser, options)
798
nodir90bc8dc2016-06-15 13:35:21 -0700799 install_packages_fn = lambda run_dir: install_packages(
nodirff531b42016-06-23 13:05:06 -0700800 run_dir, cipd.parse_package_args(options.cipd_packages),
801 options.cipd_server, options.cipd_client_package,
802 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -0700803
804 try:
nodir90bc8dc2016-06-15 13:35:21 -0700805 command = [] if options.isolated else args
806 if options.isolate_server:
807 storage = isolateserver.get_storage(
808 options.isolate_server, options.namespace)
809 with storage:
nodir56efa452016-10-12 12:17:39 -0700810 # Hashing schemes used by |storage| and |isolated_cache| MUST match.
811 assert storage.hash_algo == isolated_cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -0700812 return run_tha_test(
nodir56efa452016-10-12 12:17:39 -0700813 command, options.isolated, storage, isolated_cache,
814 options.leak_temp_dir, options.json, options.root_dir,
815 options.hard_timeout, options.grace_period, options.bot_file, args,
816 install_packages_fn, options.use_symlinks)
maruel4409e302016-07-19 14:25:51 -0700817 return run_tha_test(
nodir56efa452016-10-12 12:17:39 -0700818 command, options.isolated, None, isolated_cache, options.leak_temp_dir,
maruel4409e302016-07-19 14:25:51 -0700819 options.json, options.root_dir, options.hard_timeout,
820 options.grace_period, options.bot_file, args, install_packages_fn,
821 options.use_symlinks)
nodirbe642ff2016-06-09 15:51:51 -0700822 except cipd.Error as ex:
823 print >> sys.stderr, ex.message
824 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000825
826
827if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -0700828 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000829 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000830 fix_encoding.fix_encoding()
maruel4409e302016-07-19 14:25:51 -0700831 file_path.enable_symlink()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500832 sys.exit(main(sys.argv[1:]))