blob: 42b5c411ab9fec858246b6be547b97f1d396d13e [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
csharp@chromium.orge217f302012-11-22 16:51:53 +000047# The base directory containing this file.
48BASE_DIR = os.path.dirname(os.path.abspath(__file__))
49
50# The name of the log to use for the run_test_cases.py command
51RUN_TEST_CASES_LOG = os.path.join(BASE_DIR, 'run_test_cases.log')
52
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000053
54class ConfigError(ValueError):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +000055 """Generic failure to load a .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000056 pass
57
58
59class MappingError(OSError):
60 """Failed to recreate the tree."""
61 pass
62
63
csharp@chromium.orga92403f2012-11-20 15:13:59 +000064class DownloadFileOpener(urllib.FancyURLopener):
65 """This class is needed to get urlretrive to raise an exception on
66 404 errors, instead of still writing to the file with the error code.
67 """
68 def http_error_default(self, url, fp, errcode, errmsg, headers):
69 raise urllib2.HTTPError(url, errcode, errmsg, headers, fp)
70
71
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000072def get_flavor():
73 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
74 flavors = {
75 'cygwin': 'win',
76 'win32': 'win',
77 'darwin': 'mac',
78 'sunos5': 'solaris',
79 'freebsd7': 'freebsd',
80 'freebsd8': 'freebsd',
81 }
82 return flavors.get(sys.platform, 'linux')
83
84
85def os_link(source, link_name):
86 """Add support for os.link() on Windows."""
87 if sys.platform == 'win32':
88 if not ctypes.windll.kernel32.CreateHardLinkW(
89 unicode(link_name), unicode(source), 0):
90 raise OSError()
91 else:
92 os.link(source, link_name)
93
94
95def readable_copy(outfile, infile):
96 """Makes a copy of the file that is readable by everyone."""
97 shutil.copy(infile, outfile)
98 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
99 stat.S_IRGRP | stat.S_IROTH)
100 os.chmod(outfile, read_enabled_mode)
101
102
103def link_file(outfile, infile, action):
104 """Links a file. The type of link depends on |action|."""
105 logging.debug('Mapping %s to %s' % (infile, outfile))
106 if action not in (HARDLINK, SYMLINK, COPY):
107 raise ValueError('Unknown mapping action %s' % action)
108 if not os.path.isfile(infile):
109 raise MappingError('%s is missing' % infile)
110 if os.path.isfile(outfile):
111 raise MappingError(
112 '%s already exist; insize:%d; outsize:%d' %
113 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
114
115 if action == COPY:
116 readable_copy(outfile, infile)
117 elif action == SYMLINK and sys.platform != 'win32':
118 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000119 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000120 else:
121 try:
122 os_link(infile, outfile)
123 except OSError:
124 # Probably a different file system.
125 logging.warn(
126 'Failed to hardlink, failing back to copy %s to %s' % (
127 infile, outfile))
128 readable_copy(outfile, infile)
129
130
131def _set_write_bit(path, read_only):
132 """Sets or resets the executable bit on a file or directory."""
133 mode = os.lstat(path).st_mode
134 if read_only:
135 mode = mode & 0500
136 else:
137 mode = mode | 0200
138 if hasattr(os, 'lchmod'):
139 os.lchmod(path, mode) # pylint: disable=E1101
140 else:
141 if stat.S_ISLNK(mode):
142 # Skip symlink without lchmod() support.
143 logging.debug('Can\'t change +w bit on symlink %s' % path)
144 return
145
146 # TODO(maruel): Implement proper DACL modification on Windows.
147 os.chmod(path, mode)
148
149
150def make_writable(root, read_only):
151 """Toggle the writable bit on a directory tree."""
152 root = os.path.abspath(root)
153 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
154 for filename in filenames:
155 _set_write_bit(os.path.join(dirpath, filename), read_only)
156
157 for dirname in dirnames:
158 _set_write_bit(os.path.join(dirpath, dirname), read_only)
159
160
161def rmtree(root):
162 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
163 make_writable(root, False)
164 if sys.platform == 'win32':
165 for i in range(3):
166 try:
167 shutil.rmtree(root)
168 break
169 except WindowsError: # pylint: disable=E0602
170 delay = (i+1)*2
171 print >> sys.stderr, (
172 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
173 time.sleep(delay)
174 else:
175 shutil.rmtree(root)
176
177
178def is_same_filesystem(path1, path2):
179 """Returns True if both paths are on the same filesystem.
180
181 This is required to enable the use of hardlinks.
182 """
183 assert os.path.isabs(path1), path1
184 assert os.path.isabs(path2), path2
185 if sys.platform == 'win32':
186 # If the drive letter mismatches, assume it's a separate partition.
187 # TODO(maruel): It should look at the underlying drive, a drive letter could
188 # be a mount point to a directory on another drive.
189 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
190 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
191 if path1[0].lower() != path2[0].lower():
192 return False
193 return os.stat(path1).st_dev == os.stat(path2).st_dev
194
195
196def get_free_space(path):
197 """Returns the number of free bytes."""
198 if sys.platform == 'win32':
199 free_bytes = ctypes.c_ulonglong(0)
200 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
201 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
202 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000203 # For OSes other than Windows.
204 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000205 return f.f_bfree * f.f_frsize
206
207
208def make_temp_dir(prefix, root_dir):
209 """Returns a temporary directory on the same file system as root_dir."""
210 base_temp_dir = None
211 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
212 base_temp_dir = os.path.dirname(root_dir)
213 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
214
215
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000216def load_isolated(content):
217 """Verifies the .isolated file is valid and loads this object with the json
218 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000219 """
220 try:
221 data = json.loads(content)
222 except ValueError:
223 raise ConfigError('Failed to parse: %s...' % content[:100])
224
225 if not isinstance(data, dict):
226 raise ConfigError('Expected dict, got %r' % data)
227
228 for key, value in data.iteritems():
229 if key == 'command':
230 if not isinstance(value, list):
231 raise ConfigError('Expected list, got %r' % value)
232 for subvalue in value:
233 if not isinstance(subvalue, basestring):
234 raise ConfigError('Expected string, got %r' % subvalue)
235
236 elif key == 'files':
237 if not isinstance(value, dict):
238 raise ConfigError('Expected dict, got %r' % value)
239 for subkey, subvalue in value.iteritems():
240 if not isinstance(subkey, basestring):
241 raise ConfigError('Expected string, got %r' % subkey)
242 if not isinstance(subvalue, dict):
243 raise ConfigError('Expected dict, got %r' % subvalue)
244 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000245 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000246 if not isinstance(subsubvalue, basestring):
247 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000248 elif subsubkey == 'm':
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 == 'h':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000252 if not RE_IS_SHA1.match(subsubvalue):
253 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000254 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000255 if not isinstance(subsubvalue, int):
256 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000257 elif subsubkey == 't':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000258 if not isinstance(subsubvalue, int):
259 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000260 elif subsubkey == 'T':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000261 if not isinstance(subsubvalue, bool):
262 raise ConfigError('Expected bool, got %r' % subsubvalue)
263 else:
264 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000265 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000266 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000267 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
268 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000269
270 elif key == 'includes':
271 if not isinstance(value, list):
272 raise ConfigError('Expected list, got %r' % value)
273 for subvalue in value:
274 if not RE_IS_SHA1.match(subvalue):
275 raise ConfigError('Expected sha-1, got %r' % subvalue)
276
277 elif key == 'read_only':
278 if not isinstance(value, bool):
279 raise ConfigError('Expected bool, got %r' % value)
280
281 elif key == 'relative_cwd':
282 if not isinstance(value, basestring):
283 raise ConfigError('Expected string, got %r' % value)
284
285 elif key == 'os':
286 if value != get_flavor():
287 raise ConfigError(
288 'Expected \'os\' to be \'%s\' but got \'%s\'' %
289 (get_flavor(), value))
290
291 else:
292 raise ConfigError('Unknown key %s' % key)
293
294 return data
295
296
297def fix_python_path(cmd):
298 """Returns the fixed command line to call the right python executable."""
299 out = cmd[:]
300 if out[0] == 'python':
301 out[0] = sys.executable
302 elif out[0].endswith('.py'):
303 out.insert(0, sys.executable)
304 return out
305
306
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000307class WorkerThread(threading.Thread):
308 """Keeps the results of each task in a thread-local outputs variable."""
309 def __init__(self, tasks, *args, **kwargs):
310 super(WorkerThread, self).__init__(*args, **kwargs)
311 self._tasks = tasks
312 self.outputs = []
313 self.exceptions = []
314
315 self.daemon = True
316 self.start()
317
318 def run(self):
319 """Runs until a None task is queued."""
320 while True:
321 task = self._tasks.get()
322 if task is None:
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000323 # We're done.
324 return
325 try:
326 func, args, kwargs = task
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000327 self.outputs.append(func(*args, **kwargs))
328 except Exception, e:
329 logging.error('Caught exception! %s' % e)
330 self.exceptions.append(sys.exc_info())
331 finally:
332 self._tasks.task_done()
333
334
335class ThreadPool(object):
336 """Implements a multithreaded worker pool oriented for mapping jobs with
337 thread-local result storage.
338 """
339 QUEUE_CLASS = Queue.Queue
340
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000341 def __init__(self, num_threads, queue_size=0):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000342 logging.debug('Creating ThreadPool')
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000343 self.tasks = self.QUEUE_CLASS(queue_size)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000344 self._workers = [
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000345 WorkerThread(self.tasks, name='worker-%d' % i)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000346 for i in range(num_threads)
347 ]
348
349 def add_task(self, func, *args, **kwargs):
350 """Adds a task, a function to be executed by a worker.
351
352 The function's return value will be stored in the the worker's thread local
353 outputs list.
354 """
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000355 self.tasks.put((func, args, kwargs))
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000356
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000357 def join(self):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000358 """Extracts all the results from each threads unordered."""
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000359 self.tasks.join()
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000360 out = []
361 # Look for exceptions.
362 for w in self._workers:
363 if w.exceptions:
364 raise w.exceptions[0][0], w.exceptions[0][1], w.exceptions[0][2]
365 out.extend(w.outputs)
366 w.outputs = []
367 return out
368
369 def close(self):
370 """Closes all the threads."""
371 for _ in range(len(self._workers)):
372 # Enqueueing None causes the worker to stop.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000373 self.tasks.put(None)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000374 for t in self._workers:
375 t.join()
376
377 def __enter__(self):
378 """Enables 'with' statement."""
379 return self
380
381 def __exit__(self, exc_type, exc_value, traceback):
382 """Enables 'with' statement."""
383 self.close()
384
385
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000386def valid_file(filepath, size):
387 """Determines if the given files appears valid (currently it just checks
388 the file's size)."""
389 return (size == UNKNOWN_FILE_SIZE or size == os.stat(filepath).st_size)
390
391
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000392class Profiler(object):
393 def __init__(self, name):
394 self.name = name
395 self.start_time = None
396
397 def __enter__(self):
398 self.start_time = time.time()
399 return self
400
401 def __exit__(self, _exc_type, _exec_value, _traceback):
402 time_taken = time.time() - self.start_time
403 logging.info('Profiling: Section %s took %3.3f seconds',
404 self.name, time_taken)
405
406
407class Remote(object):
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000408 """Priority based worker queue to fetch or upload files from a
409 content-address server. Any function may be given as the fetcher/upload,
410 as long as it takes two inputs (the item contents, and their relative
411 destination).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000412
413 Supports local file system, CIFS or http remotes.
414
415 When the priority of items is equals, works in strict FIFO mode.
416 """
417 # Initial and maximum number of worker threads.
418 INITIAL_WORKERS = 2
419 MAX_WORKERS = 16
420 # Priorities.
421 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
422 INTERNAL_PRIORITY_BITS = (1<<8) - 1
423 RETRIES = 5
424
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000425 def __init__(self, destination_root):
426 # Function to fetch a remote object or upload to a remote location..
427 self._do_item = self.get_file_handler(destination_root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000428 # Contains tuple(priority, index, obj, destination).
429 self._queue = Queue.PriorityQueue()
430 # Contains tuple(priority, index, obj).
431 self._done = Queue.PriorityQueue()
432
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000433 # Contains generated exceptions that haven't been handled yet.
434 self._exceptions = Queue.Queue()
435
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000436 # To keep FIFO ordering in self._queue. It is assumed xrange's iterator is
437 # thread-safe.
438 self._next_index = xrange(0, 1<<30).__iter__().next
439
440 # Control access to the following member.
441 self._ready_lock = threading.Lock()
442 # Number of threads in wait state.
443 self._ready = 0
444
445 # Control access to the following member.
446 self._workers_lock = threading.Lock()
447 self._workers = []
448 for _ in range(self.INITIAL_WORKERS):
449 self._add_worker()
450
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000451 def join(self):
452 """Blocks until the queue is empty."""
453 self._queue.join()
454
455 def next_exception(self):
456 """Returns the next unhandled exception, or None if there is
457 no exception."""
458 try:
459 return self._exceptions.get_nowait()
460 except Queue.Empty:
461 return None
462
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000463 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000464 """Retrieves an object from the remote data store.
465
466 The smaller |priority| gets fetched first.
467
468 Thread-safe.
469 """
470 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000471 self._add_to_queue(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000472
473 def get_result(self):
474 """Returns the next file that was successfully fetched."""
475 r = self._done.get()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000476 if r[0] == -1:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000477 # It's an exception.
478 raise r[2][0], r[2][1], r[2][2]
479 return r[2]
480
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000481 def _add_to_queue(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000482 with self._ready_lock:
483 start_new_worker = not self._ready
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000484 self._queue.put((priority, self._next_index(), obj, dest, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000485 if start_new_worker:
486 self._add_worker()
487
488 def _add_worker(self):
489 """Add one worker thread if there isn't too many. Thread-safe."""
490 with self._workers_lock:
491 if len(self._workers) >= self.MAX_WORKERS:
492 return False
493 worker = threading.Thread(target=self._run)
494 self._workers.append(worker)
495 worker.daemon = True
496 worker.start()
497
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000498 def _step_done(self, result):
499 """Worker helper function"""
500 self._done.put(result)
501 self._queue.task_done()
502 if result[0] == -1:
503 self._exceptions.put(sys.exc_info())
504
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000505 def _run(self):
506 """Worker thread loop."""
507 while True:
508 try:
509 with self._ready_lock:
510 self._ready += 1
511 item = self._queue.get()
512 finally:
513 with self._ready_lock:
514 self._ready -= 1
515 if not item:
516 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000517 priority, index, obj, dest, size = item
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000518 try:
519 self._do_item(obj, dest)
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000520 if size and not valid_file(dest, size):
521 download_size = os.stat(dest).st_size
522 os.remove(dest)
523 raise IOError('File incorrect size after download of %s. Got %s and '
524 'expected %s' % (dest, download_size, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000525 except IOError:
526 # Retry a few times, lowering the priority.
527 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000528 self._add_to_queue(priority + 1, obj, dest, size)
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000529 self._queue.task_done()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000530 continue
531 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000532 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000533 except:
534 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000535 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000536 else:
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000537 self._step_done((priority, index, obj))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000538
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +0000539 def get_file_handler(self, file_or_url): # pylint: disable=R0201
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000540 """Returns a object to retrieve objects from a remote."""
541 if re.match(r'^https?://.+$', file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000542 def download_file(item, dest):
543 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
544 # easy.
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000545 try:
546 zipped_source = file_or_url.rstrip('/') + '-gzip/' + item
547 logging.debug('download_file(%s)', zipped_source)
548 connection = urllib2.urlopen(zipped_source)
549 decompressor = zlib.decompressobj()
550 with open(dest, 'wb') as f:
551 while True:
552 chunk = connection.read(ZIPPED_FILE_CHUNK)
553 if not chunk:
554 break
555 f.write(decompressor.decompress(chunk))
556 # Ensure that all the data was properly decompressed.
557 uncompressed_data = decompressor.flush()
558 assert not uncompressed_data
csharp@chromium.org186d6232012-11-26 14:36:12 +0000559 except zlib.error as e:
560 logging.debug(e)
561 raise IOError('Problem unzipping data:\n %s' % e)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000562
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)
csharp@chromium.orge217f302012-11-22 16:51:53 +0000660 logging.warn('Add unknown file %s to cache', filename)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000661 added += 1
662 if added:
663 logging.warn('Added back %d unknown files', added)
664 self.state = [
665 (filename, size) for filename, size in self.state
666 if filename not in previous
667 ]
668 self._update_lookup()
669
670 with Profiler('SetupTrimming'):
671 self.trim()
672
673 def __enter__(self):
674 return self
675
676 def __exit__(self, _exc_type, _exec_value, _traceback):
677 with Profiler('CleanupTrimming'):
678 self.trim()
679
680 logging.info(
681 '%4d (%7dkb) added', len(self._added), sum(self._added) / 1024)
682 logging.info(
683 '%4d (%7dkb) current',
684 len(self.state),
685 sum(i[1] for i in self.state) / 1024)
686 logging.info(
687 '%4d (%7dkb) removed', len(self._removed), sum(self._removed) / 1024)
688 logging.info('%7dkb free', self._free_disk / 1024)
689
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000690 def remove_file_at_index(self, index):
691 """Removes the file at the given index."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000692 try:
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000693 filename, size = self.state.pop(index)
694 # TODO(csharp): _lookup should self-update.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000695 del self._lookup[filename]
696 self._removed.append(size)
697 os.remove(self.path(filename))
698 self._dirty = True
699 except OSError as e:
700 logging.error('Error attempting to delete a file\n%s' % e)
701
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000702 def remove_lru_file(self):
703 """Removes the last recently used file."""
704 self.remove_file_at_index(0)
705
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000706 def trim(self):
707 """Trims anything we don't know, make sure enough free space exists."""
708 # Ensure maximum cache size.
709 if self.policies.max_cache_size and self.state:
710 while sum(i[1] for i in self.state) > self.policies.max_cache_size:
711 self.remove_lru_file()
712
713 # Ensure maximum number of items in the cache.
714 if self.policies.max_items and self.state:
715 while len(self.state) > self.policies.max_items:
716 self.remove_lru_file()
717
718 # Ensure enough free space.
719 self._free_disk = get_free_space(self.cache_dir)
720 while (
721 self.policies.min_free_space and
722 self.state and
723 self._free_disk < self.policies.min_free_space):
724 self.remove_lru_file()
725 self._free_disk = get_free_space(self.cache_dir)
726
727 self.save()
728
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000729 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000730 """Retrieves a file from the remote, if not already cached, and adds it to
731 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000732
733 If the file is in the cache, verifiy that the file is valid (i.e. it is
734 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000735 """
736 assert not '/' in item
737 path = self.path(item)
738 index = self._lookup.get(item)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000739
740 if index is not None:
741 if not valid_file(self.path(item), size):
742 self.remove_file_at_index(index)
743 self._update_lookup()
744 index = None
745 else:
746 assert index < len(self.state)
747 # Was already in cache. Update it's LRU value by putting it at the end.
748 self.state.append(self.state.pop(index))
749 self._dirty = True
750 self._update_lookup()
751
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000752 if index is None:
753 if item in self._pending_queue:
754 # Already pending. The same object could be referenced multiple times.
755 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000756 self.remote.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000757 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000758
759 def add(self, filepath, obj):
760 """Forcibly adds a file to the cache."""
761 if not obj in self._lookup:
762 link_file(self.path(obj), filepath, HARDLINK)
763 self._add(obj, True)
764
765 def path(self, item):
766 """Returns the path to one item."""
767 return os.path.join(self.cache_dir, item)
768
769 def save(self):
770 """Saves the LRU ordering."""
771 json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':'))
772
773 def wait_for(self, items):
774 """Starts a loop that waits for at least one of |items| to be retrieved.
775
776 Returns the first item retrieved.
777 """
778 # Flush items already present.
779 for item in items:
780 if item in self._lookup:
781 return item
782
783 assert all(i in self._pending_queue for i in items), (
784 items, self._pending_queue)
785 # Note that:
786 # len(self._pending_queue) ==
787 # ( len(self.remote._workers) - self.remote._ready +
788 # len(self._remote._queue) + len(self._remote.done))
789 # There is no lock-free way to verify that.
790 while self._pending_queue:
791 item = self.remote.get_result()
792 self._pending_queue.remove(item)
793 self._add(item, True)
794 if item in items:
795 return item
796
797 def _add(self, item, at_end):
798 """Adds an item in the internal state.
799
800 If |at_end| is False, self._lookup becomes inconsistent and
801 self._update_lookup() must be called.
802 """
803 size = os.stat(self.path(item)).st_size
804 self._added.append(size)
805 if at_end:
806 self.state.append((item, size))
807 self._lookup[item] = len(self.state) - 1
808 else:
809 self.state.insert(0, (item, size))
810 self._dirty = True
811
812 def _update_lookup(self):
813 self._lookup = dict(
814 (filename, index) for index, (filename, _) in enumerate(self.state))
815
816
817
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000818class IsolatedFile(object):
819 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000820 def __init__(self, obj_hash):
821 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000822 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000823 self.obj_hash = obj_hash
824 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000825 # .isolate and all the .isolated files recursively included by it with
826 # 'includes' key. The order of each sha-1 in 'includes', each representing a
827 # .isolated file in the hash table, is important, as the later ones are not
828 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000829 self.can_fetch = False
830
831 # Raw data.
832 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000833 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000834 self.children = []
835
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000836 # Set once the .isolated file is loaded.
837 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000838 # Set once the files are fetched.
839 self.files_fetched = False
840
841 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000842 """Verifies the .isolated file is valid and loads this object with the json
843 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000844 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000845 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
846 assert not self._is_parsed
847 self.data = load_isolated(content)
848 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
849 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000850
851 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000852 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000853
854 Preemptively request files.
855
856 Note that |files| is modified by this function.
857 """
858 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000859 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000860 return
861 logging.debug('fetch_files(%s)' % self.obj_hash)
862 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000863 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000864 # overriden files must not be fetched.
865 if filepath not in files:
866 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000867 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000868 # Preemptively request files.
869 logging.debug('fetching %s' % filepath)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000870 cache.retrieve(Remote.MED, properties['h'], properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000871 self.files_fetched = True
872
873
874class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000875 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000876 def __init__(self):
877 self.command = []
878 self.files = {}
879 self.read_only = None
880 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000881 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000882 self.root = None
883 logging.debug('Settings')
884
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000885 def load(self, cache, root_isolated_hash):
886 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000887
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000888 It enables support for "included" .isolated files. They are processed in
889 strict order but fetched asynchronously from the cache. This is important so
890 that a file in an included .isolated file that is overridden by an embedding
891 .isolated file is not fetched neededlessly. The includes are fetched in one
892 pass and the files are fetched as soon as all the ones on the left-side
893 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000894
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000895 The prioritization is very important here for nested .isolated files.
896 'includes' have the highest priority and the algorithm is optimized for both
897 deep and wide trees. A deep one is a long link of .isolated files referenced
898 one at a time by one item in 'includes'. A wide one has a large number of
899 'includes' in a single .isolated file. 'left' is defined as an included
900 .isolated file earlier in the 'includes' list. So the order of the elements
901 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000902 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000903 self.root = IsolatedFile(root_isolated_hash)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000904 cache.retrieve(Remote.HIGH, root_isolated_hash, UNKNOWN_FILE_SIZE)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000905 pending = {root_isolated_hash: self.root}
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000906 # Keeps the list of retrieved items to refuse recursive includes.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000907 retrieved = [root_isolated_hash]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000908
909 def update_self(node):
910 node.fetch_files(cache, self.files)
911 # Grabs properties.
912 if not self.command and node.data.get('command'):
913 self.command = node.data['command']
914 if self.read_only is None and node.data.get('read_only') is not None:
915 self.read_only = node.data['read_only']
916 if (self.relative_cwd is None and
917 node.data.get('relative_cwd') is not None):
918 self.relative_cwd = node.data['relative_cwd']
919
920 def traverse_tree(node):
921 if node.can_fetch:
922 if not node.files_fetched:
923 update_self(node)
924 will_break = False
925 for i in node.children:
926 if not i.can_fetch:
927 if will_break:
928 break
929 # Automatically mark the first one as fetcheable.
930 i.can_fetch = True
931 will_break = True
932 traverse_tree(i)
933
934 while pending:
935 item_hash = cache.wait_for(pending)
936 item = pending.pop(item_hash)
937 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000938 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000939 # It's the root item.
940 item.can_fetch = True
941
942 for new_child in item.children:
943 h = new_child.obj_hash
944 if h in retrieved:
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000945 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000946 pending[h] = new_child
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000947 cache.retrieve(Remote.HIGH, h, UNKNOWN_FILE_SIZE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000948
949 # Traverse the whole tree to see if files can now be fetched.
950 traverse_tree(self.root)
951 def check(n):
952 return all(check(x) for x in n.children) and n.files_fetched
953 assert check(self.root)
954 self.relative_cwd = self.relative_cwd or ''
955 self.read_only = self.read_only or False
956
957
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000958def run_tha_test(isolated_hash, cache_dir, remote, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000959 """Downloads the dependencies in the cache, hardlinks them into a temporary
960 directory and runs the executable.
961 """
962 settings = Settings()
963 with Cache(cache_dir, Remote(remote), policies) as cache:
964 outdir = make_temp_dir('run_tha_test', cache_dir)
965 try:
966 # Initiate all the files download.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000967 with Profiler('GetIsolateds') as _prof:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000968 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000969 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000970 # Adds it in the cache. While not strictly necessary, this simplifies
971 # the rest.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000972 h = hashlib.sha1(open(isolated_hash, 'r').read()).hexdigest()
973 cache.add(isolated_hash, h)
974 isolated_hash = h
975 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000976
977 if not settings.command:
978 print >> sys.stderr, 'No command to run'
979 return 1
980
981 with Profiler('GetRest') as _prof:
982 logging.debug('Creating directories')
983 # Creates the tree of directories to create.
984 directories = set(os.path.dirname(f) for f in settings.files)
985 for item in list(directories):
986 while item:
987 directories.add(item)
988 item = os.path.dirname(item)
989 for d in sorted(directories):
990 if d:
991 os.mkdir(os.path.join(outdir, d))
992
993 # Creates the links if necessary.
994 for filepath, properties in settings.files.iteritems():
995 if 'link' not in properties:
996 continue
997 outfile = os.path.join(outdir, filepath)
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000998 # symlink doesn't exist on Windows. So the 'link' property should
999 # never be specified for windows .isolated file.
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001000 os.symlink(properties['l'], outfile) # pylint: disable=E1101
1001 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001002 # It's not set on Windows.
maruel@chromium.org96768a42012-10-31 18:49:18 +00001003 lchmod = getattr(os, 'lchmod', None)
1004 if lchmod:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001005 lchmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001006
1007 # Remaining files to be processed.
1008 # Note that files could still be not be downloaded yet here.
1009 remaining = dict()
1010 for filepath, props in settings.files.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001011 if 'h' in props:
1012 remaining.setdefault(props['h'], []).append((filepath, props))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001013
1014 # Do bookkeeping while files are being downloaded in the background.
1015 cwd = os.path.join(outdir, settings.relative_cwd)
1016 if not os.path.isdir(cwd):
1017 os.makedirs(cwd)
1018 cmd = settings.command[:]
1019 # Ensure paths are correctly separated on windows.
1020 cmd[0] = cmd[0].replace('/', os.path.sep)
1021 cmd = fix_python_path(cmd)
1022
1023 # Now block on the remaining files to be downloaded and mapped.
1024 while remaining:
1025 obj = cache.wait_for(remaining)
1026 for filepath, properties in remaining.pop(obj):
1027 outfile = os.path.join(outdir, filepath)
1028 link_file(outfile, cache.path(obj), HARDLINK)
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001029 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001030 # It's not set on Windows.
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001031 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001032
1033 if settings.read_only:
1034 make_writable(outdir, True)
1035 logging.info('Running %s, cwd=%s' % (cmd, cwd))
csharp@chromium.orge217f302012-11-22 16:51:53 +00001036
1037 # TODO(csharp): This should be specified somewhere else.
1038 # Add a rotating log file if one doesn't already exist.
1039 env = os.environ.copy()
1040 env.setdefault('RUN_TEST_CASES_LOG_FILE', RUN_TEST_CASES_LOG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001041 try:
1042 with Profiler('RunTest') as _prof:
csharp@chromium.orge217f302012-11-22 16:51:53 +00001043 return subprocess.call(cmd, cwd=cwd, env=env)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001044 except OSError:
1045 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1046 raise
1047 finally:
1048 rmtree(outdir)
1049
1050
1051def main():
1052 parser = optparse.OptionParser(
1053 usage='%prog <options>', description=sys.modules[__name__].__doc__)
1054 parser.add_option(
1055 '-v', '--verbose', action='count', default=0, help='Use multiple times')
1056 parser.add_option('--no-run', action='store_true', help='Skip the run part')
1057
1058 group = optparse.OptionGroup(parser, 'Data source')
1059 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001060 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001061 metavar='FILE',
1062 help='File/url describing what to map or run')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001063 # TODO(maruel): Remove once not used anymore.
1064 group.add_option(
1065 '-m', '--manifest', dest='isolated', help=optparse.SUPPRESS_HELP)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001066 group.add_option(
1067 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001068 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001069 parser.add_option_group(group)
1070
1071 group.add_option(
1072 '-r', '--remote', metavar='URL', help='Remote where to get the items')
1073 group = optparse.OptionGroup(parser, 'Cache management')
1074 group.add_option(
1075 '--cache',
1076 default='cache',
1077 metavar='DIR',
1078 help='Cache directory, default=%default')
1079 group.add_option(
1080 '--max-cache-size',
1081 type='int',
1082 metavar='NNN',
1083 default=20*1024*1024*1024,
1084 help='Trim if the cache gets larger than this value, default=%default')
1085 group.add_option(
1086 '--min-free-space',
1087 type='int',
1088 metavar='NNN',
1089 default=1*1024*1024*1024,
1090 help='Trim if disk free space becomes lower than this value, '
1091 'default=%default')
1092 group.add_option(
1093 '--max-items',
1094 type='int',
1095 metavar='NNN',
1096 default=100000,
1097 help='Trim if more than this number of items are in the cache '
1098 'default=%default')
1099 parser.add_option_group(group)
1100
1101 options, args = parser.parse_args()
1102 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
csharp@chromium.orgff2a4662012-11-21 20:49:32 +00001103
1104 logging_console = logging.StreamHandler()
1105 logging_console.setFormatter(logging.Formatter(
1106 '%(levelname)5s %(module)15s(%(lineno)3d): %(message)s'))
1107 logging_console.setLevel(level)
1108 logging.getLogger().addHandler(logging_console)
1109
1110 logging_rotating_file = logging.handlers.RotatingFileHandler(
1111 RUN_ISOLATED_LOG_FILE,
1112 maxBytes=10 * 1024 * 1024, backupCount=5)
1113 logging_rotating_file.setLevel(logging.DEBUG)
1114 logging_rotating_file.setFormatter(logging.Formatter(
1115 '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)3d): %(message)s'))
1116 logging.getLogger().addHandler(logging_rotating_file)
1117
1118 logging.getLogger().setLevel(logging.DEBUG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001119
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001120 if bool(options.isolated) == bool(options.hash):
1121 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001122 if not options.remote:
1123 parser.error('--remote is required.')
1124 if args:
1125 parser.error('Unsupported args %s' % ' '.join(args))
1126
1127 policies = CachePolicies(
1128 options.max_cache_size, options.min_free_space, options.max_items)
1129 try:
1130 return run_tha_test(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001131 options.isolated or options.hash,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001132 os.path.abspath(options.cache),
1133 options.remote,
1134 policies)
1135 except (ConfigError, MappingError), e:
1136 print >> sys.stderr, str(e)
1137 return 1
1138
1139
1140if __name__ == '__main__':
1141 sys.exit(main())