blob: 21b979f8cd144afbf520061f3e1f886ab8075325 [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)."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000389 if size == UNKNOWN_FILE_SIZE:
390 return True
391 actual_size = os.stat(filepath).st_size
392 if size != actual_size:
393 logging.warning(
394 'Found invalid item %s; %d != %d',
395 os.path.basename(filepath), actual_size, size)
396 return False
397 return True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000398
399
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000400class Profiler(object):
401 def __init__(self, name):
402 self.name = name
403 self.start_time = None
404
405 def __enter__(self):
406 self.start_time = time.time()
407 return self
408
409 def __exit__(self, _exc_type, _exec_value, _traceback):
410 time_taken = time.time() - self.start_time
411 logging.info('Profiling: Section %s took %3.3f seconds',
412 self.name, time_taken)
413
414
415class Remote(object):
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000416 """Priority based worker queue to fetch or upload files from a
417 content-address server. Any function may be given as the fetcher/upload,
418 as long as it takes two inputs (the item contents, and their relative
419 destination).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000420
421 Supports local file system, CIFS or http remotes.
422
423 When the priority of items is equals, works in strict FIFO mode.
424 """
425 # Initial and maximum number of worker threads.
426 INITIAL_WORKERS = 2
427 MAX_WORKERS = 16
428 # Priorities.
429 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
430 INTERNAL_PRIORITY_BITS = (1<<8) - 1
431 RETRIES = 5
432
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000433 def __init__(self, destination_root):
434 # Function to fetch a remote object or upload to a remote location..
435 self._do_item = self.get_file_handler(destination_root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000436 # Contains tuple(priority, index, obj, destination).
437 self._queue = Queue.PriorityQueue()
438 # Contains tuple(priority, index, obj).
439 self._done = Queue.PriorityQueue()
440
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000441 # Contains generated exceptions that haven't been handled yet.
442 self._exceptions = Queue.Queue()
443
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000444 # To keep FIFO ordering in self._queue. It is assumed xrange's iterator is
445 # thread-safe.
446 self._next_index = xrange(0, 1<<30).__iter__().next
447
448 # Control access to the following member.
449 self._ready_lock = threading.Lock()
450 # Number of threads in wait state.
451 self._ready = 0
452
453 # Control access to the following member.
454 self._workers_lock = threading.Lock()
455 self._workers = []
456 for _ in range(self.INITIAL_WORKERS):
457 self._add_worker()
458
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000459 def join(self):
460 """Blocks until the queue is empty."""
461 self._queue.join()
462
463 def next_exception(self):
464 """Returns the next unhandled exception, or None if there is
465 no exception."""
466 try:
467 return self._exceptions.get_nowait()
468 except Queue.Empty:
469 return None
470
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000471 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000472 """Retrieves an object from the remote data store.
473
474 The smaller |priority| gets fetched first.
475
476 Thread-safe.
477 """
478 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000479 self._add_to_queue(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000480
481 def get_result(self):
482 """Returns the next file that was successfully fetched."""
483 r = self._done.get()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000484 if r[0] == -1:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000485 # It's an exception.
486 raise r[2][0], r[2][1], r[2][2]
487 return r[2]
488
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000489 def _add_to_queue(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000490 with self._ready_lock:
491 start_new_worker = not self._ready
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000492 self._queue.put((priority, self._next_index(), obj, dest, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000493 if start_new_worker:
494 self._add_worker()
495
496 def _add_worker(self):
497 """Add one worker thread if there isn't too many. Thread-safe."""
498 with self._workers_lock:
499 if len(self._workers) >= self.MAX_WORKERS:
500 return False
501 worker = threading.Thread(target=self._run)
502 self._workers.append(worker)
503 worker.daemon = True
504 worker.start()
505
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000506 def _step_done(self, result):
507 """Worker helper function"""
508 self._done.put(result)
509 self._queue.task_done()
510 if result[0] == -1:
511 self._exceptions.put(sys.exc_info())
512
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000513 def _run(self):
514 """Worker thread loop."""
515 while True:
516 try:
517 with self._ready_lock:
518 self._ready += 1
519 item = self._queue.get()
520 finally:
521 with self._ready_lock:
522 self._ready -= 1
523 if not item:
524 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000525 priority, index, obj, dest, size = item
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000526 try:
527 self._do_item(obj, dest)
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000528 if size and not valid_file(dest, size):
529 download_size = os.stat(dest).st_size
530 os.remove(dest)
531 raise IOError('File incorrect size after download of %s. Got %s and '
maruel@chromium.org3f039182012-11-27 21:32:41 +0000532 'expected %s' % (obj, download_size, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000533 except IOError:
534 # Retry a few times, lowering the priority.
535 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000536 self._add_to_queue(priority + 1, obj, dest, size)
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000537 self._queue.task_done()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000538 continue
539 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000540 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000541 except:
542 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000543 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000544 else:
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000545 self._step_done((priority, index, obj))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000546
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +0000547 def get_file_handler(self, file_or_url): # pylint: disable=R0201
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000548 """Returns a object to retrieve objects from a remote."""
549 if re.match(r'^https?://.+$', file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000550 def download_file(item, dest):
551 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
552 # easy.
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000553 try:
csharp@chromium.orgaa2d1512012-12-05 21:17:39 +0000554 zipped_source = file_or_url + item
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000555 logging.debug('download_file(%s)', zipped_source)
556 connection = urllib2.urlopen(zipped_source)
557 decompressor = zlib.decompressobj()
maruel@chromium.org3f039182012-11-27 21:32:41 +0000558 size = 0
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000559 with open(dest, 'wb') as f:
560 while True:
561 chunk = connection.read(ZIPPED_FILE_CHUNK)
562 if not chunk:
563 break
maruel@chromium.org3f039182012-11-27 21:32:41 +0000564 size += len(chunk)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000565 f.write(decompressor.decompress(chunk))
566 # Ensure that all the data was properly decompressed.
567 uncompressed_data = decompressor.flush()
568 assert not uncompressed_data
csharp@chromium.org186d6232012-11-26 14:36:12 +0000569 except zlib.error as e:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000570 # Log the first bytes to see if it's uncompressed data.
571 logging.warning('%r', e[:512])
maruel@chromium.org3f039182012-11-27 21:32:41 +0000572 raise IOError(
573 'Problem unzipping data for item %s. Got %d bytes.\n%s' %
574 (item, size, e))
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000575
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000576 return download_file
577
578 def copy_file(item, dest):
579 source = os.path.join(file_or_url, item)
580 logging.debug('copy_file(%s, %s)', source, dest)
581 shutil.copy(source, dest)
582 return copy_file
583
584
585class CachePolicies(object):
586 def __init__(self, max_cache_size, min_free_space, max_items):
587 """
588 Arguments:
589 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
590 cache is effectively a leak.
591 - min_free_space: Trim if disk free space becomes lower than this value. If
592 0, it unconditionally fill the disk.
593 - max_items: Maximum number of items to keep in the cache. If 0, do not
594 enforce a limit.
595 """
596 self.max_cache_size = max_cache_size
597 self.min_free_space = min_free_space
598 self.max_items = max_items
599
600
601class Cache(object):
602 """Stateful LRU cache.
603
604 Saves its state as json file.
605 """
606 STATE_FILE = 'state.json'
607
608 def __init__(self, cache_dir, remote, policies):
609 """
610 Arguments:
611 - cache_dir: Directory where to place the cache.
612 - remote: Remote where to fetch items from.
613 - policies: cache retention policies.
614 """
615 self.cache_dir = cache_dir
616 self.remote = remote
617 self.policies = policies
618 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
619 # The tuple(file, size) are kept as an array in a LRU style. E.g.
620 # self.state[0] is the oldest item.
621 self.state = []
maruel@chromium.org770993b2012-12-11 17:16:48 +0000622 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000623 # A lookup map to speed up searching.
624 self._lookup = {}
maruel@chromium.org770993b2012-12-11 17:16:48 +0000625 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000626
627 # Items currently being fetched. Keep it local to reduce lock contention.
628 self._pending_queue = set()
629
630 # Profiling values.
631 self._added = []
632 self._removed = []
633 self._free_disk = 0
634
maruel@chromium.org770993b2012-12-11 17:16:48 +0000635 with Profiler('Setup'):
636 if not os.path.isdir(self.cache_dir):
637 os.makedirs(self.cache_dir)
638 if os.path.isfile(self.state_file):
639 try:
640 self.state = json.load(open(self.state_file, 'r'))
641 except (IOError, ValueError), e:
642 # Too bad. The file will be overwritten and the cache cleared.
643 logging.error(
644 'Broken state file %s, ignoring.\n%s' % (self.STATE_FILE, e))
645 self._state_need_to_be_saved = True
646 if (not isinstance(self.state, list) or
647 not all(
648 isinstance(i, (list, tuple)) and len(i) == 2
649 for i in self.state)):
650 # Discard.
651 self._state_need_to_be_saved = True
652 self.state = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000653
maruel@chromium.org770993b2012-12-11 17:16:48 +0000654 # Ensure that all files listed in the state still exist and add new ones.
655 previous = set(filename for filename, _ in self.state)
656 if len(previous) != len(self.state):
657 logging.warn('Cache state is corrupted, found duplicate files')
658 self._state_need_to_be_saved = True
659 self.state = []
660
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000661 added = 0
662 for filename in os.listdir(self.cache_dir):
663 if filename == self.STATE_FILE:
664 continue
665 if filename in previous:
666 previous.remove(filename)
667 continue
668 # An untracked file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000669 if not RE_IS_SHA1.match(filename):
670 logging.warn('Removing unknown file %s from cache', filename)
671 os.remove(self.path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000672 continue
673 # Insert as the oldest file. It will be deleted eventually if not
674 # accessed.
675 self._add(filename, False)
676 logging.warn('Add unknown file %s to cache', filename)
677 added += 1
678
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000679 if added:
680 logging.warn('Added back %d unknown files', added)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000681 if previous:
682 logging.warn('Removed %d lost files', len(previous))
683 # Set explicitly in case self._add() wasn't called.
684 self._state_need_to_be_saved = True
685 # Filter out entries that were not found while keeping the previous
686 # order.
687 self.state = [
688 (filename, size) for filename, size in self.state
689 if filename not in previous
690 ]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000691 self.trim()
692
693 def __enter__(self):
694 return self
695
696 def __exit__(self, _exc_type, _exec_value, _traceback):
697 with Profiler('CleanupTrimming'):
698 self.trim()
699
700 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000701 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000702 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000703 '%5d (%8dkb) current',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000704 len(self.state),
705 sum(i[1] for i in self.state) / 1024)
706 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000707 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
708 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000709
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000710 def remove_file_at_index(self, index):
711 """Removes the file at the given index."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000712 try:
maruel@chromium.org770993b2012-12-11 17:16:48 +0000713 self._state_need_to_be_saved = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000714 filename, size = self.state.pop(index)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000715 # If the lookup was already stale, its possible the filename was not
716 # present yet.
717 self._lookup_is_stale = True
718 self._lookup.pop(filename, None)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000719 self._removed.append(size)
720 os.remove(self.path(filename))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000721 except OSError as e:
722 logging.error('Error attempting to delete a file\n%s' % e)
723
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000724 def remove_lru_file(self):
725 """Removes the last recently used file."""
726 self.remove_file_at_index(0)
727
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000728 def trim(self):
729 """Trims anything we don't know, make sure enough free space exists."""
730 # Ensure maximum cache size.
731 if self.policies.max_cache_size and self.state:
732 while sum(i[1] for i in self.state) > self.policies.max_cache_size:
733 self.remove_lru_file()
734
735 # Ensure maximum number of items in the cache.
736 if self.policies.max_items and self.state:
737 while len(self.state) > self.policies.max_items:
738 self.remove_lru_file()
739
740 # Ensure enough free space.
741 self._free_disk = get_free_space(self.cache_dir)
742 while (
743 self.policies.min_free_space and
744 self.state and
745 self._free_disk < self.policies.min_free_space):
746 self.remove_lru_file()
747 self._free_disk = get_free_space(self.cache_dir)
748
749 self.save()
750
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000751 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000752 """Retrieves a file from the remote, if not already cached, and adds it to
753 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000754
755 If the file is in the cache, verifiy that the file is valid (i.e. it is
756 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000757 """
758 assert not '/' in item
759 path = self.path(item)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000760 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000761 index = self._lookup.get(item)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000762
763 if index is not None:
764 if not valid_file(self.path(item), size):
765 self.remove_file_at_index(index)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000766 index = None
767 else:
768 assert index < len(self.state)
769 # Was already in cache. Update it's LRU value by putting it at the end.
maruel@chromium.org770993b2012-12-11 17:16:48 +0000770 self._state_need_to_be_saved = True
771 self._lookup_is_stale = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000772 self.state.append(self.state.pop(index))
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000773
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000774 if index is None:
775 if item in self._pending_queue:
776 # Already pending. The same object could be referenced multiple times.
777 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000778 self.remote.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000779 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000780
781 def add(self, filepath, obj):
782 """Forcibly adds a file to the cache."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000783 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000784 if not obj in self._lookup:
785 link_file(self.path(obj), filepath, HARDLINK)
786 self._add(obj, True)
787
788 def path(self, item):
789 """Returns the path to one item."""
790 return os.path.join(self.cache_dir, item)
791
792 def save(self):
793 """Saves the LRU ordering."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000794 if self._state_need_to_be_saved:
795 json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':'))
796 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000797
798 def wait_for(self, items):
799 """Starts a loop that waits for at least one of |items| to be retrieved.
800
801 Returns the first item retrieved.
802 """
803 # Flush items already present.
maruel@chromium.org770993b2012-12-11 17:16:48 +0000804 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000805 for item in items:
806 if item in self._lookup:
807 return item
808
809 assert all(i in self._pending_queue for i in items), (
810 items, self._pending_queue)
811 # Note that:
812 # len(self._pending_queue) ==
813 # ( len(self.remote._workers) - self.remote._ready +
814 # len(self._remote._queue) + len(self._remote.done))
815 # There is no lock-free way to verify that.
816 while self._pending_queue:
817 item = self.remote.get_result()
818 self._pending_queue.remove(item)
819 self._add(item, True)
820 if item in items:
821 return item
822
823 def _add(self, item, at_end):
824 """Adds an item in the internal state.
825
826 If |at_end| is False, self._lookup becomes inconsistent and
827 self._update_lookup() must be called.
828 """
829 size = os.stat(self.path(item)).st_size
830 self._added.append(size)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000831 self._state_need_to_be_saved = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000832 if at_end:
833 self.state.append((item, size))
834 self._lookup[item] = len(self.state) - 1
835 else:
maruel@chromium.org770993b2012-12-11 17:16:48 +0000836 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000837 self.state.insert(0, (item, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000838
839 def _update_lookup(self):
maruel@chromium.org770993b2012-12-11 17:16:48 +0000840 if self._lookup_is_stale:
841 self._lookup = dict(
842 (filename, index) for index, (filename, _) in enumerate(self.state))
843 self._lookup_is_stale = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000844
845
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000846class IsolatedFile(object):
847 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000848 def __init__(self, obj_hash):
849 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000850 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000851 self.obj_hash = obj_hash
852 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000853 # .isolate and all the .isolated files recursively included by it with
854 # 'includes' key. The order of each sha-1 in 'includes', each representing a
855 # .isolated file in the hash table, is important, as the later ones are not
856 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000857 self.can_fetch = False
858
859 # Raw data.
860 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000861 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000862 self.children = []
863
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000864 # Set once the .isolated file is loaded.
865 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000866 # Set once the files are fetched.
867 self.files_fetched = False
868
869 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000870 """Verifies the .isolated file is valid and loads this object with the json
871 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000872 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000873 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
874 assert not self._is_parsed
875 self.data = load_isolated(content)
876 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
877 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000878
879 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000880 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000881
882 Preemptively request files.
883
884 Note that |files| is modified by this function.
885 """
886 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000887 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000888 return
889 logging.debug('fetch_files(%s)' % self.obj_hash)
890 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000891 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000892 # overriden files must not be fetched.
893 if filepath not in files:
894 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000895 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000896 # Preemptively request files.
897 logging.debug('fetching %s' % filepath)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000898 cache.retrieve(Remote.MED, properties['h'], properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000899 self.files_fetched = True
900
901
902class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000903 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000904 def __init__(self):
905 self.command = []
906 self.files = {}
907 self.read_only = None
908 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000909 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000910 self.root = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000911
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000912 def load(self, cache, root_isolated_hash):
913 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000914
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000915 It enables support for "included" .isolated files. They are processed in
916 strict order but fetched asynchronously from the cache. This is important so
917 that a file in an included .isolated file that is overridden by an embedding
918 .isolated file is not fetched neededlessly. The includes are fetched in one
919 pass and the files are fetched as soon as all the ones on the left-side
920 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000921
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000922 The prioritization is very important here for nested .isolated files.
923 'includes' have the highest priority and the algorithm is optimized for both
924 deep and wide trees. A deep one is a long link of .isolated files referenced
925 one at a time by one item in 'includes'. A wide one has a large number of
926 'includes' in a single .isolated file. 'left' is defined as an included
927 .isolated file earlier in the 'includes' list. So the order of the elements
928 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000929 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000930 self.root = IsolatedFile(root_isolated_hash)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000931 cache.retrieve(Remote.HIGH, root_isolated_hash, UNKNOWN_FILE_SIZE)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000932 pending = {root_isolated_hash: self.root}
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000933 # Keeps the list of retrieved items to refuse recursive includes.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000934 retrieved = [root_isolated_hash]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000935
936 def update_self(node):
937 node.fetch_files(cache, self.files)
938 # Grabs properties.
939 if not self.command and node.data.get('command'):
940 self.command = node.data['command']
941 if self.read_only is None and node.data.get('read_only') is not None:
942 self.read_only = node.data['read_only']
943 if (self.relative_cwd is None and
944 node.data.get('relative_cwd') is not None):
945 self.relative_cwd = node.data['relative_cwd']
946
947 def traverse_tree(node):
948 if node.can_fetch:
949 if not node.files_fetched:
950 update_self(node)
951 will_break = False
952 for i in node.children:
953 if not i.can_fetch:
954 if will_break:
955 break
956 # Automatically mark the first one as fetcheable.
957 i.can_fetch = True
958 will_break = True
959 traverse_tree(i)
960
961 while pending:
962 item_hash = cache.wait_for(pending)
963 item = pending.pop(item_hash)
964 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000965 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000966 # It's the root item.
967 item.can_fetch = True
968
969 for new_child in item.children:
970 h = new_child.obj_hash
971 if h in retrieved:
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000972 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000973 pending[h] = new_child
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000974 cache.retrieve(Remote.HIGH, h, UNKNOWN_FILE_SIZE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000975
976 # Traverse the whole tree to see if files can now be fetched.
977 traverse_tree(self.root)
978 def check(n):
979 return all(check(x) for x in n.children) and n.files_fetched
980 assert check(self.root)
981 self.relative_cwd = self.relative_cwd or ''
982 self.read_only = self.read_only or False
983
984
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000985def run_tha_test(isolated_hash, cache_dir, remote, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000986 """Downloads the dependencies in the cache, hardlinks them into a temporary
987 directory and runs the executable.
988 """
989 settings = Settings()
990 with Cache(cache_dir, Remote(remote), policies) as cache:
991 outdir = make_temp_dir('run_tha_test', cache_dir)
992 try:
993 # Initiate all the files download.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000994 with Profiler('GetIsolateds') as _prof:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000995 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000996 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000997 # Adds it in the cache. While not strictly necessary, this simplifies
998 # the rest.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000999 h = hashlib.sha1(open(isolated_hash, 'r').read()).hexdigest()
1000 cache.add(isolated_hash, h)
1001 isolated_hash = h
1002 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001003
1004 if not settings.command:
1005 print >> sys.stderr, 'No command to run'
1006 return 1
1007
1008 with Profiler('GetRest') as _prof:
1009 logging.debug('Creating directories')
1010 # Creates the tree of directories to create.
1011 directories = set(os.path.dirname(f) for f in settings.files)
1012 for item in list(directories):
1013 while item:
1014 directories.add(item)
1015 item = os.path.dirname(item)
1016 for d in sorted(directories):
1017 if d:
1018 os.mkdir(os.path.join(outdir, d))
1019
1020 # Creates the links if necessary.
1021 for filepath, properties in settings.files.iteritems():
1022 if 'link' not in properties:
1023 continue
1024 outfile = os.path.join(outdir, filepath)
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +00001025 # symlink doesn't exist on Windows. So the 'link' property should
1026 # never be specified for windows .isolated file.
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001027 os.symlink(properties['l'], outfile) # pylint: disable=E1101
1028 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001029 # It's not set on Windows.
maruel@chromium.org96768a42012-10-31 18:49:18 +00001030 lchmod = getattr(os, 'lchmod', None)
1031 if lchmod:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001032 lchmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001033
1034 # Remaining files to be processed.
1035 # Note that files could still be not be downloaded yet here.
1036 remaining = dict()
1037 for filepath, props in settings.files.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001038 if 'h' in props:
1039 remaining.setdefault(props['h'], []).append((filepath, props))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001040
1041 # Do bookkeeping while files are being downloaded in the background.
1042 cwd = os.path.join(outdir, settings.relative_cwd)
1043 if not os.path.isdir(cwd):
1044 os.makedirs(cwd)
1045 cmd = settings.command[:]
1046 # Ensure paths are correctly separated on windows.
1047 cmd[0] = cmd[0].replace('/', os.path.sep)
1048 cmd = fix_python_path(cmd)
1049
1050 # Now block on the remaining files to be downloaded and mapped.
1051 while remaining:
1052 obj = cache.wait_for(remaining)
1053 for filepath, properties in remaining.pop(obj):
1054 outfile = os.path.join(outdir, filepath)
1055 link_file(outfile, cache.path(obj), HARDLINK)
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001056 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001057 # It's not set on Windows.
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001058 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001059
1060 if settings.read_only:
1061 make_writable(outdir, True)
1062 logging.info('Running %s, cwd=%s' % (cmd, cwd))
csharp@chromium.orge217f302012-11-22 16:51:53 +00001063
1064 # TODO(csharp): This should be specified somewhere else.
1065 # Add a rotating log file if one doesn't already exist.
1066 env = os.environ.copy()
1067 env.setdefault('RUN_TEST_CASES_LOG_FILE', RUN_TEST_CASES_LOG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001068 try:
1069 with Profiler('RunTest') as _prof:
csharp@chromium.orge217f302012-11-22 16:51:53 +00001070 return subprocess.call(cmd, cwd=cwd, env=env)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001071 except OSError:
1072 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1073 raise
1074 finally:
1075 rmtree(outdir)
1076
1077
1078def main():
1079 parser = optparse.OptionParser(
1080 usage='%prog <options>', description=sys.modules[__name__].__doc__)
1081 parser.add_option(
1082 '-v', '--verbose', action='count', default=0, help='Use multiple times')
1083 parser.add_option('--no-run', action='store_true', help='Skip the run part')
1084
1085 group = optparse.OptionGroup(parser, 'Data source')
1086 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001087 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001088 metavar='FILE',
1089 help='File/url describing what to map or run')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001090 # TODO(maruel): Remove once not used anymore.
1091 group.add_option(
1092 '-m', '--manifest', dest='isolated', help=optparse.SUPPRESS_HELP)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001093 group.add_option(
1094 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001095 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001096 parser.add_option_group(group)
1097
1098 group.add_option(
1099 '-r', '--remote', metavar='URL', help='Remote where to get the items')
1100 group = optparse.OptionGroup(parser, 'Cache management')
1101 group.add_option(
1102 '--cache',
1103 default='cache',
1104 metavar='DIR',
1105 help='Cache directory, default=%default')
1106 group.add_option(
1107 '--max-cache-size',
1108 type='int',
1109 metavar='NNN',
1110 default=20*1024*1024*1024,
1111 help='Trim if the cache gets larger than this value, default=%default')
1112 group.add_option(
1113 '--min-free-space',
1114 type='int',
1115 metavar='NNN',
1116 default=1*1024*1024*1024,
1117 help='Trim if disk free space becomes lower than this value, '
1118 'default=%default')
1119 group.add_option(
1120 '--max-items',
1121 type='int',
1122 metavar='NNN',
1123 default=100000,
1124 help='Trim if more than this number of items are in the cache '
1125 'default=%default')
1126 parser.add_option_group(group)
1127
1128 options, args = parser.parse_args()
1129 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
csharp@chromium.orgff2a4662012-11-21 20:49:32 +00001130
1131 logging_console = logging.StreamHandler()
1132 logging_console.setFormatter(logging.Formatter(
1133 '%(levelname)5s %(module)15s(%(lineno)3d): %(message)s'))
1134 logging_console.setLevel(level)
1135 logging.getLogger().addHandler(logging_console)
1136
1137 logging_rotating_file = logging.handlers.RotatingFileHandler(
1138 RUN_ISOLATED_LOG_FILE,
1139 maxBytes=10 * 1024 * 1024, backupCount=5)
1140 logging_rotating_file.setLevel(logging.DEBUG)
1141 logging_rotating_file.setFormatter(logging.Formatter(
1142 '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)3d): %(message)s'))
1143 logging.getLogger().addHandler(logging_rotating_file)
1144
1145 logging.getLogger().setLevel(logging.DEBUG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001146
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001147 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001148 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001149 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001150 if not options.remote:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001151 logging.debug('--remote is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001152 parser.error('--remote is required.')
1153 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001154 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001155 parser.error('Unsupported args %s' % ' '.join(args))
1156
1157 policies = CachePolicies(
1158 options.max_cache_size, options.min_free_space, options.max_items)
1159 try:
1160 return run_tha_test(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001161 options.isolated or options.hash,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001162 os.path.abspath(options.cache),
1163 options.remote,
1164 policies)
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001165 except Exception, e:
1166 # Make sure any exception is logged.
1167 logging.exception(e)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001168 return 1
1169
1170
1171if __name__ == '__main__':
1172 sys.exit(main())