blob: d5a5c34946534216d904d3e54f003eb1c105e218 [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.org8750e4b2013-09-18 02:37:57 +0000338 def __init__(self, cache_dir, pool, retriever, policies, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000339 """
340 Arguments:
341 - cache_dir: Directory where to place the cache.
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000342 - pool: isolateserver.WorkerPool.
343 - retriever: API where to fetch items from.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000344 - policies: cache retention policies.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000345 - algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000346 """
347 self.cache_dir = cache_dir
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000348 self.pool = pool
349 self.retriever = retriever
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000350 self.policies = policies
351 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000352 self.lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000353
354 # Items currently being fetched. Keep it local to reduce lock contention.
355 self._pending_queue = set()
356
357 # Profiling values.
358 self._added = []
359 self._removed = []
360 self._free_disk = 0
361
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000362 with tools.Profiler('Setup'):
maruel@chromium.org770993b2012-12-11 17:16:48 +0000363 if not os.path.isdir(self.cache_dir):
364 os.makedirs(self.cache_dir)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000365
366 # Load state of the cache.
vadimsh@chromium.orga40428e2013-07-04 15:43:14 +0000367 if os.path.isfile(self.state_file):
368 try:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000369 self.lru = lru.LRUDict.load(self.state_file)
370 except ValueError as err:
371 logging.error('Failed to load cache state: %s' % (err,))
372 # Don't want to keep broken state file.
373 os.remove(self.state_file)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000374
maruel@chromium.org770993b2012-12-11 17:16:48 +0000375 # Ensure that all files listed in the state still exist and add new ones.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000376 previous = self.lru.keys_set()
377 unknown = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000378 for filename in os.listdir(self.cache_dir):
379 if filename == self.STATE_FILE:
380 continue
381 if filename in previous:
382 previous.remove(filename)
383 continue
384 # An untracked file.
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000385 if not isolateserver.is_valid_hash(filename, algo):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000386 logging.warning('Removing unknown file %s from cache', filename)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000387 os.remove(self.path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000388 continue
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000389 # File that's not referenced in 'state.json'.
390 # TODO(vadimsh): Verify its SHA1 matches file name.
391 logging.warning('Adding unknown file %s to cache', filename)
392 unknown.append(filename)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000393
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000394 if unknown:
395 # Add as oldest files. They will be deleted eventually if not accessed.
396 self._add_oldest_list(unknown)
397 logging.warning('Added back %d unknown files', len(unknown))
398
maruel@chromium.org770993b2012-12-11 17:16:48 +0000399 if previous:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000400 # Filter out entries that were not found.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000401 logging.warning('Removed %d lost files', len(previous))
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000402 for filename in previous:
403 self.lru.pop(filename)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000404 self.trim()
405
406 def __enter__(self):
407 return self
408
409 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000410 with tools.Profiler('CleanupTrimming'):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000411 self.trim()
412
413 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000414 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000415 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000416 '%5d (%8dkb) current',
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000417 len(self.lru),
418 sum(self.lru.itervalues()) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000419 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000420 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
421 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000422
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000423 def trim(self):
424 """Trims anything we don't know, make sure enough free space exists."""
425 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000426 if self.policies.max_cache_size:
427 total_size = sum(self.lru.itervalues())
428 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000429 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000430
431 # Ensure maximum number of items in the cache.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000432 if self.policies.max_items and len(self.lru) > self.policies.max_items:
433 for _ in xrange(len(self.lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000434 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000435
436 # Ensure enough free space.
437 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000438 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000439 while (
440 self.policies.min_free_space and
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000441 self.lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000442 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000443 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000444 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000445 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000446 if trimmed_due_to_space:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000447 total = sum(self.lru.itervalues())
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000448 logging.warning(
449 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
450 'cache (%.1f%% of its maximum capacity)',
451 self._free_disk / 1024.,
452 total / 1024.,
453 100. * self.policies.max_cache_size / float(total),
454 )
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000455 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000456
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000457 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000458 """Retrieves a file from the remote, if not already cached, and adds it to
459 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000460
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000461 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 +0000462 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000463 """
464 assert not '/' in item
465 path = self.path(item)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000466 found = False
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000467
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000468 if item in self.lru:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000469 # Note that is doesn't compute the hash so it could still be corrupted.
470 if not isolateserver.is_valid_file(self.path(item), size):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000471 self.lru.pop(item)
472 self._delete_file(item, size)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000473 else:
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000474 # Was already in cache. Update it's LRU value by putting it at the end.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000475 self.lru.touch(item)
476 found = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000477
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000478 if not found:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000479 if item in self._pending_queue:
480 # Already pending. The same object could be referenced multiple times.
481 return
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000482 # TODO(maruel): It should look at the free disk space, the current cache
483 # size and the size of the new item on every new item:
484 # - Trim the cache as more entries are listed when free disk space is low,
485 # otherwise if the amount of data downloaded during the run > free disk
486 # space, it'll crash.
487 # - Make sure there's enough free disk space to fit all dependencies of
488 # this run! If not, abort early.
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000489 self.pool.add_task(priority, self._store, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000490 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000491
492 def add(self, filepath, obj):
493 """Forcibly adds a file to the cache."""
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000494 if obj not in self.lru:
495 link_file(self.path(obj), filepath, HARDLINK)
496 self._add(obj)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000497
498 def path(self, item):
499 """Returns the path to one item."""
500 return os.path.join(self.cache_dir, item)
501
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000502 def wait_for(self, items):
503 """Starts a loop that waits for at least one of |items| to be retrieved.
504
505 Returns the first item retrieved.
506 """
507 # Flush items already present.
508 for item in items:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000509 if item in self.lru:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000510 return item
511
512 assert all(i in self._pending_queue for i in items), (
513 items, self._pending_queue)
514 # Note that:
515 # len(self._pending_queue) ==
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000516 # ( len(self.remote_fetcher._workers) - self.remote_fetcher._ready +
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000517 # len(self._remote._queue) + len(self._remote.done))
518 # There is no lock-free way to verify that.
519 while self._pending_queue:
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000520 item = self.pool.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000521 self._pending_queue.remove(item)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000522 self._add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000523 if item in items:
524 return item
525
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000526 def _save(self):
527 """Saves the LRU ordering."""
528 self.lru.save(self.state_file)
529
530 def _remove_lru_file(self):
531 """Removes the last recently used file and returns its size."""
532 item, size = self.lru.pop_oldest()
533 self._delete_file(item, size)
534 return size
535
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000536 def _add(self, item):
537 """Adds an item into LRU cache marking it as a newest one."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000538 size = os.stat(self.path(item)).st_size
539 self._added.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000540 self.lru.add(item, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000541
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000542 def _add_oldest_list(self, items):
543 """Adds a bunch of items into LRU cache marking them as oldest ones."""
544 pairs = []
545 for item in items:
546 size = os.stat(self.path(item)).st_size
547 self._added.append(size)
548 pairs.append((item, size))
549 self.lru.batch_insert_oldest(pairs)
550
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000551 def _store(self, item, path, expected_size):
552 """Stores the data generated by remote_fetcher."""
553 isolateserver.file_write(path, self.retriever(item, expected_size))
554 return item
555
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000556 def _delete_file(self, item, size):
557 """Deletes cache file from the file system."""
558 self._removed.append(size)
559 try:
560 os.remove(self.path(item))
561 except OSError as e:
562 logging.error('Error attempting to delete a file\n%s' % e)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000563
564
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000565class IsolatedFile(object):
566 """Represents a single parsed .isolated file."""
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000567 def __init__(self, obj_hash, algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000568 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000569 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000570 self.obj_hash = obj_hash
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000571 self.algo = algo
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000572 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000573 # .isolate and all the .isolated files recursively included by it with
574 # 'includes' key. The order of each sha-1 in 'includes', each representing a
575 # .isolated file in the hash table, is important, as the later ones are not
576 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000577 self.can_fetch = False
578
579 # Raw data.
580 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000581 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000582 self.children = []
583
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000584 # Set once the .isolated file is loaded.
585 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000586 # Set once the files are fetched.
587 self.files_fetched = False
588
589 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000590 """Verifies the .isolated file is valid and loads this object with the json
591 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000592 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000593 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
594 assert not self._is_parsed
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000595 self.data = load_isolated(content, get_flavor(), self.algo)
596 self.children = [
597 IsolatedFile(i, self.algo) for i in self.data.get('includes', [])
598 ]
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000599 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000600
601 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000602 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000603
604 Preemptively request files.
605
606 Note that |files| is modified by this function.
607 """
608 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000609 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000610 return
611 logging.debug('fetch_files(%s)' % self.obj_hash)
612 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000613 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000614 # overriden files must not be fetched.
615 if filepath not in files:
616 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000617 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000618 # Preemptively request files.
619 logging.debug('fetching %s' % filepath)
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000620 cache.retrieve(
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000621 isolateserver.WorkerPool.MED,
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000622 properties['h'],
623 properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000624 self.files_fetched = True
625
626
627class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000628 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000629 def __init__(self):
630 self.command = []
631 self.files = {}
632 self.read_only = None
633 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000634 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000635 self.root = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000636
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000637 def load(self, cache, root_isolated_hash, algo):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000638 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000639
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000640 It enables support for "included" .isolated files. They are processed in
641 strict order but fetched asynchronously from the cache. This is important so
642 that a file in an included .isolated file that is overridden by an embedding
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000643 .isolated file is not fetched needlessly. The includes are fetched in one
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000644 pass and the files are fetched as soon as all the ones on the left-side
645 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000646
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000647 The prioritization is very important here for nested .isolated files.
648 'includes' have the highest priority and the algorithm is optimized for both
649 deep and wide trees. A deep one is a long link of .isolated files referenced
650 one at a time by one item in 'includes'. A wide one has a large number of
651 'includes' in a single .isolated file. 'left' is defined as an included
652 .isolated file earlier in the 'includes' list. So the order of the elements
653 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000654 """
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000655 self.root = IsolatedFile(root_isolated_hash, algo)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000656
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000657 # Isolated files being retrieved now: hash -> IsolatedFile instance.
658 pending = {}
659 # Set of hashes of already retrieved items to refuse recursive includes.
660 seen = set()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000661
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000662 def retrieve(isolated_file):
663 h = isolated_file.obj_hash
664 if h in seen:
665 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
666 assert h not in pending
667 seen.add(h)
668 pending[h] = isolated_file
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000669 cache.retrieve(
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000670 isolateserver.WorkerPool.HIGH,
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000671 h,
672 isolateserver.UNKNOWN_FILE_SIZE)
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000673
674 retrieve(self.root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000675
676 while pending:
677 item_hash = cache.wait_for(pending)
678 item = pending.pop(item_hash)
679 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000680 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000681 # It's the root item.
682 item.can_fetch = True
683
684 for new_child in item.children:
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000685 retrieve(new_child)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000686
687 # Traverse the whole tree to see if files can now be fetched.
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000688 self._traverse_tree(cache, self.root)
689
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000690 def check(n):
691 return all(check(x) for x in n.children) and n.files_fetched
692 assert check(self.root)
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000693
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000694 self.relative_cwd = self.relative_cwd or ''
695 self.read_only = self.read_only or False
696
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000697 def _traverse_tree(self, cache, node):
698 if node.can_fetch:
699 if not node.files_fetched:
700 self._update_self(cache, node)
701 will_break = False
702 for i in node.children:
703 if not i.can_fetch:
704 if will_break:
705 break
706 # Automatically mark the first one as fetcheable.
707 i.can_fetch = True
708 will_break = True
709 self._traverse_tree(cache, i)
710
711 def _update_self(self, cache, node):
712 node.fetch_files(cache, self.files)
713 # Grabs properties.
714 if not self.command and node.data.get('command'):
715 self.command = node.data['command']
716 if self.read_only is None and node.data.get('read_only') is not None:
717 self.read_only = node.data['read_only']
718 if (self.relative_cwd is None and
719 node.data.get('relative_cwd') is not None):
720 self.relative_cwd = node.data['relative_cwd']
721
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000722
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000723def run_tha_test(isolated_hash, cache_dir, retriever, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000724 """Downloads the dependencies in the cache, hardlinks them into a temporary
725 directory and runs the executable.
726 """
maruel@chromium.org7b844a62013-09-17 13:04:59 +0000727 algo = hashlib.sha1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000728 outdir = None
729 try:
730 settings = Settings()
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000731 # |pool| must outlive |cache|.
732 with isolateserver.WorkerPool() as pool:
733 with DiskCache(cache_dir, pool, retriever, policies, algo) as cache:
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000734 # |cache_dir| may not exist until DiskCache() instance is created.
735 outdir = make_temp_dir('run_tha_test', cache_dir)
736 # Initiate all the files download.
737 with tools.Profiler('GetIsolateds'):
738 # Optionally support local files.
739 if not isolateserver.is_valid_hash(isolated_hash, algo):
740 # Adds it in the cache. While not strictly necessary, this
741 # simplifies the rest.
742 h = isolateserver.hash_file(isolated_hash, algo)
743 cache.add(isolated_hash, h)
744 isolated_hash = h
745 settings.load(cache, isolated_hash, algo)
746
747 if not settings.command:
748 print >> sys.stderr, 'No command to run'
749 return 1
750
751 with tools.Profiler('GetRest'):
752 isolateserver.create_directories(outdir, settings.files)
753 isolateserver.create_links(outdir, settings.files.iteritems())
754 remaining = isolateserver.generate_remaining_files(
755 settings.files.iteritems())
756
757 # Do bookkeeping while files are being downloaded in the background.
758 cwd, cmd = isolateserver.setup_commands(
759 outdir, settings.relative_cwd, settings.command[:])
760
761 # Now block on the remaining files to be downloaded and mapped.
762 logging.info('Retrieving remaining files')
763 last_update = time.time()
764 with threading_utils.DeadlockDetector(DEADLOCK_TIMEOUT) as detector:
765 while remaining:
766 detector.ping()
767 obj = cache.wait_for(remaining)
768 for filepath, properties in remaining.pop(obj):
769 outfile = os.path.join(outdir, filepath)
770 link_file(outfile, cache.path(obj), HARDLINK)
771 if 'm' in properties:
772 # It's not set on Windows.
773 os.chmod(outfile, properties['m'])
774
775 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
776 msg = '%d files remaining...' % len(remaining)
777 print msg
778 logging.info(msg)
779 last_update = time.time()
780
781 if settings.read_only:
782 logging.info('Making files read only')
783 make_writable(outdir, True)
784 logging.info('Running %s, cwd=%s' % (cmd, cwd))
785
786 # TODO(csharp): This should be specified somewhere else.
787 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
788 # Add a rotating log file if one doesn't already exist.
789 env = os.environ.copy()
790 env.setdefault('RUN_TEST_CASES_LOG_FILE',
791 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000792 try:
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000793 with tools.Profiler('RunTest'):
794 return subprocess.call(cmd, cwd=cwd, env=env)
795 except OSError:
796 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
797 raise
798 finally:
799 if outdir:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000800 rmtree(outdir)
801
802
803def main():
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000804 tools.disable_buffering()
805 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000806 usage='%prog <options>',
807 version=__version__,
808 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000809
810 group = optparse.OptionGroup(parser, 'Data source')
811 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000812 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000813 metavar='FILE',
814 help='File/url describing what to map or run')
815 group.add_option(
816 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000817 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000818 group.add_option(
819 '-I', '--isolate-server', metavar='URL',
820 default=
821 'https://isolateserver.appspot.com',
822 help='Remote where to get the items. Defaults to %default')
823 group.add_option(
824 '-n', '--namespace',
825 default='default-gzip',
826 help='namespace to use when using isolateserver, default: %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000827 parser.add_option_group(group)
828
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000829 group = optparse.OptionGroup(parser, 'Cache management')
830 group.add_option(
831 '--cache',
832 default='cache',
833 metavar='DIR',
834 help='Cache directory, default=%default')
835 group.add_option(
836 '--max-cache-size',
837 type='int',
838 metavar='NNN',
839 default=20*1024*1024*1024,
840 help='Trim if the cache gets larger than this value, default=%default')
841 group.add_option(
842 '--min-free-space',
843 type='int',
844 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000845 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000846 help='Trim if disk free space becomes lower than this value, '
847 'default=%default')
848 group.add_option(
849 '--max-items',
850 type='int',
851 metavar='NNN',
852 default=100000,
853 help='Trim if more than this number of items are in the cache '
854 'default=%default')
855 parser.add_option_group(group)
856
857 options, args = parser.parse_args()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000858
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000859 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000860 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000861 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000862 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000863 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000864 parser.error('Unsupported args %s' % ' '.join(args))
865
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000866 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000867 policies = CachePolicies(
868 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000869
maruel@chromium.orgb7e79a22013-09-13 01:24:56 +0000870 retriever = isolateserver.get_storage_api(
871 options.isolate_server, options.namespace)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000872 try:
873 return run_tha_test(
874 options.isolated or options.hash,
875 options.cache,
maruel@chromium.org8750e4b2013-09-18 02:37:57 +0000876 retriever.fetch,
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000877 policies)
878 except Exception, e:
879 # Make sure any exception is logged.
880 logging.exception(e)
881 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000882
883
884if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000885 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000886 fix_encoding.fix_encoding()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000887 sys.exit(main())