blob: 813475eed23767bb3e7cd3abc0298259fd15ca27 [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):
Marc-Antoine Ruela3d702c2014-08-04 17:45:48 -0400271 # errors is a list of tuple(function, path, excinfo).
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400272 errors = []
273 shutil.rmtree(root, onerror=lambda *args: errors.append(args))
274 if not errors:
275 return True
276 if i == max_tries - 1:
277 sys.stderr.write(
278 'Failed to delete %s. The following files remain:\n' % root)
279 for _, path, _ in errors:
280 sys.stderr.write('- %s\n' % path)
281 else:
282 delay = (i+1)*2
283 sys.stderr.write(
284 'Failed to delete %s (%d files remaining).\n'
285 ' Maybe the test has a subprocess outliving it.\n'
286 ' Sleeping %d seconds.\n' %
287 (root, len(errors), delay))
288 time.sleep(delay)
289
290 # The soft way was not good enough. Try the hard way. Enumerates both:
291 # - all child processes from this process.
292 # - processes where the main executable in inside 'root'. The reason is that
293 # the ancestry may be broken so stray grand-children processes could be
294 # undetected by the first technique.
295 # This technique is not fool-proof but gets mostly there.
296 def get_processes():
297 processes = threading_utils.enum_processes_win()
298 tree_processes = threading_utils.filter_processes_tree_win(processes)
299 dir_processes = threading_utils.filter_processes_dir_win(processes, root)
300 # Convert to dict to remove duplicates.
301 processes = {p.ProcessId: p for p in tree_processes}
302 processes.update((p.ProcessId, p) for p in dir_processes)
303 processes.pop(os.getpid())
304 return processes
305
306 for i in xrange(3):
307 sys.stderr.write('Enumerating processes:\n')
308 processes = get_processes()
309 if not processes:
310 break
311 for _, proc in sorted(processes.iteritems()):
312 sys.stderr.write(
313 '- pid %d; Handles: %d; Exe: %s; Cmd: %s\n' % (
314 proc.ProcessId,
315 proc.HandleCount,
316 proc.ExecutablePath,
317 proc.CommandLine))
318 sys.stderr.write('Terminating %d processes.\n' % len(processes))
319 for pid in sorted(processes):
320 try:
321 # Killing is asynchronous.
322 os.kill(pid, 9)
323 sys.stderr.write('- %d killed\n' % pid)
324 except OSError:
325 sys.stderr.write('- failed to kill %s\n' % pid)
326 if i < 2:
327 time.sleep((i+1)*2)
328 else:
329 processes = get_processes()
330 if processes:
331 sys.stderr.write('Failed to terminate processes.\n')
Marc-Antoine Ruela3d702c2014-08-04 17:45:48 -0400332 raise errors[0][2][0], errors[0][2][1], errors[0][2][2]
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400333
334 # Now that annoying processes in root are evicted, try again.
335 errors = []
336 shutil.rmtree(root, onerror=lambda *args: errors.append(args))
337 if errors:
338 # There's no hope.
339 sys.stderr.write(
340 'Failed to delete %s. The following files remain:\n' % root)
341 for _, path, _ in errors:
342 sys.stderr.write('- %s\n' % path)
Marc-Antoine Ruela3d702c2014-08-04 17:45:48 -0400343 raise errors[0][2][0], errors[0][2][1], errors[0][2][2]
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400344 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000345
346
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000347def try_remove(filepath):
348 """Removes a file without crashing even if it doesn't exist."""
349 try:
Marc-Antoine Rueldebee212013-11-11 14:51:03 -0500350 # TODO(maruel): Not do it unless necessary since it slows this function
351 # down.
352 if sys.platform == 'win32':
353 # Deleting a read-only file will fail if it is read-only.
354 set_read_only(filepath, False)
355 else:
356 # Deleting a read-only file will fail if the directory is read-only.
357 set_read_only(os.path.dirname(filepath), False)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000358 os.remove(filepath)
359 except OSError:
360 pass
361
362
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000363def is_same_filesystem(path1, path2):
364 """Returns True if both paths are on the same filesystem.
365
366 This is required to enable the use of hardlinks.
367 """
368 assert os.path.isabs(path1), path1
369 assert os.path.isabs(path2), path2
370 if sys.platform == 'win32':
371 # If the drive letter mismatches, assume it's a separate partition.
372 # TODO(maruel): It should look at the underlying drive, a drive letter could
373 # be a mount point to a directory on another drive.
374 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
375 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
376 if path1[0].lower() != path2[0].lower():
377 return False
378 return os.stat(path1).st_dev == os.stat(path2).st_dev
379
380
381def get_free_space(path):
382 """Returns the number of free bytes."""
383 if sys.platform == 'win32':
384 free_bytes = ctypes.c_ulonglong(0)
385 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
386 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
387 return free_bytes.value
maruel@chromium.orgf43e68b2012-10-15 20:23:10 +0000388 # For OSes other than Windows.
389 f = os.statvfs(path) # pylint: disable=E1101
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000390 return f.f_bfree * f.f_frsize
391
392
393def make_temp_dir(prefix, root_dir):
394 """Returns a temporary directory on the same file system as root_dir."""
395 base_temp_dir = None
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500396 if root_dir and not is_same_filesystem(root_dir, tempfile.gettempdir()):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000397 base_temp_dir = os.path.dirname(root_dir)
398 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
399
400
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000401class CachePolicies(object):
402 def __init__(self, max_cache_size, min_free_space, max_items):
403 """
404 Arguments:
405 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
406 cache is effectively a leak.
407 - min_free_space: Trim if disk free space becomes lower than this value. If
408 0, it unconditionally fill the disk.
409 - max_items: Maximum number of items to keep in the cache. If 0, do not
410 enforce a limit.
411 """
412 self.max_cache_size = max_cache_size
413 self.min_free_space = min_free_space
414 self.max_items = max_items
415
416
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000417class DiskCache(isolateserver.LocalCache):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000418 """Stateful LRU cache in a flat hash table in a directory.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000419
420 Saves its state as json file.
421 """
422 STATE_FILE = 'state.json'
423
Vadim Shtayurae0ab1902014-04-29 10:55:27 -0700424 def __init__(self, cache_dir, policies, hash_algo):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000425 """
426 Arguments:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000427 cache_dir: directory where to place the cache.
428 policies: cache retention policies.
429 algo: hashing algorithm used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000430 """
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000431 super(DiskCache, self).__init__()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000432 self.cache_dir = cache_dir
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000433 self.policies = policies
Vadim Shtayurae0ab1902014-04-29 10:55:27 -0700434 self.hash_algo = hash_algo
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000435 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000436
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000437 # All protected methods (starting with '_') except _path should be called
438 # with this lock locked.
439 self._lock = threading_utils.LockWithAssert()
440 self._lru = lru.LRUDict()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000441
442 # Profiling values.
443 self._added = []
444 self._removed = []
445 self._free_disk = 0
446
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000447 with tools.Profiler('Setup'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000448 with self._lock:
449 self._load()
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000450
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000451 def __enter__(self):
452 return self
453
454 def __exit__(self, _exc_type, _exec_value, _traceback):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000455 with tools.Profiler('CleanupTrimming'):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000456 with self._lock:
457 self._trim()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000458
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000459 logging.info(
460 '%5d (%8dkb) added',
461 len(self._added), sum(self._added) / 1024)
462 logging.info(
463 '%5d (%8dkb) current',
464 len(self._lru),
465 sum(self._lru.itervalues()) / 1024)
466 logging.info(
467 '%5d (%8dkb) removed',
468 len(self._removed), sum(self._removed) / 1024)
469 logging.info(
470 ' %8dkb free',
471 self._free_disk / 1024)
maruel@chromium.org41601642013-09-18 19:40:46 +0000472 return False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000473
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000474 def cached_set(self):
475 with self._lock:
476 return self._lru.keys_set()
477
478 def touch(self, digest, size):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500479 """Verifies an actual file is valid.
480
481 Note that is doesn't compute the hash so it could still be corrupted if the
482 file size didn't change.
483
484 TODO(maruel): More stringent verification while keeping the check fast.
485 """
486 # Do the check outside the lock.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000487 if not isolateserver.is_valid_file(self._path(digest), size):
488 return False
489
490 # Update it's LRU position.
491 with self._lock:
492 if digest not in self._lru:
493 return False
494 self._lru.touch(digest)
495 return True
496
497 def evict(self, digest):
498 with self._lock:
499 self._lru.pop(digest)
500 self._delete_file(digest, isolateserver.UNKNOWN_FILE_SIZE)
501
502 def read(self, digest):
503 with open(self._path(digest), 'rb') as f:
504 return f.read()
505
506 def write(self, digest, content):
507 path = self._path(digest)
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500508 # A stale broken file may remain. It is possible for the file to have write
509 # access bit removed which would cause the file_write() call to fail to open
510 # in write mode. Take no chance here.
511 try_remove(path)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000512 try:
513 size = isolateserver.file_write(path, content)
514 except:
515 # There are two possible places were an exception can occur:
516 # 1) Inside |content| generator in case of network or unzipping errors.
517 # 2) Inside file_write itself in case of disk IO errors.
518 # In any case delete an incomplete file and propagate the exception to
519 # caller, it will be logged there.
520 try_remove(path)
521 raise
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500522 # Make the file read-only in the cache. This has a few side-effects since
523 # the file node is modified, so every directory entries to this file becomes
524 # read-only. It's fine here because it is a new file.
525 set_read_only(path, True)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000526 with self._lock:
527 self._add(digest, size)
528
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500529 def hardlink(self, digest, dest, file_mode):
Marc-Antoine Ruelccafe0e2013-11-08 16:15:36 -0500530 """Hardlinks the file to |dest|.
531
532 Note that the file permission bits are on the file node, not the directory
533 entry, so changing the access bit on any of the directory entries for the
534 file node will affect them all.
535 """
536 path = self._path(digest)
537 link_file(dest, path, HARDLINK)
Marc-Antoine Ruelfb199cf2013-11-12 15:38:12 -0500538 if file_mode is not None:
539 # Ignores all other bits.
540 os.chmod(dest, file_mode & 0500)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000541
542 def _load(self):
543 """Loads state of the cache from json file."""
544 self._lock.assert_locked()
545
546 if not os.path.isdir(self.cache_dir):
547 os.makedirs(self.cache_dir)
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500548 else:
549 # Make sure the cache is read-only.
550 # TODO(maruel): Calculate the cost and optimize the performance
551 # accordingly.
552 make_tree_read_only(self.cache_dir)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000553
554 # Load state of the cache.
555 if os.path.isfile(self.state_file):
556 try:
557 self._lru = lru.LRUDict.load(self.state_file)
558 except ValueError as err:
559 logging.error('Failed to load cache state: %s' % (err,))
560 # Don't want to keep broken state file.
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500561 try_remove(self.state_file)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000562
563 # Ensure that all files listed in the state still exist and add new ones.
564 previous = self._lru.keys_set()
565 unknown = []
566 for filename in os.listdir(self.cache_dir):
567 if filename == self.STATE_FILE:
568 continue
569 if filename in previous:
570 previous.remove(filename)
571 continue
572 # An untracked file.
Vadim Shtayurae0ab1902014-04-29 10:55:27 -0700573 if not isolateserver.is_valid_hash(filename, self.hash_algo):
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000574 logging.warning('Removing unknown file %s from cache', filename)
575 try_remove(self._path(filename))
576 continue
577 # File that's not referenced in 'state.json'.
578 # TODO(vadimsh): Verify its SHA1 matches file name.
579 logging.warning('Adding unknown file %s to cache', filename)
580 unknown.append(filename)
581
582 if unknown:
583 # Add as oldest files. They will be deleted eventually if not accessed.
584 self._add_oldest_list(unknown)
585 logging.warning('Added back %d unknown files', len(unknown))
586
587 if previous:
588 # Filter out entries that were not found.
589 logging.warning('Removed %d lost files', len(previous))
590 for filename in previous:
591 self._lru.pop(filename)
592 self._trim()
593
594 def _save(self):
595 """Saves the LRU ordering."""
596 self._lock.assert_locked()
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500597 if sys.platform != 'win32':
598 d = os.path.dirname(self.state_file)
599 if os.path.isdir(d):
600 # Necessary otherwise the file can't be created.
601 set_read_only(d, False)
602 if os.path.isfile(self.state_file):
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500603 set_read_only(self.state_file, False)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000604 self._lru.save(self.state_file)
605
606 def _trim(self):
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000607 """Trims anything we don't know, make sure enough free space exists."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000608 self._lock.assert_locked()
609
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000610 # Ensure maximum cache size.
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000611 if self.policies.max_cache_size:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000612 total_size = sum(self._lru.itervalues())
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000613 while total_size > self.policies.max_cache_size:
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000614 total_size -= self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000615
616 # Ensure maximum number of items in the cache.
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000617 if self.policies.max_items and len(self._lru) > self.policies.max_items:
618 for _ in xrange(len(self._lru) - self.policies.max_items):
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000619 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000620
621 # Ensure enough free space.
622 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000623 trimmed_due_to_space = False
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000624 while (
625 self.policies.min_free_space and
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000626 self._lru and
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000627 self._free_disk < self.policies.min_free_space):
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000628 trimmed_due_to_space = True
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000629 self._remove_lru_file()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000630 self._free_disk = get_free_space(self.cache_dir)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000631 if trimmed_due_to_space:
Marc-Antoine Ruel2cbc1242014-07-16 11:05:04 -0400632 total_usage = sum(self._lru.itervalues())
633 usage_percent = 0.
634 if total_usage:
635 usage_percent = 100. * self.policies.max_cache_size / float(total_usage)
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000636 logging.warning(
637 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
638 'cache (%.1f%% of its maximum capacity)',
639 self._free_disk / 1024.,
Marc-Antoine Ruel2cbc1242014-07-16 11:05:04 -0400640 total_usage / 1024.,
641 usage_percent)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000642 self._save()
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000643
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000644 def _path(self, digest):
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000645 """Returns the path to one item."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000646 return os.path.join(self.cache_dir, digest)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000647
648 def _remove_lru_file(self):
649 """Removes the last recently used file and returns its size."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000650 self._lock.assert_locked()
651 digest, size = self._lru.pop_oldest()
652 self._delete_file(digest, size)
maruel@chromium.orge45728d2013-09-16 23:23:22 +0000653 return size
654
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000655 def _add(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000656 """Adds an item into LRU cache marking it as a newest one."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000657 self._lock.assert_locked()
658 if size == isolateserver.UNKNOWN_FILE_SIZE:
659 size = os.stat(self._path(digest)).st_size
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000660 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000661 self._lru.add(digest, size)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000662
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000663 def _add_oldest_list(self, digests):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000664 """Adds a bunch of items into LRU cache marking them as oldest ones."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000665 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000666 pairs = []
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000667 for digest in digests:
668 size = os.stat(self._path(digest)).st_size
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000669 self._added.append(size)
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000670 pairs.append((digest, size))
671 self._lru.batch_insert_oldest(pairs)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000672
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000673 def _delete_file(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000674 """Deletes cache file from the file system."""
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000675 self._lock.assert_locked()
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000676 try:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000677 if size == isolateserver.UNKNOWN_FILE_SIZE:
678 size = os.stat(self._path(digest)).st_size
Marc-Antoine Ruelbcdf6d62013-11-11 16:45:14 -0500679 try_remove(self._path(digest))
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000680 self._removed.append(size)
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +0000681 except OSError as e:
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000682 logging.error('Error attempting to delete a file %s:\n%s' % (digest, e))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000683
684
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500685def change_tree_read_only(rootdir, read_only):
686 """Changes the tree read-only bits according to the read_only specification.
687
688 The flag can be 0, 1 or 2, which will affect the possibility to modify files
689 and create or delete files.
690 """
691 if read_only == 2:
692 # Files and directories (except on Windows) are marked read only. This
693 # inhibits modifying, creating or deleting files in the test directory,
694 # except on Windows where creating and deleting files is still possible.
695 make_tree_read_only(rootdir)
696 elif read_only == 1:
697 # Files are marked read only but not the directories. This inhibits
698 # modifying files but creating or deleting files is still possible.
699 make_tree_files_read_only(rootdir)
700 elif read_only in (0, None):
701 # Anything can be modified. This is the default in the .isolated file
702 # format.
703 #
704 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
705 # is not yet changed to verify the hash of the content of the files it is
706 # looking at, so that if a test modifies an input file, the file must be
707 # deleted.
708 make_tree_writeable(rootdir)
709 else:
710 raise ValueError(
711 'change_tree_read_only(%s, %s): Unknown flag %s' %
712 (rootdir, read_only, read_only))
713
714
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500715def process_command(command, out_dir):
716 """Replaces isolated specific variables in a command line."""
Vadim Shtayura51aba362014-05-14 15:39:23 -0700717 filtered = []
718 for arg in command:
719 if '${ISOLATED_OUTDIR}' in arg:
720 arg = arg.replace('${ISOLATED_OUTDIR}', out_dir).replace('/', os.sep)
721 filtered.append(arg)
722 return filtered
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500723
724
Vadim Shtayurae0ab1902014-04-29 10:55:27 -0700725def run_tha_test(isolated_hash, storage, cache, extra_args):
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500726 """Downloads the dependencies in the cache, hardlinks them into a temporary
727 directory and runs the executable from there.
728
729 A temporary directory is created to hold the output files. The content inside
730 this directory will be uploaded back to |storage| packaged as a .isolated
731 file.
732
733 Arguments:
734 isolated_hash: the sha-1 of the .isolated file that must be retrieved to
735 recreate the tree of files to run the target executable.
736 storage: an isolateserver.Storage object to retrieve remote objects. This
737 object has a reference to an isolateserver.StorageApi, which does
738 the actual I/O.
739 cache: an isolateserver.LocalCache to keep from retrieving the same objects
740 constantly by caching the objects retrieved. Can be on-disk or
741 in-memory.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500742 extra_args: optional arguments to add to the command stated in the .isolate
743 file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000744 """
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500745 run_dir = make_temp_dir('run_tha_test', cache.cache_dir)
Vadim Shtayura51aba362014-05-14 15:39:23 -0700746 out_dir = unicode(make_temp_dir('isolated_out', cache.cache_dir))
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500747 result = 0
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000748 try:
maruel@chromium.org4f2ebe42013-09-19 13:09:08 +0000749 try:
750 settings = isolateserver.fetch_isolated(
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000751 isolated_hash=isolated_hash,
752 storage=storage,
753 cache=cache,
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500754 outdir=run_dir,
vadimsh@chromium.org7b5dae32013-10-03 16:59:59 +0000755 require_command=True)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400756 except isolateserver.ConfigError:
757 on_error.report(None)
Vadim Shtayura51aba362014-05-14 15:39:23 -0700758 return 1
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000759
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500760 change_tree_read_only(run_dir, settings.read_only)
761 cwd = os.path.normpath(os.path.join(run_dir, settings.relative_cwd))
Marc-Antoine Rueldef5b802014-01-08 20:57:12 -0500762 command = settings.command + extra_args
Vadim Shtayurae4a780b2014-01-17 13:18:53 -0800763
764 # subprocess.call doesn't consider 'cwd' when searching for executable.
765 # Yet isolate can specify command relative to 'cwd'. Convert it to absolute
766 # path if necessary.
767 if not os.path.isabs(command[0]):
768 command[0] = os.path.abspath(os.path.join(cwd, command[0]))
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:
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500792 try:
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400793 if not rmtree(run_dir):
794 print >> sys.stderr, (
795 'Failed to delete the temporary directory, forcibly failing the\n'
796 'task because of it. No zombie process can outlive a successful\n'
797 'task run and still be marked as successful. Fix your stuff.')
798 result = result or 1
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500799 except OSError:
800 logging.warning('Leaking %s', run_dir)
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400801 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700802
803 # HACK(vadimsh): On Windows rmtree(run_dir) call above has
804 # a synchronization effect: it finishes only when all task child processes
805 # terminate (since a running process locks *.exe file). Examine out_dir
806 # only after that call completes (since child processes may
807 # write to out_dir too and we need to wait for them to finish).
808
809 # Upload out_dir and generate a .isolated file out of this directory.
810 # It is only done if files were written in the directory.
811 if os.listdir(out_dir):
812 with tools.Profiler('ArchiveOutput'):
813 results = isolateserver.archive_files_to_storage(
814 storage, [out_dir], None)
815 # TODO(maruel): Implement side-channel to publish this information.
816 output_data = {
817 'hash': results[0][0],
818 'namespace': storage.namespace,
819 'storage': storage.location,
820 }
821 sys.stdout.flush()
822 print(
823 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
824 tools.format_json(output_data, dense=True))
825
826 finally:
Marc-Antoine Ruelaf9d8372014-07-21 19:50:57 -0400827 try:
828 if os.path.isdir(out_dir) and not rmtree(out_dir):
829 result = result or 1
830 except OSError:
831 # The error was already printed out. Report it but that's it.
832 on_error.report(None)
833 result = 1
Vadim Shtayura51aba362014-05-14 15:39:23 -0700834
Marc-Antoine Ruel3a963792013-12-11 11:33:49 -0500835 return result
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000836
837
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500838def main(args):
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000839 tools.disable_buffering()
840 parser = tools.OptionParserWithLogging(
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000841 usage='%prog <options>',
842 version=__version__,
843 log_file=RUN_ISOLATED_LOG_FILE)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000844
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500845 data_group = optparse.OptionGroup(parser, 'Data source')
846 data_group.add_option(
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000847 '-s', '--isolated',
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000848 metavar='FILE',
849 help='File/url describing what to map or run')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500850 data_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000851 '-H', '--hash',
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000852 help='Hash of the .isolated to grab from the hash table')
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -0500853 isolateserver.add_isolate_server_options(data_group, True)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500854 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000855
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500856 cache_group = optparse.OptionGroup(parser, 'Cache management')
857 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000858 '--cache',
859 default='cache',
860 metavar='DIR',
861 help='Cache directory, default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500862 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000863 '--max-cache-size',
864 type='int',
865 metavar='NNN',
866 default=20*1024*1024*1024,
867 help='Trim if the cache gets larger than this value, default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500868 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000869 '--min-free-space',
870 type='int',
871 metavar='NNN',
maruel@chromium.org9e98e432013-05-31 17:06:51 +0000872 default=2*1024*1024*1024,
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000873 help='Trim if disk free space becomes lower than this value, '
874 'default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500875 cache_group.add_option(
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000876 '--max-items',
877 type='int',
878 metavar='NNN',
879 default=100000,
880 help='Trim if more than this number of items are in the cache '
881 'default=%default')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500882 parser.add_option_group(cache_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000883
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800884 auth.add_auth_options(parser)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500885 options, args = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800886 auth.process_auth_options(parser, options)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500887 isolateserver.process_isolate_server_options(data_group, options)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000888
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000889 if bool(options.isolated) == bool(options.hash):
maruel@chromium.org5dd75dd2012-12-03 15:11:32 +0000890 logging.debug('One and only one of --isolated or --hash is required.')
maruel@chromium.org0cd0b182012-10-22 13:34:15 +0000891 parser.error('One and only one of --isolated or --hash is required.')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000892
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000893 options.cache = os.path.abspath(options.cache)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000894 policies = CachePolicies(
895 options.max_cache_size, options.min_free_space, options.max_items)
csharp@chromium.orgffd8cf02013-01-09 21:57:38 +0000896
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400897 # |options.cache| path may not exist until DiskCache() instance is created.
898 cache = DiskCache(
899 options.cache, policies, isolateserver.get_hash_algo(options.namespace))
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700900
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400901 remote = options.isolate_server or options.indir
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700902 if file_path.is_url(remote):
903 auth.ensure_logged_in(remote)
904
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400905 with isolateserver.get_storage(remote, options.namespace) as storage:
906 # Hashing schemes used by |storage| and |cache| MUST match.
907 assert storage.hash_algo == cache.hash_algo
908 return run_tha_test(
909 options.isolated or options.hash, storage, cache, args)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000910
911
912if __name__ == '__main__':
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000913 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000914 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500915 sys.exit(main(sys.argv[1:]))