blob: 0297c70ff0a70c0a545a830fc1bc73ddbd6031f7 [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.
217 # * mktemp on linux respects $TEMPDIR, not $TMP
218 # * mktemp on OS X SOMETIMES respects $TEMPDIR
219 # * 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.
222 # * python respects TEMPDIR, TEMP, and TMP (regardless of platform)
223 # * golang respects TEMPDIR on linux+mac, TEMP on windows.
224 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.
vadimsh232f5a82017-01-20 19:23:44 -0800621 install_packages_fn: context manager dir => CipdInfo, see install_packages.
maruel4409e302016-07-19 14:25:51 -0700622 use_symlinks: create tree with symlinks instead of hardlinks.
maruela9cfd6f2015-09-15 11:03:15 -0700623
624 Returns:
625 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000626 """
nodir55be77b2016-05-03 09:39:57 -0700627 assert bool(command) ^ bool(isolated_hash)
628 extra_args = extra_args or []
nodirbe642ff2016-06-09 15:51:51 -0700629
nodir55be77b2016-05-03 09:39:57 -0700630 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
631 assert storage is not None, 'storage is None although outdir is specified'
632
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,
647 init_named_caches, leak_temp_dir, root_dir, hard_timeout, grace_period,
648 bot_file, extra_args, install_packages_fn, use_symlinks)
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
vadimsh232f5a82017-01-20 19:23:44 -0800674# Yielded by 'install_packages'.
675CipdInfo = 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):
685 """Placeholder for 'install_packages' if cipd is disabled."""
686 yield None
687
688
689@contextlib.contextmanager
nodir90bc8dc2016-06-15 13:35:21 -0700690def install_packages(
nodirff531b42016-06-23 13:05:06 -0700691 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800692 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800693 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700694
vadimsh232f5a82017-01-20 19:23:44 -0800695 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
696
697 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700698 [
699 {
700 "path": path, "package_name": package_name, "version": version,
701 },
702 ...
703 ]
vadimsh902948e2017-01-20 15:57:32 -0800704 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700705
706 such that they correspond 1:1 to all input package arguments from the command
707 line. These dictionaries make their all the way back to swarming, where they
708 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700709
vadimsh902948e2017-01-20 15:57:32 -0800710 If 'packages' list is empty, will bootstrap CIPD client, but won't install
711 any packages.
712
713 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800714 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800715
nodirbe642ff2016-06-09 15:51:51 -0700716 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700717 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800718 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700719 service_url (str): CIPD server url, e.g.
720 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700721 client_package_name (str): CIPD package name of CIPD client.
722 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700723 cache_dir (str): where to keep cache of cipd clients, packages and tags.
724 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700725 """
726 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700727
nodirbe642ff2016-06-09 15:51:51 -0700728 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700729 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700730
vadimsh902948e2017-01-20 15:57:32 -0800731 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800732 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700733 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800734 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700735
iannucci96fcccc2016-08-30 15:52:22 -0700736 package_pins = [None]*len(packages)
737 def insert_pin(path, name, version, idx):
738 path = path.replace(os.path.sep, '/')
739 package_pins[idx] = {
740 'package_name': name,
741 'path': path,
742 'version': version,
743 }
744
nodirbe642ff2016-06-09 15:51:51 -0700745 get_client_start = time.time()
746 client_manager = cipd.get_client(
747 service_url, client_package_name, client_version, cache_dir,
748 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700749
750 by_path = collections.defaultdict(list)
751 for i, (path, name, version) in enumerate(packages):
752 path = path.replace('/', os.path.sep)
753 by_path[path].append((name, version, i))
754
nodirbe642ff2016-06-09 15:51:51 -0700755 with client_manager as client:
iannucci96fcccc2016-08-30 15:52:22 -0700756 client_package = {
757 'package_name': client.package_name,
758 'version': client.instance_id,
759 }
nodirbe642ff2016-06-09 15:51:51 -0700760 get_client_duration = time.time() - get_client_start
iannucci96fcccc2016-08-30 15:52:22 -0700761 for path, pkgs in sorted(by_path.iteritems()):
nodir90bc8dc2016-06-15 13:35:21 -0700762 site_root = os.path.abspath(os.path.join(run_dir, path))
763 if not site_root.startswith(run_dir):
764 raise cipd.Error('Invalid CIPD package path "%s"' % path)
765
766 # Do not clean site_root before installation because it may contain other
767 # site roots.
768 file_path.ensure_tree(site_root, 0770)
iannucci96fcccc2016-08-30 15:52:22 -0700769 pins = client.ensure(
770 site_root, [(name, vers) for name, vers, _ in pkgs],
vadimsh232f5a82017-01-20 19:23:44 -0800771 cache_dir=cipd_cache_dir,
nodirbe642ff2016-06-09 15:51:51 -0700772 timeout=timeoutfn())
iannucci1c9a3692017-01-30 14:10:49 -0800773 for i, pin in enumerate(pins[""]):
iannucci96fcccc2016-08-30 15:52:22 -0700774 insert_pin(path, pin[0], pin[1], pkgs[i][2])
nodirbe642ff2016-06-09 15:51:51 -0700775 file_path.make_tree_files_read_only(site_root)
nodir90bc8dc2016-06-15 13:35:21 -0700776
vadimsh232f5a82017-01-20 19:23:44 -0800777 total_duration = time.time() - start
778 logging.info(
779 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700780
vadimsh232f5a82017-01-20 19:23:44 -0800781 assert None not in package_pins
iannucci96fcccc2016-08-30 15:52:22 -0700782
vadimsh232f5a82017-01-20 19:23:44 -0800783 yield CipdInfo(
784 client=client,
785 cache_dir=cipd_cache_dir,
786 stats={
787 'duration': total_duration,
788 'get_client_duration': get_client_duration,
789 },
790 pins={
791 'client_package': client_package,
792 'packages': package_pins,
793 })
nodirbe642ff2016-06-09 15:51:51 -0700794
795
nodirf33b8d62016-10-26 22:34:58 -0700796def clean_caches(options, isolate_cache, named_cache_manager):
797 """Trims isolated and named caches."""
798 # Which cache to trim first? Which of caches was used least recently?
799 with named_cache_manager.open():
800 oldest_isolated = isolate_cache.get_oldest()
801 oldest_named = named_cache_manager.get_oldest()
802 trimmers = [
803 (
804 isolate_cache.trim,
805 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
806 ),
807 (
808 lambda: named_cache_manager.trim(options.min_free_space),
809 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
810 ),
811 ]
812 trimmers.sort(key=lambda (_, ts): ts)
813 for trim, _ in trimmers:
814 trim()
815 isolate_cache.cleanup()
816
817
nodirbe642ff2016-06-09 15:51:51 -0700818def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400819 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700820 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000821 version=__version__,
822 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700823 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700824 '--clean', action='store_true',
825 help='Cleans the cache, trimming it necessary and remove corrupted items '
826 'and returns without executing anything; use with -v to know what '
827 'was done')
828 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -0700829 '--no-clean', action='store_true',
830 help='Do not clean the cache automatically on startup. This is meant for '
831 'bots where a separate execution with --clean was done earlier so '
832 'doing it again is redundant')
833 parser.add_option(
maruel4409e302016-07-19 14:25:51 -0700834 '--use-symlinks', action='store_true',
835 help='Use symlinks instead of hardlinks')
836 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700837 '--json',
838 help='dump output metadata to json file. When used, run_isolated returns '
839 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700840 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800841 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700842 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800843 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700844 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700845 parser.add_option(
846 '--bot-file',
847 help='Path to a file describing the state of the host. The content is '
848 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -0700849 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -0700850 '--output', action='append',
851 help='Specifies an output to return. If no outputs are specified, all '
852 'files located in $(ISOLATED_OUTDIR) will be returned; '
853 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
854 'specified by --output option (there can be multiple) will be '
855 'returned. Note that if a file in OUT_DIR has the same path '
856 'as an --output option, the --output version will be returned.')
857 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -0700858 '-a', '--argsfile',
859 # This is actually handled in parse_args; it's included here purely so it
860 # can make it into the help text.
861 help='Specify a file containing a JSON array of arguments to this '
862 'script. If --argsfile is provided, no other argument may be '
863 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500864 data_group = optparse.OptionGroup(parser, 'Data source')
865 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500866 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700867 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500868 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500869 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000870
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400871 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700872
873 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -0700874 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000875
Kenneth Russell61d42352014-09-15 11:41:16 -0700876 debug_group = optparse.OptionGroup(parser, 'Debugging')
877 debug_group.add_option(
878 '--leak-temp-dir',
879 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700880 help='Deliberately leak isolate\'s temp dir for later examination. '
881 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700882 debug_group.add_option(
883 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700884 parser.add_option_group(debug_group)
885
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800886 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700887
nodirf33b8d62016-10-26 22:34:58 -0700888 parser.set_defaults(
889 cache='cache',
890 cipd_cache='cipd_cache',
891 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -0700892 return parser
893
894
aludwin7556e0c2016-10-26 08:46:10 -0700895def parse_args(args):
896 # Create a fake mini-parser just to get out the "-a" command. Note that
897 # it's not documented here; instead, it's documented in create_option_parser
898 # even though that parser will never actually get to parse it. This is
899 # because --argsfile is exclusive with all other options and arguments.
900 file_argparse = argparse.ArgumentParser(add_help=False)
901 file_argparse.add_argument('-a', '--argsfile')
902 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
903 if file_args.argsfile:
904 if nonfile_args:
905 file_argparse.error('Can\'t specify --argsfile with'
906 'any other arguments (%s)' % nonfile_args)
907 try:
908 with open(file_args.argsfile, 'r') as f:
909 args = json.load(f)
910 except (IOError, OSError, ValueError) as e:
911 # We don't need to error out here - "args" is now empty,
912 # so the call below to parser.parse_args(args) will fail
913 # and print the full help text.
914 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
915
916 # Even if we failed to read the args, just call the normal parser now since it
917 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -0700918 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500919 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -0700920 return (parser, options, args)
921
922
923def main(args):
924 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700925
nodirf33b8d62016-10-26 22:34:58 -0700926 isolate_cache = isolateserver.process_cache_options(options, trim=False)
927 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -0700928 if options.clean:
929 if options.isolated:
930 parser.error('Can\'t use --isolated with --clean.')
931 if options.isolate_server:
932 parser.error('Can\'t use --isolate-server with --clean.')
933 if options.json:
934 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -0700935 if options.named_caches:
936 parser.error('Can\t use --named-cache with --clean.')
937 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -0700938 return 0
nodirf33b8d62016-10-26 22:34:58 -0700939
maruel2e8d0f52016-07-16 07:51:29 -0700940 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -0700941 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -0700942
nodir55be77b2016-05-03 09:39:57 -0700943 if not options.isolated and not args:
944 parser.error('--isolated or command to run is required.')
945
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800946 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700947
948 isolateserver.process_isolate_server_options(
949 parser, options, True, False)
950 if not options.isolate_server:
951 if options.isolated:
952 parser.error('--isolated requires --isolate-server')
953 if ISOLATED_OUTDIR_PARAMETER in args:
954 parser.error(
955 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000956
nodir90bc8dc2016-06-15 13:35:21 -0700957 if options.root_dir:
958 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -0700959 if options.json:
960 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700961
nodirbe642ff2016-06-09 15:51:51 -0700962 cipd.validate_cipd_options(parser, options)
963
vadimsh232f5a82017-01-20 19:23:44 -0800964 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -0800965 if options.cipd_enabled:
966 install_packages_fn = lambda run_dir: install_packages(
967 run_dir, cipd.parse_package_args(options.cipd_packages),
968 options.cipd_server, options.cipd_client_package,
969 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -0700970
nodird6160682017-02-02 13:03:35 -0800971 @contextlib.contextmanager
nodirf33b8d62016-10-26 22:34:58 -0700972 def init_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -0800973 # WARNING: this function depends on "options" variable defined in the outer
974 # function.
nodirf33b8d62016-10-26 22:34:58 -0700975 with named_cache_manager.open():
976 named_cache_manager.create_symlinks(run_dir, options.named_caches)
nodird6160682017-02-02 13:03:35 -0800977 try:
978 yield
979 finally:
980 if not options.leak_temp_dir:
981 named_cache_manager.delete_symlinks(run_dir, options.named_caches)
nodirf33b8d62016-10-26 22:34:58 -0700982
nodirbe642ff2016-06-09 15:51:51 -0700983 try:
nodir90bc8dc2016-06-15 13:35:21 -0700984 command = [] if options.isolated else args
985 if options.isolate_server:
986 storage = isolateserver.get_storage(
987 options.isolate_server, options.namespace)
988 with storage:
nodirf33b8d62016-10-26 22:34:58 -0700989 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
990 assert storage.hash_algo == isolate_cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -0700991 return run_tha_test(
nodirf33b8d62016-10-26 22:34:58 -0700992 command,
993 options.isolated,
994 storage,
995 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -0700996 options.output,
nodirf33b8d62016-10-26 22:34:58 -0700997 init_named_caches,
998 options.leak_temp_dir,
999 options.json, options.root_dir,
1000 options.hard_timeout,
1001 options.grace_period,
1002 options.bot_file, args,
1003 install_packages_fn,
1004 options.use_symlinks)
maruel4409e302016-07-19 14:25:51 -07001005 return run_tha_test(
nodirf33b8d62016-10-26 22:34:58 -07001006 command,
1007 options.isolated,
1008 None,
1009 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001010 options.output,
nodirf33b8d62016-10-26 22:34:58 -07001011 init_named_caches,
1012 options.leak_temp_dir,
1013 options.json,
1014 options.root_dir,
1015 options.hard_timeout,
1016 options.grace_period,
1017 options.bot_file, args,
1018 install_packages_fn,
maruel4409e302016-07-19 14:25:51 -07001019 options.use_symlinks)
nodirf33b8d62016-10-26 22:34:58 -07001020 except (cipd.Error, named_cache.Error) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001021 print >> sys.stderr, ex.message
1022 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001023
1024
1025if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001026 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001027 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001028 fix_encoding.fix_encoding()
maruel4409e302016-07-19 14:25:51 -07001029 file_path.enable_symlink()
aludwin7556e0c2016-10-26 08:46:10 -07001030
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001031 sys.exit(main(sys.argv[1:]))