blob: c515098819e755125393895fb8d30dd30483bc94 [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.
Marc-Antoine Ruele98b1122013-11-05 20:27:57 -05003# Use of this source code is governed under the Apache License, Version 2.0 that
4# can be found in the LICENSE file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00005
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
Marc-Antoine Rueld2d4d4f2013-11-10 14:32:38 -0500144def set_read_only(path, read_only):
145 """Sets or resets the write bit on a file or directory.
146
147 Zaps out access to 'group' and 'others'.
148 """
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000149 mode = os.lstat(path).st_mode
150 if read_only:
151 mode = mode & 0500
152 else:
153 mode = mode | 0200
154 if hasattr(os, 'lchmod'):
155 os.lchmod(path, mode) # pylint: disable=E1101
156 else:
157 if stat.S_ISLNK(mode):
158 # Skip symlink without lchmod() support.
159 logging.debug('Can\'t change +w bit on symlink %s' % path)
160 return
161
162 # TODO(maruel): Implement proper DACL modification on Windows.
163 os.chmod(path, mode)
164
165
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500166def make_tree_read_only(root):
167 """Makes all the files in the directories read only.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000168
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500169 Also makes the directories read only, only if it makes sense on the platform.
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500170 """
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500171 logging.info('make_tree_read_only(%s)', root)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500172 assert os.path.isabs(root), root
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500173 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
174 for filename in filenames:
175 set_read_only(os.path.join(dirpath, filename), True)
176 if sys.platform != 'win32':
177 # It must not be done on Windows.
178 for dirname in dirnames:
179 set_read_only(os.path.join(dirpath, dirname), True)
180
181
182def make_tree_deleteable(root):
183 """Changes the appropriate permissions so the files in the directories can be
184 deleted.
185
186 On Windows, the files are modified. On other platforms, modify the directory.
187
188 Warning on Windows: since file permission is modified, the file node is
189 modified. This means that for hard-linked files, every directory entry for the
190 file node has its file permission modified.
191 """
192 logging.info('make_tree_deleteable(%s)', root)
193 assert os.path.isabs(root), root
194 if sys.platform != 'win32':
195 set_read_only(root, False)
196 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
197 if sys.platform == 'win32':
198 for filename in filenames:
199 set_read_only(os.path.join(dirpath, filename), False)
200 else:
201 for dirname in dirnames:
202 set_read_only(os.path.join(dirpath, dirname), False)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000203
204
205def rmtree(root):
206 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500207 make_tree_deleteable(root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000208 if sys.platform == 'win32':
209 for i in range(3):
210 try:
211 shutil.rmtree(root)
212 break
213 except WindowsError: # pylint: disable=E0602
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500214 if i == 2:
215 raise
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000216 delay = (i+1)*2
217 print >> sys.stderr, (
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500218 'Failed to delete %s. Maybe the test has subprocess outliving it.'
219 ' Sleep %d seconds.' % (root, delay))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000220 time.sleep(delay)
221 else:
222 shutil.rmtree(root)
223
224
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000225def try_remove(filepath):
226 """Removes a file without crashing even if it doesn't exist."""
227 try:
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500228 # TODO(maruel): Not do it unless necessary since it slows this function
229 # down.
230 if sys.platform == 'win32':
231 # Deleting a read-only file will fail if it is read-only.
232 set_read_only(filepath, False)
233 else:
234 # Deleting a read-only file will fail if the directory is read-only.
235 set_read_only(os.path.dirname(filepath), False)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000236 os.remove(filepath)
237 except OSError:
238 pass
239
240
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000241def is_same_filesystem(path1, path2):
242 """Returns True if both paths are on the same filesystem.
243
244 This is required to enable the use of hardlinks.
245 """
246 assert os.path.isabs(path1), path1
247 assert os.path.isabs(path2), path2
248 if sys.platform == 'win32':
249 # If the drive letter mismatches, assume it's a separate partition.
250 # TODO(maruel): It should look at the underlying drive, a drive letter could
251 # be a mount point to a directory on another drive.
252 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
253 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
254 if path1[0].lower() != path2[0].lower():
255 return False
256 return os.stat(path1).st_dev == os.stat(path2).st_dev
257
258
259def get_free_space(path):
260 """Returns the number of free bytes."""
261 if sys.platform == 'win32':
262 free_bytes = ctypes.c_ulonglong(0)
263 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
264 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
265 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000266 # For OSes other than Windows.
267 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000268 return f.f_bfree * f.f_frsize
269
270
271def make_temp_dir(prefix, root_dir):
272 """Returns a temporary directory on the same file system as root_dir."""
273 base_temp_dir = None
274 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
275 base_temp_dir = os.path.dirname(root_dir)
276 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
277
278
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000279class CachePolicies(object):
280 def __init__(self, max_cache_size, min_free_space, max_items):
281 """
282 Arguments:
283 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
284 cache is effectively a leak.
285 - min_free_space: Trim if disk free space becomes lower than this value. If
286 0, it unconditionally fill the disk.
287 - max_items: Maximum number of items to keep in the cache. If 0, do not
288 enforce a limit.
289 """
290 self.max_cache_size = max_cache_size
291 self.min_free_space = min_free_space
292 self.max_items = max_items
293
294
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000295class DiskCache(isolateserver.LocalCache):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000296 """Stateful LRU cache in a flat hash table in a directory.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000297
298 Saves its state as json file.
299 """
300 STATE_FILE = 'state.json'
301
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000302 def __init__(self, cache_dir, policies, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000303 """
304 Arguments:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000305 cache_dir: directory where to place the cache.
306 policies: cache retention policies.
307 algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000308 """
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000309 super(DiskCache, self).__init__()
310 self.algo = algo
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000311 self.cache_dir = cache_dir
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000312 self.policies = policies
313 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000314
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000315 # All protected methods (starting with '_') except _path should be called
316 # with this lock locked.
317 self._lock = threading_utils.LockWithAssert()
318 self._lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000319
320 # Profiling values.
321 self._added = []
322 self._removed = []
323 self._free_disk = 0
324
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000325 with tools.Profiler('Setup'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000326 with self._lock:
327 self._load()
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000328
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000329 def __enter__(self):
330 return self
331
332 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000333 with tools.Profiler('CleanupTrimming'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000334 with self._lock:
335 self._trim()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000336
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000337 logging.info(
338 '%5d (%8dkb) added',
339 len(self._added), sum(self._added) / 1024)
340 logging.info(
341 '%5d (%8dkb) current',
342 len(self._lru),
343 sum(self._lru.itervalues()) / 1024)
344 logging.info(
345 '%5d (%8dkb) removed',
346 len(self._removed), sum(self._removed) / 1024)
347 logging.info(
348 ' %8dkb free',
349 self._free_disk / 1024)
maruel@chromium.org41601642013-09-18 19:40:46 +0000350 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000351
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000352 def cached_set(self):
353 with self._lock:
354 return self._lru.keys_set()
355
356 def touch(self, digest, size):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500357 """Verifies an actual file is valid.
358
359 Note that is doesn't compute the hash so it could still be corrupted if the
360 file size didn't change.
361
362 TODO(maruel): More stringent verification while keeping the check fast.
363 """
364 # Do the check outside the lock.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000365 if not isolateserver.is_valid_file(self._path(digest), size):
366 return False
367
368 # Update it's LRU position.
369 with self._lock:
370 if digest not in self._lru:
371 return False
372 self._lru.touch(digest)
373 return True
374
375 def evict(self, digest):
376 with self._lock:
377 self._lru.pop(digest)
378 self._delete_file(digest, isolateserver.UNKNOWN_FILE_SIZE)
379
380 def read(self, digest):
381 with open(self._path(digest), 'rb') as f:
382 return f.read()
383
384 def write(self, digest, content):
385 path = self._path(digest)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500386 # A stale broken file may remain. It is possible for the file to have write
387 # access bit removed which would cause the file_write() call to fail to open
388 # in write mode. Take no chance here.
389 try_remove(path)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000390 try:
391 size = isolateserver.file_write(path, content)
392 except:
393 # There are two possible places were an exception can occur:
394 # 1) Inside |content| generator in case of network or unzipping errors.
395 # 2) Inside file_write itself in case of disk IO errors.
396 # In any case delete an incomplete file and propagate the exception to
397 # caller, it will be logged there.
398 try_remove(path)
399 raise
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500400 # Make the file read-only in the cache. This has a few side-effects since
401 # the file node is modified, so every directory entries to this file becomes
402 # read-only. It's fine here because it is a new file.
403 set_read_only(path, True)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000404 with self._lock:
405 self._add(digest, size)
406
Marc-Antoine Ruel87327612013-11-06 16:28:25 -0800407 def link(self, digest, dest):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500408 """Hardlinks the file to |dest|.
409
410 Note that the file permission bits are on the file node, not the directory
411 entry, so changing the access bit on any of the directory entries for the
412 file node will affect them all.
413 """
414 path = self._path(digest)
415 link_file(dest, path, HARDLINK)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000416
417 def _load(self):
418 """Loads state of the cache from json file."""
419 self._lock.assert_locked()
420
421 if not os.path.isdir(self.cache_dir):
422 os.makedirs(self.cache_dir)
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500423 else:
424 # Make sure the cache is read-only.
425 # TODO(maruel): Calculate the cost and optimize the performance
426 # accordingly.
427 make_tree_read_only(self.cache_dir)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000428
429 # Load state of the cache.
430 if os.path.isfile(self.state_file):
431 try:
432 self._lru = lru.LRUDict.load(self.state_file)
433 except ValueError as err:
434 logging.error('Failed to load cache state: %s' % (err,))
435 # Don't want to keep broken state file.
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500436 try_remove(self.state_file)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000437
438 # Ensure that all files listed in the state still exist and add new ones.
439 previous = self._lru.keys_set()
440 unknown = []
441 for filename in os.listdir(self.cache_dir):
442 if filename == self.STATE_FILE:
443 continue
444 if filename in previous:
445 previous.remove(filename)
446 continue
447 # An untracked file.
448 if not isolateserver.is_valid_hash(filename, self.algo):
449 logging.warning('Removing unknown file %s from cache', filename)
450 try_remove(self._path(filename))
451 continue
452 # File that's not referenced in 'state.json'.
453 # TODO(vadimsh): Verify its SHA1 matches file name.
454 logging.warning('Adding unknown file %s to cache', filename)
455 unknown.append(filename)
456
457 if unknown:
458 # Add as oldest files. They will be deleted eventually if not accessed.
459 self._add_oldest_list(unknown)
460 logging.warning('Added back %d unknown files', len(unknown))
461
462 if previous:
463 # Filter out entries that were not found.
464 logging.warning('Removed %d lost files', len(previous))
465 for filename in previous:
466 self._lru.pop(filename)
467 self._trim()
468
469 def _save(self):
470 """Saves the LRU ordering."""
471 self._lock.assert_locked()
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500472 try:
473 set_read_only(self.state_file, False)
474 except OSError:
475 pass
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000476 self._lru.save(self.state_file)
477
478 def _trim(self):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000479 """Trims anything we don't know, make sure enough free space exists."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000480 self._lock.assert_locked()
481
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000482 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000483 if self.policies.max_cache_size:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000484 total_size = sum(self._lru.itervalues())
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000485 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000486 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000487
488 # Ensure maximum number of items in the cache.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000489 if self.policies.max_items and len(self._lru) > self.policies.max_items:
490 for _ in xrange(len(self._lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000491 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000492
493 # Ensure enough free space.
494 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000495 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000496 while (
497 self.policies.min_free_space and
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000498 self._lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000499 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000500 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000501 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000502 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000503 if trimmed_due_to_space:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000504 total = sum(self._lru.itervalues())
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000505 logging.warning(
506 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
507 'cache (%.1f%% of its maximum capacity)',
508 self._free_disk / 1024.,
509 total / 1024.,
510 100. * self.policies.max_cache_size / float(total),
511 )
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000512 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000513
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000514 def _path(self, digest):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000515 """Returns the path to one item."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000516 return os.path.join(self.cache_dir, digest)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000517
518 def _remove_lru_file(self):
519 """Removes the last recently used file and returns its size."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000520 self._lock.assert_locked()
521 digest, size = self._lru.pop_oldest()
522 self._delete_file(digest, size)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000523 return size
524
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000525 def _add(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000526 """Adds an item into LRU cache marking it as a newest one."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000527 self._lock.assert_locked()
528 if size == isolateserver.UNKNOWN_FILE_SIZE:
529 size = os.stat(self._path(digest)).st_size
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000530 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000531 self._lru.add(digest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000532
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000533 def _add_oldest_list(self, digests):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000534 """Adds a bunch of items into LRU cache marking them as oldest ones."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000535 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000536 pairs = []
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000537 for digest in digests:
538 size = os.stat(self._path(digest)).st_size
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000539 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000540 pairs.append((digest, size))
541 self._lru.batch_insert_oldest(pairs)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000542
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000543 def _delete_file(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000544 """Deletes cache file from the file system."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000545 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000546 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000547 if size == isolateserver.UNKNOWN_FILE_SIZE:
548 size = os.stat(self._path(digest)).st_size
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500549 try_remove(self._path(digest))
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000550 self._removed.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000551 except OSError as e:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000552 logging.error('Error attempting to delete a file %s:\n%s' % (digest, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000553
554
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000555def run_tha_test(isolated_hash, storage, cache, algo, outdir):
556 """Downloads the dependencies in the cache, hardlinks them into a |outdir|
557 and runs the executable.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000558 """
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000559 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000560 try:
561 settings = isolateserver.fetch_isolated(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000562 isolated_hash=isolated_hash,
563 storage=storage,
564 cache=cache,
565 algo=algo,
566 outdir=outdir,
567 os_flavor=get_flavor(),
568 require_command=True)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000569 except isolateserver.ConfigError as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000570 tools.report_error(e)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000571 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000572
573 if settings.read_only:
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500574 # Note that the files themselves are read only anyway. This only inhibits
575 # creating files or deleting files in the test directory.
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500576 make_tree_read_only(outdir)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000577 cwd = os.path.normpath(os.path.join(outdir, settings.relative_cwd))
578 logging.info('Running %s, cwd=%s' % (settings.command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000579
580 # TODO(csharp): This should be specified somewhere else.
581 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
582 # Add a rotating log file if one doesn't already exist.
583 env = os.environ.copy()
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000584 if MAIN_DIR:
585 env.setdefault('RUN_TEST_CASES_LOG_FILE',
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000586 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000587 try:
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000588 with tools.Profiler('RunTest'):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000589 return subprocess.call(settings.command, cwd=cwd, env=env)
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000590 except OSError:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000591 tools.report_error('Failed to run %s; cwd=%s' % (settings.command, cwd))
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000592 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000593 finally:
594 if outdir:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000595 rmtree(outdir)
596
597
598def main():
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000599 tools.disable_buffering()
600 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000601 usage='%prog <options>',
602 version=__version__,
603 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000604
605 group = optparse.OptionGroup(parser, 'Data source')
606 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000607 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000608 metavar='FILE',
609 help='File/url describing what to map or run')
610 group.add_option(
611 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000612 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000613 group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000614 '-I', '--isolate-server',
615 metavar='URL', default='',
616 help='Isolate server to use')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000617 group.add_option(
618 '-n', '--namespace',
619 default='default-gzip',
620 help='namespace to use when using isolateserver, default: %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000621 parser.add_option_group(group)
622
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000623 group = optparse.OptionGroup(parser, 'Cache management')
624 group.add_option(
625 '--cache',
626 default='cache',
627 metavar='DIR',
628 help='Cache directory, default=%default')
629 group.add_option(
630 '--max-cache-size',
631 type='int',
632 metavar='NNN',
633 default=20*1024*1024*1024,
634 help='Trim if the cache gets larger than this value, default=%default')
635 group.add_option(
636 '--min-free-space',
637 type='int',
638 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000639 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000640 help='Trim if disk free space becomes lower than this value, '
641 'default=%default')
642 group.add_option(
643 '--max-items',
644 type='int',
645 metavar='NNN',
646 default=100000,
647 help='Trim if more than this number of items are in the cache '
648 'default=%default')
649 parser.add_option_group(group)
650
651 options, args = parser.parse_args()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000652
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000653 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000654 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000655 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000656 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000657 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000658 parser.error('Unsupported args %s' % ' '.join(args))
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000659 if not options.isolate_server:
660 parser.error('--isolate-server is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000661
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000662 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000663 policies = CachePolicies(
664 options.max_cache_size, options.min_free_space, options.max_items)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000665 storage = isolateserver.get_storage(options.isolate_server, options.namespace)
666 algo = isolateserver.get_hash_algo(options.namespace)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000667
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000668 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000669 # |options.cache| may not exist until DiskCache() instance is created.
670 cache = DiskCache(options.cache, policies, algo)
671 outdir = make_temp_dir('run_tha_test', options.cache)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000672 return run_tha_test(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000673 options.isolated or options.hash, storage, cache, algo, outdir)
674 except Exception as e:
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000675 # Make sure any exception is logged.
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000676 tools.report_error(e)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000677 logging.exception(e)
678 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000679
680
681if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000682 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000683 fix_encoding.fix_encoding()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000684 sys.exit(main())