blob: 182c76ff29a80faf450d2a957e28f3cce93816cb [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
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000041import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000042
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000043
vadimsh@chromium.org85071062013-08-21 23:37:45 +000044# Absolute path to this file (can be None if running from zip on Mac).
45THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000046
47# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000048BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000049
50# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000051if zip_package.get_main_script_path():
52 MAIN_DIR = os.path.dirname(
53 os.path.abspath(zip_package.get_main_script_path()))
54else:
55 # This happens when 'import run_isolated' is executed at the python
56 # interactive prompt, in that case __file__ is undefined.
57 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000058
maruel@chromium.org6b365dc2012-10-18 19:17:56 +000059# Types of action accepted by link_file().
maruel@chromium.orgba6489b2013-07-11 20:23:33 +000060HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY = range(1, 5)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000061
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000062# The name of the log file to use.
63RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
64
csharp@chromium.orge217f302012-11-22 16:51:53 +000065# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000066RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000067
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000068
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000069def get_as_zip_package(executable=True):
70 """Returns ZipPackage with this module and all its dependencies.
71
72 If |executable| is True will store run_isolated.py as __main__.py so that
73 zip package is directly executable be python.
74 """
75 # Building a zip package when running from another zip package is
76 # unsupported and probably unneeded.
77 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000078 assert THIS_FILE_PATH
79 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000080 package = zip_package.ZipPackage(root=BASE_DIR)
81 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000082 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080083 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000084 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
85 package.add_directory(os.path.join(BASE_DIR, 'utils'))
86 return package
87
88
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -050089def hardlink(source, link_name):
90 """Hardlinks a file.
91
92 Add support for os.link() on Windows.
93 """
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000094 if sys.platform == 'win32':
95 if not ctypes.windll.kernel32.CreateHardLinkW(
96 unicode(link_name), unicode(source), 0):
97 raise OSError()
98 else:
99 os.link(source, link_name)
100
101
102def readable_copy(outfile, infile):
103 """Makes a copy of the file that is readable by everyone."""
csharp@chromium.org59d116d2013-07-05 18:04:08 +0000104 shutil.copy2(infile, outfile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000105 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
106 stat.S_IRGRP | stat.S_IROTH)
107 os.chmod(outfile, read_enabled_mode)
108
109
110def link_file(outfile, infile, action):
111 """Links a file. The type of link depends on |action|."""
112 logging.debug('Mapping %s to %s' % (infile, outfile))
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000113 if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000114 raise ValueError('Unknown mapping action %s' % action)
115 if not os.path.isfile(infile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000116 raise isolateserver.MappingError('%s is missing' % infile)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000117 if os.path.isfile(outfile):
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000118 raise isolateserver.MappingError(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000119 '%s already exist; insize:%d; outsize:%d' %
120 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
121
122 if action == COPY:
123 readable_copy(outfile, infile)
124 elif action == SYMLINK and sys.platform != 'win32':
125 # On windows, symlink are converted to hardlink and fails over to copy.
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000126 os.symlink(infile, outfile) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000127 else:
128 try:
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500129 hardlink(infile, outfile)
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000130 except OSError as e:
131 if action == HARDLINK:
maruel@chromium.org9958e4a2013-09-17 00:01:48 +0000132 raise isolateserver.MappingError(
maruel@chromium.orgba6489b2013-07-11 20:23:33 +0000133 'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000134 # Probably a different file system.
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000135 logging.warning(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000136 'Failed to hardlink, failing back to copy %s to %s' % (
137 infile, outfile))
138 readable_copy(outfile, infile)
139
140
Marc-Antoine Rueld2d4d4f2013-11-10 14:32:38 -0500141def set_read_only(path, read_only):
142 """Sets or resets the write bit on a file or directory.
143
144 Zaps out access to 'group' and 'others'.
145 """
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500146 assert isinstance(read_only, bool), read_only
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000147 mode = os.lstat(path).st_mode
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500148 # TODO(maruel): Stop removing GO bits.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000149 if read_only:
150 mode = mode & 0500
151 else:
152 mode = mode | 0200
153 if hasattr(os, 'lchmod'):
154 os.lchmod(path, mode) # pylint: disable=E1101
155 else:
156 if stat.S_ISLNK(mode):
157 # Skip symlink without lchmod() support.
Marc-Antoine Ruel45dc2902013-12-05 14:54:20 -0500158 logging.debug(
159 'Can\'t change %sw bit on symlink %s',
160 '-' if read_only else '+', path)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000161 return
162
163 # TODO(maruel): Implement proper DACL modification on Windows.
164 os.chmod(path, mode)
165
166
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500167def make_tree_read_only(root):
168 """Makes all the files in the directories read only.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000169
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500170 Also makes the directories read only, only if it makes sense on the platform.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500171
172 This means no file can be created or deleted.
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500173 """
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500174 logging.debug('make_tree_read_only(%s)', root)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500175 assert os.path.isabs(root), root
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500176 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
177 for filename in filenames:
178 set_read_only(os.path.join(dirpath, filename), True)
179 if sys.platform != 'win32':
180 # It must not be done on Windows.
181 for dirname in dirnames:
182 set_read_only(os.path.join(dirpath, dirname), True)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500183 if sys.platform != 'win32':
184 set_read_only(root, True)
185
186
187def make_tree_files_read_only(root):
188 """Makes all the files in the directories read only but not the directories
189 themselves.
190
191 This means files can be created or deleted.
192 """
193 logging.debug('make_tree_files_read_only(%s)', root)
194 assert os.path.isabs(root), root
195 if sys.platform != 'win32':
196 set_read_only(root, False)
197 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
198 for filename in filenames:
199 set_read_only(os.path.join(dirpath, filename), True)
200 if sys.platform != 'win32':
201 # It must not be done on Windows.
202 for dirname in dirnames:
203 set_read_only(os.path.join(dirpath, dirname), False)
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500204
205
206def make_tree_writeable(root):
207 """Makes all the files in the directories writeable.
208
209 Also makes the directories writeable, only if it makes sense on the platform.
210
211 It is different from make_tree_deleteable() because it unconditionally affects
212 the files.
213 """
214 logging.debug('make_tree_writeable(%s)', root)
215 assert os.path.isabs(root), root
216 if sys.platform != 'win32':
217 set_read_only(root, False)
218 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
219 for filename in filenames:
220 set_read_only(os.path.join(dirpath, filename), False)
221 if sys.platform != 'win32':
222 # It must not be done on Windows.
223 for dirname in dirnames:
224 set_read_only(os.path.join(dirpath, dirname), False)
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500225
226
227def make_tree_deleteable(root):
228 """Changes the appropriate permissions so the files in the directories can be
229 deleted.
230
231 On Windows, the files are modified. On other platforms, modify the directory.
Marc-Antoine Ruel4727bd52013-11-22 14:44:56 -0500232 It only does the minimum so the files can be deleted safely.
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500233
234 Warning on Windows: since file permission is modified, the file node is
235 modified. This means that for hard-linked files, every directory entry for the
236 file node has its file permission modified.
237 """
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500238 logging.debug('make_tree_deleteable(%s)', root)
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500239 assert os.path.isabs(root), root
240 if sys.platform != 'win32':
241 set_read_only(root, False)
242 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
243 if sys.platform == 'win32':
244 for filename in filenames:
245 set_read_only(os.path.join(dirpath, filename), False)
246 else:
247 for dirname in dirnames:
248 set_read_only(os.path.join(dirpath, dirname), False)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000249
250
251def rmtree(root):
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400252 """Wrapper around shutil.rmtree() to retry automatically on Windows.
253
254 On Windows, forcibly kills processes that are found to interfere with the
255 deletion.
256
257 Returns:
258 True on normal execution, False if berserk techniques (like killing
259 processes) had to be used.
260 """
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500261 make_tree_deleteable(root)
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500262 logging.info('rmtree(%s)', root)
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400263 if sys.platform != 'win32':
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000264 shutil.rmtree(root)
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400265 return True
266
267 # Windows is more 'challenging'. First tries the soft way: tries 3 times to
268 # delete and sleep a bit in between.
269 max_tries = 3
270 for i in xrange(max_tries):
271 errors = []
272 shutil.rmtree(root, onerror=lambda *args: errors.append(args))
273 if not errors:
274 return True
275 if i == max_tries - 1:
276 sys.stderr.write(
277 'Failed to delete %s. The following files remain:\n' % root)
278 for _, path, _ in errors:
279 sys.stderr.write('- %s\n' % path)
280 else:
281 delay = (i+1)*2
282 sys.stderr.write(
283 'Failed to delete %s (%d files remaining).\n'
284 ' Maybe the test has a subprocess outliving it.\n'
285 ' Sleeping %d seconds.\n' %
286 (root, len(errors), delay))
287 time.sleep(delay)
288
289 # The soft way was not good enough. Try the hard way. Enumerates both:
290 # - all child processes from this process.
291 # - processes where the main executable in inside 'root'. The reason is that
292 # the ancestry may be broken so stray grand-children processes could be
293 # undetected by the first technique.
294 # This technique is not fool-proof but gets mostly there.
295 def get_processes():
296 processes = threading_utils.enum_processes_win()
297 tree_processes = threading_utils.filter_processes_tree_win(processes)
298 dir_processes = threading_utils.filter_processes_dir_win(processes, root)
299 # Convert to dict to remove duplicates.
300 processes = {p.ProcessId: p for p in tree_processes}
301 processes.update((p.ProcessId, p) for p in dir_processes)
302 processes.pop(os.getpid())
303 return processes
304
305 for i in xrange(3):
306 sys.stderr.write('Enumerating processes:\n')
307 processes = get_processes()
308 if not processes:
309 break
310 for _, proc in sorted(processes.iteritems()):
311 sys.stderr.write(
312 '- pid %d; Handles: %d; Exe: %s; Cmd: %s\n' % (
313 proc.ProcessId,
314 proc.HandleCount,
315 proc.ExecutablePath,
316 proc.CommandLine))
317 sys.stderr.write('Terminating %d processes.\n' % len(processes))
318 for pid in sorted(processes):
319 try:
320 # Killing is asynchronous.
321 os.kill(pid, 9)
322 sys.stderr.write('- %d killed\n' % pid)
323 except OSError:
324 sys.stderr.write('- failed to kill %s\n' % pid)
325 if i < 2:
326 time.sleep((i+1)*2)
327 else:
328 processes = get_processes()
329 if processes:
330 sys.stderr.write('Failed to terminate processes.\n')
331 raise errors[0][0][0], errors[0][0][1], errors[0][0][2]
332
333 # Now that annoying processes in root are evicted, try again.
334 errors = []
335 shutil.rmtree(root, onerror=lambda *args: errors.append(args))
336 if errors:
337 # There's no hope.
338 sys.stderr.write(
339 'Failed to delete %s. The following files remain:\n' % root)
340 for _, path, _ in errors:
341 sys.stderr.write('- %s\n' % path)
342 raise errors[0][0][0], errors[0][0][1], errors[0][0][2]
343 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000344
345
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000346def try_remove(filepath):
347 """Removes a file without crashing even if it doesn't exist."""
348 try:
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500349 # TODO(maruel): Not do it unless necessary since it slows this function
350 # down.
351 if sys.platform == 'win32':
352 # Deleting a read-only file will fail if it is read-only.
353 set_read_only(filepath, False)
354 else:
355 # Deleting a read-only file will fail if the directory is read-only.
356 set_read_only(os.path.dirname(filepath), False)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000357 os.remove(filepath)
358 except OSError:
359 pass
360
361
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000362def is_same_filesystem(path1, path2):
363 """Returns True if both paths are on the same filesystem.
364
365 This is required to enable the use of hardlinks.
366 """
367 assert os.path.isabs(path1), path1
368 assert os.path.isabs(path2), path2
369 if sys.platform == 'win32':
370 # If the drive letter mismatches, assume it's a separate partition.
371 # TODO(maruel): It should look at the underlying drive, a drive letter could
372 # be a mount point to a directory on another drive.
373 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
374 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
375 if path1[0].lower() != path2[0].lower():
376 return False
377 return os.stat(path1).st_dev == os.stat(path2).st_dev
378
379
380def get_free_space(path):
381 """Returns the number of free bytes."""
382 if sys.platform == 'win32':
383 free_bytes = ctypes.c_ulonglong(0)
384 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
385 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
386 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000387 # For OSes other than Windows.
388 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000389 return f.f_bfree * f.f_frsize
390
391
392def make_temp_dir(prefix, root_dir):
393 """Returns a temporary directory on the same file system as root_dir."""
394 base_temp_dir = None
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500395 if root_dir and not is_same_filesystem(root_dir, tempfile.gettempdir()):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000396 base_temp_dir = os.path.dirname(root_dir)
397 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
398
399
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000400class CachePolicies(object):
401 def __init__(self, max_cache_size, min_free_space, max_items):
402 """
403 Arguments:
404 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
405 cache is effectively a leak.
406 - min_free_space: Trim if disk free space becomes lower than this value. If
407 0, it unconditionally fill the disk.
408 - max_items: Maximum number of items to keep in the cache. If 0, do not
409 enforce a limit.
410 """
411 self.max_cache_size = max_cache_size
412 self.min_free_space = min_free_space
413 self.max_items = max_items
414
415
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000416class DiskCache(isolateserver.LocalCache):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000417 """Stateful LRU cache in a flat hash table in a directory.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000418
419 Saves its state as json file.
420 """
421 STATE_FILE = 'state.json'
422
Vadim Shtayurae0ab1902014-04-29 10:55:27 -0700423 def __init__(self, cache_dir, policies, hash_algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000424 """
425 Arguments:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000426 cache_dir: directory where to place the cache.
427 policies: cache retention policies.
428 algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000429 """
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000430 super(DiskCache, self).__init__()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000431 self.cache_dir = cache_dir
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000432 self.policies = policies
Vadim Shtayurae0ab1902014-04-29 10:55:27 -0700433 self.hash_algo = hash_algo
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000434 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000435
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000436 # All protected methods (starting with '_') except _path should be called
437 # with this lock locked.
438 self._lock = threading_utils.LockWithAssert()
439 self._lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000440
441 # Profiling values.
442 self._added = []
443 self._removed = []
444 self._free_disk = 0
445
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000446 with tools.Profiler('Setup'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000447 with self._lock:
448 self._load()
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000449
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000450 def __enter__(self):
451 return self
452
453 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000454 with tools.Profiler('CleanupTrimming'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000455 with self._lock:
456 self._trim()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000457
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000458 logging.info(
459 '%5d (%8dkb) added',
460 len(self._added), sum(self._added) / 1024)
461 logging.info(
462 '%5d (%8dkb) current',
463 len(self._lru),
464 sum(self._lru.itervalues()) / 1024)
465 logging.info(
466 '%5d (%8dkb) removed',
467 len(self._removed), sum(self._removed) / 1024)
468 logging.info(
469 ' %8dkb free',
470 self._free_disk / 1024)
maruel@chromium.org41601642013-09-18 19:40:46 +0000471 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000472
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000473 def cached_set(self):
474 with self._lock:
475 return self._lru.keys_set()
476
477 def touch(self, digest, size):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500478 """Verifies an actual file is valid.
479
480 Note that is doesn't compute the hash so it could still be corrupted if the
481 file size didn't change.
482
483 TODO(maruel): More stringent verification while keeping the check fast.
484 """
485 # Do the check outside the lock.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000486 if not isolateserver.is_valid_file(self._path(digest), size):
487 return False
488
489 # Update it's LRU position.
490 with self._lock:
491 if digest not in self._lru:
492 return False
493 self._lru.touch(digest)
494 return True
495
496 def evict(self, digest):
497 with self._lock:
498 self._lru.pop(digest)
499 self._delete_file(digest, isolateserver.UNKNOWN_FILE_SIZE)
500
501 def read(self, digest):
502 with open(self._path(digest), 'rb') as f:
503 return f.read()
504
505 def write(self, digest, content):
506 path = self._path(digest)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500507 # A stale broken file may remain. It is possible for the file to have write
508 # access bit removed which would cause the file_write() call to fail to open
509 # in write mode. Take no chance here.
510 try_remove(path)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000511 try:
512 size = isolateserver.file_write(path, content)
513 except:
514 # There are two possible places were an exception can occur:
515 # 1) Inside |content| generator in case of network or unzipping errors.
516 # 2) Inside file_write itself in case of disk IO errors.
517 # In any case delete an incomplete file and propagate the exception to
518 # caller, it will be logged there.
519 try_remove(path)
520 raise
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500521 # Make the file read-only in the cache. This has a few side-effects since
522 # the file node is modified, so every directory entries to this file becomes
523 # read-only. It's fine here because it is a new file.
524 set_read_only(path, True)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000525 with self._lock:
526 self._add(digest, size)
527
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500528 def hardlink(self, digest, dest, file_mode):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500529 """Hardlinks the file to |dest|.
530
531 Note that the file permission bits are on the file node, not the directory
532 entry, so changing the access bit on any of the directory entries for the
533 file node will affect them all.
534 """
535 path = self._path(digest)
536 link_file(dest, path, HARDLINK)
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500537 if file_mode is not None:
538 # Ignores all other bits.
539 os.chmod(dest, file_mode & 0500)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000540
541 def _load(self):
542 """Loads state of the cache from json file."""
543 self._lock.assert_locked()
544
545 if not os.path.isdir(self.cache_dir):
546 os.makedirs(self.cache_dir)
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500547 else:
548 # Make sure the cache is read-only.
549 # TODO(maruel): Calculate the cost and optimize the performance
550 # accordingly.
551 make_tree_read_only(self.cache_dir)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000552
553 # Load state of the cache.
554 if os.path.isfile(self.state_file):
555 try:
556 self._lru = lru.LRUDict.load(self.state_file)
557 except ValueError as err:
558 logging.error('Failed to load cache state: %s' % (err,))
559 # Don't want to keep broken state file.
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500560 try_remove(self.state_file)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000561
562 # Ensure that all files listed in the state still exist and add new ones.
563 previous = self._lru.keys_set()
564 unknown = []
565 for filename in os.listdir(self.cache_dir):
566 if filename == self.STATE_FILE:
567 continue
568 if filename in previous:
569 previous.remove(filename)
570 continue
571 # An untracked file.
Vadim Shtayurae0ab1902014-04-29 10:55:27 -0700572 if not isolateserver.is_valid_hash(filename, self.hash_algo):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000573 logging.warning('Removing unknown file %s from cache', filename)
574 try_remove(self._path(filename))
575 continue
576 # File that's not referenced in 'state.json'.
577 # TODO(vadimsh): Verify its SHA1 matches file name.
578 logging.warning('Adding unknown file %s to cache', filename)
579 unknown.append(filename)
580
581 if unknown:
582 # Add as oldest files. They will be deleted eventually if not accessed.
583 self._add_oldest_list(unknown)
584 logging.warning('Added back %d unknown files', len(unknown))
585
586 if previous:
587 # Filter out entries that were not found.
588 logging.warning('Removed %d lost files', len(previous))
589 for filename in previous:
590 self._lru.pop(filename)
591 self._trim()
592
593 def _save(self):
594 """Saves the LRU ordering."""
595 self._lock.assert_locked()
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500596 if sys.platform != 'win32':
597 d = os.path.dirname(self.state_file)
598 if os.path.isdir(d):
599 # Necessary otherwise the file can't be created.
600 set_read_only(d, False)
601 if os.path.isfile(self.state_file):
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500602 set_read_only(self.state_file, False)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000603 self._lru.save(self.state_file)
604
605 def _trim(self):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000606 """Trims anything we don't know, make sure enough free space exists."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000607 self._lock.assert_locked()
608
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000609 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000610 if self.policies.max_cache_size:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000611 total_size = sum(self._lru.itervalues())
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000612 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000613 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000614
615 # Ensure maximum number of items in the cache.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000616 if self.policies.max_items and len(self._lru) > self.policies.max_items:
617 for _ in xrange(len(self._lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000618 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000619
620 # Ensure enough free space.
621 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000622 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000623 while (
624 self.policies.min_free_space and
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000625 self._lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000626 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000627 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000628 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000629 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000630 if trimmed_due_to_space:
Marc-Antoine Ruel2cbc1242014-07-16 11:05:04 -0400631 total_usage = sum(self._lru.itervalues())
632 usage_percent = 0.
633 if total_usage:
634 usage_percent = 100. * self.policies.max_cache_size / float(total_usage)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000635 logging.warning(
636 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
637 'cache (%.1f%% of its maximum capacity)',
638 self._free_disk / 1024.,
Marc-Antoine Ruel2cbc1242014-07-16 11:05:04 -0400639 total_usage / 1024.,
640 usage_percent)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000641 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000642
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000643 def _path(self, digest):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000644 """Returns the path to one item."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000645 return os.path.join(self.cache_dir, digest)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000646
647 def _remove_lru_file(self):
648 """Removes the last recently used file and returns its size."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000649 self._lock.assert_locked()
650 digest, size = self._lru.pop_oldest()
651 self._delete_file(digest, size)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000652 return size
653
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000654 def _add(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000655 """Adds an item into LRU cache marking it as a newest one."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000656 self._lock.assert_locked()
657 if size == isolateserver.UNKNOWN_FILE_SIZE:
658 size = os.stat(self._path(digest)).st_size
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000659 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000660 self._lru.add(digest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000661
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000662 def _add_oldest_list(self, digests):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000663 """Adds a bunch of items into LRU cache marking them as oldest ones."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000664 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000665 pairs = []
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000666 for digest in digests:
667 size = os.stat(self._path(digest)).st_size
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000668 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000669 pairs.append((digest, size))
670 self._lru.batch_insert_oldest(pairs)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000671
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000672 def _delete_file(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000673 """Deletes cache file from the file system."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000674 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000675 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000676 if size == isolateserver.UNKNOWN_FILE_SIZE:
677 size = os.stat(self._path(digest)).st_size
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500678 try_remove(self._path(digest))
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000679 self._removed.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000680 except OSError as e:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000681 logging.error('Error attempting to delete a file %s:\n%s' % (digest, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000682
683
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500684def change_tree_read_only(rootdir, read_only):
685 """Changes the tree read-only bits according to the read_only specification.
686
687 The flag can be 0, 1 or 2, which will affect the possibility to modify files
688 and create or delete files.
689 """
690 if read_only == 2:
691 # Files and directories (except on Windows) are marked read only. This
692 # inhibits modifying, creating or deleting files in the test directory,
693 # except on Windows where creating and deleting files is still possible.
694 make_tree_read_only(rootdir)
695 elif read_only == 1:
696 # Files are marked read only but not the directories. This inhibits
697 # modifying files but creating or deleting files is still possible.
698 make_tree_files_read_only(rootdir)
699 elif read_only in (0, None):
700 # Anything can be modified. This is the default in the .isolated file
701 # format.
702 #
703 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
704 # is not yet changed to verify the hash of the content of the files it is
705 # looking at, so that if a test modifies an input file, the file must be
706 # deleted.
707 make_tree_writeable(rootdir)
708 else:
709 raise ValueError(
710 'change_tree_read_only(%s, %s): Unknown flag %s' %
711 (rootdir, read_only, read_only))
712
713
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500714def process_command(command, out_dir):
715 """Replaces isolated specific variables in a command line."""
Vadim Shtayura51aba362014-05-14 15:39:23 -0700716 filtered = []
717 for arg in command:
718 if '${ISOLATED_OUTDIR}' in arg:
719 arg = arg.replace('${ISOLATED_OUTDIR}', out_dir).replace('/', os.sep)
720 filtered.append(arg)
721 return filtered
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500722
723
Vadim Shtayurae0ab1902014-04-29 10:55:27 -0700724def run_tha_test(isolated_hash, storage, cache, extra_args):
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500725 """Downloads the dependencies in the cache, hardlinks them into a temporary
726 directory and runs the executable from there.
727
728 A temporary directory is created to hold the output files. The content inside
729 this directory will be uploaded back to |storage| packaged as a .isolated
730 file.
731
732 Arguments:
733 isolated_hash: the sha-1 of the .isolated file that must be retrieved to
734 recreate the tree of files to run the target executable.
735 storage: an isolateserver.Storage object to retrieve remote objects. This
736 object has a reference to an isolateserver.StorageApi, which does
737 the actual I/O.
738 cache: an isolateserver.LocalCache to keep from retrieving the same objects
739 constantly by caching the objects retrieved. Can be on-disk or
740 in-memory.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500741 extra_args: optional arguments to add to the command stated in the .isolate
742 file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000743 """
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500744 run_dir = make_temp_dir('run_tha_test', cache.cache_dir)
Vadim Shtayura51aba362014-05-14 15:39:23 -0700745 out_dir = unicode(make_temp_dir('isolated_out', cache.cache_dir))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500746 result = 0
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000747 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000748 try:
749 settings = isolateserver.fetch_isolated(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000750 isolated_hash=isolated_hash,
751 storage=storage,
752 cache=cache,
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500753 outdir=run_dir,
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000754 require_command=True)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400755 except isolateserver.ConfigError:
756 on_error.report(None)
Vadim Shtayura51aba362014-05-14 15:39:23 -0700757 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000758
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500759 change_tree_read_only(run_dir, settings.read_only)
760 cwd = os.path.normpath(os.path.join(run_dir, settings.relative_cwd))
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500761 command = settings.command + extra_args
Vadim Shtayurae4a780b2014-01-17 13:18:53 -0800762
763 # subprocess.call doesn't consider 'cwd' when searching for executable.
764 # Yet isolate can specify command relative to 'cwd'. Convert it to absolute
765 # path if necessary.
766 if not os.path.isabs(command[0]):
767 command[0] = os.path.abspath(os.path.join(cwd, command[0]))
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500768 command = process_command(command, out_dir)
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500769 logging.info('Running %s, cwd=%s' % (command, cwd))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000770
771 # TODO(csharp): This should be specified somewhere else.
772 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
773 # Add a rotating log file if one doesn't already exist.
774 env = os.environ.copy()
maruel@chromium.org814d23f2013-10-01 19:08:00 +0000775 if MAIN_DIR:
776 env.setdefault('RUN_TEST_CASES_LOG_FILE',
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000777 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000778 try:
Vadim Shtayura51aba362014-05-14 15:39:23 -0700779 sys.stdout.flush()
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000780 with tools.Profiler('RunTest'):
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500781 result = subprocess.call(command, cwd=cwd, env=env)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700782 logging.info(
783 'Command finished with exit code %d (%s)',
784 result, hex(0xffffffff & result))
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400785 except OSError:
786 on_error.report('Failed to run %s; cwd=%s' % (command, cwd))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500787 result = 1
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500788
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000789 finally:
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500790 try:
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500791 try:
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400792 if not rmtree(run_dir):
793 print >> sys.stderr, (
794 'Failed to delete the temporary directory, forcibly failing the\n'
795 'task because of it. No zombie process can outlive a successful\n'
796 'task run and still be marked as successful. Fix your stuff.')
797 result = result or 1
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500798 except OSError:
799 logging.warning('Leaking %s', run_dir)
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400800 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700801
802 # HACK(vadimsh): On Windows rmtree(run_dir) call above has
803 # a synchronization effect: it finishes only when all task child processes
804 # terminate (since a running process locks *.exe file). Examine out_dir
805 # only after that call completes (since child processes may
806 # write to out_dir too and we need to wait for them to finish).
807
808 # Upload out_dir and generate a .isolated file out of this directory.
809 # It is only done if files were written in the directory.
810 if os.listdir(out_dir):
811 with tools.Profiler('ArchiveOutput'):
812 results = isolateserver.archive_files_to_storage(
813 storage, [out_dir], None)
814 # TODO(maruel): Implement side-channel to publish this information.
815 output_data = {
816 'hash': results[0][0],
817 'namespace': storage.namespace,
818 'storage': storage.location,
819 }
820 sys.stdout.flush()
821 print(
822 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
823 tools.format_json(output_data, dense=True))
824
825 finally:
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400826 try:
827 if os.path.isdir(out_dir) and not rmtree(out_dir):
828 result = result or 1
829 except OSError:
830 # The error was already printed out. Report it but that's it.
831 on_error.report(None)
832 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700833
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500834 return result
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000835
836
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500837def main(args):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000838 tools.disable_buffering()
839 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000840 usage='%prog <options>',
841 version=__version__,
842 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000843
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500844 data_group = optparse.OptionGroup(parser, 'Data source')
845 data_group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000846 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000847 metavar='FILE',
848 help='File/url describing what to map or run')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500849 data_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000850 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000851 help='Hash of the .isolated to grab from the hash table')
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500852 isolateserver.add_isolate_server_options(data_group, True)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500853 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000854
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500855 cache_group = optparse.OptionGroup(parser, 'Cache management')
856 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000857 '--cache',
858 default='cache',
859 metavar='DIR',
860 help='Cache directory, default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500861 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000862 '--max-cache-size',
863 type='int',
864 metavar='NNN',
865 default=20*1024*1024*1024,
866 help='Trim if the cache gets larger than this value, default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500867 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000868 '--min-free-space',
869 type='int',
870 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000871 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000872 help='Trim if disk free space becomes lower than this value, '
873 'default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500874 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000875 '--max-items',
876 type='int',
877 metavar='NNN',
878 default=100000,
879 help='Trim if more than this number of items are in the cache '
880 'default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500881 parser.add_option_group(cache_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000882
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800883 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500884 options, args = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800885 auth.process_auth_options(parser, options)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500886 isolateserver.process_isolate_server_options(data_group, options)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000887
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000888 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000889 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000890 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000891
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000892 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000893 policies = CachePolicies(
894 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000895
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400896 # |options.cache| path may not exist until DiskCache() instance is created.
897 cache = DiskCache(
898 options.cache, policies, isolateserver.get_hash_algo(options.namespace))
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700899
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400900 remote = options.isolate_server or options.indir
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700901 if file_path.is_url(remote):
902 auth.ensure_logged_in(remote)
903
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400904 with isolateserver.get_storage(remote, options.namespace) as storage:
905 # Hashing schemes used by |storage| and |cache| MUST match.
906 assert storage.hash_algo == cache.hash_algo
907 return run_tha_test(
908 options.isolated or options.hash, storage, cache, args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000909
910
911if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000912 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000913 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500914 sys.exit(main(sys.argv[1:]))