blob: c4c2ede55ff05663ce1d88814fa95c4dd2057d01 [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
csharp@chromium.orga110d792013-01-07 16:16:16 +000013import httplib
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000014import json
15import logging
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000016import logging.handlers
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000017import optparse
18import os
19import Queue
20import re
21import shutil
22import stat
23import subprocess
24import sys
25import tempfile
26import threading
27import time
28import urllib
csharp@chromium.orga92403f2012-11-20 15:13:59 +000029import urllib2
30import zlib
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000031
32
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000033# Types of action accepted by link_file().
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000034HARDLINK, SYMLINK, COPY = range(1, 4)
35
36RE_IS_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$')
37
csharp@chromium.org8dc52542012-11-08 20:29:55 +000038# The file size to be used when we don't know the correct file size,
39# generally used for .isolated files.
40UNKNOWN_FILE_SIZE = None
41
csharp@chromium.orga92403f2012-11-20 15:13:59 +000042# The size of each chunk to read when downloading and unzipping files.
43ZIPPED_FILE_CHUNK = 16 * 1024
44
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000045# The name of the log file to use.
46RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
47
csharp@chromium.orge217f302012-11-22 16:51:53 +000048# The base directory containing this file.
49BASE_DIR = os.path.dirname(os.path.abspath(__file__))
50
51# The name of the log to use for the run_test_cases.py command
52RUN_TEST_CASES_LOG = os.path.join(BASE_DIR, 'run_test_cases.log')
53
csharp@chromium.org9c59ff12012-12-12 02:32:29 +000054# The delay (in seconds) to wait between logging statements when retrieving
55# the required files. This is intended to let the user (or buildbot) know that
56# the program is still running.
57DELAY_BETWEEN_UPDATES_IN_SECS = 30
58
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000059
60class ConfigError(ValueError):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +000061 """Generic failure to load a .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000062 pass
63
64
65class MappingError(OSError):
66 """Failed to recreate the tree."""
67 pass
68
69
csharp@chromium.orga92403f2012-11-20 15:13:59 +000070class DownloadFileOpener(urllib.FancyURLopener):
71 """This class is needed to get urlretrive to raise an exception on
72 404 errors, instead of still writing to the file with the error code.
73 """
74 def http_error_default(self, url, fp, errcode, errmsg, headers):
75 raise urllib2.HTTPError(url, errcode, errmsg, headers, fp)
76
77
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000078def get_flavor():
79 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
80 flavors = {
81 'cygwin': 'win',
82 'win32': 'win',
83 'darwin': 'mac',
84 'sunos5': 'solaris',
85 'freebsd7': 'freebsd',
86 'freebsd8': 'freebsd',
87 }
88 return flavors.get(sys.platform, 'linux')
89
90
91def os_link(source, link_name):
92 """Add support for os.link() on Windows."""
93 if sys.platform == 'win32':
94 if not ctypes.windll.kernel32.CreateHardLinkW(
95 unicode(link_name), unicode(source), 0):
96 raise OSError()
97 else:
98 os.link(source, link_name)
99
100
101def readable_copy(outfile, infile):
102 """Makes a copy of the file that is readable by everyone."""
103 shutil.copy(infile, outfile)
104 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
105 stat.S_IRGRP | stat.S_IROTH)
106 os.chmod(outfile, read_enabled_mode)
107
108
109def link_file(outfile, infile, action):
110 """Links a file. The type of link depends on |action|."""
111 logging.debug('Mapping %s to %s' % (infile, outfile))
112 if action not in (HARDLINK, SYMLINK, COPY):
113 raise ValueError('Unknown mapping action %s' % action)
114 if not os.path.isfile(infile):
115 raise MappingError('%s is missing' % infile)
116 if os.path.isfile(outfile):
117 raise MappingError(
118 '%s already exist; insize:%d; outsize:%d' %
119 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
120
121 if action == COPY:
122 readable_copy(outfile, infile)
123 elif action == SYMLINK and sys.platform != 'win32':
124 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000125 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000126 else:
127 try:
128 os_link(infile, outfile)
129 except OSError:
130 # Probably a different file system.
131 logging.warn(
132 'Failed to hardlink, failing back to copy %s to %s' % (
133 infile, outfile))
134 readable_copy(outfile, infile)
135
136
137def _set_write_bit(path, read_only):
138 """Sets or resets the executable bit on a file or directory."""
139 mode = os.lstat(path).st_mode
140 if read_only:
141 mode = mode & 0500
142 else:
143 mode = mode | 0200
144 if hasattr(os, 'lchmod'):
145 os.lchmod(path, mode) # pylint: disable=E1101
146 else:
147 if stat.S_ISLNK(mode):
148 # Skip symlink without lchmod() support.
149 logging.debug('Can\'t change +w bit on symlink %s' % path)
150 return
151
152 # TODO(maruel): Implement proper DACL modification on Windows.
153 os.chmod(path, mode)
154
155
156def make_writable(root, read_only):
157 """Toggle the writable bit on a directory tree."""
maruel@chromium.org61a9b3b2012-12-12 17:18:52 +0000158 assert os.path.isabs(root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000159 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
160 for filename in filenames:
161 _set_write_bit(os.path.join(dirpath, filename), read_only)
162
163 for dirname in dirnames:
164 _set_write_bit(os.path.join(dirpath, dirname), read_only)
165
166
167def rmtree(root):
168 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
169 make_writable(root, False)
170 if sys.platform == 'win32':
171 for i in range(3):
172 try:
173 shutil.rmtree(root)
174 break
175 except WindowsError: # pylint: disable=E0602
176 delay = (i+1)*2
177 print >> sys.stderr, (
178 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
179 time.sleep(delay)
180 else:
181 shutil.rmtree(root)
182
183
184def is_same_filesystem(path1, path2):
185 """Returns True if both paths are on the same filesystem.
186
187 This is required to enable the use of hardlinks.
188 """
189 assert os.path.isabs(path1), path1
190 assert os.path.isabs(path2), path2
191 if sys.platform == 'win32':
192 # If the drive letter mismatches, assume it's a separate partition.
193 # TODO(maruel): It should look at the underlying drive, a drive letter could
194 # be a mount point to a directory on another drive.
195 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
196 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
197 if path1[0].lower() != path2[0].lower():
198 return False
199 return os.stat(path1).st_dev == os.stat(path2).st_dev
200
201
202def get_free_space(path):
203 """Returns the number of free bytes."""
204 if sys.platform == 'win32':
205 free_bytes = ctypes.c_ulonglong(0)
206 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
207 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
208 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000209 # For OSes other than Windows.
210 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000211 return f.f_bfree * f.f_frsize
212
213
214def make_temp_dir(prefix, root_dir):
215 """Returns a temporary directory on the same file system as root_dir."""
216 base_temp_dir = None
217 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
218 base_temp_dir = os.path.dirname(root_dir)
219 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
220
221
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000222def load_isolated(content):
223 """Verifies the .isolated file is valid and loads this object with the json
224 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000225 """
226 try:
227 data = json.loads(content)
228 except ValueError:
229 raise ConfigError('Failed to parse: %s...' % content[:100])
230
231 if not isinstance(data, dict):
232 raise ConfigError('Expected dict, got %r' % data)
233
234 for key, value in data.iteritems():
235 if key == 'command':
236 if not isinstance(value, list):
237 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000238 if not value:
239 raise ConfigError('Expected non-empty command')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000240 for subvalue in value:
241 if not isinstance(subvalue, basestring):
242 raise ConfigError('Expected string, got %r' % subvalue)
243
244 elif key == 'files':
245 if not isinstance(value, dict):
246 raise ConfigError('Expected dict, got %r' % value)
247 for subkey, subvalue in value.iteritems():
248 if not isinstance(subkey, basestring):
249 raise ConfigError('Expected string, got %r' % subkey)
250 if not isinstance(subvalue, dict):
251 raise ConfigError('Expected dict, got %r' % subvalue)
252 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000253 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000254 if not isinstance(subsubvalue, basestring):
255 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000256 elif subsubkey == 'm':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000257 if not isinstance(subsubvalue, int):
258 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000259 elif subsubkey == 'h':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000260 if not RE_IS_SHA1.match(subsubvalue):
261 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000262 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000263 if not isinstance(subsubvalue, int):
264 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000265 else:
266 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000267 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000268 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000269 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
270 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000271
272 elif key == 'includes':
273 if not isinstance(value, list):
274 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000275 if not value:
276 raise ConfigError('Expected non-empty includes list')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000277 for subvalue in value:
278 if not RE_IS_SHA1.match(subvalue):
279 raise ConfigError('Expected sha-1, got %r' % subvalue)
280
281 elif key == 'read_only':
282 if not isinstance(value, bool):
283 raise ConfigError('Expected bool, got %r' % value)
284
285 elif key == 'relative_cwd':
286 if not isinstance(value, basestring):
287 raise ConfigError('Expected string, got %r' % value)
288
289 elif key == 'os':
290 if value != get_flavor():
291 raise ConfigError(
292 'Expected \'os\' to be \'%s\' but got \'%s\'' %
293 (get_flavor(), value))
294
295 else:
296 raise ConfigError('Unknown key %s' % key)
297
298 return data
299
300
301def fix_python_path(cmd):
302 """Returns the fixed command line to call the right python executable."""
303 out = cmd[:]
304 if out[0] == 'python':
305 out[0] = sys.executable
306 elif out[0].endswith('.py'):
307 out.insert(0, sys.executable)
308 return out
309
310
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000311class WorkerThread(threading.Thread):
312 """Keeps the results of each task in a thread-local outputs variable."""
313 def __init__(self, tasks, *args, **kwargs):
314 super(WorkerThread, self).__init__(*args, **kwargs)
315 self._tasks = tasks
316 self.outputs = []
317 self.exceptions = []
318
319 self.daemon = True
320 self.start()
321
322 def run(self):
323 """Runs until a None task is queued."""
324 while True:
325 task = self._tasks.get()
326 if task is None:
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000327 # We're done.
328 return
329 try:
330 func, args, kwargs = task
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000331 self.outputs.append(func(*args, **kwargs))
332 except Exception, e:
333 logging.error('Caught exception! %s' % e)
334 self.exceptions.append(sys.exc_info())
335 finally:
336 self._tasks.task_done()
337
338
339class ThreadPool(object):
340 """Implements a multithreaded worker pool oriented for mapping jobs with
341 thread-local result storage.
342 """
343 QUEUE_CLASS = Queue.Queue
344
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000345 def __init__(self, num_threads, queue_size=0):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000346 logging.debug('Creating ThreadPool')
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000347 self.tasks = self.QUEUE_CLASS(queue_size)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000348 self._workers = [
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000349 WorkerThread(self.tasks, name='worker-%d' % i)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000350 for i in range(num_threads)
351 ]
352
353 def add_task(self, func, *args, **kwargs):
354 """Adds a task, a function to be executed by a worker.
355
356 The function's return value will be stored in the the worker's thread local
357 outputs list.
358 """
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000359 self.tasks.put((func, args, kwargs))
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000360
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000361 def join(self):
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000362 """Extracts all the results from each threads unordered."""
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000363 self.tasks.join()
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000364 out = []
365 # Look for exceptions.
366 for w in self._workers:
367 if w.exceptions:
368 raise w.exceptions[0][0], w.exceptions[0][1], w.exceptions[0][2]
369 out.extend(w.outputs)
370 w.outputs = []
371 return out
372
373 def close(self):
374 """Closes all the threads."""
375 for _ in range(len(self._workers)):
376 # Enqueueing None causes the worker to stop.
maruel@chromium.orgeb281652012-11-08 21:10:23 +0000377 self.tasks.put(None)
maruel@chromium.org8df128b2012-11-08 19:05:04 +0000378 for t in self._workers:
379 t.join()
380
381 def __enter__(self):
382 """Enables 'with' statement."""
383 return self
384
385 def __exit__(self, exc_type, exc_value, traceback):
386 """Enables 'with' statement."""
387 self.close()
388
389
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000390def valid_file(filepath, size):
391 """Determines if the given files appears valid (currently it just checks
392 the file's size)."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000393 if size == UNKNOWN_FILE_SIZE:
394 return True
395 actual_size = os.stat(filepath).st_size
396 if size != actual_size:
397 logging.warning(
398 'Found invalid item %s; %d != %d',
399 os.path.basename(filepath), actual_size, size)
400 return False
401 return True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000402
403
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000404class Profiler(object):
405 def __init__(self, name):
406 self.name = name
407 self.start_time = None
408
409 def __enter__(self):
410 self.start_time = time.time()
411 return self
412
413 def __exit__(self, _exc_type, _exec_value, _traceback):
414 time_taken = time.time() - self.start_time
415 logging.info('Profiling: Section %s took %3.3f seconds',
416 self.name, time_taken)
417
418
419class Remote(object):
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000420 """Priority based worker queue to fetch or upload files from a
421 content-address server. Any function may be given as the fetcher/upload,
422 as long as it takes two inputs (the item contents, and their relative
423 destination).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000424
425 Supports local file system, CIFS or http remotes.
426
427 When the priority of items is equals, works in strict FIFO mode.
428 """
429 # Initial and maximum number of worker threads.
430 INITIAL_WORKERS = 2
431 MAX_WORKERS = 16
432 # Priorities.
433 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
434 INTERNAL_PRIORITY_BITS = (1<<8) - 1
435 RETRIES = 5
436
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000437 def __init__(self, destination_root):
438 # Function to fetch a remote object or upload to a remote location..
439 self._do_item = self.get_file_handler(destination_root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000440 # Contains tuple(priority, index, obj, destination).
441 self._queue = Queue.PriorityQueue()
442 # Contains tuple(priority, index, obj).
443 self._done = Queue.PriorityQueue()
444
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000445 # Contains generated exceptions that haven't been handled yet.
446 self._exceptions = Queue.Queue()
447
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000448 # To keep FIFO ordering in self._queue. It is assumed xrange's iterator is
449 # thread-safe.
450 self._next_index = xrange(0, 1<<30).__iter__().next
451
452 # Control access to the following member.
453 self._ready_lock = threading.Lock()
454 # Number of threads in wait state.
455 self._ready = 0
456
457 # Control access to the following member.
458 self._workers_lock = threading.Lock()
459 self._workers = []
460 for _ in range(self.INITIAL_WORKERS):
461 self._add_worker()
462
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000463 def join(self):
464 """Blocks until the queue is empty."""
465 self._queue.join()
466
467 def next_exception(self):
468 """Returns the next unhandled exception, or None if there is
469 no exception."""
470 try:
471 return self._exceptions.get_nowait()
472 except Queue.Empty:
473 return None
474
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000475 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000476 """Retrieves an object from the remote data store.
477
478 The smaller |priority| gets fetched first.
479
480 Thread-safe.
481 """
482 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000483 self._add_to_queue(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000484
485 def get_result(self):
486 """Returns the next file that was successfully fetched."""
487 r = self._done.get()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000488 if r[0] == -1:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000489 # It's an exception.
490 raise r[2][0], r[2][1], r[2][2]
491 return r[2]
492
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000493 def _add_to_queue(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000494 with self._ready_lock:
495 start_new_worker = not self._ready
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000496 self._queue.put((priority, self._next_index(), obj, dest, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000497 if start_new_worker:
498 self._add_worker()
499
500 def _add_worker(self):
501 """Add one worker thread if there isn't too many. Thread-safe."""
502 with self._workers_lock:
503 if len(self._workers) >= self.MAX_WORKERS:
504 return False
505 worker = threading.Thread(target=self._run)
506 self._workers.append(worker)
507 worker.daemon = True
508 worker.start()
509
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000510 def _step_done(self, result):
511 """Worker helper function"""
512 self._done.put(result)
513 self._queue.task_done()
514 if result[0] == -1:
515 self._exceptions.put(sys.exc_info())
516
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000517 def _run(self):
518 """Worker thread loop."""
519 while True:
520 try:
521 with self._ready_lock:
522 self._ready += 1
523 item = self._queue.get()
524 finally:
525 with self._ready_lock:
526 self._ready -= 1
527 if not item:
528 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000529 priority, index, obj, dest, size = item
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000530 try:
531 self._do_item(obj, dest)
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000532 if size and not valid_file(dest, size):
533 download_size = os.stat(dest).st_size
534 os.remove(dest)
535 raise IOError('File incorrect size after download of %s. Got %s and '
maruel@chromium.org3f039182012-11-27 21:32:41 +0000536 'expected %s' % (obj, download_size, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000537 except IOError:
538 # Retry a few times, lowering the priority.
539 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000540 self._add_to_queue(priority + 1, obj, dest, size)
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000541 self._queue.task_done()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000542 continue
543 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000544 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000545 except:
546 # Transfers the exception back. It has maximum priority.
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000547 self._step_done((-1, 0, sys.exc_info()))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000548 else:
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000549 self._step_done((priority, index, obj))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000550
csharp@chromium.org59c7bcf2012-11-21 21:13:18 +0000551 def get_file_handler(self, file_or_url): # pylint: disable=R0201
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000552 """Returns a object to retrieve objects from a remote."""
553 if re.match(r'^https?://.+$', file_or_url):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000554 def download_file(item, dest):
555 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
556 # easy.
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000557 try:
csharp@chromium.orgaa2d1512012-12-05 21:17:39 +0000558 zipped_source = file_or_url + item
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000559 logging.debug('download_file(%s)', zipped_source)
560 connection = urllib2.urlopen(zipped_source)
561 decompressor = zlib.decompressobj()
maruel@chromium.org3f039182012-11-27 21:32:41 +0000562 size = 0
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000563 with open(dest, 'wb') as f:
564 while True:
565 chunk = connection.read(ZIPPED_FILE_CHUNK)
566 if not chunk:
567 break
maruel@chromium.org3f039182012-11-27 21:32:41 +0000568 size += len(chunk)
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000569 f.write(decompressor.decompress(chunk))
570 # Ensure that all the data was properly decompressed.
571 uncompressed_data = decompressor.flush()
572 assert not uncompressed_data
csharp@chromium.orga110d792013-01-07 16:16:16 +0000573 except httplib.HTTPException as e:
574 raise IOError('Encountered an HTTPException.\n%s' % e)
csharp@chromium.org186d6232012-11-26 14:36:12 +0000575 except zlib.error as e:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000576 # Log the first bytes to see if it's uncompressed data.
577 logging.warning('%r', e[:512])
maruel@chromium.org3f039182012-11-27 21:32:41 +0000578 raise IOError(
579 'Problem unzipping data for item %s. Got %d bytes.\n%s' %
580 (item, size, e))
csharp@chromium.orga92403f2012-11-20 15:13:59 +0000581
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000582 return download_file
583
584 def copy_file(item, dest):
585 source = os.path.join(file_or_url, item)
586 logging.debug('copy_file(%s, %s)', source, dest)
587 shutil.copy(source, dest)
588 return copy_file
589
590
591class CachePolicies(object):
592 def __init__(self, max_cache_size, min_free_space, max_items):
593 """
594 Arguments:
595 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
596 cache is effectively a leak.
597 - min_free_space: Trim if disk free space becomes lower than this value. If
598 0, it unconditionally fill the disk.
599 - max_items: Maximum number of items to keep in the cache. If 0, do not
600 enforce a limit.
601 """
602 self.max_cache_size = max_cache_size
603 self.min_free_space = min_free_space
604 self.max_items = max_items
605
606
607class Cache(object):
608 """Stateful LRU cache.
609
610 Saves its state as json file.
611 """
612 STATE_FILE = 'state.json'
613
614 def __init__(self, cache_dir, remote, policies):
615 """
616 Arguments:
617 - cache_dir: Directory where to place the cache.
618 - remote: Remote where to fetch items from.
619 - policies: cache retention policies.
620 """
621 self.cache_dir = cache_dir
622 self.remote = remote
623 self.policies = policies
624 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
625 # The tuple(file, size) are kept as an array in a LRU style. E.g.
626 # self.state[0] is the oldest item.
627 self.state = []
maruel@chromium.org770993b2012-12-11 17:16:48 +0000628 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000629 # A lookup map to speed up searching.
630 self._lookup = {}
maruel@chromium.org770993b2012-12-11 17:16:48 +0000631 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000632
633 # Items currently being fetched. Keep it local to reduce lock contention.
634 self._pending_queue = set()
635
636 # Profiling values.
637 self._added = []
638 self._removed = []
639 self._free_disk = 0
640
maruel@chromium.org770993b2012-12-11 17:16:48 +0000641 with Profiler('Setup'):
642 if not os.path.isdir(self.cache_dir):
643 os.makedirs(self.cache_dir)
644 if os.path.isfile(self.state_file):
645 try:
646 self.state = json.load(open(self.state_file, 'r'))
647 except (IOError, ValueError), e:
648 # Too bad. The file will be overwritten and the cache cleared.
649 logging.error(
650 'Broken state file %s, ignoring.\n%s' % (self.STATE_FILE, e))
651 self._state_need_to_be_saved = True
652 if (not isinstance(self.state, list) or
653 not all(
654 isinstance(i, (list, tuple)) and len(i) == 2
655 for i in self.state)):
656 # Discard.
657 self._state_need_to_be_saved = True
658 self.state = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000659
maruel@chromium.org770993b2012-12-11 17:16:48 +0000660 # Ensure that all files listed in the state still exist and add new ones.
661 previous = set(filename for filename, _ in self.state)
662 if len(previous) != len(self.state):
663 logging.warn('Cache state is corrupted, found duplicate files')
664 self._state_need_to_be_saved = True
665 self.state = []
666
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000667 added = 0
668 for filename in os.listdir(self.cache_dir):
669 if filename == self.STATE_FILE:
670 continue
671 if filename in previous:
672 previous.remove(filename)
673 continue
674 # An untracked file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000675 if not RE_IS_SHA1.match(filename):
676 logging.warn('Removing unknown file %s from cache', filename)
677 os.remove(self.path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000678 continue
679 # Insert as the oldest file. It will be deleted eventually if not
680 # accessed.
681 self._add(filename, False)
682 logging.warn('Add unknown file %s to cache', filename)
683 added += 1
684
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000685 if added:
686 logging.warn('Added back %d unknown files', added)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000687 if previous:
688 logging.warn('Removed %d lost files', len(previous))
689 # Set explicitly in case self._add() wasn't called.
690 self._state_need_to_be_saved = True
691 # Filter out entries that were not found while keeping the previous
692 # order.
693 self.state = [
694 (filename, size) for filename, size in self.state
695 if filename not in previous
696 ]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000697 self.trim()
698
699 def __enter__(self):
700 return self
701
702 def __exit__(self, _exc_type, _exec_value, _traceback):
703 with Profiler('CleanupTrimming'):
704 self.trim()
705
706 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000707 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000708 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000709 '%5d (%8dkb) current',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000710 len(self.state),
711 sum(i[1] for i in self.state) / 1024)
712 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000713 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
714 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000715
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000716 def remove_file_at_index(self, index):
717 """Removes the file at the given index."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000718 try:
maruel@chromium.org770993b2012-12-11 17:16:48 +0000719 self._state_need_to_be_saved = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000720 filename, size = self.state.pop(index)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000721 # If the lookup was already stale, its possible the filename was not
722 # present yet.
723 self._lookup_is_stale = True
724 self._lookup.pop(filename, None)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000725 self._removed.append(size)
726 os.remove(self.path(filename))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000727 except OSError as e:
728 logging.error('Error attempting to delete a file\n%s' % e)
729
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000730 def remove_lru_file(self):
731 """Removes the last recently used file."""
732 self.remove_file_at_index(0)
733
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000734 def trim(self):
735 """Trims anything we don't know, make sure enough free space exists."""
736 # Ensure maximum cache size.
737 if self.policies.max_cache_size and self.state:
738 while sum(i[1] for i in self.state) > self.policies.max_cache_size:
739 self.remove_lru_file()
740
741 # Ensure maximum number of items in the cache.
742 if self.policies.max_items and self.state:
743 while len(self.state) > self.policies.max_items:
744 self.remove_lru_file()
745
746 # Ensure enough free space.
747 self._free_disk = get_free_space(self.cache_dir)
748 while (
749 self.policies.min_free_space and
750 self.state and
751 self._free_disk < self.policies.min_free_space):
752 self.remove_lru_file()
753 self._free_disk = get_free_space(self.cache_dir)
754
755 self.save()
756
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000757 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000758 """Retrieves a file from the remote, if not already cached, and adds it to
759 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000760
761 If the file is in the cache, verifiy that the file is valid (i.e. it is
762 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000763 """
764 assert not '/' in item
765 path = self.path(item)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000766 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000767 index = self._lookup.get(item)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000768
769 if index is not None:
770 if not valid_file(self.path(item), size):
771 self.remove_file_at_index(index)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000772 index = None
773 else:
774 assert index < len(self.state)
775 # Was already in cache. Update it's LRU value by putting it at the end.
maruel@chromium.org770993b2012-12-11 17:16:48 +0000776 self._state_need_to_be_saved = True
777 self._lookup_is_stale = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000778 self.state.append(self.state.pop(index))
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000779
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000780 if index is None:
781 if item in self._pending_queue:
782 # Already pending. The same object could be referenced multiple times.
783 return
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000784 self.remote.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000785 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000786
787 def add(self, filepath, obj):
788 """Forcibly adds a file to the cache."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000789 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000790 if not obj in self._lookup:
791 link_file(self.path(obj), filepath, HARDLINK)
792 self._add(obj, True)
793
794 def path(self, item):
795 """Returns the path to one item."""
796 return os.path.join(self.cache_dir, item)
797
798 def save(self):
799 """Saves the LRU ordering."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000800 if self._state_need_to_be_saved:
801 json.dump(self.state, open(self.state_file, 'wb'), separators=(',',':'))
802 self._state_need_to_be_saved = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000803
804 def wait_for(self, items):
805 """Starts a loop that waits for at least one of |items| to be retrieved.
806
807 Returns the first item retrieved.
808 """
809 # Flush items already present.
maruel@chromium.org770993b2012-12-11 17:16:48 +0000810 self._update_lookup()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000811 for item in items:
812 if item in self._lookup:
813 return item
814
815 assert all(i in self._pending_queue for i in items), (
816 items, self._pending_queue)
817 # Note that:
818 # len(self._pending_queue) ==
819 # ( len(self.remote._workers) - self.remote._ready +
820 # len(self._remote._queue) + len(self._remote.done))
821 # There is no lock-free way to verify that.
822 while self._pending_queue:
823 item = self.remote.get_result()
824 self._pending_queue.remove(item)
825 self._add(item, True)
826 if item in items:
827 return item
828
829 def _add(self, item, at_end):
830 """Adds an item in the internal state.
831
832 If |at_end| is False, self._lookup becomes inconsistent and
833 self._update_lookup() must be called.
834 """
835 size = os.stat(self.path(item)).st_size
836 self._added.append(size)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000837 self._state_need_to_be_saved = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000838 if at_end:
839 self.state.append((item, size))
840 self._lookup[item] = len(self.state) - 1
841 else:
maruel@chromium.org770993b2012-12-11 17:16:48 +0000842 self._lookup_is_stale = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000843 self.state.insert(0, (item, size))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000844
845 def _update_lookup(self):
maruel@chromium.org770993b2012-12-11 17:16:48 +0000846 if self._lookup_is_stale:
847 self._lookup = dict(
848 (filename, index) for index, (filename, _) in enumerate(self.state))
849 self._lookup_is_stale = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000850
851
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000852class IsolatedFile(object):
853 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000854 def __init__(self, obj_hash):
855 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000856 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000857 self.obj_hash = obj_hash
858 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000859 # .isolate and all the .isolated files recursively included by it with
860 # 'includes' key. The order of each sha-1 in 'includes', each representing a
861 # .isolated file in the hash table, is important, as the later ones are not
862 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000863 self.can_fetch = False
864
865 # Raw data.
866 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000867 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000868 self.children = []
869
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000870 # Set once the .isolated file is loaded.
871 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000872 # Set once the files are fetched.
873 self.files_fetched = False
874
875 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000876 """Verifies the .isolated file is valid and loads this object with the json
877 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000878 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000879 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
880 assert not self._is_parsed
881 self.data = load_isolated(content)
882 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
883 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000884
885 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000886 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000887
888 Preemptively request files.
889
890 Note that |files| is modified by this function.
891 """
892 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000893 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000894 return
895 logging.debug('fetch_files(%s)' % self.obj_hash)
896 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000897 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000898 # overriden files must not be fetched.
899 if filepath not in files:
900 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000901 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000902 # Preemptively request files.
903 logging.debug('fetching %s' % filepath)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000904 cache.retrieve(Remote.MED, properties['h'], properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000905 self.files_fetched = True
906
907
908class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000909 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000910 def __init__(self):
911 self.command = []
912 self.files = {}
913 self.read_only = None
914 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000915 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000916 self.root = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000917
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000918 def load(self, cache, root_isolated_hash):
919 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000920
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000921 It enables support for "included" .isolated files. They are processed in
922 strict order but fetched asynchronously from the cache. This is important so
923 that a file in an included .isolated file that is overridden by an embedding
924 .isolated file is not fetched neededlessly. The includes are fetched in one
925 pass and the files are fetched as soon as all the ones on the left-side
926 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000927
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000928 The prioritization is very important here for nested .isolated files.
929 'includes' have the highest priority and the algorithm is optimized for both
930 deep and wide trees. A deep one is a long link of .isolated files referenced
931 one at a time by one item in 'includes'. A wide one has a large number of
932 'includes' in a single .isolated file. 'left' is defined as an included
933 .isolated file earlier in the 'includes' list. So the order of the elements
934 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000935 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000936 self.root = IsolatedFile(root_isolated_hash)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000937 cache.retrieve(Remote.HIGH, root_isolated_hash, UNKNOWN_FILE_SIZE)
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000938 pending = {root_isolated_hash: self.root}
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000939 # Keeps the list of retrieved items to refuse recursive includes.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000940 retrieved = [root_isolated_hash]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000941
942 def update_self(node):
943 node.fetch_files(cache, self.files)
944 # Grabs properties.
945 if not self.command and node.data.get('command'):
946 self.command = node.data['command']
947 if self.read_only is None and node.data.get('read_only') is not None:
948 self.read_only = node.data['read_only']
949 if (self.relative_cwd is None and
950 node.data.get('relative_cwd') is not None):
951 self.relative_cwd = node.data['relative_cwd']
952
953 def traverse_tree(node):
954 if node.can_fetch:
955 if not node.files_fetched:
956 update_self(node)
957 will_break = False
958 for i in node.children:
959 if not i.can_fetch:
960 if will_break:
961 break
962 # Automatically mark the first one as fetcheable.
963 i.can_fetch = True
964 will_break = True
965 traverse_tree(i)
966
967 while pending:
968 item_hash = cache.wait_for(pending)
969 item = pending.pop(item_hash)
970 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000971 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000972 # It's the root item.
973 item.can_fetch = True
974
975 for new_child in item.children:
976 h = new_child.obj_hash
977 if h in retrieved:
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000978 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000979 pending[h] = new_child
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000980 cache.retrieve(Remote.HIGH, h, UNKNOWN_FILE_SIZE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000981
982 # Traverse the whole tree to see if files can now be fetched.
983 traverse_tree(self.root)
984 def check(n):
985 return all(check(x) for x in n.children) and n.files_fetched
986 assert check(self.root)
987 self.relative_cwd = self.relative_cwd or ''
988 self.read_only = self.read_only or False
989
990
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000991def run_tha_test(isolated_hash, cache_dir, remote, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000992 """Downloads the dependencies in the cache, hardlinks them into a temporary
993 directory and runs the executable.
994 """
995 settings = Settings()
996 with Cache(cache_dir, Remote(remote), policies) as cache:
997 outdir = make_temp_dir('run_tha_test', cache_dir)
998 try:
999 # Initiate all the files download.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001000 with Profiler('GetIsolateds') as _prof:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001001 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001002 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001003 # Adds it in the cache. While not strictly necessary, this simplifies
1004 # the rest.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001005 h = hashlib.sha1(open(isolated_hash, 'r').read()).hexdigest()
1006 cache.add(isolated_hash, h)
1007 isolated_hash = h
1008 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001009
1010 if not settings.command:
1011 print >> sys.stderr, 'No command to run'
1012 return 1
1013
1014 with Profiler('GetRest') as _prof:
1015 logging.debug('Creating directories')
1016 # Creates the tree of directories to create.
1017 directories = set(os.path.dirname(f) for f in settings.files)
1018 for item in list(directories):
1019 while item:
1020 directories.add(item)
1021 item = os.path.dirname(item)
1022 for d in sorted(directories):
1023 if d:
1024 os.mkdir(os.path.join(outdir, d))
1025
1026 # Creates the links if necessary.
1027 for filepath, properties in settings.files.iteritems():
1028 if 'link' not in properties:
1029 continue
1030 outfile = os.path.join(outdir, filepath)
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +00001031 # symlink doesn't exist on Windows. So the 'link' property should
1032 # never be specified for windows .isolated file.
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001033 os.symlink(properties['l'], outfile) # pylint: disable=E1101
1034 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001035 # It's not set on Windows.
maruel@chromium.org96768a42012-10-31 18:49:18 +00001036 lchmod = getattr(os, 'lchmod', None)
1037 if lchmod:
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001038 lchmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001039
1040 # Remaining files to be processed.
1041 # Note that files could still be not be downloaded yet here.
1042 remaining = dict()
1043 for filepath, props in settings.files.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +00001044 if 'h' in props:
1045 remaining.setdefault(props['h'], []).append((filepath, props))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001046
1047 # Do bookkeeping while files are being downloaded in the background.
1048 cwd = os.path.join(outdir, settings.relative_cwd)
1049 if not os.path.isdir(cwd):
1050 os.makedirs(cwd)
1051 cmd = settings.command[:]
1052 # Ensure paths are correctly separated on windows.
1053 cmd[0] = cmd[0].replace('/', os.path.sep)
1054 cmd = fix_python_path(cmd)
1055
1056 # Now block on the remaining files to be downloaded and mapped.
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001057 logging.info('Retrieving remaining files')
1058 last_update = time.time()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001059 while remaining:
1060 obj = cache.wait_for(remaining)
1061 for filepath, properties in remaining.pop(obj):
1062 outfile = os.path.join(outdir, filepath)
1063 link_file(outfile, cache.path(obj), HARDLINK)
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001064 if 'm' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001065 # It's not set on Windows.
maruel@chromium.orgd02e8ed2012-11-21 20:30:14 +00001066 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001067
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001068 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
1069 logging.info('%d files remaining...' % len(remaining))
1070 last_update = time.time()
1071
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001072 if settings.read_only:
1073 make_writable(outdir, True)
1074 logging.info('Running %s, cwd=%s' % (cmd, cwd))
csharp@chromium.orge217f302012-11-22 16:51:53 +00001075
1076 # TODO(csharp): This should be specified somewhere else.
1077 # Add a rotating log file if one doesn't already exist.
1078 env = os.environ.copy()
1079 env.setdefault('RUN_TEST_CASES_LOG_FILE', RUN_TEST_CASES_LOG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001080 try:
1081 with Profiler('RunTest') as _prof:
csharp@chromium.orge217f302012-11-22 16:51:53 +00001082 return subprocess.call(cmd, cwd=cwd, env=env)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001083 except OSError:
1084 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1085 raise
1086 finally:
1087 rmtree(outdir)
1088
1089
1090def main():
1091 parser = optparse.OptionParser(
1092 usage='%prog <options>', description=sys.modules[__name__].__doc__)
1093 parser.add_option(
1094 '-v', '--verbose', action='count', default=0, help='Use multiple times')
1095 parser.add_option('--no-run', action='store_true', help='Skip the run part')
1096
1097 group = optparse.OptionGroup(parser, 'Data source')
1098 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001099 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001100 metavar='FILE',
1101 help='File/url describing what to map or run')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001102 # TODO(maruel): Remove once not used anymore.
1103 group.add_option(
1104 '-m', '--manifest', dest='isolated', help=optparse.SUPPRESS_HELP)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001105 group.add_option(
1106 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001107 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001108 parser.add_option_group(group)
1109
1110 group.add_option(
1111 '-r', '--remote', metavar='URL', help='Remote where to get the items')
1112 group = optparse.OptionGroup(parser, 'Cache management')
1113 group.add_option(
1114 '--cache',
1115 default='cache',
1116 metavar='DIR',
1117 help='Cache directory, default=%default')
1118 group.add_option(
1119 '--max-cache-size',
1120 type='int',
1121 metavar='NNN',
1122 default=20*1024*1024*1024,
1123 help='Trim if the cache gets larger than this value, default=%default')
1124 group.add_option(
1125 '--min-free-space',
1126 type='int',
1127 metavar='NNN',
1128 default=1*1024*1024*1024,
1129 help='Trim if disk free space becomes lower than this value, '
1130 'default=%default')
1131 group.add_option(
1132 '--max-items',
1133 type='int',
1134 metavar='NNN',
1135 default=100000,
1136 help='Trim if more than this number of items are in the cache '
1137 'default=%default')
1138 parser.add_option_group(group)
1139
1140 options, args = parser.parse_args()
1141 level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
csharp@chromium.orgff2a4662012-11-21 20:49:32 +00001142
1143 logging_console = logging.StreamHandler()
1144 logging_console.setFormatter(logging.Formatter(
1145 '%(levelname)5s %(module)15s(%(lineno)3d): %(message)s'))
1146 logging_console.setLevel(level)
1147 logging.getLogger().addHandler(logging_console)
1148
1149 logging_rotating_file = logging.handlers.RotatingFileHandler(
1150 RUN_ISOLATED_LOG_FILE,
1151 maxBytes=10 * 1024 * 1024, backupCount=5)
1152 logging_rotating_file.setLevel(logging.DEBUG)
1153 logging_rotating_file.setFormatter(logging.Formatter(
1154 '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)3d): %(message)s'))
1155 logging.getLogger().addHandler(logging_rotating_file)
1156
1157 logging.getLogger().setLevel(logging.DEBUG)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001158
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001159 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001160 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001161 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001162 if not options.remote:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001163 logging.debug('--remote is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001164 parser.error('--remote is required.')
1165 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001166 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001167 parser.error('Unsupported args %s' % ' '.join(args))
1168
1169 policies = CachePolicies(
1170 options.max_cache_size, options.min_free_space, options.max_items)
1171 try:
1172 return run_tha_test(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001173 options.isolated or options.hash,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001174 os.path.abspath(options.cache),
1175 options.remote,
1176 policies)
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001177 except Exception, e:
1178 # Make sure any exception is logged.
1179 logging.exception(e)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001180 return 1
1181
1182
1183if __name__ == '__main__':
1184 sys.exit(main())