blob: 6416b55b98bdbdfe83d62e225c640a837fd88731 [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
maruela9cfd6f2015-09-15 11:03:15 -070017__version__ = '0.5'
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."""
maruela9cfd6f2015-09-15 11:03:15 -0700125 def fix(arg):
Vadim Shtayura51aba362014-05-14 15:39:23 -0700126 if '${ISOLATED_OUTDIR}' in arg:
maruela9cfd6f2015-09-15 11:03:15 -0700127 return arg.replace('${ISOLATED_OUTDIR}', out_dir).replace('/', os.sep)
128 return arg
129
130 return [fix(arg) for arg in command]
131
132
133def run_command(command, cwd):
134 """Runs the command, returns the process exit code."""
135 logging.info('run_command(%s, %s)' % (command, cwd))
136 sys.stdout.flush()
137 with tools.Profiler('RunTest'):
138 try:
139 with subprocess42.Popen_with_handler(command, cwd=cwd) as p:
140 p.communicate()
141 exit_code = p.returncode
142 except OSError:
143 # This is not considered to be an internal error. The executable simply
144 # does not exit.
145 exit_code = 1
146 logging.info(
147 'Command finished with exit code %d (%s)',
148 exit_code, hex(0xffffffff & exit_code))
149 return exit_code
150
151
152def delete_and_upload(storage, out_dir, leak_temp_dir):
153 """Deletes the temporary run directory and uploads results back.
154
155 Returns:
156 tuple(outputs_ref, success)
157 - outputs_ref is a dict referring to the results archived back to the
158 isolated server, if applicable.
159 - success is False if something occurred that means that the task must
160 forcibly be considered a failure, e.g. zombie processes were left behind.
161 """
162
163 # Upload out_dir and generate a .isolated file out of this directory. It is
164 # only done if files were written in the directory.
165 outputs_ref = None
166 if os.path.isdir(out_dir) and os.listdir(out_dir):
167 with tools.Profiler('ArchiveOutput'):
168 try:
169 results = isolateserver.archive_files_to_storage(
170 storage, [out_dir], None)
171 outputs_ref = {
172 'isolated': results[0][0],
173 'isolatedserver': storage.location,
174 'namespace': storage.namespace,
175 }
176 except isolateserver.Aborted:
177 # This happens when a signal SIGTERM was received while uploading data.
178 # There is 2 causes:
179 # - The task was too slow and was about to be killed anyway due to
180 # exceeding the hard timeout.
181 # - The amount of data uploaded back is very large and took too much
182 # time to archive.
183 sys.stderr.write('Received SIGTERM while uploading')
184 # Re-raise, so it will be treated as an internal failure.
185 raise
186 try:
maruel6eeea7d2015-09-16 12:17:42 -0700187 if (not leak_temp_dir and os.path.isdir(out_dir) and
188 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700189 logging.error('Had difficulties removing out_dir %s', out_dir)
190 return outputs_ref, False
191 except OSError as e:
192 # When this happens, it means there's a process error.
193 logging.error('Had difficulties removing out_dir %s: %s', out_dir, e)
194 return outputs_ref, False
195 return outputs_ref, True
196
197
198def map_and_run(isolated_hash, storage, cache, leak_temp_dir, extra_args):
199 """Maps and run the command. Returns metadata about the result."""
200 # TODO(maruel): Include performance statistics.
201 result = {
202 'exit_code': None,
203 'internal_failure': None,
204 'outputs_ref': None,
205 'version': 1,
206 }
207 tmp_root = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
208 run_dir = make_temp_dir(u'run_tha_test', tmp_root)
209 out_dir = unicode(make_temp_dir(u'isolated_out', tmp_root))
210 try:
211 bundle = isolateserver.fetch_isolated(
212 isolated_hash=isolated_hash,
213 storage=storage,
214 cache=cache,
215 outdir=run_dir,
216 require_command=True)
217
218 change_tree_read_only(run_dir, bundle.read_only)
219 cwd = os.path.normpath(os.path.join(run_dir, bundle.relative_cwd))
220 command = bundle.command + extra_args
221 file_path.ensure_command_has_abs_path(command, cwd)
222 result['exit_code'] = run_command(process_command(command, out_dir), cwd)
223 except Exception as e:
224 # An internal error occured. Report accordingly so the swarming task will be
225 # retried automatically.
226 logging.error('internal failure: %s', e)
227 result['internal_failure'] = str(e)
228 on_error.report(None)
229 finally:
230 try:
231 if leak_temp_dir:
232 logging.warning(
233 'Deliberately leaking %s for later examination', run_dir)
maruel6eeea7d2015-09-16 12:17:42 -0700234 elif os.path.isdir(run_dir) and not file_path.rmtree(run_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700235 # On Windows rmtree(run_dir) call above has a synchronization effect: it
236 # finishes only when all task child processes terminate (since a running
237 # process locks *.exe file). Examine out_dir only after that call
238 # completes (since child processes may write to out_dir too and we need
239 # to wait for them to finish).
240 print >> sys.stderr, (
241 'Failed to delete the temporary directory, forcibly failing\n'
242 'the task because of it. No zombie process can outlive a\n'
243 'successful task run and still be marked as successful.\n'
244 'Fix your stuff.')
245 if result['exit_code'] == 0:
246 result['exit_code'] = 1
247
248 result['outputs_ref'], success = delete_and_upload(
249 storage, out_dir, leak_temp_dir)
250 if not success and result['exit_code'] == 0:
251 result['exit_code'] = 1
252 except Exception as e:
253 # Swallow any exception in the main finally clause.
254 logging.error('Leaking out_dir %s: %s', out_dir, e)
255 result['internal_failure'] = str(e)
256 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500257
258
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400259def run_tha_test(
260 isolated_hash, storage, cache, leak_temp_dir, result_json, extra_args):
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500261 """Downloads the dependencies in the cache, hardlinks them into a temporary
262 directory and runs the executable from there.
263
264 A temporary directory is created to hold the output files. The content inside
265 this directory will be uploaded back to |storage| packaged as a .isolated
266 file.
267
268 Arguments:
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500269 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500270 recreate the tree of files to run the target executable.
271 storage: an isolateserver.Storage object to retrieve remote objects. This
272 object has a reference to an isolateserver.StorageApi, which does
273 the actual I/O.
274 cache: an isolateserver.LocalCache to keep from retrieving the same objects
275 constantly by caching the objects retrieved. Can be on-disk or
276 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700277 leak_temp_dir: if true, the temporary directory will be deliberately leaked
278 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700279 result_json: file path to dump result metadata into. If set, the process
280 exit code is always 0 unless an internal error occured.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500281 extra_args: optional arguments to add to the command stated in the .isolate
282 file.
maruela9cfd6f2015-09-15 11:03:15 -0700283
284 Returns:
285 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000286 """
maruela9cfd6f2015-09-15 11:03:15 -0700287 # run_isolated exit code. Depends on if result_json is used or not.
288 result = map_and_run(
289 isolated_hash, storage, cache, leak_temp_dir, extra_args)
290 logging.info('Result:\n%s', tools.format_json(result, dense=True))
291 if result_json:
292 tools.write_json(result_json, result, dense=True)
293 # Only return 1 if there was an internal error.
294 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000295
maruela9cfd6f2015-09-15 11:03:15 -0700296 # Marshall into old-style inline output.
297 if result['outputs_ref']:
298 data = {
299 'hash': result['outputs_ref']['isolated'],
300 'namespace': result['outputs_ref']['namespace'],
301 'storage': result['outputs_ref']['isolatedserver'],
302 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500303 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700304 print(
305 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
306 tools.format_json(data, dense=True))
307 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000308
309
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500310def main(args):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000311 tools.disable_buffering()
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400312 parser = logging_utils.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000313 usage='%prog <options>',
314 version=__version__,
315 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700316 parser.add_option(
317 '--json',
318 help='dump output metadata to json file. When used, run_isolated returns '
319 'non-zero only on internal failure')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500320 data_group = optparse.OptionGroup(parser, 'Data source')
321 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500322 '-s', '--isolated',
323 help='Hash of the .isolated to grab from the isolate server')
Marc-Antoine Ruelc698ea22015-01-30 14:03:26 -0800324 data_group.add_option(
325 '-H', dest='isolated', help=optparse.SUPPRESS_HELP)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500326 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500327 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000328
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400329 isolateserver.add_cache_options(parser)
330 parser.set_defaults(cache='cache')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000331
Kenneth Russell61d42352014-09-15 11:41:16 -0700332 debug_group = optparse.OptionGroup(parser, 'Debugging')
333 debug_group.add_option(
334 '--leak-temp-dir',
335 action='store_true',
336 help='Deliberately leak isolate\'s temp dir for later examination '
337 '[default: %default]')
338 parser.add_option_group(debug_group)
339
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800340 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500341 options, args = parser.parse_args(args)
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500342 if not options.isolated:
343 parser.error('--isolated is required.')
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800344 auth.process_auth_options(parser, options)
Marc-Antoine Ruele290ada2014-12-10 19:48:49 -0500345 isolateserver.process_isolate_server_options(parser, options, True)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000346
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400347 cache = isolateserver.process_cache_options(options)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500348 with isolateserver.get_storage(
349 options.isolate_server, options.namespace) as storage:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400350 # Hashing schemes used by |storage| and |cache| MUST match.
351 assert storage.hash_algo == cache.hash_algo
352 return run_tha_test(
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400353 options.isolated, storage, cache, options.leak_temp_dir, options.json,
354 args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000355
356
357if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000358 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000359 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500360 sys.exit(main(sys.argv[1:]))