blob: 4097f54cea74901be232aec6060310927d698a77 [file] [log] [blame]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
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
14import hashlib
15import json
16import logging
17import optparse
18import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000019import re
20import shutil
21import stat
22import subprocess
23import sys
24import tempfile
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000025import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000026
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000027from third_party.depot_tools import fix_encoding
28
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000029from utils import lru
vadimsh@chromium.orgb074b162013-08-22 17:55:46 +000030from utils import threading_utils
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000031from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000032from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000033
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000034import isolateserver
maruel@chromium.org9958e4a2013-09-17 00:01:48 +000035from isolateserver import ConfigError
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000036
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000037
vadimsh@chromium.org85071062013-08-21 23:37:45 +000038# Absolute path to this file (can be None if running from zip on Mac).
39THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000040
41# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000042BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000043
44# Directory that contains currently running script file.
45MAIN_DIR = os.path.dirname(os.path.abspath(zip_package.get_main_script_path()))
46
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000047# Types of action accepted by link_file().
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000048HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY = range(1, 5)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000049
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000050# The name of the log file to use.
51RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
52
csharp@chromium.orge217f302012-11-22 16:51:53 +000053# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000054RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000055
csharp@chromium.org9c59ff12012-12-12 02:32:29 +000056# The delay (in seconds) to wait between logging statements when retrieving
57# the required files. This is intended to let the user (or buildbot) know that
58# the program is still running.
59DELAY_BETWEEN_UPDATES_IN_SECS = 30
60
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +000061# Maximum expected delay (in seconds) between successive file fetches
62# in run_tha_test. If it takes longer than that, a deadlock might be happening
63# and all stack frames for all threads are dumped to log.
64DEADLOCK_TIMEOUT = 5 * 60
65
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000066
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +000067# Used by get_flavor().
68FLAVOR_MAPPING = {
69 'cygwin': 'win',
70 'win32': 'win',
71 'darwin': 'mac',
72 'sunos5': 'solaris',
73 'freebsd7': 'freebsd',
74 'freebsd8': 'freebsd',
75}
76
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000077
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000078def get_as_zip_package(executable=True):
79 """Returns ZipPackage with this module and all its dependencies.
80
81 If |executable| is True will store run_isolated.py as __main__.py so that
82 zip package is directly executable be python.
83 """
84 # Building a zip package when running from another zip package is
85 # unsupported and probably unneeded.
86 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000087 assert THIS_FILE_PATH
88 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000089 package = zip_package.ZipPackage(root=BASE_DIR)
90 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000091 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000092 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
93 package.add_directory(os.path.join(BASE_DIR, 'utils'))
94 return package
95
96
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000097def get_flavor():
98 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +000099 return FLAVOR_MAPPING.get(sys.platform, 'linux')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000100
101
102def os_link(source, link_name):
103 """Add support for os.link() on Windows."""
104 if sys.platform == 'win32':
105 if not ctypes.windll.kernel32.CreateHardLinkW(
106 unicode(link_name), unicode(source), 0):
107 raise OSError()
108 else:
109 os.link(source, link_name)
110
111
112def readable_copy(outfile, infile):
113 """Makes a copy of the file that is readable by everyone."""
csharp@chromium.org59d116d2013-07-05 18:04:08 +0000114 shutil.copy2(infile, outfile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000115 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
116 stat.S_IRGRP | stat.S_IROTH)
117 os.chmod(outfile, read_enabled_mode)
118
119
120def link_file(outfile, infile, action):
121 """Links a file. The type of link depends on |action|."""
122 logging.debug('Mapping %s to %s' % (infile, outfile))
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000123 if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000124 raise ValueError('Unknown mapping action %s' % action)
125 if not os.path.isfile(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000126 raise isolateserver.MappingError('%s is missing' % infile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000127 if os.path.isfile(outfile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000128 raise isolateserver.MappingError(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000129 '%s already exist; insize:%d; outsize:%d' %
130 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
131
132 if action == COPY:
133 readable_copy(outfile, infile)
134 elif action == SYMLINK and sys.platform != 'win32':
135 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000136 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000137 else:
138 try:
139 os_link(infile, outfile)
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000140 except OSError as e:
141 if action == HARDLINK:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000142 raise isolateserver.MappingError(
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000143 'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000144 # Probably a different file system.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000145 logging.warning(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000146 'Failed to hardlink, failing back to copy %s to %s' % (
147 infile, outfile))
148 readable_copy(outfile, infile)
149
150
151def _set_write_bit(path, read_only):
152 """Sets or resets the executable bit on a file or directory."""
153 mode = os.lstat(path).st_mode
154 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.
163 logging.debug('Can\'t change +w bit on symlink %s' % path)
164 return
165
166 # TODO(maruel): Implement proper DACL modification on Windows.
167 os.chmod(path, mode)
168
169
170def make_writable(root, read_only):
171 """Toggle the writable bit on a directory tree."""
csharp@chromium.org837352f2013-01-17 21:17:03 +0000172 assert os.path.isabs(root), root
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000173 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
174 for filename in filenames:
175 _set_write_bit(os.path.join(dirpath, filename), read_only)
176
177 for dirname in dirnames:
178 _set_write_bit(os.path.join(dirpath, dirname), read_only)
179
180
181def rmtree(root):
182 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
183 make_writable(root, False)
184 if sys.platform == 'win32':
185 for i in range(3):
186 try:
187 shutil.rmtree(root)
188 break
189 except WindowsError: # pylint: disable=E0602
190 delay = (i+1)*2
191 print >> sys.stderr, (
192 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
193 time.sleep(delay)
194 else:
195 shutil.rmtree(root)
196
197
198def is_same_filesystem(path1, path2):
199 """Returns True if both paths are on the same filesystem.
200
201 This is required to enable the use of hardlinks.
202 """
203 assert os.path.isabs(path1), path1
204 assert os.path.isabs(path2), path2
205 if sys.platform == 'win32':
206 # If the drive letter mismatches, assume it's a separate partition.
207 # TODO(maruel): It should look at the underlying drive, a drive letter could
208 # be a mount point to a directory on another drive.
209 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
210 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
211 if path1[0].lower() != path2[0].lower():
212 return False
213 return os.stat(path1).st_dev == os.stat(path2).st_dev
214
215
216def get_free_space(path):
217 """Returns the number of free bytes."""
218 if sys.platform == 'win32':
219 free_bytes = ctypes.c_ulonglong(0)
220 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
221 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
222 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000223 # For OSes other than Windows.
224 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000225 return f.f_bfree * f.f_frsize
226
227
228def make_temp_dir(prefix, root_dir):
229 """Returns a temporary directory on the same file system as root_dir."""
230 base_temp_dir = None
231 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
232 base_temp_dir = os.path.dirname(root_dir)
233 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
234
235
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000236def load_isolated(content, os_flavor, algo):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000237 """Verifies the .isolated file is valid and loads this object with the json
238 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000239 """
240 try:
241 data = json.loads(content)
242 except ValueError:
243 raise ConfigError('Failed to parse: %s...' % content[:100])
244
245 if not isinstance(data, dict):
246 raise ConfigError('Expected dict, got %r' % data)
247
248 for key, value in data.iteritems():
249 if key == 'command':
250 if not isinstance(value, list):
251 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000252 if not value:
253 raise ConfigError('Expected non-empty command')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000254 for subvalue in value:
255 if not isinstance(subvalue, basestring):
256 raise ConfigError('Expected string, got %r' % subvalue)
257
258 elif key == 'files':
259 if not isinstance(value, dict):
260 raise ConfigError('Expected dict, got %r' % value)
261 for subkey, subvalue in value.iteritems():
262 if not isinstance(subkey, basestring):
263 raise ConfigError('Expected string, got %r' % subkey)
264 if not isinstance(subvalue, dict):
265 raise ConfigError('Expected dict, got %r' % subvalue)
266 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000267 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000268 if not isinstance(subsubvalue, basestring):
269 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000270 elif subsubkey == 'm':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000271 if not isinstance(subsubvalue, int):
272 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000273 elif subsubkey == 'h':
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000274 if not isolateserver.is_valid_hash(subsubvalue, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000275 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000276 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000277 if not isinstance(subsubvalue, int):
278 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000279 else:
280 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000281 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000282 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000283 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
284 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000285
286 elif key == 'includes':
287 if not isinstance(value, list):
288 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000289 if not value:
290 raise ConfigError('Expected non-empty includes list')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000291 for subvalue in value:
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000292 if not isolateserver.is_valid_hash(subvalue, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000293 raise ConfigError('Expected sha-1, got %r' % subvalue)
294
295 elif key == 'read_only':
296 if not isinstance(value, bool):
297 raise ConfigError('Expected bool, got %r' % value)
298
299 elif key == 'relative_cwd':
300 if not isinstance(value, basestring):
301 raise ConfigError('Expected string, got %r' % value)
302
303 elif key == 'os':
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000304 if os_flavor and value != os_flavor:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000305 raise ConfigError(
306 'Expected \'os\' to be \'%s\' but got \'%s\'' %
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000307 (os_flavor, value))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000308
309 else:
310 raise ConfigError('Unknown key %s' % key)
311
312 return data
313
314
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000315class CachePolicies(object):
316 def __init__(self, max_cache_size, min_free_space, max_items):
317 """
318 Arguments:
319 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
320 cache is effectively a leak.
321 - min_free_space: Trim if disk free space becomes lower than this value. If
322 0, it unconditionally fill the disk.
323 - max_items: Maximum number of items to keep in the cache. If 0, do not
324 enforce a limit.
325 """
326 self.max_cache_size = max_cache_size
327 self.min_free_space = min_free_space
328 self.max_items = max_items
329
330
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000331class DiskCache(object):
332 """Stateful LRU cache in a flat hash table in a directory.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000333
334 Saves its state as json file.
335 """
336 STATE_FILE = 'state.json'
337
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000338 def __init__(self, cache_dir, remote_fetcher, policies, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000339 """
340 Arguments:
341 - cache_dir: Directory where to place the cache.
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000342 - remote_fetcher: isolateserver.RemoteOperation where to fetch items from.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000343 - policies: cache retention policies.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000344 - algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000345 """
346 self.cache_dir = cache_dir
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000347 self.remote_fetcher = remote_fetcher
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000348 self.policies = policies
349 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000350 self.lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000351
352 # Items currently being fetched. Keep it local to reduce lock contention.
353 self._pending_queue = set()
354
355 # Profiling values.
356 self._added = []
357 self._removed = []
358 self._free_disk = 0
359
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000360 with tools.Profiler('Setup'):
maruel@chromium.org770993b2012-12-11 17:16:48 +0000361 if not os.path.isdir(self.cache_dir):
362 os.makedirs(self.cache_dir)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000363
364 # Load state of the cache.
vadimsh@chromium.orga40428e2013-07-04 15:43:14 +0000365 if os.path.isfile(self.state_file):
366 try:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000367 self.lru = lru.LRUDict.load(self.state_file)
368 except ValueError as err:
369 logging.error('Failed to load cache state: %s' % (err,))
370 # Don't want to keep broken state file.
371 os.remove(self.state_file)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000372
maruel@chromium.org770993b2012-12-11 17:16:48 +0000373 # Ensure that all files listed in the state still exist and add new ones.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000374 previous = self.lru.keys_set()
375 unknown = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000376 for filename in os.listdir(self.cache_dir):
377 if filename == self.STATE_FILE:
378 continue
379 if filename in previous:
380 previous.remove(filename)
381 continue
382 # An untracked file.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000383 if not isolateserver.is_valid_hash(filename, algo):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000384 logging.warning('Removing unknown file %s from cache', filename)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000385 os.remove(self.path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000386 continue
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000387 # File that's not referenced in 'state.json'.
388 # TODO(vadimsh): Verify its SHA1 matches file name.
389 logging.warning('Adding unknown file %s to cache', filename)
390 unknown.append(filename)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000391
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000392 if unknown:
393 # Add as oldest files. They will be deleted eventually if not accessed.
394 self._add_oldest_list(unknown)
395 logging.warning('Added back %d unknown files', len(unknown))
396
maruel@chromium.org770993b2012-12-11 17:16:48 +0000397 if previous:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000398 # Filter out entries that were not found.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000399 logging.warning('Removed %d lost files', len(previous))
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000400 for filename in previous:
401 self.lru.pop(filename)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000402 self.trim()
403
404 def __enter__(self):
405 return self
406
407 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000408 with tools.Profiler('CleanupTrimming'):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000409 self.trim()
410
411 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000412 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000413 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000414 '%5d (%8dkb) current',
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000415 len(self.lru),
416 sum(self.lru.itervalues()) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000417 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000418 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
419 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000420
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000421 def trim(self):
422 """Trims anything we don't know, make sure enough free space exists."""
423 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000424 if self.policies.max_cache_size:
425 total_size = sum(self.lru.itervalues())
426 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000427 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000428
429 # Ensure maximum number of items in the cache.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000430 if self.policies.max_items and len(self.lru) > self.policies.max_items:
431 for _ in xrange(len(self.lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000432 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000433
434 # Ensure enough free space.
435 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000436 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000437 while (
438 self.policies.min_free_space and
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000439 self.lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000440 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000441 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000442 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000443 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000444 if trimmed_due_to_space:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000445 total = sum(self.lru.itervalues())
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000446 logging.warning(
447 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
448 'cache (%.1f%% of its maximum capacity)',
449 self._free_disk / 1024.,
450 total / 1024.,
451 100. * self.policies.max_cache_size / float(total),
452 )
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000453 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000454
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000455 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000456 """Retrieves a file from the remote, if not already cached, and adds it to
457 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000458
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000459 If the file is in the cache, verify that the file is valid (i.e. it is
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000460 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000461 """
462 assert not '/' in item
463 path = self.path(item)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000464 found = False
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000465
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000466 if item in self.lru:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000467 # Note that is doesn't compute the hash so it could still be corrupted.
468 if not isolateserver.is_valid_file(self.path(item), size):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000469 self.lru.pop(item)
470 self._delete_file(item, size)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000471 else:
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000472 # Was already in cache. Update it's LRU value by putting it at the end.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000473 self.lru.touch(item)
474 found = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000475
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000476 if not found:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000477 if item in self._pending_queue:
478 # Already pending. The same object could be referenced multiple times.
479 return
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000480 # TODO(maruel): It should look at the free disk space, the current cache
481 # size and the size of the new item on every new item:
482 # - Trim the cache as more entries are listed when free disk space is low,
483 # otherwise if the amount of data downloaded during the run > free disk
484 # space, it'll crash.
485 # - Make sure there's enough free disk space to fit all dependencies of
486 # this run! If not, abort early.
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000487 self.remote_fetcher.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000488 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000489
490 def add(self, filepath, obj):
491 """Forcibly adds a file to the cache."""
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000492 if obj not in self.lru:
493 link_file(self.path(obj), filepath, HARDLINK)
494 self._add(obj)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000495
496 def path(self, item):
497 """Returns the path to one item."""
498 return os.path.join(self.cache_dir, item)
499
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000500 def wait_for(self, items):
501 """Starts a loop that waits for at least one of |items| to be retrieved.
502
503 Returns the first item retrieved.
504 """
505 # Flush items already present.
506 for item in items:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000507 if item in self.lru:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000508 return item
509
510 assert all(i in self._pending_queue for i in items), (
511 items, self._pending_queue)
512 # Note that:
513 # len(self._pending_queue) ==
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000514 # ( len(self.remote_fetcher._workers) - self.remote_fetcher._ready +
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000515 # len(self._remote._queue) + len(self._remote.done))
516 # There is no lock-free way to verify that.
517 while self._pending_queue:
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000518 item = self.remote_fetcher.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000519 self._pending_queue.remove(item)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000520 self._add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000521 if item in items:
522 return item
523
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000524 def _save(self):
525 """Saves the LRU ordering."""
526 self.lru.save(self.state_file)
527
528 def _remove_lru_file(self):
529 """Removes the last recently used file and returns its size."""
530 item, size = self.lru.pop_oldest()
531 self._delete_file(item, size)
532 return size
533
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000534 def _add(self, item):
535 """Adds an item into LRU cache marking it as a newest one."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000536 size = os.stat(self.path(item)).st_size
537 self._added.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000538 self.lru.add(item, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000539
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000540 def _add_oldest_list(self, items):
541 """Adds a bunch of items into LRU cache marking them as oldest ones."""
542 pairs = []
543 for item in items:
544 size = os.stat(self.path(item)).st_size
545 self._added.append(size)
546 pairs.append((item, size))
547 self.lru.batch_insert_oldest(pairs)
548
549 def _delete_file(self, item, size):
550 """Deletes cache file from the file system."""
551 self._removed.append(size)
552 try:
553 os.remove(self.path(item))
554 except OSError as e:
555 logging.error('Error attempting to delete a file\n%s' % e)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000556
557
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000558class IsolatedFile(object):
559 """Represents a single parsed .isolated file."""
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000560 def __init__(self, obj_hash, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000561 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000562 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000563 self.obj_hash = obj_hash
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000564 self.algo = algo
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000565 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000566 # .isolate and all the .isolated files recursively included by it with
567 # 'includes' key. The order of each sha-1 in 'includes', each representing a
568 # .isolated file in the hash table, is important, as the later ones are not
569 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000570 self.can_fetch = False
571
572 # Raw data.
573 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000574 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000575 self.children = []
576
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000577 # Set once the .isolated file is loaded.
578 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000579 # Set once the files are fetched.
580 self.files_fetched = False
581
582 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000583 """Verifies the .isolated file is valid and loads this object with the json
584 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000585 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000586 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
587 assert not self._is_parsed
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000588 self.data = load_isolated(content, get_flavor(), self.algo)
589 self.children = [
590 IsolatedFile(i, self.algo) for i in self.data.get('includes', [])
591 ]
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000592 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000593
594 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000595 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000596
597 Preemptively request files.
598
599 Note that |files| is modified by this function.
600 """
601 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000602 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000603 return
604 logging.debug('fetch_files(%s)' % self.obj_hash)
605 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000606 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000607 # overriden files must not be fetched.
608 if filepath not in files:
609 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000610 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000611 # Preemptively request files.
612 logging.debug('fetching %s' % filepath)
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000613 cache.retrieve(
614 isolateserver.RemoteOperation.MED,
615 properties['h'],
616 properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000617 self.files_fetched = True
618
619
620class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000621 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000622 def __init__(self):
623 self.command = []
624 self.files = {}
625 self.read_only = None
626 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000627 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000628 self.root = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000629
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000630 def load(self, cache, root_isolated_hash, algo):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000631 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000632
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000633 It enables support for "included" .isolated files. They are processed in
634 strict order but fetched asynchronously from the cache. This is important so
635 that a file in an included .isolated file that is overridden by an embedding
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000636 .isolated file is not fetched needlessly. The includes are fetched in one
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000637 pass and the files are fetched as soon as all the ones on the left-side
638 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000639
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000640 The prioritization is very important here for nested .isolated files.
641 'includes' have the highest priority and the algorithm is optimized for both
642 deep and wide trees. A deep one is a long link of .isolated files referenced
643 one at a time by one item in 'includes'. A wide one has a large number of
644 'includes' in a single .isolated file. 'left' is defined as an included
645 .isolated file earlier in the 'includes' list. So the order of the elements
646 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000647 """
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000648 self.root = IsolatedFile(root_isolated_hash, algo)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000649
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000650 # Isolated files being retrieved now: hash -> IsolatedFile instance.
651 pending = {}
652 # Set of hashes of already retrieved items to refuse recursive includes.
653 seen = set()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000654
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000655 def retrieve(isolated_file):
656 h = isolated_file.obj_hash
657 if h in seen:
658 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
659 assert h not in pending
660 seen.add(h)
661 pending[h] = isolated_file
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000662 cache.retrieve(
663 isolateserver.RemoteOperation.HIGH,
664 h,
665 isolateserver.UNKNOWN_FILE_SIZE)
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000666
667 retrieve(self.root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000668
669 while pending:
670 item_hash = cache.wait_for(pending)
671 item = pending.pop(item_hash)
672 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000673 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000674 # It's the root item.
675 item.can_fetch = True
676
677 for new_child in item.children:
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000678 retrieve(new_child)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000679
680 # Traverse the whole tree to see if files can now be fetched.
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000681 self._traverse_tree(cache, self.root)
682
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000683 def check(n):
684 return all(check(x) for x in n.children) and n.files_fetched
685 assert check(self.root)
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000686
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000687 self.relative_cwd = self.relative_cwd or ''
688 self.read_only = self.read_only or False
689
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000690 def _traverse_tree(self, cache, node):
691 if node.can_fetch:
692 if not node.files_fetched:
693 self._update_self(cache, node)
694 will_break = False
695 for i in node.children:
696 if not i.can_fetch:
697 if will_break:
698 break
699 # Automatically mark the first one as fetcheable.
700 i.can_fetch = True
701 will_break = True
702 self._traverse_tree(cache, i)
703
704 def _update_self(self, cache, node):
705 node.fetch_files(cache, self.files)
706 # Grabs properties.
707 if not self.command and node.data.get('command'):
708 self.command = node.data['command']
709 if self.read_only is None and node.data.get('read_only') is not None:
710 self.read_only = node.data['read_only']
711 if (self.relative_cwd is None and
712 node.data.get('relative_cwd') is not None):
713 self.relative_cwd = node.data['relative_cwd']
714
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000715
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000716def create_directories(base_directory, files):
717 """Creates the directory structure needed by the given list of files."""
718 logging.debug('create_directories(%s, %d)', base_directory, len(files))
719 # Creates the tree of directories to create.
720 directories = set(os.path.dirname(f) for f in files)
721 for item in list(directories):
722 while item:
723 directories.add(item)
724 item = os.path.dirname(item)
725 for d in sorted(directories):
726 if d:
727 os.mkdir(os.path.join(base_directory, d))
728
729
730def create_links(base_directory, files):
731 """Creates any links needed by the given set of files."""
732 for filepath, properties in files:
csharp@chromium.org89eaf082013-03-26 18:56:21 +0000733 if 'l' not in properties:
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000734 continue
maruel@chromium.org3320ee12013-03-28 13:23:31 +0000735 if sys.platform == 'win32':
736 # TODO(maruel): Create junctions or empty text files similar to what
737 # cygwin do?
738 logging.warning('Ignoring symlink %s', filepath)
739 continue
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000740 outfile = os.path.join(base_directory, filepath)
741 # symlink doesn't exist on Windows. So the 'link' property should
742 # never be specified for windows .isolated file.
743 os.symlink(properties['l'], outfile) # pylint: disable=E1101
744 if 'm' in properties:
745 lchmod = getattr(os, 'lchmod', None)
746 if lchmod:
747 lchmod(outfile, properties['m'])
748
749
750def setup_commands(base_directory, cwd, cmd):
751 """Correctly adjusts and then returns the required working directory
752 and command needed to run the test.
753 """
754 assert not os.path.isabs(cwd), 'The cwd must be a relative path, got %s' % cwd
755 cwd = os.path.join(base_directory, cwd)
756 if not os.path.isdir(cwd):
757 os.makedirs(cwd)
758
759 # Ensure paths are correctly separated on windows.
760 cmd[0] = cmd[0].replace('/', os.path.sep)
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000761 cmd = tools.fix_python_path(cmd)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000762
763 return cwd, cmd
764
765
766def generate_remaining_files(files):
767 """Generates a dictionary of all the remaining files to be downloaded."""
768 remaining = {}
769 for filepath, props in files:
770 if 'h' in props:
771 remaining.setdefault(props['h'], []).append((filepath, props))
772
773 return remaining
774
775
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000776def run_tha_test(isolated_hash, cache_dir, retriever, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000777 """Downloads the dependencies in the cache, hardlinks them into a temporary
778 directory and runs the executable.
779 """
780 settings = Settings()
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000781 remote = isolateserver.RemoteOperation(retriever)
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000782 algo = hashlib.sha1
783 with DiskCache(cache_dir, remote, policies, algo) as cache:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000784 outdir = make_temp_dir('run_tha_test', cache_dir)
785 try:
786 # Initiate all the files download.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000787 with tools.Profiler('GetIsolateds'):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000788 # Optionally support local files.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000789 if not isolateserver.is_valid_hash(isolated_hash, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000790 # Adds it in the cache. While not strictly necessary, this simplifies
791 # the rest.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000792 h = isolateserver.hash_file(isolated_hash, algo)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000793 cache.add(isolated_hash, h)
794 isolated_hash = h
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000795 settings.load(cache, isolated_hash, algo)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000796
797 if not settings.command:
798 print >> sys.stderr, 'No command to run'
799 return 1
800
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000801 with tools.Profiler('GetRest'):
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000802 create_directories(outdir, settings.files)
803 create_links(outdir, settings.files.iteritems())
804 remaining = generate_remaining_files(settings.files.iteritems())
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000805
806 # Do bookkeeping while files are being downloaded in the background.
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000807 cwd, cmd = setup_commands(outdir, settings.relative_cwd,
808 settings.command[:])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000809
810 # Now block on the remaining files to be downloaded and mapped.
csharp@chromium.org9c59ff12012-12-12 02:32:29 +0000811 logging.info('Retrieving remaining files')
812 last_update = time.time()
vadimsh@chromium.orgb074b162013-08-22 17:55:46 +0000813 with threading_utils.DeadlockDetector(DEADLOCK_TIMEOUT) as detector:
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +0000814 while remaining:
815 detector.ping()
816 obj = cache.wait_for(remaining)
817 for filepath, properties in remaining.pop(obj):
818 outfile = os.path.join(outdir, filepath)
maruel@chromium.orgb7c003d2013-07-24 13:04:30 +0000819 link_file(outfile, cache.path(obj), HARDLINK)
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +0000820 if 'm' in properties:
821 # It's not set on Windows.
822 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000823
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +0000824 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
825 msg = '%d files remaining...' % len(remaining)
826 print msg
827 logging.info(msg)
828 last_update = time.time()
csharp@chromium.org9c59ff12012-12-12 02:32:29 +0000829
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000830 if settings.read_only:
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +0000831 logging.info('Making files read only')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000832 make_writable(outdir, True)
833 logging.info('Running %s, cwd=%s' % (cmd, cwd))
csharp@chromium.orge217f302012-11-22 16:51:53 +0000834
835 # TODO(csharp): This should be specified somewhere else.
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000836 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
csharp@chromium.orge217f302012-11-22 16:51:53 +0000837 # Add a rotating log file if one doesn't already exist.
838 env = os.environ.copy()
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000839 env.setdefault('RUN_TEST_CASES_LOG_FILE',
840 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000841 try:
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000842 with tools.Profiler('RunTest'):
csharp@chromium.orge217f302012-11-22 16:51:53 +0000843 return subprocess.call(cmd, cwd=cwd, env=env)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000844 except OSError:
845 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
846 raise
847 finally:
848 rmtree(outdir)
849
850
851def main():
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000852 tools.disable_buffering()
853 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000854 usage='%prog <options>',
855 version=__version__,
856 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000857
858 group = optparse.OptionGroup(parser, 'Data source')
859 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000860 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000861 metavar='FILE',
862 help='File/url describing what to map or run')
863 group.add_option(
864 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000865 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000866 group.add_option(
867 '-I', '--isolate-server', metavar='URL',
868 default=
869 'https://isolateserver.appspot.com',
870 help='Remote where to get the items. Defaults to %default')
871 group.add_option(
872 '-n', '--namespace',
873 default='default-gzip',
874 help='namespace to use when using isolateserver, default: %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000875 parser.add_option_group(group)
876
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000877 group = optparse.OptionGroup(parser, 'Cache management')
878 group.add_option(
879 '--cache',
880 default='cache',
881 metavar='DIR',
882 help='Cache directory, default=%default')
883 group.add_option(
884 '--max-cache-size',
885 type='int',
886 metavar='NNN',
887 default=20*1024*1024*1024,
888 help='Trim if the cache gets larger than this value, default=%default')
889 group.add_option(
890 '--min-free-space',
891 type='int',
892 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000893 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000894 help='Trim if disk free space becomes lower than this value, '
895 'default=%default')
896 group.add_option(
897 '--max-items',
898 type='int',
899 metavar='NNN',
900 default=100000,
901 help='Trim if more than this number of items are in the cache '
902 'default=%default')
903 parser.add_option_group(group)
904
905 options, args = parser.parse_args()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000906
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000907 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000908 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000909 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000910 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000911 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000912 parser.error('Unsupported args %s' % ' '.join(args))
913
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000914 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000915 policies = CachePolicies(
916 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000917
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000918 retriever = isolateserver.get_storage_api(
919 options.isolate_server, options.namespace)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000920 try:
921 return run_tha_test(
922 options.isolated or options.hash,
923 options.cache,
924 retriever.retrieve,
925 policies)
926 except Exception, e:
927 # Make sure any exception is logged.
928 logging.exception(e)
929 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000930
931
932if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000933 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000934 fix_encoding.fix_encoding()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000935 sys.exit(main())