blob: 96c2b0ea9b0ff68223e39654200cb62584acde22 [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 Ruel62f21762017-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
vadimsh8ec66822017-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
maruel7eb6b562017-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 Ruelece7ab92017-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 Ruel62f21762017-12-07 21:35:05 -0500150 # Relative directory to start command into.
151 'relative_cwd',
Marc-Antoine Ruelece7ab92017-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 Rueldc527d12017-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 Ruelb651cad2017-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
vadimsh8ec66822017-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 Ruelaf9ea1b2017-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 Iannuccibe66ce72017-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 Iannuccibe66ce72017-11-22 12:56:50 -0800365 cwd: The directory the command will run in
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -0500366 env: environment variables to use
Robert Iannuccibe66ce72017-11-22 12:56:50 -0800367 env_prefixes: {"ENV_KEY": ['cwd', 'relative', 'paths', 'to', 'prepend']}
vadimsh232f5a82017-01-20 19:23:44 -0800368 """
Marc-Antoine Ruelaf9ea1b2017-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 Rueldc527d12017-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 Ruelaf9ea1b2017-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 Rueldc527d12017-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 Rueldc527d12017-11-29 11:19:16 -0500397 out[key] = _to_str(tmp_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800398
Marc-Antoine Ruelaf9ea1b2017-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()
martiniss3343ec02017-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:
aludwinb35146d2017-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 Ruelece7ab92017-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 Ruelece7ab92017-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,
Seth Koehlerf9d75bb2017-12-19 13:59:33 -0500586 'internal_failure': 'run_isolated did not complete properly',
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 Ruelece7ab92017-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 Ruelece7ab92017-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)
maruel13437a72017-05-26 05:33:40 -0700629 if os.path.isdir(run_dir):
630 file_path.rmtree(run_dir)
Marc-Antoine Ruel62f21762017-12-07 21:35:05 -0500631 os.mkdir(run_dir, 0700)
maruelcffa0542017-04-07 08:39:20 -0700632 else:
Marc-Antoine Ruelece7ab92017-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 Ruelece7ab92017-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 Ruel62f21762017-12-07 21:35:05 -0500640 if data.relative_cwd:
641 cwd = os.path.normpath(os.path.join(cwd, data.relative_cwd))
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500642 command = data.command
nodir55be77b2016-05-03 09:39:57 -0700643 try:
Marc-Antoine Ruelece7ab92017-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 Ruelece7ab92017-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 Ruelece7ab92017-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 Ruelece7ab92017-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 Ruelece7ab92017-12-07 10:41:12 -0500659 if not command and bundle.command:
660 command = bundle.command + data.extra_args
Marc-Antoine Ruelb2cef0f2017-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 Ruel62f21762017-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 Ruelece7ab92017-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 Ruelece7ab92017-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 Ruelece7ab92017-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:
vadimsh8ec66822017-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 Ruelece7ab92017-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)
vadimsh8ec66822017-07-25 14:08:29 -0700703 result['exit_code'], result['had_hard_timeout'] = run_command(
Marc-Antoine Ruelece7ab92017-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)
Seth Koehlerf9d75bb2017-12-19 13:59:33 -0500707
708 # We successfully ran the command, set internal_failure back to
709 # None (even if the command failed, it's not an internal error).
710 result['internal_failure'] = None
maruela9cfd6f2015-09-15 11:03:15 -0700711 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700712 # An internal error occurred. Report accordingly so the swarming task will
713 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700714 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700715 result['internal_failure'] = str(e)
716 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700717
718 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700719 finally:
720 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700721 # Try to link files to the output directory, if specified.
722 if out_dir:
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500723 link_outputs_to_outdir(run_dir, out_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700724
nodir32a1ec12016-10-26 18:34:07 -0700725 success = False
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500726 if data.leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700727 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700728 logging.warning(
729 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700730 else:
maruel84537cb2015-10-16 14:21:28 -0700731 # On Windows rmtree(run_dir) call above has a synchronization effect: it
732 # finishes only when all task child processes terminate (since a running
733 # process locks *.exe file). Examine out_dir only after that call
734 # completes (since child processes may write to out_dir too and we need
735 # to wait for them to finish).
736 if fs.isdir(run_dir):
737 try:
738 success = file_path.rmtree(run_dir)
739 except OSError as e:
740 logging.error('Failure with %s', e)
741 success = False
742 if not success:
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500743 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('run', data.grace_period))
maruel84537cb2015-10-16 14:21:28 -0700744 if result['exit_code'] == 0:
745 result['exit_code'] = 1
746 if fs.isdir(tmp_dir):
747 try:
748 success = file_path.rmtree(tmp_dir)
749 except OSError as e:
750 logging.error('Failure with %s', e)
751 success = False
752 if not success:
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500753 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('temp', data.grace_period))
maruel84537cb2015-10-16 14:21:28 -0700754 if result['exit_code'] == 0:
755 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700756
marueleb5fbee2015-09-17 13:01:36 -0700757 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700758 if out_dir:
nodir55715712016-06-03 12:28:19 -0700759 isolated_stats = result['stats'].setdefault('isolated', {})
760 result['outputs_ref'], success, isolated_stats['upload'] = (
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500761 delete_and_upload(data.storage, out_dir, data.leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700762 if not success and result['exit_code'] == 0:
763 result['exit_code'] = 1
764 except Exception as e:
765 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700766 if out_dir:
767 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700768 result['internal_failure'] = str(e)
769 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500770
771
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500772def run_tha_test(data, result_json):
nodir55be77b2016-05-03 09:39:57 -0700773 """Runs an executable and records execution metadata.
774
nodir55be77b2016-05-03 09:39:57 -0700775 If isolated_hash is specified, downloads the dependencies in the cache,
776 hardlinks them into a temporary directory and runs the command specified in
777 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500778
779 A temporary directory is created to hold the output files. The content inside
780 this directory will be uploaded back to |storage| packaged as a .isolated
781 file.
782
783 Arguments:
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500784 - data: TaskData instance.
785 - result_json: File path to dump result metadata into. If set, the process
786 exit code is always 0 unless an internal error occurred.
maruela9cfd6f2015-09-15 11:03:15 -0700787
788 Returns:
789 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000790 """
maruela76b9ee2015-12-15 06:18:08 -0800791 if result_json:
792 # Write a json output file right away in case we get killed.
793 result = {
794 'exit_code': None,
795 'had_hard_timeout': False,
796 'internal_failure': 'Was terminated before completion',
797 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700798 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800799 }
800 tools.write_json(result_json, result, dense=True)
801
maruela9cfd6f2015-09-15 11:03:15 -0700802 # run_isolated exit code. Depends on if result_json is used or not.
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -0500803 result = map_and_run(data, True)
maruela9cfd6f2015-09-15 11:03:15 -0700804 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700805
maruela9cfd6f2015-09-15 11:03:15 -0700806 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700807 # We've found tests to delete 'work' when quitting, causing an exception
808 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700809 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700810 tools.write_json(result_json, result, dense=True)
811 # Only return 1 if there was an internal error.
812 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000813
maruela9cfd6f2015-09-15 11:03:15 -0700814 # Marshall into old-style inline output.
815 if result['outputs_ref']:
816 data = {
817 'hash': result['outputs_ref']['isolated'],
818 'namespace': result['outputs_ref']['namespace'],
819 'storage': result['outputs_ref']['isolatedserver'],
820 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500821 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700822 print(
823 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
824 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800825 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700826 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000827
828
iannuccib58d10d2017-03-18 02:00:25 -0700829# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800830CipdInfo = collections.namedtuple('CipdInfo', [
831 'client', # cipd.CipdClient object
832 'cache_dir', # absolute path to bot-global cipd tag and instance cache
833 'stats', # dict with stats to return to the server
834 'pins', # dict with installed cipd pins to return to the server
835])
836
837
838@contextlib.contextmanager
839def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700840 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800841 yield None
842
843
iannuccib58d10d2017-03-18 02:00:25 -0700844def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
845 """Calls 'cipd ensure' for packages.
846
847 Args:
848 run_dir (str): root of installation.
849 cipd_cache_dir (str): the directory to use for the cipd package cache.
850 client (CipdClient): the cipd client to use
851 packages: packages to install, list [(path, package_name, version), ...].
852 timeout: max duration in seconds that this function can take.
853
854 Returns: list of pinned packages. Looks like [
855 {
856 'path': 'subdirectory',
857 'package_name': 'resolved/package/name',
858 'version': 'deadbeef...',
859 },
860 ...
861 ]
862 """
863 package_pins = [None]*len(packages)
864 def insert_pin(path, name, version, idx):
865 package_pins[idx] = {
866 'package_name': name,
867 # swarming deals with 'root' as '.'
868 'path': path or '.',
869 'version': version,
870 }
871
872 by_path = collections.defaultdict(list)
873 for i, (path, name, version) in enumerate(packages):
874 # cipd deals with 'root' as ''
875 if path == '.':
876 path = ''
877 by_path[path].append((name, version, i))
878
879 pins = client.ensure(
880 run_dir,
881 {
882 subdir: [(name, vers) for name, vers, _ in pkgs]
883 for subdir, pkgs in by_path.iteritems()
884 },
885 cache_dir=cipd_cache_dir,
886 timeout=timeout,
887 )
888
889 for subdir, pin_list in sorted(pins.iteritems()):
890 this_subdir = by_path[subdir]
891 for i, (name, version) in enumerate(pin_list):
892 insert_pin(subdir, name, version, this_subdir[i][2])
893
Robert Iannucci79070b72017-12-13 11:34:03 -0800894 assert None not in package_pins, (packages, pins, package_pins)
iannuccib58d10d2017-03-18 02:00:25 -0700895
896 return package_pins
897
898
vadimsh232f5a82017-01-20 19:23:44 -0800899@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700900def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700901 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800902 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800903 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700904
vadimsh232f5a82017-01-20 19:23:44 -0800905 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
906
907 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700908 [
909 {
910 "path": path, "package_name": package_name, "version": version,
911 },
912 ...
913 ]
vadimsh902948e2017-01-20 15:57:32 -0800914 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700915
916 such that they correspond 1:1 to all input package arguments from the command
917 line. These dictionaries make their all the way back to swarming, where they
918 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700919
vadimsh902948e2017-01-20 15:57:32 -0800920 If 'packages' list is empty, will bootstrap CIPD client, but won't install
921 any packages.
922
923 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -0800924 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -0800925
nodirbe642ff2016-06-09 15:51:51 -0700926 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700927 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -0800928 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -0700929 service_url (str): CIPD server url, e.g.
930 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700931 client_package_name (str): CIPD package name of CIPD client.
932 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700933 cache_dir (str): where to keep cache of cipd clients, packages and tags.
934 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700935 """
936 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700937
nodirbe642ff2016-06-09 15:51:51 -0700938 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700939 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700940
vadimsh902948e2017-01-20 15:57:32 -0800941 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -0800942 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -0700943 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -0800944 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -0700945
nodirbe642ff2016-06-09 15:51:51 -0700946 get_client_start = time.time()
947 client_manager = cipd.get_client(
948 service_url, client_package_name, client_version, cache_dir,
949 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -0700950
nodirbe642ff2016-06-09 15:51:51 -0700951 with client_manager as client:
952 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700953
iannuccib58d10d2017-03-18 02:00:25 -0700954 package_pins = []
955 if packages:
956 package_pins = _install_packages(
957 run_dir, cipd_cache_dir, client, packages, timeoutfn())
958
959 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -0700960
vadimsh232f5a82017-01-20 19:23:44 -0800961 total_duration = time.time() - start
962 logging.info(
963 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -0700964
vadimsh232f5a82017-01-20 19:23:44 -0800965 yield CipdInfo(
966 client=client,
967 cache_dir=cipd_cache_dir,
968 stats={
969 'duration': total_duration,
970 'get_client_duration': get_client_duration,
971 },
972 pins={
iannuccib58d10d2017-03-18 02:00:25 -0700973 'client_package': {
974 'package_name': client.package_name,
975 'version': client.instance_id,
976 },
vadimsh232f5a82017-01-20 19:23:44 -0800977 'packages': package_pins,
978 })
nodirbe642ff2016-06-09 15:51:51 -0700979
980
nodirf33b8d62016-10-26 22:34:58 -0700981def clean_caches(options, isolate_cache, named_cache_manager):
maruele6fc9382017-05-04 09:03:48 -0700982 """Trims isolated and named caches.
983
984 The goal here is to coherently trim both caches, deleting older items
985 independent of which container they belong to.
986 """
987 # TODO(maruel): Trim CIPD cache the same way.
988 total = 0
nodirf33b8d62016-10-26 22:34:58 -0700989 with named_cache_manager.open():
990 oldest_isolated = isolate_cache.get_oldest()
991 oldest_named = named_cache_manager.get_oldest()
992 trimmers = [
993 (
994 isolate_cache.trim,
995 isolate_cache.get_timestamp(oldest_isolated) if oldest_isolated else 0,
996 ),
997 (
998 lambda: named_cache_manager.trim(options.min_free_space),
999 named_cache_manager.get_timestamp(oldest_named) if oldest_named else 0,
1000 ),
1001 ]
1002 trimmers.sort(key=lambda (_, ts): ts)
maruele6fc9382017-05-04 09:03:48 -07001003 # TODO(maruel): This is incorrect, we want to trim 'items' that are strictly
1004 # the oldest independent of in which cache they live in. Right now, the
1005 # cache with the oldest item pays the price.
nodirf33b8d62016-10-26 22:34:58 -07001006 for trim, _ in trimmers:
maruele6fc9382017-05-04 09:03:48 -07001007 total += trim()
nodirf33b8d62016-10-26 22:34:58 -07001008 isolate_cache.cleanup()
maruele6fc9382017-05-04 09:03:48 -07001009 return total
nodirf33b8d62016-10-26 22:34:58 -07001010
1011
nodirbe642ff2016-06-09 15:51:51 -07001012def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001013 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -07001014 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +00001015 version=__version__,
1016 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -07001017 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -07001018 '--clean', action='store_true',
1019 help='Cleans the cache, trimming it necessary and remove corrupted items '
1020 'and returns without executing anything; use with -v to know what '
1021 'was done')
1022 parser.add_option(
maruel2e8d0f52016-07-16 07:51:29 -07001023 '--no-clean', action='store_true',
1024 help='Do not clean the cache automatically on startup. This is meant for '
1025 'bots where a separate execution with --clean was done earlier so '
1026 'doing it again is redundant')
1027 parser.add_option(
maruel4409e302016-07-19 14:25:51 -07001028 '--use-symlinks', action='store_true',
1029 help='Use symlinks instead of hardlinks')
1030 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -07001031 '--json',
1032 help='dump output metadata to json file. When used, run_isolated returns '
1033 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -07001034 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001035 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -07001036 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001037 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -07001038 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -07001039 parser.add_option(
Marc-Antoine Ruel7d179af2017-10-24 16:52:02 -07001040 '--raw-cmd', action='store_true',
1041 help='Ignore the isolated command, use the one supplied at the command '
1042 'line')
1043 parser.add_option(
Marc-Antoine Ruel62f21762017-12-07 21:35:05 -05001044 '--relative-cwd',
1045 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
1046 'requires --raw-cmd')
1047 parser.add_option(
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -05001048 '--env', default=[], action='append',
1049 help='Environment variables to set for the child process')
1050 parser.add_option(
1051 '--env-prefix', default=[], action='append',
Robert Iannuccibe66ce72017-11-22 12:56:50 -08001052 help='Specify a VAR=./path/fragment to put in the environment variable '
1053 'before executing the command. The path fragment must be relative '
1054 'to the isolated run directory, and must not contain a `..` token. '
1055 'The path will be made absolute and prepended to the indicated '
1056 '$VAR using the OS\'s path separator. Multiple items for the same '
1057 '$VAR will be prepended in order.')
1058 parser.add_option(
bpastene3ae09522016-06-10 17:12:59 -07001059 '--bot-file',
1060 help='Path to a file describing the state of the host. The content is '
1061 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -07001062 parser.add_option(
vadimsh8ec66822017-07-25 14:08:29 -07001063 '--switch-to-account',
1064 help='If given, switches LUCI_CONTEXT to given logical service account '
1065 '(e.g. "task" or "system") before launching the isolated process.')
1066 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -07001067 '--output', action='append',
1068 help='Specifies an output to return. If no outputs are specified, all '
1069 'files located in $(ISOLATED_OUTDIR) will be returned; '
1070 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
1071 'specified by --output option (there can be multiple) will be '
1072 'returned. Note that if a file in OUT_DIR has the same path '
1073 'as an --output option, the --output version will be returned.')
1074 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -07001075 '-a', '--argsfile',
1076 # This is actually handled in parse_args; it's included here purely so it
1077 # can make it into the help text.
1078 help='Specify a file containing a JSON array of arguments to this '
1079 'script. If --argsfile is provided, no other argument may be '
1080 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001081 data_group = optparse.OptionGroup(parser, 'Data source')
1082 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001083 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -07001084 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001085 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001086 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001087
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -04001088 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001089
1090 cipd.add_cipd_options(parser)
nodirf33b8d62016-10-26 22:34:58 -07001091 named_cache.add_named_cache_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001092
Kenneth Russell61d42352014-09-15 11:41:16 -07001093 debug_group = optparse.OptionGroup(parser, 'Debugging')
1094 debug_group.add_option(
1095 '--leak-temp-dir',
1096 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -07001097 help='Deliberately leak isolate\'s temp dir for later examination. '
1098 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -07001099 debug_group.add_option(
1100 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -07001101 parser.add_option_group(debug_group)
1102
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001103 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001104
nodirf33b8d62016-10-26 22:34:58 -07001105 parser.set_defaults(
1106 cache='cache',
1107 cipd_cache='cipd_cache',
1108 named_cache_root='named_caches')
nodirbe642ff2016-06-09 15:51:51 -07001109 return parser
1110
1111
aludwin7556e0c2016-10-26 08:46:10 -07001112def parse_args(args):
1113 # Create a fake mini-parser just to get out the "-a" command. Note that
1114 # it's not documented here; instead, it's documented in create_option_parser
1115 # even though that parser will never actually get to parse it. This is
1116 # because --argsfile is exclusive with all other options and arguments.
1117 file_argparse = argparse.ArgumentParser(add_help=False)
1118 file_argparse.add_argument('-a', '--argsfile')
1119 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
1120 if file_args.argsfile:
1121 if nonfile_args:
1122 file_argparse.error('Can\'t specify --argsfile with'
1123 'any other arguments (%s)' % nonfile_args)
1124 try:
1125 with open(file_args.argsfile, 'r') as f:
1126 args = json.load(f)
1127 except (IOError, OSError, ValueError) as e:
1128 # We don't need to error out here - "args" is now empty,
1129 # so the call below to parser.parse_args(args) will fail
1130 # and print the full help text.
1131 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
1132
1133 # Even if we failed to read the args, just call the normal parser now since it
1134 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -07001135 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001136 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -07001137 return (parser, options, args)
1138
1139
1140def main(args):
Marc-Antoine Rueldc527d12017-11-29 11:19:16 -05001141 # Warning: when --argsfile is used, the strings are unicode instances, when
1142 # parsed normally, the strings are str instances.
aludwin7556e0c2016-10-26 08:46:10 -07001143 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -07001144
Marc-Antoine Ruelc7a332f2017-08-25 17:37:51 -04001145 if not file_path.enable_symlink():
1146 logging.error('Symlink support is not enabled')
1147
nodirf33b8d62016-10-26 22:34:58 -07001148 isolate_cache = isolateserver.process_cache_options(options, trim=False)
1149 named_cache_manager = named_cache.process_named_cache_options(parser, options)
maruel36a963d2016-04-08 17:15:49 -07001150 if options.clean:
1151 if options.isolated:
1152 parser.error('Can\'t use --isolated with --clean.')
1153 if options.isolate_server:
1154 parser.error('Can\'t use --isolate-server with --clean.')
1155 if options.json:
1156 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -07001157 if options.named_caches:
1158 parser.error('Can\t use --named-cache with --clean.')
1159 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001160 return 0
nodirf33b8d62016-10-26 22:34:58 -07001161
maruel2e8d0f52016-07-16 07:51:29 -07001162 if not options.no_clean:
nodirf33b8d62016-10-26 22:34:58 -07001163 clean_caches(options, isolate_cache, named_cache_manager)
maruel36a963d2016-04-08 17:15:49 -07001164
nodir55be77b2016-05-03 09:39:57 -07001165 if not options.isolated and not args:
1166 parser.error('--isolated or command to run is required.')
1167
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001168 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001169
1170 isolateserver.process_isolate_server_options(
Marc-Antoine Ruelc7a332f2017-08-25 17:37:51 -04001171 parser, options, True, False)
nodir55be77b2016-05-03 09:39:57 -07001172 if not options.isolate_server:
1173 if options.isolated:
1174 parser.error('--isolated requires --isolate-server')
1175 if ISOLATED_OUTDIR_PARAMETER in args:
1176 parser.error(
1177 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001178
nodir90bc8dc2016-06-15 13:35:21 -07001179 if options.root_dir:
1180 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -07001181 if options.json:
1182 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001183
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -05001184 if any('=' not in i for i in options.env):
1185 parser.error(
1186 '--env required key=value form. value can be skipped to delete '
1187 'the variable')
Marc-Antoine Ruelb651cad2017-12-01 18:45:18 -05001188 options.env = dict(i.split('=', 1) for i in options.env)
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -05001189
1190 prefixes = {}
1191 cwd = os.path.realpath(os.getcwd())
1192 for item in options.env_prefix:
1193 if '=' not in item:
1194 parser.error(
1195 '--env-prefix %r is malformed, must be in the form `VAR=./path`'
1196 % item)
Marc-Antoine Ruelb651cad2017-12-01 18:45:18 -05001197 key, opath = item.split('=', 1)
Marc-Antoine Ruelaf9ea1b2017-11-28 18:33:39 -05001198 if os.path.isabs(opath):
1199 parser.error('--env-prefix %r path is bad, must be relative.' % opath)
1200 opath = os.path.normpath(opath)
1201 if not os.path.realpath(os.path.join(cwd, opath)).startswith(cwd):
1202 parser.error(
1203 '--env-prefix %r path is bad, must be relative and not contain `..`.'
1204 % opath)
1205 prefixes.setdefault(key, []).append(opath)
1206 options.env_prefix = prefixes
Robert Iannuccibe66ce72017-11-22 12:56:50 -08001207
nodirbe642ff2016-06-09 15:51:51 -07001208 cipd.validate_cipd_options(parser, options)
1209
vadimsh232f5a82017-01-20 19:23:44 -08001210 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -08001211 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -07001212 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -08001213 run_dir, cipd.parse_package_args(options.cipd_packages),
1214 options.cipd_server, options.cipd_client_package,
1215 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001216
nodird6160682017-02-02 13:03:35 -08001217 @contextlib.contextmanager
nodir26251c42017-05-11 13:21:53 -07001218 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001219 # WARNING: this function depends on "options" variable defined in the outer
1220 # function.
nodir26251c42017-05-11 13:21:53 -07001221 caches = [
1222 (os.path.join(run_dir, unicode(relpath)), name)
1223 for name, relpath in options.named_caches
1224 ]
nodirf33b8d62016-10-26 22:34:58 -07001225 with named_cache_manager.open():
nodir26251c42017-05-11 13:21:53 -07001226 for path, name in caches:
1227 named_cache_manager.install(path, name)
nodird6160682017-02-02 13:03:35 -08001228 try:
1229 yield
1230 finally:
dnjd5e4ecc2017-07-07 11:16:44 -07001231 # Uninstall each named cache, returning it to the cache pool. If an
1232 # uninstall fails for a given cache, it will remain in the task's
1233 # temporary space, get cleaned up by the Swarming bot, and be lost.
1234 #
1235 # If the Swarming bot cannot clean up the cache, it will handle it like
1236 # any other bot file that could not be removed.
nodir26251c42017-05-11 13:21:53 -07001237 with named_cache_manager.open():
1238 for path, name in caches:
dnjd5e4ecc2017-07-07 11:16:44 -07001239 try:
1240 named_cache_manager.uninstall(path, name)
1241 except named_cache.Error:
1242 logging.exception('Error while removing named cache %r at %r. '
1243 'The cache will be lost.', path, name)
nodirf33b8d62016-10-26 22:34:58 -07001244
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -05001245 extra_args = []
1246 command = []
1247 if options.raw_cmd:
1248 command = args
Marc-Antoine Ruel62f21762017-12-07 21:35:05 -05001249 if options.relative_cwd:
1250 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1251 if not a.startswith(os.getcwd()):
1252 parser.error(
1253 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -05001254 else:
Marc-Antoine Ruel62f21762017-12-07 21:35:05 -05001255 if options.relative_cwd:
1256 parser.error('--relative-cwd requires --raw-cmd')
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -05001257 extra_args = args
1258
1259 data = TaskData(
1260 command=command,
Marc-Antoine Ruel62f21762017-12-07 21:35:05 -05001261 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -05001262 extra_args=extra_args,
1263 isolated_hash=options.isolated,
1264 storage=None,
1265 isolate_cache=isolate_cache,
1266 outputs=options.output,
1267 install_named_caches=install_named_caches,
1268 leak_temp_dir=options.leak_temp_dir,
1269 root_dir=_to_unicode(options.root_dir),
1270 hard_timeout=options.hard_timeout,
1271 grace_period=options.grace_period,
1272 bot_file=options.bot_file,
1273 switch_to_account=options.switch_to_account,
1274 install_packages_fn=install_packages_fn,
1275 use_symlinks=options.use_symlinks,
1276 env=options.env,
1277 env_prefix=options.env_prefix)
nodirbe642ff2016-06-09 15:51:51 -07001278 try:
nodir90bc8dc2016-06-15 13:35:21 -07001279 if options.isolate_server:
1280 storage = isolateserver.get_storage(
1281 options.isolate_server, options.namespace)
1282 with storage:
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -05001283 data = data._replace(storage=storage)
nodirf33b8d62016-10-26 22:34:58 -07001284 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
1285 assert storage.hash_algo == isolate_cache.hash_algo
Marc-Antoine Ruelece7ab92017-12-07 10:41:12 -05001286 return run_tha_test(data, options.json)
1287 return run_tha_test(data, options.json)
nodirf33b8d62016-10-26 22:34:58 -07001288 except (cipd.Error, named_cache.Error) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001289 print >> sys.stderr, ex.message
1290 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001291
1292
1293if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001294 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001295 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001296 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001297 sys.exit(main(sys.argv[1:]))