blob: e768da1df2631c81c1c215aa4be907b5ba8e4e59 [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
93def os_link(source, link_name):
94 """Add support for os.link() on Windows."""
95 if sys.platform == 'win32':
96 if not ctypes.windll.kernel32.CreateHardLinkW(
97 unicode(link_name), unicode(source), 0):
98 raise OSError()
99 else:
100 os.link(source, link_name)
101
102
103def readable_copy(outfile, infile):
104 """Makes a copy of the file that is readable by everyone."""
105 shutil.copy(infile, outfile)
106 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
107 stat.S_IRGRP | stat.S_IROTH)
108 os.chmod(outfile, read_enabled_mode)
109
110
111def link_file(outfile, infile, action):
112 """Links a file. The type of link depends on |action|."""
113 logging.debug('Mapping %s to %s' % (infile, outfile))
114 if action not in (HARDLINK, SYMLINK, COPY):
115 raise ValueError('Unknown mapping action %s' % action)
116 if not os.path.isfile(infile):
117 raise MappingError('%s is missing' % infile)
118 if os.path.isfile(outfile):
119 raise MappingError(
120 '%s already exist; insize:%d; outsize:%d' %
121 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
122
123 if action == COPY:
124 readable_copy(outfile, infile)
125 elif action == SYMLINK and sys.platform != 'win32':
126 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000127 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000128 else:
129 try:
130 os_link(infile, outfile)
131 except OSError:
132 # Probably a different file system.
133 logging.warn(
134 'Failed to hardlink, failing back to copy %s to %s' % (
135 infile, outfile))
136 readable_copy(outfile, infile)
137
138
139def _set_write_bit(path, read_only):
140 """Sets or resets the executable bit on a file or directory."""
141 mode = os.lstat(path).st_mode
142 if read_only:
143 mode = mode & 0500
144 else:
145 mode = mode | 0200
146 if hasattr(os, 'lchmod'):
147 os.lchmod(path, mode) # pylint: disable=E1101
148 else:
149 if stat.S_ISLNK(mode):
150 # Skip symlink without lchmod() support.
151 logging.debug('Can\'t change +w bit on symlink %s' % path)
152 return
153
154 # TODO(maruel): Implement proper DACL modification on Windows.
155 os.chmod(path, mode)
156
157
158def make_writable(root, read_only):
159 """Toggle the writable bit on a directory tree."""
csharp@chromium.org837352f2013-01-17 21:17:03 +0000160 assert os.path.isabs(root), root
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000161 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
162 for filename in filenames:
163 _set_write_bit(os.path.join(dirpath, filename), read_only)
164
165 for dirname in dirnames:
166 _set_write_bit(os.path.join(dirpath, dirname), read_only)
167
168
169def rmtree(root):
170 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
171 make_writable(root, False)
172 if sys.platform == 'win32':
173 for i in range(3):
174 try:
175 shutil.rmtree(root)
176 break
177 except WindowsError: # pylint: disable=E0602
178 delay = (i+1)*2
179 print >> sys.stderr, (
180 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
181 time.sleep(delay)
182 else:
183 shutil.rmtree(root)
184
185
186def is_same_filesystem(path1, path2):
187 """Returns True if both paths are on the same filesystem.
188
189 This is required to enable the use of hardlinks.
190 """
191 assert os.path.isabs(path1), path1
192 assert os.path.isabs(path2), path2
193 if sys.platform == 'win32':
194 # If the drive letter mismatches, assume it's a separate partition.
195 # TODO(maruel): It should look at the underlying drive, a drive letter could
196 # be a mount point to a directory on another drive.
197 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
198 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
199 if path1[0].lower() != path2[0].lower():
200 return False
201 return os.stat(path1).st_dev == os.stat(path2).st_dev
202
203
204def get_free_space(path):
205 """Returns the number of free bytes."""
206 if sys.platform == 'win32':
207 free_bytes = ctypes.c_ulonglong(0)
208 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
209 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
210 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000211 # For OSes other than Windows.
212 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000213 return f.f_bfree * f.f_frsize
214
215
216def make_temp_dir(prefix, root_dir):
217 """Returns a temporary directory on the same file system as root_dir."""
218 base_temp_dir = None
219 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
220 base_temp_dir = os.path.dirname(root_dir)
221 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
222
223
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000224def load_isolated(content):
225 """Verifies the .isolated file is valid and loads this object with the json
226 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000227 """
228 try:
229 data = json.loads(content)
230 except ValueError:
231 raise ConfigError('Failed to parse: %s...' % content[:100])
232
233 if not isinstance(data, dict):
234 raise ConfigError('Expected dict, got %r' % data)
235
236 for key, value in data.iteritems():
237 if key == 'command':
238 if not isinstance(value, list):
239 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000240 if not value:
241 raise ConfigError('Expected non-empty command')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000242 for subvalue in value:
243 if not isinstance(subvalue, basestring):
244 raise ConfigError('Expected string, got %r' % subvalue)
245
246 elif key == 'files':
247 if not isinstance(value, dict):
248 raise ConfigError('Expected dict, got %r' % value)
249 for subkey, subvalue in value.iteritems():
250 if not isinstance(subkey, basestring):
251 raise ConfigError('Expected string, got %r' % subkey)
252 if not isinstance(subvalue, dict):
253 raise ConfigError('Expected dict, got %r' % subvalue)
254 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000255 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000256 if not isinstance(subsubvalue, basestring):
257 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000258 elif subsubkey == 'm':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000259 if not isinstance(subsubvalue, int):
260 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000261 elif subsubkey == 'h':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000262 if not RE_IS_SHA1.match(subsubvalue):
263 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000264 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000265 if not isinstance(subsubvalue, int):
266 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000267 else:
268 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000269 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000270 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000271 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
272 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000273
274 elif key == 'includes':
275 if not isinstance(value, list):
276 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000277 if not value:
278 raise ConfigError('Expected non-empty includes list')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000279 for subvalue in value:
280 if not RE_IS_SHA1.match(subvalue):
281 raise ConfigError('Expected sha-1, got %r' % subvalue)
282
283 elif key == 'read_only':
284 if not isinstance(value, bool):
285 raise ConfigError('Expected bool, got %r' % value)
286
287 elif key == 'relative_cwd':
288 if not isinstance(value, basestring):
289 raise ConfigError('Expected string, got %r' % value)
290
291 elif key == 'os':
292 if value != get_flavor():
293 raise ConfigError(
294 'Expected \'os\' to be \'%s\' but got \'%s\'' %
295 (get_flavor(), value))
296
297 else:
298 raise ConfigError('Unknown key %s' % key)
299
300 return data
301
302
303def fix_python_path(cmd):
304 """Returns the fixed command line to call the right python executable."""
305 out = cmd[:]
306 if out[0] == 'python':
307 out[0] = sys.executable
308 elif out[0].endswith('.py'):
309 out.insert(0, sys.executable)
310 return out
311
312
maruel@chromium.orgef333122013-03-12 20:36:40 +0000313def url_open(url, data=None, retry_404=False, content_type=None):
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000314 """Attempts to open the given url multiple times.
315
316 |data| can be either:
317 -None for a GET request
318 -str for pre-encoded data
319 -list for data to be encoded
320 -dict for data to be encoded (COUNT_KEY will be added in this case)
321
322 If no wait_duration is given, the default wait time will exponentially
323 increase between each retry.
324
325 Returns a file-like object, where the response may be read from, or None
326 if it was unable to connect.
327 """
328 method = 'GET' if data is None else 'POST'
329
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000330 if isinstance(data, dict) and COUNT_KEY in data:
331 logging.error('%s already existed in the data passed into UlrOpen. It '
332 'would be overwritten. Aborting UrlOpen', COUNT_KEY)
333 return None
334
maruel@chromium.orgef333122013-03-12 20:36:40 +0000335 assert not ((method != 'POST') and content_type), (
336 'Can\'t use content_type on GET')
337
338 def make_request(extra):
339 """Returns a urllib2.Request instance for this specific retry."""
340 if isinstance(data, str) or data is None:
341 payload = data
342 else:
343 if isinstance(data, dict):
344 payload = data.items()
345 else:
346 payload = data[:]
347 payload.extend(extra.iteritems())
348 payload = urllib.urlencode(payload)
349
350 new_url = url
351 if isinstance(data, str) or data is None:
352 # In these cases, add the extra parameter to the query part of the url.
353 url_parts = list(urlparse.urlparse(new_url))
354 # Append the query parameter.
355 if url_parts[4] and extra:
356 url_parts[4] += '&'
357 url_parts[4] += urllib.urlencode(extra)
358 new_url = urlparse.urlunparse(url_parts)
359
360 request = urllib2.Request(new_url, data=payload)
361 if payload is not None:
362 if content_type:
363 request.add_header('Content-Type', content_type)
364 request.add_header('Content-Length', len(payload))
365 return request
366
367 return url_open_request(make_request, retry_404)
368
369
370def url_open_request(make_request, retry_404=False):
371 """Internal version of url_open() for users that need special handling.
372 """
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000373 last_error = None
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000374 for attempt in range(MAX_URL_OPEN_ATTEMPTS):
maruel@chromium.orgef333122013-03-12 20:36:40 +0000375 extra = {COUNT_KEY: attempt} if attempt else {}
376 request = make_request(extra)
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000377 try:
maruel@chromium.orgef333122013-03-12 20:36:40 +0000378 url_response = urllib2.urlopen(request)
maruel@chromium.orgf04becf2013-03-14 19:09:11 +0000379 logging.debug('url_open(%s) succeeded', request.get_full_url())
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000380 return url_response
381 except urllib2.HTTPError as e:
csharp@chromium.orge9c8d942013-03-11 20:48:36 +0000382 if e.code < 500 and not (retry_404 and e.code == 404):
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000383 # This HTTPError means we reached the server and there was a problem
384 # with the request, so don't retry.
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000385 logging.error(
386 'Able to connect to %s but an exception was thrown.\n%s\n%s',
387 request.get_full_url(), e, e.read())
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000388 return None
389
390 # The HTTPError was due to a server error, so retry the attempt.
391 logging.warning('Able to connect to %s on attempt %d.\nException: %s ',
maruel@chromium.orgef333122013-03-12 20:36:40 +0000392 request.get_full_url(), attempt, e)
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000393 last_error = e
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000394
395 except (urllib2.URLError, httplib.HTTPException) as e:
396 logging.warning('Unable to open url %s on attempt %d.\nException: %s',
maruel@chromium.orgef333122013-03-12 20:36:40 +0000397 request.get_full_url(), attempt, e)
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000398 last_error = e
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000399
400 # Only sleep if we are going to try again.
401 if attempt != MAX_URL_OPEN_ATTEMPTS - 1:
402 duration = random.random() * 3 + math.pow(1.5, (attempt + 1))
403 duration = min(10, max(0.1, duration))
404 time.sleep(duration)
405
maruel@chromium.org8ac504b2013-03-13 16:25:45 +0000406 logging.error('Unable to open given url, %s, after %d attempts.\n%s',
407 request.get_full_url(), MAX_URL_OPEN_ATTEMPTS, last_error)
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000408 return None
409
410
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000411class ThreadPool(object):
412 """Implements a multithreaded worker pool oriented for mapping jobs with
413 thread-local result storage.
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000414
415 Arguments:
416 - initial_threads: Number of threads to start immediately. Can be 0 if it is
417 uncertain that threads will be needed.
418 - max_threads: Maximum number of threads that will be started when all the
419 threads are busy working. Often the number of CPU cores.
420 - queue_size: Maximum number of tasks to buffer in the queue. 0 for unlimited
421 queue. A non-zero value may make add_task() blocking.
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000422 """
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000423 QUEUE_CLASS = Queue.PriorityQueue
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000424
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000425 def __init__(self, initial_threads, max_threads, queue_size):
426 logging.debug(
427 'ThreadPool(%d, %d, %d)', initial_threads, max_threads, queue_size)
428 assert initial_threads <= max_threads
429 # Update this check once 256 cores CPU are common.
430 assert max_threads <= 256
431
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000432 self.tasks = self.QUEUE_CLASS(queue_size)
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000433 self._max_threads = max_threads
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000434
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000435 # Mutables.
436 self._num_of_added_tasks_lock = threading.Lock()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000437 self._num_of_added_tasks = 0
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000438 self._outputs_exceptions_cond = threading.Condition()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000439 self._outputs = []
440 self._exceptions = []
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000441 # Number of threads in wait state.
442 self._ready_lock = threading.Lock()
443 self._ready = 0
444 self._workers_lock = threading.Lock()
445 self._workers = []
446 for _ in range(initial_threads):
447 self._add_worker()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000448
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000449 def _add_worker(self):
450 """Adds one worker thread if there isn't too many. Thread-safe."""
451 # Better to take the lock two times than hold it for too long.
452 with self._workers_lock:
453 if len(self._workers) >= self._max_threads:
454 return False
455 worker = threading.Thread(target=self._run)
456 with self._workers_lock:
457 if len(self._workers) >= self._max_threads:
458 return False
459 self._workers.append(worker)
460 worker.daemon = True
461 worker.start()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000462
maruel@chromium.org831958f2013-01-22 15:01:46 +0000463 def add_task(self, priority, func, *args, **kwargs):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000464 """Adds a task, a function to be executed by a worker.
465
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000466 |priority| can adjust the priority of the task versus others. Lower priority
maruel@chromium.org831958f2013-01-22 15:01:46 +0000467 takes precedence.
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000468
469 Returns the index of the item added, e.g. the total number of enqueued items
470 up to now.
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000471 """
maruel@chromium.org831958f2013-01-22 15:01:46 +0000472 assert isinstance(priority, int)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000473 assert callable(func)
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000474 with self._ready_lock:
475 start_new_worker = not self._ready
476 with self._num_of_added_tasks_lock:
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000477 self._num_of_added_tasks += 1
478 index = self._num_of_added_tasks
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000479 self.tasks.put((priority, index, func, args, kwargs))
480 if start_new_worker:
481 self._add_worker()
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000482 return index
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000483
484 def _run(self):
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000485 """Worker thread loop. Runs until a None task is queued."""
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000486 while True:
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000487 try:
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000488 with self._ready_lock:
489 self._ready += 1
490 task = self.tasks.get()
491 finally:
492 with self._ready_lock:
493 self._ready -= 1
494 try:
495 if task is None:
496 # We're done.
497 return
498 _priority, _index, func, args, kwargs = task
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000499 out = func(*args, **kwargs)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000500 if out is not None:
501 self._outputs_exceptions_cond.acquire()
502 try:
503 self._outputs.append(out)
504 self._outputs_exceptions_cond.notifyAll()
505 finally:
506 self._outputs_exceptions_cond.release()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000507 except Exception as e:
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000508 logging.warning('Caught exception: %s', e)
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000509 exc_info = sys.exc_info()
maruel@chromium.org97cd0be2013-03-13 14:01:36 +0000510 logging.info(''.join(traceback.format_tb(exc_info[2])))
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000511 self._outputs_exceptions_cond.acquire()
512 try:
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000513 self._exceptions.append(exc_info)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000514 self._outputs_exceptions_cond.notifyAll()
515 finally:
516 self._outputs_exceptions_cond.release()
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000517 finally:
csharp@chromium.org60991182013-03-18 13:44:17 +0000518 try:
519 self.tasks.task_done()
520 except Exception as e:
521 # We need to catch and log this error here because this is the root
522 # function for the thread, nothing higher will catch the error.
523 logging.exception('Caught exception while marking task as done: %s',
524 e)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000525
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000526 def join(self):
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000527 """Extracts all the results from each threads unordered.
528
529 Call repeatedly to extract all the exceptions if desired.
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000530
531 Note: will wait for all work items to be done before returning an exception.
532 To get an exception early, use get_one_result().
maruel@chromium.org5a1446a2013-01-17 15:13:27 +0000533 """
534 # TODO(maruel): Stop waiting as soon as an exception is caught.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000535 self.tasks.join()
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000536 self._outputs_exceptions_cond.acquire()
537 try:
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000538 if self._exceptions:
539 e = self._exceptions.pop(0)
540 raise e[0], e[1], e[2]
maruel@chromium.org6b0c9ec2013-01-18 00:34:31 +0000541 out = self._outputs
542 self._outputs = []
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000543 finally:
544 self._outputs_exceptions_cond.release()
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000545 return out
546
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000547 def get_one_result(self):
548 """Returns the next item that was generated or raises an exception if one
549 occured.
550
551 Warning: this function will hang if there is no work item left. Use join
552 instead.
553 """
554 self._outputs_exceptions_cond.acquire()
555 try:
556 while True:
557 if self._exceptions:
558 e = self._exceptions.pop(0)
559 raise e[0], e[1], e[2]
560 if self._outputs:
561 return self._outputs.pop(0)
562 self._outputs_exceptions_cond.wait()
563 finally:
564 self._outputs_exceptions_cond.release()
565
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000566 def close(self):
567 """Closes all the threads."""
568 for _ in range(len(self._workers)):
569 # Enqueueing None causes the worker to stop.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000570 self.tasks.put(None)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000571 for t in self._workers:
572 t.join()
573
574 def __enter__(self):
575 """Enables 'with' statement."""
576 return self
577
maruel@chromium.org97cd0be2013-03-13 14:01:36 +0000578 def __exit__(self, _exc_type, _exc_value, _traceback):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000579 """Enables 'with' statement."""
580 self.close()
581
582
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000583def valid_file(filepath, size):
584 """Determines if the given files appears valid (currently it just checks
585 the file's size)."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000586 if size == UNKNOWN_FILE_SIZE:
587 return True
588 actual_size = os.stat(filepath).st_size
589 if size != actual_size:
590 logging.warning(
591 'Found invalid item %s; %d != %d',
592 os.path.basename(filepath), actual_size, size)
593 return False
594 return True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000595
596
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000597class Profiler(object):
598 def __init__(self, name):
599 self.name = name
600 self.start_time = None
601
602 def __enter__(self):
603 self.start_time = time.time()
604 return self
605
606 def __exit__(self, _exc_type, _exec_value, _traceback):
607 time_taken = time.time() - self.start_time
608 logging.info('Profiling: Section %s took %3.3f seconds',
609 self.name, time_taken)
610
611
612class Remote(object):
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000613 """Priority based worker queue to fetch or upload files from a
614 content-address server. Any function may be given as the fetcher/upload,
615 as long as it takes two inputs (the item contents, and their relative
616 destination).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000617
618 Supports local file system, CIFS or http remotes.
619
620 When the priority of items is equals, works in strict FIFO mode.
621 """
622 # Initial and maximum number of worker threads.
623 INITIAL_WORKERS = 2
624 MAX_WORKERS = 16
625 # Priorities.
626 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
627 INTERNAL_PRIORITY_BITS = (1<<8) - 1
628 RETRIES = 5
629
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000630 def __init__(self, destination_root):
631 # Function to fetch a remote object or upload to a remote location..
632 self._do_item = self.get_file_handler(destination_root)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000633 # Contains tuple(priority, obj).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000634 self._done = Queue.PriorityQueue()
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000635 self._pool = ThreadPool(self.INITIAL_WORKERS, self.MAX_WORKERS, 0)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000636
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000637 def join(self):
638 """Blocks until the queue is empty."""
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000639 return self._pool.join()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000640
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000641 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000642 """Retrieves an object from the remote data store.
643
644 The smaller |priority| gets fetched first.
645
646 Thread-safe.
647 """
648 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000649 return self._add_item(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000650
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000651 def _add_item(self, priority, obj, dest, size):
652 assert isinstance(obj, basestring), obj
653 assert isinstance(dest, basestring), dest
654 assert size is None or isinstance(size, int), size
655 return self._pool.add_task(
656 priority, self._task_executer, priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000657
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000658 def get_one_result(self):
659 return self._pool.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000660
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000661 def _task_executer(self, priority, obj, dest, size):
662 """Wraps self._do_item to trap and retry on IOError exceptions."""
663 try:
664 self._do_item(obj, dest)
665 if size and not valid_file(dest, size):
666 download_size = os.stat(dest).st_size
667 os.remove(dest)
668 raise IOError('File incorrect size after download of %s. Got %s and '
669 'expected %s' % (obj, download_size, size))
670 # TODO(maruel): Technically, we'd want to have an output queue to be a
671 # PriorityQueue.
672 return obj
673 except IOError as e:
674 logging.debug('Caught IOError: %s', e)
675 # Retry a few times, lowering the priority.
676 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
677 self._add_item(priority + 1, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000678 return
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000679 raise
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000680
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +0000681 def get_file_handler(self, file_or_url): # pylint: disable=R0201
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000682 """Returns a object to retrieve objects from a remote."""
683 if re.match(r'^https?://.+$', file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000684 def download_file(item, dest):
685 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
686 # easy.
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000687 try:
csharp@chromium.orgaa2d1512012-12-05 21:17:39 +0000688 zipped_source = file_or_url + item
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000689 logging.debug('download_file(%s)', zipped_source)
csharp@chromium.orge9c8d942013-03-11 20:48:36 +0000690
691 # Because the app engine DB is only eventually consistent, retry
692 # 404 errors because the file might just not be visible yet (even
693 # though it has been uploaded).
694 connection = url_open(zipped_source, retry_404=True)
csharp@chromium.orgf13eec02013-03-11 18:22:56 +0000695 if not connection:
696 raise IOError('Unable to open connection to %s' % zipped_source)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000697 decompressor = zlib.decompressobj()
maruel@chromium.org3f039182012-11-27 21:32:41 +0000698 size = 0
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000699 with open(dest, 'wb') as f:
700 while True:
701 chunk = connection.read(ZIPPED_FILE_CHUNK)
702 if not chunk:
703 break
maruel@chromium.org3f039182012-11-27 21:32:41 +0000704 size += len(chunk)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000705 f.write(decompressor.decompress(chunk))
706 # Ensure that all the data was properly decompressed.
707 uncompressed_data = decompressor.flush()
708 assert not uncompressed_data
csharp@chromium.org549669e2013-01-22 19:48:17 +0000709 except IOError:
710 logging.error('Encountered an exception with (%s, %s)' % (item, dest))
711 raise
csharp@chromium.orga110d792013-01-07 16:16:16 +0000712 except httplib.HTTPException as e:
713 raise IOError('Encountered an HTTPException.\n%s' % e)
csharp@chromium.org186d6232012-11-26 14:36:12 +0000714 except zlib.error as e:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000715 # Log the first bytes to see if it's uncompressed data.
716 logging.warning('%r', e[:512])
maruel@chromium.org3f039182012-11-27 21:32:41 +0000717 raise IOError(
718 'Problem unzipping data for item %s. Got %d bytes.\n%s' %
719 (item, size, e))
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000720
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000721 return download_file
722
723 def copy_file(item, dest):
724 source = os.path.join(file_or_url, item)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000725 if source == dest:
726 logging.info('Source and destination are the same, no action required')
727 return
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000728 logging.debug('copy_file(%s, %s)', source, dest)
729 shutil.copy(source, dest)
730 return copy_file
731
732
733class CachePolicies(object):
734 def __init__(self, max_cache_size, min_free_space, max_items):
735 """
736 Arguments:
737 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
738 cache is effectively a leak.
739 - min_free_space: Trim if disk free space becomes lower than this value. If
740 0, it unconditionally fill the disk.
741 - max_items: Maximum number of items to keep in the cache. If 0, do not
742 enforce a limit.
743 """
744 self.max_cache_size = max_cache_size
745 self.min_free_space = min_free_space
746 self.max_items = max_items
747
748
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000749class NoCache(object):
750 """This class is intended to be usable everywhere the Cache class is.
751 Instead of downloading to a cache, all files are downloaded to the target
752 directory and then moved to where they are needed.
753 """
754
755 def __init__(self, target_directory, remote):
756 self.target_directory = target_directory
757 self.remote = remote
758
759 def retrieve(self, priority, item, size):
760 """Get the request file."""
761 self.remote.add_item(priority, item, self.path(item), size)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000762 self.remote.get_one_result()
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000763
764 def wait_for(self, items):
765 """Download the first item of the given list if it is missing."""
766 item = items.iterkeys().next()
767
768 if not os.path.exists(self.path(item)):
769 self.remote.add_item(Remote.MED, item, self.path(item), UNKNOWN_FILE_SIZE)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000770 downloaded = self.remote.get_one_result()
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000771 assert downloaded == item
772
773 return item
774
775 def path(self, item):
776 return os.path.join(self.target_directory, item)
777
778
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000779class Cache(object):
780 """Stateful LRU cache.
781
782 Saves its state as json file.
783 """
784 STATE_FILE = 'state.json'
785
786 def __init__(self, cache_dir, remote, policies):
787 """
788 Arguments:
789 - cache_dir: Directory where to place the cache.
790 - remote: Remote where to fetch items from.
791 - policies: cache retention policies.
792 """
793 self.cache_dir = cache_dir
794 self.remote = remote
795 self.policies = policies
796 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
797 # The tuple(file, size) are kept as an array in a LRU style. E.g.
798 # self.state[0] is the oldest item.
799 self.state = []
maruel@chromium.org770993b2012-12-11 17:16:48 +0000800 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000801 # A lookup map to speed up searching.
802 self._lookup = {}
maruel@chromium.org770993b2012-12-11 17:16:48 +0000803 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000804
805 # Items currently being fetched. Keep it local to reduce lock contention.
806 self._pending_queue = set()
807
808 # Profiling values.
809 self._added = []
810 self._removed = []
811 self._free_disk = 0
812
maruel@chromium.org770993b2012-12-11 17:16:48 +0000813 with Profiler('Setup'):
814 if not os.path.isdir(self.cache_dir):
815 os.makedirs(self.cache_dir)
816 if os.path.isfile(self.state_file):
817 try:
818 self.state = json.load(open(self.state_file, 'r'))
819 except (IOError, ValueError), e:
820 # Too bad. The file will be overwritten and the cache cleared.
821 logging.error(
822 'Broken state file %s, ignoring.\n%s' % (self.STATE_FILE, e))
823 self._state_need_to_be_saved = True
824 if (not isinstance(self.state, list) or
825 not all(
826 isinstance(i, (list, tuple)) and len(i) == 2
827 for i in self.state)):
828 # Discard.
829 self._state_need_to_be_saved = True
830 self.state = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000831
maruel@chromium.org770993b2012-12-11 17:16:48 +0000832 # Ensure that all files listed in the state still exist and add new ones.
833 previous = set(filename for filename, _ in self.state)
834 if len(previous) != len(self.state):
835 logging.warn('Cache state is corrupted, found duplicate files')
836 self._state_need_to_be_saved = True
837 self.state = []
838
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000839 added = 0
840 for filename in os.listdir(self.cache_dir):
841 if filename == self.STATE_FILE:
842 continue
843 if filename in previous:
844 previous.remove(filename)
845 continue
846 # An untracked file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000847 if not RE_IS_SHA1.match(filename):
848 logging.warn('Removing unknown file %s from cache', filename)
849 os.remove(self.path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000850 continue
851 # Insert as the oldest file. It will be deleted eventually if not
852 # accessed.
853 self._add(filename, False)
854 logging.warn('Add unknown file %s to cache', filename)
855 added += 1
856
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000857 if added:
858 logging.warn('Added back %d unknown files', added)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000859 if previous:
860 logging.warn('Removed %d lost files', len(previous))
861 # Set explicitly in case self._add() wasn't called.
862 self._state_need_to_be_saved = True
863 # Filter out entries that were not found while keeping the previous
864 # order.
865 self.state = [
866 (filename, size) for filename, size in self.state
867 if filename not in previous
868 ]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000869 self.trim()
870
871 def __enter__(self):
872 return self
873
874 def __exit__(self, _exc_type, _exec_value, _traceback):
875 with Profiler('CleanupTrimming'):
876 self.trim()
877
878 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000879 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000880 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000881 '%5d (%8dkb) current',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000882 len(self.state),
883 sum(i[1] for i in self.state) / 1024)
884 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000885 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
886 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000887
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000888 def remove_file_at_index(self, index):
889 """Removes the file at the given index."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000890 try:
maruel@chromium.org770993b2012-12-11 17:16:48 +0000891 self._state_need_to_be_saved = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000892 filename, size = self.state.pop(index)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000893 # If the lookup was already stale, its possible the filename was not
894 # present yet.
895 self._lookup_is_stale = True
896 self._lookup.pop(filename, None)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000897 self._removed.append(size)
898 os.remove(self.path(filename))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000899 except OSError as e:
900 logging.error('Error attempting to delete a file\n%s' % e)
901
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000902 def remove_lru_file(self):
903 """Removes the last recently used file."""
904 self.remove_file_at_index(0)
905
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000906 def trim(self):
907 """Trims anything we don't know, make sure enough free space exists."""
908 # Ensure maximum cache size.
909 if self.policies.max_cache_size and self.state:
910 while sum(i[1] for i in self.state) > self.policies.max_cache_size:
911 self.remove_lru_file()
912
913 # Ensure maximum number of items in the cache.
914 if self.policies.max_items and self.state:
915 while len(self.state) > self.policies.max_items:
916 self.remove_lru_file()
917
918 # Ensure enough free space.
919 self._free_disk = get_free_space(self.cache_dir)
920 while (
921 self.policies.min_free_space and
922 self.state and
923 self._free_disk < self.policies.min_free_space):
924 self.remove_lru_file()
925 self._free_disk = get_free_space(self.cache_dir)
926
927 self.save()
928
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000929 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000930 """Retrieves a file from the remote, if not already cached, and adds it to
931 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000932
933 If the file is in the cache, verifiy that the file is valid (i.e. it is
934 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000935 """
936 assert not '/' in item
937 path = self.path(item)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000938 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000939 index = self._lookup.get(item)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000940
941 if index is not None:
942 if not valid_file(self.path(item), size):
943 self.remove_file_at_index(index)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000944 index = None
945 else:
946 assert index < len(self.state)
947 # Was already in cache. Update it's LRU value by putting it at the end.
maruel@chromium.org770993b2012-12-11 17:16:48 +0000948 self._state_need_to_be_saved = True
949 self._lookup_is_stale = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000950 self.state.append(self.state.pop(index))
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000951
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000952 if index is None:
953 if item in self._pending_queue:
954 # Already pending. The same object could be referenced multiple times.
955 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000956 self.remote.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000957 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000958
959 def add(self, filepath, obj):
960 """Forcibly adds a file to the cache."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000961 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000962 if not obj in self._lookup:
963 link_file(self.path(obj), filepath, HARDLINK)
964 self._add(obj, True)
965
966 def path(self, item):
967 """Returns the path to one item."""
968 return os.path.join(self.cache_dir, item)
969
970 def save(self):
971 """Saves the LRU ordering."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000972 if self._state_need_to_be_saved:
973 json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':'))
974 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000975
976 def wait_for(self, items):
977 """Starts a loop that waits for at least one of |items| to be retrieved.
978
979 Returns the first item retrieved.
980 """
981 # Flush items already present.
maruel@chromium.org770993b2012-12-11 17:16:48 +0000982 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000983 for item in items:
984 if item in self._lookup:
985 return item
986
987 assert all(i in self._pending_queue for i in items), (
988 items, self._pending_queue)
989 # Note that:
990 # len(self._pending_queue) ==
991 # ( len(self.remote._workers) - self.remote._ready +
992 # len(self._remote._queue) + len(self._remote.done))
993 # There is no lock-free way to verify that.
994 while self._pending_queue:
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000995 item = self.remote.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000996 self._pending_queue.remove(item)
997 self._add(item, True)
998 if item in items:
999 return item
1000
1001 def _add(self, item, at_end):
1002 """Adds an item in the internal state.
1003
1004 If |at_end| is False, self._lookup becomes inconsistent and
1005 self._update_lookup() must be called.
1006 """
1007 size = os.stat(self.path(item)).st_size
1008 self._added.append(size)
maruel@chromium.org770993b2012-12-11 17:16:48 +00001009 self._state_need_to_be_saved = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001010 if at_end:
1011 self.state.append((item, size))
1012 self._lookup[item] = len(self.state) - 1
1013 else:
maruel@chromium.org770993b2012-12-11 17:16:48 +00001014 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001015 self.state.insert(0, (item, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001016
1017 def _update_lookup(self):
maruel@chromium.org770993b2012-12-11 17:16:48 +00001018 if self._lookup_is_stale:
1019 self._lookup = dict(
1020 (filename, index) for index, (filename, _) in enumerate(self.state))
1021 self._lookup_is_stale = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001022
1023
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001024class IsolatedFile(object):
1025 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001026 def __init__(self, obj_hash):
1027 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001028 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001029 self.obj_hash = obj_hash
1030 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001031 # .isolate and all the .isolated files recursively included by it with
1032 # 'includes' key. The order of each sha-1 in 'includes', each representing a
1033 # .isolated file in the hash table, is important, as the later ones are not
1034 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001035 self.can_fetch = False
1036
1037 # Raw data.
1038 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001039 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001040 self.children = []
1041
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001042 # Set once the .isolated file is loaded.
1043 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001044 # Set once the files are fetched.
1045 self.files_fetched = False
1046
1047 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001048 """Verifies the .isolated file is valid and loads this object with the json
1049 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001050 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001051 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
1052 assert not self._is_parsed
1053 self.data = load_isolated(content)
1054 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
1055 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001056
1057 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001058 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001059
1060 Preemptively request files.
1061
1062 Note that |files| is modified by this function.
1063 """
1064 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001065 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001066 return
1067 logging.debug('fetch_files(%s)' % self.obj_hash)
1068 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001069 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001070 # overriden files must not be fetched.
1071 if filepath not in files:
1072 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001073 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001074 # Preemptively request files.
1075 logging.debug('fetching %s' % filepath)
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001076 cache.retrieve(Remote.MED, properties['h'], properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001077 self.files_fetched = True
1078
1079
1080class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001081 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001082 def __init__(self):
1083 self.command = []
1084 self.files = {}
1085 self.read_only = None
1086 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001087 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001088 self.root = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001089
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001090 def load(self, cache, root_isolated_hash):
1091 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001092
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001093 It enables support for "included" .isolated files. They are processed in
1094 strict order but fetched asynchronously from the cache. This is important so
1095 that a file in an included .isolated file that is overridden by an embedding
1096 .isolated file is not fetched neededlessly. The includes are fetched in one
1097 pass and the files are fetched as soon as all the ones on the left-side
1098 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001099
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001100 The prioritization is very important here for nested .isolated files.
1101 'includes' have the highest priority and the algorithm is optimized for both
1102 deep and wide trees. A deep one is a long link of .isolated files referenced
1103 one at a time by one item in 'includes'. A wide one has a large number of
1104 'includes' in a single .isolated file. 'left' is defined as an included
1105 .isolated file earlier in the 'includes' list. So the order of the elements
1106 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001107 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001108 self.root = IsolatedFile(root_isolated_hash)
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001109 cache.retrieve(Remote.HIGH, root_isolated_hash, UNKNOWN_FILE_SIZE)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001110 pending = {root_isolated_hash: self.root}
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001111 # Keeps the list of retrieved items to refuse recursive includes.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001112 retrieved = [root_isolated_hash]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001113
1114 def update_self(node):
1115 node.fetch_files(cache, self.files)
1116 # Grabs properties.
1117 if not self.command and node.data.get('command'):
1118 self.command = node.data['command']
1119 if self.read_only is None and node.data.get('read_only') is not None:
1120 self.read_only = node.data['read_only']
1121 if (self.relative_cwd is None and
1122 node.data.get('relative_cwd') is not None):
1123 self.relative_cwd = node.data['relative_cwd']
1124
1125 def traverse_tree(node):
1126 if node.can_fetch:
1127 if not node.files_fetched:
1128 update_self(node)
1129 will_break = False
1130 for i in node.children:
1131 if not i.can_fetch:
1132 if will_break:
1133 break
1134 # Automatically mark the first one as fetcheable.
1135 i.can_fetch = True
1136 will_break = True
1137 traverse_tree(i)
1138
1139 while pending:
1140 item_hash = cache.wait_for(pending)
1141 item = pending.pop(item_hash)
1142 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001143 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001144 # It's the root item.
1145 item.can_fetch = True
1146
1147 for new_child in item.children:
1148 h = new_child.obj_hash
1149 if h in retrieved:
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001150 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001151 pending[h] = new_child
csharp@chromium.org8dc52542012-11-08 20:29:55 +00001152 cache.retrieve(Remote.HIGH, h, UNKNOWN_FILE_SIZE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001153
1154 # Traverse the whole tree to see if files can now be fetched.
1155 traverse_tree(self.root)
1156 def check(n):
1157 return all(check(x) for x in n.children) and n.files_fetched
1158 assert check(self.root)
1159 self.relative_cwd = self.relative_cwd or ''
1160 self.read_only = self.read_only or False
1161
1162
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001163def create_directories(base_directory, files):
1164 """Creates the directory structure needed by the given list of files."""
1165 logging.debug('create_directories(%s, %d)', base_directory, len(files))
1166 # Creates the tree of directories to create.
1167 directories = set(os.path.dirname(f) for f in files)
1168 for item in list(directories):
1169 while item:
1170 directories.add(item)
1171 item = os.path.dirname(item)
1172 for d in sorted(directories):
1173 if d:
1174 os.mkdir(os.path.join(base_directory, d))
1175
1176
1177def create_links(base_directory, files):
1178 """Creates any links needed by the given set of files."""
1179 for filepath, properties in files:
1180 if 'link' not in properties:
1181 continue
1182 outfile = os.path.join(base_directory, filepath)
1183 # symlink doesn't exist on Windows. So the 'link' property should
1184 # never be specified for windows .isolated file.
1185 os.symlink(properties['l'], outfile) # pylint: disable=E1101
1186 if 'm' in properties:
1187 lchmod = getattr(os, 'lchmod', None)
1188 if lchmod:
1189 lchmod(outfile, properties['m'])
1190
1191
1192def setup_commands(base_directory, cwd, cmd):
1193 """Correctly adjusts and then returns the required working directory
1194 and command needed to run the test.
1195 """
1196 assert not os.path.isabs(cwd), 'The cwd must be a relative path, got %s' % cwd
1197 cwd = os.path.join(base_directory, cwd)
1198 if not os.path.isdir(cwd):
1199 os.makedirs(cwd)
1200
1201 # Ensure paths are correctly separated on windows.
1202 cmd[0] = cmd[0].replace('/', os.path.sep)
1203 cmd = fix_python_path(cmd)
1204
1205 return cwd, cmd
1206
1207
1208def generate_remaining_files(files):
1209 """Generates a dictionary of all the remaining files to be downloaded."""
1210 remaining = {}
1211 for filepath, props in files:
1212 if 'h' in props:
1213 remaining.setdefault(props['h'], []).append((filepath, props))
1214
1215 return remaining
1216
1217
1218def download_test_data(isolated_hash, target_directory, remote):
1219 """Downloads the dependencies to the given directory."""
1220 if not os.path.exists(target_directory):
1221 os.makedirs(target_directory)
1222
1223 settings = Settings()
1224 no_cache = NoCache(target_directory, Remote(remote))
1225
1226 # Download all the isolated files.
1227 with Profiler('GetIsolateds') as _prof:
1228 settings.load(no_cache, isolated_hash)
1229
1230 if not settings.command:
1231 print >> sys.stderr, 'No command to run'
1232 return 1
1233
1234 with Profiler('GetRest') as _prof:
1235 create_directories(target_directory, settings.files)
1236 create_links(target_directory, settings.files.iteritems())
1237
1238 cwd, cmd = setup_commands(target_directory, settings.relative_cwd,
1239 settings.command[:])
1240
1241 remaining = generate_remaining_files(settings.files.iteritems())
1242
1243 # Now block on the remaining files to be downloaded and mapped.
1244 logging.info('Retrieving remaining files')
1245 last_update = time.time()
1246 while remaining:
1247 obj = no_cache.wait_for(remaining)
1248 files = remaining.pop(obj)
1249
1250 for i, (filepath, properties) in enumerate(files):
1251 outfile = os.path.join(target_directory, filepath)
1252 logging.info(no_cache.path(obj))
1253
1254 if i + 1 == len(files):
1255 os.rename(no_cache.path(obj), outfile)
1256 else:
1257 shutil.copyfile(no_cache.path(obj), outfile)
1258
1259 if 'm' in properties:
1260 # It's not set on Windows.
1261 os.chmod(outfile, properties['m'])
1262
1263 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
1264 logging.info('%d files remaining...' % len(remaining))
1265 last_update = time.time()
1266
1267 print('.isolated files successfully downloaded and setup in %s' %
1268 target_directory)
1269 print('To run this test please run the command %s from the directory %s' %
1270 (cmd, cwd))
1271
1272 return 0
1273
1274
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001275def run_tha_test(isolated_hash, cache_dir, remote, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001276 """Downloads the dependencies in the cache, hardlinks them into a temporary
1277 directory and runs the executable.
1278 """
1279 settings = Settings()
1280 with Cache(cache_dir, Remote(remote), policies) as cache:
1281 outdir = make_temp_dir('run_tha_test', cache_dir)
1282 try:
1283 # Initiate all the files download.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001284 with Profiler('GetIsolateds') as _prof:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001285 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001286 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001287 # Adds it in the cache. While not strictly necessary, this simplifies
1288 # the rest.
maruel@chromium.orgcb3c3d52013-03-14 18:55:30 +00001289 h = hashlib.sha1(open(isolated_hash, 'rb').read()).hexdigest()
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001290 cache.add(isolated_hash, h)
1291 isolated_hash = h
1292 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001293
1294 if not settings.command:
1295 print >> sys.stderr, 'No command to run'
1296 return 1
1297
1298 with Profiler('GetRest') as _prof:
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001299 create_directories(outdir, settings.files)
1300 create_links(outdir, settings.files.iteritems())
1301 remaining = generate_remaining_files(settings.files.iteritems())
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001302
1303 # Do bookkeeping while files are being downloaded in the background.
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001304 cwd, cmd = setup_commands(outdir, settings.relative_cwd,
1305 settings.command[:])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001306
1307 # Now block on the remaining files to be downloaded and mapped.
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001308 logging.info('Retrieving remaining files')
1309 last_update = time.time()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001310 while remaining:
1311 obj = cache.wait_for(remaining)
1312 for filepath, properties in remaining.pop(obj):
1313 outfile = os.path.join(outdir, filepath)
1314 link_file(outfile, cache.path(obj), HARDLINK)
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001315 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001316 # It's not set on Windows.
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001317 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001318
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001319 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
1320 logging.info('%d files remaining...' % len(remaining))
1321 last_update = time.time()
1322
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001323 if settings.read_only:
1324 make_writable(outdir, True)
1325 logging.info('Running %s, cwd=%s' % (cmd, cwd))
csharp@chromium.orge217f302012-11-22 16:51:53 +00001326
1327 # TODO(csharp): This should be specified somewhere else.
1328 # Add a rotating log file if one doesn't already exist.
1329 env = os.environ.copy()
1330 env.setdefault('RUN_TEST_CASES_LOG_FILE', RUN_TEST_CASES_LOG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001331 try:
1332 with Profiler('RunTest') as _prof:
csharp@chromium.orge217f302012-11-22 16:51:53 +00001333 return subprocess.call(cmd, cwd=cwd, env=env)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001334 except OSError:
1335 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1336 raise
1337 finally:
1338 rmtree(outdir)
1339
1340
1341def main():
1342 parser = optparse.OptionParser(
1343 usage='%prog <options>', description=sys.modules[__name__].__doc__)
1344 parser.add_option(
1345 '-v', '--verbose', action='count', default=0, help='Use multiple times')
1346 parser.add_option('--no-run', action='store_true', help='Skip the run part')
1347
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001348 group = optparse.OptionGroup(parser, 'Download')
1349 group.add_option(
1350 '--download', metavar='DEST',
1351 help='Downloads files to DEST and returns without running, instead of '
1352 'downloading and then running from a temporary directory.')
1353 parser.add_option_group(group)
1354
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001355 group = optparse.OptionGroup(parser, 'Data source')
1356 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001357 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001358 metavar='FILE',
1359 help='File/url describing what to map or run')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001360 # TODO(maruel): Remove once not used anymore.
1361 group.add_option(
1362 '-m', '--manifest', dest='isolated', help=optparse.SUPPRESS_HELP)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001363 group.add_option(
1364 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001365 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001366 parser.add_option_group(group)
1367
1368 group.add_option(
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001369 '-r', '--remote', metavar='URL',
1370 default=
1371 'https://isolateserver.appspot.com/content/retrieve/default-gzip/',
1372 help='Remote where to get the items. Defaults to %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001373 group = optparse.OptionGroup(parser, 'Cache management')
1374 group.add_option(
1375 '--cache',
1376 default='cache',
1377 metavar='DIR',
1378 help='Cache directory, default=%default')
1379 group.add_option(
1380 '--max-cache-size',
1381 type='int',
1382 metavar='NNN',
1383 default=20*1024*1024*1024,
1384 help='Trim if the cache gets larger than this value, default=%default')
1385 group.add_option(
1386 '--min-free-space',
1387 type='int',
1388 metavar='NNN',
1389 default=1*1024*1024*1024,
1390 help='Trim if disk free space becomes lower than this value, '
1391 'default=%default')
1392 group.add_option(
1393 '--max-items',
1394 type='int',
1395 metavar='NNN',
1396 default=100000,
1397 help='Trim if more than this number of items are in the cache '
1398 'default=%default')
1399 parser.add_option_group(group)
1400
1401 options, args = parser.parse_args()
1402 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
csharp@chromium.orgff2a4662012-11-21 20:49:32 +00001403
1404 logging_console = logging.StreamHandler()
1405 logging_console.setFormatter(logging.Formatter(
1406 '%(levelname)5s %(module)15s(%(lineno)3d): %(message)s'))
1407 logging_console.setLevel(level)
1408 logging.getLogger().addHandler(logging_console)
1409
1410 logging_rotating_file = logging.handlers.RotatingFileHandler(
1411 RUN_ISOLATED_LOG_FILE,
1412 maxBytes=10 * 1024 * 1024, backupCount=5)
1413 logging_rotating_file.setLevel(logging.DEBUG)
1414 logging_rotating_file.setFormatter(logging.Formatter(
1415 '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)3d): %(message)s'))
1416 logging.getLogger().addHandler(logging_rotating_file)
1417
1418 logging.getLogger().setLevel(logging.DEBUG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001419
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001420 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001421 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001422 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001423 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001424 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001425 parser.error('Unsupported args %s' % ' '.join(args))
1426
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001427 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001428 policies = CachePolicies(
1429 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001430
1431 if options.download:
1432 return download_test_data(options.isolated or options.hash,
1433 options.download, options.remote)
1434 else:
1435 try:
1436 return run_tha_test(
1437 options.isolated or options.hash,
1438 options.cache,
1439 options.remote,
1440 policies)
1441 except Exception, e:
1442 # Make sure any exception is logged.
1443 logging.exception(e)
1444 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001445
1446
1447if __name__ == '__main__':
1448 sys.exit(main())