blob: 768dccfd2bdecd33ff347a7909d7f1ceefaff190 [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
13import json
14import logging
15import optparse
16import os
17import Queue
18import re
19import shutil
20import stat
21import subprocess
22import sys
23import tempfile
24import threading
25import time
26import urllib
csharp@chromium.orga92403f2012-11-20 15:13:59 +000027import urllib2
28import zlib
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000029
30
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000031# Types of action accepted by link_file().
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000032HARDLINK, SYMLINK, COPY = range(1, 4)
33
34RE_IS_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$')
35
csharp@chromium.org8dc52542012-11-08 20:29:55 +000036# The file size to be used when we don't know the correct file size,
37# generally used for .isolated files.
38UNKNOWN_FILE_SIZE = None
39
csharp@chromium.orga92403f2012-11-20 15:13:59 +000040# The size of each chunk to read when downloading and unzipping files.
41ZIPPED_FILE_CHUNK = 16 * 1024
42
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000043
44class ConfigError(ValueError):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +000045 """Generic failure to load a .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000046 pass
47
48
49class MappingError(OSError):
50 """Failed to recreate the tree."""
51 pass
52
53
csharp@chromium.orga92403f2012-11-20 15:13:59 +000054class DownloadFileOpener(urllib.FancyURLopener):
55 """This class is needed to get urlretrive to raise an exception on
56 404 errors, instead of still writing to the file with the error code.
57 """
58 def http_error_default(self, url, fp, errcode, errmsg, headers):
59 raise urllib2.HTTPError(url, errcode, errmsg, headers, fp)
60
61
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000062def get_flavor():
63 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
64 flavors = {
65 'cygwin': 'win',
66 'win32': 'win',
67 'darwin': 'mac',
68 'sunos5': 'solaris',
69 'freebsd7': 'freebsd',
70 'freebsd8': 'freebsd',
71 }
72 return flavors.get(sys.platform, 'linux')
73
74
75def os_link(source, link_name):
76 """Add support for os.link() on Windows."""
77 if sys.platform == 'win32':
78 if not ctypes.windll.kernel32.CreateHardLinkW(
79 unicode(link_name), unicode(source), 0):
80 raise OSError()
81 else:
82 os.link(source, link_name)
83
84
85def readable_copy(outfile, infile):
86 """Makes a copy of the file that is readable by everyone."""
87 shutil.copy(infile, outfile)
88 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
89 stat.S_IRGRP | stat.S_IROTH)
90 os.chmod(outfile, read_enabled_mode)
91
92
93def link_file(outfile, infile, action):
94 """Links a file. The type of link depends on |action|."""
95 logging.debug('Mapping %s to %s' % (infile, outfile))
96 if action not in (HARDLINK, SYMLINK, COPY):
97 raise ValueError('Unknown mapping action %s' % action)
98 if not os.path.isfile(infile):
99 raise MappingError('%s is missing' % infile)
100 if os.path.isfile(outfile):
101 raise MappingError(
102 '%s already exist; insize:%d; outsize:%d' %
103 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
104
105 if action == COPY:
106 readable_copy(outfile, infile)
107 elif action == SYMLINK and sys.platform != 'win32':
108 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000109 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000110 else:
111 try:
112 os_link(infile, outfile)
113 except OSError:
114 # Probably a different file system.
115 logging.warn(
116 'Failed to hardlink, failing back to copy %s to %s' % (
117 infile, outfile))
118 readable_copy(outfile, infile)
119
120
121def _set_write_bit(path, read_only):
122 """Sets or resets the executable bit on a file or directory."""
123 mode = os.lstat(path).st_mode
124 if read_only:
125 mode = mode & 0500
126 else:
127 mode = mode | 0200
128 if hasattr(os, 'lchmod'):
129 os.lchmod(path, mode) # pylint: disable=E1101
130 else:
131 if stat.S_ISLNK(mode):
132 # Skip symlink without lchmod() support.
133 logging.debug('Can\'t change +w bit on symlink %s' % path)
134 return
135
136 # TODO(maruel): Implement proper DACL modification on Windows.
137 os.chmod(path, mode)
138
139
140def make_writable(root, read_only):
141 """Toggle the writable bit on a directory tree."""
142 root = os.path.abspath(root)
143 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
144 for filename in filenames:
145 _set_write_bit(os.path.join(dirpath, filename), read_only)
146
147 for dirname in dirnames:
148 _set_write_bit(os.path.join(dirpath, dirname), read_only)
149
150
151def rmtree(root):
152 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
153 make_writable(root, False)
154 if sys.platform == 'win32':
155 for i in range(3):
156 try:
157 shutil.rmtree(root)
158 break
159 except WindowsError: # pylint: disable=E0602
160 delay = (i+1)*2
161 print >> sys.stderr, (
162 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
163 time.sleep(delay)
164 else:
165 shutil.rmtree(root)
166
167
168def is_same_filesystem(path1, path2):
169 """Returns True if both paths are on the same filesystem.
170
171 This is required to enable the use of hardlinks.
172 """
173 assert os.path.isabs(path1), path1
174 assert os.path.isabs(path2), path2
175 if sys.platform == 'win32':
176 # If the drive letter mismatches, assume it's a separate partition.
177 # TODO(maruel): It should look at the underlying drive, a drive letter could
178 # be a mount point to a directory on another drive.
179 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
180 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
181 if path1[0].lower() != path2[0].lower():
182 return False
183 return os.stat(path1).st_dev == os.stat(path2).st_dev
184
185
186def get_free_space(path):
187 """Returns the number of free bytes."""
188 if sys.platform == 'win32':
189 free_bytes = ctypes.c_ulonglong(0)
190 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
191 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
192 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000193 # For OSes other than Windows.
194 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000195 return f.f_bfree * f.f_frsize
196
197
198def make_temp_dir(prefix, root_dir):
199 """Returns a temporary directory on the same file system as root_dir."""
200 base_temp_dir = None
201 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
202 base_temp_dir = os.path.dirname(root_dir)
203 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
204
205
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000206def load_isolated(content):
207 """Verifies the .isolated file is valid and loads this object with the json
208 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000209 """
210 try:
211 data = json.loads(content)
212 except ValueError:
213 raise ConfigError('Failed to parse: %s...' % content[:100])
214
215 if not isinstance(data, dict):
216 raise ConfigError('Expected dict, got %r' % data)
217
218 for key, value in data.iteritems():
219 if key == 'command':
220 if not isinstance(value, list):
221 raise ConfigError('Expected list, got %r' % value)
222 for subvalue in value:
223 if not isinstance(subvalue, basestring):
224 raise ConfigError('Expected string, got %r' % subvalue)
225
226 elif key == 'files':
227 if not isinstance(value, dict):
228 raise ConfigError('Expected dict, got %r' % value)
229 for subkey, subvalue in value.iteritems():
230 if not isinstance(subkey, basestring):
231 raise ConfigError('Expected string, got %r' % subkey)
232 if not isinstance(subvalue, dict):
233 raise ConfigError('Expected dict, got %r' % subvalue)
234 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000235 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000236 if not isinstance(subsubvalue, basestring):
237 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000238 elif subsubkey == 'm':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000239 if not isinstance(subsubvalue, int):
240 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000241 elif subsubkey == 'h':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000242 if not RE_IS_SHA1.match(subsubvalue):
243 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000244 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000245 if not isinstance(subsubvalue, int):
246 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000247 elif subsubkey == 't':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000248 if not isinstance(subsubvalue, int):
249 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000250 elif subsubkey == 'T':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000251 if not isinstance(subsubvalue, bool):
252 raise ConfigError('Expected bool, got %r' % subsubvalue)
253 else:
254 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000255 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000256 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000257 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
258 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000259
260 elif key == 'includes':
261 if not isinstance(value, list):
262 raise ConfigError('Expected list, got %r' % value)
263 for subvalue in value:
264 if not RE_IS_SHA1.match(subvalue):
265 raise ConfigError('Expected sha-1, got %r' % subvalue)
266
267 elif key == 'read_only':
268 if not isinstance(value, bool):
269 raise ConfigError('Expected bool, got %r' % value)
270
271 elif key == 'relative_cwd':
272 if not isinstance(value, basestring):
273 raise ConfigError('Expected string, got %r' % value)
274
275 elif key == 'os':
276 if value != get_flavor():
277 raise ConfigError(
278 'Expected \'os\' to be \'%s\' but got \'%s\'' %
279 (get_flavor(), value))
280
281 else:
282 raise ConfigError('Unknown key %s' % key)
283
284 return data
285
286
287def fix_python_path(cmd):
288 """Returns the fixed command line to call the right python executable."""
289 out = cmd[:]
290 if out[0] == 'python':
291 out[0] = sys.executable
292 elif out[0].endswith('.py'):
293 out.insert(0, sys.executable)
294 return out
295
296
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000297class WorkerThread(threading.Thread):
298 """Keeps the results of each task in a thread-local outputs variable."""
299 def __init__(self, tasks, *args, **kwargs):
300 super(WorkerThread, self).__init__(*args, **kwargs)
301 self._tasks = tasks
302 self.outputs = []
303 self.exceptions = []
304
305 self.daemon = True
306 self.start()
307
308 def run(self):
309 """Runs until a None task is queued."""
310 while True:
311 task = self._tasks.get()
312 if task is None:
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000313 # We're done.
314 return
315 try:
316 func, args, kwargs = task
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000317 self.outputs.append(func(*args, **kwargs))
318 except Exception, e:
319 logging.error('Caught exception! %s' % e)
320 self.exceptions.append(sys.exc_info())
321 finally:
322 self._tasks.task_done()
323
324
325class ThreadPool(object):
326 """Implements a multithreaded worker pool oriented for mapping jobs with
327 thread-local result storage.
328 """
329 QUEUE_CLASS = Queue.Queue
330
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000331 def __init__(self, num_threads, queue_size=0):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000332 logging.debug('Creating ThreadPool')
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000333 self.tasks = self.QUEUE_CLASS(queue_size)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000334 self._workers = [
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000335 WorkerThread(self.tasks, name='worker-%d' % i)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000336 for i in range(num_threads)
337 ]
338
339 def add_task(self, func, *args, **kwargs):
340 """Adds a task, a function to be executed by a worker.
341
342 The function's return value will be stored in the the worker's thread local
343 outputs list.
344 """
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000345 self.tasks.put((func, args, kwargs))
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000346
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000347 def join(self):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000348 """Extracts all the results from each threads unordered."""
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000349 self.tasks.join()
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000350 out = []
351 # Look for exceptions.
352 for w in self._workers:
353 if w.exceptions:
354 raise w.exceptions[0][0], w.exceptions[0][1], w.exceptions[0][2]
355 out.extend(w.outputs)
356 w.outputs = []
357 return out
358
359 def close(self):
360 """Closes all the threads."""
361 for _ in range(len(self._workers)):
362 # Enqueueing None causes the worker to stop.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000363 self.tasks.put(None)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000364 for t in self._workers:
365 t.join()
366
367 def __enter__(self):
368 """Enables 'with' statement."""
369 return self
370
371 def __exit__(self, exc_type, exc_value, traceback):
372 """Enables 'with' statement."""
373 self.close()
374
375
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000376def valid_file(filepath, size):
377 """Determines if the given files appears valid (currently it just checks
378 the file's size)."""
379 return (size == UNKNOWN_FILE_SIZE or size == os.stat(filepath).st_size)
380
381
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000382class Profiler(object):
383 def __init__(self, name):
384 self.name = name
385 self.start_time = None
386
387 def __enter__(self):
388 self.start_time = time.time()
389 return self
390
391 def __exit__(self, _exc_type, _exec_value, _traceback):
392 time_taken = time.time() - self.start_time
393 logging.info('Profiling: Section %s took %3.3f seconds',
394 self.name, time_taken)
395
396
397class Remote(object):
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000398 """Priority based worker queue to fetch or upload files from a
399 content-address server. Any function may be given as the fetcher/upload,
400 as long as it takes two inputs (the item contents, and their relative
401 destination).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000402
403 Supports local file system, CIFS or http remotes.
404
405 When the priority of items is equals, works in strict FIFO mode.
406 """
407 # Initial and maximum number of worker threads.
408 INITIAL_WORKERS = 2
409 MAX_WORKERS = 16
410 # Priorities.
411 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
412 INTERNAL_PRIORITY_BITS = (1<<8) - 1
413 RETRIES = 5
414
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000415 def __init__(self, destination_root):
416 # Function to fetch a remote object or upload to a remote location..
417 self._do_item = self.get_file_handler(destination_root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000418 # Contains tuple(priority, index, obj, destination).
419 self._queue = Queue.PriorityQueue()
420 # Contains tuple(priority, index, obj).
421 self._done = Queue.PriorityQueue()
422
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000423 # Contains generated exceptions that haven't been handled yet.
424 self._exceptions = Queue.Queue()
425
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000426 # To keep FIFO ordering in self._queue. It is assumed xrange's iterator is
427 # thread-safe.
428 self._next_index = xrange(0, 1<<30).__iter__().next
429
430 # Control access to the following member.
431 self._ready_lock = threading.Lock()
432 # Number of threads in wait state.
433 self._ready = 0
434
435 # Control access to the following member.
436 self._workers_lock = threading.Lock()
437 self._workers = []
438 for _ in range(self.INITIAL_WORKERS):
439 self._add_worker()
440
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000441 def join(self):
442 """Blocks until the queue is empty."""
443 self._queue.join()
444
445 def next_exception(self):
446 """Returns the next unhandled exception, or None if there is
447 no exception."""
448 try:
449 return self._exceptions.get_nowait()
450 except Queue.Empty:
451 return None
452
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000453 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000454 """Retrieves an object from the remote data store.
455
456 The smaller |priority| gets fetched first.
457
458 Thread-safe.
459 """
460 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000461 self._add_to_queue(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000462
463 def get_result(self):
464 """Returns the next file that was successfully fetched."""
465 r = self._done.get()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000466 if r[0] == -1:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000467 # It's an exception.
468 raise r[2][0], r[2][1], r[2][2]
469 return r[2]
470
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000471 def _add_to_queue(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000472 with self._ready_lock:
473 start_new_worker = not self._ready
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000474 self._queue.put((priority, self._next_index(), obj, dest, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000475 if start_new_worker:
476 self._add_worker()
477
478 def _add_worker(self):
479 """Add one worker thread if there isn't too many. Thread-safe."""
480 with self._workers_lock:
481 if len(self._workers) >= self.MAX_WORKERS:
482 return False
483 worker = threading.Thread(target=self._run)
484 self._workers.append(worker)
485 worker.daemon = True
486 worker.start()
487
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000488 def _step_done(self, result):
489 """Worker helper function"""
490 self._done.put(result)
491 self._queue.task_done()
492 if result[0] == -1:
493 self._exceptions.put(sys.exc_info())
494
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000495 def _run(self):
496 """Worker thread loop."""
497 while True:
498 try:
499 with self._ready_lock:
500 self._ready += 1
501 item = self._queue.get()
502 finally:
503 with self._ready_lock:
504 self._ready -= 1
505 if not item:
506 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000507 priority, index, obj, dest, size = item
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000508 try:
509 self._do_item(obj, dest)
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000510 if size and not valid_file(dest, size):
511 download_size = os.stat(dest).st_size
512 os.remove(dest)
513 raise IOError('File incorrect size after download of %s. Got %s and '
514 'expected %s' % (dest, download_size, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000515 except IOError:
516 # Retry a few times, lowering the priority.
517 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000518 self._add_to_queue(priority + 1, obj, dest, size)
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000519 self._queue.task_done()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000520 continue
521 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000522 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000523 except:
524 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000525 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000526 else:
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000527 self._step_done((priority, index, obj))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000528
529 @staticmethod
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000530 def get_file_handler(file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000531 """Returns a object to retrieve objects from a remote."""
532 if re.match(r'^https?://.+$', file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000533 def download_file(item, dest):
534 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
535 # easy.
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000536
537 # TODO(csharp): This is a temporary workaround to generate the gzipped
538 # url, remove once the files are always zipped before being uploaded.
539 try:
540 zipped_source = file_or_url.rstrip('/') + '-gzip/' + item
541 logging.debug('download_file(%s)', zipped_source)
542 connection = urllib2.urlopen(zipped_source)
543 decompressor = zlib.decompressobj()
544 with open(dest, 'wb') as f:
545 while True:
546 chunk = connection.read(ZIPPED_FILE_CHUNK)
547 if not chunk:
548 break
549 f.write(decompressor.decompress(chunk))
550 # Ensure that all the data was properly decompressed.
551 uncompressed_data = decompressor.flush()
552 assert not uncompressed_data
553 except urllib2.URLError:
554 # Try the unzipped version
555 unzipped_source = file_or_url + item
556 logging.debug('Zipped version missing, try unzipped version')
557 logging.debug('download_file(%s, %s)', unzipped_source, dest)
558 DownloadFileOpener().retrieve(unzipped_source, dest)
559
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000560 return download_file
561
562 def copy_file(item, dest):
563 source = os.path.join(file_or_url, item)
564 logging.debug('copy_file(%s, %s)', source, dest)
565 shutil.copy(source, dest)
566 return copy_file
567
568
569class CachePolicies(object):
570 def __init__(self, max_cache_size, min_free_space, max_items):
571 """
572 Arguments:
573 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
574 cache is effectively a leak.
575 - min_free_space: Trim if disk free space becomes lower than this value. If
576 0, it unconditionally fill the disk.
577 - max_items: Maximum number of items to keep in the cache. If 0, do not
578 enforce a limit.
579 """
580 self.max_cache_size = max_cache_size
581 self.min_free_space = min_free_space
582 self.max_items = max_items
583
584
585class Cache(object):
586 """Stateful LRU cache.
587
588 Saves its state as json file.
589 """
590 STATE_FILE = 'state.json'
591
592 def __init__(self, cache_dir, remote, policies):
593 """
594 Arguments:
595 - cache_dir: Directory where to place the cache.
596 - remote: Remote where to fetch items from.
597 - policies: cache retention policies.
598 """
599 self.cache_dir = cache_dir
600 self.remote = remote
601 self.policies = policies
602 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
603 # The tuple(file, size) are kept as an array in a LRU style. E.g.
604 # self.state[0] is the oldest item.
605 self.state = []
606 # A lookup map to speed up searching.
607 self._lookup = {}
608 self._dirty = False
609
610 # Items currently being fetched. Keep it local to reduce lock contention.
611 self._pending_queue = set()
612
613 # Profiling values.
614 self._added = []
615 self._removed = []
616 self._free_disk = 0
617
618 if not os.path.isdir(self.cache_dir):
619 os.makedirs(self.cache_dir)
620 if os.path.isfile(self.state_file):
621 try:
622 self.state = json.load(open(self.state_file, 'r'))
623 except (IOError, ValueError), e:
624 # Too bad. The file will be overwritten and the cache cleared.
625 logging.error(
626 'Broken state file %s, ignoring.\n%s' % (self.STATE_FILE, e))
627 if (not isinstance(self.state, list) or
628 not all(
629 isinstance(i, (list, tuple)) and len(i) == 2 for i in self.state)):
630 # Discard.
631 self.state = []
632 self._dirty = True
633
634 # Ensure that all files listed in the state still exist and add new ones.
635 previous = set(filename for filename, _ in self.state)
636 if len(previous) != len(self.state):
637 logging.warn('Cache state is corrupted')
638 self._dirty = True
639 self.state = []
640 else:
641 added = 0
642 for filename in os.listdir(self.cache_dir):
643 if filename == self.STATE_FILE:
644 continue
645 if filename in previous:
646 previous.remove(filename)
647 continue
648 # An untracked file.
649 self._dirty = True
650 if not RE_IS_SHA1.match(filename):
651 logging.warn('Removing unknown file %s from cache', filename)
652 os.remove(self.path(filename))
653 else:
654 # Insert as the oldest file. It will be deleted eventually if not
655 # accessed.
656 self._add(filename, False)
657 added += 1
658 if added:
659 logging.warn('Added back %d unknown files', added)
660 self.state = [
661 (filename, size) for filename, size in self.state
662 if filename not in previous
663 ]
664 self._update_lookup()
665
666 with Profiler('SetupTrimming'):
667 self.trim()
668
669 def __enter__(self):
670 return self
671
672 def __exit__(self, _exc_type, _exec_value, _traceback):
673 with Profiler('CleanupTrimming'):
674 self.trim()
675
676 logging.info(
677 '%4d (%7dkb) added', len(self._added), sum(self._added) / 1024)
678 logging.info(
679 '%4d (%7dkb) current',
680 len(self.state),
681 sum(i[1] for i in self.state) / 1024)
682 logging.info(
683 '%4d (%7dkb) removed', len(self._removed), sum(self._removed) / 1024)
684 logging.info('%7dkb free', self._free_disk / 1024)
685
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000686 def remove_file_at_index(self, index):
687 """Removes the file at the given index."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000688 try:
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000689 filename, size = self.state.pop(index)
690 # TODO(csharp): _lookup should self-update.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000691 del self._lookup[filename]
692 self._removed.append(size)
693 os.remove(self.path(filename))
694 self._dirty = True
695 except OSError as e:
696 logging.error('Error attempting to delete a file\n%s' % e)
697
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000698 def remove_lru_file(self):
699 """Removes the last recently used file."""
700 self.remove_file_at_index(0)
701
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000702 def trim(self):
703 """Trims anything we don't know, make sure enough free space exists."""
704 # Ensure maximum cache size.
705 if self.policies.max_cache_size and self.state:
706 while sum(i[1] for i in self.state) > self.policies.max_cache_size:
707 self.remove_lru_file()
708
709 # Ensure maximum number of items in the cache.
710 if self.policies.max_items and self.state:
711 while len(self.state) > self.policies.max_items:
712 self.remove_lru_file()
713
714 # Ensure enough free space.
715 self._free_disk = get_free_space(self.cache_dir)
716 while (
717 self.policies.min_free_space and
718 self.state and
719 self._free_disk < self.policies.min_free_space):
720 self.remove_lru_file()
721 self._free_disk = get_free_space(self.cache_dir)
722
723 self.save()
724
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000725 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000726 """Retrieves a file from the remote, if not already cached, and adds it to
727 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000728
729 If the file is in the cache, verifiy that the file is valid (i.e. it is
730 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000731 """
732 assert not '/' in item
733 path = self.path(item)
734 index = self._lookup.get(item)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000735
736 if index is not None:
737 if not valid_file(self.path(item), size):
738 self.remove_file_at_index(index)
739 self._update_lookup()
740 index = None
741 else:
742 assert index < len(self.state)
743 # Was already in cache. Update it's LRU value by putting it at the end.
744 self.state.append(self.state.pop(index))
745 self._dirty = True
746 self._update_lookup()
747
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000748 if index is None:
749 if item in self._pending_queue:
750 # Already pending. The same object could be referenced multiple times.
751 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000752 self.remote.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000753 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000754
755 def add(self, filepath, obj):
756 """Forcibly adds a file to the cache."""
757 if not obj in self._lookup:
758 link_file(self.path(obj), filepath, HARDLINK)
759 self._add(obj, True)
760
761 def path(self, item):
762 """Returns the path to one item."""
763 return os.path.join(self.cache_dir, item)
764
765 def save(self):
766 """Saves the LRU ordering."""
767 json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':'))
768
769 def wait_for(self, items):
770 """Starts a loop that waits for at least one of |items| to be retrieved.
771
772 Returns the first item retrieved.
773 """
774 # Flush items already present.
775 for item in items:
776 if item in self._lookup:
777 return item
778
779 assert all(i in self._pending_queue for i in items), (
780 items, self._pending_queue)
781 # Note that:
782 # len(self._pending_queue) ==
783 # ( len(self.remote._workers) - self.remote._ready +
784 # len(self._remote._queue) + len(self._remote.done))
785 # There is no lock-free way to verify that.
786 while self._pending_queue:
787 item = self.remote.get_result()
788 self._pending_queue.remove(item)
789 self._add(item, True)
790 if item in items:
791 return item
792
793 def _add(self, item, at_end):
794 """Adds an item in the internal state.
795
796 If |at_end| is False, self._lookup becomes inconsistent and
797 self._update_lookup() must be called.
798 """
799 size = os.stat(self.path(item)).st_size
800 self._added.append(size)
801 if at_end:
802 self.state.append((item, size))
803 self._lookup[item] = len(self.state) - 1
804 else:
805 self.state.insert(0, (item, size))
806 self._dirty = True
807
808 def _update_lookup(self):
809 self._lookup = dict(
810 (filename, index) for index, (filename, _) in enumerate(self.state))
811
812
813
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000814class IsolatedFile(object):
815 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000816 def __init__(self, obj_hash):
817 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000818 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000819 self.obj_hash = obj_hash
820 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000821 # .isolate and all the .isolated files recursively included by it with
822 # 'includes' key. The order of each sha-1 in 'includes', each representing a
823 # .isolated file in the hash table, is important, as the later ones are not
824 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000825 self.can_fetch = False
826
827 # Raw data.
828 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000829 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000830 self.children = []
831
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000832 # Set once the .isolated file is loaded.
833 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000834 # Set once the files are fetched.
835 self.files_fetched = False
836
837 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000838 """Verifies the .isolated file is valid and loads this object with the json
839 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000840 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000841 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
842 assert not self._is_parsed
843 self.data = load_isolated(content)
844 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
845 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000846
847 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000848 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000849
850 Preemptively request files.
851
852 Note that |files| is modified by this function.
853 """
854 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000855 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000856 return
857 logging.debug('fetch_files(%s)' % self.obj_hash)
858 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000859 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000860 # overriden files must not be fetched.
861 if filepath not in files:
862 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000863 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000864 # Preemptively request files.
865 logging.debug('fetching %s' % filepath)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000866 cache.retrieve(Remote.MED, properties['h'], properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000867 self.files_fetched = True
868
869
870class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000871 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000872 def __init__(self):
873 self.command = []
874 self.files = {}
875 self.read_only = None
876 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000877 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000878 self.root = None
879 logging.debug('Settings')
880
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000881 def load(self, cache, root_isolated_hash):
882 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000883
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000884 It enables support for "included" .isolated files. They are processed in
885 strict order but fetched asynchronously from the cache. This is important so
886 that a file in an included .isolated file that is overridden by an embedding
887 .isolated file is not fetched neededlessly. The includes are fetched in one
888 pass and the files are fetched as soon as all the ones on the left-side
889 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000890
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000891 The prioritization is very important here for nested .isolated files.
892 'includes' have the highest priority and the algorithm is optimized for both
893 deep and wide trees. A deep one is a long link of .isolated files referenced
894 one at a time by one item in 'includes'. A wide one has a large number of
895 'includes' in a single .isolated file. 'left' is defined as an included
896 .isolated file earlier in the 'includes' list. So the order of the elements
897 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000898 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000899 self.root = IsolatedFile(root_isolated_hash)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000900 cache.retrieve(Remote.HIGH, root_isolated_hash, UNKNOWN_FILE_SIZE)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000901 pending = {root_isolated_hash: self.root}
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000902 # Keeps the list of retrieved items to refuse recursive includes.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000903 retrieved = [root_isolated_hash]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000904
905 def update_self(node):
906 node.fetch_files(cache, self.files)
907 # Grabs properties.
908 if not self.command and node.data.get('command'):
909 self.command = node.data['command']
910 if self.read_only is None and node.data.get('read_only') is not None:
911 self.read_only = node.data['read_only']
912 if (self.relative_cwd is None and
913 node.data.get('relative_cwd') is not None):
914 self.relative_cwd = node.data['relative_cwd']
915
916 def traverse_tree(node):
917 if node.can_fetch:
918 if not node.files_fetched:
919 update_self(node)
920 will_break = False
921 for i in node.children:
922 if not i.can_fetch:
923 if will_break:
924 break
925 # Automatically mark the first one as fetcheable.
926 i.can_fetch = True
927 will_break = True
928 traverse_tree(i)
929
930 while pending:
931 item_hash = cache.wait_for(pending)
932 item = pending.pop(item_hash)
933 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000934 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000935 # It's the root item.
936 item.can_fetch = True
937
938 for new_child in item.children:
939 h = new_child.obj_hash
940 if h in retrieved:
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000941 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000942 pending[h] = new_child
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000943 cache.retrieve(Remote.HIGH, h, UNKNOWN_FILE_SIZE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000944
945 # Traverse the whole tree to see if files can now be fetched.
946 traverse_tree(self.root)
947 def check(n):
948 return all(check(x) for x in n.children) and n.files_fetched
949 assert check(self.root)
950 self.relative_cwd = self.relative_cwd or ''
951 self.read_only = self.read_only or False
952
953
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000954def run_tha_test(isolated_hash, cache_dir, remote, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000955 """Downloads the dependencies in the cache, hardlinks them into a temporary
956 directory and runs the executable.
957 """
958 settings = Settings()
959 with Cache(cache_dir, Remote(remote), policies) as cache:
960 outdir = make_temp_dir('run_tha_test', cache_dir)
961 try:
962 # Initiate all the files download.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000963 with Profiler('GetIsolateds') as _prof:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000964 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000965 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000966 # Adds it in the cache. While not strictly necessary, this simplifies
967 # the rest.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000968 h = hashlib.sha1(open(isolated_hash, 'r').read()).hexdigest()
969 cache.add(isolated_hash, h)
970 isolated_hash = h
971 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000972
973 if not settings.command:
974 print >> sys.stderr, 'No command to run'
975 return 1
976
977 with Profiler('GetRest') as _prof:
978 logging.debug('Creating directories')
979 # Creates the tree of directories to create.
980 directories = set(os.path.dirname(f) for f in settings.files)
981 for item in list(directories):
982 while item:
983 directories.add(item)
984 item = os.path.dirname(item)
985 for d in sorted(directories):
986 if d:
987 os.mkdir(os.path.join(outdir, d))
988
989 # Creates the links if necessary.
990 for filepath, properties in settings.files.iteritems():
991 if 'link' not in properties:
992 continue
993 outfile = os.path.join(outdir, filepath)
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000994 # symlink doesn't exist on Windows. So the 'link' property should
995 # never be specified for windows .isolated file.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000996 os.symlink(properties['l'], outfile) # pylint: disable=E1101
997 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000998 # It's not set on Windows.
maruel@chromium.org96768a42012-10-31 18:49:18 +0000999 lchmod = getattr(os, 'lchmod', None)
1000 if lchmod:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001001 lchmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001002
1003 # Remaining files to be processed.
1004 # Note that files could still be not be downloaded yet here.
1005 remaining = dict()
1006 for filepath, props in settings.files.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001007 if 'h' in props:
1008 remaining.setdefault(props['h'], []).append((filepath, props))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001009
1010 # Do bookkeeping while files are being downloaded in the background.
1011 cwd = os.path.join(outdir, settings.relative_cwd)
1012 if not os.path.isdir(cwd):
1013 os.makedirs(cwd)
1014 cmd = settings.command[:]
1015 # Ensure paths are correctly separated on windows.
1016 cmd[0] = cmd[0].replace('/', os.path.sep)
1017 cmd = fix_python_path(cmd)
1018
1019 # Now block on the remaining files to be downloaded and mapped.
1020 while remaining:
1021 obj = cache.wait_for(remaining)
1022 for filepath, properties in remaining.pop(obj):
1023 outfile = os.path.join(outdir, filepath)
1024 link_file(outfile, cache.path(obj), HARDLINK)
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001025 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001026 # It's not set on Windows.
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001027 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001028
1029 if settings.read_only:
1030 make_writable(outdir, True)
1031 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1032 try:
1033 with Profiler('RunTest') as _prof:
1034 return subprocess.call(cmd, cwd=cwd)
1035 except OSError:
1036 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1037 raise
1038 finally:
1039 rmtree(outdir)
1040
1041
1042def main():
1043 parser = optparse.OptionParser(
1044 usage='%prog <options>', description=sys.modules[__name__].__doc__)
1045 parser.add_option(
1046 '-v', '--verbose', action='count', default=0, help='Use multiple times')
1047 parser.add_option('--no-run', action='store_true', help='Skip the run part')
1048
1049 group = optparse.OptionGroup(parser, 'Data source')
1050 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001051 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001052 metavar='FILE',
1053 help='File/url describing what to map or run')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001054 # TODO(maruel): Remove once not used anymore.
1055 group.add_option(
1056 '-m', '--manifest', dest='isolated', help=optparse.SUPPRESS_HELP)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001057 group.add_option(
1058 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001059 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001060 parser.add_option_group(group)
1061
1062 group.add_option(
1063 '-r', '--remote', metavar='URL', help='Remote where to get the items')
1064 group = optparse.OptionGroup(parser, 'Cache management')
1065 group.add_option(
1066 '--cache',
1067 default='cache',
1068 metavar='DIR',
1069 help='Cache directory, default=%default')
1070 group.add_option(
1071 '--max-cache-size',
1072 type='int',
1073 metavar='NNN',
1074 default=20*1024*1024*1024,
1075 help='Trim if the cache gets larger than this value, default=%default')
1076 group.add_option(
1077 '--min-free-space',
1078 type='int',
1079 metavar='NNN',
1080 default=1*1024*1024*1024,
1081 help='Trim if disk free space becomes lower than this value, '
1082 'default=%default')
1083 group.add_option(
1084 '--max-items',
1085 type='int',
1086 metavar='NNN',
1087 default=100000,
1088 help='Trim if more than this number of items are in the cache '
1089 'default=%default')
1090 parser.add_option_group(group)
1091
1092 options, args = parser.parse_args()
1093 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
1094 logging.basicConfig(
1095 level=level,
1096 format='%(levelname)5s %(module)15s(%(lineno)3d): %(message)s')
1097
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001098 if bool(options.isolated) == bool(options.hash):
1099 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001100 if not options.remote:
1101 parser.error('--remote is required.')
1102 if args:
1103 parser.error('Unsupported args %s' % ' '.join(args))
1104
1105 policies = CachePolicies(
1106 options.max_cache_size, options.min_free_space, options.max_items)
1107 try:
1108 return run_tha_test(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001109 options.isolated or options.hash,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001110 os.path.abspath(options.cache),
1111 options.remote,
1112 policies)
1113 except (ConfigError, MappingError), e:
1114 print >> sys.stderr, str(e)
1115 return 1
1116
1117
1118if __name__ == '__main__':
1119 sys.exit(main())