blob: b9ab90e698f00746077fbf11aaba67cd7764d9bd [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
216 key = {'darwin': 'TMPDIR', 'win32': 'TEMP'}.get(sys.platform, 'TMP')
217 env[key] = to_fs_enc(tmp_dir)
218
219 if cipd_info:
220 bin_dir = os.path.dirname(cipd_info.client.binary_path)
221 env['PATH'] = '%s%s%s' % (to_fs_enc(bin_dir), os.pathsep, env['PATH'])
222 env['CIPD_CACHE_DIR'] = to_fs_enc(cipd_info.cache_dir)
223
224 return env
225
226
227def run_command(command, cwd, env, hard_timeout, grace_period):
maruel6be7f9e2015-10-01 12:25:30 -0700228 """Runs the command.
229
230 Returns:
231 tuple(process exit code, bool if had a hard timeout)
232 """
maruela9cfd6f2015-09-15 11:03:15 -0700233 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700234
maruel6be7f9e2015-10-01 12:25:30 -0700235 exit_code = None
236 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700237 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700238 proc = None
239 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700240 try:
maruel6be7f9e2015-10-01 12:25:30 -0700241 # TODO(maruel): This code is imperfect. It doesn't handle well signals
242 # during the download phase and there's short windows were things can go
243 # wrong.
244 def handler(signum, _frame):
245 if proc and not had_signal:
246 logging.info('Received signal %d', signum)
247 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700248 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700249
250 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
251 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
252 try:
253 exit_code = proc.wait(hard_timeout or None)
254 except subprocess42.TimeoutExpired:
255 if not had_signal:
256 logging.warning('Hard timeout')
257 had_hard_timeout = True
258 logging.warning('Sending SIGTERM')
259 proc.terminate()
260
261 # Ignore signals in grace period. Forcibly give the grace period to the
262 # child process.
263 if exit_code is None:
264 ignore = lambda *_: None
265 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
266 try:
267 exit_code = proc.wait(grace_period or None)
268 except subprocess42.TimeoutExpired:
269 # Now kill for real. The user can distinguish between the
270 # following states:
271 # - signal but process exited within grace period,
272 # hard_timed_out will be set but the process exit code will be
273 # script provided.
274 # - processed exited late, exit code will be -9 on posix.
275 logging.warning('Grace exhausted; sending SIGKILL')
276 proc.kill()
277 logging.info('Waiting for proces exit')
278 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700279 except OSError:
280 # This is not considered to be an internal error. The executable simply
281 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800282 sys.stderr.write(
283 '<The executable does not exist or a dependent library is missing>\n'
284 '<Check for missing .so/.dll in the .isolate or GN file>\n'
285 '<Command: %s>\n' % command)
286 if os.environ.get('SWARMING_TASK_ID'):
287 # Give an additional hint when running as a swarming task.
288 sys.stderr.write(
289 '<See the task\'s page for commands to help diagnose this issue '
290 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700291 exit_code = 1
292 logging.info(
293 'Command finished with exit code %d (%s)',
294 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700295 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700296
297
maruel4409e302016-07-19 14:25:51 -0700298def fetch_and_map(isolated_hash, storage, cache, outdir, use_symlinks):
299 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700300 start = time.time()
301 bundle = isolateserver.fetch_isolated(
302 isolated_hash=isolated_hash,
303 storage=storage,
304 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700305 outdir=outdir,
306 use_symlinks=use_symlinks)
nodir6f801882016-04-29 14:41:50 -0700307 return bundle, {
308 'duration': time.time() - start,
309 'initial_number_items': cache.initial_number_items,
310 'initial_size': cache.initial_size,
311 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
312 'items_hot': base64.b64encode(
tansell9e04a8d2016-07-28 09:31:59 -0700313 large.pack(sorted(set(cache.used) - set(cache.added)))),
nodir6f801882016-04-29 14:41:50 -0700314 }
315
316
aludwin0a8e17d2016-10-27 15:57:39 -0700317def link_outputs_to_outdir(run_dir, out_dir, outputs):
318 """Links any named outputs to out_dir so they can be uploaded.
319
320 Raises an error if the file already exists in that directory.
321 """
322 if not outputs:
323 return
324 isolateserver.create_directories(out_dir, outputs)
325 for o in outputs:
326 try:
327 file_path.link_file(
328 os.path.join(out_dir, o),
329 os.path.join(run_dir, o),
330 file_path.HARDLINK_WITH_FALLBACK)
331 except OSError as e:
aludwin81178302016-11-30 17:18:49 -0800332 logging.info("Couldn't collect output file %s: %s", o, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700333
334
maruela9cfd6f2015-09-15 11:03:15 -0700335def delete_and_upload(storage, out_dir, leak_temp_dir):
336 """Deletes the temporary run directory and uploads results back.
337
338 Returns:
nodir6f801882016-04-29 14:41:50 -0700339 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700340 - outputs_ref: a dict referring to the results archived back to the isolated
341 server, if applicable.
342 - success: False if something occurred that means that the task must
343 forcibly be considered a failure, e.g. zombie processes were left
344 behind.
nodir6f801882016-04-29 14:41:50 -0700345 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700346 """
maruela9cfd6f2015-09-15 11:03:15 -0700347 # Upload out_dir and generate a .isolated file out of this directory. It is
348 # only done if files were written in the directory.
349 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700350 cold = []
351 hot = []
nodir6f801882016-04-29 14:41:50 -0700352 start = time.time()
353
maruel12e30012015-10-09 11:55:35 -0700354 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700355 with tools.Profiler('ArchiveOutput'):
356 try:
maruel064c0a32016-04-05 11:47:15 -0700357 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700358 storage, [out_dir], None)
359 outputs_ref = {
360 'isolated': results[0][0],
361 'isolatedserver': storage.location,
362 'namespace': storage.namespace,
363 }
maruel064c0a32016-04-05 11:47:15 -0700364 cold = sorted(i.size for i in f_cold)
365 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700366 except isolateserver.Aborted:
367 # This happens when a signal SIGTERM was received while uploading data.
368 # There is 2 causes:
369 # - The task was too slow and was about to be killed anyway due to
370 # exceeding the hard timeout.
371 # - The amount of data uploaded back is very large and took too much
372 # time to archive.
373 sys.stderr.write('Received SIGTERM while uploading')
374 # Re-raise, so it will be treated as an internal failure.
375 raise
nodir6f801882016-04-29 14:41:50 -0700376
377 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700378 try:
maruel12e30012015-10-09 11:55:35 -0700379 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700380 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700381 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700382 else:
383 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700384 except OSError as e:
385 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700386 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700387 stats = {
388 'duration': time.time() - start,
389 'items_cold': base64.b64encode(large.pack(cold)),
390 'items_hot': base64.b64encode(large.pack(hot)),
391 }
392 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700393
394
marueleb5fbee2015-09-17 13:01:36 -0700395def map_and_run(
aludwin0a8e17d2016-10-27 15:57:39 -0700396 command, isolated_hash, storage, isolate_cache, outputs, init_name_caches,
nodirf33b8d62016-10-26 22:34:58 -0700397 leak_temp_dir, root_dir, hard_timeout, grace_period, bot_file, extra_args,
398 install_packages_fn, use_symlinks):
nodir55be77b2016-05-03 09:39:57 -0700399 """Runs a command with optional isolated input/output.
400
401 See run_tha_test for argument documentation.
402
403 Returns metadata about the result.
404 """
nodir56efa452016-10-12 12:17:39 -0700405 assert root_dir or root_dir is None
nodir55be77b2016-05-03 09:39:57 -0700406 assert bool(command) ^ bool(isolated_hash)
maruela9cfd6f2015-09-15 11:03:15 -0700407 result = {
maruel064c0a32016-04-05 11:47:15 -0700408 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700409 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700410 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700411 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700412 'stats': {
nodir55715712016-06-03 12:28:19 -0700413 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700414 # 'cipd': {
415 # 'duration': 0.,
416 # 'get_client_duration': 0.,
417 # },
nodir55715712016-06-03 12:28:19 -0700418 # 'download': {
419 # 'duration': 0.,
420 # 'initial_number_items': 0,
421 # 'initial_size': 0,
422 # 'items_cold': '<large.pack()>',
423 # 'items_hot': '<large.pack()>',
424 # },
425 # 'upload': {
426 # 'duration': 0.,
427 # 'items_cold': '<large.pack()>',
428 # 'items_hot': '<large.pack()>',
429 # },
maruel064c0a32016-04-05 11:47:15 -0700430 # },
431 },
iannucci96fcccc2016-08-30 15:52:22 -0700432 # 'cipd_pins': {
433 # 'packages': [
434 # {'package_name': ..., 'version': ..., 'path': ...},
435 # ...
436 # ],
437 # 'client_package': {'package_name': ..., 'version': ...},
438 # },
maruela9cfd6f2015-09-15 11:03:15 -0700439 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700440 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700441 }
nodirbe642ff2016-06-09 15:51:51 -0700442
marueleb5fbee2015-09-17 13:01:36 -0700443 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700444 file_path.ensure_tree(root_dir, 0700)
nodir56efa452016-10-12 12:17:39 -0700445 elif isolate_cache.cache_dir:
446 root_dir = os.path.dirname(isolate_cache.cache_dir)
maruele2f2cb82016-07-13 14:41:03 -0700447 # See comment for these constants.
448 run_dir = make_temp_dir(ISOLATED_RUN_DIR, root_dir)
maruel03e11842016-07-14 10:50:16 -0700449 # storage should be normally set but don't crash if it is not. This can happen
450 # as Swarming task can run without an isolate server.
maruele2f2cb82016-07-13 14:41:03 -0700451 out_dir = make_temp_dir(ISOLATED_OUT_DIR, root_dir) if storage else None
452 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, root_dir)
nodir55be77b2016-05-03 09:39:57 -0700453 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700454
nodir55be77b2016-05-03 09:39:57 -0700455 try:
vadimsh232f5a82017-01-20 19:23:44 -0800456 with install_packages_fn(run_dir) as cipd_info:
457 if cipd_info:
458 result['stats']['cipd'] = cipd_info.stats
459 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700460
vadimsh232f5a82017-01-20 19:23:44 -0800461 if isolated_hash:
462 isolated_stats = result['stats'].setdefault('isolated', {})
463 bundle, isolated_stats['download'] = fetch_and_map(
464 isolated_hash=isolated_hash,
465 storage=storage,
466 cache=isolate_cache,
467 outdir=run_dir,
468 use_symlinks=use_symlinks)
469 if not bundle.command:
470 # Handle this as a task failure, not an internal failure.
471 sys.stderr.write(
472 '<The .isolated doesn\'t declare any command to run!>\n'
473 '<Check your .isolate for missing \'command\' variable>\n')
474 if os.environ.get('SWARMING_TASK_ID'):
475 # Give an additional hint when running as a swarming task.
476 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
477 result['exit_code'] = 1
478 return result
nodir55be77b2016-05-03 09:39:57 -0700479
vadimsh232f5a82017-01-20 19:23:44 -0800480 change_tree_read_only(run_dir, bundle.read_only)
481 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
482 command = bundle.command + extra_args
nodirbe642ff2016-06-09 15:51:51 -0700483
vadimsh232f5a82017-01-20 19:23:44 -0800484 # If we have an explicit list of files to return, make sure their
485 # directories exist now.
486 if storage and outputs:
487 isolateserver.create_directories(run_dir, outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700488
vadimsh232f5a82017-01-20 19:23:44 -0800489 command = tools.fix_python_path(command)
490 command = process_command(command, out_dir, bot_file)
491 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700492
vadimsh232f5a82017-01-20 19:23:44 -0800493 init_name_caches(run_dir)
nodirf33b8d62016-10-26 22:34:58 -0700494
vadimsh232f5a82017-01-20 19:23:44 -0800495 sys.stdout.flush()
496 start = time.time()
497 try:
498 result['exit_code'], result['had_hard_timeout'] = run_command(
499 command, cwd, get_command_env(tmp_dir, cipd_info),
500 hard_timeout, grace_period)
501 finally:
502 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700503 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700504 # An internal error occurred. Report accordingly so the swarming task will
505 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700506 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700507 result['internal_failure'] = str(e)
508 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700509
510 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700511 finally:
512 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700513 # Try to link files to the output directory, if specified.
514 if out_dir:
515 link_outputs_to_outdir(run_dir, out_dir, outputs)
516
nodir32a1ec12016-10-26 18:34:07 -0700517 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700518 if leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700519 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700520 logging.warning(
521 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700522 else:
maruel84537cb2015-10-16 14:21:28 -0700523 # On Windows rmtree(run_dir) call above has a synchronization effect: it
524 # finishes only when all task child processes terminate (since a running
525 # process locks *.exe file). Examine out_dir only after that call
526 # completes (since child processes may write to out_dir too and we need
527 # to wait for them to finish).
528 if fs.isdir(run_dir):
529 try:
530 success = file_path.rmtree(run_dir)
531 except OSError as e:
532 logging.error('Failure with %s', e)
533 success = False
534 if not success:
535 print >> sys.stderr, (
536 'Failed to delete the run directory, forcibly failing\n'
537 'the task because of it. No zombie process can outlive a\n'
538 'successful task run and still be marked as successful.\n'
539 'Fix your stuff.')
540 if result['exit_code'] == 0:
541 result['exit_code'] = 1
542 if fs.isdir(tmp_dir):
543 try:
544 success = file_path.rmtree(tmp_dir)
545 except OSError as e:
546 logging.error('Failure with %s', e)
547 success = False
548 if not success:
549 print >> sys.stderr, (
550 'Failed to delete the temporary directory, forcibly failing\n'
551 'the task because of it. No zombie process can outlive a\n'
552 'successful task run and still be marked as successful.\n'
553 'Fix your stuff.')
554 if result['exit_code'] == 0:
555 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700556
marueleb5fbee2015-09-17 13:01:36 -0700557 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700558 if out_dir:
nodir55715712016-06-03 12:28:19 -0700559 isolated_stats = result['stats'].setdefault('isolated', {})
560 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700561 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700562 if not success and result['exit_code'] == 0:
563 result['exit_code'] = 1
564 except Exception as e:
565 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700566 if out_dir:
567 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700568 result['internal_failure'] = str(e)
569 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500570
571
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400572def run_tha_test(
aludwin0a8e17d2016-10-27 15:57:39 -0700573 command, isolated_hash, storage, isolate_cache, outputs, init_name_caches,
nodirf33b8d62016-10-26 22:34:58 -0700574 leak_temp_dir, result_json, root_dir, hard_timeout, grace_period, bot_file,
575 extra_args, install_packages_fn, use_symlinks):
nodir55be77b2016-05-03 09:39:57 -0700576 """Runs an executable and records execution metadata.
577
578 Either command or isolated_hash must be specified.
579
580 If isolated_hash is specified, downloads the dependencies in the cache,
581 hardlinks them into a temporary directory and runs the command specified in
582 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500583
584 A temporary directory is created to hold the output files. The content inside
585 this directory will be uploaded back to |storage| packaged as a .isolated
586 file.
587
588 Arguments:
nodir55be77b2016-05-03 09:39:57 -0700589 command: the command to run, a list of strings. Mutually exclusive with
590 isolated_hash.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500591 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500592 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700593 The command specified in the .isolated is executed.
594 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500595 storage: an isolateserver.Storage object to retrieve remote objects. This
596 object has a reference to an isolateserver.StorageApi, which does
597 the actual I/O.
nodir6b945692016-10-19 19:09:06 -0700598 isolate_cache: an isolateserver.LocalCache to keep from retrieving the
599 same objects constantly by caching the objects retrieved.
600 Can be on-disk or in-memory.
nodirf33b8d62016-10-26 22:34:58 -0700601 init_name_caches: a function (run_dir) => void that creates symlinks for
602 named caches in |run_dir|.
Kenneth Russell61d42352014-09-15 11:41:16 -0700603 leak_temp_dir: if true, the temporary directory will be deliberately leaked
604 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700605 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700606 exit code is always 0 unless an internal error occurred.
nodir90bc8dc2016-06-15 13:35:21 -0700607 root_dir: path to the directory to use to create the temporary directory. If
marueleb5fbee2015-09-17 13:01:36 -0700608 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700609 hard_timeout: kills the process if it lasts more than this amount of
610 seconds.
611 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500612 extra_args: optional arguments to add to the command stated in the .isolate
nodir55be77b2016-05-03 09:39:57 -0700613 file. Ignored if isolate_hash is empty.
vadimsh232f5a82017-01-20 19:23:44 -0800614 install_packages_fn: context manager dir => CipdInfo, see install_packages.
maruel4409e302016-07-19 14:25:51 -0700615 use_symlinks: create tree with symlinks instead of hardlinks.
maruela9cfd6f2015-09-15 11:03:15 -0700616
617 Returns:
618 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000619 """
nodir55be77b2016-05-03 09:39:57 -0700620 assert bool(command) ^ bool(isolated_hash)
621 extra_args = extra_args or []
nodirbe642ff2016-06-09 15:51:51 -0700622
nodir55be77b2016-05-03 09:39:57 -0700623 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
624 assert storage is not None, 'storage is None although outdir is specified'
625
maruela76b9ee2015-12-15 06:18:08 -0800626 if result_json:
627 # Write a json output file right away in case we get killed.
628 result = {
629 'exit_code': None,
630 'had_hard_timeout': False,
631 'internal_failure': 'Was terminated before completion',
632 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700633 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800634 }
635 tools.write_json(result_json, result, dense=True)
636
maruela9cfd6f2015-09-15 11:03:15 -0700637 # run_isolated exit code. Depends on if result_json is used or not.
638 result = map_and_run(
aludwin0a8e17d2016-10-27 15:57:39 -0700639 command, isolated_hash, storage, isolate_cache, outputs, init_name_caches,
nodirf33b8d62016-10-26 22:34:58 -0700640 leak_temp_dir, root_dir, hard_timeout, grace_period, bot_file, extra_args,
641 install_packages_fn, use_symlinks)
maruela9cfd6f2015-09-15 11:03:15 -0700642 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700643
maruela9cfd6f2015-09-15 11:03:15 -0700644 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700645 # We've found tests to delete 'work' when quitting, causing an exception
646 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700647 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700648 tools.write_json(result_json, result, dense=True)
649 # Only return 1 if there was an internal error.
650 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000651
maruela9cfd6f2015-09-15 11:03:15 -0700652 # Marshall into old-style inline output.
653 if result['outputs_ref']:
654 data = {
655 'hash': result['outputs_ref']['isolated'],
656 'namespace': result['outputs_ref']['namespace'],
657 'storage': result['outputs_ref']['isolatedserver'],
658 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500659 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700660 print(
661 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
662 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800663 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700664 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000665
666
vadimsh232f5a82017-01-20 19:23:44 -0800667# Yielded by 'install_packages'.
668CipdInfo = collections.namedtuple('CipdInfo', [
669 'client', # cipd.CipdClient object
670 'cache_dir', # absolute path to bot-global cipd tag and instance cache
671 'stats', # dict with stats to return to the server
672 'pins', # dict with installed cipd pins to return to the server
673])
674
675
676@contextlib.contextmanager
677def noop_install_packages(_run_dir):
678 """Placeholder for 'install_packages' if cipd is disabled."""
679 yield None
680
681
682@contextlib.contextmanager
nodir90bc8dc2016-06-15 13:35:21 -0700683def install_packages(
nodirff531b42016-06-23 13:05:06 -0700684 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800685 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800686 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700687
vadimsh232f5a82017-01-20 19:23:44 -0800688 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
689
690 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700691 [
692 {
693 "path": path, "package_name": package_name, "version": version,
694 },
695 ...
696 ]
vadimsh902948e2017-01-20 15:57:32 -0800697 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700698
699 such that they correspond 1:1 to all input package arguments from the command
700 line. These dictionaries make their all the way back to swarming, where they
701 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700702
vadimsh902948e2017-01-20 15:57:32 -0800703 If 'packages' list is empty, will bootstrap CIPD client, but won't install
704 any packages.
705
706 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800707 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800708
nodirbe642ff2016-06-09 15:51:51 -0700709 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700710 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800711 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700712 service_url (str): CIPD server url, e.g.
713 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700714 client_package_name (str): CIPD package name of CIPD client.
715 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700716 cache_dir (str): where to keep cache of cipd clients, packages and tags.
717 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700718 """
719 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700720
nodirbe642ff2016-06-09 15:51:51 -0700721 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700722 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700723
vadimsh902948e2017-01-20 15:57:32 -0800724 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800725 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700726 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800727 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700728
iannucci96fcccc2016-08-30 15:52:22 -0700729 package_pins = [None]*len(packages)
730 def insert_pin(path, name, version, idx):
731 path = path.replace(os.path.sep, '/')
732 package_pins[idx] = {
733 'package_name': name,
734 'path': path,
735 'version': version,
736 }
737
nodirbe642ff2016-06-09 15:51:51 -0700738 get_client_start = time.time()
739 client_manager = cipd.get_client(
740 service_url, client_package_name, client_version, cache_dir,
741 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700742
743 by_path = collections.defaultdict(list)
744 for i, (path, name, version) in enumerate(packages):
745 path = path.replace('/', os.path.sep)
746 by_path[path].append((name, version, i))
747
nodirbe642ff2016-06-09 15:51:51 -0700748 with client_manager as client:
iannucci96fcccc2016-08-30 15:52:22 -0700749 client_package = {
750 'package_name': client.package_name,
751 'version': client.instance_id,
752 }
nodirbe642ff2016-06-09 15:51:51 -0700753 get_client_duration = time.time() - get_client_start
iannucci96fcccc2016-08-30 15:52:22 -0700754 for path, pkgs in sorted(by_path.iteritems()):
nodir90bc8dc2016-06-15 13:35:21 -0700755 site_root = os.path.abspath(os.path.join(run_dir, path))
756 if not site_root.startswith(run_dir):
757 raise cipd.Error('Invalid CIPD package path "%s"' % path)
758
759 # Do not clean site_root before installation because it may contain other
760 # site roots.
761 file_path.ensure_tree(site_root, 0770)
iannucci96fcccc2016-08-30 15:52:22 -0700762 pins = client.ensure(
763 site_root, [(name, vers) for name, vers, _ in pkgs],
vadimsh232f5a82017-01-20 19:23:44 -0800764 cache_dir=cipd_cache_dir,
nodirbe642ff2016-06-09 15:51:51 -0700765 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700766 for i, pin in enumerate(pins):
767 insert_pin(path, pin[0], pin[1], pkgs[i][2])
nodirbe642ff2016-06-09 15:51:51 -0700768 file_path.make_tree_files_read_only(site_root)
nodir90bc8dc2016-06-15 13:35:21 -0700769
vadimsh232f5a82017-01-20 19:23:44 -0800770 total_duration = time.time() - start
771 logging.info(
772 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700773
vadimsh232f5a82017-01-20 19:23:44 -0800774 assert None not in package_pins
iannucci96fcccc2016-08-30 15:52:22 -0700775
vadimsh232f5a82017-01-20 19:23:44 -0800776 yield CipdInfo(
777 client=client,
778 cache_dir=cipd_cache_dir,
779 stats={
780 'duration': total_duration,
781 'get_client_duration': get_client_duration,
782 },
783 pins={
784 'client_package': client_package,
785 'packages': package_pins,
786 })
nodirbe642ff2016-06-09 15:51:51 -0700787
788
nodirf33b8d62016-10-26 22:34:58 -0700789def clean_caches(options, isolate_cache, named_cache_manager):
790 """Trims isolated and named caches."""
791 # Which cache to trim first? Which of caches was used least recently?
792 with named_cache_manager.open():
793 oldest_isolated = isolate_cache.get_oldest()
794 oldest_named = named_cache_manager.get_oldest()
795 trimmers = [
796 (
797 isolate_cache.trim,
798 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
799 ),
800 (
801 lambda: named_cache_manager.trim(options.min_free_space),
802 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
803 ),
804 ]
805 trimmers.sort(key=lambda (_, ts): ts)
806 for trim, _ in trimmers:
807 trim()
808 isolate_cache.cleanup()
809
810
nodirbe642ff2016-06-09 15:51:51 -0700811def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400812 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700813 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000814 version=__version__,
815 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700816 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700817 '--clean', action='store_true',
818 help='Cleans the cache, trimming it necessary and remove corrupted items '
819 'and returns without executing anything; use with -v to know what '
820 'was done')
821 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -0700822 '--no-clean', action='store_true',
823 help='Do not clean the cache automatically on startup. This is meant for '
824 'bots where a separate execution with --clean was done earlier so '
825 'doing it again is redundant')
826 parser.add_option(
maruel4409e302016-07-19 14:25:51 -0700827 '--use-symlinks', action='store_true',
828 help='Use symlinks instead of hardlinks')
829 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700830 '--json',
831 help='dump output metadata to json file. When used, run_isolated returns '
832 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700833 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800834 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700835 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800836 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700837 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700838 parser.add_option(
839 '--bot-file',
840 help='Path to a file describing the state of the host. The content is '
841 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -0700842 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -0700843 '--output', action='append',
844 help='Specifies an output to return. If no outputs are specified, all '
845 'files located in $(ISOLATED_OUTDIR) will be returned; '
846 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
847 'specified by --output option (there can be multiple) will be '
848 'returned. Note that if a file in OUT_DIR has the same path '
849 'as an --output option, the --output version will be returned.')
850 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -0700851 '-a', '--argsfile',
852 # This is actually handled in parse_args; it's included here purely so it
853 # can make it into the help text.
854 help='Specify a file containing a JSON array of arguments to this '
855 'script. If --argsfile is provided, no other argument may be '
856 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500857 data_group = optparse.OptionGroup(parser, 'Data source')
858 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500859 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700860 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500861 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500862 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000863
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400864 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700865
866 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -0700867 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000868
Kenneth Russell61d42352014-09-15 11:41:16 -0700869 debug_group = optparse.OptionGroup(parser, 'Debugging')
870 debug_group.add_option(
871 '--leak-temp-dir',
872 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700873 help='Deliberately leak isolate\'s temp dir for later examination. '
874 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700875 debug_group.add_option(
876 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700877 parser.add_option_group(debug_group)
878
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800879 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700880
nodirf33b8d62016-10-26 22:34:58 -0700881 parser.set_defaults(
882 cache='cache',
883 cipd_cache='cipd_cache',
884 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -0700885 return parser
886
887
aludwin7556e0c2016-10-26 08:46:10 -0700888def parse_args(args):
889 # Create a fake mini-parser just to get out the "-a" command. Note that
890 # it's not documented here; instead, it's documented in create_option_parser
891 # even though that parser will never actually get to parse it. This is
892 # because --argsfile is exclusive with all other options and arguments.
893 file_argparse = argparse.ArgumentParser(add_help=False)
894 file_argparse.add_argument('-a', '--argsfile')
895 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
896 if file_args.argsfile:
897 if nonfile_args:
898 file_argparse.error('Can\'t specify --argsfile with'
899 'any other arguments (%s)' % nonfile_args)
900 try:
901 with open(file_args.argsfile, 'r') as f:
902 args = json.load(f)
903 except (IOError, OSError, ValueError) as e:
904 # We don't need to error out here - "args" is now empty,
905 # so the call below to parser.parse_args(args) will fail
906 # and print the full help text.
907 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
908
909 # Even if we failed to read the args, just call the normal parser now since it
910 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -0700911 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500912 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -0700913 return (parser, options, args)
914
915
916def main(args):
917 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700918
nodirf33b8d62016-10-26 22:34:58 -0700919 isolate_cache = isolateserver.process_cache_options(options, trim=False)
920 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -0700921 if options.clean:
922 if options.isolated:
923 parser.error('Can\'t use --isolated with --clean.')
924 if options.isolate_server:
925 parser.error('Can\'t use --isolate-server with --clean.')
926 if options.json:
927 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -0700928 if options.named_caches:
929 parser.error('Can\t use --named-cache with --clean.')
930 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -0700931 return 0
nodirf33b8d62016-10-26 22:34:58 -0700932
maruel2e8d0f52016-07-16 07:51:29 -0700933 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -0700934 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -0700935
nodir55be77b2016-05-03 09:39:57 -0700936 if not options.isolated and not args:
937 parser.error('--isolated or command to run is required.')
938
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800939 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700940
941 isolateserver.process_isolate_server_options(
942 parser, options, True, False)
943 if not options.isolate_server:
944 if options.isolated:
945 parser.error('--isolated requires --isolate-server')
946 if ISOLATED_OUTDIR_PARAMETER in args:
947 parser.error(
948 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000949
nodir90bc8dc2016-06-15 13:35:21 -0700950 if options.root_dir:
951 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -0700952 if options.json:
953 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700954
nodirbe642ff2016-06-09 15:51:51 -0700955 cipd.validate_cipd_options(parser, options)
956
vadimsh232f5a82017-01-20 19:23:44 -0800957 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -0800958 if options.cipd_enabled:
959 install_packages_fn = lambda run_dir: install_packages(
960 run_dir, cipd.parse_package_args(options.cipd_packages),
961 options.cipd_server, options.cipd_client_package,
962 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -0700963
nodirf33b8d62016-10-26 22:34:58 -0700964 def init_named_caches(run_dir):
965 with named_cache_manager.open():
966 named_cache_manager.create_symlinks(run_dir, options.named_caches)
967
nodirbe642ff2016-06-09 15:51:51 -0700968 try:
nodir90bc8dc2016-06-15 13:35:21 -0700969 command = [] if options.isolated else args
970 if options.isolate_server:
971 storage = isolateserver.get_storage(
972 options.isolate_server, options.namespace)
973 with storage:
nodirf33b8d62016-10-26 22:34:58 -0700974 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
975 assert storage.hash_algo == isolate_cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -0700976 return run_tha_test(
nodirf33b8d62016-10-26 22:34:58 -0700977 command,
978 options.isolated,
979 storage,
980 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -0700981 options.output,
nodirf33b8d62016-10-26 22:34:58 -0700982 init_named_caches,
983 options.leak_temp_dir,
984 options.json, options.root_dir,
985 options.hard_timeout,
986 options.grace_period,
987 options.bot_file, args,
988 install_packages_fn,
989 options.use_symlinks)
maruel4409e302016-07-19 14:25:51 -0700990 return run_tha_test(
nodirf33b8d62016-10-26 22:34:58 -0700991 command,
992 options.isolated,
993 None,
994 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -0700995 options.output,
nodirf33b8d62016-10-26 22:34:58 -0700996 init_named_caches,
997 options.leak_temp_dir,
998 options.json,
999 options.root_dir,
1000 options.hard_timeout,
1001 options.grace_period,
1002 options.bot_file, args,
1003 install_packages_fn,
maruel4409e302016-07-19 14:25:51 -07001004 options.use_symlinks)
nodirf33b8d62016-10-26 22:34:58 -07001005 except (cipd.Error, named_cache.Error) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001006 print >> sys.stderr, ex.message
1007 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001008
1009
1010if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001011 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001012 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001013 fix_encoding.fix_encoding()
maruel4409e302016-07-19 14:25:51 -07001014 file_path.enable_symlink()
aludwin7556e0c2016-10-26 08:46:10 -07001015
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001016 sys.exit(main(sys.argv[1:]))