blob: 0cdf3a63509b6cce432b2e4adb4a01b3cd351bfe [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
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -050030__version__ = '0.10.4'
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
vadimsh9c54b2c2017-07-25 14:08:29 -070055from libs import luci_context
56
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080057import auth
nodirbe642ff2016-06-09 15:51:51 -070058import cipd
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000059import isolateserver
nodirf33b8d62016-10-26 22:34:58 -070060import named_cache
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000061
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000062
vadimsh@chromium.org85071062013-08-21 23:37:45 +000063# Absolute path to this file (can be None if running from zip on Mac).
tansella4949442016-06-23 22:34:32 -070064THIS_FILE_PATH = os.path.abspath(
65 __file__.decode(sys.getfilesystemencoding())) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000066
67# Directory that contains this file (might be inside zip package).
tansella4949442016-06-23 22:34:32 -070068BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__.decode(
69 sys.getfilesystemencoding()) else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000070
71# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000072if zip_package.get_main_script_path():
73 MAIN_DIR = os.path.dirname(
74 os.path.abspath(zip_package.get_main_script_path()))
75else:
76 # This happens when 'import run_isolated' is executed at the python
77 # interactive prompt, in that case __file__ is undefined.
78 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000079
maruele2f2cb82016-07-13 14:41:03 -070080
81# Magic variables that can be found in the isolate task command line.
82ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
83EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
84SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
85
86
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000087# The name of the log file to use.
88RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
89
maruele2f2cb82016-07-13 14:41:03 -070090
csharp@chromium.orge217f302012-11-22 16:51:53 +000091# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000092RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000093
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000094
maruele2f2cb82016-07-13 14:41:03 -070095# Use short names for temporary directories. This is driven by Windows, which
96# imposes a relatively short maximum path length of 260 characters, often
97# referred to as MAX_PATH. It is relatively easy to create files with longer
98# path length. A use case is with recursive depedency treesV like npm packages.
99#
100# It is recommended to start the script with a `root_dir` as short as
101# possible.
102# - ir stands for isolated_run
103# - io stands for isolated_out
104# - it stands for isolated_tmp
105ISOLATED_RUN_DIR = u'ir'
106ISOLATED_OUT_DIR = u'io'
107ISOLATED_TMP_DIR = u'it'
108
109
marueld928c862017-06-08 08:20:04 -0700110OUTLIVING_ZOMBIE_MSG = """\
111*** Swarming tried multiple times to delete the %s directory and failed ***
112*** Hard failing the task ***
113
114Swarming detected that your testing script ran an executable, which may have
115started a child executable, and the main script returned early, leaving the
116children executables playing around unguided.
117
118You don't want to leave children processes outliving the task on the Swarming
119bot, do you? The Swarming bot doesn't.
120
121How to fix?
122- For any process that starts children processes, make sure all children
123 processes terminated properly before each parent process exits. This is
124 especially important in very deep process trees.
125 - This must be done properly both in normal successful task and in case of
126 task failure. Cleanup is very important.
127- The Swarming bot sends a SIGTERM in case of timeout.
128 - You have %s seconds to comply after the signal was sent to the process
129 before the process is forcibly killed.
130- To achieve not leaking children processes in case of signals on timeout, you
131 MUST handle signals in each executable / python script and propagate them to
132 children processes.
133 - When your test script (python or binary) receives a signal like SIGTERM or
134 CTRL_BREAK_EVENT on Windows), send it to all children processes and wait for
135 them to terminate before quitting.
136
137See
138https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Bot.md#graceful-termination-aka-the-sigterm-and-sigkill-dance
139for more information.
140
141*** May the SIGKILL force be with you ***
142"""
143
144
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500145TaskData = collections.namedtuple(
146 'TaskData', [
147 # List of strings; the command line to use, independent of what was
148 # specified in the isolated file.
149 'command',
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500150 # Relative directory to start command into.
151 'relative_cwd',
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500152 # List of strings; the arguments to add to the command specified in the
153 # isolated file.
154 'extra_args',
155 # Hash of the .isolated file that must be retrieved to recreate the tree
156 # of files to run the target executable. The command specified in the
157 # .isolated is executed. Mutually exclusive with command argument.
158 'isolated_hash',
159 # isolateserver.Storage instance to retrieve remote objects. This object
160 # has a reference to an isolateserver.StorageApi, which does the actual
161 # I/O.
162 'storage',
163 # isolateserver.LocalCache instance to keep from retrieving the same
164 # objects constantly by caching the objects retrieved. Can be on-disk or
165 # in-memory.
166 'isolate_cache',
167 # List of paths relative to root_dir to put into the output isolated
168 # bundle upon task completion (see link_outputs_to_outdir).
169 'outputs',
170 # Function (run_dir) => context manager that installs named caches into
171 # |run_dir|.
172 'install_named_caches',
173 # If True, the temporary directory will be deliberately leaked for later
174 # examination.
175 'leak_temp_dir',
176 # Path to the directory to use to create the temporary directory. If not
177 # specified, a random temporary directory is created.
178 'root_dir',
179 # Kills the process if it lasts more than this amount of seconds.
180 'hard_timeout',
181 # Number of seconds to wait between SIGTERM and SIGKILL.
182 'grace_period',
183 # Path to a file with bot state, used in place of ${SWARMING_BOT_FILE}
184 # task command line argument.
185 'bot_file',
186 # Logical account to switch LUCI_CONTEXT into.
187 'switch_to_account',
188 # Context manager dir => CipdInfo, see install_client_and_packages.
189 'install_packages_fn',
190 # Create tree with symlinks instead of hardlinks.
191 'use_symlinks',
192 # Environment variables to set.
193 'env',
194 # Environment variables to mutate with relative directories.
195 # Example: {"ENV_KEY": ['relative', 'paths', 'to', 'prepend']}
196 'env_prefix'])
197
198
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000199def get_as_zip_package(executable=True):
200 """Returns ZipPackage with this module and all its dependencies.
201
202 If |executable| is True will store run_isolated.py as __main__.py so that
203 zip package is directly executable be python.
204 """
205 # Building a zip package when running from another zip package is
206 # unsupported and probably unneeded.
207 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +0000208 assert THIS_FILE_PATH
209 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000210 package = zip_package.ZipPackage(root=BASE_DIR)
211 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
aludwin81178302016-11-30 17:18:49 -0800212 package.add_python_file(os.path.join(BASE_DIR, 'isolate_storage.py'))
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400213 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000214 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800215 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
nodirbe642ff2016-06-09 15:51:51 -0700216 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
nodirf33b8d62016-10-26 22:34:58 -0700217 package.add_python_file(os.path.join(BASE_DIR, 'named_cache.py'))
tanselle4288c32016-07-28 09:45:40 -0700218 package.add_directory(os.path.join(BASE_DIR, 'libs'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000219 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
220 package.add_directory(os.path.join(BASE_DIR, 'utils'))
221 return package
222
223
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500224def _to_str(s):
225 """Downgrades a unicode instance to str. Pass str through as-is."""
226 if isinstance(s, str):
227 return s
228 # This is technically incorrect, especially on Windows. In theory
229 # sys.getfilesystemencoding() should be used to use the right 'ANSI code
230 # page' on Windows, but that causes other problems, as the character set
231 # is very limited.
232 return s.encode('utf-8')
233
234
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -0500235def _to_unicode(s):
236 """Upgrades a str instance to unicode. Pass unicode through as-is."""
237 if isinstance(s, unicode) or s is None:
238 return s
239 return s.decode('utf-8')
240
241
maruel03e11842016-07-14 10:50:16 -0700242def make_temp_dir(prefix, root_dir):
243 """Returns a new unique temporary directory."""
244 return unicode(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000245
246
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500247def change_tree_read_only(rootdir, read_only):
248 """Changes the tree read-only bits according to the read_only specification.
249
250 The flag can be 0, 1 or 2, which will affect the possibility to modify files
251 and create or delete files.
252 """
253 if read_only == 2:
254 # Files and directories (except on Windows) are marked read only. This
255 # inhibits modifying, creating or deleting files in the test directory,
256 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400257 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500258 elif read_only == 1:
259 # Files are marked read only but not the directories. This inhibits
260 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400261 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500262 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500263 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500264 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
265 # is not yet changed to verify the hash of the content of the files it is
266 # looking at, so that if a test modifies an input file, the file must be
267 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400268 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500269 else:
270 raise ValueError(
271 'change_tree_read_only(%s, %s): Unknown flag %s' %
272 (rootdir, read_only, read_only))
273
274
vadimsh9c54b2c2017-07-25 14:08:29 -0700275@contextlib.contextmanager
276def set_luci_context_account(account, tmp_dir):
277 """Sets LUCI_CONTEXT account to be used by the task.
278
279 If 'account' is None or '', does nothing at all. This happens when
280 run_isolated.py is called without '--switch-to-account' flag. In this case,
281 if run_isolated.py is running in some LUCI_CONTEXT environment, the task will
282 just inherit whatever account is already set. This may happen is users invoke
283 run_isolated.py explicitly from their code.
284
285 If the requested account is not defined in the context, switches to
286 non-authenticated access. This happens for Swarming tasks that don't use
287 'task' service accounts.
288
289 If not using LUCI_CONTEXT-based auth, does nothing.
290 If already running as requested account, does nothing.
291 """
292 if not account:
293 # Not actually switching.
294 yield
295 return
296
297 local_auth = luci_context.read('local_auth')
298 if not local_auth:
299 # Not using LUCI_CONTEXT auth at all.
300 yield
301 return
302
303 # See LUCI_CONTEXT.md for the format of 'local_auth'.
304 if local_auth.get('default_account_id') == account:
305 # Already set, no need to switch.
306 yield
307 return
308
309 available = {a['id'] for a in local_auth.get('accounts') or []}
310 if account in available:
311 logging.info('Switching default LUCI_CONTEXT account to %r', account)
312 local_auth['default_account_id'] = account
313 else:
314 logging.warning(
315 'Requested LUCI_CONTEXT account %r is not available (have only %r), '
316 'disabling authentication', account, sorted(available))
317 local_auth.pop('default_account_id', None)
318
319 with luci_context.write(_tmpdir=tmp_dir, local_auth=local_auth):
320 yield
321
322
nodir90bc8dc2016-06-15 13:35:21 -0700323def process_command(command, out_dir, bot_file):
nodirbe642ff2016-06-09 15:51:51 -0700324 """Replaces variables in a command line.
325
326 Raises:
327 ValueError if a parameter is requested in |command| but its value is not
328 provided.
329 """
maruela9cfd6f2015-09-15 11:03:15 -0700330 def fix(arg):
nodirbe642ff2016-06-09 15:51:51 -0700331 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
332 replace_slash = False
nodir55be77b2016-05-03 09:39:57 -0700333 if ISOLATED_OUTDIR_PARAMETER in arg:
nodirbe642ff2016-06-09 15:51:51 -0700334 if not out_dir:
maruel7f63a272016-07-12 12:40:36 -0700335 raise ValueError(
336 'output directory is requested in command, but not provided; '
337 'please specify one')
nodir55be77b2016-05-03 09:39:57 -0700338 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
nodirbe642ff2016-06-09 15:51:51 -0700339 replace_slash = True
nodir90bc8dc2016-06-15 13:35:21 -0700340 if SWARMING_BOT_FILE_PARAMETER in arg:
341 if bot_file:
342 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
343 replace_slash = True
344 else:
345 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command, but no '
346 'bot_file specified. Leaving parameter unchanged.')
nodirbe642ff2016-06-09 15:51:51 -0700347 if replace_slash:
348 # Replace slashes only if parameters are present
nodir55be77b2016-05-03 09:39:57 -0700349 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
350 arg = arg.replace('/', os.sep)
maruela9cfd6f2015-09-15 11:03:15 -0700351 return arg
352
353 return [fix(arg) for arg in command]
354
355
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500356def get_command_env(tmp_dir, cipd_info, cwd, env, env_prefixes):
vadimsh232f5a82017-01-20 19:23:44 -0800357 """Returns full OS environment to run a command in.
358
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800359 Sets up TEMP, puts directory with cipd binary in front of PATH, exposes
360 CIPD_CACHE_DIR env var, and installs all env_prefixes.
vadimsh232f5a82017-01-20 19:23:44 -0800361
362 Args:
363 tmp_dir: temp directory.
364 cipd_info: CipdInfo object is cipd client is used, None if not.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800365 cwd: The directory the command will run in
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500366 env: environment variables to use
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800367 env_prefixes: {"ENV_KEY": ['cwd', 'relative', 'paths', 'to', 'prepend']}
vadimsh232f5a82017-01-20 19:23:44 -0800368 """
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500369 out = os.environ.copy()
370 for k, v in env.iteritems():
371 if not v:
372 del out[k]
373 else:
374 out[k] = v
375
376 if cipd_info:
377 bin_dir = os.path.dirname(cipd_info.client.binary_path)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500378 out['PATH'] = '%s%s%s' % (_to_str(bin_dir), os.pathsep, out['PATH'])
379 out['CIPD_CACHE_DIR'] = _to_str(cipd_info.cache_dir)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500380
381 for key, paths in env_prefixes.iteritems():
382 paths = [os.path.normpath(os.path.join(cwd, p)) for p in paths]
383 cur = out.get(key)
384 if cur:
385 paths.append(cur)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500386 out[key] = _to_str(os.path.pathsep.join(paths))
vadimsh232f5a82017-01-20 19:23:44 -0800387
iannucciac0342c2017-02-24 05:47:01 -0800388 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
iannucci460def72017-02-24 10:49:48 -0800389 # * mktemp on linux respects $TMPDIR, not $TMP
390 # * mktemp on OS X SOMETIMES respects $TMPDIR
iannucciac0342c2017-02-24 05:47:01 -0800391 # * chromium's base utils respects $TMPDIR on linux, $TEMP on windows.
392 # Unfortunately at the time of writing it completely ignores all envvars
393 # on OS X.
iannucci460def72017-02-24 10:49:48 -0800394 # * python respects TMPDIR, TEMP, and TMP (regardless of platform)
395 # * golang respects TMPDIR on linux+mac, TEMP on windows.
iannucciac0342c2017-02-24 05:47:01 -0800396 key = {'win32': 'TEMP'}.get(sys.platform, 'TMPDIR')
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500397 out[key] = _to_str(tmp_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800398
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500399 return out
vadimsh232f5a82017-01-20 19:23:44 -0800400
401
402def run_command(command, cwd, env, hard_timeout, grace_period):
maruel6be7f9e2015-10-01 12:25:30 -0700403 """Runs the command.
404
405 Returns:
406 tuple(process exit code, bool if had a hard timeout)
407 """
maruela9cfd6f2015-09-15 11:03:15 -0700408 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700409
maruel6be7f9e2015-10-01 12:25:30 -0700410 exit_code = None
411 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700412 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700413 proc = None
414 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700415 try:
maruel6be7f9e2015-10-01 12:25:30 -0700416 # TODO(maruel): This code is imperfect. It doesn't handle well signals
417 # during the download phase and there's short windows were things can go
418 # wrong.
419 def handler(signum, _frame):
420 if proc and not had_signal:
421 logging.info('Received signal %d', signum)
422 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700423 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700424
425 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
426 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
427 try:
428 exit_code = proc.wait(hard_timeout or None)
429 except subprocess42.TimeoutExpired:
430 if not had_signal:
431 logging.warning('Hard timeout')
432 had_hard_timeout = True
433 logging.warning('Sending SIGTERM')
434 proc.terminate()
435
436 # Ignore signals in grace period. Forcibly give the grace period to the
437 # child process.
438 if exit_code is None:
439 ignore = lambda *_: None
440 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
441 try:
442 exit_code = proc.wait(grace_period or None)
443 except subprocess42.TimeoutExpired:
444 # Now kill for real. The user can distinguish between the
445 # following states:
446 # - signal but process exited within grace period,
447 # hard_timed_out will be set but the process exit code will be
448 # script provided.
449 # - processed exited late, exit code will be -9 on posix.
450 logging.warning('Grace exhausted; sending SIGKILL')
451 proc.kill()
martiniss5c8043e2017-08-01 17:09:43 -0700452 logging.info('Waiting for process exit')
maruel6be7f9e2015-10-01 12:25:30 -0700453 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700454 except OSError:
455 # This is not considered to be an internal error. The executable simply
456 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800457 sys.stderr.write(
458 '<The executable does not exist or a dependent library is missing>\n'
459 '<Check for missing .so/.dll in the .isolate or GN file>\n'
460 '<Command: %s>\n' % command)
461 if os.environ.get('SWARMING_TASK_ID'):
462 # Give an additional hint when running as a swarming task.
463 sys.stderr.write(
464 '<See the task\'s page for commands to help diagnose this issue '
465 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700466 exit_code = 1
467 logging.info(
468 'Command finished with exit code %d (%s)',
469 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700470 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700471
472
maruel4409e302016-07-19 14:25:51 -0700473def fetch_and_map(isolated_hash, storage, cache, outdir, use_symlinks):
474 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700475 start = time.time()
476 bundle = isolateserver.fetch_isolated(
477 isolated_hash=isolated_hash,
478 storage=storage,
479 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700480 outdir=outdir,
481 use_symlinks=use_symlinks)
nodir6f801882016-04-29 14:41:50 -0700482 return bundle, {
483 'duration': time.time() - start,
484 'initial_number_items': cache.initial_number_items,
485 'initial_size': cache.initial_size,
486 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
487 'items_hot': base64.b64encode(
tansell9e04a8d2016-07-28 09:31:59 -0700488 large.pack(sorted(set(cache.used) - set(cache.added)))),
nodir6f801882016-04-29 14:41:50 -0700489 }
490
491
aludwin0a8e17d2016-10-27 15:57:39 -0700492def link_outputs_to_outdir(run_dir, out_dir, outputs):
493 """Links any named outputs to out_dir so they can be uploaded.
494
495 Raises an error if the file already exists in that directory.
496 """
497 if not outputs:
498 return
499 isolateserver.create_directories(out_dir, outputs)
500 for o in outputs:
501 try:
aludwinf31ab802017-06-12 06:03:00 -0700502 infile = os.path.join(run_dir, o)
503 outfile = os.path.join(out_dir, o)
504 if fs.islink(infile):
505 # TODO(aludwin): handle directories
506 fs.copy2(infile, outfile)
507 else:
508 file_path.link_file(outfile, infile, file_path.HARDLINK_WITH_FALLBACK)
aludwin0a8e17d2016-10-27 15:57:39 -0700509 except OSError as e:
aludwin81178302016-11-30 17:18:49 -0800510 logging.info("Couldn't collect output file %s: %s", o, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700511
512
maruela9cfd6f2015-09-15 11:03:15 -0700513def delete_and_upload(storage, out_dir, leak_temp_dir):
514 """Deletes the temporary run directory and uploads results back.
515
516 Returns:
nodir6f801882016-04-29 14:41:50 -0700517 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700518 - outputs_ref: a dict referring to the results archived back to the isolated
519 server, if applicable.
520 - success: False if something occurred that means that the task must
521 forcibly be considered a failure, e.g. zombie processes were left
522 behind.
nodir6f801882016-04-29 14:41:50 -0700523 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700524 """
maruela9cfd6f2015-09-15 11:03:15 -0700525 # Upload out_dir and generate a .isolated file out of this directory. It is
526 # only done if files were written in the directory.
527 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700528 cold = []
529 hot = []
nodir6f801882016-04-29 14:41:50 -0700530 start = time.time()
531
maruel12e30012015-10-09 11:55:35 -0700532 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700533 with tools.Profiler('ArchiveOutput'):
534 try:
maruel064c0a32016-04-05 11:47:15 -0700535 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700536 storage, [out_dir], None)
537 outputs_ref = {
538 'isolated': results[0][0],
539 'isolatedserver': storage.location,
540 'namespace': storage.namespace,
541 }
maruel064c0a32016-04-05 11:47:15 -0700542 cold = sorted(i.size for i in f_cold)
543 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700544 except isolateserver.Aborted:
545 # This happens when a signal SIGTERM was received while uploading data.
546 # There is 2 causes:
547 # - The task was too slow and was about to be killed anyway due to
548 # exceeding the hard timeout.
549 # - The amount of data uploaded back is very large and took too much
550 # time to archive.
551 sys.stderr.write('Received SIGTERM while uploading')
552 # Re-raise, so it will be treated as an internal failure.
553 raise
nodir6f801882016-04-29 14:41:50 -0700554
555 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700556 try:
maruel12e30012015-10-09 11:55:35 -0700557 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700558 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700559 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700560 else:
561 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700562 except OSError as e:
563 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700564 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700565 stats = {
566 'duration': time.time() - start,
567 'items_cold': base64.b64encode(large.pack(cold)),
568 'items_hot': base64.b64encode(large.pack(hot)),
569 }
570 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700571
572
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500573def map_and_run(data, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700574 """Runs a command with optional isolated input/output.
575
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500576 Arguments:
577 - data: TaskData instance.
578 - constant_run_path: TODO
nodir55be77b2016-05-03 09:39:57 -0700579
580 Returns metadata about the result.
581 """
maruela9cfd6f2015-09-15 11:03:15 -0700582 result = {
maruel064c0a32016-04-05 11:47:15 -0700583 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700584 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700585 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700586 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700587 'stats': {
nodir55715712016-06-03 12:28:19 -0700588 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700589 # 'cipd': {
590 # 'duration': 0.,
591 # 'get_client_duration': 0.,
592 # },
nodir55715712016-06-03 12:28:19 -0700593 # 'download': {
594 # 'duration': 0.,
595 # 'initial_number_items': 0,
596 # 'initial_size': 0,
597 # 'items_cold': '<large.pack()>',
598 # 'items_hot': '<large.pack()>',
599 # },
600 # 'upload': {
601 # 'duration': 0.,
602 # 'items_cold': '<large.pack()>',
603 # 'items_hot': '<large.pack()>',
604 # },
maruel064c0a32016-04-05 11:47:15 -0700605 # },
606 },
iannucci96fcccc2016-08-30 15:52:22 -0700607 # 'cipd_pins': {
608 # 'packages': [
609 # {'package_name': ..., 'version': ..., 'path': ...},
610 # ...
611 # ],
612 # 'client_package': {'package_name': ..., 'version': ...},
613 # },
maruela9cfd6f2015-09-15 11:03:15 -0700614 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700615 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700616 }
nodirbe642ff2016-06-09 15:51:51 -0700617
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500618 if data.root_dir:
619 file_path.ensure_tree(data.root_dir, 0700)
620 elif data.isolate_cache.cache_dir:
621 data = data._replace(
622 root_dir=os.path.dirname(data.isolate_cache.cache_dir))
maruele2f2cb82016-07-13 14:41:03 -0700623 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700624 # If root_dir is not specified, it is not constant.
625 # TODO(maruel): This is not obvious. Change this to become an error once we
626 # make the constant_run_path an exposed flag.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500627 if constant_run_path and data.root_dir:
628 run_dir = os.path.join(data.root_dir, ISOLATED_RUN_DIR)
maruel5c4eed82017-05-26 05:33:40 -0700629 if os.path.isdir(run_dir):
630 file_path.rmtree(run_dir)
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500631 os.mkdir(run_dir, 0700)
maruelcffa0542017-04-07 08:39:20 -0700632 else:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500633 run_dir = make_temp_dir(ISOLATED_RUN_DIR, data.root_dir)
maruel03e11842016-07-14 10:50:16 -0700634 # storage should be normally set but don't crash if it is not. This can happen
635 # as Swarming task can run without an isolate server.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500636 out_dir = make_temp_dir(
637 ISOLATED_OUT_DIR, data.root_dir) if data.storage else None
638 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, data.root_dir)
nodir55be77b2016-05-03 09:39:57 -0700639 cwd = run_dir
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500640 if data.relative_cwd:
641 cwd = os.path.normpath(os.path.join(cwd, data.relative_cwd))
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500642 command = data.command
nodir55be77b2016-05-03 09:39:57 -0700643 try:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500644 with data.install_packages_fn(run_dir) as cipd_info:
vadimsh232f5a82017-01-20 19:23:44 -0800645 if cipd_info:
646 result['stats']['cipd'] = cipd_info.stats
647 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700648
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500649 if data.isolated_hash:
vadimsh232f5a82017-01-20 19:23:44 -0800650 isolated_stats = result['stats'].setdefault('isolated', {})
651 bundle, isolated_stats['download'] = fetch_and_map(
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500652 isolated_hash=data.isolated_hash,
653 storage=data.storage,
654 cache=data.isolate_cache,
vadimsh232f5a82017-01-20 19:23:44 -0800655 outdir=run_dir,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500656 use_symlinks=data.use_symlinks)
vadimsh232f5a82017-01-20 19:23:44 -0800657 change_tree_read_only(run_dir, bundle.read_only)
maruelabec63c2017-04-26 11:53:24 -0700658 # Inject the command
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500659 if not command and bundle.command:
660 command = bundle.command + data.extra_args
Marc-Antoine Rueld704a1f2017-10-31 10:51:23 -0400661 # Only set the relative directory if the isolated file specified a
662 # command, and no raw command was specified.
663 if bundle.relative_cwd:
664 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
maruelabec63c2017-04-26 11:53:24 -0700665
666 if not command:
667 # Handle this as a task failure, not an internal failure.
668 sys.stderr.write(
669 '<No command was specified!>\n'
670 '<Please secify a command when triggering your Swarming task>\n')
671 result['exit_code'] = 1
672 return result
nodirbe642ff2016-06-09 15:51:51 -0700673
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500674 if not cwd.startswith(run_dir):
675 # Handle this as a task failure, not an internal failure. This is a
676 # 'last chance' way to gate against directory escape.
677 sys.stderr.write('<Relative CWD is outside of run directory!>\n')
678 result['exit_code'] = 1
679 return result
680
681 if not os.path.isdir(cwd):
682 # Accepts relative_cwd that does not exist.
683 os.makedirs(cwd, 0700)
684
vadimsh232f5a82017-01-20 19:23:44 -0800685 # If we have an explicit list of files to return, make sure their
686 # directories exist now.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500687 if data.storage and data.outputs:
688 isolateserver.create_directories(run_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700689
vadimsh232f5a82017-01-20 19:23:44 -0800690 command = tools.fix_python_path(command)
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500691 command = process_command(command, out_dir, data.bot_file)
vadimsh232f5a82017-01-20 19:23:44 -0800692 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700693
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500694 with data.install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -0800695 sys.stdout.flush()
696 start = time.time()
697 try:
vadimsh9c54b2c2017-07-25 14:08:29 -0700698 # Need to switch the default account before 'get_command_env' call,
699 # so it can grab correct value of LUCI_CONTEXT env var.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500700 with set_luci_context_account(data.switch_to_account, tmp_dir):
701 env = get_command_env(
702 tmp_dir, cipd_info, cwd, data.env, data.env_prefix)
vadimsh9c54b2c2017-07-25 14:08:29 -0700703 result['exit_code'], result['had_hard_timeout'] = run_command(
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500704 command, cwd, env, data.hard_timeout, data.grace_period)
nodird6160682017-02-02 13:03:35 -0800705 finally:
706 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700707 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700708 # An internal error occurred. Report accordingly so the swarming task will
709 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700710 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700711 result['internal_failure'] = str(e)
712 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700713
714 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700715 finally:
716 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700717 # Try to link files to the output directory, if specified.
718 if out_dir:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500719 link_outputs_to_outdir(run_dir, out_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700720
nodir32a1ec12016-10-26 18:34:07 -0700721 success = False
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500722 if data.leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700723 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700724 logging.warning(
725 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700726 else:
maruel84537cb2015-10-16 14:21:28 -0700727 # On Windows rmtree(run_dir) call above has a synchronization effect: it
728 # finishes only when all task child processes terminate (since a running
729 # process locks *.exe file). Examine out_dir only after that call
730 # completes (since child processes may write to out_dir too and we need
731 # to wait for them to finish).
732 if fs.isdir(run_dir):
733 try:
734 success = file_path.rmtree(run_dir)
735 except OSError as e:
736 logging.error('Failure with %s', e)
737 success = False
738 if not success:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500739 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('run', data.grace_period))
maruel84537cb2015-10-16 14:21:28 -0700740 if result['exit_code'] == 0:
741 result['exit_code'] = 1
742 if fs.isdir(tmp_dir):
743 try:
744 success = file_path.rmtree(tmp_dir)
745 except OSError as e:
746 logging.error('Failure with %s', e)
747 success = False
748 if not success:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500749 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('temp', data.grace_period))
maruel84537cb2015-10-16 14:21:28 -0700750 if result['exit_code'] == 0:
751 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700752
marueleb5fbee2015-09-17 13:01:36 -0700753 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700754 if out_dir:
nodir55715712016-06-03 12:28:19 -0700755 isolated_stats = result['stats'].setdefault('isolated', {})
756 result['outputs_ref'], success, isolated_stats['upload'] = (
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500757 delete_and_upload(data.storage, out_dir, data.leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700758 if not success and result['exit_code'] == 0:
759 result['exit_code'] = 1
760 except Exception as e:
761 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700762 if out_dir:
763 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700764 result['internal_failure'] = str(e)
765 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500766
767
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500768def run_tha_test(data, result_json):
nodir55be77b2016-05-03 09:39:57 -0700769 """Runs an executable and records execution metadata.
770
nodir55be77b2016-05-03 09:39:57 -0700771 If isolated_hash is specified, downloads the dependencies in the cache,
772 hardlinks them into a temporary directory and runs the command specified in
773 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500774
775 A temporary directory is created to hold the output files. The content inside
776 this directory will be uploaded back to |storage| packaged as a .isolated
777 file.
778
779 Arguments:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500780 - data: TaskData instance.
781 - result_json: File path to dump result metadata into. If set, the process
782 exit code is always 0 unless an internal error occurred.
maruela9cfd6f2015-09-15 11:03:15 -0700783
784 Returns:
785 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000786 """
maruela76b9ee2015-12-15 06:18:08 -0800787 if result_json:
788 # Write a json output file right away in case we get killed.
789 result = {
790 'exit_code': None,
791 'had_hard_timeout': False,
792 'internal_failure': 'Was terminated before completion',
793 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700794 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800795 }
796 tools.write_json(result_json, result, dense=True)
797
maruela9cfd6f2015-09-15 11:03:15 -0700798 # run_isolated exit code. Depends on if result_json is used or not.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500799 result = map_and_run(data, True)
maruela9cfd6f2015-09-15 11:03:15 -0700800 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700801
maruela9cfd6f2015-09-15 11:03:15 -0700802 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700803 # We've found tests to delete 'work' when quitting, causing an exception
804 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700805 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700806 tools.write_json(result_json, result, dense=True)
807 # Only return 1 if there was an internal error.
808 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000809
maruela9cfd6f2015-09-15 11:03:15 -0700810 # Marshall into old-style inline output.
811 if result['outputs_ref']:
812 data = {
813 'hash': result['outputs_ref']['isolated'],
814 'namespace': result['outputs_ref']['namespace'],
815 'storage': result['outputs_ref']['isolatedserver'],
816 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500817 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700818 print(
819 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
820 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800821 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700822 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000823
824
iannuccib58d10d2017-03-18 02:00:25 -0700825# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800826CipdInfo = collections.namedtuple('CipdInfo', [
827 'client', # cipd.CipdClient object
828 'cache_dir', # absolute path to bot-global cipd tag and instance cache
829 'stats', # dict with stats to return to the server
830 'pins', # dict with installed cipd pins to return to the server
831])
832
833
834@contextlib.contextmanager
835def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700836 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800837 yield None
838
839
iannuccib58d10d2017-03-18 02:00:25 -0700840def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
841 """Calls 'cipd ensure' for packages.
842
843 Args:
844 run_dir (str): root of installation.
845 cipd_cache_dir (str): the directory to use for the cipd package cache.
846 client (CipdClient): the cipd client to use
847 packages: packages to install, list [(path, package_name, version), ...].
848 timeout: max duration in seconds that this function can take.
849
850 Returns: list of pinned packages. Looks like [
851 {
852 'path': 'subdirectory',
853 'package_name': 'resolved/package/name',
854 'version': 'deadbeef...',
855 },
856 ...
857 ]
858 """
859 package_pins = [None]*len(packages)
860 def insert_pin(path, name, version, idx):
861 package_pins[idx] = {
862 'package_name': name,
863 # swarming deals with 'root' as '.'
864 'path': path or '.',
865 'version': version,
866 }
867
868 by_path = collections.defaultdict(list)
869 for i, (path, name, version) in enumerate(packages):
870 # cipd deals with 'root' as ''
871 if path == '.':
872 path = ''
873 by_path[path].append((name, version, i))
874
875 pins = client.ensure(
876 run_dir,
877 {
878 subdir: [(name, vers) for name, vers, _ in pkgs]
879 for subdir, pkgs in by_path.iteritems()
880 },
881 cache_dir=cipd_cache_dir,
882 timeout=timeout,
883 )
884
885 for subdir, pin_list in sorted(pins.iteritems()):
886 this_subdir = by_path[subdir]
887 for i, (name, version) in enumerate(pin_list):
888 insert_pin(subdir, name, version, this_subdir[i][2])
889
Robert Iannucci461b30d2017-12-13 11:34:03 -0800890 assert None not in package_pins, (packages, pins, package_pins)
iannuccib58d10d2017-03-18 02:00:25 -0700891
892 return package_pins
893
894
vadimsh232f5a82017-01-20 19:23:44 -0800895@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700896def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700897 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800898 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800899 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700900
vadimsh232f5a82017-01-20 19:23:44 -0800901 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
902
903 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700904 [
905 {
906 "path": path, "package_name": package_name, "version": version,
907 },
908 ...
909 ]
vadimsh902948e2017-01-20 15:57:32 -0800910 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700911
912 such that they correspond 1:1 to all input package arguments from the command
913 line. These dictionaries make their all the way back to swarming, where they
914 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700915
vadimsh902948e2017-01-20 15:57:32 -0800916 If 'packages' list is empty, will bootstrap CIPD client, but won't install
917 any packages.
918
919 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800920 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800921
nodirbe642ff2016-06-09 15:51:51 -0700922 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700923 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800924 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700925 service_url (str): CIPD server url, e.g.
926 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700927 client_package_name (str): CIPD package name of CIPD client.
928 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700929 cache_dir (str): where to keep cache of cipd clients, packages and tags.
930 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700931 """
932 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700933
nodirbe642ff2016-06-09 15:51:51 -0700934 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700935 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700936
vadimsh902948e2017-01-20 15:57:32 -0800937 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800938 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700939 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800940 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700941
nodirbe642ff2016-06-09 15:51:51 -0700942 get_client_start = time.time()
943 client_manager = cipd.get_client(
944 service_url, client_package_name, client_version, cache_dir,
945 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700946
nodirbe642ff2016-06-09 15:51:51 -0700947 with client_manager as client:
948 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700949
iannuccib58d10d2017-03-18 02:00:25 -0700950 package_pins = []
951 if packages:
952 package_pins = _install_packages(
953 run_dir, cipd_cache_dir, client, packages, timeoutfn())
954
955 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700956
vadimsh232f5a82017-01-20 19:23:44 -0800957 total_duration = time.time() - start
958 logging.info(
959 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700960
vadimsh232f5a82017-01-20 19:23:44 -0800961 yield CipdInfo(
962 client=client,
963 cache_dir=cipd_cache_dir,
964 stats={
965 'duration': total_duration,
966 'get_client_duration': get_client_duration,
967 },
968 pins={
iannuccib58d10d2017-03-18 02:00:25 -0700969 'client_package': {
970 'package_name': client.package_name,
971 'version': client.instance_id,
972 },
vadimsh232f5a82017-01-20 19:23:44 -0800973 'packages': package_pins,
974 })
nodirbe642ff2016-06-09 15:51:51 -0700975
976
nodirf33b8d62016-10-26 22:34:58 -0700977def clean_caches(options, isolate_cache, named_cache_manager):
maruele6fc9382017-05-04 09:03:48 -0700978 """Trims isolated and named caches.
979
980 The goal here is to coherently trim both caches, deleting older items
981 independent of which container they belong to.
982 """
983 # TODO(maruel): Trim CIPD cache the same way.
984 total = 0
nodirf33b8d62016-10-26 22:34:58 -0700985 with named_cache_manager.open():
986 oldest_isolated = isolate_cache.get_oldest()
987 oldest_named = named_cache_manager.get_oldest()
988 trimmers = [
989 (
990 isolate_cache.trim,
991 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
992 ),
993 (
994 lambda: named_cache_manager.trim(options.min_free_space),
995 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
996 ),
997 ]
998 trimmers.sort(key=lambda (_, ts): ts)
maruele6fc9382017-05-04 09:03:48 -0700999 # TODO(maruel): This is incorrect, we want to trim 'items' that are strictly
1000 # the oldest independent of in which cache they live in. Right now, the
1001 # cache with the oldest item pays the price.
nodirf33b8d62016-10-26 22:34:58 -07001002 for trim, _ in trimmers:
maruele6fc9382017-05-04 09:03:48 -07001003 total += trim()
nodirf33b8d62016-10-26 22:34:58 -07001004 isolate_cache.cleanup()
maruele6fc9382017-05-04 09:03:48 -07001005 return total
nodirf33b8d62016-10-26 22:34:58 -07001006
1007
nodirbe642ff2016-06-09 15:51:51 -07001008def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001009 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -07001010 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +00001011 version=__version__,
1012 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -07001013 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -07001014 '--clean', action='store_true',
1015 help='Cleans the cache, trimming it necessary and remove corrupted items '
1016 'and returns without executing anything; use with -v to know what '
1017 'was done')
1018 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -07001019 '--no-clean', action='store_true',
1020 help='Do not clean the cache automatically on startup. This is meant for '
1021 'bots where a separate execution with --clean was done earlier so '
1022 'doing it again is redundant')
1023 parser.add_option(
maruel4409e302016-07-19 14:25:51 -07001024 '--use-symlinks', action='store_true',
1025 help='Use symlinks instead of hardlinks')
1026 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -07001027 '--json',
1028 help='dump output metadata to json file. When used, run_isolated returns '
1029 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -07001030 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001031 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -07001032 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001033 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -07001034 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -07001035 parser.add_option(
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001036 '--raw-cmd', action='store_true',
1037 help='Ignore the isolated command, use the one supplied at the command '
1038 'line')
1039 parser.add_option(
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001040 '--relative-cwd',
1041 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
1042 'requires --raw-cmd')
1043 parser.add_option(
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001044 '--env', default=[], action='append',
1045 help='Environment variables to set for the child process')
1046 parser.add_option(
1047 '--env-prefix', default=[], action='append',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001048 help='Specify a VAR=./path/fragment to put in the environment variable '
1049 'before executing the command. The path fragment must be relative '
1050 'to the isolated run directory, and must not contain a `..` token. '
1051 'The path will be made absolute and prepended to the indicated '
1052 '$VAR using the OS\'s path separator. Multiple items for the same '
1053 '$VAR will be prepended in order.')
1054 parser.add_option(
bpastene3ae09522016-06-10 17:12:59 -07001055 '--bot-file',
1056 help='Path to a file describing the state of the host. The content is '
1057 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -07001058 parser.add_option(
vadimsh9c54b2c2017-07-25 14:08:29 -07001059 '--switch-to-account',
1060 help='If given, switches LUCI_CONTEXT to given logical service account '
1061 '(e.g. "task" or "system") before launching the isolated process.')
1062 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -07001063 '--output', action='append',
1064 help='Specifies an output to return. If no outputs are specified, all '
1065 'files located in $(ISOLATED_OUTDIR) will be returned; '
1066 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
1067 'specified by --output option (there can be multiple) will be '
1068 'returned. Note that if a file in OUT_DIR has the same path '
1069 'as an --output option, the --output version will be returned.')
1070 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -07001071 '-a', '--argsfile',
1072 # This is actually handled in parse_args; it's included here purely so it
1073 # can make it into the help text.
1074 help='Specify a file containing a JSON array of arguments to this '
1075 'script. If --argsfile is provided, no other argument may be '
1076 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001077 data_group = optparse.OptionGroup(parser, 'Data source')
1078 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001079 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -07001080 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001081 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001082 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001083
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -04001084 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001085
1086 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -07001087 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001088
Kenneth Russell61d42352014-09-15 11:41:16 -07001089 debug_group = optparse.OptionGroup(parser, 'Debugging')
1090 debug_group.add_option(
1091 '--leak-temp-dir',
1092 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -07001093 help='Deliberately leak isolate\'s temp dir for later examination. '
1094 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -07001095 debug_group.add_option(
1096 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -07001097 parser.add_option_group(debug_group)
1098
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001099 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001100
nodirf33b8d62016-10-26 22:34:58 -07001101 parser.set_defaults(
1102 cache='cache',
1103 cipd_cache='cipd_cache',
1104 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -07001105 return parser
1106
1107
aludwin7556e0c2016-10-26 08:46:10 -07001108def parse_args(args):
1109 # Create a fake mini-parser just to get out the "-a" command. Note that
1110 # it's not documented here; instead, it's documented in create_option_parser
1111 # even though that parser will never actually get to parse it. This is
1112 # because --argsfile is exclusive with all other options and arguments.
1113 file_argparse = argparse.ArgumentParser(add_help=False)
1114 file_argparse.add_argument('-a', '--argsfile')
1115 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
1116 if file_args.argsfile:
1117 if nonfile_args:
1118 file_argparse.error('Can\'t specify --argsfile with'
1119 'any other arguments (%s)' % nonfile_args)
1120 try:
1121 with open(file_args.argsfile, 'r') as f:
1122 args = json.load(f)
1123 except (IOError, OSError, ValueError) as e:
1124 # We don't need to error out here - "args" is now empty,
1125 # so the call below to parser.parse_args(args) will fail
1126 # and print the full help text.
1127 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
1128
1129 # Even if we failed to read the args, just call the normal parser now since it
1130 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -07001131 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001132 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -07001133 return (parser, options, args)
1134
1135
1136def main(args):
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -05001137 # Warning: when --argsfile is used, the strings are unicode instances, when
1138 # parsed normally, the strings are str instances.
aludwin7556e0c2016-10-26 08:46:10 -07001139 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -07001140
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001141 if not file_path.enable_symlink():
1142 logging.error('Symlink support is not enabled')
1143
nodirf33b8d62016-10-26 22:34:58 -07001144 isolate_cache = isolateserver.process_cache_options(options, trim=False)
1145 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -07001146 if options.clean:
1147 if options.isolated:
1148 parser.error('Can\'t use --isolated with --clean.')
1149 if options.isolate_server:
1150 parser.error('Can\'t use --isolate-server with --clean.')
1151 if options.json:
1152 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -07001153 if options.named_caches:
1154 parser.error('Can\t use --named-cache with --clean.')
1155 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001156 return 0
nodirf33b8d62016-10-26 22:34:58 -07001157
maruel2e8d0f52016-07-16 07:51:29 -07001158 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -07001159 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001160
nodir55be77b2016-05-03 09:39:57 -07001161 if not options.isolated and not args:
1162 parser.error('--isolated or command to run is required.')
1163
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001164 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001165
1166 isolateserver.process_isolate_server_options(
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001167 parser, options, True, False)
nodir55be77b2016-05-03 09:39:57 -07001168 if not options.isolate_server:
1169 if options.isolated:
1170 parser.error('--isolated requires --isolate-server')
1171 if ISOLATED_OUTDIR_PARAMETER in args:
1172 parser.error(
1173 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001174
nodir90bc8dc2016-06-15 13:35:21 -07001175 if options.root_dir:
1176 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -07001177 if options.json:
1178 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001179
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001180 if any('=' not in i for i in options.env):
1181 parser.error(
1182 '--env required key=value form. value can be skipped to delete '
1183 'the variable')
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001184 options.env = dict(i.split('=', 1) for i in options.env)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001185
1186 prefixes = {}
1187 cwd = os.path.realpath(os.getcwd())
1188 for item in options.env_prefix:
1189 if '=' not in item:
1190 parser.error(
1191 '--env-prefix %r is malformed, must be in the form `VAR=./path`'
1192 % item)
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001193 key, opath = item.split('=', 1)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001194 if os.path.isabs(opath):
1195 parser.error('--env-prefix %r path is bad, must be relative.' % opath)
1196 opath = os.path.normpath(opath)
1197 if not os.path.realpath(os.path.join(cwd, opath)).startswith(cwd):
1198 parser.error(
1199 '--env-prefix %r path is bad, must be relative and not contain `..`.'
1200 % opath)
1201 prefixes.setdefault(key, []).append(opath)
1202 options.env_prefix = prefixes
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001203
nodirbe642ff2016-06-09 15:51:51 -07001204 cipd.validate_cipd_options(parser, options)
1205
vadimsh232f5a82017-01-20 19:23:44 -08001206 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -08001207 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -07001208 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -08001209 run_dir, cipd.parse_package_args(options.cipd_packages),
1210 options.cipd_server, options.cipd_client_package,
1211 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001212
nodird6160682017-02-02 13:03:35 -08001213 @contextlib.contextmanager
nodir0ae98b32017-05-11 13:21:53 -07001214 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001215 # WARNING: this function depends on "options" variable defined in the outer
1216 # function.
nodir0ae98b32017-05-11 13:21:53 -07001217 caches = [
1218 (os.path.join(run_dir, unicode(relpath)), name)
1219 for name, relpath in options.named_caches
1220 ]
nodirf33b8d62016-10-26 22:34:58 -07001221 with named_cache_manager.open():
nodir0ae98b32017-05-11 13:21:53 -07001222 for path, name in caches:
1223 named_cache_manager.install(path, name)
nodird6160682017-02-02 13:03:35 -08001224 try:
1225 yield
1226 finally:
dnje289d132017-07-07 11:16:44 -07001227 # Uninstall each named cache, returning it to the cache pool. If an
1228 # uninstall fails for a given cache, it will remain in the task's
1229 # temporary space, get cleaned up by the Swarming bot, and be lost.
1230 #
1231 # If the Swarming bot cannot clean up the cache, it will handle it like
1232 # any other bot file that could not be removed.
nodir0ae98b32017-05-11 13:21:53 -07001233 with named_cache_manager.open():
1234 for path, name in caches:
dnje289d132017-07-07 11:16:44 -07001235 try:
1236 named_cache_manager.uninstall(path, name)
1237 except named_cache.Error:
1238 logging.exception('Error while removing named cache %r at %r. '
1239 'The cache will be lost.', path, name)
nodirf33b8d62016-10-26 22:34:58 -07001240
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001241 extra_args = []
1242 command = []
1243 if options.raw_cmd:
1244 command = args
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001245 if options.relative_cwd:
1246 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1247 if not a.startswith(os.getcwd()):
1248 parser.error(
1249 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001250 else:
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001251 if options.relative_cwd:
1252 parser.error('--relative-cwd requires --raw-cmd')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001253 extra_args = args
1254
1255 data = TaskData(
1256 command=command,
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001257 relative_cwd=options.relative_cwd,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001258 extra_args=extra_args,
1259 isolated_hash=options.isolated,
1260 storage=None,
1261 isolate_cache=isolate_cache,
1262 outputs=options.output,
1263 install_named_caches=install_named_caches,
1264 leak_temp_dir=options.leak_temp_dir,
1265 root_dir=_to_unicode(options.root_dir),
1266 hard_timeout=options.hard_timeout,
1267 grace_period=options.grace_period,
1268 bot_file=options.bot_file,
1269 switch_to_account=options.switch_to_account,
1270 install_packages_fn=install_packages_fn,
1271 use_symlinks=options.use_symlinks,
1272 env=options.env,
1273 env_prefix=options.env_prefix)
nodirbe642ff2016-06-09 15:51:51 -07001274 try:
nodir90bc8dc2016-06-15 13:35:21 -07001275 if options.isolate_server:
1276 storage = isolateserver.get_storage(
1277 options.isolate_server, options.namespace)
1278 with storage:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001279 data = data._replace(storage=storage)
nodirf33b8d62016-10-26 22:34:58 -07001280 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
1281 assert storage.hash_algo == isolate_cache.hash_algo
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001282 return run_tha_test(data, options.json)
1283 return run_tha_test(data, options.json)
nodirf33b8d62016-10-26 22:34:58 -07001284 except (cipd.Error, named_cache.Error) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001285 print >> sys.stderr, ex.message
1286 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001287
1288
1289if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001290 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001291 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001292 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001293 sys.exit(main(sys.argv[1:]))