blob: 9c6f0fff004f98e903f6f16dd3d76c6e6575a4dc [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.
3# Use of this source code is governed by the Apache v2.0 license that can be
4# 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:
147 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
148 # Replace slashes only if ISOLATED_OUTDIR_PARAMETER is present
149 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
150 arg = arg.replace('/', os.sep)
maruela9cfd6f2015-09-15 11:03:15 -0700151 return arg
152
153 return [fix(arg) for arg in command]
154
155
maruel6be7f9e2015-10-01 12:25:30 -0700156def run_command(command, cwd, tmp_dir, hard_timeout, grace_period):
157 """Runs the command.
158
159 Returns:
160 tuple(process exit code, bool if had a hard timeout)
161 """
maruela9cfd6f2015-09-15 11:03:15 -0700162 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700163
164 env = os.environ.copy()
165 if sys.platform == 'darwin':
166 env['TMPDIR'] = tmp_dir.encode('ascii')
167 elif sys.platform == 'win32':
marueldf2329b2016-01-19 15:33:23 -0800168 env['TEMP'] = tmp_dir.encode('ascii')
marueleb5fbee2015-09-17 13:01:36 -0700169 else:
170 env['TMP'] = tmp_dir.encode('ascii')
maruel6be7f9e2015-10-01 12:25:30 -0700171 exit_code = None
172 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700173 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700174 proc = None
175 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700176 try:
maruel6be7f9e2015-10-01 12:25:30 -0700177 # TODO(maruel): This code is imperfect. It doesn't handle well signals
178 # during the download phase and there's short windows were things can go
179 # wrong.
180 def handler(signum, _frame):
181 if proc and not had_signal:
182 logging.info('Received signal %d', signum)
183 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700184 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700185
186 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
187 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
188 try:
189 exit_code = proc.wait(hard_timeout or None)
190 except subprocess42.TimeoutExpired:
191 if not had_signal:
192 logging.warning('Hard timeout')
193 had_hard_timeout = True
194 logging.warning('Sending SIGTERM')
195 proc.terminate()
196
197 # Ignore signals in grace period. Forcibly give the grace period to the
198 # child process.
199 if exit_code is None:
200 ignore = lambda *_: None
201 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
202 try:
203 exit_code = proc.wait(grace_period or None)
204 except subprocess42.TimeoutExpired:
205 # Now kill for real. The user can distinguish between the
206 # following states:
207 # - signal but process exited within grace period,
208 # hard_timed_out will be set but the process exit code will be
209 # script provided.
210 # - processed exited late, exit code will be -9 on posix.
211 logging.warning('Grace exhausted; sending SIGKILL')
212 proc.kill()
213 logging.info('Waiting for proces exit')
214 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700215 except OSError:
216 # This is not considered to be an internal error. The executable simply
217 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800218 sys.stderr.write(
219 '<The executable does not exist or a dependent library is missing>\n'
220 '<Check for missing .so/.dll in the .isolate or GN file>\n'
221 '<Command: %s>\n' % command)
222 if os.environ.get('SWARMING_TASK_ID'):
223 # Give an additional hint when running as a swarming task.
224 sys.stderr.write(
225 '<See the task\'s page for commands to help diagnose this issue '
226 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700227 exit_code = 1
228 logging.info(
229 'Command finished with exit code %d (%s)',
230 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700231 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700232
233
nodir6f801882016-04-29 14:41:50 -0700234def fetch_and_measure(isolated_hash, storage, cache, outdir):
235 """Fetches an isolated and returns (bundle, stats)."""
236 start = time.time()
237 bundle = isolateserver.fetch_isolated(
238 isolated_hash=isolated_hash,
239 storage=storage,
240 cache=cache,
241 outdir=outdir)
242 return bundle, {
243 'duration': time.time() - start,
244 'initial_number_items': cache.initial_number_items,
245 'initial_size': cache.initial_size,
246 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
247 'items_hot': base64.b64encode(
248 large.pack(sorted(set(cache.linked) - set(cache.added)))),
249 }
250
251
maruela9cfd6f2015-09-15 11:03:15 -0700252def delete_and_upload(storage, out_dir, leak_temp_dir):
253 """Deletes the temporary run directory and uploads results back.
254
255 Returns:
nodir6f801882016-04-29 14:41:50 -0700256 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700257 - outputs_ref: a dict referring to the results archived back to the isolated
258 server, if applicable.
259 - success: False if something occurred that means that the task must
260 forcibly be considered a failure, e.g. zombie processes were left
261 behind.
nodir6f801882016-04-29 14:41:50 -0700262 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700263 """
264
265 # Upload out_dir and generate a .isolated file out of this directory. It is
266 # only done if files were written in the directory.
267 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700268 cold = []
269 hot = []
nodir6f801882016-04-29 14:41:50 -0700270 start = time.time()
271
maruel12e30012015-10-09 11:55:35 -0700272 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700273 with tools.Profiler('ArchiveOutput'):
274 try:
maruel064c0a32016-04-05 11:47:15 -0700275 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700276 storage, [out_dir], None)
277 outputs_ref = {
278 'isolated': results[0][0],
279 'isolatedserver': storage.location,
280 'namespace': storage.namespace,
281 }
maruel064c0a32016-04-05 11:47:15 -0700282 cold = sorted(i.size for i in f_cold)
283 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700284 except isolateserver.Aborted:
285 # This happens when a signal SIGTERM was received while uploading data.
286 # There is 2 causes:
287 # - The task was too slow and was about to be killed anyway due to
288 # exceeding the hard timeout.
289 # - The amount of data uploaded back is very large and took too much
290 # time to archive.
291 sys.stderr.write('Received SIGTERM while uploading')
292 # Re-raise, so it will be treated as an internal failure.
293 raise
nodir6f801882016-04-29 14:41:50 -0700294
295 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700296 try:
maruel12e30012015-10-09 11:55:35 -0700297 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700298 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700299 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700300 else:
301 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700302 except OSError as e:
303 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700304 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700305 stats = {
306 'duration': time.time() - start,
307 'items_cold': base64.b64encode(large.pack(cold)),
308 'items_hot': base64.b64encode(large.pack(hot)),
309 }
310 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700311
312
marueleb5fbee2015-09-17 13:01:36 -0700313def map_and_run(
nodir55be77b2016-05-03 09:39:57 -0700314 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
315 hard_timeout, grace_period, extra_args):
316 """Runs a command with optional isolated input/output.
317
318 See run_tha_test for argument documentation.
319
320 Returns metadata about the result.
321 """
322 assert bool(command) ^ bool(isolated_hash)
maruela9cfd6f2015-09-15 11:03:15 -0700323 result = {
maruel064c0a32016-04-05 11:47:15 -0700324 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700325 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700326 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700327 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700328 'stats': {
329 # 'download': {
330 # 'duration': 0.,
331 # 'initial_number_items': 0,
332 # 'initial_size': 0,
333 # 'items_cold': '<large.pack()>',
334 # 'items_hot': '<large.pack()>',
335 # },
336 # 'upload': {
337 # 'duration': 0.,
338 # 'items_cold': '<large.pack()>',
339 # 'items_hot': '<large.pack()>',
340 # },
341 },
maruela9cfd6f2015-09-15 11:03:15 -0700342 'outputs_ref': None,
maruel064c0a32016-04-05 11:47:15 -0700343 'version': 3,
maruela9cfd6f2015-09-15 11:03:15 -0700344 }
marueleb5fbee2015-09-17 13:01:36 -0700345 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700346 file_path.ensure_tree(root_dir, 0700)
marueleb5fbee2015-09-17 13:01:36 -0700347 prefix = u''
348 else:
349 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
350 prefix = u'isolated_'
351 run_dir = make_temp_dir(prefix + u'run', root_dir)
352 out_dir = make_temp_dir(prefix + u'out', root_dir)
353 tmp_dir = make_temp_dir(prefix + u'tmp', root_dir)
nodir55be77b2016-05-03 09:39:57 -0700354 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700355
nodir55be77b2016-05-03 09:39:57 -0700356 try:
357 if isolated_hash:
358 bundle, result['stats']['download'] = fetch_and_measure(
359 isolated_hash=isolated_hash,
360 storage=storage,
361 cache=cache,
362 outdir=run_dir)
363 if not bundle.command:
364 # Handle this as a task failure, not an internal failure.
365 sys.stderr.write(
366 '<The .isolated doesn\'t declare any command to run!>\n'
367 '<Check your .isolate for missing \'command\' variable>\n')
368 if os.environ.get('SWARMING_TASK_ID'):
369 # Give an additional hint when running as a swarming task.
370 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
371 result['exit_code'] = 1
372 return result
373
374 change_tree_read_only(run_dir, bundle.read_only)
375 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
376 command = bundle.command + extra_args
nodir34d673c2016-05-24 09:30:48 -0700377 command = tools.fix_python_path(command)
maruela9cfd6f2015-09-15 11:03:15 -0700378 file_path.ensure_command_has_abs_path(command, cwd)
maruel064c0a32016-04-05 11:47:15 -0700379 sys.stdout.flush()
380 start = time.time()
381 try:
382 result['exit_code'], result['had_hard_timeout'] = run_command(
383 process_command(command, out_dir), cwd, tmp_dir, hard_timeout,
384 grace_period)
385 finally:
386 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700387 except Exception as e:
388 # An internal error occured. Report accordingly so the swarming task will be
389 # retried automatically.
maruel12e30012015-10-09 11:55:35 -0700390 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700391 result['internal_failure'] = str(e)
392 on_error.report(None)
393 finally:
394 try:
395 if leak_temp_dir:
396 logging.warning(
397 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700398 else:
maruel84537cb2015-10-16 14:21:28 -0700399 # On Windows rmtree(run_dir) call above has a synchronization effect: it
400 # finishes only when all task child processes terminate (since a running
401 # process locks *.exe file). Examine out_dir only after that call
402 # completes (since child processes may write to out_dir too and we need
403 # to wait for them to finish).
404 if fs.isdir(run_dir):
405 try:
406 success = file_path.rmtree(run_dir)
407 except OSError as e:
408 logging.error('Failure with %s', e)
409 success = False
410 if not success:
411 print >> sys.stderr, (
412 'Failed to delete the run directory, forcibly failing\n'
413 'the task because of it. No zombie process can outlive a\n'
414 'successful task run and still be marked as successful.\n'
415 'Fix your stuff.')
416 if result['exit_code'] == 0:
417 result['exit_code'] = 1
418 if fs.isdir(tmp_dir):
419 try:
420 success = file_path.rmtree(tmp_dir)
421 except OSError as e:
422 logging.error('Failure with %s', e)
423 success = False
424 if not success:
425 print >> sys.stderr, (
426 'Failed to delete the temporary directory, forcibly failing\n'
427 'the task because of it. No zombie process can outlive a\n'
428 'successful task run and still be marked as successful.\n'
429 'Fix your stuff.')
430 if result['exit_code'] == 0:
431 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700432
marueleb5fbee2015-09-17 13:01:36 -0700433 # This deletes out_dir if leak_temp_dir is not set.
nodir6f801882016-04-29 14:41:50 -0700434 result['outputs_ref'], success, result['stats']['upload'] = (
435 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700436 if not success and result['exit_code'] == 0:
437 result['exit_code'] = 1
438 except Exception as e:
439 # Swallow any exception in the main finally clause.
maruel12e30012015-10-09 11:55:35 -0700440 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700441 result['internal_failure'] = str(e)
442 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500443
444
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400445def run_tha_test(
nodir55be77b2016-05-03 09:39:57 -0700446 command, isolated_hash, storage, cache, leak_temp_dir, result_json,
447 root_dir, hard_timeout, grace_period, extra_args):
448 """Runs an executable and records execution metadata.
449
450 Either command or isolated_hash must be specified.
451
452 If isolated_hash is specified, downloads the dependencies in the cache,
453 hardlinks them into a temporary directory and runs the command specified in
454 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500455
456 A temporary directory is created to hold the output files. The content inside
457 this directory will be uploaded back to |storage| packaged as a .isolated
458 file.
459
460 Arguments:
nodir55be77b2016-05-03 09:39:57 -0700461 command: the command to run, a list of strings. Mutually exclusive with
462 isolated_hash.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500463 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500464 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700465 The command specified in the .isolated is executed.
466 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500467 storage: an isolateserver.Storage object to retrieve remote objects. This
468 object has a reference to an isolateserver.StorageApi, which does
469 the actual I/O.
470 cache: an isolateserver.LocalCache to keep from retrieving the same objects
471 constantly by caching the objects retrieved. Can be on-disk or
472 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700473 leak_temp_dir: if true, the temporary directory will be deliberately leaked
474 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700475 result_json: file path to dump result metadata into. If set, the process
476 exit code is always 0 unless an internal error occured.
marueleb5fbee2015-09-17 13:01:36 -0700477 root_dir: directory to the path to use to create the temporary directory. If
478 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700479 hard_timeout: kills the process if it lasts more than this amount of
480 seconds.
481 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500482 extra_args: optional arguments to add to the command stated in the .isolate
nodir55be77b2016-05-03 09:39:57 -0700483 file. Ignored if isolate_hash is empty.
maruela9cfd6f2015-09-15 11:03:15 -0700484
485 Returns:
486 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000487 """
nodir55be77b2016-05-03 09:39:57 -0700488 assert bool(command) ^ bool(isolated_hash)
489 extra_args = extra_args or []
490 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
491 assert storage is not None, 'storage is None although outdir is specified'
492
maruela76b9ee2015-12-15 06:18:08 -0800493 if result_json:
494 # Write a json output file right away in case we get killed.
495 result = {
496 'exit_code': None,
497 'had_hard_timeout': False,
498 'internal_failure': 'Was terminated before completion',
499 'outputs_ref': None,
500 'version': 2,
501 }
502 tools.write_json(result_json, result, dense=True)
503
maruela9cfd6f2015-09-15 11:03:15 -0700504 # run_isolated exit code. Depends on if result_json is used or not.
505 result = map_and_run(
nodir55be77b2016-05-03 09:39:57 -0700506 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
507 hard_timeout, grace_period, extra_args)
maruela9cfd6f2015-09-15 11:03:15 -0700508 logging.info('Result:\n%s', tools.format_json(result, dense=True))
509 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700510 # We've found tests to delete 'work' when quitting, causing an exception
511 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700512 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700513 tools.write_json(result_json, result, dense=True)
514 # Only return 1 if there was an internal error.
515 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000516
maruela9cfd6f2015-09-15 11:03:15 -0700517 # Marshall into old-style inline output.
518 if result['outputs_ref']:
519 data = {
520 'hash': result['outputs_ref']['isolated'],
521 'namespace': result['outputs_ref']['namespace'],
522 'storage': result['outputs_ref']['isolatedserver'],
523 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500524 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700525 print(
526 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
527 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800528 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700529 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000530
531
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500532def main(args):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400533 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700534 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000535 version=__version__,
536 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700537 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700538 '--clean', action='store_true',
539 help='Cleans the cache, trimming it necessary and remove corrupted items '
540 'and returns without executing anything; use with -v to know what '
541 'was done')
542 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700543 '--json',
544 help='dump output metadata to json file. When used, run_isolated returns '
545 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700546 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800547 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700548 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800549 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700550 help='Grace period between SIGTERM and SIGKILL')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500551 data_group = optparse.OptionGroup(parser, 'Data source')
552 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500553 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700554 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500555 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500556 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000557
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400558 isolateserver.add_cache_options(parser)
559 parser.set_defaults(cache='cache')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000560
Kenneth Russell61d42352014-09-15 11:41:16 -0700561 debug_group = optparse.OptionGroup(parser, 'Debugging')
562 debug_group.add_option(
563 '--leak-temp-dir',
564 action='store_true',
565 help='Deliberately leak isolate\'s temp dir for later examination '
nodir55be77b2016-05-03 09:39:57 -0700566 '[default: %default]')
marueleb5fbee2015-09-17 13:01:36 -0700567 debug_group.add_option(
568 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700569 parser.add_option_group(debug_group)
570
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800571 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500572 options, args = parser.parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700573
574 cache = isolateserver.process_cache_options(options)
575 if options.clean:
576 if options.isolated:
577 parser.error('Can\'t use --isolated with --clean.')
578 if options.isolate_server:
579 parser.error('Can\'t use --isolate-server with --clean.')
580 if options.json:
581 parser.error('Can\'t use --json with --clean.')
582 cache.cleanup()
583 return 0
584
nodir55be77b2016-05-03 09:39:57 -0700585 if not options.isolated and not args:
586 parser.error('--isolated or command to run is required.')
587
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800588 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700589
590 isolateserver.process_isolate_server_options(
591 parser, options, True, False)
592 if not options.isolate_server:
593 if options.isolated:
594 parser.error('--isolated requires --isolate-server')
595 if ISOLATED_OUTDIR_PARAMETER in args:
596 parser.error(
597 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000598
maruel12e30012015-10-09 11:55:35 -0700599 if options.root_dir:
600 options.root_dir = unicode(os.path.abspath(options.root_dir))
601 if options.json:
602 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700603
604 command = [] if options.isolated else args
605 if options.isolate_server:
606 storage = isolateserver.get_storage(
nodird07a3982016-05-03 10:20:32 -0700607 options.isolate_server, options.namespace)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400608 # Hashing schemes used by |storage| and |cache| MUST match.
nodird07a3982016-05-03 10:20:32 -0700609 with storage:
610 assert storage.hash_algo == cache.hash_algo
611 return run_tha_test(
612 command, options.isolated, storage, cache, options.leak_temp_dir,
613 options.json, options.root_dir, options.hard_timeout,
614 options.grace_period, args)
nodir55be77b2016-05-03 09:39:57 -0700615 else:
616 return run_tha_test(
617 command, options.isolated, None, cache, options.leak_temp_dir,
618 options.json, options.root_dir, options.hard_timeout,
619 options.grace_period, args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000620
621
622if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000623 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000624 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500625 sys.exit(main(sys.argv[1:]))