blob: 27033784781451633f8687411a6b39dd77174cd6 [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 Ruelccafe0e2013-11-08 16:15:36 -0500166def make_directories_read_only(root, read_only):
167 """Toggle the writable bit on a directory tree.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000168
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500169 Do not touch the files themselves, only the directories. Only this is
170 necessary to be able to delete the files.
171 """
172 assert os.path.isabs(root), root
173 for dirpath, dirnames, _filenames in os.walk(root, topdown=True):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000174 for dirname in dirnames:
Marc-Antoine Rueld2d4d4f2013-11-10 14:32:38 -0500175 set_read_only(os.path.join(dirpath, dirname), read_only)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000176
177
178def rmtree(root):
179 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500180 make_directories_read_only(root, False)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000181 if sys.platform == 'win32':
182 for i in range(3):
183 try:
184 shutil.rmtree(root)
185 break
186 except WindowsError: # pylint: disable=E0602
187 delay = (i+1)*2
188 print >> sys.stderr, (
189 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
190 time.sleep(delay)
191 else:
192 shutil.rmtree(root)
193
194
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000195def try_remove(filepath):
196 """Removes a file without crashing even if it doesn't exist."""
197 try:
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500198 # Deleting a read-only file will fail if the directory is read-only. This
199 # means make_directories_read_only(os.path.dirname(filepath), False) should
200 # have been called before this call.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000201 os.remove(filepath)
202 except OSError:
203 pass
204
205
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000206def is_same_filesystem(path1, path2):
207 """Returns True if both paths are on the same filesystem.
208
209 This is required to enable the use of hardlinks.
210 """
211 assert os.path.isabs(path1), path1
212 assert os.path.isabs(path2), path2
213 if sys.platform == 'win32':
214 # If the drive letter mismatches, assume it's a separate partition.
215 # TODO(maruel): It should look at the underlying drive, a drive letter could
216 # be a mount point to a directory on another drive.
217 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
218 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
219 if path1[0].lower() != path2[0].lower():
220 return False
221 return os.stat(path1).st_dev == os.stat(path2).st_dev
222
223
224def get_free_space(path):
225 """Returns the number of free bytes."""
226 if sys.platform == 'win32':
227 free_bytes = ctypes.c_ulonglong(0)
228 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
229 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
230 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000231 # For OSes other than Windows.
232 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000233 return f.f_bfree * f.f_frsize
234
235
236def make_temp_dir(prefix, root_dir):
237 """Returns a temporary directory on the same file system as root_dir."""
238 base_temp_dir = None
239 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
240 base_temp_dir = os.path.dirname(root_dir)
241 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
242
243
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000244class CachePolicies(object):
245 def __init__(self, max_cache_size, min_free_space, max_items):
246 """
247 Arguments:
248 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
249 cache is effectively a leak.
250 - min_free_space: Trim if disk free space becomes lower than this value. If
251 0, it unconditionally fill the disk.
252 - max_items: Maximum number of items to keep in the cache. If 0, do not
253 enforce a limit.
254 """
255 self.max_cache_size = max_cache_size
256 self.min_free_space = min_free_space
257 self.max_items = max_items
258
259
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000260class DiskCache(isolateserver.LocalCache):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000261 """Stateful LRU cache in a flat hash table in a directory.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000262
263 Saves its state as json file.
264 """
265 STATE_FILE = 'state.json'
266
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000267 def __init__(self, cache_dir, policies, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000268 """
269 Arguments:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000270 cache_dir: directory where to place the cache.
271 policies: cache retention policies.
272 algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000273 """
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000274 super(DiskCache, self).__init__()
275 self.algo = algo
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000276 self.cache_dir = cache_dir
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000277 self.policies = policies
278 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000279
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000280 # All protected methods (starting with '_') except _path should be called
281 # with this lock locked.
282 self._lock = threading_utils.LockWithAssert()
283 self._lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000284
285 # Profiling values.
286 self._added = []
287 self._removed = []
288 self._free_disk = 0
289
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000290 with tools.Profiler('Setup'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000291 with self._lock:
292 self._load()
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000293
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000294 def __enter__(self):
295 return self
296
297 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000298 with tools.Profiler('CleanupTrimming'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000299 with self._lock:
300 self._trim()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000301
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000302 logging.info(
303 '%5d (%8dkb) added',
304 len(self._added), sum(self._added) / 1024)
305 logging.info(
306 '%5d (%8dkb) current',
307 len(self._lru),
308 sum(self._lru.itervalues()) / 1024)
309 logging.info(
310 '%5d (%8dkb) removed',
311 len(self._removed), sum(self._removed) / 1024)
312 logging.info(
313 ' %8dkb free',
314 self._free_disk / 1024)
maruel@chromium.org41601642013-09-18 19:40:46 +0000315 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000316
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000317 def cached_set(self):
318 with self._lock:
319 return self._lru.keys_set()
320
321 def touch(self, digest, size):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500322 """Verifies an actual file is valid.
323
324 Note that is doesn't compute the hash so it could still be corrupted if the
325 file size didn't change.
326
327 TODO(maruel): More stringent verification while keeping the check fast.
328 """
329 # Do the check outside the lock.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000330 if not isolateserver.is_valid_file(self._path(digest), size):
331 return False
332
333 # Update it's LRU position.
334 with self._lock:
335 if digest not in self._lru:
336 return False
337 self._lru.touch(digest)
338 return True
339
340 def evict(self, digest):
341 with self._lock:
342 self._lru.pop(digest)
343 self._delete_file(digest, isolateserver.UNKNOWN_FILE_SIZE)
344
345 def read(self, digest):
346 with open(self._path(digest), 'rb') as f:
347 return f.read()
348
349 def write(self, digest, content):
350 path = self._path(digest)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500351 # A stale broken file may remain. It is possible for the file to have write
352 # access bit removed which would cause the file_write() call to fail to open
353 # in write mode. Take no chance here.
354 try_remove(path)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000355 try:
356 size = isolateserver.file_write(path, content)
357 except:
358 # There are two possible places were an exception can occur:
359 # 1) Inside |content| generator in case of network or unzipping errors.
360 # 2) Inside file_write itself in case of disk IO errors.
361 # In any case delete an incomplete file and propagate the exception to
362 # caller, it will be logged there.
363 try_remove(path)
364 raise
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500365 # TODO(maruel): Then make the file read-only in the cache.
366 # This has a few side-effects since the file node is modified, so every
367 # directory entries to this file becomes read-only. This will be changed in
368 # a follow up CL.
Marc-Antoine Rueld2d4d4f2013-11-10 14:32:38 -0500369 # set_read_only(path, True)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000370 with self._lock:
371 self._add(digest, size)
372
Marc-Antoine Ruel87327612013-11-06 16:28:25 -0800373 def link(self, digest, dest):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500374 """Hardlinks the file to |dest|.
375
376 Note that the file permission bits are on the file node, not the directory
377 entry, so changing the access bit on any of the directory entries for the
378 file node will affect them all.
379 """
380 path = self._path(digest)
381 link_file(dest, path, HARDLINK)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000382
383 def _load(self):
384 """Loads state of the cache from json file."""
385 self._lock.assert_locked()
386
387 if not os.path.isdir(self.cache_dir):
388 os.makedirs(self.cache_dir)
389
390 # Load state of the cache.
391 if os.path.isfile(self.state_file):
392 try:
393 self._lru = lru.LRUDict.load(self.state_file)
394 except ValueError as err:
395 logging.error('Failed to load cache state: %s' % (err,))
396 # Don't want to keep broken state file.
397 os.remove(self.state_file)
398
399 # Ensure that all files listed in the state still exist and add new ones.
400 previous = self._lru.keys_set()
401 unknown = []
402 for filename in os.listdir(self.cache_dir):
403 if filename == self.STATE_FILE:
404 continue
405 if filename in previous:
406 previous.remove(filename)
407 continue
408 # An untracked file.
409 if not isolateserver.is_valid_hash(filename, self.algo):
410 logging.warning('Removing unknown file %s from cache', filename)
411 try_remove(self._path(filename))
412 continue
413 # File that's not referenced in 'state.json'.
414 # TODO(vadimsh): Verify its SHA1 matches file name.
415 logging.warning('Adding unknown file %s to cache', filename)
416 unknown.append(filename)
417
418 if unknown:
419 # Add as oldest files. They will be deleted eventually if not accessed.
420 self._add_oldest_list(unknown)
421 logging.warning('Added back %d unknown files', len(unknown))
422
423 if previous:
424 # Filter out entries that were not found.
425 logging.warning('Removed %d lost files', len(previous))
426 for filename in previous:
427 self._lru.pop(filename)
428 self._trim()
429
430 def _save(self):
431 """Saves the LRU ordering."""
432 self._lock.assert_locked()
433 self._lru.save(self.state_file)
434
435 def _trim(self):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000436 """Trims anything we don't know, make sure enough free space exists."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000437 self._lock.assert_locked()
438
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000439 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000440 if self.policies.max_cache_size:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000441 total_size = sum(self._lru.itervalues())
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000442 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000443 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000444
445 # Ensure maximum number of items in the cache.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000446 if self.policies.max_items and len(self._lru) > self.policies.max_items:
447 for _ in xrange(len(self._lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000448 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000449
450 # Ensure enough free space.
451 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000452 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000453 while (
454 self.policies.min_free_space and
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000455 self._lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000456 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000457 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000458 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000459 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000460 if trimmed_due_to_space:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000461 total = sum(self._lru.itervalues())
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000462 logging.warning(
463 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
464 'cache (%.1f%% of its maximum capacity)',
465 self._free_disk / 1024.,
466 total / 1024.,
467 100. * self.policies.max_cache_size / float(total),
468 )
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000469 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000470
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000471 def _path(self, digest):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000472 """Returns the path to one item."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000473 return os.path.join(self.cache_dir, digest)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000474
475 def _remove_lru_file(self):
476 """Removes the last recently used file and returns its size."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000477 self._lock.assert_locked()
478 digest, size = self._lru.pop_oldest()
479 self._delete_file(digest, size)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000480 return size
481
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000482 def _add(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000483 """Adds an item into LRU cache marking it as a newest one."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000484 self._lock.assert_locked()
485 if size == isolateserver.UNKNOWN_FILE_SIZE:
486 size = os.stat(self._path(digest)).st_size
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000487 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000488 self._lru.add(digest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000489
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000490 def _add_oldest_list(self, digests):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000491 """Adds a bunch of items into LRU cache marking them as oldest ones."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000492 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000493 pairs = []
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000494 for digest in digests:
495 size = os.stat(self._path(digest)).st_size
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000496 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000497 pairs.append((digest, size))
498 self._lru.batch_insert_oldest(pairs)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000499
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000500 def _delete_file(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000501 """Deletes cache file from the file system."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000502 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000503 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000504 if size == isolateserver.UNKNOWN_FILE_SIZE:
505 size = os.stat(self._path(digest)).st_size
506 os.remove(self._path(digest))
507 self._removed.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000508 except OSError as e:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000509 logging.error('Error attempting to delete a file %s:\n%s' % (digest, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000510
511
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000512def run_tha_test(isolated_hash, storage, cache, algo, outdir):
513 """Downloads the dependencies in the cache, hardlinks them into a |outdir|
514 and runs the executable.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000515 """
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000516 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000517 try:
518 settings = isolateserver.fetch_isolated(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000519 isolated_hash=isolated_hash,
520 storage=storage,
521 cache=cache,
522 algo=algo,
523 outdir=outdir,
524 os_flavor=get_flavor(),
525 require_command=True)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000526 except isolateserver.ConfigError as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000527 tools.report_error(e)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000528 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000529
530 if settings.read_only:
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500531 # Note that the files themselves are read only anyway. This only inhibits
532 # creating files or deleting files in the test directory.
533 logging.info('Making directories read only')
534 make_directories_read_only(outdir, True)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000535 cwd = os.path.normpath(os.path.join(outdir, settings.relative_cwd))
536 logging.info('Running %s, cwd=%s' % (settings.command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000537
538 # TODO(csharp): This should be specified somewhere else.
539 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
540 # Add a rotating log file if one doesn't already exist.
541 env = os.environ.copy()
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000542 if MAIN_DIR:
543 env.setdefault('RUN_TEST_CASES_LOG_FILE',
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000544 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000545 try:
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000546 with tools.Profiler('RunTest'):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000547 return subprocess.call(settings.command, cwd=cwd, env=env)
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000548 except OSError:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000549 tools.report_error('Failed to run %s; cwd=%s' % (settings.command, cwd))
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000550 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000551 finally:
552 if outdir:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000553 rmtree(outdir)
554
555
556def main():
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000557 tools.disable_buffering()
558 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000559 usage='%prog <options>',
560 version=__version__,
561 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000562
563 group = optparse.OptionGroup(parser, 'Data source')
564 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000565 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000566 metavar='FILE',
567 help='File/url describing what to map or run')
568 group.add_option(
569 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000570 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000571 group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000572 '-I', '--isolate-server',
573 metavar='URL', default='',
574 help='Isolate server to use')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000575 group.add_option(
576 '-n', '--namespace',
577 default='default-gzip',
578 help='namespace to use when using isolateserver, default: %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000579 parser.add_option_group(group)
580
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000581 group = optparse.OptionGroup(parser, 'Cache management')
582 group.add_option(
583 '--cache',
584 default='cache',
585 metavar='DIR',
586 help='Cache directory, default=%default')
587 group.add_option(
588 '--max-cache-size',
589 type='int',
590 metavar='NNN',
591 default=20*1024*1024*1024,
592 help='Trim if the cache gets larger than this value, default=%default')
593 group.add_option(
594 '--min-free-space',
595 type='int',
596 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000597 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000598 help='Trim if disk free space becomes lower than this value, '
599 'default=%default')
600 group.add_option(
601 '--max-items',
602 type='int',
603 metavar='NNN',
604 default=100000,
605 help='Trim if more than this number of items are in the cache '
606 'default=%default')
607 parser.add_option_group(group)
608
609 options, args = parser.parse_args()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000610
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000611 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000612 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000613 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000614 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000615 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000616 parser.error('Unsupported args %s' % ' '.join(args))
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000617 if not options.isolate_server:
618 parser.error('--isolate-server is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000619
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000620 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000621 policies = CachePolicies(
622 options.max_cache_size, options.min_free_space, options.max_items)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000623 storage = isolateserver.get_storage(options.isolate_server, options.namespace)
624 algo = isolateserver.get_hash_algo(options.namespace)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000625
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000626 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000627 # |options.cache| may not exist until DiskCache() instance is created.
628 cache = DiskCache(options.cache, policies, algo)
629 outdir = make_temp_dir('run_tha_test', options.cache)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000630 return run_tha_test(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000631 options.isolated or options.hash, storage, cache, algo, outdir)
632 except Exception as e:
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000633 # Make sure any exception is logged.
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000634 tools.report_error(e)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000635 logging.exception(e)
636 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000637
638
639if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000640 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000641 fix_encoding.fix_encoding()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000642 sys.exit(main())