blob: 9e5d57d0bf1dc3e6d73f16e4f505ae10b91710b5 [file] [log] [blame]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001#!/usr/bin/env python
Marc-Antoine Ruel8add1242013-11-05 17:28:27 -05002# Copyright 2012 The Swarming Authors. All rights reserved.
Marc-Antoine Ruele98b1122013-11-05 20:27:57 -05003# Use of this source code is governed under the Apache License, Version 2.0 that
4# can be found in the LICENSE file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00005
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
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -05008To improve performance, it keeps a local cache. The local cache can safely be
9deleted.
10
11Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
12temporary directory upon execution of the command specified in the .isolated
13file. All content written to this directory will be uploaded upon termination
14and the .isolated file describing this directory will be printed to stdout.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000015"""
16
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040017__version__ = '0.3.2'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000018
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000019import ctypes
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000020import logging
21import optparse
22import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000023import re
24import shutil
25import stat
26import subprocess
27import sys
28import tempfile
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000029import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000030
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000031from third_party.depot_tools import fix_encoding
32
Vadim Shtayura6b555c12014-07-23 16:22:18 -070033from utils import file_path
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000034from utils import lru
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040035from utils import on_error
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +000036from utils import threading_utils
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000037from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000038from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000039
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080040import auth
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040041import isolated_format
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000042import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000043
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000044
vadimsh@chromium.org85071062013-08-21 23:37:45 +000045# Absolute path to this file (can be None if running from zip on Mac).
46THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000047
48# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000049BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000050
51# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000052if zip_package.get_main_script_path():
53 MAIN_DIR = os.path.dirname(
54 os.path.abspath(zip_package.get_main_script_path()))
55else:
56 # This happens when 'import run_isolated' is executed at the python
57 # interactive prompt, in that case __file__ is undefined.
58 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000059
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000060# Types of action accepted by link_file().
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000061HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY = range(1, 5)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000062
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000063# The name of the log file to use.
64RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
65
csharp@chromium.orge217f302012-11-22 16:51:53 +000066# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000067RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000068
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000069
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000070def get_as_zip_package(executable=True):
71 """Returns ZipPackage with this module and all its dependencies.
72
73 If |executable| is True will store run_isolated.py as __main__.py so that
74 zip package is directly executable be python.
75 """
76 # Building a zip package when running from another zip package is
77 # unsupported and probably unneeded.
78 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000079 assert THIS_FILE_PATH
80 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000081 package = zip_package.ZipPackage(root=BASE_DIR)
82 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040083 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000084 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080085 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000086 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
87 package.add_directory(os.path.join(BASE_DIR, 'utils'))
88 return package
89
90
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -050091def hardlink(source, link_name):
92 """Hardlinks a file.
93
94 Add support for os.link() on Windows.
95 """
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000096 if sys.platform == 'win32':
97 if not ctypes.windll.kernel32.CreateHardLinkW(
98 unicode(link_name), unicode(source), 0):
99 raise OSError()
100 else:
101 os.link(source, link_name)
102
103
104def readable_copy(outfile, infile):
105 """Makes a copy of the file that is readable by everyone."""
csharp@chromium.org59d116d2013-07-05 18:04:08 +0000106 shutil.copy2(infile, outfile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000107 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
108 stat.S_IRGRP | stat.S_IROTH)
109 os.chmod(outfile, read_enabled_mode)
110
111
112def link_file(outfile, infile, action):
113 """Links a file. The type of link depends on |action|."""
114 logging.debug('Mapping %s to %s' % (infile, outfile))
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000115 if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000116 raise ValueError('Unknown mapping action %s' % action)
117 if not os.path.isfile(infile):
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400118 raise isolated_format.MappingError('%s is missing' % infile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000119 if os.path.isfile(outfile):
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400120 raise isolated_format.MappingError(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000121 '%s already exist; insize:%d; outsize:%d' %
122 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
123
124 if action == COPY:
125 readable_copy(outfile, infile)
126 elif action == SYMLINK and sys.platform != 'win32':
127 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000128 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000129 else:
130 try:
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500131 hardlink(infile, outfile)
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000132 except OSError as e:
133 if action == HARDLINK:
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400134 raise isolated_format.MappingError(
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000135 'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000136 # Probably a different file system.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000137 logging.warning(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000138 'Failed to hardlink, failing back to copy %s to %s' % (
139 infile, outfile))
140 readable_copy(outfile, infile)
141
142
Marc-Antoine Rueld2d4d4f2013-11-10 14:32:38 -0500143def set_read_only(path, read_only):
144 """Sets or resets the write bit on a file or directory.
145
146 Zaps out access to 'group' and 'others'.
147 """
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500148 assert isinstance(read_only, bool), read_only
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000149 mode = os.lstat(path).st_mode
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500150 # TODO(maruel): Stop removing GO bits.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000151 if read_only:
152 mode = mode & 0500
153 else:
154 mode = mode | 0200
155 if hasattr(os, 'lchmod'):
156 os.lchmod(path, mode) # pylint: disable=E1101
157 else:
158 if stat.S_ISLNK(mode):
159 # Skip symlink without lchmod() support.
Marc-Antoine Ruel45dc2902013-12-05 14:54:20 -0500160 logging.debug(
161 'Can\'t change %sw bit on symlink %s',
162 '-' if read_only else '+', path)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000163 return
164
165 # TODO(maruel): Implement proper DACL modification on Windows.
166 os.chmod(path, mode)
167
168
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500169def make_tree_read_only(root):
170 """Makes all the files in the directories read only.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000171
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500172 Also makes the directories read only, only if it makes sense on the platform.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500173
174 This means no file can be created or deleted.
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500175 """
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500176 logging.debug('make_tree_read_only(%s)', root)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500177 assert os.path.isabs(root), root
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500178 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
179 for filename in filenames:
180 set_read_only(os.path.join(dirpath, filename), True)
181 if sys.platform != 'win32':
182 # It must not be done on Windows.
183 for dirname in dirnames:
184 set_read_only(os.path.join(dirpath, dirname), True)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500185 if sys.platform != 'win32':
186 set_read_only(root, True)
187
188
189def make_tree_files_read_only(root):
190 """Makes all the files in the directories read only but not the directories
191 themselves.
192
193 This means files can be created or deleted.
194 """
195 logging.debug('make_tree_files_read_only(%s)', root)
196 assert os.path.isabs(root), root
197 if sys.platform != 'win32':
198 set_read_only(root, False)
199 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
200 for filename in filenames:
201 set_read_only(os.path.join(dirpath, filename), True)
202 if sys.platform != 'win32':
203 # It must not be done on Windows.
204 for dirname in dirnames:
205 set_read_only(os.path.join(dirpath, dirname), False)
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500206
207
208def make_tree_writeable(root):
209 """Makes all the files in the directories writeable.
210
211 Also makes the directories writeable, only if it makes sense on the platform.
212
213 It is different from make_tree_deleteable() because it unconditionally affects
214 the files.
215 """
216 logging.debug('make_tree_writeable(%s)', root)
217 assert os.path.isabs(root), root
218 if sys.platform != 'win32':
219 set_read_only(root, False)
220 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
221 for filename in filenames:
222 set_read_only(os.path.join(dirpath, filename), False)
223 if sys.platform != 'win32':
224 # It must not be done on Windows.
225 for dirname in dirnames:
226 set_read_only(os.path.join(dirpath, dirname), False)
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500227
228
229def make_tree_deleteable(root):
230 """Changes the appropriate permissions so the files in the directories can be
231 deleted.
232
233 On Windows, the files are modified. On other platforms, modify the directory.
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500234 It only does the minimum so the files can be deleted safely.
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500235
236 Warning on Windows: since file permission is modified, the file node is
237 modified. This means that for hard-linked files, every directory entry for the
238 file node has its file permission modified.
239 """
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500240 logging.debug('make_tree_deleteable(%s)', root)
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500241 assert os.path.isabs(root), root
242 if sys.platform != 'win32':
243 set_read_only(root, False)
244 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
245 if sys.platform == 'win32':
246 for filename in filenames:
247 set_read_only(os.path.join(dirpath, filename), False)
248 else:
249 for dirname in dirnames:
250 set_read_only(os.path.join(dirpath, dirname), False)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000251
252
253def rmtree(root):
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400254 """Wrapper around shutil.rmtree() to retry automatically on Windows.
255
256 On Windows, forcibly kills processes that are found to interfere with the
257 deletion.
258
259 Returns:
260 True on normal execution, False if berserk techniques (like killing
261 processes) had to be used.
262 """
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500263 make_tree_deleteable(root)
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500264 logging.info('rmtree(%s)', root)
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400265 if sys.platform != 'win32':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000266 shutil.rmtree(root)
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400267 return True
268
269 # Windows is more 'challenging'. First tries the soft way: tries 3 times to
270 # delete and sleep a bit in between.
271 max_tries = 3
272 for i in xrange(max_tries):
Marc-Antoine Ruela3d702c2014-08-04 17:45:48 -0400273 # errors is a list of tuple(function, path, excinfo).
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400274 errors = []
275 shutil.rmtree(root, onerror=lambda *args: errors.append(args))
276 if not errors:
277 return True
278 if i == max_tries - 1:
279 sys.stderr.write(
280 'Failed to delete %s. The following files remain:\n' % root)
281 for _, path, _ in errors:
282 sys.stderr.write('- %s\n' % path)
283 else:
284 delay = (i+1)*2
285 sys.stderr.write(
286 'Failed to delete %s (%d files remaining).\n'
287 ' Maybe the test has a subprocess outliving it.\n'
288 ' Sleeping %d seconds.\n' %
289 (root, len(errors), delay))
290 time.sleep(delay)
291
292 # The soft way was not good enough. Try the hard way. Enumerates both:
293 # - all child processes from this process.
294 # - processes where the main executable in inside 'root'. The reason is that
295 # the ancestry may be broken so stray grand-children processes could be
296 # undetected by the first technique.
297 # This technique is not fool-proof but gets mostly there.
298 def get_processes():
299 processes = threading_utils.enum_processes_win()
300 tree_processes = threading_utils.filter_processes_tree_win(processes)
301 dir_processes = threading_utils.filter_processes_dir_win(processes, root)
302 # Convert to dict to remove duplicates.
303 processes = {p.ProcessId: p for p in tree_processes}
304 processes.update((p.ProcessId, p) for p in dir_processes)
305 processes.pop(os.getpid())
306 return processes
307
308 for i in xrange(3):
309 sys.stderr.write('Enumerating processes:\n')
310 processes = get_processes()
311 if not processes:
312 break
313 for _, proc in sorted(processes.iteritems()):
314 sys.stderr.write(
315 '- pid %d; Handles: %d; Exe: %s; Cmd: %s\n' % (
316 proc.ProcessId,
317 proc.HandleCount,
318 proc.ExecutablePath,
319 proc.CommandLine))
320 sys.stderr.write('Terminating %d processes.\n' % len(processes))
321 for pid in sorted(processes):
322 try:
323 # Killing is asynchronous.
324 os.kill(pid, 9)
325 sys.stderr.write('- %d killed\n' % pid)
326 except OSError:
327 sys.stderr.write('- failed to kill %s\n' % pid)
328 if i < 2:
329 time.sleep((i+1)*2)
330 else:
331 processes = get_processes()
332 if processes:
333 sys.stderr.write('Failed to terminate processes.\n')
Marc-Antoine Ruela3d702c2014-08-04 17:45:48 -0400334 raise errors[0][2][0], errors[0][2][1], errors[0][2][2]
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400335
336 # Now that annoying processes in root are evicted, try again.
337 errors = []
338 shutil.rmtree(root, onerror=lambda *args: errors.append(args))
339 if errors:
340 # There's no hope.
341 sys.stderr.write(
342 'Failed to delete %s. The following files remain:\n' % root)
343 for _, path, _ in errors:
344 sys.stderr.write('- %s\n' % path)
Marc-Antoine Ruela3d702c2014-08-04 17:45:48 -0400345 raise errors[0][2][0], errors[0][2][1], errors[0][2][2]
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400346 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000347
348
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000349def try_remove(filepath):
350 """Removes a file without crashing even if it doesn't exist."""
351 try:
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500352 # TODO(maruel): Not do it unless necessary since it slows this function
353 # down.
354 if sys.platform == 'win32':
355 # Deleting a read-only file will fail if it is read-only.
356 set_read_only(filepath, False)
357 else:
358 # Deleting a read-only file will fail if the directory is read-only.
359 set_read_only(os.path.dirname(filepath), False)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000360 os.remove(filepath)
361 except OSError:
362 pass
363
364
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000365def is_same_filesystem(path1, path2):
366 """Returns True if both paths are on the same filesystem.
367
368 This is required to enable the use of hardlinks.
369 """
370 assert os.path.isabs(path1), path1
371 assert os.path.isabs(path2), path2
372 if sys.platform == 'win32':
373 # If the drive letter mismatches, assume it's a separate partition.
374 # TODO(maruel): It should look at the underlying drive, a drive letter could
375 # be a mount point to a directory on another drive.
376 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
377 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
378 if path1[0].lower() != path2[0].lower():
379 return False
380 return os.stat(path1).st_dev == os.stat(path2).st_dev
381
382
383def get_free_space(path):
384 """Returns the number of free bytes."""
385 if sys.platform == 'win32':
386 free_bytes = ctypes.c_ulonglong(0)
387 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
388 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
389 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000390 # For OSes other than Windows.
391 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000392 return f.f_bfree * f.f_frsize
393
394
395def make_temp_dir(prefix, root_dir):
396 """Returns a temporary directory on the same file system as root_dir."""
397 base_temp_dir = None
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500398 if root_dir and not is_same_filesystem(root_dir, tempfile.gettempdir()):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000399 base_temp_dir = os.path.dirname(root_dir)
400 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
401
402
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000403class CachePolicies(object):
404 def __init__(self, max_cache_size, min_free_space, max_items):
405 """
406 Arguments:
407 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
408 cache is effectively a leak.
409 - min_free_space: Trim if disk free space becomes lower than this value. If
410 0, it unconditionally fill the disk.
411 - max_items: Maximum number of items to keep in the cache. If 0, do not
412 enforce a limit.
413 """
414 self.max_cache_size = max_cache_size
415 self.min_free_space = min_free_space
416 self.max_items = max_items
417
418
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000419class DiskCache(isolateserver.LocalCache):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000420 """Stateful LRU cache in a flat hash table in a directory.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000421
422 Saves its state as json file.
423 """
424 STATE_FILE = 'state.json'
425
Vadim Shtayurae0ab1902014-04-29 10:55:27 -0700426 def __init__(self, cache_dir, policies, hash_algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000427 """
428 Arguments:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000429 cache_dir: directory where to place the cache.
430 policies: cache retention policies.
431 algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000432 """
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000433 super(DiskCache, self).__init__()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000434 self.cache_dir = cache_dir
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000435 self.policies = policies
Vadim Shtayurae0ab1902014-04-29 10:55:27 -0700436 self.hash_algo = hash_algo
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000437 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000438
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000439 # All protected methods (starting with '_') except _path should be called
440 # with this lock locked.
441 self._lock = threading_utils.LockWithAssert()
442 self._lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000443
444 # Profiling values.
445 self._added = []
446 self._removed = []
447 self._free_disk = 0
448
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000449 with tools.Profiler('Setup'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000450 with self._lock:
451 self._load()
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000452
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000453 def __enter__(self):
454 return self
455
456 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000457 with tools.Profiler('CleanupTrimming'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000458 with self._lock:
459 self._trim()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000460
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000461 logging.info(
462 '%5d (%8dkb) added',
463 len(self._added), sum(self._added) / 1024)
464 logging.info(
465 '%5d (%8dkb) current',
466 len(self._lru),
467 sum(self._lru.itervalues()) / 1024)
468 logging.info(
469 '%5d (%8dkb) removed',
470 len(self._removed), sum(self._removed) / 1024)
471 logging.info(
472 ' %8dkb free',
473 self._free_disk / 1024)
maruel@chromium.org41601642013-09-18 19:40:46 +0000474 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000475
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000476 def cached_set(self):
477 with self._lock:
478 return self._lru.keys_set()
479
480 def touch(self, digest, size):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500481 """Verifies an actual file is valid.
482
483 Note that is doesn't compute the hash so it could still be corrupted if the
484 file size didn't change.
485
486 TODO(maruel): More stringent verification while keeping the check fast.
487 """
488 # Do the check outside the lock.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000489 if not isolateserver.is_valid_file(self._path(digest), size):
490 return False
491
492 # Update it's LRU position.
493 with self._lock:
494 if digest not in self._lru:
495 return False
496 self._lru.touch(digest)
497 return True
498
499 def evict(self, digest):
500 with self._lock:
501 self._lru.pop(digest)
Vadim Shtayura3148e072014-09-02 18:51:52 -0700502 self._delete_file(digest, isolateserver.UNKNOWN_FILE_SIZE)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000503
504 def read(self, digest):
505 with open(self._path(digest), 'rb') as f:
506 return f.read()
507
508 def write(self, digest, content):
509 path = self._path(digest)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500510 # A stale broken file may remain. It is possible for the file to have write
511 # access bit removed which would cause the file_write() call to fail to open
512 # in write mode. Take no chance here.
513 try_remove(path)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000514 try:
515 size = isolateserver.file_write(path, content)
516 except:
517 # There are two possible places were an exception can occur:
518 # 1) Inside |content| generator in case of network or unzipping errors.
519 # 2) Inside file_write itself in case of disk IO errors.
520 # In any case delete an incomplete file and propagate the exception to
521 # caller, it will be logged there.
522 try_remove(path)
523 raise
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500524 # Make the file read-only in the cache. This has a few side-effects since
525 # the file node is modified, so every directory entries to this file becomes
526 # read-only. It's fine here because it is a new file.
527 set_read_only(path, True)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000528 with self._lock:
529 self._add(digest, size)
530
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500531 def hardlink(self, digest, dest, file_mode):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500532 """Hardlinks the file to |dest|.
533
534 Note that the file permission bits are on the file node, not the directory
535 entry, so changing the access bit on any of the directory entries for the
536 file node will affect them all.
537 """
538 path = self._path(digest)
539 link_file(dest, path, HARDLINK)
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500540 if file_mode is not None:
541 # Ignores all other bits.
542 os.chmod(dest, file_mode & 0500)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000543
544 def _load(self):
545 """Loads state of the cache from json file."""
546 self._lock.assert_locked()
547
548 if not os.path.isdir(self.cache_dir):
549 os.makedirs(self.cache_dir)
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500550 else:
551 # Make sure the cache is read-only.
552 # TODO(maruel): Calculate the cost and optimize the performance
553 # accordingly.
554 make_tree_read_only(self.cache_dir)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000555
556 # Load state of the cache.
557 if os.path.isfile(self.state_file):
558 try:
559 self._lru = lru.LRUDict.load(self.state_file)
560 except ValueError as err:
561 logging.error('Failed to load cache state: %s' % (err,))
562 # Don't want to keep broken state file.
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500563 try_remove(self.state_file)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000564
565 # Ensure that all files listed in the state still exist and add new ones.
566 previous = self._lru.keys_set()
567 unknown = []
568 for filename in os.listdir(self.cache_dir):
569 if filename == self.STATE_FILE:
570 continue
571 if filename in previous:
572 previous.remove(filename)
573 continue
574 # An untracked file.
Marc-Antoine Ruel14b5fd82014-09-08 20:15:50 -0400575 if not isolated_format.is_valid_hash(filename, self.hash_algo):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000576 logging.warning('Removing unknown file %s from cache', filename)
577 try_remove(self._path(filename))
578 continue
579 # File that's not referenced in 'state.json'.
580 # TODO(vadimsh): Verify its SHA1 matches file name.
581 logging.warning('Adding unknown file %s to cache', filename)
582 unknown.append(filename)
583
584 if unknown:
585 # Add as oldest files. They will be deleted eventually if not accessed.
586 self._add_oldest_list(unknown)
587 logging.warning('Added back %d unknown files', len(unknown))
588
589 if previous:
590 # Filter out entries that were not found.
591 logging.warning('Removed %d lost files', len(previous))
592 for filename in previous:
593 self._lru.pop(filename)
594 self._trim()
595
596 def _save(self):
597 """Saves the LRU ordering."""
598 self._lock.assert_locked()
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500599 if sys.platform != 'win32':
600 d = os.path.dirname(self.state_file)
601 if os.path.isdir(d):
602 # Necessary otherwise the file can't be created.
603 set_read_only(d, False)
604 if os.path.isfile(self.state_file):
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500605 set_read_only(self.state_file, False)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000606 self._lru.save(self.state_file)
607
608 def _trim(self):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000609 """Trims anything we don't know, make sure enough free space exists."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000610 self._lock.assert_locked()
611
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000612 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000613 if self.policies.max_cache_size:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000614 total_size = sum(self._lru.itervalues())
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000615 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000616 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000617
618 # Ensure maximum number of items in the cache.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000619 if self.policies.max_items and len(self._lru) > self.policies.max_items:
620 for _ in xrange(len(self._lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000621 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000622
623 # Ensure enough free space.
624 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000625 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000626 while (
627 self.policies.min_free_space and
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000628 self._lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000629 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000630 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000631 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000632 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000633 if trimmed_due_to_space:
Marc-Antoine Ruel2cbc1242014-07-16 11:05:04 -0400634 total_usage = sum(self._lru.itervalues())
635 usage_percent = 0.
636 if total_usage:
637 usage_percent = 100. * self.policies.max_cache_size / float(total_usage)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000638 logging.warning(
639 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
640 'cache (%.1f%% of its maximum capacity)',
641 self._free_disk / 1024.,
Marc-Antoine Ruel2cbc1242014-07-16 11:05:04 -0400642 total_usage / 1024.,
643 usage_percent)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000644 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000645
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000646 def _path(self, digest):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000647 """Returns the path to one item."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000648 return os.path.join(self.cache_dir, digest)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000649
650 def _remove_lru_file(self):
651 """Removes the last recently used file and returns its size."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000652 self._lock.assert_locked()
653 digest, size = self._lru.pop_oldest()
654 self._delete_file(digest, size)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000655 return size
656
Vadim Shtayura3148e072014-09-02 18:51:52 -0700657 def _add(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000658 """Adds an item into LRU cache marking it as a newest one."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000659 self._lock.assert_locked()
Vadim Shtayura3148e072014-09-02 18:51:52 -0700660 if size == isolateserver.UNKNOWN_FILE_SIZE:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000661 size = os.stat(self._path(digest)).st_size
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000662 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000663 self._lru.add(digest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000664
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000665 def _add_oldest_list(self, digests):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000666 """Adds a bunch of items into LRU cache marking them as oldest ones."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000667 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000668 pairs = []
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000669 for digest in digests:
670 size = os.stat(self._path(digest)).st_size
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000671 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000672 pairs.append((digest, size))
673 self._lru.batch_insert_oldest(pairs)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000674
Vadim Shtayura3148e072014-09-02 18:51:52 -0700675 def _delete_file(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000676 """Deletes cache file from the file system."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000677 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000678 try:
Vadim Shtayura3148e072014-09-02 18:51:52 -0700679 if size == isolateserver.UNKNOWN_FILE_SIZE:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000680 size = os.stat(self._path(digest)).st_size
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500681 try_remove(self._path(digest))
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000682 self._removed.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000683 except OSError as e:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000684 logging.error('Error attempting to delete a file %s:\n%s' % (digest, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000685
686
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500687def change_tree_read_only(rootdir, read_only):
688 """Changes the tree read-only bits according to the read_only specification.
689
690 The flag can be 0, 1 or 2, which will affect the possibility to modify files
691 and create or delete files.
692 """
693 if read_only == 2:
694 # Files and directories (except on Windows) are marked read only. This
695 # inhibits modifying, creating or deleting files in the test directory,
696 # except on Windows where creating and deleting files is still possible.
697 make_tree_read_only(rootdir)
698 elif read_only == 1:
699 # Files are marked read only but not the directories. This inhibits
700 # modifying files but creating or deleting files is still possible.
701 make_tree_files_read_only(rootdir)
702 elif read_only in (0, None):
703 # Anything can be modified. This is the default in the .isolated file
704 # format.
705 #
706 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
707 # is not yet changed to verify the hash of the content of the files it is
708 # looking at, so that if a test modifies an input file, the file must be
709 # deleted.
710 make_tree_writeable(rootdir)
711 else:
712 raise ValueError(
713 'change_tree_read_only(%s, %s): Unknown flag %s' %
714 (rootdir, read_only, read_only))
715
716
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500717def process_command(command, out_dir):
718 """Replaces isolated specific variables in a command line."""
Vadim Shtayura51aba362014-05-14 15:39:23 -0700719 filtered = []
720 for arg in command:
721 if '${ISOLATED_OUTDIR}' in arg:
722 arg = arg.replace('${ISOLATED_OUTDIR}', out_dir).replace('/', os.sep)
723 filtered.append(arg)
724 return filtered
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500725
726
Kenneth Russell61d42352014-09-15 11:41:16 -0700727def run_tha_test(isolated_hash, storage, cache, leak_temp_dir, extra_args):
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500728 """Downloads the dependencies in the cache, hardlinks them into a temporary
729 directory and runs the executable from there.
730
731 A temporary directory is created to hold the output files. The content inside
732 this directory will be uploaded back to |storage| packaged as a .isolated
733 file.
734
735 Arguments:
736 isolated_hash: the sha-1 of the .isolated file that must be retrieved to
737 recreate the tree of files to run the target executable.
738 storage: an isolateserver.Storage object to retrieve remote objects. This
739 object has a reference to an isolateserver.StorageApi, which does
740 the actual I/O.
741 cache: an isolateserver.LocalCache to keep from retrieving the same objects
742 constantly by caching the objects retrieved. Can be on-disk or
743 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700744 leak_temp_dir: if true, the temporary directory will be deliberately leaked
745 for later examination.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500746 extra_args: optional arguments to add to the command stated in the .isolate
747 file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000748 """
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500749 run_dir = make_temp_dir('run_tha_test', cache.cache_dir)
Vadim Shtayura51aba362014-05-14 15:39:23 -0700750 out_dir = unicode(make_temp_dir('isolated_out', cache.cache_dir))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500751 result = 0
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000752 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000753 try:
Vadim Shtayura7f7459c2014-09-04 13:25:10 -0700754 bundle = isolateserver.fetch_isolated(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000755 isolated_hash=isolated_hash,
756 storage=storage,
757 cache=cache,
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500758 outdir=run_dir,
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000759 require_command=True)
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -0400760 except isolated_format.IsolatedError:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400761 on_error.report(None)
Vadim Shtayura51aba362014-05-14 15:39:23 -0700762 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000763
Vadim Shtayura7f7459c2014-09-04 13:25:10 -0700764 change_tree_read_only(run_dir, bundle.read_only)
765 cwd = os.path.normpath(os.path.join(run_dir, bundle.relative_cwd))
766 command = bundle.command + extra_args
Vadim Shtayurae4a780b2014-01-17 13:18:53 -0800767
John Abd-El-Malek3f998682014-09-17 17:48:09 -0700768 file_path.ensure_command_has_abs_path(command, cwd)
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500769 command = process_command(command, out_dir)
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500770 logging.info('Running %s, cwd=%s' % (command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000771
772 # TODO(csharp): This should be specified somewhere else.
773 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
774 # Add a rotating log file if one doesn't already exist.
775 env = os.environ.copy()
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000776 if MAIN_DIR:
777 env.setdefault('RUN_TEST_CASES_LOG_FILE',
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000778 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000779 try:
Vadim Shtayura51aba362014-05-14 15:39:23 -0700780 sys.stdout.flush()
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000781 with tools.Profiler('RunTest'):
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500782 result = subprocess.call(command, cwd=cwd, env=env)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700783 logging.info(
784 'Command finished with exit code %d (%s)',
785 result, hex(0xffffffff & result))
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400786 except OSError:
787 on_error.report('Failed to run %s; cwd=%s' % (command, cwd))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500788 result = 1
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500789
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000790 finally:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500791 try:
Kenneth Russell61d42352014-09-15 11:41:16 -0700792 if leak_temp_dir:
793 logging.warning('Deliberately leaking %s for later examination',
794 run_dir)
795 else:
796 try:
797 if not rmtree(run_dir):
798 print >> sys.stderr, (
799 'Failed to delete the temporary directory, forcibly failing\n'
800 'the task because of it. No zombie process can outlive a\n'
801 'successful task run and still be marked as successful.\n'
802 'Fix your stuff.')
803 result = result or 1
804 except OSError:
805 logging.warning('Leaking %s', run_dir)
806 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700807
808 # HACK(vadimsh): On Windows rmtree(run_dir) call above has
809 # a synchronization effect: it finishes only when all task child processes
810 # terminate (since a running process locks *.exe file). Examine out_dir
811 # only after that call completes (since child processes may
812 # write to out_dir too and we need to wait for them to finish).
813
814 # Upload out_dir and generate a .isolated file out of this directory.
815 # It is only done if files were written in the directory.
816 if os.listdir(out_dir):
817 with tools.Profiler('ArchiveOutput'):
818 results = isolateserver.archive_files_to_storage(
819 storage, [out_dir], None)
820 # TODO(maruel): Implement side-channel to publish this information.
821 output_data = {
822 'hash': results[0][0],
823 'namespace': storage.namespace,
824 'storage': storage.location,
825 }
826 sys.stdout.flush()
827 print(
828 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
829 tools.format_json(output_data, dense=True))
830
831 finally:
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400832 try:
833 if os.path.isdir(out_dir) and not rmtree(out_dir):
834 result = result or 1
835 except OSError:
836 # The error was already printed out. Report it but that's it.
837 on_error.report(None)
838 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700839
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500840 return result
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000841
842
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500843def main(args):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000844 tools.disable_buffering()
845 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000846 usage='%prog <options>',
847 version=__version__,
848 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000849
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500850 data_group = optparse.OptionGroup(parser, 'Data source')
851 data_group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000852 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000853 metavar='FILE',
854 help='File/url describing what to map or run')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500855 data_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000856 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000857 help='Hash of the .isolated to grab from the hash table')
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500858 isolateserver.add_isolate_server_options(data_group, True)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500859 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000860
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500861 cache_group = optparse.OptionGroup(parser, 'Cache management')
862 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000863 '--cache',
864 default='cache',
865 metavar='DIR',
866 help='Cache directory, default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500867 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000868 '--max-cache-size',
869 type='int',
870 metavar='NNN',
871 default=20*1024*1024*1024,
872 help='Trim if the cache gets larger than this value, default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500873 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000874 '--min-free-space',
875 type='int',
876 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000877 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000878 help='Trim if disk free space becomes lower than this value, '
879 'default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500880 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000881 '--max-items',
882 type='int',
883 metavar='NNN',
884 default=100000,
885 help='Trim if more than this number of items are in the cache '
886 'default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500887 parser.add_option_group(cache_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000888
Kenneth Russell61d42352014-09-15 11:41:16 -0700889 debug_group = optparse.OptionGroup(parser, 'Debugging')
890 debug_group.add_option(
891 '--leak-temp-dir',
892 action='store_true',
893 help='Deliberately leak isolate\'s temp dir for later examination '
894 '[default: %default]')
895 parser.add_option_group(debug_group)
896
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800897 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500898 options, args = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800899 auth.process_auth_options(parser, options)
Vadim Shtayura2a8db3b2014-09-09 13:49:56 -0700900 isolateserver.process_isolate_server_options(parser, options)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000901
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000902 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000903 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000904 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000905
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000906 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000907 policies = CachePolicies(
908 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000909
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400910 # |options.cache| path may not exist until DiskCache() instance is created.
911 cache = DiskCache(
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400912 options.cache, policies, isolated_format.get_hash_algo(options.namespace))
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700913
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400914 remote = options.isolate_server or options.indir
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700915 if file_path.is_url(remote):
916 auth.ensure_logged_in(remote)
917
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400918 with isolateserver.get_storage(remote, options.namespace) as storage:
919 # Hashing schemes used by |storage| and |cache| MUST match.
920 assert storage.hash_algo == cache.hash_algo
921 return run_tha_test(
Kenneth Russell61d42352014-09-15 11:41:16 -0700922 options.isolated or options.hash, storage, cache,
923 options.leak_temp_dir, args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000924
925
926if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000927 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000928 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500929 sys.exit(main(sys.argv[1:]))