blob: f087bacb8a78b7261fb3c5fbc5076d54d8927ab7 [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 Ruelb8513132018-11-20 19:48:53 +000065import isolate_storage
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -040066import local_caching
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000067
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000068
vadimsh@chromium.org85071062013-08-21 23:37:45 +000069# Absolute path to this file (can be None if running from zip on Mac).
tansella4949442016-06-23 22:34:32 -070070THIS_FILE_PATH = os.path.abspath(
71 __file__.decode(sys.getfilesystemencoding())) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000072
73# Directory that contains this file (might be inside zip package).
tansella4949442016-06-23 22:34:32 -070074BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__.decode(
75 sys.getfilesystemencoding()) else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000076
77# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000078if zip_package.get_main_script_path():
79 MAIN_DIR = os.path.dirname(
80 os.path.abspath(zip_package.get_main_script_path()))
81else:
82 # This happens when 'import run_isolated' is executed at the python
83 # interactive prompt, in that case __file__ is undefined.
84 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000085
maruele2f2cb82016-07-13 14:41:03 -070086
87# Magic variables that can be found in the isolate task command line.
88ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
89EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
90SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
91
92
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000093# The name of the log file to use.
94RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
95
maruele2f2cb82016-07-13 14:41:03 -070096
csharp@chromium.orge217f302012-11-22 16:51:53 +000097# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000098RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000099
vadimsh@chromium.org87d63262013-04-04 19:34:21 +0000100
maruele2f2cb82016-07-13 14:41:03 -0700101# Use short names for temporary directories. This is driven by Windows, which
102# imposes a relatively short maximum path length of 260 characters, often
103# referred to as MAX_PATH. It is relatively easy to create files with longer
104# path length. A use case is with recursive depedency treesV like npm packages.
105#
106# It is recommended to start the script with a `root_dir` as short as
107# possible.
108# - ir stands for isolated_run
109# - io stands for isolated_out
110# - it stands for isolated_tmp
111ISOLATED_RUN_DIR = u'ir'
112ISOLATED_OUT_DIR = u'io'
113ISOLATED_TMP_DIR = u'it'
114
115
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400116# Keep synced with task_request.py
117CACHE_NAME_RE = re.compile(ur'^[a-z0-9_]{1,4096}$')
118
119
marueld928c862017-06-08 08:20:04 -0700120OUTLIVING_ZOMBIE_MSG = """\
121*** Swarming tried multiple times to delete the %s directory and failed ***
122*** Hard failing the task ***
123
124Swarming detected that your testing script ran an executable, which may have
125started a child executable, and the main script returned early, leaving the
126children executables playing around unguided.
127
128You don't want to leave children processes outliving the task on the Swarming
129bot, do you? The Swarming bot doesn't.
130
131How to fix?
132- For any process that starts children processes, make sure all children
133 processes terminated properly before each parent process exits. This is
134 especially important in very deep process trees.
135 - This must be done properly both in normal successful task and in case of
136 task failure. Cleanup is very important.
137- The Swarming bot sends a SIGTERM in case of timeout.
138 - You have %s seconds to comply after the signal was sent to the process
139 before the process is forcibly killed.
140- To achieve not leaking children processes in case of signals on timeout, you
141 MUST handle signals in each executable / python script and propagate them to
142 children processes.
143 - When your test script (python or binary) receives a signal like SIGTERM or
144 CTRL_BREAK_EVENT on Windows), send it to all children processes and wait for
145 them to terminate before quitting.
146
147See
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -0400148https://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 -0700149for more information.
150
151*** May the SIGKILL force be with you ***
152"""
153
154
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000155# Currently hardcoded. Eventually could be exposed as a flag once there's value.
156# 3 weeks
157MAX_AGE_SECS = 21*24*60*60
158
159
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500160TaskData = collections.namedtuple(
161 'TaskData', [
162 # List of strings; the command line to use, independent of what was
163 # specified in the isolated file.
164 'command',
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500165 # Relative directory to start command into.
166 'relative_cwd',
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500167 # List of strings; the arguments to add to the command specified in the
168 # isolated file.
169 'extra_args',
170 # Hash of the .isolated file that must be retrieved to recreate the tree
171 # of files to run the target executable. The command specified in the
172 # .isolated is executed. Mutually exclusive with command argument.
173 'isolated_hash',
174 # isolateserver.Storage instance to retrieve remote objects. This object
175 # has a reference to an isolateserver.StorageApi, which does the actual
176 # I/O.
177 'storage',
178 # isolateserver.LocalCache instance to keep from retrieving the same
179 # objects constantly by caching the objects retrieved. Can be on-disk or
180 # in-memory.
181 'isolate_cache',
182 # List of paths relative to root_dir to put into the output isolated
183 # bundle upon task completion (see link_outputs_to_outdir).
184 'outputs',
185 # Function (run_dir) => context manager that installs named caches into
186 # |run_dir|.
187 'install_named_caches',
188 # If True, the temporary directory will be deliberately leaked for later
189 # examination.
190 'leak_temp_dir',
191 # Path to the directory to use to create the temporary directory. If not
192 # specified, a random temporary directory is created.
193 'root_dir',
194 # Kills the process if it lasts more than this amount of seconds.
195 'hard_timeout',
196 # Number of seconds to wait between SIGTERM and SIGKILL.
197 'grace_period',
198 # Path to a file with bot state, used in place of ${SWARMING_BOT_FILE}
199 # task command line argument.
200 'bot_file',
201 # Logical account to switch LUCI_CONTEXT into.
202 'switch_to_account',
203 # Context manager dir => CipdInfo, see install_client_and_packages.
204 'install_packages_fn',
205 # Create tree with symlinks instead of hardlinks.
206 'use_symlinks',
207 # Environment variables to set.
208 'env',
209 # Environment variables to mutate with relative directories.
210 # Example: {"ENV_KEY": ['relative', 'paths', 'to', 'prepend']}
211 'env_prefix'])
212
213
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000214def get_as_zip_package(executable=True):
215 """Returns ZipPackage with this module and all its dependencies.
216
217 If |executable| is True will store run_isolated.py as __main__.py so that
218 zip package is directly executable be python.
219 """
220 # Building a zip package when running from another zip package is
221 # unsupported and probably unneeded.
222 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +0000223 assert THIS_FILE_PATH
224 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000225 package = zip_package.ZipPackage(root=BASE_DIR)
226 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
aludwin81178302016-11-30 17:18:49 -0800227 package.add_python_file(os.path.join(BASE_DIR, 'isolate_storage.py'))
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -0400228 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000229 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800230 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
nodirbe642ff2016-06-09 15:51:51 -0700231 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
Marc-Antoine Ruel34f5f282018-05-16 16:04:31 -0400232 package.add_python_file(os.path.join(BASE_DIR, 'local_caching.py'))
tanselle4288c32016-07-28 09:45:40 -0700233 package.add_directory(os.path.join(BASE_DIR, 'libs'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000234 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
235 package.add_directory(os.path.join(BASE_DIR, 'utils'))
236 return package
237
238
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500239def _to_str(s):
240 """Downgrades a unicode instance to str. Pass str through as-is."""
241 if isinstance(s, str):
242 return s
243 # This is technically incorrect, especially on Windows. In theory
244 # sys.getfilesystemencoding() should be used to use the right 'ANSI code
245 # page' on Windows, but that causes other problems, as the character set
246 # is very limited.
247 return s.encode('utf-8')
248
249
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -0500250def _to_unicode(s):
251 """Upgrades a str instance to unicode. Pass unicode through as-is."""
252 if isinstance(s, unicode) or s is None:
253 return s
254 return s.decode('utf-8')
255
256
maruel03e11842016-07-14 10:50:16 -0700257def make_temp_dir(prefix, root_dir):
258 """Returns a new unique temporary directory."""
259 return unicode(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000260
261
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500262def change_tree_read_only(rootdir, read_only):
263 """Changes the tree read-only bits according to the read_only specification.
264
265 The flag can be 0, 1 or 2, which will affect the possibility to modify files
266 and create or delete files.
267 """
268 if read_only == 2:
269 # Files and directories (except on Windows) are marked read only. This
270 # inhibits modifying, creating or deleting files in the test directory,
271 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400272 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500273 elif read_only == 1:
274 # Files are marked read only but not the directories. This inhibits
275 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400276 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500277 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500278 # Anything can be modified.
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400279 # TODO(maruel): This is currently dangerous as long as
280 # DiskContentAddressedCache.touch() is not yet changed to verify the hash of
281 # the content of the files it is looking at, so that if a test modifies an
282 # input file, the file must be deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400283 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500284 else:
285 raise ValueError(
286 'change_tree_read_only(%s, %s): Unknown flag %s' %
287 (rootdir, read_only, read_only))
288
289
vadimsh9c54b2c2017-07-25 14:08:29 -0700290@contextlib.contextmanager
291def set_luci_context_account(account, tmp_dir):
292 """Sets LUCI_CONTEXT account to be used by the task.
293
294 If 'account' is None or '', does nothing at all. This happens when
295 run_isolated.py is called without '--switch-to-account' flag. In this case,
296 if run_isolated.py is running in some LUCI_CONTEXT environment, the task will
297 just inherit whatever account is already set. This may happen is users invoke
298 run_isolated.py explicitly from their code.
299
300 If the requested account is not defined in the context, switches to
301 non-authenticated access. This happens for Swarming tasks that don't use
302 'task' service accounts.
303
304 If not using LUCI_CONTEXT-based auth, does nothing.
305 If already running as requested account, does nothing.
306 """
307 if not account:
308 # Not actually switching.
309 yield
310 return
311
312 local_auth = luci_context.read('local_auth')
313 if not local_auth:
314 # Not using LUCI_CONTEXT auth at all.
315 yield
316 return
317
318 # See LUCI_CONTEXT.md for the format of 'local_auth'.
319 if local_auth.get('default_account_id') == account:
320 # Already set, no need to switch.
321 yield
322 return
323
324 available = {a['id'] for a in local_auth.get('accounts') or []}
325 if account in available:
326 logging.info('Switching default LUCI_CONTEXT account to %r', account)
327 local_auth['default_account_id'] = account
328 else:
329 logging.warning(
330 'Requested LUCI_CONTEXT account %r is not available (have only %r), '
331 'disabling authentication', account, sorted(available))
332 local_auth.pop('default_account_id', None)
333
334 with luci_context.write(_tmpdir=tmp_dir, local_auth=local_auth):
335 yield
336
337
nodir90bc8dc2016-06-15 13:35:21 -0700338def process_command(command, out_dir, bot_file):
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000339 """Replaces parameters in a command line.
nodirbe642ff2016-06-09 15:51:51 -0700340
341 Raises:
342 ValueError if a parameter is requested in |command| but its value is not
343 provided.
344 """
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000345 return [replace_parameters(arg, out_dir, bot_file) for arg in command]
346
347
348def replace_parameters(arg, out_dir, bot_file):
349 """Replaces parameter tokens with appropriate values in a string.
350
351 Raises:
352 ValueError if a parameter is requested in |arg| but its value is not
353 provided.
354 """
355 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
356 replace_slash = False
357 if ISOLATED_OUTDIR_PARAMETER in arg:
358 if not out_dir:
359 raise ValueError(
360 'output directory is requested in command or env var, but not '
361 'provided; please specify one')
362 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
363 replace_slash = True
364 if SWARMING_BOT_FILE_PARAMETER in arg:
365 if bot_file:
366 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
nodirbe642ff2016-06-09 15:51:51 -0700367 replace_slash = True
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000368 else:
369 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command or env '
370 'var, but no bot_file specified. Leaving parameter '
371 'unchanged.')
372 if replace_slash:
373 # Replace slashes only if parameters are present
374 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
375 arg = arg.replace('/', os.sep)
376 return arg
maruela9cfd6f2015-09-15 11:03:15 -0700377
378
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000379
380def get_command_env(tmp_dir, cipd_info, run_dir, env, env_prefixes, out_dir,
381 bot_file):
vadimsh232f5a82017-01-20 19:23:44 -0800382 """Returns full OS environment to run a command in.
383
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800384 Sets up TEMP, puts directory with cipd binary in front of PATH, exposes
385 CIPD_CACHE_DIR env var, and installs all env_prefixes.
vadimsh232f5a82017-01-20 19:23:44 -0800386
387 Args:
388 tmp_dir: temp directory.
389 cipd_info: CipdInfo object is cipd client is used, None if not.
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500390 run_dir: The root directory the isolated tree is mapped in.
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500391 env: environment variables to use
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800392 env_prefixes: {"ENV_KEY": ['cwd', 'relative', 'paths', 'to', 'prepend']}
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000393 out_dir: Isolated output directory. Required to be != None if any of the
394 env vars contain ISOLATED_OUTDIR_PARAMETER.
395 bot_file: Required to be != None if any of the env vars contain
396 SWARMING_BOT_FILE_PARAMETER.
vadimsh232f5a82017-01-20 19:23:44 -0800397 """
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500398 out = os.environ.copy()
399 for k, v in env.iteritems():
400 if not v:
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500401 out.pop(k, None)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500402 else:
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000403 out[k] = replace_parameters(v, out_dir, bot_file)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500404
405 if cipd_info:
406 bin_dir = os.path.dirname(cipd_info.client.binary_path)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500407 out['PATH'] = '%s%s%s' % (_to_str(bin_dir), os.pathsep, out['PATH'])
408 out['CIPD_CACHE_DIR'] = _to_str(cipd_info.cache_dir)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500409
410 for key, paths in env_prefixes.iteritems():
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500411 assert isinstance(paths, list), paths
412 paths = [os.path.normpath(os.path.join(run_dir, p)) for p in paths]
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500413 cur = out.get(key)
414 if cur:
415 paths.append(cur)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500416 out[key] = _to_str(os.path.pathsep.join(paths))
vadimsh232f5a82017-01-20 19:23:44 -0800417
Marc-Antoine Ruelefb30b12018-07-25 18:34:36 +0000418 tmp_dir = _to_str(tmp_dir)
419 # pylint: disable=line-too-long
420 # * python respects $TMPDIR, $TEMP, and $TMP in this order, regardless of
421 # platform. So $TMPDIR must be set on all platforms.
422 # https://github.com/python/cpython/blob/2.7/Lib/tempfile.py#L155
423 out['TMPDIR'] = tmp_dir
424 if sys.platform == 'win32':
425 # * chromium's base utils uses GetTempPath().
426 # https://cs.chromium.org/chromium/src/base/files/file_util_win.cc?q=GetTempPath
427 # * Go uses GetTempPath().
428 # * GetTempDir() uses %TMP%, then %TEMP%, then other stuff. So %TMP% must be
429 # set.
430 # https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-gettemppathw
431 out['TMP'] = tmp_dir
432 # https://blogs.msdn.microsoft.com/oldnewthing/20150417-00/?p=44213
433 out['TEMP'] = tmp_dir
434 elif sys.platform == 'darwin':
435 # * Chromium uses an hack on macOS before calling into
436 # NSTemporaryDirectory().
437 # https://cs.chromium.org/chromium/src/base/files/file_util_mac.mm?q=GetTempDir
438 # https://developer.apple.com/documentation/foundation/1409211-nstemporarydirectory
439 out['MAC_CHROMIUM_TMPDIR'] = tmp_dir
440 else:
441 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
442 # http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
443 # * mktemp on linux respects $TMPDIR.
444 # * Chromium respects $TMPDIR on linux.
445 # https://cs.chromium.org/chromium/src/base/files/file_util_posix.cc?q=GetTempDir
446 # * Go uses $TMPDIR.
447 # https://go.googlesource.com/go/+/go1.10.3/src/os/file_unix.go#307
448 pass
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500449 return out
vadimsh232f5a82017-01-20 19:23:44 -0800450
451
452def run_command(command, cwd, env, hard_timeout, grace_period):
maruel6be7f9e2015-10-01 12:25:30 -0700453 """Runs the command.
454
455 Returns:
456 tuple(process exit code, bool if had a hard timeout)
457 """
maruela9cfd6f2015-09-15 11:03:15 -0700458 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700459
maruel6be7f9e2015-10-01 12:25:30 -0700460 exit_code = None
461 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700462 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700463 proc = None
464 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700465 try:
maruel6be7f9e2015-10-01 12:25:30 -0700466 # TODO(maruel): This code is imperfect. It doesn't handle well signals
467 # during the download phase and there's short windows were things can go
468 # wrong.
469 def handler(signum, _frame):
470 if proc and not had_signal:
471 logging.info('Received signal %d', signum)
472 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700473 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700474
475 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
476 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
477 try:
478 exit_code = proc.wait(hard_timeout or None)
479 except subprocess42.TimeoutExpired:
480 if not had_signal:
481 logging.warning('Hard timeout')
482 had_hard_timeout = True
483 logging.warning('Sending SIGTERM')
484 proc.terminate()
485
486 # Ignore signals in grace period. Forcibly give the grace period to the
487 # child process.
488 if exit_code is None:
489 ignore = lambda *_: None
490 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
491 try:
492 exit_code = proc.wait(grace_period or None)
493 except subprocess42.TimeoutExpired:
494 # Now kill for real. The user can distinguish between the
495 # following states:
496 # - signal but process exited within grace period,
497 # hard_timed_out will be set but the process exit code will be
498 # script provided.
499 # - processed exited late, exit code will be -9 on posix.
500 logging.warning('Grace exhausted; sending SIGKILL')
501 proc.kill()
martiniss5c8043e2017-08-01 17:09:43 -0700502 logging.info('Waiting for process exit')
maruel6be7f9e2015-10-01 12:25:30 -0700503 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700504 except OSError:
505 # This is not considered to be an internal error. The executable simply
506 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800507 sys.stderr.write(
508 '<The executable does not exist or a dependent library is missing>\n'
509 '<Check for missing .so/.dll in the .isolate or GN file>\n'
510 '<Command: %s>\n' % command)
511 if os.environ.get('SWARMING_TASK_ID'):
512 # Give an additional hint when running as a swarming task.
513 sys.stderr.write(
514 '<See the task\'s page for commands to help diagnose this issue '
515 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700516 exit_code = 1
517 logging.info(
518 'Command finished with exit code %d (%s)',
519 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700520 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700521
522
maruel4409e302016-07-19 14:25:51 -0700523def fetch_and_map(isolated_hash, storage, cache, outdir, use_symlinks):
524 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700525 start = time.time()
526 bundle = isolateserver.fetch_isolated(
527 isolated_hash=isolated_hash,
528 storage=storage,
529 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700530 outdir=outdir,
531 use_symlinks=use_symlinks)
nodir6f801882016-04-29 14:41:50 -0700532 return bundle, {
533 'duration': time.time() - start,
nodir6f801882016-04-29 14:41:50 -0700534 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
535 'items_hot': base64.b64encode(
tansell9e04a8d2016-07-28 09:31:59 -0700536 large.pack(sorted(set(cache.used) - set(cache.added)))),
nodir6f801882016-04-29 14:41:50 -0700537 }
538
539
aludwin0a8e17d2016-10-27 15:57:39 -0700540def link_outputs_to_outdir(run_dir, out_dir, outputs):
541 """Links any named outputs to out_dir so they can be uploaded.
542
543 Raises an error if the file already exists in that directory.
544 """
545 if not outputs:
546 return
547 isolateserver.create_directories(out_dir, outputs)
548 for o in outputs:
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -0400549 copy_recursively(os.path.join(run_dir, o), os.path.join(out_dir, o))
550
551
552def copy_recursively(src, dst):
553 """Efficiently copies a file or directory from src_dir to dst_dir.
554
555 `item` may be a file, directory, or a symlink to a file or directory.
556 All symlinks are replaced with their targets, so the resulting
557 directory structure in dst_dir will never have any symlinks.
558
559 To increase speed, copy_recursively hardlinks individual files into the
560 (newly created) directory structure if possible, unlike Python's
561 shutil.copytree().
562 """
563 orig_src = src
564 try:
565 # Replace symlinks with their final target.
566 while fs.islink(src):
567 res = fs.readlink(src)
568 src = os.path.join(os.path.dirname(src), res)
569 # TODO(sadafm): Explicitly handle cyclic symlinks.
570
571 # Note that fs.isfile (which is a wrapper around os.path.isfile) throws
572 # an exception if src does not exist. A warning will be logged in that case.
573 if fs.isfile(src):
574 file_path.link_file(dst, src, file_path.HARDLINK_WITH_FALLBACK)
575 return
576
577 if not fs.exists(dst):
578 os.makedirs(dst)
579
580 for child in fs.listdir(src):
581 copy_recursively(os.path.join(src, child), os.path.join(dst, child))
582
583 except OSError as e:
584 if e.errno == errno.ENOENT:
585 logging.warning('Path %s does not exist or %s is a broken symlink',
586 src, orig_src)
587 else:
588 logging.info("Couldn't collect output file %s: %s", src, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700589
590
maruela9cfd6f2015-09-15 11:03:15 -0700591def delete_and_upload(storage, out_dir, leak_temp_dir):
592 """Deletes the temporary run directory and uploads results back.
593
594 Returns:
nodir6f801882016-04-29 14:41:50 -0700595 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700596 - outputs_ref: a dict referring to the results archived back to the isolated
597 server, if applicable.
598 - success: False if something occurred that means that the task must
599 forcibly be considered a failure, e.g. zombie processes were left
600 behind.
nodir6f801882016-04-29 14:41:50 -0700601 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700602 """
maruela9cfd6f2015-09-15 11:03:15 -0700603 # Upload out_dir and generate a .isolated file out of this directory. It is
604 # only done if files were written in the directory.
605 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700606 cold = []
607 hot = []
nodir6f801882016-04-29 14:41:50 -0700608 start = time.time()
609
maruel12e30012015-10-09 11:55:35 -0700610 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700611 with tools.Profiler('ArchiveOutput'):
612 try:
maruel064c0a32016-04-05 11:47:15 -0700613 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700614 storage, [out_dir], None)
615 outputs_ref = {
616 'isolated': results[0][0],
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000617 'isolatedserver': storage.server_ref.url,
618 'namespace': storage.server_ref.namespace,
maruela9cfd6f2015-09-15 11:03:15 -0700619 }
maruel064c0a32016-04-05 11:47:15 -0700620 cold = sorted(i.size for i in f_cold)
621 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700622 except isolateserver.Aborted:
623 # This happens when a signal SIGTERM was received while uploading data.
624 # There is 2 causes:
625 # - The task was too slow and was about to be killed anyway due to
626 # exceeding the hard timeout.
627 # - The amount of data uploaded back is very large and took too much
628 # time to archive.
629 sys.stderr.write('Received SIGTERM while uploading')
630 # Re-raise, so it will be treated as an internal failure.
631 raise
nodir6f801882016-04-29 14:41:50 -0700632
633 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700634 try:
maruel12e30012015-10-09 11:55:35 -0700635 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700636 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700637 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700638 else:
639 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700640 except OSError as e:
641 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700642 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700643 stats = {
644 'duration': time.time() - start,
645 'items_cold': base64.b64encode(large.pack(cold)),
646 'items_hot': base64.b64encode(large.pack(hot)),
647 }
648 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700649
650
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500651def map_and_run(data, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700652 """Runs a command with optional isolated input/output.
653
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500654 Arguments:
655 - data: TaskData instance.
656 - constant_run_path: TODO
nodir55be77b2016-05-03 09:39:57 -0700657
658 Returns metadata about the result.
659 """
maruela9cfd6f2015-09-15 11:03:15 -0700660 result = {
maruel064c0a32016-04-05 11:47:15 -0700661 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700662 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700663 'had_hard_timeout': False,
Seth Koehler49139812017-12-19 13:59:33 -0500664 'internal_failure': 'run_isolated did not complete properly',
maruel064c0a32016-04-05 11:47:15 -0700665 'stats': {
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000666 'isolated': {
667 #'cipd': {
668 # 'duration': 0.,
669 # 'get_client_duration': 0.,
670 #},
671 'download': {
672 #'duration': 0.,
673 'initial_number_items': len(data.isolate_cache),
674 'initial_size': data.isolate_cache.total_size,
675 #'items_cold': '<large.pack()>',
676 #'items_hot': '<large.pack()>',
677 },
678 #'upload': {
679 # 'duration': 0.,
680 # 'items_cold': '<large.pack()>',
681 # 'items_hot': '<large.pack()>',
682 #},
683 },
maruel064c0a32016-04-05 11:47:15 -0700684 },
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000685 #'cipd_pins': {
686 # 'packages': [
687 # {'package_name': ..., 'version': ..., 'path': ...},
688 # ...
689 # ],
690 # 'client_package': {'package_name': ..., 'version': ...},
691 #},
maruela9cfd6f2015-09-15 11:03:15 -0700692 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700693 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700694 }
nodirbe642ff2016-06-09 15:51:51 -0700695
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500696 if data.root_dir:
697 file_path.ensure_tree(data.root_dir, 0700)
698 elif data.isolate_cache.cache_dir:
699 data = data._replace(
700 root_dir=os.path.dirname(data.isolate_cache.cache_dir))
maruele2f2cb82016-07-13 14:41:03 -0700701 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700702 # If root_dir is not specified, it is not constant.
703 # TODO(maruel): This is not obvious. Change this to become an error once we
704 # make the constant_run_path an exposed flag.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500705 if constant_run_path and data.root_dir:
706 run_dir = os.path.join(data.root_dir, ISOLATED_RUN_DIR)
maruel5c4eed82017-05-26 05:33:40 -0700707 if os.path.isdir(run_dir):
708 file_path.rmtree(run_dir)
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500709 os.mkdir(run_dir, 0700)
maruelcffa0542017-04-07 08:39:20 -0700710 else:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500711 run_dir = make_temp_dir(ISOLATED_RUN_DIR, data.root_dir)
maruel03e11842016-07-14 10:50:16 -0700712 # storage should be normally set but don't crash if it is not. This can happen
713 # as Swarming task can run without an isolate server.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500714 out_dir = make_temp_dir(
715 ISOLATED_OUT_DIR, data.root_dir) if data.storage else None
716 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, data.root_dir)
nodir55be77b2016-05-03 09:39:57 -0700717 cwd = run_dir
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500718 if data.relative_cwd:
719 cwd = os.path.normpath(os.path.join(cwd, data.relative_cwd))
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500720 command = data.command
nodir55be77b2016-05-03 09:39:57 -0700721 try:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500722 with data.install_packages_fn(run_dir) as cipd_info:
vadimsh232f5a82017-01-20 19:23:44 -0800723 if cipd_info:
724 result['stats']['cipd'] = cipd_info.stats
725 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700726
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500727 if data.isolated_hash:
vadimsh232f5a82017-01-20 19:23:44 -0800728 isolated_stats = result['stats'].setdefault('isolated', {})
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000729 bundle, stats = fetch_and_map(
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500730 isolated_hash=data.isolated_hash,
731 storage=data.storage,
732 cache=data.isolate_cache,
vadimsh232f5a82017-01-20 19:23:44 -0800733 outdir=run_dir,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500734 use_symlinks=data.use_symlinks)
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000735 isolated_stats['download'].update(stats)
vadimsh232f5a82017-01-20 19:23:44 -0800736 change_tree_read_only(run_dir, bundle.read_only)
maruelabec63c2017-04-26 11:53:24 -0700737 # Inject the command
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500738 if not command and bundle.command:
739 command = bundle.command + data.extra_args
Marc-Antoine Rueld704a1f2017-10-31 10:51:23 -0400740 # Only set the relative directory if the isolated file specified a
741 # command, and no raw command was specified.
742 if bundle.relative_cwd:
743 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
maruelabec63c2017-04-26 11:53:24 -0700744
745 if not command:
746 # Handle this as a task failure, not an internal failure.
747 sys.stderr.write(
748 '<No command was specified!>\n'
749 '<Please secify a command when triggering your Swarming task>\n')
750 result['exit_code'] = 1
751 return result
nodirbe642ff2016-06-09 15:51:51 -0700752
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500753 if not cwd.startswith(run_dir):
754 # Handle this as a task failure, not an internal failure. This is a
755 # 'last chance' way to gate against directory escape.
756 sys.stderr.write('<Relative CWD is outside of run directory!>\n')
757 result['exit_code'] = 1
758 return result
759
760 if not os.path.isdir(cwd):
761 # Accepts relative_cwd that does not exist.
762 os.makedirs(cwd, 0700)
763
vadimsh232f5a82017-01-20 19:23:44 -0800764 # If we have an explicit list of files to return, make sure their
765 # directories exist now.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500766 if data.storage and data.outputs:
767 isolateserver.create_directories(run_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700768
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500769 with data.install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -0800770 sys.stdout.flush()
771 start = time.time()
772 try:
vadimsh9c54b2c2017-07-25 14:08:29 -0700773 # Need to switch the default account before 'get_command_env' call,
774 # so it can grab correct value of LUCI_CONTEXT env var.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500775 with set_luci_context_account(data.switch_to_account, tmp_dir):
776 env = get_command_env(
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000777 tmp_dir, cipd_info, run_dir, data.env, data.env_prefix, out_dir,
778 data.bot_file)
Robert Iannucci24ae76a2018-02-26 12:51:18 -0800779 command = tools.fix_python_cmd(command, env)
780 command = process_command(command, out_dir, data.bot_file)
781 file_path.ensure_command_has_abs_path(command, cwd)
782
vadimsh9c54b2c2017-07-25 14:08:29 -0700783 result['exit_code'], result['had_hard_timeout'] = run_command(
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500784 command, cwd, env, data.hard_timeout, data.grace_period)
nodird6160682017-02-02 13:03:35 -0800785 finally:
786 result['duration'] = max(time.time() - start, 0)
Seth Koehler49139812017-12-19 13:59:33 -0500787
788 # We successfully ran the command, set internal_failure back to
789 # None (even if the command failed, it's not an internal error).
790 result['internal_failure'] = None
maruela9cfd6f2015-09-15 11:03:15 -0700791 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700792 # An internal error occurred. Report accordingly so the swarming task will
793 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700794 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700795 result['internal_failure'] = str(e)
796 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700797
798 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700799 finally:
800 try:
aludwin0a8e17d2016-10-27 15:57:39 -0700801 # Try to link files to the output directory, if specified.
802 if out_dir:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500803 link_outputs_to_outdir(run_dir, out_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700804
nodir32a1ec12016-10-26 18:34:07 -0700805 success = False
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500806 if data.leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700807 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700808 logging.warning(
809 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700810 else:
maruel84537cb2015-10-16 14:21:28 -0700811 # On Windows rmtree(run_dir) call above has a synchronization effect: it
812 # finishes only when all task child processes terminate (since a running
813 # process locks *.exe file). Examine out_dir only after that call
814 # completes (since child processes may write to out_dir too and we need
815 # to wait for them to finish).
816 if fs.isdir(run_dir):
817 try:
818 success = file_path.rmtree(run_dir)
819 except OSError as e:
Marc-Antoine Ruel44699b32018-09-24 23:31:50 +0000820 logging.error('rmtree(%r) failed: %s', run_dir, e)
maruel84537cb2015-10-16 14:21:28 -0700821 success = False
822 if not success:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500823 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('run', data.grace_period))
maruel84537cb2015-10-16 14:21:28 -0700824 if result['exit_code'] == 0:
825 result['exit_code'] = 1
826 if fs.isdir(tmp_dir):
827 try:
828 success = file_path.rmtree(tmp_dir)
829 except OSError as e:
Marc-Antoine Ruel44699b32018-09-24 23:31:50 +0000830 logging.error('rmtree(%r) failed: %s', tmp_dir, e)
maruel84537cb2015-10-16 14:21:28 -0700831 success = False
832 if not success:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500833 sys.stderr.write(OUTLIVING_ZOMBIE_MSG % ('temp', data.grace_period))
maruel84537cb2015-10-16 14:21:28 -0700834 if result['exit_code'] == 0:
835 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700836
marueleb5fbee2015-09-17 13:01:36 -0700837 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700838 if out_dir:
nodir55715712016-06-03 12:28:19 -0700839 isolated_stats = result['stats'].setdefault('isolated', {})
840 result['outputs_ref'], success, isolated_stats['upload'] = (
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500841 delete_and_upload(data.storage, out_dir, data.leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700842 if not success and result['exit_code'] == 0:
843 result['exit_code'] = 1
844 except Exception as e:
845 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700846 if out_dir:
847 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700848 result['internal_failure'] = str(e)
849 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500850
851
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500852def run_tha_test(data, result_json):
nodir55be77b2016-05-03 09:39:57 -0700853 """Runs an executable and records execution metadata.
854
nodir55be77b2016-05-03 09:39:57 -0700855 If isolated_hash is specified, downloads the dependencies in the cache,
856 hardlinks them into a temporary directory and runs the command specified in
857 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500858
859 A temporary directory is created to hold the output files. The content inside
860 this directory will be uploaded back to |storage| packaged as a .isolated
861 file.
862
863 Arguments:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500864 - data: TaskData instance.
865 - result_json: File path to dump result metadata into. If set, the process
866 exit code is always 0 unless an internal error occurred.
maruela9cfd6f2015-09-15 11:03:15 -0700867
868 Returns:
869 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000870 """
maruela76b9ee2015-12-15 06:18:08 -0800871 if result_json:
872 # Write a json output file right away in case we get killed.
873 result = {
874 'exit_code': None,
875 'had_hard_timeout': False,
876 'internal_failure': 'Was terminated before completion',
877 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700878 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800879 }
880 tools.write_json(result_json, result, dense=True)
881
maruela9cfd6f2015-09-15 11:03:15 -0700882 # run_isolated exit code. Depends on if result_json is used or not.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500883 result = map_and_run(data, True)
maruela9cfd6f2015-09-15 11:03:15 -0700884 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700885
maruela9cfd6f2015-09-15 11:03:15 -0700886 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700887 # We've found tests to delete 'work' when quitting, causing an exception
888 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700889 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700890 tools.write_json(result_json, result, dense=True)
891 # Only return 1 if there was an internal error.
892 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000893
maruela9cfd6f2015-09-15 11:03:15 -0700894 # Marshall into old-style inline output.
895 if result['outputs_ref']:
896 data = {
897 'hash': result['outputs_ref']['isolated'],
898 'namespace': result['outputs_ref']['namespace'],
899 'storage': result['outputs_ref']['isolatedserver'],
900 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500901 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700902 print(
903 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
904 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800905 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700906 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000907
908
iannuccib58d10d2017-03-18 02:00:25 -0700909# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -0800910CipdInfo = collections.namedtuple('CipdInfo', [
911 'client', # cipd.CipdClient object
912 'cache_dir', # absolute path to bot-global cipd tag and instance cache
913 'stats', # dict with stats to return to the server
914 'pins', # dict with installed cipd pins to return to the server
915])
916
917
918@contextlib.contextmanager
919def noop_install_packages(_run_dir):
iannuccib58d10d2017-03-18 02:00:25 -0700920 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -0800921 yield None
922
923
iannuccib58d10d2017-03-18 02:00:25 -0700924def _install_packages(run_dir, cipd_cache_dir, client, packages, timeout):
925 """Calls 'cipd ensure' for packages.
926
927 Args:
928 run_dir (str): root of installation.
929 cipd_cache_dir (str): the directory to use for the cipd package cache.
930 client (CipdClient): the cipd client to use
931 packages: packages to install, list [(path, package_name, version), ...].
932 timeout: max duration in seconds that this function can take.
933
934 Returns: list of pinned packages. Looks like [
935 {
936 'path': 'subdirectory',
937 'package_name': 'resolved/package/name',
938 'version': 'deadbeef...',
939 },
940 ...
941 ]
942 """
943 package_pins = [None]*len(packages)
944 def insert_pin(path, name, version, idx):
945 package_pins[idx] = {
946 'package_name': name,
947 # swarming deals with 'root' as '.'
948 'path': path or '.',
949 'version': version,
950 }
951
952 by_path = collections.defaultdict(list)
953 for i, (path, name, version) in enumerate(packages):
954 # cipd deals with 'root' as ''
955 if path == '.':
956 path = ''
957 by_path[path].append((name, version, i))
958
959 pins = client.ensure(
960 run_dir,
961 {
962 subdir: [(name, vers) for name, vers, _ in pkgs]
963 for subdir, pkgs in by_path.iteritems()
964 },
965 cache_dir=cipd_cache_dir,
966 timeout=timeout,
967 )
968
969 for subdir, pin_list in sorted(pins.iteritems()):
970 this_subdir = by_path[subdir]
971 for i, (name, version) in enumerate(pin_list):
972 insert_pin(subdir, name, version, this_subdir[i][2])
973
Robert Iannucci461b30d2017-12-13 11:34:03 -0800974 assert None not in package_pins, (packages, pins, package_pins)
iannuccib58d10d2017-03-18 02:00:25 -0700975
976 return package_pins
977
978
vadimsh232f5a82017-01-20 19:23:44 -0800979@contextlib.contextmanager
iannuccib58d10d2017-03-18 02:00:25 -0700980def install_client_and_packages(
nodirff531b42016-06-23 13:05:06 -0700981 run_dir, packages, service_url, client_package_name,
vadimsh232f5a82017-01-20 19:23:44 -0800982 client_version, cache_dir, timeout=None):
vadimsh902948e2017-01-20 15:57:32 -0800983 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -0700984
vadimsh232f5a82017-01-20 19:23:44 -0800985 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
986
987 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -0700988 [
989 {
990 "path": path, "package_name": package_name, "version": version,
991 },
992 ...
993 ]
vadimsh902948e2017-01-20 15:57:32 -0800994 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -0700995
996 such that they correspond 1:1 to all input package arguments from the command
997 line. These dictionaries make their all the way back to swarming, where they
998 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -0700999
vadimsh902948e2017-01-20 15:57:32 -08001000 If 'packages' list is empty, will bootstrap CIPD client, but won't install
1001 any packages.
1002
1003 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -08001004 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -08001005
nodirbe642ff2016-06-09 15:51:51 -07001006 Args:
nodir90bc8dc2016-06-15 13:35:21 -07001007 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -08001008 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -07001009 service_url (str): CIPD server url, e.g.
1010 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -07001011 client_package_name (str): CIPD package name of CIPD client.
1012 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -07001013 cache_dir (str): where to keep cache of cipd clients, packages and tags.
1014 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -07001015 """
1016 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -07001017
nodirbe642ff2016-06-09 15:51:51 -07001018 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -07001019 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -07001020
vadimsh902948e2017-01-20 15:57:32 -08001021 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -08001022 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -07001023 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -08001024 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -07001025
nodirbe642ff2016-06-09 15:51:51 -07001026 get_client_start = time.time()
1027 client_manager = cipd.get_client(
1028 service_url, client_package_name, client_version, cache_dir,
1029 timeout=timeoutfn())
iannucci96fcccc2016-08-30 15:52:22 -07001030
nodirbe642ff2016-06-09 15:51:51 -07001031 with client_manager as client:
1032 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -07001033
iannuccib58d10d2017-03-18 02:00:25 -07001034 package_pins = []
1035 if packages:
1036 package_pins = _install_packages(
1037 run_dir, cipd_cache_dir, client, packages, timeoutfn())
1038
1039 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -07001040
vadimsh232f5a82017-01-20 19:23:44 -08001041 total_duration = time.time() - start
1042 logging.info(
1043 'Installing CIPD client and packages took %d seconds', total_duration)
nodir90bc8dc2016-06-15 13:35:21 -07001044
vadimsh232f5a82017-01-20 19:23:44 -08001045 yield CipdInfo(
1046 client=client,
1047 cache_dir=cipd_cache_dir,
1048 stats={
1049 'duration': total_duration,
1050 'get_client_duration': get_client_duration,
1051 },
1052 pins={
iannuccib58d10d2017-03-18 02:00:25 -07001053 'client_package': {
1054 'package_name': client.package_name,
1055 'version': client.instance_id,
1056 },
vadimsh232f5a82017-01-20 19:23:44 -08001057 'packages': package_pins,
1058 })
nodirbe642ff2016-06-09 15:51:51 -07001059
1060
1061def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001062 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -07001063 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +00001064 version=__version__,
1065 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -07001066 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -07001067 '--clean', action='store_true',
1068 help='Cleans the cache, trimming it necessary and remove corrupted items '
1069 'and returns without executing anything; use with -v to know what '
1070 'was done')
1071 parser.add_option(
maruel4409e302016-07-19 14:25:51 -07001072 '--use-symlinks', action='store_true',
1073 help='Use symlinks instead of hardlinks')
1074 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -07001075 '--json',
1076 help='dump output metadata to json file. When used, run_isolated returns '
1077 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -07001078 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001079 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -07001080 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001081 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -07001082 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -07001083 parser.add_option(
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001084 '--raw-cmd', action='store_true',
1085 help='Ignore the isolated command, use the one supplied at the command '
1086 'line')
1087 parser.add_option(
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001088 '--relative-cwd',
1089 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
1090 'requires --raw-cmd')
1091 parser.add_option(
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001092 '--env', default=[], action='append',
1093 help='Environment variables to set for the child process')
1094 parser.add_option(
1095 '--env-prefix', default=[], action='append',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001096 help='Specify a VAR=./path/fragment to put in the environment variable '
1097 'before executing the command. The path fragment must be relative '
1098 'to the isolated run directory, and must not contain a `..` token. '
1099 'The path will be made absolute and prepended to the indicated '
1100 '$VAR using the OS\'s path separator. Multiple items for the same '
1101 '$VAR will be prepended in order.')
1102 parser.add_option(
bpastene3ae09522016-06-10 17:12:59 -07001103 '--bot-file',
1104 help='Path to a file describing the state of the host. The content is '
1105 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -07001106 parser.add_option(
vadimsh9c54b2c2017-07-25 14:08:29 -07001107 '--switch-to-account',
1108 help='If given, switches LUCI_CONTEXT to given logical service account '
1109 '(e.g. "task" or "system") before launching the isolated process.')
1110 parser.add_option(
aludwin0a8e17d2016-10-27 15:57:39 -07001111 '--output', action='append',
1112 help='Specifies an output to return. If no outputs are specified, all '
1113 'files located in $(ISOLATED_OUTDIR) will be returned; '
1114 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
1115 'specified by --output option (there can be multiple) will be '
1116 'returned. Note that if a file in OUT_DIR has the same path '
1117 'as an --output option, the --output version will be returned.')
1118 parser.add_option(
aludwin7556e0c2016-10-26 08:46:10 -07001119 '-a', '--argsfile',
1120 # This is actually handled in parse_args; it's included here purely so it
1121 # can make it into the help text.
1122 help='Specify a file containing a JSON array of arguments to this '
1123 'script. If --argsfile is provided, no other argument may be '
1124 'provided on the command line.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001125 data_group = optparse.OptionGroup(parser, 'Data source')
1126 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001127 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -07001128 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001129 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -05001130 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001131
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -04001132 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001133
1134 cipd.add_cipd_options(parser)
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001135
1136 group = optparse.OptionGroup(parser, 'Named caches')
1137 group.add_option(
1138 '--named-cache',
1139 dest='named_caches',
1140 action='append',
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001141 nargs=3,
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001142 default=[],
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001143 help='A named cache to request. Accepts 3 arguments: name, path, hint. '
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001144 'name identifies the cache, must match regex [a-z0-9_]{1,4096}. '
1145 'path is a path relative to the run dir where the cache directory '
1146 'must be put to. '
1147 'This option can be specified more than once.')
1148 group.add_option(
1149 '--named-cache-root', default='named_caches',
1150 help='Cache root directory. Default=%default')
1151 parser.add_option_group(group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001152
Kenneth Russell61d42352014-09-15 11:41:16 -07001153 debug_group = optparse.OptionGroup(parser, 'Debugging')
1154 debug_group.add_option(
1155 '--leak-temp-dir',
1156 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -07001157 help='Deliberately leak isolate\'s temp dir for later examination. '
1158 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -07001159 debug_group.add_option(
1160 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -07001161 parser.add_option_group(debug_group)
1162
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001163 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001164
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001165 parser.set_defaults(cache='cache', cipd_cache='cipd_cache')
nodirbe642ff2016-06-09 15:51:51 -07001166 return parser
1167
1168
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001169def process_named_cache_options(parser, options, time_fn=None):
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001170 """Validates named cache options and returns a CacheManager."""
1171 if options.named_caches and not options.named_cache_root:
1172 parser.error('--named-cache is specified, but --named-cache-root is empty')
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001173 for name, path, hint in options.named_caches:
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001174 if not CACHE_NAME_RE.match(name):
1175 parser.error(
1176 'cache name %r does not match %r' % (name, CACHE_NAME_RE.pattern))
1177 if not path:
1178 parser.error('cache path cannot be empty')
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001179 try:
1180 long(hint)
1181 except ValueError:
1182 parser.error('cache hint must be a number')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001183 if options.named_cache_root:
1184 # Make these configurable later if there is use case but for now it's fairly
1185 # safe values.
1186 # In practice, a fair chunk of bots are already recycled on a daily schedule
1187 # so this code doesn't have any effect to them, unless they are preloaded
1188 # with a really old cache.
1189 policies = local_caching.CachePolicies(
1190 # 1TiB.
1191 max_cache_size=1024*1024*1024*1024,
1192 min_free_space=options.min_free_space,
1193 max_items=50,
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +00001194 max_age_secs=MAX_AGE_SECS)
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001195 root_dir = unicode(os.path.abspath(options.named_cache_root))
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001196 return local_caching.NamedCache(root_dir, policies, time_fn=time_fn)
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001197 return None
1198
1199
aludwin7556e0c2016-10-26 08:46:10 -07001200def parse_args(args):
1201 # Create a fake mini-parser just to get out the "-a" command. Note that
1202 # it's not documented here; instead, it's documented in create_option_parser
1203 # even though that parser will never actually get to parse it. This is
1204 # because --argsfile is exclusive with all other options and arguments.
1205 file_argparse = argparse.ArgumentParser(add_help=False)
1206 file_argparse.add_argument('-a', '--argsfile')
1207 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
1208 if file_args.argsfile:
1209 if nonfile_args:
1210 file_argparse.error('Can\'t specify --argsfile with'
1211 'any other arguments (%s)' % nonfile_args)
1212 try:
1213 with open(file_args.argsfile, 'r') as f:
1214 args = json.load(f)
1215 except (IOError, OSError, ValueError) as e:
1216 # We don't need to error out here - "args" is now empty,
1217 # so the call below to parser.parse_args(args) will fail
1218 # and print the full help text.
1219 print >> sys.stderr, 'Couldn\'t read arguments: %s' % e
1220
1221 # Even if we failed to read the args, just call the normal parser now since it
1222 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -07001223 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001224 options, args = parser.parse_args(args)
aludwin7556e0c2016-10-26 08:46:10 -07001225 return (parser, options, args)
1226
1227
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001228def _calc_named_cache_hint(named_cache, named_caches):
1229 """Returns the expected size of the missing named caches."""
1230 present = named_cache.available
1231 size = 0
1232 for name, _, hint in named_caches:
1233 if name not in present:
1234 hint = long(hint)
1235 if hint > 0:
1236 size += hint
1237 return size
1238
1239
aludwin7556e0c2016-10-26 08:46:10 -07001240def main(args):
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -05001241 # Warning: when --argsfile is used, the strings are unicode instances, when
1242 # parsed normally, the strings are str instances.
aludwin7556e0c2016-10-26 08:46:10 -07001243 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -07001244
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001245 if not file_path.enable_symlink():
1246 logging.error('Symlink support is not enabled')
1247
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001248 named_cache = process_named_cache_options(parser, options)
Marc-Antoine Ruel0d8b0f62018-09-10 14:40:35 +00001249 # hint is 0 if there's no named cache.
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001250 hint = _calc_named_cache_hint(named_cache, options.named_caches)
1251 if hint:
1252 # Increase the --min-free-space value by the hint, and recreate the
1253 # NamedCache instance so it gets the updated CachePolicy.
1254 options.min_free_space += hint
1255 named_cache = process_named_cache_options(parser, options)
1256
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001257 # TODO(maruel): CIPD caches should be defined at an higher level here too, so
1258 # they can be cleaned the same way.
nodirf33b8d62016-10-26 22:34:58 -07001259 isolate_cache = isolateserver.process_cache_options(options, trim=False)
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001260 caches = []
1261 if isolate_cache:
1262 caches.append(isolate_cache)
1263 if named_cache:
1264 caches.append(named_cache)
1265 root = caches[0].cache_dir if caches else unicode(os.getcwd())
maruel36a963d2016-04-08 17:15:49 -07001266 if options.clean:
1267 if options.isolated:
1268 parser.error('Can\'t use --isolated with --clean.')
1269 if options.isolate_server:
1270 parser.error('Can\'t use --isolate-server with --clean.')
1271 if options.json:
1272 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -07001273 if options.named_caches:
1274 parser.error('Can\t use --named-cache with --clean.')
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001275 # Trim first, then clean.
1276 local_caching.trim_caches(
1277 caches,
1278 root,
1279 min_free_space=options.min_free_space,
1280 max_age_secs=MAX_AGE_SECS)
1281 for c in caches:
Marc-Antoine Ruel87fc2222018-06-18 13:09:24 +00001282 c.cleanup()
maruel36a963d2016-04-08 17:15:49 -07001283 return 0
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001284
1285 # Trim must still be done for the following case:
1286 # - named-cache was used
1287 # - some entries, with a large hint, where missing
1288 # - --min-free-space was increased accordingly, thus trimming is needed
1289 # Otherwise, this will have no effect, as bot_main calls run_isolated with
1290 # --clean after each task.
1291 if hint:
1292 logging.info('Additional trimming of %d bytes', hint)
Marc-Antoine Ruel0d8b0f62018-09-10 14:40:35 +00001293 local_caching.trim_caches(
1294 caches,
1295 root,
1296 min_free_space=options.min_free_space,
1297 max_age_secs=MAX_AGE_SECS)
maruel36a963d2016-04-08 17:15:49 -07001298
nodir55be77b2016-05-03 09:39:57 -07001299 if not options.isolated and not args:
1300 parser.error('--isolated or command to run is required.')
1301
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001302 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001303
1304 isolateserver.process_isolate_server_options(
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001305 parser, options, True, False)
nodir55be77b2016-05-03 09:39:57 -07001306 if not options.isolate_server:
1307 if options.isolated:
1308 parser.error('--isolated requires --isolate-server')
1309 if ISOLATED_OUTDIR_PARAMETER in args:
1310 parser.error(
1311 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001312
nodir90bc8dc2016-06-15 13:35:21 -07001313 if options.root_dir:
1314 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -07001315 if options.json:
1316 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001317
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001318 if any('=' not in i for i in options.env):
1319 parser.error(
1320 '--env required key=value form. value can be skipped to delete '
1321 'the variable')
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001322 options.env = dict(i.split('=', 1) for i in options.env)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001323
1324 prefixes = {}
1325 cwd = os.path.realpath(os.getcwd())
1326 for item in options.env_prefix:
1327 if '=' not in item:
1328 parser.error(
1329 '--env-prefix %r is malformed, must be in the form `VAR=./path`'
1330 % item)
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001331 key, opath = item.split('=', 1)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001332 if os.path.isabs(opath):
1333 parser.error('--env-prefix %r path is bad, must be relative.' % opath)
1334 opath = os.path.normpath(opath)
1335 if not os.path.realpath(os.path.join(cwd, opath)).startswith(cwd):
1336 parser.error(
1337 '--env-prefix %r path is bad, must be relative and not contain `..`.'
1338 % opath)
1339 prefixes.setdefault(key, []).append(opath)
1340 options.env_prefix = prefixes
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001341
nodirbe642ff2016-06-09 15:51:51 -07001342 cipd.validate_cipd_options(parser, options)
1343
vadimsh232f5a82017-01-20 19:23:44 -08001344 install_packages_fn = noop_install_packages
vadimsh902948e2017-01-20 15:57:32 -08001345 if options.cipd_enabled:
iannuccib58d10d2017-03-18 02:00:25 -07001346 install_packages_fn = lambda run_dir: install_client_and_packages(
vadimsh902948e2017-01-20 15:57:32 -08001347 run_dir, cipd.parse_package_args(options.cipd_packages),
1348 options.cipd_server, options.cipd_client_package,
1349 options.cipd_client_version, cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -07001350
nodird6160682017-02-02 13:03:35 -08001351 @contextlib.contextmanager
nodir0ae98b32017-05-11 13:21:53 -07001352 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001353 # WARNING: this function depends on "options" variable defined in the outer
1354 # function.
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001355 assert unicode(run_dir), repr(run_dir)
1356 assert os.path.isabs(run_dir), run_dir
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001357 named_caches = [
nodir0ae98b32017-05-11 13:21:53 -07001358 (os.path.join(run_dir, unicode(relpath)), name)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001359 for name, relpath, _ in options.named_caches
nodir0ae98b32017-05-11 13:21:53 -07001360 ]
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001361 for path, name in named_caches:
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001362 named_cache.install(path, name)
nodird6160682017-02-02 13:03:35 -08001363 try:
1364 yield
1365 finally:
dnje289d132017-07-07 11:16:44 -07001366 # Uninstall each named cache, returning it to the cache pool. If an
1367 # uninstall fails for a given cache, it will remain in the task's
1368 # temporary space, get cleaned up by the Swarming bot, and be lost.
1369 #
1370 # If the Swarming bot cannot clean up the cache, it will handle it like
1371 # any other bot file that could not be removed.
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001372 for path, name in reversed(named_caches):
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001373 try:
Marc-Antoine Ruele9558372018-08-03 03:41:22 +00001374 # uninstall() doesn't trim but does call save() implicitly. Trimming
1375 # *must* be done manually via periodic 'run_isolated.py --clean'.
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001376 named_cache.uninstall(path, name)
1377 except local_caching.NamedCacheError:
1378 logging.exception('Error while removing named cache %r at %r. '
1379 'The cache will be lost.', path, name)
nodirf33b8d62016-10-26 22:34:58 -07001380
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001381 extra_args = []
1382 command = []
1383 if options.raw_cmd:
1384 command = args
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001385 if options.relative_cwd:
1386 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1387 if not a.startswith(os.getcwd()):
1388 parser.error(
1389 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001390 else:
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001391 if options.relative_cwd:
1392 parser.error('--relative-cwd requires --raw-cmd')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001393 extra_args = args
1394
1395 data = TaskData(
1396 command=command,
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001397 relative_cwd=options.relative_cwd,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001398 extra_args=extra_args,
1399 isolated_hash=options.isolated,
1400 storage=None,
1401 isolate_cache=isolate_cache,
1402 outputs=options.output,
1403 install_named_caches=install_named_caches,
1404 leak_temp_dir=options.leak_temp_dir,
1405 root_dir=_to_unicode(options.root_dir),
1406 hard_timeout=options.hard_timeout,
1407 grace_period=options.grace_period,
1408 bot_file=options.bot_file,
1409 switch_to_account=options.switch_to_account,
1410 install_packages_fn=install_packages_fn,
1411 use_symlinks=options.use_symlinks,
1412 env=options.env,
1413 env_prefix=options.env_prefix)
nodirbe642ff2016-06-09 15:51:51 -07001414 try:
nodir90bc8dc2016-06-15 13:35:21 -07001415 if options.isolate_server:
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001416 server_ref = isolate_storage.ServerRef(
nodir90bc8dc2016-06-15 13:35:21 -07001417 options.isolate_server, options.namespace)
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001418 storage = isolateserver.get_storage(server_ref)
nodir90bc8dc2016-06-15 13:35:21 -07001419 with storage:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001420 data = data._replace(storage=storage)
nodirf33b8d62016-10-26 22:34:58 -07001421 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001422 assert storage.server_ref.hash_algo == server_ref.hash_algo
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001423 return run_tha_test(data, options.json)
1424 return run_tha_test(data, options.json)
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001425 except (
1426 cipd.Error,
1427 local_caching.NamedCacheError,
Marc-Antoine Ruelb6e9e232018-11-20 00:12:33 +00001428 local_caching.NoMoreSpace) as ex:
nodirbe642ff2016-06-09 15:51:51 -07001429 print >> sys.stderr, ex.message
1430 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001431
1432
1433if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001434 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001435 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001436 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001437 sys.exit(main(sys.argv[1:]))