blob: 8248244832917b7f846029c154e6ca0cc0ea0cd4 [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:
maruel12e30012015-10-09 11:55:35 -0700306 if not fs.isdir(root_dir):
307 fs.makedirs(root_dir, 0700)
marueleb5fbee2015-09-17 13:01:36 -0700308 prefix = u''
309 else:
310 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
311 prefix = u'isolated_'
312 run_dir = make_temp_dir(prefix + u'run', root_dir)
313 out_dir = make_temp_dir(prefix + u'out', root_dir)
314 tmp_dir = make_temp_dir(prefix + u'tmp', root_dir)
maruela9cfd6f2015-09-15 11:03:15 -0700315 try:
maruel064c0a32016-04-05 11:47:15 -0700316 start = time.time()
maruelb8d88d12016-04-08 12:54:01 -0700317 bundle = isolateserver.fetch_isolated(
318 isolated_hash=isolated_hash,
319 storage=storage,
320 cache=cache,
321 outdir=run_dir)
322 if not bundle.command:
maruela72f46e2016-02-24 11:05:45 -0800323 # Handle this as a task failure, not an internal failure.
324 sys.stderr.write(
325 '<The .isolated doesn\'t declare any command to run!>\n'
326 '<Check your .isolate for missing \'command\' variable>\n')
327 if os.environ.get('SWARMING_TASK_ID'):
328 # Give an additional hint when running as a swarming task.
329 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
330 result['exit_code'] = 1
331 return result
maruel064c0a32016-04-05 11:47:15 -0700332 result['stats']['download'] = {
333 'duration': time.time() - start,
334 'initial_number_items': cache.initial_number_items,
335 'initial_size': cache.initial_size,
336 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
337 'items_hot': base64.b64encode(
338 large.pack(sorted(set(cache.linked) - set(cache.added)))),
339 }
maruela9cfd6f2015-09-15 11:03:15 -0700340
341 change_tree_read_only(run_dir, bundle.read_only)
342 cwd = os.path.normpath(os.path.join(run_dir, bundle.relative_cwd))
343 command = bundle.command + extra_args
344 file_path.ensure_command_has_abs_path(command, cwd)
maruel064c0a32016-04-05 11:47:15 -0700345 sys.stdout.flush()
346 start = time.time()
347 try:
348 result['exit_code'], result['had_hard_timeout'] = run_command(
349 process_command(command, out_dir), cwd, tmp_dir, hard_timeout,
350 grace_period)
351 finally:
352 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700353 except Exception as e:
354 # An internal error occured. Report accordingly so the swarming task will be
355 # retried automatically.
maruel12e30012015-10-09 11:55:35 -0700356 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700357 result['internal_failure'] = str(e)
358 on_error.report(None)
359 finally:
360 try:
361 if leak_temp_dir:
362 logging.warning(
363 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700364 else:
maruel84537cb2015-10-16 14:21:28 -0700365 # On Windows rmtree(run_dir) call above has a synchronization effect: it
366 # finishes only when all task child processes terminate (since a running
367 # process locks *.exe file). Examine out_dir only after that call
368 # completes (since child processes may write to out_dir too and we need
369 # to wait for them to finish).
370 if fs.isdir(run_dir):
371 try:
372 success = file_path.rmtree(run_dir)
373 except OSError as e:
374 logging.error('Failure with %s', e)
375 success = False
376 if not success:
377 print >> sys.stderr, (
378 'Failed to delete the run directory, forcibly failing\n'
379 'the task because of it. No zombie process can outlive a\n'
380 'successful task run and still be marked as successful.\n'
381 'Fix your stuff.')
382 if result['exit_code'] == 0:
383 result['exit_code'] = 1
384 if fs.isdir(tmp_dir):
385 try:
386 success = file_path.rmtree(tmp_dir)
387 except OSError as e:
388 logging.error('Failure with %s', e)
389 success = False
390 if not success:
391 print >> sys.stderr, (
392 'Failed to delete the temporary directory, forcibly failing\n'
393 'the task because of it. No zombie process can outlive a\n'
394 'successful task run and still be marked as successful.\n'
395 'Fix your stuff.')
396 if result['exit_code'] == 0:
397 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700398
marueleb5fbee2015-09-17 13:01:36 -0700399 # This deletes out_dir if leak_temp_dir is not set.
maruel064c0a32016-04-05 11:47:15 -0700400 start = time.time()
401 result['outputs_ref'], success, cold, hot = delete_and_upload(
maruela9cfd6f2015-09-15 11:03:15 -0700402 storage, out_dir, leak_temp_dir)
maruel064c0a32016-04-05 11:47:15 -0700403 result['stats']['upload'] = {
404 'duration': time.time() - start,
405 'items_cold': base64.b64encode(large.pack(cold)),
406 'items_hot': base64.b64encode(large.pack(hot)),
407 }
maruela9cfd6f2015-09-15 11:03:15 -0700408 if not success and result['exit_code'] == 0:
409 result['exit_code'] = 1
410 except Exception as e:
411 # Swallow any exception in the main finally clause.
maruel12e30012015-10-09 11:55:35 -0700412 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700413 result['internal_failure'] = str(e)
414 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500415
416
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400417def run_tha_test(
marueleb5fbee2015-09-17 13:01:36 -0700418 isolated_hash, storage, cache, leak_temp_dir, result_json, root_dir,
maruel6be7f9e2015-10-01 12:25:30 -0700419 hard_timeout, grace_period, extra_args):
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500420 """Downloads the dependencies in the cache, hardlinks them into a temporary
421 directory and runs the executable from there.
422
423 A temporary directory is created to hold the output files. The content inside
424 this directory will be uploaded back to |storage| packaged as a .isolated
425 file.
426
427 Arguments:
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500428 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500429 recreate the tree of files to run the target executable.
430 storage: an isolateserver.Storage object to retrieve remote objects. This
431 object has a reference to an isolateserver.StorageApi, which does
432 the actual I/O.
433 cache: an isolateserver.LocalCache to keep from retrieving the same objects
434 constantly by caching the objects retrieved. Can be on-disk or
435 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700436 leak_temp_dir: if true, the temporary directory will be deliberately leaked
437 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700438 result_json: file path to dump result metadata into. If set, the process
439 exit code is always 0 unless an internal error occured.
marueleb5fbee2015-09-17 13:01:36 -0700440 root_dir: directory to the path to use to create the temporary directory. If
441 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700442 hard_timeout: kills the process if it lasts more than this amount of
443 seconds.
444 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500445 extra_args: optional arguments to add to the command stated in the .isolate
446 file.
maruela9cfd6f2015-09-15 11:03:15 -0700447
448 Returns:
449 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000450 """
maruela76b9ee2015-12-15 06:18:08 -0800451 if result_json:
452 # Write a json output file right away in case we get killed.
453 result = {
454 'exit_code': None,
455 'had_hard_timeout': False,
456 'internal_failure': 'Was terminated before completion',
457 'outputs_ref': None,
458 'version': 2,
459 }
460 tools.write_json(result_json, result, dense=True)
461
maruela9cfd6f2015-09-15 11:03:15 -0700462 # run_isolated exit code. Depends on if result_json is used or not.
463 result = map_and_run(
maruel6be7f9e2015-10-01 12:25:30 -0700464 isolated_hash, storage, cache, leak_temp_dir, root_dir, hard_timeout,
465 grace_period, extra_args)
maruela9cfd6f2015-09-15 11:03:15 -0700466 logging.info('Result:\n%s', tools.format_json(result, dense=True))
467 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700468 # We've found tests to delete 'work' when quitting, causing an exception
469 # here. Try to recreate the directory if necessary.
470 work_dir = os.path.dirname(result_json)
maruel12e30012015-10-09 11:55:35 -0700471 if not fs.isdir(work_dir):
472 fs.mkdir(work_dir)
maruela9cfd6f2015-09-15 11:03:15 -0700473 tools.write_json(result_json, result, dense=True)
474 # Only return 1 if there was an internal error.
475 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000476
maruela9cfd6f2015-09-15 11:03:15 -0700477 # Marshall into old-style inline output.
478 if result['outputs_ref']:
479 data = {
480 'hash': result['outputs_ref']['isolated'],
481 'namespace': result['outputs_ref']['namespace'],
482 'storage': result['outputs_ref']['isolatedserver'],
483 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500484 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700485 print(
486 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
487 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800488 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700489 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000490
491
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500492def main(args):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400493 parser = logging_utils.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000494 usage='%prog <options>',
495 version=__version__,
496 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700497 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700498 '--clean', action='store_true',
499 help='Cleans the cache, trimming it necessary and remove corrupted items '
500 'and returns without executing anything; use with -v to know what '
501 'was done')
502 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700503 '--json',
504 help='dump output metadata to json file. When used, run_isolated returns '
505 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700506 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800507 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700508 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800509 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700510 help='Grace period between SIGTERM and SIGKILL')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500511 data_group = optparse.OptionGroup(parser, 'Data source')
512 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500513 '-s', '--isolated',
514 help='Hash of the .isolated to grab from the isolate server')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500515 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500516 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000517
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400518 isolateserver.add_cache_options(parser)
519 parser.set_defaults(cache='cache')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000520
Kenneth Russell61d42352014-09-15 11:41:16 -0700521 debug_group = optparse.OptionGroup(parser, 'Debugging')
522 debug_group.add_option(
523 '--leak-temp-dir',
524 action='store_true',
525 help='Deliberately leak isolate\'s temp dir for later examination '
526 '[default: %default]')
marueleb5fbee2015-09-17 13:01:36 -0700527 debug_group.add_option(
528 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700529 parser.add_option_group(debug_group)
530
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800531 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500532 options, args = parser.parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700533
534 cache = isolateserver.process_cache_options(options)
535 if options.clean:
536 if options.isolated:
537 parser.error('Can\'t use --isolated with --clean.')
538 if options.isolate_server:
539 parser.error('Can\'t use --isolate-server with --clean.')
540 if options.json:
541 parser.error('Can\'t use --json with --clean.')
542 cache.cleanup()
543 return 0
544
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800545 auth.process_auth_options(parser, options)
Marc-Antoine Ruele290ada2014-12-10 19:48:49 -0500546 isolateserver.process_isolate_server_options(parser, options, True)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000547
maruel12e30012015-10-09 11:55:35 -0700548 if options.root_dir:
549 options.root_dir = unicode(os.path.abspath(options.root_dir))
550 if options.json:
551 options.json = unicode(os.path.abspath(options.json))
maruel36a963d2016-04-08 17:15:49 -0700552 if not options.isolated:
553 parser.error('--isolated is required.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500554 with isolateserver.get_storage(
555 options.isolate_server, options.namespace) as storage:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400556 # Hashing schemes used by |storage| and |cache| MUST match.
557 assert storage.hash_algo == cache.hash_algo
558 return run_tha_test(
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400559 options.isolated, storage, cache, options.leak_temp_dir, options.json,
maruel6be7f9e2015-10-01 12:25:30 -0700560 options.root_dir, options.hard_timeout, options.grace_period, args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000561
562
563if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000564 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000565 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500566 sys.exit(main(sys.argv[1:]))