blob: 8883a8bc7d3128ccb92a7286bca5663d9bd5e8bb [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
16Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
17temporary directory upon execution of the command specified in the .isolated
18file. All content written to this directory will be uploaded upon termination
19and the .isolated file describing this directory will be printed to stdout.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000020"""
21
nodir55be77b2016-05-03 09:39:57 -070022__version__ = '0.7.0'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000023
maruel064c0a32016-04-05 11:47:15 -070024import base64
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000025import logging
26import optparse
27import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000028import sys
29import tempfile
maruel064c0a32016-04-05 11:47:15 -070030import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000031
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000032from third_party.depot_tools import fix_encoding
33
Vadim Shtayura6b555c12014-07-23 16:22:18 -070034from utils import file_path
maruel12e30012015-10-09 11:55:35 -070035from utils import fs
maruel064c0a32016-04-05 11:47:15 -070036from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040037from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040038from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050039from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000040from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000041from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000042
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080043import auth
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000044import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000045
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000046
nodir55be77b2016-05-03 09:39:57 -070047ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
48
vadimsh@chromium.org85071062013-08-21 23:37:45 +000049# Absolute path to this file (can be None if running from zip on Mac).
50THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000051
52# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000053BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000054
55# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000056if zip_package.get_main_script_path():
57 MAIN_DIR = os.path.dirname(
58 os.path.abspath(zip_package.get_main_script_path()))
59else:
60 # This happens when 'import run_isolated' is executed at the python
61 # interactive prompt, in that case __file__ is undefined.
62 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000063
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000064# The name of the log file to use.
65RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
66
csharp@chromium.orge217f302012-11-22 16:51:53 +000067# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000068RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000069
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000070
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000071def get_as_zip_package(executable=True):
72 """Returns ZipPackage with this module and all its dependencies.
73
74 If |executable| is True will store run_isolated.py as __main__.py so that
75 zip package is directly executable be python.
76 """
77 # Building a zip package when running from another zip package is
78 # unsupported and probably unneeded.
79 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000080 assert THIS_FILE_PATH
81 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000082 package = zip_package.ZipPackage(root=BASE_DIR)
83 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040084 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000085 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080086 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000087 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
88 package.add_directory(os.path.join(BASE_DIR, 'utils'))
89 return package
90
91
Vadim Shtayuracb0b7432015-07-31 13:26:50 -070092def make_temp_dir(prefix, root_dir=None):
93 """Returns a temporary directory.
94
95 If root_dir is given and /tmp is on same file system as root_dir, uses /tmp.
96 Otherwise makes a new temp directory under root_dir.
maruel79d5e062016-04-08 13:39:57 -070097
98 Except on OSX, because it's dangerous to create hardlinks in $TMPDIR on OSX!
99 /System/Library/LaunchDaemons/com.apple.bsd.dirhelper.plist runs every day at
100 3:35am and deletes all files older than 3 days in $TMPDIR, but hardlinks do
101 not have the inode modification time updated, so they tend to be old, thus
102 they get deleted.
Vadim Shtayuracb0b7432015-07-31 13:26:50 -0700103 """
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000104 base_temp_dir = None
maruel79d5e062016-04-08 13:39:57 -0700105 real_temp_dir = unicode(tempfile.gettempdir())
106 if sys.platform == 'darwin':
107 # Nope! Nope! Nope!
108 assert root_dir, 'It is unsafe to create hardlinks in $TMPDIR'
109 base_temp_dir = root_dir
110 elif root_dir and not file_path.is_same_filesystem(root_dir, real_temp_dir):
Paweł Hajdan, Jrf7d58722015-04-27 14:54:42 +0200111 base_temp_dir = root_dir
marueleb5fbee2015-09-17 13:01:36 -0700112 return unicode(tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000113
114
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500115def change_tree_read_only(rootdir, read_only):
116 """Changes the tree read-only bits according to the read_only specification.
117
118 The flag can be 0, 1 or 2, which will affect the possibility to modify files
119 and create or delete files.
120 """
121 if read_only == 2:
122 # Files and directories (except on Windows) are marked read only. This
123 # inhibits modifying, creating or deleting files in the test directory,
124 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400125 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500126 elif read_only == 1:
127 # Files are marked read only but not the directories. This inhibits
128 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400129 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500130 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500131 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500132 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
133 # is not yet changed to verify the hash of the content of the files it is
134 # looking at, so that if a test modifies an input file, the file must be
135 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400136 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500137 else:
138 raise ValueError(
139 'change_tree_read_only(%s, %s): Unknown flag %s' %
140 (rootdir, read_only, read_only))
141
142
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500143def process_command(command, out_dir):
144 """Replaces isolated specific variables in a command line."""
maruela9cfd6f2015-09-15 11:03:15 -0700145 def fix(arg):
nodir55be77b2016-05-03 09:39:57 -0700146 if ISOLATED_OUTDIR_PARAMETER in arg:
nodir9130f072016-05-27 13:59:08 -0700147 assert out_dir
nodir55be77b2016-05-03 09:39:57 -0700148 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
149 # Replace slashes only if ISOLATED_OUTDIR_PARAMETER is present
150 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
151 arg = arg.replace('/', os.sep)
maruela9cfd6f2015-09-15 11:03:15 -0700152 return arg
153
154 return [fix(arg) for arg in command]
155
156
maruel6be7f9e2015-10-01 12:25:30 -0700157def run_command(command, cwd, tmp_dir, hard_timeout, grace_period):
158 """Runs the command.
159
160 Returns:
161 tuple(process exit code, bool if had a hard timeout)
162 """
maruela9cfd6f2015-09-15 11:03:15 -0700163 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700164
165 env = os.environ.copy()
166 if sys.platform == 'darwin':
167 env['TMPDIR'] = tmp_dir.encode('ascii')
168 elif sys.platform == 'win32':
marueldf2329b2016-01-19 15:33:23 -0800169 env['TEMP'] = tmp_dir.encode('ascii')
marueleb5fbee2015-09-17 13:01:36 -0700170 else:
171 env['TMP'] = tmp_dir.encode('ascii')
maruel6be7f9e2015-10-01 12:25:30 -0700172 exit_code = None
173 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700174 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700175 proc = None
176 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700177 try:
maruel6be7f9e2015-10-01 12:25:30 -0700178 # TODO(maruel): This code is imperfect. It doesn't handle well signals
179 # during the download phase and there's short windows were things can go
180 # wrong.
181 def handler(signum, _frame):
182 if proc and not had_signal:
183 logging.info('Received signal %d', signum)
184 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700185 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700186
187 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
188 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
189 try:
190 exit_code = proc.wait(hard_timeout or None)
191 except subprocess42.TimeoutExpired:
192 if not had_signal:
193 logging.warning('Hard timeout')
194 had_hard_timeout = True
195 logging.warning('Sending SIGTERM')
196 proc.terminate()
197
198 # Ignore signals in grace period. Forcibly give the grace period to the
199 # child process.
200 if exit_code is None:
201 ignore = lambda *_: None
202 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
203 try:
204 exit_code = proc.wait(grace_period or None)
205 except subprocess42.TimeoutExpired:
206 # Now kill for real. The user can distinguish between the
207 # following states:
208 # - signal but process exited within grace period,
209 # hard_timed_out will be set but the process exit code will be
210 # script provided.
211 # - processed exited late, exit code will be -9 on posix.
212 logging.warning('Grace exhausted; sending SIGKILL')
213 proc.kill()
214 logging.info('Waiting for proces exit')
215 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700216 except OSError:
217 # This is not considered to be an internal error. The executable simply
218 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800219 sys.stderr.write(
220 '<The executable does not exist or a dependent library is missing>\n'
221 '<Check for missing .so/.dll in the .isolate or GN file>\n'
222 '<Command: %s>\n' % command)
223 if os.environ.get('SWARMING_TASK_ID'):
224 # Give an additional hint when running as a swarming task.
225 sys.stderr.write(
226 '<See the task\'s page for commands to help diagnose this issue '
227 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700228 exit_code = 1
229 logging.info(
230 'Command finished with exit code %d (%s)',
231 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700232 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700233
234
nodir6f801882016-04-29 14:41:50 -0700235def fetch_and_measure(isolated_hash, storage, cache, outdir):
236 """Fetches an isolated and returns (bundle, stats)."""
237 start = time.time()
238 bundle = isolateserver.fetch_isolated(
239 isolated_hash=isolated_hash,
240 storage=storage,
241 cache=cache,
242 outdir=outdir)
243 return bundle, {
244 'duration': time.time() - start,
245 'initial_number_items': cache.initial_number_items,
246 'initial_size': cache.initial_size,
247 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
248 'items_hot': base64.b64encode(
249 large.pack(sorted(set(cache.linked) - set(cache.added)))),
250 }
251
252
maruela9cfd6f2015-09-15 11:03:15 -0700253def delete_and_upload(storage, out_dir, leak_temp_dir):
254 """Deletes the temporary run directory and uploads results back.
255
256 Returns:
nodir6f801882016-04-29 14:41:50 -0700257 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700258 - outputs_ref: a dict referring to the results archived back to the isolated
259 server, if applicable.
260 - success: False if something occurred that means that the task must
261 forcibly be considered a failure, e.g. zombie processes were left
262 behind.
nodir6f801882016-04-29 14:41:50 -0700263 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700264 """
265
266 # Upload out_dir and generate a .isolated file out of this directory. It is
267 # only done if files were written in the directory.
268 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700269 cold = []
270 hot = []
nodir6f801882016-04-29 14:41:50 -0700271 start = time.time()
272
maruel12e30012015-10-09 11:55:35 -0700273 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700274 with tools.Profiler('ArchiveOutput'):
275 try:
maruel064c0a32016-04-05 11:47:15 -0700276 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700277 storage, [out_dir], None)
278 outputs_ref = {
279 'isolated': results[0][0],
280 'isolatedserver': storage.location,
281 'namespace': storage.namespace,
282 }
maruel064c0a32016-04-05 11:47:15 -0700283 cold = sorted(i.size for i in f_cold)
284 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700285 except isolateserver.Aborted:
286 # This happens when a signal SIGTERM was received while uploading data.
287 # There is 2 causes:
288 # - The task was too slow and was about to be killed anyway due to
289 # exceeding the hard timeout.
290 # - The amount of data uploaded back is very large and took too much
291 # time to archive.
292 sys.stderr.write('Received SIGTERM while uploading')
293 # Re-raise, so it will be treated as an internal failure.
294 raise
nodir6f801882016-04-29 14:41:50 -0700295
296 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700297 try:
maruel12e30012015-10-09 11:55:35 -0700298 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700299 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700300 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700301 else:
302 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700303 except OSError as e:
304 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700305 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700306 stats = {
307 'duration': time.time() - start,
308 'items_cold': base64.b64encode(large.pack(cold)),
309 'items_hot': base64.b64encode(large.pack(hot)),
310 }
311 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700312
313
marueleb5fbee2015-09-17 13:01:36 -0700314def map_and_run(
nodir55be77b2016-05-03 09:39:57 -0700315 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
316 hard_timeout, grace_period, extra_args):
317 """Runs a command with optional isolated input/output.
318
319 See run_tha_test for argument documentation.
320
321 Returns metadata about the result.
322 """
323 assert bool(command) ^ bool(isolated_hash)
maruela9cfd6f2015-09-15 11:03:15 -0700324 result = {
maruel064c0a32016-04-05 11:47:15 -0700325 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700326 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700327 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700328 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700329 'stats': {
330 # 'download': {
331 # 'duration': 0.,
332 # 'initial_number_items': 0,
333 # 'initial_size': 0,
334 # 'items_cold': '<large.pack()>',
335 # 'items_hot': '<large.pack()>',
336 # },
337 # 'upload': {
338 # 'duration': 0.,
339 # 'items_cold': '<large.pack()>',
340 # 'items_hot': '<large.pack()>',
341 # },
342 },
maruela9cfd6f2015-09-15 11:03:15 -0700343 'outputs_ref': None,
maruel064c0a32016-04-05 11:47:15 -0700344 'version': 3,
maruela9cfd6f2015-09-15 11:03:15 -0700345 }
marueleb5fbee2015-09-17 13:01:36 -0700346 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700347 file_path.ensure_tree(root_dir, 0700)
marueleb5fbee2015-09-17 13:01:36 -0700348 prefix = u''
349 else:
350 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
351 prefix = u'isolated_'
352 run_dir = make_temp_dir(prefix + u'run', root_dir)
nodir9130f072016-05-27 13:59:08 -0700353 out_dir = make_temp_dir(prefix + u'out', root_dir) if storage else None
marueleb5fbee2015-09-17 13:01:36 -0700354 tmp_dir = make_temp_dir(prefix + u'tmp', root_dir)
nodir55be77b2016-05-03 09:39:57 -0700355 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700356
nodir55be77b2016-05-03 09:39:57 -0700357 try:
358 if isolated_hash:
359 bundle, result['stats']['download'] = fetch_and_measure(
360 isolated_hash=isolated_hash,
361 storage=storage,
362 cache=cache,
363 outdir=run_dir)
364 if not bundle.command:
365 # Handle this as a task failure, not an internal failure.
366 sys.stderr.write(
367 '<The .isolated doesn\'t declare any command to run!>\n'
368 '<Check your .isolate for missing \'command\' variable>\n')
369 if os.environ.get('SWARMING_TASK_ID'):
370 # Give an additional hint when running as a swarming task.
371 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
372 result['exit_code'] = 1
373 return result
374
375 change_tree_read_only(run_dir, bundle.read_only)
376 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
377 command = bundle.command + extra_args
nodir34d673c2016-05-24 09:30:48 -0700378 command = tools.fix_python_path(command)
maruela9cfd6f2015-09-15 11:03:15 -0700379 file_path.ensure_command_has_abs_path(command, cwd)
maruel064c0a32016-04-05 11:47:15 -0700380 sys.stdout.flush()
381 start = time.time()
382 try:
383 result['exit_code'], result['had_hard_timeout'] = run_command(
384 process_command(command, out_dir), cwd, tmp_dir, hard_timeout,
385 grace_period)
386 finally:
387 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700388 except Exception as e:
389 # An internal error occured. Report accordingly so the swarming task will be
390 # retried automatically.
maruel12e30012015-10-09 11:55:35 -0700391 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700392 result['internal_failure'] = str(e)
393 on_error.report(None)
394 finally:
395 try:
396 if leak_temp_dir:
397 logging.warning(
398 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700399 else:
maruel84537cb2015-10-16 14:21:28 -0700400 # On Windows rmtree(run_dir) call above has a synchronization effect: it
401 # finishes only when all task child processes terminate (since a running
402 # process locks *.exe file). Examine out_dir only after that call
403 # completes (since child processes may write to out_dir too and we need
404 # to wait for them to finish).
405 if fs.isdir(run_dir):
406 try:
407 success = file_path.rmtree(run_dir)
408 except OSError as e:
409 logging.error('Failure with %s', e)
410 success = False
411 if not success:
412 print >> sys.stderr, (
413 'Failed to delete the run directory, forcibly failing\n'
414 'the task because of it. No zombie process can outlive a\n'
415 'successful task run and still be marked as successful.\n'
416 'Fix your stuff.')
417 if result['exit_code'] == 0:
418 result['exit_code'] = 1
419 if fs.isdir(tmp_dir):
420 try:
421 success = file_path.rmtree(tmp_dir)
422 except OSError as e:
423 logging.error('Failure with %s', e)
424 success = False
425 if not success:
426 print >> sys.stderr, (
427 'Failed to delete the temporary directory, forcibly failing\n'
428 'the task because of it. No zombie process can outlive a\n'
429 'successful task run and still be marked as successful.\n'
430 'Fix your stuff.')
431 if result['exit_code'] == 0:
432 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700433
marueleb5fbee2015-09-17 13:01:36 -0700434 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700435 if out_dir:
436 result['outputs_ref'], success, result['stats']['upload'] = (
437 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700438 if not success and result['exit_code'] == 0:
439 result['exit_code'] = 1
440 except Exception as e:
441 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700442 if out_dir:
443 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700444 result['internal_failure'] = str(e)
445 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500446
447
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400448def run_tha_test(
nodir55be77b2016-05-03 09:39:57 -0700449 command, isolated_hash, storage, cache, leak_temp_dir, result_json,
450 root_dir, hard_timeout, grace_period, extra_args):
451 """Runs an executable and records execution metadata.
452
453 Either command or isolated_hash must be specified.
454
455 If isolated_hash is specified, downloads the dependencies in the cache,
456 hardlinks them into a temporary directory and runs the command specified in
457 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500458
459 A temporary directory is created to hold the output files. The content inside
460 this directory will be uploaded back to |storage| packaged as a .isolated
461 file.
462
463 Arguments:
nodir55be77b2016-05-03 09:39:57 -0700464 command: the command to run, a list of strings. Mutually exclusive with
465 isolated_hash.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500466 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500467 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700468 The command specified in the .isolated is executed.
469 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500470 storage: an isolateserver.Storage object to retrieve remote objects. This
471 object has a reference to an isolateserver.StorageApi, which does
472 the actual I/O.
473 cache: an isolateserver.LocalCache to keep from retrieving the same objects
474 constantly by caching the objects retrieved. Can be on-disk or
475 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700476 leak_temp_dir: if true, the temporary directory will be deliberately leaked
477 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700478 result_json: file path to dump result metadata into. If set, the process
479 exit code is always 0 unless an internal error occured.
marueleb5fbee2015-09-17 13:01:36 -0700480 root_dir: directory to the path to use to create the temporary directory. If
481 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700482 hard_timeout: kills the process if it lasts more than this amount of
483 seconds.
484 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500485 extra_args: optional arguments to add to the command stated in the .isolate
nodir55be77b2016-05-03 09:39:57 -0700486 file. Ignored if isolate_hash is empty.
maruela9cfd6f2015-09-15 11:03:15 -0700487
488 Returns:
489 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000490 """
nodir55be77b2016-05-03 09:39:57 -0700491 assert bool(command) ^ bool(isolated_hash)
492 extra_args = extra_args or []
493 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
494 assert storage is not None, 'storage is None although outdir is specified'
495
maruela76b9ee2015-12-15 06:18:08 -0800496 if result_json:
497 # Write a json output file right away in case we get killed.
498 result = {
499 'exit_code': None,
500 'had_hard_timeout': False,
501 'internal_failure': 'Was terminated before completion',
502 'outputs_ref': None,
503 'version': 2,
504 }
505 tools.write_json(result_json, result, dense=True)
506
maruela9cfd6f2015-09-15 11:03:15 -0700507 # run_isolated exit code. Depends on if result_json is used or not.
508 result = map_and_run(
nodir55be77b2016-05-03 09:39:57 -0700509 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
510 hard_timeout, grace_period, extra_args)
maruela9cfd6f2015-09-15 11:03:15 -0700511 logging.info('Result:\n%s', tools.format_json(result, dense=True))
512 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700513 # We've found tests to delete 'work' when quitting, causing an exception
514 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700515 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700516 tools.write_json(result_json, result, dense=True)
517 # Only return 1 if there was an internal error.
518 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000519
maruela9cfd6f2015-09-15 11:03:15 -0700520 # Marshall into old-style inline output.
521 if result['outputs_ref']:
522 data = {
523 'hash': result['outputs_ref']['isolated'],
524 'namespace': result['outputs_ref']['namespace'],
525 'storage': result['outputs_ref']['isolatedserver'],
526 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500527 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700528 print(
529 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
530 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800531 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700532 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000533
534
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500535def main(args):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400536 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700537 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000538 version=__version__,
539 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700540 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700541 '--clean', action='store_true',
542 help='Cleans the cache, trimming it necessary and remove corrupted items '
543 'and returns without executing anything; use with -v to know what '
544 'was done')
545 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700546 '--json',
547 help='dump output metadata to json file. When used, run_isolated returns '
548 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700549 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800550 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700551 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800552 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700553 help='Grace period between SIGTERM and SIGKILL')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500554 data_group = optparse.OptionGroup(parser, 'Data source')
555 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500556 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700557 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500558 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500559 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000560
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400561 isolateserver.add_cache_options(parser)
562 parser.set_defaults(cache='cache')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000563
Kenneth Russell61d42352014-09-15 11:41:16 -0700564 debug_group = optparse.OptionGroup(parser, 'Debugging')
565 debug_group.add_option(
566 '--leak-temp-dir',
567 action='store_true',
568 help='Deliberately leak isolate\'s temp dir for later examination '
nodir55be77b2016-05-03 09:39:57 -0700569 '[default: %default]')
marueleb5fbee2015-09-17 13:01:36 -0700570 debug_group.add_option(
571 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700572 parser.add_option_group(debug_group)
573
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800574 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500575 options, args = parser.parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700576
577 cache = isolateserver.process_cache_options(options)
578 if options.clean:
579 if options.isolated:
580 parser.error('Can\'t use --isolated with --clean.')
581 if options.isolate_server:
582 parser.error('Can\'t use --isolate-server with --clean.')
583 if options.json:
584 parser.error('Can\'t use --json with --clean.')
585 cache.cleanup()
586 return 0
587
nodir55be77b2016-05-03 09:39:57 -0700588 if not options.isolated and not args:
589 parser.error('--isolated or command to run is required.')
590
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800591 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700592
593 isolateserver.process_isolate_server_options(
594 parser, options, True, False)
595 if not options.isolate_server:
596 if options.isolated:
597 parser.error('--isolated requires --isolate-server')
598 if ISOLATED_OUTDIR_PARAMETER in args:
599 parser.error(
600 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000601
maruel12e30012015-10-09 11:55:35 -0700602 if options.root_dir:
603 options.root_dir = unicode(os.path.abspath(options.root_dir))
604 if options.json:
605 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700606
607 command = [] if options.isolated else args
608 if options.isolate_server:
609 storage = isolateserver.get_storage(
nodird07a3982016-05-03 10:20:32 -0700610 options.isolate_server, options.namespace)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400611 # Hashing schemes used by |storage| and |cache| MUST match.
nodird07a3982016-05-03 10:20:32 -0700612 with storage:
613 assert storage.hash_algo == cache.hash_algo
614 return run_tha_test(
615 command, options.isolated, storage, cache, options.leak_temp_dir,
616 options.json, options.root_dir, options.hard_timeout,
617 options.grace_period, args)
nodir55be77b2016-05-03 09:39:57 -0700618 else:
619 return run_tha_test(
620 command, options.isolated, None, cache, options.leak_temp_dir,
621 options.json, options.root_dir, options.hard_timeout,
622 options.grace_period, args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000623
624
625if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000626 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000627 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500628 sys.exit(main(sys.argv[1:]))