blob: 424b10f02bb893221ba89e7bd8dcb7b9b69e28be [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(
nodir220308c2017-02-01 19:32:53 -0800404 command, isolated_hash, storage, isolate_cache, outputs, init_named_caches,
nodirf33b8d62016-10-26 22:34:58 -0700405 leak_temp_dir, root_dir, hard_timeout, grace_period, bot_file, extra_args,
maruelcffa0542017-04-07 08:39:20 -0700406 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 """
nodir56efa452016-10-12 12:17:39 -0700413 assert root_dir or root_dir is None
nodir55be77b2016-05-03 09:39:57 -0700414 assert bool(command) ^ bool(isolated_hash)
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)
461 os.mkdir(run_dir)
462 else:
463 run_dir = make_temp_dir(ISOLATED_RUN_DIR, root_dir)
maruel03e11842016-07-14 10:50:16 -0700464 # storage should be normally set but don't crash if it is not. This can happen
465 # as Swarming task can run without an isolate server.
maruele2f2cb82016-07-13 14:41:03 -0700466 out_dir = make_temp_dir(ISOLATED_OUT_DIR, root_dir) if storage else None
467 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, root_dir)
nodir55be77b2016-05-03 09:39:57 -0700468 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700469
nodir55be77b2016-05-03 09:39:57 -0700470 try:
vadimsh232f5a82017-01-20 19:23:44 -0800471 with install_packages_fn(run_dir) as cipd_info:
472 if cipd_info:
473 result['stats']['cipd'] = cipd_info.stats
474 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700475
vadimsh232f5a82017-01-20 19:23:44 -0800476 if isolated_hash:
477 isolated_stats = result['stats'].setdefault('isolated', {})
478 bundle, isolated_stats['download'] = fetch_and_map(
479 isolated_hash=isolated_hash,
480 storage=storage,
481 cache=isolate_cache,
482 outdir=run_dir,
483 use_symlinks=use_symlinks)
484 if not bundle.command:
485 # Handle this as a task failure, not an internal failure.
486 sys.stderr.write(
487 '<The .isolated doesn\'t declare any command to run!>\n'
488 '<Check your .isolate for missing \'command\' variable>\n')
489 if os.environ.get('SWARMING_TASK_ID'):
490 # Give an additional hint when running as a swarming task.
491 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
492 result['exit_code'] = 1
493 return result
nodir55be77b2016-05-03 09:39:57 -0700494
vadimsh232f5a82017-01-20 19:23:44 -0800495 change_tree_read_only(run_dir, bundle.read_only)
496 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
497 command = bundle.command + extra_args
nodirbe642ff2016-06-09 15:51:51 -0700498
vadimsh232f5a82017-01-20 19:23:44 -0800499 # If we have an explicit list of files to return, make sure their
500 # directories exist now.
501 if storage and outputs:
502 isolateserver.create_directories(run_dir, outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700503
vadimsh232f5a82017-01-20 19:23:44 -0800504 command = tools.fix_python_path(command)
505 command = process_command(command, out_dir, bot_file)
506 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700507
nodird6160682017-02-02 13:03:35 -0800508 with init_named_caches(run_dir):
509 sys.stdout.flush()
510 start = time.time()
511 try:
512 result['exit_code'], result['had_hard_timeout'] = run_command(
513 command, cwd, get_command_env(tmp_dir, cipd_info),
514 hard_timeout, grace_period)
515 finally:
516 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700517 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700518 # An internal error occurred. Report accordingly so the swarming task will
519 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700520 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700521 result['internal_failure'] = str(e)
522 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700523
524 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700525 finally:
526 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700527 # Try to link files to the output directory, if specified.
528 if out_dir:
529 link_outputs_to_outdir(run_dir, out_dir, outputs)
530
nodir32a1ec12016-10-26 18:34:07 -0700531 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700532 if leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700533 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700534 logging.warning(
535 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700536 else:
maruel84537cb2015-10-16 14:21:28 -0700537 # On Windows rmtree(run_dir) call above has a synchronization effect: it
538 # finishes only when all task child processes terminate (since a running
539 # process locks *.exe file). Examine out_dir only after that call
540 # completes (since child processes may write to out_dir too and we need
541 # to wait for them to finish).
542 if fs.isdir(run_dir):
543 try:
544 success = file_path.rmtree(run_dir)
545 except OSError as e:
546 logging.error('Failure with %s', e)
547 success = False
548 if not success:
549 print >> sys.stderr, (
550 'Failed to delete the run directory, forcibly failing\n'
551 'the task because of it. No zombie process can outlive a\n'
552 'successful task run and still be marked as successful.\n'
553 'Fix your stuff.')
554 if result['exit_code'] == 0:
555 result['exit_code'] = 1
556 if fs.isdir(tmp_dir):
557 try:
558 success = file_path.rmtree(tmp_dir)
559 except OSError as e:
560 logging.error('Failure with %s', e)
561 success = False
562 if not success:
563 print >> sys.stderr, (
564 'Failed to delete the temporary directory, forcibly failing\n'
565 'the task because of it. No zombie process can outlive a\n'
566 'successful task run and still be marked as successful.\n'
567 'Fix your stuff.')
568 if result['exit_code'] == 0:
569 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700570
marueleb5fbee2015-09-17 13:01:36 -0700571 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700572 if out_dir:
nodir55715712016-06-03 12:28:19 -0700573 isolated_stats = result['stats'].setdefault('isolated', {})
574 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700575 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700576 if not success and result['exit_code'] == 0:
577 result['exit_code'] = 1
578 except Exception as e:
579 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700580 if out_dir:
581 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700582 result['internal_failure'] = str(e)
583 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500584
585
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400586def run_tha_test(
nodir220308c2017-02-01 19:32:53 -0800587 command, isolated_hash, storage, isolate_cache, outputs, init_named_caches,
nodirf33b8d62016-10-26 22:34:58 -0700588 leak_temp_dir, result_json, root_dir, hard_timeout, grace_period, bot_file,
589 extra_args, install_packages_fn, use_symlinks):
nodir55be77b2016-05-03 09:39:57 -0700590 """Runs an executable and records execution metadata.
591
592 Either command or isolated_hash must be specified.
593
594 If isolated_hash is specified, downloads the dependencies in the cache,
595 hardlinks them into a temporary directory and runs the command specified in
596 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500597
598 A temporary directory is created to hold the output files. The content inside
599 this directory will be uploaded back to |storage| packaged as a .isolated
600 file.
601
602 Arguments:
nodir55be77b2016-05-03 09:39:57 -0700603 command: the command to run, a list of strings. Mutually exclusive with
604 isolated_hash.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500605 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500606 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700607 The command specified in the .isolated is executed.
608 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500609 storage: an isolateserver.Storage object to retrieve remote objects. This
610 object has a reference to an isolateserver.StorageApi, which does
611 the actual I/O.
nodir6b945692016-10-19 19:09:06 -0700612 isolate_cache: an isolateserver.LocalCache to keep from retrieving the
613 same objects constantly by caching the objects retrieved.
614 Can be on-disk or in-memory.
nodird6160682017-02-02 13:03:35 -0800615 init_named_caches: a function (run_dir) => context manager that creates
616 symlinks for named caches in |run_dir|.
Kenneth Russell61d42352014-09-15 11:41:16 -0700617 leak_temp_dir: if true, the temporary directory will be deliberately leaked
618 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700619 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700620 exit code is always 0 unless an internal error occurred.
nodir90bc8dc2016-06-15 13:35:21 -0700621 root_dir: path to the directory to use to create the temporary directory. If
marueleb5fbee2015-09-17 13:01:36 -0700622 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700623 hard_timeout: kills the process if it lasts more than this amount of
624 seconds.
625 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500626 extra_args: optional arguments to add to the command stated in the .isolate
nodir55be77b2016-05-03 09:39:57 -0700627 file. Ignored if isolate_hash is empty.
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 """
nodir55be77b2016-05-03 09:39:57 -0700635 assert bool(command) ^ bool(isolated_hash)
636 extra_args = extra_args or []
nodirbe642ff2016-06-09 15:51:51 -0700637
nodir55be77b2016-05-03 09:39:57 -0700638 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
639 assert storage is not None, 'storage is None although outdir is specified'
640
maruela76b9ee2015-12-15 06:18:08 -0800641 if result_json:
642 # Write a json output file right away in case we get killed.
643 result = {
644 'exit_code': None,
645 'had_hard_timeout': False,
646 'internal_failure': 'Was terminated before completion',
647 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700648 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800649 }
650 tools.write_json(result_json, result, dense=True)
651
maruela9cfd6f2015-09-15 11:03:15 -0700652 # run_isolated exit code. Depends on if result_json is used or not.
653 result = map_and_run(
nodir220308c2017-02-01 19:32:53 -0800654 command, isolated_hash, storage, isolate_cache, outputs,
655 init_named_caches, leak_temp_dir, root_dir, hard_timeout, grace_period,
maruelcffa0542017-04-07 08:39:20 -0700656 bot_file, extra_args, install_packages_fn, use_symlinks, True)
maruela9cfd6f2015-09-15 11:03:15 -0700657 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700658
maruela9cfd6f2015-09-15 11:03:15 -0700659 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700660 # We've found tests to delete 'work' when quitting, causing an exception
661 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700662 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700663 tools.write_json(result_json, result, dense=True)
664 # Only return 1 if there was an internal error.
665 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000666
maruela9cfd6f2015-09-15 11:03:15 -0700667 # Marshall into old-style inline output.
668 if result['outputs_ref']:
669 data = {
670 'hash': result['outputs_ref']['isolated'],
671 'namespace': result['outputs_ref']['namespace'],
672 'storage': result['outputs_ref']['isolatedserver'],
673 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500674 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700675 print(
676 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
677 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800678 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700679 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000680
681
iannuccib58d10d2017-03-18 02:00:25 -0700682# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800683CipdInfo = collections.namedtuple('CipdInfo', [
684 'client', # cipd.CipdClient object
685 'cache_dir', # absolute path to bot-global cipd tag and instance cache
686 'stats', # dict with stats to return to the server
687 'pins', # dict with installed cipd pins to return to the server
688])
689
690
691@contextlib.contextmanager
692def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700693 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800694 yield None
695
696
iannuccib58d10d2017-03-18 02:00:25 -0700697def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
698 """Calls 'cipd ensure' for packages.
699
700 Args:
701 run_dir (str): root of installation.
702 cipd_cache_dir (str): the directory to use for the cipd package cache.
703 client (CipdClient): the cipd client to use
704 packages: packages to install, list [(path, package_name, version), ...].
705 timeout: max duration in seconds that this function can take.
706
707 Returns: list of pinned packages. Looks like [
708 {
709 'path': 'subdirectory',
710 'package_name': 'resolved/package/name',
711 'version': 'deadbeef...',
712 },
713 ...
714 ]
715 """
716 package_pins = [None]*len(packages)
717 def insert_pin(path, name, version, idx):
718 package_pins[idx] = {
719 'package_name': name,
720 # swarming deals with 'root' as '.'
721 'path': path or '.',
722 'version': version,
723 }
724
725 by_path = collections.defaultdict(list)
726 for i, (path, name, version) in enumerate(packages):
727 # cipd deals with 'root' as ''
728 if path == '.':
729 path = ''
730 by_path[path].append((name, version, i))
731
732 pins = client.ensure(
733 run_dir,
734 {
735 subdir: [(name, vers) for name, vers, _ in pkgs]
736 for subdir, pkgs in by_path.iteritems()
737 },
738 cache_dir=cipd_cache_dir,
739 timeout=timeout,
740 )
741
742 for subdir, pin_list in sorted(pins.iteritems()):
743 this_subdir = by_path[subdir]
744 for i, (name, version) in enumerate(pin_list):
745 insert_pin(subdir, name, version, this_subdir[i][2])
746
747 assert None not in package_pins
748
749 return package_pins
750
751
vadimsh232f5a82017-01-20 19:23:44 -0800752@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700753def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700754 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800755 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800756 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700757
vadimsh232f5a82017-01-20 19:23:44 -0800758 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
759
760 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700761 [
762 {
763 "path": path, "package_name": package_name, "version": version,
764 },
765 ...
766 ]
vadimsh902948e2017-01-20 15:57:32 -0800767 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700768
769 such that they correspond 1:1 to all input package arguments from the command
770 line. These dictionaries make their all the way back to swarming, where they
771 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700772
vadimsh902948e2017-01-20 15:57:32 -0800773 If 'packages' list is empty, will bootstrap CIPD client, but won't install
774 any packages.
775
776 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800777 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800778
nodirbe642ff2016-06-09 15:51:51 -0700779 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700780 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800781 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700782 service_url (str): CIPD server url, e.g.
783 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700784 client_package_name (str): CIPD package name of CIPD client.
785 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700786 cache_dir (str): where to keep cache of cipd clients, packages and tags.
787 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700788 """
789 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700790
nodirbe642ff2016-06-09 15:51:51 -0700791 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700792 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700793
vadimsh902948e2017-01-20 15:57:32 -0800794 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800795 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700796 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800797 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700798
nodirbe642ff2016-06-09 15:51:51 -0700799 get_client_start = time.time()
800 client_manager = cipd.get_client(
801 service_url, client_package_name, client_version, cache_dir,
802 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700803
nodirbe642ff2016-06-09 15:51:51 -0700804 with client_manager as client:
805 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700806
iannuccib58d10d2017-03-18 02:00:25 -0700807 package_pins = []
808 if packages:
809 package_pins = _install_packages(
810 run_dir, cipd_cache_dir, client, packages, timeoutfn())
811
812 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700813
vadimsh232f5a82017-01-20 19:23:44 -0800814 total_duration = time.time() - start
815 logging.info(
816 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700817
vadimsh232f5a82017-01-20 19:23:44 -0800818 yield CipdInfo(
819 client=client,
820 cache_dir=cipd_cache_dir,
821 stats={
822 'duration': total_duration,
823 'get_client_duration': get_client_duration,
824 },
825 pins={
iannuccib58d10d2017-03-18 02:00:25 -0700826 'client_package': {
827 'package_name': client.package_name,
828 'version': client.instance_id,
829 },
vadimsh232f5a82017-01-20 19:23:44 -0800830 'packages': package_pins,
831 })
nodirbe642ff2016-06-09 15:51:51 -0700832
833
nodirf33b8d62016-10-26 22:34:58 -0700834def clean_caches(options, isolate_cache, named_cache_manager):
835 """Trims isolated and named caches."""
836 # Which cache to trim first? Which of caches was used least recently?
837 with named_cache_manager.open():
838 oldest_isolated = isolate_cache.get_oldest()
839 oldest_named = named_cache_manager.get_oldest()
840 trimmers = [
841 (
842 isolate_cache.trim,
843 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
844 ),
845 (
846 lambda: named_cache_manager.trim(options.min_free_space),
847 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
848 ),
849 ]
850 trimmers.sort(key=lambda (_, ts): ts)
851 for trim, _ in trimmers:
852 trim()
853 isolate_cache.cleanup()
854
855
nodirbe642ff2016-06-09 15:51:51 -0700856def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400857 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700858 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000859 version=__version__,
860 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700861 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700862 '--clean', action='store_true',
863 help='Cleans the cache, trimming it necessary and remove corrupted items '
864 'and returns without executing anything; use with -v to know what '
865 'was done')
866 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -0700867 '--no-clean', action='store_true',
868 help='Do not clean the cache automatically on startup. This is meant for '
869 'bots where a separate execution with --clean was done earlier so '
870 'doing it again is redundant')
871 parser.add_option(
maruel4409e302016-07-19 14:25:51 -0700872 '--use-symlinks', action='store_true',
873 help='Use symlinks instead of hardlinks')
874 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700875 '--json',
876 help='dump output metadata to json file. When used, run_isolated returns '
877 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700878 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800879 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700880 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800881 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700882 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700883 parser.add_option(
884 '--bot-file',
885 help='Path to a file describing the state of the host. The content is '
886 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -0700887 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -0700888 '--output', action='append',
889 help='Specifies an output to return. If no outputs are specified, all '
890 'files located in $(ISOLATED_OUTDIR) will be returned; '
891 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
892 'specified by --output option (there can be multiple) will be '
893 'returned. Note that if a file in OUT_DIR has the same path '
894 'as an --output option, the --output version will be returned.')
895 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -0700896 '-a', '--argsfile',
897 # This is actually handled in parse_args; it's included here purely so it
898 # can make it into the help text.
899 help='Specify a file containing a JSON array of arguments to this '
900 'script. If --argsfile is provided, no other argument may be '
901 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500902 data_group = optparse.OptionGroup(parser, 'Data source')
903 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500904 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700905 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500906 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500907 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000908
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400909 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700910
911 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -0700912 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000913
Kenneth Russell61d42352014-09-15 11:41:16 -0700914 debug_group = optparse.OptionGroup(parser, 'Debugging')
915 debug_group.add_option(
916 '--leak-temp-dir',
917 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700918 help='Deliberately leak isolate\'s temp dir for later examination. '
919 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700920 debug_group.add_option(
921 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700922 parser.add_option_group(debug_group)
923
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800924 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700925
nodirf33b8d62016-10-26 22:34:58 -0700926 parser.set_defaults(
927 cache='cache',
928 cipd_cache='cipd_cache',
929 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -0700930 return parser
931
932
aludwin7556e0c2016-10-26 08:46:10 -0700933def parse_args(args):
934 # Create a fake mini-parser just to get out the "-a" command. Note that
935 # it's not documented here; instead, it's documented in create_option_parser
936 # even though that parser will never actually get to parse it. This is
937 # because --argsfile is exclusive with all other options and arguments.
938 file_argparse = argparse.ArgumentParser(add_help=False)
939 file_argparse.add_argument('-a', '--argsfile')
940 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
941 if file_args.argsfile:
942 if nonfile_args:
943 file_argparse.error('Can\'t specify --argsfile with'
944 'any other arguments (%s)' % nonfile_args)
945 try:
946 with open(file_args.argsfile, 'r') as f:
947 args = json.load(f)
948 except (IOError, OSError, ValueError) as e:
949 # We don't need to error out here - "args" is now empty,
950 # so the call below to parser.parse_args(args) will fail
951 # and print the full help text.
952 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
953
954 # Even if we failed to read the args, just call the normal parser now since it
955 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -0700956 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500957 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -0700958 return (parser, options, args)
959
960
961def main(args):
962 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700963
nodirf33b8d62016-10-26 22:34:58 -0700964 isolate_cache = isolateserver.process_cache_options(options, trim=False)
965 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -0700966 if options.clean:
967 if options.isolated:
968 parser.error('Can\'t use --isolated with --clean.')
969 if options.isolate_server:
970 parser.error('Can\'t use --isolate-server with --clean.')
971 if options.json:
972 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -0700973 if options.named_caches:
974 parser.error('Can\t use --named-cache with --clean.')
975 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -0700976 return 0
nodirf33b8d62016-10-26 22:34:58 -0700977
maruel2e8d0f52016-07-16 07:51:29 -0700978 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -0700979 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -0700980
nodir55be77b2016-05-03 09:39:57 -0700981 if not options.isolated and not args:
982 parser.error('--isolated or command to run is required.')
983
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800984 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700985
986 isolateserver.process_isolate_server_options(
987 parser, options, True, False)
988 if not options.isolate_server:
989 if options.isolated:
990 parser.error('--isolated requires --isolate-server')
991 if ISOLATED_OUTDIR_PARAMETER in args:
992 parser.error(
993 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000994
nodir90bc8dc2016-06-15 13:35:21 -0700995 if options.root_dir:
996 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -0700997 if options.json:
998 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700999
nodirbe642ff2016-06-09 15:51:51 -07001000 cipd.validate_cipd_options(parser, options)
1001
vadimsh232f5a82017-01-20 19:23:44 -08001002 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -08001003 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -07001004 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -08001005 run_dir, cipd.parse_package_args(options.cipd_packages),
1006 options.cipd_server, options.cipd_client_package,
1007 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001008
nodird6160682017-02-02 13:03:35 -08001009 @contextlib.contextmanager
nodirf33b8d62016-10-26 22:34:58 -07001010 def init_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001011 # WARNING: this function depends on "options" variable defined in the outer
1012 # function.
nodirf33b8d62016-10-26 22:34:58 -07001013 with named_cache_manager.open():
1014 named_cache_manager.create_symlinks(run_dir, options.named_caches)
nodird6160682017-02-02 13:03:35 -08001015 try:
1016 yield
1017 finally:
1018 if not options.leak_temp_dir:
1019 named_cache_manager.delete_symlinks(run_dir, options.named_caches)
nodirf33b8d62016-10-26 22:34:58 -07001020
nodirbe642ff2016-06-09 15:51:51 -07001021 try:
nodir90bc8dc2016-06-15 13:35:21 -07001022 command = [] if options.isolated else args
1023 if options.isolate_server:
1024 storage = isolateserver.get_storage(
1025 options.isolate_server, options.namespace)
1026 with storage:
nodirf33b8d62016-10-26 22:34:58 -07001027 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
1028 assert storage.hash_algo == isolate_cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -07001029 return run_tha_test(
nodirf33b8d62016-10-26 22:34:58 -07001030 command,
1031 options.isolated,
1032 storage,
1033 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001034 options.output,
nodirf33b8d62016-10-26 22:34:58 -07001035 init_named_caches,
1036 options.leak_temp_dir,
1037 options.json, options.root_dir,
1038 options.hard_timeout,
1039 options.grace_period,
1040 options.bot_file, args,
1041 install_packages_fn,
1042 options.use_symlinks)
maruel4409e302016-07-19 14:25:51 -07001043 return run_tha_test(
nodirf33b8d62016-10-26 22:34:58 -07001044 command,
1045 options.isolated,
1046 None,
1047 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001048 options.output,
nodirf33b8d62016-10-26 22:34:58 -07001049 init_named_caches,
1050 options.leak_temp_dir,
1051 options.json,
1052 options.root_dir,
1053 options.hard_timeout,
1054 options.grace_period,
1055 options.bot_file, args,
1056 install_packages_fn,
maruel4409e302016-07-19 14:25:51 -07001057 options.use_symlinks)
nodirf33b8d62016-10-26 22:34:58 -07001058 except (cipd.Error, named_cache.Error) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001059 print >> sys.stderr, ex.message
1060 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001061
1062
1063if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001064 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001065 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001066 fix_encoding.fix_encoding()
maruel4409e302016-07-19 14:25:51 -07001067 file_path.enable_symlink()
aludwin7556e0c2016-10-26 08:46:10 -07001068
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001069 sys.exit(main(sys.argv[1:]))