blob: 61061c482586fdd22aa16ffeb7c209bc8a2201b0 [file] [log] [blame]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001#!/usr/bin/env python
Marc-Antoine Ruel8add1242013-11-05 17:28:27 -05002# Copyright 2012 The Swarming Authors. All rights reserved.
3# Use of this source code is governed by the Apache v2.0 license that can be
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00004# 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
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000014import logging
15import optparse
16import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000017import re
18import shutil
19import stat
20import subprocess
21import sys
22import tempfile
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000023import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000024
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000025from third_party.depot_tools import fix_encoding
26
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000027from utils import lru
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +000028from utils import threading_utils
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.org87d63262013-04-04 19:34:21 +000059
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +000060# Used by get_flavor().
61FLAVOR_MAPPING = {
62 'cygwin': 'win',
63 'win32': 'win',
64 'darwin': 'mac',
65 'sunos5': 'solaris',
66 'freebsd7': 'freebsd',
67 'freebsd8': 'freebsd',
68}
69
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000070
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000071def get_as_zip_package(executable=True):
72 """Returns ZipPackage with this module and all its dependencies.
73
74 If |executable| is True will store run_isolated.py as __main__.py so that
75 zip package is directly executable be python.
76 """
77 # Building a zip package when running from another zip package is
78 # unsupported and probably unneeded.
79 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000080 assert THIS_FILE_PATH
81 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000082 package = zip_package.ZipPackage(root=BASE_DIR)
83 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000084 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000085 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
86 package.add_directory(os.path.join(BASE_DIR, 'utils'))
87 return package
88
89
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000090def get_flavor():
91 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +000092 return FLAVOR_MAPPING.get(sys.platform, 'linux')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000093
94
95def os_link(source, link_name):
96 """Add support for os.link() on Windows."""
97 if sys.platform == 'win32':
98 if not ctypes.windll.kernel32.CreateHardLinkW(
99 unicode(link_name), unicode(source), 0):
100 raise OSError()
101 else:
102 os.link(source, link_name)
103
104
105def readable_copy(outfile, infile):
106 """Makes a copy of the file that is readable by everyone."""
csharp@chromium.org59d116d2013-07-05 18:04:08 +0000107 shutil.copy2(infile, outfile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000108 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
109 stat.S_IRGRP | stat.S_IROTH)
110 os.chmod(outfile, read_enabled_mode)
111
112
113def link_file(outfile, infile, action):
114 """Links a file. The type of link depends on |action|."""
115 logging.debug('Mapping %s to %s' % (infile, outfile))
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000116 if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000117 raise ValueError('Unknown mapping action %s' % action)
118 if not os.path.isfile(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000119 raise isolateserver.MappingError('%s is missing' % infile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000120 if os.path.isfile(outfile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000121 raise isolateserver.MappingError(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000122 '%s already exist; insize:%d; outsize:%d' %
123 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
124
125 if action == COPY:
126 readable_copy(outfile, infile)
127 elif action == SYMLINK and sys.platform != 'win32':
128 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000129 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000130 else:
131 try:
132 os_link(infile, outfile)
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000133 except OSError as e:
134 if action == HARDLINK:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000135 raise isolateserver.MappingError(
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000136 'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000137 # Probably a different file system.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000138 logging.warning(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000139 'Failed to hardlink, failing back to copy %s to %s' % (
140 infile, outfile))
141 readable_copy(outfile, infile)
142
143
144def _set_write_bit(path, read_only):
145 """Sets or resets the executable bit on a file or directory."""
146 mode = os.lstat(path).st_mode
147 if read_only:
148 mode = mode & 0500
149 else:
150 mode = mode | 0200
151 if hasattr(os, 'lchmod'):
152 os.lchmod(path, mode) # pylint: disable=E1101
153 else:
154 if stat.S_ISLNK(mode):
155 # Skip symlink without lchmod() support.
156 logging.debug('Can\'t change +w bit on symlink %s' % path)
157 return
158
159 # TODO(maruel): Implement proper DACL modification on Windows.
160 os.chmod(path, mode)
161
162
163def make_writable(root, read_only):
164 """Toggle the writable bit on a directory tree."""
csharp@chromium.org837352f2013-01-17 21:17:03 +0000165 assert os.path.isabs(root), root
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000166 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
167 for filename in filenames:
168 _set_write_bit(os.path.join(dirpath, filename), read_only)
169
170 for dirname in dirnames:
171 _set_write_bit(os.path.join(dirpath, dirname), read_only)
172
173
174def rmtree(root):
175 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
176 make_writable(root, False)
177 if sys.platform == 'win32':
178 for i in range(3):
179 try:
180 shutil.rmtree(root)
181 break
182 except WindowsError: # pylint: disable=E0602
183 delay = (i+1)*2
184 print >> sys.stderr, (
185 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
186 time.sleep(delay)
187 else:
188 shutil.rmtree(root)
189
190
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000191def try_remove(filepath):
192 """Removes a file without crashing even if it doesn't exist."""
193 try:
194 os.remove(filepath)
195 except OSError:
196 pass
197
198
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000199def is_same_filesystem(path1, path2):
200 """Returns True if both paths are on the same filesystem.
201
202 This is required to enable the use of hardlinks.
203 """
204 assert os.path.isabs(path1), path1
205 assert os.path.isabs(path2), path2
206 if sys.platform == 'win32':
207 # If the drive letter mismatches, assume it's a separate partition.
208 # TODO(maruel): It should look at the underlying drive, a drive letter could
209 # be a mount point to a directory on another drive.
210 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
211 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
212 if path1[0].lower() != path2[0].lower():
213 return False
214 return os.stat(path1).st_dev == os.stat(path2).st_dev
215
216
217def get_free_space(path):
218 """Returns the number of free bytes."""
219 if sys.platform == 'win32':
220 free_bytes = ctypes.c_ulonglong(0)
221 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
222 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
223 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000224 # For OSes other than Windows.
225 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000226 return f.f_bfree * f.f_frsize
227
228
229def make_temp_dir(prefix, root_dir):
230 """Returns a temporary directory on the same file system as root_dir."""
231 base_temp_dir = None
232 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
233 base_temp_dir = os.path.dirname(root_dir)
234 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
235
236
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000237class CachePolicies(object):
238 def __init__(self, max_cache_size, min_free_space, max_items):
239 """
240 Arguments:
241 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
242 cache is effectively a leak.
243 - min_free_space: Trim if disk free space becomes lower than this value. If
244 0, it unconditionally fill the disk.
245 - max_items: Maximum number of items to keep in the cache. If 0, do not
246 enforce a limit.
247 """
248 self.max_cache_size = max_cache_size
249 self.min_free_space = min_free_space
250 self.max_items = max_items
251
252
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000253class DiskCache(isolateserver.LocalCache):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000254 """Stateful LRU cache in a flat hash table in a directory.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000255
256 Saves its state as json file.
257 """
258 STATE_FILE = 'state.json'
259
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000260 def __init__(self, cache_dir, policies, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000261 """
262 Arguments:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000263 cache_dir: directory where to place the cache.
264 policies: cache retention policies.
265 algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000266 """
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000267 super(DiskCache, self).__init__()
268 self.algo = algo
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000269 self.cache_dir = cache_dir
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000270 self.policies = policies
271 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000272
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000273 # All protected methods (starting with '_') except _path should be called
274 # with this lock locked.
275 self._lock = threading_utils.LockWithAssert()
276 self._lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000277
278 # Profiling values.
279 self._added = []
280 self._removed = []
281 self._free_disk = 0
282
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000283 with tools.Profiler('Setup'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000284 with self._lock:
285 self._load()
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000286
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000287 def __enter__(self):
288 return self
289
290 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000291 with tools.Profiler('CleanupTrimming'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000292 with self._lock:
293 self._trim()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000294
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000295 logging.info(
296 '%5d (%8dkb) added',
297 len(self._added), sum(self._added) / 1024)
298 logging.info(
299 '%5d (%8dkb) current',
300 len(self._lru),
301 sum(self._lru.itervalues()) / 1024)
302 logging.info(
303 '%5d (%8dkb) removed',
304 len(self._removed), sum(self._removed) / 1024)
305 logging.info(
306 ' %8dkb free',
307 self._free_disk / 1024)
maruel@chromium.org41601642013-09-18 19:40:46 +0000308 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000309
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000310 def cached_set(self):
311 with self._lock:
312 return self._lru.keys_set()
313
314 def touch(self, digest, size):
315 # Verify an actual file is valid. Note that is doesn't compute the hash so
316 # it could still be corrupted. Do it outside the lock.
317 if not isolateserver.is_valid_file(self._path(digest), size):
318 return False
319
320 # Update it's LRU position.
321 with self._lock:
322 if digest not in self._lru:
323 return False
324 self._lru.touch(digest)
325 return True
326
327 def evict(self, digest):
328 with self._lock:
329 self._lru.pop(digest)
330 self._delete_file(digest, isolateserver.UNKNOWN_FILE_SIZE)
331
332 def read(self, digest):
333 with open(self._path(digest), 'rb') as f:
334 return f.read()
335
336 def write(self, digest, content):
337 path = self._path(digest)
338 try:
339 size = isolateserver.file_write(path, content)
340 except:
341 # There are two possible places were an exception can occur:
342 # 1) Inside |content| generator in case of network or unzipping errors.
343 # 2) Inside file_write itself in case of disk IO errors.
344 # In any case delete an incomplete file and propagate the exception to
345 # caller, it will be logged there.
346 try_remove(path)
347 raise
348 with self._lock:
349 self._add(digest, size)
350
351 def link(self, digest, dest, file_mode=None):
352 link_file(dest, self._path(digest), HARDLINK)
353 if file_mode is not None:
354 os.chmod(dest, file_mode)
355
356 def _load(self):
357 """Loads state of the cache from json file."""
358 self._lock.assert_locked()
359
360 if not os.path.isdir(self.cache_dir):
361 os.makedirs(self.cache_dir)
362
363 # Load state of the cache.
364 if os.path.isfile(self.state_file):
365 try:
366 self._lru = lru.LRUDict.load(self.state_file)
367 except ValueError as err:
368 logging.error('Failed to load cache state: %s' % (err,))
369 # Don't want to keep broken state file.
370 os.remove(self.state_file)
371
372 # Ensure that all files listed in the state still exist and add new ones.
373 previous = self._lru.keys_set()
374 unknown = []
375 for filename in os.listdir(self.cache_dir):
376 if filename == self.STATE_FILE:
377 continue
378 if filename in previous:
379 previous.remove(filename)
380 continue
381 # An untracked file.
382 if not isolateserver.is_valid_hash(filename, self.algo):
383 logging.warning('Removing unknown file %s from cache', filename)
384 try_remove(self._path(filename))
385 continue
386 # File that's not referenced in 'state.json'.
387 # TODO(vadimsh): Verify its SHA1 matches file name.
388 logging.warning('Adding unknown file %s to cache', filename)
389 unknown.append(filename)
390
391 if unknown:
392 # Add as oldest files. They will be deleted eventually if not accessed.
393 self._add_oldest_list(unknown)
394 logging.warning('Added back %d unknown files', len(unknown))
395
396 if previous:
397 # Filter out entries that were not found.
398 logging.warning('Removed %d lost files', len(previous))
399 for filename in previous:
400 self._lru.pop(filename)
401 self._trim()
402
403 def _save(self):
404 """Saves the LRU ordering."""
405 self._lock.assert_locked()
406 self._lru.save(self.state_file)
407
408 def _trim(self):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000409 """Trims anything we don't know, make sure enough free space exists."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000410 self._lock.assert_locked()
411
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000412 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000413 if self.policies.max_cache_size:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000414 total_size = sum(self._lru.itervalues())
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000415 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000416 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000417
418 # Ensure maximum number of items in the cache.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000419 if self.policies.max_items and len(self._lru) > self.policies.max_items:
420 for _ in xrange(len(self._lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000421 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000422
423 # Ensure enough free space.
424 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000425 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000426 while (
427 self.policies.min_free_space and
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000428 self._lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000429 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000430 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000431 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000432 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000433 if trimmed_due_to_space:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000434 total = sum(self._lru.itervalues())
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000435 logging.warning(
436 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
437 'cache (%.1f%% of its maximum capacity)',
438 self._free_disk / 1024.,
439 total / 1024.,
440 100. * self.policies.max_cache_size / float(total),
441 )
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000442 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000443
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000444 def _path(self, digest):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000445 """Returns the path to one item."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000446 return os.path.join(self.cache_dir, digest)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000447
448 def _remove_lru_file(self):
449 """Removes the last recently used file and returns its size."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000450 self._lock.assert_locked()
451 digest, size = self._lru.pop_oldest()
452 self._delete_file(digest, size)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000453 return size
454
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000455 def _add(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000456 """Adds an item into LRU cache marking it as a newest one."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000457 self._lock.assert_locked()
458 if size == isolateserver.UNKNOWN_FILE_SIZE:
459 size = os.stat(self._path(digest)).st_size
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000460 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000461 self._lru.add(digest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000462
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000463 def _add_oldest_list(self, digests):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000464 """Adds a bunch of items into LRU cache marking them as oldest ones."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000465 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000466 pairs = []
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000467 for digest in digests:
468 size = os.stat(self._path(digest)).st_size
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000469 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000470 pairs.append((digest, size))
471 self._lru.batch_insert_oldest(pairs)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000472
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000473 def _delete_file(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000474 """Deletes cache file from the file system."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000475 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000476 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000477 if size == isolateserver.UNKNOWN_FILE_SIZE:
478 size = os.stat(self._path(digest)).st_size
479 os.remove(self._path(digest))
480 self._removed.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000481 except OSError as e:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000482 logging.error('Error attempting to delete a file %s:\n%s' % (digest, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000483
484
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000485def run_tha_test(isolated_hash, storage, cache, algo, outdir):
486 """Downloads the dependencies in the cache, hardlinks them into a |outdir|
487 and runs the executable.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000488 """
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000489 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000490 try:
491 settings = isolateserver.fetch_isolated(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000492 isolated_hash=isolated_hash,
493 storage=storage,
494 cache=cache,
495 algo=algo,
496 outdir=outdir,
497 os_flavor=get_flavor(),
498 require_command=True)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000499 except isolateserver.ConfigError as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000500 tools.report_error(e)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000501 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000502
503 if settings.read_only:
504 logging.info('Making files read only')
505 make_writable(outdir, True)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000506 cwd = os.path.normpath(os.path.join(outdir, settings.relative_cwd))
507 logging.info('Running %s, cwd=%s' % (settings.command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000508
509 # TODO(csharp): This should be specified somewhere else.
510 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
511 # Add a rotating log file if one doesn't already exist.
512 env = os.environ.copy()
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000513 if MAIN_DIR:
514 env.setdefault('RUN_TEST_CASES_LOG_FILE',
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000515 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000516 try:
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000517 with tools.Profiler('RunTest'):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000518 return subprocess.call(settings.command, cwd=cwd, env=env)
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000519 except OSError:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000520 tools.report_error('Failed to run %s; cwd=%s' % (settings.command, cwd))
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000521 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000522 finally:
523 if outdir:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000524 rmtree(outdir)
525
526
527def main():
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000528 tools.disable_buffering()
529 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000530 usage='%prog <options>',
531 version=__version__,
532 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000533
534 group = optparse.OptionGroup(parser, 'Data source')
535 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000536 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000537 metavar='FILE',
538 help='File/url describing what to map or run')
539 group.add_option(
540 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000541 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000542 group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000543 '-I', '--isolate-server',
544 metavar='URL', default='',
545 help='Isolate server to use')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000546 group.add_option(
547 '-n', '--namespace',
548 default='default-gzip',
549 help='namespace to use when using isolateserver, default: %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000550 parser.add_option_group(group)
551
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000552 group = optparse.OptionGroup(parser, 'Cache management')
553 group.add_option(
554 '--cache',
555 default='cache',
556 metavar='DIR',
557 help='Cache directory, default=%default')
558 group.add_option(
559 '--max-cache-size',
560 type='int',
561 metavar='NNN',
562 default=20*1024*1024*1024,
563 help='Trim if the cache gets larger than this value, default=%default')
564 group.add_option(
565 '--min-free-space',
566 type='int',
567 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000568 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000569 help='Trim if disk free space becomes lower than this value, '
570 'default=%default')
571 group.add_option(
572 '--max-items',
573 type='int',
574 metavar='NNN',
575 default=100000,
576 help='Trim if more than this number of items are in the cache '
577 'default=%default')
578 parser.add_option_group(group)
579
580 options, args = parser.parse_args()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000581
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000582 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000583 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000584 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000585 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000586 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000587 parser.error('Unsupported args %s' % ' '.join(args))
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000588 if not options.isolate_server:
589 parser.error('--isolate-server is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000590
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000591 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000592 policies = CachePolicies(
593 options.max_cache_size, options.min_free_space, options.max_items)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000594 storage = isolateserver.get_storage(options.isolate_server, options.namespace)
595 algo = isolateserver.get_hash_algo(options.namespace)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000596
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000597 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000598 # |options.cache| may not exist until DiskCache() instance is created.
599 cache = DiskCache(options.cache, policies, algo)
600 outdir = make_temp_dir('run_tha_test', options.cache)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000601 return run_tha_test(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000602 options.isolated or options.hash, storage, cache, algo, outdir)
603 except Exception as e:
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000604 # Make sure any exception is logged.
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000605 tools.report_error(e)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000606 logging.exception(e)
607 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000608
609
610if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000611 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000612 fix_encoding.fix_encoding()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000613 sys.exit(main())