blob: 71eda13af27725000dae619b0049341e7e88d85a [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
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080032import auth
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000033import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000034
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000035
vadimsh@chromium.org85071062013-08-21 23:37:45 +000036# Absolute path to this file (can be None if running from zip on Mac).
37THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000038
39# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000040BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000041
42# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000043if zip_package.get_main_script_path():
44 MAIN_DIR = os.path.dirname(
45 os.path.abspath(zip_package.get_main_script_path()))
46else:
47 # This happens when 'import run_isolated' is executed at the python
48 # interactive prompt, in that case __file__ is undefined.
49 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000050
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000051# Types of action accepted by link_file().
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000052HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY = range(1, 5)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000053
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000054# The name of the log file to use.
55RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
56
csharp@chromium.orge217f302012-11-22 16:51:53 +000057# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000058RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000059
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000060
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +000061# Used by get_flavor().
62FLAVOR_MAPPING = {
63 'cygwin': 'win',
64 'win32': 'win',
65 'darwin': 'mac',
66 'sunos5': 'solaris',
67 'freebsd7': 'freebsd',
68 'freebsd8': 'freebsd',
69}
70
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000071
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000072def get_as_zip_package(executable=True):
73 """Returns ZipPackage with this module and all its dependencies.
74
75 If |executable| is True will store run_isolated.py as __main__.py so that
76 zip package is directly executable be python.
77 """
78 # Building a zip package when running from another zip package is
79 # unsupported and probably unneeded.
80 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000081 assert THIS_FILE_PATH
82 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000083 package = zip_package.ZipPackage(root=BASE_DIR)
84 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000085 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080086 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000087 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
88 package.add_directory(os.path.join(BASE_DIR, 'utils'))
89 return package
90
91
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000092def get_flavor():
93 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +000094 return FLAVOR_MAPPING.get(sys.platform, 'linux')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000095
96
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -050097def hardlink(source, link_name):
98 """Hardlinks a file.
99
100 Add support for os.link() on Windows.
101 """
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000102 if sys.platform == 'win32':
103 if not ctypes.windll.kernel32.CreateHardLinkW(
104 unicode(link_name), unicode(source), 0):
105 raise OSError()
106 else:
107 os.link(source, link_name)
108
109
110def readable_copy(outfile, infile):
111 """Makes a copy of the file that is readable by everyone."""
csharp@chromium.org59d116d2013-07-05 18:04:08 +0000112 shutil.copy2(infile, outfile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000113 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
114 stat.S_IRGRP | stat.S_IROTH)
115 os.chmod(outfile, read_enabled_mode)
116
117
118def link_file(outfile, infile, action):
119 """Links a file. The type of link depends on |action|."""
120 logging.debug('Mapping %s to %s' % (infile, outfile))
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000121 if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000122 raise ValueError('Unknown mapping action %s' % action)
123 if not os.path.isfile(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000124 raise isolateserver.MappingError('%s is missing' % infile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000125 if os.path.isfile(outfile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000126 raise isolateserver.MappingError(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000127 '%s already exist; insize:%d; outsize:%d' %
128 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
129
130 if action == COPY:
131 readable_copy(outfile, infile)
132 elif action == SYMLINK and sys.platform != 'win32':
133 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000134 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000135 else:
136 try:
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500137 hardlink(infile, outfile)
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000138 except OSError as e:
139 if action == HARDLINK:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000140 raise isolateserver.MappingError(
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000141 'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000142 # Probably a different file system.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000143 logging.warning(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000144 'Failed to hardlink, failing back to copy %s to %s' % (
145 infile, outfile))
146 readable_copy(outfile, infile)
147
148
Marc-Antoine Rueld2d4d4f2013-11-10 14:32:38 -0500149def set_read_only(path, read_only):
150 """Sets or resets the write bit on a file or directory.
151
152 Zaps out access to 'group' and 'others'.
153 """
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500154 assert isinstance(read_only, bool), read_only
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000155 mode = os.lstat(path).st_mode
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500156 # TODO(maruel): Stop removing GO bits.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000157 if read_only:
158 mode = mode & 0500
159 else:
160 mode = mode | 0200
161 if hasattr(os, 'lchmod'):
162 os.lchmod(path, mode) # pylint: disable=E1101
163 else:
164 if stat.S_ISLNK(mode):
165 # Skip symlink without lchmod() support.
Marc-Antoine Ruel45dc2902013-12-05 14:54:20 -0500166 logging.debug(
167 'Can\'t change %sw bit on symlink %s',
168 '-' if read_only else '+', path)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000169 return
170
171 # TODO(maruel): Implement proper DACL modification on Windows.
172 os.chmod(path, mode)
173
174
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500175def make_tree_read_only(root):
176 """Makes all the files in the directories read only.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000177
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500178 Also makes the directories read only, only if it makes sense on the platform.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500179
180 This means no file can be created or deleted.
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500181 """
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500182 logging.debug('make_tree_read_only(%s)', root)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500183 assert os.path.isabs(root), root
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500184 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
185 for filename in filenames:
186 set_read_only(os.path.join(dirpath, filename), True)
187 if sys.platform != 'win32':
188 # It must not be done on Windows.
189 for dirname in dirnames:
190 set_read_only(os.path.join(dirpath, dirname), True)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500191 if sys.platform != 'win32':
192 set_read_only(root, True)
193
194
195def make_tree_files_read_only(root):
196 """Makes all the files in the directories read only but not the directories
197 themselves.
198
199 This means files can be created or deleted.
200 """
201 logging.debug('make_tree_files_read_only(%s)', root)
202 assert os.path.isabs(root), root
203 if sys.platform != 'win32':
204 set_read_only(root, False)
205 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
206 for filename in filenames:
207 set_read_only(os.path.join(dirpath, filename), True)
208 if sys.platform != 'win32':
209 # It must not be done on Windows.
210 for dirname in dirnames:
211 set_read_only(os.path.join(dirpath, dirname), False)
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500212
213
214def make_tree_writeable(root):
215 """Makes all the files in the directories writeable.
216
217 Also makes the directories writeable, only if it makes sense on the platform.
218
219 It is different from make_tree_deleteable() because it unconditionally affects
220 the files.
221 """
222 logging.debug('make_tree_writeable(%s)', root)
223 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 for filename in filenames:
228 set_read_only(os.path.join(dirpath, filename), False)
229 if sys.platform != 'win32':
230 # It must not be done on Windows.
231 for dirname in dirnames:
232 set_read_only(os.path.join(dirpath, dirname), False)
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500233
234
235def make_tree_deleteable(root):
236 """Changes the appropriate permissions so the files in the directories can be
237 deleted.
238
239 On Windows, the files are modified. On other platforms, modify the directory.
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500240 It only does the minimum so the files can be deleted safely.
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500241
242 Warning on Windows: since file permission is modified, the file node is
243 modified. This means that for hard-linked files, every directory entry for the
244 file node has its file permission modified.
245 """
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500246 logging.debug('make_tree_deleteable(%s)', root)
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500247 assert os.path.isabs(root), root
248 if sys.platform != 'win32':
249 set_read_only(root, False)
250 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
251 if sys.platform == 'win32':
252 for filename in filenames:
253 set_read_only(os.path.join(dirpath, filename), False)
254 else:
255 for dirname in dirnames:
256 set_read_only(os.path.join(dirpath, dirname), False)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000257
258
259def rmtree(root):
260 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500261 make_tree_deleteable(root)
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500262 logging.info('rmtree(%s)', root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000263 if sys.platform == 'win32':
264 for i in range(3):
265 try:
266 shutil.rmtree(root)
267 break
268 except WindowsError: # pylint: disable=E0602
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500269 if i == 2:
270 raise
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000271 delay = (i+1)*2
272 print >> sys.stderr, (
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500273 'Failed to delete %s. Maybe the test has subprocess outliving it.'
274 ' Sleep %d seconds.' % (root, delay))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000275 time.sleep(delay)
276 else:
277 shutil.rmtree(root)
278
279
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000280def try_remove(filepath):
281 """Removes a file without crashing even if it doesn't exist."""
282 try:
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500283 # TODO(maruel): Not do it unless necessary since it slows this function
284 # down.
285 if sys.platform == 'win32':
286 # Deleting a read-only file will fail if it is read-only.
287 set_read_only(filepath, False)
288 else:
289 # Deleting a read-only file will fail if the directory is read-only.
290 set_read_only(os.path.dirname(filepath), False)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000291 os.remove(filepath)
292 except OSError:
293 pass
294
295
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000296def is_same_filesystem(path1, path2):
297 """Returns True if both paths are on the same filesystem.
298
299 This is required to enable the use of hardlinks.
300 """
301 assert os.path.isabs(path1), path1
302 assert os.path.isabs(path2), path2
303 if sys.platform == 'win32':
304 # If the drive letter mismatches, assume it's a separate partition.
305 # TODO(maruel): It should look at the underlying drive, a drive letter could
306 # be a mount point to a directory on another drive.
307 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
308 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
309 if path1[0].lower() != path2[0].lower():
310 return False
311 return os.stat(path1).st_dev == os.stat(path2).st_dev
312
313
314def get_free_space(path):
315 """Returns the number of free bytes."""
316 if sys.platform == 'win32':
317 free_bytes = ctypes.c_ulonglong(0)
318 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
319 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
320 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000321 # For OSes other than Windows.
322 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000323 return f.f_bfree * f.f_frsize
324
325
326def make_temp_dir(prefix, root_dir):
327 """Returns a temporary directory on the same file system as root_dir."""
328 base_temp_dir = None
329 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
330 base_temp_dir = os.path.dirname(root_dir)
331 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
332
333
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000334class CachePolicies(object):
335 def __init__(self, max_cache_size, min_free_space, max_items):
336 """
337 Arguments:
338 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
339 cache is effectively a leak.
340 - min_free_space: Trim if disk free space becomes lower than this value. If
341 0, it unconditionally fill the disk.
342 - max_items: Maximum number of items to keep in the cache. If 0, do not
343 enforce a limit.
344 """
345 self.max_cache_size = max_cache_size
346 self.min_free_space = min_free_space
347 self.max_items = max_items
348
349
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000350class DiskCache(isolateserver.LocalCache):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000351 """Stateful LRU cache in a flat hash table in a directory.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000352
353 Saves its state as json file.
354 """
355 STATE_FILE = 'state.json'
356
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000357 def __init__(self, cache_dir, policies, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000358 """
359 Arguments:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000360 cache_dir: directory where to place the cache.
361 policies: cache retention policies.
362 algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000363 """
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000364 super(DiskCache, self).__init__()
365 self.algo = algo
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000366 self.cache_dir = cache_dir
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000367 self.policies = policies
368 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000369
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000370 # All protected methods (starting with '_') except _path should be called
371 # with this lock locked.
372 self._lock = threading_utils.LockWithAssert()
373 self._lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000374
375 # Profiling values.
376 self._added = []
377 self._removed = []
378 self._free_disk = 0
379
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000380 with tools.Profiler('Setup'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000381 with self._lock:
382 self._load()
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000383
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000384 def __enter__(self):
385 return self
386
387 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000388 with tools.Profiler('CleanupTrimming'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000389 with self._lock:
390 self._trim()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000391
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000392 logging.info(
393 '%5d (%8dkb) added',
394 len(self._added), sum(self._added) / 1024)
395 logging.info(
396 '%5d (%8dkb) current',
397 len(self._lru),
398 sum(self._lru.itervalues()) / 1024)
399 logging.info(
400 '%5d (%8dkb) removed',
401 len(self._removed), sum(self._removed) / 1024)
402 logging.info(
403 ' %8dkb free',
404 self._free_disk / 1024)
maruel@chromium.org41601642013-09-18 19:40:46 +0000405 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000406
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000407 def cached_set(self):
408 with self._lock:
409 return self._lru.keys_set()
410
411 def touch(self, digest, size):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500412 """Verifies an actual file is valid.
413
414 Note that is doesn't compute the hash so it could still be corrupted if the
415 file size didn't change.
416
417 TODO(maruel): More stringent verification while keeping the check fast.
418 """
419 # Do the check outside the lock.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000420 if not isolateserver.is_valid_file(self._path(digest), size):
421 return False
422
423 # Update it's LRU position.
424 with self._lock:
425 if digest not in self._lru:
426 return False
427 self._lru.touch(digest)
428 return True
429
430 def evict(self, digest):
431 with self._lock:
432 self._lru.pop(digest)
433 self._delete_file(digest, isolateserver.UNKNOWN_FILE_SIZE)
434
435 def read(self, digest):
436 with open(self._path(digest), 'rb') as f:
437 return f.read()
438
439 def write(self, digest, content):
440 path = self._path(digest)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500441 # A stale broken file may remain. It is possible for the file to have write
442 # access bit removed which would cause the file_write() call to fail to open
443 # in write mode. Take no chance here.
444 try_remove(path)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000445 try:
446 size = isolateserver.file_write(path, content)
447 except:
448 # There are two possible places were an exception can occur:
449 # 1) Inside |content| generator in case of network or unzipping errors.
450 # 2) Inside file_write itself in case of disk IO errors.
451 # In any case delete an incomplete file and propagate the exception to
452 # caller, it will be logged there.
453 try_remove(path)
454 raise
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500455 # Make the file read-only in the cache. This has a few side-effects since
456 # the file node is modified, so every directory entries to this file becomes
457 # read-only. It's fine here because it is a new file.
458 set_read_only(path, True)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000459 with self._lock:
460 self._add(digest, size)
461
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500462 def hardlink(self, digest, dest, file_mode):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500463 """Hardlinks the file to |dest|.
464
465 Note that the file permission bits are on the file node, not the directory
466 entry, so changing the access bit on any of the directory entries for the
467 file node will affect them all.
468 """
469 path = self._path(digest)
470 link_file(dest, path, HARDLINK)
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500471 if file_mode is not None:
472 # Ignores all other bits.
473 os.chmod(dest, file_mode & 0500)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000474
475 def _load(self):
476 """Loads state of the cache from json file."""
477 self._lock.assert_locked()
478
479 if not os.path.isdir(self.cache_dir):
480 os.makedirs(self.cache_dir)
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500481 else:
482 # Make sure the cache is read-only.
483 # TODO(maruel): Calculate the cost and optimize the performance
484 # accordingly.
485 make_tree_read_only(self.cache_dir)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000486
487 # Load state of the cache.
488 if os.path.isfile(self.state_file):
489 try:
490 self._lru = lru.LRUDict.load(self.state_file)
491 except ValueError as err:
492 logging.error('Failed to load cache state: %s' % (err,))
493 # Don't want to keep broken state file.
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500494 try_remove(self.state_file)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000495
496 # Ensure that all files listed in the state still exist and add new ones.
497 previous = self._lru.keys_set()
498 unknown = []
499 for filename in os.listdir(self.cache_dir):
500 if filename == self.STATE_FILE:
501 continue
502 if filename in previous:
503 previous.remove(filename)
504 continue
505 # An untracked file.
506 if not isolateserver.is_valid_hash(filename, self.algo):
507 logging.warning('Removing unknown file %s from cache', filename)
508 try_remove(self._path(filename))
509 continue
510 # File that's not referenced in 'state.json'.
511 # TODO(vadimsh): Verify its SHA1 matches file name.
512 logging.warning('Adding unknown file %s to cache', filename)
513 unknown.append(filename)
514
515 if unknown:
516 # Add as oldest files. They will be deleted eventually if not accessed.
517 self._add_oldest_list(unknown)
518 logging.warning('Added back %d unknown files', len(unknown))
519
520 if previous:
521 # Filter out entries that were not found.
522 logging.warning('Removed %d lost files', len(previous))
523 for filename in previous:
524 self._lru.pop(filename)
525 self._trim()
526
527 def _save(self):
528 """Saves the LRU ordering."""
529 self._lock.assert_locked()
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500530 if sys.platform != 'win32':
531 d = os.path.dirname(self.state_file)
532 if os.path.isdir(d):
533 # Necessary otherwise the file can't be created.
534 set_read_only(d, False)
535 if os.path.isfile(self.state_file):
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500536 set_read_only(self.state_file, False)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000537 self._lru.save(self.state_file)
538
539 def _trim(self):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000540 """Trims anything we don't know, make sure enough free space exists."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000541 self._lock.assert_locked()
542
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000543 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000544 if self.policies.max_cache_size:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000545 total_size = sum(self._lru.itervalues())
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000546 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000547 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000548
549 # Ensure maximum number of items in the cache.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000550 if self.policies.max_items and len(self._lru) > self.policies.max_items:
551 for _ in xrange(len(self._lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000552 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000553
554 # Ensure enough free space.
555 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000556 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000557 while (
558 self.policies.min_free_space and
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000559 self._lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000560 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000561 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000562 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000563 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000564 if trimmed_due_to_space:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000565 total = sum(self._lru.itervalues())
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000566 logging.warning(
567 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
568 'cache (%.1f%% of its maximum capacity)',
569 self._free_disk / 1024.,
570 total / 1024.,
571 100. * self.policies.max_cache_size / float(total),
572 )
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000573 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000574
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000575 def _path(self, digest):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000576 """Returns the path to one item."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000577 return os.path.join(self.cache_dir, digest)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000578
579 def _remove_lru_file(self):
580 """Removes the last recently used file and returns its size."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000581 self._lock.assert_locked()
582 digest, size = self._lru.pop_oldest()
583 self._delete_file(digest, size)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000584 return size
585
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000586 def _add(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000587 """Adds an item into LRU cache marking it as a newest one."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000588 self._lock.assert_locked()
589 if size == isolateserver.UNKNOWN_FILE_SIZE:
590 size = os.stat(self._path(digest)).st_size
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000591 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000592 self._lru.add(digest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000593
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000594 def _add_oldest_list(self, digests):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000595 """Adds a bunch of items into LRU cache marking them as oldest ones."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000596 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000597 pairs = []
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000598 for digest in digests:
599 size = os.stat(self._path(digest)).st_size
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000600 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000601 pairs.append((digest, size))
602 self._lru.batch_insert_oldest(pairs)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000603
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000604 def _delete_file(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000605 """Deletes cache file from the file system."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000606 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000607 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000608 if size == isolateserver.UNKNOWN_FILE_SIZE:
609 size = os.stat(self._path(digest)).st_size
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500610 try_remove(self._path(digest))
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000611 self._removed.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000612 except OSError as e:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000613 logging.error('Error attempting to delete a file %s:\n%s' % (digest, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000614
615
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500616def change_tree_read_only(rootdir, read_only):
617 """Changes the tree read-only bits according to the read_only specification.
618
619 The flag can be 0, 1 or 2, which will affect the possibility to modify files
620 and create or delete files.
621 """
622 if read_only == 2:
623 # Files and directories (except on Windows) are marked read only. This
624 # inhibits modifying, creating or deleting files in the test directory,
625 # except on Windows where creating and deleting files is still possible.
626 make_tree_read_only(rootdir)
627 elif read_only == 1:
628 # Files are marked read only but not the directories. This inhibits
629 # modifying files but creating or deleting files is still possible.
630 make_tree_files_read_only(rootdir)
631 elif read_only in (0, None):
632 # Anything can be modified. This is the default in the .isolated file
633 # format.
634 #
635 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
636 # is not yet changed to verify the hash of the content of the files it is
637 # looking at, so that if a test modifies an input file, the file must be
638 # deleted.
639 make_tree_writeable(rootdir)
640 else:
641 raise ValueError(
642 'change_tree_read_only(%s, %s): Unknown flag %s' %
643 (rootdir, read_only, read_only))
644
645
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500646def run_tha_test(isolated_hash, storage, cache, algo, outdir, extra_args):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000647 """Downloads the dependencies in the cache, hardlinks them into a |outdir|
648 and runs the executable.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000649 """
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500650 result = 0
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000651 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000652 try:
653 settings = isolateserver.fetch_isolated(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000654 isolated_hash=isolated_hash,
655 storage=storage,
656 cache=cache,
657 algo=algo,
658 outdir=outdir,
659 os_flavor=get_flavor(),
660 require_command=True)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000661 except isolateserver.ConfigError as e:
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000662 tools.report_error(e)
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500663 result = 1
664 return result
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000665
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500666 change_tree_read_only(outdir, settings.read_only)
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000667 cwd = os.path.normpath(os.path.join(outdir, settings.relative_cwd))
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500668 command = settings.command + extra_args
Vadim Shtayurae4a780b2014-01-17 13:18:53 -0800669
670 # subprocess.call doesn't consider 'cwd' when searching for executable.
671 # Yet isolate can specify command relative to 'cwd'. Convert it to absolute
672 # path if necessary.
673 if not os.path.isabs(command[0]):
674 command[0] = os.path.abspath(os.path.join(cwd, command[0]))
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500675 logging.info('Running %s, cwd=%s' % (command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000676
677 # TODO(csharp): This should be specified somewhere else.
678 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
679 # Add a rotating log file if one doesn't already exist.
680 env = os.environ.copy()
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000681 if MAIN_DIR:
682 env.setdefault('RUN_TEST_CASES_LOG_FILE',
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000683 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000684 try:
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000685 with tools.Profiler('RunTest'):
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500686 result = subprocess.call(command, cwd=cwd, env=env)
Vadim Shtayurae4a780b2014-01-17 13:18:53 -0800687 except OSError as e:
688 tools.report_error('Failed to run %s; cwd=%s: %s' % (command, cwd, e))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500689 result = 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000690 finally:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500691 try:
692 rmtree(outdir)
693 except OSError:
694 logging.warning('Leaking %s', outdir)
695 # Swallow the exception so it doesn't generate an infrastructure error.
696 #
697 # It usually happens on Windows when a child process is not properly
698 # terminated, usually because of a test case starting child processes
699 # that time out. This causes files to be locked and it becomes
700 # impossible to delete them.
701 #
702 # Only report an infrastructure error if the test didn't fail. This is
703 # because a swarming bot will likely not reboot. This situation will
704 # cause accumulation of temporary hardlink trees.
705 if not result:
706 raise
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500707 return result
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000708
709
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500710def main(args):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000711 tools.disable_buffering()
712 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000713 usage='%prog <options>',
714 version=__version__,
715 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000716
717 group = optparse.OptionGroup(parser, 'Data source')
718 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000719 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000720 metavar='FILE',
721 help='File/url describing what to map or run')
722 group.add_option(
723 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000724 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000725 group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000726 '-I', '--isolate-server',
727 metavar='URL', default='',
728 help='Isolate server to use')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000729 group.add_option(
730 '-n', '--namespace',
731 default='default-gzip',
732 help='namespace to use when using isolateserver, default: %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000733 parser.add_option_group(group)
734
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000735 group = optparse.OptionGroup(parser, 'Cache management')
736 group.add_option(
737 '--cache',
738 default='cache',
739 metavar='DIR',
740 help='Cache directory, default=%default')
741 group.add_option(
742 '--max-cache-size',
743 type='int',
744 metavar='NNN',
745 default=20*1024*1024*1024,
746 help='Trim if the cache gets larger than this value, default=%default')
747 group.add_option(
748 '--min-free-space',
749 type='int',
750 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000751 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000752 help='Trim if disk free space becomes lower than this value, '
753 'default=%default')
754 group.add_option(
755 '--max-items',
756 type='int',
757 metavar='NNN',
758 default=100000,
759 help='Trim if more than this number of items are in the cache '
760 'default=%default')
761 parser.add_option_group(group)
762
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800763 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500764 options, args = parser.parse_args(args)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800765 auth.process_auth_options(options)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000766
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000767 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000768 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000769 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.orge9403ab2013-09-20 18:03:49 +0000770 if not options.isolate_server:
771 parser.error('--isolate-server is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000772
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000773 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000774 policies = CachePolicies(
775 options.max_cache_size, options.min_free_space, options.max_items)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000776 algo = isolateserver.get_hash_algo(options.namespace)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000777
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000778 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000779 # |options.cache| may not exist until DiskCache() instance is created.
780 cache = DiskCache(options.cache, policies, algo)
781 outdir = make_temp_dir('run_tha_test', options.cache)
Vadim Shtayura3172be52013-12-03 12:49:05 -0800782 with isolateserver.get_storage(
783 options.isolate_server, options.namespace) as storage:
784 return run_tha_test(
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500785 options.isolated or options.hash, storage, cache, algo, outdir, args)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000786 except Exception as e:
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000787 # Make sure any exception is logged.
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +0000788 tools.report_error(e)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000789 logging.exception(e)
790 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000791
792
793if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000794 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000795 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500796 sys.exit(main(sys.argv[1:]))