Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(127)

Side by Side Diff: swarm_client/run_isolated.py

Issue 69143004: Delete swarm_client. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/
Patch Set: Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « swarm_client/isolateserver.py ('k') | swarm_client/swarming.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/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
6 """Reads a .isolated, creates a tree of hardlinks and runs the test.
7
8 Keeps a local cache.
9 """
10
11 __version__ = '0.2'
12
13 import ctypes
14 import logging
15 import optparse
16 import os
17 import re
18 import shutil
19 import stat
20 import subprocess
21 import sys
22 import tempfile
23 import time
24
25 from third_party.depot_tools import fix_encoding
26
27 from utils import lru
28 from utils import threading_utils
29 from utils import tools
30 from utils import zip_package
31
32 import isolateserver
33
34
35 # Absolute path to this file (can be None if running from zip on Mac).
36 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
37
38 # Directory that contains this file (might be inside zip package).
39 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
40
41 # Directory that contains currently running script file.
42 if zip_package.get_main_script_path():
43 MAIN_DIR = os.path.dirname(
44 os.path.abspath(zip_package.get_main_script_path()))
45 else:
46 # This happens when 'import run_isolated' is executed at the python
47 # interactive prompt, in that case __file__ is undefined.
48 MAIN_DIR = None
49
50 # Types of action accepted by link_file().
51 HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY = range(1, 5)
52
53 # The name of the log file to use.
54 RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
55
56 # The name of the log to use for the run_test_cases.py command
57 RUN_TEST_CASES_LOG = 'run_test_cases.log'
58
59
60 # Used by get_flavor().
61 FLAVOR_MAPPING = {
62 'cygwin': 'win',
63 'win32': 'win',
64 'darwin': 'mac',
65 'sunos5': 'solaris',
66 'freebsd7': 'freebsd',
67 'freebsd8': 'freebsd',
68 }
69
70
71 def get_as_zip_package(executable=True):
72 """Returns ZipPackage with this module and all its dependencies.
73
74 If |executable| is True will store run_isolated.py as __main__.py so that
75 zip package is directly executable be python.
76 """
77 # Building a zip package when running from another zip package is
78 # unsupported and probably unneeded.
79 assert not zip_package.is_zipped_module(sys.modules[__name__])
80 assert THIS_FILE_PATH
81 assert BASE_DIR
82 package = zip_package.ZipPackage(root=BASE_DIR)
83 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
84 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
85 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
86 package.add_directory(os.path.join(BASE_DIR, 'utils'))
87 return package
88
89
90 def get_flavor():
91 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
92 return FLAVOR_MAPPING.get(sys.platform, 'linux')
93
94
95 def os_link(source, link_name):
96 """Add support for os.link() on Windows."""
97 if sys.platform == 'win32':
98 if not ctypes.windll.kernel32.CreateHardLinkW(
99 unicode(link_name), unicode(source), 0):
100 raise OSError()
101 else:
102 os.link(source, link_name)
103
104
105 def readable_copy(outfile, infile):
106 """Makes a copy of the file that is readable by everyone."""
107 shutil.copy2(infile, outfile)
108 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
109 stat.S_IRGRP | stat.S_IROTH)
110 os.chmod(outfile, read_enabled_mode)
111
112
113 def link_file(outfile, infile, action):
114 """Links a file. The type of link depends on |action|."""
115 logging.debug('Mapping %s to %s' % (infile, outfile))
116 if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
117 raise ValueError('Unknown mapping action %s' % action)
118 if not os.path.isfile(infile):
119 raise isolateserver.MappingError('%s is missing' % infile)
120 if os.path.isfile(outfile):
121 raise isolateserver.MappingError(
122 '%s already exist; insize:%d; outsize:%d' %
123 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
124
125 if action == COPY:
126 readable_copy(outfile, infile)
127 elif action == SYMLINK and sys.platform != 'win32':
128 # On windows, symlink are converted to hardlink and fails over to copy.
129 os.symlink(infile, outfile) # pylint: disable=E1101
130 else:
131 try:
132 os_link(infile, outfile)
133 except OSError as e:
134 if action == HARDLINK:
135 raise isolateserver.MappingError(
136 'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
137 # Probably a different file system.
138 logging.warning(
139 'Failed to hardlink, failing back to copy %s to %s' % (
140 infile, outfile))
141 readable_copy(outfile, infile)
142
143
144 def _set_write_bit(path, read_only):
145 """Sets or resets the executable bit on a file or directory."""
146 mode = os.lstat(path).st_mode
147 if read_only:
148 mode = mode & 0500
149 else:
150 mode = mode | 0200
151 if hasattr(os, 'lchmod'):
152 os.lchmod(path, mode) # pylint: disable=E1101
153 else:
154 if stat.S_ISLNK(mode):
155 # Skip symlink without lchmod() support.
156 logging.debug('Can\'t change +w bit on symlink %s' % path)
157 return
158
159 # TODO(maruel): Implement proper DACL modification on Windows.
160 os.chmod(path, mode)
161
162
163 def make_writable(root, read_only):
164 """Toggle the writable bit on a directory tree."""
165 assert os.path.isabs(root), root
166 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
167 for filename in filenames:
168 _set_write_bit(os.path.join(dirpath, filename), read_only)
169
170 for dirname in dirnames:
171 _set_write_bit(os.path.join(dirpath, dirname), read_only)
172
173
174 def rmtree(root):
175 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
176 make_writable(root, False)
177 if sys.platform == 'win32':
178 for i in range(3):
179 try:
180 shutil.rmtree(root)
181 break
182 except WindowsError: # pylint: disable=E0602
183 delay = (i+1)*2
184 print >> sys.stderr, (
185 'The test has subprocess outliving it. Sleep %d seconds.' % delay)
186 time.sleep(delay)
187 else:
188 shutil.rmtree(root)
189
190
191 def try_remove(filepath):
192 """Removes a file without crashing even if it doesn't exist."""
193 try:
194 os.remove(filepath)
195 except OSError:
196 pass
197
198
199 def is_same_filesystem(path1, path2):
200 """Returns True if both paths are on the same filesystem.
201
202 This is required to enable the use of hardlinks.
203 """
204 assert os.path.isabs(path1), path1
205 assert os.path.isabs(path2), path2
206 if sys.platform == 'win32':
207 # If the drive letter mismatches, assume it's a separate partition.
208 # TODO(maruel): It should look at the underlying drive, a drive letter could
209 # be a mount point to a directory on another drive.
210 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
211 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
212 if path1[0].lower() != path2[0].lower():
213 return False
214 return os.stat(path1).st_dev == os.stat(path2).st_dev
215
216
217 def get_free_space(path):
218 """Returns the number of free bytes."""
219 if sys.platform == 'win32':
220 free_bytes = ctypes.c_ulonglong(0)
221 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
222 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
223 return free_bytes.value
224 # For OSes other than Windows.
225 f = os.statvfs(path) # pylint: disable=E1101
226 return f.f_bfree * f.f_frsize
227
228
229 def make_temp_dir(prefix, root_dir):
230 """Returns a temporary directory on the same file system as root_dir."""
231 base_temp_dir = None
232 if not is_same_filesystem(root_dir, tempfile.gettempdir()):
233 base_temp_dir = os.path.dirname(root_dir)
234 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
235
236
237 class CachePolicies(object):
238 def __init__(self, max_cache_size, min_free_space, max_items):
239 """
240 Arguments:
241 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
242 cache is effectively a leak.
243 - min_free_space: Trim if disk free space becomes lower than this value. If
244 0, it unconditionally fill the disk.
245 - max_items: Maximum number of items to keep in the cache. If 0, do not
246 enforce a limit.
247 """
248 self.max_cache_size = max_cache_size
249 self.min_free_space = min_free_space
250 self.max_items = max_items
251
252
253 class DiskCache(isolateserver.LocalCache):
254 """Stateful LRU cache in a flat hash table in a directory.
255
256 Saves its state as json file.
257 """
258 STATE_FILE = 'state.json'
259
260 def __init__(self, cache_dir, policies, algo):
261 """
262 Arguments:
263 cache_dir: directory where to place the cache.
264 policies: cache retention policies.
265 algo: hashing algorithm used.
266 """
267 super(DiskCache, self).__init__()
268 self.algo = algo
269 self.cache_dir = cache_dir
270 self.policies = policies
271 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
272
273 # All protected methods (starting with '_') except _path should be called
274 # with this lock locked.
275 self._lock = threading_utils.LockWithAssert()
276 self._lru = lru.LRUDict()
277
278 # Profiling values.
279 self._added = []
280 self._removed = []
281 self._free_disk = 0
282
283 with tools.Profiler('Setup'):
284 with self._lock:
285 self._load()
286
287 def __enter__(self):
288 return self
289
290 def __exit__(self, _exc_type, _exec_value, _traceback):
291 with tools.Profiler('CleanupTrimming'):
292 with self._lock:
293 self._trim()
294
295 logging.info(
296 '%5d (%8dkb) added',
297 len(self._added), sum(self._added) / 1024)
298 logging.info(
299 '%5d (%8dkb) current',
300 len(self._lru),
301 sum(self._lru.itervalues()) / 1024)
302 logging.info(
303 '%5d (%8dkb) removed',
304 len(self._removed), sum(self._removed) / 1024)
305 logging.info(
306 ' %8dkb free',
307 self._free_disk / 1024)
308 return False
309
310 def cached_set(self):
311 with self._lock:
312 return self._lru.keys_set()
313
314 def touch(self, digest, size):
315 # Verify an actual file is valid. Note that is doesn't compute the hash so
316 # it could still be corrupted. Do it outside the lock.
317 if not isolateserver.is_valid_file(self._path(digest), size):
318 return False
319
320 # Update it's LRU position.
321 with self._lock:
322 if digest not in self._lru:
323 return False
324 self._lru.touch(digest)
325 return True
326
327 def evict(self, digest):
328 with self._lock:
329 self._lru.pop(digest)
330 self._delete_file(digest, isolateserver.UNKNOWN_FILE_SIZE)
331
332 def read(self, digest):
333 with open(self._path(digest), 'rb') as f:
334 return f.read()
335
336 def write(self, digest, content):
337 path = self._path(digest)
338 try:
339 size = isolateserver.file_write(path, content)
340 except:
341 # There are two possible places were an exception can occur:
342 # 1) Inside |content| generator in case of network or unzipping errors.
343 # 2) Inside file_write itself in case of disk IO errors.
344 # In any case delete an incomplete file and propagate the exception to
345 # caller, it will be logged there.
346 try_remove(path)
347 raise
348 with self._lock:
349 self._add(digest, size)
350
351 def link(self, digest, dest, file_mode=None):
352 link_file(dest, self._path(digest), HARDLINK)
353 if file_mode is not None:
354 os.chmod(dest, file_mode)
355
356 def _load(self):
357 """Loads state of the cache from json file."""
358 self._lock.assert_locked()
359
360 if not os.path.isdir(self.cache_dir):
361 os.makedirs(self.cache_dir)
362
363 # Load state of the cache.
364 if os.path.isfile(self.state_file):
365 try:
366 self._lru = lru.LRUDict.load(self.state_file)
367 except ValueError as err:
368 logging.error('Failed to load cache state: %s' % (err,))
369 # Don't want to keep broken state file.
370 os.remove(self.state_file)
371
372 # Ensure that all files listed in the state still exist and add new ones.
373 previous = self._lru.keys_set()
374 unknown = []
375 for filename in os.listdir(self.cache_dir):
376 if filename == self.STATE_FILE:
377 continue
378 if filename in previous:
379 previous.remove(filename)
380 continue
381 # An untracked file.
382 if not isolateserver.is_valid_hash(filename, self.algo):
383 logging.warning('Removing unknown file %s from cache', filename)
384 try_remove(self._path(filename))
385 continue
386 # File that's not referenced in 'state.json'.
387 # TODO(vadimsh): Verify its SHA1 matches file name.
388 logging.warning('Adding unknown file %s to cache', filename)
389 unknown.append(filename)
390
391 if unknown:
392 # Add as oldest files. They will be deleted eventually if not accessed.
393 self._add_oldest_list(unknown)
394 logging.warning('Added back %d unknown files', len(unknown))
395
396 if previous:
397 # Filter out entries that were not found.
398 logging.warning('Removed %d lost files', len(previous))
399 for filename in previous:
400 self._lru.pop(filename)
401 self._trim()
402
403 def _save(self):
404 """Saves the LRU ordering."""
405 self._lock.assert_locked()
406 self._lru.save(self.state_file)
407
408 def _trim(self):
409 """Trims anything we don't know, make sure enough free space exists."""
410 self._lock.assert_locked()
411
412 # Ensure maximum cache size.
413 if self.policies.max_cache_size:
414 total_size = sum(self._lru.itervalues())
415 while total_size > self.policies.max_cache_size:
416 total_size -= self._remove_lru_file()
417
418 # Ensure maximum number of items in the cache.
419 if self.policies.max_items and len(self._lru) > self.policies.max_items:
420 for _ in xrange(len(self._lru) - self.policies.max_items):
421 self._remove_lru_file()
422
423 # Ensure enough free space.
424 self._free_disk = get_free_space(self.cache_dir)
425 trimmed_due_to_space = False
426 while (
427 self.policies.min_free_space and
428 self._lru and
429 self._free_disk < self.policies.min_free_space):
430 trimmed_due_to_space = True
431 self._remove_lru_file()
432 self._free_disk = get_free_space(self.cache_dir)
433 if trimmed_due_to_space:
434 total = sum(self._lru.itervalues())
435 logging.warning(
436 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
437 'cache (%.1f%% of its maximum capacity)',
438 self._free_disk / 1024.,
439 total / 1024.,
440 100. * self.policies.max_cache_size / float(total),
441 )
442 self._save()
443
444 def _path(self, digest):
445 """Returns the path to one item."""
446 return os.path.join(self.cache_dir, digest)
447
448 def _remove_lru_file(self):
449 """Removes the last recently used file and returns its size."""
450 self._lock.assert_locked()
451 digest, size = self._lru.pop_oldest()
452 self._delete_file(digest, size)
453 return size
454
455 def _add(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
456 """Adds an item into LRU cache marking it as a newest one."""
457 self._lock.assert_locked()
458 if size == isolateserver.UNKNOWN_FILE_SIZE:
459 size = os.stat(self._path(digest)).st_size
460 self._added.append(size)
461 self._lru.add(digest, size)
462
463 def _add_oldest_list(self, digests):
464 """Adds a bunch of items into LRU cache marking them as oldest ones."""
465 self._lock.assert_locked()
466 pairs = []
467 for digest in digests:
468 size = os.stat(self._path(digest)).st_size
469 self._added.append(size)
470 pairs.append((digest, size))
471 self._lru.batch_insert_oldest(pairs)
472
473 def _delete_file(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
474 """Deletes cache file from the file system."""
475 self._lock.assert_locked()
476 try:
477 if size == isolateserver.UNKNOWN_FILE_SIZE:
478 size = os.stat(self._path(digest)).st_size
479 os.remove(self._path(digest))
480 self._removed.append(size)
481 except OSError as e:
482 logging.error('Error attempting to delete a file %s:\n%s' % (digest, e))
483
484
485 def run_tha_test(isolated_hash, storage, cache, algo, outdir):
486 """Downloads the dependencies in the cache, hardlinks them into a |outdir|
487 and runs the executable.
488 """
489 try:
490 try:
491 settings = isolateserver.fetch_isolated(
492 isolated_hash=isolated_hash,
493 storage=storage,
494 cache=cache,
495 algo=algo,
496 outdir=outdir,
497 os_flavor=get_flavor(),
498 require_command=True)
499 except isolateserver.ConfigError as e:
500 tools.report_error(e)
501 return 1
502
503 if settings.read_only:
504 logging.info('Making files read only')
505 make_writable(outdir, True)
506 cwd = os.path.normpath(os.path.join(outdir, settings.relative_cwd))
507 logging.info('Running %s, cwd=%s' % (settings.command, cwd))
508
509 # TODO(csharp): This should be specified somewhere else.
510 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
511 # Add a rotating log file if one doesn't already exist.
512 env = os.environ.copy()
513 if MAIN_DIR:
514 env.setdefault('RUN_TEST_CASES_LOG_FILE',
515 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
516 try:
517 with tools.Profiler('RunTest'):
518 return subprocess.call(settings.command, cwd=cwd, env=env)
519 except OSError:
520 tools.report_error('Failed to run %s; cwd=%s' % (settings.command, cwd))
521 return 1
522 finally:
523 if outdir:
524 rmtree(outdir)
525
526
527 def main():
528 tools.disable_buffering()
529 parser = tools.OptionParserWithLogging(
530 usage='%prog <options>',
531 version=__version__,
532 log_file=RUN_ISOLATED_LOG_FILE)
533
534 group = optparse.OptionGroup(parser, 'Data source')
535 group.add_option(
536 '-s', '--isolated',
537 metavar='FILE',
538 help='File/url describing what to map or run')
539 group.add_option(
540 '-H', '--hash',
541 help='Hash of the .isolated to grab from the hash table')
542 group.add_option(
543 '-I', '--isolate-server',
544 metavar='URL', default='',
545 help='Isolate server to use')
546 group.add_option(
547 '-n', '--namespace',
548 default='default-gzip',
549 help='namespace to use when using isolateserver, default: %default')
550 parser.add_option_group(group)
551
552 group = optparse.OptionGroup(parser, 'Cache management')
553 group.add_option(
554 '--cache',
555 default='cache',
556 metavar='DIR',
557 help='Cache directory, default=%default')
558 group.add_option(
559 '--max-cache-size',
560 type='int',
561 metavar='NNN',
562 default=20*1024*1024*1024,
563 help='Trim if the cache gets larger than this value, default=%default')
564 group.add_option(
565 '--min-free-space',
566 type='int',
567 metavar='NNN',
568 default=2*1024*1024*1024,
569 help='Trim if disk free space becomes lower than this value, '
570 'default=%default')
571 group.add_option(
572 '--max-items',
573 type='int',
574 metavar='NNN',
575 default=100000,
576 help='Trim if more than this number of items are in the cache '
577 'default=%default')
578 parser.add_option_group(group)
579
580 options, args = parser.parse_args()
581
582 if bool(options.isolated) == bool(options.hash):
583 logging.debug('One and only one of --isolated or --hash is required.')
584 parser.error('One and only one of --isolated or --hash is required.')
585 if args:
586 logging.debug('Unsupported args %s' % ' '.join(args))
587 parser.error('Unsupported args %s' % ' '.join(args))
588 if not options.isolate_server:
589 parser.error('--isolate-server is required.')
590
591 options.cache = os.path.abspath(options.cache)
592 policies = CachePolicies(
593 options.max_cache_size, options.min_free_space, options.max_items)
594 storage = isolateserver.get_storage(options.isolate_server, options.namespace)
595 algo = isolateserver.get_hash_algo(options.namespace)
596
597 try:
598 # |options.cache| may not exist until DiskCache() instance is created.
599 cache = DiskCache(options.cache, policies, algo)
600 outdir = make_temp_dir('run_tha_test', options.cache)
601 return run_tha_test(
602 options.isolated or options.hash, storage, cache, algo, outdir)
603 except Exception as e:
604 # Make sure any exception is logged.
605 tools.report_error(e)
606 logging.exception(e)
607 return 1
608
609
610 if __name__ == '__main__':
611 # Ensure that we are always running with the correct encoding.
612 fix_encoding.fix_encoding()
613 sys.exit(main())
OLDNEW
« no previous file with comments | « swarm_client/isolateserver.py ('k') | swarm_client/swarming.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698