blob: f74a8391dca5839bdb1fa94093c4aad20b49c584 [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
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00006"""Reads a .isolated, creates a tree of hardlinks and runs the test.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00007
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -05008To improve performance, it keeps a local cache. The local cache can safely be
9deleted.
10
11Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
12temporary directory upon execution of the command specified in the .isolated
13file. All content written to this directory will be uploaded upon termination
14and the .isolated file describing this directory will be printed to stdout.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000015"""
16
nodir6f801882016-04-29 14:41:50 -070017__version__ = '0.6.1'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000018
maruel064c0a32016-04-05 11:47:15 -070019import base64
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000020import logging
21import optparse
22import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000023import sys
24import tempfile
maruel064c0a32016-04-05 11:47:15 -070025import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000026
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000027from third_party.depot_tools import fix_encoding
28
Vadim Shtayura6b555c12014-07-23 16:22:18 -070029from utils import file_path
maruel12e30012015-10-09 11:55:35 -070030from utils import fs
maruel064c0a32016-04-05 11:47:15 -070031from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040032from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040033from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050034from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000035from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000036from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000037
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080038import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040039import isolated_format
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000040import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000041
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000042
vadimsh@chromium.org85071062013-08-21 23:37:45 +000043# Absolute path to this file (can be None if running from zip on Mac).
44THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000045
46# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000047BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000048
49# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000050if zip_package.get_main_script_path():
51 MAIN_DIR = os.path.dirname(
52 os.path.abspath(zip_package.get_main_script_path()))
53else:
54 # This happens when 'import run_isolated' is executed at the python
55 # interactive prompt, in that case __file__ is undefined.
56 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000057
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000058# The name of the log file to use.
59RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
60
csharp@chromium.orge217f302012-11-22 16:51:53 +000061# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000062RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000063
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000064
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000065def get_as_zip_package(executable=True):
66 """Returns ZipPackage with this module and all its dependencies.
67
68 If |executable| is True will store run_isolated.py as __main__.py so that
69 zip package is directly executable be python.
70 """
71 # Building a zip package when running from another zip package is
72 # unsupported and probably unneeded.
73 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000074 assert THIS_FILE_PATH
75 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000076 package = zip_package.ZipPackage(root=BASE_DIR)
77 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040078 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000079 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080080 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000081 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
82 package.add_directory(os.path.join(BASE_DIR, 'utils'))
83 return package
84
85
Vadim Shtayuracb0b7432015-07-31 13:26:50 -070086def make_temp_dir(prefix, root_dir=None):
87 """Returns a temporary directory.
88
89 If root_dir is given and /tmp is on same file system as root_dir, uses /tmp.
90 Otherwise makes a new temp directory under root_dir.
maruel79d5e062016-04-08 13:39:57 -070091
92 Except on OSX, because it's dangerous to create hardlinks in $TMPDIR on OSX!
93 /System/Library/LaunchDaemons/com.apple.bsd.dirhelper.plist runs every day at
94 3:35am and deletes all files older than 3 days in $TMPDIR, but hardlinks do
95 not have the inode modification time updated, so they tend to be old, thus
96 they get deleted.
Vadim Shtayuracb0b7432015-07-31 13:26:50 -070097 """
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000098 base_temp_dir = None
maruel79d5e062016-04-08 13:39:57 -070099 real_temp_dir = unicode(tempfile.gettempdir())
100 if sys.platform == 'darwin':
101 # Nope! Nope! Nope!
102 assert root_dir, 'It is unsafe to create hardlinks in $TMPDIR'
103 base_temp_dir = root_dir
104 elif root_dir and not file_path.is_same_filesystem(root_dir, real_temp_dir):
Paweł Hajdan, Jrf7d58722015-04-27 14:54:42 +0200105 base_temp_dir = root_dir
marueleb5fbee2015-09-17 13:01:36 -0700106 return unicode(tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000107
108
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500109def change_tree_read_only(rootdir, read_only):
110 """Changes the tree read-only bits according to the read_only specification.
111
112 The flag can be 0, 1 or 2, which will affect the possibility to modify files
113 and create or delete files.
114 """
115 if read_only == 2:
116 # Files and directories (except on Windows) are marked read only. This
117 # inhibits modifying, creating or deleting files in the test directory,
118 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400119 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500120 elif read_only == 1:
121 # Files are marked read only but not the directories. This inhibits
122 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400123 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500124 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500125 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500126 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
127 # is not yet changed to verify the hash of the content of the files it is
128 # looking at, so that if a test modifies an input file, the file must be
129 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400130 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500131 else:
132 raise ValueError(
133 'change_tree_read_only(%s, %s): Unknown flag %s' %
134 (rootdir, read_only, read_only))
135
136
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500137def process_command(command, out_dir):
138 """Replaces isolated specific variables in a command line."""
maruela9cfd6f2015-09-15 11:03:15 -0700139 def fix(arg):
Vadim Shtayura51aba362014-05-14 15:39:23 -0700140 if '${ISOLATED_OUTDIR}' in arg:
maruela9cfd6f2015-09-15 11:03:15 -0700141 return arg.replace('${ISOLATED_OUTDIR}', out_dir).replace('/', os.sep)
142 return arg
143
144 return [fix(arg) for arg in command]
145
146
maruel6be7f9e2015-10-01 12:25:30 -0700147def run_command(command, cwd, tmp_dir, hard_timeout, grace_period):
148 """Runs the command.
149
150 Returns:
151 tuple(process exit code, bool if had a hard timeout)
152 """
maruela9cfd6f2015-09-15 11:03:15 -0700153 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700154
155 env = os.environ.copy()
156 if sys.platform == 'darwin':
157 env['TMPDIR'] = tmp_dir.encode('ascii')
158 elif sys.platform == 'win32':
marueldf2329b2016-01-19 15:33:23 -0800159 env['TEMP'] = tmp_dir.encode('ascii')
marueleb5fbee2015-09-17 13:01:36 -0700160 else:
161 env['TMP'] = tmp_dir.encode('ascii')
maruel6be7f9e2015-10-01 12:25:30 -0700162 exit_code = None
163 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700164 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700165 proc = None
166 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700167 try:
maruel6be7f9e2015-10-01 12:25:30 -0700168 # TODO(maruel): This code is imperfect. It doesn't handle well signals
169 # during the download phase and there's short windows were things can go
170 # wrong.
171 def handler(signum, _frame):
172 if proc and not had_signal:
173 logging.info('Received signal %d', signum)
174 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700175 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700176
177 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
178 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
179 try:
180 exit_code = proc.wait(hard_timeout or None)
181 except subprocess42.TimeoutExpired:
182 if not had_signal:
183 logging.warning('Hard timeout')
184 had_hard_timeout = True
185 logging.warning('Sending SIGTERM')
186 proc.terminate()
187
188 # Ignore signals in grace period. Forcibly give the grace period to the
189 # child process.
190 if exit_code is None:
191 ignore = lambda *_: None
192 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
193 try:
194 exit_code = proc.wait(grace_period or None)
195 except subprocess42.TimeoutExpired:
196 # Now kill for real. The user can distinguish between the
197 # following states:
198 # - signal but process exited within grace period,
199 # hard_timed_out will be set but the process exit code will be
200 # script provided.
201 # - processed exited late, exit code will be -9 on posix.
202 logging.warning('Grace exhausted; sending SIGKILL')
203 proc.kill()
204 logging.info('Waiting for proces exit')
205 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700206 except OSError:
207 # This is not considered to be an internal error. The executable simply
208 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800209 sys.stderr.write(
210 '<The executable does not exist or a dependent library is missing>\n'
211 '<Check for missing .so/.dll in the .isolate or GN file>\n'
212 '<Command: %s>\n' % command)
213 if os.environ.get('SWARMING_TASK_ID'):
214 # Give an additional hint when running as a swarming task.
215 sys.stderr.write(
216 '<See the task\'s page for commands to help diagnose this issue '
217 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700218 exit_code = 1
219 logging.info(
220 'Command finished with exit code %d (%s)',
221 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700222 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700223
224
nodir6f801882016-04-29 14:41:50 -0700225def fetch_and_measure(isolated_hash, storage, cache, outdir):
226 """Fetches an isolated and returns (bundle, stats)."""
227 start = time.time()
228 bundle = isolateserver.fetch_isolated(
229 isolated_hash=isolated_hash,
230 storage=storage,
231 cache=cache,
232 outdir=outdir)
233 return bundle, {
234 'duration': time.time() - start,
235 'initial_number_items': cache.initial_number_items,
236 'initial_size': cache.initial_size,
237 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
238 'items_hot': base64.b64encode(
239 large.pack(sorted(set(cache.linked) - set(cache.added)))),
240 }
241
242
maruela9cfd6f2015-09-15 11:03:15 -0700243def delete_and_upload(storage, out_dir, leak_temp_dir):
244 """Deletes the temporary run directory and uploads results back.
245
246 Returns:
nodir6f801882016-04-29 14:41:50 -0700247 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700248 - outputs_ref: a dict referring to the results archived back to the isolated
249 server, if applicable.
250 - success: False if something occurred that means that the task must
251 forcibly be considered a failure, e.g. zombie processes were left
252 behind.
nodir6f801882016-04-29 14:41:50 -0700253 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700254 """
255
256 # Upload out_dir and generate a .isolated file out of this directory. It is
257 # only done if files were written in the directory.
258 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700259 cold = []
260 hot = []
nodir6f801882016-04-29 14:41:50 -0700261 start = time.time()
262
maruel12e30012015-10-09 11:55:35 -0700263 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700264 with tools.Profiler('ArchiveOutput'):
265 try:
maruel064c0a32016-04-05 11:47:15 -0700266 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700267 storage, [out_dir], None)
268 outputs_ref = {
269 'isolated': results[0][0],
270 'isolatedserver': storage.location,
271 'namespace': storage.namespace,
272 }
maruel064c0a32016-04-05 11:47:15 -0700273 cold = sorted(i.size for i in f_cold)
274 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700275 except isolateserver.Aborted:
276 # This happens when a signal SIGTERM was received while uploading data.
277 # There is 2 causes:
278 # - The task was too slow and was about to be killed anyway due to
279 # exceeding the hard timeout.
280 # - The amount of data uploaded back is very large and took too much
281 # time to archive.
282 sys.stderr.write('Received SIGTERM while uploading')
283 # Re-raise, so it will be treated as an internal failure.
284 raise
nodir6f801882016-04-29 14:41:50 -0700285
286 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700287 try:
maruel12e30012015-10-09 11:55:35 -0700288 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700289 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700290 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700291 else:
292 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700293 except OSError as e:
294 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700295 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700296 stats = {
297 'duration': time.time() - start,
298 'items_cold': base64.b64encode(large.pack(cold)),
299 'items_hot': base64.b64encode(large.pack(hot)),
300 }
301 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700302
303
marueleb5fbee2015-09-17 13:01:36 -0700304def map_and_run(
maruel6be7f9e2015-10-01 12:25:30 -0700305 isolated_hash, storage, cache, leak_temp_dir, root_dir, hard_timeout,
306 grace_period, extra_args):
maruela9cfd6f2015-09-15 11:03:15 -0700307 """Maps and run the command. Returns metadata about the result."""
maruela9cfd6f2015-09-15 11:03:15 -0700308 result = {
maruel064c0a32016-04-05 11:47:15 -0700309 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700310 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700311 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700312 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700313 'stats': {
314 # 'download': {
315 # 'duration': 0.,
316 # 'initial_number_items': 0,
317 # 'initial_size': 0,
318 # 'items_cold': '<large.pack()>',
319 # 'items_hot': '<large.pack()>',
320 # },
321 # 'upload': {
322 # 'duration': 0.,
323 # 'items_cold': '<large.pack()>',
324 # 'items_hot': '<large.pack()>',
325 # },
326 },
maruela9cfd6f2015-09-15 11:03:15 -0700327 'outputs_ref': None,
maruel064c0a32016-04-05 11:47:15 -0700328 'version': 3,
maruela9cfd6f2015-09-15 11:03:15 -0700329 }
marueleb5fbee2015-09-17 13:01:36 -0700330 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700331 file_path.ensure_tree(root_dir, 0700)
marueleb5fbee2015-09-17 13:01:36 -0700332 prefix = u''
333 else:
334 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
335 prefix = u'isolated_'
336 run_dir = make_temp_dir(prefix + u'run', root_dir)
337 out_dir = make_temp_dir(prefix + u'out', root_dir)
338 tmp_dir = make_temp_dir(prefix + u'tmp', root_dir)
maruela9cfd6f2015-09-15 11:03:15 -0700339 try:
nodir6f801882016-04-29 14:41:50 -0700340 bundle, result['stats']['download'] = fetch_and_measure(
maruelb8d88d12016-04-08 12:54:01 -0700341 isolated_hash=isolated_hash,
342 storage=storage,
343 cache=cache,
344 outdir=run_dir)
345 if not bundle.command:
maruela72f46e2016-02-24 11:05:45 -0800346 # Handle this as a task failure, not an internal failure.
347 sys.stderr.write(
348 '<The .isolated doesn\'t declare any command to run!>\n'
349 '<Check your .isolate for missing \'command\' variable>\n')
350 if os.environ.get('SWARMING_TASK_ID'):
351 # Give an additional hint when running as a swarming task.
352 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
353 result['exit_code'] = 1
354 return result
maruela9cfd6f2015-09-15 11:03:15 -0700355
356 change_tree_read_only(run_dir, bundle.read_only)
357 cwd = os.path.normpath(os.path.join(run_dir, bundle.relative_cwd))
358 command = bundle.command + extra_args
359 file_path.ensure_command_has_abs_path(command, cwd)
maruel064c0a32016-04-05 11:47:15 -0700360 sys.stdout.flush()
361 start = time.time()
362 try:
363 result['exit_code'], result['had_hard_timeout'] = run_command(
364 process_command(command, out_dir), cwd, tmp_dir, hard_timeout,
365 grace_period)
366 finally:
367 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700368 except Exception as e:
369 # An internal error occured. Report accordingly so the swarming task will be
370 # retried automatically.
maruel12e30012015-10-09 11:55:35 -0700371 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700372 result['internal_failure'] = str(e)
373 on_error.report(None)
374 finally:
375 try:
376 if leak_temp_dir:
377 logging.warning(
378 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700379 else:
maruel84537cb2015-10-16 14:21:28 -0700380 # On Windows rmtree(run_dir) call above has a synchronization effect: it
381 # finishes only when all task child processes terminate (since a running
382 # process locks *.exe file). Examine out_dir only after that call
383 # completes (since child processes may write to out_dir too and we need
384 # to wait for them to finish).
385 if fs.isdir(run_dir):
386 try:
387 success = file_path.rmtree(run_dir)
388 except OSError as e:
389 logging.error('Failure with %s', e)
390 success = False
391 if not success:
392 print >> sys.stderr, (
393 'Failed to delete the run directory, forcibly failing\n'
394 'the task because of it. No zombie process can outlive a\n'
395 'successful task run and still be marked as successful.\n'
396 'Fix your stuff.')
397 if result['exit_code'] == 0:
398 result['exit_code'] = 1
399 if fs.isdir(tmp_dir):
400 try:
401 success = file_path.rmtree(tmp_dir)
402 except OSError as e:
403 logging.error('Failure with %s', e)
404 success = False
405 if not success:
406 print >> sys.stderr, (
407 'Failed to delete the temporary directory, forcibly failing\n'
408 'the task because of it. No zombie process can outlive a\n'
409 'successful task run and still be marked as successful.\n'
410 'Fix your stuff.')
411 if result['exit_code'] == 0:
412 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700413
marueleb5fbee2015-09-17 13:01:36 -0700414 # This deletes out_dir if leak_temp_dir is not set.
nodir6f801882016-04-29 14:41:50 -0700415 result['outputs_ref'], success, result['stats']['upload'] = (
416 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700417 if not success and result['exit_code'] == 0:
418 result['exit_code'] = 1
419 except Exception as e:
420 # Swallow any exception in the main finally clause.
maruel12e30012015-10-09 11:55:35 -0700421 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700422 result['internal_failure'] = str(e)
423 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500424
425
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400426def run_tha_test(
marueleb5fbee2015-09-17 13:01:36 -0700427 isolated_hash, storage, cache, leak_temp_dir, result_json, root_dir,
maruel6be7f9e2015-10-01 12:25:30 -0700428 hard_timeout, grace_period, extra_args):
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500429 """Downloads the dependencies in the cache, hardlinks them into a temporary
430 directory and runs the executable from there.
431
432 A temporary directory is created to hold the output files. The content inside
433 this directory will be uploaded back to |storage| packaged as a .isolated
434 file.
435
436 Arguments:
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500437 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500438 recreate the tree of files to run the target executable.
439 storage: an isolateserver.Storage object to retrieve remote objects. This
440 object has a reference to an isolateserver.StorageApi, which does
441 the actual I/O.
442 cache: an isolateserver.LocalCache to keep from retrieving the same objects
443 constantly by caching the objects retrieved. Can be on-disk or
444 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700445 leak_temp_dir: if true, the temporary directory will be deliberately leaked
446 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700447 result_json: file path to dump result metadata into. If set, the process
448 exit code is always 0 unless an internal error occured.
marueleb5fbee2015-09-17 13:01:36 -0700449 root_dir: directory to the path to use to create the temporary directory. If
450 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700451 hard_timeout: kills the process if it lasts more than this amount of
452 seconds.
453 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500454 extra_args: optional arguments to add to the command stated in the .isolate
455 file.
maruela9cfd6f2015-09-15 11:03:15 -0700456
457 Returns:
458 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000459 """
maruela76b9ee2015-12-15 06:18:08 -0800460 if result_json:
461 # Write a json output file right away in case we get killed.
462 result = {
463 'exit_code': None,
464 'had_hard_timeout': False,
465 'internal_failure': 'Was terminated before completion',
466 'outputs_ref': None,
467 'version': 2,
468 }
469 tools.write_json(result_json, result, dense=True)
470
maruela9cfd6f2015-09-15 11:03:15 -0700471 # run_isolated exit code. Depends on if result_json is used or not.
472 result = map_and_run(
maruel6be7f9e2015-10-01 12:25:30 -0700473 isolated_hash, storage, cache, leak_temp_dir, root_dir, hard_timeout,
474 grace_period, extra_args)
maruela9cfd6f2015-09-15 11:03:15 -0700475 logging.info('Result:\n%s', tools.format_json(result, dense=True))
476 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700477 # We've found tests to delete 'work' when quitting, causing an exception
478 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700479 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700480 tools.write_json(result_json, result, dense=True)
481 # Only return 1 if there was an internal error.
482 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000483
maruela9cfd6f2015-09-15 11:03:15 -0700484 # Marshall into old-style inline output.
485 if result['outputs_ref']:
486 data = {
487 'hash': result['outputs_ref']['isolated'],
488 'namespace': result['outputs_ref']['namespace'],
489 'storage': result['outputs_ref']['isolatedserver'],
490 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500491 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700492 print(
493 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
494 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800495 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700496 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000497
498
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500499def main(args):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400500 parser = logging_utils.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000501 usage='%prog <options>',
502 version=__version__,
503 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700504 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700505 '--clean', action='store_true',
506 help='Cleans the cache, trimming it necessary and remove corrupted items '
507 'and returns without executing anything; use with -v to know what '
508 'was done')
509 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700510 '--json',
511 help='dump output metadata to json file. When used, run_isolated returns '
512 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700513 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800514 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700515 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800516 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700517 help='Grace period between SIGTERM and SIGKILL')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500518 data_group = optparse.OptionGroup(parser, 'Data source')
519 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500520 '-s', '--isolated',
521 help='Hash of the .isolated to grab from the isolate server')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500522 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500523 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000524
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400525 isolateserver.add_cache_options(parser)
526 parser.set_defaults(cache='cache')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000527
Kenneth Russell61d42352014-09-15 11:41:16 -0700528 debug_group = optparse.OptionGroup(parser, 'Debugging')
529 debug_group.add_option(
530 '--leak-temp-dir',
531 action='store_true',
532 help='Deliberately leak isolate\'s temp dir for later examination '
533 '[default: %default]')
marueleb5fbee2015-09-17 13:01:36 -0700534 debug_group.add_option(
535 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700536 parser.add_option_group(debug_group)
537
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800538 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500539 options, args = parser.parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700540
541 cache = isolateserver.process_cache_options(options)
542 if options.clean:
543 if options.isolated:
544 parser.error('Can\'t use --isolated with --clean.')
545 if options.isolate_server:
546 parser.error('Can\'t use --isolate-server with --clean.')
547 if options.json:
548 parser.error('Can\'t use --json with --clean.')
549 cache.cleanup()
550 return 0
551
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800552 auth.process_auth_options(parser, options)
Marc-Antoine Ruele290ada2014-12-10 19:48:49 -0500553 isolateserver.process_isolate_server_options(parser, options, True)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000554
maruel12e30012015-10-09 11:55:35 -0700555 if options.root_dir:
556 options.root_dir = unicode(os.path.abspath(options.root_dir))
557 if options.json:
558 options.json = unicode(os.path.abspath(options.json))
maruel36a963d2016-04-08 17:15:49 -0700559 if not options.isolated:
560 parser.error('--isolated is required.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500561 with isolateserver.get_storage(
562 options.isolate_server, options.namespace) as storage:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400563 # Hashing schemes used by |storage| and |cache| MUST match.
564 assert storage.hash_algo == cache.hash_algo
565 return run_tha_test(
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400566 options.isolated, storage, cache, options.leak_temp_dir, options.json,
maruel6be7f9e2015-10-01 12:25:30 -0700567 options.root_dir, options.hard_timeout, options.grace_period, args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000568
569
570if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000571 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000572 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500573 sys.exit(main(sys.argv[1:]))