blob: 39fb039144d8d9e8fddaa409437225d4a858bc0b [file] [log] [blame]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2012 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00005
nodir55be77b2016-05-03 09:39:57 -07006"""Runs a command with optional isolated input/output.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00007
nodir55be77b2016-05-03 09:39:57 -07008Despite name "run_isolated", can run a generic non-isolated command specified as
9args.
10
11If input isolated hash is provided, fetches it, creates a tree of hard links,
12appends args to the command in the fetched isolated and runs it.
13To improve performance, keeps a local cache.
14The local cache can safely be deleted.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -050015
nodirbe642ff2016-06-09 15:51:51 -070016Any ${EXECUTABLE_SUFFIX} on the command line will be replaced with ".exe" string
17on Windows and "" on other platforms.
18
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -050019Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
20temporary directory upon execution of the command specified in the .isolated
21file. All content written to this directory will be uploaded upon termination
22and the .isolated file describing this directory will be printed to stdout.
bpastene447c1992016-06-20 15:21:47 -070023
24Any ${SWARMING_BOT_FILE} on the command line will be replaced by the value of
25the --bot-file parameter. This file is used by a swarming bot to communicate
26state of the host to tasks. It is written to by the swarming bot's
27on_before_task() hook in the swarming server's custom bot_config.py.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000028"""
29
maruelcffa0542017-04-07 08:39:20 -070030__version__ = '0.9.1'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000031
aludwin7556e0c2016-10-26 08:46:10 -070032import argparse
maruel064c0a32016-04-05 11:47:15 -070033import base64
iannucci96fcccc2016-08-30 15:52:22 -070034import collections
vadimsh232f5a82017-01-20 19:23:44 -080035import contextlib
aludwin7556e0c2016-10-26 08:46:10 -070036import json
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000037import logging
38import optparse
39import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000040import sys
41import tempfile
maruel064c0a32016-04-05 11:47:15 -070042import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000043
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000044from third_party.depot_tools import fix_encoding
45
Vadim Shtayura6b555c12014-07-23 16:22:18 -070046from utils import file_path
maruel12e30012015-10-09 11:55:35 -070047from utils import fs
maruel064c0a32016-04-05 11:47:15 -070048from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040049from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040050from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050051from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000052from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000053from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000054
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080055import auth
nodirbe642ff2016-06-09 15:51:51 -070056import cipd
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000057import isolateserver
nodirf33b8d62016-10-26 22:34:58 -070058import named_cache
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000059
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000060
vadimsh@chromium.org85071062013-08-21 23:37:45 +000061# Absolute path to this file (can be None if running from zip on Mac).
tansella4949442016-06-23 22:34:32 -070062THIS_FILE_PATH = os.path.abspath(
63 __file__.decode(sys.getfilesystemencoding())) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000064
65# Directory that contains this file (might be inside zip package).
tansella4949442016-06-23 22:34:32 -070066BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__.decode(
67 sys.getfilesystemencoding()) else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000068
69# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000070if zip_package.get_main_script_path():
71 MAIN_DIR = os.path.dirname(
72 os.path.abspath(zip_package.get_main_script_path()))
73else:
74 # This happens when 'import run_isolated' is executed at the python
75 # interactive prompt, in that case __file__ is undefined.
76 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000077
maruele2f2cb82016-07-13 14:41:03 -070078
79# Magic variables that can be found in the isolate task command line.
80ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
81EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
82SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
83
84
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000085# The name of the log file to use.
86RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
87
maruele2f2cb82016-07-13 14:41:03 -070088
csharp@chromium.orge217f302012-11-22 16:51:53 +000089# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000090RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000091
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000092
maruele2f2cb82016-07-13 14:41:03 -070093# Use short names for temporary directories. This is driven by Windows, which
94# imposes a relatively short maximum path length of 260 characters, often
95# referred to as MAX_PATH. It is relatively easy to create files with longer
96# path length. A use case is with recursive depedency treesV like npm packages.
97#
98# It is recommended to start the script with a `root_dir` as short as
99# possible.
100# - ir stands for isolated_run
101# - io stands for isolated_out
102# - it stands for isolated_tmp
103ISOLATED_RUN_DIR = u'ir'
104ISOLATED_OUT_DIR = u'io'
105ISOLATED_TMP_DIR = u'it'
106
107
marueld928c862017-06-08 08:20:04 -0700108OUTLIVING_ZOMBIE_MSG = """\
109*** Swarming tried multiple times to delete the %s directory and failed ***
110*** Hard failing the task ***
111
112Swarming detected that your testing script ran an executable, which may have
113started a child executable, and the main script returned early, leaving the
114children executables playing around unguided.
115
116You don't want to leave children processes outliving the task on the Swarming
117bot, do you? The Swarming bot doesn't.
118
119How to fix?
120- For any process that starts children processes, make sure all children
121 processes terminated properly before each parent process exits. This is
122 especially important in very deep process trees.
123 - This must be done properly both in normal successful task and in case of
124 task failure. Cleanup is very important.
125- The Swarming bot sends a SIGTERM in case of timeout.
126 - You have %s seconds to comply after the signal was sent to the process
127 before the process is forcibly killed.
128- To achieve not leaking children processes in case of signals on timeout, you
129 MUST handle signals in each executable / python script and propagate them to
130 children processes.
131 - When your test script (python or binary) receives a signal like SIGTERM or
132 CTRL_BREAK_EVENT on Windows), send it to all children processes and wait for
133 them to terminate before quitting.
134
135See
136https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Bot.md#graceful-termination-aka-the-sigterm-and-sigkill-dance
137for more information.
138
139*** May the SIGKILL force be with you ***
140"""
141
142
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000143def get_as_zip_package(executable=True):
144 """Returns ZipPackage with this module and all its dependencies.
145
146 If |executable| is True will store run_isolated.py as __main__.py so that
147 zip package is directly executable be python.
148 """
149 # Building a zip package when running from another zip package is
150 # unsupported and probably unneeded.
151 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +0000152 assert THIS_FILE_PATH
153 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000154 package = zip_package.ZipPackage(root=BASE_DIR)
155 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
aludwin81178302016-11-30 17:18:49 -0800156 package.add_python_file(os.path.join(BASE_DIR, 'isolate_storage.py'))
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400157 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000158 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800159 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
nodirbe642ff2016-06-09 15:51:51 -0700160 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
nodirf33b8d62016-10-26 22:34:58 -0700161 package.add_python_file(os.path.join(BASE_DIR, 'named_cache.py'))
tanselle4288c32016-07-28 09:45:40 -0700162 package.add_directory(os.path.join(BASE_DIR, 'libs'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000163 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
164 package.add_directory(os.path.join(BASE_DIR, 'utils'))
165 return package
166
167
maruel03e11842016-07-14 10:50:16 -0700168def make_temp_dir(prefix, root_dir):
169 """Returns a new unique temporary directory."""
170 return unicode(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000171
172
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500173def change_tree_read_only(rootdir, read_only):
174 """Changes the tree read-only bits according to the read_only specification.
175
176 The flag can be 0, 1 or 2, which will affect the possibility to modify files
177 and create or delete files.
178 """
179 if read_only == 2:
180 # Files and directories (except on Windows) are marked read only. This
181 # inhibits modifying, creating or deleting files in the test directory,
182 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400183 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500184 elif read_only == 1:
185 # Files are marked read only but not the directories. This inhibits
186 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400187 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500188 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500189 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500190 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
191 # is not yet changed to verify the hash of the content of the files it is
192 # looking at, so that if a test modifies an input file, the file must be
193 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400194 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500195 else:
196 raise ValueError(
197 'change_tree_read_only(%s, %s): Unknown flag %s' %
198 (rootdir, read_only, read_only))
199
200
nodir90bc8dc2016-06-15 13:35:21 -0700201def process_command(command, out_dir, bot_file):
nodirbe642ff2016-06-09 15:51:51 -0700202 """Replaces variables in a command line.
203
204 Raises:
205 ValueError if a parameter is requested in |command| but its value is not
206 provided.
207 """
maruela9cfd6f2015-09-15 11:03:15 -0700208 def fix(arg):
nodirbe642ff2016-06-09 15:51:51 -0700209 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
210 replace_slash = False
nodir55be77b2016-05-03 09:39:57 -0700211 if ISOLATED_OUTDIR_PARAMETER in arg:
nodirbe642ff2016-06-09 15:51:51 -0700212 if not out_dir:
maruel7f63a272016-07-12 12:40:36 -0700213 raise ValueError(
214 'output directory is requested in command, but not provided; '
215 'please specify one')
nodir55be77b2016-05-03 09:39:57 -0700216 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
nodirbe642ff2016-06-09 15:51:51 -0700217 replace_slash = True
nodir90bc8dc2016-06-15 13:35:21 -0700218 if SWARMING_BOT_FILE_PARAMETER in arg:
219 if bot_file:
220 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
221 replace_slash = True
222 else:
223 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command, but no '
224 'bot_file specified. Leaving parameter unchanged.')
nodirbe642ff2016-06-09 15:51:51 -0700225 if replace_slash:
226 # Replace slashes only if parameters are present
nodir55be77b2016-05-03 09:39:57 -0700227 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
228 arg = arg.replace('/', os.sep)
maruela9cfd6f2015-09-15 11:03:15 -0700229 return arg
230
231 return [fix(arg) for arg in command]
232
233
vadimsh232f5a82017-01-20 19:23:44 -0800234def get_command_env(tmp_dir, cipd_info):
235 """Returns full OS environment to run a command in.
236
237 Sets up TEMP, puts directory with cipd binary in front of PATH, and exposes
238 CIPD_CACHE_DIR env var.
239
240 Args:
241 tmp_dir: temp directory.
242 cipd_info: CipdInfo object is cipd client is used, None if not.
243 """
244 def to_fs_enc(s):
245 if isinstance(s, str):
246 return s
247 return s.encode(sys.getfilesystemencoding())
248
249 env = os.environ.copy()
250
iannucciac0342c2017-02-24 05:47:01 -0800251 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
iannucci460def72017-02-24 10:49:48 -0800252 # * mktemp on linux respects $TMPDIR, not $TMP
253 # * mktemp on OS X SOMETIMES respects $TMPDIR
iannucciac0342c2017-02-24 05:47:01 -0800254 # * chromium's base utils respects $TMPDIR on linux, $TEMP on windows.
255 # Unfortunately at the time of writing it completely ignores all envvars
256 # on OS X.
iannucci460def72017-02-24 10:49:48 -0800257 # * python respects TMPDIR, TEMP, and TMP (regardless of platform)
258 # * golang respects TMPDIR on linux+mac, TEMP on windows.
iannucciac0342c2017-02-24 05:47:01 -0800259 key = {'win32': 'TEMP'}.get(sys.platform, 'TMPDIR')
vadimsh232f5a82017-01-20 19:23:44 -0800260 env[key] = to_fs_enc(tmp_dir)
261
262 if cipd_info:
263 bin_dir = os.path.dirname(cipd_info.client.binary_path)
264 env['PATH'] = '%s%s%s' % (to_fs_enc(bin_dir), os.pathsep, env['PATH'])
265 env['CIPD_CACHE_DIR'] = to_fs_enc(cipd_info.cache_dir)
266
267 return env
268
269
270def run_command(command, cwd, env, hard_timeout, grace_period):
maruel6be7f9e2015-10-01 12:25:30 -0700271 """Runs the command.
272
273 Returns:
274 tuple(process exit code, bool if had a hard timeout)
275 """
maruela9cfd6f2015-09-15 11:03:15 -0700276 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700277
maruel6be7f9e2015-10-01 12:25:30 -0700278 exit_code = None
279 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700280 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700281 proc = None
282 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700283 try:
maruel6be7f9e2015-10-01 12:25:30 -0700284 # TODO(maruel): This code is imperfect. It doesn't handle well signals
285 # during the download phase and there's short windows were things can go
286 # wrong.
287 def handler(signum, _frame):
288 if proc and not had_signal:
289 logging.info('Received signal %d', signum)
290 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700291 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700292
293 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
294 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
295 try:
296 exit_code = proc.wait(hard_timeout or None)
297 except subprocess42.TimeoutExpired:
298 if not had_signal:
299 logging.warning('Hard timeout')
300 had_hard_timeout = True
301 logging.warning('Sending SIGTERM')
302 proc.terminate()
303
304 # Ignore signals in grace period. Forcibly give the grace period to the
305 # child process.
306 if exit_code is None:
307 ignore = lambda *_: None
308 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
309 try:
310 exit_code = proc.wait(grace_period or None)
311 except subprocess42.TimeoutExpired:
312 # Now kill for real. The user can distinguish between the
313 # following states:
314 # - signal but process exited within grace period,
315 # hard_timed_out will be set but the process exit code will be
316 # script provided.
317 # - processed exited late, exit code will be -9 on posix.
318 logging.warning('Grace exhausted; sending SIGKILL')
319 proc.kill()
320 logging.info('Waiting for proces exit')
321 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700322 except OSError:
323 # This is not considered to be an internal error. The executable simply
324 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800325 sys.stderr.write(
326 '<The executable does not exist or a dependent library is missing>\n'
327 '<Check for missing .so/.dll in the .isolate or GN file>\n'
328 '<Command: %s>\n' % command)
329 if os.environ.get('SWARMING_TASK_ID'):
330 # Give an additional hint when running as a swarming task.
331 sys.stderr.write(
332 '<See the task\'s page for commands to help diagnose this issue '
333 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700334 exit_code = 1
335 logging.info(
336 'Command finished with exit code %d (%s)',
337 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700338 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700339
340
maruel4409e302016-07-19 14:25:51 -0700341def fetch_and_map(isolated_hash, storage, cache, outdir, use_symlinks):
342 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700343 start = time.time()
344 bundle = isolateserver.fetch_isolated(
345 isolated_hash=isolated_hash,
346 storage=storage,
347 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700348 outdir=outdir,
349 use_symlinks=use_symlinks)
nodir6f801882016-04-29 14:41:50 -0700350 return bundle, {
351 'duration': time.time() - start,
352 'initial_number_items': cache.initial_number_items,
353 'initial_size': cache.initial_size,
354 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
355 'items_hot': base64.b64encode(
tansell9e04a8d2016-07-28 09:31:59 -0700356 large.pack(sorted(set(cache.used) - set(cache.added)))),
nodir6f801882016-04-29 14:41:50 -0700357 }
358
359
aludwin0a8e17d2016-10-27 15:57:39 -0700360def link_outputs_to_outdir(run_dir, out_dir, outputs):
361 """Links any named outputs to out_dir so they can be uploaded.
362
363 Raises an error if the file already exists in that directory.
364 """
365 if not outputs:
366 return
367 isolateserver.create_directories(out_dir, outputs)
368 for o in outputs:
369 try:
370 file_path.link_file(
371 os.path.join(out_dir, o),
372 os.path.join(run_dir, o),
373 file_path.HARDLINK_WITH_FALLBACK)
374 except OSError as e:
aludwin81178302016-11-30 17:18:49 -0800375 logging.info("Couldn't collect output file %s: %s", o, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700376
377
maruela9cfd6f2015-09-15 11:03:15 -0700378def delete_and_upload(storage, out_dir, leak_temp_dir):
379 """Deletes the temporary run directory and uploads results back.
380
381 Returns:
nodir6f801882016-04-29 14:41:50 -0700382 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700383 - outputs_ref: a dict referring to the results archived back to the isolated
384 server, if applicable.
385 - success: False if something occurred that means that the task must
386 forcibly be considered a failure, e.g. zombie processes were left
387 behind.
nodir6f801882016-04-29 14:41:50 -0700388 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700389 """
maruela9cfd6f2015-09-15 11:03:15 -0700390 # Upload out_dir and generate a .isolated file out of this directory. It is
391 # only done if files were written in the directory.
392 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700393 cold = []
394 hot = []
nodir6f801882016-04-29 14:41:50 -0700395 start = time.time()
396
maruel12e30012015-10-09 11:55:35 -0700397 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700398 with tools.Profiler('ArchiveOutput'):
399 try:
maruel064c0a32016-04-05 11:47:15 -0700400 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700401 storage, [out_dir], None)
402 outputs_ref = {
403 'isolated': results[0][0],
404 'isolatedserver': storage.location,
405 'namespace': storage.namespace,
406 }
maruel064c0a32016-04-05 11:47:15 -0700407 cold = sorted(i.size for i in f_cold)
408 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700409 except isolateserver.Aborted:
410 # This happens when a signal SIGTERM was received while uploading data.
411 # There is 2 causes:
412 # - The task was too slow and was about to be killed anyway due to
413 # exceeding the hard timeout.
414 # - The amount of data uploaded back is very large and took too much
415 # time to archive.
416 sys.stderr.write('Received SIGTERM while uploading')
417 # Re-raise, so it will be treated as an internal failure.
418 raise
nodir6f801882016-04-29 14:41:50 -0700419
420 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700421 try:
maruel12e30012015-10-09 11:55:35 -0700422 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700423 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700424 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700425 else:
426 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700427 except OSError as e:
428 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700429 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700430 stats = {
431 'duration': time.time() - start,
432 'items_cold': base64.b64encode(large.pack(cold)),
433 'items_hot': base64.b64encode(large.pack(hot)),
434 }
435 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700436
437
marueleb5fbee2015-09-17 13:01:36 -0700438def map_and_run(
nodir0ae98b32017-05-11 13:21:53 -0700439 command, isolated_hash, storage, isolate_cache, outputs,
440 install_named_caches, leak_temp_dir, root_dir, hard_timeout, grace_period,
441 bot_file, install_packages_fn, use_symlinks, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700442 """Runs a command with optional isolated input/output.
443
444 See run_tha_test for argument documentation.
445
446 Returns metadata about the result.
447 """
maruelabec63c2017-04-26 11:53:24 -0700448 assert isinstance(command, list), command
nodir56efa452016-10-12 12:17:39 -0700449 assert root_dir or root_dir is None
maruela9cfd6f2015-09-15 11:03:15 -0700450 result = {
maruel064c0a32016-04-05 11:47:15 -0700451 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700452 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700453 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700454 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700455 'stats': {
nodir55715712016-06-03 12:28:19 -0700456 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700457 # 'cipd': {
458 # 'duration': 0.,
459 # 'get_client_duration': 0.,
460 # },
nodir55715712016-06-03 12:28:19 -0700461 # 'download': {
462 # 'duration': 0.,
463 # 'initial_number_items': 0,
464 # 'initial_size': 0,
465 # 'items_cold': '<large.pack()>',
466 # 'items_hot': '<large.pack()>',
467 # },
468 # 'upload': {
469 # 'duration': 0.,
470 # 'items_cold': '<large.pack()>',
471 # 'items_hot': '<large.pack()>',
472 # },
maruel064c0a32016-04-05 11:47:15 -0700473 # },
474 },
iannucci96fcccc2016-08-30 15:52:22 -0700475 # 'cipd_pins': {
476 # 'packages': [
477 # {'package_name': ..., 'version': ..., 'path': ...},
478 # ...
479 # ],
480 # 'client_package': {'package_name': ..., 'version': ...},
481 # },
maruela9cfd6f2015-09-15 11:03:15 -0700482 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700483 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700484 }
nodirbe642ff2016-06-09 15:51:51 -0700485
marueleb5fbee2015-09-17 13:01:36 -0700486 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700487 file_path.ensure_tree(root_dir, 0700)
nodir56efa452016-10-12 12:17:39 -0700488 elif isolate_cache.cache_dir:
489 root_dir = os.path.dirname(isolate_cache.cache_dir)
maruele2f2cb82016-07-13 14:41:03 -0700490 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700491 # If root_dir is not specified, it is not constant.
492 # TODO(maruel): This is not obvious. Change this to become an error once we
493 # make the constant_run_path an exposed flag.
494 if constant_run_path and root_dir:
495 run_dir = os.path.join(root_dir, ISOLATED_RUN_DIR)
maruel5c4eed82017-05-26 05:33:40 -0700496 if os.path.isdir(run_dir):
497 file_path.rmtree(run_dir)
maruelcffa0542017-04-07 08:39:20 -0700498 os.mkdir(run_dir)
499 else:
500 run_dir = make_temp_dir(ISOLATED_RUN_DIR, root_dir)
maruel03e11842016-07-14 10:50:16 -0700501 # storage should be normally set but don't crash if it is not. This can happen
502 # as Swarming task can run without an isolate server.
maruele2f2cb82016-07-13 14:41:03 -0700503 out_dir = make_temp_dir(ISOLATED_OUT_DIR, root_dir) if storage else None
504 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, root_dir)
nodir55be77b2016-05-03 09:39:57 -0700505 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700506
nodir55be77b2016-05-03 09:39:57 -0700507 try:
vadimsh232f5a82017-01-20 19:23:44 -0800508 with install_packages_fn(run_dir) as cipd_info:
509 if cipd_info:
510 result['stats']['cipd'] = cipd_info.stats
511 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700512
vadimsh232f5a82017-01-20 19:23:44 -0800513 if isolated_hash:
514 isolated_stats = result['stats'].setdefault('isolated', {})
515 bundle, isolated_stats['download'] = fetch_and_map(
516 isolated_hash=isolated_hash,
517 storage=storage,
518 cache=isolate_cache,
519 outdir=run_dir,
520 use_symlinks=use_symlinks)
vadimsh232f5a82017-01-20 19:23:44 -0800521 change_tree_read_only(run_dir, bundle.read_only)
522 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
maruelabec63c2017-04-26 11:53:24 -0700523 # Inject the command
524 if bundle.command:
525 command = bundle.command + command
526
527 if not command:
528 # Handle this as a task failure, not an internal failure.
529 sys.stderr.write(
530 '<No command was specified!>\n'
531 '<Please secify a command when triggering your Swarming task>\n')
532 result['exit_code'] = 1
533 return result
nodirbe642ff2016-06-09 15:51:51 -0700534
vadimsh232f5a82017-01-20 19:23:44 -0800535 # If we have an explicit list of files to return, make sure their
536 # directories exist now.
537 if storage and outputs:
538 isolateserver.create_directories(run_dir, outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700539
vadimsh232f5a82017-01-20 19:23:44 -0800540 command = tools.fix_python_path(command)
541 command = process_command(command, out_dir, bot_file)
542 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700543
nodir0ae98b32017-05-11 13:21:53 -0700544 with install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -0800545 sys.stdout.flush()
546 start = time.time()
547 try:
548 result['exit_code'], result['had_hard_timeout'] = run_command(
549 command, cwd, get_command_env(tmp_dir, cipd_info),
550 hard_timeout, grace_period)
551 finally:
552 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700553 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700554 # An internal error occurred. Report accordingly so the swarming task will
555 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700556 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700557 result['internal_failure'] = str(e)
558 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700559
560 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700561 finally:
562 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700563 # Try to link files to the output directory, if specified.
564 if out_dir:
565 link_outputs_to_outdir(run_dir, out_dir, outputs)
566
nodir32a1ec12016-10-26 18:34:07 -0700567 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700568 if leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700569 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700570 logging.warning(
571 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700572 else:
maruel84537cb2015-10-16 14:21:28 -0700573 # On Windows rmtree(run_dir) call above has a synchronization effect: it
574 # finishes only when all task child processes terminate (since a running
575 # process locks *.exe file). Examine out_dir only after that call
576 # completes (since child processes may write to out_dir too and we need
577 # to wait for them to finish).
578 if fs.isdir(run_dir):
579 try:
580 success = file_path.rmtree(run_dir)
581 except OSError as e:
582 logging.error('Failure with %s', e)
583 success = False
584 if not success:
marueld928c862017-06-08 08:20:04 -0700585 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('run', grace_period))
maruel84537cb2015-10-16 14:21:28 -0700586 if result['exit_code'] == 0:
587 result['exit_code'] = 1
588 if fs.isdir(tmp_dir):
589 try:
590 success = file_path.rmtree(tmp_dir)
591 except OSError as e:
592 logging.error('Failure with %s', e)
593 success = False
594 if not success:
marueld928c862017-06-08 08:20:04 -0700595 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('run', grace_period))
maruel84537cb2015-10-16 14:21:28 -0700596 if result['exit_code'] == 0:
597 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700598
marueleb5fbee2015-09-17 13:01:36 -0700599 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700600 if out_dir:
nodir55715712016-06-03 12:28:19 -0700601 isolated_stats = result['stats'].setdefault('isolated', {})
602 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700603 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700604 if not success and result['exit_code'] == 0:
605 result['exit_code'] = 1
606 except Exception as e:
607 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700608 if out_dir:
609 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700610 result['internal_failure'] = str(e)
611 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500612
613
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400614def run_tha_test(
nodir0ae98b32017-05-11 13:21:53 -0700615 command, isolated_hash, storage, isolate_cache, outputs,
616 install_named_caches, leak_temp_dir, result_json, root_dir, hard_timeout,
617 grace_period, bot_file, install_packages_fn, use_symlinks):
nodir55be77b2016-05-03 09:39:57 -0700618 """Runs an executable and records execution metadata.
619
620 Either command or isolated_hash must be specified.
621
622 If isolated_hash is specified, downloads the dependencies in the cache,
623 hardlinks them into a temporary directory and runs the command specified in
624 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500625
626 A temporary directory is created to hold the output files. The content inside
627 this directory will be uploaded back to |storage| packaged as a .isolated
628 file.
629
630 Arguments:
maruelabec63c2017-04-26 11:53:24 -0700631 command: a list of string; the command to run OR optional arguments to add
632 to the command stated in the .isolated file if a command was
633 specified.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500634 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500635 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700636 The command specified in the .isolated is executed.
637 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500638 storage: an isolateserver.Storage object to retrieve remote objects. This
639 object has a reference to an isolateserver.StorageApi, which does
640 the actual I/O.
nodir6b945692016-10-19 19:09:06 -0700641 isolate_cache: an isolateserver.LocalCache to keep from retrieving the
642 same objects constantly by caching the objects retrieved.
643 Can be on-disk or in-memory.
nodir0ae98b32017-05-11 13:21:53 -0700644 install_named_caches: a function (run_dir) => context manager that installs
645 named caches into |run_dir|.
Kenneth Russell61d42352014-09-15 11:41:16 -0700646 leak_temp_dir: if true, the temporary directory will be deliberately leaked
647 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700648 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700649 exit code is always 0 unless an internal error occurred.
nodir90bc8dc2016-06-15 13:35:21 -0700650 root_dir: path to the directory to use to create the temporary directory. If
marueleb5fbee2015-09-17 13:01:36 -0700651 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700652 hard_timeout: kills the process if it lasts more than this amount of
653 seconds.
654 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
iannuccib58d10d2017-03-18 02:00:25 -0700655 install_packages_fn: context manager dir => CipdInfo, see
656 install_client_and_packages.
maruel4409e302016-07-19 14:25:51 -0700657 use_symlinks: create tree with symlinks instead of hardlinks.
maruela9cfd6f2015-09-15 11:03:15 -0700658
659 Returns:
660 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000661 """
maruela76b9ee2015-12-15 06:18:08 -0800662 if result_json:
663 # Write a json output file right away in case we get killed.
664 result = {
665 'exit_code': None,
666 'had_hard_timeout': False,
667 'internal_failure': 'Was terminated before completion',
668 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700669 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800670 }
671 tools.write_json(result_json, result, dense=True)
672
maruela9cfd6f2015-09-15 11:03:15 -0700673 # run_isolated exit code. Depends on if result_json is used or not.
674 result = map_and_run(
nodir220308c2017-02-01 19:32:53 -0800675 command, isolated_hash, storage, isolate_cache, outputs,
nodir0ae98b32017-05-11 13:21:53 -0700676 install_named_caches, leak_temp_dir, root_dir, hard_timeout, grace_period,
maruelabec63c2017-04-26 11:53:24 -0700677 bot_file, install_packages_fn, use_symlinks, True)
maruela9cfd6f2015-09-15 11:03:15 -0700678 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700679
maruela9cfd6f2015-09-15 11:03:15 -0700680 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700681 # We've found tests to delete 'work' when quitting, causing an exception
682 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700683 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700684 tools.write_json(result_json, result, dense=True)
685 # Only return 1 if there was an internal error.
686 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000687
maruela9cfd6f2015-09-15 11:03:15 -0700688 # Marshall into old-style inline output.
689 if result['outputs_ref']:
690 data = {
691 'hash': result['outputs_ref']['isolated'],
692 'namespace': result['outputs_ref']['namespace'],
693 'storage': result['outputs_ref']['isolatedserver'],
694 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500695 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700696 print(
697 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
698 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800699 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700700 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000701
702
iannuccib58d10d2017-03-18 02:00:25 -0700703# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800704CipdInfo = collections.namedtuple('CipdInfo', [
705 'client', # cipd.CipdClient object
706 'cache_dir', # absolute path to bot-global cipd tag and instance cache
707 'stats', # dict with stats to return to the server
708 'pins', # dict with installed cipd pins to return to the server
709])
710
711
712@contextlib.contextmanager
713def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700714 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800715 yield None
716
717
iannuccib58d10d2017-03-18 02:00:25 -0700718def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
719 """Calls 'cipd ensure' for packages.
720
721 Args:
722 run_dir (str): root of installation.
723 cipd_cache_dir (str): the directory to use for the cipd package cache.
724 client (CipdClient): the cipd client to use
725 packages: packages to install, list [(path, package_name, version), ...].
726 timeout: max duration in seconds that this function can take.
727
728 Returns: list of pinned packages. Looks like [
729 {
730 'path': 'subdirectory',
731 'package_name': 'resolved/package/name',
732 'version': 'deadbeef...',
733 },
734 ...
735 ]
736 """
737 package_pins = [None]*len(packages)
738 def insert_pin(path, name, version, idx):
739 package_pins[idx] = {
740 'package_name': name,
741 # swarming deals with 'root' as '.'
742 'path': path or '.',
743 'version': version,
744 }
745
746 by_path = collections.defaultdict(list)
747 for i, (path, name, version) in enumerate(packages):
748 # cipd deals with 'root' as ''
749 if path == '.':
750 path = ''
751 by_path[path].append((name, version, i))
752
753 pins = client.ensure(
754 run_dir,
755 {
756 subdir: [(name, vers) for name, vers, _ in pkgs]
757 for subdir, pkgs in by_path.iteritems()
758 },
759 cache_dir=cipd_cache_dir,
760 timeout=timeout,
761 )
762
763 for subdir, pin_list in sorted(pins.iteritems()):
764 this_subdir = by_path[subdir]
765 for i, (name, version) in enumerate(pin_list):
766 insert_pin(subdir, name, version, this_subdir[i][2])
767
768 assert None not in package_pins
769
770 return package_pins
771
772
vadimsh232f5a82017-01-20 19:23:44 -0800773@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700774def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700775 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800776 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800777 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700778
vadimsh232f5a82017-01-20 19:23:44 -0800779 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
780
781 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700782 [
783 {
784 "path": path, "package_name": package_name, "version": version,
785 },
786 ...
787 ]
vadimsh902948e2017-01-20 15:57:32 -0800788 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700789
790 such that they correspond 1:1 to all input package arguments from the command
791 line. These dictionaries make their all the way back to swarming, where they
792 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700793
vadimsh902948e2017-01-20 15:57:32 -0800794 If 'packages' list is empty, will bootstrap CIPD client, but won't install
795 any packages.
796
797 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800798 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800799
nodirbe642ff2016-06-09 15:51:51 -0700800 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700801 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800802 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700803 service_url (str): CIPD server url, e.g.
804 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700805 client_package_name (str): CIPD package name of CIPD client.
806 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700807 cache_dir (str): where to keep cache of cipd clients, packages and tags.
808 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700809 """
810 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700811
nodirbe642ff2016-06-09 15:51:51 -0700812 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700813 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700814
vadimsh902948e2017-01-20 15:57:32 -0800815 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800816 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700817 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800818 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700819
nodirbe642ff2016-06-09 15:51:51 -0700820 get_client_start = time.time()
821 client_manager = cipd.get_client(
822 service_url, client_package_name, client_version, cache_dir,
823 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700824
nodirbe642ff2016-06-09 15:51:51 -0700825 with client_manager as client:
826 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700827
iannuccib58d10d2017-03-18 02:00:25 -0700828 package_pins = []
829 if packages:
830 package_pins = _install_packages(
831 run_dir, cipd_cache_dir, client, packages, timeoutfn())
832
833 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700834
vadimsh232f5a82017-01-20 19:23:44 -0800835 total_duration = time.time() - start
836 logging.info(
837 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700838
vadimsh232f5a82017-01-20 19:23:44 -0800839 yield CipdInfo(
840 client=client,
841 cache_dir=cipd_cache_dir,
842 stats={
843 'duration': total_duration,
844 'get_client_duration': get_client_duration,
845 },
846 pins={
iannuccib58d10d2017-03-18 02:00:25 -0700847 'client_package': {
848 'package_name': client.package_name,
849 'version': client.instance_id,
850 },
vadimsh232f5a82017-01-20 19:23:44 -0800851 'packages': package_pins,
852 })
nodirbe642ff2016-06-09 15:51:51 -0700853
854
nodirf33b8d62016-10-26 22:34:58 -0700855def clean_caches(options, isolate_cache, named_cache_manager):
maruele6fc9382017-05-04 09:03:48 -0700856 """Trims isolated and named caches.
857
858 The goal here is to coherently trim both caches, deleting older items
859 independent of which container they belong to.
860 """
861 # TODO(maruel): Trim CIPD cache the same way.
862 total = 0
nodirf33b8d62016-10-26 22:34:58 -0700863 with named_cache_manager.open():
864 oldest_isolated = isolate_cache.get_oldest()
865 oldest_named = named_cache_manager.get_oldest()
866 trimmers = [
867 (
868 isolate_cache.trim,
869 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
870 ),
871 (
872 lambda: named_cache_manager.trim(options.min_free_space),
873 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
874 ),
875 ]
876 trimmers.sort(key=lambda (_, ts): ts)
maruele6fc9382017-05-04 09:03:48 -0700877 # TODO(maruel): This is incorrect, we want to trim 'items' that are strictly
878 # the oldest independent of in which cache they live in. Right now, the
879 # cache with the oldest item pays the price.
nodirf33b8d62016-10-26 22:34:58 -0700880 for trim, _ in trimmers:
maruele6fc9382017-05-04 09:03:48 -0700881 total += trim()
nodirf33b8d62016-10-26 22:34:58 -0700882 isolate_cache.cleanup()
maruele6fc9382017-05-04 09:03:48 -0700883 return total
nodirf33b8d62016-10-26 22:34:58 -0700884
885
nodirbe642ff2016-06-09 15:51:51 -0700886def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400887 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700888 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000889 version=__version__,
890 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700891 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700892 '--clean', action='store_true',
893 help='Cleans the cache, trimming it necessary and remove corrupted items '
894 'and returns without executing anything; use with -v to know what '
895 'was done')
896 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -0700897 '--no-clean', action='store_true',
898 help='Do not clean the cache automatically on startup. This is meant for '
899 'bots where a separate execution with --clean was done earlier so '
900 'doing it again is redundant')
901 parser.add_option(
maruel4409e302016-07-19 14:25:51 -0700902 '--use-symlinks', action='store_true',
903 help='Use symlinks instead of hardlinks')
904 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700905 '--json',
906 help='dump output metadata to json file. When used, run_isolated returns '
907 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700908 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800909 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700910 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800911 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700912 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700913 parser.add_option(
914 '--bot-file',
915 help='Path to a file describing the state of the host. The content is '
916 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -0700917 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -0700918 '--output', action='append',
919 help='Specifies an output to return. If no outputs are specified, all '
920 'files located in $(ISOLATED_OUTDIR) will be returned; '
921 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
922 'specified by --output option (there can be multiple) will be '
923 'returned. Note that if a file in OUT_DIR has the same path '
924 'as an --output option, the --output version will be returned.')
925 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -0700926 '-a', '--argsfile',
927 # This is actually handled in parse_args; it's included here purely so it
928 # can make it into the help text.
929 help='Specify a file containing a JSON array of arguments to this '
930 'script. If --argsfile is provided, no other argument may be '
931 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500932 data_group = optparse.OptionGroup(parser, 'Data source')
933 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500934 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700935 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500936 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500937 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000938
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400939 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700940
941 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -0700942 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000943
Kenneth Russell61d42352014-09-15 11:41:16 -0700944 debug_group = optparse.OptionGroup(parser, 'Debugging')
945 debug_group.add_option(
946 '--leak-temp-dir',
947 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700948 help='Deliberately leak isolate\'s temp dir for later examination. '
949 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700950 debug_group.add_option(
951 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700952 parser.add_option_group(debug_group)
953
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800954 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700955
nodirf33b8d62016-10-26 22:34:58 -0700956 parser.set_defaults(
957 cache='cache',
958 cipd_cache='cipd_cache',
959 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -0700960 return parser
961
962
aludwin7556e0c2016-10-26 08:46:10 -0700963def parse_args(args):
964 # Create a fake mini-parser just to get out the "-a" command. Note that
965 # it's not documented here; instead, it's documented in create_option_parser
966 # even though that parser will never actually get to parse it. This is
967 # because --argsfile is exclusive with all other options and arguments.
968 file_argparse = argparse.ArgumentParser(add_help=False)
969 file_argparse.add_argument('-a', '--argsfile')
970 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
971 if file_args.argsfile:
972 if nonfile_args:
973 file_argparse.error('Can\'t specify --argsfile with'
974 'any other arguments (%s)' % nonfile_args)
975 try:
976 with open(file_args.argsfile, 'r') as f:
977 args = json.load(f)
978 except (IOError, OSError, ValueError) as e:
979 # We don't need to error out here - "args" is now empty,
980 # so the call below to parser.parse_args(args) will fail
981 # and print the full help text.
982 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
983
984 # Even if we failed to read the args, just call the normal parser now since it
985 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -0700986 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500987 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -0700988 return (parser, options, args)
989
990
991def main(args):
992 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700993
nodirf33b8d62016-10-26 22:34:58 -0700994 isolate_cache = isolateserver.process_cache_options(options, trim=False)
995 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -0700996 if options.clean:
997 if options.isolated:
998 parser.error('Can\'t use --isolated with --clean.')
999 if options.isolate_server:
1000 parser.error('Can\'t use --isolate-server with --clean.')
1001 if options.json:
1002 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -07001003 if options.named_caches:
1004 parser.error('Can\t use --named-cache with --clean.')
1005 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001006 return 0
nodirf33b8d62016-10-26 22:34:58 -07001007
maruel2e8d0f52016-07-16 07:51:29 -07001008 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -07001009 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001010
nodir55be77b2016-05-03 09:39:57 -07001011 if not options.isolated and not args:
1012 parser.error('--isolated or command to run is required.')
1013
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001014 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001015
1016 isolateserver.process_isolate_server_options(
1017 parser, options, True, False)
1018 if not options.isolate_server:
1019 if options.isolated:
1020 parser.error('--isolated requires --isolate-server')
1021 if ISOLATED_OUTDIR_PARAMETER in args:
1022 parser.error(
1023 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001024
nodir90bc8dc2016-06-15 13:35:21 -07001025 if options.root_dir:
1026 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -07001027 if options.json:
1028 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001029
nodirbe642ff2016-06-09 15:51:51 -07001030 cipd.validate_cipd_options(parser, options)
1031
vadimsh232f5a82017-01-20 19:23:44 -08001032 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -08001033 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -07001034 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -08001035 run_dir, cipd.parse_package_args(options.cipd_packages),
1036 options.cipd_server, options.cipd_client_package,
1037 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001038
nodird6160682017-02-02 13:03:35 -08001039 @contextlib.contextmanager
nodir0ae98b32017-05-11 13:21:53 -07001040 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001041 # WARNING: this function depends on "options" variable defined in the outer
1042 # function.
nodir0ae98b32017-05-11 13:21:53 -07001043 caches = [
1044 (os.path.join(run_dir, unicode(relpath)), name)
1045 for name, relpath in options.named_caches
1046 ]
nodirf33b8d62016-10-26 22:34:58 -07001047 with named_cache_manager.open():
nodir0ae98b32017-05-11 13:21:53 -07001048 for path, name in caches:
1049 named_cache_manager.install(path, name)
nodird6160682017-02-02 13:03:35 -08001050 try:
1051 yield
1052 finally:
nodir0ae98b32017-05-11 13:21:53 -07001053 with named_cache_manager.open():
1054 for path, name in caches:
1055 named_cache_manager.uninstall(path, name)
nodirf33b8d62016-10-26 22:34:58 -07001056
nodirbe642ff2016-06-09 15:51:51 -07001057 try:
nodir90bc8dc2016-06-15 13:35:21 -07001058 if options.isolate_server:
1059 storage = isolateserver.get_storage(
1060 options.isolate_server, options.namespace)
1061 with storage:
nodirf33b8d62016-10-26 22:34:58 -07001062 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
1063 assert storage.hash_algo == isolate_cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -07001064 return run_tha_test(
maruelabec63c2017-04-26 11:53:24 -07001065 args,
nodirf33b8d62016-10-26 22:34:58 -07001066 options.isolated,
1067 storage,
1068 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001069 options.output,
nodir0ae98b32017-05-11 13:21:53 -07001070 install_named_caches,
nodirf33b8d62016-10-26 22:34:58 -07001071 options.leak_temp_dir,
1072 options.json, options.root_dir,
1073 options.hard_timeout,
1074 options.grace_period,
maruelabec63c2017-04-26 11:53:24 -07001075 options.bot_file,
nodirf33b8d62016-10-26 22:34:58 -07001076 install_packages_fn,
1077 options.use_symlinks)
maruel4409e302016-07-19 14:25:51 -07001078 return run_tha_test(
maruelabec63c2017-04-26 11:53:24 -07001079 args,
nodirf33b8d62016-10-26 22:34:58 -07001080 options.isolated,
1081 None,
1082 isolate_cache,
aludwin0a8e17d2016-10-27 15:57:39 -07001083 options.output,
nodir0ae98b32017-05-11 13:21:53 -07001084 install_named_caches,
nodirf33b8d62016-10-26 22:34:58 -07001085 options.leak_temp_dir,
1086 options.json,
1087 options.root_dir,
1088 options.hard_timeout,
1089 options.grace_period,
maruelabec63c2017-04-26 11:53:24 -07001090 options.bot_file,
nodirf33b8d62016-10-26 22:34:58 -07001091 install_packages_fn,
maruel4409e302016-07-19 14:25:51 -07001092 options.use_symlinks)
nodirf33b8d62016-10-26 22:34:58 -07001093 except (cipd.Error, named_cache.Error) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001094 print >> sys.stderr, ex.message
1095 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001096
1097
1098if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001099 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001100 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001101 fix_encoding.fix_encoding()
maruel4409e302016-07-19 14:25:51 -07001102 file_path.enable_symlink()
aludwin7556e0c2016-10-26 08:46:10 -07001103
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001104 sys.exit(main(sys.argv[1:]))