blob: cc0ed47716886e617c19e45cee55f7208657c3ac [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 Ruel9ec1e9f2017-12-20 16:36:54 -0500356def get_command_env(tmp_dir, cipd_info, run_dir, 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.
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500365 run_dir: The root directory the isolated tree is mapped 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:
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500372 out.pop(k, None)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500373 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():
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500382 assert isinstance(paths, list), paths
383 paths = [os.path.normpath(os.path.join(run_dir, p)) for p in paths]
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500384 cur = out.get(key)
385 if cur:
386 paths.append(cur)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500387 out[key] = _to_str(os.path.pathsep.join(paths))
vadimsh232f5a82017-01-20 19:23:44 -0800388
iannucciac0342c2017-02-24 05:47:01 -0800389 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
iannucci460def72017-02-24 10:49:48 -0800390 # * mktemp on linux respects $TMPDIR, not $TMP
391 # * mktemp on OS X SOMETIMES respects $TMPDIR
iannucciac0342c2017-02-24 05:47:01 -0800392 # * chromium's base utils respects $TMPDIR on linux, $TEMP on windows.
393 # Unfortunately at the time of writing it completely ignores all envvars
394 # on OS X.
iannucci460def72017-02-24 10:49:48 -0800395 # * python respects TMPDIR, TEMP, and TMP (regardless of platform)
396 # * golang respects TMPDIR on linux+mac, TEMP on windows.
iannucciac0342c2017-02-24 05:47:01 -0800397 key = {'win32': 'TEMP'}.get(sys.platform, 'TMPDIR')
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500398 out[key] = _to_str(tmp_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800399
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500400 return out
vadimsh232f5a82017-01-20 19:23:44 -0800401
402
403def run_command(command, cwd, env, hard_timeout, grace_period):
maruel6be7f9e2015-10-01 12:25:30 -0700404 """Runs the command.
405
406 Returns:
407 tuple(process exit code, bool if had a hard timeout)
408 """
maruela9cfd6f2015-09-15 11:03:15 -0700409 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700410
maruel6be7f9e2015-10-01 12:25:30 -0700411 exit_code = None
412 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700413 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700414 proc = None
415 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700416 try:
maruel6be7f9e2015-10-01 12:25:30 -0700417 # TODO(maruel): This code is imperfect. It doesn't handle well signals
418 # during the download phase and there's short windows were things can go
419 # wrong.
420 def handler(signum, _frame):
421 if proc and not had_signal:
422 logging.info('Received signal %d', signum)
423 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700424 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700425
426 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
427 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
428 try:
429 exit_code = proc.wait(hard_timeout or None)
430 except subprocess42.TimeoutExpired:
431 if not had_signal:
432 logging.warning('Hard timeout')
433 had_hard_timeout = True
434 logging.warning('Sending SIGTERM')
435 proc.terminate()
436
437 # Ignore signals in grace period. Forcibly give the grace period to the
438 # child process.
439 if exit_code is None:
440 ignore = lambda *_: None
441 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
442 try:
443 exit_code = proc.wait(grace_period or None)
444 except subprocess42.TimeoutExpired:
445 # Now kill for real. The user can distinguish between the
446 # following states:
447 # - signal but process exited within grace period,
448 # hard_timed_out will be set but the process exit code will be
449 # script provided.
450 # - processed exited late, exit code will be -9 on posix.
451 logging.warning('Grace exhausted; sending SIGKILL')
452 proc.kill()
martiniss5c8043e2017-08-01 17:09:43 -0700453 logging.info('Waiting for process exit')
maruel6be7f9e2015-10-01 12:25:30 -0700454 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700455 except OSError:
456 # This is not considered to be an internal error. The executable simply
457 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800458 sys.stderr.write(
459 '<The executable does not exist or a dependent library is missing>\n'
460 '<Check for missing .so/.dll in the .isolate or GN file>\n'
461 '<Command: %s>\n' % command)
462 if os.environ.get('SWARMING_TASK_ID'):
463 # Give an additional hint when running as a swarming task.
464 sys.stderr.write(
465 '<See the task\'s page for commands to help diagnose this issue '
466 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700467 exit_code = 1
468 logging.info(
469 'Command finished with exit code %d (%s)',
470 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700471 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700472
473
maruel4409e302016-07-19 14:25:51 -0700474def fetch_and_map(isolated_hash, storage, cache, outdir, use_symlinks):
475 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700476 start = time.time()
477 bundle = isolateserver.fetch_isolated(
478 isolated_hash=isolated_hash,
479 storage=storage,
480 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700481 outdir=outdir,
482 use_symlinks=use_symlinks)
nodir6f801882016-04-29 14:41:50 -0700483 return bundle, {
484 'duration': time.time() - start,
485 'initial_number_items': cache.initial_number_items,
486 'initial_size': cache.initial_size,
487 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
488 'items_hot': base64.b64encode(
tansell9e04a8d2016-07-28 09:31:59 -0700489 large.pack(sorted(set(cache.used) - set(cache.added)))),
nodir6f801882016-04-29 14:41:50 -0700490 }
491
492
aludwin0a8e17d2016-10-27 15:57:39 -0700493def link_outputs_to_outdir(run_dir, out_dir, outputs):
494 """Links any named outputs to out_dir so they can be uploaded.
495
496 Raises an error if the file already exists in that directory.
497 """
498 if not outputs:
499 return
500 isolateserver.create_directories(out_dir, outputs)
501 for o in outputs:
502 try:
aludwinf31ab802017-06-12 06:03:00 -0700503 infile = os.path.join(run_dir, o)
504 outfile = os.path.join(out_dir, o)
505 if fs.islink(infile):
506 # TODO(aludwin): handle directories
507 fs.copy2(infile, outfile)
508 else:
509 file_path.link_file(outfile, infile, file_path.HARDLINK_WITH_FALLBACK)
aludwin0a8e17d2016-10-27 15:57:39 -0700510 except OSError as e:
aludwin81178302016-11-30 17:18:49 -0800511 logging.info("Couldn't collect output file %s: %s", o, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700512
513
maruela9cfd6f2015-09-15 11:03:15 -0700514def delete_and_upload(storage, out_dir, leak_temp_dir):
515 """Deletes the temporary run directory and uploads results back.
516
517 Returns:
nodir6f801882016-04-29 14:41:50 -0700518 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700519 - outputs_ref: a dict referring to the results archived back to the isolated
520 server, if applicable.
521 - success: False if something occurred that means that the task must
522 forcibly be considered a failure, e.g. zombie processes were left
523 behind.
nodir6f801882016-04-29 14:41:50 -0700524 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700525 """
maruela9cfd6f2015-09-15 11:03:15 -0700526 # Upload out_dir and generate a .isolated file out of this directory. It is
527 # only done if files were written in the directory.
528 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700529 cold = []
530 hot = []
nodir6f801882016-04-29 14:41:50 -0700531 start = time.time()
532
maruel12e30012015-10-09 11:55:35 -0700533 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700534 with tools.Profiler('ArchiveOutput'):
535 try:
maruel064c0a32016-04-05 11:47:15 -0700536 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700537 storage, [out_dir], None)
538 outputs_ref = {
539 'isolated': results[0][0],
540 'isolatedserver': storage.location,
541 'namespace': storage.namespace,
542 }
maruel064c0a32016-04-05 11:47:15 -0700543 cold = sorted(i.size for i in f_cold)
544 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700545 except isolateserver.Aborted:
546 # This happens when a signal SIGTERM was received while uploading data.
547 # There is 2 causes:
548 # - The task was too slow and was about to be killed anyway due to
549 # exceeding the hard timeout.
550 # - The amount of data uploaded back is very large and took too much
551 # time to archive.
552 sys.stderr.write('Received SIGTERM while uploading')
553 # Re-raise, so it will be treated as an internal failure.
554 raise
nodir6f801882016-04-29 14:41:50 -0700555
556 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700557 try:
maruel12e30012015-10-09 11:55:35 -0700558 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700559 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700560 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700561 else:
562 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700563 except OSError as e:
564 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700565 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700566 stats = {
567 'duration': time.time() - start,
568 'items_cold': base64.b64encode(large.pack(cold)),
569 'items_hot': base64.b64encode(large.pack(hot)),
570 }
571 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700572
573
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500574def map_and_run(data, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700575 """Runs a command with optional isolated input/output.
576
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500577 Arguments:
578 - data: TaskData instance.
579 - constant_run_path: TODO
nodir55be77b2016-05-03 09:39:57 -0700580
581 Returns metadata about the result.
582 """
maruela9cfd6f2015-09-15 11:03:15 -0700583 result = {
maruel064c0a32016-04-05 11:47:15 -0700584 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700585 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700586 'had_hard_timeout': False,
Seth Koehler49139812017-12-19 13:59:33 -0500587 'internal_failure': 'run_isolated did not complete properly',
maruel064c0a32016-04-05 11:47:15 -0700588 'stats': {
nodir55715712016-06-03 12:28:19 -0700589 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700590 # 'cipd': {
591 # 'duration': 0.,
592 # 'get_client_duration': 0.,
593 # },
nodir55715712016-06-03 12:28:19 -0700594 # 'download': {
595 # 'duration': 0.,
596 # 'initial_number_items': 0,
597 # 'initial_size': 0,
598 # 'items_cold': '<large.pack()>',
599 # 'items_hot': '<large.pack()>',
600 # },
601 # 'upload': {
602 # 'duration': 0.,
603 # 'items_cold': '<large.pack()>',
604 # 'items_hot': '<large.pack()>',
605 # },
maruel064c0a32016-04-05 11:47:15 -0700606 # },
607 },
iannucci96fcccc2016-08-30 15:52:22 -0700608 # 'cipd_pins': {
609 # 'packages': [
610 # {'package_name': ..., 'version': ..., 'path': ...},
611 # ...
612 # ],
613 # 'client_package': {'package_name': ..., 'version': ...},
614 # },
maruela9cfd6f2015-09-15 11:03:15 -0700615 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700616 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700617 }
nodirbe642ff2016-06-09 15:51:51 -0700618
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500619 if data.root_dir:
620 file_path.ensure_tree(data.root_dir, 0700)
621 elif data.isolate_cache.cache_dir:
622 data = data._replace(
623 root_dir=os.path.dirname(data.isolate_cache.cache_dir))
maruele2f2cb82016-07-13 14:41:03 -0700624 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700625 # If root_dir is not specified, it is not constant.
626 # TODO(maruel): This is not obvious. Change this to become an error once we
627 # make the constant_run_path an exposed flag.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500628 if constant_run_path and data.root_dir:
629 run_dir = os.path.join(data.root_dir, ISOLATED_RUN_DIR)
maruel5c4eed82017-05-26 05:33:40 -0700630 if os.path.isdir(run_dir):
631 file_path.rmtree(run_dir)
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500632 os.mkdir(run_dir, 0700)
maruelcffa0542017-04-07 08:39:20 -0700633 else:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500634 run_dir = make_temp_dir(ISOLATED_RUN_DIR, data.root_dir)
maruel03e11842016-07-14 10:50:16 -0700635 # storage should be normally set but don't crash if it is not. This can happen
636 # as Swarming task can run without an isolate server.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500637 out_dir = make_temp_dir(
638 ISOLATED_OUT_DIR, data.root_dir) if data.storage else None
639 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, data.root_dir)
nodir55be77b2016-05-03 09:39:57 -0700640 cwd = run_dir
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500641 if data.relative_cwd:
642 cwd = os.path.normpath(os.path.join(cwd, data.relative_cwd))
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500643 command = data.command
nodir55be77b2016-05-03 09:39:57 -0700644 try:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500645 with data.install_packages_fn(run_dir) as cipd_info:
vadimsh232f5a82017-01-20 19:23:44 -0800646 if cipd_info:
647 result['stats']['cipd'] = cipd_info.stats
648 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700649
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500650 if data.isolated_hash:
vadimsh232f5a82017-01-20 19:23:44 -0800651 isolated_stats = result['stats'].setdefault('isolated', {})
652 bundle, isolated_stats['download'] = fetch_and_map(
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500653 isolated_hash=data.isolated_hash,
654 storage=data.storage,
655 cache=data.isolate_cache,
vadimsh232f5a82017-01-20 19:23:44 -0800656 outdir=run_dir,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500657 use_symlinks=data.use_symlinks)
vadimsh232f5a82017-01-20 19:23:44 -0800658 change_tree_read_only(run_dir, bundle.read_only)
maruelabec63c2017-04-26 11:53:24 -0700659 # Inject the command
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500660 if not command and bundle.command:
661 command = bundle.command + data.extra_args
Marc-Antoine Rueld704a1f2017-10-31 10:51:23 -0400662 # Only set the relative directory if the isolated file specified a
663 # command, and no raw command was specified.
664 if bundle.relative_cwd:
665 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
maruelabec63c2017-04-26 11:53:24 -0700666
667 if not command:
668 # Handle this as a task failure, not an internal failure.
669 sys.stderr.write(
670 '<No command was specified!>\n'
671 '<Please secify a command when triggering your Swarming task>\n')
672 result['exit_code'] = 1
673 return result
nodirbe642ff2016-06-09 15:51:51 -0700674
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500675 if not cwd.startswith(run_dir):
676 # Handle this as a task failure, not an internal failure. This is a
677 # 'last chance' way to gate against directory escape.
678 sys.stderr.write('<Relative CWD is outside of run directory!>\n')
679 result['exit_code'] = 1
680 return result
681
682 if not os.path.isdir(cwd):
683 # Accepts relative_cwd that does not exist.
684 os.makedirs(cwd, 0700)
685
vadimsh232f5a82017-01-20 19:23:44 -0800686 # If we have an explicit list of files to return, make sure their
687 # directories exist now.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500688 if data.storage and data.outputs:
689 isolateserver.create_directories(run_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700690
vadimsh232f5a82017-01-20 19:23:44 -0800691 command = tools.fix_python_path(command)
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500692 command = process_command(command, out_dir, data.bot_file)
vadimsh232f5a82017-01-20 19:23:44 -0800693 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700694
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500695 with data.install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -0800696 sys.stdout.flush()
697 start = time.time()
698 try:
vadimsh9c54b2c2017-07-25 14:08:29 -0700699 # Need to switch the default account before 'get_command_env' call,
700 # so it can grab correct value of LUCI_CONTEXT env var.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500701 with set_luci_context_account(data.switch_to_account, tmp_dir):
702 env = get_command_env(
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500703 tmp_dir, cipd_info, run_dir, data.env, data.env_prefix)
vadimsh9c54b2c2017-07-25 14:08:29 -0700704 result['exit_code'], result['had_hard_timeout'] = run_command(
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500705 command, cwd, env, data.hard_timeout, data.grace_period)
nodird6160682017-02-02 13:03:35 -0800706 finally:
707 result['duration'] = max(time.time() - start, 0)
Seth Koehler49139812017-12-19 13:59:33 -0500708
709 # We successfully ran the command, set internal_failure back to
710 # None (even if the command failed, it's not an internal error).
711 result['internal_failure'] = None
maruela9cfd6f2015-09-15 11:03:15 -0700712 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700713 # An internal error occurred. Report accordingly so the swarming task will
714 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700715 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700716 result['internal_failure'] = str(e)
717 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700718
719 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700720 finally:
721 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700722 # Try to link files to the output directory, if specified.
723 if out_dir:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500724 link_outputs_to_outdir(run_dir, out_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700725
nodir32a1ec12016-10-26 18:34:07 -0700726 success = False
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500727 if data.leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700728 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700729 logging.warning(
730 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700731 else:
maruel84537cb2015-10-16 14:21:28 -0700732 # On Windows rmtree(run_dir) call above has a synchronization effect: it
733 # finishes only when all task child processes terminate (since a running
734 # process locks *.exe file). Examine out_dir only after that call
735 # completes (since child processes may write to out_dir too and we need
736 # to wait for them to finish).
737 if fs.isdir(run_dir):
738 try:
739 success = file_path.rmtree(run_dir)
740 except OSError as e:
741 logging.error('Failure with %s', e)
742 success = False
743 if not success:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500744 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('run', data.grace_period))
maruel84537cb2015-10-16 14:21:28 -0700745 if result['exit_code'] == 0:
746 result['exit_code'] = 1
747 if fs.isdir(tmp_dir):
748 try:
749 success = file_path.rmtree(tmp_dir)
750 except OSError as e:
751 logging.error('Failure with %s', e)
752 success = False
753 if not success:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500754 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('temp', data.grace_period))
maruel84537cb2015-10-16 14:21:28 -0700755 if result['exit_code'] == 0:
756 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700757
marueleb5fbee2015-09-17 13:01:36 -0700758 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700759 if out_dir:
nodir55715712016-06-03 12:28:19 -0700760 isolated_stats = result['stats'].setdefault('isolated', {})
761 result['outputs_ref'], success, isolated_stats['upload'] = (
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500762 delete_and_upload(data.storage, out_dir, data.leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700763 if not success and result['exit_code'] == 0:
764 result['exit_code'] = 1
765 except Exception as e:
766 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700767 if out_dir:
768 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700769 result['internal_failure'] = str(e)
770 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500771
772
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500773def run_tha_test(data, result_json):
nodir55be77b2016-05-03 09:39:57 -0700774 """Runs an executable and records execution metadata.
775
nodir55be77b2016-05-03 09:39:57 -0700776 If isolated_hash is specified, downloads the dependencies in the cache,
777 hardlinks them into a temporary directory and runs the command specified in
778 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500779
780 A temporary directory is created to hold the output files. The content inside
781 this directory will be uploaded back to |storage| packaged as a .isolated
782 file.
783
784 Arguments:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500785 - data: TaskData instance.
786 - result_json: File path to dump result metadata into. If set, the process
787 exit code is always 0 unless an internal error occurred.
maruela9cfd6f2015-09-15 11:03:15 -0700788
789 Returns:
790 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000791 """
maruela76b9ee2015-12-15 06:18:08 -0800792 if result_json:
793 # Write a json output file right away in case we get killed.
794 result = {
795 'exit_code': None,
796 'had_hard_timeout': False,
797 'internal_failure': 'Was terminated before completion',
798 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700799 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800800 }
801 tools.write_json(result_json, result, dense=True)
802
maruela9cfd6f2015-09-15 11:03:15 -0700803 # run_isolated exit code. Depends on if result_json is used or not.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500804 result = map_and_run(data, True)
maruela9cfd6f2015-09-15 11:03:15 -0700805 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700806
maruela9cfd6f2015-09-15 11:03:15 -0700807 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700808 # We've found tests to delete 'work' when quitting, causing an exception
809 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700810 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700811 tools.write_json(result_json, result, dense=True)
812 # Only return 1 if there was an internal error.
813 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000814
maruela9cfd6f2015-09-15 11:03:15 -0700815 # Marshall into old-style inline output.
816 if result['outputs_ref']:
817 data = {
818 'hash': result['outputs_ref']['isolated'],
819 'namespace': result['outputs_ref']['namespace'],
820 'storage': result['outputs_ref']['isolatedserver'],
821 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500822 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700823 print(
824 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
825 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800826 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700827 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000828
829
iannuccib58d10d2017-03-18 02:00:25 -0700830# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800831CipdInfo = collections.namedtuple('CipdInfo', [
832 'client', # cipd.CipdClient object
833 'cache_dir', # absolute path to bot-global cipd tag and instance cache
834 'stats', # dict with stats to return to the server
835 'pins', # dict with installed cipd pins to return to the server
836])
837
838
839@contextlib.contextmanager
840def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700841 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800842 yield None
843
844
iannuccib58d10d2017-03-18 02:00:25 -0700845def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
846 """Calls 'cipd ensure' for packages.
847
848 Args:
849 run_dir (str): root of installation.
850 cipd_cache_dir (str): the directory to use for the cipd package cache.
851 client (CipdClient): the cipd client to use
852 packages: packages to install, list [(path, package_name, version), ...].
853 timeout: max duration in seconds that this function can take.
854
855 Returns: list of pinned packages. Looks like [
856 {
857 'path': 'subdirectory',
858 'package_name': 'resolved/package/name',
859 'version': 'deadbeef...',
860 },
861 ...
862 ]
863 """
864 package_pins = [None]*len(packages)
865 def insert_pin(path, name, version, idx):
866 package_pins[idx] = {
867 'package_name': name,
868 # swarming deals with 'root' as '.'
869 'path': path or '.',
870 'version': version,
871 }
872
873 by_path = collections.defaultdict(list)
874 for i, (path, name, version) in enumerate(packages):
875 # cipd deals with 'root' as ''
876 if path == '.':
877 path = ''
878 by_path[path].append((name, version, i))
879
880 pins = client.ensure(
881 run_dir,
882 {
883 subdir: [(name, vers) for name, vers, _ in pkgs]
884 for subdir, pkgs in by_path.iteritems()
885 },
886 cache_dir=cipd_cache_dir,
887 timeout=timeout,
888 )
889
890 for subdir, pin_list in sorted(pins.iteritems()):
891 this_subdir = by_path[subdir]
892 for i, (name, version) in enumerate(pin_list):
893 insert_pin(subdir, name, version, this_subdir[i][2])
894
Robert Iannucci461b30d2017-12-13 11:34:03 -0800895 assert None not in package_pins, (packages, pins, package_pins)
iannuccib58d10d2017-03-18 02:00:25 -0700896
897 return package_pins
898
899
vadimsh232f5a82017-01-20 19:23:44 -0800900@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700901def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700902 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800903 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800904 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700905
vadimsh232f5a82017-01-20 19:23:44 -0800906 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
907
908 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700909 [
910 {
911 "path": path, "package_name": package_name, "version": version,
912 },
913 ...
914 ]
vadimsh902948e2017-01-20 15:57:32 -0800915 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700916
917 such that they correspond 1:1 to all input package arguments from the command
918 line. These dictionaries make their all the way back to swarming, where they
919 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700920
vadimsh902948e2017-01-20 15:57:32 -0800921 If 'packages' list is empty, will bootstrap CIPD client, but won't install
922 any packages.
923
924 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800925 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800926
nodirbe642ff2016-06-09 15:51:51 -0700927 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700928 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800929 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700930 service_url (str): CIPD server url, e.g.
931 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700932 client_package_name (str): CIPD package name of CIPD client.
933 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700934 cache_dir (str): where to keep cache of cipd clients, packages and tags.
935 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700936 """
937 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700938
nodirbe642ff2016-06-09 15:51:51 -0700939 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700940 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700941
vadimsh902948e2017-01-20 15:57:32 -0800942 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800943 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700944 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800945 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700946
nodirbe642ff2016-06-09 15:51:51 -0700947 get_client_start = time.time()
948 client_manager = cipd.get_client(
949 service_url, client_package_name, client_version, cache_dir,
950 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700951
nodirbe642ff2016-06-09 15:51:51 -0700952 with client_manager as client:
953 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700954
iannuccib58d10d2017-03-18 02:00:25 -0700955 package_pins = []
956 if packages:
957 package_pins = _install_packages(
958 run_dir, cipd_cache_dir, client, packages, timeoutfn())
959
960 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700961
vadimsh232f5a82017-01-20 19:23:44 -0800962 total_duration = time.time() - start
963 logging.info(
964 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700965
vadimsh232f5a82017-01-20 19:23:44 -0800966 yield CipdInfo(
967 client=client,
968 cache_dir=cipd_cache_dir,
969 stats={
970 'duration': total_duration,
971 'get_client_duration': get_client_duration,
972 },
973 pins={
iannuccib58d10d2017-03-18 02:00:25 -0700974 'client_package': {
975 'package_name': client.package_name,
976 'version': client.instance_id,
977 },
vadimsh232f5a82017-01-20 19:23:44 -0800978 'packages': package_pins,
979 })
nodirbe642ff2016-06-09 15:51:51 -0700980
981
nodirf33b8d62016-10-26 22:34:58 -0700982def clean_caches(options, isolate_cache, named_cache_manager):
maruele6fc9382017-05-04 09:03:48 -0700983 """Trims isolated and named caches.
984
985 The goal here is to coherently trim both caches, deleting older items
986 independent of which container they belong to.
987 """
988 # TODO(maruel): Trim CIPD cache the same way.
989 total = 0
nodirf33b8d62016-10-26 22:34:58 -0700990 with named_cache_manager.open():
991 oldest_isolated = isolate_cache.get_oldest()
992 oldest_named = named_cache_manager.get_oldest()
993 trimmers = [
994 (
995 isolate_cache.trim,
996 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
997 ),
998 (
999 lambda: named_cache_manager.trim(options.min_free_space),
1000 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
1001 ),
1002 ]
1003 trimmers.sort(key=lambda (_, ts): ts)
maruele6fc9382017-05-04 09:03:48 -07001004 # TODO(maruel): This is incorrect, we want to trim 'items' that are strictly
1005 # the oldest independent of in which cache they live in. Right now, the
1006 # cache with the oldest item pays the price.
nodirf33b8d62016-10-26 22:34:58 -07001007 for trim, _ in trimmers:
maruele6fc9382017-05-04 09:03:48 -07001008 total += trim()
nodirf33b8d62016-10-26 22:34:58 -07001009 isolate_cache.cleanup()
maruele6fc9382017-05-04 09:03:48 -07001010 return total
nodirf33b8d62016-10-26 22:34:58 -07001011
1012
nodirbe642ff2016-06-09 15:51:51 -07001013def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001014 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -07001015 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +00001016 version=__version__,
1017 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -07001018 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -07001019 '--clean', action='store_true',
1020 help='Cleans the cache, trimming it necessary and remove corrupted items '
1021 'and returns without executing anything; use with -v to know what '
1022 'was done')
1023 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -07001024 '--no-clean', action='store_true',
1025 help='Do not clean the cache automatically on startup. This is meant for '
1026 'bots where a separate execution with --clean was done earlier so '
1027 'doing it again is redundant')
1028 parser.add_option(
maruel4409e302016-07-19 14:25:51 -07001029 '--use-symlinks', action='store_true',
1030 help='Use symlinks instead of hardlinks')
1031 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -07001032 '--json',
1033 help='dump output metadata to json file. When used, run_isolated returns '
1034 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -07001035 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001036 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -07001037 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001038 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -07001039 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -07001040 parser.add_option(
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001041 '--raw-cmd', action='store_true',
1042 help='Ignore the isolated command, use the one supplied at the command '
1043 'line')
1044 parser.add_option(
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001045 '--relative-cwd',
1046 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
1047 'requires --raw-cmd')
1048 parser.add_option(
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001049 '--env', default=[], action='append',
1050 help='Environment variables to set for the child process')
1051 parser.add_option(
1052 '--env-prefix', default=[], action='append',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001053 help='Specify a VAR=./path/fragment to put in the environment variable '
1054 'before executing the command. The path fragment must be relative '
1055 'to the isolated run directory, and must not contain a `..` token. '
1056 'The path will be made absolute and prepended to the indicated '
1057 '$VAR using the OS\'s path separator. Multiple items for the same '
1058 '$VAR will be prepended in order.')
1059 parser.add_option(
bpastene3ae09522016-06-10 17:12:59 -07001060 '--bot-file',
1061 help='Path to a file describing the state of the host. The content is '
1062 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -07001063 parser.add_option(
vadimsh9c54b2c2017-07-25 14:08:29 -07001064 '--switch-to-account',
1065 help='If given, switches LUCI_CONTEXT to given logical service account '
1066 '(e.g. "task" or "system") before launching the isolated process.')
1067 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -07001068 '--output', action='append',
1069 help='Specifies an output to return. If no outputs are specified, all '
1070 'files located in $(ISOLATED_OUTDIR) will be returned; '
1071 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
1072 'specified by --output option (there can be multiple) will be '
1073 'returned. Note that if a file in OUT_DIR has the same path '
1074 'as an --output option, the --output version will be returned.')
1075 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -07001076 '-a', '--argsfile',
1077 # This is actually handled in parse_args; it's included here purely so it
1078 # can make it into the help text.
1079 help='Specify a file containing a JSON array of arguments to this '
1080 'script. If --argsfile is provided, no other argument may be '
1081 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001082 data_group = optparse.OptionGroup(parser, 'Data source')
1083 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001084 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -07001085 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001086 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001087 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001088
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -04001089 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001090
1091 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -07001092 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001093
Kenneth Russell61d42352014-09-15 11:41:16 -07001094 debug_group = optparse.OptionGroup(parser, 'Debugging')
1095 debug_group.add_option(
1096 '--leak-temp-dir',
1097 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -07001098 help='Deliberately leak isolate\'s temp dir for later examination. '
1099 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -07001100 debug_group.add_option(
1101 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -07001102 parser.add_option_group(debug_group)
1103
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001104 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001105
nodirf33b8d62016-10-26 22:34:58 -07001106 parser.set_defaults(
1107 cache='cache',
1108 cipd_cache='cipd_cache',
1109 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -07001110 return parser
1111
1112
aludwin7556e0c2016-10-26 08:46:10 -07001113def parse_args(args):
1114 # Create a fake mini-parser just to get out the "-a" command. Note that
1115 # it's not documented here; instead, it's documented in create_option_parser
1116 # even though that parser will never actually get to parse it. This is
1117 # because --argsfile is exclusive with all other options and arguments.
1118 file_argparse = argparse.ArgumentParser(add_help=False)
1119 file_argparse.add_argument('-a', '--argsfile')
1120 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
1121 if file_args.argsfile:
1122 if nonfile_args:
1123 file_argparse.error('Can\'t specify --argsfile with'
1124 'any other arguments (%s)' % nonfile_args)
1125 try:
1126 with open(file_args.argsfile, 'r') as f:
1127 args = json.load(f)
1128 except (IOError, OSError, ValueError) as e:
1129 # We don't need to error out here - "args" is now empty,
1130 # so the call below to parser.parse_args(args) will fail
1131 # and print the full help text.
1132 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
1133
1134 # Even if we failed to read the args, just call the normal parser now since it
1135 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -07001136 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001137 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -07001138 return (parser, options, args)
1139
1140
1141def main(args):
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -05001142 # Warning: when --argsfile is used, the strings are unicode instances, when
1143 # parsed normally, the strings are str instances.
aludwin7556e0c2016-10-26 08:46:10 -07001144 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -07001145
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001146 if not file_path.enable_symlink():
1147 logging.error('Symlink support is not enabled')
1148
nodirf33b8d62016-10-26 22:34:58 -07001149 isolate_cache = isolateserver.process_cache_options(options, trim=False)
1150 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -07001151 if options.clean:
1152 if options.isolated:
1153 parser.error('Can\'t use --isolated with --clean.')
1154 if options.isolate_server:
1155 parser.error('Can\'t use --isolate-server with --clean.')
1156 if options.json:
1157 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -07001158 if options.named_caches:
1159 parser.error('Can\t use --named-cache with --clean.')
1160 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001161 return 0
nodirf33b8d62016-10-26 22:34:58 -07001162
maruel2e8d0f52016-07-16 07:51:29 -07001163 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -07001164 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001165
nodir55be77b2016-05-03 09:39:57 -07001166 if not options.isolated and not args:
1167 parser.error('--isolated or command to run is required.')
1168
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001169 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001170
1171 isolateserver.process_isolate_server_options(
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001172 parser, options, True, False)
nodir55be77b2016-05-03 09:39:57 -07001173 if not options.isolate_server:
1174 if options.isolated:
1175 parser.error('--isolated requires --isolate-server')
1176 if ISOLATED_OUTDIR_PARAMETER in args:
1177 parser.error(
1178 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001179
nodir90bc8dc2016-06-15 13:35:21 -07001180 if options.root_dir:
1181 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -07001182 if options.json:
1183 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001184
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001185 if any('=' not in i for i in options.env):
1186 parser.error(
1187 '--env required key=value form. value can be skipped to delete '
1188 'the variable')
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001189 options.env = dict(i.split('=', 1) for i in options.env)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001190
1191 prefixes = {}
1192 cwd = os.path.realpath(os.getcwd())
1193 for item in options.env_prefix:
1194 if '=' not in item:
1195 parser.error(
1196 '--env-prefix %r is malformed, must be in the form `VAR=./path`'
1197 % item)
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001198 key, opath = item.split('=', 1)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001199 if os.path.isabs(opath):
1200 parser.error('--env-prefix %r path is bad, must be relative.' % opath)
1201 opath = os.path.normpath(opath)
1202 if not os.path.realpath(os.path.join(cwd, opath)).startswith(cwd):
1203 parser.error(
1204 '--env-prefix %r path is bad, must be relative and not contain `..`.'
1205 % opath)
1206 prefixes.setdefault(key, []).append(opath)
1207 options.env_prefix = prefixes
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001208
nodirbe642ff2016-06-09 15:51:51 -07001209 cipd.validate_cipd_options(parser, options)
1210
vadimsh232f5a82017-01-20 19:23:44 -08001211 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -08001212 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -07001213 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -08001214 run_dir, cipd.parse_package_args(options.cipd_packages),
1215 options.cipd_server, options.cipd_client_package,
1216 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001217
nodird6160682017-02-02 13:03:35 -08001218 @contextlib.contextmanager
nodir0ae98b32017-05-11 13:21:53 -07001219 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001220 # WARNING: this function depends on "options" variable defined in the outer
1221 # function.
nodir0ae98b32017-05-11 13:21:53 -07001222 caches = [
1223 (os.path.join(run_dir, unicode(relpath)), name)
1224 for name, relpath in options.named_caches
1225 ]
nodirf33b8d62016-10-26 22:34:58 -07001226 with named_cache_manager.open():
nodir0ae98b32017-05-11 13:21:53 -07001227 for path, name in caches:
1228 named_cache_manager.install(path, name)
nodird6160682017-02-02 13:03:35 -08001229 try:
1230 yield
1231 finally:
dnje289d132017-07-07 11:16:44 -07001232 # Uninstall each named cache, returning it to the cache pool. If an
1233 # uninstall fails for a given cache, it will remain in the task's
1234 # temporary space, get cleaned up by the Swarming bot, and be lost.
1235 #
1236 # If the Swarming bot cannot clean up the cache, it will handle it like
1237 # any other bot file that could not be removed.
nodir0ae98b32017-05-11 13:21:53 -07001238 with named_cache_manager.open():
1239 for path, name in caches:
dnje289d132017-07-07 11:16:44 -07001240 try:
1241 named_cache_manager.uninstall(path, name)
1242 except named_cache.Error:
1243 logging.exception('Error while removing named cache %r at %r. '
1244 'The cache will be lost.', path, name)
nodirf33b8d62016-10-26 22:34:58 -07001245
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001246 extra_args = []
1247 command = []
1248 if options.raw_cmd:
1249 command = args
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001250 if options.relative_cwd:
1251 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1252 if not a.startswith(os.getcwd()):
1253 parser.error(
1254 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001255 else:
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001256 if options.relative_cwd:
1257 parser.error('--relative-cwd requires --raw-cmd')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001258 extra_args = args
1259
1260 data = TaskData(
1261 command=command,
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001262 relative_cwd=options.relative_cwd,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001263 extra_args=extra_args,
1264 isolated_hash=options.isolated,
1265 storage=None,
1266 isolate_cache=isolate_cache,
1267 outputs=options.output,
1268 install_named_caches=install_named_caches,
1269 leak_temp_dir=options.leak_temp_dir,
1270 root_dir=_to_unicode(options.root_dir),
1271 hard_timeout=options.hard_timeout,
1272 grace_period=options.grace_period,
1273 bot_file=options.bot_file,
1274 switch_to_account=options.switch_to_account,
1275 install_packages_fn=install_packages_fn,
1276 use_symlinks=options.use_symlinks,
1277 env=options.env,
1278 env_prefix=options.env_prefix)
nodirbe642ff2016-06-09 15:51:51 -07001279 try:
nodir90bc8dc2016-06-15 13:35:21 -07001280 if options.isolate_server:
1281 storage = isolateserver.get_storage(
1282 options.isolate_server, options.namespace)
1283 with storage:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001284 data = data._replace(storage=storage)
nodirf33b8d62016-10-26 22:34:58 -07001285 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
1286 assert storage.hash_algo == isolate_cache.hash_algo
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001287 return run_tha_test(data, options.json)
1288 return run_tha_test(data, options.json)
nodirf33b8d62016-10-26 22:34:58 -07001289 except (cipd.Error, named_cache.Error) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001290 print >> sys.stderr, ex.message
1291 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001292
1293
1294if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001295 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001296 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001297 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001298 sys.exit(main(sys.argv[1:]))