blob: c5ab51d2c88a51d30aba8e6a3322d99a419951b2 [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.org9c72d4e2012-09-28 19:20:25 +000014import json
15import logging
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000016import logging.handlers
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000017import math
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000018import optparse
19import os
20import Queue
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000021import random
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000022import re
23import shutil
24import stat
25import subprocess
26import sys
27import tempfile
28import threading
29import time
maruel@chromium.org97cd0be2013-03-13 14:01:36 +000030import traceback
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000031import urllib
csharp@chromium.orga92403f2012-11-20 15:13:59 +000032import urllib2
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000033import urlparse
csharp@chromium.orga92403f2012-11-20 15:13:59 +000034import zlib
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000035
36
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000037# Types of action accepted by link_file().
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000038HARDLINK, SYMLINK, COPY = range(1, 4)
39
40RE_IS_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$')
41
csharp@chromium.org8dc52542012-11-08 20:29:55 +000042# The file size to be used when we don't know the correct file size,
43# generally used for .isolated files.
44UNKNOWN_FILE_SIZE = None
45
csharp@chromium.orga92403f2012-11-20 15:13:59 +000046# The size of each chunk to read when downloading and unzipping files.
47ZIPPED_FILE_CHUNK = 16 * 1024
48
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000049# The name of the log file to use.
50RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
51
csharp@chromium.orge217f302012-11-22 16:51:53 +000052# The base directory containing this file.
53BASE_DIR = os.path.dirname(os.path.abspath(__file__))
54
55# The name of the log to use for the run_test_cases.py command
56RUN_TEST_CASES_LOG = os.path.join(BASE_DIR, 'run_test_cases.log')
57
csharp@chromium.org9c59ff12012-12-12 02:32:29 +000058# The delay (in seconds) to wait between logging statements when retrieving
59# the required files. This is intended to let the user (or buildbot) know that
60# the program is still running.
61DELAY_BETWEEN_UPDATES_IN_SECS = 30
62
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000063# The name of the key to store the count of url attempts.
64COUNT_KEY = 'UrlOpenAttempt'
65
66# The maximum number of attempts to trying opening a url before aborting.
67MAX_URL_OPEN_ATTEMPTS = 20
68
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000069
70class ConfigError(ValueError):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +000071 """Generic failure to load a .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000072 pass
73
74
75class MappingError(OSError):
76 """Failed to recreate the tree."""
77 pass
78
79
80def get_flavor():
81 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
82 flavors = {
83 'cygwin': 'win',
84 'win32': 'win',
85 'darwin': 'mac',
86 'sunos5': 'solaris',
87 'freebsd7': 'freebsd',
88 'freebsd8': 'freebsd',
89 }
90 return flavors.get(sys.platform, 'linux')
91
92
maruel@chromium.org46e61cc2013-03-25 19:55:34 +000093class Unbuffered(object):
94 """Disable buffering on a file object."""
95 def __init__(self, stream):
96 self.stream = stream
97
98 def write(self, data):
99 self.stream.write(data)
100 if '\n' in data:
101 self.stream.flush()
102
103 def __getattr__(self, attr):
104 return getattr(self.stream, attr)
105
106
107def disable_buffering():
108 """Makes this process and child processes stdout unbuffered."""
109 if not os.environ.get('PYTHONUNBUFFERED'):
110 # Since sys.stdout is a C++ object, it's impossible to do
111 # sys.stdout.write = lambda...
112 sys.stdout = Unbuffered(sys.stdout)
113 os.environ['PYTHONUNBUFFERED'] = 'x'
114
115
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000116def os_link(source, link_name):
117 """Add support for os.link() on Windows."""
118 if sys.platform == 'win32':
119 if not ctypes.windll.kernel32.CreateHardLinkW(
120 unicode(link_name), unicode(source), 0):
121 raise OSError()
122 else:
123 os.link(source, link_name)
124
125
126def readable_copy(outfile, infile):
127 """Makes a copy of the file that is readable by everyone."""
128 shutil.copy(infile, outfile)
129 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
130 stat.S_IRGRP | stat.S_IROTH)
131 os.chmod(outfile, read_enabled_mode)
132
133
134def link_file(outfile, infile, action):
135 """Links a file. The type of link depends on |action|."""
136 logging.debug('Mapping %s to %s' % (infile, outfile))
137 if action not in (HARDLINK, SYMLINK, COPY):
138 raise ValueError('Unknown mapping action %s' % action)
139 if not os.path.isfile(infile):
140 raise MappingError('%s is missing' % infile)
141 if os.path.isfile(outfile):
142 raise MappingError(
143 '%s already exist; insize:%d; outsize:%d' %
144 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
145
146 if action == COPY:
147 readable_copy(outfile, infile)
148 elif action == SYMLINK and sys.platform != 'win32':
149 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000150 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000151 else:
152 try:
153 os_link(infile, outfile)
154 except OSError:
155 # Probably a different file system.
156 logging.warn(
157 'Failed to hardlink, failing back to copy %s to %s' % (
158 infile, outfile))
159 readable_copy(outfile, infile)
160
161
162def _set_write_bit(path, read_only):
163 """Sets or resets the executable bit on a file or directory."""
164 mode = os.lstat(path).st_mode
165 if read_only:
166 mode = mode & 0500
167 else:
168 mode = mode | 0200
169 if hasattr(os, 'lchmod'):
170 os.lchmod(path, mode) # pylint: disable=E1101
171 else:
172 if stat.S_ISLNK(mode):
173 # Skip symlink without lchmod() support.
174 logging.debug('Can\'t change +w bit on symlink %s' % path)
175 return
176
177 # TODO(maruel): Implement proper DACL modification on Windows.
178 os.chmod(path, mode)
179
180
181def make_writable(root, read_only):
182 """Toggle the writable bit on a directory tree."""
csharp@chromium.org837352f2013-01-17 21:17:03 +0000183 assert os.path.isabs(root), root
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000184 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
185 for filename in filenames:
186 _set_write_bit(os.path.join(dirpath, filename), read_only)
187
188 for dirname in dirnames:
189 _set_write_bit(os.path.join(dirpath, dirname), read_only)
190
191
192def rmtree(root):
193 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
194 make_writable(root, False)
195 if sys.platform == 'win32':
196 for i in range(3):
197 try:
198 shutil.rmtree(root)
199 break
200 except WindowsError: # pylint: disable=E0602
201 delay = (i+1)*2
202 print >> sys.stderr, (
203 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
204 time.sleep(delay)
205 else:
206 shutil.rmtree(root)
207
208
209def is_same_filesystem(path1, path2):
210 """Returns True if both paths are on the same filesystem.
211
212 This is required to enable the use of hardlinks.
213 """
214 assert os.path.isabs(path1), path1
215 assert os.path.isabs(path2), path2
216 if sys.platform == 'win32':
217 # If the drive letter mismatches, assume it's a separate partition.
218 # TODO(maruel): It should look at the underlying drive, a drive letter could
219 # be a mount point to a directory on another drive.
220 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
221 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
222 if path1[0].lower() != path2[0].lower():
223 return False
224 return os.stat(path1).st_dev == os.stat(path2).st_dev
225
226
227def get_free_space(path):
228 """Returns the number of free bytes."""
229 if sys.platform == 'win32':
230 free_bytes = ctypes.c_ulonglong(0)
231 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
232 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
233 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000234 # For OSes other than Windows.
235 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000236 return f.f_bfree * f.f_frsize
237
238
239def make_temp_dir(prefix, root_dir):
240 """Returns a temporary directory on the same file system as root_dir."""
241 base_temp_dir = None
242 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
243 base_temp_dir = os.path.dirname(root_dir)
244 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
245
246
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000247def load_isolated(content):
248 """Verifies the .isolated file is valid and loads this object with the json
249 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000250 """
251 try:
252 data = json.loads(content)
253 except ValueError:
254 raise ConfigError('Failed to parse: %s...' % content[:100])
255
256 if not isinstance(data, dict):
257 raise ConfigError('Expected dict, got %r' % data)
258
259 for key, value in data.iteritems():
260 if key == 'command':
261 if not isinstance(value, list):
262 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000263 if not value:
264 raise ConfigError('Expected non-empty command')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000265 for subvalue in value:
266 if not isinstance(subvalue, basestring):
267 raise ConfigError('Expected string, got %r' % subvalue)
268
269 elif key == 'files':
270 if not isinstance(value, dict):
271 raise ConfigError('Expected dict, got %r' % value)
272 for subkey, subvalue in value.iteritems():
273 if not isinstance(subkey, basestring):
274 raise ConfigError('Expected string, got %r' % subkey)
275 if not isinstance(subvalue, dict):
276 raise ConfigError('Expected dict, got %r' % subvalue)
277 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000278 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000279 if not isinstance(subsubvalue, basestring):
280 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000281 elif subsubkey == 'm':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000282 if not isinstance(subsubvalue, int):
283 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000284 elif subsubkey == 'h':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000285 if not RE_IS_SHA1.match(subsubvalue):
286 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000287 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000288 if not isinstance(subsubvalue, int):
289 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000290 else:
291 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000292 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000293 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000294 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
295 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000296
297 elif key == 'includes':
298 if not isinstance(value, list):
299 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000300 if not value:
301 raise ConfigError('Expected non-empty includes list')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000302 for subvalue in value:
303 if not RE_IS_SHA1.match(subvalue):
304 raise ConfigError('Expected sha-1, got %r' % subvalue)
305
306 elif key == 'read_only':
307 if not isinstance(value, bool):
308 raise ConfigError('Expected bool, got %r' % value)
309
310 elif key == 'relative_cwd':
311 if not isinstance(value, basestring):
312 raise ConfigError('Expected string, got %r' % value)
313
314 elif key == 'os':
315 if value != get_flavor():
316 raise ConfigError(
317 'Expected \'os\' to be \'%s\' but got \'%s\'' %
318 (get_flavor(), value))
319
320 else:
321 raise ConfigError('Unknown key %s' % key)
322
323 return data
324
325
326def fix_python_path(cmd):
327 """Returns the fixed command line to call the right python executable."""
328 out = cmd[:]
329 if out[0] == 'python':
330 out[0] = sys.executable
331 elif out[0].endswith('.py'):
332 out.insert(0, sys.executable)
333 return out
334
335
maruel@chromium.orgef333122013-03-12 20:36:40 +0000336def url_open(url, data=None, retry_404=False, content_type=None):
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000337 """Attempts to open the given url multiple times.
338
339 |data| can be either:
340 -None for a GET request
341 -str for pre-encoded data
342 -list for data to be encoded
343 -dict for data to be encoded (COUNT_KEY will be added in this case)
344
345 If no wait_duration is given, the default wait time will exponentially
346 increase between each retry.
347
348 Returns a file-like object, where the response may be read from, or None
349 if it was unable to connect.
350 """
351 method = 'GET' if data is None else 'POST'
352
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000353 if isinstance(data, dict) and COUNT_KEY in data:
354 logging.error('%s already existed in the data passed into UlrOpen. It '
355 'would be overwritten. Aborting UrlOpen', COUNT_KEY)
356 return None
357
maruel@chromium.orgef333122013-03-12 20:36:40 +0000358 assert not ((method != 'POST') and content_type), (
359 'Can\'t use content_type on GET')
360
361 def make_request(extra):
362 """Returns a urllib2.Request instance for this specific retry."""
363 if isinstance(data, str) or data is None:
364 payload = data
365 else:
366 if isinstance(data, dict):
367 payload = data.items()
368 else:
369 payload = data[:]
370 payload.extend(extra.iteritems())
371 payload = urllib.urlencode(payload)
372
373 new_url = url
374 if isinstance(data, str) or data is None:
375 # In these cases, add the extra parameter to the query part of the url.
376 url_parts = list(urlparse.urlparse(new_url))
377 # Append the query parameter.
378 if url_parts[4] and extra:
379 url_parts[4] += '&'
380 url_parts[4] += urllib.urlencode(extra)
381 new_url = urlparse.urlunparse(url_parts)
382
383 request = urllib2.Request(new_url, data=payload)
384 if payload is not None:
385 if content_type:
386 request.add_header('Content-Type', content_type)
387 request.add_header('Content-Length', len(payload))
388 return request
389
390 return url_open_request(make_request, retry_404)
391
392
393def url_open_request(make_request, retry_404=False):
394 """Internal version of url_open() for users that need special handling.
395 """
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000396 last_error = None
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000397 for attempt in range(MAX_URL_OPEN_ATTEMPTS):
maruel@chromium.orgef333122013-03-12 20:36:40 +0000398 extra = {COUNT_KEY: attempt} if attempt else {}
399 request = make_request(extra)
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000400 try:
maruel@chromium.orgef333122013-03-12 20:36:40 +0000401 url_response = urllib2.urlopen(request)
maruel@chromium.orgf04becf2013-03-14 19:09:11 +0000402 logging.debug('url_open(%s) succeeded', request.get_full_url())
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000403 return url_response
404 except urllib2.HTTPError as e:
csharp@chromium.orge9c8d942013-03-11 20:48:36 +0000405 if e.code < 500 and not (retry_404 and e.code == 404):
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000406 # This HTTPError means we reached the server and there was a problem
407 # with the request, so don't retry.
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000408 logging.error(
409 'Able to connect to %s but an exception was thrown.\n%s\n%s',
410 request.get_full_url(), e, e.read())
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000411 return None
412
413 # The HTTPError was due to a server error, so retry the attempt.
414 logging.warning('Able to connect to %s on attempt %d.\nException: %s ',
maruel@chromium.orgef333122013-03-12 20:36:40 +0000415 request.get_full_url(), attempt, e)
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000416 last_error = e
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000417
418 except (urllib2.URLError, httplib.HTTPException) as e:
419 logging.warning('Unable to open url %s on attempt %d.\nException: %s',
maruel@chromium.orgef333122013-03-12 20:36:40 +0000420 request.get_full_url(), attempt, e)
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000421 last_error = e
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000422
423 # Only sleep if we are going to try again.
424 if attempt != MAX_URL_OPEN_ATTEMPTS - 1:
425 duration = random.random() * 3 + math.pow(1.5, (attempt + 1))
426 duration = min(10, max(0.1, duration))
427 time.sleep(duration)
428
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000429 logging.error('Unable to open given url, %s, after %d attempts.\n%s',
430 request.get_full_url(), MAX_URL_OPEN_ATTEMPTS, last_error)
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000431 return None
432
433
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000434class ThreadPool(object):
435 """Implements a multithreaded worker pool oriented for mapping jobs with
436 thread-local result storage.
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000437
438 Arguments:
439 - initial_threads: Number of threads to start immediately. Can be 0 if it is
440 uncertain that threads will be needed.
441 - max_threads: Maximum number of threads that will be started when all the
442 threads are busy working. Often the number of CPU cores.
443 - queue_size: Maximum number of tasks to buffer in the queue. 0 for unlimited
444 queue. A non-zero value may make add_task() blocking.
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000445 """
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000446 QUEUE_CLASS = Queue.PriorityQueue
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000447
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000448 def __init__(self, initial_threads, max_threads, queue_size):
449 logging.debug(
450 'ThreadPool(%d, %d, %d)', initial_threads, max_threads, queue_size)
451 assert initial_threads <= max_threads
452 # Update this check once 256 cores CPU are common.
453 assert max_threads <= 256
454
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000455 self.tasks = self.QUEUE_CLASS(queue_size)
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000456 self._max_threads = max_threads
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000457
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000458 # Mutables.
459 self._num_of_added_tasks_lock = threading.Lock()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000460 self._num_of_added_tasks = 0
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000461 self._outputs_exceptions_cond = threading.Condition()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000462 self._outputs = []
463 self._exceptions = []
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000464 # Number of threads in wait state.
465 self._ready_lock = threading.Lock()
466 self._ready = 0
467 self._workers_lock = threading.Lock()
468 self._workers = []
469 for _ in range(initial_threads):
470 self._add_worker()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000471
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000472 def _add_worker(self):
473 """Adds one worker thread if there isn't too many. Thread-safe."""
474 # Better to take the lock two times than hold it for too long.
475 with self._workers_lock:
476 if len(self._workers) >= self._max_threads:
477 return False
478 worker = threading.Thread(target=self._run)
479 with self._workers_lock:
480 if len(self._workers) >= self._max_threads:
481 return False
482 self._workers.append(worker)
483 worker.daemon = True
484 worker.start()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000485
maruel@chromium.org831958f2013-01-22 15:01:46 +0000486 def add_task(self, priority, func, *args, **kwargs):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000487 """Adds a task, a function to be executed by a worker.
488
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000489 |priority| can adjust the priority of the task versus others. Lower priority
maruel@chromium.org831958f2013-01-22 15:01:46 +0000490 takes precedence.
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000491
492 Returns the index of the item added, e.g. the total number of enqueued items
493 up to now.
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000494 """
maruel@chromium.org831958f2013-01-22 15:01:46 +0000495 assert isinstance(priority, int)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000496 assert callable(func)
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000497 with self._ready_lock:
498 start_new_worker = not self._ready
499 with self._num_of_added_tasks_lock:
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000500 self._num_of_added_tasks += 1
501 index = self._num_of_added_tasks
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000502 self.tasks.put((priority, index, func, args, kwargs))
503 if start_new_worker:
504 self._add_worker()
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000505 return index
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000506
507 def _run(self):
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000508 """Worker thread loop. Runs until a None task is queued."""
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000509 while True:
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000510 try:
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000511 with self._ready_lock:
512 self._ready += 1
513 task = self.tasks.get()
514 finally:
515 with self._ready_lock:
516 self._ready -= 1
517 try:
518 if task is None:
519 # We're done.
520 return
521 _priority, _index, func, args, kwargs = task
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000522 out = func(*args, **kwargs)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000523 if out is not None:
524 self._outputs_exceptions_cond.acquire()
525 try:
526 self._outputs.append(out)
527 self._outputs_exceptions_cond.notifyAll()
528 finally:
529 self._outputs_exceptions_cond.release()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000530 except Exception as e:
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000531 logging.warning('Caught exception: %s', e)
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000532 exc_info = sys.exc_info()
maruel@chromium.org97cd0be2013-03-13 14:01:36 +0000533 logging.info(''.join(traceback.format_tb(exc_info[2])))
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000534 self._outputs_exceptions_cond.acquire()
535 try:
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000536 self._exceptions.append(exc_info)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000537 self._outputs_exceptions_cond.notifyAll()
538 finally:
539 self._outputs_exceptions_cond.release()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000540 finally:
csharp@chromium.org60991182013-03-18 13:44:17 +0000541 try:
542 self.tasks.task_done()
543 except Exception as e:
544 # We need to catch and log this error here because this is the root
545 # function for the thread, nothing higher will catch the error.
546 logging.exception('Caught exception while marking task as done: %s',
547 e)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000548
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000549 def join(self):
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000550 """Extracts all the results from each threads unordered.
551
552 Call repeatedly to extract all the exceptions if desired.
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000553
554 Note: will wait for all work items to be done before returning an exception.
555 To get an exception early, use get_one_result().
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000556 """
557 # TODO(maruel): Stop waiting as soon as an exception is caught.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000558 self.tasks.join()
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000559 self._outputs_exceptions_cond.acquire()
560 try:
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000561 if self._exceptions:
562 e = self._exceptions.pop(0)
563 raise e[0], e[1], e[2]
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000564 out = self._outputs
565 self._outputs = []
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000566 finally:
567 self._outputs_exceptions_cond.release()
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000568 return out
569
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000570 def get_one_result(self):
571 """Returns the next item that was generated or raises an exception if one
572 occured.
573
574 Warning: this function will hang if there is no work item left. Use join
575 instead.
576 """
577 self._outputs_exceptions_cond.acquire()
578 try:
579 while True:
580 if self._exceptions:
581 e = self._exceptions.pop(0)
582 raise e[0], e[1], e[2]
583 if self._outputs:
584 return self._outputs.pop(0)
585 self._outputs_exceptions_cond.wait()
586 finally:
587 self._outputs_exceptions_cond.release()
588
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000589 def close(self):
590 """Closes all the threads."""
591 for _ in range(len(self._workers)):
592 # Enqueueing None causes the worker to stop.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000593 self.tasks.put(None)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000594 for t in self._workers:
595 t.join()
596
597 def __enter__(self):
598 """Enables 'with' statement."""
599 return self
600
maruel@chromium.org97cd0be2013-03-13 14:01:36 +0000601 def __exit__(self, _exc_type, _exc_value, _traceback):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000602 """Enables 'with' statement."""
603 self.close()
604
605
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000606def valid_file(filepath, size):
607 """Determines if the given files appears valid (currently it just checks
608 the file's size)."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000609 if size == UNKNOWN_FILE_SIZE:
610 return True
611 actual_size = os.stat(filepath).st_size
612 if size != actual_size:
613 logging.warning(
614 'Found invalid item %s; %d != %d',
615 os.path.basename(filepath), actual_size, size)
616 return False
617 return True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000618
619
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000620class Profiler(object):
621 def __init__(self, name):
622 self.name = name
623 self.start_time = None
624
625 def __enter__(self):
626 self.start_time = time.time()
627 return self
628
629 def __exit__(self, _exc_type, _exec_value, _traceback):
630 time_taken = time.time() - self.start_time
631 logging.info('Profiling: Section %s took %3.3f seconds',
632 self.name, time_taken)
633
634
635class Remote(object):
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000636 """Priority based worker queue to fetch or upload files from a
637 content-address server. Any function may be given as the fetcher/upload,
638 as long as it takes two inputs (the item contents, and their relative
639 destination).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000640
641 Supports local file system, CIFS or http remotes.
642
643 When the priority of items is equals, works in strict FIFO mode.
644 """
645 # Initial and maximum number of worker threads.
646 INITIAL_WORKERS = 2
647 MAX_WORKERS = 16
648 # Priorities.
649 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
650 INTERNAL_PRIORITY_BITS = (1<<8) - 1
651 RETRIES = 5
652
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000653 def __init__(self, destination_root):
654 # Function to fetch a remote object or upload to a remote location..
655 self._do_item = self.get_file_handler(destination_root)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000656 # Contains tuple(priority, obj).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000657 self._done = Queue.PriorityQueue()
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000658 self._pool = ThreadPool(self.INITIAL_WORKERS, self.MAX_WORKERS, 0)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000659
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000660 def join(self):
661 """Blocks until the queue is empty."""
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000662 return self._pool.join()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000663
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000664 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000665 """Retrieves an object from the remote data store.
666
667 The smaller |priority| gets fetched first.
668
669 Thread-safe.
670 """
671 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000672 return self._add_item(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000673
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000674 def _add_item(self, priority, obj, dest, size):
675 assert isinstance(obj, basestring), obj
676 assert isinstance(dest, basestring), dest
677 assert size is None or isinstance(size, int), size
678 return self._pool.add_task(
679 priority, self._task_executer, priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000680
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000681 def get_one_result(self):
682 return self._pool.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000683
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000684 def _task_executer(self, priority, obj, dest, size):
685 """Wraps self._do_item to trap and retry on IOError exceptions."""
686 try:
687 self._do_item(obj, dest)
688 if size and not valid_file(dest, size):
689 download_size = os.stat(dest).st_size
690 os.remove(dest)
691 raise IOError('File incorrect size after download of %s. Got %s and '
692 'expected %s' % (obj, download_size, size))
693 # TODO(maruel): Technically, we'd want to have an output queue to be a
694 # PriorityQueue.
695 return obj
696 except IOError as e:
697 logging.debug('Caught IOError: %s', e)
698 # Retry a few times, lowering the priority.
699 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
700 self._add_item(priority + 1, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000701 return
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000702 raise
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000703
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +0000704 def get_file_handler(self, file_or_url): # pylint: disable=R0201
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000705 """Returns a object to retrieve objects from a remote."""
706 if re.match(r'^https?://.+$', file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000707 def download_file(item, dest):
708 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
709 # easy.
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000710 try:
csharp@chromium.orgaa2d1512012-12-05 21:17:39 +0000711 zipped_source = file_or_url + item
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000712 logging.debug('download_file(%s)', zipped_source)
csharp@chromium.orge9c8d942013-03-11 20:48:36 +0000713
714 # Because the app engine DB is only eventually consistent, retry
715 # 404 errors because the file might just not be visible yet (even
716 # though it has been uploaded).
717 connection = url_open(zipped_source, retry_404=True)
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000718 if not connection:
719 raise IOError('Unable to open connection to %s' % zipped_source)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000720 decompressor = zlib.decompressobj()
maruel@chromium.org3f039182012-11-27 21:32:41 +0000721 size = 0
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000722 with open(dest, 'wb') as f:
723 while True:
724 chunk = connection.read(ZIPPED_FILE_CHUNK)
725 if not chunk:
726 break
maruel@chromium.org3f039182012-11-27 21:32:41 +0000727 size += len(chunk)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000728 f.write(decompressor.decompress(chunk))
729 # Ensure that all the data was properly decompressed.
730 uncompressed_data = decompressor.flush()
731 assert not uncompressed_data
csharp@chromium.org549669e2013-01-22 19:48:17 +0000732 except IOError:
733 logging.error('Encountered an exception with (%s, %s)' % (item, dest))
734 raise
csharp@chromium.orga110d792013-01-07 16:16:16 +0000735 except httplib.HTTPException as e:
736 raise IOError('Encountered an HTTPException.\n%s' % e)
csharp@chromium.org186d6232012-11-26 14:36:12 +0000737 except zlib.error as e:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000738 # Log the first bytes to see if it's uncompressed data.
739 logging.warning('%r', e[:512])
maruel@chromium.org3f039182012-11-27 21:32:41 +0000740 raise IOError(
741 'Problem unzipping data for item %s. Got %d bytes.\n%s' %
742 (item, size, e))
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000743
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000744 return download_file
745
746 def copy_file(item, dest):
747 source = os.path.join(file_or_url, item)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000748 if source == dest:
749 logging.info('Source and destination are the same, no action required')
750 return
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000751 logging.debug('copy_file(%s, %s)', source, dest)
752 shutil.copy(source, dest)
753 return copy_file
754
755
756class CachePolicies(object):
757 def __init__(self, max_cache_size, min_free_space, max_items):
758 """
759 Arguments:
760 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
761 cache is effectively a leak.
762 - min_free_space: Trim if disk free space becomes lower than this value. If
763 0, it unconditionally fill the disk.
764 - max_items: Maximum number of items to keep in the cache. If 0, do not
765 enforce a limit.
766 """
767 self.max_cache_size = max_cache_size
768 self.min_free_space = min_free_space
769 self.max_items = max_items
770
771
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000772class NoCache(object):
773 """This class is intended to be usable everywhere the Cache class is.
774 Instead of downloading to a cache, all files are downloaded to the target
775 directory and then moved to where they are needed.
776 """
777
778 def __init__(self, target_directory, remote):
779 self.target_directory = target_directory
780 self.remote = remote
781
782 def retrieve(self, priority, item, size):
783 """Get the request file."""
784 self.remote.add_item(priority, item, self.path(item), size)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000785 self.remote.get_one_result()
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000786
787 def wait_for(self, items):
788 """Download the first item of the given list if it is missing."""
789 item = items.iterkeys().next()
790
791 if not os.path.exists(self.path(item)):
792 self.remote.add_item(Remote.MED, item, self.path(item), UNKNOWN_FILE_SIZE)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000793 downloaded = self.remote.get_one_result()
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000794 assert downloaded == item
795
796 return item
797
798 def path(self, item):
799 return os.path.join(self.target_directory, item)
800
801
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000802class Cache(object):
803 """Stateful LRU cache.
804
805 Saves its state as json file.
806 """
807 STATE_FILE = 'state.json'
808
809 def __init__(self, cache_dir, remote, policies):
810 """
811 Arguments:
812 - cache_dir: Directory where to place the cache.
813 - remote: Remote where to fetch items from.
814 - policies: cache retention policies.
815 """
816 self.cache_dir = cache_dir
817 self.remote = remote
818 self.policies = policies
819 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
820 # The tuple(file, size) are kept as an array in a LRU style. E.g.
821 # self.state[0] is the oldest item.
822 self.state = []
maruel@chromium.org770993b2012-12-11 17:16:48 +0000823 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000824 # A lookup map to speed up searching.
825 self._lookup = {}
maruel@chromium.org770993b2012-12-11 17:16:48 +0000826 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000827
828 # Items currently being fetched. Keep it local to reduce lock contention.
829 self._pending_queue = set()
830
831 # Profiling values.
832 self._added = []
833 self._removed = []
834 self._free_disk = 0
835
maruel@chromium.org770993b2012-12-11 17:16:48 +0000836 with Profiler('Setup'):
837 if not os.path.isdir(self.cache_dir):
838 os.makedirs(self.cache_dir)
839 if os.path.isfile(self.state_file):
840 try:
841 self.state = json.load(open(self.state_file, 'r'))
842 except (IOError, ValueError), e:
843 # Too bad. The file will be overwritten and the cache cleared.
844 logging.error(
845 'Broken state file %s, ignoring.\n%s' % (self.STATE_FILE, e))
846 self._state_need_to_be_saved = True
847 if (not isinstance(self.state, list) or
848 not all(
849 isinstance(i, (list, tuple)) and len(i) == 2
850 for i in self.state)):
851 # Discard.
852 self._state_need_to_be_saved = True
853 self.state = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000854
maruel@chromium.org770993b2012-12-11 17:16:48 +0000855 # Ensure that all files listed in the state still exist and add new ones.
856 previous = set(filename for filename, _ in self.state)
857 if len(previous) != len(self.state):
858 logging.warn('Cache state is corrupted, found duplicate files')
859 self._state_need_to_be_saved = True
860 self.state = []
861
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000862 added = 0
863 for filename in os.listdir(self.cache_dir):
864 if filename == self.STATE_FILE:
865 continue
866 if filename in previous:
867 previous.remove(filename)
868 continue
869 # An untracked file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000870 if not RE_IS_SHA1.match(filename):
871 logging.warn('Removing unknown file %s from cache', filename)
872 os.remove(self.path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000873 continue
874 # Insert as the oldest file. It will be deleted eventually if not
875 # accessed.
876 self._add(filename, False)
877 logging.warn('Add unknown file %s to cache', filename)
878 added += 1
879
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000880 if added:
881 logging.warn('Added back %d unknown files', added)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000882 if previous:
883 logging.warn('Removed %d lost files', len(previous))
884 # Set explicitly in case self._add() wasn't called.
885 self._state_need_to_be_saved = True
886 # Filter out entries that were not found while keeping the previous
887 # order.
888 self.state = [
889 (filename, size) for filename, size in self.state
890 if filename not in previous
891 ]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000892 self.trim()
893
894 def __enter__(self):
895 return self
896
897 def __exit__(self, _exc_type, _exec_value, _traceback):
898 with Profiler('CleanupTrimming'):
899 self.trim()
900
901 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000902 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000903 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000904 '%5d (%8dkb) current',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000905 len(self.state),
906 sum(i[1] for i in self.state) / 1024)
907 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000908 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
909 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000910
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000911 def remove_file_at_index(self, index):
912 """Removes the file at the given index."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000913 try:
maruel@chromium.org770993b2012-12-11 17:16:48 +0000914 self._state_need_to_be_saved = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000915 filename, size = self.state.pop(index)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000916 # If the lookup was already stale, its possible the filename was not
917 # present yet.
918 self._lookup_is_stale = True
919 self._lookup.pop(filename, None)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000920 self._removed.append(size)
921 os.remove(self.path(filename))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000922 except OSError as e:
923 logging.error('Error attempting to delete a file\n%s' % e)
924
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000925 def remove_lru_file(self):
926 """Removes the last recently used file."""
927 self.remove_file_at_index(0)
928
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000929 def trim(self):
930 """Trims anything we don't know, make sure enough free space exists."""
931 # Ensure maximum cache size.
932 if self.policies.max_cache_size and self.state:
933 while sum(i[1] for i in self.state) > self.policies.max_cache_size:
934 self.remove_lru_file()
935
936 # Ensure maximum number of items in the cache.
937 if self.policies.max_items and self.state:
938 while len(self.state) > self.policies.max_items:
939 self.remove_lru_file()
940
941 # Ensure enough free space.
942 self._free_disk = get_free_space(self.cache_dir)
943 while (
944 self.policies.min_free_space and
945 self.state and
946 self._free_disk < self.policies.min_free_space):
947 self.remove_lru_file()
948 self._free_disk = get_free_space(self.cache_dir)
949
950 self.save()
951
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000952 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000953 """Retrieves a file from the remote, if not already cached, and adds it to
954 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000955
956 If the file is in the cache, verifiy that the file is valid (i.e. it is
957 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000958 """
959 assert not '/' in item
960 path = self.path(item)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000961 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000962 index = self._lookup.get(item)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000963
964 if index is not None:
965 if not valid_file(self.path(item), size):
966 self.remove_file_at_index(index)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000967 index = None
968 else:
969 assert index < len(self.state)
970 # Was already in cache. Update it's LRU value by putting it at the end.
maruel@chromium.org770993b2012-12-11 17:16:48 +0000971 self._state_need_to_be_saved = True
972 self._lookup_is_stale = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000973 self.state.append(self.state.pop(index))
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000974
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000975 if index is None:
976 if item in self._pending_queue:
977 # Already pending. The same object could be referenced multiple times.
978 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000979 self.remote.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000980 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000981
982 def add(self, filepath, obj):
983 """Forcibly adds a file to the cache."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000984 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000985 if not obj in self._lookup:
986 link_file(self.path(obj), filepath, HARDLINK)
987 self._add(obj, True)
988
989 def path(self, item):
990 """Returns the path to one item."""
991 return os.path.join(self.cache_dir, item)
992
993 def save(self):
994 """Saves the LRU ordering."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000995 if self._state_need_to_be_saved:
996 json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':'))
997 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000998
999 def wait_for(self, items):
1000 """Starts a loop that waits for at least one of |items| to be retrieved.
1001
1002 Returns the first item retrieved.
1003 """
1004 # Flush items already present.
maruel@chromium.org770993b2012-12-11 17:16:48 +00001005 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001006 for item in items:
1007 if item in self._lookup:
1008 return item
1009
1010 assert all(i in self._pending_queue for i in items), (
1011 items, self._pending_queue)
1012 # Note that:
1013 # len(self._pending_queue) ==
1014 # ( len(self.remote._workers) - self.remote._ready +
1015 # len(self._remote._queue) + len(self._remote.done))
1016 # There is no lock-free way to verify that.
1017 while self._pending_queue:
maruel@chromium.org13eca0b2013-01-22 16:42:21 +00001018 item = self.remote.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001019 self._pending_queue.remove(item)
1020 self._add(item, True)
1021 if item in items:
1022 return item
1023
1024 def _add(self, item, at_end):
1025 """Adds an item in the internal state.
1026
1027 If |at_end| is False, self._lookup becomes inconsistent and
1028 self._update_lookup() must be called.
1029 """
1030 size = os.stat(self.path(item)).st_size
1031 self._added.append(size)
maruel@chromium.org770993b2012-12-11 17:16:48 +00001032 self._state_need_to_be_saved = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001033 if at_end:
1034 self.state.append((item, size))
1035 self._lookup[item] = len(self.state) - 1
1036 else:
maruel@chromium.org770993b2012-12-11 17:16:48 +00001037 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001038 self.state.insert(0, (item, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001039
1040 def _update_lookup(self):
maruel@chromium.org770993b2012-12-11 17:16:48 +00001041 if self._lookup_is_stale:
1042 self._lookup = dict(
1043 (filename, index) for index, (filename, _) in enumerate(self.state))
1044 self._lookup_is_stale = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001045
1046
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001047class IsolatedFile(object):
1048 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001049 def __init__(self, obj_hash):
1050 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001051 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001052 self.obj_hash = obj_hash
1053 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001054 # .isolate and all the .isolated files recursively included by it with
1055 # 'includes' key. The order of each sha-1 in 'includes', each representing a
1056 # .isolated file in the hash table, is important, as the later ones are not
1057 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001058 self.can_fetch = False
1059
1060 # Raw data.
1061 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001062 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001063 self.children = []
1064
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001065 # Set once the .isolated file is loaded.
1066 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001067 # Set once the files are fetched.
1068 self.files_fetched = False
1069
1070 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001071 """Verifies the .isolated file is valid and loads this object with the json
1072 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001073 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001074 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
1075 assert not self._is_parsed
1076 self.data = load_isolated(content)
1077 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
1078 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001079
1080 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001081 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001082
1083 Preemptively request files.
1084
1085 Note that |files| is modified by this function.
1086 """
1087 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001088 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001089 return
1090 logging.debug('fetch_files(%s)' % self.obj_hash)
1091 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001092 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001093 # overriden files must not be fetched.
1094 if filepath not in files:
1095 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001096 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001097 # Preemptively request files.
1098 logging.debug('fetching %s' % filepath)
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001099 cache.retrieve(Remote.MED, properties['h'], properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001100 self.files_fetched = True
1101
1102
1103class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001104 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001105 def __init__(self):
1106 self.command = []
1107 self.files = {}
1108 self.read_only = None
1109 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001110 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001111 self.root = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001112
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001113 def load(self, cache, root_isolated_hash):
1114 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001115
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001116 It enables support for "included" .isolated files. They are processed in
1117 strict order but fetched asynchronously from the cache. This is important so
1118 that a file in an included .isolated file that is overridden by an embedding
1119 .isolated file is not fetched neededlessly. The includes are fetched in one
1120 pass and the files are fetched as soon as all the ones on the left-side
1121 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001122
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001123 The prioritization is very important here for nested .isolated files.
1124 'includes' have the highest priority and the algorithm is optimized for both
1125 deep and wide trees. A deep one is a long link of .isolated files referenced
1126 one at a time by one item in 'includes'. A wide one has a large number of
1127 'includes' in a single .isolated file. 'left' is defined as an included
1128 .isolated file earlier in the 'includes' list. So the order of the elements
1129 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001130 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001131 self.root = IsolatedFile(root_isolated_hash)
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001132 cache.retrieve(Remote.HIGH, root_isolated_hash, UNKNOWN_FILE_SIZE)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001133 pending = {root_isolated_hash: self.root}
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001134 # Keeps the list of retrieved items to refuse recursive includes.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001135 retrieved = [root_isolated_hash]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001136
1137 def update_self(node):
1138 node.fetch_files(cache, self.files)
1139 # Grabs properties.
1140 if not self.command and node.data.get('command'):
1141 self.command = node.data['command']
1142 if self.read_only is None and node.data.get('read_only') is not None:
1143 self.read_only = node.data['read_only']
1144 if (self.relative_cwd is None and
1145 node.data.get('relative_cwd') is not None):
1146 self.relative_cwd = node.data['relative_cwd']
1147
1148 def traverse_tree(node):
1149 if node.can_fetch:
1150 if not node.files_fetched:
1151 update_self(node)
1152 will_break = False
1153 for i in node.children:
1154 if not i.can_fetch:
1155 if will_break:
1156 break
1157 # Automatically mark the first one as fetcheable.
1158 i.can_fetch = True
1159 will_break = True
1160 traverse_tree(i)
1161
1162 while pending:
1163 item_hash = cache.wait_for(pending)
1164 item = pending.pop(item_hash)
1165 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001166 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001167 # It's the root item.
1168 item.can_fetch = True
1169
1170 for new_child in item.children:
1171 h = new_child.obj_hash
1172 if h in retrieved:
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001173 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001174 pending[h] = new_child
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001175 cache.retrieve(Remote.HIGH, h, UNKNOWN_FILE_SIZE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001176
1177 # Traverse the whole tree to see if files can now be fetched.
1178 traverse_tree(self.root)
1179 def check(n):
1180 return all(check(x) for x in n.children) and n.files_fetched
1181 assert check(self.root)
1182 self.relative_cwd = self.relative_cwd or ''
1183 self.read_only = self.read_only or False
1184
1185
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001186def create_directories(base_directory, files):
1187 """Creates the directory structure needed by the given list of files."""
1188 logging.debug('create_directories(%s, %d)', base_directory, len(files))
1189 # Creates the tree of directories to create.
1190 directories = set(os.path.dirname(f) for f in files)
1191 for item in list(directories):
1192 while item:
1193 directories.add(item)
1194 item = os.path.dirname(item)
1195 for d in sorted(directories):
1196 if d:
1197 os.mkdir(os.path.join(base_directory, d))
1198
1199
1200def create_links(base_directory, files):
1201 """Creates any links needed by the given set of files."""
1202 for filepath, properties in files:
1203 if 'link' not in properties:
1204 continue
1205 outfile = os.path.join(base_directory, filepath)
1206 # symlink doesn't exist on Windows. So the 'link' property should
1207 # never be specified for windows .isolated file.
1208 os.symlink(properties['l'], outfile) # pylint: disable=E1101
1209 if 'm' in properties:
1210 lchmod = getattr(os, 'lchmod', None)
1211 if lchmod:
1212 lchmod(outfile, properties['m'])
1213
1214
1215def setup_commands(base_directory, cwd, cmd):
1216 """Correctly adjusts and then returns the required working directory
1217 and command needed to run the test.
1218 """
1219 assert not os.path.isabs(cwd), 'The cwd must be a relative path, got %s' % cwd
1220 cwd = os.path.join(base_directory, cwd)
1221 if not os.path.isdir(cwd):
1222 os.makedirs(cwd)
1223
1224 # Ensure paths are correctly separated on windows.
1225 cmd[0] = cmd[0].replace('/', os.path.sep)
1226 cmd = fix_python_path(cmd)
1227
1228 return cwd, cmd
1229
1230
1231def generate_remaining_files(files):
1232 """Generates a dictionary of all the remaining files to be downloaded."""
1233 remaining = {}
1234 for filepath, props in files:
1235 if 'h' in props:
1236 remaining.setdefault(props['h'], []).append((filepath, props))
1237
1238 return remaining
1239
1240
1241def download_test_data(isolated_hash, target_directory, remote):
1242 """Downloads the dependencies to the given directory."""
1243 if not os.path.exists(target_directory):
1244 os.makedirs(target_directory)
1245
1246 settings = Settings()
1247 no_cache = NoCache(target_directory, Remote(remote))
1248
1249 # Download all the isolated files.
1250 with Profiler('GetIsolateds') as _prof:
1251 settings.load(no_cache, isolated_hash)
1252
1253 if not settings.command:
1254 print >> sys.stderr, 'No command to run'
1255 return 1
1256
1257 with Profiler('GetRest') as _prof:
1258 create_directories(target_directory, settings.files)
1259 create_links(target_directory, settings.files.iteritems())
1260
1261 cwd, cmd = setup_commands(target_directory, settings.relative_cwd,
1262 settings.command[:])
1263
1264 remaining = generate_remaining_files(settings.files.iteritems())
1265
1266 # Now block on the remaining files to be downloaded and mapped.
1267 logging.info('Retrieving remaining files')
1268 last_update = time.time()
1269 while remaining:
1270 obj = no_cache.wait_for(remaining)
1271 files = remaining.pop(obj)
1272
1273 for i, (filepath, properties) in enumerate(files):
1274 outfile = os.path.join(target_directory, filepath)
1275 logging.info(no_cache.path(obj))
1276
1277 if i + 1 == len(files):
1278 os.rename(no_cache.path(obj), outfile)
1279 else:
1280 shutil.copyfile(no_cache.path(obj), outfile)
1281
1282 if 'm' in properties:
1283 # It's not set on Windows.
1284 os.chmod(outfile, properties['m'])
1285
1286 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
1287 logging.info('%d files remaining...' % len(remaining))
1288 last_update = time.time()
1289
1290 print('.isolated files successfully downloaded and setup in %s' %
1291 target_directory)
1292 print('To run this test please run the command %s from the directory %s' %
1293 (cmd, cwd))
1294
1295 return 0
1296
1297
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001298def run_tha_test(isolated_hash, cache_dir, remote, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001299 """Downloads the dependencies in the cache, hardlinks them into a temporary
1300 directory and runs the executable.
1301 """
1302 settings = Settings()
1303 with Cache(cache_dir, Remote(remote), policies) as cache:
1304 outdir = make_temp_dir('run_tha_test', cache_dir)
1305 try:
1306 # Initiate all the files download.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001307 with Profiler('GetIsolateds') as _prof:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001308 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001309 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001310 # Adds it in the cache. While not strictly necessary, this simplifies
1311 # the rest.
maruel@chromium.orgcb3c3d52013-03-14 18:55:30 +00001312 h = hashlib.sha1(open(isolated_hash, 'rb').read()).hexdigest()
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001313 cache.add(isolated_hash, h)
1314 isolated_hash = h
1315 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001316
1317 if not settings.command:
1318 print >> sys.stderr, 'No command to run'
1319 return 1
1320
1321 with Profiler('GetRest') as _prof:
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001322 create_directories(outdir, settings.files)
1323 create_links(outdir, settings.files.iteritems())
1324 remaining = generate_remaining_files(settings.files.iteritems())
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001325
1326 # Do bookkeeping while files are being downloaded in the background.
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001327 cwd, cmd = setup_commands(outdir, settings.relative_cwd,
1328 settings.command[:])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001329
1330 # Now block on the remaining files to be downloaded and mapped.
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001331 logging.info('Retrieving remaining files')
1332 last_update = time.time()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001333 while remaining:
1334 obj = cache.wait_for(remaining)
1335 for filepath, properties in remaining.pop(obj):
1336 outfile = os.path.join(outdir, filepath)
1337 link_file(outfile, cache.path(obj), HARDLINK)
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001338 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001339 # It's not set on Windows.
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001340 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001341
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001342 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
1343 logging.info('%d files remaining...' % len(remaining))
1344 last_update = time.time()
1345
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001346 if settings.read_only:
1347 make_writable(outdir, True)
1348 logging.info('Running %s, cwd=%s' % (cmd, cwd))
csharp@chromium.orge217f302012-11-22 16:51:53 +00001349
1350 # TODO(csharp): This should be specified somewhere else.
1351 # Add a rotating log file if one doesn't already exist.
1352 env = os.environ.copy()
1353 env.setdefault('RUN_TEST_CASES_LOG_FILE', RUN_TEST_CASES_LOG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001354 try:
1355 with Profiler('RunTest') as _prof:
csharp@chromium.orge217f302012-11-22 16:51:53 +00001356 return subprocess.call(cmd, cwd=cwd, env=env)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001357 except OSError:
1358 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1359 raise
1360 finally:
1361 rmtree(outdir)
1362
1363
1364def main():
maruel@chromium.org46e61cc2013-03-25 19:55:34 +00001365 disable_buffering()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001366 parser = optparse.OptionParser(
1367 usage='%prog <options>', description=sys.modules[__name__].__doc__)
1368 parser.add_option(
1369 '-v', '--verbose', action='count', default=0, help='Use multiple times')
1370 parser.add_option('--no-run', action='store_true', help='Skip the run part')
1371
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001372 group = optparse.OptionGroup(parser, 'Download')
1373 group.add_option(
1374 '--download', metavar='DEST',
1375 help='Downloads files to DEST and returns without running, instead of '
1376 'downloading and then running from a temporary directory.')
1377 parser.add_option_group(group)
1378
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001379 group = optparse.OptionGroup(parser, 'Data source')
1380 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001381 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001382 metavar='FILE',
1383 help='File/url describing what to map or run')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001384 # TODO(maruel): Remove once not used anymore.
1385 group.add_option(
1386 '-m', '--manifest', dest='isolated', help=optparse.SUPPRESS_HELP)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001387 group.add_option(
1388 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001389 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001390 parser.add_option_group(group)
1391
1392 group.add_option(
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001393 '-r', '--remote', metavar='URL',
1394 default=
1395 'https://isolateserver.appspot.com/content/retrieve/default-gzip/',
1396 help='Remote where to get the items. Defaults to %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001397 group = optparse.OptionGroup(parser, 'Cache management')
1398 group.add_option(
1399 '--cache',
1400 default='cache',
1401 metavar='DIR',
1402 help='Cache directory, default=%default')
1403 group.add_option(
1404 '--max-cache-size',
1405 type='int',
1406 metavar='NNN',
1407 default=20*1024*1024*1024,
1408 help='Trim if the cache gets larger than this value, default=%default')
1409 group.add_option(
1410 '--min-free-space',
1411 type='int',
1412 metavar='NNN',
1413 default=1*1024*1024*1024,
1414 help='Trim if disk free space becomes lower than this value, '
1415 'default=%default')
1416 group.add_option(
1417 '--max-items',
1418 type='int',
1419 metavar='NNN',
1420 default=100000,
1421 help='Trim if more than this number of items are in the cache '
1422 'default=%default')
1423 parser.add_option_group(group)
1424
1425 options, args = parser.parse_args()
1426 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
csharp@chromium.orgff2a4662012-11-21 20:49:32 +00001427
1428 logging_console = logging.StreamHandler()
1429 logging_console.setFormatter(logging.Formatter(
1430 '%(levelname)5s %(module)15s(%(lineno)3d): %(message)s'))
1431 logging_console.setLevel(level)
1432 logging.getLogger().addHandler(logging_console)
1433
1434 logging_rotating_file = logging.handlers.RotatingFileHandler(
1435 RUN_ISOLATED_LOG_FILE,
1436 maxBytes=10 * 1024 * 1024, backupCount=5)
1437 logging_rotating_file.setLevel(logging.DEBUG)
1438 logging_rotating_file.setFormatter(logging.Formatter(
1439 '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)3d): %(message)s'))
1440 logging.getLogger().addHandler(logging_rotating_file)
1441
1442 logging.getLogger().setLevel(logging.DEBUG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001443
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001444 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001445 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001446 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001447 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001448 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001449 parser.error('Unsupported args %s' % ' '.join(args))
1450
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001451 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001452 policies = CachePolicies(
1453 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001454
1455 if options.download:
1456 return download_test_data(options.isolated or options.hash,
1457 options.download, options.remote)
1458 else:
1459 try:
1460 return run_tha_test(
1461 options.isolated or options.hash,
1462 options.cache,
1463 options.remote,
1464 policies)
1465 except Exception, e:
1466 # Make sure any exception is logged.
1467 logging.exception(e)
1468 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001469
1470
1471if __name__ == '__main__':
1472 sys.exit(main())