blob: 20bcd2e605e870cb809f9979f59ed91084cba53c [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
maruel064c0a32016-04-05 11:47:15 -070017__version__ = '0.6.0'
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
225def delete_and_upload(storage, out_dir, leak_temp_dir):
226 """Deletes the temporary run directory and uploads results back.
227
228 Returns:
maruel064c0a32016-04-05 11:47:15 -0700229 tuple(outputs_ref, success, cold, hot)
230 - outputs_ref: a dict referring to the results archived back to the isolated
231 server, if applicable.
232 - success: False if something occurred that means that the task must
233 forcibly be considered a failure, e.g. zombie processes were left
234 behind.
235 - cold: list of size of cold items, they had to be uploaded.
236 - hot: list of size of hot items, they didn't have to be uploaded.
maruela9cfd6f2015-09-15 11:03:15 -0700237 """
238
239 # Upload out_dir and generate a .isolated file out of this directory. It is
240 # only done if files were written in the directory.
241 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700242 cold = []
243 hot = []
maruel12e30012015-10-09 11:55:35 -0700244 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700245 with tools.Profiler('ArchiveOutput'):
246 try:
maruel064c0a32016-04-05 11:47:15 -0700247 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700248 storage, [out_dir], None)
249 outputs_ref = {
250 'isolated': results[0][0],
251 'isolatedserver': storage.location,
252 'namespace': storage.namespace,
253 }
maruel064c0a32016-04-05 11:47:15 -0700254 cold = sorted(i.size for i in f_cold)
255 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700256 except isolateserver.Aborted:
257 # This happens when a signal SIGTERM was received while uploading data.
258 # There is 2 causes:
259 # - The task was too slow and was about to be killed anyway due to
260 # exceeding the hard timeout.
261 # - The amount of data uploaded back is very large and took too much
262 # time to archive.
263 sys.stderr.write('Received SIGTERM while uploading')
264 # Re-raise, so it will be treated as an internal failure.
265 raise
266 try:
maruel12e30012015-10-09 11:55:35 -0700267 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700268 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700269 logging.error('Had difficulties removing out_dir %s', out_dir)
maruel064c0a32016-04-05 11:47:15 -0700270 return outputs_ref, False, cold, hot
maruela9cfd6f2015-09-15 11:03:15 -0700271 except OSError as e:
272 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700273 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
maruel064c0a32016-04-05 11:47:15 -0700274 return outputs_ref, False, cold, hot
275 return outputs_ref, True, cold, hot
276
maruela9cfd6f2015-09-15 11:03:15 -0700277
278
marueleb5fbee2015-09-17 13:01:36 -0700279def map_and_run(
maruel6be7f9e2015-10-01 12:25:30 -0700280 isolated_hash, storage, cache, leak_temp_dir, root_dir, hard_timeout,
281 grace_period, extra_args):
maruela9cfd6f2015-09-15 11:03:15 -0700282 """Maps and run the command. Returns metadata about the result."""
maruela9cfd6f2015-09-15 11:03:15 -0700283 result = {
maruel064c0a32016-04-05 11:47:15 -0700284 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700285 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700286 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700287 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700288 'stats': {
289 # 'download': {
290 # 'duration': 0.,
291 # 'initial_number_items': 0,
292 # 'initial_size': 0,
293 # 'items_cold': '<large.pack()>',
294 # 'items_hot': '<large.pack()>',
295 # },
296 # 'upload': {
297 # 'duration': 0.,
298 # 'items_cold': '<large.pack()>',
299 # 'items_hot': '<large.pack()>',
300 # },
301 },
maruela9cfd6f2015-09-15 11:03:15 -0700302 'outputs_ref': None,
maruel064c0a32016-04-05 11:47:15 -0700303 'version': 3,
maruela9cfd6f2015-09-15 11:03:15 -0700304 }
marueleb5fbee2015-09-17 13:01:36 -0700305 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700306 file_path.ensure_tree(root_dir, 0700)
marueleb5fbee2015-09-17 13:01:36 -0700307 prefix = u''
308 else:
309 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
310 prefix = u'isolated_'
311 run_dir = make_temp_dir(prefix + u'run', root_dir)
312 out_dir = make_temp_dir(prefix + u'out', root_dir)
313 tmp_dir = make_temp_dir(prefix + u'tmp', root_dir)
maruela9cfd6f2015-09-15 11:03:15 -0700314 try:
maruel064c0a32016-04-05 11:47:15 -0700315 start = time.time()
maruelb8d88d12016-04-08 12:54:01 -0700316 bundle = isolateserver.fetch_isolated(
317 isolated_hash=isolated_hash,
318 storage=storage,
319 cache=cache,
320 outdir=run_dir)
321 if not bundle.command:
maruela72f46e2016-02-24 11:05:45 -0800322 # Handle this as a task failure, not an internal failure.
323 sys.stderr.write(
324 '<The .isolated doesn\'t declare any command to run!>\n'
325 '<Check your .isolate for missing \'command\' variable>\n')
326 if os.environ.get('SWARMING_TASK_ID'):
327 # Give an additional hint when running as a swarming task.
328 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
329 result['exit_code'] = 1
330 return result
maruel064c0a32016-04-05 11:47:15 -0700331 result['stats']['download'] = {
332 'duration': time.time() - start,
333 'initial_number_items': cache.initial_number_items,
334 'initial_size': cache.initial_size,
335 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
336 'items_hot': base64.b64encode(
337 large.pack(sorted(set(cache.linked) - set(cache.added)))),
338 }
maruela9cfd6f2015-09-15 11:03:15 -0700339
340 change_tree_read_only(run_dir, bundle.read_only)
341 cwd = os.path.normpath(os.path.join(run_dir, bundle.relative_cwd))
342 command = bundle.command + extra_args
343 file_path.ensure_command_has_abs_path(command, cwd)
maruel064c0a32016-04-05 11:47:15 -0700344 sys.stdout.flush()
345 start = time.time()
346 try:
347 result['exit_code'], result['had_hard_timeout'] = run_command(
348 process_command(command, out_dir), cwd, tmp_dir, hard_timeout,
349 grace_period)
350 finally:
351 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700352 except Exception as e:
353 # An internal error occured. Report accordingly so the swarming task will be
354 # retried automatically.
maruel12e30012015-10-09 11:55:35 -0700355 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700356 result['internal_failure'] = str(e)
357 on_error.report(None)
358 finally:
359 try:
360 if leak_temp_dir:
361 logging.warning(
362 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700363 else:
maruel84537cb2015-10-16 14:21:28 -0700364 # On Windows rmtree(run_dir) call above has a synchronization effect: it
365 # finishes only when all task child processes terminate (since a running
366 # process locks *.exe file). Examine out_dir only after that call
367 # completes (since child processes may write to out_dir too and we need
368 # to wait for them to finish).
369 if fs.isdir(run_dir):
370 try:
371 success = file_path.rmtree(run_dir)
372 except OSError as e:
373 logging.error('Failure with %s', e)
374 success = False
375 if not success:
376 print >> sys.stderr, (
377 'Failed to delete the run directory, forcibly failing\n'
378 'the task because of it. No zombie process can outlive a\n'
379 'successful task run and still be marked as successful.\n'
380 'Fix your stuff.')
381 if result['exit_code'] == 0:
382 result['exit_code'] = 1
383 if fs.isdir(tmp_dir):
384 try:
385 success = file_path.rmtree(tmp_dir)
386 except OSError as e:
387 logging.error('Failure with %s', e)
388 success = False
389 if not success:
390 print >> sys.stderr, (
391 'Failed to delete the temporary directory, forcibly failing\n'
392 'the task because of it. No zombie process can outlive a\n'
393 'successful task run and still be marked as successful.\n'
394 'Fix your stuff.')
395 if result['exit_code'] == 0:
396 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700397
marueleb5fbee2015-09-17 13:01:36 -0700398 # This deletes out_dir if leak_temp_dir is not set.
maruel064c0a32016-04-05 11:47:15 -0700399 start = time.time()
400 result['outputs_ref'], success, cold, hot = delete_and_upload(
maruela9cfd6f2015-09-15 11:03:15 -0700401 storage, out_dir, leak_temp_dir)
maruel064c0a32016-04-05 11:47:15 -0700402 result['stats']['upload'] = {
403 'duration': time.time() - start,
404 'items_cold': base64.b64encode(large.pack(cold)),
405 'items_hot': base64.b64encode(large.pack(hot)),
406 }
maruela9cfd6f2015-09-15 11:03:15 -0700407 if not success and result['exit_code'] == 0:
408 result['exit_code'] = 1
409 except Exception as e:
410 # Swallow any exception in the main finally clause.
maruel12e30012015-10-09 11:55:35 -0700411 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700412 result['internal_failure'] = str(e)
413 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500414
415
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400416def run_tha_test(
marueleb5fbee2015-09-17 13:01:36 -0700417 isolated_hash, storage, cache, leak_temp_dir, result_json, root_dir,
maruel6be7f9e2015-10-01 12:25:30 -0700418 hard_timeout, grace_period, extra_args):
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500419 """Downloads the dependencies in the cache, hardlinks them into a temporary
420 directory and runs the executable from there.
421
422 A temporary directory is created to hold the output files. The content inside
423 this directory will be uploaded back to |storage| packaged as a .isolated
424 file.
425
426 Arguments:
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500427 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500428 recreate the tree of files to run the target executable.
429 storage: an isolateserver.Storage object to retrieve remote objects. This
430 object has a reference to an isolateserver.StorageApi, which does
431 the actual I/O.
432 cache: an isolateserver.LocalCache to keep from retrieving the same objects
433 constantly by caching the objects retrieved. Can be on-disk or
434 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700435 leak_temp_dir: if true, the temporary directory will be deliberately leaked
436 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700437 result_json: file path to dump result metadata into. If set, the process
438 exit code is always 0 unless an internal error occured.
marueleb5fbee2015-09-17 13:01:36 -0700439 root_dir: directory to the path to use to create the temporary directory. If
440 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700441 hard_timeout: kills the process if it lasts more than this amount of
442 seconds.
443 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500444 extra_args: optional arguments to add to the command stated in the .isolate
445 file.
maruela9cfd6f2015-09-15 11:03:15 -0700446
447 Returns:
448 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000449 """
maruela76b9ee2015-12-15 06:18:08 -0800450 if result_json:
451 # Write a json output file right away in case we get killed.
452 result = {
453 'exit_code': None,
454 'had_hard_timeout': False,
455 'internal_failure': 'Was terminated before completion',
456 'outputs_ref': None,
457 'version': 2,
458 }
459 tools.write_json(result_json, result, dense=True)
460
maruela9cfd6f2015-09-15 11:03:15 -0700461 # run_isolated exit code. Depends on if result_json is used or not.
462 result = map_and_run(
maruel6be7f9e2015-10-01 12:25:30 -0700463 isolated_hash, storage, cache, leak_temp_dir, root_dir, hard_timeout,
464 grace_period, extra_args)
maruela9cfd6f2015-09-15 11:03:15 -0700465 logging.info('Result:\n%s', tools.format_json(result, dense=True))
466 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700467 # We've found tests to delete 'work' when quitting, causing an exception
468 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700469 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700470 tools.write_json(result_json, result, dense=True)
471 # Only return 1 if there was an internal error.
472 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000473
maruela9cfd6f2015-09-15 11:03:15 -0700474 # Marshall into old-style inline output.
475 if result['outputs_ref']:
476 data = {
477 'hash': result['outputs_ref']['isolated'],
478 'namespace': result['outputs_ref']['namespace'],
479 'storage': result['outputs_ref']['isolatedserver'],
480 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500481 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700482 print(
483 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
484 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800485 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700486 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000487
488
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500489def main(args):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400490 parser = logging_utils.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000491 usage='%prog <options>',
492 version=__version__,
493 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700494 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700495 '--clean', action='store_true',
496 help='Cleans the cache, trimming it necessary and remove corrupted items '
497 'and returns without executing anything; use with -v to know what '
498 'was done')
499 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700500 '--json',
501 help='dump output metadata to json file. When used, run_isolated returns '
502 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700503 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800504 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700505 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800506 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700507 help='Grace period between SIGTERM and SIGKILL')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500508 data_group = optparse.OptionGroup(parser, 'Data source')
509 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500510 '-s', '--isolated',
511 help='Hash of the .isolated to grab from the isolate server')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500512 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500513 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000514
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400515 isolateserver.add_cache_options(parser)
516 parser.set_defaults(cache='cache')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000517
Kenneth Russell61d42352014-09-15 11:41:16 -0700518 debug_group = optparse.OptionGroup(parser, 'Debugging')
519 debug_group.add_option(
520 '--leak-temp-dir',
521 action='store_true',
522 help='Deliberately leak isolate\'s temp dir for later examination '
523 '[default: %default]')
marueleb5fbee2015-09-17 13:01:36 -0700524 debug_group.add_option(
525 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700526 parser.add_option_group(debug_group)
527
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800528 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500529 options, args = parser.parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700530
531 cache = isolateserver.process_cache_options(options)
532 if options.clean:
533 if options.isolated:
534 parser.error('Can\'t use --isolated with --clean.')
535 if options.isolate_server:
536 parser.error('Can\'t use --isolate-server with --clean.')
537 if options.json:
538 parser.error('Can\'t use --json with --clean.')
539 cache.cleanup()
540 return 0
541
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800542 auth.process_auth_options(parser, options)
Marc-Antoine Ruele290ada2014-12-10 19:48:49 -0500543 isolateserver.process_isolate_server_options(parser, options, True)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000544
maruel12e30012015-10-09 11:55:35 -0700545 if options.root_dir:
546 options.root_dir = unicode(os.path.abspath(options.root_dir))
547 if options.json:
548 options.json = unicode(os.path.abspath(options.json))
maruel36a963d2016-04-08 17:15:49 -0700549 if not options.isolated:
550 parser.error('--isolated is required.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500551 with isolateserver.get_storage(
552 options.isolate_server, options.namespace) as storage:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400553 # Hashing schemes used by |storage| and |cache| MUST match.
554 assert storage.hash_algo == cache.hash_algo
555 return run_tha_test(
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400556 options.isolated, storage, cache, options.leak_temp_dir, options.json,
maruel6be7f9e2015-10-01 12:25:30 -0700557 options.root_dir, options.hard_timeout, options.grace_period, args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000558
559
560if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000561 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000562 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500563 sys.exit(main(sys.argv[1:]))