blob: 5c79b942ef5cc3b76169192433dfe43f5ee13c32 [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(
nodir26251c42017-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)
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)
vadimsh232f5a82017-01-20 19:23:44 -0800484 change_tree_read_only(run_dir, bundle.read_only)
485 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
maruelabec63c2017-04-26 11:53:24 -0700486 # Inject the command
487 if bundle.command:
488 command = bundle.command + command
489
490 if not command:
491 # Handle this as a task failure, not an internal failure.
492 sys.stderr.write(
493 '<No command was specified!>\n'
494 '<Please secify a command when triggering your Swarming task>\n')
495 result['exit_code'] = 1
496 return result
nodirbe642ff2016-06-09 15:51:51 -0700497
vadimsh232f5a82017-01-20 19:23:44 -0800498 # If we have an explicit list of files to return, make sure their
499 # directories exist now.
500 if storage and outputs:
501 isolateserver.create_directories(run_dir, outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700502
vadimsh232f5a82017-01-20 19:23:44 -0800503 command = tools.fix_python_path(command)
504 command = process_command(command, out_dir, bot_file)
505 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700506
nodir26251c42017-05-11 13:21:53 -0700507 with install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -0800508 sys.stdout.flush()
509 start = time.time()
510 try:
511 result['exit_code'], result['had_hard_timeout'] = run_command(
512 command, cwd, get_command_env(tmp_dir, cipd_info),
513 hard_timeout, grace_period)
514 finally:
515 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700516 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700517 # An internal error occurred. Report accordingly so the swarming task will
518 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700519 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700520 result['internal_failure'] = str(e)
521 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700522
523 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700524 finally:
525 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700526 # Try to link files to the output directory, if specified.
527 if out_dir:
528 link_outputs_to_outdir(run_dir, out_dir, outputs)
529
nodir32a1ec12016-10-26 18:34:07 -0700530 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700531 if leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700532 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700533 logging.warning(
534 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700535 else:
maruel84537cb2015-10-16 14:21:28 -0700536 # On Windows rmtree(run_dir) call above has a synchronization effect: it
537 # finishes only when all task child processes terminate (since a running
538 # process locks *.exe file). Examine out_dir only after that call
539 # completes (since child processes may write to out_dir too and we need
540 # to wait for them to finish).
541 if fs.isdir(run_dir):
542 try:
543 success = file_path.rmtree(run_dir)
544 except OSError as e:
545 logging.error('Failure with %s', e)
546 success = False
547 if not success:
548 print >> sys.stderr, (
qyearsley468b7e52017-05-09 16:17:26 -0700549 'Failed to delete the run directory, thus failing the task.\n'
550 'This may be due to a subprocess outliving the main task\n'
551 'process, holding on to resources. Please fix the task so\n'
552 'that it releases resources and cleans up subprocesses.')
maruel84537cb2015-10-16 14:21:28 -0700553 if result['exit_code'] == 0:
554 result['exit_code'] = 1
555 if fs.isdir(tmp_dir):
556 try:
557 success = file_path.rmtree(tmp_dir)
558 except OSError as e:
559 logging.error('Failure with %s', e)
560 success = False
561 if not success:
562 print >> sys.stderr, (
qyearsley468b7e52017-05-09 16:17:26 -0700563 'Failed to delete the temp directory, thus failing the task.\n'
564 'This may be due to a subprocess outliving the main task\n'
565 'process, holding on to resources. Please fix the task so\n'
566 'that it releases resources and cleans up subprocesses.')
maruel84537cb2015-10-16 14:21:28 -0700567 if result['exit_code'] == 0:
568 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700569
marueleb5fbee2015-09-17 13:01:36 -0700570 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700571 if out_dir:
nodir55715712016-06-03 12:28:19 -0700572 isolated_stats = result['stats'].setdefault('isolated', {})
573 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700574 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700575 if not success and result['exit_code'] == 0:
576 result['exit_code'] = 1
577 except Exception as e:
578 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700579 if out_dir:
580 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700581 result['internal_failure'] = str(e)
582 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500583
584
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400585def run_tha_test(
nodir26251c42017-05-11 13:21:53 -0700586 command, isolated_hash, storage, isolate_cache, outputs,
587 install_named_caches, leak_temp_dir, result_json, root_dir, hard_timeout,
588 grace_period, bot_file, install_packages_fn, use_symlinks):
nodir55be77b2016-05-03 09:39:57 -0700589 """Runs an executable and records execution metadata.
590
591 Either command or isolated_hash must be specified.
592
593 If isolated_hash is specified, downloads the dependencies in the cache,
594 hardlinks them into a temporary directory and runs the command specified in
595 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500596
597 A temporary directory is created to hold the output files. The content inside
598 this directory will be uploaded back to |storage| packaged as a .isolated
599 file.
600
601 Arguments:
maruelabec63c2017-04-26 11:53:24 -0700602 command: a list of string; the command to run OR optional arguments to add
603 to the command stated in the .isolated file if a command was
604 specified.
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.
nodir26251c42017-05-11 13:21:53 -0700615 install_named_caches: a function (run_dir) => context manager that installs
616 named caches into |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.
iannuccib58d10d2017-03-18 02:00:25 -0700626 install_packages_fn: context manager dir => CipdInfo, see
627 install_client_and_packages.
maruel4409e302016-07-19 14:25:51 -0700628 use_symlinks: create tree with symlinks instead of hardlinks.
maruela9cfd6f2015-09-15 11:03:15 -0700629
630 Returns:
631 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000632 """
maruela76b9ee2015-12-15 06:18:08 -0800633 if result_json:
634 # Write a json output file right away in case we get killed.
635 result = {
636 'exit_code': None,
637 'had_hard_timeout': False,
638 'internal_failure': 'Was terminated before completion',
639 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700640 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800641 }
642 tools.write_json(result_json, result, dense=True)
643
maruela9cfd6f2015-09-15 11:03:15 -0700644 # run_isolated exit code. Depends on if result_json is used or not.
645 result = map_and_run(
nodir220308c2017-02-01 19:32:53 -0800646 command, isolated_hash, storage, isolate_cache, outputs,
nodir26251c42017-05-11 13:21:53 -0700647 install_named_caches, leak_temp_dir, root_dir, hard_timeout, grace_period,
maruelabec63c2017-04-26 11:53:24 -0700648 bot_file, install_packages_fn, use_symlinks, True)
maruela9cfd6f2015-09-15 11:03:15 -0700649 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700650
maruela9cfd6f2015-09-15 11:03:15 -0700651 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700652 # We've found tests to delete 'work' when quitting, causing an exception
653 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700654 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700655 tools.write_json(result_json, result, dense=True)
656 # Only return 1 if there was an internal error.
657 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000658
maruela9cfd6f2015-09-15 11:03:15 -0700659 # Marshall into old-style inline output.
660 if result['outputs_ref']:
661 data = {
662 'hash': result['outputs_ref']['isolated'],
663 'namespace': result['outputs_ref']['namespace'],
664 'storage': result['outputs_ref']['isolatedserver'],
665 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500666 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700667 print(
668 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
669 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800670 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700671 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000672
673
iannuccib58d10d2017-03-18 02:00:25 -0700674# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800675CipdInfo = collections.namedtuple('CipdInfo', [
676 'client', # cipd.CipdClient object
677 'cache_dir', # absolute path to bot-global cipd tag and instance cache
678 'stats', # dict with stats to return to the server
679 'pins', # dict with installed cipd pins to return to the server
680])
681
682
683@contextlib.contextmanager
684def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700685 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800686 yield None
687
688
iannuccib58d10d2017-03-18 02:00:25 -0700689def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
690 """Calls 'cipd ensure' for packages.
691
692 Args:
693 run_dir (str): root of installation.
694 cipd_cache_dir (str): the directory to use for the cipd package cache.
695 client (CipdClient): the cipd client to use
696 packages: packages to install, list [(path, package_name, version), ...].
697 timeout: max duration in seconds that this function can take.
698
699 Returns: list of pinned packages. Looks like [
700 {
701 'path': 'subdirectory',
702 'package_name': 'resolved/package/name',
703 'version': 'deadbeef...',
704 },
705 ...
706 ]
707 """
708 package_pins = [None]*len(packages)
709 def insert_pin(path, name, version, idx):
710 package_pins[idx] = {
711 'package_name': name,
712 # swarming deals with 'root' as '.'
713 'path': path or '.',
714 'version': version,
715 }
716
717 by_path = collections.defaultdict(list)
718 for i, (path, name, version) in enumerate(packages):
719 # cipd deals with 'root' as ''
720 if path == '.':
721 path = ''
722 by_path[path].append((name, version, i))
723
724 pins = client.ensure(
725 run_dir,
726 {
727 subdir: [(name, vers) for name, vers, _ in pkgs]
728 for subdir, pkgs in by_path.iteritems()
729 },
730 cache_dir=cipd_cache_dir,
731 timeout=timeout,
732 )
733
734 for subdir, pin_list in sorted(pins.iteritems()):
735 this_subdir = by_path[subdir]
736 for i, (name, version) in enumerate(pin_list):
737 insert_pin(subdir, name, version, this_subdir[i][2])
738
739 assert None not in package_pins
740
741 return package_pins
742
743
vadimsh232f5a82017-01-20 19:23:44 -0800744@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700745def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700746 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800747 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800748 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700749
vadimsh232f5a82017-01-20 19:23:44 -0800750 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
751
752 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700753 [
754 {
755 "path": path, "package_name": package_name, "version": version,
756 },
757 ...
758 ]
vadimsh902948e2017-01-20 15:57:32 -0800759 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700760
761 such that they correspond 1:1 to all input package arguments from the command
762 line. These dictionaries make their all the way back to swarming, where they
763 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700764
vadimsh902948e2017-01-20 15:57:32 -0800765 If 'packages' list is empty, will bootstrap CIPD client, but won't install
766 any packages.
767
768 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800769 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800770
nodirbe642ff2016-06-09 15:51:51 -0700771 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700772 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800773 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700774 service_url (str): CIPD server url, e.g.
775 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700776 client_package_name (str): CIPD package name of CIPD client.
777 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700778 cache_dir (str): where to keep cache of cipd clients, packages and tags.
779 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700780 """
781 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700782
nodirbe642ff2016-06-09 15:51:51 -0700783 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700784 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700785
vadimsh902948e2017-01-20 15:57:32 -0800786 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800787 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700788 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800789 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700790
nodirbe642ff2016-06-09 15:51:51 -0700791 get_client_start = time.time()
792 client_manager = cipd.get_client(
793 service_url, client_package_name, client_version, cache_dir,
794 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700795
nodirbe642ff2016-06-09 15:51:51 -0700796 with client_manager as client:
797 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700798
iannuccib58d10d2017-03-18 02:00:25 -0700799 package_pins = []
800 if packages:
801 package_pins = _install_packages(
802 run_dir, cipd_cache_dir, client, packages, timeoutfn())
803
804 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700805
vadimsh232f5a82017-01-20 19:23:44 -0800806 total_duration = time.time() - start
807 logging.info(
808 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700809
vadimsh232f5a82017-01-20 19:23:44 -0800810 yield CipdInfo(
811 client=client,
812 cache_dir=cipd_cache_dir,
813 stats={
814 'duration': total_duration,
815 'get_client_duration': get_client_duration,
816 },
817 pins={
iannuccib58d10d2017-03-18 02:00:25 -0700818 'client_package': {
819 'package_name': client.package_name,
820 'version': client.instance_id,
821 },
vadimsh232f5a82017-01-20 19:23:44 -0800822 'packages': package_pins,
823 })
nodirbe642ff2016-06-09 15:51:51 -0700824
825
nodirf33b8d62016-10-26 22:34:58 -0700826def clean_caches(options, isolate_cache, named_cache_manager):
maruele6fc9382017-05-04 09:03:48 -0700827 """Trims isolated and named caches.
828
829 The goal here is to coherently trim both caches, deleting older items
830 independent of which container they belong to.
831 """
832 # TODO(maruel): Trim CIPD cache the same way.
833 total = 0
nodirf33b8d62016-10-26 22:34:58 -0700834 with named_cache_manager.open():
835 oldest_isolated = isolate_cache.get_oldest()
836 oldest_named = named_cache_manager.get_oldest()
837 trimmers = [
838 (
839 isolate_cache.trim,
840 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
841 ),
842 (
843 lambda: named_cache_manager.trim(options.min_free_space),
844 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
845 ),
846 ]
847 trimmers.sort(key=lambda (_, ts): ts)
maruele6fc9382017-05-04 09:03:48 -0700848 # TODO(maruel): This is incorrect, we want to trim 'items' that are strictly
849 # the oldest independent of in which cache they live in. Right now, the
850 # cache with the oldest item pays the price.
nodirf33b8d62016-10-26 22:34:58 -0700851 for trim, _ in trimmers:
maruele6fc9382017-05-04 09:03:48 -0700852 total += trim()
nodirf33b8d62016-10-26 22:34:58 -0700853 isolate_cache.cleanup()
maruele6fc9382017-05-04 09:03:48 -0700854 return total
nodirf33b8d62016-10-26 22:34:58 -0700855
856
nodirbe642ff2016-06-09 15:51:51 -0700857def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400858 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700859 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000860 version=__version__,
861 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700862 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700863 '--clean', action='store_true',
864 help='Cleans the cache, trimming it necessary and remove corrupted items '
865 'and returns without executing anything; use with -v to know what '
866 'was done')
867 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -0700868 '--no-clean', action='store_true',
869 help='Do not clean the cache automatically on startup. This is meant for '
870 'bots where a separate execution with --clean was done earlier so '
871 'doing it again is redundant')
872 parser.add_option(
maruel4409e302016-07-19 14:25:51 -0700873 '--use-symlinks', action='store_true',
874 help='Use symlinks instead of hardlinks')
875 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700876 '--json',
877 help='dump output metadata to json file. When used, run_isolated returns '
878 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700879 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800880 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700881 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800882 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700883 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700884 parser.add_option(
885 '--bot-file',
886 help='Path to a file describing the state of the host. The content is '
887 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -0700888 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -0700889 '--output', action='append',
890 help='Specifies an output to return. If no outputs are specified, all '
891 'files located in $(ISOLATED_OUTDIR) will be returned; '
892 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
893 'specified by --output option (there can be multiple) will be '
894 'returned. Note that if a file in OUT_DIR has the same path '
895 'as an --output option, the --output version will be returned.')
896 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -0700897 '-a', '--argsfile',
898 # This is actually handled in parse_args; it's included here purely so it
899 # can make it into the help text.
900 help='Specify a file containing a JSON array of arguments to this '
901 'script. If --argsfile is provided, no other argument may be '
902 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500903 data_group = optparse.OptionGroup(parser, 'Data source')
904 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500905 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700906 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500907 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500908 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000909
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400910 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700911
912 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -0700913 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000914
Kenneth Russell61d42352014-09-15 11:41:16 -0700915 debug_group = optparse.OptionGroup(parser, 'Debugging')
916 debug_group.add_option(
917 '--leak-temp-dir',
918 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700919 help='Deliberately leak isolate\'s temp dir for later examination. '
920 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700921 debug_group.add_option(
922 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700923 parser.add_option_group(debug_group)
924
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800925 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700926
nodirf33b8d62016-10-26 22:34:58 -0700927 parser.set_defaults(
928 cache='cache',
929 cipd_cache='cipd_cache',
930 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -0700931 return parser
932
933
aludwin7556e0c2016-10-26 08:46:10 -0700934def parse_args(args):
935 # Create a fake mini-parser just to get out the "-a" command. Note that
936 # it's not documented here; instead, it's documented in create_option_parser
937 # even though that parser will never actually get to parse it. This is
938 # because --argsfile is exclusive with all other options and arguments.
939 file_argparse = argparse.ArgumentParser(add_help=False)
940 file_argparse.add_argument('-a', '--argsfile')
941 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
942 if file_args.argsfile:
943 if nonfile_args:
944 file_argparse.error('Can\'t specify --argsfile with'
945 'any other arguments (%s)' % nonfile_args)
946 try:
947 with open(file_args.argsfile, 'r') as f:
948 args = json.load(f)
949 except (IOError, OSError, ValueError) as e:
950 # We don't need to error out here - "args" is now empty,
951 # so the call below to parser.parse_args(args) will fail
952 # and print the full help text.
953 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
954
955 # Even if we failed to read the args, just call the normal parser now since it
956 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -0700957 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500958 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -0700959 return (parser, options, args)
960
961
962def main(args):
963 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700964
nodirf33b8d62016-10-26 22:34:58 -0700965 isolate_cache = isolateserver.process_cache_options(options, trim=False)
966 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -0700967 if options.clean:
968 if options.isolated:
969 parser.error('Can\'t use --isolated with --clean.')
970 if options.isolate_server:
971 parser.error('Can\'t use --isolate-server with --clean.')
972 if options.json:
973 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -0700974 if options.named_caches:
975 parser.error('Can\t use --named-cache with --clean.')
976 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -0700977 return 0
nodirf33b8d62016-10-26 22:34:58 -0700978
maruel2e8d0f52016-07-16 07:51:29 -0700979 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -0700980 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -0700981
nodir55be77b2016-05-03 09:39:57 -0700982 if not options.isolated and not args:
983 parser.error('--isolated or command to run is required.')
984
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800985 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700986
987 isolateserver.process_isolate_server_options(
988 parser, options, True, False)
989 if not options.isolate_server:
990 if options.isolated:
991 parser.error('--isolated requires --isolate-server')
992 if ISOLATED_OUTDIR_PARAMETER in args:
993 parser.error(
994 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000995
nodir90bc8dc2016-06-15 13:35:21 -0700996 if options.root_dir:
997 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -0700998 if options.json:
999 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001000
nodirbe642ff2016-06-09 15:51:51 -07001001 cipd.validate_cipd_options(parser, options)
1002
vadimsh232f5a82017-01-20 19:23:44 -08001003 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -08001004 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -07001005 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -08001006 run_dir, cipd.parse_package_args(options.cipd_packages),
1007 options.cipd_server, options.cipd_client_package,
1008 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001009
nodird6160682017-02-02 13:03:35 -08001010 @contextlib.contextmanager
nodir26251c42017-05-11 13:21:53 -07001011 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001012 # WARNING: this function depends on "options" variable defined in the outer
1013 # function.
nodir26251c42017-05-11 13:21:53 -07001014 caches = [
1015 (os.path.join(run_dir, unicode(relpath)), name)
1016 for name, relpath in options.named_caches
1017 ]
nodirf33b8d62016-10-26 22:34:58 -07001018 with named_cache_manager.open():
nodir26251c42017-05-11 13:21:53 -07001019 for path, name in caches:
1020 named_cache_manager.install(path, name)
nodird6160682017-02-02 13:03:35 -08001021 try:
1022 yield
1023 finally:
nodir26251c42017-05-11 13:21:53 -07001024 with named_cache_manager.open():
1025 for path, name in caches:
1026 named_cache_manager.uninstall(path, name)
nodirf33b8d62016-10-26 22:34:58 -07001027
nodirbe642ff2016-06-09 15:51:51 -07001028 try:
nodir90bc8dc2016-06-15 13:35:21 -07001029 if options.isolate_server:
1030 storage = isolateserver.get_storage(
1031 options.isolate_server, options.namespace)
1032 with storage:
nodirf33b8d62016-10-26 22:34:58 -07001033 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
1034 assert storage.hash_algo == isolate_cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -07001035 return run_tha_test(
maruelabec63c2017-04-26 11:53:24 -07001036 args,
nodirf33b8d62016-10-26 22:34:58 -07001037 options.isolated,
1038 storage,
1039 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001040 options.output,
nodir26251c42017-05-11 13:21:53 -07001041 install_named_caches,
nodirf33b8d62016-10-26 22:34:58 -07001042 options.leak_temp_dir,
1043 options.json, options.root_dir,
1044 options.hard_timeout,
1045 options.grace_period,
maruelabec63c2017-04-26 11:53:24 -07001046 options.bot_file,
nodirf33b8d62016-10-26 22:34:58 -07001047 install_packages_fn,
1048 options.use_symlinks)
maruel4409e302016-07-19 14:25:51 -07001049 return run_tha_test(
maruelabec63c2017-04-26 11:53:24 -07001050 args,
nodirf33b8d62016-10-26 22:34:58 -07001051 options.isolated,
1052 None,
1053 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001054 options.output,
nodir26251c42017-05-11 13:21:53 -07001055 install_named_caches,
nodirf33b8d62016-10-26 22:34:58 -07001056 options.leak_temp_dir,
1057 options.json,
1058 options.root_dir,
1059 options.hard_timeout,
1060 options.grace_period,
maruelabec63c2017-04-26 11:53:24 -07001061 options.bot_file,
nodirf33b8d62016-10-26 22:34:58 -07001062 install_packages_fn,
maruel4409e302016-07-19 14:25:51 -07001063 options.use_symlinks)
nodirf33b8d62016-10-26 22:34:58 -07001064 except (cipd.Error, named_cache.Error) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001065 print >> sys.stderr, ex.message
1066 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001067
1068
1069if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001070 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001071 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001072 fix_encoding.fix_encoding()
maruel4409e302016-07-19 14:25:51 -07001073 file_path.enable_symlink()
aludwin7556e0c2016-10-26 08:46:10 -07001074
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001075 sys.exit(main(sys.argv[1:]))