blob: 2e2c18241f98792914b070175b7a4256d665eb7a [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
16import optparse
17import os
18import Queue
csharp@chromium.orgf13eec02013-03-11 18:22:56 +000019import random
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000020import re
21import shutil
22import stat
23import subprocess
24import sys
25import tempfile
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000026import time
csharp@chromium.orga92403f2012-11-20 15:13:59 +000027import zlib
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000028
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000029from third_party.depot_tools import fix_encoding
30
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000031from utils import lru
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000032from utils import net
vadimsh@chromium.orgb074b162013-08-22 17:55:46 +000033from utils import threading_utils
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000034from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000035from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000036
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000037
vadimsh@chromium.org85071062013-08-21 23:37:45 +000038# Absolute path to this file (can be None if running from zip on Mac).
39THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000040
41# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000042BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000043
44# Directory that contains currently running script file.
45MAIN_DIR = os.path.dirname(os.path.abspath(zip_package.get_main_script_path()))
46
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000047# Types of action accepted by link_file().
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000048HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY = range(1, 5)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000049
50RE_IS_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$')
51
csharp@chromium.org8dc52542012-11-08 20:29:55 +000052# The file size to be used when we don't know the correct file size,
53# generally used for .isolated files.
54UNKNOWN_FILE_SIZE = None
55
csharp@chromium.orga92403f2012-11-20 15:13:59 +000056# The size of each chunk to read when downloading and unzipping files.
57ZIPPED_FILE_CHUNK = 16 * 1024
58
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000059# The name of the log file to use.
60RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
61
csharp@chromium.orge217f302012-11-22 16:51:53 +000062# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000063RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000064
csharp@chromium.org9c59ff12012-12-12 02:32:29 +000065# The delay (in seconds) to wait between logging statements when retrieving
66# the required files. This is intended to let the user (or buildbot) know that
67# the program is still running.
68DELAY_BETWEEN_UPDATES_IN_SECS = 30
69
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +000070# Maximum expected delay (in seconds) between successive file fetches
71# in run_tha_test. If it takes longer than that, a deadlock might be happening
72# and all stack frames for all threads are dumped to log.
73DEADLOCK_TIMEOUT = 5 * 60
74
vadimsh@chromium.org80f73002013-07-12 14:52:44 +000075# Read timeout in seconds for downloads from isolate storage. If there's no
76# response from the server within this timeout whole download will be aborted.
77DOWNLOAD_READ_TIMEOUT = 60
78
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000079
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +000080# Used by get_flavor().
81FLAVOR_MAPPING = {
82 'cygwin': 'win',
83 'win32': 'win',
84 'darwin': 'mac',
85 'sunos5': 'solaris',
86 'freebsd7': 'freebsd',
87 'freebsd8': 'freebsd',
88}
89
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000090
91class ConfigError(ValueError):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +000092 """Generic failure to load a .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000093 pass
94
95
96class MappingError(OSError):
97 """Failed to recreate the tree."""
98 pass
99
100
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000101def get_as_zip_package(executable=True):
102 """Returns ZipPackage with this module and all its dependencies.
103
104 If |executable| is True will store run_isolated.py as __main__.py so that
105 zip package is directly executable be python.
106 """
107 # Building a zip package when running from another zip package is
108 # unsupported and probably unneeded.
109 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +0000110 assert THIS_FILE_PATH
111 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000112 package = zip_package.ZipPackage(root=BASE_DIR)
113 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
114 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
115 package.add_directory(os.path.join(BASE_DIR, 'utils'))
116 return package
117
118
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000119def get_flavor():
120 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
maruel@chromium.org9e9ceaa2013-04-05 15:42:42 +0000121 return FLAVOR_MAPPING.get(sys.platform, 'linux')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000122
123
124def os_link(source, link_name):
125 """Add support for os.link() on Windows."""
126 if sys.platform == 'win32':
127 if not ctypes.windll.kernel32.CreateHardLinkW(
128 unicode(link_name), unicode(source), 0):
129 raise OSError()
130 else:
131 os.link(source, link_name)
132
133
134def readable_copy(outfile, infile):
135 """Makes a copy of the file that is readable by everyone."""
csharp@chromium.org59d116d2013-07-05 18:04:08 +0000136 shutil.copy2(infile, outfile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000137 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
138 stat.S_IRGRP | stat.S_IROTH)
139 os.chmod(outfile, read_enabled_mode)
140
141
142def link_file(outfile, infile, action):
143 """Links a file. The type of link depends on |action|."""
144 logging.debug('Mapping %s to %s' % (infile, outfile))
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000145 if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000146 raise ValueError('Unknown mapping action %s' % action)
147 if not os.path.isfile(infile):
148 raise MappingError('%s is missing' % infile)
149 if os.path.isfile(outfile):
150 raise MappingError(
151 '%s already exist; insize:%d; outsize:%d' %
152 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
153
154 if action == COPY:
155 readable_copy(outfile, infile)
156 elif action == SYMLINK and sys.platform != 'win32':
157 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000158 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000159 else:
160 try:
161 os_link(infile, outfile)
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000162 except OSError as e:
163 if action == HARDLINK:
164 raise MappingError(
165 'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000166 # Probably a different file system.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000167 logging.warning(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000168 'Failed to hardlink, failing back to copy %s to %s' % (
169 infile, outfile))
170 readable_copy(outfile, infile)
171
172
173def _set_write_bit(path, read_only):
174 """Sets or resets the executable bit on a file or directory."""
175 mode = os.lstat(path).st_mode
176 if read_only:
177 mode = mode & 0500
178 else:
179 mode = mode | 0200
180 if hasattr(os, 'lchmod'):
181 os.lchmod(path, mode) # pylint: disable=E1101
182 else:
183 if stat.S_ISLNK(mode):
184 # Skip symlink without lchmod() support.
185 logging.debug('Can\'t change +w bit on symlink %s' % path)
186 return
187
188 # TODO(maruel): Implement proper DACL modification on Windows.
189 os.chmod(path, mode)
190
191
192def make_writable(root, read_only):
193 """Toggle the writable bit on a directory tree."""
csharp@chromium.org837352f2013-01-17 21:17:03 +0000194 assert os.path.isabs(root), root
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000195 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
196 for filename in filenames:
197 _set_write_bit(os.path.join(dirpath, filename), read_only)
198
199 for dirname in dirnames:
200 _set_write_bit(os.path.join(dirpath, dirname), read_only)
201
202
203def rmtree(root):
204 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
205 make_writable(root, False)
206 if sys.platform == 'win32':
207 for i in range(3):
208 try:
209 shutil.rmtree(root)
210 break
211 except WindowsError: # pylint: disable=E0602
212 delay = (i+1)*2
213 print >> sys.stderr, (
214 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
215 time.sleep(delay)
216 else:
217 shutil.rmtree(root)
218
219
220def is_same_filesystem(path1, path2):
221 """Returns True if both paths are on the same filesystem.
222
223 This is required to enable the use of hardlinks.
224 """
225 assert os.path.isabs(path1), path1
226 assert os.path.isabs(path2), path2
227 if sys.platform == 'win32':
228 # If the drive letter mismatches, assume it's a separate partition.
229 # TODO(maruel): It should look at the underlying drive, a drive letter could
230 # be a mount point to a directory on another drive.
231 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
232 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
233 if path1[0].lower() != path2[0].lower():
234 return False
235 return os.stat(path1).st_dev == os.stat(path2).st_dev
236
237
238def get_free_space(path):
239 """Returns the number of free bytes."""
240 if sys.platform == 'win32':
241 free_bytes = ctypes.c_ulonglong(0)
242 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
243 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
244 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000245 # For OSes other than Windows.
246 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000247 return f.f_bfree * f.f_frsize
248
249
250def make_temp_dir(prefix, root_dir):
251 """Returns a temporary directory on the same file system as root_dir."""
252 base_temp_dir = None
253 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
254 base_temp_dir = os.path.dirname(root_dir)
255 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
256
257
frankf@chromium.org3348ee02013-06-27 14:53:17 +0000258def load_isolated(content, os_flavor=None):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000259 """Verifies the .isolated file is valid and loads this object with the json
260 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000261 """
262 try:
263 data = json.loads(content)
264 except ValueError:
265 raise ConfigError('Failed to parse: %s...' % content[:100])
266
267 if not isinstance(data, dict):
268 raise ConfigError('Expected dict, got %r' % data)
269
270 for key, value in data.iteritems():
271 if key == 'command':
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 command')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000276 for subvalue in value:
277 if not isinstance(subvalue, basestring):
278 raise ConfigError('Expected string, got %r' % subvalue)
279
280 elif key == 'files':
281 if not isinstance(value, dict):
282 raise ConfigError('Expected dict, got %r' % value)
283 for subkey, subvalue in value.iteritems():
284 if not isinstance(subkey, basestring):
285 raise ConfigError('Expected string, got %r' % subkey)
286 if not isinstance(subvalue, dict):
287 raise ConfigError('Expected dict, got %r' % subvalue)
288 for subsubkey, subsubvalue in subvalue.iteritems():
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000289 if subsubkey == 'l':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000290 if not isinstance(subsubvalue, basestring):
291 raise ConfigError('Expected string, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000292 elif subsubkey == 'm':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000293 if not isinstance(subsubvalue, int):
294 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000295 elif subsubkey == 'h':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000296 if not RE_IS_SHA1.match(subsubvalue):
297 raise ConfigError('Expected sha-1, got %r' % subsubvalue)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000298 elif subsubkey == 's':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000299 if not isinstance(subsubvalue, int):
300 raise ConfigError('Expected int, got %r' % subsubvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000301 else:
302 raise ConfigError('Unknown subsubkey %s' % subsubkey)
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000303 if bool('h' in subvalue) and bool('l' in subvalue):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000304 raise ConfigError(
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000305 'Did not expect both \'h\' (sha-1) and \'l\' (link), got: %r' %
306 subvalue)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000307
308 elif key == 'includes':
309 if not isinstance(value, list):
310 raise ConfigError('Expected list, got %r' % value)
maruel@chromium.org89ad2db2012-12-12 14:29:22 +0000311 if not value:
312 raise ConfigError('Expected non-empty includes list')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000313 for subvalue in value:
314 if not RE_IS_SHA1.match(subvalue):
315 raise ConfigError('Expected sha-1, got %r' % subvalue)
316
317 elif key == 'read_only':
318 if not isinstance(value, bool):
319 raise ConfigError('Expected bool, got %r' % value)
320
321 elif key == 'relative_cwd':
322 if not isinstance(value, basestring):
323 raise ConfigError('Expected string, got %r' % value)
324
325 elif key == 'os':
frankf@chromium.org3348ee02013-06-27 14:53:17 +0000326 expected_value = os_flavor or get_flavor()
327 if value != expected_value:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000328 raise ConfigError(
329 'Expected \'os\' to be \'%s\' but got \'%s\'' %
frankf@chromium.org3348ee02013-06-27 14:53:17 +0000330 (expected_value, value))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000331
332 else:
333 raise ConfigError('Unknown key %s' % key)
334
335 return data
336
337
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000338def valid_file(filepath, size):
339 """Determines if the given files appears valid (currently it just checks
340 the file's size)."""
maruel@chromium.org770993b2012-12-11 17:16:48 +0000341 if size == UNKNOWN_FILE_SIZE:
342 return True
343 actual_size = os.stat(filepath).st_size
344 if size != actual_size:
345 logging.warning(
346 'Found invalid item %s; %d != %d',
347 os.path.basename(filepath), actual_size, size)
348 return False
349 return True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000350
351
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000352class IsolateServer(object):
353 """Client class to download or upload to Isolate Server."""
354 def __init__(self, base_url):
355 self.base_url = base_url
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000356
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000357 def retrieve(self, item, dest):
358 # TODO(maruel): Reuse HTTP connections. The stdlib doesn't make this
359 # easy.
360 try:
361 zipped_source = self.base_url + item
362 logging.debug('download_file(%s)', zipped_source)
363
364 # Because the app engine DB is only eventually consistent, retry
365 # 404 errors because the file might just not be visible yet (even
366 # though it has been uploaded).
367 connection = net.url_open(
368 zipped_source, retry_404=True,
369 read_timeout=DOWNLOAD_READ_TIMEOUT)
370 if not connection:
371 raise IOError('Unable to open connection to %s' % zipped_source)
372
373 content_length = connection.content_length
374 decompressor = zlib.decompressobj()
375 size = 0
376 with open(dest, 'wb') as f:
377 while True:
378 chunk = connection.read(ZIPPED_FILE_CHUNK)
379 if not chunk:
380 break
381 size += len(chunk)
382 f.write(decompressor.decompress(chunk))
383 # Ensure that all the data was properly decompressed.
384 uncompressed_data = decompressor.flush()
385 assert not uncompressed_data
386 except IOError as e:
387 logging.error('Failed to download %s at %s.\n%s', item, dest, e)
388 raise
389 except httplib.HTTPException as e:
390 msg = 'HTTPException while retrieving %s at %s.\n%s' % (item, dest, e)
391 logging.error(msg)
392 raise IOError(msg)
393 except zlib.error as e:
394 msg = 'Corrupted zlib for item %s. Processed %d of %s bytes.\n%s' % (
395 item, size, content_length, e)
396 logging.error(msg)
397
398 # Testing seems to show that if a few machines are trying to download
399 # the same blob, they can cause each other to fail. So if we hit a
400 # zip error, this is the most likely cause (it only downloads some of
401 # the data). Randomly sleep for between 5 and 25 seconds to try and
402 # spread out the downloads.
403 # TODO(csharp): Switch from blobstorage to cloud storage and see if
404 # that solves the issue.
405 sleep_duration = (random.random() * 20) + 5
406 time.sleep(sleep_duration)
407 raise IOError(msg)
408
409
410class FileSystem(object):
411 """Fetches data from the file system.
412
413 The common use case is a NFS/CIFS file server that is mounted locally that is
414 used to fetch the file on a local partition.
415 """
416 def __init__(self, base_path):
417 self.base_path = base_path
418
419 def retrieve(self, item, dest):
420 source = os.path.join(self.base_path, item)
421 if source == dest:
422 logging.info('Source and destination are the same, no action required')
423 return
424 logging.debug('copy_file(%s, %s)', source, dest)
425 shutil.copy(source, dest)
426
427
428def get_storage_api(file_or_url):
429 """Returns an object that implements .retrieve()."""
430 if re.match(r'^https?://.+$', file_or_url):
431 return IsolateServer(file_or_url)
432 else:
433 return FileSystem(file_or_url)
434
435
436class RemoteOperation(object):
437 """Priority based worker queue to operate on action items.
438
439 It execute a function with the given task items. It is specialized to download
440 files.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000441
442 When the priority of items is equals, works in strict FIFO mode.
443 """
444 # Initial and maximum number of worker threads.
445 INITIAL_WORKERS = 2
446 MAX_WORKERS = 16
447 # Priorities.
448 LOW, MED, HIGH = (1<<8, 2<<8, 3<<8)
449 INTERNAL_PRIORITY_BITS = (1<<8) - 1
450 RETRIES = 5
451
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000452 def __init__(self, do_item):
453 # Function to fetch a remote object or upload to a remote location.
454 self._do_item = do_item
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000455 # Contains tuple(priority, obj).
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000456 self._done = Queue.PriorityQueue()
vadimsh@chromium.orgb074b162013-08-22 17:55:46 +0000457 self._pool = threading_utils.ThreadPool(
458 self.INITIAL_WORKERS, self.MAX_WORKERS, 0, 'remote')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000459
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000460 def join(self):
461 """Blocks until the queue is empty."""
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000462 return self._pool.join()
maruel@chromium.orgfb155e92012-09-28 20:36:54 +0000463
vadimsh@chromium.org53f8d5a2013-06-19 13:03:55 +0000464 def close(self):
465 """Terminates all worker threads."""
466 self._pool.close()
467
csharp@chromium.orgdf2968f2012-11-16 20:25:37 +0000468 def add_item(self, priority, obj, dest, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000469 """Retrieves an object from the remote data store.
470
471 The smaller |priority| gets fetched first.
472
473 Thread-safe.
474 """
475 assert (priority & self.INTERNAL_PRIORITY_BITS) == 0
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000476 return self._add_item(priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000477
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000478 def _add_item(self, priority, obj, dest, size):
479 assert isinstance(obj, basestring), obj
480 assert isinstance(dest, basestring), dest
481 assert size is None or isinstance(size, int), size
482 return self._pool.add_task(
483 priority, self._task_executer, priority, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000484
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000485 def get_one_result(self):
486 return self._pool.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000487
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000488 def _task_executer(self, priority, obj, dest, size):
489 """Wraps self._do_item to trap and retry on IOError exceptions."""
490 try:
491 self._do_item(obj, dest)
492 if size and not valid_file(dest, size):
493 download_size = os.stat(dest).st_size
494 os.remove(dest)
495 raise IOError('File incorrect size after download of %s. Got %s and '
496 'expected %s' % (obj, download_size, size))
497 # TODO(maruel): Technically, we'd want to have an output queue to be a
498 # PriorityQueue.
499 return obj
500 except IOError as e:
501 logging.debug('Caught IOError: %s', e)
vadimsh@chromium.org80f73002013-07-12 14:52:44 +0000502 # Remove unfinished download.
503 if os.path.exists(dest):
504 os.remove(dest)
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000505 # Retry a few times, lowering the priority.
506 if (priority & self.INTERNAL_PRIORITY_BITS) < self.RETRIES:
507 self._add_item(priority + 1, obj, dest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000508 return
maruel@chromium.org13eca0b2013-01-22 16:42:21 +0000509 raise
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000510
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000511
512class CachePolicies(object):
513 def __init__(self, max_cache_size, min_free_space, max_items):
514 """
515 Arguments:
516 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
517 cache is effectively a leak.
518 - min_free_space: Trim if disk free space becomes lower than this value. If
519 0, it unconditionally fill the disk.
520 - max_items: Maximum number of items to keep in the cache. If 0, do not
521 enforce a limit.
522 """
523 self.max_cache_size = max_cache_size
524 self.min_free_space = min_free_space
525 self.max_items = max_items
526
527
528class Cache(object):
529 """Stateful LRU cache.
530
531 Saves its state as json file.
532 """
533 STATE_FILE = 'state.json'
534
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000535 def __init__(self, cache_dir, remote_fetcher, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000536 """
537 Arguments:
538 - cache_dir: Directory where to place the cache.
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000539 - remote_fetcher: RemoteOperation where to fetch items from.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000540 - policies: cache retention policies.
541 """
542 self.cache_dir = cache_dir
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000543 self.remote_fetcher = remote_fetcher
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000544 self.policies = policies
545 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000546 self.lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000547
548 # Items currently being fetched. Keep it local to reduce lock contention.
549 self._pending_queue = set()
550
551 # Profiling values.
552 self._added = []
553 self._removed = []
554 self._free_disk = 0
555
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000556 with tools.Profiler('Setup'):
maruel@chromium.org770993b2012-12-11 17:16:48 +0000557 if not os.path.isdir(self.cache_dir):
558 os.makedirs(self.cache_dir)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000559
560 # Load state of the cache.
vadimsh@chromium.orga40428e2013-07-04 15:43:14 +0000561 if os.path.isfile(self.state_file):
562 try:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000563 self.lru = lru.LRUDict.load(self.state_file)
564 except ValueError as err:
565 logging.error('Failed to load cache state: %s' % (err,))
566 # Don't want to keep broken state file.
567 os.remove(self.state_file)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000568
maruel@chromium.org770993b2012-12-11 17:16:48 +0000569 # Ensure that all files listed in the state still exist and add new ones.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000570 previous = self.lru.keys_set()
571 unknown = []
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000572 for filename in os.listdir(self.cache_dir):
573 if filename == self.STATE_FILE:
574 continue
575 if filename in previous:
576 previous.remove(filename)
577 continue
578 # An untracked file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000579 if not RE_IS_SHA1.match(filename):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000580 logging.warning('Removing unknown file %s from cache', filename)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000581 os.remove(self.path(filename))
maruel@chromium.org770993b2012-12-11 17:16:48 +0000582 continue
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000583 # File that's not referenced in 'state.json'.
584 # TODO(vadimsh): Verify its SHA1 matches file name.
585 logging.warning('Adding unknown file %s to cache', filename)
586 unknown.append(filename)
maruel@chromium.org770993b2012-12-11 17:16:48 +0000587
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000588 if unknown:
589 # Add as oldest files. They will be deleted eventually if not accessed.
590 self._add_oldest_list(unknown)
591 logging.warning('Added back %d unknown files', len(unknown))
592
maruel@chromium.org770993b2012-12-11 17:16:48 +0000593 if previous:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000594 # Filter out entries that were not found.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000595 logging.warning('Removed %d lost files', len(previous))
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000596 for filename in previous:
597 self.lru.pop(filename)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000598 self.trim()
599
600 def __enter__(self):
601 return self
602
603 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000604 with tools.Profiler('CleanupTrimming'):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000605 self.trim()
606
607 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000608 '%5d (%8dkb) added', len(self._added), sum(self._added) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000609 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000610 '%5d (%8dkb) current',
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000611 len(self.lru),
612 sum(self.lru.itervalues()) / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000613 logging.info(
maruel@chromium.org5fd6f472012-12-11 00:26:08 +0000614 '%5d (%8dkb) removed', len(self._removed), sum(self._removed) / 1024)
615 logging.info(' %8dkb free', self._free_disk / 1024)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000616
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000617 def remove_lru_file(self):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000618 """Removes the last recently used file and returns its size."""
619 item, size = self.lru.pop_oldest()
620 self._delete_file(item, size)
621 return size
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000622
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000623 def trim(self):
624 """Trims anything we don't know, make sure enough free space exists."""
625 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000626 if self.policies.max_cache_size:
627 total_size = sum(self.lru.itervalues())
628 while total_size > self.policies.max_cache_size:
629 total_size -= self.remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000630
631 # Ensure maximum number of items in the cache.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000632 if self.policies.max_items and len(self.lru) > self.policies.max_items:
633 for _ in xrange(len(self.lru) - self.policies.max_items):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000634 self.remove_lru_file()
635
636 # Ensure enough free space.
637 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000638 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000639 while (
640 self.policies.min_free_space and
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000641 self.lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000642 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000643 trimmed_due_to_space = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000644 self.remove_lru_file()
645 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000646 if trimmed_due_to_space:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000647 total = sum(self.lru.itervalues())
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000648 logging.warning(
649 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
650 'cache (%.1f%% of its maximum capacity)',
651 self._free_disk / 1024.,
652 total / 1024.,
653 100. * self.policies.max_cache_size / float(total),
654 )
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000655 self.save()
656
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000657 def retrieve(self, priority, item, size):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000658 """Retrieves a file from the remote, if not already cached, and adds it to
659 the cache.
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000660
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000661 If the file is in the cache, verify that the file is valid (i.e. it is
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000662 the correct size), retrieving it again if it isn't.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000663 """
664 assert not '/' in item
665 path = self.path(item)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000666 found = False
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000667
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000668 if item in self.lru:
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000669 if not valid_file(self.path(item), size):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000670 self.lru.pop(item)
671 self._delete_file(item, size)
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000672 else:
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000673 # Was already in cache. Update it's LRU value by putting it at the end.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000674 self.lru.touch(item)
675 found = True
csharp@chromium.org8dc52542012-11-08 20:29:55 +0000676
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000677 if not found:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000678 if item in self._pending_queue:
679 # Already pending. The same object could be referenced multiple times.
680 return
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000681 # TODO(maruel): It should look at the free disk space, the current cache
682 # size and the size of the new item on every new item:
683 # - Trim the cache as more entries are listed when free disk space is low,
684 # otherwise if the amount of data downloaded during the run > free disk
685 # space, it'll crash.
686 # - Make sure there's enough free disk space to fit all dependencies of
687 # this run! If not, abort early.
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000688 self.remote_fetcher.add_item(priority, item, path, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000689 self._pending_queue.add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000690
691 def add(self, filepath, obj):
692 """Forcibly adds a file to the cache."""
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000693 if obj not in self.lru:
694 link_file(self.path(obj), filepath, HARDLINK)
695 self._add(obj)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000696
697 def path(self, item):
698 """Returns the path to one item."""
699 return os.path.join(self.cache_dir, item)
700
701 def save(self):
702 """Saves the LRU ordering."""
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000703 self.lru.save(self.state_file)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000704
705 def wait_for(self, items):
706 """Starts a loop that waits for at least one of |items| to be retrieved.
707
708 Returns the first item retrieved.
709 """
710 # Flush items already present.
711 for item in items:
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000712 if item in self.lru:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000713 return item
714
715 assert all(i in self._pending_queue for i in items), (
716 items, self._pending_queue)
717 # Note that:
718 # len(self._pending_queue) ==
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000719 # ( len(self.remote_fetcher._workers) - self.remote_fetcher._ready +
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000720 # len(self._remote._queue) + len(self._remote.done))
721 # There is no lock-free way to verify that.
722 while self._pending_queue:
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000723 item = self.remote_fetcher.get_one_result()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000724 self._pending_queue.remove(item)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000725 self._add(item)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000726 if item in items:
727 return item
728
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000729 def _add(self, item):
730 """Adds an item into LRU cache marking it as a newest one."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000731 size = os.stat(self.path(item)).st_size
732 self._added.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000733 self.lru.add(item, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000734
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000735 def _add_oldest_list(self, items):
736 """Adds a bunch of items into LRU cache marking them as oldest ones."""
737 pairs = []
738 for item in items:
739 size = os.stat(self.path(item)).st_size
740 self._added.append(size)
741 pairs.append((item, size))
742 self.lru.batch_insert_oldest(pairs)
743
744 def _delete_file(self, item, size):
745 """Deletes cache file from the file system."""
746 self._removed.append(size)
747 try:
748 os.remove(self.path(item))
749 except OSError as e:
750 logging.error('Error attempting to delete a file\n%s' % e)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000751
752
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000753class IsolatedFile(object):
754 """Represents a single parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000755 def __init__(self, obj_hash):
756 """|obj_hash| is really the sha-1 of the file."""
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000757 logging.debug('IsolatedFile(%s)' % obj_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000758 self.obj_hash = obj_hash
759 # Set once all the left-side of the tree is parsed. 'Tree' here means the
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000760 # .isolate and all the .isolated files recursively included by it with
761 # 'includes' key. The order of each sha-1 in 'includes', each representing a
762 # .isolated file in the hash table, is important, as the later ones are not
763 # processed until the firsts are retrieved and read.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000764 self.can_fetch = False
765
766 # Raw data.
767 self.data = {}
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000768 # A IsolatedFile instance, one per object in self.includes.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000769 self.children = []
770
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000771 # Set once the .isolated file is loaded.
772 self._is_parsed = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000773 # Set once the files are fetched.
774 self.files_fetched = False
775
776 def load(self, content):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000777 """Verifies the .isolated file is valid and loads this object with the json
778 data.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000779 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000780 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
781 assert not self._is_parsed
782 self.data = load_isolated(content)
783 self.children = [IsolatedFile(i) for i in self.data.get('includes', [])]
784 self._is_parsed = True
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000785
786 def fetch_files(self, cache, files):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000787 """Adds files in this .isolated file not present in |files| dictionary.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000788
789 Preemptively request files.
790
791 Note that |files| is modified by this function.
792 """
793 assert self.can_fetch
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000794 if not self._is_parsed or self.files_fetched:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000795 return
796 logging.debug('fetch_files(%s)' % self.obj_hash)
797 for filepath, properties in self.data.get('files', {}).iteritems():
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000798 # Root isolated has priority on the files being mapped. In particular,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000799 # overriden files must not be fetched.
800 if filepath not in files:
801 files[filepath] = properties
maruel@chromium.orge5c17132012-11-21 18:18:46 +0000802 if 'h' in properties:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000803 # Preemptively request files.
804 logging.debug('fetching %s' % filepath)
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000805 cache.retrieve(RemoteOperation.MED, properties['h'], properties['s'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000806 self.files_fetched = True
807
808
809class Settings(object):
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000810 """Results of a completely parsed .isolated file."""
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000811 def __init__(self):
812 self.command = []
813 self.files = {}
814 self.read_only = None
815 self.relative_cwd = None
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000816 # The main .isolated file, a IsolatedFile instance.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000817 self.root = None
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000818
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000819 def load(self, cache, root_isolated_hash):
820 """Loads the .isolated and all the included .isolated asynchronously.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000821
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000822 It enables support for "included" .isolated files. They are processed in
823 strict order but fetched asynchronously from the cache. This is important so
824 that a file in an included .isolated file that is overridden by an embedding
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000825 .isolated file is not fetched needlessly. The includes are fetched in one
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000826 pass and the files are fetched as soon as all the ones on the left-side
827 of the tree were fetched.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000828
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000829 The prioritization is very important here for nested .isolated files.
830 'includes' have the highest priority and the algorithm is optimized for both
831 deep and wide trees. A deep one is a long link of .isolated files referenced
832 one at a time by one item in 'includes'. A wide one has a large number of
833 'includes' in a single .isolated file. 'left' is defined as an included
834 .isolated file earlier in the 'includes' list. So the order of the elements
835 in 'includes' is important.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000836 """
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000837 self.root = IsolatedFile(root_isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000838
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000839 # Isolated files being retrieved now: hash -> IsolatedFile instance.
840 pending = {}
841 # Set of hashes of already retrieved items to refuse recursive includes.
842 seen = set()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000843
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000844 def retrieve(isolated_file):
845 h = isolated_file.obj_hash
846 if h in seen:
847 raise ConfigError('IsolatedFile %s is retrieved recursively' % h)
848 assert h not in pending
849 seen.add(h)
850 pending[h] = isolated_file
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000851 cache.retrieve(RemoteOperation.HIGH, h, UNKNOWN_FILE_SIZE)
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000852
853 retrieve(self.root)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000854
855 while pending:
856 item_hash = cache.wait_for(pending)
857 item = pending.pop(item_hash)
858 item.load(open(cache.path(item_hash), 'r').read())
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000859 if item_hash == root_isolated_hash:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000860 # It's the root item.
861 item.can_fetch = True
862
863 for new_child in item.children:
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000864 retrieve(new_child)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000865
866 # Traverse the whole tree to see if files can now be fetched.
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000867 self._traverse_tree(cache, self.root)
868
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000869 def check(n):
870 return all(check(x) for x in n.children) and n.files_fetched
871 assert check(self.root)
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000872
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000873 self.relative_cwd = self.relative_cwd or ''
874 self.read_only = self.read_only or False
875
vadimsh@chromium.orgf4c063e2013-07-04 14:23:31 +0000876 def _traverse_tree(self, cache, node):
877 if node.can_fetch:
878 if not node.files_fetched:
879 self._update_self(cache, node)
880 will_break = False
881 for i in node.children:
882 if not i.can_fetch:
883 if will_break:
884 break
885 # Automatically mark the first one as fetcheable.
886 i.can_fetch = True
887 will_break = True
888 self._traverse_tree(cache, i)
889
890 def _update_self(self, cache, node):
891 node.fetch_files(cache, self.files)
892 # Grabs properties.
893 if not self.command and node.data.get('command'):
894 self.command = node.data['command']
895 if self.read_only is None and node.data.get('read_only') is not None:
896 self.read_only = node.data['read_only']
897 if (self.relative_cwd is None and
898 node.data.get('relative_cwd') is not None):
899 self.relative_cwd = node.data['relative_cwd']
900
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000901
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000902def create_directories(base_directory, files):
903 """Creates the directory structure needed by the given list of files."""
904 logging.debug('create_directories(%s, %d)', base_directory, len(files))
905 # Creates the tree of directories to create.
906 directories = set(os.path.dirname(f) for f in files)
907 for item in list(directories):
908 while item:
909 directories.add(item)
910 item = os.path.dirname(item)
911 for d in sorted(directories):
912 if d:
913 os.mkdir(os.path.join(base_directory, d))
914
915
916def create_links(base_directory, files):
917 """Creates any links needed by the given set of files."""
918 for filepath, properties in files:
csharp@chromium.org89eaf082013-03-26 18:56:21 +0000919 if 'l' not in properties:
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000920 continue
maruel@chromium.org3320ee12013-03-28 13:23:31 +0000921 if sys.platform == 'win32':
922 # TODO(maruel): Create junctions or empty text files similar to what
923 # cygwin do?
924 logging.warning('Ignoring symlink %s', filepath)
925 continue
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000926 outfile = os.path.join(base_directory, filepath)
927 # symlink doesn't exist on Windows. So the 'link' property should
928 # never be specified for windows .isolated file.
929 os.symlink(properties['l'], outfile) # pylint: disable=E1101
930 if 'm' in properties:
931 lchmod = getattr(os, 'lchmod', None)
932 if lchmod:
933 lchmod(outfile, properties['m'])
934
935
936def setup_commands(base_directory, cwd, cmd):
937 """Correctly adjusts and then returns the required working directory
938 and command needed to run the test.
939 """
940 assert not os.path.isabs(cwd), 'The cwd must be a relative path, got %s' % cwd
941 cwd = os.path.join(base_directory, cwd)
942 if not os.path.isdir(cwd):
943 os.makedirs(cwd)
944
945 # Ensure paths are correctly separated on windows.
946 cmd[0] = cmd[0].replace('/', os.path.sep)
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000947 cmd = tools.fix_python_path(cmd)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000948
949 return cwd, cmd
950
951
952def generate_remaining_files(files):
953 """Generates a dictionary of all the remaining files to be downloaded."""
954 remaining = {}
955 for filepath, props in files:
956 if 'h' in props:
957 remaining.setdefault(props['h'], []).append((filepath, props))
958
959 return remaining
960
961
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000962def run_tha_test(isolated_hash, cache_dir, retriever, policies):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000963 """Downloads the dependencies in the cache, hardlinks them into a temporary
964 directory and runs the executable.
965 """
966 settings = Settings()
maruel@chromium.org3e42ce82013-09-12 18:36:59 +0000967 with Cache(cache_dir, RemoteOperation(retriever), policies) as cache:
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000968 outdir = make_temp_dir('run_tha_test', cache_dir)
969 try:
970 # Initiate all the files download.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000971 with tools.Profiler('GetIsolateds'):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000972 # Optionally support local files.
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000973 if not RE_IS_SHA1.match(isolated_hash):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000974 # Adds it in the cache. While not strictly necessary, this simplifies
975 # the rest.
maruel@chromium.orgcb3c3d52013-03-14 18:55:30 +0000976 h = hashlib.sha1(open(isolated_hash, 'rb').read()).hexdigest()
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000977 cache.add(isolated_hash, h)
978 isolated_hash = h
979 settings.load(cache, isolated_hash)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000980
981 if not settings.command:
982 print >> sys.stderr, 'No command to run'
983 return 1
984
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000985 with tools.Profiler('GetRest'):
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000986 create_directories(outdir, settings.files)
987 create_links(outdir, settings.files.iteritems())
988 remaining = generate_remaining_files(settings.files.iteritems())
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000989
990 # Do bookkeeping while files are being downloaded in the background.
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000991 cwd, cmd = setup_commands(outdir, settings.relative_cwd,
992 settings.command[:])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000993
994 # Now block on the remaining files to be downloaded and mapped.
csharp@chromium.org9c59ff12012-12-12 02:32:29 +0000995 logging.info('Retrieving remaining files')
996 last_update = time.time()
vadimsh@chromium.orgb074b162013-08-22 17:55:46 +0000997 with threading_utils.DeadlockDetector(DEADLOCK_TIMEOUT) as detector:
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +0000998 while remaining:
999 detector.ping()
1000 obj = cache.wait_for(remaining)
1001 for filepath, properties in remaining.pop(obj):
1002 outfile = os.path.join(outdir, filepath)
maruel@chromium.orgb7c003d2013-07-24 13:04:30 +00001003 link_file(outfile, cache.path(obj), HARDLINK)
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +00001004 if 'm' in properties:
1005 # It's not set on Windows.
1006 os.chmod(outfile, properties['m'])
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001007
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +00001008 if time.time() - last_update > DELAY_BETWEEN_UPDATES_IN_SECS:
1009 msg = '%d files remaining...' % len(remaining)
1010 print msg
1011 logging.info(msg)
1012 last_update = time.time()
csharp@chromium.org9c59ff12012-12-12 02:32:29 +00001013
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001014 if settings.read_only:
vadimsh@chromium.org5db0f4f2013-07-04 13:57:02 +00001015 logging.info('Making files read only')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001016 make_writable(outdir, True)
1017 logging.info('Running %s, cwd=%s' % (cmd, cwd))
csharp@chromium.orge217f302012-11-22 16:51:53 +00001018
1019 # TODO(csharp): This should be specified somewhere else.
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +00001020 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
csharp@chromium.orge217f302012-11-22 16:51:53 +00001021 # Add a rotating log file if one doesn't already exist.
1022 env = os.environ.copy()
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +00001023 env.setdefault('RUN_TEST_CASES_LOG_FILE',
1024 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001025 try:
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001026 with tools.Profiler('RunTest'):
csharp@chromium.orge217f302012-11-22 16:51:53 +00001027 return subprocess.call(cmd, cwd=cwd, env=env)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001028 except OSError:
1029 print >> sys.stderr, 'Failed to run %s; cwd=%s' % (cmd, cwd)
1030 raise
1031 finally:
1032 rmtree(outdir)
1033
1034
1035def main():
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001036 tools.disable_buffering()
1037 parser = tools.OptionParserWithLogging(
maruel@chromium.orgea101982013-07-24 15:54:29 +00001038 usage='%prog <options>', log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001039
1040 group = optparse.OptionGroup(parser, 'Data source')
1041 group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001042 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001043 metavar='FILE',
1044 help='File/url describing what to map or run')
1045 group.add_option(
1046 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001047 help='Hash of the .isolated to grab from the hash table')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001048 parser.add_option_group(group)
1049
1050 group.add_option(
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001051 '-r', '--remote', metavar='URL',
1052 default=
1053 'https://isolateserver.appspot.com/content/retrieve/default-gzip/',
1054 help='Remote where to get the items. Defaults to %default')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001055 group = optparse.OptionGroup(parser, 'Cache management')
1056 group.add_option(
1057 '--cache',
1058 default='cache',
1059 metavar='DIR',
1060 help='Cache directory, default=%default')
1061 group.add_option(
1062 '--max-cache-size',
1063 type='int',
1064 metavar='NNN',
1065 default=20*1024*1024*1024,
1066 help='Trim if the cache gets larger than this value, default=%default')
1067 group.add_option(
1068 '--min-free-space',
1069 type='int',
1070 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +00001071 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001072 help='Trim if disk free space becomes lower than this value, '
1073 'default=%default')
1074 group.add_option(
1075 '--max-items',
1076 type='int',
1077 metavar='NNN',
1078 default=100000,
1079 help='Trim if more than this number of items are in the cache '
1080 'default=%default')
1081 parser.add_option_group(group)
1082
1083 options, args = parser.parse_args()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001084
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001085 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001086 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +00001087 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001088 if args:
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +00001089 logging.debug('Unsupported args %s' % ' '.join(args))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001090 parser.error('Unsupported args %s' % ' '.join(args))
1091
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001092 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001093 policies = CachePolicies(
1094 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +00001095
maruel@chromium.org3e42ce82013-09-12 18:36:59 +00001096 retriever = get_storage_api(options.remote)
1097 try:
1098 return run_tha_test(
1099 options.isolated or options.hash,
1100 options.cache,
1101 retriever.retrieve,
1102 policies)
1103 except Exception, e:
1104 # Make sure any exception is logged.
1105 logging.exception(e)
1106 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001107
1108
1109if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001110 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001111 fix_encoding.fix_encoding()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001112 sys.exit(main())