blob: 8bccaaaf8a240b1a0df5b4bc967742eedc3d685a [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
maruela9cfd6f2015-09-15 11:03:15 -0700377 file_path.ensure_command_has_abs_path(command, cwd)
maruel064c0a32016-04-05 11:47:15 -0700378 sys.stdout.flush()
379 start = time.time()
380 try:
381 result['exit_code'], result['had_hard_timeout'] = run_command(
382 process_command(command, out_dir), cwd, tmp_dir, hard_timeout,
383 grace_period)
384 finally:
385 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700386 except Exception as e:
387 # An internal error occured. Report accordingly so the swarming task will be
388 # retried automatically.
maruel12e30012015-10-09 11:55:35 -0700389 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700390 result['internal_failure'] = str(e)
391 on_error.report(None)
392 finally:
393 try:
394 if leak_temp_dir:
395 logging.warning(
396 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700397 else:
maruel84537cb2015-10-16 14:21:28 -0700398 # On Windows rmtree(run_dir) call above has a synchronization effect: it
399 # finishes only when all task child processes terminate (since a running
400 # process locks *.exe file). Examine out_dir only after that call
401 # completes (since child processes may write to out_dir too and we need
402 # to wait for them to finish).
403 if fs.isdir(run_dir):
404 try:
405 success = file_path.rmtree(run_dir)
406 except OSError as e:
407 logging.error('Failure with %s', e)
408 success = False
409 if not success:
410 print >> sys.stderr, (
411 'Failed to delete the run directory, forcibly failing\n'
412 'the task because of it. No zombie process can outlive a\n'
413 'successful task run and still be marked as successful.\n'
414 'Fix your stuff.')
415 if result['exit_code'] == 0:
416 result['exit_code'] = 1
417 if fs.isdir(tmp_dir):
418 try:
419 success = file_path.rmtree(tmp_dir)
420 except OSError as e:
421 logging.error('Failure with %s', e)
422 success = False
423 if not success:
424 print >> sys.stderr, (
425 'Failed to delete the temporary directory, forcibly failing\n'
426 'the task because of it. No zombie process can outlive a\n'
427 'successful task run and still be marked as successful.\n'
428 'Fix your stuff.')
429 if result['exit_code'] == 0:
430 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700431
marueleb5fbee2015-09-17 13:01:36 -0700432 # This deletes out_dir if leak_temp_dir is not set.
nodir6f801882016-04-29 14:41:50 -0700433 result['outputs_ref'], success, result['stats']['upload'] = (
434 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700435 if not success and result['exit_code'] == 0:
436 result['exit_code'] = 1
437 except Exception as e:
438 # Swallow any exception in the main finally clause.
maruel12e30012015-10-09 11:55:35 -0700439 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700440 result['internal_failure'] = str(e)
441 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500442
443
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400444def run_tha_test(
nodir55be77b2016-05-03 09:39:57 -0700445 command, isolated_hash, storage, cache, leak_temp_dir, result_json,
446 root_dir, hard_timeout, grace_period, extra_args):
447 """Runs an executable and records execution metadata.
448
449 Either command or isolated_hash must be specified.
450
451 If isolated_hash is specified, downloads the dependencies in the cache,
452 hardlinks them into a temporary directory and runs the command specified in
453 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500454
455 A temporary directory is created to hold the output files. The content inside
456 this directory will be uploaded back to |storage| packaged as a .isolated
457 file.
458
459 Arguments:
nodir55be77b2016-05-03 09:39:57 -0700460 command: the command to run, a list of strings. Mutually exclusive with
461 isolated_hash.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500462 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500463 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700464 The command specified in the .isolated is executed.
465 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500466 storage: an isolateserver.Storage object to retrieve remote objects. This
467 object has a reference to an isolateserver.StorageApi, which does
468 the actual I/O.
469 cache: an isolateserver.LocalCache to keep from retrieving the same objects
470 constantly by caching the objects retrieved. Can be on-disk or
471 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700472 leak_temp_dir: if true, the temporary directory will be deliberately leaked
473 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700474 result_json: file path to dump result metadata into. If set, the process
475 exit code is always 0 unless an internal error occured.
marueleb5fbee2015-09-17 13:01:36 -0700476 root_dir: directory to the path to use to create the temporary directory. If
477 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700478 hard_timeout: kills the process if it lasts more than this amount of
479 seconds.
480 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500481 extra_args: optional arguments to add to the command stated in the .isolate
nodir55be77b2016-05-03 09:39:57 -0700482 file. Ignored if isolate_hash is empty.
maruela9cfd6f2015-09-15 11:03:15 -0700483
484 Returns:
485 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000486 """
nodir55be77b2016-05-03 09:39:57 -0700487 assert bool(command) ^ bool(isolated_hash)
488 extra_args = extra_args or []
489 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
490 assert storage is not None, 'storage is None although outdir is specified'
491
maruela76b9ee2015-12-15 06:18:08 -0800492 if result_json:
493 # Write a json output file right away in case we get killed.
494 result = {
495 'exit_code': None,
496 'had_hard_timeout': False,
497 'internal_failure': 'Was terminated before completion',
498 'outputs_ref': None,
499 'version': 2,
500 }
501 tools.write_json(result_json, result, dense=True)
502
maruela9cfd6f2015-09-15 11:03:15 -0700503 # run_isolated exit code. Depends on if result_json is used or not.
504 result = map_and_run(
nodir55be77b2016-05-03 09:39:57 -0700505 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
506 hard_timeout, grace_period, extra_args)
maruela9cfd6f2015-09-15 11:03:15 -0700507 logging.info('Result:\n%s', tools.format_json(result, dense=True))
508 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700509 # We've found tests to delete 'work' when quitting, causing an exception
510 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700511 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700512 tools.write_json(result_json, result, dense=True)
513 # Only return 1 if there was an internal error.
514 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000515
maruela9cfd6f2015-09-15 11:03:15 -0700516 # Marshall into old-style inline output.
517 if result['outputs_ref']:
518 data = {
519 'hash': result['outputs_ref']['isolated'],
520 'namespace': result['outputs_ref']['namespace'],
521 'storage': result['outputs_ref']['isolatedserver'],
522 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500523 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700524 print(
525 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
526 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800527 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700528 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000529
530
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500531def main(args):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400532 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700533 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000534 version=__version__,
535 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700536 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700537 '--clean', action='store_true',
538 help='Cleans the cache, trimming it necessary and remove corrupted items '
539 'and returns without executing anything; use with -v to know what '
540 'was done')
541 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700542 '--json',
543 help='dump output metadata to json file. When used, run_isolated returns '
544 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700545 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800546 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700547 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800548 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700549 help='Grace period between SIGTERM and SIGKILL')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500550 data_group = optparse.OptionGroup(parser, 'Data source')
551 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500552 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700553 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500554 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500555 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000556
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400557 isolateserver.add_cache_options(parser)
558 parser.set_defaults(cache='cache')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000559
Kenneth Russell61d42352014-09-15 11:41:16 -0700560 debug_group = optparse.OptionGroup(parser, 'Debugging')
561 debug_group.add_option(
562 '--leak-temp-dir',
563 action='store_true',
564 help='Deliberately leak isolate\'s temp dir for later examination '
nodir55be77b2016-05-03 09:39:57 -0700565 '[default: %default]')
marueleb5fbee2015-09-17 13:01:36 -0700566 debug_group.add_option(
567 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700568 parser.add_option_group(debug_group)
569
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800570 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500571 options, args = parser.parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700572
573 cache = isolateserver.process_cache_options(options)
574 if options.clean:
575 if options.isolated:
576 parser.error('Can\'t use --isolated with --clean.')
577 if options.isolate_server:
578 parser.error('Can\'t use --isolate-server with --clean.')
579 if options.json:
580 parser.error('Can\'t use --json with --clean.')
581 cache.cleanup()
582 return 0
583
nodir55be77b2016-05-03 09:39:57 -0700584 if not options.isolated and not args:
585 parser.error('--isolated or command to run is required.')
586
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800587 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700588
589 isolateserver.process_isolate_server_options(
590 parser, options, True, False)
591 if not options.isolate_server:
592 if options.isolated:
593 parser.error('--isolated requires --isolate-server')
594 if ISOLATED_OUTDIR_PARAMETER in args:
595 parser.error(
596 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000597
maruel12e30012015-10-09 11:55:35 -0700598 if options.root_dir:
599 options.root_dir = unicode(os.path.abspath(options.root_dir))
600 if options.json:
601 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700602
603 command = [] if options.isolated else args
604 if options.isolate_server:
605 storage = isolateserver.get_storage(
606 options.isolate_server, options.namespace)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400607 # Hashing schemes used by |storage| and |cache| MUST match.
608 assert storage.hash_algo == cache.hash_algo
609 return run_tha_test(
nodir55be77b2016-05-03 09:39:57 -0700610 command, options.isolated, storage, cache, options.leak_temp_dir,
611 options.json, options.root_dir, options.hard_timeout,
612 options.grace_period, args)
613 else:
614 return run_tha_test(
615 command, options.isolated, None, cache, options.leak_temp_dir,
616 options.json, options.root_dir, options.hard_timeout,
617 options.grace_period, args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000618
619
620if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000621 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000622 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500623 sys.exit(main(sys.argv[1:]))