blob: bf580e271eda208a28ba84b3f44eff05283193d4 [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:
461 if leak_temp_dir:
462 logging.warning(
463 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700464 else:
maruel84537cb2015-10-16 14:21:28 -0700465 # On Windows rmtree(run_dir) call above has a synchronization effect: it
466 # finishes only when all task child processes terminate (since a running
467 # process locks *.exe file). Examine out_dir only after that call
468 # completes (since child processes may write to out_dir too and we need
469 # to wait for them to finish).
470 if fs.isdir(run_dir):
471 try:
472 success = file_path.rmtree(run_dir)
473 except OSError as e:
474 logging.error('Failure with %s', e)
475 success = False
476 if not success:
477 print >> sys.stderr, (
478 'Failed to delete the run directory, forcibly failing\n'
479 'the task because of it. No zombie process can outlive a\n'
480 'successful task run and still be marked as successful.\n'
481 'Fix your stuff.')
482 if result['exit_code'] == 0:
483 result['exit_code'] = 1
484 if fs.isdir(tmp_dir):
485 try:
486 success = file_path.rmtree(tmp_dir)
487 except OSError as e:
488 logging.error('Failure with %s', e)
489 success = False
490 if not success:
491 print >> sys.stderr, (
492 'Failed to delete the temporary directory, forcibly failing\n'
493 'the task because of it. No zombie process can outlive a\n'
494 'successful task run and still be marked as successful.\n'
495 'Fix your stuff.')
496 if result['exit_code'] == 0:
497 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700498
marueleb5fbee2015-09-17 13:01:36 -0700499 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700500 if out_dir:
nodir55715712016-06-03 12:28:19 -0700501 isolated_stats = result['stats'].setdefault('isolated', {})
502 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700503 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700504 if not success and result['exit_code'] == 0:
505 result['exit_code'] = 1
506 except Exception as e:
507 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700508 if out_dir:
509 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700510 result['internal_failure'] = str(e)
511 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500512
513
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400514def run_tha_test(
nodir6b945692016-10-19 19:09:06 -0700515 command, isolated_hash, storage, isolate_cache, leak_temp_dir, result_json,
bpastene3ae09522016-06-10 17:12:59 -0700516 root_dir, hard_timeout, grace_period, bot_file, extra_args,
maruel4409e302016-07-19 14:25:51 -0700517 install_packages_fn, use_symlinks):
nodir55be77b2016-05-03 09:39:57 -0700518 """Runs an executable and records execution metadata.
519
520 Either command or isolated_hash must be specified.
521
522 If isolated_hash is specified, downloads the dependencies in the cache,
523 hardlinks them into a temporary directory and runs the command specified in
524 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500525
526 A temporary directory is created to hold the output files. The content inside
527 this directory will be uploaded back to |storage| packaged as a .isolated
528 file.
529
530 Arguments:
nodir55be77b2016-05-03 09:39:57 -0700531 command: the command to run, a list of strings. Mutually exclusive with
532 isolated_hash.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500533 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500534 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700535 The command specified in the .isolated is executed.
536 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500537 storage: an isolateserver.Storage object to retrieve remote objects. This
538 object has a reference to an isolateserver.StorageApi, which does
539 the actual I/O.
nodir6b945692016-10-19 19:09:06 -0700540 isolate_cache: an isolateserver.LocalCache to keep from retrieving the
541 same objects constantly by caching the objects retrieved.
542 Can be on-disk or in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700543 leak_temp_dir: if true, the temporary directory will be deliberately leaked
544 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700545 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700546 exit code is always 0 unless an internal error occurred.
nodir90bc8dc2016-06-15 13:35:21 -0700547 root_dir: path to the directory to use to create the temporary directory. If
marueleb5fbee2015-09-17 13:01:36 -0700548 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700549 hard_timeout: kills the process if it lasts more than this amount of
550 seconds.
551 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500552 extra_args: optional arguments to add to the command stated in the .isolate
nodir55be77b2016-05-03 09:39:57 -0700553 file. Ignored if isolate_hash is empty.
iannucci96fcccc2016-08-30 15:52:22 -0700554 install_packages_fn: function (dir) => {"stats": cipd_stats, "pins":
555 cipd_pins}. Installs packages.
maruel4409e302016-07-19 14:25:51 -0700556 use_symlinks: create tree with symlinks instead of hardlinks.
maruela9cfd6f2015-09-15 11:03:15 -0700557
558 Returns:
559 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000560 """
nodir55be77b2016-05-03 09:39:57 -0700561 assert bool(command) ^ bool(isolated_hash)
562 extra_args = extra_args or []
nodirbe642ff2016-06-09 15:51:51 -0700563
nodir55be77b2016-05-03 09:39:57 -0700564 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
565 assert storage is not None, 'storage is None although outdir is specified'
566
maruela76b9ee2015-12-15 06:18:08 -0800567 if result_json:
568 # Write a json output file right away in case we get killed.
569 result = {
570 'exit_code': None,
571 'had_hard_timeout': False,
572 'internal_failure': 'Was terminated before completion',
573 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700574 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800575 }
576 tools.write_json(result_json, result, dense=True)
577
maruela9cfd6f2015-09-15 11:03:15 -0700578 # run_isolated exit code. Depends on if result_json is used or not.
579 result = map_and_run(
nodir6b945692016-10-19 19:09:06 -0700580 command, isolated_hash, storage, isolate_cache, leak_temp_dir, root_dir,
maruel4409e302016-07-19 14:25:51 -0700581 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn,
582 use_symlinks)
maruela9cfd6f2015-09-15 11:03:15 -0700583 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700584
maruela9cfd6f2015-09-15 11:03:15 -0700585 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700586 # We've found tests to delete 'work' when quitting, causing an exception
587 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700588 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700589 tools.write_json(result_json, result, dense=True)
590 # Only return 1 if there was an internal error.
591 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000592
maruela9cfd6f2015-09-15 11:03:15 -0700593 # Marshall into old-style inline output.
594 if result['outputs_ref']:
595 data = {
596 'hash': result['outputs_ref']['isolated'],
597 'namespace': result['outputs_ref']['namespace'],
598 'storage': result['outputs_ref']['isolatedserver'],
599 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500600 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700601 print(
602 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
603 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800604 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700605 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000606
607
nodir90bc8dc2016-06-15 13:35:21 -0700608def install_packages(
nodirff531b42016-06-23 13:05:06 -0700609 run_dir, packages, service_url, client_package_name,
nodir90bc8dc2016-06-15 13:35:21 -0700610 client_version, cache_dir=None, timeout=None):
iannucci96fcccc2016-08-30 15:52:22 -0700611 """Installs packages. Returns stats, cipd client info and pins.
612
613 pins and the cipd client info are in the form of:
614 [
615 {
616 "path": path, "package_name": package_name, "version": version,
617 },
618 ...
619 ]
620 (the cipd client info is a single dictionary instead of a list)
621
622 such that they correspond 1:1 to all input package arguments from the command
623 line. These dictionaries make their all the way back to swarming, where they
624 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700625
626 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700627 run_dir (str): root of installation.
iannucci96fcccc2016-08-30 15:52:22 -0700628 packages: packages to install, list [(path, package_name, version), ...]
nodirbe642ff2016-06-09 15:51:51 -0700629 service_url (str): CIPD server url, e.g.
630 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700631 client_package_name (str): CIPD package name of CIPD client.
632 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700633 cache_dir (str): where to keep cache of cipd clients, packages and tags.
634 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700635 """
636 assert cache_dir
nodirff531b42016-06-23 13:05:06 -0700637 if not packages:
nodir90bc8dc2016-06-15 13:35:21 -0700638 return None
639
nodirbe642ff2016-06-09 15:51:51 -0700640 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700641 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700642 cache_dir = os.path.abspath(cache_dir)
643
nodir90bc8dc2016-06-15 13:35:21 -0700644 run_dir = os.path.abspath(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700645
iannucci96fcccc2016-08-30 15:52:22 -0700646 package_pins = [None]*len(packages)
647 def insert_pin(path, name, version, idx):
648 path = path.replace(os.path.sep, '/')
649 package_pins[idx] = {
650 'package_name': name,
651 'path': path,
652 'version': version,
653 }
654
nodirbe642ff2016-06-09 15:51:51 -0700655 get_client_start = time.time()
656 client_manager = cipd.get_client(
657 service_url, client_package_name, client_version, cache_dir,
658 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700659
660 by_path = collections.defaultdict(list)
661 for i, (path, name, version) in enumerate(packages):
662 path = path.replace('/', os.path.sep)
663 by_path[path].append((name, version, i))
664
nodirbe642ff2016-06-09 15:51:51 -0700665 with client_manager as client:
iannucci96fcccc2016-08-30 15:52:22 -0700666 client_package = {
667 'package_name': client.package_name,
668 'version': client.instance_id,
669 }
nodirbe642ff2016-06-09 15:51:51 -0700670 get_client_duration = time.time() - get_client_start
iannucci96fcccc2016-08-30 15:52:22 -0700671 for path, pkgs in sorted(by_path.iteritems()):
nodir90bc8dc2016-06-15 13:35:21 -0700672 site_root = os.path.abspath(os.path.join(run_dir, path))
673 if not site_root.startswith(run_dir):
674 raise cipd.Error('Invalid CIPD package path "%s"' % path)
675
676 # Do not clean site_root before installation because it may contain other
677 # site roots.
678 file_path.ensure_tree(site_root, 0770)
iannucci96fcccc2016-08-30 15:52:22 -0700679 pins = client.ensure(
680 site_root, [(name, vers) for name, vers, _ in pkgs],
nodirbe642ff2016-06-09 15:51:51 -0700681 cache_dir=os.path.join(cache_dir, 'cipd_internal'),
682 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700683 for i, pin in enumerate(pins):
684 insert_pin(path, pin[0], pin[1], pkgs[i][2])
nodirbe642ff2016-06-09 15:51:51 -0700685 file_path.make_tree_files_read_only(site_root)
nodir90bc8dc2016-06-15 13:35:21 -0700686
687 total_duration = time.time() - start
688 logging.info(
689 'Installing CIPD client and packages took %d seconds', total_duration)
690
iannucci96fcccc2016-08-30 15:52:22 -0700691 assert None not in package_pins
692
nodir90bc8dc2016-06-15 13:35:21 -0700693 return {
iannucci96fcccc2016-08-30 15:52:22 -0700694 'stats': {
695 'duration': total_duration,
696 'get_client_duration': get_client_duration,
697 },
698 'cipd_pins': {
699 'client_package': client_package,
700 'packages': package_pins,
701 }
nodir90bc8dc2016-06-15 13:35:21 -0700702 }
nodirbe642ff2016-06-09 15:51:51 -0700703
704
705def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400706 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700707 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000708 version=__version__,
709 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700710 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700711 '--clean', action='store_true',
712 help='Cleans the cache, trimming it necessary and remove corrupted items '
713 'and returns without executing anything; use with -v to know what '
714 'was done')
715 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -0700716 '--no-clean', action='store_true',
717 help='Do not clean the cache automatically on startup. This is meant for '
718 'bots where a separate execution with --clean was done earlier so '
719 'doing it again is redundant')
720 parser.add_option(
maruel4409e302016-07-19 14:25:51 -0700721 '--use-symlinks', action='store_true',
722 help='Use symlinks instead of hardlinks')
723 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700724 '--json',
725 help='dump output metadata to json file. When used, run_isolated returns '
726 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700727 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800728 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700729 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800730 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700731 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700732 parser.add_option(
733 '--bot-file',
734 help='Path to a file describing the state of the host. The content is '
735 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -0700736 parser.add_option(
737 '-a', '--argsfile',
738 # This is actually handled in parse_args; it's included here purely so it
739 # can make it into the help text.
740 help='Specify a file containing a JSON array of arguments to this '
741 'script. If --argsfile is provided, no other argument may be '
742 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500743 data_group = optparse.OptionGroup(parser, 'Data source')
744 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500745 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700746 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500747 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500748 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000749
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400750 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700751
752 cipd.add_cipd_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000753
Kenneth Russell61d42352014-09-15 11:41:16 -0700754 debug_group = optparse.OptionGroup(parser, 'Debugging')
755 debug_group.add_option(
756 '--leak-temp-dir',
757 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700758 help='Deliberately leak isolate\'s temp dir for later examination. '
759 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700760 debug_group.add_option(
761 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700762 parser.add_option_group(debug_group)
763
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800764 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700765
766 parser.set_defaults(cache='cache', cipd_cache='cipd_cache')
767 return parser
768
769
aludwin7556e0c2016-10-26 08:46:10 -0700770def parse_args(args):
771 # Create a fake mini-parser just to get out the "-a" command. Note that
772 # it's not documented here; instead, it's documented in create_option_parser
773 # even though that parser will never actually get to parse it. This is
774 # because --argsfile is exclusive with all other options and arguments.
775 file_argparse = argparse.ArgumentParser(add_help=False)
776 file_argparse.add_argument('-a', '--argsfile')
777 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
778 if file_args.argsfile:
779 if nonfile_args:
780 file_argparse.error('Can\'t specify --argsfile with'
781 'any other arguments (%s)' % nonfile_args)
782 try:
783 with open(file_args.argsfile, 'r') as f:
784 args = json.load(f)
785 except (IOError, OSError, ValueError) as e:
786 # We don't need to error out here - "args" is now empty,
787 # so the call below to parser.parse_args(args) will fail
788 # and print the full help text.
789 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
790
791 # Even if we failed to read the args, just call the normal parser now since it
792 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -0700793 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500794 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -0700795 return (parser, options, args)
796
797
798def main(args):
799 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700800
nodir56efa452016-10-12 12:17:39 -0700801 isolated_cache = isolateserver.process_cache_options(options)
maruel36a963d2016-04-08 17:15:49 -0700802 if options.clean:
803 if options.isolated:
804 parser.error('Can\'t use --isolated with --clean.')
805 if options.isolate_server:
806 parser.error('Can\'t use --isolate-server with --clean.')
807 if options.json:
808 parser.error('Can\'t use --json with --clean.')
nodir56efa452016-10-12 12:17:39 -0700809 isolated_cache.cleanup()
maruel36a963d2016-04-08 17:15:49 -0700810 return 0
maruel2e8d0f52016-07-16 07:51:29 -0700811 if not options.no_clean:
nodir56efa452016-10-12 12:17:39 -0700812 isolated_cache.cleanup()
maruel36a963d2016-04-08 17:15:49 -0700813
nodir55be77b2016-05-03 09:39:57 -0700814 if not options.isolated and not args:
815 parser.error('--isolated or command to run is required.')
816
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800817 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700818
819 isolateserver.process_isolate_server_options(
820 parser, options, True, False)
821 if not options.isolate_server:
822 if options.isolated:
823 parser.error('--isolated requires --isolate-server')
824 if ISOLATED_OUTDIR_PARAMETER in args:
825 parser.error(
826 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000827
nodir90bc8dc2016-06-15 13:35:21 -0700828 if options.root_dir:
829 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -0700830 if options.json:
831 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700832
nodirbe642ff2016-06-09 15:51:51 -0700833 cipd.validate_cipd_options(parser, options)
834
nodir90bc8dc2016-06-15 13:35:21 -0700835 install_packages_fn = lambda run_dir: install_packages(
nodirff531b42016-06-23 13:05:06 -0700836 run_dir, cipd.parse_package_args(options.cipd_packages),
837 options.cipd_server, options.cipd_client_package,
838 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -0700839
840 try:
nodir90bc8dc2016-06-15 13:35:21 -0700841 command = [] if options.isolated else args
842 if options.isolate_server:
843 storage = isolateserver.get_storage(
844 options.isolate_server, options.namespace)
845 with storage:
nodir56efa452016-10-12 12:17:39 -0700846 # Hashing schemes used by |storage| and |isolated_cache| MUST match.
847 assert storage.hash_algo == isolated_cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -0700848 return run_tha_test(
nodir56efa452016-10-12 12:17:39 -0700849 command, options.isolated, storage, isolated_cache,
850 options.leak_temp_dir, options.json, options.root_dir,
851 options.hard_timeout, options.grace_period, options.bot_file, args,
852 install_packages_fn, options.use_symlinks)
maruel4409e302016-07-19 14:25:51 -0700853 return run_tha_test(
nodir56efa452016-10-12 12:17:39 -0700854 command, options.isolated, None, isolated_cache, options.leak_temp_dir,
maruel4409e302016-07-19 14:25:51 -0700855 options.json, options.root_dir, options.hard_timeout,
856 options.grace_period, options.bot_file, args, install_packages_fn,
857 options.use_symlinks)
nodirbe642ff2016-06-09 15:51:51 -0700858 except cipd.Error as ex:
859 print >> sys.stderr, ex.message
860 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000861
862
863if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -0700864 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000865 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000866 fix_encoding.fix_encoding()
maruel4409e302016-07-19 14:25:51 -0700867 file_path.enable_symlink()
aludwin7556e0c2016-10-26 08:46:10 -0700868
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500869 sys.exit(main(sys.argv[1:]))