blob: e7e22368e8e6216e0b100b96480ce04dca814a1c [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
csharp@chromium.org9c59ff12012-12-12 02:32:29 +000053# The delay (in seconds) to wait between logging statements when retrieving
54# the required files. This is intended to let the user (or buildbot) know that
55# the program is still running.
56DELAY_BETWEEN_UPDATES_IN_SECS = 30
57
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000058
59class ConfigError(ValueError):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +000060 """Generic failure to load a .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000061 pass
62
63
64class MappingError(OSError):
65 """Failed to recreate the tree."""
66 pass
67
68
csharp@chromium.orga92403f2012-11-20 15:13:59 +000069class DownloadFileOpener(urllib.FancyURLopener):
70 """This class is needed to get urlretrive to raise an exception on
71 404 errors, instead of still writing to the file with the error code.
72 """
73 def http_error_default(self, url, fp, errcode, errmsg, headers):
74 raise urllib2.HTTPError(url, errcode, errmsg, headers, fp)
75
76
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000077def get_flavor():
78 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
79 flavors = {
80 'cygwin': 'win',
81 'win32': 'win',
82 'darwin': 'mac',
83 'sunos5': 'solaris',
84 'freebsd7': 'freebsd',
85 'freebsd8': 'freebsd',
86 }
87 return flavors.get(sys.platform, 'linux')
88
89
90def os_link(source, link_name):
91 """Add support for os.link() on Windows."""
92 if sys.platform == 'win32':
93 if not ctypes.windll.kernel32.CreateHardLinkW(
94 unicode(link_name), unicode(source), 0):
95 raise OSError()
96 else:
97 os.link(source, link_name)
98
99
100def readable_copy(outfile, infile):
101 """Makes a copy of the file that is readable by everyone."""
102 shutil.copy(infile, outfile)
103 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
104 stat.S_IRGRP | stat.S_IROTH)
105 os.chmod(outfile, read_enabled_mode)
106
107
108def link_file(outfile, infile, action):
109 """Links a file. The type of link depends on |action|."""
110 logging.debug('Mapping %s to %s' % (infile, outfile))
111 if action not in (HARDLINK, SYMLINK, COPY):
112 raise ValueError('Unknown mapping action %s' % action)
113 if not os.path.isfile(infile):
114 raise MappingError('%s is missing' % infile)
115 if os.path.isfile(outfile):
116 raise MappingError(
117 '%s already exist; insize:%d; outsize:%d' %
118 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
119
120 if action == COPY:
121 readable_copy(outfile, infile)
122 elif action == SYMLINK and sys.platform != 'win32':
123 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000124 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000125 else:
126 try:
127 os_link(infile, outfile)
128 except OSError:
129 # Probably a different file system.
130 logging.warn(
131 'Failed to hardlink, failing back to copy %s to %s' % (
132 infile, outfile))
133 readable_copy(outfile, infile)
134
135
136def _set_write_bit(path, read_only):
137 """Sets or resets the executable bit on a file or directory."""
138 mode = os.lstat(path).st_mode
139 if read_only:
140 mode = mode & 0500
141 else:
142 mode = mode | 0200
143 if hasattr(os, 'lchmod'):
144 os.lchmod(path, mode) # pylint: disable=E1101
145 else:
146 if stat.S_ISLNK(mode):
147 # Skip symlink without lchmod() support.
148 logging.debug('Can\'t change +w bit on symlink %s' % path)
149 return
150
151 # TODO(maruel): Implement proper DACL modification on Windows.
152 os.chmod(path, mode)
153
154
155def make_writable(root, read_only):
156 """Toggle the writable bit on a directory tree."""
157 root = os.path.abspath(root)
158 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
159 for filename in filenames:
160 _set_write_bit(os.path.join(dirpath, filename), read_only)
161
162 for dirname in dirnames:
163 _set_write_bit(os.path.join(dirpath, dirname), read_only)
164
165
166def rmtree(root):
167 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
168 make_writable(root, False)
169 if sys.platform == 'win32':
170 for i in range(3):
171 try:
172 shutil.rmtree(root)
173 break
174 except WindowsError: # pylint: disable=E0602
175 delay = (i+1)*2
176 print >> sys.stderr, (
177 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
178 time.sleep(delay)
179 else:
180 shutil.rmtree(root)
181
182
183def is_same_filesystem(path1, path2):
184 """Returns True if both paths are on the same filesystem.
185
186 This is required to enable the use of hardlinks.
187 """
188 assert os.path.isabs(path1), path1
189 assert os.path.isabs(path2), path2
190 if sys.platform == 'win32':
191 # If the drive letter mismatches, assume it's a separate partition.
192 # TODO(maruel): It should look at the underlying drive, a drive letter could
193 # be a mount point to a directory on another drive.
194 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
195 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
196 if path1[0].lower() != path2[0].lower():
197 return False
198 return os.stat(path1).st_dev == os.stat(path2).st_dev
199
200
201def get_free_space(path):
202 """Returns the number of free bytes."""
203 if sys.platform == 'win32':
204 free_bytes = ctypes.c_ulonglong(0)
205 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
206 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
207 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000208 # For OSes other than Windows.
209 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000210 return f.f_bfree * f.f_frsize
211
212
213def make_temp_dir(prefix, root_dir):
214 """Returns a temporary directory on the same file system as root_dir."""
215 base_temp_dir = None
216 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
217 base_temp_dir = os.path.dirname(root_dir)
218 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
219
220
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000221def load_isolated(content):
222 """Verifies the .isolated file is valid and loads this object with the json
223 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000224 """
225 try:
226 data = json.loads(content)
227 except ValueError:
228 raise ConfigError('Failed to parse: %s...' % content[:100])
229
230 if not isinstance(data, dict):
231 raise ConfigError('Expected dict, got %r' % data)
232
233 for key, value in data.iteritems():
234 if key == 'command':
235 if not isinstance(value, list):
236 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000237 if not value:
238 raise ConfigError('Expected non-empty command')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000239 for subvalue in value:
240 if not isinstance(subvalue, basestring):
241 raise ConfigError('Expected string, got %r' % subvalue)
242
243 elif key == 'files':
244 if not isinstance(value, dict):
245 raise ConfigError('Expected dict, got %r' % value)
246 for subkey, subvalue in value.iteritems():
247 if not isinstance(subkey, basestring):
248 raise ConfigError('Expected string, got %r' % subkey)
249 if not isinstance(subvalue, dict):
250 raise ConfigError('Expected dict, got %r' % subvalue)
251 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000252 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000253 if not isinstance(subsubvalue, basestring):
254 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000255 elif subsubkey == 'm':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000256 if not isinstance(subsubvalue, int):
257 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000258 elif subsubkey == 'h':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000259 if not RE_IS_SHA1.match(subsubvalue):
260 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000261 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000262 if not isinstance(subsubvalue, int):
263 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000264 else:
265 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000266 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000267 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000268 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
269 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000270
271 elif key == 'includes':
272 if not isinstance(value, list):
273 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000274 if not value:
275 raise ConfigError('Expected non-empty includes list')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000276 for subvalue in value:
277 if not RE_IS_SHA1.match(subvalue):
278 raise ConfigError('Expected sha-1, got %r' % subvalue)
279
280 elif key == 'read_only':
281 if not isinstance(value, bool):
282 raise ConfigError('Expected bool, got %r' % value)
283
284 elif key == 'relative_cwd':
285 if not isinstance(value, basestring):
286 raise ConfigError('Expected string, got %r' % value)
287
288 elif key == 'os':
289 if value != get_flavor():
290 raise ConfigError(
291 'Expected \'os\' to be \'%s\' but got \'%s\'' %
292 (get_flavor(), value))
293
294 else:
295 raise ConfigError('Unknown key %s' % key)
296
297 return data
298
299
300def fix_python_path(cmd):
301 """Returns the fixed command line to call the right python executable."""
302 out = cmd[:]
303 if out[0] == 'python':
304 out[0] = sys.executable
305 elif out[0].endswith('.py'):
306 out.insert(0, sys.executable)
307 return out
308
309
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000310class WorkerThread(threading.Thread):
311 """Keeps the results of each task in a thread-local outputs variable."""
312 def __init__(self, tasks, *args, **kwargs):
313 super(WorkerThread, self).__init__(*args, **kwargs)
314 self._tasks = tasks
315 self.outputs = []
316 self.exceptions = []
317
318 self.daemon = True
319 self.start()
320
321 def run(self):
322 """Runs until a None task is queued."""
323 while True:
324 task = self._tasks.get()
325 if task is None:
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000326 # We're done.
327 return
328 try:
329 func, args, kwargs = task
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000330 self.outputs.append(func(*args, **kwargs))
331 except Exception, e:
332 logging.error('Caught exception! %s' % e)
333 self.exceptions.append(sys.exc_info())
334 finally:
335 self._tasks.task_done()
336
337
338class ThreadPool(object):
339 """Implements a multithreaded worker pool oriented for mapping jobs with
340 thread-local result storage.
341 """
342 QUEUE_CLASS = Queue.Queue
343
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000344 def __init__(self, num_threads, queue_size=0):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000345 logging.debug('Creating ThreadPool')
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000346 self.tasks = self.QUEUE_CLASS(queue_size)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000347 self._workers = [
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000348 WorkerThread(self.tasks, name='worker-%d' % i)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000349 for i in range(num_threads)
350 ]
351
352 def add_task(self, func, *args, **kwargs):
353 """Adds a task, a function to be executed by a worker.
354
355 The function's return value will be stored in the the worker's thread local
356 outputs list.
357 """
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000358 self.tasks.put((func, args, kwargs))
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000359
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000360 def join(self):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000361 """Extracts all the results from each threads unordered."""
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000362 self.tasks.join()
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000363 out = []
364 # Look for exceptions.
365 for w in self._workers:
366 if w.exceptions:
367 raise w.exceptions[0][0], w.exceptions[0][1], w.exceptions[0][2]
368 out.extend(w.outputs)
369 w.outputs = []
370 return out
371
372 def close(self):
373 """Closes all the threads."""
374 for _ in range(len(self._workers)):
375 # Enqueueing None causes the worker to stop.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000376 self.tasks.put(None)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000377 for t in self._workers:
378 t.join()
379
380 def __enter__(self):
381 """Enables 'with' statement."""
382 return self
383
384 def __exit__(self, exc_type, exc_value, traceback):
385 """Enables 'with' statement."""
386 self.close()
387
388
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000389def valid_file(filepath, size):
390 """Determines if the given files appears valid (currently it just checks
391 the file's size)."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000392 if size == UNKNOWN_FILE_SIZE:
393 return True
394 actual_size = os.stat(filepath).st_size
395 if size != actual_size:
396 logging.warning(
397 'Found invalid item %s; %d != %d',
398 os.path.basename(filepath), actual_size, size)
399 return False
400 return True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000401
402
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000403class Profiler(object):
404 def __init__(self, name):
405 self.name = name
406 self.start_time = None
407
408 def __enter__(self):
409 self.start_time = time.time()
410 return self
411
412 def __exit__(self, _exc_type, _exec_value, _traceback):
413 time_taken = time.time() - self.start_time
414 logging.info('Profiling: Section %s took %3.3f seconds',
415 self.name, time_taken)
416
417
418class Remote(object):
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000419 """Priority based worker queue to fetch or upload files from a
420 content-address server. Any function may be given as the fetcher/upload,
421 as long as it takes two inputs (the item contents, and their relative
422 destination).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000423
424 Supports local file system, CIFS or http remotes.
425
426 When the priority of items is equals, works in strict FIFO mode.
427 """
428 # Initial and maximum number of worker threads.
429 INITIAL_WORKERS = 2
430 MAX_WORKERS = 16
431 # Priorities.
432 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
433 INTERNAL_PRIORITY_BITS = (1<<8) - 1
434 RETRIES = 5
435
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000436 def __init__(self, destination_root):
437 # Function to fetch a remote object or upload to a remote location..
438 self._do_item = self.get_file_handler(destination_root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000439 # Contains tuple(priority, index, obj, destination).
440 self._queue = Queue.PriorityQueue()
441 # Contains tuple(priority, index, obj).
442 self._done = Queue.PriorityQueue()
443
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000444 # Contains generated exceptions that haven't been handled yet.
445 self._exceptions = Queue.Queue()
446
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000447 # To keep FIFO ordering in self._queue. It is assumed xrange's iterator is
448 # thread-safe.
449 self._next_index = xrange(0, 1<<30).__iter__().next
450
451 # Control access to the following member.
452 self._ready_lock = threading.Lock()
453 # Number of threads in wait state.
454 self._ready = 0
455
456 # Control access to the following member.
457 self._workers_lock = threading.Lock()
458 self._workers = []
459 for _ in range(self.INITIAL_WORKERS):
460 self._add_worker()
461
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000462 def join(self):
463 """Blocks until the queue is empty."""
464 self._queue.join()
465
466 def next_exception(self):
467 """Returns the next unhandled exception, or None if there is
468 no exception."""
469 try:
470 return self._exceptions.get_nowait()
471 except Queue.Empty:
472 return None
473
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000474 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000475 """Retrieves an object from the remote data store.
476
477 The smaller |priority| gets fetched first.
478
479 Thread-safe.
480 """
481 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000482 self._add_to_queue(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000483
484 def get_result(self):
485 """Returns the next file that was successfully fetched."""
486 r = self._done.get()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000487 if r[0] == -1:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000488 # It's an exception.
489 raise r[2][0], r[2][1], r[2][2]
490 return r[2]
491
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000492 def _add_to_queue(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000493 with self._ready_lock:
494 start_new_worker = not self._ready
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000495 self._queue.put((priority, self._next_index(), obj, dest, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000496 if start_new_worker:
497 self._add_worker()
498
499 def _add_worker(self):
500 """Add one worker thread if there isn't too many. Thread-safe."""
501 with self._workers_lock:
502 if len(self._workers) >= self.MAX_WORKERS:
503 return False
504 worker = threading.Thread(target=self._run)
505 self._workers.append(worker)
506 worker.daemon = True
507 worker.start()
508
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000509 def _step_done(self, result):
510 """Worker helper function"""
511 self._done.put(result)
512 self._queue.task_done()
513 if result[0] == -1:
514 self._exceptions.put(sys.exc_info())
515
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000516 def _run(self):
517 """Worker thread loop."""
518 while True:
519 try:
520 with self._ready_lock:
521 self._ready += 1
522 item = self._queue.get()
523 finally:
524 with self._ready_lock:
525 self._ready -= 1
526 if not item:
527 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000528 priority, index, obj, dest, size = item
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000529 try:
530 self._do_item(obj, dest)
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000531 if size and not valid_file(dest, size):
532 download_size = os.stat(dest).st_size
533 os.remove(dest)
534 raise IOError('File incorrect size after download of %s. Got %s and '
maruel@chromium.org3f039182012-11-27 21:32:41 +0000535 'expected %s' % (obj, download_size, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000536 except IOError:
537 # Retry a few times, lowering the priority.
538 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000539 self._add_to_queue(priority + 1, obj, dest, size)
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000540 self._queue.task_done()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000541 continue
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 except:
545 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000546 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000547 else:
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000548 self._step_done((priority, index, obj))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000549
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +0000550 def get_file_handler(self, file_or_url): # pylint: disable=R0201
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000551 """Returns a object to retrieve objects from a remote."""
552 if re.match(r'^https?://.+$', file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000553 def download_file(item, dest):
554 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
555 # easy.
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000556 try:
csharp@chromium.orgaa2d1512012-12-05 21:17:39 +0000557 zipped_source = file_or_url + item
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000558 logging.debug('download_file(%s)', zipped_source)
559 connection = urllib2.urlopen(zipped_source)
560 decompressor = zlib.decompressobj()
maruel@chromium.org3f039182012-11-27 21:32:41 +0000561 size = 0
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000562 with open(dest, 'wb') as f:
563 while True:
564 chunk = connection.read(ZIPPED_FILE_CHUNK)
565 if not chunk:
566 break
maruel@chromium.org3f039182012-11-27 21:32:41 +0000567 size += len(chunk)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000568 f.write(decompressor.decompress(chunk))
569 # Ensure that all the data was properly decompressed.
570 uncompressed_data = decompressor.flush()
571 assert not uncompressed_data
csharp@chromium.org186d6232012-11-26 14:36:12 +0000572 except zlib.error as e:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000573 # Log the first bytes to see if it's uncompressed data.
574 logging.warning('%r', e[:512])
maruel@chromium.org3f039182012-11-27 21:32:41 +0000575 raise IOError(
576 'Problem unzipping data for item %s. Got %d bytes.\n%s' %
577 (item, size, e))
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000578
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000579 return download_file
580
581 def copy_file(item, dest):
582 source = os.path.join(file_or_url, item)
583 logging.debug('copy_file(%s, %s)', source, dest)
584 shutil.copy(source, dest)
585 return copy_file
586
587
588class CachePolicies(object):
589 def __init__(self, max_cache_size, min_free_space, max_items):
590 """
591 Arguments:
592 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
593 cache is effectively a leak.
594 - min_free_space: Trim if disk free space becomes lower than this value. If
595 0, it unconditionally fill the disk.
596 - max_items: Maximum number of items to keep in the cache. If 0, do not
597 enforce a limit.
598 """
599 self.max_cache_size = max_cache_size
600 self.min_free_space = min_free_space
601 self.max_items = max_items
602
603
604class Cache(object):
605 """Stateful LRU cache.
606
607 Saves its state as json file.
608 """
609 STATE_FILE = 'state.json'
610
611 def __init__(self, cache_dir, remote, policies):
612 """
613 Arguments:
614 - cache_dir: Directory where to place the cache.
615 - remote: Remote where to fetch items from.
616 - policies: cache retention policies.
617 """
618 self.cache_dir = cache_dir
619 self.remote = remote
620 self.policies = policies
621 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
622 # The tuple(file, size) are kept as an array in a LRU style. E.g.
623 # self.state[0] is the oldest item.
624 self.state = []
maruel@chromium.org770993b2012-12-11 17:16:48 +0000625 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000626 # A lookup map to speed up searching.
627 self._lookup = {}
maruel@chromium.org770993b2012-12-11 17:16:48 +0000628 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000629
630 # Items currently being fetched. Keep it local to reduce lock contention.
631 self._pending_queue = set()
632
633 # Profiling values.
634 self._added = []
635 self._removed = []
636 self._free_disk = 0
637
maruel@chromium.org770993b2012-12-11 17:16:48 +0000638 with Profiler('Setup'):
639 if not os.path.isdir(self.cache_dir):
640 os.makedirs(self.cache_dir)
641 if os.path.isfile(self.state_file):
642 try:
643 self.state = json.load(open(self.state_file, 'r'))
644 except (IOError, ValueError), e:
645 # Too bad. The file will be overwritten and the cache cleared.
646 logging.error(
647 'Broken state file %s, ignoring.\n%s' % (self.STATE_FILE, e))
648 self._state_need_to_be_saved = True
649 if (not isinstance(self.state, list) or
650 not all(
651 isinstance(i, (list, tuple)) and len(i) == 2
652 for i in self.state)):
653 # Discard.
654 self._state_need_to_be_saved = True
655 self.state = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000656
maruel@chromium.org770993b2012-12-11 17:16:48 +0000657 # Ensure that all files listed in the state still exist and add new ones.
658 previous = set(filename for filename, _ in self.state)
659 if len(previous) != len(self.state):
660 logging.warn('Cache state is corrupted, found duplicate files')
661 self._state_need_to_be_saved = True
662 self.state = []
663
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000664 added = 0
665 for filename in os.listdir(self.cache_dir):
666 if filename == self.STATE_FILE:
667 continue
668 if filename in previous:
669 previous.remove(filename)
670 continue
671 # An untracked file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000672 if not RE_IS_SHA1.match(filename):
673 logging.warn('Removing unknown file %s from cache', filename)
674 os.remove(self.path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000675 continue
676 # Insert as the oldest file. It will be deleted eventually if not
677 # accessed.
678 self._add(filename, False)
679 logging.warn('Add unknown file %s to cache', filename)
680 added += 1
681
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000682 if added:
683 logging.warn('Added back %d unknown files', added)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000684 if previous:
685 logging.warn('Removed %d lost files', len(previous))
686 # Set explicitly in case self._add() wasn't called.
687 self._state_need_to_be_saved = True
688 # Filter out entries that were not found while keeping the previous
689 # order.
690 self.state = [
691 (filename, size) for filename, size in self.state
692 if filename not in previous
693 ]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000694 self.trim()
695
696 def __enter__(self):
697 return self
698
699 def __exit__(self, _exc_type, _exec_value, _traceback):
700 with Profiler('CleanupTrimming'):
701 self.trim()
702
703 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000704 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000705 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000706 '%5d (%8dkb) current',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000707 len(self.state),
708 sum(i[1] for i in self.state) / 1024)
709 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000710 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
711 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000712
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000713 def remove_file_at_index(self, index):
714 """Removes the file at the given index."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000715 try:
maruel@chromium.org770993b2012-12-11 17:16:48 +0000716 self._state_need_to_be_saved = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000717 filename, size = self.state.pop(index)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000718 # If the lookup was already stale, its possible the filename was not
719 # present yet.
720 self._lookup_is_stale = True
721 self._lookup.pop(filename, None)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000722 self._removed.append(size)
723 os.remove(self.path(filename))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000724 except OSError as e:
725 logging.error('Error attempting to delete a file\n%s' % e)
726
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000727 def remove_lru_file(self):
728 """Removes the last recently used file."""
729 self.remove_file_at_index(0)
730
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000731 def trim(self):
732 """Trims anything we don't know, make sure enough free space exists."""
733 # Ensure maximum cache size.
734 if self.policies.max_cache_size and self.state:
735 while sum(i[1] for i in self.state) > self.policies.max_cache_size:
736 self.remove_lru_file()
737
738 # Ensure maximum number of items in the cache.
739 if self.policies.max_items and self.state:
740 while len(self.state) > self.policies.max_items:
741 self.remove_lru_file()
742
743 # Ensure enough free space.
744 self._free_disk = get_free_space(self.cache_dir)
745 while (
746 self.policies.min_free_space and
747 self.state and
748 self._free_disk < self.policies.min_free_space):
749 self.remove_lru_file()
750 self._free_disk = get_free_space(self.cache_dir)
751
752 self.save()
753
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000754 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000755 """Retrieves a file from the remote, if not already cached, and adds it to
756 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000757
758 If the file is in the cache, verifiy that the file is valid (i.e. it is
759 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000760 """
761 assert not '/' in item
762 path = self.path(item)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000763 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000764 index = self._lookup.get(item)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000765
766 if index is not None:
767 if not valid_file(self.path(item), size):
768 self.remove_file_at_index(index)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000769 index = None
770 else:
771 assert index < len(self.state)
772 # Was already in cache. Update it's LRU value by putting it at the end.
maruel@chromium.org770993b2012-12-11 17:16:48 +0000773 self._state_need_to_be_saved = True
774 self._lookup_is_stale = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000775 self.state.append(self.state.pop(index))
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000776
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000777 if index is None:
778 if item in self._pending_queue:
779 # Already pending. The same object could be referenced multiple times.
780 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000781 self.remote.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000782 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000783
784 def add(self, filepath, obj):
785 """Forcibly adds a file to the cache."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000786 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000787 if not obj in self._lookup:
788 link_file(self.path(obj), filepath, HARDLINK)
789 self._add(obj, True)
790
791 def path(self, item):
792 """Returns the path to one item."""
793 return os.path.join(self.cache_dir, item)
794
795 def save(self):
796 """Saves the LRU ordering."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000797 if self._state_need_to_be_saved:
798 json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':'))
799 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000800
801 def wait_for(self, items):
802 """Starts a loop that waits for at least one of |items| to be retrieved.
803
804 Returns the first item retrieved.
805 """
806 # Flush items already present.
maruel@chromium.org770993b2012-12-11 17:16:48 +0000807 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000808 for item in items:
809 if item in self._lookup:
810 return item
811
812 assert all(i in self._pending_queue for i in items), (
813 items, self._pending_queue)
814 # Note that:
815 # len(self._pending_queue) ==
816 # ( len(self.remote._workers) - self.remote._ready +
817 # len(self._remote._queue) + len(self._remote.done))
818 # There is no lock-free way to verify that.
819 while self._pending_queue:
820 item = self.remote.get_result()
821 self._pending_queue.remove(item)
822 self._add(item, True)
823 if item in items:
824 return item
825
826 def _add(self, item, at_end):
827 """Adds an item in the internal state.
828
829 If |at_end| is False, self._lookup becomes inconsistent and
830 self._update_lookup() must be called.
831 """
832 size = os.stat(self.path(item)).st_size
833 self._added.append(size)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000834 self._state_need_to_be_saved = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000835 if at_end:
836 self.state.append((item, size))
837 self._lookup[item] = len(self.state) - 1
838 else:
maruel@chromium.org770993b2012-12-11 17:16:48 +0000839 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000840 self.state.insert(0, (item, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000841
842 def _update_lookup(self):
maruel@chromium.org770993b2012-12-11 17:16:48 +0000843 if self._lookup_is_stale:
844 self._lookup = dict(
845 (filename, index) for index, (filename, _) in enumerate(self.state))
846 self._lookup_is_stale = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000847
848
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000849class IsolatedFile(object):
850 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000851 def __init__(self, obj_hash):
852 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000853 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000854 self.obj_hash = obj_hash
855 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000856 # .isolate and all the .isolated files recursively included by it with
857 # 'includes' key. The order of each sha-1 in 'includes', each representing a
858 # .isolated file in the hash table, is important, as the later ones are not
859 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000860 self.can_fetch = False
861
862 # Raw data.
863 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000864 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000865 self.children = []
866
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000867 # Set once the .isolated file is loaded.
868 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000869 # Set once the files are fetched.
870 self.files_fetched = False
871
872 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000873 """Verifies the .isolated file is valid and loads this object with the json
874 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000875 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000876 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
877 assert not self._is_parsed
878 self.data = load_isolated(content)
879 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
880 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000881
882 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000883 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000884
885 Preemptively request files.
886
887 Note that |files| is modified by this function.
888 """
889 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000890 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000891 return
892 logging.debug('fetch_files(%s)' % self.obj_hash)
893 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000894 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000895 # overriden files must not be fetched.
896 if filepath not in files:
897 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000898 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000899 # Preemptively request files.
900 logging.debug('fetching %s' % filepath)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000901 cache.retrieve(Remote.MED, properties['h'], properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000902 self.files_fetched = True
903
904
905class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000906 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000907 def __init__(self):
908 self.command = []
909 self.files = {}
910 self.read_only = None
911 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000912 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000913 self.root = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000914
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000915 def load(self, cache, root_isolated_hash):
916 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000917
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000918 It enables support for "included" .isolated files. They are processed in
919 strict order but fetched asynchronously from the cache. This is important so
920 that a file in an included .isolated file that is overridden by an embedding
921 .isolated file is not fetched neededlessly. The includes are fetched in one
922 pass and the files are fetched as soon as all the ones on the left-side
923 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000924
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000925 The prioritization is very important here for nested .isolated files.
926 'includes' have the highest priority and the algorithm is optimized for both
927 deep and wide trees. A deep one is a long link of .isolated files referenced
928 one at a time by one item in 'includes'. A wide one has a large number of
929 'includes' in a single .isolated file. 'left' is defined as an included
930 .isolated file earlier in the 'includes' list. So the order of the elements
931 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000932 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000933 self.root = IsolatedFile(root_isolated_hash)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000934 cache.retrieve(Remote.HIGH, root_isolated_hash, UNKNOWN_FILE_SIZE)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000935 pending = {root_isolated_hash: self.root}
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000936 # Keeps the list of retrieved items to refuse recursive includes.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000937 retrieved = [root_isolated_hash]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000938
939 def update_self(node):
940 node.fetch_files(cache, self.files)
941 # Grabs properties.
942 if not self.command and node.data.get('command'):
943 self.command = node.data['command']
944 if self.read_only is None and node.data.get('read_only') is not None:
945 self.read_only = node.data['read_only']
946 if (self.relative_cwd is None and
947 node.data.get('relative_cwd') is not None):
948 self.relative_cwd = node.data['relative_cwd']
949
950 def traverse_tree(node):
951 if node.can_fetch:
952 if not node.files_fetched:
953 update_self(node)
954 will_break = False
955 for i in node.children:
956 if not i.can_fetch:
957 if will_break:
958 break
959 # Automatically mark the first one as fetcheable.
960 i.can_fetch = True
961 will_break = True
962 traverse_tree(i)
963
964 while pending:
965 item_hash = cache.wait_for(pending)
966 item = pending.pop(item_hash)
967 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000968 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000969 # It's the root item.
970 item.can_fetch = True
971
972 for new_child in item.children:
973 h = new_child.obj_hash
974 if h in retrieved:
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000975 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000976 pending[h] = new_child
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000977 cache.retrieve(Remote.HIGH, h, UNKNOWN_FILE_SIZE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000978
979 # Traverse the whole tree to see if files can now be fetched.
980 traverse_tree(self.root)
981 def check(n):
982 return all(check(x) for x in n.children) and n.files_fetched
983 assert check(self.root)
984 self.relative_cwd = self.relative_cwd or ''
985 self.read_only = self.read_only or False
986
987
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000988def run_tha_test(isolated_hash, cache_dir, remote, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000989 """Downloads the dependencies in the cache, hardlinks them into a temporary
990 directory and runs the executable.
991 """
992 settings = Settings()
993 with Cache(cache_dir, Remote(remote), policies) as cache:
994 outdir = make_temp_dir('run_tha_test', cache_dir)
995 try:
996 # Initiate all the files download.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000997 with Profiler('GetIsolateds') as _prof:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000998 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000999 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001000 # Adds it in the cache. While not strictly necessary, this simplifies
1001 # the rest.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001002 h = hashlib.sha1(open(isolated_hash, 'r').read()).hexdigest()
1003 cache.add(isolated_hash, h)
1004 isolated_hash = h
1005 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001006
1007 if not settings.command:
1008 print >> sys.stderr, 'No command to run'
1009 return 1
1010
1011 with Profiler('GetRest') as _prof:
1012 logging.debug('Creating directories')
1013 # Creates the tree of directories to create.
1014 directories = set(os.path.dirname(f) for f in settings.files)
1015 for item in list(directories):
1016 while item:
1017 directories.add(item)
1018 item = os.path.dirname(item)
1019 for d in sorted(directories):
1020 if d:
1021 os.mkdir(os.path.join(outdir, d))
1022
1023 # Creates the links if necessary.
1024 for filepath, properties in settings.files.iteritems():
1025 if 'link' not in properties:
1026 continue
1027 outfile = os.path.join(outdir, filepath)
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +00001028 # symlink doesn't exist on Windows. So the 'link' property should
1029 # never be specified for windows .isolated file.
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001030 os.symlink(properties['l'], outfile) # pylint: disable=E1101
1031 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001032 # It's not set on Windows.
maruel@chromium.org96768a42012-10-31 18:49:18 +00001033 lchmod = getattr(os, 'lchmod', None)
1034 if lchmod:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001035 lchmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001036
1037 # Remaining files to be processed.
1038 # Note that files could still be not be downloaded yet here.
1039 remaining = dict()
1040 for filepath, props in settings.files.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001041 if 'h' in props:
1042 remaining.setdefault(props['h'], []).append((filepath, props))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001043
1044 # Do bookkeeping while files are being downloaded in the background.
1045 cwd = os.path.join(outdir, settings.relative_cwd)
1046 if not os.path.isdir(cwd):
1047 os.makedirs(cwd)
1048 cmd = settings.command[:]
1049 # Ensure paths are correctly separated on windows.
1050 cmd[0] = cmd[0].replace('/', os.path.sep)
1051 cmd = fix_python_path(cmd)
1052
1053 # Now block on the remaining files to be downloaded and mapped.
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001054 logging.info('Retrieving remaining files')
1055 last_update = time.time()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001056 while remaining:
1057 obj = cache.wait_for(remaining)
1058 for filepath, properties in remaining.pop(obj):
1059 outfile = os.path.join(outdir, filepath)
1060 link_file(outfile, cache.path(obj), HARDLINK)
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001061 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001062 # It's not set on Windows.
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001063 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001064
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001065 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
1066 logging.info('%d files remaining...' % len(remaining))
1067 last_update = time.time()
1068
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001069 if settings.read_only:
1070 make_writable(outdir, True)
1071 logging.info('Running %s, cwd=%s' % (cmd, cwd))
csharp@chromium.orge217f302012-11-22 16:51:53 +00001072
1073 # TODO(csharp): This should be specified somewhere else.
1074 # Add a rotating log file if one doesn't already exist.
1075 env = os.environ.copy()
1076 env.setdefault('RUN_TEST_CASES_LOG_FILE', RUN_TEST_CASES_LOG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001077 try:
1078 with Profiler('RunTest') as _prof:
csharp@chromium.orge217f302012-11-22 16:51:53 +00001079 return subprocess.call(cmd, cwd=cwd, env=env)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001080 except OSError:
1081 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1082 raise
1083 finally:
1084 rmtree(outdir)
1085
1086
1087def main():
1088 parser = optparse.OptionParser(
1089 usage='%prog <options>', description=sys.modules[__name__].__doc__)
1090 parser.add_option(
1091 '-v', '--verbose', action='count', default=0, help='Use multiple times')
1092 parser.add_option('--no-run', action='store_true', help='Skip the run part')
1093
1094 group = optparse.OptionGroup(parser, 'Data source')
1095 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001096 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001097 metavar='FILE',
1098 help='File/url describing what to map or run')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001099 # TODO(maruel): Remove once not used anymore.
1100 group.add_option(
1101 '-m', '--manifest', dest='isolated', help=optparse.SUPPRESS_HELP)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001102 group.add_option(
1103 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001104 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001105 parser.add_option_group(group)
1106
1107 group.add_option(
1108 '-r', '--remote', metavar='URL', help='Remote where to get the items')
1109 group = optparse.OptionGroup(parser, 'Cache management')
1110 group.add_option(
1111 '--cache',
1112 default='cache',
1113 metavar='DIR',
1114 help='Cache directory, default=%default')
1115 group.add_option(
1116 '--max-cache-size',
1117 type='int',
1118 metavar='NNN',
1119 default=20*1024*1024*1024,
1120 help='Trim if the cache gets larger than this value, default=%default')
1121 group.add_option(
1122 '--min-free-space',
1123 type='int',
1124 metavar='NNN',
1125 default=1*1024*1024*1024,
1126 help='Trim if disk free space becomes lower than this value, '
1127 'default=%default')
1128 group.add_option(
1129 '--max-items',
1130 type='int',
1131 metavar='NNN',
1132 default=100000,
1133 help='Trim if more than this number of items are in the cache '
1134 'default=%default')
1135 parser.add_option_group(group)
1136
1137 options, args = parser.parse_args()
1138 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
csharp@chromium.orgff2a4662012-11-21 20:49:32 +00001139
1140 logging_console = logging.StreamHandler()
1141 logging_console.setFormatter(logging.Formatter(
1142 '%(levelname)5s %(module)15s(%(lineno)3d): %(message)s'))
1143 logging_console.setLevel(level)
1144 logging.getLogger().addHandler(logging_console)
1145
1146 logging_rotating_file = logging.handlers.RotatingFileHandler(
1147 RUN_ISOLATED_LOG_FILE,
1148 maxBytes=10 * 1024 * 1024, backupCount=5)
1149 logging_rotating_file.setLevel(logging.DEBUG)
1150 logging_rotating_file.setFormatter(logging.Formatter(
1151 '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)3d): %(message)s'))
1152 logging.getLogger().addHandler(logging_rotating_file)
1153
1154 logging.getLogger().setLevel(logging.DEBUG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001155
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001156 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001157 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001158 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001159 if not options.remote:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001160 logging.debug('--remote is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001161 parser.error('--remote is required.')
1162 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001163 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001164 parser.error('Unsupported args %s' % ' '.join(args))
1165
1166 policies = CachePolicies(
1167 options.max_cache_size, options.min_free_space, options.max_items)
1168 try:
1169 return run_tha_test(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001170 options.isolated or options.hash,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001171 os.path.abspath(options.cache),
1172 options.remote,
1173 policies)
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001174 except Exception, e:
1175 # Make sure any exception is logged.
1176 logging.exception(e)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001177 return 1
1178
1179
1180if __name__ == '__main__':
1181 sys.exit(main())