Upstream version 11.40.277.0
[platform/framework/web/crosswalk.git] / src / tools / swarming_client / run_isolated.py
1 #!/usr/bin/env python
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.
5
6 """Reads a .isolated, creates a tree of hardlinks and runs the test.
7
8 To improve performance, it keeps a local cache. The local cache can safely be
9 deleted.
10
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.
15 """
16
17 __version__ = '0.3.2'
18
19 import logging
20 import optparse
21 import os
22 import subprocess
23 import sys
24 import tempfile
25
26 from third_party.depot_tools import fix_encoding
27
28 from utils import file_path
29 from utils import on_error
30 from utils import tools
31 from utils import zip_package
32
33 import auth
34 import isolated_format
35 import isolateserver
36
37
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
40
41 # Directory that contains this file (might be inside zip package).
42 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
43
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()))
48 else:
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
52
53 # The name of the log file to use.
54 RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
55
56 # The name of the log to use for the run_test_cases.py command
57 RUN_TEST_CASES_LOG = 'run_test_cases.log'
58
59
60 def 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__])
69   assert THIS_FILE_PATH
70   assert BASE_DIR
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'))
78   return package
79
80
81 def make_temp_dir(prefix, root_dir):
82   """Returns a temporary directory on the same file system as root_dir."""
83   base_temp_dir = None
84   if (root_dir and
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)
88
89
90 def 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.
100     file_path.make_tree_read_only(rootdir)
101   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.
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
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.
113     file_path.make_tree_writeable(rootdir)
114   else:
115     raise ValueError(
116         'change_tree_read_only(%s, %s): Unknown flag %s' %
117         (rootdir, read_only, read_only))
118
119
120 def process_command(command, out_dir):
121   """Replaces isolated specific variables in a command line."""
122   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
128
129
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.
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.
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
150                 file.
151   """
152   run_dir = make_temp_dir('run_tha_test', cache.cache_dir)
153   out_dir = unicode(make_temp_dir('isolated_out', cache.cache_dir))
154   result = 0
155   try:
156     try:
157       bundle = isolateserver.fetch_isolated(
158           isolated_hash=isolated_hash,
159           storage=storage,
160           cache=cache,
161           outdir=run_dir,
162           require_command=True)
163     except isolated_format.IsolatedError:
164       on_error.report(None)
165       return 1
166
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
170
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))
174
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()
179     if MAIN_DIR:
180       env.setdefault('RUN_TEST_CASES_LOG_FILE',
181           os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
182     try:
183       sys.stdout.flush()
184       with tools.Profiler('RunTest'):
185         result = subprocess.call(command, cwd=cwd, env=env)
186         logging.info(
187             'Command finished with exit code %d (%s)',
188             result, hex(0xffffffff & result))
189     except OSError:
190       on_error.report('Failed to run %s; cwd=%s' % (command, cwd))
191       result = 1
192
193   finally:
194     try:
195       if leak_temp_dir:
196         logging.warning('Deliberately leaking %s for later examination',
197                         run_dir)
198       else:
199         try:
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'
205                 'Fix your stuff.')
206             result = result or 1
207         except OSError:
208           logging.warning('Leaking %s', run_dir)
209           result = 1
210
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:
235       try:
236         if os.path.isdir(out_dir) and not file_path.rmtree(out_dir):
237           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
242
243   return result
244
245
246 def main(args):
247   tools.disable_buffering()
248   parser = tools.OptionParserWithLogging(
249       usage='%prog <options>',
250       version=__version__,
251       log_file=RUN_ISOLATED_LOG_FILE)
252
253   data_group = optparse.OptionGroup(parser, 'Data source')
254   data_group.add_option(
255       '-s', '--isolated',
256       metavar='FILE',
257       help='File/url describing what to map or run')
258   data_group.add_option(
259       '-H', '--hash',
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)
263
264   isolateserver.add_cache_options(parser)
265   parser.set_defaults(cache='cache')
266
267   debug_group = optparse.OptionGroup(parser, 'Debugging')
268   debug_group.add_option(
269       '--leak-temp-dir',
270       action='store_true',
271       help='Deliberately leak isolate\'s temp dir for later examination '
272           '[default: %default]')
273   parser.add_option_group(debug_group)
274
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)
279
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.')
283
284   cache = isolateserver.process_cache_options(options)
285
286   remote = options.isolate_server or options.indir
287   if file_path.is_url(remote):
288     auth.ensure_logged_in(remote)
289
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
293     return run_tha_test(
294         options.isolated or options.hash, storage, cache,
295         options.leak_temp_dir, args)
296
297
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:]))