blob: f605934b74172796e44eb2a7b4ad42100379ee65 [file] [log] [blame]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001#!/usr/bin/env python
Marc-Antoine Ruel8add1242013-11-05 17:28:27 -05002# Copyright 2012 The Swarming Authors. All rights reserved.
Marc-Antoine Ruele98b1122013-11-05 20:27:57 -05003# Use of this source code is governed under the Apache License, Version 2.0 that
4# can be 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
maruel49f04682015-09-03 13:50:26 -070017__version__ = '0.4.3'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000018
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000019import logging
20import optparse
21import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000022import sys
23import tempfile
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000024
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000025from third_party.depot_tools import fix_encoding
26
Vadim Shtayura6b555c12014-07-23 16:22:18 -070027from utils import file_path
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040028from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040029from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050030from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000031from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000032from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000033
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080034import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040035import isolated_format
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000036import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000037
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000038
vadimsh@chromium.org85071062013-08-21 23:37:45 +000039# Absolute path to this file (can be None if running from zip on Mac).
40THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000041
42# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000043BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000044
45# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000046if zip_package.get_main_script_path():
47 MAIN_DIR = os.path.dirname(
48 os.path.abspath(zip_package.get_main_script_path()))
49else:
50 # This happens when 'import run_isolated' is executed at the python
51 # interactive prompt, in that case __file__ is undefined.
52 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000053
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000054# The name of the log file to use.
55RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
56
csharp@chromium.orge217f302012-11-22 16:51:53 +000057# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000058RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000059
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000060
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000061def get_as_zip_package(executable=True):
62 """Returns ZipPackage with this module and all its dependencies.
63
64 If |executable| is True will store run_isolated.py as __main__.py so that
65 zip package is directly executable be python.
66 """
67 # Building a zip package when running from another zip package is
68 # unsupported and probably unneeded.
69 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000070 assert THIS_FILE_PATH
71 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000072 package = zip_package.ZipPackage(root=BASE_DIR)
73 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040074 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000075 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080076 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000077 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
78 package.add_directory(os.path.join(BASE_DIR, 'utils'))
79 return package
80
81
Vadim Shtayuracb0b7432015-07-31 13:26:50 -070082def make_temp_dir(prefix, root_dir=None):
83 """Returns a temporary directory.
84
85 If root_dir is given and /tmp is on same file system as root_dir, uses /tmp.
86 Otherwise makes a new temp directory under root_dir.
87 """
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000088 base_temp_dir = None
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040089 if (root_dir and
90 not file_path.is_same_filesystem(root_dir, tempfile.gettempdir())):
Paweł Hajdan, Jrf7d58722015-04-27 14:54:42 +020091 base_temp_dir = root_dir
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000092 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
93
94
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -050095def change_tree_read_only(rootdir, read_only):
96 """Changes the tree read-only bits according to the read_only specification.
97
98 The flag can be 0, 1 or 2, which will affect the possibility to modify files
99 and create or delete files.
100 """
101 if read_only == 2:
102 # Files and directories (except on Windows) are marked read only. This
103 # inhibits modifying, creating or deleting files in the test directory,
104 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400105 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500106 elif read_only == 1:
107 # Files are marked read only but not the directories. This inhibits
108 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400109 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500110 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500111 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500112 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
113 # is not yet changed to verify the hash of the content of the files it is
114 # looking at, so that if a test modifies an input file, the file must be
115 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400116 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500117 else:
118 raise ValueError(
119 'change_tree_read_only(%s, %s): Unknown flag %s' %
120 (rootdir, read_only, read_only))
121
122
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500123def process_command(command, out_dir):
124 """Replaces isolated specific variables in a command line."""
Vadim Shtayura51aba362014-05-14 15:39:23 -0700125 filtered = []
126 for arg in command:
127 if '${ISOLATED_OUTDIR}' in arg:
128 arg = arg.replace('${ISOLATED_OUTDIR}', out_dir).replace('/', os.sep)
129 filtered.append(arg)
130 return filtered
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500131
132
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400133def run_tha_test(
134 isolated_hash, storage, cache, leak_temp_dir, result_json, extra_args):
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500135 """Downloads the dependencies in the cache, hardlinks them into a temporary
136 directory and runs the executable from there.
137
138 A temporary directory is created to hold the output files. The content inside
139 this directory will be uploaded back to |storage| packaged as a .isolated
140 file.
141
142 Arguments:
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500143 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500144 recreate the tree of files to run the target executable.
145 storage: an isolateserver.Storage object to retrieve remote objects. This
146 object has a reference to an isolateserver.StorageApi, which does
147 the actual I/O.
148 cache: an isolateserver.LocalCache to keep from retrieving the same objects
149 constantly by caching the objects retrieved. Can be on-disk or
150 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700151 leak_temp_dir: if true, the temporary directory will be deliberately leaked
152 for later examination.
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400153 result_json: file path to dump result metadata into.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500154 extra_args: optional arguments to add to the command stated in the .isolate
155 file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000156 """
Vadim Shtayuracb0b7432015-07-31 13:26:50 -0700157 tmp_root = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
158 run_dir = make_temp_dir(u'run_tha_test', tmp_root)
159 out_dir = unicode(make_temp_dir(u'isolated_out', tmp_root))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500160 result = 0
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000161 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000162 try:
Vadim Shtayura7f7459c2014-09-04 13:25:10 -0700163 bundle = isolateserver.fetch_isolated(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000164 isolated_hash=isolated_hash,
165 storage=storage,
166 cache=cache,
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500167 outdir=run_dir,
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000168 require_command=True)
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400169 except isolated_format.IsolatedError:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400170 on_error.report(None)
Vadim Shtayura51aba362014-05-14 15:39:23 -0700171 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000172
Vadim Shtayura7f7459c2014-09-04 13:25:10 -0700173 change_tree_read_only(run_dir, bundle.read_only)
174 cwd = os.path.normpath(os.path.join(run_dir, bundle.relative_cwd))
175 command = bundle.command + extra_args
Vadim Shtayurae4a780b2014-01-17 13:18:53 -0800176
John Abd-El-Malek3f998682014-09-17 17:48:09 -0700177 file_path.ensure_command_has_abs_path(command, cwd)
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500178 command = process_command(command, out_dir)
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500179 logging.info('Running %s, cwd=%s' % (command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000180
181 # TODO(csharp): This should be specified somewhere else.
182 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
183 # Add a rotating log file if one doesn't already exist.
184 env = os.environ.copy()
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000185 if MAIN_DIR:
186 env.setdefault('RUN_TEST_CASES_LOG_FILE',
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000187 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500188 sys.stdout.flush()
189 with tools.Profiler('RunTest'):
190 try:
191 with subprocess42.Popen_with_handler(command, cwd=cwd, env=env) as p:
192 p.communicate()
193 result = p.returncode
194 except OSError:
195 on_error.report('Failed to run %s; cwd=%s' % (command, cwd))
196 result = 1
197 logging.info(
198 'Command finished with exit code %d (%s)',
199 result, hex(0xffffffff & result))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000200 finally:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500201 try:
Kenneth Russell61d42352014-09-15 11:41:16 -0700202 if leak_temp_dir:
203 logging.warning('Deliberately leaking %s for later examination',
204 run_dir)
205 else:
206 try:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400207 if not file_path.rmtree(run_dir):
Kenneth Russell61d42352014-09-15 11:41:16 -0700208 print >> sys.stderr, (
209 'Failed to delete the temporary directory, forcibly failing\n'
210 'the task because of it. No zombie process can outlive a\n'
211 'successful task run and still be marked as successful.\n'
212 'Fix your stuff.')
213 result = result or 1
Vadim Shtayuracb0b7432015-07-31 13:26:50 -0700214 except OSError as exc:
215 logging.error('Leaking run_dir %s: %s', run_dir, exc)
Kenneth Russell61d42352014-09-15 11:41:16 -0700216 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700217
218 # HACK(vadimsh): On Windows rmtree(run_dir) call above has
219 # a synchronization effect: it finishes only when all task child processes
220 # terminate (since a running process locks *.exe file). Examine out_dir
221 # only after that call completes (since child processes may
222 # write to out_dir too and we need to wait for them to finish).
223
224 # Upload out_dir and generate a .isolated file out of this directory.
225 # It is only done if files were written in the directory.
Marc-Antoine Ruel7eb35c82015-01-16 20:30:45 -0500226 if os.path.isdir(out_dir) and os.listdir(out_dir):
Vadim Shtayura51aba362014-05-14 15:39:23 -0700227 with tools.Profiler('ArchiveOutput'):
Marc-Antoine Ruel933027e2015-05-04 14:20:18 -0400228 try:
229 results = isolateserver.archive_files_to_storage(
230 storage, [out_dir], None)
231 except isolateserver.Aborted:
232 # This happens when a signal SIGTERM was received while uploading
233 # data. There is 2 causes:
234 # - The task was too slow and was about to be killed anyway due to
235 # exceeding the hard timeout.
236 # - The amount of data uploaded back is very large and took too much
237 # time to archive.
238 #
239 # There's 3 options to handle this:
240 # - Ignore the upload failure as a silent failure. This can be
241 # detected client side by the fact no result file exists.
242 # - Return as if the task failed. This is not factually correct.
243 # - Return an internal failure. Sadly, it's impossible at this level
244 # at the moment.
245 #
246 # For now, silently drop the upload.
247 #
248 # In any case, the process only has a very short grace period so it
249 # needs to exit right away.
250 sys.stderr.write('Received SIGTERM while uploading')
251 results = None
252
253 if results:
Marc-Antoine Ruel933027e2015-05-04 14:20:18 -0400254 output_data = {
255 'hash': results[0][0],
256 'namespace': storage.namespace,
257 'storage': storage.location,
258 }
259 sys.stdout.flush()
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400260 # TODO(maruel): Skip this when result_json is set. swarming.py needs
261 # to be updated first.
262 data = tools.format_json(output_data, dense=True)
263 print('[run_isolated_out_hack]%s[/run_isolated_out_hack]' % data)
264 if result_json:
265 output_data = {
266 'isolated': results[0][0],
maruel49f04682015-09-03 13:50:26 -0700267 'isolatedserver': storage.location,
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400268 'namespace': storage.namespace,
269 }
270 tools.write_json(result_json, output_data, dense=True)
Vadim Shtayura51aba362014-05-14 15:39:23 -0700271
272 finally:
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400273 try:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400274 if os.path.isdir(out_dir) and not file_path.rmtree(out_dir):
Vadim Shtayura2866a222015-07-31 14:44:54 -0700275 logging.error('Had difficulties removing out_dir %s', out_dir)
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400276 result = result or 1
Vadim Shtayuracb0b7432015-07-31 13:26:50 -0700277 except OSError as exc:
278 # Only report on non-Windows or on Windows when the process had
279 # succeeded. Due to the way file sharing works on Windows, it's sadly
280 # expected that file deletion may fail when a test failed.
281 logging.error('Failed to remove out_dir %s: %s', out_dir, exc)
Marc-Antoine Ruelfc059e22015-02-26 14:02:38 -0500282 if sys.platform != 'win32' or not result:
283 on_error.report(None)
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400284 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700285
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500286 return result
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000287
288
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500289def main(args):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000290 tools.disable_buffering()
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400291 parser = logging_utils.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000292 usage='%prog <options>',
293 version=__version__,
294 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000295
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400296 parser.add_option('--json', help='dump output metadata to json file')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500297 data_group = optparse.OptionGroup(parser, 'Data source')
298 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500299 '-s', '--isolated',
300 help='Hash of the .isolated to grab from the isolate server')
Marc-Antoine Ruelc698ea22015-01-30 14:03:26 -0800301 data_group.add_option(
302 '-H', dest='isolated', help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500303 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500304 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000305
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400306 isolateserver.add_cache_options(parser)
307 parser.set_defaults(cache='cache')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000308
Kenneth Russell61d42352014-09-15 11:41:16 -0700309 debug_group = optparse.OptionGroup(parser, 'Debugging')
310 debug_group.add_option(
311 '--leak-temp-dir',
312 action='store_true',
313 help='Deliberately leak isolate\'s temp dir for later examination '
314 '[default: %default]')
315 parser.add_option_group(debug_group)
316
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800317 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500318 options, args = parser.parse_args(args)
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500319 if not options.isolated:
320 parser.error('--isolated is required.')
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800321 auth.process_auth_options(parser, options)
Marc-Antoine Ruele290ada2014-12-10 19:48:49 -0500322 isolateserver.process_isolate_server_options(parser, options, True)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000323
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400324 cache = isolateserver.process_cache_options(options)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500325 with isolateserver.get_storage(
326 options.isolate_server, options.namespace) as storage:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400327 # Hashing schemes used by |storage| and |cache| MUST match.
328 assert storage.hash_algo == cache.hash_algo
329 return run_tha_test(
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400330 options.isolated, storage, cache, options.leak_temp_dir, options.json,
331 args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000332
333
334if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000335 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000336 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500337 sys.exit(main(sys.argv[1:]))