| Index: tools/isolate/tree_creator.py
|
| diff --git a/tools/isolate/tree_creator.py b/tools/isolate/tree_creator.py
|
| old mode 100644
|
| new mode 100755
|
| index ad0990cc63d6d7ec0c5705924e34db4d3ab7e8aa..074f7214c9605ba07eef322cfb73d316d6de74bc
|
| --- a/tools/isolate/tree_creator.py
|
| +++ b/tools/isolate/tree_creator.py
|
| @@ -1,21 +1,25 @@
|
| +#!/usr/bin/env python
|
| # Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| # Use of this source code is governed by a BSD-style license that can be
|
| # found in the LICENSE file.
|
|
|
| -"""File related utility functions.
|
| +"""Reads a manifest, creates a tree of hardlinks and runs the test.
|
|
|
| -Creates a tree of hardlinks, symlinks or copy the inputs files. Calculate files
|
| -hash.
|
| +Keeps a local cache.
|
| """
|
|
|
| import ctypes
|
| -import hashlib
|
| +import json
|
| import logging
|
| +import optparse
|
| import os
|
| +import re
|
| import shutil
|
| -import stat
|
| +import subprocess
|
| import sys
|
| +import tempfile
|
| import time
|
| +import urllib
|
|
|
|
|
| # Types of action accepted by recreate_tree().
|
| @@ -37,77 +41,20 @@ def os_link(source, link_name):
|
| os.link(source, link_name)
|
|
|
|
|
| -def expand_directories(indir, infiles, blacklist):
|
| - """Expands the directories, applies the blacklist and verifies files exist."""
|
| - logging.debug('expand_directories(%s, %s, %s)' % (indir, infiles, blacklist))
|
| - outfiles = []
|
| - for relfile in infiles:
|
| - if os.path.isabs(relfile):
|
| - raise MappingError('Can\'t map absolute path %s' % relfile)
|
| - infile = os.path.normpath(os.path.join(indir, relfile))
|
| - if not infile.startswith(indir):
|
| - raise MappingError('Can\'t map file %s outside %s' % (infile, indir))
|
| -
|
| - if relfile.endswith('/'):
|
| - if not os.path.isdir(infile):
|
| - raise MappingError(
|
| - 'Input directory %s must have a trailing slash' % infile)
|
| - for dirpath, dirnames, filenames in os.walk(infile):
|
| - # Convert the absolute path to subdir + relative subdirectory.
|
| - relpath = dirpath[len(indir)+1:]
|
| - outfiles.extend(os.path.join(relpath, f) for f in filenames)
|
| - for index, dirname in enumerate(dirnames):
|
| - # Do not process blacklisted directories.
|
| - if blacklist(os.path.join(relpath, dirname)):
|
| - del dirnames[index]
|
| - else:
|
| - if not os.path.isfile(infile):
|
| - raise MappingError('Input file %s doesn\'t exist' % infile)
|
| - outfiles.append(relfile)
|
| - return outfiles
|
| -
|
| -
|
| -def process_inputs(indir, infiles, need_hash, read_only):
|
| - """Returns a dictionary of input files, populated with the files' mode and
|
| - hash.
|
| -
|
| - The file mode is manipulated if read_only is True. In practice, we only save
|
| - one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r).
|
| - """
|
| - outdict = {}
|
| - for infile in infiles:
|
| - filepath = os.path.join(indir, infile)
|
| - filemode = stat.S_IMODE(os.stat(filepath).st_mode)
|
| - # Remove write access for non-owner.
|
| - filemode &= ~(stat.S_IWGRP | stat.S_IWOTH)
|
| - if read_only:
|
| - filemode &= ~stat.S_IWUSR
|
| - if filemode & stat.S_IXUSR:
|
| - filemode |= (stat.S_IXGRP | stat.S_IXOTH)
|
| - else:
|
| - filemode &= ~(stat.S_IXGRP | stat.S_IXOTH)
|
| - outdict[infile] = {
|
| - 'mode': filemode,
|
| - }
|
| - if need_hash:
|
| - h = hashlib.sha1()
|
| - with open(filepath, 'rb') as f:
|
| - h.update(f.read())
|
| - outdict[infile]['sha-1'] = h.hexdigest()
|
| - return outdict
|
| -
|
| -
|
| def link_file(outfile, infile, action):
|
| """Links a file. The type of link depends on |action|."""
|
| logging.debug('Mapping %s to %s' % (infile, outfile))
|
| + if action not in (HARDLINK, SYMLINK, COPY):
|
| + raise ValueError('Unknown mapping action %s' % action)
|
| if os.path.isfile(outfile):
|
| raise MappingError('%s already exist' % outfile)
|
|
|
| if action == COPY:
|
| shutil.copy(infile, outfile)
|
| elif action == SYMLINK and sys.platform != 'win32':
|
| + # On windows, symlink are converted to hardlink and fails over to copy.
|
| os.symlink(infile, outfile)
|
| - elif action == HARDLINK:
|
| + else:
|
| try:
|
| os_link(infile, outfile)
|
| except OSError:
|
| @@ -116,38 +63,6 @@ def link_file(outfile, infile, action):
|
| 'Failed to hardlink, failing back to copy %s to %s' % (
|
| infile, outfile))
|
| shutil.copy(infile, outfile)
|
| - else:
|
| - raise ValueError('Unknown mapping action %s' % action)
|
| -
|
| -
|
| -def recreate_tree(outdir, indir, infiles, action):
|
| - """Creates a new tree with only the input files in it.
|
| -
|
| - Arguments:
|
| - outdir: Output directory to create the files in.
|
| - indir: Root directory the infiles are based in.
|
| - infiles: List of files to map from |indir| to |outdir|.
|
| - action: See assert below.
|
| - """
|
| - logging.debug(
|
| - 'recreate_tree(%s, %s, %s, %s)' % (outdir, indir, infiles, action))
|
| - logging.info('Mapping from %s to %s' % (indir, outdir))
|
| -
|
| - assert action in (HARDLINK, SYMLINK, COPY)
|
| - outdir = os.path.normpath(outdir)
|
| - if not os.path.isdir(outdir):
|
| - logging.info ('Creating %s' % outdir)
|
| - os.makedirs(outdir)
|
| - # Do not call abspath until the directory exists.
|
| - outdir = os.path.abspath(outdir)
|
| -
|
| - for relfile in infiles:
|
| - infile = os.path.join(indir, relfile)
|
| - outfile = os.path.join(outdir, relfile)
|
| - outsubdir = os.path.dirname(outfile)
|
| - if not os.path.isdir(outsubdir):
|
| - os.makedirs(outsubdir)
|
| - link_file(outfile, infile, action)
|
|
|
|
|
| def _set_write_bit(path, read_only):
|
| @@ -177,6 +92,7 @@ def make_writable(root, read_only):
|
|
|
| def rmtree(root):
|
| """Wrapper around shutil.rmtree() to retry automatically on Windows."""
|
| + make_writable(root, False)
|
| if sys.platform == 'win32':
|
| for i in range(3):
|
| try:
|
| @@ -189,3 +105,206 @@ def rmtree(root):
|
| time.sleep(delay)
|
| else:
|
| shutil.rmtree(root)
|
| +
|
| +
|
| +def open_remote(file_or_url):
|
| + """Reads a file or url."""
|
| + if re.match(r'^https?://.+$', file_or_url):
|
| + return urllib.urlopen(file_or_url)
|
| + return open(file_or_url, 'rb')
|
| +
|
| +
|
| +def download_or_copy(file_or_url, dest):
|
| + """Copies a file or download an url."""
|
| + if re.match(r'^https?://.+$', file_or_url):
|
| + urllib.urlretrieve(file_or_url, dest)
|
| + else:
|
| + shutil.copy(file_or_url, dest)
|
| +
|
| +
|
| +def get_free_space(path):
|
| + """Returns the number of free bytes."""
|
| + if sys.platform == 'win32':
|
| + free_bytes = ctypes.c_ulonglong(0)
|
| + ctypes.windll.kernel32.GetDiskFreeSpaceExW(
|
| + ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
|
| + return free_bytes.value
|
| + f = os.statvfs(path)
|
| + return f.f_bfree * f.f_frsize
|
| +
|
| +
|
| +class Cache(object):
|
| + """Stateful LRU cache.
|
| +
|
| + Saves its state as json file.
|
| + """
|
| + STATE_FILE = 'state.json'
|
| +
|
| + def __init__(self, cache_dir, remote, max_cache_size, min_free_space):
|
| + """
|
| + Arguments:
|
| + - cache_dir: Directory where to place the cache.
|
| + - remote: Remote directory (NFS, SMB, etc) or HTTP url to fetch the objects
|
| + from
|
| + - max_cache_size: Trim if the cache gets larger than this value. If 0, the
|
| + cache is effectively a leak.
|
| + - min_free_space: Trim if disk free space becomes lower than this value. If
|
| + 0, it unconditionally fill the disk.
|
| + """
|
| + self.cache_dir = cache_dir
|
| + self.remote = remote
|
| + self.max_cache_size = max_cache_size
|
| + self.min_free_space = min_free_space
|
| + self.state_file = os.path.join(cache_dir, self.STATE_FILE)
|
| + # The files are kept as an array in a LRU style. E.g. self.state[0] is the
|
| + # oldest item.
|
| + self.state = []
|
| +
|
| + if not os.path.isdir(self.cache_dir):
|
| + os.makedirs(self.cache_dir)
|
| + if os.path.isfile(self.state_file):
|
| + try:
|
| + self.state = json.load(open(self.state_file, 'rb'))
|
| + except ValueError:
|
| + # Too bad. The file will be overwritten and the cache cleared.
|
| + pass
|
| + self.trim()
|
| +
|
| + def trim(self):
|
| + """Trims anything we don't know, make sure enough free space exists."""
|
| + for f in os.listdir(self.cache_dir):
|
| + if f == self.STATE_FILE or f in self.state:
|
| + continue
|
| + logging.warn('Unknown file %s from cache' % f)
|
| + # Insert as the oldest file. It will be deleted eventually if not
|
| + # accessed.
|
| + self.state.insert(0, f)
|
| +
|
| + # Ensure enough free space.
|
| + while (
|
| + self.min_free_space and
|
| + self.state and
|
| + get_free_space(self.cache_dir) < self.min_free_space):
|
| + os.remove(self.path(self.state.pop(0)))
|
| +
|
| + # Ensure maximum cache size.
|
| + if self.max_cache_size and self.state:
|
| + sizes = [os.stat(self.path(f)).st_size for f in self.state]
|
| + while sizes and sum(sizes) > self.max_cache_size:
|
| + # Delete the oldest item.
|
| + os.remove(self.path(self.state.pop(0)))
|
| + sizes.pop(0)
|
| +
|
| + self.save()
|
| +
|
| + def retrieve(self, item):
|
| + """Retrieves a file from the remote and add it to the cache."""
|
| + assert not '/' in item
|
| + try:
|
| + index = self.state.index(item)
|
| + # Was already in cache. Update it's LRU value.
|
| + self.state.pop(index)
|
| + self.state.append(item)
|
| + return False
|
| + except ValueError:
|
| + out = self.path(item)
|
| + download_or_copy(os.path.join(self.remote, item), out)
|
| + self.state.append(item)
|
| + return True
|
| + finally:
|
| + self.save()
|
| +
|
| + def path(self, item):
|
| + """Returns the path to one item."""
|
| + return os.path.join(self.cache_dir, item)
|
| +
|
| + def save(self):
|
| + """Saves the LRU ordering."""
|
| + json.dump(self.state, open(self.state_file, 'wb'))
|
| +
|
| +
|
| +def run_tha_test(manifest, cache_dir, remote, max_cache_size, min_free_space):
|
| + """Downloads the dependencies in the cache, hardlinks them into a temporary
|
| + directory and runs the executable.
|
| + """
|
| + cache = Cache(cache_dir, remote, max_cache_size, min_free_space)
|
| + outdir = tempfile.mkdtemp(prefix='run_tha_test')
|
| + try:
|
| + for filepath, properties in manifest['files'].iteritems():
|
| + infile = properties['sha-1']
|
| + outfile = os.path.join(outdir, filepath)
|
| + cache.retrieve(infile)
|
| + outfiledir = os.path.dirname(outfile)
|
| + if not os.path.isdir(outfiledir):
|
| + os.makedirs(outfiledir)
|
| + link_file(outfile, cache.path(infile), HARDLINK)
|
| + os.chmod(outfile, properties['mode'])
|
| +
|
| + cwd = os.path.join(outdir, manifest['relative_cwd'])
|
| + if not os.path.isdir(cwd):
|
| + os.makedirs(cwd)
|
| + if manifest.get('read_only'):
|
| + make_writable(outdir, True)
|
| + cmd = manifest['command']
|
| + logging.info('Running %s, cwd=%s' % (cmd, cwd))
|
| + return subprocess.call(cmd, cwd=cwd)
|
| + finally:
|
| + # Save first, in case an exception occur in the following lines, then clean
|
| + # up.
|
| + cache.save()
|
| + rmtree(outdir)
|
| + cache.trim()
|
| +
|
| +
|
| +def main():
|
| + parser = optparse.OptionParser(
|
| + usage='%prog <options>', description=sys.modules[__name__].__doc__)
|
| + parser.add_option(
|
| + '-v', '--verbose', action='count', default=0, help='Use multiple times')
|
| + parser.add_option(
|
| + '-m', '--manifest',
|
| + metavar='FILE',
|
| + help='File/url describing what to map or run')
|
| + parser.add_option('--no-run', action='store_true', help='Skip the run part')
|
| + parser.add_option(
|
| + '--cache',
|
| + default='cache',
|
| + metavar='DIR',
|
| + help='Cache directory, default=%default')
|
| + parser.add_option(
|
| + '-r', '--remote', metavar='URL', help='Remote where to get the items')
|
| + parser.add_option(
|
| + '--max-cache-size',
|
| + type='int',
|
| + metavar='NNN',
|
| + default=20*1024*1024*1024,
|
| + help='Trim if the cache gets larger than this value, default=%default')
|
| + parser.add_option(
|
| + '--min-free-space',
|
| + type='int',
|
| + metavar='NNN',
|
| + default=1*1024*1024*1024,
|
| + help='Trim if disk free space becomes lower than this value, '
|
| + 'default=%default')
|
| +
|
| + options, args = parser.parse_args()
|
| + level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)]
|
| + logging.basicConfig(
|
| + level=level,
|
| + format='%(levelname)5s %(module)15s(%(lineno)3d): %(message)s')
|
| +
|
| + if not options.manifest:
|
| + parser.error('--manifest is required.')
|
| + if not options.remote:
|
| + parser.error('--remote is required.')
|
| + if args:
|
| + parser.error('Unsupported args %s' % ' '.join(args))
|
| +
|
| + manifest = json.load(open_remote(options.manifest))
|
| + return run_tha_test(
|
| + manifest, os.path.abspath(options.cache), options.remote,
|
| + options.max_cache_size, options.min_free_space)
|
| +
|
| +
|
| +if __name__ == '__main__':
|
| + sys.exit(main())
|
|
|