blob: e5df3ecb15fad56be7321875fd6d6ac13e15dea6 [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
csharp@chromium.orgbfb98742013-03-26 20:28:36 +000016import locale
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000017import logging
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000018import logging.handlers
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000019import math
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000020import optparse
21import os
22import Queue
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000023import random
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000024import re
25import shutil
26import stat
27import subprocess
28import sys
29import tempfile
30import threading
31import time
maruel@chromium.org97cd0be2013-03-13 14:01:36 +000032import traceback
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000033import urllib
csharp@chromium.orga92403f2012-11-20 15:13:59 +000034import urllib2
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000035import urlparse
csharp@chromium.orga92403f2012-11-20 15:13:59 +000036import zlib
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000037
38
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000039# Types of action accepted by link_file().
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000040HARDLINK, SYMLINK, COPY = range(1, 4)
41
42RE_IS_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$')
43
csharp@chromium.org8dc52542012-11-08 20:29:55 +000044# The file size to be used when we don't know the correct file size,
45# generally used for .isolated files.
46UNKNOWN_FILE_SIZE = None
47
csharp@chromium.orga92403f2012-11-20 15:13:59 +000048# The size of each chunk to read when downloading and unzipping files.
49ZIPPED_FILE_CHUNK = 16 * 1024
50
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000051# The name of the log file to use.
52RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
53
csharp@chromium.orge217f302012-11-22 16:51:53 +000054# The base directory containing this file.
55BASE_DIR = os.path.dirname(os.path.abspath(__file__))
56
57# The name of the log to use for the run_test_cases.py command
58RUN_TEST_CASES_LOG = os.path.join(BASE_DIR, 'run_test_cases.log')
59
csharp@chromium.org9c59ff12012-12-12 02:32:29 +000060# The delay (in seconds) to wait between logging statements when retrieving
61# the required files. This is intended to let the user (or buildbot) know that
62# the program is still running.
63DELAY_BETWEEN_UPDATES_IN_SECS = 30
64
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000065# The name of the key to store the count of url attempts.
66COUNT_KEY = 'UrlOpenAttempt'
67
68# The maximum number of attempts to trying opening a url before aborting.
69MAX_URL_OPEN_ATTEMPTS = 20
70
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000071
72class ConfigError(ValueError):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +000073 """Generic failure to load a .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000074 pass
75
76
77class MappingError(OSError):
78 """Failed to recreate the tree."""
79 pass
80
81
82def get_flavor():
83 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
84 flavors = {
85 'cygwin': 'win',
86 'win32': 'win',
87 'darwin': 'mac',
88 'sunos5': 'solaris',
89 'freebsd7': 'freebsd',
90 'freebsd8': 'freebsd',
91 }
92 return flavors.get(sys.platform, 'linux')
93
94
csharp@chromium.orgbfb98742013-03-26 20:28:36 +000095def fix_default_encoding():
96 """Forces utf8 solidly on all platforms.
97
98 By default python execution environment is lazy and defaults to ascii
99 encoding.
100
101 http://uucode.com/blog/2007/03/23/shut-up-you-dummy-7-bit-python/
102 """
103 if sys.getdefaultencoding() == 'utf-8':
104 return False
105
106 # Regenerate setdefaultencoding.
107 reload(sys)
108 # Module 'sys' has no 'setdefaultencoding' member
109 # pylint: disable=E1101
110 sys.setdefaultencoding('utf-8')
111 for attr in dir(locale):
112 if attr[0:3] != 'LC_':
113 continue
114 aref = getattr(locale, attr)
115 try:
116 locale.setlocale(aref, '')
117 except locale.Error:
118 continue
119 try:
120 lang = locale.getlocale(aref)[0]
121 except (TypeError, ValueError):
122 continue
123 if lang:
124 try:
125 locale.setlocale(aref, (lang, 'UTF-8'))
126 except locale.Error:
127 os.environ[attr] = lang + '.UTF-8'
128 try:
129 locale.setlocale(locale.LC_ALL, '')
130 except locale.Error:
131 pass
132 return True
133
134
maruel@chromium.org46e61cc2013-03-25 19:55:34 +0000135class Unbuffered(object):
136 """Disable buffering on a file object."""
137 def __init__(self, stream):
138 self.stream = stream
139
140 def write(self, data):
141 self.stream.write(data)
142 if '\n' in data:
143 self.stream.flush()
144
145 def __getattr__(self, attr):
146 return getattr(self.stream, attr)
147
148
149def disable_buffering():
150 """Makes this process and child processes stdout unbuffered."""
151 if not os.environ.get('PYTHONUNBUFFERED'):
152 # Since sys.stdout is a C++ object, it's impossible to do
153 # sys.stdout.write = lambda...
154 sys.stdout = Unbuffered(sys.stdout)
155 os.environ['PYTHONUNBUFFERED'] = 'x'
156
157
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000158def os_link(source, link_name):
159 """Add support for os.link() on Windows."""
160 if sys.platform == 'win32':
161 if not ctypes.windll.kernel32.CreateHardLinkW(
162 unicode(link_name), unicode(source), 0):
163 raise OSError()
164 else:
165 os.link(source, link_name)
166
167
168def readable_copy(outfile, infile):
169 """Makes a copy of the file that is readable by everyone."""
170 shutil.copy(infile, outfile)
171 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
172 stat.S_IRGRP | stat.S_IROTH)
173 os.chmod(outfile, read_enabled_mode)
174
175
176def link_file(outfile, infile, action):
177 """Links a file. The type of link depends on |action|."""
178 logging.debug('Mapping %s to %s' % (infile, outfile))
179 if action not in (HARDLINK, SYMLINK, COPY):
180 raise ValueError('Unknown mapping action %s' % action)
181 if not os.path.isfile(infile):
182 raise MappingError('%s is missing' % infile)
183 if os.path.isfile(outfile):
184 raise MappingError(
185 '%s already exist; insize:%d; outsize:%d' %
186 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
187
188 if action == COPY:
189 readable_copy(outfile, infile)
190 elif action == SYMLINK and sys.platform != 'win32':
191 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000192 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000193 else:
194 try:
195 os_link(infile, outfile)
196 except OSError:
197 # Probably a different file system.
198 logging.warn(
199 'Failed to hardlink, failing back to copy %s to %s' % (
200 infile, outfile))
201 readable_copy(outfile, infile)
202
203
204def _set_write_bit(path, read_only):
205 """Sets or resets the executable bit on a file or directory."""
206 mode = os.lstat(path).st_mode
207 if read_only:
208 mode = mode & 0500
209 else:
210 mode = mode | 0200
211 if hasattr(os, 'lchmod'):
212 os.lchmod(path, mode) # pylint: disable=E1101
213 else:
214 if stat.S_ISLNK(mode):
215 # Skip symlink without lchmod() support.
216 logging.debug('Can\'t change +w bit on symlink %s' % path)
217 return
218
219 # TODO(maruel): Implement proper DACL modification on Windows.
220 os.chmod(path, mode)
221
222
223def make_writable(root, read_only):
224 """Toggle the writable bit on a directory tree."""
csharp@chromium.org837352f2013-01-17 21:17:03 +0000225 assert os.path.isabs(root), root
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000226 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
227 for filename in filenames:
228 _set_write_bit(os.path.join(dirpath, filename), read_only)
229
230 for dirname in dirnames:
231 _set_write_bit(os.path.join(dirpath, dirname), read_only)
232
233
234def rmtree(root):
235 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
236 make_writable(root, False)
237 if sys.platform == 'win32':
238 for i in range(3):
239 try:
240 shutil.rmtree(root)
241 break
242 except WindowsError: # pylint: disable=E0602
243 delay = (i+1)*2
244 print >> sys.stderr, (
245 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
246 time.sleep(delay)
247 else:
248 shutil.rmtree(root)
249
250
251def is_same_filesystem(path1, path2):
252 """Returns True if both paths are on the same filesystem.
253
254 This is required to enable the use of hardlinks.
255 """
256 assert os.path.isabs(path1), path1
257 assert os.path.isabs(path2), path2
258 if sys.platform == 'win32':
259 # If the drive letter mismatches, assume it's a separate partition.
260 # TODO(maruel): It should look at the underlying drive, a drive letter could
261 # be a mount point to a directory on another drive.
262 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
263 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
264 if path1[0].lower() != path2[0].lower():
265 return False
266 return os.stat(path1).st_dev == os.stat(path2).st_dev
267
268
269def get_free_space(path):
270 """Returns the number of free bytes."""
271 if sys.platform == 'win32':
272 free_bytes = ctypes.c_ulonglong(0)
273 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
274 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
275 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000276 # For OSes other than Windows.
277 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000278 return f.f_bfree * f.f_frsize
279
280
281def make_temp_dir(prefix, root_dir):
282 """Returns a temporary directory on the same file system as root_dir."""
283 base_temp_dir = None
284 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
285 base_temp_dir = os.path.dirname(root_dir)
286 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
287
288
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000289def load_isolated(content):
290 """Verifies the .isolated file is valid and loads this object with the json
291 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000292 """
293 try:
294 data = json.loads(content)
295 except ValueError:
296 raise ConfigError('Failed to parse: %s...' % content[:100])
297
298 if not isinstance(data, dict):
299 raise ConfigError('Expected dict, got %r' % data)
300
301 for key, value in data.iteritems():
302 if key == 'command':
303 if not isinstance(value, list):
304 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000305 if not value:
306 raise ConfigError('Expected non-empty command')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000307 for subvalue in value:
308 if not isinstance(subvalue, basestring):
309 raise ConfigError('Expected string, got %r' % subvalue)
310
311 elif key == 'files':
312 if not isinstance(value, dict):
313 raise ConfigError('Expected dict, got %r' % value)
314 for subkey, subvalue in value.iteritems():
315 if not isinstance(subkey, basestring):
316 raise ConfigError('Expected string, got %r' % subkey)
317 if not isinstance(subvalue, dict):
318 raise ConfigError('Expected dict, got %r' % subvalue)
319 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000320 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000321 if not isinstance(subsubvalue, basestring):
322 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000323 elif subsubkey == 'm':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000324 if not isinstance(subsubvalue, int):
325 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000326 elif subsubkey == 'h':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000327 if not RE_IS_SHA1.match(subsubvalue):
328 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000329 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000330 if not isinstance(subsubvalue, int):
331 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000332 else:
333 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000334 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000335 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000336 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
337 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000338
339 elif key == 'includes':
340 if not isinstance(value, list):
341 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000342 if not value:
343 raise ConfigError('Expected non-empty includes list')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000344 for subvalue in value:
345 if not RE_IS_SHA1.match(subvalue):
346 raise ConfigError('Expected sha-1, got %r' % subvalue)
347
348 elif key == 'read_only':
349 if not isinstance(value, bool):
350 raise ConfigError('Expected bool, got %r' % value)
351
352 elif key == 'relative_cwd':
353 if not isinstance(value, basestring):
354 raise ConfigError('Expected string, got %r' % value)
355
356 elif key == 'os':
357 if value != get_flavor():
358 raise ConfigError(
359 'Expected \'os\' to be \'%s\' but got \'%s\'' %
360 (get_flavor(), value))
361
362 else:
363 raise ConfigError('Unknown key %s' % key)
364
365 return data
366
367
368def fix_python_path(cmd):
369 """Returns the fixed command line to call the right python executable."""
370 out = cmd[:]
371 if out[0] == 'python':
372 out[0] = sys.executable
373 elif out[0].endswith('.py'):
374 out.insert(0, sys.executable)
375 return out
376
377
maruel@chromium.orgef333122013-03-12 20:36:40 +0000378def url_open(url, data=None, retry_404=False, content_type=None):
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000379 """Attempts to open the given url multiple times.
380
381 |data| can be either:
382 -None for a GET request
383 -str for pre-encoded data
384 -list for data to be encoded
385 -dict for data to be encoded (COUNT_KEY will be added in this case)
386
387 If no wait_duration is given, the default wait time will exponentially
388 increase between each retry.
389
390 Returns a file-like object, where the response may be read from, or None
391 if it was unable to connect.
392 """
393 method = 'GET' if data is None else 'POST'
394
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000395 if isinstance(data, dict) and COUNT_KEY in data:
396 logging.error('%s already existed in the data passed into UlrOpen. It '
397 'would be overwritten. Aborting UrlOpen', COUNT_KEY)
398 return None
399
maruel@chromium.orgef333122013-03-12 20:36:40 +0000400 assert not ((method != 'POST') and content_type), (
401 'Can\'t use content_type on GET')
402
403 def make_request(extra):
404 """Returns a urllib2.Request instance for this specific retry."""
405 if isinstance(data, str) or data is None:
406 payload = data
407 else:
408 if isinstance(data, dict):
409 payload = data.items()
410 else:
411 payload = data[:]
412 payload.extend(extra.iteritems())
413 payload = urllib.urlencode(payload)
414
415 new_url = url
416 if isinstance(data, str) or data is None:
417 # In these cases, add the extra parameter to the query part of the url.
418 url_parts = list(urlparse.urlparse(new_url))
419 # Append the query parameter.
420 if url_parts[4] and extra:
421 url_parts[4] += '&'
422 url_parts[4] += urllib.urlencode(extra)
423 new_url = urlparse.urlunparse(url_parts)
424
425 request = urllib2.Request(new_url, data=payload)
426 if payload is not None:
427 if content_type:
428 request.add_header('Content-Type', content_type)
429 request.add_header('Content-Length', len(payload))
430 return request
431
432 return url_open_request(make_request, retry_404)
433
434
435def url_open_request(make_request, retry_404=False):
436 """Internal version of url_open() for users that need special handling.
437 """
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000438 last_error = None
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000439 for attempt in range(MAX_URL_OPEN_ATTEMPTS):
maruel@chromium.orgef333122013-03-12 20:36:40 +0000440 extra = {COUNT_KEY: attempt} if attempt else {}
441 request = make_request(extra)
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000442 try:
maruel@chromium.orgef333122013-03-12 20:36:40 +0000443 url_response = urllib2.urlopen(request)
maruel@chromium.orgf04becf2013-03-14 19:09:11 +0000444 logging.debug('url_open(%s) succeeded', request.get_full_url())
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000445 return url_response
446 except urllib2.HTTPError as e:
csharp@chromium.orge9c8d942013-03-11 20:48:36 +0000447 if e.code < 500 and not (retry_404 and e.code == 404):
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000448 # This HTTPError means we reached the server and there was a problem
449 # with the request, so don't retry.
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000450 logging.error(
451 'Able to connect to %s but an exception was thrown.\n%s\n%s',
452 request.get_full_url(), e, e.read())
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000453 return None
454
455 # The HTTPError was due to a server error, so retry the attempt.
456 logging.warning('Able to connect to %s on attempt %d.\nException: %s ',
maruel@chromium.orgef333122013-03-12 20:36:40 +0000457 request.get_full_url(), attempt, e)
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000458 last_error = e
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000459
460 except (urllib2.URLError, httplib.HTTPException) as e:
461 logging.warning('Unable to open url %s on attempt %d.\nException: %s',
maruel@chromium.orgef333122013-03-12 20:36:40 +0000462 request.get_full_url(), attempt, e)
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000463 last_error = e
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000464
465 # Only sleep if we are going to try again.
466 if attempt != MAX_URL_OPEN_ATTEMPTS - 1:
467 duration = random.random() * 3 + math.pow(1.5, (attempt + 1))
468 duration = min(10, max(0.1, duration))
469 time.sleep(duration)
470
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000471 logging.error('Unable to open given url, %s, after %d attempts.\n%s',
472 request.get_full_url(), MAX_URL_OPEN_ATTEMPTS, last_error)
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000473 return None
474
475
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000476class ThreadPool(object):
477 """Implements a multithreaded worker pool oriented for mapping jobs with
478 thread-local result storage.
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000479
480 Arguments:
481 - initial_threads: Number of threads to start immediately. Can be 0 if it is
482 uncertain that threads will be needed.
483 - max_threads: Maximum number of threads that will be started when all the
484 threads are busy working. Often the number of CPU cores.
485 - queue_size: Maximum number of tasks to buffer in the queue. 0 for unlimited
486 queue. A non-zero value may make add_task() blocking.
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000487 """
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000488 QUEUE_CLASS = Queue.PriorityQueue
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000489
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000490 def __init__(self, initial_threads, max_threads, queue_size):
491 logging.debug(
492 'ThreadPool(%d, %d, %d)', initial_threads, max_threads, queue_size)
493 assert initial_threads <= max_threads
494 # Update this check once 256 cores CPU are common.
495 assert max_threads <= 256
496
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000497 self.tasks = self.QUEUE_CLASS(queue_size)
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000498 self._max_threads = max_threads
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000499
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000500 # Mutables.
501 self._num_of_added_tasks_lock = threading.Lock()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000502 self._num_of_added_tasks = 0
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000503 self._outputs_exceptions_cond = threading.Condition()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000504 self._outputs = []
505 self._exceptions = []
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000506 # Number of threads in wait state.
507 self._ready_lock = threading.Lock()
508 self._ready = 0
509 self._workers_lock = threading.Lock()
510 self._workers = []
511 for _ in range(initial_threads):
512 self._add_worker()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000513
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000514 def _add_worker(self):
515 """Adds one worker thread if there isn't too many. Thread-safe."""
516 # Better to take the lock two times than hold it for too long.
517 with self._workers_lock:
518 if len(self._workers) >= self._max_threads:
519 return False
520 worker = threading.Thread(target=self._run)
521 with self._workers_lock:
522 if len(self._workers) >= self._max_threads:
523 return False
524 self._workers.append(worker)
525 worker.daemon = True
526 worker.start()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000527
maruel@chromium.org831958f2013-01-22 15:01:46 +0000528 def add_task(self, priority, func, *args, **kwargs):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000529 """Adds a task, a function to be executed by a worker.
530
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000531 |priority| can adjust the priority of the task versus others. Lower priority
maruel@chromium.org831958f2013-01-22 15:01:46 +0000532 takes precedence.
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000533
maruel@chromium.orgedd25d02013-03-26 14:38:00 +0000534 |func| can either return a return value to be added to the output list or
535 be a generator which can emit multiple values.
536
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000537 Returns the index of the item added, e.g. the total number of enqueued items
538 up to now.
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000539 """
maruel@chromium.org831958f2013-01-22 15:01:46 +0000540 assert isinstance(priority, int)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000541 assert callable(func)
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000542 with self._ready_lock:
543 start_new_worker = not self._ready
544 with self._num_of_added_tasks_lock:
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000545 self._num_of_added_tasks += 1
546 index = self._num_of_added_tasks
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000547 self.tasks.put((priority, index, func, args, kwargs))
548 if start_new_worker:
549 self._add_worker()
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000550 return index
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000551
552 def _run(self):
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000553 """Worker thread loop. Runs until a None task is queued."""
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000554 while True:
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000555 try:
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000556 with self._ready_lock:
557 self._ready += 1
558 task = self.tasks.get()
559 finally:
560 with self._ready_lock:
561 self._ready -= 1
562 try:
563 if task is None:
564 # We're done.
565 return
566 _priority, _index, func, args, kwargs = task
maruel@chromium.orgedd25d02013-03-26 14:38:00 +0000567 if inspect.isgeneratorfunction(func):
568 for out in func(*args, **kwargs):
569 self._output_append(out)
570 else:
571 out = func(*args, **kwargs)
572 self._output_append(out)
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000573 except Exception as e:
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000574 logging.warning('Caught exception: %s', e)
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000575 exc_info = sys.exc_info()
maruel@chromium.org97cd0be2013-03-13 14:01:36 +0000576 logging.info(''.join(traceback.format_tb(exc_info[2])))
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000577 self._outputs_exceptions_cond.acquire()
578 try:
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000579 self._exceptions.append(exc_info)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000580 self._outputs_exceptions_cond.notifyAll()
581 finally:
582 self._outputs_exceptions_cond.release()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000583 finally:
csharp@chromium.org60991182013-03-18 13:44:17 +0000584 try:
585 self.tasks.task_done()
586 except Exception as e:
587 # We need to catch and log this error here because this is the root
588 # function for the thread, nothing higher will catch the error.
589 logging.exception('Caught exception while marking task as done: %s',
590 e)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000591
maruel@chromium.orgedd25d02013-03-26 14:38:00 +0000592 def _output_append(self, out):
593 if out is not None:
594 self._outputs_exceptions_cond.acquire()
595 try:
596 self._outputs.append(out)
597 self._outputs_exceptions_cond.notifyAll()
598 finally:
599 self._outputs_exceptions_cond.release()
600
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000601 def join(self):
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000602 """Extracts all the results from each threads unordered.
603
604 Call repeatedly to extract all the exceptions if desired.
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000605
606 Note: will wait for all work items to be done before returning an exception.
607 To get an exception early, use get_one_result().
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000608 """
609 # TODO(maruel): Stop waiting as soon as an exception is caught.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000610 self.tasks.join()
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000611 self._outputs_exceptions_cond.acquire()
612 try:
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000613 if self._exceptions:
614 e = self._exceptions.pop(0)
615 raise e[0], e[1], e[2]
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000616 out = self._outputs
617 self._outputs = []
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000618 finally:
619 self._outputs_exceptions_cond.release()
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000620 return out
621
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000622 def get_one_result(self):
623 """Returns the next item that was generated or raises an exception if one
624 occured.
625
626 Warning: this function will hang if there is no work item left. Use join
627 instead.
628 """
629 self._outputs_exceptions_cond.acquire()
630 try:
631 while True:
632 if self._exceptions:
633 e = self._exceptions.pop(0)
634 raise e[0], e[1], e[2]
635 if self._outputs:
636 return self._outputs.pop(0)
637 self._outputs_exceptions_cond.wait()
638 finally:
639 self._outputs_exceptions_cond.release()
640
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000641 def close(self):
642 """Closes all the threads."""
643 for _ in range(len(self._workers)):
644 # Enqueueing None causes the worker to stop.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000645 self.tasks.put(None)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000646 for t in self._workers:
647 t.join()
648
649 def __enter__(self):
650 """Enables 'with' statement."""
651 return self
652
maruel@chromium.org97cd0be2013-03-13 14:01:36 +0000653 def __exit__(self, _exc_type, _exc_value, _traceback):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000654 """Enables 'with' statement."""
655 self.close()
656
657
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000658def valid_file(filepath, size):
659 """Determines if the given files appears valid (currently it just checks
660 the file's size)."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000661 if size == UNKNOWN_FILE_SIZE:
662 return True
663 actual_size = os.stat(filepath).st_size
664 if size != actual_size:
665 logging.warning(
666 'Found invalid item %s; %d != %d',
667 os.path.basename(filepath), actual_size, size)
668 return False
669 return True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000670
671
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000672class Profiler(object):
673 def __init__(self, name):
674 self.name = name
675 self.start_time = None
676
677 def __enter__(self):
678 self.start_time = time.time()
679 return self
680
681 def __exit__(self, _exc_type, _exec_value, _traceback):
682 time_taken = time.time() - self.start_time
683 logging.info('Profiling: Section %s took %3.3f seconds',
684 self.name, time_taken)
685
686
687class Remote(object):
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000688 """Priority based worker queue to fetch or upload files from a
689 content-address server. Any function may be given as the fetcher/upload,
690 as long as it takes two inputs (the item contents, and their relative
691 destination).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000692
693 Supports local file system, CIFS or http remotes.
694
695 When the priority of items is equals, works in strict FIFO mode.
696 """
697 # Initial and maximum number of worker threads.
698 INITIAL_WORKERS = 2
699 MAX_WORKERS = 16
700 # Priorities.
701 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
702 INTERNAL_PRIORITY_BITS = (1<<8) - 1
703 RETRIES = 5
704
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000705 def __init__(self, destination_root):
706 # Function to fetch a remote object or upload to a remote location..
707 self._do_item = self.get_file_handler(destination_root)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000708 # Contains tuple(priority, obj).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000709 self._done = Queue.PriorityQueue()
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000710 self._pool = ThreadPool(self.INITIAL_WORKERS, self.MAX_WORKERS, 0)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000711
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000712 def join(self):
713 """Blocks until the queue is empty."""
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000714 return self._pool.join()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000715
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000716 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000717 """Retrieves an object from the remote data store.
718
719 The smaller |priority| gets fetched first.
720
721 Thread-safe.
722 """
723 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000724 return self._add_item(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000725
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000726 def _add_item(self, priority, obj, dest, size):
727 assert isinstance(obj, basestring), obj
728 assert isinstance(dest, basestring), dest
729 assert size is None or isinstance(size, int), size
730 return self._pool.add_task(
731 priority, self._task_executer, priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000732
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000733 def get_one_result(self):
734 return self._pool.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000735
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000736 def _task_executer(self, priority, obj, dest, size):
737 """Wraps self._do_item to trap and retry on IOError exceptions."""
738 try:
739 self._do_item(obj, dest)
740 if size and not valid_file(dest, size):
741 download_size = os.stat(dest).st_size
742 os.remove(dest)
743 raise IOError('File incorrect size after download of %s. Got %s and '
744 'expected %s' % (obj, download_size, size))
745 # TODO(maruel): Technically, we'd want to have an output queue to be a
746 # PriorityQueue.
747 return obj
748 except IOError as e:
749 logging.debug('Caught IOError: %s', e)
750 # Retry a few times, lowering the priority.
751 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
752 self._add_item(priority + 1, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000753 return
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000754 raise
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000755
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +0000756 def get_file_handler(self, file_or_url): # pylint: disable=R0201
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000757 """Returns a object to retrieve objects from a remote."""
758 if re.match(r'^https?://.+$', file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000759 def download_file(item, dest):
760 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
761 # easy.
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000762 try:
csharp@chromium.orgaa2d1512012-12-05 21:17:39 +0000763 zipped_source = file_or_url + item
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000764 logging.debug('download_file(%s)', zipped_source)
csharp@chromium.orge9c8d942013-03-11 20:48:36 +0000765
766 # Because the app engine DB is only eventually consistent, retry
767 # 404 errors because the file might just not be visible yet (even
768 # though it has been uploaded).
769 connection = url_open(zipped_source, retry_404=True)
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000770 if not connection:
771 raise IOError('Unable to open connection to %s' % zipped_source)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000772 decompressor = zlib.decompressobj()
maruel@chromium.org3f039182012-11-27 21:32:41 +0000773 size = 0
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000774 with open(dest, 'wb') as f:
775 while True:
776 chunk = connection.read(ZIPPED_FILE_CHUNK)
777 if not chunk:
778 break
maruel@chromium.org3f039182012-11-27 21:32:41 +0000779 size += len(chunk)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000780 f.write(decompressor.decompress(chunk))
781 # Ensure that all the data was properly decompressed.
782 uncompressed_data = decompressor.flush()
783 assert not uncompressed_data
csharp@chromium.org549669e2013-01-22 19:48:17 +0000784 except IOError:
785 logging.error('Encountered an exception with (%s, %s)' % (item, dest))
786 raise
csharp@chromium.orga110d792013-01-07 16:16:16 +0000787 except httplib.HTTPException as e:
788 raise IOError('Encountered an HTTPException.\n%s' % e)
csharp@chromium.org186d6232012-11-26 14:36:12 +0000789 except zlib.error as e:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000790 # Log the first bytes to see if it's uncompressed data.
791 logging.warning('%r', e[:512])
maruel@chromium.org3f039182012-11-27 21:32:41 +0000792 raise IOError(
793 'Problem unzipping data for item %s. Got %d bytes.\n%s' %
794 (item, size, e))
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000795
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000796 return download_file
797
798 def copy_file(item, dest):
799 source = os.path.join(file_or_url, item)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000800 if source == dest:
801 logging.info('Source and destination are the same, no action required')
802 return
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000803 logging.debug('copy_file(%s, %s)', source, dest)
804 shutil.copy(source, dest)
805 return copy_file
806
807
808class CachePolicies(object):
809 def __init__(self, max_cache_size, min_free_space, max_items):
810 """
811 Arguments:
812 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
813 cache is effectively a leak.
814 - min_free_space: Trim if disk free space becomes lower than this value. If
815 0, it unconditionally fill the disk.
816 - max_items: Maximum number of items to keep in the cache. If 0, do not
817 enforce a limit.
818 """
819 self.max_cache_size = max_cache_size
820 self.min_free_space = min_free_space
821 self.max_items = max_items
822
823
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000824class NoCache(object):
825 """This class is intended to be usable everywhere the Cache class is.
826 Instead of downloading to a cache, all files are downloaded to the target
827 directory and then moved to where they are needed.
828 """
829
830 def __init__(self, target_directory, remote):
831 self.target_directory = target_directory
832 self.remote = remote
833
834 def retrieve(self, priority, item, size):
835 """Get the request file."""
836 self.remote.add_item(priority, item, self.path(item), size)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000837 self.remote.get_one_result()
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000838
839 def wait_for(self, items):
840 """Download the first item of the given list if it is missing."""
841 item = items.iterkeys().next()
842
843 if not os.path.exists(self.path(item)):
844 self.remote.add_item(Remote.MED, item, self.path(item), UNKNOWN_FILE_SIZE)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000845 downloaded = self.remote.get_one_result()
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000846 assert downloaded == item
847
848 return item
849
850 def path(self, item):
851 return os.path.join(self.target_directory, item)
852
853
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000854class Cache(object):
855 """Stateful LRU cache.
856
857 Saves its state as json file.
858 """
859 STATE_FILE = 'state.json'
860
861 def __init__(self, cache_dir, remote, policies):
862 """
863 Arguments:
864 - cache_dir: Directory where to place the cache.
865 - remote: Remote where to fetch items from.
866 - policies: cache retention policies.
867 """
868 self.cache_dir = cache_dir
869 self.remote = remote
870 self.policies = policies
871 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
872 # The tuple(file, size) are kept as an array in a LRU style. E.g.
873 # self.state[0] is the oldest item.
874 self.state = []
maruel@chromium.org770993b2012-12-11 17:16:48 +0000875 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000876 # A lookup map to speed up searching.
877 self._lookup = {}
maruel@chromium.org770993b2012-12-11 17:16:48 +0000878 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000879
880 # Items currently being fetched. Keep it local to reduce lock contention.
881 self._pending_queue = set()
882
883 # Profiling values.
884 self._added = []
885 self._removed = []
886 self._free_disk = 0
887
maruel@chromium.org770993b2012-12-11 17:16:48 +0000888 with Profiler('Setup'):
889 if not os.path.isdir(self.cache_dir):
890 os.makedirs(self.cache_dir)
891 if os.path.isfile(self.state_file):
892 try:
893 self.state = json.load(open(self.state_file, 'r'))
894 except (IOError, ValueError), e:
895 # Too bad. The file will be overwritten and the cache cleared.
896 logging.error(
897 'Broken state file %s, ignoring.\n%s' % (self.STATE_FILE, e))
898 self._state_need_to_be_saved = True
899 if (not isinstance(self.state, list) or
900 not all(
901 isinstance(i, (list, tuple)) and len(i) == 2
902 for i in self.state)):
903 # Discard.
904 self._state_need_to_be_saved = True
905 self.state = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000906
maruel@chromium.org770993b2012-12-11 17:16:48 +0000907 # Ensure that all files listed in the state still exist and add new ones.
908 previous = set(filename for filename, _ in self.state)
909 if len(previous) != len(self.state):
910 logging.warn('Cache state is corrupted, found duplicate files')
911 self._state_need_to_be_saved = True
912 self.state = []
913
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000914 added = 0
915 for filename in os.listdir(self.cache_dir):
916 if filename == self.STATE_FILE:
917 continue
918 if filename in previous:
919 previous.remove(filename)
920 continue
921 # An untracked file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000922 if not RE_IS_SHA1.match(filename):
923 logging.warn('Removing unknown file %s from cache', filename)
924 os.remove(self.path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000925 continue
926 # Insert as the oldest file. It will be deleted eventually if not
927 # accessed.
928 self._add(filename, False)
929 logging.warn('Add unknown file %s to cache', filename)
930 added += 1
931
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000932 if added:
933 logging.warn('Added back %d unknown files', added)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000934 if previous:
935 logging.warn('Removed %d lost files', len(previous))
936 # Set explicitly in case self._add() wasn't called.
937 self._state_need_to_be_saved = True
938 # Filter out entries that were not found while keeping the previous
939 # order.
940 self.state = [
941 (filename, size) for filename, size in self.state
942 if filename not in previous
943 ]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000944 self.trim()
945
946 def __enter__(self):
947 return self
948
949 def __exit__(self, _exc_type, _exec_value, _traceback):
950 with Profiler('CleanupTrimming'):
951 self.trim()
952
953 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000954 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000955 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000956 '%5d (%8dkb) current',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000957 len(self.state),
958 sum(i[1] for i in self.state) / 1024)
959 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000960 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
961 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000962
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000963 def remove_file_at_index(self, index):
964 """Removes the file at the given index."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000965 try:
maruel@chromium.org770993b2012-12-11 17:16:48 +0000966 self._state_need_to_be_saved = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000967 filename, size = self.state.pop(index)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000968 # If the lookup was already stale, its possible the filename was not
969 # present yet.
970 self._lookup_is_stale = True
971 self._lookup.pop(filename, None)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000972 self._removed.append(size)
973 os.remove(self.path(filename))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000974 except OSError as e:
975 logging.error('Error attempting to delete a file\n%s' % e)
976
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000977 def remove_lru_file(self):
978 """Removes the last recently used file."""
979 self.remove_file_at_index(0)
980
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000981 def trim(self):
982 """Trims anything we don't know, make sure enough free space exists."""
983 # Ensure maximum cache size.
984 if self.policies.max_cache_size and self.state:
985 while sum(i[1] for i in self.state) > self.policies.max_cache_size:
986 self.remove_lru_file()
987
988 # Ensure maximum number of items in the cache.
989 if self.policies.max_items and self.state:
990 while len(self.state) > self.policies.max_items:
991 self.remove_lru_file()
992
993 # Ensure enough free space.
994 self._free_disk = get_free_space(self.cache_dir)
995 while (
996 self.policies.min_free_space and
997 self.state and
998 self._free_disk < self.policies.min_free_space):
999 self.remove_lru_file()
1000 self._free_disk = get_free_space(self.cache_dir)
1001
1002 self.save()
1003
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001004 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001005 """Retrieves a file from the remote, if not already cached, and adds it to
1006 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001007
1008 If the file is in the cache, verifiy that the file is valid (i.e. it is
1009 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001010 """
1011 assert not '/' in item
1012 path = self.path(item)
maruel@chromium.org770993b2012-12-11 17:16:48 +00001013 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001014 index = self._lookup.get(item)
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001015
1016 if index is not None:
1017 if not valid_file(self.path(item), size):
1018 self.remove_file_at_index(index)
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001019 index = None
1020 else:
1021 assert index < len(self.state)
1022 # Was already in cache. Update it's LRU value by putting it at the end.
maruel@chromium.org770993b2012-12-11 17:16:48 +00001023 self._state_need_to_be_saved = True
1024 self._lookup_is_stale = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001025 self.state.append(self.state.pop(index))
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001026
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001027 if index is None:
1028 if item in self._pending_queue:
1029 # Already pending. The same object could be referenced multiple times.
1030 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +00001031 self.remote.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001032 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001033
1034 def add(self, filepath, obj):
1035 """Forcibly adds a file to the cache."""
maruel@chromium.org770993b2012-12-11 17:16:48 +00001036 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001037 if not obj in self._lookup:
1038 link_file(self.path(obj), filepath, HARDLINK)
1039 self._add(obj, True)
1040
1041 def path(self, item):
1042 """Returns the path to one item."""
1043 return os.path.join(self.cache_dir, item)
1044
1045 def save(self):
1046 """Saves the LRU ordering."""
maruel@chromium.org770993b2012-12-11 17:16:48 +00001047 if self._state_need_to_be_saved:
1048 json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':'))
1049 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001050
1051 def wait_for(self, items):
1052 """Starts a loop that waits for at least one of |items| to be retrieved.
1053
1054 Returns the first item retrieved.
1055 """
1056 # Flush items already present.
maruel@chromium.org770993b2012-12-11 17:16:48 +00001057 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001058 for item in items:
1059 if item in self._lookup:
1060 return item
1061
1062 assert all(i in self._pending_queue for i in items), (
1063 items, self._pending_queue)
1064 # Note that:
1065 # len(self._pending_queue) ==
1066 # ( len(self.remote._workers) - self.remote._ready +
1067 # len(self._remote._queue) + len(self._remote.done))
1068 # There is no lock-free way to verify that.
1069 while self._pending_queue:
maruel@chromium.org13eca0b2013-01-22 16:42:21 +00001070 item = self.remote.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001071 self._pending_queue.remove(item)
1072 self._add(item, True)
1073 if item in items:
1074 return item
1075
1076 def _add(self, item, at_end):
1077 """Adds an item in the internal state.
1078
1079 If |at_end| is False, self._lookup becomes inconsistent and
1080 self._update_lookup() must be called.
1081 """
1082 size = os.stat(self.path(item)).st_size
1083 self._added.append(size)
maruel@chromium.org770993b2012-12-11 17:16:48 +00001084 self._state_need_to_be_saved = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001085 if at_end:
1086 self.state.append((item, size))
1087 self._lookup[item] = len(self.state) - 1
1088 else:
maruel@chromium.org770993b2012-12-11 17:16:48 +00001089 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001090 self.state.insert(0, (item, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001091
1092 def _update_lookup(self):
maruel@chromium.org770993b2012-12-11 17:16:48 +00001093 if self._lookup_is_stale:
1094 self._lookup = dict(
1095 (filename, index) for index, (filename, _) in enumerate(self.state))
1096 self._lookup_is_stale = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001097
1098
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001099class IsolatedFile(object):
1100 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001101 def __init__(self, obj_hash):
1102 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001103 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001104 self.obj_hash = obj_hash
1105 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001106 # .isolate and all the .isolated files recursively included by it with
1107 # 'includes' key. The order of each sha-1 in 'includes', each representing a
1108 # .isolated file in the hash table, is important, as the later ones are not
1109 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001110 self.can_fetch = False
1111
1112 # Raw data.
1113 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001114 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001115 self.children = []
1116
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001117 # Set once the .isolated file is loaded.
1118 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001119 # Set once the files are fetched.
1120 self.files_fetched = False
1121
1122 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001123 """Verifies the .isolated file is valid and loads this object with the json
1124 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001125 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001126 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
1127 assert not self._is_parsed
1128 self.data = load_isolated(content)
1129 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
1130 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001131
1132 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001133 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001134
1135 Preemptively request files.
1136
1137 Note that |files| is modified by this function.
1138 """
1139 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001140 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001141 return
1142 logging.debug('fetch_files(%s)' % self.obj_hash)
1143 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001144 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001145 # overriden files must not be fetched.
1146 if filepath not in files:
1147 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001148 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001149 # Preemptively request files.
1150 logging.debug('fetching %s' % filepath)
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001151 cache.retrieve(Remote.MED, properties['h'], properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001152 self.files_fetched = True
1153
1154
1155class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001156 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001157 def __init__(self):
1158 self.command = []
1159 self.files = {}
1160 self.read_only = None
1161 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001162 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001163 self.root = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001164
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001165 def load(self, cache, root_isolated_hash):
1166 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001167
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001168 It enables support for "included" .isolated files. They are processed in
1169 strict order but fetched asynchronously from the cache. This is important so
1170 that a file in an included .isolated file that is overridden by an embedding
1171 .isolated file is not fetched neededlessly. The includes are fetched in one
1172 pass and the files are fetched as soon as all the ones on the left-side
1173 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001174
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001175 The prioritization is very important here for nested .isolated files.
1176 'includes' have the highest priority and the algorithm is optimized for both
1177 deep and wide trees. A deep one is a long link of .isolated files referenced
1178 one at a time by one item in 'includes'. A wide one has a large number of
1179 'includes' in a single .isolated file. 'left' is defined as an included
1180 .isolated file earlier in the 'includes' list. So the order of the elements
1181 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001182 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001183 self.root = IsolatedFile(root_isolated_hash)
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001184 cache.retrieve(Remote.HIGH, root_isolated_hash, UNKNOWN_FILE_SIZE)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001185 pending = {root_isolated_hash: self.root}
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001186 # Keeps the list of retrieved items to refuse recursive includes.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001187 retrieved = [root_isolated_hash]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001188
1189 def update_self(node):
1190 node.fetch_files(cache, self.files)
1191 # Grabs properties.
1192 if not self.command and node.data.get('command'):
1193 self.command = node.data['command']
1194 if self.read_only is None and node.data.get('read_only') is not None:
1195 self.read_only = node.data['read_only']
1196 if (self.relative_cwd is None and
1197 node.data.get('relative_cwd') is not None):
1198 self.relative_cwd = node.data['relative_cwd']
1199
1200 def traverse_tree(node):
1201 if node.can_fetch:
1202 if not node.files_fetched:
1203 update_self(node)
1204 will_break = False
1205 for i in node.children:
1206 if not i.can_fetch:
1207 if will_break:
1208 break
1209 # Automatically mark the first one as fetcheable.
1210 i.can_fetch = True
1211 will_break = True
1212 traverse_tree(i)
1213
1214 while pending:
1215 item_hash = cache.wait_for(pending)
1216 item = pending.pop(item_hash)
1217 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001218 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001219 # It's the root item.
1220 item.can_fetch = True
1221
1222 for new_child in item.children:
1223 h = new_child.obj_hash
1224 if h in retrieved:
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001225 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001226 pending[h] = new_child
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001227 cache.retrieve(Remote.HIGH, h, UNKNOWN_FILE_SIZE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001228
1229 # Traverse the whole tree to see if files can now be fetched.
1230 traverse_tree(self.root)
1231 def check(n):
1232 return all(check(x) for x in n.children) and n.files_fetched
1233 assert check(self.root)
1234 self.relative_cwd = self.relative_cwd or ''
1235 self.read_only = self.read_only or False
1236
1237
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001238def create_directories(base_directory, files):
1239 """Creates the directory structure needed by the given list of files."""
1240 logging.debug('create_directories(%s, %d)', base_directory, len(files))
1241 # Creates the tree of directories to create.
1242 directories = set(os.path.dirname(f) for f in files)
1243 for item in list(directories):
1244 while item:
1245 directories.add(item)
1246 item = os.path.dirname(item)
1247 for d in sorted(directories):
1248 if d:
1249 os.mkdir(os.path.join(base_directory, d))
1250
1251
1252def create_links(base_directory, files):
1253 """Creates any links needed by the given set of files."""
1254 for filepath, properties in files:
csharp@chromium.org89eaf082013-03-26 18:56:21 +00001255 if 'l' not in properties:
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001256 continue
1257 outfile = os.path.join(base_directory, filepath)
1258 # symlink doesn't exist on Windows. So the 'link' property should
1259 # never be specified for windows .isolated file.
1260 os.symlink(properties['l'], outfile) # pylint: disable=E1101
1261 if 'm' in properties:
1262 lchmod = getattr(os, 'lchmod', None)
1263 if lchmod:
1264 lchmod(outfile, properties['m'])
1265
1266
1267def setup_commands(base_directory, cwd, cmd):
1268 """Correctly adjusts and then returns the required working directory
1269 and command needed to run the test.
1270 """
1271 assert not os.path.isabs(cwd), 'The cwd must be a relative path, got %s' % cwd
1272 cwd = os.path.join(base_directory, cwd)
1273 if not os.path.isdir(cwd):
1274 os.makedirs(cwd)
1275
1276 # Ensure paths are correctly separated on windows.
1277 cmd[0] = cmd[0].replace('/', os.path.sep)
1278 cmd = fix_python_path(cmd)
1279
1280 return cwd, cmd
1281
1282
1283def generate_remaining_files(files):
1284 """Generates a dictionary of all the remaining files to be downloaded."""
1285 remaining = {}
1286 for filepath, props in files:
1287 if 'h' in props:
1288 remaining.setdefault(props['h'], []).append((filepath, props))
1289
1290 return remaining
1291
1292
1293def download_test_data(isolated_hash, target_directory, remote):
1294 """Downloads the dependencies to the given directory."""
1295 if not os.path.exists(target_directory):
1296 os.makedirs(target_directory)
1297
1298 settings = Settings()
1299 no_cache = NoCache(target_directory, Remote(remote))
1300
1301 # Download all the isolated files.
1302 with Profiler('GetIsolateds') as _prof:
1303 settings.load(no_cache, isolated_hash)
1304
1305 if not settings.command:
1306 print >> sys.stderr, 'No command to run'
1307 return 1
1308
1309 with Profiler('GetRest') as _prof:
1310 create_directories(target_directory, settings.files)
1311 create_links(target_directory, settings.files.iteritems())
1312
1313 cwd, cmd = setup_commands(target_directory, settings.relative_cwd,
1314 settings.command[:])
1315
1316 remaining = generate_remaining_files(settings.files.iteritems())
1317
1318 # Now block on the remaining files to be downloaded and mapped.
1319 logging.info('Retrieving remaining files')
1320 last_update = time.time()
1321 while remaining:
1322 obj = no_cache.wait_for(remaining)
1323 files = remaining.pop(obj)
1324
1325 for i, (filepath, properties) in enumerate(files):
1326 outfile = os.path.join(target_directory, filepath)
1327 logging.info(no_cache.path(obj))
1328
1329 if i + 1 == len(files):
1330 os.rename(no_cache.path(obj), outfile)
1331 else:
1332 shutil.copyfile(no_cache.path(obj), outfile)
1333
1334 if 'm' in properties:
1335 # It's not set on Windows.
1336 os.chmod(outfile, properties['m'])
1337
1338 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
1339 logging.info('%d files remaining...' % len(remaining))
1340 last_update = time.time()
1341
1342 print('.isolated files successfully downloaded and setup in %s' %
1343 target_directory)
1344 print('To run this test please run the command %s from the directory %s' %
1345 (cmd, cwd))
1346
1347 return 0
1348
1349
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001350def run_tha_test(isolated_hash, cache_dir, remote, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001351 """Downloads the dependencies in the cache, hardlinks them into a temporary
1352 directory and runs the executable.
1353 """
1354 settings = Settings()
1355 with Cache(cache_dir, Remote(remote), policies) as cache:
1356 outdir = make_temp_dir('run_tha_test', cache_dir)
1357 try:
1358 # Initiate all the files download.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001359 with Profiler('GetIsolateds') as _prof:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001360 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001361 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001362 # Adds it in the cache. While not strictly necessary, this simplifies
1363 # the rest.
maruel@chromium.orgcb3c3d52013-03-14 18:55:30 +00001364 h = hashlib.sha1(open(isolated_hash, 'rb').read()).hexdigest()
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001365 cache.add(isolated_hash, h)
1366 isolated_hash = h
1367 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001368
1369 if not settings.command:
1370 print >> sys.stderr, 'No command to run'
1371 return 1
1372
1373 with Profiler('GetRest') as _prof:
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001374 create_directories(outdir, settings.files)
1375 create_links(outdir, settings.files.iteritems())
1376 remaining = generate_remaining_files(settings.files.iteritems())
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001377
1378 # Do bookkeeping while files are being downloaded in the background.
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001379 cwd, cmd = setup_commands(outdir, settings.relative_cwd,
1380 settings.command[:])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001381
1382 # Now block on the remaining files to be downloaded and mapped.
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001383 logging.info('Retrieving remaining files')
1384 last_update = time.time()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001385 while remaining:
1386 obj = cache.wait_for(remaining)
1387 for filepath, properties in remaining.pop(obj):
1388 outfile = os.path.join(outdir, filepath)
1389 link_file(outfile, cache.path(obj), HARDLINK)
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001390 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001391 # It's not set on Windows.
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001392 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001393
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001394 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
1395 logging.info('%d files remaining...' % len(remaining))
1396 last_update = time.time()
1397
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001398 if settings.read_only:
1399 make_writable(outdir, True)
1400 logging.info('Running %s, cwd=%s' % (cmd, cwd))
csharp@chromium.orge217f302012-11-22 16:51:53 +00001401
1402 # TODO(csharp): This should be specified somewhere else.
1403 # Add a rotating log file if one doesn't already exist.
1404 env = os.environ.copy()
1405 env.setdefault('RUN_TEST_CASES_LOG_FILE', RUN_TEST_CASES_LOG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001406 try:
1407 with Profiler('RunTest') as _prof:
csharp@chromium.orge217f302012-11-22 16:51:53 +00001408 return subprocess.call(cmd, cwd=cwd, env=env)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001409 except OSError:
1410 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1411 raise
1412 finally:
1413 rmtree(outdir)
1414
1415
1416def main():
maruel@chromium.org46e61cc2013-03-25 19:55:34 +00001417 disable_buffering()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001418 parser = optparse.OptionParser(
1419 usage='%prog <options>', description=sys.modules[__name__].__doc__)
1420 parser.add_option(
1421 '-v', '--verbose', action='count', default=0, help='Use multiple times')
1422 parser.add_option('--no-run', action='store_true', help='Skip the run part')
1423
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001424 group = optparse.OptionGroup(parser, 'Download')
1425 group.add_option(
1426 '--download', metavar='DEST',
1427 help='Downloads files to DEST and returns without running, instead of '
1428 'downloading and then running from a temporary directory.')
1429 parser.add_option_group(group)
1430
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001431 group = optparse.OptionGroup(parser, 'Data source')
1432 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001433 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001434 metavar='FILE',
1435 help='File/url describing what to map or run')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001436 # TODO(maruel): Remove once not used anymore.
1437 group.add_option(
1438 '-m', '--manifest', dest='isolated', help=optparse.SUPPRESS_HELP)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001439 group.add_option(
1440 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001441 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001442 parser.add_option_group(group)
1443
1444 group.add_option(
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001445 '-r', '--remote', metavar='URL',
1446 default=
1447 'https://isolateserver.appspot.com/content/retrieve/default-gzip/',
1448 help='Remote where to get the items. Defaults to %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001449 group = optparse.OptionGroup(parser, 'Cache management')
1450 group.add_option(
1451 '--cache',
1452 default='cache',
1453 metavar='DIR',
1454 help='Cache directory, default=%default')
1455 group.add_option(
1456 '--max-cache-size',
1457 type='int',
1458 metavar='NNN',
1459 default=20*1024*1024*1024,
1460 help='Trim if the cache gets larger than this value, default=%default')
1461 group.add_option(
1462 '--min-free-space',
1463 type='int',
1464 metavar='NNN',
1465 default=1*1024*1024*1024,
1466 help='Trim if disk free space becomes lower than this value, '
1467 'default=%default')
1468 group.add_option(
1469 '--max-items',
1470 type='int',
1471 metavar='NNN',
1472 default=100000,
1473 help='Trim if more than this number of items are in the cache '
1474 'default=%default')
1475 parser.add_option_group(group)
1476
1477 options, args = parser.parse_args()
1478 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
csharp@chromium.orgff2a4662012-11-21 20:49:32 +00001479
1480 logging_console = logging.StreamHandler()
1481 logging_console.setFormatter(logging.Formatter(
1482 '%(levelname)5s %(module)15s(%(lineno)3d): %(message)s'))
1483 logging_console.setLevel(level)
1484 logging.getLogger().addHandler(logging_console)
1485
1486 logging_rotating_file = logging.handlers.RotatingFileHandler(
1487 RUN_ISOLATED_LOG_FILE,
1488 maxBytes=10 * 1024 * 1024, backupCount=5)
1489 logging_rotating_file.setLevel(logging.DEBUG)
1490 logging_rotating_file.setFormatter(logging.Formatter(
1491 '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)3d): %(message)s'))
1492 logging.getLogger().addHandler(logging_rotating_file)
1493
1494 logging.getLogger().setLevel(logging.DEBUG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001495
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001496 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001497 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001498 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001499 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001500 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001501 parser.error('Unsupported args %s' % ' '.join(args))
1502
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001503 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001504 policies = CachePolicies(
1505 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001506
1507 if options.download:
1508 return download_test_data(options.isolated or options.hash,
1509 options.download, options.remote)
1510 else:
1511 try:
1512 return run_tha_test(
1513 options.isolated or options.hash,
1514 options.cache,
1515 options.remote,
1516 policies)
1517 except Exception, e:
1518 # Make sure any exception is logged.
1519 logging.exception(e)
1520 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001521
1522
1523if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001524 # Ensure that we are always running with the correct encoding.
1525 fix_default_encoding()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001526 sys.exit(main())