blob: 28e521e90289f48a1569f8cc37e1d648b12396d9 [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
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -050095def hardlink(source, link_name):
96 """Hardlinks a file.
97
98 Add support for os.link() on Windows.
99 """
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000100 if sys.platform == 'win32':
101 if not ctypes.windll.kernel32.CreateHardLinkW(
102 unicode(link_name), unicode(source), 0):
103 raise OSError()
104 else:
105 os.link(source, link_name)
106
107
108def readable_copy(outfile, infile):
109 """Makes a copy of the file that is readable by everyone."""
csharp@chromium.org59d116d2013-07-05 18:04:08 +0000110 shutil.copy2(infile, outfile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000111 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
112 stat.S_IRGRP | stat.S_IROTH)
113 os.chmod(outfile, read_enabled_mode)
114
115
116def link_file(outfile, infile, action):
117 """Links a file. The type of link depends on |action|."""
118 logging.debug('Mapping %s to %s' % (infile, outfile))
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000119 if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000120 raise ValueError('Unknown mapping action %s' % action)
121 if not os.path.isfile(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000122 raise isolateserver.MappingError('%s is missing' % infile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000123 if os.path.isfile(outfile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000124 raise isolateserver.MappingError(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000125 '%s already exist; insize:%d; outsize:%d' %
126 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
127
128 if action == COPY:
129 readable_copy(outfile, infile)
130 elif action == SYMLINK and sys.platform != 'win32':
131 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000132 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000133 else:
134 try:
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500135 hardlink(infile, outfile)
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000136 except OSError as e:
137 if action == HARDLINK:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000138 raise isolateserver.MappingError(
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000139 'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000140 # Probably a different file system.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000141 logging.warning(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000142 'Failed to hardlink, failing back to copy %s to %s' % (
143 infile, outfile))
144 readable_copy(outfile, infile)
145
146
Marc-Antoine Rueld2d4d4f2013-11-10 14:32:38 -0500147def set_read_only(path, read_only):
148 """Sets or resets the write bit on a file or directory.
149
150 Zaps out access to 'group' and 'others'.
151 """
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000152 mode = os.lstat(path).st_mode
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500153 # TODO(maruel): Stop removing GO bits.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000154 if read_only:
155 mode = mode & 0500
156 else:
157 mode = mode | 0200
158 if hasattr(os, 'lchmod'):
159 os.lchmod(path, mode) # pylint: disable=E1101
160 else:
161 if stat.S_ISLNK(mode):
162 # Skip symlink without lchmod() support.
Marc-Antoine Ruel45dc2902013-12-05 14:54:20 -0500163 logging.debug(
164 'Can\'t change %sw bit on symlink %s',
165 '-' if read_only else '+', path)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000166 return
167
168 # TODO(maruel): Implement proper DACL modification on Windows.
169 os.chmod(path, mode)
170
171
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500172def make_tree_read_only(root):
173 """Makes all the files in the directories read only.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000174
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500175 Also makes the directories read only, only if it makes sense on the platform.
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500176 """
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500177 logging.debug('make_tree_read_only(%s)', root)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500178 assert os.path.isabs(root), root
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500179 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
180 for filename in filenames:
181 set_read_only(os.path.join(dirpath, filename), True)
182 if sys.platform != 'win32':
183 # It must not be done on Windows.
184 for dirname in dirnames:
185 set_read_only(os.path.join(dirpath, dirname), True)
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500186 # TODO(maruel): Investigate if it makes sense.
187 #set_read_only(root, True)
188
189
190def make_tree_writeable(root):
191 """Makes all the files in the directories writeable.
192
193 Also makes the directories writeable, only if it makes sense on the platform.
194
195 It is different from make_tree_deleteable() because it unconditionally affects
196 the files.
197 """
198 logging.debug('make_tree_writeable(%s)', root)
199 assert os.path.isabs(root), root
200 if sys.platform != 'win32':
201 set_read_only(root, False)
202 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
203 for filename in filenames:
204 set_read_only(os.path.join(dirpath, filename), False)
205 if sys.platform != 'win32':
206 # It must not be done on Windows.
207 for dirname in dirnames:
208 set_read_only(os.path.join(dirpath, dirname), False)
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500209
210
211def make_tree_deleteable(root):
212 """Changes the appropriate permissions so the files in the directories can be
213 deleted.
214
215 On Windows, the files are modified. On other platforms, modify the directory.
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500216 It only does the minimum so the files can be deleted safely.
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500217
218 Warning on Windows: since file permission is modified, the file node is
219 modified. This means that for hard-linked files, every directory entry for the
220 file node has its file permission modified.
221 """
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500222 logging.debug('make_tree_deleteable(%s)', root)
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500223 assert os.path.isabs(root), root
224 if sys.platform != 'win32':
225 set_read_only(root, False)
226 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
227 if sys.platform == 'win32':
228 for filename in filenames:
229 set_read_only(os.path.join(dirpath, filename), False)
230 else:
231 for dirname in dirnames:
232 set_read_only(os.path.join(dirpath, dirname), False)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000233
234
235def rmtree(root):
236 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500237 make_tree_deleteable(root)
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500238 logging.info('rmtree(%s)', root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000239 if sys.platform == 'win32':
240 for i in range(3):
241 try:
242 shutil.rmtree(root)
243 break
244 except WindowsError: # pylint: disable=E0602
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500245 if i == 2:
246 raise
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000247 delay = (i+1)*2
248 print >> sys.stderr, (
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500249 'Failed to delete %s. Maybe the test has subprocess outliving it.'
250 ' Sleep %d seconds.' % (root, delay))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000251 time.sleep(delay)
252 else:
253 shutil.rmtree(root)
254
255
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000256def try_remove(filepath):
257 """Removes a file without crashing even if it doesn't exist."""
258 try:
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500259 # TODO(maruel): Not do it unless necessary since it slows this function
260 # down.
261 if sys.platform == 'win32':
262 # Deleting a read-only file will fail if it is read-only.
263 set_read_only(filepath, False)
264 else:
265 # Deleting a read-only file will fail if the directory is read-only.
266 set_read_only(os.path.dirname(filepath), False)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000267 os.remove(filepath)
268 except OSError:
269 pass
270
271
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000272def is_same_filesystem(path1, path2):
273 """Returns True if both paths are on the same filesystem.
274
275 This is required to enable the use of hardlinks.
276 """
277 assert os.path.isabs(path1), path1
278 assert os.path.isabs(path2), path2
279 if sys.platform == 'win32':
280 # If the drive letter mismatches, assume it's a separate partition.
281 # TODO(maruel): It should look at the underlying drive, a drive letter could
282 # be a mount point to a directory on another drive.
283 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
284 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
285 if path1[0].lower() != path2[0].lower():
286 return False
287 return os.stat(path1).st_dev == os.stat(path2).st_dev
288
289
290def get_free_space(path):
291 """Returns the number of free bytes."""
292 if sys.platform == 'win32':
293 free_bytes = ctypes.c_ulonglong(0)
294 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
295 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
296 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000297 # For OSes other than Windows.
298 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000299 return f.f_bfree * f.f_frsize
300
301
302def make_temp_dir(prefix, root_dir):
303 """Returns a temporary directory on the same file system as root_dir."""
304 base_temp_dir = None
305 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
306 base_temp_dir = os.path.dirname(root_dir)
307 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
308
309
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000310class CachePolicies(object):
311 def __init__(self, max_cache_size, min_free_space, max_items):
312 """
313 Arguments:
314 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
315 cache is effectively a leak.
316 - min_free_space: Trim if disk free space becomes lower than this value. If
317 0, it unconditionally fill the disk.
318 - max_items: Maximum number of items to keep in the cache. If 0, do not
319 enforce a limit.
320 """
321 self.max_cache_size = max_cache_size
322 self.min_free_space = min_free_space
323 self.max_items = max_items
324
325
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000326class DiskCache(isolateserver.LocalCache):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000327 """Stateful LRU cache in a flat hash table in a directory.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000328
329 Saves its state as json file.
330 """
331 STATE_FILE = 'state.json'
332
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000333 def __init__(self, cache_dir, policies, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000334 """
335 Arguments:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000336 cache_dir: directory where to place the cache.
337 policies: cache retention policies.
338 algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000339 """
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000340 super(DiskCache, self).__init__()
341 self.algo = algo
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000342 self.cache_dir = cache_dir
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000343 self.policies = policies
344 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000345
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000346 # All protected methods (starting with '_') except _path should be called
347 # with this lock locked.
348 self._lock = threading_utils.LockWithAssert()
349 self._lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000350
351 # Profiling values.
352 self._added = []
353 self._removed = []
354 self._free_disk = 0
355
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000356 with tools.Profiler('Setup'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000357 with self._lock:
358 self._load()
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000359
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000360 def __enter__(self):
361 return self
362
363 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000364 with tools.Profiler('CleanupTrimming'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000365 with self._lock:
366 self._trim()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000367
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000368 logging.info(
369 '%5d (%8dkb) added',
370 len(self._added), sum(self._added) / 1024)
371 logging.info(
372 '%5d (%8dkb) current',
373 len(self._lru),
374 sum(self._lru.itervalues()) / 1024)
375 logging.info(
376 '%5d (%8dkb) removed',
377 len(self._removed), sum(self._removed) / 1024)
378 logging.info(
379 ' %8dkb free',
380 self._free_disk / 1024)
maruel@chromium.org41601642013-09-18 19:40:46 +0000381 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000382
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000383 def cached_set(self):
384 with self._lock:
385 return self._lru.keys_set()
386
387 def touch(self, digest, size):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500388 """Verifies an actual file is valid.
389
390 Note that is doesn't compute the hash so it could still be corrupted if the
391 file size didn't change.
392
393 TODO(maruel): More stringent verification while keeping the check fast.
394 """
395 # Do the check outside the lock.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000396 if not isolateserver.is_valid_file(self._path(digest), size):
397 return False
398
399 # Update it's LRU position.
400 with self._lock:
401 if digest not in self._lru:
402 return False
403 self._lru.touch(digest)
404 return True
405
406 def evict(self, digest):
407 with self._lock:
408 self._lru.pop(digest)
409 self._delete_file(digest, isolateserver.UNKNOWN_FILE_SIZE)
410
411 def read(self, digest):
412 with open(self._path(digest), 'rb') as f:
413 return f.read()
414
415 def write(self, digest, content):
416 path = self._path(digest)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500417 # A stale broken file may remain. It is possible for the file to have write
418 # access bit removed which would cause the file_write() call to fail to open
419 # in write mode. Take no chance here.
420 try_remove(path)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000421 try:
422 size = isolateserver.file_write(path, content)
423 except:
424 # There are two possible places were an exception can occur:
425 # 1) Inside |content| generator in case of network or unzipping errors.
426 # 2) Inside file_write itself in case of disk IO errors.
427 # In any case delete an incomplete file and propagate the exception to
428 # caller, it will be logged there.
429 try_remove(path)
430 raise
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500431 # Make the file read-only in the cache. This has a few side-effects since
432 # the file node is modified, so every directory entries to this file becomes
433 # read-only. It's fine here because it is a new file.
434 set_read_only(path, True)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000435 with self._lock:
436 self._add(digest, size)
437
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500438 def hardlink(self, digest, dest, file_mode):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500439 """Hardlinks the file to |dest|.
440
441 Note that the file permission bits are on the file node, not the directory
442 entry, so changing the access bit on any of the directory entries for the
443 file node will affect them all.
444 """
445 path = self._path(digest)
446 link_file(dest, path, HARDLINK)
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500447 if file_mode is not None:
448 # Ignores all other bits.
449 os.chmod(dest, file_mode & 0500)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000450
451 def _load(self):
452 """Loads state of the cache from json file."""
453 self._lock.assert_locked()
454
455 if not os.path.isdir(self.cache_dir):
456 os.makedirs(self.cache_dir)
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500457 else:
458 # Make sure the cache is read-only.
459 # TODO(maruel): Calculate the cost and optimize the performance
460 # accordingly.
461 make_tree_read_only(self.cache_dir)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000462
463 # Load state of the cache.
464 if os.path.isfile(self.state_file):
465 try:
466 self._lru = lru.LRUDict.load(self.state_file)
467 except ValueError as err:
468 logging.error('Failed to load cache state: %s' % (err,))
469 # Don't want to keep broken state file.
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500470 try_remove(self.state_file)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000471
472 # Ensure that all files listed in the state still exist and add new ones.
473 previous = self._lru.keys_set()
474 unknown = []
475 for filename in os.listdir(self.cache_dir):
476 if filename == self.STATE_FILE:
477 continue
478 if filename in previous:
479 previous.remove(filename)
480 continue
481 # An untracked file.
482 if not isolateserver.is_valid_hash(filename, self.algo):
483 logging.warning('Removing unknown file %s from cache', filename)
484 try_remove(self._path(filename))
485 continue
486 # File that's not referenced in 'state.json'.
487 # TODO(vadimsh): Verify its SHA1 matches file name.
488 logging.warning('Adding unknown file %s to cache', filename)
489 unknown.append(filename)
490
491 if unknown:
492 # Add as oldest files. They will be deleted eventually if not accessed.
493 self._add_oldest_list(unknown)
494 logging.warning('Added back %d unknown files', len(unknown))
495
496 if previous:
497 # Filter out entries that were not found.
498 logging.warning('Removed %d lost files', len(previous))
499 for filename in previous:
500 self._lru.pop(filename)
501 self._trim()
502
503 def _save(self):
504 """Saves the LRU ordering."""
505 self._lock.assert_locked()
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500506 try:
507 set_read_only(self.state_file, False)
508 except OSError:
509 pass
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000510 self._lru.save(self.state_file)
511
512 def _trim(self):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000513 """Trims anything we don't know, make sure enough free space exists."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000514 self._lock.assert_locked()
515
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000516 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000517 if self.policies.max_cache_size:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000518 total_size = sum(self._lru.itervalues())
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000519 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000520 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000521
522 # Ensure maximum number of items in the cache.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000523 if self.policies.max_items and len(self._lru) > self.policies.max_items:
524 for _ in xrange(len(self._lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000525 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000526
527 # Ensure enough free space.
528 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000529 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000530 while (
531 self.policies.min_free_space and
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000532 self._lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000533 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000534 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000535 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000536 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000537 if trimmed_due_to_space:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000538 total = sum(self._lru.itervalues())
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000539 logging.warning(
540 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
541 'cache (%.1f%% of its maximum capacity)',
542 self._free_disk / 1024.,
543 total / 1024.,
544 100. * self.policies.max_cache_size / float(total),
545 )
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000546 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000547
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000548 def _path(self, digest):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000549 """Returns the path to one item."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000550 return os.path.join(self.cache_dir, digest)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000551
552 def _remove_lru_file(self):
553 """Removes the last recently used file and returns its size."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000554 self._lock.assert_locked()
555 digest, size = self._lru.pop_oldest()
556 self._delete_file(digest, size)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000557 return size
558
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000559 def _add(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000560 """Adds an item into LRU cache marking it as a newest one."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000561 self._lock.assert_locked()
562 if size == isolateserver.UNKNOWN_FILE_SIZE:
563 size = os.stat(self._path(digest)).st_size
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000564 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000565 self._lru.add(digest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000566
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000567 def _add_oldest_list(self, digests):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000568 """Adds a bunch of items into LRU cache marking them as oldest ones."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000569 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000570 pairs = []
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000571 for digest in digests:
572 size = os.stat(self._path(digest)).st_size
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000573 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000574 pairs.append((digest, size))
575 self._lru.batch_insert_oldest(pairs)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000576
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000577 def _delete_file(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000578 """Deletes cache file from the file system."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000579 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000580 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000581 if size == isolateserver.UNKNOWN_FILE_SIZE:
582 size = os.stat(self._path(digest)).st_size
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500583 try_remove(self._path(digest))
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000584 self._removed.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000585 except OSError as e:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000586 logging.error('Error attempting to delete a file %s:\n%s' % (digest, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000587
588
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000589def run_tha_test(isolated_hash, storage, cache, algo, outdir):
590 """Downloads the dependencies in the cache, hardlinks them into a |outdir|
591 and runs the executable.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000592 """
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500593 result = 0
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000594 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000595 try:
596 settings = isolateserver.fetch_isolated(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000597 isolated_hash=isolated_hash,
598 storage=storage,
599 cache=cache,
600 algo=algo,
601 outdir=outdir,
602 os_flavor=get_flavor(),
603 require_command=True)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000604 except isolateserver.ConfigError as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000605 tools.report_error(e)
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500606 result = 1
607 return result
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000608
609 if settings.read_only:
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500610 # Note that the files themselves are read only anyway. This only inhibits
611 # creating files or deleting files in the test directory.
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500612 make_tree_read_only(outdir)
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500613 else:
614 # This code is safe to keep but DiskCache.touch() must be changed to
615 # verify the hash of the content of the files it is looking at, so that if
616 # a test modifies an input file, the file must be deleted.
617 make_tree_writeable(outdir)
618
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000619 cwd = os.path.normpath(os.path.join(outdir, settings.relative_cwd))
620 logging.info('Running %s, cwd=%s' % (settings.command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000621
622 # TODO(csharp): This should be specified somewhere else.
623 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
624 # Add a rotating log file if one doesn't already exist.
625 env = os.environ.copy()
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000626 if MAIN_DIR:
627 env.setdefault('RUN_TEST_CASES_LOG_FILE',
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000628 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000629 try:
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000630 with tools.Profiler('RunTest'):
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500631 result = subprocess.call(settings.command, cwd=cwd, env=env)
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000632 except OSError:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000633 tools.report_error('Failed to run %s; cwd=%s' % (settings.command, cwd))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500634 result = 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000635 finally:
636 if outdir:
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500637 try:
638 rmtree(outdir)
639 except OSError:
640 # Swallow the exception so it doesn't generate an infrastructure error.
641 #
642 # It usually happens on Windows when a child process is not properly
643 # terminated, usually because of a test case starting child processes
644 # that time out. This causes files to be locked and it becomes
645 # impossible to delete them.
646 #
647 # Only report an infrastructure error if the test didn't fail. This is
648 # because a swarming bot will likely not reboot. This situation will
649 # cause accumulation of temporary hardlink trees.
650 if result:
651 logging.warning('Leaking %s' % outdir)
652 else:
653 raise
654 return result
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000655
656
657def main():
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000658 tools.disable_buffering()
659 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000660 usage='%prog <options>',
661 version=__version__,
662 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000663
664 group = optparse.OptionGroup(parser, 'Data source')
665 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000666 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000667 metavar='FILE',
668 help='File/url describing what to map or run')
669 group.add_option(
670 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000671 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000672 group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000673 '-I', '--isolate-server',
674 metavar='URL', default='',
675 help='Isolate server to use')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000676 group.add_option(
677 '-n', '--namespace',
678 default='default-gzip',
679 help='namespace to use when using isolateserver, default: %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000680 parser.add_option_group(group)
681
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000682 group = optparse.OptionGroup(parser, 'Cache management')
683 group.add_option(
684 '--cache',
685 default='cache',
686 metavar='DIR',
687 help='Cache directory, default=%default')
688 group.add_option(
689 '--max-cache-size',
690 type='int',
691 metavar='NNN',
692 default=20*1024*1024*1024,
693 help='Trim if the cache gets larger than this value, default=%default')
694 group.add_option(
695 '--min-free-space',
696 type='int',
697 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000698 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000699 help='Trim if disk free space becomes lower than this value, '
700 'default=%default')
701 group.add_option(
702 '--max-items',
703 type='int',
704 metavar='NNN',
705 default=100000,
706 help='Trim if more than this number of items are in the cache '
707 'default=%default')
708 parser.add_option_group(group)
709
710 options, args = parser.parse_args()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000711
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000712 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000713 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000714 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000715 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000716 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000717 parser.error('Unsupported args %s' % ' '.join(args))
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000718 if not options.isolate_server:
719 parser.error('--isolate-server is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000720
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000721 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000722 policies = CachePolicies(
723 options.max_cache_size, options.min_free_space, options.max_items)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000724 algo = isolateserver.get_hash_algo(options.namespace)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000725
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000726 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000727 # |options.cache| may not exist until DiskCache() instance is created.
728 cache = DiskCache(options.cache, policies, algo)
729 outdir = make_temp_dir('run_tha_test', options.cache)
Vadim Shtayura3172be52013-12-03 12:49:05 -0800730 with isolateserver.get_storage(
731 options.isolate_server, options.namespace) as storage:
732 return run_tha_test(
733 options.isolated or options.hash, storage, cache, algo, outdir)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000734 except Exception as e:
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000735 # Make sure any exception is logged.
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000736 tools.report_error(e)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000737 logging.exception(e)
738 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000739
740
741if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000742 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000743 fix_encoding.fix_encoding()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000744 sys.exit(main())