2 # Copyright 2012 The Swarming Authors. All rights reserved.
3 # Use of this source code is governed under the Apache License, Version 2.0 that
4 # can be found in the LICENSE file.
6 """Reads a .isolated, creates a tree of hardlinks and runs the test.
8 To improve performance, it keeps a local cache. The local cache can safely be
11 Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
12 temporary directory upon execution of the command specified in the .isolated
13 file. All content written to this directory will be uploaded upon termination
14 and the .isolated file describing this directory will be printed to stdout.
26 from third_party.depot_tools import fix_encoding
28 from utils import file_path
29 from utils import on_error
30 from utils import tools
31 from utils import zip_package
34 import isolated_format
38 # Absolute path to this file (can be None if running from zip on Mac).
39 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
41 # Directory that contains this file (might be inside zip package).
42 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
44 # Directory that contains currently running script file.
45 if zip_package.get_main_script_path():
46 MAIN_DIR = os.path.dirname(
47 os.path.abspath(zip_package.get_main_script_path()))
49 # This happens when 'import run_isolated' is executed at the python
50 # interactive prompt, in that case __file__ is undefined.
53 # The name of the log file to use.
54 RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
56 # The name of the log to use for the run_test_cases.py command
57 RUN_TEST_CASES_LOG = 'run_test_cases.log'
60 def get_as_zip_package(executable=True):
61 """Returns ZipPackage with this module and all its dependencies.
63 If |executable| is True will store run_isolated.py as __main__.py so that
64 zip package is directly executable be python.
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__])
71 package = zip_package.ZipPackage(root=BASE_DIR)
72 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
73 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
74 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
75 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
76 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
77 package.add_directory(os.path.join(BASE_DIR, 'utils'))
81 def make_temp_dir(prefix, root_dir):
82 """Returns a temporary directory on the same file system as root_dir."""
85 not file_path.is_same_filesystem(root_dir, tempfile.gettempdir())):
86 base_temp_dir = os.path.dirname(root_dir)
87 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
90 def change_tree_read_only(rootdir, read_only):
91 """Changes the tree read-only bits according to the read_only specification.
93 The flag can be 0, 1 or 2, which will affect the possibility to modify files
94 and create or delete files.
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.
100 file_path.make_tree_read_only(rootdir)
102 # Files are marked read only but not the directories. This inhibits
103 # modifying files but creating or deleting files is still possible.
104 file_path.make_tree_files_read_only(rootdir)
105 elif read_only in (0, None):
106 # Anything can be modified. This is the default in the .isolated file
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
113 file_path.make_tree_writeable(rootdir)
116 'change_tree_read_only(%s, %s): Unknown flag %s' %
117 (rootdir, read_only, read_only))
120 def process_command(command, out_dir):
121 """Replaces isolated specific variables in a command line."""
124 if '${ISOLATED_OUTDIR}' in arg:
125 arg = arg.replace('${ISOLATED_OUTDIR}', out_dir).replace('/', os.sep)
130 def run_tha_test(isolated_hash, storage, cache, leak_temp_dir, extra_args):
131 """Downloads the dependencies in the cache, hardlinks them into a temporary
132 directory and runs the executable from there.
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
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
144 cache: an isolateserver.LocalCache to keep from retrieving the same objects
145 constantly by caching the objects retrieved. Can be on-disk or
147 leak_temp_dir: if true, the temporary directory will be deliberately leaked
148 for later examination.
149 extra_args: optional arguments to add to the command stated in the .isolate
152 run_dir = make_temp_dir('run_tha_test', cache.cache_dir)
153 out_dir = unicode(make_temp_dir('isolated_out', cache.cache_dir))
157 bundle = isolateserver.fetch_isolated(
158 isolated_hash=isolated_hash,
162 require_command=True)
163 except isolated_format.IsolatedError:
164 on_error.report(None)
167 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
171 file_path.ensure_command_has_abs_path(command, cwd)
172 command = process_command(command, out_dir)
173 logging.info('Running %s, cwd=%s' % (command, cwd))
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()
180 env.setdefault('RUN_TEST_CASES_LOG_FILE',
181 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
184 with tools.Profiler('RunTest'):
185 result = subprocess.call(command, cwd=cwd, env=env)
187 'Command finished with exit code %d (%s)',
188 result, hex(0xffffffff & result))
190 on_error.report('Failed to run %s; cwd=%s' % (command, cwd))
196 logging.warning('Deliberately leaking %s for later examination',
200 if not file_path.rmtree(run_dir):
201 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'
208 logging.warning('Leaking %s', run_dir)
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).
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.
225 'hash': results[0][0],
226 'namespace': storage.namespace,
227 'storage': storage.location,
231 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
232 tools.format_json(output_data, dense=True))
236 if os.path.isdir(out_dir) and not file_path.rmtree(out_dir):
239 # The error was already printed out. Report it but that's it.
240 on_error.report(None)
247 tools.disable_buffering()
248 parser = tools.OptionParserWithLogging(
249 usage='%prog <options>',
251 log_file=RUN_ISOLATED_LOG_FILE)
253 data_group = optparse.OptionGroup(parser, 'Data source')
254 data_group.add_option(
257 help='File/url describing what to map or run')
258 data_group.add_option(
260 help='Hash of the .isolated to grab from the hash table')
261 isolateserver.add_isolate_server_options(data_group, True)
262 parser.add_option_group(data_group)
264 isolateserver.add_cache_options(parser)
265 parser.set_defaults(cache='cache')
267 debug_group = optparse.OptionGroup(parser, 'Debugging')
268 debug_group.add_option(
271 help='Deliberately leak isolate\'s temp dir for later examination '
272 '[default: %default]')
273 parser.add_option_group(debug_group)
275 auth.add_auth_options(parser)
276 options, args = parser.parse_args(args)
277 auth.process_auth_options(parser, options)
278 isolateserver.process_isolate_server_options(parser, options)
280 if bool(options.isolated) == bool(options.hash):
281 logging.debug('One and only one of --isolated or --hash is required.')
282 parser.error('One and only one of --isolated or --hash is required.')
284 cache = isolateserver.process_cache_options(options)
286 remote = options.isolate_server or options.indir
287 if file_path.is_url(remote):
288 auth.ensure_logged_in(remote)
290 with isolateserver.get_storage(remote, options.namespace) as storage:
291 # Hashing schemes used by |storage| and |cache| MUST match.
292 assert storage.hash_algo == cache.hash_algo
294 options.isolated or options.hash, storage, cache,
295 options.leak_temp_dir, args)
298 if __name__ == '__main__':
299 # Ensure that we are always running with the correct encoding.
300 fix_encoding.fix_encoding()
301 sys.exit(main(sys.argv[1:]))