blob: 00edf14f6ddd770e7def2a4c692638523dfed8ec [file] [log] [blame]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2012 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00005
nodir55be77b2016-05-03 09:39:57 -07006"""Runs a command with optional isolated input/output.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00007
nodir55be77b2016-05-03 09:39:57 -07008Despite name "run_isolated", can run a generic non-isolated command specified as
9args.
10
11If input isolated hash is provided, fetches it, creates a tree of hard links,
12appends args to the command in the fetched isolated and runs it.
13To improve performance, keeps a local cache.
14The local cache can safely be deleted.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -050015
nodirbe642ff2016-06-09 15:51:51 -070016Any ${EXECUTABLE_SUFFIX} on the command line will be replaced with ".exe" string
17on Windows and "" on other platforms.
18
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -050019Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
20temporary directory upon execution of the command specified in the .isolated
21file. All content written to this directory will be uploaded upon termination
22and the .isolated file describing this directory will be printed to stdout.
bpastene447c1992016-06-20 15:21:47 -070023
24Any ${SWARMING_BOT_FILE} on the command line will be replaced by the value of
25the --bot-file parameter. This file is used by a swarming bot to communicate
26state of the host to tasks. It is written to by the swarming bot's
27on_before_task() hook in the swarming server's custom bot_config.py.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000028"""
29
maruelcffa0542017-04-07 08:39:20 -070030__version__ = '0.9.1'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000031
aludwin7556e0c2016-10-26 08:46:10 -070032import argparse
maruel064c0a32016-04-05 11:47:15 -070033import base64
iannucci96fcccc2016-08-30 15:52:22 -070034import collections
vadimsh232f5a82017-01-20 19:23:44 -080035import contextlib
aludwin7556e0c2016-10-26 08:46:10 -070036import json
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000037import logging
38import optparse
39import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000040import sys
41import tempfile
maruel064c0a32016-04-05 11:47:15 -070042import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000043
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000044from third_party.depot_tools import fix_encoding
45
Vadim Shtayura6b555c12014-07-23 16:22:18 -070046from utils import file_path
maruel12e30012015-10-09 11:55:35 -070047from utils import fs
maruel064c0a32016-04-05 11:47:15 -070048from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040049from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040050from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050051from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000052from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000053from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000054
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080055import auth
nodirbe642ff2016-06-09 15:51:51 -070056import cipd
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000057import isolateserver
nodirf33b8d62016-10-26 22:34:58 -070058import named_cache
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000059
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000060
vadimsh@chromium.org85071062013-08-21 23:37:45 +000061# Absolute path to this file (can be None if running from zip on Mac).
tansella4949442016-06-23 22:34:32 -070062THIS_FILE_PATH = os.path.abspath(
63 __file__.decode(sys.getfilesystemencoding())) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000064
65# Directory that contains this file (might be inside zip package).
tansella4949442016-06-23 22:34:32 -070066BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__.decode(
67 sys.getfilesystemencoding()) else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000068
69# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000070if zip_package.get_main_script_path():
71 MAIN_DIR = os.path.dirname(
72 os.path.abspath(zip_package.get_main_script_path()))
73else:
74 # This happens when 'import run_isolated' is executed at the python
75 # interactive prompt, in that case __file__ is undefined.
76 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000077
maruele2f2cb82016-07-13 14:41:03 -070078
79# Magic variables that can be found in the isolate task command line.
80ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
81EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
82SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
83
84
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000085# The name of the log file to use.
86RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
87
maruele2f2cb82016-07-13 14:41:03 -070088
csharp@chromium.orge217f302012-11-22 16:51:53 +000089# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000090RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000091
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000092
maruele2f2cb82016-07-13 14:41:03 -070093# Use short names for temporary directories. This is driven by Windows, which
94# imposes a relatively short maximum path length of 260 characters, often
95# referred to as MAX_PATH. It is relatively easy to create files with longer
96# path length. A use case is with recursive depedency treesV like npm packages.
97#
98# It is recommended to start the script with a `root_dir` as short as
99# possible.
100# - ir stands for isolated_run
101# - io stands for isolated_out
102# - it stands for isolated_tmp
103ISOLATED_RUN_DIR = u'ir'
104ISOLATED_OUT_DIR = u'io'
105ISOLATED_TMP_DIR = u'it'
106
107
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000108def get_as_zip_package(executable=True):
109 """Returns ZipPackage with this module and all its dependencies.
110
111 If |executable| is True will store run_isolated.py as __main__.py so that
112 zip package is directly executable be python.
113 """
114 # Building a zip package when running from another zip package is
115 # unsupported and probably unneeded.
116 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +0000117 assert THIS_FILE_PATH
118 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000119 package = zip_package.ZipPackage(root=BASE_DIR)
120 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
aludwin81178302016-11-30 17:18:49 -0800121 package.add_python_file(os.path.join(BASE_DIR, 'isolate_storage.py'))
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400122 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000123 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800124 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
nodirbe642ff2016-06-09 15:51:51 -0700125 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
nodirf33b8d62016-10-26 22:34:58 -0700126 package.add_python_file(os.path.join(BASE_DIR, 'named_cache.py'))
tanselle4288c32016-07-28 09:45:40 -0700127 package.add_directory(os.path.join(BASE_DIR, 'libs'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000128 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
129 package.add_directory(os.path.join(BASE_DIR, 'utils'))
130 return package
131
132
maruel03e11842016-07-14 10:50:16 -0700133def make_temp_dir(prefix, root_dir):
134 """Returns a new unique temporary directory."""
135 return unicode(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000136
137
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500138def change_tree_read_only(rootdir, read_only):
139 """Changes the tree read-only bits according to the read_only specification.
140
141 The flag can be 0, 1 or 2, which will affect the possibility to modify files
142 and create or delete files.
143 """
144 if read_only == 2:
145 # Files and directories (except on Windows) are marked read only. This
146 # inhibits modifying, creating or deleting files in the test directory,
147 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400148 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500149 elif read_only == 1:
150 # Files are marked read only but not the directories. This inhibits
151 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400152 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500153 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500154 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500155 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
156 # is not yet changed to verify the hash of the content of the files it is
157 # looking at, so that if a test modifies an input file, the file must be
158 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400159 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500160 else:
161 raise ValueError(
162 'change_tree_read_only(%s, %s): Unknown flag %s' %
163 (rootdir, read_only, read_only))
164
165
nodir90bc8dc2016-06-15 13:35:21 -0700166def process_command(command, out_dir, bot_file):
nodirbe642ff2016-06-09 15:51:51 -0700167 """Replaces variables in a command line.
168
169 Raises:
170 ValueError if a parameter is requested in |command| but its value is not
171 provided.
172 """
maruela9cfd6f2015-09-15 11:03:15 -0700173 def fix(arg):
nodirbe642ff2016-06-09 15:51:51 -0700174 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
175 replace_slash = False
nodir55be77b2016-05-03 09:39:57 -0700176 if ISOLATED_OUTDIR_PARAMETER in arg:
nodirbe642ff2016-06-09 15:51:51 -0700177 if not out_dir:
maruel7f63a272016-07-12 12:40:36 -0700178 raise ValueError(
179 'output directory is requested in command, but not provided; '
180 'please specify one')
nodir55be77b2016-05-03 09:39:57 -0700181 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
nodirbe642ff2016-06-09 15:51:51 -0700182 replace_slash = True
nodir90bc8dc2016-06-15 13:35:21 -0700183 if SWARMING_BOT_FILE_PARAMETER in arg:
184 if bot_file:
185 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
186 replace_slash = True
187 else:
188 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command, but no '
189 'bot_file specified. Leaving parameter unchanged.')
nodirbe642ff2016-06-09 15:51:51 -0700190 if replace_slash:
191 # Replace slashes only if parameters are present
nodir55be77b2016-05-03 09:39:57 -0700192 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
193 arg = arg.replace('/', os.sep)
maruela9cfd6f2015-09-15 11:03:15 -0700194 return arg
195
196 return [fix(arg) for arg in command]
197
198
vadimsh232f5a82017-01-20 19:23:44 -0800199def get_command_env(tmp_dir, cipd_info):
200 """Returns full OS environment to run a command in.
201
202 Sets up TEMP, puts directory with cipd binary in front of PATH, and exposes
203 CIPD_CACHE_DIR env var.
204
205 Args:
206 tmp_dir: temp directory.
207 cipd_info: CipdInfo object is cipd client is used, None if not.
208 """
209 def to_fs_enc(s):
210 if isinstance(s, str):
211 return s
212 return s.encode(sys.getfilesystemencoding())
213
214 env = os.environ.copy()
215
iannucciac0342c2017-02-24 05:47:01 -0800216 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
iannucci460def72017-02-24 10:49:48 -0800217 # * mktemp on linux respects $TMPDIR, not $TMP
218 # * mktemp on OS X SOMETIMES respects $TMPDIR
iannucciac0342c2017-02-24 05:47:01 -0800219 # * chromium's base utils respects $TMPDIR on linux, $TEMP on windows.
220 # Unfortunately at the time of writing it completely ignores all envvars
221 # on OS X.
iannucci460def72017-02-24 10:49:48 -0800222 # * python respects TMPDIR, TEMP, and TMP (regardless of platform)
223 # * golang respects TMPDIR on linux+mac, TEMP on windows.
iannucciac0342c2017-02-24 05:47:01 -0800224 key = {'win32': 'TEMP'}.get(sys.platform, 'TMPDIR')
vadimsh232f5a82017-01-20 19:23:44 -0800225 env[key] = to_fs_enc(tmp_dir)
226
227 if cipd_info:
228 bin_dir = os.path.dirname(cipd_info.client.binary_path)
229 env['PATH'] = '%s%s%s' % (to_fs_enc(bin_dir), os.pathsep, env['PATH'])
230 env['CIPD_CACHE_DIR'] = to_fs_enc(cipd_info.cache_dir)
231
232 return env
233
234
235def run_command(command, cwd, env, hard_timeout, grace_period):
maruel6be7f9e2015-10-01 12:25:30 -0700236 """Runs the command.
237
238 Returns:
239 tuple(process exit code, bool if had a hard timeout)
240 """
maruela9cfd6f2015-09-15 11:03:15 -0700241 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700242
maruel6be7f9e2015-10-01 12:25:30 -0700243 exit_code = None
244 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700245 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700246 proc = None
247 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700248 try:
maruel6be7f9e2015-10-01 12:25:30 -0700249 # TODO(maruel): This code is imperfect. It doesn't handle well signals
250 # during the download phase and there's short windows were things can go
251 # wrong.
252 def handler(signum, _frame):
253 if proc and not had_signal:
254 logging.info('Received signal %d', signum)
255 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700256 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700257
258 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
259 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
260 try:
261 exit_code = proc.wait(hard_timeout or None)
262 except subprocess42.TimeoutExpired:
263 if not had_signal:
264 logging.warning('Hard timeout')
265 had_hard_timeout = True
266 logging.warning('Sending SIGTERM')
267 proc.terminate()
268
269 # Ignore signals in grace period. Forcibly give the grace period to the
270 # child process.
271 if exit_code is None:
272 ignore = lambda *_: None
273 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
274 try:
275 exit_code = proc.wait(grace_period or None)
276 except subprocess42.TimeoutExpired:
277 # Now kill for real. The user can distinguish between the
278 # following states:
279 # - signal but process exited within grace period,
280 # hard_timed_out will be set but the process exit code will be
281 # script provided.
282 # - processed exited late, exit code will be -9 on posix.
283 logging.warning('Grace exhausted; sending SIGKILL')
284 proc.kill()
285 logging.info('Waiting for proces exit')
286 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700287 except OSError:
288 # This is not considered to be an internal error. The executable simply
289 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800290 sys.stderr.write(
291 '<The executable does not exist or a dependent library is missing>\n'
292 '<Check for missing .so/.dll in the .isolate or GN file>\n'
293 '<Command: %s>\n' % command)
294 if os.environ.get('SWARMING_TASK_ID'):
295 # Give an additional hint when running as a swarming task.
296 sys.stderr.write(
297 '<See the task\'s page for commands to help diagnose this issue '
298 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700299 exit_code = 1
300 logging.info(
301 'Command finished with exit code %d (%s)',
302 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700303 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700304
305
maruel4409e302016-07-19 14:25:51 -0700306def fetch_and_map(isolated_hash, storage, cache, outdir, use_symlinks):
307 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700308 start = time.time()
309 bundle = isolateserver.fetch_isolated(
310 isolated_hash=isolated_hash,
311 storage=storage,
312 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700313 outdir=outdir,
314 use_symlinks=use_symlinks)
nodir6f801882016-04-29 14:41:50 -0700315 return bundle, {
316 'duration': time.time() - start,
317 'initial_number_items': cache.initial_number_items,
318 'initial_size': cache.initial_size,
319 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
320 'items_hot': base64.b64encode(
tansell9e04a8d2016-07-28 09:31:59 -0700321 large.pack(sorted(set(cache.used) - set(cache.added)))),
nodir6f801882016-04-29 14:41:50 -0700322 }
323
324
aludwin0a8e17d2016-10-27 15:57:39 -0700325def link_outputs_to_outdir(run_dir, out_dir, outputs):
326 """Links any named outputs to out_dir so they can be uploaded.
327
328 Raises an error if the file already exists in that directory.
329 """
330 if not outputs:
331 return
332 isolateserver.create_directories(out_dir, outputs)
333 for o in outputs:
334 try:
335 file_path.link_file(
336 os.path.join(out_dir, o),
337 os.path.join(run_dir, o),
338 file_path.HARDLINK_WITH_FALLBACK)
339 except OSError as e:
aludwin81178302016-11-30 17:18:49 -0800340 logging.info("Couldn't collect output file %s: %s", o, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700341
342
maruela9cfd6f2015-09-15 11:03:15 -0700343def delete_and_upload(storage, out_dir, leak_temp_dir):
344 """Deletes the temporary run directory and uploads results back.
345
346 Returns:
nodir6f801882016-04-29 14:41:50 -0700347 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700348 - outputs_ref: a dict referring to the results archived back to the isolated
349 server, if applicable.
350 - success: False if something occurred that means that the task must
351 forcibly be considered a failure, e.g. zombie processes were left
352 behind.
nodir6f801882016-04-29 14:41:50 -0700353 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700354 """
maruela9cfd6f2015-09-15 11:03:15 -0700355 # Upload out_dir and generate a .isolated file out of this directory. It is
356 # only done if files were written in the directory.
357 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700358 cold = []
359 hot = []
nodir6f801882016-04-29 14:41:50 -0700360 start = time.time()
361
maruel12e30012015-10-09 11:55:35 -0700362 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700363 with tools.Profiler('ArchiveOutput'):
364 try:
maruel064c0a32016-04-05 11:47:15 -0700365 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700366 storage, [out_dir], None)
367 outputs_ref = {
368 'isolated': results[0][0],
369 'isolatedserver': storage.location,
370 'namespace': storage.namespace,
371 }
maruel064c0a32016-04-05 11:47:15 -0700372 cold = sorted(i.size for i in f_cold)
373 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700374 except isolateserver.Aborted:
375 # This happens when a signal SIGTERM was received while uploading data.
376 # There is 2 causes:
377 # - The task was too slow and was about to be killed anyway due to
378 # exceeding the hard timeout.
379 # - The amount of data uploaded back is very large and took too much
380 # time to archive.
381 sys.stderr.write('Received SIGTERM while uploading')
382 # Re-raise, so it will be treated as an internal failure.
383 raise
nodir6f801882016-04-29 14:41:50 -0700384
385 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700386 try:
maruel12e30012015-10-09 11:55:35 -0700387 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700388 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700389 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700390 else:
391 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700392 except OSError as e:
393 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700394 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700395 stats = {
396 'duration': time.time() - start,
397 'items_cold': base64.b64encode(large.pack(cold)),
398 'items_hot': base64.b64encode(large.pack(hot)),
399 }
400 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700401
402
marueleb5fbee2015-09-17 13:01:36 -0700403def map_and_run(
nodir0ae98b32017-05-11 13:21:53 -0700404 command, isolated_hash, storage, isolate_cache, outputs,
405 install_named_caches, leak_temp_dir, root_dir, hard_timeout, grace_period,
406 bot_file, install_packages_fn, use_symlinks, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700407 """Runs a command with optional isolated input/output.
408
409 See run_tha_test for argument documentation.
410
411 Returns metadata about the result.
412 """
maruelabec63c2017-04-26 11:53:24 -0700413 assert isinstance(command, list), command
nodir56efa452016-10-12 12:17:39 -0700414 assert root_dir or root_dir is None
maruela9cfd6f2015-09-15 11:03:15 -0700415 result = {
maruel064c0a32016-04-05 11:47:15 -0700416 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700417 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700418 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700419 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700420 'stats': {
nodir55715712016-06-03 12:28:19 -0700421 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700422 # 'cipd': {
423 # 'duration': 0.,
424 # 'get_client_duration': 0.,
425 # },
nodir55715712016-06-03 12:28:19 -0700426 # 'download': {
427 # 'duration': 0.,
428 # 'initial_number_items': 0,
429 # 'initial_size': 0,
430 # 'items_cold': '<large.pack()>',
431 # 'items_hot': '<large.pack()>',
432 # },
433 # 'upload': {
434 # 'duration': 0.,
435 # 'items_cold': '<large.pack()>',
436 # 'items_hot': '<large.pack()>',
437 # },
maruel064c0a32016-04-05 11:47:15 -0700438 # },
439 },
iannucci96fcccc2016-08-30 15:52:22 -0700440 # 'cipd_pins': {
441 # 'packages': [
442 # {'package_name': ..., 'version': ..., 'path': ...},
443 # ...
444 # ],
445 # 'client_package': {'package_name': ..., 'version': ...},
446 # },
maruela9cfd6f2015-09-15 11:03:15 -0700447 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700448 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700449 }
nodirbe642ff2016-06-09 15:51:51 -0700450
marueleb5fbee2015-09-17 13:01:36 -0700451 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700452 file_path.ensure_tree(root_dir, 0700)
nodir56efa452016-10-12 12:17:39 -0700453 elif isolate_cache.cache_dir:
454 root_dir = os.path.dirname(isolate_cache.cache_dir)
maruele2f2cb82016-07-13 14:41:03 -0700455 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700456 # If root_dir is not specified, it is not constant.
457 # TODO(maruel): This is not obvious. Change this to become an error once we
458 # make the constant_run_path an exposed flag.
459 if constant_run_path and root_dir:
460 run_dir = os.path.join(root_dir, ISOLATED_RUN_DIR)
maruel5c4eed82017-05-26 05:33:40 -0700461 if os.path.isdir(run_dir):
462 file_path.rmtree(run_dir)
maruelcffa0542017-04-07 08:39:20 -0700463 os.mkdir(run_dir)
464 else:
465 run_dir = make_temp_dir(ISOLATED_RUN_DIR, root_dir)
maruel03e11842016-07-14 10:50:16 -0700466 # storage should be normally set but don't crash if it is not. This can happen
467 # as Swarming task can run without an isolate server.
maruele2f2cb82016-07-13 14:41:03 -0700468 out_dir = make_temp_dir(ISOLATED_OUT_DIR, root_dir) if storage else None
469 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, root_dir)
nodir55be77b2016-05-03 09:39:57 -0700470 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700471
nodir55be77b2016-05-03 09:39:57 -0700472 try:
vadimsh232f5a82017-01-20 19:23:44 -0800473 with install_packages_fn(run_dir) as cipd_info:
474 if cipd_info:
475 result['stats']['cipd'] = cipd_info.stats
476 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700477
vadimsh232f5a82017-01-20 19:23:44 -0800478 if isolated_hash:
479 isolated_stats = result['stats'].setdefault('isolated', {})
480 bundle, isolated_stats['download'] = fetch_and_map(
481 isolated_hash=isolated_hash,
482 storage=storage,
483 cache=isolate_cache,
484 outdir=run_dir,
485 use_symlinks=use_symlinks)
vadimsh232f5a82017-01-20 19:23:44 -0800486 change_tree_read_only(run_dir, bundle.read_only)
487 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
maruelabec63c2017-04-26 11:53:24 -0700488 # Inject the command
489 if bundle.command:
490 command = bundle.command + command
491
492 if not command:
493 # Handle this as a task failure, not an internal failure.
494 sys.stderr.write(
495 '<No command was specified!>\n'
496 '<Please secify a command when triggering your Swarming task>\n')
497 result['exit_code'] = 1
498 return result
nodirbe642ff2016-06-09 15:51:51 -0700499
vadimsh232f5a82017-01-20 19:23:44 -0800500 # If we have an explicit list of files to return, make sure their
501 # directories exist now.
502 if storage and outputs:
503 isolateserver.create_directories(run_dir, outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700504
vadimsh232f5a82017-01-20 19:23:44 -0800505 command = tools.fix_python_path(command)
506 command = process_command(command, out_dir, bot_file)
507 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700508
nodir0ae98b32017-05-11 13:21:53 -0700509 with install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -0800510 sys.stdout.flush()
511 start = time.time()
512 try:
513 result['exit_code'], result['had_hard_timeout'] = run_command(
514 command, cwd, get_command_env(tmp_dir, cipd_info),
515 hard_timeout, grace_period)
516 finally:
517 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700518 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700519 # An internal error occurred. Report accordingly so the swarming task will
520 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700521 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700522 result['internal_failure'] = str(e)
523 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700524
525 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700526 finally:
527 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700528 # Try to link files to the output directory, if specified.
529 if out_dir:
530 link_outputs_to_outdir(run_dir, out_dir, outputs)
531
nodir32a1ec12016-10-26 18:34:07 -0700532 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700533 if leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700534 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700535 logging.warning(
536 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700537 else:
maruel84537cb2015-10-16 14:21:28 -0700538 # On Windows rmtree(run_dir) call above has a synchronization effect: it
539 # finishes only when all task child processes terminate (since a running
540 # process locks *.exe file). Examine out_dir only after that call
541 # completes (since child processes may write to out_dir too and we need
542 # to wait for them to finish).
543 if fs.isdir(run_dir):
544 try:
545 success = file_path.rmtree(run_dir)
546 except OSError as e:
547 logging.error('Failure with %s', e)
548 success = False
549 if not success:
550 print >> sys.stderr, (
qyearsleybf6fb2b2017-05-09 16:17:26 -0700551 'Failed to delete the run directory, thus failing the task.\n'
552 'This may be due to a subprocess outliving the main task\n'
553 'process, holding on to resources. Please fix the task so\n'
554 'that it releases resources and cleans up subprocesses.')
maruel84537cb2015-10-16 14:21:28 -0700555 if result['exit_code'] == 0:
556 result['exit_code'] = 1
557 if fs.isdir(tmp_dir):
558 try:
559 success = file_path.rmtree(tmp_dir)
560 except OSError as e:
561 logging.error('Failure with %s', e)
562 success = False
563 if not success:
564 print >> sys.stderr, (
qyearsleybf6fb2b2017-05-09 16:17:26 -0700565 'Failed to delete the temp directory, thus failing the task.\n'
566 'This may be due to a subprocess outliving the main task\n'
567 'process, holding on to resources. Please fix the task so\n'
568 'that it releases resources and cleans up subprocesses.')
maruel84537cb2015-10-16 14:21:28 -0700569 if result['exit_code'] == 0:
570 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700571
marueleb5fbee2015-09-17 13:01:36 -0700572 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700573 if out_dir:
nodir55715712016-06-03 12:28:19 -0700574 isolated_stats = result['stats'].setdefault('isolated', {})
575 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700576 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700577 if not success and result['exit_code'] == 0:
578 result['exit_code'] = 1
579 except Exception as e:
580 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700581 if out_dir:
582 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700583 result['internal_failure'] = str(e)
584 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500585
586
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400587def run_tha_test(
nodir0ae98b32017-05-11 13:21:53 -0700588 command, isolated_hash, storage, isolate_cache, outputs,
589 install_named_caches, leak_temp_dir, result_json, root_dir, hard_timeout,
590 grace_period, bot_file, install_packages_fn, use_symlinks):
nodir55be77b2016-05-03 09:39:57 -0700591 """Runs an executable and records execution metadata.
592
593 Either command or isolated_hash must be specified.
594
595 If isolated_hash is specified, downloads the dependencies in the cache,
596 hardlinks them into a temporary directory and runs the command specified in
597 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500598
599 A temporary directory is created to hold the output files. The content inside
600 this directory will be uploaded back to |storage| packaged as a .isolated
601 file.
602
603 Arguments:
maruelabec63c2017-04-26 11:53:24 -0700604 command: a list of string; the command to run OR optional arguments to add
605 to the command stated in the .isolated file if a command was
606 specified.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500607 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500608 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700609 The command specified in the .isolated is executed.
610 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500611 storage: an isolateserver.Storage object to retrieve remote objects. This
612 object has a reference to an isolateserver.StorageApi, which does
613 the actual I/O.
nodir6b945692016-10-19 19:09:06 -0700614 isolate_cache: an isolateserver.LocalCache to keep from retrieving the
615 same objects constantly by caching the objects retrieved.
616 Can be on-disk or in-memory.
nodir0ae98b32017-05-11 13:21:53 -0700617 install_named_caches: a function (run_dir) => context manager that installs
618 named caches into |run_dir|.
Kenneth Russell61d42352014-09-15 11:41:16 -0700619 leak_temp_dir: if true, the temporary directory will be deliberately leaked
620 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700621 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700622 exit code is always 0 unless an internal error occurred.
nodir90bc8dc2016-06-15 13:35:21 -0700623 root_dir: path to the directory to use to create the temporary directory. If
marueleb5fbee2015-09-17 13:01:36 -0700624 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700625 hard_timeout: kills the process if it lasts more than this amount of
626 seconds.
627 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
iannuccib58d10d2017-03-18 02:00:25 -0700628 install_packages_fn: context manager dir => CipdInfo, see
629 install_client_and_packages.
maruel4409e302016-07-19 14:25:51 -0700630 use_symlinks: create tree with symlinks instead of hardlinks.
maruela9cfd6f2015-09-15 11:03:15 -0700631
632 Returns:
633 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000634 """
maruela76b9ee2015-12-15 06:18:08 -0800635 if result_json:
636 # Write a json output file right away in case we get killed.
637 result = {
638 'exit_code': None,
639 'had_hard_timeout': False,
640 'internal_failure': 'Was terminated before completion',
641 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700642 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800643 }
644 tools.write_json(result_json, result, dense=True)
645
maruela9cfd6f2015-09-15 11:03:15 -0700646 # run_isolated exit code. Depends on if result_json is used or not.
647 result = map_and_run(
nodir220308c2017-02-01 19:32:53 -0800648 command, isolated_hash, storage, isolate_cache, outputs,
nodir0ae98b32017-05-11 13:21:53 -0700649 install_named_caches, leak_temp_dir, root_dir, hard_timeout, grace_period,
maruelabec63c2017-04-26 11:53:24 -0700650 bot_file, install_packages_fn, use_symlinks, True)
maruela9cfd6f2015-09-15 11:03:15 -0700651 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700652
maruela9cfd6f2015-09-15 11:03:15 -0700653 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700654 # We've found tests to delete 'work' when quitting, causing an exception
655 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700656 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700657 tools.write_json(result_json, result, dense=True)
658 # Only return 1 if there was an internal error.
659 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000660
maruela9cfd6f2015-09-15 11:03:15 -0700661 # Marshall into old-style inline output.
662 if result['outputs_ref']:
663 data = {
664 'hash': result['outputs_ref']['isolated'],
665 'namespace': result['outputs_ref']['namespace'],
666 'storage': result['outputs_ref']['isolatedserver'],
667 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500668 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700669 print(
670 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
671 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800672 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700673 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000674
675
iannuccib58d10d2017-03-18 02:00:25 -0700676# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800677CipdInfo = collections.namedtuple('CipdInfo', [
678 'client', # cipd.CipdClient object
679 'cache_dir', # absolute path to bot-global cipd tag and instance cache
680 'stats', # dict with stats to return to the server
681 'pins', # dict with installed cipd pins to return to the server
682])
683
684
685@contextlib.contextmanager
686def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700687 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800688 yield None
689
690
iannuccib58d10d2017-03-18 02:00:25 -0700691def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
692 """Calls 'cipd ensure' for packages.
693
694 Args:
695 run_dir (str): root of installation.
696 cipd_cache_dir (str): the directory to use for the cipd package cache.
697 client (CipdClient): the cipd client to use
698 packages: packages to install, list [(path, package_name, version), ...].
699 timeout: max duration in seconds that this function can take.
700
701 Returns: list of pinned packages. Looks like [
702 {
703 'path': 'subdirectory',
704 'package_name': 'resolved/package/name',
705 'version': 'deadbeef...',
706 },
707 ...
708 ]
709 """
710 package_pins = [None]*len(packages)
711 def insert_pin(path, name, version, idx):
712 package_pins[idx] = {
713 'package_name': name,
714 # swarming deals with 'root' as '.'
715 'path': path or '.',
716 'version': version,
717 }
718
719 by_path = collections.defaultdict(list)
720 for i, (path, name, version) in enumerate(packages):
721 # cipd deals with 'root' as ''
722 if path == '.':
723 path = ''
724 by_path[path].append((name, version, i))
725
726 pins = client.ensure(
727 run_dir,
728 {
729 subdir: [(name, vers) for name, vers, _ in pkgs]
730 for subdir, pkgs in by_path.iteritems()
731 },
732 cache_dir=cipd_cache_dir,
733 timeout=timeout,
734 )
735
736 for subdir, pin_list in sorted(pins.iteritems()):
737 this_subdir = by_path[subdir]
738 for i, (name, version) in enumerate(pin_list):
739 insert_pin(subdir, name, version, this_subdir[i][2])
740
741 assert None not in package_pins
742
743 return package_pins
744
745
vadimsh232f5a82017-01-20 19:23:44 -0800746@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700747def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700748 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800749 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800750 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700751
vadimsh232f5a82017-01-20 19:23:44 -0800752 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
753
754 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700755 [
756 {
757 "path": path, "package_name": package_name, "version": version,
758 },
759 ...
760 ]
vadimsh902948e2017-01-20 15:57:32 -0800761 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700762
763 such that they correspond 1:1 to all input package arguments from the command
764 line. These dictionaries make their all the way back to swarming, where they
765 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700766
vadimsh902948e2017-01-20 15:57:32 -0800767 If 'packages' list is empty, will bootstrap CIPD client, but won't install
768 any packages.
769
770 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800771 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800772
nodirbe642ff2016-06-09 15:51:51 -0700773 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700774 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800775 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700776 service_url (str): CIPD server url, e.g.
777 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700778 client_package_name (str): CIPD package name of CIPD client.
779 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700780 cache_dir (str): where to keep cache of cipd clients, packages and tags.
781 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700782 """
783 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700784
nodirbe642ff2016-06-09 15:51:51 -0700785 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700786 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700787
vadimsh902948e2017-01-20 15:57:32 -0800788 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800789 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700790 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800791 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700792
nodirbe642ff2016-06-09 15:51:51 -0700793 get_client_start = time.time()
794 client_manager = cipd.get_client(
795 service_url, client_package_name, client_version, cache_dir,
796 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700797
nodirbe642ff2016-06-09 15:51:51 -0700798 with client_manager as client:
799 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700800
iannuccib58d10d2017-03-18 02:00:25 -0700801 package_pins = []
802 if packages:
803 package_pins = _install_packages(
804 run_dir, cipd_cache_dir, client, packages, timeoutfn())
805
806 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700807
vadimsh232f5a82017-01-20 19:23:44 -0800808 total_duration = time.time() - start
809 logging.info(
810 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700811
vadimsh232f5a82017-01-20 19:23:44 -0800812 yield CipdInfo(
813 client=client,
814 cache_dir=cipd_cache_dir,
815 stats={
816 'duration': total_duration,
817 'get_client_duration': get_client_duration,
818 },
819 pins={
iannuccib58d10d2017-03-18 02:00:25 -0700820 'client_package': {
821 'package_name': client.package_name,
822 'version': client.instance_id,
823 },
vadimsh232f5a82017-01-20 19:23:44 -0800824 'packages': package_pins,
825 })
nodirbe642ff2016-06-09 15:51:51 -0700826
827
nodirf33b8d62016-10-26 22:34:58 -0700828def clean_caches(options, isolate_cache, named_cache_manager):
maruele6fc9382017-05-04 09:03:48 -0700829 """Trims isolated and named caches.
830
831 The goal here is to coherently trim both caches, deleting older items
832 independent of which container they belong to.
833 """
834 # TODO(maruel): Trim CIPD cache the same way.
835 total = 0
nodirf33b8d62016-10-26 22:34:58 -0700836 with named_cache_manager.open():
837 oldest_isolated = isolate_cache.get_oldest()
838 oldest_named = named_cache_manager.get_oldest()
839 trimmers = [
840 (
841 isolate_cache.trim,
842 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
843 ),
844 (
845 lambda: named_cache_manager.trim(options.min_free_space),
846 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
847 ),
848 ]
849 trimmers.sort(key=lambda (_, ts): ts)
maruele6fc9382017-05-04 09:03:48 -0700850 # TODO(maruel): This is incorrect, we want to trim 'items' that are strictly
851 # the oldest independent of in which cache they live in. Right now, the
852 # cache with the oldest item pays the price.
nodirf33b8d62016-10-26 22:34:58 -0700853 for trim, _ in trimmers:
maruele6fc9382017-05-04 09:03:48 -0700854 total += trim()
nodirf33b8d62016-10-26 22:34:58 -0700855 isolate_cache.cleanup()
maruele6fc9382017-05-04 09:03:48 -0700856 return total
nodirf33b8d62016-10-26 22:34:58 -0700857
858
nodirbe642ff2016-06-09 15:51:51 -0700859def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400860 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700861 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000862 version=__version__,
863 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700864 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700865 '--clean', action='store_true',
866 help='Cleans the cache, trimming it necessary and remove corrupted items '
867 'and returns without executing anything; use with -v to know what '
868 'was done')
869 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -0700870 '--no-clean', action='store_true',
871 help='Do not clean the cache automatically on startup. This is meant for '
872 'bots where a separate execution with --clean was done earlier so '
873 'doing it again is redundant')
874 parser.add_option(
maruel4409e302016-07-19 14:25:51 -0700875 '--use-symlinks', action='store_true',
876 help='Use symlinks instead of hardlinks')
877 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700878 '--json',
879 help='dump output metadata to json file. When used, run_isolated returns '
880 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700881 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800882 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700883 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800884 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700885 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700886 parser.add_option(
887 '--bot-file',
888 help='Path to a file describing the state of the host. The content is '
889 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -0700890 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -0700891 '--output', action='append',
892 help='Specifies an output to return. If no outputs are specified, all '
893 'files located in $(ISOLATED_OUTDIR) will be returned; '
894 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
895 'specified by --output option (there can be multiple) will be '
896 'returned. Note that if a file in OUT_DIR has the same path '
897 'as an --output option, the --output version will be returned.')
898 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -0700899 '-a', '--argsfile',
900 # This is actually handled in parse_args; it's included here purely so it
901 # can make it into the help text.
902 help='Specify a file containing a JSON array of arguments to this '
903 'script. If --argsfile is provided, no other argument may be '
904 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500905 data_group = optparse.OptionGroup(parser, 'Data source')
906 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500907 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700908 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500909 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500910 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000911
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400912 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700913
914 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -0700915 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000916
Kenneth Russell61d42352014-09-15 11:41:16 -0700917 debug_group = optparse.OptionGroup(parser, 'Debugging')
918 debug_group.add_option(
919 '--leak-temp-dir',
920 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700921 help='Deliberately leak isolate\'s temp dir for later examination. '
922 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700923 debug_group.add_option(
924 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700925 parser.add_option_group(debug_group)
926
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800927 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700928
nodirf33b8d62016-10-26 22:34:58 -0700929 parser.set_defaults(
930 cache='cache',
931 cipd_cache='cipd_cache',
932 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -0700933 return parser
934
935
aludwin7556e0c2016-10-26 08:46:10 -0700936def parse_args(args):
937 # Create a fake mini-parser just to get out the "-a" command. Note that
938 # it's not documented here; instead, it's documented in create_option_parser
939 # even though that parser will never actually get to parse it. This is
940 # because --argsfile is exclusive with all other options and arguments.
941 file_argparse = argparse.ArgumentParser(add_help=False)
942 file_argparse.add_argument('-a', '--argsfile')
943 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
944 if file_args.argsfile:
945 if nonfile_args:
946 file_argparse.error('Can\'t specify --argsfile with'
947 'any other arguments (%s)' % nonfile_args)
948 try:
949 with open(file_args.argsfile, 'r') as f:
950 args = json.load(f)
951 except (IOError, OSError, ValueError) as e:
952 # We don't need to error out here - "args" is now empty,
953 # so the call below to parser.parse_args(args) will fail
954 # and print the full help text.
955 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
956
957 # Even if we failed to read the args, just call the normal parser now since it
958 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -0700959 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500960 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -0700961 return (parser, options, args)
962
963
964def main(args):
965 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700966
nodirf33b8d62016-10-26 22:34:58 -0700967 isolate_cache = isolateserver.process_cache_options(options, trim=False)
968 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -0700969 if options.clean:
970 if options.isolated:
971 parser.error('Can\'t use --isolated with --clean.')
972 if options.isolate_server:
973 parser.error('Can\'t use --isolate-server with --clean.')
974 if options.json:
975 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -0700976 if options.named_caches:
977 parser.error('Can\t use --named-cache with --clean.')
978 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -0700979 return 0
nodirf33b8d62016-10-26 22:34:58 -0700980
maruel2e8d0f52016-07-16 07:51:29 -0700981 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -0700982 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -0700983
nodir55be77b2016-05-03 09:39:57 -0700984 if not options.isolated and not args:
985 parser.error('--isolated or command to run is required.')
986
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800987 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700988
989 isolateserver.process_isolate_server_options(
990 parser, options, True, False)
991 if not options.isolate_server:
992 if options.isolated:
993 parser.error('--isolated requires --isolate-server')
994 if ISOLATED_OUTDIR_PARAMETER in args:
995 parser.error(
996 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000997
nodir90bc8dc2016-06-15 13:35:21 -0700998 if options.root_dir:
999 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -07001000 if options.json:
1001 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001002
nodirbe642ff2016-06-09 15:51:51 -07001003 cipd.validate_cipd_options(parser, options)
1004
vadimsh232f5a82017-01-20 19:23:44 -08001005 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -08001006 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -07001007 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -08001008 run_dir, cipd.parse_package_args(options.cipd_packages),
1009 options.cipd_server, options.cipd_client_package,
1010 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001011
nodird6160682017-02-02 13:03:35 -08001012 @contextlib.contextmanager
nodir0ae98b32017-05-11 13:21:53 -07001013 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001014 # WARNING: this function depends on "options" variable defined in the outer
1015 # function.
nodir0ae98b32017-05-11 13:21:53 -07001016 caches = [
1017 (os.path.join(run_dir, unicode(relpath)), name)
1018 for name, relpath in options.named_caches
1019 ]
nodirf33b8d62016-10-26 22:34:58 -07001020 with named_cache_manager.open():
nodir0ae98b32017-05-11 13:21:53 -07001021 for path, name in caches:
1022 named_cache_manager.install(path, name)
nodird6160682017-02-02 13:03:35 -08001023 try:
1024 yield
1025 finally:
nodir0ae98b32017-05-11 13:21:53 -07001026 with named_cache_manager.open():
1027 for path, name in caches:
1028 named_cache_manager.uninstall(path, name)
nodirf33b8d62016-10-26 22:34:58 -07001029
nodirbe642ff2016-06-09 15:51:51 -07001030 try:
nodir90bc8dc2016-06-15 13:35:21 -07001031 if options.isolate_server:
1032 storage = isolateserver.get_storage(
1033 options.isolate_server, options.namespace)
1034 with storage:
nodirf33b8d62016-10-26 22:34:58 -07001035 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
1036 assert storage.hash_algo == isolate_cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -07001037 return run_tha_test(
maruelabec63c2017-04-26 11:53:24 -07001038 args,
nodirf33b8d62016-10-26 22:34:58 -07001039 options.isolated,
1040 storage,
1041 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001042 options.output,
nodir0ae98b32017-05-11 13:21:53 -07001043 install_named_caches,
nodirf33b8d62016-10-26 22:34:58 -07001044 options.leak_temp_dir,
1045 options.json, options.root_dir,
1046 options.hard_timeout,
1047 options.grace_period,
maruelabec63c2017-04-26 11:53:24 -07001048 options.bot_file,
nodirf33b8d62016-10-26 22:34:58 -07001049 install_packages_fn,
1050 options.use_symlinks)
maruel4409e302016-07-19 14:25:51 -07001051 return run_tha_test(
maruelabec63c2017-04-26 11:53:24 -07001052 args,
nodirf33b8d62016-10-26 22:34:58 -07001053 options.isolated,
1054 None,
1055 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001056 options.output,
nodir0ae98b32017-05-11 13:21:53 -07001057 install_named_caches,
nodirf33b8d62016-10-26 22:34:58 -07001058 options.leak_temp_dir,
1059 options.json,
1060 options.root_dir,
1061 options.hard_timeout,
1062 options.grace_period,
maruelabec63c2017-04-26 11:53:24 -07001063 options.bot_file,
nodirf33b8d62016-10-26 22:34:58 -07001064 install_packages_fn,
maruel4409e302016-07-19 14:25:51 -07001065 options.use_symlinks)
nodirf33b8d62016-10-26 22:34:58 -07001066 except (cipd.Error, named_cache.Error) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001067 print >> sys.stderr, ex.message
1068 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001069
1070
1071if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001072 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001073 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001074 fix_encoding.fix_encoding()
maruel4409e302016-07-19 14:25:51 -07001075 file_path.enable_symlink()
aludwin7556e0c2016-10-26 08:46:10 -07001076
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001077 sys.exit(main(sys.argv[1:]))