Upstream version 5.34.104.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.1'
18
19 import ctypes
20 import logging
21 import optparse
22 import os
23 import re
24 import shutil
25 import stat
26 import subprocess
27 import sys
28 import tempfile
29 import time
30
31 from third_party.depot_tools import fix_encoding
32
33 from utils import lru
34 from utils import threading_utils
35 from utils import tools
36 from utils import zip_package
37
38 import auth
39 import isolateserver
40
41
42 # Absolute path to this file (can be None if running from zip on Mac).
43 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
44
45 # Directory that contains this file (might be inside zip package).
46 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
47
48 # Directory that contains currently running script file.
49 if zip_package.get_main_script_path():
50   MAIN_DIR = os.path.dirname(
51       os.path.abspath(zip_package.get_main_script_path()))
52 else:
53   # This happens when 'import run_isolated' is executed at the python
54   # interactive prompt, in that case __file__ is undefined.
55   MAIN_DIR = None
56
57 # Types of action accepted by link_file().
58 HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY = range(1, 5)
59
60 # The name of the log file to use.
61 RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
62
63 # The name of the log to use for the run_test_cases.py command
64 RUN_TEST_CASES_LOG = 'run_test_cases.log'
65
66
67 # Used by get_flavor().
68 FLAVOR_MAPPING = {
69   'cygwin': 'win',
70   'win32': 'win',
71   'darwin': 'mac',
72   'sunos5': 'solaris',
73   'freebsd7': 'freebsd',
74   'freebsd8': 'freebsd',
75 }
76
77
78 def get_as_zip_package(executable=True):
79   """Returns ZipPackage with this module and all its dependencies.
80
81   If |executable| is True will store run_isolated.py as __main__.py so that
82   zip package is directly executable be python.
83   """
84   # Building a zip package when running from another zip package is
85   # unsupported and probably unneeded.
86   assert not zip_package.is_zipped_module(sys.modules[__name__])
87   assert THIS_FILE_PATH
88   assert BASE_DIR
89   package = zip_package.ZipPackage(root=BASE_DIR)
90   package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
91   package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
92   package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
93   package.add_directory(os.path.join(BASE_DIR, 'third_party'))
94   package.add_directory(os.path.join(BASE_DIR, 'utils'))
95   return package
96
97
98 def get_flavor():
99   """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
100   return FLAVOR_MAPPING.get(sys.platform, 'linux')
101
102
103 def hardlink(source, link_name):
104   """Hardlinks a file.
105
106   Add support for os.link() on Windows.
107   """
108   if sys.platform == 'win32':
109     if not ctypes.windll.kernel32.CreateHardLinkW(
110         unicode(link_name), unicode(source), 0):
111       raise OSError()
112   else:
113     os.link(source, link_name)
114
115
116 def readable_copy(outfile, infile):
117   """Makes a copy of the file that is readable by everyone."""
118   shutil.copy2(infile, outfile)
119   read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
120                        stat.S_IRGRP | stat.S_IROTH)
121   os.chmod(outfile, read_enabled_mode)
122
123
124 def link_file(outfile, infile, action):
125   """Links a file. The type of link depends on |action|."""
126   logging.debug('Mapping %s to %s' % (infile, outfile))
127   if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
128     raise ValueError('Unknown mapping action %s' % action)
129   if not os.path.isfile(infile):
130     raise isolateserver.MappingError('%s is missing' % infile)
131   if os.path.isfile(outfile):
132     raise isolateserver.MappingError(
133         '%s already exist; insize:%d; outsize:%d' %
134         (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
135
136   if action == COPY:
137     readable_copy(outfile, infile)
138   elif action == SYMLINK and sys.platform != 'win32':
139     # On windows, symlink are converted to hardlink and fails over to copy.
140     os.symlink(infile, outfile)  # pylint: disable=E1101
141   else:
142     try:
143       hardlink(infile, outfile)
144     except OSError as e:
145       if action == HARDLINK:
146         raise isolateserver.MappingError(
147             'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
148       # Probably a different file system.
149       logging.warning(
150           'Failed to hardlink, failing back to copy %s to %s' % (
151             infile, outfile))
152       readable_copy(outfile, infile)
153
154
155 def set_read_only(path, read_only):
156   """Sets or resets the write bit on a file or directory.
157
158   Zaps out access to 'group' and 'others'.
159   """
160   assert isinstance(read_only, bool), read_only
161   mode = os.lstat(path).st_mode
162   # TODO(maruel): Stop removing GO bits.
163   if read_only:
164     mode = mode & 0500
165   else:
166     mode = mode | 0200
167   if hasattr(os, 'lchmod'):
168     os.lchmod(path, mode)  # pylint: disable=E1101
169   else:
170     if stat.S_ISLNK(mode):
171       # Skip symlink without lchmod() support.
172       logging.debug(
173           'Can\'t change %sw bit on symlink %s',
174           '-' if read_only else '+', path)
175       return
176
177     # TODO(maruel): Implement proper DACL modification on Windows.
178     os.chmod(path, mode)
179
180
181 def make_tree_read_only(root):
182   """Makes all the files in the directories read only.
183
184   Also makes the directories read only, only if it makes sense on the platform.
185
186   This means no file can be created or deleted.
187   """
188   logging.debug('make_tree_read_only(%s)', root)
189   assert os.path.isabs(root), root
190   for dirpath, dirnames, filenames in os.walk(root, topdown=True):
191     for filename in filenames:
192       set_read_only(os.path.join(dirpath, filename), True)
193     if sys.platform != 'win32':
194       # It must not be done on Windows.
195       for dirname in dirnames:
196         set_read_only(os.path.join(dirpath, dirname), True)
197   if sys.platform != 'win32':
198     set_read_only(root, True)
199
200
201 def make_tree_files_read_only(root):
202   """Makes all the files in the directories read only but not the directories
203   themselves.
204
205   This means files can be created or deleted.
206   """
207   logging.debug('make_tree_files_read_only(%s)', root)
208   assert os.path.isabs(root), root
209   if sys.platform != 'win32':
210     set_read_only(root, False)
211   for dirpath, dirnames, filenames in os.walk(root, topdown=True):
212     for filename in filenames:
213       set_read_only(os.path.join(dirpath, filename), True)
214     if sys.platform != 'win32':
215       # It must not be done on Windows.
216       for dirname in dirnames:
217         set_read_only(os.path.join(dirpath, dirname), False)
218
219
220 def make_tree_writeable(root):
221   """Makes all the files in the directories writeable.
222
223   Also makes the directories writeable, only if it makes sense on the platform.
224
225   It is different from make_tree_deleteable() because it unconditionally affects
226   the files.
227   """
228   logging.debug('make_tree_writeable(%s)', root)
229   assert os.path.isabs(root), root
230   if sys.platform != 'win32':
231     set_read_only(root, False)
232   for dirpath, dirnames, filenames in os.walk(root, topdown=True):
233     for filename in filenames:
234       set_read_only(os.path.join(dirpath, filename), False)
235     if sys.platform != 'win32':
236       # It must not be done on Windows.
237       for dirname in dirnames:
238         set_read_only(os.path.join(dirpath, dirname), False)
239
240
241 def make_tree_deleteable(root):
242   """Changes the appropriate permissions so the files in the directories can be
243   deleted.
244
245   On Windows, the files are modified. On other platforms, modify the directory.
246   It only does the minimum so the files can be deleted safely.
247
248   Warning on Windows: since file permission is modified, the file node is
249   modified. This means that for hard-linked files, every directory entry for the
250   file node has its file permission modified.
251   """
252   logging.debug('make_tree_deleteable(%s)', root)
253   assert os.path.isabs(root), root
254   if sys.platform != 'win32':
255     set_read_only(root, False)
256   for dirpath, dirnames, filenames in os.walk(root, topdown=True):
257     if sys.platform == 'win32':
258       for filename in filenames:
259         set_read_only(os.path.join(dirpath, filename), False)
260     else:
261       for dirname in dirnames:
262         set_read_only(os.path.join(dirpath, dirname), False)
263
264
265 def rmtree(root):
266   """Wrapper around shutil.rmtree() to retry automatically on Windows."""
267   make_tree_deleteable(root)
268   logging.info('rmtree(%s)', root)
269   if sys.platform == 'win32':
270     for i in range(3):
271       try:
272         shutil.rmtree(root)
273         break
274       except WindowsError:  # pylint: disable=E0602
275         if i == 2:
276           raise
277         delay = (i+1)*2
278         print >> sys.stderr, (
279             'Failed to delete %s. Maybe the test has subprocess outliving it.'
280             ' Sleep %d seconds.' % (root, delay))
281         time.sleep(delay)
282   else:
283     shutil.rmtree(root)
284
285
286 def try_remove(filepath):
287   """Removes a file without crashing even if it doesn't exist."""
288   try:
289     # TODO(maruel): Not do it unless necessary since it slows this function
290     # down.
291     if sys.platform == 'win32':
292       # Deleting a read-only file will fail if it is read-only.
293       set_read_only(filepath, False)
294     else:
295       # Deleting a read-only file will fail if the directory is read-only.
296       set_read_only(os.path.dirname(filepath), False)
297     os.remove(filepath)
298   except OSError:
299     pass
300
301
302 def is_same_filesystem(path1, path2):
303   """Returns True if both paths are on the same filesystem.
304
305   This is required to enable the use of hardlinks.
306   """
307   assert os.path.isabs(path1), path1
308   assert os.path.isabs(path2), path2
309   if sys.platform == 'win32':
310     # If the drive letter mismatches, assume it's a separate partition.
311     # TODO(maruel): It should look at the underlying drive, a drive letter could
312     # be a mount point to a directory on another drive.
313     assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
314     assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
315     if path1[0].lower() != path2[0].lower():
316       return False
317   return os.stat(path1).st_dev == os.stat(path2).st_dev
318
319
320 def get_free_space(path):
321   """Returns the number of free bytes."""
322   if sys.platform == 'win32':
323     free_bytes = ctypes.c_ulonglong(0)
324     ctypes.windll.kernel32.GetDiskFreeSpaceExW(
325         ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
326     return free_bytes.value
327   # For OSes other than Windows.
328   f = os.statvfs(path)  # pylint: disable=E1101
329   return f.f_bfree * f.f_frsize
330
331
332 def make_temp_dir(prefix, root_dir):
333   """Returns a temporary directory on the same file system as root_dir."""
334   base_temp_dir = None
335   if root_dir and not is_same_filesystem(root_dir, tempfile.gettempdir()):
336     base_temp_dir = os.path.dirname(root_dir)
337   return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
338
339
340 class CachePolicies(object):
341   def __init__(self, max_cache_size, min_free_space, max_items):
342     """
343     Arguments:
344     - max_cache_size: Trim if the cache gets larger than this value. If 0, the
345                       cache is effectively a leak.
346     - min_free_space: Trim if disk free space becomes lower than this value. If
347                       0, it unconditionally fill the disk.
348     - max_items: Maximum number of items to keep in the cache. If 0, do not
349                  enforce a limit.
350     """
351     self.max_cache_size = max_cache_size
352     self.min_free_space = min_free_space
353     self.max_items = max_items
354
355
356 class DiskCache(isolateserver.LocalCache):
357   """Stateful LRU cache in a flat hash table in a directory.
358
359   Saves its state as json file.
360   """
361   STATE_FILE = 'state.json'
362
363   def __init__(self, cache_dir, policies, algo):
364     """
365     Arguments:
366       cache_dir: directory where to place the cache.
367       policies: cache retention policies.
368       algo: hashing algorithm used.
369     """
370     super(DiskCache, self).__init__()
371     self.algo = algo
372     self.cache_dir = cache_dir
373     self.policies = policies
374     self.state_file = os.path.join(cache_dir, self.STATE_FILE)
375
376     # All protected methods (starting with '_') except _path should be called
377     # with this lock locked.
378     self._lock = threading_utils.LockWithAssert()
379     self._lru = lru.LRUDict()
380
381     # Profiling values.
382     self._added = []
383     self._removed = []
384     self._free_disk = 0
385
386     with tools.Profiler('Setup'):
387       with self._lock:
388         self._load()
389
390   def __enter__(self):
391     return self
392
393   def __exit__(self, _exc_type, _exec_value, _traceback):
394     with tools.Profiler('CleanupTrimming'):
395       with self._lock:
396         self._trim()
397
398         logging.info(
399             '%5d (%8dkb) added',
400             len(self._added), sum(self._added) / 1024)
401         logging.info(
402             '%5d (%8dkb) current',
403             len(self._lru),
404             sum(self._lru.itervalues()) / 1024)
405         logging.info(
406             '%5d (%8dkb) removed',
407             len(self._removed), sum(self._removed) / 1024)
408         logging.info(
409             '       %8dkb free',
410             self._free_disk / 1024)
411     return False
412
413   def cached_set(self):
414     with self._lock:
415       return self._lru.keys_set()
416
417   def touch(self, digest, size):
418     """Verifies an actual file is valid.
419
420     Note that is doesn't compute the hash so it could still be corrupted if the
421     file size didn't change.
422
423     TODO(maruel): More stringent verification while keeping the check fast.
424     """
425     # Do the check outside the lock.
426     if not isolateserver.is_valid_file(self._path(digest), size):
427       return False
428
429     # Update it's LRU position.
430     with self._lock:
431       if digest not in self._lru:
432         return False
433       self._lru.touch(digest)
434     return True
435
436   def evict(self, digest):
437     with self._lock:
438       self._lru.pop(digest)
439       self._delete_file(digest, isolateserver.UNKNOWN_FILE_SIZE)
440
441   def read(self, digest):
442     with open(self._path(digest), 'rb') as f:
443       return f.read()
444
445   def write(self, digest, content):
446     path = self._path(digest)
447     # A stale broken file may remain. It is possible for the file to have write
448     # access bit removed which would cause the file_write() call to fail to open
449     # in write mode. Take no chance here.
450     try_remove(path)
451     try:
452       size = isolateserver.file_write(path, content)
453     except:
454       # There are two possible places were an exception can occur:
455       #   1) Inside |content| generator in case of network or unzipping errors.
456       #   2) Inside file_write itself in case of disk IO errors.
457       # In any case delete an incomplete file and propagate the exception to
458       # caller, it will be logged there.
459       try_remove(path)
460       raise
461     # Make the file read-only in the cache.  This has a few side-effects since
462     # the file node is modified, so every directory entries to this file becomes
463     # read-only. It's fine here because it is a new file.
464     set_read_only(path, True)
465     with self._lock:
466       self._add(digest, size)
467
468   def hardlink(self, digest, dest, file_mode):
469     """Hardlinks the file to |dest|.
470
471     Note that the file permission bits are on the file node, not the directory
472     entry, so changing the access bit on any of the directory entries for the
473     file node will affect them all.
474     """
475     path = self._path(digest)
476     link_file(dest, path, HARDLINK)
477     if file_mode is not None:
478       # Ignores all other bits.
479       os.chmod(dest, file_mode & 0500)
480
481   def _load(self):
482     """Loads state of the cache from json file."""
483     self._lock.assert_locked()
484
485     if not os.path.isdir(self.cache_dir):
486       os.makedirs(self.cache_dir)
487     else:
488       # Make sure the cache is read-only.
489       # TODO(maruel): Calculate the cost and optimize the performance
490       # accordingly.
491       make_tree_read_only(self.cache_dir)
492
493     # Load state of the cache.
494     if os.path.isfile(self.state_file):
495       try:
496         self._lru = lru.LRUDict.load(self.state_file)
497       except ValueError as err:
498         logging.error('Failed to load cache state: %s' % (err,))
499         # Don't want to keep broken state file.
500         try_remove(self.state_file)
501
502     # Ensure that all files listed in the state still exist and add new ones.
503     previous = self._lru.keys_set()
504     unknown = []
505     for filename in os.listdir(self.cache_dir):
506       if filename == self.STATE_FILE:
507         continue
508       if filename in previous:
509         previous.remove(filename)
510         continue
511       # An untracked file.
512       if not isolateserver.is_valid_hash(filename, self.algo):
513         logging.warning('Removing unknown file %s from cache', filename)
514         try_remove(self._path(filename))
515         continue
516       # File that's not referenced in 'state.json'.
517       # TODO(vadimsh): Verify its SHA1 matches file name.
518       logging.warning('Adding unknown file %s to cache', filename)
519       unknown.append(filename)
520
521     if unknown:
522       # Add as oldest files. They will be deleted eventually if not accessed.
523       self._add_oldest_list(unknown)
524       logging.warning('Added back %d unknown files', len(unknown))
525
526     if previous:
527       # Filter out entries that were not found.
528       logging.warning('Removed %d lost files', len(previous))
529       for filename in previous:
530         self._lru.pop(filename)
531     self._trim()
532
533   def _save(self):
534     """Saves the LRU ordering."""
535     self._lock.assert_locked()
536     if sys.platform != 'win32':
537       d = os.path.dirname(self.state_file)
538       if os.path.isdir(d):
539         # Necessary otherwise the file can't be created.
540         set_read_only(d, False)
541     if os.path.isfile(self.state_file):
542       set_read_only(self.state_file, False)
543     self._lru.save(self.state_file)
544
545   def _trim(self):
546     """Trims anything we don't know, make sure enough free space exists."""
547     self._lock.assert_locked()
548
549     # Ensure maximum cache size.
550     if self.policies.max_cache_size:
551       total_size = sum(self._lru.itervalues())
552       while total_size > self.policies.max_cache_size:
553         total_size -= self._remove_lru_file()
554
555     # Ensure maximum number of items in the cache.
556     if self.policies.max_items and len(self._lru) > self.policies.max_items:
557       for _ in xrange(len(self._lru) - self.policies.max_items):
558         self._remove_lru_file()
559
560     # Ensure enough free space.
561     self._free_disk = get_free_space(self.cache_dir)
562     trimmed_due_to_space = False
563     while (
564         self.policies.min_free_space and
565         self._lru and
566         self._free_disk < self.policies.min_free_space):
567       trimmed_due_to_space = True
568       self._remove_lru_file()
569       self._free_disk = get_free_space(self.cache_dir)
570     if trimmed_due_to_space:
571       total = sum(self._lru.itervalues())
572       logging.warning(
573           'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
574           'cache (%.1f%% of its maximum capacity)',
575           self._free_disk / 1024.,
576           total / 1024.,
577           100. * self.policies.max_cache_size / float(total),
578           )
579     self._save()
580
581   def _path(self, digest):
582     """Returns the path to one item."""
583     return os.path.join(self.cache_dir, digest)
584
585   def _remove_lru_file(self):
586     """Removes the last recently used file and returns its size."""
587     self._lock.assert_locked()
588     digest, size = self._lru.pop_oldest()
589     self._delete_file(digest, size)
590     return size
591
592   def _add(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
593     """Adds an item into LRU cache marking it as a newest one."""
594     self._lock.assert_locked()
595     if size == isolateserver.UNKNOWN_FILE_SIZE:
596       size = os.stat(self._path(digest)).st_size
597     self._added.append(size)
598     self._lru.add(digest, size)
599
600   def _add_oldest_list(self, digests):
601     """Adds a bunch of items into LRU cache marking them as oldest ones."""
602     self._lock.assert_locked()
603     pairs = []
604     for digest in digests:
605       size = os.stat(self._path(digest)).st_size
606       self._added.append(size)
607       pairs.append((digest, size))
608     self._lru.batch_insert_oldest(pairs)
609
610   def _delete_file(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
611     """Deletes cache file from the file system."""
612     self._lock.assert_locked()
613     try:
614       if size == isolateserver.UNKNOWN_FILE_SIZE:
615         size = os.stat(self._path(digest)).st_size
616       try_remove(self._path(digest))
617       self._removed.append(size)
618     except OSError as e:
619       logging.error('Error attempting to delete a file %s:\n%s' % (digest, e))
620
621
622 def change_tree_read_only(rootdir, read_only):
623   """Changes the tree read-only bits according to the read_only specification.
624
625   The flag can be 0, 1 or 2, which will affect the possibility to modify files
626   and create or delete files.
627   """
628   if read_only == 2:
629     # Files and directories (except on Windows) are marked read only. This
630     # inhibits modifying, creating or deleting files in the test directory,
631     # except on Windows where creating and deleting files is still possible.
632     make_tree_read_only(rootdir)
633   elif read_only == 1:
634     # Files are marked read only but not the directories. This inhibits
635     # modifying files but creating or deleting files is still possible.
636     make_tree_files_read_only(rootdir)
637   elif read_only in (0, None):
638     # Anything can be modified. This is the default in the .isolated file
639     # format.
640     #
641     # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
642     # is not yet changed to verify the hash of the content of the files it is
643     # looking at, so that if a test modifies an input file, the file must be
644     # deleted.
645     make_tree_writeable(rootdir)
646   else:
647     raise ValueError(
648         'change_tree_read_only(%s, %s): Unknown flag %s' %
649         (rootdir, read_only, read_only))
650
651
652 def process_command(command, out_dir):
653   """Replaces isolated specific variables in a command line."""
654   return [c.replace('${ISOLATED_OUTDIR}', out_dir) for c in command]
655
656
657 def run_tha_test(isolated_hash, storage, cache, algo, extra_args):
658   """Downloads the dependencies in the cache, hardlinks them into a temporary
659   directory and runs the executable from there.
660
661   A temporary directory is created to hold the output files. The content inside
662   this directory will be uploaded back to |storage| packaged as a .isolated
663   file.
664
665   Arguments:
666     isolated_hash: the sha-1 of the .isolated file that must be retrieved to
667                    recreate the tree of files to run the target executable.
668     storage: an isolateserver.Storage object to retrieve remote objects. This
669              object has a reference to an isolateserver.StorageApi, which does
670              the actual I/O.
671     cache: an isolateserver.LocalCache to keep from retrieving the same objects
672            constantly by caching the objects retrieved. Can be on-disk or
673            in-memory.
674     algo: an hashlib class to hash content. Usually hashlib.sha1.
675     extra_args: optional arguments to add to the command stated in the .isolate
676                 file.
677   """
678   run_dir = make_temp_dir('run_tha_test', cache.cache_dir)
679   out_dir = unicode(tempfile.mkdtemp(prefix='run_tha_test'))
680   result = 0
681   try:
682     try:
683       settings = isolateserver.fetch_isolated(
684           isolated_hash=isolated_hash,
685           storage=storage,
686           cache=cache,
687           algo=algo,
688           outdir=run_dir,
689           os_flavor=get_flavor(),
690           require_command=True)
691     except isolateserver.ConfigError as e:
692       tools.report_error(e)
693       result = 1
694       return result
695
696     change_tree_read_only(run_dir, settings.read_only)
697     cwd = os.path.normpath(os.path.join(run_dir, settings.relative_cwd))
698     command = settings.command + extra_args
699
700     # subprocess.call doesn't consider 'cwd' when searching for executable.
701     # Yet isolate can specify command relative to 'cwd'. Convert it to absolute
702     # path if necessary.
703     if not os.path.isabs(command[0]):
704       command[0] = os.path.abspath(os.path.join(cwd, command[0]))
705     command = process_command(command, out_dir)
706     logging.info('Running %s, cwd=%s' % (command, cwd))
707
708     # TODO(csharp): This should be specified somewhere else.
709     # TODO(vadimsh): Pass it via 'env_vars' in manifest.
710     # Add a rotating log file if one doesn't already exist.
711     env = os.environ.copy()
712     if MAIN_DIR:
713       env.setdefault('RUN_TEST_CASES_LOG_FILE',
714           os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
715     try:
716       with tools.Profiler('RunTest'):
717         result = subprocess.call(command, cwd=cwd, env=env)
718     except OSError as e:
719       tools.report_error('Failed to run %s; cwd=%s: %s' % (command, cwd, e))
720       result = 1
721
722     # Upload out_dir and generate a .isolated file out of this directory. It is
723     # only done if files were written in the directory.
724     if os.listdir(out_dir):
725       with tools.Profiler('ArchiveOutput'):
726         results = isolateserver.archive_files_to_storage(
727             storage, algo, [out_dir], None)
728       # TODO(maruel): Implement side-channel to publish this information.
729       print('run_isolated output: %s' % results[0][0])
730
731   finally:
732     try:
733       rmtree(out_dir)
734     finally:
735       try:
736         rmtree(run_dir)
737       except OSError:
738         logging.warning('Leaking %s', run_dir)
739         # Swallow the exception so it doesn't generate an infrastructure error.
740         #
741         # It usually happens on Windows when a child process is not properly
742         # terminated, usually because of a test case starting child processes
743         # that time out. This causes files to be locked and it becomes
744         # impossible to delete them.
745         #
746         # Only report an infrastructure error if the test didn't fail. This is
747         # because a swarming bot will likely not reboot. This situation will
748         # cause accumulation of temporary hardlink trees.
749         if not result:
750           raise
751   return result
752
753
754 def main(args):
755   tools.disable_buffering()
756   parser = tools.OptionParserWithLogging(
757       usage='%prog <options>',
758       version=__version__,
759       log_file=RUN_ISOLATED_LOG_FILE)
760
761   data_group = optparse.OptionGroup(parser, 'Data source')
762   data_group.add_option(
763       '-s', '--isolated',
764       metavar='FILE',
765       help='File/url describing what to map or run')
766   data_group.add_option(
767       '-H', '--hash',
768       help='Hash of the .isolated to grab from the hash table')
769   isolateserver.add_isolate_server_options(data_group, True)
770   parser.add_option_group(data_group)
771
772   cache_group = optparse.OptionGroup(parser, 'Cache management')
773   cache_group.add_option(
774       '--cache',
775       default='cache',
776       metavar='DIR',
777       help='Cache directory, default=%default')
778   cache_group.add_option(
779       '--max-cache-size',
780       type='int',
781       metavar='NNN',
782       default=20*1024*1024*1024,
783       help='Trim if the cache gets larger than this value, default=%default')
784   cache_group.add_option(
785       '--min-free-space',
786       type='int',
787       metavar='NNN',
788       default=2*1024*1024*1024,
789       help='Trim if disk free space becomes lower than this value, '
790            'default=%default')
791   cache_group.add_option(
792       '--max-items',
793       type='int',
794       metavar='NNN',
795       default=100000,
796       help='Trim if more than this number of items are in the cache '
797            'default=%default')
798   parser.add_option_group(cache_group)
799
800   auth.add_auth_options(parser)
801   options, args = parser.parse_args(args)
802   auth.process_auth_options(parser, options)
803   isolateserver.process_isolate_server_options(data_group, options)
804
805   if bool(options.isolated) == bool(options.hash):
806     logging.debug('One and only one of --isolated or --hash is required.')
807     parser.error('One and only one of --isolated or --hash is required.')
808
809   options.cache = os.path.abspath(options.cache)
810   policies = CachePolicies(
811       options.max_cache_size, options.min_free_space, options.max_items)
812   algo = isolateserver.get_hash_algo(options.namespace)
813
814   try:
815     # |options.cache| may not exist until DiskCache() instance is created.
816     cache = DiskCache(options.cache, policies, algo)
817     remote = options.isolate_server or options.indir
818     with isolateserver.get_storage(remote, options.namespace) as storage:
819       return run_tha_test(
820           options.isolated or options.hash, storage, cache, algo, args)
821   except Exception as e:
822     # Make sure any exception is logged.
823     tools.report_error(e)
824     logging.exception(e)
825     return 1
826
827
828 if __name__ == '__main__':
829   # Ensure that we are always running with the correct encoding.
830   fix_encoding.fix_encoding()
831   sys.exit(main(sys.argv[1:]))