blob: fb67f09da4f8612116e66bc29c64c8770b6a0ef9 [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
aludwin7556e0c2016-10-26 08:46:10 -070030__version__ = '0.8.6'
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
aludwin7556e0c2016-10-26 08:46:10 -070035import json
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000036import logging
37import optparse
38import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000039import sys
40import tempfile
maruel064c0a32016-04-05 11:47:15 -070041import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000042
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000043from third_party.depot_tools import fix_encoding
44
Vadim Shtayura6b555c12014-07-23 16:22:18 -070045from utils import file_path
maruel12e30012015-10-09 11:55:35 -070046from utils import fs
maruel064c0a32016-04-05 11:47:15 -070047from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040048from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040049from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050050from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000051from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000052from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000053
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080054import auth
nodirbe642ff2016-06-09 15:51:51 -070055import cipd
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000056import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000057
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000058
vadimsh@chromium.org85071062013-08-21 23:37:45 +000059# Absolute path to this file (can be None if running from zip on Mac).
tansella4949442016-06-23 22:34:32 -070060THIS_FILE_PATH = os.path.abspath(
61 __file__.decode(sys.getfilesystemencoding())) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000062
63# Directory that contains this file (might be inside zip package).
tansella4949442016-06-23 22:34:32 -070064BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__.decode(
65 sys.getfilesystemencoding()) else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000066
67# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000068if zip_package.get_main_script_path():
69 MAIN_DIR = os.path.dirname(
70 os.path.abspath(zip_package.get_main_script_path()))
71else:
72 # This happens when 'import run_isolated' is executed at the python
73 # interactive prompt, in that case __file__ is undefined.
74 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000075
maruele2f2cb82016-07-13 14:41:03 -070076
77# Magic variables that can be found in the isolate task command line.
78ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
79EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
80SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
81
82
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000083# The name of the log file to use.
84RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
85
maruele2f2cb82016-07-13 14:41:03 -070086
csharp@chromium.orge217f302012-11-22 16:51:53 +000087# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000088RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000089
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000090
maruele2f2cb82016-07-13 14:41:03 -070091# Use short names for temporary directories. This is driven by Windows, which
92# imposes a relatively short maximum path length of 260 characters, often
93# referred to as MAX_PATH. It is relatively easy to create files with longer
94# path length. A use case is with recursive depedency treesV like npm packages.
95#
96# It is recommended to start the script with a `root_dir` as short as
97# possible.
98# - ir stands for isolated_run
99# - io stands for isolated_out
100# - it stands for isolated_tmp
101ISOLATED_RUN_DIR = u'ir'
102ISOLATED_OUT_DIR = u'io'
103ISOLATED_TMP_DIR = u'it'
104
105
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000106def get_as_zip_package(executable=True):
107 """Returns ZipPackage with this module and all its dependencies.
108
109 If |executable| is True will store run_isolated.py as __main__.py so that
110 zip package is directly executable be python.
111 """
112 # Building a zip package when running from another zip package is
113 # unsupported and probably unneeded.
114 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +0000115 assert THIS_FILE_PATH
116 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000117 package = zip_package.ZipPackage(root=BASE_DIR)
118 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400119 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000120 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800121 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
nodirbe642ff2016-06-09 15:51:51 -0700122 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
tanselle4288c32016-07-28 09:45:40 -0700123 package.add_directory(os.path.join(BASE_DIR, 'libs'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000124 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
125 package.add_directory(os.path.join(BASE_DIR, 'utils'))
126 return package
127
128
maruel03e11842016-07-14 10:50:16 -0700129def make_temp_dir(prefix, root_dir):
130 """Returns a new unique temporary directory."""
131 return unicode(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000132
133
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500134def change_tree_read_only(rootdir, read_only):
135 """Changes the tree read-only bits according to the read_only specification.
136
137 The flag can be 0, 1 or 2, which will affect the possibility to modify files
138 and create or delete files.
139 """
140 if read_only == 2:
141 # Files and directories (except on Windows) are marked read only. This
142 # inhibits modifying, creating or deleting files in the test directory,
143 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400144 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500145 elif read_only == 1:
146 # Files are marked read only but not the directories. This inhibits
147 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400148 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500149 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500150 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500151 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
152 # is not yet changed to verify the hash of the content of the files it is
153 # looking at, so that if a test modifies an input file, the file must be
154 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400155 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500156 else:
157 raise ValueError(
158 'change_tree_read_only(%s, %s): Unknown flag %s' %
159 (rootdir, read_only, read_only))
160
161
nodir90bc8dc2016-06-15 13:35:21 -0700162def process_command(command, out_dir, bot_file):
nodirbe642ff2016-06-09 15:51:51 -0700163 """Replaces variables in a command line.
164
165 Raises:
166 ValueError if a parameter is requested in |command| but its value is not
167 provided.
168 """
maruela9cfd6f2015-09-15 11:03:15 -0700169 def fix(arg):
nodirbe642ff2016-06-09 15:51:51 -0700170 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
171 replace_slash = False
nodir55be77b2016-05-03 09:39:57 -0700172 if ISOLATED_OUTDIR_PARAMETER in arg:
nodirbe642ff2016-06-09 15:51:51 -0700173 if not out_dir:
maruel7f63a272016-07-12 12:40:36 -0700174 raise ValueError(
175 'output directory is requested in command, but not provided; '
176 'please specify one')
nodir55be77b2016-05-03 09:39:57 -0700177 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
nodirbe642ff2016-06-09 15:51:51 -0700178 replace_slash = True
nodir90bc8dc2016-06-15 13:35:21 -0700179 if SWARMING_BOT_FILE_PARAMETER in arg:
180 if bot_file:
181 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
182 replace_slash = True
183 else:
184 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command, but no '
185 'bot_file specified. Leaving parameter unchanged.')
nodirbe642ff2016-06-09 15:51:51 -0700186 if replace_slash:
187 # Replace slashes only if parameters are present
nodir55be77b2016-05-03 09:39:57 -0700188 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
189 arg = arg.replace('/', os.sep)
maruela9cfd6f2015-09-15 11:03:15 -0700190 return arg
191
192 return [fix(arg) for arg in command]
193
194
maruel6be7f9e2015-10-01 12:25:30 -0700195def run_command(command, cwd, tmp_dir, hard_timeout, grace_period):
196 """Runs the command.
197
198 Returns:
199 tuple(process exit code, bool if had a hard timeout)
200 """
maruela9cfd6f2015-09-15 11:03:15 -0700201 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700202
203 env = os.environ.copy()
204 if sys.platform == 'darwin':
tansella4949442016-06-23 22:34:32 -0700205 env['TMPDIR'] = tmp_dir.encode(sys.getfilesystemencoding())
marueleb5fbee2015-09-17 13:01:36 -0700206 elif sys.platform == 'win32':
tansella4949442016-06-23 22:34:32 -0700207 env['TEMP'] = tmp_dir.encode(sys.getfilesystemencoding())
marueleb5fbee2015-09-17 13:01:36 -0700208 else:
tansella4949442016-06-23 22:34:32 -0700209 env['TMP'] = tmp_dir.encode(sys.getfilesystemencoding())
maruel6be7f9e2015-10-01 12:25:30 -0700210 exit_code = None
211 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700212 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700213 proc = None
214 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700215 try:
maruel6be7f9e2015-10-01 12:25:30 -0700216 # TODO(maruel): This code is imperfect. It doesn't handle well signals
217 # during the download phase and there's short windows were things can go
218 # wrong.
219 def handler(signum, _frame):
220 if proc and not had_signal:
221 logging.info('Received signal %d', signum)
222 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700223 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700224
225 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
226 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
227 try:
228 exit_code = proc.wait(hard_timeout or None)
229 except subprocess42.TimeoutExpired:
230 if not had_signal:
231 logging.warning('Hard timeout')
232 had_hard_timeout = True
233 logging.warning('Sending SIGTERM')
234 proc.terminate()
235
236 # Ignore signals in grace period. Forcibly give the grace period to the
237 # child process.
238 if exit_code is None:
239 ignore = lambda *_: None
240 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
241 try:
242 exit_code = proc.wait(grace_period or None)
243 except subprocess42.TimeoutExpired:
244 # Now kill for real. The user can distinguish between the
245 # following states:
246 # - signal but process exited within grace period,
247 # hard_timed_out will be set but the process exit code will be
248 # script provided.
249 # - processed exited late, exit code will be -9 on posix.
250 logging.warning('Grace exhausted; sending SIGKILL')
251 proc.kill()
252 logging.info('Waiting for proces exit')
253 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700254 except OSError:
255 # This is not considered to be an internal error. The executable simply
256 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800257 sys.stderr.write(
258 '<The executable does not exist or a dependent library is missing>\n'
259 '<Check for missing .so/.dll in the .isolate or GN file>\n'
260 '<Command: %s>\n' % command)
261 if os.environ.get('SWARMING_TASK_ID'):
262 # Give an additional hint when running as a swarming task.
263 sys.stderr.write(
264 '<See the task\'s page for commands to help diagnose this issue '
265 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700266 exit_code = 1
267 logging.info(
268 'Command finished with exit code %d (%s)',
269 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700270 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700271
272
maruel4409e302016-07-19 14:25:51 -0700273def fetch_and_map(isolated_hash, storage, cache, outdir, use_symlinks):
274 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700275 start = time.time()
276 bundle = isolateserver.fetch_isolated(
277 isolated_hash=isolated_hash,
278 storage=storage,
279 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700280 outdir=outdir,
281 use_symlinks=use_symlinks)
nodir6f801882016-04-29 14:41:50 -0700282 return bundle, {
283 'duration': time.time() - start,
284 'initial_number_items': cache.initial_number_items,
285 'initial_size': cache.initial_size,
286 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
287 'items_hot': base64.b64encode(
tansell9e04a8d2016-07-28 09:31:59 -0700288 large.pack(sorted(set(cache.used) - set(cache.added)))),
nodir6f801882016-04-29 14:41:50 -0700289 }
290
291
maruela9cfd6f2015-09-15 11:03:15 -0700292def delete_and_upload(storage, out_dir, leak_temp_dir):
293 """Deletes the temporary run directory and uploads results back.
294
295 Returns:
nodir6f801882016-04-29 14:41:50 -0700296 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700297 - outputs_ref: a dict referring to the results archived back to the isolated
298 server, if applicable.
299 - success: False if something occurred that means that the task must
300 forcibly be considered a failure, e.g. zombie processes were left
301 behind.
nodir6f801882016-04-29 14:41:50 -0700302 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700303 """
304
305 # Upload out_dir and generate a .isolated file out of this directory. It is
306 # only done if files were written in the directory.
307 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700308 cold = []
309 hot = []
nodir6f801882016-04-29 14:41:50 -0700310 start = time.time()
311
maruel12e30012015-10-09 11:55:35 -0700312 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700313 with tools.Profiler('ArchiveOutput'):
314 try:
maruel064c0a32016-04-05 11:47:15 -0700315 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700316 storage, [out_dir], None)
317 outputs_ref = {
318 'isolated': results[0][0],
319 'isolatedserver': storage.location,
320 'namespace': storage.namespace,
321 }
maruel064c0a32016-04-05 11:47:15 -0700322 cold = sorted(i.size for i in f_cold)
323 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700324 except isolateserver.Aborted:
325 # This happens when a signal SIGTERM was received while uploading data.
326 # There is 2 causes:
327 # - The task was too slow and was about to be killed anyway due to
328 # exceeding the hard timeout.
329 # - The amount of data uploaded back is very large and took too much
330 # time to archive.
331 sys.stderr.write('Received SIGTERM while uploading')
332 # Re-raise, so it will be treated as an internal failure.
333 raise
nodir6f801882016-04-29 14:41:50 -0700334
335 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700336 try:
maruel12e30012015-10-09 11:55:35 -0700337 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700338 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700339 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700340 else:
341 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700342 except OSError as e:
343 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700344 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700345 stats = {
346 'duration': time.time() - start,
347 'items_cold': base64.b64encode(large.pack(cold)),
348 'items_hot': base64.b64encode(large.pack(hot)),
349 }
350 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700351
352
marueleb5fbee2015-09-17 13:01:36 -0700353def map_and_run(
nodir56efa452016-10-12 12:17:39 -0700354 command, isolated_hash, storage, isolate_cache, leak_temp_dir, root_dir,
maruel4409e302016-07-19 14:25:51 -0700355 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn,
356 use_symlinks):
nodir55be77b2016-05-03 09:39:57 -0700357 """Runs a command with optional isolated input/output.
358
359 See run_tha_test for argument documentation.
360
361 Returns metadata about the result.
362 """
nodir56efa452016-10-12 12:17:39 -0700363 assert root_dir or root_dir is None
nodir55be77b2016-05-03 09:39:57 -0700364 assert bool(command) ^ bool(isolated_hash)
maruela9cfd6f2015-09-15 11:03:15 -0700365 result = {
maruel064c0a32016-04-05 11:47:15 -0700366 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700367 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700368 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700369 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700370 'stats': {
nodir55715712016-06-03 12:28:19 -0700371 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700372 # 'cipd': {
373 # 'duration': 0.,
374 # 'get_client_duration': 0.,
375 # },
nodir55715712016-06-03 12:28:19 -0700376 # 'download': {
377 # 'duration': 0.,
378 # 'initial_number_items': 0,
379 # 'initial_size': 0,
380 # 'items_cold': '<large.pack()>',
381 # 'items_hot': '<large.pack()>',
382 # },
383 # 'upload': {
384 # 'duration': 0.,
385 # 'items_cold': '<large.pack()>',
386 # 'items_hot': '<large.pack()>',
387 # },
maruel064c0a32016-04-05 11:47:15 -0700388 # },
389 },
iannucci96fcccc2016-08-30 15:52:22 -0700390 # 'cipd_pins': {
391 # 'packages': [
392 # {'package_name': ..., 'version': ..., 'path': ...},
393 # ...
394 # ],
395 # 'client_package': {'package_name': ..., 'version': ...},
396 # },
maruela9cfd6f2015-09-15 11:03:15 -0700397 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700398 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700399 }
nodirbe642ff2016-06-09 15:51:51 -0700400
marueleb5fbee2015-09-17 13:01:36 -0700401 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700402 file_path.ensure_tree(root_dir, 0700)
nodir56efa452016-10-12 12:17:39 -0700403 elif isolate_cache.cache_dir:
404 root_dir = os.path.dirname(isolate_cache.cache_dir)
maruele2f2cb82016-07-13 14:41:03 -0700405 # See comment for these constants.
406 run_dir = make_temp_dir(ISOLATED_RUN_DIR, root_dir)
maruel03e11842016-07-14 10:50:16 -0700407 # storage should be normally set but don't crash if it is not. This can happen
408 # as Swarming task can run without an isolate server.
maruele2f2cb82016-07-13 14:41:03 -0700409 out_dir = make_temp_dir(ISOLATED_OUT_DIR, root_dir) if storage else None
410 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, root_dir)
nodir55be77b2016-05-03 09:39:57 -0700411 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700412
nodir55be77b2016-05-03 09:39:57 -0700413 try:
iannucci96fcccc2016-08-30 15:52:22 -0700414 cipd_info = install_packages_fn(run_dir)
415 if cipd_info:
416 result['stats']['cipd'] = cipd_info['stats']
417 result['cipd_pins'] = cipd_info['cipd_pins']
nodir90bc8dc2016-06-15 13:35:21 -0700418
nodir55be77b2016-05-03 09:39:57 -0700419 if isolated_hash:
nodir55715712016-06-03 12:28:19 -0700420 isolated_stats = result['stats'].setdefault('isolated', {})
maruel4409e302016-07-19 14:25:51 -0700421 bundle, isolated_stats['download'] = fetch_and_map(
nodir55be77b2016-05-03 09:39:57 -0700422 isolated_hash=isolated_hash,
423 storage=storage,
nodir56efa452016-10-12 12:17:39 -0700424 cache=isolate_cache,
maruel4409e302016-07-19 14:25:51 -0700425 outdir=run_dir,
426 use_symlinks=use_symlinks)
nodir55be77b2016-05-03 09:39:57 -0700427 if not bundle.command:
428 # Handle this as a task failure, not an internal failure.
429 sys.stderr.write(
430 '<The .isolated doesn\'t declare any command to run!>\n'
431 '<Check your .isolate for missing \'command\' variable>\n')
432 if os.environ.get('SWARMING_TASK_ID'):
433 # Give an additional hint when running as a swarming task.
434 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
435 result['exit_code'] = 1
436 return result
437
438 change_tree_read_only(run_dir, bundle.read_only)
439 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
440 command = bundle.command + extra_args
nodirbe642ff2016-06-09 15:51:51 -0700441
nodir34d673c2016-05-24 09:30:48 -0700442 command = tools.fix_python_path(command)
nodir90bc8dc2016-06-15 13:35:21 -0700443 command = process_command(command, out_dir, bot_file)
maruela9cfd6f2015-09-15 11:03:15 -0700444 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700445
maruel064c0a32016-04-05 11:47:15 -0700446 sys.stdout.flush()
447 start = time.time()
448 try:
449 result['exit_code'], result['had_hard_timeout'] = run_command(
nodirbe642ff2016-06-09 15:51:51 -0700450 command, cwd, tmp_dir, hard_timeout, grace_period)
maruel064c0a32016-04-05 11:47:15 -0700451 finally:
452 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700453 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700454 # An internal error occurred. Report accordingly so the swarming task will
455 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700456 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700457 result['internal_failure'] = str(e)
458 on_error.report(None)
459 finally:
460 try:
nodir32a1ec12016-10-26 18:34:07 -0700461 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700462 if leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700463 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700464 logging.warning(
465 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700466 else:
maruel84537cb2015-10-16 14:21:28 -0700467 # On Windows rmtree(run_dir) call above has a synchronization effect: it
468 # finishes only when all task child processes terminate (since a running
469 # process locks *.exe file). Examine out_dir only after that call
470 # completes (since child processes may write to out_dir too and we need
471 # to wait for them to finish).
472 if fs.isdir(run_dir):
473 try:
474 success = file_path.rmtree(run_dir)
475 except OSError as e:
476 logging.error('Failure with %s', e)
477 success = False
478 if not success:
479 print >> sys.stderr, (
480 'Failed to delete the run directory, forcibly failing\n'
481 'the task because of it. No zombie process can outlive a\n'
482 'successful task run and still be marked as successful.\n'
483 'Fix your stuff.')
484 if result['exit_code'] == 0:
485 result['exit_code'] = 1
486 if fs.isdir(tmp_dir):
487 try:
488 success = file_path.rmtree(tmp_dir)
489 except OSError as e:
490 logging.error('Failure with %s', e)
491 success = False
492 if not success:
493 print >> sys.stderr, (
494 'Failed to delete the temporary directory, forcibly failing\n'
495 'the task because of it. No zombie process can outlive a\n'
496 'successful task run and still be marked as successful.\n'
497 'Fix your stuff.')
498 if result['exit_code'] == 0:
499 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700500
marueleb5fbee2015-09-17 13:01:36 -0700501 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700502 if out_dir:
nodir55715712016-06-03 12:28:19 -0700503 isolated_stats = result['stats'].setdefault('isolated', {})
504 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700505 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700506 if not success and result['exit_code'] == 0:
507 result['exit_code'] = 1
508 except Exception as e:
509 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700510 if out_dir:
511 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700512 result['internal_failure'] = str(e)
513 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500514
515
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400516def run_tha_test(
nodir6b945692016-10-19 19:09:06 -0700517 command, isolated_hash, storage, isolate_cache, leak_temp_dir, result_json,
bpastene3ae09522016-06-10 17:12:59 -0700518 root_dir, hard_timeout, grace_period, bot_file, extra_args,
maruel4409e302016-07-19 14:25:51 -0700519 install_packages_fn, use_symlinks):
nodir55be77b2016-05-03 09:39:57 -0700520 """Runs an executable and records execution metadata.
521
522 Either command or isolated_hash must be specified.
523
524 If isolated_hash is specified, downloads the dependencies in the cache,
525 hardlinks them into a temporary directory and runs the command specified in
526 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500527
528 A temporary directory is created to hold the output files. The content inside
529 this directory will be uploaded back to |storage| packaged as a .isolated
530 file.
531
532 Arguments:
nodir55be77b2016-05-03 09:39:57 -0700533 command: the command to run, a list of strings. Mutually exclusive with
534 isolated_hash.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500535 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500536 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700537 The command specified in the .isolated is executed.
538 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500539 storage: an isolateserver.Storage object to retrieve remote objects. This
540 object has a reference to an isolateserver.StorageApi, which does
541 the actual I/O.
nodir6b945692016-10-19 19:09:06 -0700542 isolate_cache: an isolateserver.LocalCache to keep from retrieving the
543 same objects constantly by caching the objects retrieved.
544 Can be on-disk or in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700545 leak_temp_dir: if true, the temporary directory will be deliberately leaked
546 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700547 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700548 exit code is always 0 unless an internal error occurred.
nodir90bc8dc2016-06-15 13:35:21 -0700549 root_dir: path to the directory to use to create the temporary directory. If
marueleb5fbee2015-09-17 13:01:36 -0700550 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700551 hard_timeout: kills the process if it lasts more than this amount of
552 seconds.
553 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500554 extra_args: optional arguments to add to the command stated in the .isolate
nodir55be77b2016-05-03 09:39:57 -0700555 file. Ignored if isolate_hash is empty.
iannucci96fcccc2016-08-30 15:52:22 -0700556 install_packages_fn: function (dir) => {"stats": cipd_stats, "pins":
557 cipd_pins}. Installs packages.
maruel4409e302016-07-19 14:25:51 -0700558 use_symlinks: create tree with symlinks instead of hardlinks.
maruela9cfd6f2015-09-15 11:03:15 -0700559
560 Returns:
561 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000562 """
nodir55be77b2016-05-03 09:39:57 -0700563 assert bool(command) ^ bool(isolated_hash)
564 extra_args = extra_args or []
nodirbe642ff2016-06-09 15:51:51 -0700565
nodir55be77b2016-05-03 09:39:57 -0700566 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
567 assert storage is not None, 'storage is None although outdir is specified'
568
maruela76b9ee2015-12-15 06:18:08 -0800569 if result_json:
570 # Write a json output file right away in case we get killed.
571 result = {
572 'exit_code': None,
573 'had_hard_timeout': False,
574 'internal_failure': 'Was terminated before completion',
575 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700576 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800577 }
578 tools.write_json(result_json, result, dense=True)
579
maruela9cfd6f2015-09-15 11:03:15 -0700580 # run_isolated exit code. Depends on if result_json is used or not.
581 result = map_and_run(
nodir6b945692016-10-19 19:09:06 -0700582 command, isolated_hash, storage, isolate_cache, leak_temp_dir, root_dir,
maruel4409e302016-07-19 14:25:51 -0700583 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn,
584 use_symlinks)
maruela9cfd6f2015-09-15 11:03:15 -0700585 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700586
maruela9cfd6f2015-09-15 11:03:15 -0700587 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700588 # We've found tests to delete 'work' when quitting, causing an exception
589 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700590 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700591 tools.write_json(result_json, result, dense=True)
592 # Only return 1 if there was an internal error.
593 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000594
maruela9cfd6f2015-09-15 11:03:15 -0700595 # Marshall into old-style inline output.
596 if result['outputs_ref']:
597 data = {
598 'hash': result['outputs_ref']['isolated'],
599 'namespace': result['outputs_ref']['namespace'],
600 'storage': result['outputs_ref']['isolatedserver'],
601 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500602 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700603 print(
604 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
605 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800606 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700607 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000608
609
nodir90bc8dc2016-06-15 13:35:21 -0700610def install_packages(
nodirff531b42016-06-23 13:05:06 -0700611 run_dir, packages, service_url, client_package_name,
nodir90bc8dc2016-06-15 13:35:21 -0700612 client_version, cache_dir=None, timeout=None):
iannucci96fcccc2016-08-30 15:52:22 -0700613 """Installs packages. Returns stats, cipd client info and pins.
614
615 pins and the cipd client info are in the form of:
616 [
617 {
618 "path": path, "package_name": package_name, "version": version,
619 },
620 ...
621 ]
622 (the cipd client info is a single dictionary instead of a list)
623
624 such that they correspond 1:1 to all input package arguments from the command
625 line. These dictionaries make their all the way back to swarming, where they
626 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700627
628 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700629 run_dir (str): root of installation.
iannucci96fcccc2016-08-30 15:52:22 -0700630 packages: packages to install, list [(path, package_name, version), ...]
nodirbe642ff2016-06-09 15:51:51 -0700631 service_url (str): CIPD server url, e.g.
632 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700633 client_package_name (str): CIPD package name of CIPD client.
634 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700635 cache_dir (str): where to keep cache of cipd clients, packages and tags.
636 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700637 """
638 assert cache_dir
nodirff531b42016-06-23 13:05:06 -0700639 if not packages:
nodir90bc8dc2016-06-15 13:35:21 -0700640 return None
641
nodirbe642ff2016-06-09 15:51:51 -0700642 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700643 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700644 cache_dir = os.path.abspath(cache_dir)
645
nodir90bc8dc2016-06-15 13:35:21 -0700646 run_dir = os.path.abspath(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700647
iannucci96fcccc2016-08-30 15:52:22 -0700648 package_pins = [None]*len(packages)
649 def insert_pin(path, name, version, idx):
650 path = path.replace(os.path.sep, '/')
651 package_pins[idx] = {
652 'package_name': name,
653 'path': path,
654 'version': version,
655 }
656
nodirbe642ff2016-06-09 15:51:51 -0700657 get_client_start = time.time()
658 client_manager = cipd.get_client(
659 service_url, client_package_name, client_version, cache_dir,
660 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700661
662 by_path = collections.defaultdict(list)
663 for i, (path, name, version) in enumerate(packages):
664 path = path.replace('/', os.path.sep)
665 by_path[path].append((name, version, i))
666
nodirbe642ff2016-06-09 15:51:51 -0700667 with client_manager as client:
iannucci96fcccc2016-08-30 15:52:22 -0700668 client_package = {
669 'package_name': client.package_name,
670 'version': client.instance_id,
671 }
nodirbe642ff2016-06-09 15:51:51 -0700672 get_client_duration = time.time() - get_client_start
iannucci96fcccc2016-08-30 15:52:22 -0700673 for path, pkgs in sorted(by_path.iteritems()):
nodir90bc8dc2016-06-15 13:35:21 -0700674 site_root = os.path.abspath(os.path.join(run_dir, path))
675 if not site_root.startswith(run_dir):
676 raise cipd.Error('Invalid CIPD package path "%s"' % path)
677
678 # Do not clean site_root before installation because it may contain other
679 # site roots.
680 file_path.ensure_tree(site_root, 0770)
iannucci96fcccc2016-08-30 15:52:22 -0700681 pins = client.ensure(
682 site_root, [(name, vers) for name, vers, _ in pkgs],
nodirbe642ff2016-06-09 15:51:51 -0700683 cache_dir=os.path.join(cache_dir, 'cipd_internal'),
684 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700685 for i, pin in enumerate(pins):
686 insert_pin(path, pin[0], pin[1], pkgs[i][2])
nodirbe642ff2016-06-09 15:51:51 -0700687 file_path.make_tree_files_read_only(site_root)
nodir90bc8dc2016-06-15 13:35:21 -0700688
689 total_duration = time.time() - start
690 logging.info(
691 'Installing CIPD client and packages took %d seconds', total_duration)
692
iannucci96fcccc2016-08-30 15:52:22 -0700693 assert None not in package_pins
694
nodir90bc8dc2016-06-15 13:35:21 -0700695 return {
iannucci96fcccc2016-08-30 15:52:22 -0700696 'stats': {
697 'duration': total_duration,
698 'get_client_duration': get_client_duration,
699 },
700 'cipd_pins': {
701 'client_package': client_package,
702 'packages': package_pins,
703 }
nodir90bc8dc2016-06-15 13:35:21 -0700704 }
nodirbe642ff2016-06-09 15:51:51 -0700705
706
707def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400708 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700709 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000710 version=__version__,
711 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700712 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700713 '--clean', action='store_true',
714 help='Cleans the cache, trimming it necessary and remove corrupted items '
715 'and returns without executing anything; use with -v to know what '
716 'was done')
717 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -0700718 '--no-clean', action='store_true',
719 help='Do not clean the cache automatically on startup. This is meant for '
720 'bots where a separate execution with --clean was done earlier so '
721 'doing it again is redundant')
722 parser.add_option(
maruel4409e302016-07-19 14:25:51 -0700723 '--use-symlinks', action='store_true',
724 help='Use symlinks instead of hardlinks')
725 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700726 '--json',
727 help='dump output metadata to json file. When used, run_isolated returns '
728 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700729 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800730 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700731 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800732 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700733 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700734 parser.add_option(
735 '--bot-file',
736 help='Path to a file describing the state of the host. The content is '
737 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -0700738 parser.add_option(
739 '-a', '--argsfile',
740 # This is actually handled in parse_args; it's included here purely so it
741 # can make it into the help text.
742 help='Specify a file containing a JSON array of arguments to this '
743 'script. If --argsfile is provided, no other argument may be '
744 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500745 data_group = optparse.OptionGroup(parser, 'Data source')
746 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500747 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700748 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500749 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500750 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000751
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400752 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700753
754 cipd.add_cipd_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000755
Kenneth Russell61d42352014-09-15 11:41:16 -0700756 debug_group = optparse.OptionGroup(parser, 'Debugging')
757 debug_group.add_option(
758 '--leak-temp-dir',
759 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700760 help='Deliberately leak isolate\'s temp dir for later examination. '
761 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700762 debug_group.add_option(
763 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700764 parser.add_option_group(debug_group)
765
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800766 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700767
768 parser.set_defaults(cache='cache', cipd_cache='cipd_cache')
769 return parser
770
771
aludwin7556e0c2016-10-26 08:46:10 -0700772def parse_args(args):
773 # Create a fake mini-parser just to get out the "-a" command. Note that
774 # it's not documented here; instead, it's documented in create_option_parser
775 # even though that parser will never actually get to parse it. This is
776 # because --argsfile is exclusive with all other options and arguments.
777 file_argparse = argparse.ArgumentParser(add_help=False)
778 file_argparse.add_argument('-a', '--argsfile')
779 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
780 if file_args.argsfile:
781 if nonfile_args:
782 file_argparse.error('Can\'t specify --argsfile with'
783 'any other arguments (%s)' % nonfile_args)
784 try:
785 with open(file_args.argsfile, 'r') as f:
786 args = json.load(f)
787 except (IOError, OSError, ValueError) as e:
788 # We don't need to error out here - "args" is now empty,
789 # so the call below to parser.parse_args(args) will fail
790 # and print the full help text.
791 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
792
793 # Even if we failed to read the args, just call the normal parser now since it
794 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -0700795 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500796 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -0700797 return (parser, options, args)
798
799
800def main(args):
801 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700802
nodir56efa452016-10-12 12:17:39 -0700803 isolated_cache = isolateserver.process_cache_options(options)
maruel36a963d2016-04-08 17:15:49 -0700804 if options.clean:
805 if options.isolated:
806 parser.error('Can\'t use --isolated with --clean.')
807 if options.isolate_server:
808 parser.error('Can\'t use --isolate-server with --clean.')
809 if options.json:
810 parser.error('Can\'t use --json with --clean.')
nodir56efa452016-10-12 12:17:39 -0700811 isolated_cache.cleanup()
maruel36a963d2016-04-08 17:15:49 -0700812 return 0
maruel2e8d0f52016-07-16 07:51:29 -0700813 if not options.no_clean:
nodir56efa452016-10-12 12:17:39 -0700814 isolated_cache.cleanup()
maruel36a963d2016-04-08 17:15:49 -0700815
nodir55be77b2016-05-03 09:39:57 -0700816 if not options.isolated and not args:
817 parser.error('--isolated or command to run is required.')
818
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800819 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700820
821 isolateserver.process_isolate_server_options(
822 parser, options, True, False)
823 if not options.isolate_server:
824 if options.isolated:
825 parser.error('--isolated requires --isolate-server')
826 if ISOLATED_OUTDIR_PARAMETER in args:
827 parser.error(
828 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000829
nodir90bc8dc2016-06-15 13:35:21 -0700830 if options.root_dir:
831 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -0700832 if options.json:
833 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700834
nodirbe642ff2016-06-09 15:51:51 -0700835 cipd.validate_cipd_options(parser, options)
836
nodir90bc8dc2016-06-15 13:35:21 -0700837 install_packages_fn = lambda run_dir: install_packages(
nodirff531b42016-06-23 13:05:06 -0700838 run_dir, cipd.parse_package_args(options.cipd_packages),
839 options.cipd_server, options.cipd_client_package,
840 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -0700841
842 try:
nodir90bc8dc2016-06-15 13:35:21 -0700843 command = [] if options.isolated else args
844 if options.isolate_server:
845 storage = isolateserver.get_storage(
846 options.isolate_server, options.namespace)
847 with storage:
nodir56efa452016-10-12 12:17:39 -0700848 # Hashing schemes used by |storage| and |isolated_cache| MUST match.
849 assert storage.hash_algo == isolated_cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -0700850 return run_tha_test(
nodir56efa452016-10-12 12:17:39 -0700851 command, options.isolated, storage, isolated_cache,
852 options.leak_temp_dir, options.json, options.root_dir,
853 options.hard_timeout, options.grace_period, options.bot_file, args,
854 install_packages_fn, options.use_symlinks)
maruel4409e302016-07-19 14:25:51 -0700855 return run_tha_test(
nodir56efa452016-10-12 12:17:39 -0700856 command, options.isolated, None, isolated_cache, options.leak_temp_dir,
maruel4409e302016-07-19 14:25:51 -0700857 options.json, options.root_dir, options.hard_timeout,
858 options.grace_period, options.bot_file, args, install_packages_fn,
859 options.use_symlinks)
nodirbe642ff2016-06-09 15:51:51 -0700860 except cipd.Error as ex:
861 print >> sys.stderr, ex.message
862 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000863
864
865if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -0700866 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000867 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000868 fix_encoding.fix_encoding()
maruel4409e302016-07-19 14:25:51 -0700869 file_path.enable_symlink()
aludwin7556e0c2016-10-26 08:46:10 -0700870
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500871 sys.exit(main(sys.argv[1:]))