blob: d42916d52255293463abf8f4282ec056c53b75fe [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 Ruel185ded42015-01-28 20:49:18 -050017__version__ = '0.4.1'
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 Ruelcfb60852014-07-02 15:22:00 -040028from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050029from utils import subprocess42
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))
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500180 sys.stdout.flush()
181 with tools.Profiler('RunTest'):
182 try:
183 with subprocess42.Popen_with_handler(command, cwd=cwd, env=env) as p:
184 p.communicate()
185 result = p.returncode
186 except OSError:
187 on_error.report('Failed to run %s; cwd=%s' % (command, cwd))
188 result = 1
189 logging.info(
190 'Command finished with exit code %d (%s)',
191 result, hex(0xffffffff & result))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000192 finally:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500193 try:
Kenneth Russell61d42352014-09-15 11:41:16 -0700194 if leak_temp_dir:
195 logging.warning('Deliberately leaking %s for later examination',
196 run_dir)
197 else:
198 try:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400199 if not file_path.rmtree(run_dir):
Kenneth Russell61d42352014-09-15 11:41:16 -0700200 print >> sys.stderr, (
201 'Failed to delete the temporary directory, forcibly failing\n'
202 'the task because of it. No zombie process can outlive a\n'
203 'successful task run and still be marked as successful.\n'
204 'Fix your stuff.')
205 result = result or 1
206 except OSError:
207 logging.warning('Leaking %s', run_dir)
208 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700209
210 # HACK(vadimsh): On Windows rmtree(run_dir) call above has
211 # a synchronization effect: it finishes only when all task child processes
212 # terminate (since a running process locks *.exe file). Examine out_dir
213 # only after that call completes (since child processes may
214 # write to out_dir too and we need to wait for them to finish).
215
216 # Upload out_dir and generate a .isolated file out of this directory.
217 # It is only done if files were written in the directory.
Marc-Antoine Ruel7eb35c82015-01-16 20:30:45 -0500218 if os.path.isdir(out_dir) and os.listdir(out_dir):
Vadim Shtayura51aba362014-05-14 15:39:23 -0700219 with tools.Profiler('ArchiveOutput'):
220 results = isolateserver.archive_files_to_storage(
221 storage, [out_dir], None)
222 # TODO(maruel): Implement side-channel to publish this information.
223 output_data = {
224 'hash': results[0][0],
225 'namespace': storage.namespace,
226 'storage': storage.location,
227 }
228 sys.stdout.flush()
229 print(
230 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
231 tools.format_json(output_data, dense=True))
232
233 finally:
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400234 try:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400235 if os.path.isdir(out_dir) and not file_path.rmtree(out_dir):
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400236 result = result or 1
237 except OSError:
Marc-Antoine Ruelfc059e22015-02-26 14:02:38 -0500238 # The error was already printed out. Report it but that's it. Only
239 # report on non-Windows or on Windows when the process had succeeded.
240 # Due to the way file sharing works on Windows, it's sadly expected that
241 # file deletion may fail when a test failed.
242 if sys.platform != 'win32' or not result:
243 on_error.report(None)
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400244 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700245
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500246 return result
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000247
248
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500249def main(args):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000250 tools.disable_buffering()
251 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000252 usage='%prog <options>',
253 version=__version__,
254 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000255
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500256 data_group = optparse.OptionGroup(parser, 'Data source')
257 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500258 '-s', '--isolated',
259 help='Hash of the .isolated to grab from the isolate server')
Marc-Antoine Ruelc698ea22015-01-30 14:03:26 -0800260 data_group.add_option(
261 '-H', dest='isolated', help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500262 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500263 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000264
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400265 isolateserver.add_cache_options(parser)
266 parser.set_defaults(cache='cache')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000267
Kenneth Russell61d42352014-09-15 11:41:16 -0700268 debug_group = optparse.OptionGroup(parser, 'Debugging')
269 debug_group.add_option(
270 '--leak-temp-dir',
271 action='store_true',
272 help='Deliberately leak isolate\'s temp dir for later examination '
273 '[default: %default]')
274 parser.add_option_group(debug_group)
275
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800276 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500277 options, args = parser.parse_args(args)
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500278 if not options.isolated:
279 parser.error('--isolated is required.')
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800280 auth.process_auth_options(parser, options)
Marc-Antoine Ruele290ada2014-12-10 19:48:49 -0500281 isolateserver.process_isolate_server_options(parser, options, True)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000282
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400283 cache = isolateserver.process_cache_options(options)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500284 with isolateserver.get_storage(
285 options.isolate_server, options.namespace) as storage:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400286 # Hashing schemes used by |storage| and |cache| MUST match.
287 assert storage.hash_algo == cache.hash_algo
288 return run_tha_test(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500289 options.isolated, storage, cache, options.leak_temp_dir, args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000290
291
292if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000293 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000294 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500295 sys.exit(main(sys.argv[1:]))