blob: 6509b469356cf79e16fce7643b87eaa2924b46c6 [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 Ruelcfb60852014-07-02 15:22:00 -040017__version__ = '0.3.2'
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):
106 # Anything can be modified. This is the default in the .isolated file
107 # format.
108 #
109 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
110 # is not yet changed to verify the hash of the content of the files it is
111 # looking at, so that if a test modifies an input file, the file must be
112 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400113 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500114 else:
115 raise ValueError(
116 'change_tree_read_only(%s, %s): Unknown flag %s' %
117 (rootdir, read_only, read_only))
118
119
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500120def process_command(command, out_dir):
121 """Replaces isolated specific variables in a command line."""
Vadim Shtayura51aba362014-05-14 15:39:23 -0700122 filtered = []
123 for arg in command:
124 if '${ISOLATED_OUTDIR}' in arg:
125 arg = arg.replace('${ISOLATED_OUTDIR}', out_dir).replace('/', os.sep)
126 filtered.append(arg)
127 return filtered
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500128
129
Kenneth Russell61d42352014-09-15 11:41:16 -0700130def run_tha_test(isolated_hash, storage, cache, leak_temp_dir, extra_args):
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500131 """Downloads the dependencies in the cache, hardlinks them into a temporary
132 directory and runs the executable from there.
133
134 A temporary directory is created to hold the output files. The content inside
135 this directory will be uploaded back to |storage| packaged as a .isolated
136 file.
137
138 Arguments:
139 isolated_hash: the sha-1 of the .isolated file that must be retrieved to
140 recreate the tree of files to run the target executable.
141 storage: an isolateserver.Storage object to retrieve remote objects. This
142 object has a reference to an isolateserver.StorageApi, which does
143 the actual I/O.
144 cache: an isolateserver.LocalCache to keep from retrieving the same objects
145 constantly by caching the objects retrieved. Can be on-disk or
146 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700147 leak_temp_dir: if true, the temporary directory will be deliberately leaked
148 for later examination.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500149 extra_args: optional arguments to add to the command stated in the .isolate
150 file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000151 """
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500152 run_dir = make_temp_dir('run_tha_test', cache.cache_dir)
Vadim Shtayura51aba362014-05-14 15:39:23 -0700153 out_dir = unicode(make_temp_dir('isolated_out', cache.cache_dir))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500154 result = 0
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000155 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000156 try:
Vadim Shtayura7f7459c2014-09-04 13:25:10 -0700157 bundle = isolateserver.fetch_isolated(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000158 isolated_hash=isolated_hash,
159 storage=storage,
160 cache=cache,
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500161 outdir=run_dir,
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000162 require_command=True)
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400163 except isolated_format.IsolatedError:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400164 on_error.report(None)
Vadim Shtayura51aba362014-05-14 15:39:23 -0700165 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000166
Vadim Shtayura7f7459c2014-09-04 13:25:10 -0700167 change_tree_read_only(run_dir, bundle.read_only)
168 cwd = os.path.normpath(os.path.join(run_dir, bundle.relative_cwd))
169 command = bundle.command + extra_args
Vadim Shtayurae4a780b2014-01-17 13:18:53 -0800170
John Abd-El-Malek3f998682014-09-17 17:48:09 -0700171 file_path.ensure_command_has_abs_path(command, cwd)
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500172 command = process_command(command, out_dir)
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500173 logging.info('Running %s, cwd=%s' % (command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000174
175 # TODO(csharp): This should be specified somewhere else.
176 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
177 # Add a rotating log file if one doesn't already exist.
178 env = os.environ.copy()
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000179 if MAIN_DIR:
180 env.setdefault('RUN_TEST_CASES_LOG_FILE',
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000181 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000182 try:
Vadim Shtayura51aba362014-05-14 15:39:23 -0700183 sys.stdout.flush()
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000184 with tools.Profiler('RunTest'):
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500185 result = subprocess.call(command, cwd=cwd, env=env)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700186 logging.info(
187 'Command finished with exit code %d (%s)',
188 result, hex(0xffffffff & result))
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400189 except OSError:
190 on_error.report('Failed to run %s; cwd=%s' % (command, cwd))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500191 result = 1
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500192
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000193 finally:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500194 try:
Kenneth Russell61d42352014-09-15 11:41:16 -0700195 if leak_temp_dir:
196 logging.warning('Deliberately leaking %s for later examination',
197 run_dir)
198 else:
199 try:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400200 if not file_path.rmtree(run_dir):
Kenneth Russell61d42352014-09-15 11:41:16 -0700201 print >> sys.stderr, (
202 'Failed to delete the temporary directory, forcibly failing\n'
203 'the task because of it. No zombie process can outlive a\n'
204 'successful task run and still be marked as successful.\n'
205 'Fix your stuff.')
206 result = result or 1
207 except OSError:
208 logging.warning('Leaking %s', run_dir)
209 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700210
211 # HACK(vadimsh): On Windows rmtree(run_dir) call above has
212 # a synchronization effect: it finishes only when all task child processes
213 # terminate (since a running process locks *.exe file). Examine out_dir
214 # only after that call completes (since child processes may
215 # write to out_dir too and we need to wait for them to finish).
216
217 # Upload out_dir and generate a .isolated file out of this directory.
218 # It is only done if files were written in the directory.
219 if os.listdir(out_dir):
220 with tools.Profiler('ArchiveOutput'):
221 results = isolateserver.archive_files_to_storage(
222 storage, [out_dir], None)
223 # TODO(maruel): Implement side-channel to publish this information.
224 output_data = {
225 'hash': results[0][0],
226 'namespace': storage.namespace,
227 'storage': storage.location,
228 }
229 sys.stdout.flush()
230 print(
231 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
232 tools.format_json(output_data, dense=True))
233
234 finally:
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400235 try:
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400236 if os.path.isdir(out_dir) and not file_path.rmtree(out_dir):
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400237 result = result or 1
238 except OSError:
239 # The error was already printed out. Report it but that's it.
240 on_error.report(None)
241 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700242
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500243 return result
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000244
245
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500246def main(args):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000247 tools.disable_buffering()
248 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000249 usage='%prog <options>',
250 version=__version__,
251 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000252
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500253 data_group = optparse.OptionGroup(parser, 'Data source')
254 data_group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000255 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000256 metavar='FILE',
257 help='File/url describing what to map or run')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500258 data_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000259 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000260 help='Hash of the .isolated to grab from the hash table')
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500261 isolateserver.add_isolate_server_options(data_group, True)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500262 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000263
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500264 cache_group = optparse.OptionGroup(parser, 'Cache management')
265 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000266 '--cache',
267 default='cache',
268 metavar='DIR',
269 help='Cache directory, default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500270 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000271 '--max-cache-size',
272 type='int',
273 metavar='NNN',
274 default=20*1024*1024*1024,
275 help='Trim if the cache gets larger than this value, default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500276 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000277 '--min-free-space',
278 type='int',
279 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000280 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000281 help='Trim if disk free space becomes lower than this value, '
282 'default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500283 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000284 '--max-items',
285 type='int',
286 metavar='NNN',
287 default=100000,
288 help='Trim if more than this number of items are in the cache '
289 'default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500290 parser.add_option_group(cache_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000291
Kenneth Russell61d42352014-09-15 11:41:16 -0700292 debug_group = optparse.OptionGroup(parser, 'Debugging')
293 debug_group.add_option(
294 '--leak-temp-dir',
295 action='store_true',
296 help='Deliberately leak isolate\'s temp dir for later examination '
297 '[default: %default]')
298 parser.add_option_group(debug_group)
299
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800300 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500301 options, args = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800302 auth.process_auth_options(parser, options)
Vadim Shtayura2a8db3b2014-09-09 13:49:56 -0700303 isolateserver.process_isolate_server_options(parser, options)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000304
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000305 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000306 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000307 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000308
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000309 options.cache = os.path.abspath(options.cache)
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400310 policies = isolateserver.CachePolicies(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000311 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000312
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400313 # |options.cache| path may not exist until DiskCache() instance is created.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400314 cache = isolateserver.DiskCache(
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400315 options.cache, policies, isolated_format.get_hash_algo(options.namespace))
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700316
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400317 remote = options.isolate_server or options.indir
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700318 if file_path.is_url(remote):
319 auth.ensure_logged_in(remote)
320
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400321 with isolateserver.get_storage(remote, options.namespace) as storage:
322 # Hashing schemes used by |storage| and |cache| MUST match.
323 assert storage.hash_algo == cache.hash_algo
324 return run_tha_test(
Kenneth Russell61d42352014-09-15 11:41:16 -0700325 options.isolated or options.hash, storage, cache,
326 options.leak_temp_dir, args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000327
328
329if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000330 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000331 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500332 sys.exit(main(sys.argv[1:]))