blob: 0db8175039809b14a6b11879d4116d4fa6a76e3f [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.org9c72d4e2012-09-28 19:20:25 +000011import ctypes
12import hashlib
csharp@chromium.orga110d792013-01-07 16:16:16 +000013import httplib
maruel@chromium.orgedd25d02013-03-26 14:38:00 +000014import inspect
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000015import json
16import logging
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000017import logging.handlers
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000018import math
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000019import optparse
20import os
21import Queue
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000022import random
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000023import re
24import shutil
25import stat
26import subprocess
27import sys
28import tempfile
29import threading
30import time
maruel@chromium.org97cd0be2013-03-13 14:01:36 +000031import traceback
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000032import urllib
csharp@chromium.orga92403f2012-11-20 15:13:59 +000033import urllib2
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000034import urlparse
csharp@chromium.orga92403f2012-11-20 15:13:59 +000035import zlib
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000036
37
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000038# Types of action accepted by link_file().
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000039HARDLINK, SYMLINK, COPY = range(1, 4)
40
41RE_IS_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$')
42
csharp@chromium.org8dc52542012-11-08 20:29:55 +000043# The file size to be used when we don't know the correct file size,
44# generally used for .isolated files.
45UNKNOWN_FILE_SIZE = None
46
csharp@chromium.orga92403f2012-11-20 15:13:59 +000047# The size of each chunk to read when downloading and unzipping files.
48ZIPPED_FILE_CHUNK = 16 * 1024
49
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 base directory containing this file.
54BASE_DIR = os.path.dirname(os.path.abspath(__file__))
55
56# The name of the log to use for the run_test_cases.py command
57RUN_TEST_CASES_LOG = os.path.join(BASE_DIR, 'run_test_cases.log')
58
csharp@chromium.org9c59ff12012-12-12 02:32:29 +000059# The delay (in seconds) to wait between logging statements when retrieving
60# the required files. This is intended to let the user (or buildbot) know that
61# the program is still running.
62DELAY_BETWEEN_UPDATES_IN_SECS = 30
63
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000064# The name of the key to store the count of url attempts.
65COUNT_KEY = 'UrlOpenAttempt'
66
67# The maximum number of attempts to trying opening a url before aborting.
68MAX_URL_OPEN_ATTEMPTS = 20
69
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000070
71class ConfigError(ValueError):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +000072 """Generic failure to load a .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000073 pass
74
75
76class MappingError(OSError):
77 """Failed to recreate the tree."""
78 pass
79
80
81def get_flavor():
82 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
83 flavors = {
84 'cygwin': 'win',
85 'win32': 'win',
86 'darwin': 'mac',
87 'sunos5': 'solaris',
88 'freebsd7': 'freebsd',
89 'freebsd8': 'freebsd',
90 }
91 return flavors.get(sys.platform, 'linux')
92
93
maruel@chromium.org46e61cc2013-03-25 19:55:34 +000094class Unbuffered(object):
95 """Disable buffering on a file object."""
96 def __init__(self, stream):
97 self.stream = stream
98
99 def write(self, data):
100 self.stream.write(data)
101 if '\n' in data:
102 self.stream.flush()
103
104 def __getattr__(self, attr):
105 return getattr(self.stream, attr)
106
107
108def disable_buffering():
109 """Makes this process and child processes stdout unbuffered."""
110 if not os.environ.get('PYTHONUNBUFFERED'):
111 # Since sys.stdout is a C++ object, it's impossible to do
112 # sys.stdout.write = lambda...
113 sys.stdout = Unbuffered(sys.stdout)
114 os.environ['PYTHONUNBUFFERED'] = 'x'
115
116
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000117def os_link(source, link_name):
118 """Add support for os.link() on Windows."""
119 if sys.platform == 'win32':
120 if not ctypes.windll.kernel32.CreateHardLinkW(
121 unicode(link_name), unicode(source), 0):
122 raise OSError()
123 else:
124 os.link(source, link_name)
125
126
127def readable_copy(outfile, infile):
128 """Makes a copy of the file that is readable by everyone."""
129 shutil.copy(infile, outfile)
130 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
131 stat.S_IRGRP | stat.S_IROTH)
132 os.chmod(outfile, read_enabled_mode)
133
134
135def link_file(outfile, infile, action):
136 """Links a file. The type of link depends on |action|."""
137 logging.debug('Mapping %s to %s' % (infile, outfile))
138 if action not in (HARDLINK, SYMLINK, COPY):
139 raise ValueError('Unknown mapping action %s' % action)
140 if not os.path.isfile(infile):
141 raise MappingError('%s is missing' % infile)
142 if os.path.isfile(outfile):
143 raise MappingError(
144 '%s already exist; insize:%d; outsize:%d' %
145 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
146
147 if action == COPY:
148 readable_copy(outfile, infile)
149 elif action == SYMLINK and sys.platform != 'win32':
150 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000151 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000152 else:
153 try:
154 os_link(infile, outfile)
155 except OSError:
156 # Probably a different file system.
157 logging.warn(
158 'Failed to hardlink, failing back to copy %s to %s' % (
159 infile, outfile))
160 readable_copy(outfile, infile)
161
162
163def _set_write_bit(path, read_only):
164 """Sets or resets the executable bit on a file or directory."""
165 mode = os.lstat(path).st_mode
166 if read_only:
167 mode = mode & 0500
168 else:
169 mode = mode | 0200
170 if hasattr(os, 'lchmod'):
171 os.lchmod(path, mode) # pylint: disable=E1101
172 else:
173 if stat.S_ISLNK(mode):
174 # Skip symlink without lchmod() support.
175 logging.debug('Can\'t change +w bit on symlink %s' % path)
176 return
177
178 # TODO(maruel): Implement proper DACL modification on Windows.
179 os.chmod(path, mode)
180
181
182def make_writable(root, read_only):
183 """Toggle the writable bit on a directory tree."""
csharp@chromium.org837352f2013-01-17 21:17:03 +0000184 assert os.path.isabs(root), root
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000185 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
186 for filename in filenames:
187 _set_write_bit(os.path.join(dirpath, filename), read_only)
188
189 for dirname in dirnames:
190 _set_write_bit(os.path.join(dirpath, dirname), read_only)
191
192
193def rmtree(root):
194 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
195 make_writable(root, False)
196 if sys.platform == 'win32':
197 for i in range(3):
198 try:
199 shutil.rmtree(root)
200 break
201 except WindowsError: # pylint: disable=E0602
202 delay = (i+1)*2
203 print >> sys.stderr, (
204 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
205 time.sleep(delay)
206 else:
207 shutil.rmtree(root)
208
209
210def is_same_filesystem(path1, path2):
211 """Returns True if both paths are on the same filesystem.
212
213 This is required to enable the use of hardlinks.
214 """
215 assert os.path.isabs(path1), path1
216 assert os.path.isabs(path2), path2
217 if sys.platform == 'win32':
218 # If the drive letter mismatches, assume it's a separate partition.
219 # TODO(maruel): It should look at the underlying drive, a drive letter could
220 # be a mount point to a directory on another drive.
221 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
222 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
223 if path1[0].lower() != path2[0].lower():
224 return False
225 return os.stat(path1).st_dev == os.stat(path2).st_dev
226
227
228def get_free_space(path):
229 """Returns the number of free bytes."""
230 if sys.platform == 'win32':
231 free_bytes = ctypes.c_ulonglong(0)
232 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
233 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
234 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000235 # For OSes other than Windows.
236 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000237 return f.f_bfree * f.f_frsize
238
239
240def make_temp_dir(prefix, root_dir):
241 """Returns a temporary directory on the same file system as root_dir."""
242 base_temp_dir = None
243 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
244 base_temp_dir = os.path.dirname(root_dir)
245 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
246
247
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000248def load_isolated(content):
249 """Verifies the .isolated file is valid and loads this object with the json
250 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000251 """
252 try:
253 data = json.loads(content)
254 except ValueError:
255 raise ConfigError('Failed to parse: %s...' % content[:100])
256
257 if not isinstance(data, dict):
258 raise ConfigError('Expected dict, got %r' % data)
259
260 for key, value in data.iteritems():
261 if key == 'command':
262 if not isinstance(value, list):
263 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000264 if not value:
265 raise ConfigError('Expected non-empty command')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000266 for subvalue in value:
267 if not isinstance(subvalue, basestring):
268 raise ConfigError('Expected string, got %r' % subvalue)
269
270 elif key == 'files':
271 if not isinstance(value, dict):
272 raise ConfigError('Expected dict, got %r' % value)
273 for subkey, subvalue in value.iteritems():
274 if not isinstance(subkey, basestring):
275 raise ConfigError('Expected string, got %r' % subkey)
276 if not isinstance(subvalue, dict):
277 raise ConfigError('Expected dict, got %r' % subvalue)
278 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000279 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000280 if not isinstance(subsubvalue, basestring):
281 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000282 elif subsubkey == 'm':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000283 if not isinstance(subsubvalue, int):
284 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000285 elif subsubkey == 'h':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000286 if not RE_IS_SHA1.match(subsubvalue):
287 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000288 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000289 if not isinstance(subsubvalue, int):
290 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000291 else:
292 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000293 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000294 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000295 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
296 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000297
298 elif key == 'includes':
299 if not isinstance(value, list):
300 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000301 if not value:
302 raise ConfigError('Expected non-empty includes list')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000303 for subvalue in value:
304 if not RE_IS_SHA1.match(subvalue):
305 raise ConfigError('Expected sha-1, got %r' % subvalue)
306
307 elif key == 'read_only':
308 if not isinstance(value, bool):
309 raise ConfigError('Expected bool, got %r' % value)
310
311 elif key == 'relative_cwd':
312 if not isinstance(value, basestring):
313 raise ConfigError('Expected string, got %r' % value)
314
315 elif key == 'os':
316 if value != get_flavor():
317 raise ConfigError(
318 'Expected \'os\' to be \'%s\' but got \'%s\'' %
319 (get_flavor(), value))
320
321 else:
322 raise ConfigError('Unknown key %s' % key)
323
324 return data
325
326
327def fix_python_path(cmd):
328 """Returns the fixed command line to call the right python executable."""
329 out = cmd[:]
330 if out[0] == 'python':
331 out[0] = sys.executable
332 elif out[0].endswith('.py'):
333 out.insert(0, sys.executable)
334 return out
335
336
maruel@chromium.orgef333122013-03-12 20:36:40 +0000337def url_open(url, data=None, retry_404=False, content_type=None):
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000338 """Attempts to open the given url multiple times.
339
340 |data| can be either:
341 -None for a GET request
342 -str for pre-encoded data
343 -list for data to be encoded
344 -dict for data to be encoded (COUNT_KEY will be added in this case)
345
346 If no wait_duration is given, the default wait time will exponentially
347 increase between each retry.
348
349 Returns a file-like object, where the response may be read from, or None
350 if it was unable to connect.
351 """
352 method = 'GET' if data is None else 'POST'
353
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000354 if isinstance(data, dict) and COUNT_KEY in data:
355 logging.error('%s already existed in the data passed into UlrOpen. It '
356 'would be overwritten. Aborting UrlOpen', COUNT_KEY)
357 return None
358
maruel@chromium.orgef333122013-03-12 20:36:40 +0000359 assert not ((method != 'POST') and content_type), (
360 'Can\'t use content_type on GET')
361
362 def make_request(extra):
363 """Returns a urllib2.Request instance for this specific retry."""
364 if isinstance(data, str) or data is None:
365 payload = data
366 else:
367 if isinstance(data, dict):
368 payload = data.items()
369 else:
370 payload = data[:]
371 payload.extend(extra.iteritems())
372 payload = urllib.urlencode(payload)
373
374 new_url = url
375 if isinstance(data, str) or data is None:
376 # In these cases, add the extra parameter to the query part of the url.
377 url_parts = list(urlparse.urlparse(new_url))
378 # Append the query parameter.
379 if url_parts[4] and extra:
380 url_parts[4] += '&'
381 url_parts[4] += urllib.urlencode(extra)
382 new_url = urlparse.urlunparse(url_parts)
383
384 request = urllib2.Request(new_url, data=payload)
385 if payload is not None:
386 if content_type:
387 request.add_header('Content-Type', content_type)
388 request.add_header('Content-Length', len(payload))
389 return request
390
391 return url_open_request(make_request, retry_404)
392
393
394def url_open_request(make_request, retry_404=False):
395 """Internal version of url_open() for users that need special handling.
396 """
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000397 last_error = None
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000398 for attempt in range(MAX_URL_OPEN_ATTEMPTS):
maruel@chromium.orgef333122013-03-12 20:36:40 +0000399 extra = {COUNT_KEY: attempt} if attempt else {}
400 request = make_request(extra)
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000401 try:
maruel@chromium.orgef333122013-03-12 20:36:40 +0000402 url_response = urllib2.urlopen(request)
maruel@chromium.orgf04becf2013-03-14 19:09:11 +0000403 logging.debug('url_open(%s) succeeded', request.get_full_url())
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000404 return url_response
405 except urllib2.HTTPError as e:
csharp@chromium.orge9c8d942013-03-11 20:48:36 +0000406 if e.code < 500 and not (retry_404 and e.code == 404):
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000407 # This HTTPError means we reached the server and there was a problem
408 # with the request, so don't retry.
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000409 logging.error(
410 'Able to connect to %s but an exception was thrown.\n%s\n%s',
411 request.get_full_url(), e, e.read())
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000412 return None
413
414 # The HTTPError was due to a server error, so retry the attempt.
415 logging.warning('Able to connect to %s on attempt %d.\nException: %s ',
maruel@chromium.orgef333122013-03-12 20:36:40 +0000416 request.get_full_url(), attempt, e)
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000417 last_error = e
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000418
419 except (urllib2.URLError, httplib.HTTPException) as e:
420 logging.warning('Unable to open url %s on attempt %d.\nException: %s',
maruel@chromium.orgef333122013-03-12 20:36:40 +0000421 request.get_full_url(), attempt, e)
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000422 last_error = e
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000423
424 # Only sleep if we are going to try again.
425 if attempt != MAX_URL_OPEN_ATTEMPTS - 1:
426 duration = random.random() * 3 + math.pow(1.5, (attempt + 1))
427 duration = min(10, max(0.1, duration))
428 time.sleep(duration)
429
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000430 logging.error('Unable to open given url, %s, after %d attempts.\n%s',
431 request.get_full_url(), MAX_URL_OPEN_ATTEMPTS, last_error)
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000432 return None
433
434
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000435class ThreadPool(object):
436 """Implements a multithreaded worker pool oriented for mapping jobs with
437 thread-local result storage.
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000438
439 Arguments:
440 - initial_threads: Number of threads to start immediately. Can be 0 if it is
441 uncertain that threads will be needed.
442 - max_threads: Maximum number of threads that will be started when all the
443 threads are busy working. Often the number of CPU cores.
444 - queue_size: Maximum number of tasks to buffer in the queue. 0 for unlimited
445 queue. A non-zero value may make add_task() blocking.
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000446 """
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000447 QUEUE_CLASS = Queue.PriorityQueue
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000448
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000449 def __init__(self, initial_threads, max_threads, queue_size):
450 logging.debug(
451 'ThreadPool(%d, %d, %d)', initial_threads, max_threads, queue_size)
452 assert initial_threads <= max_threads
453 # Update this check once 256 cores CPU are common.
454 assert max_threads <= 256
455
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000456 self.tasks = self.QUEUE_CLASS(queue_size)
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000457 self._max_threads = max_threads
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000458
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000459 # Mutables.
460 self._num_of_added_tasks_lock = threading.Lock()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000461 self._num_of_added_tasks = 0
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000462 self._outputs_exceptions_cond = threading.Condition()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000463 self._outputs = []
464 self._exceptions = []
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000465 # Number of threads in wait state.
466 self._ready_lock = threading.Lock()
467 self._ready = 0
468 self._workers_lock = threading.Lock()
469 self._workers = []
470 for _ in range(initial_threads):
471 self._add_worker()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000472
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000473 def _add_worker(self):
474 """Adds one worker thread if there isn't too many. Thread-safe."""
475 # Better to take the lock two times than hold it for too long.
476 with self._workers_lock:
477 if len(self._workers) >= self._max_threads:
478 return False
479 worker = threading.Thread(target=self._run)
480 with self._workers_lock:
481 if len(self._workers) >= self._max_threads:
482 return False
483 self._workers.append(worker)
484 worker.daemon = True
485 worker.start()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000486
maruel@chromium.org831958f2013-01-22 15:01:46 +0000487 def add_task(self, priority, func, *args, **kwargs):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000488 """Adds a task, a function to be executed by a worker.
489
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000490 |priority| can adjust the priority of the task versus others. Lower priority
maruel@chromium.org831958f2013-01-22 15:01:46 +0000491 takes precedence.
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000492
maruel@chromium.orgedd25d02013-03-26 14:38:00 +0000493 |func| can either return a return value to be added to the output list or
494 be a generator which can emit multiple values.
495
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000496 Returns the index of the item added, e.g. the total number of enqueued items
497 up to now.
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000498 """
maruel@chromium.org831958f2013-01-22 15:01:46 +0000499 assert isinstance(priority, int)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000500 assert callable(func)
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000501 with self._ready_lock:
502 start_new_worker = not self._ready
503 with self._num_of_added_tasks_lock:
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000504 self._num_of_added_tasks += 1
505 index = self._num_of_added_tasks
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000506 self.tasks.put((priority, index, func, args, kwargs))
507 if start_new_worker:
508 self._add_worker()
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000509 return index
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000510
511 def _run(self):
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000512 """Worker thread loop. Runs until a None task is queued."""
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000513 while True:
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000514 try:
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000515 with self._ready_lock:
516 self._ready += 1
517 task = self.tasks.get()
518 finally:
519 with self._ready_lock:
520 self._ready -= 1
521 try:
522 if task is None:
523 # We're done.
524 return
525 _priority, _index, func, args, kwargs = task
maruel@chromium.orgedd25d02013-03-26 14:38:00 +0000526 if inspect.isgeneratorfunction(func):
527 for out in func(*args, **kwargs):
528 self._output_append(out)
529 else:
530 out = func(*args, **kwargs)
531 self._output_append(out)
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000532 except Exception as e:
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000533 logging.warning('Caught exception: %s', e)
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000534 exc_info = sys.exc_info()
maruel@chromium.org97cd0be2013-03-13 14:01:36 +0000535 logging.info(''.join(traceback.format_tb(exc_info[2])))
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000536 self._outputs_exceptions_cond.acquire()
537 try:
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000538 self._exceptions.append(exc_info)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000539 self._outputs_exceptions_cond.notifyAll()
540 finally:
541 self._outputs_exceptions_cond.release()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000542 finally:
csharp@chromium.org60991182013-03-18 13:44:17 +0000543 try:
544 self.tasks.task_done()
545 except Exception as e:
546 # We need to catch and log this error here because this is the root
547 # function for the thread, nothing higher will catch the error.
548 logging.exception('Caught exception while marking task as done: %s',
549 e)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000550
maruel@chromium.orgedd25d02013-03-26 14:38:00 +0000551 def _output_append(self, out):
552 if out is not None:
553 self._outputs_exceptions_cond.acquire()
554 try:
555 self._outputs.append(out)
556 self._outputs_exceptions_cond.notifyAll()
557 finally:
558 self._outputs_exceptions_cond.release()
559
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000560 def join(self):
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000561 """Extracts all the results from each threads unordered.
562
563 Call repeatedly to extract all the exceptions if desired.
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000564
565 Note: will wait for all work items to be done before returning an exception.
566 To get an exception early, use get_one_result().
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000567 """
568 # TODO(maruel): Stop waiting as soon as an exception is caught.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000569 self.tasks.join()
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000570 self._outputs_exceptions_cond.acquire()
571 try:
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000572 if self._exceptions:
573 e = self._exceptions.pop(0)
574 raise e[0], e[1], e[2]
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000575 out = self._outputs
576 self._outputs = []
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000577 finally:
578 self._outputs_exceptions_cond.release()
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000579 return out
580
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000581 def get_one_result(self):
582 """Returns the next item that was generated or raises an exception if one
583 occured.
584
585 Warning: this function will hang if there is no work item left. Use join
586 instead.
587 """
588 self._outputs_exceptions_cond.acquire()
589 try:
590 while True:
591 if self._exceptions:
592 e = self._exceptions.pop(0)
593 raise e[0], e[1], e[2]
594 if self._outputs:
595 return self._outputs.pop(0)
596 self._outputs_exceptions_cond.wait()
597 finally:
598 self._outputs_exceptions_cond.release()
599
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000600 def close(self):
601 """Closes all the threads."""
602 for _ in range(len(self._workers)):
603 # Enqueueing None causes the worker to stop.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000604 self.tasks.put(None)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000605 for t in self._workers:
606 t.join()
607
608 def __enter__(self):
609 """Enables 'with' statement."""
610 return self
611
maruel@chromium.org97cd0be2013-03-13 14:01:36 +0000612 def __exit__(self, _exc_type, _exc_value, _traceback):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000613 """Enables 'with' statement."""
614 self.close()
615
616
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000617def valid_file(filepath, size):
618 """Determines if the given files appears valid (currently it just checks
619 the file's size)."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000620 if size == UNKNOWN_FILE_SIZE:
621 return True
622 actual_size = os.stat(filepath).st_size
623 if size != actual_size:
624 logging.warning(
625 'Found invalid item %s; %d != %d',
626 os.path.basename(filepath), actual_size, size)
627 return False
628 return True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000629
630
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000631class Profiler(object):
632 def __init__(self, name):
633 self.name = name
634 self.start_time = None
635
636 def __enter__(self):
637 self.start_time = time.time()
638 return self
639
640 def __exit__(self, _exc_type, _exec_value, _traceback):
641 time_taken = time.time() - self.start_time
642 logging.info('Profiling: Section %s took %3.3f seconds',
643 self.name, time_taken)
644
645
646class Remote(object):
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000647 """Priority based worker queue to fetch or upload files from a
648 content-address server. Any function may be given as the fetcher/upload,
649 as long as it takes two inputs (the item contents, and their relative
650 destination).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000651
652 Supports local file system, CIFS or http remotes.
653
654 When the priority of items is equals, works in strict FIFO mode.
655 """
656 # Initial and maximum number of worker threads.
657 INITIAL_WORKERS = 2
658 MAX_WORKERS = 16
659 # Priorities.
660 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
661 INTERNAL_PRIORITY_BITS = (1<<8) - 1
662 RETRIES = 5
663
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000664 def __init__(self, destination_root):
665 # Function to fetch a remote object or upload to a remote location..
666 self._do_item = self.get_file_handler(destination_root)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000667 # Contains tuple(priority, obj).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000668 self._done = Queue.PriorityQueue()
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000669 self._pool = ThreadPool(self.INITIAL_WORKERS, self.MAX_WORKERS, 0)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000670
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000671 def join(self):
672 """Blocks until the queue is empty."""
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000673 return self._pool.join()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000674
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000675 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000676 """Retrieves an object from the remote data store.
677
678 The smaller |priority| gets fetched first.
679
680 Thread-safe.
681 """
682 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000683 return self._add_item(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000684
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000685 def _add_item(self, priority, obj, dest, size):
686 assert isinstance(obj, basestring), obj
687 assert isinstance(dest, basestring), dest
688 assert size is None or isinstance(size, int), size
689 return self._pool.add_task(
690 priority, self._task_executer, priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000691
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000692 def get_one_result(self):
693 return self._pool.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000694
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000695 def _task_executer(self, priority, obj, dest, size):
696 """Wraps self._do_item to trap and retry on IOError exceptions."""
697 try:
698 self._do_item(obj, dest)
699 if size and not valid_file(dest, size):
700 download_size = os.stat(dest).st_size
701 os.remove(dest)
702 raise IOError('File incorrect size after download of %s. Got %s and '
703 'expected %s' % (obj, download_size, size))
704 # TODO(maruel): Technically, we'd want to have an output queue to be a
705 # PriorityQueue.
706 return obj
707 except IOError as e:
708 logging.debug('Caught IOError: %s', e)
709 # Retry a few times, lowering the priority.
710 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
711 self._add_item(priority + 1, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000712 return
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000713 raise
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000714
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +0000715 def get_file_handler(self, file_or_url): # pylint: disable=R0201
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000716 """Returns a object to retrieve objects from a remote."""
717 if re.match(r'^https?://.+$', file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000718 def download_file(item, dest):
719 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
720 # easy.
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000721 try:
csharp@chromium.orgaa2d1512012-12-05 21:17:39 +0000722 zipped_source = file_or_url + item
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000723 logging.debug('download_file(%s)', zipped_source)
csharp@chromium.orge9c8d942013-03-11 20:48:36 +0000724
725 # Because the app engine DB is only eventually consistent, retry
726 # 404 errors because the file might just not be visible yet (even
727 # though it has been uploaded).
728 connection = url_open(zipped_source, retry_404=True)
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000729 if not connection:
730 raise IOError('Unable to open connection to %s' % zipped_source)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000731 decompressor = zlib.decompressobj()
maruel@chromium.org3f039182012-11-27 21:32:41 +0000732 size = 0
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000733 with open(dest, 'wb') as f:
734 while True:
735 chunk = connection.read(ZIPPED_FILE_CHUNK)
736 if not chunk:
737 break
maruel@chromium.org3f039182012-11-27 21:32:41 +0000738 size += len(chunk)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000739 f.write(decompressor.decompress(chunk))
740 # Ensure that all the data was properly decompressed.
741 uncompressed_data = decompressor.flush()
742 assert not uncompressed_data
csharp@chromium.org549669e2013-01-22 19:48:17 +0000743 except IOError:
744 logging.error('Encountered an exception with (%s, %s)' % (item, dest))
745 raise
csharp@chromium.orga110d792013-01-07 16:16:16 +0000746 except httplib.HTTPException as e:
747 raise IOError('Encountered an HTTPException.\n%s' % e)
csharp@chromium.org186d6232012-11-26 14:36:12 +0000748 except zlib.error as e:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000749 # Log the first bytes to see if it's uncompressed data.
750 logging.warning('%r', e[:512])
maruel@chromium.org3f039182012-11-27 21:32:41 +0000751 raise IOError(
752 'Problem unzipping data for item %s. Got %d bytes.\n%s' %
753 (item, size, e))
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000754
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000755 return download_file
756
757 def copy_file(item, dest):
758 source = os.path.join(file_or_url, item)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000759 if source == dest:
760 logging.info('Source and destination are the same, no action required')
761 return
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000762 logging.debug('copy_file(%s, %s)', source, dest)
763 shutil.copy(source, dest)
764 return copy_file
765
766
767class CachePolicies(object):
768 def __init__(self, max_cache_size, min_free_space, max_items):
769 """
770 Arguments:
771 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
772 cache is effectively a leak.
773 - min_free_space: Trim if disk free space becomes lower than this value. If
774 0, it unconditionally fill the disk.
775 - max_items: Maximum number of items to keep in the cache. If 0, do not
776 enforce a limit.
777 """
778 self.max_cache_size = max_cache_size
779 self.min_free_space = min_free_space
780 self.max_items = max_items
781
782
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000783class NoCache(object):
784 """This class is intended to be usable everywhere the Cache class is.
785 Instead of downloading to a cache, all files are downloaded to the target
786 directory and then moved to where they are needed.
787 """
788
789 def __init__(self, target_directory, remote):
790 self.target_directory = target_directory
791 self.remote = remote
792
793 def retrieve(self, priority, item, size):
794 """Get the request file."""
795 self.remote.add_item(priority, item, self.path(item), size)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000796 self.remote.get_one_result()
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000797
798 def wait_for(self, items):
799 """Download the first item of the given list if it is missing."""
800 item = items.iterkeys().next()
801
802 if not os.path.exists(self.path(item)):
803 self.remote.add_item(Remote.MED, item, self.path(item), UNKNOWN_FILE_SIZE)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000804 downloaded = self.remote.get_one_result()
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000805 assert downloaded == item
806
807 return item
808
809 def path(self, item):
810 return os.path.join(self.target_directory, item)
811
812
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000813class Cache(object):
814 """Stateful LRU cache.
815
816 Saves its state as json file.
817 """
818 STATE_FILE = 'state.json'
819
820 def __init__(self, cache_dir, remote, policies):
821 """
822 Arguments:
823 - cache_dir: Directory where to place the cache.
824 - remote: Remote where to fetch items from.
825 - policies: cache retention policies.
826 """
827 self.cache_dir = cache_dir
828 self.remote = remote
829 self.policies = policies
830 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
831 # The tuple(file, size) are kept as an array in a LRU style. E.g.
832 # self.state[0] is the oldest item.
833 self.state = []
maruel@chromium.org770993b2012-12-11 17:16:48 +0000834 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000835 # A lookup map to speed up searching.
836 self._lookup = {}
maruel@chromium.org770993b2012-12-11 17:16:48 +0000837 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000838
839 # Items currently being fetched. Keep it local to reduce lock contention.
840 self._pending_queue = set()
841
842 # Profiling values.
843 self._added = []
844 self._removed = []
845 self._free_disk = 0
846
maruel@chromium.org770993b2012-12-11 17:16:48 +0000847 with Profiler('Setup'):
848 if not os.path.isdir(self.cache_dir):
849 os.makedirs(self.cache_dir)
850 if os.path.isfile(self.state_file):
851 try:
852 self.state = json.load(open(self.state_file, 'r'))
853 except (IOError, ValueError), e:
854 # Too bad. The file will be overwritten and the cache cleared.
855 logging.error(
856 'Broken state file %s, ignoring.\n%s' % (self.STATE_FILE, e))
857 self._state_need_to_be_saved = True
858 if (not isinstance(self.state, list) or
859 not all(
860 isinstance(i, (list, tuple)) and len(i) == 2
861 for i in self.state)):
862 # Discard.
863 self._state_need_to_be_saved = True
864 self.state = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000865
maruel@chromium.org770993b2012-12-11 17:16:48 +0000866 # Ensure that all files listed in the state still exist and add new ones.
867 previous = set(filename for filename, _ in self.state)
868 if len(previous) != len(self.state):
869 logging.warn('Cache state is corrupted, found duplicate files')
870 self._state_need_to_be_saved = True
871 self.state = []
872
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000873 added = 0
874 for filename in os.listdir(self.cache_dir):
875 if filename == self.STATE_FILE:
876 continue
877 if filename in previous:
878 previous.remove(filename)
879 continue
880 # An untracked file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000881 if not RE_IS_SHA1.match(filename):
882 logging.warn('Removing unknown file %s from cache', filename)
883 os.remove(self.path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000884 continue
885 # Insert as the oldest file. It will be deleted eventually if not
886 # accessed.
887 self._add(filename, False)
888 logging.warn('Add unknown file %s to cache', filename)
889 added += 1
890
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000891 if added:
892 logging.warn('Added back %d unknown files', added)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000893 if previous:
894 logging.warn('Removed %d lost files', len(previous))
895 # Set explicitly in case self._add() wasn't called.
896 self._state_need_to_be_saved = True
897 # Filter out entries that were not found while keeping the previous
898 # order.
899 self.state = [
900 (filename, size) for filename, size in self.state
901 if filename not in previous
902 ]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000903 self.trim()
904
905 def __enter__(self):
906 return self
907
908 def __exit__(self, _exc_type, _exec_value, _traceback):
909 with Profiler('CleanupTrimming'):
910 self.trim()
911
912 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000913 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000914 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000915 '%5d (%8dkb) current',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000916 len(self.state),
917 sum(i[1] for i in self.state) / 1024)
918 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000919 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
920 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000921
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000922 def remove_file_at_index(self, index):
923 """Removes the file at the given index."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000924 try:
maruel@chromium.org770993b2012-12-11 17:16:48 +0000925 self._state_need_to_be_saved = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000926 filename, size = self.state.pop(index)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000927 # If the lookup was already stale, its possible the filename was not
928 # present yet.
929 self._lookup_is_stale = True
930 self._lookup.pop(filename, None)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000931 self._removed.append(size)
932 os.remove(self.path(filename))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000933 except OSError as e:
934 logging.error('Error attempting to delete a file\n%s' % e)
935
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000936 def remove_lru_file(self):
937 """Removes the last recently used file."""
938 self.remove_file_at_index(0)
939
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000940 def trim(self):
941 """Trims anything we don't know, make sure enough free space exists."""
942 # Ensure maximum cache size.
943 if self.policies.max_cache_size and self.state:
944 while sum(i[1] for i in self.state) > self.policies.max_cache_size:
945 self.remove_lru_file()
946
947 # Ensure maximum number of items in the cache.
948 if self.policies.max_items and self.state:
949 while len(self.state) > self.policies.max_items:
950 self.remove_lru_file()
951
952 # Ensure enough free space.
953 self._free_disk = get_free_space(self.cache_dir)
954 while (
955 self.policies.min_free_space and
956 self.state and
957 self._free_disk < self.policies.min_free_space):
958 self.remove_lru_file()
959 self._free_disk = get_free_space(self.cache_dir)
960
961 self.save()
962
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000963 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000964 """Retrieves a file from the remote, if not already cached, and adds it to
965 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000966
967 If the file is in the cache, verifiy that the file is valid (i.e. it is
968 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000969 """
970 assert not '/' in item
971 path = self.path(item)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000972 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000973 index = self._lookup.get(item)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000974
975 if index is not None:
976 if not valid_file(self.path(item), size):
977 self.remove_file_at_index(index)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000978 index = None
979 else:
980 assert index < len(self.state)
981 # Was already in cache. Update it's LRU value by putting it at the end.
maruel@chromium.org770993b2012-12-11 17:16:48 +0000982 self._state_need_to_be_saved = True
983 self._lookup_is_stale = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000984 self.state.append(self.state.pop(index))
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000985
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000986 if index is None:
987 if item in self._pending_queue:
988 # Already pending. The same object could be referenced multiple times.
989 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000990 self.remote.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000991 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000992
993 def add(self, filepath, obj):
994 """Forcibly adds a file to the cache."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000995 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000996 if not obj in self._lookup:
997 link_file(self.path(obj), filepath, HARDLINK)
998 self._add(obj, True)
999
1000 def path(self, item):
1001 """Returns the path to one item."""
1002 return os.path.join(self.cache_dir, item)
1003
1004 def save(self):
1005 """Saves the LRU ordering."""
maruel@chromium.org770993b2012-12-11 17:16:48 +00001006 if self._state_need_to_be_saved:
1007 json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':'))
1008 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001009
1010 def wait_for(self, items):
1011 """Starts a loop that waits for at least one of |items| to be retrieved.
1012
1013 Returns the first item retrieved.
1014 """
1015 # Flush items already present.
maruel@chromium.org770993b2012-12-11 17:16:48 +00001016 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001017 for item in items:
1018 if item in self._lookup:
1019 return item
1020
1021 assert all(i in self._pending_queue for i in items), (
1022 items, self._pending_queue)
1023 # Note that:
1024 # len(self._pending_queue) ==
1025 # ( len(self.remote._workers) - self.remote._ready +
1026 # len(self._remote._queue) + len(self._remote.done))
1027 # There is no lock-free way to verify that.
1028 while self._pending_queue:
maruel@chromium.org13eca0b2013-01-22 16:42:21 +00001029 item = self.remote.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001030 self._pending_queue.remove(item)
1031 self._add(item, True)
1032 if item in items:
1033 return item
1034
1035 def _add(self, item, at_end):
1036 """Adds an item in the internal state.
1037
1038 If |at_end| is False, self._lookup becomes inconsistent and
1039 self._update_lookup() must be called.
1040 """
1041 size = os.stat(self.path(item)).st_size
1042 self._added.append(size)
maruel@chromium.org770993b2012-12-11 17:16:48 +00001043 self._state_need_to_be_saved = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001044 if at_end:
1045 self.state.append((item, size))
1046 self._lookup[item] = len(self.state) - 1
1047 else:
maruel@chromium.org770993b2012-12-11 17:16:48 +00001048 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001049 self.state.insert(0, (item, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001050
1051 def _update_lookup(self):
maruel@chromium.org770993b2012-12-11 17:16:48 +00001052 if self._lookup_is_stale:
1053 self._lookup = dict(
1054 (filename, index) for index, (filename, _) in enumerate(self.state))
1055 self._lookup_is_stale = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001056
1057
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001058class IsolatedFile(object):
1059 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001060 def __init__(self, obj_hash):
1061 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001062 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001063 self.obj_hash = obj_hash
1064 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001065 # .isolate and all the .isolated files recursively included by it with
1066 # 'includes' key. The order of each sha-1 in 'includes', each representing a
1067 # .isolated file in the hash table, is important, as the later ones are not
1068 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001069 self.can_fetch = False
1070
1071 # Raw data.
1072 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001073 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001074 self.children = []
1075
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001076 # Set once the .isolated file is loaded.
1077 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001078 # Set once the files are fetched.
1079 self.files_fetched = False
1080
1081 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001082 """Verifies the .isolated file is valid and loads this object with the json
1083 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001084 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001085 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
1086 assert not self._is_parsed
1087 self.data = load_isolated(content)
1088 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
1089 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001090
1091 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001092 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001093
1094 Preemptively request files.
1095
1096 Note that |files| is modified by this function.
1097 """
1098 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001099 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001100 return
1101 logging.debug('fetch_files(%s)' % self.obj_hash)
1102 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001103 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001104 # overriden files must not be fetched.
1105 if filepath not in files:
1106 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001107 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001108 # Preemptively request files.
1109 logging.debug('fetching %s' % filepath)
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001110 cache.retrieve(Remote.MED, properties['h'], properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001111 self.files_fetched = True
1112
1113
1114class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001115 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001116 def __init__(self):
1117 self.command = []
1118 self.files = {}
1119 self.read_only = None
1120 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001121 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001122 self.root = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001123
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001124 def load(self, cache, root_isolated_hash):
1125 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001126
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001127 It enables support for "included" .isolated files. They are processed in
1128 strict order but fetched asynchronously from the cache. This is important so
1129 that a file in an included .isolated file that is overridden by an embedding
1130 .isolated file is not fetched neededlessly. The includes are fetched in one
1131 pass and the files are fetched as soon as all the ones on the left-side
1132 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001133
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001134 The prioritization is very important here for nested .isolated files.
1135 'includes' have the highest priority and the algorithm is optimized for both
1136 deep and wide trees. A deep one is a long link of .isolated files referenced
1137 one at a time by one item in 'includes'. A wide one has a large number of
1138 'includes' in a single .isolated file. 'left' is defined as an included
1139 .isolated file earlier in the 'includes' list. So the order of the elements
1140 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001141 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001142 self.root = IsolatedFile(root_isolated_hash)
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001143 cache.retrieve(Remote.HIGH, root_isolated_hash, UNKNOWN_FILE_SIZE)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001144 pending = {root_isolated_hash: self.root}
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001145 # Keeps the list of retrieved items to refuse recursive includes.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001146 retrieved = [root_isolated_hash]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001147
1148 def update_self(node):
1149 node.fetch_files(cache, self.files)
1150 # Grabs properties.
1151 if not self.command and node.data.get('command'):
1152 self.command = node.data['command']
1153 if self.read_only is None and node.data.get('read_only') is not None:
1154 self.read_only = node.data['read_only']
1155 if (self.relative_cwd is None and
1156 node.data.get('relative_cwd') is not None):
1157 self.relative_cwd = node.data['relative_cwd']
1158
1159 def traverse_tree(node):
1160 if node.can_fetch:
1161 if not node.files_fetched:
1162 update_self(node)
1163 will_break = False
1164 for i in node.children:
1165 if not i.can_fetch:
1166 if will_break:
1167 break
1168 # Automatically mark the first one as fetcheable.
1169 i.can_fetch = True
1170 will_break = True
1171 traverse_tree(i)
1172
1173 while pending:
1174 item_hash = cache.wait_for(pending)
1175 item = pending.pop(item_hash)
1176 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001177 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001178 # It's the root item.
1179 item.can_fetch = True
1180
1181 for new_child in item.children:
1182 h = new_child.obj_hash
1183 if h in retrieved:
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001184 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001185 pending[h] = new_child
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001186 cache.retrieve(Remote.HIGH, h, UNKNOWN_FILE_SIZE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001187
1188 # Traverse the whole tree to see if files can now be fetched.
1189 traverse_tree(self.root)
1190 def check(n):
1191 return all(check(x) for x in n.children) and n.files_fetched
1192 assert check(self.root)
1193 self.relative_cwd = self.relative_cwd or ''
1194 self.read_only = self.read_only or False
1195
1196
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001197def create_directories(base_directory, files):
1198 """Creates the directory structure needed by the given list of files."""
1199 logging.debug('create_directories(%s, %d)', base_directory, len(files))
1200 # Creates the tree of directories to create.
1201 directories = set(os.path.dirname(f) for f in files)
1202 for item in list(directories):
1203 while item:
1204 directories.add(item)
1205 item = os.path.dirname(item)
1206 for d in sorted(directories):
1207 if d:
1208 os.mkdir(os.path.join(base_directory, d))
1209
1210
1211def create_links(base_directory, files):
1212 """Creates any links needed by the given set of files."""
1213 for filepath, properties in files:
1214 if 'link' not in properties:
1215 continue
1216 outfile = os.path.join(base_directory, filepath)
1217 # symlink doesn't exist on Windows. So the 'link' property should
1218 # never be specified for windows .isolated file.
1219 os.symlink(properties['l'], outfile) # pylint: disable=E1101
1220 if 'm' in properties:
1221 lchmod = getattr(os, 'lchmod', None)
1222 if lchmod:
1223 lchmod(outfile, properties['m'])
1224
1225
1226def setup_commands(base_directory, cwd, cmd):
1227 """Correctly adjusts and then returns the required working directory
1228 and command needed to run the test.
1229 """
1230 assert not os.path.isabs(cwd), 'The cwd must be a relative path, got %s' % cwd
1231 cwd = os.path.join(base_directory, cwd)
1232 if not os.path.isdir(cwd):
1233 os.makedirs(cwd)
1234
1235 # Ensure paths are correctly separated on windows.
1236 cmd[0] = cmd[0].replace('/', os.path.sep)
1237 cmd = fix_python_path(cmd)
1238
1239 return cwd, cmd
1240
1241
1242def generate_remaining_files(files):
1243 """Generates a dictionary of all the remaining files to be downloaded."""
1244 remaining = {}
1245 for filepath, props in files:
1246 if 'h' in props:
1247 remaining.setdefault(props['h'], []).append((filepath, props))
1248
1249 return remaining
1250
1251
1252def download_test_data(isolated_hash, target_directory, remote):
1253 """Downloads the dependencies to the given directory."""
1254 if not os.path.exists(target_directory):
1255 os.makedirs(target_directory)
1256
1257 settings = Settings()
1258 no_cache = NoCache(target_directory, Remote(remote))
1259
1260 # Download all the isolated files.
1261 with Profiler('GetIsolateds') as _prof:
1262 settings.load(no_cache, isolated_hash)
1263
1264 if not settings.command:
1265 print >> sys.stderr, 'No command to run'
1266 return 1
1267
1268 with Profiler('GetRest') as _prof:
1269 create_directories(target_directory, settings.files)
1270 create_links(target_directory, settings.files.iteritems())
1271
1272 cwd, cmd = setup_commands(target_directory, settings.relative_cwd,
1273 settings.command[:])
1274
1275 remaining = generate_remaining_files(settings.files.iteritems())
1276
1277 # Now block on the remaining files to be downloaded and mapped.
1278 logging.info('Retrieving remaining files')
1279 last_update = time.time()
1280 while remaining:
1281 obj = no_cache.wait_for(remaining)
1282 files = remaining.pop(obj)
1283
1284 for i, (filepath, properties) in enumerate(files):
1285 outfile = os.path.join(target_directory, filepath)
1286 logging.info(no_cache.path(obj))
1287
1288 if i + 1 == len(files):
1289 os.rename(no_cache.path(obj), outfile)
1290 else:
1291 shutil.copyfile(no_cache.path(obj), outfile)
1292
1293 if 'm' in properties:
1294 # It's not set on Windows.
1295 os.chmod(outfile, properties['m'])
1296
1297 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
1298 logging.info('%d files remaining...' % len(remaining))
1299 last_update = time.time()
1300
1301 print('.isolated files successfully downloaded and setup in %s' %
1302 target_directory)
1303 print('To run this test please run the command %s from the directory %s' %
1304 (cmd, cwd))
1305
1306 return 0
1307
1308
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001309def run_tha_test(isolated_hash, cache_dir, remote, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001310 """Downloads the dependencies in the cache, hardlinks them into a temporary
1311 directory and runs the executable.
1312 """
1313 settings = Settings()
1314 with Cache(cache_dir, Remote(remote), policies) as cache:
1315 outdir = make_temp_dir('run_tha_test', cache_dir)
1316 try:
1317 # Initiate all the files download.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001318 with Profiler('GetIsolateds') as _prof:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001319 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001320 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001321 # Adds it in the cache. While not strictly necessary, this simplifies
1322 # the rest.
maruel@chromium.orgcb3c3d52013-03-14 18:55:30 +00001323 h = hashlib.sha1(open(isolated_hash, 'rb').read()).hexdigest()
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001324 cache.add(isolated_hash, h)
1325 isolated_hash = h
1326 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001327
1328 if not settings.command:
1329 print >> sys.stderr, 'No command to run'
1330 return 1
1331
1332 with Profiler('GetRest') as _prof:
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001333 create_directories(outdir, settings.files)
1334 create_links(outdir, settings.files.iteritems())
1335 remaining = generate_remaining_files(settings.files.iteritems())
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001336
1337 # Do bookkeeping while files are being downloaded in the background.
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001338 cwd, cmd = setup_commands(outdir, settings.relative_cwd,
1339 settings.command[:])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001340
1341 # Now block on the remaining files to be downloaded and mapped.
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001342 logging.info('Retrieving remaining files')
1343 last_update = time.time()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001344 while remaining:
1345 obj = cache.wait_for(remaining)
1346 for filepath, properties in remaining.pop(obj):
1347 outfile = os.path.join(outdir, filepath)
1348 link_file(outfile, cache.path(obj), HARDLINK)
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001349 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001350 # It's not set on Windows.
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001351 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001352
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001353 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
1354 logging.info('%d files remaining...' % len(remaining))
1355 last_update = time.time()
1356
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001357 if settings.read_only:
1358 make_writable(outdir, True)
1359 logging.info('Running %s, cwd=%s' % (cmd, cwd))
csharp@chromium.orge217f302012-11-22 16:51:53 +00001360
1361 # TODO(csharp): This should be specified somewhere else.
1362 # Add a rotating log file if one doesn't already exist.
1363 env = os.environ.copy()
1364 env.setdefault('RUN_TEST_CASES_LOG_FILE', RUN_TEST_CASES_LOG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001365 try:
1366 with Profiler('RunTest') as _prof:
csharp@chromium.orge217f302012-11-22 16:51:53 +00001367 return subprocess.call(cmd, cwd=cwd, env=env)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001368 except OSError:
1369 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1370 raise
1371 finally:
1372 rmtree(outdir)
1373
1374
1375def main():
maruel@chromium.org46e61cc2013-03-25 19:55:34 +00001376 disable_buffering()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001377 parser = optparse.OptionParser(
1378 usage='%prog <options>', description=sys.modules[__name__].__doc__)
1379 parser.add_option(
1380 '-v', '--verbose', action='count', default=0, help='Use multiple times')
1381 parser.add_option('--no-run', action='store_true', help='Skip the run part')
1382
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001383 group = optparse.OptionGroup(parser, 'Download')
1384 group.add_option(
1385 '--download', metavar='DEST',
1386 help='Downloads files to DEST and returns without running, instead of '
1387 'downloading and then running from a temporary directory.')
1388 parser.add_option_group(group)
1389
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001390 group = optparse.OptionGroup(parser, 'Data source')
1391 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001392 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001393 metavar='FILE',
1394 help='File/url describing what to map or run')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001395 # TODO(maruel): Remove once not used anymore.
1396 group.add_option(
1397 '-m', '--manifest', dest='isolated', help=optparse.SUPPRESS_HELP)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001398 group.add_option(
1399 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001400 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001401 parser.add_option_group(group)
1402
1403 group.add_option(
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001404 '-r', '--remote', metavar='URL',
1405 default=
1406 'https://isolateserver.appspot.com/content/retrieve/default-gzip/',
1407 help='Remote where to get the items. Defaults to %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001408 group = optparse.OptionGroup(parser, 'Cache management')
1409 group.add_option(
1410 '--cache',
1411 default='cache',
1412 metavar='DIR',
1413 help='Cache directory, default=%default')
1414 group.add_option(
1415 '--max-cache-size',
1416 type='int',
1417 metavar='NNN',
1418 default=20*1024*1024*1024,
1419 help='Trim if the cache gets larger than this value, default=%default')
1420 group.add_option(
1421 '--min-free-space',
1422 type='int',
1423 metavar='NNN',
1424 default=1*1024*1024*1024,
1425 help='Trim if disk free space becomes lower than this value, '
1426 'default=%default')
1427 group.add_option(
1428 '--max-items',
1429 type='int',
1430 metavar='NNN',
1431 default=100000,
1432 help='Trim if more than this number of items are in the cache '
1433 'default=%default')
1434 parser.add_option_group(group)
1435
1436 options, args = parser.parse_args()
1437 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
csharp@chromium.orgff2a4662012-11-21 20:49:32 +00001438
1439 logging_console = logging.StreamHandler()
1440 logging_console.setFormatter(logging.Formatter(
1441 '%(levelname)5s %(module)15s(%(lineno)3d): %(message)s'))
1442 logging_console.setLevel(level)
1443 logging.getLogger().addHandler(logging_console)
1444
1445 logging_rotating_file = logging.handlers.RotatingFileHandler(
1446 RUN_ISOLATED_LOG_FILE,
1447 maxBytes=10 * 1024 * 1024, backupCount=5)
1448 logging_rotating_file.setLevel(logging.DEBUG)
1449 logging_rotating_file.setFormatter(logging.Formatter(
1450 '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)3d): %(message)s'))
1451 logging.getLogger().addHandler(logging_rotating_file)
1452
1453 logging.getLogger().setLevel(logging.DEBUG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001454
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001455 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001456 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001457 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001458 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001459 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001460 parser.error('Unsupported args %s' % ' '.join(args))
1461
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001462 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001463 policies = CachePolicies(
1464 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001465
1466 if options.download:
1467 return download_test_data(options.isolated or options.hash,
1468 options.download, options.remote)
1469 else:
1470 try:
1471 return run_tha_test(
1472 options.isolated or options.hash,
1473 options.cache,
1474 options.remote,
1475 policies)
1476 except Exception, e:
1477 # Make sure any exception is logged.
1478 logging.exception(e)
1479 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001480
1481
1482if __name__ == '__main__':
1483 sys.exit(main())