blob: 950f33c67431f9e77aad7228c671ade216125329 [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.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000042if zip_package.get_main_script_path():
43 MAIN_DIR = os.path.dirname(
44 os.path.abspath(zip_package.get_main_script_path()))
45else:
46 # This happens when 'import run_isolated' is executed at the python
47 # interactive prompt, in that case __file__ is undefined.
48 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000049
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000050# Types of action accepted by link_file().
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000051HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY = range(1, 5)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000052
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000053# The name of the log file to use.
54RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
55
csharp@chromium.orge217f302012-11-22 16:51:53 +000056# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000057RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000058
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +000059# Maximum expected delay (in seconds) between successive file fetches
60# in run_tha_test. If it takes longer than that, a deadlock might be happening
61# and all stack frames for all threads are dumped to log.
62DEADLOCK_TIMEOUT = 5 * 60
63
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000064
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +000065# Used by get_flavor().
66FLAVOR_MAPPING = {
67 'cygwin': 'win',
68 'win32': 'win',
69 'darwin': 'mac',
70 'sunos5': 'solaris',
71 'freebsd7': 'freebsd',
72 'freebsd8': 'freebsd',
73}
74
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000075
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000076def get_as_zip_package(executable=True):
77 """Returns ZipPackage with this module and all its dependencies.
78
79 If |executable| is True will store run_isolated.py as __main__.py so that
80 zip package is directly executable be python.
81 """
82 # Building a zip package when running from another zip package is
83 # unsupported and probably unneeded.
84 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000085 assert THIS_FILE_PATH
86 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000087 package = zip_package.ZipPackage(root=BASE_DIR)
88 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000089 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000090 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
91 package.add_directory(os.path.join(BASE_DIR, 'utils'))
92 return package
93
94
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000095def get_flavor():
96 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +000097 return FLAVOR_MAPPING.get(sys.platform, 'linux')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000098
99
100def os_link(source, link_name):
101 """Add support for os.link() on Windows."""
102 if sys.platform == 'win32':
103 if not ctypes.windll.kernel32.CreateHardLinkW(
104 unicode(link_name), unicode(source), 0):
105 raise OSError()
106 else:
107 os.link(source, link_name)
108
109
110def readable_copy(outfile, infile):
111 """Makes a copy of the file that is readable by everyone."""
csharp@chromium.org59d116d2013-07-05 18:04:08 +0000112 shutil.copy2(infile, outfile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000113 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
114 stat.S_IRGRP | stat.S_IROTH)
115 os.chmod(outfile, read_enabled_mode)
116
117
118def link_file(outfile, infile, action):
119 """Links a file. The type of link depends on |action|."""
120 logging.debug('Mapping %s to %s' % (infile, outfile))
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000121 if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000122 raise ValueError('Unknown mapping action %s' % action)
123 if not os.path.isfile(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000124 raise isolateserver.MappingError('%s is missing' % infile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000125 if os.path.isfile(outfile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000126 raise isolateserver.MappingError(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000127 '%s already exist; insize:%d; outsize:%d' %
128 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
129
130 if action == COPY:
131 readable_copy(outfile, infile)
132 elif action == SYMLINK and sys.platform != 'win32':
133 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000134 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000135 else:
136 try:
137 os_link(infile, outfile)
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000138 except OSError as e:
139 if action == HARDLINK:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000140 raise isolateserver.MappingError(
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000141 'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000142 # Probably a different file system.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000143 logging.warning(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000144 'Failed to hardlink, failing back to copy %s to %s' % (
145 infile, outfile))
146 readable_copy(outfile, infile)
147
148
149def _set_write_bit(path, read_only):
150 """Sets or resets the executable bit on a file or directory."""
151 mode = os.lstat(path).st_mode
152 if read_only:
153 mode = mode & 0500
154 else:
155 mode = mode | 0200
156 if hasattr(os, 'lchmod'):
157 os.lchmod(path, mode) # pylint: disable=E1101
158 else:
159 if stat.S_ISLNK(mode):
160 # Skip symlink without lchmod() support.
161 logging.debug('Can\'t change +w bit on symlink %s' % path)
162 return
163
164 # TODO(maruel): Implement proper DACL modification on Windows.
165 os.chmod(path, mode)
166
167
168def make_writable(root, read_only):
169 """Toggle the writable bit on a directory tree."""
csharp@chromium.org837352f2013-01-17 21:17:03 +0000170 assert os.path.isabs(root), root
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000171 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
172 for filename in filenames:
173 _set_write_bit(os.path.join(dirpath, filename), read_only)
174
175 for dirname in dirnames:
176 _set_write_bit(os.path.join(dirpath, dirname), read_only)
177
178
179def rmtree(root):
180 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
181 make_writable(root, False)
182 if sys.platform == 'win32':
183 for i in range(3):
184 try:
185 shutil.rmtree(root)
186 break
187 except WindowsError: # pylint: disable=E0602
188 delay = (i+1)*2
189 print >> sys.stderr, (
190 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
191 time.sleep(delay)
192 else:
193 shutil.rmtree(root)
194
195
196def is_same_filesystem(path1, path2):
197 """Returns True if both paths are on the same filesystem.
198
199 This is required to enable the use of hardlinks.
200 """
201 assert os.path.isabs(path1), path1
202 assert os.path.isabs(path2), path2
203 if sys.platform == 'win32':
204 # If the drive letter mismatches, assume it's a separate partition.
205 # TODO(maruel): It should look at the underlying drive, a drive letter could
206 # be a mount point to a directory on another drive.
207 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
208 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
209 if path1[0].lower() != path2[0].lower():
210 return False
211 return os.stat(path1).st_dev == os.stat(path2).st_dev
212
213
214def get_free_space(path):
215 """Returns the number of free bytes."""
216 if sys.platform == 'win32':
217 free_bytes = ctypes.c_ulonglong(0)
218 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
219 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
220 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000221 # For OSes other than Windows.
222 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000223 return f.f_bfree * f.f_frsize
224
225
226def make_temp_dir(prefix, root_dir):
227 """Returns a temporary directory on the same file system as root_dir."""
228 base_temp_dir = None
229 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
230 base_temp_dir = os.path.dirname(root_dir)
231 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
232
233
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000234class CachePolicies(object):
235 def __init__(self, max_cache_size, min_free_space, max_items):
236 """
237 Arguments:
238 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
239 cache is effectively a leak.
240 - min_free_space: Trim if disk free space becomes lower than this value. If
241 0, it unconditionally fill the disk.
242 - max_items: Maximum number of items to keep in the cache. If 0, do not
243 enforce a limit.
244 """
245 self.max_cache_size = max_cache_size
246 self.min_free_space = min_free_space
247 self.max_items = max_items
248
249
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000250class DiskCache(object):
251 """Stateful LRU cache in a flat hash table in a directory.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000252
253 Saves its state as json file.
254 """
255 STATE_FILE = 'state.json'
256
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000257 def __init__(self, cache_dir, retriever, policies, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000258 """
259 Arguments:
260 - cache_dir: Directory where to place the cache.
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000261 - retriever: API where to fetch items from.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000262 - policies: cache retention policies.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000263 - algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000264 """
265 self.cache_dir = cache_dir
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000266 self.retriever = retriever
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000267 self.policies = policies
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000268 self._pool = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000269 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000270 self.lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000271
272 # Items currently being fetched. Keep it local to reduce lock contention.
273 self._pending_queue = set()
274
275 # Profiling values.
276 self._added = []
277 self._removed = []
278 self._free_disk = 0
279
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000280 with tools.Profiler('Setup'):
maruel@chromium.org770993b2012-12-11 17:16:48 +0000281 if not os.path.isdir(self.cache_dir):
282 os.makedirs(self.cache_dir)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000283
284 # Load state of the cache.
vadimsh@chromium.orga40428e2013-07-04 15:43:14 +0000285 if os.path.isfile(self.state_file):
286 try:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000287 self.lru = lru.LRUDict.load(self.state_file)
288 except ValueError as err:
289 logging.error('Failed to load cache state: %s' % (err,))
290 # Don't want to keep broken state file.
291 os.remove(self.state_file)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000292
maruel@chromium.org770993b2012-12-11 17:16:48 +0000293 # Ensure that all files listed in the state still exist and add new ones.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000294 previous = self.lru.keys_set()
295 unknown = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000296 for filename in os.listdir(self.cache_dir):
297 if filename == self.STATE_FILE:
298 continue
299 if filename in previous:
300 previous.remove(filename)
301 continue
302 # An untracked file.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000303 if not isolateserver.is_valid_hash(filename, algo):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000304 logging.warning('Removing unknown file %s from cache', filename)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000305 os.remove(self._path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000306 continue
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000307 # File that's not referenced in 'state.json'.
308 # TODO(vadimsh): Verify its SHA1 matches file name.
309 logging.warning('Adding unknown file %s to cache', filename)
310 unknown.append(filename)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000311
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000312 if unknown:
313 # Add as oldest files. They will be deleted eventually if not accessed.
314 self._add_oldest_list(unknown)
315 logging.warning('Added back %d unknown files', len(unknown))
316
maruel@chromium.org770993b2012-12-11 17:16:48 +0000317 if previous:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000318 # Filter out entries that were not found.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000319 logging.warning('Removed %d lost files', len(previous))
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000320 for filename in previous:
321 self.lru.pop(filename)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000322 self.trim()
323
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000324 def set_pool(self, pool):
325 """Sets an isolateserver.WorkerPool."""
326 self._pool = pool
327
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000328 def __enter__(self):
329 return self
330
331 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000332 with tools.Profiler('CleanupTrimming'):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000333 self.trim()
334
335 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000336 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000337 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000338 '%5d (%8dkb) current',
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000339 len(self.lru),
340 sum(self.lru.itervalues()) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000341 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000342 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
343 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org41601642013-09-18 19:40:46 +0000344 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000345
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000346 def trim(self):
347 """Trims anything we don't know, make sure enough free space exists."""
348 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000349 if self.policies.max_cache_size:
350 total_size = sum(self.lru.itervalues())
351 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000352 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000353
354 # Ensure maximum number of items in the cache.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000355 if self.policies.max_items and len(self.lru) > self.policies.max_items:
356 for _ in xrange(len(self.lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000357 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000358
359 # Ensure enough free space.
360 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000361 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000362 while (
363 self.policies.min_free_space and
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000364 self.lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000365 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000366 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000367 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000368 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000369 if trimmed_due_to_space:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000370 total = sum(self.lru.itervalues())
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000371 logging.warning(
372 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
373 'cache (%.1f%% of its maximum capacity)',
374 self._free_disk / 1024.,
375 total / 1024.,
376 100. * self.policies.max_cache_size / float(total),
377 )
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000378 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000379
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000380 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000381 """Retrieves a file from the remote, if not already cached, and adds it to
382 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000383
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000384 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 +0000385 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000386 """
387 assert not '/' in item
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000388 path = self._path(item)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000389 found = False
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000390
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000391 if item in self.lru:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000392 # Note that is doesn't compute the hash so it could still be corrupted.
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000393 if not isolateserver.is_valid_file(self._path(item), size):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000394 self.lru.pop(item)
395 self._delete_file(item, size)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000396 else:
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000397 # Was already in cache. Update it's LRU value by putting it at the end.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000398 self.lru.touch(item)
399 found = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000400
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000401 if not found:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000402 if item in self._pending_queue:
403 # Already pending. The same object could be referenced multiple times.
404 return
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000405 # TODO(maruel): It should look at the free disk space, the current cache
406 # size and the size of the new item on every new item:
407 # - Trim the cache as more entries are listed when free disk space is low,
408 # otherwise if the amount of data downloaded during the run > free disk
409 # space, it'll crash.
410 # - Make sure there's enough free disk space to fit all dependencies of
411 # this run! If not, abort early.
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000412 self._pool.add_task(priority, self._store, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000413 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000414
415 def add(self, filepath, obj):
416 """Forcibly adds a file to the cache."""
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000417 if obj not in self.lru:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000418 link_file(self._path(obj), filepath, HARDLINK)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000419 self._add(obj)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000420
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000421 def store_to(self, obj, dest):
422 link_file(dest, self._path(obj), HARDLINK)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000423
maruel@chromium.org41601642013-09-18 19:40:46 +0000424 def read(self, item):
425 """Reads an item from the cache."""
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000426 with open(self._path(item), 'rb') as f:
maruel@chromium.org41601642013-09-18 19:40:46 +0000427 return f.read()
428
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000429 def wait_for(self, items):
430 """Starts a loop that waits for at least one of |items| to be retrieved.
431
432 Returns the first item retrieved.
433 """
434 # Flush items already present.
435 for item in items:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000436 if item in self.lru:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000437 return item
438
439 assert all(i in self._pending_queue for i in items), (
440 items, self._pending_queue)
441 # Note that:
442 # len(self._pending_queue) ==
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000443 # ( len(self.remote_fetcher._workers) - self.remote_fetcher._ready +
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000444 # len(self._remote._queue) + len(self._remote.done))
445 # There is no lock-free way to verify that.
446 while self._pending_queue:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000447 item = self._pool.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000448 self._pending_queue.remove(item)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000449 self._add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000450 if item in items:
451 return item
452
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000453 def _path(self, item):
454 """Returns the path to one item."""
455 return os.path.join(self.cache_dir, item)
456
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000457 def _save(self):
458 """Saves the LRU ordering."""
459 self.lru.save(self.state_file)
460
461 def _remove_lru_file(self):
462 """Removes the last recently used file and returns its size."""
463 item, size = self.lru.pop_oldest()
464 self._delete_file(item, size)
465 return size
466
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000467 def _add(self, item):
468 """Adds an item into LRU cache marking it as a newest one."""
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000469 size = os.stat(self._path(item)).st_size
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000470 self._added.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000471 self.lru.add(item, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000472
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000473 def _add_oldest_list(self, items):
474 """Adds a bunch of items into LRU cache marking them as oldest ones."""
475 pairs = []
476 for item in items:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000477 size = os.stat(self._path(item)).st_size
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000478 self._added.append(size)
479 pairs.append((item, size))
480 self.lru.batch_insert_oldest(pairs)
481
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000482 def _store(self, item, path, expected_size):
483 """Stores the data generated by remote_fetcher."""
484 isolateserver.file_write(path, self.retriever(item, expected_size))
485 return item
486
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000487 def _delete_file(self, item, size):
488 """Deletes cache file from the file system."""
489 self._removed.append(size)
490 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000491 os.remove(self._path(item))
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000492 except OSError as e:
493 logging.error('Error attempting to delete a file\n%s' % e)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000494
495
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000496def run_tha_test(isolated_hash, cache_dir, retriever, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000497 """Downloads the dependencies in the cache, hardlinks them into a temporary
498 directory and runs the executable.
499 """
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000500 algo = hashlib.sha1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000501 outdir = None
502 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000503 cache = DiskCache(cache_dir, retriever, policies, algo)
504 # |cache_dir| may not exist until DiskCache() instance is created.
505 outdir = make_temp_dir('run_tha_test', cache_dir)
506 try:
507 settings = isolateserver.fetch_isolated(
508 isolated_hash, cache, outdir, get_flavor(), algo, True)
509 except isolateserver.ConfigError as e:
510 print >> sys.stderr, str(e)
511 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000512
513 if settings.read_only:
514 logging.info('Making files read only')
515 make_writable(outdir, True)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000516 cwd = os.path.normpath(os.path.join(outdir, settings.relative_cwd))
517 logging.info('Running %s, cwd=%s' % (settings.command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000518
519 # TODO(csharp): This should be specified somewhere else.
520 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
521 # Add a rotating log file if one doesn't already exist.
522 env = os.environ.copy()
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000523 if MAIN_DIR:
524 env.setdefault('RUN_TEST_CASES_LOG_FILE',
525 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000526 try:
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000527 with tools.Profiler('RunTest'):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000528 return subprocess.call(settings.command, cwd=cwd, env=env)
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000529 except OSError:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000530 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (settings.command, cwd)
531 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000532 finally:
533 if outdir:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000534 rmtree(outdir)
535
536
537def main():
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000538 tools.disable_buffering()
539 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000540 usage='%prog <options>',
541 version=__version__,
542 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000543
544 group = optparse.OptionGroup(parser, 'Data source')
545 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000546 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000547 metavar='FILE',
548 help='File/url describing what to map or run')
549 group.add_option(
550 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000551 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000552 group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000553 '-I', '--isolate-server',
554 metavar='URL', default='',
555 help='Isolate server to use')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000556 group.add_option(
557 '-n', '--namespace',
558 default='default-gzip',
559 help='namespace to use when using isolateserver, default: %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000560 parser.add_option_group(group)
561
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000562 group = optparse.OptionGroup(parser, 'Cache management')
563 group.add_option(
564 '--cache',
565 default='cache',
566 metavar='DIR',
567 help='Cache directory, default=%default')
568 group.add_option(
569 '--max-cache-size',
570 type='int',
571 metavar='NNN',
572 default=20*1024*1024*1024,
573 help='Trim if the cache gets larger than this value, default=%default')
574 group.add_option(
575 '--min-free-space',
576 type='int',
577 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000578 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000579 help='Trim if disk free space becomes lower than this value, '
580 'default=%default')
581 group.add_option(
582 '--max-items',
583 type='int',
584 metavar='NNN',
585 default=100000,
586 help='Trim if more than this number of items are in the cache '
587 'default=%default')
588 parser.add_option_group(group)
589
590 options, args = parser.parse_args()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000591
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000592 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000593 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000594 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000595 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000596 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000597 parser.error('Unsupported args %s' % ' '.join(args))
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000598 if not options.isolate_server:
599 parser.error('--isolate-server is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000600
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000601 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000602 policies = CachePolicies(
603 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000604
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000605 retriever = isolateserver.get_storage_api(
606 options.isolate_server, options.namespace)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000607 try:
608 return run_tha_test(
609 options.isolated or options.hash,
610 options.cache,
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000611 retriever.fetch,
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000612 policies)
613 except Exception, e:
614 # Make sure any exception is logged.
615 logging.exception(e)
616 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000617
618
619if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000620 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000621 fix_encoding.fix_encoding()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000622 sys.exit(main())