blob: 6ce3a68565ba18249e17052645326f947bb1dd51 [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 """
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500152 assert isinstance(read_only, bool), read_only
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000153 mode = os.lstat(path).st_mode
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500154 # TODO(maruel): Stop removing GO bits.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000155 if read_only:
156 mode = mode & 0500
157 else:
158 mode = mode | 0200
159 if hasattr(os, 'lchmod'):
160 os.lchmod(path, mode) # pylint: disable=E1101
161 else:
162 if stat.S_ISLNK(mode):
163 # Skip symlink without lchmod() support.
Marc-Antoine Ruel45dc2902013-12-05 14:54:20 -0500164 logging.debug(
165 'Can\'t change %sw bit on symlink %s',
166 '-' if read_only else '+', path)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000167 return
168
169 # TODO(maruel): Implement proper DACL modification on Windows.
170 os.chmod(path, mode)
171
172
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500173def make_tree_read_only(root):
174 """Makes all the files in the directories read only.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000175
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500176 Also makes the directories read only, only if it makes sense on the platform.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500177
178 This means no file can be created or deleted.
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500179 """
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500180 logging.debug('make_tree_read_only(%s)', root)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500181 assert os.path.isabs(root), root
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500182 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
183 for filename in filenames:
184 set_read_only(os.path.join(dirpath, filename), True)
185 if sys.platform != 'win32':
186 # It must not be done on Windows.
187 for dirname in dirnames:
188 set_read_only(os.path.join(dirpath, dirname), True)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500189 if sys.platform != 'win32':
190 set_read_only(root, True)
191
192
193def make_tree_files_read_only(root):
194 """Makes all the files in the directories read only but not the directories
195 themselves.
196
197 This means files can be created or deleted.
198 """
199 logging.debug('make_tree_files_read_only(%s)', root)
200 assert os.path.isabs(root), root
201 if sys.platform != 'win32':
202 set_read_only(root, False)
203 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
204 for filename in filenames:
205 set_read_only(os.path.join(dirpath, filename), True)
206 if sys.platform != 'win32':
207 # It must not be done on Windows.
208 for dirname in dirnames:
209 set_read_only(os.path.join(dirpath, dirname), False)
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500210
211
212def make_tree_writeable(root):
213 """Makes all the files in the directories writeable.
214
215 Also makes the directories writeable, only if it makes sense on the platform.
216
217 It is different from make_tree_deleteable() because it unconditionally affects
218 the files.
219 """
220 logging.debug('make_tree_writeable(%s)', root)
221 assert os.path.isabs(root), root
222 if sys.platform != 'win32':
223 set_read_only(root, False)
224 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
225 for filename in filenames:
226 set_read_only(os.path.join(dirpath, filename), False)
227 if sys.platform != 'win32':
228 # It must not be done on Windows.
229 for dirname in dirnames:
230 set_read_only(os.path.join(dirpath, dirname), False)
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500231
232
233def make_tree_deleteable(root):
234 """Changes the appropriate permissions so the files in the directories can be
235 deleted.
236
237 On Windows, the files are modified. On other platforms, modify the directory.
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500238 It only does the minimum so the files can be deleted safely.
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500239
240 Warning on Windows: since file permission is modified, the file node is
241 modified. This means that for hard-linked files, every directory entry for the
242 file node has its file permission modified.
243 """
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500244 logging.debug('make_tree_deleteable(%s)', root)
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500245 assert os.path.isabs(root), root
246 if sys.platform != 'win32':
247 set_read_only(root, False)
248 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
249 if sys.platform == 'win32':
250 for filename in filenames:
251 set_read_only(os.path.join(dirpath, filename), False)
252 else:
253 for dirname in dirnames:
254 set_read_only(os.path.join(dirpath, dirname), False)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000255
256
257def rmtree(root):
258 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500259 make_tree_deleteable(root)
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500260 logging.info('rmtree(%s)', root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000261 if sys.platform == 'win32':
262 for i in range(3):
263 try:
264 shutil.rmtree(root)
265 break
266 except WindowsError: # pylint: disable=E0602
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500267 if i == 2:
268 raise
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000269 delay = (i+1)*2
270 print >> sys.stderr, (
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500271 'Failed to delete %s. Maybe the test has subprocess outliving it.'
272 ' Sleep %d seconds.' % (root, delay))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000273 time.sleep(delay)
274 else:
275 shutil.rmtree(root)
276
277
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000278def try_remove(filepath):
279 """Removes a file without crashing even if it doesn't exist."""
280 try:
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500281 # TODO(maruel): Not do it unless necessary since it slows this function
282 # down.
283 if sys.platform == 'win32':
284 # Deleting a read-only file will fail if it is read-only.
285 set_read_only(filepath, False)
286 else:
287 # Deleting a read-only file will fail if the directory is read-only.
288 set_read_only(os.path.dirname(filepath), False)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000289 os.remove(filepath)
290 except OSError:
291 pass
292
293
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000294def is_same_filesystem(path1, path2):
295 """Returns True if both paths are on the same filesystem.
296
297 This is required to enable the use of hardlinks.
298 """
299 assert os.path.isabs(path1), path1
300 assert os.path.isabs(path2), path2
301 if sys.platform == 'win32':
302 # If the drive letter mismatches, assume it's a separate partition.
303 # TODO(maruel): It should look at the underlying drive, a drive letter could
304 # be a mount point to a directory on another drive.
305 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
306 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
307 if path1[0].lower() != path2[0].lower():
308 return False
309 return os.stat(path1).st_dev == os.stat(path2).st_dev
310
311
312def get_free_space(path):
313 """Returns the number of free bytes."""
314 if sys.platform == 'win32':
315 free_bytes = ctypes.c_ulonglong(0)
316 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
317 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
318 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000319 # For OSes other than Windows.
320 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000321 return f.f_bfree * f.f_frsize
322
323
324def make_temp_dir(prefix, root_dir):
325 """Returns a temporary directory on the same file system as root_dir."""
326 base_temp_dir = None
327 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
328 base_temp_dir = os.path.dirname(root_dir)
329 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
330
331
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000332class CachePolicies(object):
333 def __init__(self, max_cache_size, min_free_space, max_items):
334 """
335 Arguments:
336 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
337 cache is effectively a leak.
338 - min_free_space: Trim if disk free space becomes lower than this value. If
339 0, it unconditionally fill the disk.
340 - max_items: Maximum number of items to keep in the cache. If 0, do not
341 enforce a limit.
342 """
343 self.max_cache_size = max_cache_size
344 self.min_free_space = min_free_space
345 self.max_items = max_items
346
347
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000348class DiskCache(isolateserver.LocalCache):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000349 """Stateful LRU cache in a flat hash table in a directory.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000350
351 Saves its state as json file.
352 """
353 STATE_FILE = 'state.json'
354
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000355 def __init__(self, cache_dir, policies, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000356 """
357 Arguments:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000358 cache_dir: directory where to place the cache.
359 policies: cache retention policies.
360 algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000361 """
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000362 super(DiskCache, self).__init__()
363 self.algo = algo
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000364 self.cache_dir = cache_dir
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000365 self.policies = policies
366 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000367
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000368 # All protected methods (starting with '_') except _path should be called
369 # with this lock locked.
370 self._lock = threading_utils.LockWithAssert()
371 self._lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000372
373 # Profiling values.
374 self._added = []
375 self._removed = []
376 self._free_disk = 0
377
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000378 with tools.Profiler('Setup'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000379 with self._lock:
380 self._load()
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000381
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000382 def __enter__(self):
383 return self
384
385 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000386 with tools.Profiler('CleanupTrimming'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000387 with self._lock:
388 self._trim()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000389
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000390 logging.info(
391 '%5d (%8dkb) added',
392 len(self._added), sum(self._added) / 1024)
393 logging.info(
394 '%5d (%8dkb) current',
395 len(self._lru),
396 sum(self._lru.itervalues()) / 1024)
397 logging.info(
398 '%5d (%8dkb) removed',
399 len(self._removed), sum(self._removed) / 1024)
400 logging.info(
401 ' %8dkb free',
402 self._free_disk / 1024)
maruel@chromium.org41601642013-09-18 19:40:46 +0000403 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000404
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000405 def cached_set(self):
406 with self._lock:
407 return self._lru.keys_set()
408
409 def touch(self, digest, size):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500410 """Verifies an actual file is valid.
411
412 Note that is doesn't compute the hash so it could still be corrupted if the
413 file size didn't change.
414
415 TODO(maruel): More stringent verification while keeping the check fast.
416 """
417 # Do the check outside the lock.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000418 if not isolateserver.is_valid_file(self._path(digest), size):
419 return False
420
421 # Update it's LRU position.
422 with self._lock:
423 if digest not in self._lru:
424 return False
425 self._lru.touch(digest)
426 return True
427
428 def evict(self, digest):
429 with self._lock:
430 self._lru.pop(digest)
431 self._delete_file(digest, isolateserver.UNKNOWN_FILE_SIZE)
432
433 def read(self, digest):
434 with open(self._path(digest), 'rb') as f:
435 return f.read()
436
437 def write(self, digest, content):
438 path = self._path(digest)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500439 # A stale broken file may remain. It is possible for the file to have write
440 # access bit removed which would cause the file_write() call to fail to open
441 # in write mode. Take no chance here.
442 try_remove(path)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000443 try:
444 size = isolateserver.file_write(path, content)
445 except:
446 # There are two possible places were an exception can occur:
447 # 1) Inside |content| generator in case of network or unzipping errors.
448 # 2) Inside file_write itself in case of disk IO errors.
449 # In any case delete an incomplete file and propagate the exception to
450 # caller, it will be logged there.
451 try_remove(path)
452 raise
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500453 # Make the file read-only in the cache. This has a few side-effects since
454 # the file node is modified, so every directory entries to this file becomes
455 # read-only. It's fine here because it is a new file.
456 set_read_only(path, True)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000457 with self._lock:
458 self._add(digest, size)
459
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500460 def hardlink(self, digest, dest, file_mode):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500461 """Hardlinks the file to |dest|.
462
463 Note that the file permission bits are on the file node, not the directory
464 entry, so changing the access bit on any of the directory entries for the
465 file node will affect them all.
466 """
467 path = self._path(digest)
468 link_file(dest, path, HARDLINK)
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500469 if file_mode is not None:
470 # Ignores all other bits.
471 os.chmod(dest, file_mode & 0500)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000472
473 def _load(self):
474 """Loads state of the cache from json file."""
475 self._lock.assert_locked()
476
477 if not os.path.isdir(self.cache_dir):
478 os.makedirs(self.cache_dir)
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500479 else:
480 # Make sure the cache is read-only.
481 # TODO(maruel): Calculate the cost and optimize the performance
482 # accordingly.
483 make_tree_read_only(self.cache_dir)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000484
485 # Load state of the cache.
486 if os.path.isfile(self.state_file):
487 try:
488 self._lru = lru.LRUDict.load(self.state_file)
489 except ValueError as err:
490 logging.error('Failed to load cache state: %s' % (err,))
491 # Don't want to keep broken state file.
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500492 try_remove(self.state_file)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000493
494 # Ensure that all files listed in the state still exist and add new ones.
495 previous = self._lru.keys_set()
496 unknown = []
497 for filename in os.listdir(self.cache_dir):
498 if filename == self.STATE_FILE:
499 continue
500 if filename in previous:
501 previous.remove(filename)
502 continue
503 # An untracked file.
504 if not isolateserver.is_valid_hash(filename, self.algo):
505 logging.warning('Removing unknown file %s from cache', filename)
506 try_remove(self._path(filename))
507 continue
508 # File that's not referenced in 'state.json'.
509 # TODO(vadimsh): Verify its SHA1 matches file name.
510 logging.warning('Adding unknown file %s to cache', filename)
511 unknown.append(filename)
512
513 if unknown:
514 # Add as oldest files. They will be deleted eventually if not accessed.
515 self._add_oldest_list(unknown)
516 logging.warning('Added back %d unknown files', len(unknown))
517
518 if previous:
519 # Filter out entries that were not found.
520 logging.warning('Removed %d lost files', len(previous))
521 for filename in previous:
522 self._lru.pop(filename)
523 self._trim()
524
525 def _save(self):
526 """Saves the LRU ordering."""
527 self._lock.assert_locked()
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500528 if sys.platform != 'win32':
529 d = os.path.dirname(self.state_file)
530 if os.path.isdir(d):
531 # Necessary otherwise the file can't be created.
532 set_read_only(d, False)
533 if os.path.isfile(self.state_file):
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500534 set_read_only(self.state_file, False)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000535 self._lru.save(self.state_file)
536
537 def _trim(self):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000538 """Trims anything we don't know, make sure enough free space exists."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000539 self._lock.assert_locked()
540
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000541 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000542 if self.policies.max_cache_size:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000543 total_size = sum(self._lru.itervalues())
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000544 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000545 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000546
547 # Ensure maximum number of items in the cache.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000548 if self.policies.max_items and len(self._lru) > self.policies.max_items:
549 for _ in xrange(len(self._lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000550 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000551
552 # Ensure enough free space.
553 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000554 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000555 while (
556 self.policies.min_free_space and
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000557 self._lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000558 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000559 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000560 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000561 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000562 if trimmed_due_to_space:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000563 total = sum(self._lru.itervalues())
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000564 logging.warning(
565 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
566 'cache (%.1f%% of its maximum capacity)',
567 self._free_disk / 1024.,
568 total / 1024.,
569 100. * self.policies.max_cache_size / float(total),
570 )
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000571 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000572
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000573 def _path(self, digest):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000574 """Returns the path to one item."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000575 return os.path.join(self.cache_dir, digest)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000576
577 def _remove_lru_file(self):
578 """Removes the last recently used file and returns its size."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000579 self._lock.assert_locked()
580 digest, size = self._lru.pop_oldest()
581 self._delete_file(digest, size)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000582 return size
583
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000584 def _add(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000585 """Adds an item into LRU cache marking it as a newest one."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000586 self._lock.assert_locked()
587 if size == isolateserver.UNKNOWN_FILE_SIZE:
588 size = os.stat(self._path(digest)).st_size
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000589 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000590 self._lru.add(digest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000591
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000592 def _add_oldest_list(self, digests):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000593 """Adds a bunch of items into LRU cache marking them as oldest ones."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000594 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000595 pairs = []
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000596 for digest in digests:
597 size = os.stat(self._path(digest)).st_size
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000598 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000599 pairs.append((digest, size))
600 self._lru.batch_insert_oldest(pairs)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000601
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000602 def _delete_file(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000603 """Deletes cache file from the file system."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000604 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000605 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000606 if size == isolateserver.UNKNOWN_FILE_SIZE:
607 size = os.stat(self._path(digest)).st_size
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500608 try_remove(self._path(digest))
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000609 self._removed.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000610 except OSError as e:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000611 logging.error('Error attempting to delete a file %s:\n%s' % (digest, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000612
613
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500614def change_tree_read_only(rootdir, read_only):
615 """Changes the tree read-only bits according to the read_only specification.
616
617 The flag can be 0, 1 or 2, which will affect the possibility to modify files
618 and create or delete files.
619 """
620 if read_only == 2:
621 # Files and directories (except on Windows) are marked read only. This
622 # inhibits modifying, creating or deleting files in the test directory,
623 # except on Windows where creating and deleting files is still possible.
624 make_tree_read_only(rootdir)
625 elif read_only == 1:
626 # Files are marked read only but not the directories. This inhibits
627 # modifying files but creating or deleting files is still possible.
628 make_tree_files_read_only(rootdir)
629 elif read_only in (0, None):
630 # Anything can be modified. This is the default in the .isolated file
631 # format.
632 #
633 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
634 # is not yet changed to verify the hash of the content of the files it is
635 # looking at, so that if a test modifies an input file, the file must be
636 # deleted.
637 make_tree_writeable(rootdir)
638 else:
639 raise ValueError(
640 'change_tree_read_only(%s, %s): Unknown flag %s' %
641 (rootdir, read_only, read_only))
642
643
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500644def run_tha_test(isolated_hash, storage, cache, algo, outdir, extra_args):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000645 """Downloads the dependencies in the cache, hardlinks them into a |outdir|
646 and runs the executable.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000647 """
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500648 result = 0
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000649 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000650 try:
651 settings = isolateserver.fetch_isolated(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000652 isolated_hash=isolated_hash,
653 storage=storage,
654 cache=cache,
655 algo=algo,
656 outdir=outdir,
657 os_flavor=get_flavor(),
658 require_command=True)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000659 except isolateserver.ConfigError as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000660 tools.report_error(e)
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500661 result = 1
662 return result
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000663
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500664 change_tree_read_only(outdir, settings.read_only)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000665 cwd = os.path.normpath(os.path.join(outdir, settings.relative_cwd))
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500666 command = settings.command + extra_args
667 logging.info('Running %s, cwd=%s' % (command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000668
669 # TODO(csharp): This should be specified somewhere else.
670 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
671 # Add a rotating log file if one doesn't already exist.
672 env = os.environ.copy()
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000673 if MAIN_DIR:
674 env.setdefault('RUN_TEST_CASES_LOG_FILE',
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000675 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000676 try:
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000677 with tools.Profiler('RunTest'):
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500678 result = subprocess.call(command, cwd=cwd, env=env)
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000679 except OSError:
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500680 tools.report_error('Failed to run %s; cwd=%s' % (command, cwd))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500681 result = 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000682 finally:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500683 try:
684 rmtree(outdir)
685 except OSError:
686 logging.warning('Leaking %s', outdir)
687 # Swallow the exception so it doesn't generate an infrastructure error.
688 #
689 # It usually happens on Windows when a child process is not properly
690 # terminated, usually because of a test case starting child processes
691 # that time out. This causes files to be locked and it becomes
692 # impossible to delete them.
693 #
694 # Only report an infrastructure error if the test didn't fail. This is
695 # because a swarming bot will likely not reboot. This situation will
696 # cause accumulation of temporary hardlink trees.
697 if not result:
698 raise
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500699 return result
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000700
701
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500702def main(args):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000703 tools.disable_buffering()
704 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000705 usage='%prog <options>',
706 version=__version__,
707 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000708
709 group = optparse.OptionGroup(parser, 'Data source')
710 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000711 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000712 metavar='FILE',
713 help='File/url describing what to map or run')
714 group.add_option(
715 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000716 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000717 group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000718 '-I', '--isolate-server',
719 metavar='URL', default='',
720 help='Isolate server to use')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000721 group.add_option(
722 '-n', '--namespace',
723 default='default-gzip',
724 help='namespace to use when using isolateserver, default: %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000725 parser.add_option_group(group)
726
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000727 group = optparse.OptionGroup(parser, 'Cache management')
728 group.add_option(
729 '--cache',
730 default='cache',
731 metavar='DIR',
732 help='Cache directory, default=%default')
733 group.add_option(
734 '--max-cache-size',
735 type='int',
736 metavar='NNN',
737 default=20*1024*1024*1024,
738 help='Trim if the cache gets larger than this value, default=%default')
739 group.add_option(
740 '--min-free-space',
741 type='int',
742 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000743 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000744 help='Trim if disk free space becomes lower than this value, '
745 'default=%default')
746 group.add_option(
747 '--max-items',
748 type='int',
749 metavar='NNN',
750 default=100000,
751 help='Trim if more than this number of items are in the cache '
752 'default=%default')
753 parser.add_option_group(group)
754
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500755 options, args = parser.parse_args(args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000756
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000757 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000758 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000759 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000760 if not options.isolate_server:
761 parser.error('--isolate-server is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000762
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000763 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000764 policies = CachePolicies(
765 options.max_cache_size, options.min_free_space, options.max_items)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000766 algo = isolateserver.get_hash_algo(options.namespace)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000767
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000768 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000769 # |options.cache| may not exist until DiskCache() instance is created.
770 cache = DiskCache(options.cache, policies, algo)
771 outdir = make_temp_dir('run_tha_test', options.cache)
Vadim Shtayura3172be52013-12-03 12:49:05 -0800772 with isolateserver.get_storage(
773 options.isolate_server, options.namespace) as storage:
774 return run_tha_test(
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500775 options.isolated or options.hash, storage, cache, algo, outdir, args)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000776 except Exception as e:
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000777 # Make sure any exception is logged.
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000778 tools.report_error(e)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000779 logging.exception(e)
780 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000781
782
783if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000784 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000785 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500786 sys.exit(main(sys.argv[1:]))