blob: 19c084ac183650b13d62adbdb13a8623253c66a6 [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
nodirf33b8d62016-10-26 22:34:58 -070030__version__ = '0.9'
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,
406 install_packages_fn, use_symlinks):
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.
456 run_dir = make_temp_dir(ISOLATED_RUN_DIR, root_dir)
maruel03e11842016-07-14 10:50:16 -0700457 # storage should be normally set but don't crash if it is not. This can happen
458 # as Swarming task can run without an isolate server.
maruele2f2cb82016-07-13 14:41:03 -0700459 out_dir = make_temp_dir(ISOLATED_OUT_DIR, root_dir) if storage else None
460 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, root_dir)
nodir55be77b2016-05-03 09:39:57 -0700461 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700462
nodir55be77b2016-05-03 09:39:57 -0700463 try:
vadimsh232f5a82017-01-20 19:23:44 -0800464 with install_packages_fn(run_dir) as cipd_info:
465 if cipd_info:
466 result['stats']['cipd'] = cipd_info.stats
467 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700468
vadimsh232f5a82017-01-20 19:23:44 -0800469 if isolated_hash:
470 isolated_stats = result['stats'].setdefault('isolated', {})
471 bundle, isolated_stats['download'] = fetch_and_map(
472 isolated_hash=isolated_hash,
473 storage=storage,
474 cache=isolate_cache,
475 outdir=run_dir,
476 use_symlinks=use_symlinks)
477 if not bundle.command:
478 # Handle this as a task failure, not an internal failure.
479 sys.stderr.write(
480 '<The .isolated doesn\'t declare any command to run!>\n'
481 '<Check your .isolate for missing \'command\' variable>\n')
482 if os.environ.get('SWARMING_TASK_ID'):
483 # Give an additional hint when running as a swarming task.
484 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
485 result['exit_code'] = 1
486 return result
nodir55be77b2016-05-03 09:39:57 -0700487
vadimsh232f5a82017-01-20 19:23:44 -0800488 change_tree_read_only(run_dir, bundle.read_only)
489 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
490 command = bundle.command + extra_args
nodirbe642ff2016-06-09 15:51:51 -0700491
vadimsh232f5a82017-01-20 19:23:44 -0800492 # If we have an explicit list of files to return, make sure their
493 # directories exist now.
494 if storage and outputs:
495 isolateserver.create_directories(run_dir, outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700496
vadimsh232f5a82017-01-20 19:23:44 -0800497 command = tools.fix_python_path(command)
498 command = process_command(command, out_dir, bot_file)
499 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700500
nodird6160682017-02-02 13:03:35 -0800501 with init_named_caches(run_dir):
502 sys.stdout.flush()
503 start = time.time()
504 try:
505 result['exit_code'], result['had_hard_timeout'] = run_command(
506 command, cwd, get_command_env(tmp_dir, cipd_info),
507 hard_timeout, grace_period)
508 finally:
509 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700510 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700511 # An internal error occurred. Report accordingly so the swarming task will
512 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700513 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700514 result['internal_failure'] = str(e)
515 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700516
517 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700518 finally:
519 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700520 # Try to link files to the output directory, if specified.
521 if out_dir:
522 link_outputs_to_outdir(run_dir, out_dir, outputs)
523
nodir32a1ec12016-10-26 18:34:07 -0700524 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700525 if leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700526 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700527 logging.warning(
528 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700529 else:
maruel84537cb2015-10-16 14:21:28 -0700530 # On Windows rmtree(run_dir) call above has a synchronization effect: it
531 # finishes only when all task child processes terminate (since a running
532 # process locks *.exe file). Examine out_dir only after that call
533 # completes (since child processes may write to out_dir too and we need
534 # to wait for them to finish).
535 if fs.isdir(run_dir):
536 try:
537 success = file_path.rmtree(run_dir)
538 except OSError as e:
539 logging.error('Failure with %s', e)
540 success = False
541 if not success:
542 print >> sys.stderr, (
543 'Failed to delete the run directory, forcibly failing\n'
544 'the task because of it. No zombie process can outlive a\n'
545 'successful task run and still be marked as successful.\n'
546 'Fix your stuff.')
547 if result['exit_code'] == 0:
548 result['exit_code'] = 1
549 if fs.isdir(tmp_dir):
550 try:
551 success = file_path.rmtree(tmp_dir)
552 except OSError as e:
553 logging.error('Failure with %s', e)
554 success = False
555 if not success:
556 print >> sys.stderr, (
557 'Failed to delete the temporary directory, forcibly failing\n'
558 'the task because of it. No zombie process can outlive a\n'
559 'successful task run and still be marked as successful.\n'
560 'Fix your stuff.')
561 if result['exit_code'] == 0:
562 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700563
marueleb5fbee2015-09-17 13:01:36 -0700564 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700565 if out_dir:
nodir55715712016-06-03 12:28:19 -0700566 isolated_stats = result['stats'].setdefault('isolated', {})
567 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700568 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700569 if not success and result['exit_code'] == 0:
570 result['exit_code'] = 1
571 except Exception as e:
572 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700573 if out_dir:
574 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700575 result['internal_failure'] = str(e)
576 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500577
578
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400579def run_tha_test(
nodir220308c2017-02-01 19:32:53 -0800580 command, isolated_hash, storage, isolate_cache, outputs, init_named_caches,
nodirf33b8d62016-10-26 22:34:58 -0700581 leak_temp_dir, result_json, root_dir, hard_timeout, grace_period, bot_file,
582 extra_args, install_packages_fn, use_symlinks):
nodir55be77b2016-05-03 09:39:57 -0700583 """Runs an executable and records execution metadata.
584
585 Either command or isolated_hash must be specified.
586
587 If isolated_hash is specified, downloads the dependencies in the cache,
588 hardlinks them into a temporary directory and runs the command specified in
589 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500590
591 A temporary directory is created to hold the output files. The content inside
592 this directory will be uploaded back to |storage| packaged as a .isolated
593 file.
594
595 Arguments:
nodir55be77b2016-05-03 09:39:57 -0700596 command: the command to run, a list of strings. Mutually exclusive with
597 isolated_hash.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500598 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500599 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700600 The command specified in the .isolated is executed.
601 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500602 storage: an isolateserver.Storage object to retrieve remote objects. This
603 object has a reference to an isolateserver.StorageApi, which does
604 the actual I/O.
nodir6b945692016-10-19 19:09:06 -0700605 isolate_cache: an isolateserver.LocalCache to keep from retrieving the
606 same objects constantly by caching the objects retrieved.
607 Can be on-disk or in-memory.
nodird6160682017-02-02 13:03:35 -0800608 init_named_caches: a function (run_dir) => context manager that creates
609 symlinks for named caches in |run_dir|.
Kenneth Russell61d42352014-09-15 11:41:16 -0700610 leak_temp_dir: if true, the temporary directory will be deliberately leaked
611 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700612 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700613 exit code is always 0 unless an internal error occurred.
nodir90bc8dc2016-06-15 13:35:21 -0700614 root_dir: path to the directory to use to create the temporary directory. If
marueleb5fbee2015-09-17 13:01:36 -0700615 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700616 hard_timeout: kills the process if it lasts more than this amount of
617 seconds.
618 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500619 extra_args: optional arguments to add to the command stated in the .isolate
nodir55be77b2016-05-03 09:39:57 -0700620 file. Ignored if isolate_hash is empty.
iannuccib58d10d2017-03-18 02:00:25 -0700621 install_packages_fn: context manager dir => CipdInfo, see
622 install_client_and_packages.
maruel4409e302016-07-19 14:25:51 -0700623 use_symlinks: create tree with symlinks instead of hardlinks.
maruela9cfd6f2015-09-15 11:03:15 -0700624
625 Returns:
626 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000627 """
nodir55be77b2016-05-03 09:39:57 -0700628 assert bool(command) ^ bool(isolated_hash)
629 extra_args = extra_args or []
nodirbe642ff2016-06-09 15:51:51 -0700630
nodir55be77b2016-05-03 09:39:57 -0700631 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
632 assert storage is not None, 'storage is None although outdir is specified'
633
maruela76b9ee2015-12-15 06:18:08 -0800634 if result_json:
635 # Write a json output file right away in case we get killed.
636 result = {
637 'exit_code': None,
638 'had_hard_timeout': False,
639 'internal_failure': 'Was terminated before completion',
640 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700641 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800642 }
643 tools.write_json(result_json, result, dense=True)
644
maruela9cfd6f2015-09-15 11:03:15 -0700645 # run_isolated exit code. Depends on if result_json is used or not.
646 result = map_and_run(
nodir220308c2017-02-01 19:32:53 -0800647 command, isolated_hash, storage, isolate_cache, outputs,
648 init_named_caches, leak_temp_dir, root_dir, hard_timeout, grace_period,
649 bot_file, extra_args, install_packages_fn, use_symlinks)
maruela9cfd6f2015-09-15 11:03:15 -0700650 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700651
maruela9cfd6f2015-09-15 11:03:15 -0700652 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700653 # We've found tests to delete 'work' when quitting, causing an exception
654 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700655 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700656 tools.write_json(result_json, result, dense=True)
657 # Only return 1 if there was an internal error.
658 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000659
maruela9cfd6f2015-09-15 11:03:15 -0700660 # Marshall into old-style inline output.
661 if result['outputs_ref']:
662 data = {
663 'hash': result['outputs_ref']['isolated'],
664 'namespace': result['outputs_ref']['namespace'],
665 'storage': result['outputs_ref']['isolatedserver'],
666 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500667 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700668 print(
669 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
670 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800671 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700672 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000673
674
iannuccib58d10d2017-03-18 02:00:25 -0700675# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800676CipdInfo = collections.namedtuple('CipdInfo', [
677 'client', # cipd.CipdClient object
678 'cache_dir', # absolute path to bot-global cipd tag and instance cache
679 'stats', # dict with stats to return to the server
680 'pins', # dict with installed cipd pins to return to the server
681])
682
683
684@contextlib.contextmanager
685def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700686 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800687 yield None
688
689
iannuccib58d10d2017-03-18 02:00:25 -0700690def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
691 """Calls 'cipd ensure' for packages.
692
693 Args:
694 run_dir (str): root of installation.
695 cipd_cache_dir (str): the directory to use for the cipd package cache.
696 client (CipdClient): the cipd client to use
697 packages: packages to install, list [(path, package_name, version), ...].
698 timeout: max duration in seconds that this function can take.
699
700 Returns: list of pinned packages. Looks like [
701 {
702 'path': 'subdirectory',
703 'package_name': 'resolved/package/name',
704 'version': 'deadbeef...',
705 },
706 ...
707 ]
708 """
709 package_pins = [None]*len(packages)
710 def insert_pin(path, name, version, idx):
711 package_pins[idx] = {
712 'package_name': name,
713 # swarming deals with 'root' as '.'
714 'path': path or '.',
715 'version': version,
716 }
717
718 by_path = collections.defaultdict(list)
719 for i, (path, name, version) in enumerate(packages):
720 # cipd deals with 'root' as ''
721 if path == '.':
722 path = ''
723 by_path[path].append((name, version, i))
724
725 pins = client.ensure(
726 run_dir,
727 {
728 subdir: [(name, vers) for name, vers, _ in pkgs]
729 for subdir, pkgs in by_path.iteritems()
730 },
731 cache_dir=cipd_cache_dir,
732 timeout=timeout,
733 )
734
735 for subdir, pin_list in sorted(pins.iteritems()):
736 this_subdir = by_path[subdir]
737 for i, (name, version) in enumerate(pin_list):
738 insert_pin(subdir, name, version, this_subdir[i][2])
739
740 assert None not in package_pins
741
742 return package_pins
743
744
vadimsh232f5a82017-01-20 19:23:44 -0800745@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700746def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700747 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800748 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800749 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700750
vadimsh232f5a82017-01-20 19:23:44 -0800751 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
752
753 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700754 [
755 {
756 "path": path, "package_name": package_name, "version": version,
757 },
758 ...
759 ]
vadimsh902948e2017-01-20 15:57:32 -0800760 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700761
762 such that they correspond 1:1 to all input package arguments from the command
763 line. These dictionaries make their all the way back to swarming, where they
764 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700765
vadimsh902948e2017-01-20 15:57:32 -0800766 If 'packages' list is empty, will bootstrap CIPD client, but won't install
767 any packages.
768
769 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800770 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800771
nodirbe642ff2016-06-09 15:51:51 -0700772 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700773 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800774 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700775 service_url (str): CIPD server url, e.g.
776 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700777 client_package_name (str): CIPD package name of CIPD client.
778 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700779 cache_dir (str): where to keep cache of cipd clients, packages and tags.
780 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700781 """
782 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700783
nodirbe642ff2016-06-09 15:51:51 -0700784 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700785 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700786
vadimsh902948e2017-01-20 15:57:32 -0800787 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800788 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700789 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800790 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700791
nodirbe642ff2016-06-09 15:51:51 -0700792 get_client_start = time.time()
793 client_manager = cipd.get_client(
794 service_url, client_package_name, client_version, cache_dir,
795 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700796
nodirbe642ff2016-06-09 15:51:51 -0700797 with client_manager as client:
798 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700799
iannuccib58d10d2017-03-18 02:00:25 -0700800 package_pins = []
801 if packages:
802 package_pins = _install_packages(
803 run_dir, cipd_cache_dir, client, packages, timeoutfn())
804
805 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700806
vadimsh232f5a82017-01-20 19:23:44 -0800807 total_duration = time.time() - start
808 logging.info(
809 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700810
vadimsh232f5a82017-01-20 19:23:44 -0800811 yield CipdInfo(
812 client=client,
813 cache_dir=cipd_cache_dir,
814 stats={
815 'duration': total_duration,
816 'get_client_duration': get_client_duration,
817 },
818 pins={
iannuccib58d10d2017-03-18 02:00:25 -0700819 'client_package': {
820 'package_name': client.package_name,
821 'version': client.instance_id,
822 },
vadimsh232f5a82017-01-20 19:23:44 -0800823 'packages': package_pins,
824 })
nodirbe642ff2016-06-09 15:51:51 -0700825
826
nodirf33b8d62016-10-26 22:34:58 -0700827def clean_caches(options, isolate_cache, named_cache_manager):
828 """Trims isolated and named caches."""
829 # Which cache to trim first? Which of caches was used least recently?
830 with named_cache_manager.open():
831 oldest_isolated = isolate_cache.get_oldest()
832 oldest_named = named_cache_manager.get_oldest()
833 trimmers = [
834 (
835 isolate_cache.trim,
836 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
837 ),
838 (
839 lambda: named_cache_manager.trim(options.min_free_space),
840 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
841 ),
842 ]
843 trimmers.sort(key=lambda (_, ts): ts)
844 for trim, _ in trimmers:
845 trim()
846 isolate_cache.cleanup()
847
848
nodirbe642ff2016-06-09 15:51:51 -0700849def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400850 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700851 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000852 version=__version__,
853 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700854 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700855 '--clean', action='store_true',
856 help='Cleans the cache, trimming it necessary and remove corrupted items '
857 'and returns without executing anything; use with -v to know what '
858 'was done')
859 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -0700860 '--no-clean', action='store_true',
861 help='Do not clean the cache automatically on startup. This is meant for '
862 'bots where a separate execution with --clean was done earlier so '
863 'doing it again is redundant')
864 parser.add_option(
maruel4409e302016-07-19 14:25:51 -0700865 '--use-symlinks', action='store_true',
866 help='Use symlinks instead of hardlinks')
867 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700868 '--json',
869 help='dump output metadata to json file. When used, run_isolated returns '
870 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700871 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800872 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700873 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800874 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700875 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700876 parser.add_option(
877 '--bot-file',
878 help='Path to a file describing the state of the host. The content is '
879 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -0700880 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -0700881 '--output', action='append',
882 help='Specifies an output to return. If no outputs are specified, all '
883 'files located in $(ISOLATED_OUTDIR) will be returned; '
884 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
885 'specified by --output option (there can be multiple) will be '
886 'returned. Note that if a file in OUT_DIR has the same path '
887 'as an --output option, the --output version will be returned.')
888 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -0700889 '-a', '--argsfile',
890 # This is actually handled in parse_args; it's included here purely so it
891 # can make it into the help text.
892 help='Specify a file containing a JSON array of arguments to this '
893 'script. If --argsfile is provided, no other argument may be '
894 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500895 data_group = optparse.OptionGroup(parser, 'Data source')
896 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500897 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700898 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500899 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500900 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000901
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400902 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700903
904 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -0700905 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000906
Kenneth Russell61d42352014-09-15 11:41:16 -0700907 debug_group = optparse.OptionGroup(parser, 'Debugging')
908 debug_group.add_option(
909 '--leak-temp-dir',
910 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700911 help='Deliberately leak isolate\'s temp dir for later examination. '
912 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700913 debug_group.add_option(
914 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700915 parser.add_option_group(debug_group)
916
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800917 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700918
nodirf33b8d62016-10-26 22:34:58 -0700919 parser.set_defaults(
920 cache='cache',
921 cipd_cache='cipd_cache',
922 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -0700923 return parser
924
925
aludwin7556e0c2016-10-26 08:46:10 -0700926def parse_args(args):
927 # Create a fake mini-parser just to get out the "-a" command. Note that
928 # it's not documented here; instead, it's documented in create_option_parser
929 # even though that parser will never actually get to parse it. This is
930 # because --argsfile is exclusive with all other options and arguments.
931 file_argparse = argparse.ArgumentParser(add_help=False)
932 file_argparse.add_argument('-a', '--argsfile')
933 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
934 if file_args.argsfile:
935 if nonfile_args:
936 file_argparse.error('Can\'t specify --argsfile with'
937 'any other arguments (%s)' % nonfile_args)
938 try:
939 with open(file_args.argsfile, 'r') as f:
940 args = json.load(f)
941 except (IOError, OSError, ValueError) as e:
942 # We don't need to error out here - "args" is now empty,
943 # so the call below to parser.parse_args(args) will fail
944 # and print the full help text.
945 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
946
947 # Even if we failed to read the args, just call the normal parser now since it
948 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -0700949 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500950 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -0700951 return (parser, options, args)
952
953
954def main(args):
955 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700956
nodirf33b8d62016-10-26 22:34:58 -0700957 isolate_cache = isolateserver.process_cache_options(options, trim=False)
958 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -0700959 if options.clean:
960 if options.isolated:
961 parser.error('Can\'t use --isolated with --clean.')
962 if options.isolate_server:
963 parser.error('Can\'t use --isolate-server with --clean.')
964 if options.json:
965 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -0700966 if options.named_caches:
967 parser.error('Can\t use --named-cache with --clean.')
968 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -0700969 return 0
nodirf33b8d62016-10-26 22:34:58 -0700970
maruel2e8d0f52016-07-16 07:51:29 -0700971 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -0700972 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -0700973
nodir55be77b2016-05-03 09:39:57 -0700974 if not options.isolated and not args:
975 parser.error('--isolated or command to run is required.')
976
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800977 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700978
979 isolateserver.process_isolate_server_options(
980 parser, options, True, False)
981 if not options.isolate_server:
982 if options.isolated:
983 parser.error('--isolated requires --isolate-server')
984 if ISOLATED_OUTDIR_PARAMETER in args:
985 parser.error(
986 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000987
nodir90bc8dc2016-06-15 13:35:21 -0700988 if options.root_dir:
989 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -0700990 if options.json:
991 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700992
nodirbe642ff2016-06-09 15:51:51 -0700993 cipd.validate_cipd_options(parser, options)
994
vadimsh232f5a82017-01-20 19:23:44 -0800995 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -0800996 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -0700997 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -0800998 run_dir, cipd.parse_package_args(options.cipd_packages),
999 options.cipd_server, options.cipd_client_package,
1000 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001001
nodird6160682017-02-02 13:03:35 -08001002 @contextlib.contextmanager
nodirf33b8d62016-10-26 22:34:58 -07001003 def init_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001004 # WARNING: this function depends on "options" variable defined in the outer
1005 # function.
nodirf33b8d62016-10-26 22:34:58 -07001006 with named_cache_manager.open():
1007 named_cache_manager.create_symlinks(run_dir, options.named_caches)
nodird6160682017-02-02 13:03:35 -08001008 try:
1009 yield
1010 finally:
1011 if not options.leak_temp_dir:
1012 named_cache_manager.delete_symlinks(run_dir, options.named_caches)
nodirf33b8d62016-10-26 22:34:58 -07001013
nodirbe642ff2016-06-09 15:51:51 -07001014 try:
nodir90bc8dc2016-06-15 13:35:21 -07001015 command = [] if options.isolated else args
1016 if options.isolate_server:
1017 storage = isolateserver.get_storage(
1018 options.isolate_server, options.namespace)
1019 with storage:
nodirf33b8d62016-10-26 22:34:58 -07001020 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
1021 assert storage.hash_algo == isolate_cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -07001022 return run_tha_test(
nodirf33b8d62016-10-26 22:34:58 -07001023 command,
1024 options.isolated,
1025 storage,
1026 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001027 options.output,
nodirf33b8d62016-10-26 22:34:58 -07001028 init_named_caches,
1029 options.leak_temp_dir,
1030 options.json, options.root_dir,
1031 options.hard_timeout,
1032 options.grace_period,
1033 options.bot_file, args,
1034 install_packages_fn,
1035 options.use_symlinks)
maruel4409e302016-07-19 14:25:51 -07001036 return run_tha_test(
nodirf33b8d62016-10-26 22:34:58 -07001037 command,
1038 options.isolated,
1039 None,
1040 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001041 options.output,
nodirf33b8d62016-10-26 22:34:58 -07001042 init_named_caches,
1043 options.leak_temp_dir,
1044 options.json,
1045 options.root_dir,
1046 options.hard_timeout,
1047 options.grace_period,
1048 options.bot_file, args,
1049 install_packages_fn,
maruel4409e302016-07-19 14:25:51 -07001050 options.use_symlinks)
nodirf33b8d62016-10-26 22:34:58 -07001051 except (cipd.Error, named_cache.Error) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001052 print >> sys.stderr, ex.message
1053 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001054
1055
1056if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001057 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001058 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001059 fix_encoding.fix_encoding()
maruel4409e302016-07-19 14:25:51 -07001060 file_path.enable_symlink()
aludwin7556e0c2016-10-26 08:46:10 -07001061
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001062 sys.exit(main(sys.argv[1:]))