blob: ed8a5f5b8b7d4e407bbbdbbc696d9a658d3bd668 [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
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000015import logging.handlers
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000016import optparse
17import os
18import Queue
19import re
20import shutil
21import stat
22import subprocess
23import sys
24import tempfile
25import threading
26import time
27import urllib
csharp@chromium.orga92403f2012-11-20 15:13:59 +000028import urllib2
29import zlib
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000030
31
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000032# Types of action accepted by link_file().
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000033HARDLINK, SYMLINK, COPY = range(1, 4)
34
35RE_IS_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$')
36
csharp@chromium.org8dc52542012-11-08 20:29:55 +000037# The file size to be used when we don't know the correct file size,
38# generally used for .isolated files.
39UNKNOWN_FILE_SIZE = None
40
csharp@chromium.orga92403f2012-11-20 15:13:59 +000041# The size of each chunk to read when downloading and unzipping files.
42ZIPPED_FILE_CHUNK = 16 * 1024
43
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000044# The name of the log file to use.
45RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
46
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000047
48class ConfigError(ValueError):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +000049 """Generic failure to load a .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000050 pass
51
52
53class MappingError(OSError):
54 """Failed to recreate the tree."""
55 pass
56
57
csharp@chromium.orga92403f2012-11-20 15:13:59 +000058class DownloadFileOpener(urllib.FancyURLopener):
59 """This class is needed to get urlretrive to raise an exception on
60 404 errors, instead of still writing to the file with the error code.
61 """
62 def http_error_default(self, url, fp, errcode, errmsg, headers):
63 raise urllib2.HTTPError(url, errcode, errmsg, headers, fp)
64
65
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000066def get_flavor():
67 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
68 flavors = {
69 'cygwin': 'win',
70 'win32': 'win',
71 'darwin': 'mac',
72 'sunos5': 'solaris',
73 'freebsd7': 'freebsd',
74 'freebsd8': 'freebsd',
75 }
76 return flavors.get(sys.platform, 'linux')
77
78
79def os_link(source, link_name):
80 """Add support for os.link() on Windows."""
81 if sys.platform == 'win32':
82 if not ctypes.windll.kernel32.CreateHardLinkW(
83 unicode(link_name), unicode(source), 0):
84 raise OSError()
85 else:
86 os.link(source, link_name)
87
88
89def readable_copy(outfile, infile):
90 """Makes a copy of the file that is readable by everyone."""
91 shutil.copy(infile, outfile)
92 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
93 stat.S_IRGRP | stat.S_IROTH)
94 os.chmod(outfile, read_enabled_mode)
95
96
97def link_file(outfile, infile, action):
98 """Links a file. The type of link depends on |action|."""
99 logging.debug('Mapping %s to %s' % (infile, outfile))
100 if action not in (HARDLINK, SYMLINK, COPY):
101 raise ValueError('Unknown mapping action %s' % action)
102 if not os.path.isfile(infile):
103 raise MappingError('%s is missing' % infile)
104 if os.path.isfile(outfile):
105 raise MappingError(
106 '%s already exist; insize:%d; outsize:%d' %
107 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
108
109 if action == COPY:
110 readable_copy(outfile, infile)
111 elif action == SYMLINK and sys.platform != 'win32':
112 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000113 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000114 else:
115 try:
116 os_link(infile, outfile)
117 except OSError:
118 # Probably a different file system.
119 logging.warn(
120 'Failed to hardlink, failing back to copy %s to %s' % (
121 infile, outfile))
122 readable_copy(outfile, infile)
123
124
125def _set_write_bit(path, read_only):
126 """Sets or resets the executable bit on a file or directory."""
127 mode = os.lstat(path).st_mode
128 if read_only:
129 mode = mode & 0500
130 else:
131 mode = mode | 0200
132 if hasattr(os, 'lchmod'):
133 os.lchmod(path, mode) # pylint: disable=E1101
134 else:
135 if stat.S_ISLNK(mode):
136 # Skip symlink without lchmod() support.
137 logging.debug('Can\'t change +w bit on symlink %s' % path)
138 return
139
140 # TODO(maruel): Implement proper DACL modification on Windows.
141 os.chmod(path, mode)
142
143
144def make_writable(root, read_only):
145 """Toggle the writable bit on a directory tree."""
146 root = os.path.abspath(root)
147 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
148 for filename in filenames:
149 _set_write_bit(os.path.join(dirpath, filename), read_only)
150
151 for dirname in dirnames:
152 _set_write_bit(os.path.join(dirpath, dirname), read_only)
153
154
155def rmtree(root):
156 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
157 make_writable(root, False)
158 if sys.platform == 'win32':
159 for i in range(3):
160 try:
161 shutil.rmtree(root)
162 break
163 except WindowsError: # pylint: disable=E0602
164 delay = (i+1)*2
165 print >> sys.stderr, (
166 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
167 time.sleep(delay)
168 else:
169 shutil.rmtree(root)
170
171
172def is_same_filesystem(path1, path2):
173 """Returns True if both paths are on the same filesystem.
174
175 This is required to enable the use of hardlinks.
176 """
177 assert os.path.isabs(path1), path1
178 assert os.path.isabs(path2), path2
179 if sys.platform == 'win32':
180 # If the drive letter mismatches, assume it's a separate partition.
181 # TODO(maruel): It should look at the underlying drive, a drive letter could
182 # be a mount point to a directory on another drive.
183 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
184 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
185 if path1[0].lower() != path2[0].lower():
186 return False
187 return os.stat(path1).st_dev == os.stat(path2).st_dev
188
189
190def get_free_space(path):
191 """Returns the number of free bytes."""
192 if sys.platform == 'win32':
193 free_bytes = ctypes.c_ulonglong(0)
194 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
195 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
196 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000197 # For OSes other than Windows.
198 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000199 return f.f_bfree * f.f_frsize
200
201
202def make_temp_dir(prefix, root_dir):
203 """Returns a temporary directory on the same file system as root_dir."""
204 base_temp_dir = None
205 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
206 base_temp_dir = os.path.dirname(root_dir)
207 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
208
209
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000210def load_isolated(content):
211 """Verifies the .isolated file is valid and loads this object with the json
212 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000213 """
214 try:
215 data = json.loads(content)
216 except ValueError:
217 raise ConfigError('Failed to parse: %s...' % content[:100])
218
219 if not isinstance(data, dict):
220 raise ConfigError('Expected dict, got %r' % data)
221
222 for key, value in data.iteritems():
223 if key == 'command':
224 if not isinstance(value, list):
225 raise ConfigError('Expected list, got %r' % value)
226 for subvalue in value:
227 if not isinstance(subvalue, basestring):
228 raise ConfigError('Expected string, got %r' % subvalue)
229
230 elif key == 'files':
231 if not isinstance(value, dict):
232 raise ConfigError('Expected dict, got %r' % value)
233 for subkey, subvalue in value.iteritems():
234 if not isinstance(subkey, basestring):
235 raise ConfigError('Expected string, got %r' % subkey)
236 if not isinstance(subvalue, dict):
237 raise ConfigError('Expected dict, got %r' % subvalue)
238 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000239 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000240 if not isinstance(subsubvalue, basestring):
241 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000242 elif subsubkey == 'm':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000243 if not isinstance(subsubvalue, int):
244 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000245 elif subsubkey == 'h':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000246 if not RE_IS_SHA1.match(subsubvalue):
247 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000248 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000249 if not isinstance(subsubvalue, int):
250 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000251 elif subsubkey == 't':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000252 if not isinstance(subsubvalue, int):
253 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000254 elif subsubkey == 'T':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000255 if not isinstance(subsubvalue, bool):
256 raise ConfigError('Expected bool, got %r' % subsubvalue)
257 else:
258 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000259 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000260 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000261 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
262 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000263
264 elif key == 'includes':
265 if not isinstance(value, list):
266 raise ConfigError('Expected list, got %r' % value)
267 for subvalue in value:
268 if not RE_IS_SHA1.match(subvalue):
269 raise ConfigError('Expected sha-1, got %r' % subvalue)
270
271 elif key == 'read_only':
272 if not isinstance(value, bool):
273 raise ConfigError('Expected bool, got %r' % value)
274
275 elif key == 'relative_cwd':
276 if not isinstance(value, basestring):
277 raise ConfigError('Expected string, got %r' % value)
278
279 elif key == 'os':
280 if value != get_flavor():
281 raise ConfigError(
282 'Expected \'os\' to be \'%s\' but got \'%s\'' %
283 (get_flavor(), value))
284
285 else:
286 raise ConfigError('Unknown key %s' % key)
287
288 return data
289
290
291def fix_python_path(cmd):
292 """Returns the fixed command line to call the right python executable."""
293 out = cmd[:]
294 if out[0] == 'python':
295 out[0] = sys.executable
296 elif out[0].endswith('.py'):
297 out.insert(0, sys.executable)
298 return out
299
300
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000301class WorkerThread(threading.Thread):
302 """Keeps the results of each task in a thread-local outputs variable."""
303 def __init__(self, tasks, *args, **kwargs):
304 super(WorkerThread, self).__init__(*args, **kwargs)
305 self._tasks = tasks
306 self.outputs = []
307 self.exceptions = []
308
309 self.daemon = True
310 self.start()
311
312 def run(self):
313 """Runs until a None task is queued."""
314 while True:
315 task = self._tasks.get()
316 if task is None:
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000317 # We're done.
318 return
319 try:
320 func, args, kwargs = task
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000321 self.outputs.append(func(*args, **kwargs))
322 except Exception, e:
323 logging.error('Caught exception! %s' % e)
324 self.exceptions.append(sys.exc_info())
325 finally:
326 self._tasks.task_done()
327
328
329class ThreadPool(object):
330 """Implements a multithreaded worker pool oriented for mapping jobs with
331 thread-local result storage.
332 """
333 QUEUE_CLASS = Queue.Queue
334
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000335 def __init__(self, num_threads, queue_size=0):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000336 logging.debug('Creating ThreadPool')
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000337 self.tasks = self.QUEUE_CLASS(queue_size)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000338 self._workers = [
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000339 WorkerThread(self.tasks, name='worker-%d' % i)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000340 for i in range(num_threads)
341 ]
342
343 def add_task(self, func, *args, **kwargs):
344 """Adds a task, a function to be executed by a worker.
345
346 The function's return value will be stored in the the worker's thread local
347 outputs list.
348 """
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000349 self.tasks.put((func, args, kwargs))
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000350
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000351 def join(self):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000352 """Extracts all the results from each threads unordered."""
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000353 self.tasks.join()
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000354 out = []
355 # Look for exceptions.
356 for w in self._workers:
357 if w.exceptions:
358 raise w.exceptions[0][0], w.exceptions[0][1], w.exceptions[0][2]
359 out.extend(w.outputs)
360 w.outputs = []
361 return out
362
363 def close(self):
364 """Closes all the threads."""
365 for _ in range(len(self._workers)):
366 # Enqueueing None causes the worker to stop.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000367 self.tasks.put(None)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000368 for t in self._workers:
369 t.join()
370
371 def __enter__(self):
372 """Enables 'with' statement."""
373 return self
374
375 def __exit__(self, exc_type, exc_value, traceback):
376 """Enables 'with' statement."""
377 self.close()
378
379
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000380def valid_file(filepath, size):
381 """Determines if the given files appears valid (currently it just checks
382 the file's size)."""
383 return (size == UNKNOWN_FILE_SIZE or size == os.stat(filepath).st_size)
384
385
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000386class Profiler(object):
387 def __init__(self, name):
388 self.name = name
389 self.start_time = None
390
391 def __enter__(self):
392 self.start_time = time.time()
393 return self
394
395 def __exit__(self, _exc_type, _exec_value, _traceback):
396 time_taken = time.time() - self.start_time
397 logging.info('Profiling: Section %s took %3.3f seconds',
398 self.name, time_taken)
399
400
401class Remote(object):
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000402 """Priority based worker queue to fetch or upload files from a
403 content-address server. Any function may be given as the fetcher/upload,
404 as long as it takes two inputs (the item contents, and their relative
405 destination).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000406
407 Supports local file system, CIFS or http remotes.
408
409 When the priority of items is equals, works in strict FIFO mode.
410 """
411 # Initial and maximum number of worker threads.
412 INITIAL_WORKERS = 2
413 MAX_WORKERS = 16
414 # Priorities.
415 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
416 INTERNAL_PRIORITY_BITS = (1<<8) - 1
417 RETRIES = 5
418
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000419 def __init__(self, destination_root):
420 # Function to fetch a remote object or upload to a remote location..
421 self._do_item = self.get_file_handler(destination_root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000422 # Contains tuple(priority, index, obj, destination).
423 self._queue = Queue.PriorityQueue()
424 # Contains tuple(priority, index, obj).
425 self._done = Queue.PriorityQueue()
426
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000427 # Contains generated exceptions that haven't been handled yet.
428 self._exceptions = Queue.Queue()
429
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000430 # To keep FIFO ordering in self._queue. It is assumed xrange's iterator is
431 # thread-safe.
432 self._next_index = xrange(0, 1<<30).__iter__().next
433
434 # Control access to the following member.
435 self._ready_lock = threading.Lock()
436 # Number of threads in wait state.
437 self._ready = 0
438
439 # Control access to the following member.
440 self._workers_lock = threading.Lock()
441 self._workers = []
442 for _ in range(self.INITIAL_WORKERS):
443 self._add_worker()
444
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000445 def join(self):
446 """Blocks until the queue is empty."""
447 self._queue.join()
448
449 def next_exception(self):
450 """Returns the next unhandled exception, or None if there is
451 no exception."""
452 try:
453 return self._exceptions.get_nowait()
454 except Queue.Empty:
455 return None
456
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000457 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000458 """Retrieves an object from the remote data store.
459
460 The smaller |priority| gets fetched first.
461
462 Thread-safe.
463 """
464 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000465 self._add_to_queue(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000466
467 def get_result(self):
468 """Returns the next file that was successfully fetched."""
469 r = self._done.get()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000470 if r[0] == -1:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000471 # It's an exception.
472 raise r[2][0], r[2][1], r[2][2]
473 return r[2]
474
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000475 def _add_to_queue(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000476 with self._ready_lock:
477 start_new_worker = not self._ready
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000478 self._queue.put((priority, self._next_index(), obj, dest, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000479 if start_new_worker:
480 self._add_worker()
481
482 def _add_worker(self):
483 """Add one worker thread if there isn't too many. Thread-safe."""
484 with self._workers_lock:
485 if len(self._workers) >= self.MAX_WORKERS:
486 return False
487 worker = threading.Thread(target=self._run)
488 self._workers.append(worker)
489 worker.daemon = True
490 worker.start()
491
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000492 def _step_done(self, result):
493 """Worker helper function"""
494 self._done.put(result)
495 self._queue.task_done()
496 if result[0] == -1:
497 self._exceptions.put(sys.exc_info())
498
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000499 def _run(self):
500 """Worker thread loop."""
501 while True:
502 try:
503 with self._ready_lock:
504 self._ready += 1
505 item = self._queue.get()
506 finally:
507 with self._ready_lock:
508 self._ready -= 1
509 if not item:
510 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000511 priority, index, obj, dest, size = item
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000512 try:
513 self._do_item(obj, dest)
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000514 if size and not valid_file(dest, size):
515 download_size = os.stat(dest).st_size
516 os.remove(dest)
517 raise IOError('File incorrect size after download of %s. Got %s and '
518 'expected %s' % (dest, download_size, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000519 except IOError:
520 # Retry a few times, lowering the priority.
521 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000522 self._add_to_queue(priority + 1, obj, dest, size)
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000523 self._queue.task_done()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000524 continue
525 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000526 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000527 except:
528 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000529 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000530 else:
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000531 self._step_done((priority, index, obj))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000532
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +0000533 def get_file_handler(self, file_or_url): # pylint: disable=R0201
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000534 """Returns a object to retrieve objects from a remote."""
535 if re.match(r'^https?://.+$', file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000536 def download_file(item, dest):
537 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
538 # easy.
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000539
540 # TODO(csharp): This is a temporary workaround to generate the gzipped
541 # url, remove once the files are always zipped before being uploaded.
542 try:
543 zipped_source = file_or_url.rstrip('/') + '-gzip/' + item
544 logging.debug('download_file(%s)', zipped_source)
545 connection = urllib2.urlopen(zipped_source)
546 decompressor = zlib.decompressobj()
547 with open(dest, 'wb') as f:
548 while True:
549 chunk = connection.read(ZIPPED_FILE_CHUNK)
550 if not chunk:
551 break
552 f.write(decompressor.decompress(chunk))
553 # Ensure that all the data was properly decompressed.
554 uncompressed_data = decompressor.flush()
555 assert not uncompressed_data
556 except urllib2.URLError:
557 # Try the unzipped version
558 unzipped_source = file_or_url + item
559 logging.debug('Zipped version missing, try unzipped version')
560 logging.debug('download_file(%s, %s)', unzipped_source, dest)
561 DownloadFileOpener().retrieve(unzipped_source, dest)
562
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000563 return download_file
564
565 def copy_file(item, dest):
566 source = os.path.join(file_or_url, item)
567 logging.debug('copy_file(%s, %s)', source, dest)
568 shutil.copy(source, dest)
569 return copy_file
570
571
572class CachePolicies(object):
573 def __init__(self, max_cache_size, min_free_space, max_items):
574 """
575 Arguments:
576 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
577 cache is effectively a leak.
578 - min_free_space: Trim if disk free space becomes lower than this value. If
579 0, it unconditionally fill the disk.
580 - max_items: Maximum number of items to keep in the cache. If 0, do not
581 enforce a limit.
582 """
583 self.max_cache_size = max_cache_size
584 self.min_free_space = min_free_space
585 self.max_items = max_items
586
587
588class Cache(object):
589 """Stateful LRU cache.
590
591 Saves its state as json file.
592 """
593 STATE_FILE = 'state.json'
594
595 def __init__(self, cache_dir, remote, policies):
596 """
597 Arguments:
598 - cache_dir: Directory where to place the cache.
599 - remote: Remote where to fetch items from.
600 - policies: cache retention policies.
601 """
602 self.cache_dir = cache_dir
603 self.remote = remote
604 self.policies = policies
605 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
606 # The tuple(file, size) are kept as an array in a LRU style. E.g.
607 # self.state[0] is the oldest item.
608 self.state = []
609 # A lookup map to speed up searching.
610 self._lookup = {}
611 self._dirty = False
612
613 # Items currently being fetched. Keep it local to reduce lock contention.
614 self._pending_queue = set()
615
616 # Profiling values.
617 self._added = []
618 self._removed = []
619 self._free_disk = 0
620
621 if not os.path.isdir(self.cache_dir):
622 os.makedirs(self.cache_dir)
623 if os.path.isfile(self.state_file):
624 try:
625 self.state = json.load(open(self.state_file, 'r'))
626 except (IOError, ValueError), e:
627 # Too bad. The file will be overwritten and the cache cleared.
628 logging.error(
629 'Broken state file %s, ignoring.\n%s' % (self.STATE_FILE, e))
630 if (not isinstance(self.state, list) or
631 not all(
632 isinstance(i, (list, tuple)) and len(i) == 2 for i in self.state)):
633 # Discard.
634 self.state = []
635 self._dirty = True
636
637 # Ensure that all files listed in the state still exist and add new ones.
638 previous = set(filename for filename, _ in self.state)
639 if len(previous) != len(self.state):
640 logging.warn('Cache state is corrupted')
641 self._dirty = True
642 self.state = []
643 else:
644 added = 0
645 for filename in os.listdir(self.cache_dir):
646 if filename == self.STATE_FILE:
647 continue
648 if filename in previous:
649 previous.remove(filename)
650 continue
651 # An untracked file.
652 self._dirty = True
653 if not RE_IS_SHA1.match(filename):
654 logging.warn('Removing unknown file %s from cache', filename)
655 os.remove(self.path(filename))
656 else:
657 # Insert as the oldest file. It will be deleted eventually if not
658 # accessed.
659 self._add(filename, False)
660 added += 1
661 if added:
662 logging.warn('Added back %d unknown files', added)
663 self.state = [
664 (filename, size) for filename, size in self.state
665 if filename not in previous
666 ]
667 self._update_lookup()
668
669 with Profiler('SetupTrimming'):
670 self.trim()
671
672 def __enter__(self):
673 return self
674
675 def __exit__(self, _exc_type, _exec_value, _traceback):
676 with Profiler('CleanupTrimming'):
677 self.trim()
678
679 logging.info(
680 '%4d (%7dkb) added', len(self._added), sum(self._added) / 1024)
681 logging.info(
682 '%4d (%7dkb) current',
683 len(self.state),
684 sum(i[1] for i in self.state) / 1024)
685 logging.info(
686 '%4d (%7dkb) removed', len(self._removed), sum(self._removed) / 1024)
687 logging.info('%7dkb free', self._free_disk / 1024)
688
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000689 def remove_file_at_index(self, index):
690 """Removes the file at the given index."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000691 try:
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000692 filename, size = self.state.pop(index)
693 # TODO(csharp): _lookup should self-update.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000694 del self._lookup[filename]
695 self._removed.append(size)
696 os.remove(self.path(filename))
697 self._dirty = True
698 except OSError as e:
699 logging.error('Error attempting to delete a file\n%s' % e)
700
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000701 def remove_lru_file(self):
702 """Removes the last recently used file."""
703 self.remove_file_at_index(0)
704
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000705 def trim(self):
706 """Trims anything we don't know, make sure enough free space exists."""
707 # Ensure maximum cache size.
708 if self.policies.max_cache_size and self.state:
709 while sum(i[1] for i in self.state) > self.policies.max_cache_size:
710 self.remove_lru_file()
711
712 # Ensure maximum number of items in the cache.
713 if self.policies.max_items and self.state:
714 while len(self.state) > self.policies.max_items:
715 self.remove_lru_file()
716
717 # Ensure enough free space.
718 self._free_disk = get_free_space(self.cache_dir)
719 while (
720 self.policies.min_free_space and
721 self.state and
722 self._free_disk < self.policies.min_free_space):
723 self.remove_lru_file()
724 self._free_disk = get_free_space(self.cache_dir)
725
726 self.save()
727
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000728 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000729 """Retrieves a file from the remote, if not already cached, and adds it to
730 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000731
732 If the file is in the cache, verifiy that the file is valid (i.e. it is
733 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000734 """
735 assert not '/' in item
736 path = self.path(item)
737 index = self._lookup.get(item)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000738
739 if index is not None:
740 if not valid_file(self.path(item), size):
741 self.remove_file_at_index(index)
742 self._update_lookup()
743 index = None
744 else:
745 assert index < len(self.state)
746 # Was already in cache. Update it's LRU value by putting it at the end.
747 self.state.append(self.state.pop(index))
748 self._dirty = True
749 self._update_lookup()
750
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000751 if index is None:
752 if item in self._pending_queue:
753 # Already pending. The same object could be referenced multiple times.
754 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000755 self.remote.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000756 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000757
758 def add(self, filepath, obj):
759 """Forcibly adds a file to the cache."""
760 if not obj in self._lookup:
761 link_file(self.path(obj), filepath, HARDLINK)
762 self._add(obj, True)
763
764 def path(self, item):
765 """Returns the path to one item."""
766 return os.path.join(self.cache_dir, item)
767
768 def save(self):
769 """Saves the LRU ordering."""
770 json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':'))
771
772 def wait_for(self, items):
773 """Starts a loop that waits for at least one of |items| to be retrieved.
774
775 Returns the first item retrieved.
776 """
777 # Flush items already present.
778 for item in items:
779 if item in self._lookup:
780 return item
781
782 assert all(i in self._pending_queue for i in items), (
783 items, self._pending_queue)
784 # Note that:
785 # len(self._pending_queue) ==
786 # ( len(self.remote._workers) - self.remote._ready +
787 # len(self._remote._queue) + len(self._remote.done))
788 # There is no lock-free way to verify that.
789 while self._pending_queue:
790 item = self.remote.get_result()
791 self._pending_queue.remove(item)
792 self._add(item, True)
793 if item in items:
794 return item
795
796 def _add(self, item, at_end):
797 """Adds an item in the internal state.
798
799 If |at_end| is False, self._lookup becomes inconsistent and
800 self._update_lookup() must be called.
801 """
802 size = os.stat(self.path(item)).st_size
803 self._added.append(size)
804 if at_end:
805 self.state.append((item, size))
806 self._lookup[item] = len(self.state) - 1
807 else:
808 self.state.insert(0, (item, size))
809 self._dirty = True
810
811 def _update_lookup(self):
812 self._lookup = dict(
813 (filename, index) for index, (filename, _) in enumerate(self.state))
814
815
816
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000817class IsolatedFile(object):
818 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000819 def __init__(self, obj_hash):
820 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000821 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000822 self.obj_hash = obj_hash
823 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000824 # .isolate and all the .isolated files recursively included by it with
825 # 'includes' key. The order of each sha-1 in 'includes', each representing a
826 # .isolated file in the hash table, is important, as the later ones are not
827 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000828 self.can_fetch = False
829
830 # Raw data.
831 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000832 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000833 self.children = []
834
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000835 # Set once the .isolated file is loaded.
836 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000837 # Set once the files are fetched.
838 self.files_fetched = False
839
840 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000841 """Verifies the .isolated file is valid and loads this object with the json
842 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000843 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000844 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
845 assert not self._is_parsed
846 self.data = load_isolated(content)
847 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
848 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000849
850 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000851 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000852
853 Preemptively request files.
854
855 Note that |files| is modified by this function.
856 """
857 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000858 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000859 return
860 logging.debug('fetch_files(%s)' % self.obj_hash)
861 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000862 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000863 # overriden files must not be fetched.
864 if filepath not in files:
865 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000866 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000867 # Preemptively request files.
868 logging.debug('fetching %s' % filepath)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000869 cache.retrieve(Remote.MED, properties['h'], properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000870 self.files_fetched = True
871
872
873class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000874 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000875 def __init__(self):
876 self.command = []
877 self.files = {}
878 self.read_only = None
879 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000880 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000881 self.root = None
882 logging.debug('Settings')
883
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000884 def load(self, cache, root_isolated_hash):
885 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000886
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000887 It enables support for "included" .isolated files. They are processed in
888 strict order but fetched asynchronously from the cache. This is important so
889 that a file in an included .isolated file that is overridden by an embedding
890 .isolated file is not fetched neededlessly. The includes are fetched in one
891 pass and the files are fetched as soon as all the ones on the left-side
892 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000893
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000894 The prioritization is very important here for nested .isolated files.
895 'includes' have the highest priority and the algorithm is optimized for both
896 deep and wide trees. A deep one is a long link of .isolated files referenced
897 one at a time by one item in 'includes'. A wide one has a large number of
898 'includes' in a single .isolated file. 'left' is defined as an included
899 .isolated file earlier in the 'includes' list. So the order of the elements
900 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000901 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000902 self.root = IsolatedFile(root_isolated_hash)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000903 cache.retrieve(Remote.HIGH, root_isolated_hash, UNKNOWN_FILE_SIZE)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000904 pending = {root_isolated_hash: self.root}
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000905 # Keeps the list of retrieved items to refuse recursive includes.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000906 retrieved = [root_isolated_hash]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000907
908 def update_self(node):
909 node.fetch_files(cache, self.files)
910 # Grabs properties.
911 if not self.command and node.data.get('command'):
912 self.command = node.data['command']
913 if self.read_only is None and node.data.get('read_only') is not None:
914 self.read_only = node.data['read_only']
915 if (self.relative_cwd is None and
916 node.data.get('relative_cwd') is not None):
917 self.relative_cwd = node.data['relative_cwd']
918
919 def traverse_tree(node):
920 if node.can_fetch:
921 if not node.files_fetched:
922 update_self(node)
923 will_break = False
924 for i in node.children:
925 if not i.can_fetch:
926 if will_break:
927 break
928 # Automatically mark the first one as fetcheable.
929 i.can_fetch = True
930 will_break = True
931 traverse_tree(i)
932
933 while pending:
934 item_hash = cache.wait_for(pending)
935 item = pending.pop(item_hash)
936 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000937 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000938 # It's the root item.
939 item.can_fetch = True
940
941 for new_child in item.children:
942 h = new_child.obj_hash
943 if h in retrieved:
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000944 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000945 pending[h] = new_child
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000946 cache.retrieve(Remote.HIGH, h, UNKNOWN_FILE_SIZE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000947
948 # Traverse the whole tree to see if files can now be fetched.
949 traverse_tree(self.root)
950 def check(n):
951 return all(check(x) for x in n.children) and n.files_fetched
952 assert check(self.root)
953 self.relative_cwd = self.relative_cwd or ''
954 self.read_only = self.read_only or False
955
956
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000957def run_tha_test(isolated_hash, cache_dir, remote, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000958 """Downloads the dependencies in the cache, hardlinks them into a temporary
959 directory and runs the executable.
960 """
961 settings = Settings()
962 with Cache(cache_dir, Remote(remote), policies) as cache:
963 outdir = make_temp_dir('run_tha_test', cache_dir)
964 try:
965 # Initiate all the files download.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000966 with Profiler('GetIsolateds') as _prof:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000967 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000968 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000969 # Adds it in the cache. While not strictly necessary, this simplifies
970 # the rest.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000971 h = hashlib.sha1(open(isolated_hash, 'r').read()).hexdigest()
972 cache.add(isolated_hash, h)
973 isolated_hash = h
974 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000975
976 if not settings.command:
977 print >> sys.stderr, 'No command to run'
978 return 1
979
980 with Profiler('GetRest') as _prof:
981 logging.debug('Creating directories')
982 # Creates the tree of directories to create.
983 directories = set(os.path.dirname(f) for f in settings.files)
984 for item in list(directories):
985 while item:
986 directories.add(item)
987 item = os.path.dirname(item)
988 for d in sorted(directories):
989 if d:
990 os.mkdir(os.path.join(outdir, d))
991
992 # Creates the links if necessary.
993 for filepath, properties in settings.files.iteritems():
994 if 'link' not in properties:
995 continue
996 outfile = os.path.join(outdir, filepath)
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000997 # symlink doesn't exist on Windows. So the 'link' property should
998 # never be specified for windows .isolated file.
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000999 os.symlink(properties['l'], outfile) # pylint: disable=E1101
1000 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001001 # It's not set on Windows.
maruel@chromium.org96768a42012-10-31 18:49:18 +00001002 lchmod = getattr(os, 'lchmod', None)
1003 if lchmod:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001004 lchmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001005
1006 # Remaining files to be processed.
1007 # Note that files could still be not be downloaded yet here.
1008 remaining = dict()
1009 for filepath, props in settings.files.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001010 if 'h' in props:
1011 remaining.setdefault(props['h'], []).append((filepath, props))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001012
1013 # Do bookkeeping while files are being downloaded in the background.
1014 cwd = os.path.join(outdir, settings.relative_cwd)
1015 if not os.path.isdir(cwd):
1016 os.makedirs(cwd)
1017 cmd = settings.command[:]
1018 # Ensure paths are correctly separated on windows.
1019 cmd[0] = cmd[0].replace('/', os.path.sep)
1020 cmd = fix_python_path(cmd)
1021
1022 # Now block on the remaining files to be downloaded and mapped.
1023 while remaining:
1024 obj = cache.wait_for(remaining)
1025 for filepath, properties in remaining.pop(obj):
1026 outfile = os.path.join(outdir, filepath)
1027 link_file(outfile, cache.path(obj), HARDLINK)
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001028 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001029 # It's not set on Windows.
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001030 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001031
1032 if settings.read_only:
1033 make_writable(outdir, True)
1034 logging.info('Running %s, cwd=%s' % (cmd, cwd))
1035 try:
1036 with Profiler('RunTest') as _prof:
1037 return subprocess.call(cmd, cwd=cwd)
1038 except OSError:
1039 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1040 raise
1041 finally:
1042 rmtree(outdir)
1043
1044
1045def main():
1046 parser = optparse.OptionParser(
1047 usage='%prog <options>', description=sys.modules[__name__].__doc__)
1048 parser.add_option(
1049 '-v', '--verbose', action='count', default=0, help='Use multiple times')
1050 parser.add_option('--no-run', action='store_true', help='Skip the run part')
1051
1052 group = optparse.OptionGroup(parser, 'Data source')
1053 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001054 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001055 metavar='FILE',
1056 help='File/url describing what to map or run')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001057 # TODO(maruel): Remove once not used anymore.
1058 group.add_option(
1059 '-m', '--manifest', dest='isolated', help=optparse.SUPPRESS_HELP)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001060 group.add_option(
1061 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001062 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001063 parser.add_option_group(group)
1064
1065 group.add_option(
1066 '-r', '--remote', metavar='URL', help='Remote where to get the items')
1067 group = optparse.OptionGroup(parser, 'Cache management')
1068 group.add_option(
1069 '--cache',
1070 default='cache',
1071 metavar='DIR',
1072 help='Cache directory, default=%default')
1073 group.add_option(
1074 '--max-cache-size',
1075 type='int',
1076 metavar='NNN',
1077 default=20*1024*1024*1024,
1078 help='Trim if the cache gets larger than this value, default=%default')
1079 group.add_option(
1080 '--min-free-space',
1081 type='int',
1082 metavar='NNN',
1083 default=1*1024*1024*1024,
1084 help='Trim if disk free space becomes lower than this value, '
1085 'default=%default')
1086 group.add_option(
1087 '--max-items',
1088 type='int',
1089 metavar='NNN',
1090 default=100000,
1091 help='Trim if more than this number of items are in the cache '
1092 'default=%default')
1093 parser.add_option_group(group)
1094
1095 options, args = parser.parse_args()
1096 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
csharp@chromium.orgff2a4662012-11-21 20:49:32 +00001097
1098 logging_console = logging.StreamHandler()
1099 logging_console.setFormatter(logging.Formatter(
1100 '%(levelname)5s %(module)15s(%(lineno)3d): %(message)s'))
1101 logging_console.setLevel(level)
1102 logging.getLogger().addHandler(logging_console)
1103
1104 logging_rotating_file = logging.handlers.RotatingFileHandler(
1105 RUN_ISOLATED_LOG_FILE,
1106 maxBytes=10 * 1024 * 1024, backupCount=5)
1107 logging_rotating_file.setLevel(logging.DEBUG)
1108 logging_rotating_file.setFormatter(logging.Formatter(
1109 '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)3d): %(message)s'))
1110 logging.getLogger().addHandler(logging_rotating_file)
1111
1112 logging.getLogger().setLevel(logging.DEBUG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001113
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001114 if bool(options.isolated) == bool(options.hash):
1115 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001116 if not options.remote:
1117 parser.error('--remote is required.')
1118 if args:
1119 parser.error('Unsupported args %s' % ' '.join(args))
1120
1121 policies = CachePolicies(
1122 options.max_cache_size, options.min_free_space, options.max_items)
1123 try:
1124 return run_tha_test(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001125 options.isolated or options.hash,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001126 os.path.abspath(options.cache),
1127 options.remote,
1128 policies)
1129 except (ConfigError, MappingError), e:
1130 print >> sys.stderr, str(e)
1131 return 1
1132
1133
1134if __name__ == '__main__':
1135 sys.exit(main())