blob: 10bc315d65aea3bd4e1fbcd88ca220383cd12ee8 [file] [log] [blame]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
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
8Keeps a local cache.
9"""
10
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +000011__version__ = '0.2'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000012
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000013import ctypes
14import hashlib
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000015import logging
16import optparse
17import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000018import re
19import shutil
20import stat
21import subprocess
22import sys
23import tempfile
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000024import time
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
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000028from utils import lru
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000029from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000030from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000031
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000032import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000033
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000034
vadimsh@chromium.org85071062013-08-21 23:37:45 +000035# Absolute path to this file (can be None if running from zip on Mac).
36THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000037
38# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000039BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000040
41# Directory that contains currently running script file.
42MAIN_DIR = os.path.dirname(os.path.abspath(zip_package.get_main_script_path()))
43
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000044# Types of action accepted by link_file().
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000045HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY = range(1, 5)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000046
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000047# The name of the log file to use.
48RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
49
csharp@chromium.orge217f302012-11-22 16:51:53 +000050# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000051RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000052
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +000053# Maximum expected delay (in seconds) between successive file fetches
54# in run_tha_test. If it takes longer than that, a deadlock might be happening
55# and all stack frames for all threads are dumped to log.
56DEADLOCK_TIMEOUT = 5 * 60
57
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000058
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +000059# Used by get_flavor().
60FLAVOR_MAPPING = {
61 'cygwin': 'win',
62 'win32': 'win',
63 'darwin': 'mac',
64 'sunos5': 'solaris',
65 'freebsd7': 'freebsd',
66 'freebsd8': 'freebsd',
67}
68
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000069
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000070def get_as_zip_package(executable=True):
71 """Returns ZipPackage with this module and all its dependencies.
72
73 If |executable| is True will store run_isolated.py as __main__.py so that
74 zip package is directly executable be python.
75 """
76 # Building a zip package when running from another zip package is
77 # unsupported and probably unneeded.
78 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000079 assert THIS_FILE_PATH
80 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000081 package = zip_package.ZipPackage(root=BASE_DIR)
82 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000083 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000084 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
85 package.add_directory(os.path.join(BASE_DIR, 'utils'))
86 return package
87
88
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000089def get_flavor():
90 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +000091 return FLAVOR_MAPPING.get(sys.platform, 'linux')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000092
93
94def os_link(source, link_name):
95 """Add support for os.link() on Windows."""
96 if sys.platform == 'win32':
97 if not ctypes.windll.kernel32.CreateHardLinkW(
98 unicode(link_name), unicode(source), 0):
99 raise OSError()
100 else:
101 os.link(source, link_name)
102
103
104def readable_copy(outfile, infile):
105 """Makes a copy of the file that is readable by everyone."""
csharp@chromium.org59d116d2013-07-05 18:04:08 +0000106 shutil.copy2(infile, outfile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000107 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
108 stat.S_IRGRP | stat.S_IROTH)
109 os.chmod(outfile, read_enabled_mode)
110
111
112def link_file(outfile, infile, action):
113 """Links a file. The type of link depends on |action|."""
114 logging.debug('Mapping %s to %s' % (infile, outfile))
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000115 if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000116 raise ValueError('Unknown mapping action %s' % action)
117 if not os.path.isfile(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000118 raise isolateserver.MappingError('%s is missing' % infile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000119 if os.path.isfile(outfile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000120 raise isolateserver.MappingError(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000121 '%s already exist; insize:%d; outsize:%d' %
122 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
123
124 if action == COPY:
125 readable_copy(outfile, infile)
126 elif action == SYMLINK and sys.platform != 'win32':
127 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000128 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000129 else:
130 try:
131 os_link(infile, outfile)
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000132 except OSError as e:
133 if action == HARDLINK:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000134 raise isolateserver.MappingError(
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000135 'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000136 # Probably a different file system.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000137 logging.warning(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000138 'Failed to hardlink, failing back to copy %s to %s' % (
139 infile, outfile))
140 readable_copy(outfile, infile)
141
142
143def _set_write_bit(path, read_only):
144 """Sets or resets the executable bit on a file or directory."""
145 mode = os.lstat(path).st_mode
146 if read_only:
147 mode = mode & 0500
148 else:
149 mode = mode | 0200
150 if hasattr(os, 'lchmod'):
151 os.lchmod(path, mode) # pylint: disable=E1101
152 else:
153 if stat.S_ISLNK(mode):
154 # Skip symlink without lchmod() support.
155 logging.debug('Can\'t change +w bit on symlink %s' % path)
156 return
157
158 # TODO(maruel): Implement proper DACL modification on Windows.
159 os.chmod(path, mode)
160
161
162def make_writable(root, read_only):
163 """Toggle the writable bit on a directory tree."""
csharp@chromium.org837352f2013-01-17 21:17:03 +0000164 assert os.path.isabs(root), root
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000165 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
166 for filename in filenames:
167 _set_write_bit(os.path.join(dirpath, filename), read_only)
168
169 for dirname in dirnames:
170 _set_write_bit(os.path.join(dirpath, dirname), read_only)
171
172
173def rmtree(root):
174 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
175 make_writable(root, False)
176 if sys.platform == 'win32':
177 for i in range(3):
178 try:
179 shutil.rmtree(root)
180 break
181 except WindowsError: # pylint: disable=E0602
182 delay = (i+1)*2
183 print >> sys.stderr, (
184 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
185 time.sleep(delay)
186 else:
187 shutil.rmtree(root)
188
189
190def is_same_filesystem(path1, path2):
191 """Returns True if both paths are on the same filesystem.
192
193 This is required to enable the use of hardlinks.
194 """
195 assert os.path.isabs(path1), path1
196 assert os.path.isabs(path2), path2
197 if sys.platform == 'win32':
198 # If the drive letter mismatches, assume it's a separate partition.
199 # TODO(maruel): It should look at the underlying drive, a drive letter could
200 # be a mount point to a directory on another drive.
201 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
202 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
203 if path1[0].lower() != path2[0].lower():
204 return False
205 return os.stat(path1).st_dev == os.stat(path2).st_dev
206
207
208def get_free_space(path):
209 """Returns the number of free bytes."""
210 if sys.platform == 'win32':
211 free_bytes = ctypes.c_ulonglong(0)
212 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
213 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
214 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000215 # For OSes other than Windows.
216 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000217 return f.f_bfree * f.f_frsize
218
219
220def make_temp_dir(prefix, root_dir):
221 """Returns a temporary directory on the same file system as root_dir."""
222 base_temp_dir = None
223 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
224 base_temp_dir = os.path.dirname(root_dir)
225 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
226
227
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000228class CachePolicies(object):
229 def __init__(self, max_cache_size, min_free_space, max_items):
230 """
231 Arguments:
232 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
233 cache is effectively a leak.
234 - min_free_space: Trim if disk free space becomes lower than this value. If
235 0, it unconditionally fill the disk.
236 - max_items: Maximum number of items to keep in the cache. If 0, do not
237 enforce a limit.
238 """
239 self.max_cache_size = max_cache_size
240 self.min_free_space = min_free_space
241 self.max_items = max_items
242
243
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000244class DiskCache(object):
245 """Stateful LRU cache in a flat hash table in a directory.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000246
247 Saves its state as json file.
248 """
249 STATE_FILE = 'state.json'
250
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000251 def __init__(self, cache_dir, retriever, policies, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000252 """
253 Arguments:
254 - cache_dir: Directory where to place the cache.
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000255 - retriever: API where to fetch items from.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000256 - policies: cache retention policies.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000257 - algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000258 """
259 self.cache_dir = cache_dir
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000260 self.retriever = retriever
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000261 self.policies = policies
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000262 self._pool = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000263 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000264 self.lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000265
266 # Items currently being fetched. Keep it local to reduce lock contention.
267 self._pending_queue = set()
268
269 # Profiling values.
270 self._added = []
271 self._removed = []
272 self._free_disk = 0
273
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000274 with tools.Profiler('Setup'):
maruel@chromium.org770993b2012-12-11 17:16:48 +0000275 if not os.path.isdir(self.cache_dir):
276 os.makedirs(self.cache_dir)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000277
278 # Load state of the cache.
vadimsh@chromium.orga40428e2013-07-04 15:43:14 +0000279 if os.path.isfile(self.state_file):
280 try:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000281 self.lru = lru.LRUDict.load(self.state_file)
282 except ValueError as err:
283 logging.error('Failed to load cache state: %s' % (err,))
284 # Don't want to keep broken state file.
285 os.remove(self.state_file)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000286
maruel@chromium.org770993b2012-12-11 17:16:48 +0000287 # Ensure that all files listed in the state still exist and add new ones.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000288 previous = self.lru.keys_set()
289 unknown = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000290 for filename in os.listdir(self.cache_dir):
291 if filename == self.STATE_FILE:
292 continue
293 if filename in previous:
294 previous.remove(filename)
295 continue
296 # An untracked file.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000297 if not isolateserver.is_valid_hash(filename, algo):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000298 logging.warning('Removing unknown file %s from cache', filename)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000299 os.remove(self._path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000300 continue
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000301 # File that's not referenced in 'state.json'.
302 # TODO(vadimsh): Verify its SHA1 matches file name.
303 logging.warning('Adding unknown file %s to cache', filename)
304 unknown.append(filename)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000305
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000306 if unknown:
307 # Add as oldest files. They will be deleted eventually if not accessed.
308 self._add_oldest_list(unknown)
309 logging.warning('Added back %d unknown files', len(unknown))
310
maruel@chromium.org770993b2012-12-11 17:16:48 +0000311 if previous:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000312 # Filter out entries that were not found.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000313 logging.warning('Removed %d lost files', len(previous))
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000314 for filename in previous:
315 self.lru.pop(filename)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000316 self.trim()
317
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000318 def set_pool(self, pool):
319 """Sets an isolateserver.WorkerPool."""
320 self._pool = pool
321
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000322 def __enter__(self):
323 return self
324
325 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000326 with tools.Profiler('CleanupTrimming'):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000327 self.trim()
328
329 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000330 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000331 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000332 '%5d (%8dkb) current',
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000333 len(self.lru),
334 sum(self.lru.itervalues()) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000335 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000336 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
337 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org41601642013-09-18 19:40:46 +0000338 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000339
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000340 def trim(self):
341 """Trims anything we don't know, make sure enough free space exists."""
342 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000343 if self.policies.max_cache_size:
344 total_size = sum(self.lru.itervalues())
345 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000346 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000347
348 # Ensure maximum number of items in the cache.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000349 if self.policies.max_items and len(self.lru) > self.policies.max_items:
350 for _ in xrange(len(self.lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000351 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000352
353 # Ensure enough free space.
354 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000355 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000356 while (
357 self.policies.min_free_space and
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000358 self.lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000359 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000360 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000361 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000362 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000363 if trimmed_due_to_space:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000364 total = sum(self.lru.itervalues())
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000365 logging.warning(
366 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
367 'cache (%.1f%% of its maximum capacity)',
368 self._free_disk / 1024.,
369 total / 1024.,
370 100. * self.policies.max_cache_size / float(total),
371 )
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000372 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000373
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000374 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000375 """Retrieves a file from the remote, if not already cached, and adds it to
376 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000377
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000378 If the file is in the cache, verify that the file is valid (i.e. it is
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000379 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000380 """
381 assert not '/' in item
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000382 path = self._path(item)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000383 found = False
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000384
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000385 if item in self.lru:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000386 # Note that is doesn't compute the hash so it could still be corrupted.
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000387 if not isolateserver.is_valid_file(self._path(item), size):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000388 self.lru.pop(item)
389 self._delete_file(item, size)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000390 else:
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000391 # Was already in cache. Update it's LRU value by putting it at the end.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000392 self.lru.touch(item)
393 found = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000394
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000395 if not found:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000396 if item in self._pending_queue:
397 # Already pending. The same object could be referenced multiple times.
398 return
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000399 # TODO(maruel): It should look at the free disk space, the current cache
400 # size and the size of the new item on every new item:
401 # - Trim the cache as more entries are listed when free disk space is low,
402 # otherwise if the amount of data downloaded during the run > free disk
403 # space, it'll crash.
404 # - Make sure there's enough free disk space to fit all dependencies of
405 # this run! If not, abort early.
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000406 self._pool.add_task(priority, self._store, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000407 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000408
409 def add(self, filepath, obj):
410 """Forcibly adds a file to the cache."""
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000411 if obj not in self.lru:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000412 link_file(self._path(obj), filepath, HARDLINK)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000413 self._add(obj)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000414
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000415 def store_to(self, obj, dest):
416 link_file(dest, self._path(obj), HARDLINK)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000417
maruel@chromium.org41601642013-09-18 19:40:46 +0000418 def read(self, item):
419 """Reads an item from the cache."""
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000420 with open(self._path(item), 'rb') as f:
maruel@chromium.org41601642013-09-18 19:40:46 +0000421 return f.read()
422
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000423 def wait_for(self, items):
424 """Starts a loop that waits for at least one of |items| to be retrieved.
425
426 Returns the first item retrieved.
427 """
428 # Flush items already present.
429 for item in items:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000430 if item in self.lru:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000431 return item
432
433 assert all(i in self._pending_queue for i in items), (
434 items, self._pending_queue)
435 # Note that:
436 # len(self._pending_queue) ==
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000437 # ( len(self.remote_fetcher._workers) - self.remote_fetcher._ready +
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000438 # len(self._remote._queue) + len(self._remote.done))
439 # There is no lock-free way to verify that.
440 while self._pending_queue:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000441 item = self._pool.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000442 self._pending_queue.remove(item)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000443 self._add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000444 if item in items:
445 return item
446
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000447 def _path(self, item):
448 """Returns the path to one item."""
449 return os.path.join(self.cache_dir, item)
450
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000451 def _save(self):
452 """Saves the LRU ordering."""
453 self.lru.save(self.state_file)
454
455 def _remove_lru_file(self):
456 """Removes the last recently used file and returns its size."""
457 item, size = self.lru.pop_oldest()
458 self._delete_file(item, size)
459 return size
460
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000461 def _add(self, item):
462 """Adds an item into LRU cache marking it as a newest one."""
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000463 size = os.stat(self._path(item)).st_size
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000464 self._added.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000465 self.lru.add(item, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000466
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000467 def _add_oldest_list(self, items):
468 """Adds a bunch of items into LRU cache marking them as oldest ones."""
469 pairs = []
470 for item in items:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000471 size = os.stat(self._path(item)).st_size
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000472 self._added.append(size)
473 pairs.append((item, size))
474 self.lru.batch_insert_oldest(pairs)
475
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000476 def _store(self, item, path, expected_size):
477 """Stores the data generated by remote_fetcher."""
478 isolateserver.file_write(path, self.retriever(item, expected_size))
479 return item
480
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000481 def _delete_file(self, item, size):
482 """Deletes cache file from the file system."""
483 self._removed.append(size)
484 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000485 os.remove(self._path(item))
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000486 except OSError as e:
487 logging.error('Error attempting to delete a file\n%s' % e)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000488
489
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000490def run_tha_test(isolated_hash, cache_dir, retriever, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000491 """Downloads the dependencies in the cache, hardlinks them into a temporary
492 directory and runs the executable.
493 """
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000494 algo = hashlib.sha1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000495 outdir = None
496 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000497 cache = DiskCache(cache_dir, retriever, policies, algo)
498 # |cache_dir| may not exist until DiskCache() instance is created.
499 outdir = make_temp_dir('run_tha_test', cache_dir)
500 try:
501 settings = isolateserver.fetch_isolated(
502 isolated_hash, cache, outdir, get_flavor(), algo, True)
503 except isolateserver.ConfigError as e:
504 print >> sys.stderr, str(e)
505 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000506
507 if settings.read_only:
508 logging.info('Making files read only')
509 make_writable(outdir, True)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000510 cwd = os.path.normpath(os.path.join(outdir, settings.relative_cwd))
511 logging.info('Running %s, cwd=%s' % (settings.command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000512
513 # TODO(csharp): This should be specified somewhere else.
514 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
515 # Add a rotating log file if one doesn't already exist.
516 env = os.environ.copy()
517 env.setdefault('RUN_TEST_CASES_LOG_FILE',
518 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000519 try:
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000520 with tools.Profiler('RunTest'):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000521 return subprocess.call(settings.command, cwd=cwd, env=env)
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000522 except OSError:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000523 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (settings.command, cwd)
524 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000525 finally:
526 if outdir:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000527 rmtree(outdir)
528
529
530def main():
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000531 tools.disable_buffering()
532 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000533 usage='%prog <options>',
534 version=__version__,
535 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000536
537 group = optparse.OptionGroup(parser, 'Data source')
538 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000539 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000540 metavar='FILE',
541 help='File/url describing what to map or run')
542 group.add_option(
543 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000544 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000545 group.add_option(
546 '-I', '--isolate-server', metavar='URL',
547 default=
548 'https://isolateserver.appspot.com',
549 help='Remote where to get the items. Defaults to %default')
550 group.add_option(
551 '-n', '--namespace',
552 default='default-gzip',
553 help='namespace to use when using isolateserver, default: %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000554 parser.add_option_group(group)
555
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000556 group = optparse.OptionGroup(parser, 'Cache management')
557 group.add_option(
558 '--cache',
559 default='cache',
560 metavar='DIR',
561 help='Cache directory, default=%default')
562 group.add_option(
563 '--max-cache-size',
564 type='int',
565 metavar='NNN',
566 default=20*1024*1024*1024,
567 help='Trim if the cache gets larger than this value, default=%default')
568 group.add_option(
569 '--min-free-space',
570 type='int',
571 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000572 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000573 help='Trim if disk free space becomes lower than this value, '
574 'default=%default')
575 group.add_option(
576 '--max-items',
577 type='int',
578 metavar='NNN',
579 default=100000,
580 help='Trim if more than this number of items are in the cache '
581 'default=%default')
582 parser.add_option_group(group)
583
584 options, args = parser.parse_args()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000585
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000586 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000587 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000588 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000589 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000590 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000591 parser.error('Unsupported args %s' % ' '.join(args))
592
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000593 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000594 policies = CachePolicies(
595 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000596
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000597 retriever = isolateserver.get_storage_api(
598 options.isolate_server, options.namespace)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000599 try:
600 return run_tha_test(
601 options.isolated or options.hash,
602 options.cache,
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000603 retriever.fetch,
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000604 policies)
605 except Exception, e:
606 # Make sure any exception is logged.
607 logging.exception(e)
608 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000609
610
611if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000612 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000613 fix_encoding.fix_encoding()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000614 sys.exit(main())