blob: 9d18885ec5e7d3731fe9bdaaf33afd3de05adc95 [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
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -050017__version__ = '0.3.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 subprocess
23import sys
24import tempfile
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000025
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000026from third_party.depot_tools import fix_encoding
27
Vadim Shtayura6b555c12014-07-23 16:22:18 -070028from utils import file_path
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040029from utils import on_error
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000030from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000031from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000032
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080033import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040034import isolated_format
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000035import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000036
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000037
vadimsh@chromium.org85071062013-08-21 23:37:45 +000038# Absolute path to this file (can be None if running from zip on Mac).
39THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000040
41# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000042BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000043
44# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000045if zip_package.get_main_script_path():
46 MAIN_DIR = os.path.dirname(
47 os.path.abspath(zip_package.get_main_script_path()))
48else:
49 # This happens when 'import run_isolated' is executed at the python
50 # interactive prompt, in that case __file__ is undefined.
51 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000052
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000053# The name of the log file to use.
54RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
55
csharp@chromium.orge217f302012-11-22 16:51:53 +000056# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000057RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000058
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000059
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000060def get_as_zip_package(executable=True):
61 """Returns ZipPackage with this module and all its dependencies.
62
63 If |executable| is True will store run_isolated.py as __main__.py so that
64 zip package is directly executable be python.
65 """
66 # Building a zip package when running from another zip package is
67 # unsupported and probably unneeded.
68 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000069 assert THIS_FILE_PATH
70 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000071 package = zip_package.ZipPackage(root=BASE_DIR)
72 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040073 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000074 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080075 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000076 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
77 package.add_directory(os.path.join(BASE_DIR, 'utils'))
78 return package
79
80
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000081def make_temp_dir(prefix, root_dir):
82 """Returns a temporary directory on the same file system as root_dir."""
83 base_temp_dir = None
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -040084 if (root_dir and
85 not file_path.is_same_filesystem(root_dir, tempfile.gettempdir())):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000086 base_temp_dir = os.path.dirname(root_dir)
87 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
88
89
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -050090def change_tree_read_only(rootdir, read_only):
91 """Changes the tree read-only bits according to the read_only specification.
92
93 The flag can be 0, 1 or 2, which will affect the possibility to modify files
94 and create or delete files.
95 """
96 if read_only == 2:
97 # Files and directories (except on Windows) are marked read only. This
98 # inhibits modifying, creating or deleting files in the test directory,
99 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400100 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500101 elif read_only == 1:
102 # Files are marked read only but not the directories. This inhibits
103 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400104 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500105 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500106 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500107 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
108 # is not yet changed to verify the hash of the content of the files it is
109 # looking at, so that if a test modifies an input file, the file must be
110 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400111 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500112 else:
113 raise ValueError(
114 'change_tree_read_only(%s, %s): Unknown flag %s' %
115 (rootdir, read_only, read_only))
116
117
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500118def process_command(command, out_dir):
119 """Replaces isolated specific variables in a command line."""
Vadim Shtayura51aba362014-05-14 15:39:23 -0700120 filtered = []
121 for arg in command:
122 if '${ISOLATED_OUTDIR}' in arg:
123 arg = arg.replace('${ISOLATED_OUTDIR}', out_dir).replace('/', os.sep)
124 filtered.append(arg)
125 return filtered
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500126
127
Kenneth Russell61d42352014-09-15 11:41:16 -0700128def run_tha_test(isolated_hash, storage, cache, leak_temp_dir, extra_args):
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500129 """Downloads the dependencies in the cache, hardlinks them into a temporary
130 directory and runs the executable from there.
131
132 A temporary directory is created to hold the output files. The content inside
133 this directory will be uploaded back to |storage| packaged as a .isolated
134 file.
135
136 Arguments:
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500137 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500138 recreate the tree of files to run the target executable.
139 storage: an isolateserver.Storage object to retrieve remote objects. This
140 object has a reference to an isolateserver.StorageApi, which does
141 the actual I/O.
142 cache: an isolateserver.LocalCache to keep from retrieving the same objects
143 constantly by caching the objects retrieved. Can be on-disk or
144 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700145 leak_temp_dir: if true, the temporary directory will be deliberately leaked
146 for later examination.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500147 extra_args: optional arguments to add to the command stated in the .isolate
148 file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000149 """
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500150 run_dir = make_temp_dir('run_tha_test', cache.cache_dir)
Vadim Shtayura51aba362014-05-14 15:39:23 -0700151 out_dir = unicode(make_temp_dir('isolated_out', cache.cache_dir))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500152 result = 0
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000153 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000154 try:
Vadim Shtayura7f7459c2014-09-04 13:25:10 -0700155 bundle = isolateserver.fetch_isolated(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000156 isolated_hash=isolated_hash,
157 storage=storage,
158 cache=cache,
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500159 outdir=run_dir,
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000160 require_command=True)
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400161 except isolated_format.IsolatedError:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400162 on_error.report(None)
Vadim Shtayura51aba362014-05-14 15:39:23 -0700163 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000164
Vadim Shtayura7f7459c2014-09-04 13:25:10 -0700165 change_tree_read_only(run_dir, bundle.read_only)
166 cwd = os.path.normpath(os.path.join(run_dir, bundle.relative_cwd))
167 command = bundle.command + extra_args
Vadim Shtayurae4a780b2014-01-17 13:18:53 -0800168
John Abd-El-Malek3f998682014-09-17 17:48:09 -0700169 file_path.ensure_command_has_abs_path(command, cwd)
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500170 command = process_command(command, out_dir)
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500171 logging.info('Running %s, cwd=%s' % (command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000172
173 # TODO(csharp): This should be specified somewhere else.
174 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
175 # Add a rotating log file if one doesn't already exist.
176 env = os.environ.copy()
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000177 if MAIN_DIR:
178 env.setdefault('RUN_TEST_CASES_LOG_FILE',
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000179 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000180 try:
Vadim Shtayura51aba362014-05-14 15:39:23 -0700181 sys.stdout.flush()
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000182 with tools.Profiler('RunTest'):
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500183 result = subprocess.call(command, cwd=cwd, env=env)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700184 logging.info(
185 'Command finished with exit code %d (%s)',
186 result, hex(0xffffffff & result))
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400187 except OSError:
188 on_error.report('Failed to run %s; cwd=%s' % (command, cwd))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500189 result = 1
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500190
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000191 finally:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500192 try:
Kenneth Russell61d42352014-09-15 11:41:16 -0700193 if leak_temp_dir:
194 logging.warning('Deliberately leaking %s for later examination',
195 run_dir)
196 else:
197 try:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400198 if not file_path.rmtree(run_dir):
Kenneth Russell61d42352014-09-15 11:41:16 -0700199 print >> sys.stderr, (
200 'Failed to delete the temporary directory, forcibly failing\n'
201 'the task because of it. No zombie process can outlive a\n'
202 'successful task run and still be marked as successful.\n'
203 'Fix your stuff.')
204 result = result or 1
205 except OSError:
206 logging.warning('Leaking %s', run_dir)
207 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700208
209 # HACK(vadimsh): On Windows rmtree(run_dir) call above has
210 # a synchronization effect: it finishes only when all task child processes
211 # terminate (since a running process locks *.exe file). Examine out_dir
212 # only after that call completes (since child processes may
213 # write to out_dir too and we need to wait for them to finish).
214
215 # Upload out_dir and generate a .isolated file out of this directory.
216 # It is only done if files were written in the directory.
217 if os.listdir(out_dir):
218 with tools.Profiler('ArchiveOutput'):
219 results = isolateserver.archive_files_to_storage(
220 storage, [out_dir], None)
221 # TODO(maruel): Implement side-channel to publish this information.
222 output_data = {
223 'hash': results[0][0],
224 'namespace': storage.namespace,
225 'storage': storage.location,
226 }
227 sys.stdout.flush()
228 print(
229 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
230 tools.format_json(output_data, dense=True))
231
232 finally:
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400233 try:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400234 if os.path.isdir(out_dir) and not file_path.rmtree(out_dir):
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400235 result = result or 1
236 except OSError:
237 # The error was already printed out. Report it but that's it.
238 on_error.report(None)
239 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700240
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500241 return result
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000242
243
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500244def main(args):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000245 tools.disable_buffering()
246 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000247 usage='%prog <options>',
248 version=__version__,
249 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000250
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500251 data_group = optparse.OptionGroup(parser, 'Data source')
252 data_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000253 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000254 help='Hash of the .isolated to grab from the hash table')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500255 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500256 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000257
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400258 isolateserver.add_cache_options(parser)
259 parser.set_defaults(cache='cache')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000260
Kenneth Russell61d42352014-09-15 11:41:16 -0700261 debug_group = optparse.OptionGroup(parser, 'Debugging')
262 debug_group.add_option(
263 '--leak-temp-dir',
264 action='store_true',
265 help='Deliberately leak isolate\'s temp dir for later examination '
266 '[default: %default]')
267 parser.add_option_group(debug_group)
268
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800269 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500270 options, args = parser.parse_args(args)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500271 if not options.hash:
272 parser.error('--hash is required.')
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800273 auth.process_auth_options(parser, options)
Marc-Antoine Ruele290ada2014-12-10 19:48:49 -0500274 isolateserver.process_isolate_server_options(parser, options, True)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000275
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400276 cache = isolateserver.process_cache_options(options)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500277 with isolateserver.get_storage(
278 options.isolate_server, options.namespace) as storage:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400279 # Hashing schemes used by |storage| and |cache| MUST match.
280 assert storage.hash_algo == cache.hash_algo
281 return run_tha_test(
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500282 options.hash, storage, cache, options.leak_temp_dir, args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000283
284
285if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000286 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000287 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500288 sys.exit(main(sys.argv[1:]))