blob: 5a8a992c6a9ba4c31961d2519d40a3f453d51499 [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
Roberto Carrillo71ade6d2018-10-08 22:30:24 +000016Any ${EXECUTABLE_SUFFIX} on the command line or the environment variables passed
17with the --env option will be replaced with ".exe" string on Windows and "" on
18other platforms.
nodirbe642ff2016-06-09 15:51:51 -070019
Roberto Carrillo71ade6d2018-10-08 22:30:24 +000020Any ${ISOLATED_OUTDIR} on the command line or the environment variables passed
21with the --env option will be replaced by the location of a temporary directory
22upon execution of the command specified in the .isolated file. All content
23written to this directory will be uploaded upon termination and the .isolated
24file describing this directory will be printed to stdout.
bpastene447c1992016-06-20 15:21:47 -070025
Roberto Carrillo71ade6d2018-10-08 22:30:24 +000026Any ${SWARMING_BOT_FILE} on the command line or the environment variables
27passed with the --env option will be replaced by the value of the --bot-file
28parameter. This file is used by a swarming bot to communicate state of the host
29to tasks. It is written to by the swarming bot's on_before_task() hook in the
30swarming server's custom bot_config.py.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000031"""
32
Roberto Carrillo71ade6d2018-10-08 22:30:24 +000033__version__ = '0.11.1'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000034
aludwin7556e0c2016-10-26 08:46:10 -070035import argparse
maruel064c0a32016-04-05 11:47:15 -070036import base64
iannucci96fcccc2016-08-30 15:52:22 -070037import collections
vadimsh232f5a82017-01-20 19:23:44 -080038import contextlib
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -040039import errno
aludwin7556e0c2016-10-26 08:46:10 -070040import json
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000041import logging
42import optparse
43import os
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -040044import re
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000045import sys
46import tempfile
maruel064c0a32016-04-05 11:47:15 -070047import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000048
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000049from third_party.depot_tools import fix_encoding
50
Vadim Shtayura6b555c12014-07-23 16:22:18 -070051from utils import file_path
maruel12e30012015-10-09 11:55:35 -070052from utils import fs
maruel064c0a32016-04-05 11:47:15 -070053from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040054from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040055from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050056from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000057from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000058from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000059
vadimsh9c54b2c2017-07-25 14:08:29 -070060from libs import luci_context
61
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080062import auth
nodirbe642ff2016-06-09 15:51:51 -070063import cipd
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000064import isolateserver
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -040065import local_caching
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000066
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000067
vadimsh@chromium.org85071062013-08-21 23:37:45 +000068# Absolute path to this file (can be None if running from zip on Mac).
tansella4949442016-06-23 22:34:32 -070069THIS_FILE_PATH = os.path.abspath(
70 __file__.decode(sys.getfilesystemencoding())) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000071
72# Directory that contains this file (might be inside zip package).
tansella4949442016-06-23 22:34:32 -070073BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__.decode(
74 sys.getfilesystemencoding()) else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000075
76# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000077if zip_package.get_main_script_path():
78 MAIN_DIR = os.path.dirname(
79 os.path.abspath(zip_package.get_main_script_path()))
80else:
81 # This happens when 'import run_isolated' is executed at the python
82 # interactive prompt, in that case __file__ is undefined.
83 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000084
maruele2f2cb82016-07-13 14:41:03 -070085
86# Magic variables that can be found in the isolate task command line.
87ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
88EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
89SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
90
91
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000092# The name of the log file to use.
93RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
94
maruele2f2cb82016-07-13 14:41:03 -070095
csharp@chromium.orge217f302012-11-22 16:51:53 +000096# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000097RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000098
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000099
maruele2f2cb82016-07-13 14:41:03 -0700100# Use short names for temporary directories. This is driven by Windows, which
101# imposes a relatively short maximum path length of 260 characters, often
102# referred to as MAX_PATH. It is relatively easy to create files with longer
103# path length. A use case is with recursive depedency treesV like npm packages.
104#
105# It is recommended to start the script with a `root_dir` as short as
106# possible.
107# - ir stands for isolated_run
108# - io stands for isolated_out
109# - it stands for isolated_tmp
110ISOLATED_RUN_DIR = u'ir'
111ISOLATED_OUT_DIR = u'io'
112ISOLATED_TMP_DIR = u'it'
113
114
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400115# Keep synced with task_request.py
116CACHE_NAME_RE = re.compile(ur'^[a-z0-9_]{1,4096}$')
117
118
marueld928c862017-06-08 08:20:04 -0700119OUTLIVING_ZOMBIE_MSG = """\
120*** Swarming tried multiple times to delete the %s directory and failed ***
121*** Hard failing the task ***
122
123Swarming detected that your testing script ran an executable, which may have
124started a child executable, and the main script returned early, leaving the
125children executables playing around unguided.
126
127You don't want to leave children processes outliving the task on the Swarming
128bot, do you? The Swarming bot doesn't.
129
130How to fix?
131- For any process that starts children processes, make sure all children
132 processes terminated properly before each parent process exits. This is
133 especially important in very deep process trees.
134 - This must be done properly both in normal successful task and in case of
135 task failure. Cleanup is very important.
136- The Swarming bot sends a SIGTERM in case of timeout.
137 - You have %s seconds to comply after the signal was sent to the process
138 before the process is forcibly killed.
139- To achieve not leaking children processes in case of signals on timeout, you
140 MUST handle signals in each executable / python script and propagate them to
141 children processes.
142 - When your test script (python or binary) receives a signal like SIGTERM or
143 CTRL_BREAK_EVENT on Windows), send it to all children processes and wait for
144 them to terminate before quitting.
145
146See
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -0400147https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Bot.md#Graceful-termination_aka-the-SIGTERM-and-SIGKILL-dance
marueld928c862017-06-08 08:20:04 -0700148for more information.
149
150*** May the SIGKILL force be with you ***
151"""
152
153
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000154# Currently hardcoded. Eventually could be exposed as a flag once there's value.
155# 3 weeks
156MAX_AGE_SECS = 21*24*60*60
157
158
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500159TaskData = collections.namedtuple(
160 'TaskData', [
161 # List of strings; the command line to use, independent of what was
162 # specified in the isolated file.
163 'command',
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500164 # Relative directory to start command into.
165 'relative_cwd',
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500166 # List of strings; the arguments to add to the command specified in the
167 # isolated file.
168 'extra_args',
169 # Hash of the .isolated file that must be retrieved to recreate the tree
170 # of files to run the target executable. The command specified in the
171 # .isolated is executed. Mutually exclusive with command argument.
172 'isolated_hash',
173 # isolateserver.Storage instance to retrieve remote objects. This object
174 # has a reference to an isolateserver.StorageApi, which does the actual
175 # I/O.
176 'storage',
177 # isolateserver.LocalCache instance to keep from retrieving the same
178 # objects constantly by caching the objects retrieved. Can be on-disk or
179 # in-memory.
180 'isolate_cache',
181 # List of paths relative to root_dir to put into the output isolated
182 # bundle upon task completion (see link_outputs_to_outdir).
183 'outputs',
184 # Function (run_dir) => context manager that installs named caches into
185 # |run_dir|.
186 'install_named_caches',
187 # If True, the temporary directory will be deliberately leaked for later
188 # examination.
189 'leak_temp_dir',
190 # Path to the directory to use to create the temporary directory. If not
191 # specified, a random temporary directory is created.
192 'root_dir',
193 # Kills the process if it lasts more than this amount of seconds.
194 'hard_timeout',
195 # Number of seconds to wait between SIGTERM and SIGKILL.
196 'grace_period',
197 # Path to a file with bot state, used in place of ${SWARMING_BOT_FILE}
198 # task command line argument.
199 'bot_file',
200 # Logical account to switch LUCI_CONTEXT into.
201 'switch_to_account',
202 # Context manager dir => CipdInfo, see install_client_and_packages.
203 'install_packages_fn',
204 # Create tree with symlinks instead of hardlinks.
205 'use_symlinks',
206 # Environment variables to set.
207 'env',
208 # Environment variables to mutate with relative directories.
209 # Example: {"ENV_KEY": ['relative', 'paths', 'to', 'prepend']}
210 'env_prefix'])
211
212
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000213def get_as_zip_package(executable=True):
214 """Returns ZipPackage with this module and all its dependencies.
215
216 If |executable| is True will store run_isolated.py as __main__.py so that
217 zip package is directly executable be python.
218 """
219 # Building a zip package when running from another zip package is
220 # unsupported and probably unneeded.
221 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +0000222 assert THIS_FILE_PATH
223 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000224 package = zip_package.ZipPackage(root=BASE_DIR)
225 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
aludwin81178302016-11-30 17:18:49 -0800226 package.add_python_file(os.path.join(BASE_DIR, 'isolate_storage.py'))
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400227 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000228 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800229 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
nodirbe642ff2016-06-09 15:51:51 -0700230 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
Marc-Antoine Ruel34f5f282018-05-16 16:04:31 -0400231 package.add_python_file(os.path.join(BASE_DIR, 'local_caching.py'))
tanselle4288c32016-07-28 09:45:40 -0700232 package.add_directory(os.path.join(BASE_DIR, 'libs'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000233 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
234 package.add_directory(os.path.join(BASE_DIR, 'utils'))
235 return package
236
237
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500238def _to_str(s):
239 """Downgrades a unicode instance to str. Pass str through as-is."""
240 if isinstance(s, str):
241 return s
242 # This is technically incorrect, especially on Windows. In theory
243 # sys.getfilesystemencoding() should be used to use the right 'ANSI code
244 # page' on Windows, but that causes other problems, as the character set
245 # is very limited.
246 return s.encode('utf-8')
247
248
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -0500249def _to_unicode(s):
250 """Upgrades a str instance to unicode. Pass unicode through as-is."""
251 if isinstance(s, unicode) or s is None:
252 return s
253 return s.decode('utf-8')
254
255
maruel03e11842016-07-14 10:50:16 -0700256def make_temp_dir(prefix, root_dir):
257 """Returns a new unique temporary directory."""
258 return unicode(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000259
260
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500261def change_tree_read_only(rootdir, read_only):
262 """Changes the tree read-only bits according to the read_only specification.
263
264 The flag can be 0, 1 or 2, which will affect the possibility to modify files
265 and create or delete files.
266 """
267 if read_only == 2:
268 # Files and directories (except on Windows) are marked read only. This
269 # inhibits modifying, creating or deleting files in the test directory,
270 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400271 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500272 elif read_only == 1:
273 # Files are marked read only but not the directories. This inhibits
274 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400275 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500276 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500277 # Anything can be modified.
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400278 # TODO(maruel): This is currently dangerous as long as
279 # DiskContentAddressedCache.touch() is not yet changed to verify the hash of
280 # the content of the files it is looking at, so that if a test modifies an
281 # input file, the file must be deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400282 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500283 else:
284 raise ValueError(
285 'change_tree_read_only(%s, %s): Unknown flag %s' %
286 (rootdir, read_only, read_only))
287
288
vadimsh9c54b2c2017-07-25 14:08:29 -0700289@contextlib.contextmanager
290def set_luci_context_account(account, tmp_dir):
291 """Sets LUCI_CONTEXT account to be used by the task.
292
293 If 'account' is None or '', does nothing at all. This happens when
294 run_isolated.py is called without '--switch-to-account' flag. In this case,
295 if run_isolated.py is running in some LUCI_CONTEXT environment, the task will
296 just inherit whatever account is already set. This may happen is users invoke
297 run_isolated.py explicitly from their code.
298
299 If the requested account is not defined in the context, switches to
300 non-authenticated access. This happens for Swarming tasks that don't use
301 'task' service accounts.
302
303 If not using LUCI_CONTEXT-based auth, does nothing.
304 If already running as requested account, does nothing.
305 """
306 if not account:
307 # Not actually switching.
308 yield
309 return
310
311 local_auth = luci_context.read('local_auth')
312 if not local_auth:
313 # Not using LUCI_CONTEXT auth at all.
314 yield
315 return
316
317 # See LUCI_CONTEXT.md for the format of 'local_auth'.
318 if local_auth.get('default_account_id') == account:
319 # Already set, no need to switch.
320 yield
321 return
322
323 available = {a['id'] for a in local_auth.get('accounts') or []}
324 if account in available:
325 logging.info('Switching default LUCI_CONTEXT account to %r', account)
326 local_auth['default_account_id'] = account
327 else:
328 logging.warning(
329 'Requested LUCI_CONTEXT account %r is not available (have only %r), '
330 'disabling authentication', account, sorted(available))
331 local_auth.pop('default_account_id', None)
332
333 with luci_context.write(_tmpdir=tmp_dir, local_auth=local_auth):
334 yield
335
336
nodir90bc8dc2016-06-15 13:35:21 -0700337def process_command(command, out_dir, bot_file):
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000338 """Replaces parameters in a command line.
nodirbe642ff2016-06-09 15:51:51 -0700339
340 Raises:
341 ValueError if a parameter is requested in |command| but its value is not
342 provided.
343 """
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000344 return [replace_parameters(arg, out_dir, bot_file) for arg in command]
345
346
347def replace_parameters(arg, out_dir, bot_file):
348 """Replaces parameter tokens with appropriate values in a string.
349
350 Raises:
351 ValueError if a parameter is requested in |arg| but its value is not
352 provided.
353 """
354 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
355 replace_slash = False
356 if ISOLATED_OUTDIR_PARAMETER in arg:
357 if not out_dir:
358 raise ValueError(
359 'output directory is requested in command or env var, but not '
360 'provided; please specify one')
361 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
362 replace_slash = True
363 if SWARMING_BOT_FILE_PARAMETER in arg:
364 if bot_file:
365 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
nodirbe642ff2016-06-09 15:51:51 -0700366 replace_slash = True
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000367 else:
368 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command or env '
369 'var, but no bot_file specified. Leaving parameter '
370 'unchanged.')
371 if replace_slash:
372 # Replace slashes only if parameters are present
373 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
374 arg = arg.replace('/', os.sep)
375 return arg
maruela9cfd6f2015-09-15 11:03:15 -0700376
377
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000378
379def get_command_env(tmp_dir, cipd_info, run_dir, env, env_prefixes, out_dir,
380 bot_file):
vadimsh232f5a82017-01-20 19:23:44 -0800381 """Returns full OS environment to run a command in.
382
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800383 Sets up TEMP, puts directory with cipd binary in front of PATH, exposes
384 CIPD_CACHE_DIR env var, and installs all env_prefixes.
vadimsh232f5a82017-01-20 19:23:44 -0800385
386 Args:
387 tmp_dir: temp directory.
388 cipd_info: CipdInfo object is cipd client is used, None if not.
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500389 run_dir: The root directory the isolated tree is mapped in.
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500390 env: environment variables to use
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800391 env_prefixes: {"ENV_KEY": ['cwd', 'relative', 'paths', 'to', 'prepend']}
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000392 out_dir: Isolated output directory. Required to be != None if any of the
393 env vars contain ISOLATED_OUTDIR_PARAMETER.
394 bot_file: Required to be != None if any of the env vars contain
395 SWARMING_BOT_FILE_PARAMETER.
vadimsh232f5a82017-01-20 19:23:44 -0800396 """
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500397 out = os.environ.copy()
398 for k, v in env.iteritems():
399 if not v:
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500400 out.pop(k, None)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500401 else:
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000402 out[k] = replace_parameters(v, out_dir, bot_file)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500403
404 if cipd_info:
405 bin_dir = os.path.dirname(cipd_info.client.binary_path)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500406 out['PATH'] = '%s%s%s' % (_to_str(bin_dir), os.pathsep, out['PATH'])
407 out['CIPD_CACHE_DIR'] = _to_str(cipd_info.cache_dir)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500408
409 for key, paths in env_prefixes.iteritems():
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500410 assert isinstance(paths, list), paths
411 paths = [os.path.normpath(os.path.join(run_dir, p)) for p in paths]
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500412 cur = out.get(key)
413 if cur:
414 paths.append(cur)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500415 out[key] = _to_str(os.path.pathsep.join(paths))
vadimsh232f5a82017-01-20 19:23:44 -0800416
Marc-Antoine Ruelefb30b12018-07-25 18:34:36 +0000417 tmp_dir = _to_str(tmp_dir)
418 # pylint: disable=line-too-long
419 # * python respects $TMPDIR, $TEMP, and $TMP in this order, regardless of
420 # platform. So $TMPDIR must be set on all platforms.
421 # https://github.com/python/cpython/blob/2.7/Lib/tempfile.py#L155
422 out['TMPDIR'] = tmp_dir
423 if sys.platform == 'win32':
424 # * chromium's base utils uses GetTempPath().
425 # https://cs.chromium.org/chromium/src/base/files/file_util_win.cc?q=GetTempPath
426 # * Go uses GetTempPath().
427 # * GetTempDir() uses %TMP%, then %TEMP%, then other stuff. So %TMP% must be
428 # set.
429 # https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-gettemppathw
430 out['TMP'] = tmp_dir
431 # https://blogs.msdn.microsoft.com/oldnewthing/20150417-00/?p=44213
432 out['TEMP'] = tmp_dir
433 elif sys.platform == 'darwin':
434 # * Chromium uses an hack on macOS before calling into
435 # NSTemporaryDirectory().
436 # https://cs.chromium.org/chromium/src/base/files/file_util_mac.mm?q=GetTempDir
437 # https://developer.apple.com/documentation/foundation/1409211-nstemporarydirectory
438 out['MAC_CHROMIUM_TMPDIR'] = tmp_dir
439 else:
440 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
441 # http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
442 # * mktemp on linux respects $TMPDIR.
443 # * Chromium respects $TMPDIR on linux.
444 # https://cs.chromium.org/chromium/src/base/files/file_util_posix.cc?q=GetTempDir
445 # * Go uses $TMPDIR.
446 # https://go.googlesource.com/go/+/go1.10.3/src/os/file_unix.go#307
447 pass
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500448 return out
vadimsh232f5a82017-01-20 19:23:44 -0800449
450
451def run_command(command, cwd, env, hard_timeout, grace_period):
maruel6be7f9e2015-10-01 12:25:30 -0700452 """Runs the command.
453
454 Returns:
455 tuple(process exit code, bool if had a hard timeout)
456 """
maruela9cfd6f2015-09-15 11:03:15 -0700457 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700458
maruel6be7f9e2015-10-01 12:25:30 -0700459 exit_code = None
460 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700461 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700462 proc = None
463 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700464 try:
maruel6be7f9e2015-10-01 12:25:30 -0700465 # TODO(maruel): This code is imperfect. It doesn't handle well signals
466 # during the download phase and there's short windows were things can go
467 # wrong.
468 def handler(signum, _frame):
469 if proc and not had_signal:
470 logging.info('Received signal %d', signum)
471 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700472 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700473
474 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
475 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
476 try:
477 exit_code = proc.wait(hard_timeout or None)
478 except subprocess42.TimeoutExpired:
479 if not had_signal:
480 logging.warning('Hard timeout')
481 had_hard_timeout = True
482 logging.warning('Sending SIGTERM')
483 proc.terminate()
484
485 # Ignore signals in grace period. Forcibly give the grace period to the
486 # child process.
487 if exit_code is None:
488 ignore = lambda *_: None
489 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
490 try:
491 exit_code = proc.wait(grace_period or None)
492 except subprocess42.TimeoutExpired:
493 # Now kill for real. The user can distinguish between the
494 # following states:
495 # - signal but process exited within grace period,
496 # hard_timed_out will be set but the process exit code will be
497 # script provided.
498 # - processed exited late, exit code will be -9 on posix.
499 logging.warning('Grace exhausted; sending SIGKILL')
500 proc.kill()
martiniss5c8043e2017-08-01 17:09:43 -0700501 logging.info('Waiting for process exit')
maruel6be7f9e2015-10-01 12:25:30 -0700502 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700503 except OSError:
504 # This is not considered to be an internal error. The executable simply
505 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800506 sys.stderr.write(
507 '<The executable does not exist or a dependent library is missing>\n'
508 '<Check for missing .so/.dll in the .isolate or GN file>\n'
509 '<Command: %s>\n' % command)
510 if os.environ.get('SWARMING_TASK_ID'):
511 # Give an additional hint when running as a swarming task.
512 sys.stderr.write(
513 '<See the task\'s page for commands to help diagnose this issue '
514 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700515 exit_code = 1
516 logging.info(
517 'Command finished with exit code %d (%s)',
518 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700519 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700520
521
maruel4409e302016-07-19 14:25:51 -0700522def fetch_and_map(isolated_hash, storage, cache, outdir, use_symlinks):
523 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700524 start = time.time()
525 bundle = isolateserver.fetch_isolated(
526 isolated_hash=isolated_hash,
527 storage=storage,
528 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700529 outdir=outdir,
530 use_symlinks=use_symlinks)
nodir6f801882016-04-29 14:41:50 -0700531 return bundle, {
532 'duration': time.time() - start,
nodir6f801882016-04-29 14:41:50 -0700533 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
534 'items_hot': base64.b64encode(
tansell9e04a8d2016-07-28 09:31:59 -0700535 large.pack(sorted(set(cache.used) - set(cache.added)))),
nodir6f801882016-04-29 14:41:50 -0700536 }
537
538
aludwin0a8e17d2016-10-27 15:57:39 -0700539def link_outputs_to_outdir(run_dir, out_dir, outputs):
540 """Links any named outputs to out_dir so they can be uploaded.
541
542 Raises an error if the file already exists in that directory.
543 """
544 if not outputs:
545 return
546 isolateserver.create_directories(out_dir, outputs)
547 for o in outputs:
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -0400548 copy_recursively(os.path.join(run_dir, o), os.path.join(out_dir, o))
549
550
551def copy_recursively(src, dst):
552 """Efficiently copies a file or directory from src_dir to dst_dir.
553
554 `item` may be a file, directory, or a symlink to a file or directory.
555 All symlinks are replaced with their targets, so the resulting
556 directory structure in dst_dir will never have any symlinks.
557
558 To increase speed, copy_recursively hardlinks individual files into the
559 (newly created) directory structure if possible, unlike Python's
560 shutil.copytree().
561 """
562 orig_src = src
563 try:
564 # Replace symlinks with their final target.
565 while fs.islink(src):
566 res = fs.readlink(src)
567 src = os.path.join(os.path.dirname(src), res)
568 # TODO(sadafm): Explicitly handle cyclic symlinks.
569
570 # Note that fs.isfile (which is a wrapper around os.path.isfile) throws
571 # an exception if src does not exist. A warning will be logged in that case.
572 if fs.isfile(src):
573 file_path.link_file(dst, src, file_path.HARDLINK_WITH_FALLBACK)
574 return
575
576 if not fs.exists(dst):
577 os.makedirs(dst)
578
579 for child in fs.listdir(src):
580 copy_recursively(os.path.join(src, child), os.path.join(dst, child))
581
582 except OSError as e:
583 if e.errno == errno.ENOENT:
584 logging.warning('Path %s does not exist or %s is a broken symlink',
585 src, orig_src)
586 else:
587 logging.info("Couldn't collect output file %s: %s", src, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700588
589
maruela9cfd6f2015-09-15 11:03:15 -0700590def delete_and_upload(storage, out_dir, leak_temp_dir):
591 """Deletes the temporary run directory and uploads results back.
592
593 Returns:
nodir6f801882016-04-29 14:41:50 -0700594 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700595 - outputs_ref: a dict referring to the results archived back to the isolated
596 server, if applicable.
597 - success: False if something occurred that means that the task must
598 forcibly be considered a failure, e.g. zombie processes were left
599 behind.
nodir6f801882016-04-29 14:41:50 -0700600 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700601 """
maruela9cfd6f2015-09-15 11:03:15 -0700602 # Upload out_dir and generate a .isolated file out of this directory. It is
603 # only done if files were written in the directory.
604 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700605 cold = []
606 hot = []
nodir6f801882016-04-29 14:41:50 -0700607 start = time.time()
608
maruel12e30012015-10-09 11:55:35 -0700609 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700610 with tools.Profiler('ArchiveOutput'):
611 try:
maruel064c0a32016-04-05 11:47:15 -0700612 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700613 storage, [out_dir], None)
614 outputs_ref = {
615 'isolated': results[0][0],
616 'isolatedserver': storage.location,
617 'namespace': storage.namespace,
618 }
maruel064c0a32016-04-05 11:47:15 -0700619 cold = sorted(i.size for i in f_cold)
620 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700621 except isolateserver.Aborted:
622 # This happens when a signal SIGTERM was received while uploading data.
623 # There is 2 causes:
624 # - The task was too slow and was about to be killed anyway due to
625 # exceeding the hard timeout.
626 # - The amount of data uploaded back is very large and took too much
627 # time to archive.
628 sys.stderr.write('Received SIGTERM while uploading')
629 # Re-raise, so it will be treated as an internal failure.
630 raise
nodir6f801882016-04-29 14:41:50 -0700631
632 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700633 try:
maruel12e30012015-10-09 11:55:35 -0700634 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700635 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700636 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700637 else:
638 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700639 except OSError as e:
640 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700641 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700642 stats = {
643 'duration': time.time() - start,
644 'items_cold': base64.b64encode(large.pack(cold)),
645 'items_hot': base64.b64encode(large.pack(hot)),
646 }
647 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700648
649
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500650def map_and_run(data, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700651 """Runs a command with optional isolated input/output.
652
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500653 Arguments:
654 - data: TaskData instance.
655 - constant_run_path: TODO
nodir55be77b2016-05-03 09:39:57 -0700656
657 Returns metadata about the result.
658 """
maruela9cfd6f2015-09-15 11:03:15 -0700659 result = {
maruel064c0a32016-04-05 11:47:15 -0700660 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700661 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700662 'had_hard_timeout': False,
Seth Koehler49139812017-12-19 13:59:33 -0500663 'internal_failure': 'run_isolated did not complete properly',
maruel064c0a32016-04-05 11:47:15 -0700664 'stats': {
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000665 'isolated': {
666 #'cipd': {
667 # 'duration': 0.,
668 # 'get_client_duration': 0.,
669 #},
670 'download': {
671 #'duration': 0.,
672 'initial_number_items': len(data.isolate_cache),
673 'initial_size': data.isolate_cache.total_size,
674 #'items_cold': '<large.pack()>',
675 #'items_hot': '<large.pack()>',
676 },
677 #'upload': {
678 # 'duration': 0.,
679 # 'items_cold': '<large.pack()>',
680 # 'items_hot': '<large.pack()>',
681 #},
682 },
maruel064c0a32016-04-05 11:47:15 -0700683 },
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000684 #'cipd_pins': {
685 # 'packages': [
686 # {'package_name': ..., 'version': ..., 'path': ...},
687 # ...
688 # ],
689 # 'client_package': {'package_name': ..., 'version': ...},
690 #},
maruela9cfd6f2015-09-15 11:03:15 -0700691 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700692 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700693 }
nodirbe642ff2016-06-09 15:51:51 -0700694
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500695 if data.root_dir:
696 file_path.ensure_tree(data.root_dir, 0700)
697 elif data.isolate_cache.cache_dir:
698 data = data._replace(
699 root_dir=os.path.dirname(data.isolate_cache.cache_dir))
maruele2f2cb82016-07-13 14:41:03 -0700700 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700701 # If root_dir is not specified, it is not constant.
702 # TODO(maruel): This is not obvious. Change this to become an error once we
703 # make the constant_run_path an exposed flag.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500704 if constant_run_path and data.root_dir:
705 run_dir = os.path.join(data.root_dir, ISOLATED_RUN_DIR)
maruel5c4eed82017-05-26 05:33:40 -0700706 if os.path.isdir(run_dir):
707 file_path.rmtree(run_dir)
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500708 os.mkdir(run_dir, 0700)
maruelcffa0542017-04-07 08:39:20 -0700709 else:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500710 run_dir = make_temp_dir(ISOLATED_RUN_DIR, data.root_dir)
maruel03e11842016-07-14 10:50:16 -0700711 # storage should be normally set but don't crash if it is not. This can happen
712 # as Swarming task can run without an isolate server.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500713 out_dir = make_temp_dir(
714 ISOLATED_OUT_DIR, data.root_dir) if data.storage else None
715 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, data.root_dir)
nodir55be77b2016-05-03 09:39:57 -0700716 cwd = run_dir
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500717 if data.relative_cwd:
718 cwd = os.path.normpath(os.path.join(cwd, data.relative_cwd))
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500719 command = data.command
nodir55be77b2016-05-03 09:39:57 -0700720 try:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500721 with data.install_packages_fn(run_dir) as cipd_info:
vadimsh232f5a82017-01-20 19:23:44 -0800722 if cipd_info:
723 result['stats']['cipd'] = cipd_info.stats
724 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700725
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500726 if data.isolated_hash:
vadimsh232f5a82017-01-20 19:23:44 -0800727 isolated_stats = result['stats'].setdefault('isolated', {})
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000728 bundle, stats = fetch_and_map(
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500729 isolated_hash=data.isolated_hash,
730 storage=data.storage,
731 cache=data.isolate_cache,
vadimsh232f5a82017-01-20 19:23:44 -0800732 outdir=run_dir,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500733 use_symlinks=data.use_symlinks)
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000734 isolated_stats['download'].update(stats)
vadimsh232f5a82017-01-20 19:23:44 -0800735 change_tree_read_only(run_dir, bundle.read_only)
maruelabec63c2017-04-26 11:53:24 -0700736 # Inject the command
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500737 if not command and bundle.command:
738 command = bundle.command + data.extra_args
Marc-Antoine Rueld704a1f2017-10-31 10:51:23 -0400739 # Only set the relative directory if the isolated file specified a
740 # command, and no raw command was specified.
741 if bundle.relative_cwd:
742 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
maruelabec63c2017-04-26 11:53:24 -0700743
744 if not command:
745 # Handle this as a task failure, not an internal failure.
746 sys.stderr.write(
747 '<No command was specified!>\n'
748 '<Please secify a command when triggering your Swarming task>\n')
749 result['exit_code'] = 1
750 return result
nodirbe642ff2016-06-09 15:51:51 -0700751
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500752 if not cwd.startswith(run_dir):
753 # Handle this as a task failure, not an internal failure. This is a
754 # 'last chance' way to gate against directory escape.
755 sys.stderr.write('<Relative CWD is outside of run directory!>\n')
756 result['exit_code'] = 1
757 return result
758
759 if not os.path.isdir(cwd):
760 # Accepts relative_cwd that does not exist.
761 os.makedirs(cwd, 0700)
762
vadimsh232f5a82017-01-20 19:23:44 -0800763 # If we have an explicit list of files to return, make sure their
764 # directories exist now.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500765 if data.storage and data.outputs:
766 isolateserver.create_directories(run_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700767
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500768 with data.install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -0800769 sys.stdout.flush()
770 start = time.time()
771 try:
vadimsh9c54b2c2017-07-25 14:08:29 -0700772 # Need to switch the default account before 'get_command_env' call,
773 # so it can grab correct value of LUCI_CONTEXT env var.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500774 with set_luci_context_account(data.switch_to_account, tmp_dir):
775 env = get_command_env(
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000776 tmp_dir, cipd_info, run_dir, data.env, data.env_prefix, out_dir,
777 data.bot_file)
Robert Iannucci24ae76a2018-02-26 12:51:18 -0800778 command = tools.fix_python_cmd(command, env)
779 command = process_command(command, out_dir, data.bot_file)
780 file_path.ensure_command_has_abs_path(command, cwd)
781
vadimsh9c54b2c2017-07-25 14:08:29 -0700782 result['exit_code'], result['had_hard_timeout'] = run_command(
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500783 command, cwd, env, data.hard_timeout, data.grace_period)
nodird6160682017-02-02 13:03:35 -0800784 finally:
785 result['duration'] = max(time.time() - start, 0)
Seth Koehler49139812017-12-19 13:59:33 -0500786
787 # We successfully ran the command, set internal_failure back to
788 # None (even if the command failed, it's not an internal error).
789 result['internal_failure'] = None
maruela9cfd6f2015-09-15 11:03:15 -0700790 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700791 # An internal error occurred. Report accordingly so the swarming task will
792 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700793 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700794 result['internal_failure'] = str(e)
795 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700796
797 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700798 finally:
799 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700800 # Try to link files to the output directory, if specified.
801 if out_dir:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500802 link_outputs_to_outdir(run_dir, out_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700803
nodir32a1ec12016-10-26 18:34:07 -0700804 success = False
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500805 if data.leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700806 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700807 logging.warning(
808 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700809 else:
maruel84537cb2015-10-16 14:21:28 -0700810 # On Windows rmtree(run_dir) call above has a synchronization effect: it
811 # finishes only when all task child processes terminate (since a running
812 # process locks *.exe file). Examine out_dir only after that call
813 # completes (since child processes may write to out_dir too and we need
814 # to wait for them to finish).
815 if fs.isdir(run_dir):
816 try:
817 success = file_path.rmtree(run_dir)
818 except OSError as e:
Marc-Antoine Ruel44699b32018-09-24 23:31:50 +0000819 logging.error('rmtree(%r) failed: %s', run_dir, e)
maruel84537cb2015-10-16 14:21:28 -0700820 success = False
821 if not success:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500822 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('run', data.grace_period))
maruel84537cb2015-10-16 14:21:28 -0700823 if result['exit_code'] == 0:
824 result['exit_code'] = 1
825 if fs.isdir(tmp_dir):
826 try:
827 success = file_path.rmtree(tmp_dir)
828 except OSError as e:
Marc-Antoine Ruel44699b32018-09-24 23:31:50 +0000829 logging.error('rmtree(%r) failed: %s', tmp_dir, e)
maruel84537cb2015-10-16 14:21:28 -0700830 success = False
831 if not success:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500832 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('temp', data.grace_period))
maruel84537cb2015-10-16 14:21:28 -0700833 if result['exit_code'] == 0:
834 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700835
marueleb5fbee2015-09-17 13:01:36 -0700836 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700837 if out_dir:
nodir55715712016-06-03 12:28:19 -0700838 isolated_stats = result['stats'].setdefault('isolated', {})
839 result['outputs_ref'], success, isolated_stats['upload'] = (
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500840 delete_and_upload(data.storage, out_dir, data.leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700841 if not success and result['exit_code'] == 0:
842 result['exit_code'] = 1
843 except Exception as e:
844 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700845 if out_dir:
846 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700847 result['internal_failure'] = str(e)
848 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500849
850
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500851def run_tha_test(data, result_json):
nodir55be77b2016-05-03 09:39:57 -0700852 """Runs an executable and records execution metadata.
853
nodir55be77b2016-05-03 09:39:57 -0700854 If isolated_hash is specified, downloads the dependencies in the cache,
855 hardlinks them into a temporary directory and runs the command specified in
856 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500857
858 A temporary directory is created to hold the output files. The content inside
859 this directory will be uploaded back to |storage| packaged as a .isolated
860 file.
861
862 Arguments:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500863 - data: TaskData instance.
864 - result_json: File path to dump result metadata into. If set, the process
865 exit code is always 0 unless an internal error occurred.
maruela9cfd6f2015-09-15 11:03:15 -0700866
867 Returns:
868 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000869 """
maruela76b9ee2015-12-15 06:18:08 -0800870 if result_json:
871 # Write a json output file right away in case we get killed.
872 result = {
873 'exit_code': None,
874 'had_hard_timeout': False,
875 'internal_failure': 'Was terminated before completion',
876 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700877 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800878 }
879 tools.write_json(result_json, result, dense=True)
880
maruela9cfd6f2015-09-15 11:03:15 -0700881 # run_isolated exit code. Depends on if result_json is used or not.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500882 result = map_and_run(data, True)
maruela9cfd6f2015-09-15 11:03:15 -0700883 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700884
maruela9cfd6f2015-09-15 11:03:15 -0700885 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700886 # We've found tests to delete 'work' when quitting, causing an exception
887 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700888 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700889 tools.write_json(result_json, result, dense=True)
890 # Only return 1 if there was an internal error.
891 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000892
maruela9cfd6f2015-09-15 11:03:15 -0700893 # Marshall into old-style inline output.
894 if result['outputs_ref']:
895 data = {
896 'hash': result['outputs_ref']['isolated'],
897 'namespace': result['outputs_ref']['namespace'],
898 'storage': result['outputs_ref']['isolatedserver'],
899 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500900 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700901 print(
902 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
903 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800904 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700905 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000906
907
iannuccib58d10d2017-03-18 02:00:25 -0700908# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800909CipdInfo = collections.namedtuple('CipdInfo', [
910 'client', # cipd.CipdClient object
911 'cache_dir', # absolute path to bot-global cipd tag and instance cache
912 'stats', # dict with stats to return to the server
913 'pins', # dict with installed cipd pins to return to the server
914])
915
916
917@contextlib.contextmanager
918def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700919 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800920 yield None
921
922
iannuccib58d10d2017-03-18 02:00:25 -0700923def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
924 """Calls 'cipd ensure' for packages.
925
926 Args:
927 run_dir (str): root of installation.
928 cipd_cache_dir (str): the directory to use for the cipd package cache.
929 client (CipdClient): the cipd client to use
930 packages: packages to install, list [(path, package_name, version), ...].
931 timeout: max duration in seconds that this function can take.
932
933 Returns: list of pinned packages. Looks like [
934 {
935 'path': 'subdirectory',
936 'package_name': 'resolved/package/name',
937 'version': 'deadbeef...',
938 },
939 ...
940 ]
941 """
942 package_pins = [None]*len(packages)
943 def insert_pin(path, name, version, idx):
944 package_pins[idx] = {
945 'package_name': name,
946 # swarming deals with 'root' as '.'
947 'path': path or '.',
948 'version': version,
949 }
950
951 by_path = collections.defaultdict(list)
952 for i, (path, name, version) in enumerate(packages):
953 # cipd deals with 'root' as ''
954 if path == '.':
955 path = ''
956 by_path[path].append((name, version, i))
957
958 pins = client.ensure(
959 run_dir,
960 {
961 subdir: [(name, vers) for name, vers, _ in pkgs]
962 for subdir, pkgs in by_path.iteritems()
963 },
964 cache_dir=cipd_cache_dir,
965 timeout=timeout,
966 )
967
968 for subdir, pin_list in sorted(pins.iteritems()):
969 this_subdir = by_path[subdir]
970 for i, (name, version) in enumerate(pin_list):
971 insert_pin(subdir, name, version, this_subdir[i][2])
972
Robert Iannucci461b30d2017-12-13 11:34:03 -0800973 assert None not in package_pins, (packages, pins, package_pins)
iannuccib58d10d2017-03-18 02:00:25 -0700974
975 return package_pins
976
977
vadimsh232f5a82017-01-20 19:23:44 -0800978@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700979def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700980 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800981 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800982 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700983
vadimsh232f5a82017-01-20 19:23:44 -0800984 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
985
986 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700987 [
988 {
989 "path": path, "package_name": package_name, "version": version,
990 },
991 ...
992 ]
vadimsh902948e2017-01-20 15:57:32 -0800993 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700994
995 such that they correspond 1:1 to all input package arguments from the command
996 line. These dictionaries make their all the way back to swarming, where they
997 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700998
vadimsh902948e2017-01-20 15:57:32 -0800999 If 'packages' list is empty, will bootstrap CIPD client, but won't install
1000 any packages.
1001
1002 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -08001003 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -08001004
nodirbe642ff2016-06-09 15:51:51 -07001005 Args:
nodir90bc8dc2016-06-15 13:35:21 -07001006 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -08001007 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -07001008 service_url (str): CIPD server url, e.g.
1009 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -07001010 client_package_name (str): CIPD package name of CIPD client.
1011 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -07001012 cache_dir (str): where to keep cache of cipd clients, packages and tags.
1013 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -07001014 """
1015 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -07001016
nodirbe642ff2016-06-09 15:51:51 -07001017 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -07001018 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -07001019
vadimsh902948e2017-01-20 15:57:32 -08001020 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -08001021 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -07001022 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -08001023 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -07001024
nodirbe642ff2016-06-09 15:51:51 -07001025 get_client_start = time.time()
1026 client_manager = cipd.get_client(
1027 service_url, client_package_name, client_version, cache_dir,
1028 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -07001029
nodirbe642ff2016-06-09 15:51:51 -07001030 with client_manager as client:
1031 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -07001032
iannuccib58d10d2017-03-18 02:00:25 -07001033 package_pins = []
1034 if packages:
1035 package_pins = _install_packages(
1036 run_dir, cipd_cache_dir, client, packages, timeoutfn())
1037
1038 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -07001039
vadimsh232f5a82017-01-20 19:23:44 -08001040 total_duration = time.time() - start
1041 logging.info(
1042 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -07001043
vadimsh232f5a82017-01-20 19:23:44 -08001044 yield CipdInfo(
1045 client=client,
1046 cache_dir=cipd_cache_dir,
1047 stats={
1048 'duration': total_duration,
1049 'get_client_duration': get_client_duration,
1050 },
1051 pins={
iannuccib58d10d2017-03-18 02:00:25 -07001052 'client_package': {
1053 'package_name': client.package_name,
1054 'version': client.instance_id,
1055 },
vadimsh232f5a82017-01-20 19:23:44 -08001056 'packages': package_pins,
1057 })
nodirbe642ff2016-06-09 15:51:51 -07001058
1059
1060def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001061 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -07001062 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +00001063 version=__version__,
1064 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -07001065 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -07001066 '--clean', action='store_true',
1067 help='Cleans the cache, trimming it necessary and remove corrupted items '
1068 'and returns without executing anything; use with -v to know what '
1069 'was done')
1070 parser.add_option(
maruel4409e302016-07-19 14:25:51 -07001071 '--use-symlinks', action='store_true',
1072 help='Use symlinks instead of hardlinks')
1073 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -07001074 '--json',
1075 help='dump output metadata to json file. When used, run_isolated returns '
1076 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -07001077 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001078 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -07001079 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001080 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -07001081 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -07001082 parser.add_option(
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001083 '--raw-cmd', action='store_true',
1084 help='Ignore the isolated command, use the one supplied at the command '
1085 'line')
1086 parser.add_option(
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001087 '--relative-cwd',
1088 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
1089 'requires --raw-cmd')
1090 parser.add_option(
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001091 '--env', default=[], action='append',
1092 help='Environment variables to set for the child process')
1093 parser.add_option(
1094 '--env-prefix', default=[], action='append',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001095 help='Specify a VAR=./path/fragment to put in the environment variable '
1096 'before executing the command. The path fragment must be relative '
1097 'to the isolated run directory, and must not contain a `..` token. '
1098 'The path will be made absolute and prepended to the indicated '
1099 '$VAR using the OS\'s path separator. Multiple items for the same '
1100 '$VAR will be prepended in order.')
1101 parser.add_option(
bpastene3ae09522016-06-10 17:12:59 -07001102 '--bot-file',
1103 help='Path to a file describing the state of the host. The content is '
1104 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -07001105 parser.add_option(
vadimsh9c54b2c2017-07-25 14:08:29 -07001106 '--switch-to-account',
1107 help='If given, switches LUCI_CONTEXT to given logical service account '
1108 '(e.g. "task" or "system") before launching the isolated process.')
1109 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -07001110 '--output', action='append',
1111 help='Specifies an output to return. If no outputs are specified, all '
1112 'files located in $(ISOLATED_OUTDIR) will be returned; '
1113 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
1114 'specified by --output option (there can be multiple) will be '
1115 'returned. Note that if a file in OUT_DIR has the same path '
1116 'as an --output option, the --output version will be returned.')
1117 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -07001118 '-a', '--argsfile',
1119 # This is actually handled in parse_args; it's included here purely so it
1120 # can make it into the help text.
1121 help='Specify a file containing a JSON array of arguments to this '
1122 'script. If --argsfile is provided, no other argument may be '
1123 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001124 data_group = optparse.OptionGroup(parser, 'Data source')
1125 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001126 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -07001127 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001128 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001129 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001130
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -04001131 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001132
1133 cipd.add_cipd_options(parser)
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001134
1135 group = optparse.OptionGroup(parser, 'Named caches')
1136 group.add_option(
1137 '--named-cache',
1138 dest='named_caches',
1139 action='append',
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001140 nargs=3,
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001141 default=[],
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001142 help='A named cache to request. Accepts 3 arguments: name, path, hint. '
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001143 'name identifies the cache, must match regex [a-z0-9_]{1,4096}. '
1144 'path is a path relative to the run dir where the cache directory '
1145 'must be put to. '
1146 'This option can be specified more than once.')
1147 group.add_option(
1148 '--named-cache-root', default='named_caches',
1149 help='Cache root directory. Default=%default')
1150 parser.add_option_group(group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001151
Kenneth Russell61d42352014-09-15 11:41:16 -07001152 debug_group = optparse.OptionGroup(parser, 'Debugging')
1153 debug_group.add_option(
1154 '--leak-temp-dir',
1155 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -07001156 help='Deliberately leak isolate\'s temp dir for later examination. '
1157 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -07001158 debug_group.add_option(
1159 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -07001160 parser.add_option_group(debug_group)
1161
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001162 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001163
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001164 parser.set_defaults(cache='cache', cipd_cache='cipd_cache')
nodirbe642ff2016-06-09 15:51:51 -07001165 return parser
1166
1167
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001168def process_named_cache_options(parser, options, time_fn=None):
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001169 """Validates named cache options and returns a CacheManager."""
1170 if options.named_caches and not options.named_cache_root:
1171 parser.error('--named-cache is specified, but --named-cache-root is empty')
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001172 for name, path, hint in options.named_caches:
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001173 if not CACHE_NAME_RE.match(name):
1174 parser.error(
1175 'cache name %r does not match %r' % (name, CACHE_NAME_RE.pattern))
1176 if not path:
1177 parser.error('cache path cannot be empty')
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001178 try:
1179 long(hint)
1180 except ValueError:
1181 parser.error('cache hint must be a number')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001182 if options.named_cache_root:
1183 # Make these configurable later if there is use case but for now it's fairly
1184 # safe values.
1185 # In practice, a fair chunk of bots are already recycled on a daily schedule
1186 # so this code doesn't have any effect to them, unless they are preloaded
1187 # with a really old cache.
1188 policies = local_caching.CachePolicies(
1189 # 1TiB.
1190 max_cache_size=1024*1024*1024*1024,
1191 min_free_space=options.min_free_space,
1192 max_items=50,
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +00001193 max_age_secs=MAX_AGE_SECS)
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001194 root_dir = unicode(os.path.abspath(options.named_cache_root))
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001195 return local_caching.NamedCache(root_dir, policies, time_fn=time_fn)
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001196 return None
1197
1198
aludwin7556e0c2016-10-26 08:46:10 -07001199def parse_args(args):
1200 # Create a fake mini-parser just to get out the "-a" command. Note that
1201 # it's not documented here; instead, it's documented in create_option_parser
1202 # even though that parser will never actually get to parse it. This is
1203 # because --argsfile is exclusive with all other options and arguments.
1204 file_argparse = argparse.ArgumentParser(add_help=False)
1205 file_argparse.add_argument('-a', '--argsfile')
1206 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
1207 if file_args.argsfile:
1208 if nonfile_args:
1209 file_argparse.error('Can\'t specify --argsfile with'
1210 'any other arguments (%s)' % nonfile_args)
1211 try:
1212 with open(file_args.argsfile, 'r') as f:
1213 args = json.load(f)
1214 except (IOError, OSError, ValueError) as e:
1215 # We don't need to error out here - "args" is now empty,
1216 # so the call below to parser.parse_args(args) will fail
1217 # and print the full help text.
1218 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
1219
1220 # Even if we failed to read the args, just call the normal parser now since it
1221 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -07001222 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001223 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -07001224 return (parser, options, args)
1225
1226
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001227def _calc_named_cache_hint(named_cache, named_caches):
1228 """Returns the expected size of the missing named caches."""
1229 present = named_cache.available
1230 size = 0
1231 for name, _, hint in named_caches:
1232 if name not in present:
1233 hint = long(hint)
1234 if hint > 0:
1235 size += hint
1236 return size
1237
1238
aludwin7556e0c2016-10-26 08:46:10 -07001239def main(args):
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -05001240 # Warning: when --argsfile is used, the strings are unicode instances, when
1241 # parsed normally, the strings are str instances.
aludwin7556e0c2016-10-26 08:46:10 -07001242 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -07001243
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001244 if not file_path.enable_symlink():
1245 logging.error('Symlink support is not enabled')
1246
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001247 named_cache = process_named_cache_options(parser, options)
Marc-Antoine Ruel0d8b0f62018-09-10 14:40:35 +00001248 # hint is 0 if there's no named cache.
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001249 hint = _calc_named_cache_hint(named_cache, options.named_caches)
1250 if hint:
1251 # Increase the --min-free-space value by the hint, and recreate the
1252 # NamedCache instance so it gets the updated CachePolicy.
1253 options.min_free_space += hint
1254 named_cache = process_named_cache_options(parser, options)
1255
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001256 # TODO(maruel): CIPD caches should be defined at an higher level here too, so
1257 # they can be cleaned the same way.
nodirf33b8d62016-10-26 22:34:58 -07001258 isolate_cache = isolateserver.process_cache_options(options, trim=False)
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001259 caches = []
1260 if isolate_cache:
1261 caches.append(isolate_cache)
1262 if named_cache:
1263 caches.append(named_cache)
1264 root = caches[0].cache_dir if caches else unicode(os.getcwd())
maruel36a963d2016-04-08 17:15:49 -07001265 if options.clean:
1266 if options.isolated:
1267 parser.error('Can\'t use --isolated with --clean.')
1268 if options.isolate_server:
1269 parser.error('Can\'t use --isolate-server with --clean.')
1270 if options.json:
1271 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -07001272 if options.named_caches:
1273 parser.error('Can\t use --named-cache with --clean.')
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001274 # Trim first, then clean.
1275 local_caching.trim_caches(
1276 caches,
1277 root,
1278 min_free_space=options.min_free_space,
1279 max_age_secs=MAX_AGE_SECS)
1280 for c in caches:
Marc-Antoine Ruel87fc2222018-06-18 13:09:24 +00001281 c.cleanup()
maruel36a963d2016-04-08 17:15:49 -07001282 return 0
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001283
1284 # Trim must still be done for the following case:
1285 # - named-cache was used
1286 # - some entries, with a large hint, where missing
1287 # - --min-free-space was increased accordingly, thus trimming is needed
1288 # Otherwise, this will have no effect, as bot_main calls run_isolated with
1289 # --clean after each task.
1290 if hint:
1291 logging.info('Additional trimming of %d bytes', hint)
Marc-Antoine Ruel0d8b0f62018-09-10 14:40:35 +00001292 local_caching.trim_caches(
1293 caches,
1294 root,
1295 min_free_space=options.min_free_space,
1296 max_age_secs=MAX_AGE_SECS)
maruel36a963d2016-04-08 17:15:49 -07001297
nodir55be77b2016-05-03 09:39:57 -07001298 if not options.isolated and not args:
1299 parser.error('--isolated or command to run is required.')
1300
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001301 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001302
1303 isolateserver.process_isolate_server_options(
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001304 parser, options, True, False)
nodir55be77b2016-05-03 09:39:57 -07001305 if not options.isolate_server:
1306 if options.isolated:
1307 parser.error('--isolated requires --isolate-server')
1308 if ISOLATED_OUTDIR_PARAMETER in args:
1309 parser.error(
1310 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001311
nodir90bc8dc2016-06-15 13:35:21 -07001312 if options.root_dir:
1313 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -07001314 if options.json:
1315 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001316
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001317 if any('=' not in i for i in options.env):
1318 parser.error(
1319 '--env required key=value form. value can be skipped to delete '
1320 'the variable')
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001321 options.env = dict(i.split('=', 1) for i in options.env)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001322
1323 prefixes = {}
1324 cwd = os.path.realpath(os.getcwd())
1325 for item in options.env_prefix:
1326 if '=' not in item:
1327 parser.error(
1328 '--env-prefix %r is malformed, must be in the form `VAR=./path`'
1329 % item)
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001330 key, opath = item.split('=', 1)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001331 if os.path.isabs(opath):
1332 parser.error('--env-prefix %r path is bad, must be relative.' % opath)
1333 opath = os.path.normpath(opath)
1334 if not os.path.realpath(os.path.join(cwd, opath)).startswith(cwd):
1335 parser.error(
1336 '--env-prefix %r path is bad, must be relative and not contain `..`.'
1337 % opath)
1338 prefixes.setdefault(key, []).append(opath)
1339 options.env_prefix = prefixes
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001340
nodirbe642ff2016-06-09 15:51:51 -07001341 cipd.validate_cipd_options(parser, options)
1342
vadimsh232f5a82017-01-20 19:23:44 -08001343 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -08001344 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -07001345 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -08001346 run_dir, cipd.parse_package_args(options.cipd_packages),
1347 options.cipd_server, options.cipd_client_package,
1348 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001349
nodird6160682017-02-02 13:03:35 -08001350 @contextlib.contextmanager
nodir0ae98b32017-05-11 13:21:53 -07001351 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001352 # WARNING: this function depends on "options" variable defined in the outer
1353 # function.
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001354 assert unicode(run_dir), repr(run_dir)
1355 assert os.path.isabs(run_dir), run_dir
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001356 named_caches = [
nodir0ae98b32017-05-11 13:21:53 -07001357 (os.path.join(run_dir, unicode(relpath)), name)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001358 for name, relpath, _ in options.named_caches
nodir0ae98b32017-05-11 13:21:53 -07001359 ]
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001360 for path, name in named_caches:
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001361 named_cache.install(path, name)
nodird6160682017-02-02 13:03:35 -08001362 try:
1363 yield
1364 finally:
dnje289d132017-07-07 11:16:44 -07001365 # Uninstall each named cache, returning it to the cache pool. If an
1366 # uninstall fails for a given cache, it will remain in the task's
1367 # temporary space, get cleaned up by the Swarming bot, and be lost.
1368 #
1369 # If the Swarming bot cannot clean up the cache, it will handle it like
1370 # any other bot file that could not be removed.
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001371 for path, name in reversed(named_caches):
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001372 try:
Marc-Antoine Ruele9558372018-08-03 03:41:22 +00001373 # uninstall() doesn't trim but does call save() implicitly. Trimming
1374 # *must* be done manually via periodic 'run_isolated.py --clean'.
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001375 named_cache.uninstall(path, name)
1376 except local_caching.NamedCacheError:
1377 logging.exception('Error while removing named cache %r at %r. '
1378 'The cache will be lost.', path, name)
nodirf33b8d62016-10-26 22:34:58 -07001379
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001380 extra_args = []
1381 command = []
1382 if options.raw_cmd:
1383 command = args
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001384 if options.relative_cwd:
1385 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1386 if not a.startswith(os.getcwd()):
1387 parser.error(
1388 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001389 else:
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001390 if options.relative_cwd:
1391 parser.error('--relative-cwd requires --raw-cmd')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001392 extra_args = args
1393
1394 data = TaskData(
1395 command=command,
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001396 relative_cwd=options.relative_cwd,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001397 extra_args=extra_args,
1398 isolated_hash=options.isolated,
1399 storage=None,
1400 isolate_cache=isolate_cache,
1401 outputs=options.output,
1402 install_named_caches=install_named_caches,
1403 leak_temp_dir=options.leak_temp_dir,
1404 root_dir=_to_unicode(options.root_dir),
1405 hard_timeout=options.hard_timeout,
1406 grace_period=options.grace_period,
1407 bot_file=options.bot_file,
1408 switch_to_account=options.switch_to_account,
1409 install_packages_fn=install_packages_fn,
1410 use_symlinks=options.use_symlinks,
1411 env=options.env,
1412 env_prefix=options.env_prefix)
nodirbe642ff2016-06-09 15:51:51 -07001413 try:
nodir90bc8dc2016-06-15 13:35:21 -07001414 if options.isolate_server:
1415 storage = isolateserver.get_storage(
1416 options.isolate_server, options.namespace)
1417 with storage:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001418 data = data._replace(storage=storage)
nodirf33b8d62016-10-26 22:34:58 -07001419 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
1420 assert storage.hash_algo == isolate_cache.hash_algo
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001421 return run_tha_test(data, options.json)
1422 return run_tha_test(data, options.json)
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001423 except (
1424 cipd.Error,
1425 local_caching.NamedCacheError,
1426 local_caching.NotFoundError) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001427 print >> sys.stderr, ex.message
1428 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001429
1430
1431if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001432 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001433 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001434 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001435 sys.exit(main(sys.argv[1:]))