blob: 56b954e0eb7c9646dc4d4de134e228fc6d9ce71f [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
Marc-Antoine Rueleed2f3a2019-03-14 00:00:40 +00008run_isolated takes cares of setting up a temporary environment, running a
9command, and tearing it down.
nodir55be77b2016-05-03 09:39:57 -070010
Marc-Antoine Rueleed2f3a2019-03-14 00:00:40 +000011It handles downloading and uploading isolated files, mapping CIPD packages and
12reusing stateful named caches.
13
14The isolated files, CIPD packages and named caches are kept as a global LRU
15cache.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -050016
Roberto Carrillo71ade6d2018-10-08 22:30:24 +000017Any ${EXECUTABLE_SUFFIX} on the command line or the environment variables passed
18with the --env option will be replaced with ".exe" string on Windows and "" on
19other platforms.
nodirbe642ff2016-06-09 15:51:51 -070020
Roberto Carrillo71ade6d2018-10-08 22:30:24 +000021Any ${ISOLATED_OUTDIR} on the command line or the environment variables passed
22with the --env option will be replaced by the location of a temporary directory
23upon execution of the command specified in the .isolated file. All content
24written to this directory will be uploaded upon termination and the .isolated
25file describing this directory will be printed to stdout.
bpastene447c1992016-06-20 15:21:47 -070026
Marc-Antoine Rueleed2f3a2019-03-14 00:00:40 +000027Any ${SWARMING_BOT_FILE} on the command line or the environment variables passed
28with the --env option will be replaced by the value of the --bot-file parameter.
29This file is used by a swarming bot to communicate state of the host to tasks.
30It is written to by the swarming bot's on_before_task() hook in the swarming
31server's custom bot_config.py.
32
33See
34https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
35for all the variables.
36
37See
38https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/swarming_bot/config/bot_config.py
39for more information about bot_config.py.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000040"""
41
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +000042from __future__ import print_function
43
44__version__ = '1.0.1'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000045
aludwin7556e0c2016-10-26 08:46:10 -070046import argparse
maruel064c0a32016-04-05 11:47:15 -070047import base64
iannucci96fcccc2016-08-30 15:52:22 -070048import collections
vadimsh232f5a82017-01-20 19:23:44 -080049import contextlib
Ye Kuangfff1e502020-07-13 13:21:57 +000050import distutils
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -040051import errno
aludwin7556e0c2016-10-26 08:46:10 -070052import json
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000053import logging
54import optparse
55import os
Takuto Ikuta5c59a842020-01-24 03:05:24 +000056import platform
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -040057import re
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000058import sys
59import tempfile
maruel064c0a32016-04-05 11:47:15 -070060import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000061
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000062from utils import tools
63tools.force_local_third_party()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000064
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000065# third_party/
66from depot_tools import fix_encoding
Takuto Ikuta6e2ff962019-10-29 12:35:27 +000067import six
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000068
69# pylint: disable=ungrouped-imports
70import auth
71import cipd
72import isolate_storage
73import isolateserver
74import local_caching
75from libs import luci_context
Vadim Shtayura6b555c12014-07-23 16:22:18 -070076from utils import file_path
maruel12e30012015-10-09 11:55:35 -070077from utils import fs
maruel064c0a32016-04-05 11:47:15 -070078from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040079from utils import logging_utils
Ye Kuang2dd17442020-04-22 08:45:52 +000080from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040081from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050082from utils import subprocess42
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000083
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000084
maruele2f2cb82016-07-13 14:41:03 -070085# Magic variables that can be found in the isolate task command line.
86ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
87EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
88SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
89
90
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000091# The name of the log file to use.
92RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
93
maruele2f2cb82016-07-13 14:41:03 -070094
csharp@chromium.orge217f302012-11-22 16:51:53 +000095# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000096RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000097
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000098
maruele2f2cb82016-07-13 14:41:03 -070099# Use short names for temporary directories. This is driven by Windows, which
100# imposes a relatively short maximum path length of 260 characters, often
101# referred to as MAX_PATH. It is relatively easy to create files with longer
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +0000102# path length. A use case is with recursive dependency trees like npm packages.
maruele2f2cb82016-07-13 14:41:03 -0700103#
104# It is recommended to start the script with a `root_dir` as short as
105# possible.
106# - ir stands for isolated_run
107# - io stands for isolated_out
108# - it stands for isolated_tmp
Takuto Ikutab7ce0e32019-11-27 23:26:18 +0000109# - ic stands for isolated_client
maruele2f2cb82016-07-13 14:41:03 -0700110ISOLATED_RUN_DIR = u'ir'
111ISOLATED_OUT_DIR = u'io'
112ISOLATED_TMP_DIR = u'it'
Takuto Ikutab7ce0e32019-11-27 23:26:18 +0000113ISOLATED_CLIENT_DIR = u'ic'
maruele2f2cb82016-07-13 14:41:03 -0700114
Takuto Ikuta02edca22019-11-29 10:04:51 +0000115# TODO(tikuta): take these parameter from luci-config?
Takuto Ikutac8c92e62020-04-01 07:07:29 +0000116# Update tag by `./client/update_isolated.sh`.
117# Or take revision from
Takuto Ikutab7ce0e32019-11-27 23:26:18 +0000118# https://ci.chromium.org/p/infra-internal/g/infra-packagers/console
Takuto Ikuta02edca22019-11-29 10:04:51 +0000119ISOLATED_PACKAGE = 'infra/tools/luci/isolated/${platform}'
Takuto Ikuta3d3dcf52020-08-24 08:24:16 +0000120ISOLATED_REVISION = 'git_revision:3ccf4cc0119188dbc4befff330348d972b15711d'
maruele2f2cb82016-07-13 14:41:03 -0700121
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400122# Keep synced with task_request.py
Lei Leife202df2019-06-11 17:33:34 +0000123CACHE_NAME_RE = re.compile(r'^[a-z0-9_]{1,4096}$')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400124
125
marueld928c862017-06-08 08:20:04 -0700126OUTLIVING_ZOMBIE_MSG = """\
127*** Swarming tried multiple times to delete the %s directory and failed ***
128*** Hard failing the task ***
129
130Swarming detected that your testing script ran an executable, which may have
131started a child executable, and the main script returned early, leaving the
132children executables playing around unguided.
133
134You don't want to leave children processes outliving the task on the Swarming
135bot, do you? The Swarming bot doesn't.
136
137How to fix?
138- For any process that starts children processes, make sure all children
139 processes terminated properly before each parent process exits. This is
140 especially important in very deep process trees.
141 - This must be done properly both in normal successful task and in case of
142 task failure. Cleanup is very important.
143- The Swarming bot sends a SIGTERM in case of timeout.
144 - You have %s seconds to comply after the signal was sent to the process
145 before the process is forcibly killed.
146- To achieve not leaking children processes in case of signals on timeout, you
147 MUST handle signals in each executable / python script and propagate them to
148 children processes.
149 - When your test script (python or binary) receives a signal like SIGTERM or
150 CTRL_BREAK_EVENT on Windows), send it to all children processes and wait for
151 them to terminate before quitting.
152
153See
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -0400154https://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 -0700155for more information.
156
157*** May the SIGKILL force be with you ***
158"""
159
160
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000161# Currently hardcoded. Eventually could be exposed as a flag once there's value.
162# 3 weeks
163MAX_AGE_SECS = 21*24*60*60
164
Ye Kuang72e6fe82020-08-05 06:30:04 +0000165# TODO(1099655): Enable this once all prod issues are gone.
166_USE_GO_ISOLATED_TO_UPLOAD = False
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000167
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500168TaskData = collections.namedtuple(
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000169 'TaskData',
170 [
Takuto Ikuta9a319502019-11-26 07:40:14 +0000171 # List of strings; the command line to use, independent of what was
172 # specified in the isolated file.
173 'command',
174 # Relative directory to start command into.
175 'relative_cwd',
176 # List of strings; the arguments to add to the command specified in the
177 # isolated file.
178 'extra_args',
179 # Hash of the .isolated file that must be retrieved to recreate the tree
180 # of files to run the target executable. The command specified in the
181 # .isolated is executed. Mutually exclusive with command argument.
182 'isolated_hash',
183 # isolateserver.Storage instance to retrieve remote objects. This object
184 # has a reference to an isolateserver.StorageApi, which does the actual
185 # I/O.
186 'storage',
187 # isolateserver.LocalCache instance to keep from retrieving the same
188 # objects constantly by caching the objects retrieved. Can be on-disk or
189 # in-memory.
190 'isolate_cache',
Junji Watanabe54925c32020-09-08 00:56:18 +0000191 # Digest of the input root on RBE-CAS.
192 'cas_digest',
193 # Full CAS instance name.
194 'cas_instance',
Takuto Ikuta9a319502019-11-26 07:40:14 +0000195 # List of paths relative to root_dir to put into the output isolated
196 # bundle upon task completion (see link_outputs_to_outdir).
197 'outputs',
198 # Function (run_dir) => context manager that installs named caches into
199 # |run_dir|.
200 'install_named_caches',
201 # If True, the temporary directory will be deliberately leaked for later
202 # examination.
203 'leak_temp_dir',
204 # Path to the directory to use to create the temporary directory. If not
205 # specified, a random temporary directory is created.
206 'root_dir',
207 # Kills the process if it lasts more than this amount of seconds.
208 'hard_timeout',
209 # Number of seconds to wait between SIGTERM and SIGKILL.
210 'grace_period',
211 # Path to a file with bot state, used in place of ${SWARMING_BOT_FILE}
212 # task command line argument.
213 'bot_file',
214 # Logical account to switch LUCI_CONTEXT into.
215 'switch_to_account',
216 # Context manager dir => CipdInfo, see install_client_and_packages.
217 'install_packages_fn',
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000218 # Use go isolated client.
219 'use_go_isolated',
Takuto Ikuta057c5342019-12-03 04:05:05 +0000220 # Cache directory for go isolated client.
221 'go_cache_dir',
Takuto Ikuta879788c2020-01-10 08:00:26 +0000222 # Parameters passed to go isolated client.
223 'go_cache_policies',
Takuto Ikuta9a319502019-11-26 07:40:14 +0000224 # Environment variables to set.
225 'env',
226 # Environment variables to mutate with relative directories.
227 # Example: {"ENV_KEY": ['relative', 'paths', 'to', 'prepend']}
228 'env_prefix',
229 # Lowers the task process priority.
230 'lower_priority',
231 # subprocess42.Containment instance. Can be None.
232 'containment',
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000233 ])
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500234
235
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500236def _to_str(s):
237 """Downgrades a unicode instance to str. Pass str through as-is."""
238 if isinstance(s, str):
239 return s
240 # This is technically incorrect, especially on Windows. In theory
241 # sys.getfilesystemencoding() should be used to use the right 'ANSI code
242 # page' on Windows, but that causes other problems, as the character set
243 # is very limited.
244 return s.encode('utf-8')
245
246
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -0500247def _to_unicode(s):
248 """Upgrades a str instance to unicode. Pass unicode through as-is."""
Takuto Ikuta95459dd2019-10-29 12:39:47 +0000249 if isinstance(s, six.text_type) or s is None:
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -0500250 return s
251 return s.decode('utf-8')
252
253
maruel03e11842016-07-14 10:50:16 -0700254def make_temp_dir(prefix, root_dir):
255 """Returns a new unique temporary directory."""
Takuto Ikuta6e2ff962019-10-29 12:35:27 +0000256 return six.text_type(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000257
258
vadimsh9c54b2c2017-07-25 14:08:29 -0700259@contextlib.contextmanager
260def set_luci_context_account(account, tmp_dir):
261 """Sets LUCI_CONTEXT account to be used by the task.
262
263 If 'account' is None or '', does nothing at all. This happens when
264 run_isolated.py is called without '--switch-to-account' flag. In this case,
265 if run_isolated.py is running in some LUCI_CONTEXT environment, the task will
Takuto Ikuta33e2ff32019-09-30 12:44:03 +0000266 just inherit whatever account is already set. This may happen if users invoke
vadimsh9c54b2c2017-07-25 14:08:29 -0700267 run_isolated.py explicitly from their code.
268
269 If the requested account is not defined in the context, switches to
270 non-authenticated access. This happens for Swarming tasks that don't use
271 'task' service accounts.
272
273 If not using LUCI_CONTEXT-based auth, does nothing.
274 If already running as requested account, does nothing.
275 """
276 if not account:
277 # Not actually switching.
278 yield
279 return
280
281 local_auth = luci_context.read('local_auth')
282 if not local_auth:
283 # Not using LUCI_CONTEXT auth at all.
284 yield
285 return
286
287 # See LUCI_CONTEXT.md for the format of 'local_auth'.
288 if local_auth.get('default_account_id') == account:
289 # Already set, no need to switch.
290 yield
291 return
292
293 available = {a['id'] for a in local_auth.get('accounts') or []}
294 if account in available:
295 logging.info('Switching default LUCI_CONTEXT account to %r', account)
296 local_auth['default_account_id'] = account
297 else:
298 logging.warning(
299 'Requested LUCI_CONTEXT account %r is not available (have only %r), '
300 'disabling authentication', account, sorted(available))
301 local_auth.pop('default_account_id', None)
302
303 with luci_context.write(_tmpdir=tmp_dir, local_auth=local_auth):
304 yield
305
306
nodir90bc8dc2016-06-15 13:35:21 -0700307def process_command(command, out_dir, bot_file):
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000308 """Replaces parameters in a command line.
nodirbe642ff2016-06-09 15:51:51 -0700309
310 Raises:
311 ValueError if a parameter is requested in |command| but its value is not
312 provided.
313 """
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000314 return [replace_parameters(arg, out_dir, bot_file) for arg in command]
315
316
317def replace_parameters(arg, out_dir, bot_file):
318 """Replaces parameter tokens with appropriate values in a string.
319
320 Raises:
321 ValueError if a parameter is requested in |arg| but its value is not
322 provided.
323 """
324 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
325 replace_slash = False
326 if ISOLATED_OUTDIR_PARAMETER in arg:
327 if not out_dir:
328 raise ValueError(
329 'output directory is requested in command or env var, but not '
330 'provided; please specify one')
331 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
332 replace_slash = True
333 if SWARMING_BOT_FILE_PARAMETER in arg:
334 if bot_file:
335 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
nodirbe642ff2016-06-09 15:51:51 -0700336 replace_slash = True
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000337 else:
338 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command or env '
339 'var, but no bot_file specified. Leaving parameter '
340 'unchanged.')
341 if replace_slash:
342 # Replace slashes only if parameters are present
343 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
344 arg = arg.replace('/', os.sep)
345 return arg
maruela9cfd6f2015-09-15 11:03:15 -0700346
347
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000348
349def get_command_env(tmp_dir, cipd_info, run_dir, env, env_prefixes, out_dir,
350 bot_file):
vadimsh232f5a82017-01-20 19:23:44 -0800351 """Returns full OS environment to run a command in.
352
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800353 Sets up TEMP, puts directory with cipd binary in front of PATH, exposes
354 CIPD_CACHE_DIR env var, and installs all env_prefixes.
vadimsh232f5a82017-01-20 19:23:44 -0800355
356 Args:
357 tmp_dir: temp directory.
358 cipd_info: CipdInfo object is cipd client is used, None if not.
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500359 run_dir: The root directory the isolated tree is mapped in.
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500360 env: environment variables to use
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800361 env_prefixes: {"ENV_KEY": ['cwd', 'relative', 'paths', 'to', 'prepend']}
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000362 out_dir: Isolated output directory. Required to be != None if any of the
363 env vars contain ISOLATED_OUTDIR_PARAMETER.
364 bot_file: Required to be != None if any of the env vars contain
365 SWARMING_BOT_FILE_PARAMETER.
vadimsh232f5a82017-01-20 19:23:44 -0800366 """
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500367 out = os.environ.copy()
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000368 for k, v in env.items():
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500369 if not v:
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500370 out.pop(k, None)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500371 else:
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000372 out[k] = replace_parameters(v, out_dir, bot_file)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500373
374 if cipd_info:
375 bin_dir = os.path.dirname(cipd_info.client.binary_path)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500376 out['PATH'] = '%s%s%s' % (_to_str(bin_dir), os.pathsep, out['PATH'])
377 out['CIPD_CACHE_DIR'] = _to_str(cipd_info.cache_dir)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500378
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000379 for key, paths in env_prefixes.items():
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500380 assert isinstance(paths, list), paths
381 paths = [os.path.normpath(os.path.join(run_dir, p)) for p in paths]
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500382 cur = out.get(key)
383 if cur:
384 paths.append(cur)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500385 out[key] = _to_str(os.path.pathsep.join(paths))
vadimsh232f5a82017-01-20 19:23:44 -0800386
Marc-Antoine Ruelefb30b12018-07-25 18:34:36 +0000387 tmp_dir = _to_str(tmp_dir)
388 # pylint: disable=line-too-long
389 # * python respects $TMPDIR, $TEMP, and $TMP in this order, regardless of
390 # platform. So $TMPDIR must be set on all platforms.
391 # https://github.com/python/cpython/blob/2.7/Lib/tempfile.py#L155
392 out['TMPDIR'] = tmp_dir
393 if sys.platform == 'win32':
394 # * chromium's base utils uses GetTempPath().
395 # https://cs.chromium.org/chromium/src/base/files/file_util_win.cc?q=GetTempPath
396 # * Go uses GetTempPath().
397 # * GetTempDir() uses %TMP%, then %TEMP%, then other stuff. So %TMP% must be
398 # set.
399 # https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-gettemppathw
400 out['TMP'] = tmp_dir
401 # https://blogs.msdn.microsoft.com/oldnewthing/20150417-00/?p=44213
402 out['TEMP'] = tmp_dir
403 elif sys.platform == 'darwin':
404 # * Chromium uses an hack on macOS before calling into
405 # NSTemporaryDirectory().
406 # https://cs.chromium.org/chromium/src/base/files/file_util_mac.mm?q=GetTempDir
407 # https://developer.apple.com/documentation/foundation/1409211-nstemporarydirectory
408 out['MAC_CHROMIUM_TMPDIR'] = tmp_dir
409 else:
410 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
411 # http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
412 # * mktemp on linux respects $TMPDIR.
413 # * Chromium respects $TMPDIR on linux.
414 # https://cs.chromium.org/chromium/src/base/files/file_util_posix.cc?q=GetTempDir
415 # * Go uses $TMPDIR.
416 # https://go.googlesource.com/go/+/go1.10.3/src/os/file_unix.go#307
417 pass
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500418 return out
vadimsh232f5a82017-01-20 19:23:44 -0800419
420
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000421def run_command(
422 command, cwd, env, hard_timeout, grace_period, lower_priority, containment):
maruel6be7f9e2015-10-01 12:25:30 -0700423 """Runs the command.
424
425 Returns:
426 tuple(process exit code, bool if had a hard timeout)
427 """
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000428 logging.info(
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000429 'run_command(%s, %s, %s, %s, %s, %s)',
430 command, cwd, hard_timeout, grace_period, lower_priority, containment)
marueleb5fbee2015-09-17 13:01:36 -0700431
maruel6be7f9e2015-10-01 12:25:30 -0700432 exit_code = None
433 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700434 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700435 proc = None
436 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700437 try:
maruel6be7f9e2015-10-01 12:25:30 -0700438 # TODO(maruel): This code is imperfect. It doesn't handle well signals
439 # during the download phase and there's short windows were things can go
440 # wrong.
441 def handler(signum, _frame):
442 if proc and not had_signal:
443 logging.info('Received signal %d', signum)
444 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700445 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700446
Marc-Antoine Ruel30b80fe2019-02-08 13:51:31 +0000447 proc = subprocess42.Popen(
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000448 command, cwd=cwd, env=env, detached=True, close_fds=True,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000449 lower_priority=lower_priority, containment=containment)
maruel6be7f9e2015-10-01 12:25:30 -0700450 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
451 try:
John Budorickc398f092019-06-10 22:49:44 +0000452 exit_code = proc.wait(hard_timeout or None)
maruel6be7f9e2015-10-01 12:25:30 -0700453 except subprocess42.TimeoutExpired:
454 if not had_signal:
455 logging.warning('Hard timeout')
456 had_hard_timeout = True
457 logging.warning('Sending SIGTERM')
458 proc.terminate()
459
460 # Ignore signals in grace period. Forcibly give the grace period to the
461 # child process.
462 if exit_code is None:
463 ignore = lambda *_: None
464 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
465 try:
466 exit_code = proc.wait(grace_period or None)
467 except subprocess42.TimeoutExpired:
468 # Now kill for real. The user can distinguish between the
469 # following states:
470 # - signal but process exited within grace period,
471 # hard_timed_out will be set but the process exit code will be
472 # script provided.
473 # - processed exited late, exit code will be -9 on posix.
474 logging.warning('Grace exhausted; sending SIGKILL')
475 proc.kill()
martiniss5c8043e2017-08-01 17:09:43 -0700476 logging.info('Waiting for process exit')
maruel6be7f9e2015-10-01 12:25:30 -0700477 exit_code = proc.wait()
Takuto Ikutaeccf0862020-03-19 03:05:55 +0000478 except OSError as e:
maruela9cfd6f2015-09-15 11:03:15 -0700479 # This is not considered to be an internal error. The executable simply
480 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800481 sys.stderr.write(
tikuta2d678212019-09-23 23:12:08 +0000482 '<The executable does not exist, a dependent library is missing or '
483 'the command line is too long>\n'
484 '<Check for missing .so/.dll in the .isolate or GN file or length of '
485 'command line args>\n'
Takuto Ikutaeccf0862020-03-19 03:05:55 +0000486 '<Command: %s, Exception: %s>\n' % (command, e))
maruela72f46e2016-02-24 11:05:45 -0800487 if os.environ.get('SWARMING_TASK_ID'):
488 # Give an additional hint when running as a swarming task.
489 sys.stderr.write(
490 '<See the task\'s page for commands to help diagnose this issue '
491 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700492 exit_code = 1
493 logging.info(
494 'Command finished with exit code %d (%s)',
495 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700496 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700497
498
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000499def _run_go_isolated_and_wait(cmd):
500 """
501 Runs a Go `isolated` command and wait for its completion.
502
503 While this is a generic function to launch a subprocess, it has logic that
504 is specific to Go `isolated` for waiting and logging.
505
506 Returns:
507 The subprocess object
508 """
Ye Kuang3c40e9f2020-07-28 13:15:25 +0000509 cmd_str = ' '.join(cmd)
Ye Kuangc1d800f2020-07-28 10:14:55 +0000510 try:
511 proc = subprocess42.Popen(cmd)
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000512
Ye Kuangc1d800f2020-07-28 10:14:55 +0000513 exceeded_max_timeout = True
514 check_period_sec = 30
515 max_checks = 100
516 # max timeout = max_checks * check_period_sec = 50 minutes
517 for i in range(max_checks):
518 # This is to prevent I/O timeout error during isolated setup.
519 try:
520 retcode = proc.wait(check_period_sec)
521 if retcode != 0:
522 raise ValueError("retcode is not 0: %s (cmd=%s)" % (retcode, cmd_str))
523 exceeded_max_timeout = False
524 break
525 except subprocess42.TimeoutExpired:
526 print('still running isolated (after %d seconds)' %
527 ((i + 1) * check_period_sec))
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000528
Ye Kuangc1d800f2020-07-28 10:14:55 +0000529 if exceeded_max_timeout:
530 proc.terminate()
531 try:
532 proc.wait(check_period_sec)
533 except subprocess42.TimeoutExpired:
534 logging.exception(
535 "failed to terminate? timeout happened after %d seconds",
536 check_period_sec)
537 proc.kill()
538 proc.wait()
539 # Raise unconditionally, because |proc| was forcefully terminated.
540 raise ValueError("timedout after %d seconds (cmd=%s)" %
541 (check_period_sec * max_checks, cmd_str))
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000542
Ye Kuangc1d800f2020-07-28 10:14:55 +0000543 return proc
544 except Exception:
545 logging.exception('Failed to run Go cmd %s', cmd_str)
546 raise
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000547
548
Takuto Ikuta879788c2020-01-10 08:00:26 +0000549def _fetch_and_map_with_go(isolated_hash, storage, outdir, go_cache_dir,
550 policies, isolated_client):
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000551 """
552 Fetches an isolated tree using go client, create the tree and returns
553 (bundle, stats).
554 """
555 start = time.time()
556 server_ref = storage.server_ref
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000557 result_json_handle, result_json_path = tempfile.mkstemp(
558 prefix=u'fetch-and-map-result-', suffix=u'.json')
559 os.close(result_json_handle)
560 try:
Ye Kuanga98764c2020-04-09 03:17:37 +0000561 cmd = [
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000562 isolated_client,
563 'download',
564 '-isolate-server',
565 server_ref.url,
566 '-namespace',
567 server_ref.namespace,
568 '-isolated',
569 isolated_hash,
570
571 # flags for cache
572 '-cache-dir',
Takuto Ikuta057c5342019-12-03 04:05:05 +0000573 go_cache_dir,
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000574 '-cache-max-items',
Takuto Ikuta50bc0552019-12-03 03:26:46 +0000575 str(policies.max_items),
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000576 '-cache-max-size',
Takuto Ikuta50bc0552019-12-03 03:26:46 +0000577 str(policies.max_cache_size),
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000578 '-cache-min-free-space',
Takuto Ikuta50bc0552019-12-03 03:26:46 +0000579 str(policies.min_free_space),
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000580
581 # flags for output
582 '-output-dir',
583 outdir,
584 '-fetch-and-map-result-json',
585 result_json_path,
Ye Kuanga98764c2020-04-09 03:17:37 +0000586 ]
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000587 _run_go_isolated_and_wait(cmd)
Takuto Ikuta3153e3b2020-02-18 06:11:47 +0000588
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000589 with open(result_json_path) as json_file:
590 result_json = json.load(json_file)
591
592 isolated = result_json['isolated']
593 bundle = isolateserver.IsolatedBundle(filter_cb=None)
594 # Only following properties are used in caller.
595 bundle.command = isolated.get('command')
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000596 bundle.relative_cwd = isolated.get('relative_cwd')
597
598 return bundle, {
599 'duration': time.time() - start,
600 'items_cold': result_json['items_cold'],
601 'items_hot': result_json['items_hot'],
602 }
603 finally:
604 fs.remove(result_json_path)
605
606
607# TODO(crbug.com/932396): remove this function.
Takuto Ikuta16fac4b2019-12-09 04:57:18 +0000608def fetch_and_map(isolated_hash, storage, cache, outdir):
maruel4409e302016-07-19 14:25:51 -0700609 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700610 start = time.time()
611 bundle = isolateserver.fetch_isolated(
612 isolated_hash=isolated_hash,
613 storage=storage,
614 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700615 outdir=outdir,
Takuto Ikuta16fac4b2019-12-09 04:57:18 +0000616 use_symlinks=False)
Takuto Ikuta2b9640e2019-06-19 00:53:23 +0000617 hot = (collections.Counter(cache.used) -
618 collections.Counter(cache.added)).elements()
nodir6f801882016-04-29 14:41:50 -0700619 return bundle, {
Takuto Ikuta630f99d2020-07-02 12:59:35 +0000620 'duration': time.time() - start,
621 'items_cold': base64.b64encode(large.pack(sorted(cache.added))).decode(),
622 'items_hot': base64.b64encode(large.pack(sorted(hot))).decode(),
nodir6f801882016-04-29 14:41:50 -0700623 }
624
625
aludwin0a8e17d2016-10-27 15:57:39 -0700626def link_outputs_to_outdir(run_dir, out_dir, outputs):
627 """Links any named outputs to out_dir so they can be uploaded.
628
629 Raises an error if the file already exists in that directory.
630 """
631 if not outputs:
632 return
633 isolateserver.create_directories(out_dir, outputs)
634 for o in outputs:
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -0400635 copy_recursively(os.path.join(run_dir, o), os.path.join(out_dir, o))
636
637
638def copy_recursively(src, dst):
639 """Efficiently copies a file or directory from src_dir to dst_dir.
640
641 `item` may be a file, directory, or a symlink to a file or directory.
642 All symlinks are replaced with their targets, so the resulting
643 directory structure in dst_dir will never have any symlinks.
644
645 To increase speed, copy_recursively hardlinks individual files into the
646 (newly created) directory structure if possible, unlike Python's
647 shutil.copytree().
648 """
649 orig_src = src
650 try:
651 # Replace symlinks with their final target.
652 while fs.islink(src):
653 res = fs.readlink(src)
654 src = os.path.join(os.path.dirname(src), res)
655 # TODO(sadafm): Explicitly handle cyclic symlinks.
656
657 # Note that fs.isfile (which is a wrapper around os.path.isfile) throws
658 # an exception if src does not exist. A warning will be logged in that case.
659 if fs.isfile(src):
660 file_path.link_file(dst, src, file_path.HARDLINK_WITH_FALLBACK)
661 return
662
663 if not fs.exists(dst):
664 os.makedirs(dst)
665
666 for child in fs.listdir(src):
667 copy_recursively(os.path.join(src, child), os.path.join(dst, child))
668
669 except OSError as e:
670 if e.errno == errno.ENOENT:
671 logging.warning('Path %s does not exist or %s is a broken symlink',
672 src, orig_src)
673 else:
674 logging.info("Couldn't collect output file %s: %s", src, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700675
676
Ye Kuangfb0bad62020-07-28 08:07:25 +0000677def _upload_with_py(storage, out_dir):
678
679 def process_stats(f_st):
680 st = sorted(i.size for i in f_st)
681 return base64.b64encode(large.pack(st)).decode()
682
683 try:
684 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
685 storage, [out_dir], None, verify_push=True)
686
687 isolated = list(results.values())[0]
688 cold = process_stats(f_cold)
689 hot = process_stats(f_hot)
690 return isolated, cold, hot
691
692 except isolateserver.Aborted:
693 # This happens when a signal SIGTERM was received while uploading data.
694 # There is 2 causes:
695 # - The task was too slow and was about to be killed anyway due to
696 # exceeding the hard timeout.
697 # - The amount of data uploaded back is very large and took too much
698 # time to archive.
699 sys.stderr.write('Received SIGTERM while uploading')
700 # Re-raise, so it will be treated as an internal failure.
701 raise
702
703
704def _upload_with_go(storage, outdir, isolated_client):
705 """
706 Uploads results back using the Go `isolated` CLI.
707 """
708 server_ref = storage.server_ref
709 isolated_handle, isolated_path = tempfile.mkstemp(
710 prefix=u'isolated-hash-', suffix=u'.txt')
711 stats_json_handle, stats_json_path = tempfile.mkstemp(
712 prefix=u'dump-stats-', suffix=u'.json')
713 os.close(isolated_handle)
714 os.close(stats_json_handle)
715 try:
716 cmd = [
717 isolated_client,
718 'archive',
719 '-isolate-server',
720 server_ref.url,
721 '-namespace',
722 server_ref.namespace,
723 '-dirs',
724 # Format: <working directory>:<relative path to dir>
725 outdir + ':',
726
727 # output
728 '-dump-hash',
729 isolated_path,
730 '-dump-stats-json',
731 stats_json_path,
Ye Kuangbc4e8402020-07-29 09:54:30 +0000732 '-quiet',
Ye Kuangfb0bad62020-07-28 08:07:25 +0000733 ]
Ye Kuang0023dc52020-08-04 05:28:41 +0000734 # Will do exponential backoff, e.g. 10, 20, 40...
735 # This mitigates https://crbug.com/1094369, where there is a data race on
736 # the uploaded files.
737 backoff = 10
Takuto Ikutae0bfec72020-08-28 02:52:52 +0000738 started = time.time()
Ye Kuang0023dc52020-08-04 05:28:41 +0000739 while True:
740 try:
741 _run_go_isolated_and_wait(cmd)
742 break
743 except Exception:
Takuto Ikutae0bfec72020-08-28 02:52:52 +0000744 if time.time() > started + 60 * 2:
745 # This is to not wait task having leaked process long time.
Ye Kuang0023dc52020-08-04 05:28:41 +0000746 raise
747
748 on_error.report('error before %d second backoff' % backoff)
749 logging.exception(
750 '_run_go_isolated_and_wait() failed, will retry after %d seconds',
751 backoff)
752 time.sleep(backoff)
753 backoff *= 2
Ye Kuangfb0bad62020-07-28 08:07:25 +0000754
755 with open(isolated_path) as isol_file:
756 isolated = isol_file.read()
757 with open(stats_json_path) as json_file:
758 stats_json = json.load(json_file)
759
760 return isolated, stats_json['items_cold'], stats_json['items_hot']
761 finally:
762 fs.remove(isolated_path)
763 fs.remove(stats_json_path)
764
765
Ye Kuangbc4e8402020-07-29 09:54:30 +0000766def upload_out_dir(storage, out_dir, go_isolated_client):
767 """Uploads the results in |out_dir| back, if there is any.
maruela9cfd6f2015-09-15 11:03:15 -0700768
769 Returns:
Ye Kuangbc4e8402020-07-29 09:54:30 +0000770 tuple(outputs_ref, stats)
maruel064c0a32016-04-05 11:47:15 -0700771 - outputs_ref: a dict referring to the results archived back to the isolated
772 server, if applicable.
nodir6f801882016-04-29 14:41:50 -0700773 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700774 """
maruela9cfd6f2015-09-15 11:03:15 -0700775 # Upload out_dir and generate a .isolated file out of this directory. It is
776 # only done if files were written in the directory.
777 outputs_ref = None
Ye Kuangfb0bad62020-07-28 08:07:25 +0000778 cold = ''
779 hot = ''
nodir6f801882016-04-29 14:41:50 -0700780 start = time.time()
781
maruel12e30012015-10-09 11:55:35 -0700782 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700783 with tools.Profiler('ArchiveOutput'):
Ye Kuangfb0bad62020-07-28 08:07:25 +0000784 isolated = None
Ye Kuang72e6fe82020-08-05 06:30:04 +0000785 if _USE_GO_ISOLATED_TO_UPLOAD and go_isolated_client is not None:
Ye Kuangfb0bad62020-07-28 08:07:25 +0000786 isolated, cold, hot = _upload_with_go(storage, out_dir,
787 go_isolated_client)
Ye Kuang72e6fe82020-08-05 06:30:04 +0000788 else:
789 isolated, cold, hot = _upload_with_py(storage, out_dir)
Ye Kuangfb0bad62020-07-28 08:07:25 +0000790 outputs_ref = {
791 'isolated': isolated,
792 'isolatedserver': storage.server_ref.url,
793 'namespace': storage.server_ref.namespace,
794 }
nodir6f801882016-04-29 14:41:50 -0700795
nodir6f801882016-04-29 14:41:50 -0700796 stats = {
Takuto Ikuta630f99d2020-07-02 12:59:35 +0000797 'duration': time.time() - start,
Ye Kuangfb0bad62020-07-28 08:07:25 +0000798 'items_cold': cold,
799 'items_hot': hot,
nodir6f801882016-04-29 14:41:50 -0700800 }
Ye Kuangbc4e8402020-07-29 09:54:30 +0000801 return outputs_ref, stats
maruela9cfd6f2015-09-15 11:03:15 -0700802
803
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500804def map_and_run(data, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700805 """Runs a command with optional isolated input/output.
806
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500807 Arguments:
808 - data: TaskData instance.
809 - constant_run_path: TODO
nodir55be77b2016-05-03 09:39:57 -0700810
811 Returns metadata about the result.
812 """
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +0000813
814 if data.isolate_cache:
815 download_stats = {
816 #'duration': 0.,
817 'initial_number_items': len(data.isolate_cache),
818 'initial_size': data.isolate_cache.total_size,
819 #'items_cold': '<large.pack()>',
820 #'items_hot': '<large.pack()>',
821 }
822 else:
823 # TODO(tikuta): take stats from state.json in this case too.
824 download_stats = {}
825
maruela9cfd6f2015-09-15 11:03:15 -0700826 result = {
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000827 'duration': None,
828 'exit_code': None,
829 'had_hard_timeout': False,
830 'internal_failure': 'run_isolated did not complete properly',
831 'stats': {
832 #'cipd': {
833 # 'duration': 0.,
834 # 'get_client_duration': 0.,
835 #},
836 'isolated': {
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +0000837 'download': download_stats,
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000838 #'upload': {
839 # 'duration': 0.,
840 # 'items_cold': '<large.pack()>',
841 # 'items_hot': '<large.pack()>',
842 #},
843 },
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000844 },
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000845 #'cipd_pins': {
846 # 'packages': [
847 # {'package_name': ..., 'version': ..., 'path': ...},
848 # ...
849 # ],
850 # 'client_package': {'package_name': ..., 'version': ...},
851 #},
852 'outputs_ref': None,
Junji Watanabe54925c32020-09-08 00:56:18 +0000853 'cas_output_root': None,
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000854 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700855 }
nodirbe642ff2016-06-09 15:51:51 -0700856
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500857 if data.root_dir:
Lei Leife202df2019-06-11 17:33:34 +0000858 file_path.ensure_tree(data.root_dir, 0o700)
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +0000859 elif data.use_go_isolated:
860 data = data._replace(root_dir=os.path.dirname(data.go_cache_dir))
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500861 elif data.isolate_cache.cache_dir:
862 data = data._replace(
863 root_dir=os.path.dirname(data.isolate_cache.cache_dir))
maruele2f2cb82016-07-13 14:41:03 -0700864 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700865 # If root_dir is not specified, it is not constant.
866 # TODO(maruel): This is not obvious. Change this to become an error once we
867 # make the constant_run_path an exposed flag.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500868 if constant_run_path and data.root_dir:
869 run_dir = os.path.join(data.root_dir, ISOLATED_RUN_DIR)
maruel5c4eed82017-05-26 05:33:40 -0700870 if os.path.isdir(run_dir):
871 file_path.rmtree(run_dir)
Lei Leife202df2019-06-11 17:33:34 +0000872 os.mkdir(run_dir, 0o700)
maruelcffa0542017-04-07 08:39:20 -0700873 else:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500874 run_dir = make_temp_dir(ISOLATED_RUN_DIR, data.root_dir)
maruel03e11842016-07-14 10:50:16 -0700875 # storage should be normally set but don't crash if it is not. This can happen
876 # as Swarming task can run without an isolate server.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500877 out_dir = make_temp_dir(
878 ISOLATED_OUT_DIR, data.root_dir) if data.storage else None
879 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, data.root_dir)
Takuto Ikutab7ce0e32019-11-27 23:26:18 +0000880 isolated_client_dir = make_temp_dir(ISOLATED_CLIENT_DIR, data.root_dir)
nodir55be77b2016-05-03 09:39:57 -0700881 cwd = run_dir
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500882 if data.relative_cwd:
883 cwd = os.path.normpath(os.path.join(cwd, data.relative_cwd))
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500884 command = data.command
Ye Kuangfb0bad62020-07-28 08:07:25 +0000885 go_isolated_client = None
886 if data.use_go_isolated:
887 go_isolated_client = os.path.join(isolated_client_dir,
888 'isolated' + cipd.EXECUTABLE_SUFFIX)
nodir55be77b2016-05-03 09:39:57 -0700889 try:
Takuto Ikutab7ce0e32019-11-27 23:26:18 +0000890 with data.install_packages_fn(run_dir, isolated_client_dir) as cipd_info:
vadimsh232f5a82017-01-20 19:23:44 -0800891 if cipd_info:
892 result['stats']['cipd'] = cipd_info.stats
893 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700894
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500895 if data.isolated_hash:
vadimsh232f5a82017-01-20 19:23:44 -0800896 isolated_stats = result['stats'].setdefault('isolated', {})
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000897 if data.use_go_isolated:
Takuto Ikuta90397ca2020-01-08 10:07:55 +0000898 bundle, stats = _fetch_and_map_with_go(
899 isolated_hash=data.isolated_hash,
900 storage=data.storage,
Takuto Ikuta90397ca2020-01-08 10:07:55 +0000901 outdir=run_dir,
902 go_cache_dir=data.go_cache_dir,
Takuto Ikuta879788c2020-01-10 08:00:26 +0000903 policies=data.go_cache_policies,
Ye Kuangfb0bad62020-07-28 08:07:25 +0000904 isolated_client=go_isolated_client)
Takuto Ikuta90397ca2020-01-08 10:07:55 +0000905 else:
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000906 bundle, stats = fetch_and_map(
907 isolated_hash=data.isolated_hash,
908 storage=data.storage,
909 cache=data.isolate_cache,
Takuto Ikuta16fac4b2019-12-09 04:57:18 +0000910 outdir=run_dir)
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000911 isolated_stats['download'].update(stats)
Takuto Ikutab58dbd12020-06-05 09:29:14 +0000912
maruelabec63c2017-04-26 11:53:24 -0700913 # Inject the command
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500914 if not command and bundle.command:
915 command = bundle.command + data.extra_args
Marc-Antoine Rueld704a1f2017-10-31 10:51:23 -0400916 # Only set the relative directory if the isolated file specified a
917 # command, and no raw command was specified.
918 if bundle.relative_cwd:
919 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
maruelabec63c2017-04-26 11:53:24 -0700920
Junji Watanabe54925c32020-09-08 00:56:18 +0000921 elif data.cas_digest:
922 # TODO(crbug.com/1117004): download inputs from CAS.
923 # stats = _fetch_and_map_with_cas_client(
924 # instance=data.cas_instance,
925 # digest=data.cas_digest,
926 # out_dir=data.out_dir,
927 # cas_client=cas_client,
928 # ...(cache options)...
929 # )
930 #
931 # TODO(crbug.com/1117004): update downlaod stats.
932 # isolated_stats['download'].update(stats)
933 pass
934
maruelabec63c2017-04-26 11:53:24 -0700935 if not command:
936 # Handle this as a task failure, not an internal failure.
937 sys.stderr.write(
938 '<No command was specified!>\n'
939 '<Please secify a command when triggering your Swarming task>\n')
940 result['exit_code'] = 1
941 return result
nodirbe642ff2016-06-09 15:51:51 -0700942
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500943 if not cwd.startswith(run_dir):
944 # Handle this as a task failure, not an internal failure. This is a
945 # 'last chance' way to gate against directory escape.
946 sys.stderr.write('<Relative CWD is outside of run directory!>\n')
947 result['exit_code'] = 1
948 return result
949
950 if not os.path.isdir(cwd):
951 # Accepts relative_cwd that does not exist.
Lei Leife202df2019-06-11 17:33:34 +0000952 os.makedirs(cwd, 0o700)
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500953
vadimsh232f5a82017-01-20 19:23:44 -0800954 # If we have an explicit list of files to return, make sure their
955 # directories exist now.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500956 if data.storage and data.outputs:
957 isolateserver.create_directories(run_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700958
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500959 with data.install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -0800960 sys.stdout.flush()
961 start = time.time()
962 try:
vadimsh9c54b2c2017-07-25 14:08:29 -0700963 # Need to switch the default account before 'get_command_env' call,
964 # so it can grab correct value of LUCI_CONTEXT env var.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500965 with set_luci_context_account(data.switch_to_account, tmp_dir):
966 env = get_command_env(
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000967 tmp_dir, cipd_info, run_dir, data.env, data.env_prefix, out_dir,
968 data.bot_file)
Brian Sheedy7a761172019-08-30 22:55:14 +0000969 command = tools.find_executable(command, env)
Robert Iannucci24ae76a2018-02-26 12:51:18 -0800970 command = process_command(command, out_dir, data.bot_file)
971 file_path.ensure_command_has_abs_path(command, cwd)
972
vadimsh9c54b2c2017-07-25 14:08:29 -0700973 result['exit_code'], result['had_hard_timeout'] = run_command(
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000974 command, cwd, env, data.hard_timeout, data.grace_period,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000975 data.lower_priority, data.containment)
nodird6160682017-02-02 13:03:35 -0800976 finally:
977 result['duration'] = max(time.time() - start, 0)
Seth Koehler49139812017-12-19 13:59:33 -0500978
Ye Kuangbc4e8402020-07-29 09:54:30 +0000979 if out_dir:
980 # Try to link files to the output directory, if specified.
981 link_outputs_to_outdir(run_dir, out_dir, data.outputs)
982 isolated_stats = result['stats'].setdefault('isolated', {})
983 # This could use |go_isolated_client|, so make sure it runs when the
984 # CIPD package still exists.
985 result['outputs_ref'], isolated_stats['upload'] = (
986 upload_out_dir(data.storage, out_dir, go_isolated_client))
Junji Watanabe54925c32020-09-08 00:56:18 +0000987 # TODO(crbug.com/1117004): upload to CAS if the inputs are on CAS.
988 # The `cas_output_root` will be updated instead of `outputs_ref`.
989 result['cas_output_root'] = None
Seth Koehler49139812017-12-19 13:59:33 -0500990 # We successfully ran the command, set internal_failure back to
991 # None (even if the command failed, it's not an internal error).
992 result['internal_failure'] = None
maruela9cfd6f2015-09-15 11:03:15 -0700993 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700994 # An internal error occurred. Report accordingly so the swarming task will
995 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700996 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700997 result['internal_failure'] = str(e)
998 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700999
1000 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -07001001 finally:
1002 try:
Ye Kuangbc4e8402020-07-29 09:54:30 +00001003 success = True
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001004 if data.leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -07001005 success = True
maruela9cfd6f2015-09-15 11:03:15 -07001006 logging.warning(
1007 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -07001008 else:
maruel84537cb2015-10-16 14:21:28 -07001009 # On Windows rmtree(run_dir) call above has a synchronization effect: it
1010 # finishes only when all task child processes terminate (since a running
1011 # process locks *.exe file). Examine out_dir only after that call
1012 # completes (since child processes may write to out_dir too and we need
1013 # to wait for them to finish).
Ye Kuangbc4e8402020-07-29 09:54:30 +00001014 dirs_to_remove = [run_dir, tmp_dir, isolated_client_dir]
1015 if out_dir:
1016 dirs_to_remove.append(out_dir)
1017 for directory in dirs_to_remove:
Takuto Ikuta69c0d662019-11-27 01:18:08 +00001018 if not fs.isdir(directory):
1019 continue
maruel84537cb2015-10-16 14:21:28 -07001020 try:
Ye Kuangbc4e8402020-07-29 09:54:30 +00001021 success = success and file_path.rmtree(directory)
maruel84537cb2015-10-16 14:21:28 -07001022 except OSError as e:
Takuto Ikuta69c0d662019-11-27 01:18:08 +00001023 logging.error('rmtree(%r) failed: %s', directory, e)
maruel84537cb2015-10-16 14:21:28 -07001024 success = False
1025 if not success:
Takuto Ikuta69c0d662019-11-27 01:18:08 +00001026 sys.stderr.write(
1027 OUTLIVING_ZOMBIE_MSG % (directory, data.grace_period))
Takuto Ikutad7d64e12020-07-31 06:18:45 +00001028 subprocess42.check_call(['tasklist.exe', '/V'], stdout=sys.stderr)
maruel84537cb2015-10-16 14:21:28 -07001029 if result['exit_code'] == 0:
1030 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -07001031
maruela9cfd6f2015-09-15 11:03:15 -07001032 if not success and result['exit_code'] == 0:
1033 result['exit_code'] = 1
1034 except Exception as e:
1035 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -07001036 if out_dir:
1037 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -07001038 result['internal_failure'] = str(e)
Takuto Ikutaa9a907b2020-04-17 08:50:50 +00001039 on_error.report(None)
maruela9cfd6f2015-09-15 11:03:15 -07001040 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -05001041
1042
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001043def run_tha_test(data, result_json):
nodir55be77b2016-05-03 09:39:57 -07001044 """Runs an executable and records execution metadata.
1045
nodir55be77b2016-05-03 09:39:57 -07001046 If isolated_hash is specified, downloads the dependencies in the cache,
1047 hardlinks them into a temporary directory and runs the command specified in
1048 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -05001049
1050 A temporary directory is created to hold the output files. The content inside
1051 this directory will be uploaded back to |storage| packaged as a .isolated
1052 file.
1053
1054 Arguments:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001055 - data: TaskData instance.
1056 - result_json: File path to dump result metadata into. If set, the process
1057 exit code is always 0 unless an internal error occurred.
maruela9cfd6f2015-09-15 11:03:15 -07001058
1059 Returns:
1060 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001061 """
maruela76b9ee2015-12-15 06:18:08 -08001062 if result_json:
1063 # Write a json output file right away in case we get killed.
1064 result = {
Junji Watanabe54925c32020-09-08 00:56:18 +00001065 'exit_code': None,
1066 'had_hard_timeout': False,
1067 'internal_failure': 'Was terminated before completion',
1068 'outputs_ref': None,
1069 'cas_output_root': None,
1070 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -08001071 }
1072 tools.write_json(result_json, result, dense=True)
1073
maruela9cfd6f2015-09-15 11:03:15 -07001074 # run_isolated exit code. Depends on if result_json is used or not.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001075 result = map_and_run(data, True)
maruela9cfd6f2015-09-15 11:03:15 -07001076 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -07001077
maruela9cfd6f2015-09-15 11:03:15 -07001078 if result_json:
maruel05d5a882015-09-21 13:59:02 -07001079 # We've found tests to delete 'work' when quitting, causing an exception
1080 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -07001081 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -07001082 tools.write_json(result_json, result, dense=True)
1083 # Only return 1 if there was an internal error.
1084 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +00001085
maruela9cfd6f2015-09-15 11:03:15 -07001086 # Marshall into old-style inline output.
1087 if result['outputs_ref']:
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001088 # pylint: disable=unsubscriptable-object
maruela9cfd6f2015-09-15 11:03:15 -07001089 data = {
Junji Watanabe38b28b02020-04-23 10:23:30 +00001090 'hash': result['outputs_ref']['isolated'],
1091 'namespace': result['outputs_ref']['namespace'],
1092 'storage': result['outputs_ref']['isolatedserver'],
maruela9cfd6f2015-09-15 11:03:15 -07001093 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -05001094 sys.stdout.flush()
Junji Watanabe38b28b02020-04-23 10:23:30 +00001095 print('[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
1096 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -08001097 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -07001098 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001099
1100
iannuccib58d10d2017-03-18 02:00:25 -07001101# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -08001102CipdInfo = collections.namedtuple('CipdInfo', [
1103 'client', # cipd.CipdClient object
1104 'cache_dir', # absolute path to bot-global cipd tag and instance cache
1105 'stats', # dict with stats to return to the server
1106 'pins', # dict with installed cipd pins to return to the server
1107])
1108
1109
1110@contextlib.contextmanager
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001111def noop_install_packages(_run_dir, _isolated_dir):
iannuccib58d10d2017-03-18 02:00:25 -07001112 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -08001113 yield None
1114
1115
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001116def _install_packages(run_dir, cipd_cache_dir, client, packages):
iannuccib58d10d2017-03-18 02:00:25 -07001117 """Calls 'cipd ensure' for packages.
1118
1119 Args:
1120 run_dir (str): root of installation.
1121 cipd_cache_dir (str): the directory to use for the cipd package cache.
1122 client (CipdClient): the cipd client to use
1123 packages: packages to install, list [(path, package_name, version), ...].
iannuccib58d10d2017-03-18 02:00:25 -07001124
1125 Returns: list of pinned packages. Looks like [
1126 {
1127 'path': 'subdirectory',
1128 'package_name': 'resolved/package/name',
1129 'version': 'deadbeef...',
1130 },
1131 ...
1132 ]
1133 """
1134 package_pins = [None]*len(packages)
1135 def insert_pin(path, name, version, idx):
1136 package_pins[idx] = {
1137 'package_name': name,
1138 # swarming deals with 'root' as '.'
1139 'path': path or '.',
1140 'version': version,
1141 }
1142
1143 by_path = collections.defaultdict(list)
1144 for i, (path, name, version) in enumerate(packages):
1145 # cipd deals with 'root' as ''
1146 if path == '.':
1147 path = ''
1148 by_path[path].append((name, version, i))
1149
1150 pins = client.ensure(
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001151 run_dir,
1152 {
1153 subdir: [(name, vers) for name, vers, _ in pkgs
1154 ] for subdir, pkgs in by_path.items()
1155 },
1156 cache_dir=cipd_cache_dir,
iannuccib58d10d2017-03-18 02:00:25 -07001157 )
1158
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001159 for subdir, pin_list in sorted(pins.items()):
iannuccib58d10d2017-03-18 02:00:25 -07001160 this_subdir = by_path[subdir]
1161 for i, (name, version) in enumerate(pin_list):
1162 insert_pin(subdir, name, version, this_subdir[i][2])
1163
Robert Iannucci461b30d2017-12-13 11:34:03 -08001164 assert None not in package_pins, (packages, pins, package_pins)
iannuccib58d10d2017-03-18 02:00:25 -07001165
1166 return package_pins
1167
1168
vadimsh232f5a82017-01-20 19:23:44 -08001169@contextlib.contextmanager
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001170def install_client_and_packages(run_dir, packages, service_url,
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001171 client_package_name, client_version, cache_dir,
1172 isolated_dir):
vadimsh902948e2017-01-20 15:57:32 -08001173 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -07001174
vadimsh232f5a82017-01-20 19:23:44 -08001175 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
1176
1177 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -07001178 [
1179 {
1180 "path": path, "package_name": package_name, "version": version,
1181 },
1182 ...
1183 ]
vadimsh902948e2017-01-20 15:57:32 -08001184 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -07001185
1186 such that they correspond 1:1 to all input package arguments from the command
1187 line. These dictionaries make their all the way back to swarming, where they
1188 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -07001189
vadimsh902948e2017-01-20 15:57:32 -08001190 If 'packages' list is empty, will bootstrap CIPD client, but won't install
1191 any packages.
1192
1193 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -08001194 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -08001195
nodirbe642ff2016-06-09 15:51:51 -07001196 Args:
nodir90bc8dc2016-06-15 13:35:21 -07001197 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -08001198 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -07001199 service_url (str): CIPD server url, e.g.
1200 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -07001201 client_package_name (str): CIPD package name of CIPD client.
1202 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -07001203 cache_dir (str): where to keep cache of cipd clients, packages and tags.
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001204 isolated_dir (str): where to download isolated client.
nodirbe642ff2016-06-09 15:51:51 -07001205 """
1206 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -07001207
nodirbe642ff2016-06-09 15:51:51 -07001208 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -07001209
vadimsh902948e2017-01-20 15:57:32 -08001210 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -08001211 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -07001212 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -08001213 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -07001214
nodirbe642ff2016-06-09 15:51:51 -07001215 get_client_start = time.time()
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001216 client_manager = cipd.get_client(service_url, client_package_name,
1217 client_version, cache_dir)
iannucci96fcccc2016-08-30 15:52:22 -07001218
nodirbe642ff2016-06-09 15:51:51 -07001219 with client_manager as client:
1220 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -07001221
iannuccib58d10d2017-03-18 02:00:25 -07001222 package_pins = []
1223 if packages:
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001224 package_pins = _install_packages(run_dir, cipd_cache_dir, client,
1225 packages)
iannuccib58d10d2017-03-18 02:00:25 -07001226
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001227 # Install isolated client to |isolated_dir|.
Takuto Ikuta02edca22019-11-29 10:04:51 +00001228 _install_packages(isolated_dir, cipd_cache_dir, client,
1229 [('', ISOLATED_PACKAGE, ISOLATED_REVISION)])
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001230
iannuccib58d10d2017-03-18 02:00:25 -07001231 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -07001232
vadimsh232f5a82017-01-20 19:23:44 -08001233 total_duration = time.time() - start
Junji Watanabe38b28b02020-04-23 10:23:30 +00001234 logging.info('Installing CIPD client and packages took %d seconds',
1235 total_duration)
nodir90bc8dc2016-06-15 13:35:21 -07001236
vadimsh232f5a82017-01-20 19:23:44 -08001237 yield CipdInfo(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001238 client=client,
1239 cache_dir=cipd_cache_dir,
1240 stats={
1241 'duration': total_duration,
1242 'get_client_duration': get_client_duration,
iannuccib58d10d2017-03-18 02:00:25 -07001243 },
Junji Watanabe38b28b02020-04-23 10:23:30 +00001244 pins={
1245 'client_package': {
1246 'package_name': client.package_name,
1247 'version': client.instance_id,
1248 },
1249 'packages': package_pins,
1250 })
nodirbe642ff2016-06-09 15:51:51 -07001251
1252
1253def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001254 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -07001255 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +00001256 version=__version__,
1257 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -07001258 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001259 '--clean',
1260 action='store_true',
maruel36a963d2016-04-08 17:15:49 -07001261 help='Cleans the cache, trimming it necessary and remove corrupted items '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001262 'and returns without executing anything; use with -v to know what '
1263 'was done')
maruel36a963d2016-04-08 17:15:49 -07001264 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -07001265 '--json',
1266 help='dump output metadata to json file. When used, run_isolated returns '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001267 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -07001268 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001269 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -07001270 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001271 '--grace-period',
1272 type='float',
maruel6be7f9e2015-10-01 12:25:30 -07001273 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -07001274 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001275 '--raw-cmd',
1276 action='store_true',
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001277 help='Ignore the isolated command, use the one supplied at the command '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001278 'line')
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001279 parser.add_option(
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001280 '--relative-cwd',
1281 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001282 'requires --raw-cmd')
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001283 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001284 '--env',
1285 default=[],
1286 action='append',
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001287 help='Environment variables to set for the child process')
1288 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001289 '--env-prefix',
1290 default=[],
1291 action='append',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001292 help='Specify a VAR=./path/fragment to put in the environment variable '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001293 'before executing the command. The path fragment must be relative '
1294 'to the isolated run directory, and must not contain a `..` token. '
1295 'The path will be made absolute and prepended to the indicated '
1296 '$VAR using the OS\'s path separator. Multiple items for the same '
1297 '$VAR will be prepended in order.')
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001298 parser.add_option(
bpastene3ae09522016-06-10 17:12:59 -07001299 '--bot-file',
1300 help='Path to a file describing the state of the host. The content is '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001301 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -07001302 parser.add_option(
vadimsh9c54b2c2017-07-25 14:08:29 -07001303 '--switch-to-account',
1304 help='If given, switches LUCI_CONTEXT to given logical service account '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001305 '(e.g. "task" or "system") before launching the isolated process.')
vadimsh9c54b2c2017-07-25 14:08:29 -07001306 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001307 '--output',
1308 action='append',
aludwin0a8e17d2016-10-27 15:57:39 -07001309 help='Specifies an output to return. If no outputs are specified, all '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001310 'files located in $(ISOLATED_OUTDIR) will be returned; '
1311 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
1312 'specified by --output option (there can be multiple) will be '
1313 'returned. Note that if a file in OUT_DIR has the same path '
1314 'as an --output option, the --output version will be returned.')
aludwin0a8e17d2016-10-27 15:57:39 -07001315 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001316 '-a',
1317 '--argsfile',
aludwin7556e0c2016-10-26 08:46:10 -07001318 # This is actually handled in parse_args; it's included here purely so it
1319 # can make it into the help text.
1320 help='Specify a file containing a JSON array of arguments to this '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001321 'script. If --argsfile is provided, no other argument may be '
1322 'provided on the command line.')
Takuto Ikutad4be2f12020-05-12 02:15:25 +00001323 parser.add_option(
1324 '--report-on-exception',
1325 action='store_true',
1326 help='Whether report exception during execution to isolate server. '
1327 'This flag should only be used in swarming bot.')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001328
1329 group = optparse.OptionGroup(parser, 'Data source')
Junji Watanabe54925c32020-09-08 00:56:18 +00001330 # Deprecated. Isoate server is being migrated to RBE-CAS.
1331 # Remove --isolated and isolate server options after migration.
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001332 group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001333 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -07001334 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001335 isolateserver.add_isolate_server_options(group)
Junji Watanabe54925c32020-09-08 00:56:18 +00001336 group.add_option(
1337 '--cas-instance', help='Full CAS instance name for input/output files.')
1338 group.add_option(
1339 '--cas-digest',
1340 help='Digest of the input root on RBE-CAS. The format is '
1341 '`{hash}/{size_bytes}`.')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001342 parser.add_option_group(group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001343
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -04001344 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001345
1346 cipd.add_cipd_options(parser)
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001347
1348 group = optparse.OptionGroup(parser, 'Named caches')
1349 group.add_option(
1350 '--named-cache',
1351 dest='named_caches',
1352 action='append',
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001353 nargs=3,
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001354 default=[],
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001355 help='A named cache to request. Accepts 3 arguments: name, path, hint. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001356 'name identifies the cache, must match regex [a-z0-9_]{1,4096}. '
1357 'path is a path relative to the run dir where the cache directory '
1358 'must be put to. '
1359 'This option can be specified more than once.')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001360 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001361 '--named-cache-root',
1362 default='named_caches',
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001363 help='Cache root directory. Default=%default')
1364 parser.add_option_group(group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001365
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001366 group = optparse.OptionGroup(parser, 'Process containment')
1367 parser.add_option(
1368 '--lower-priority', action='store_true',
1369 help='Lowers the child process priority')
1370 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001371 '--containment-type',
1372 choices=('NONE', 'AUTO', 'JOB_OBJECT'),
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001373 default='NONE',
1374 help='Type of container to use')
1375 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001376 '--limit-processes',
1377 type='int',
1378 default=0,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001379 help='Maximum number of active processes in the containment')
1380 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001381 '--limit-total-committed-memory',
1382 type='int',
1383 default=0,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001384 help='Maximum sum of committed memory in the containment')
1385 parser.add_option_group(group)
1386
1387 group = optparse.OptionGroup(parser, 'Debugging')
1388 group.add_option(
Kenneth Russell61d42352014-09-15 11:41:16 -07001389 '--leak-temp-dir',
1390 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -07001391 help='Deliberately leak isolate\'s temp dir for later examination. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001392 'Default: %default')
1393 group.add_option('--root-dir', help='Use a directory instead of a random one')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001394 parser.add_option_group(group)
Kenneth Russell61d42352014-09-15 11:41:16 -07001395
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001396 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001397
Ye Kuang1d096cb2020-06-26 08:38:21 +00001398 parser.set_defaults(cache='cache')
nodirbe642ff2016-06-09 15:51:51 -07001399 return parser
1400
1401
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001402def process_named_cache_options(parser, options, time_fn=None):
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001403 """Validates named cache options and returns a CacheManager."""
1404 if options.named_caches and not options.named_cache_root:
1405 parser.error('--named-cache is specified, but --named-cache-root is empty')
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001406 for name, path, hint in options.named_caches:
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001407 if not CACHE_NAME_RE.match(name):
1408 parser.error(
1409 'cache name %r does not match %r' % (name, CACHE_NAME_RE.pattern))
1410 if not path:
1411 parser.error('cache path cannot be empty')
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001412 try:
Takuto Ikuta630f99d2020-07-02 12:59:35 +00001413 int(hint)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001414 except ValueError:
1415 parser.error('cache hint must be a number')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001416 if options.named_cache_root:
1417 # Make these configurable later if there is use case but for now it's fairly
1418 # safe values.
1419 # In practice, a fair chunk of bots are already recycled on a daily schedule
1420 # so this code doesn't have any effect to them, unless they are preloaded
1421 # with a really old cache.
1422 policies = local_caching.CachePolicies(
1423 # 1TiB.
1424 max_cache_size=1024*1024*1024*1024,
1425 min_free_space=options.min_free_space,
1426 max_items=50,
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +00001427 max_age_secs=MAX_AGE_SECS)
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001428 root_dir = six.text_type(os.path.abspath(options.named_cache_root))
John Budorickc6186972020-02-26 00:58:14 +00001429 cache = local_caching.NamedCache(root_dir, policies, time_fn=time_fn)
1430 # Touch any named caches we're going to use to minimize thrashing
1431 # between tasks that request some (but not all) of the same named caches.
John Budorick0a4dab62020-03-02 22:23:35 +00001432 cache.touch(*[name for name, _, _ in options.named_caches])
John Budorickc6186972020-02-26 00:58:14 +00001433 return cache
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001434 return None
1435
1436
aludwin7556e0c2016-10-26 08:46:10 -07001437def parse_args(args):
1438 # Create a fake mini-parser just to get out the "-a" command. Note that
1439 # it's not documented here; instead, it's documented in create_option_parser
1440 # even though that parser will never actually get to parse it. This is
1441 # because --argsfile is exclusive with all other options and arguments.
1442 file_argparse = argparse.ArgumentParser(add_help=False)
1443 file_argparse.add_argument('-a', '--argsfile')
1444 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
1445 if file_args.argsfile:
1446 if nonfile_args:
1447 file_argparse.error('Can\'t specify --argsfile with'
1448 'any other arguments (%s)' % nonfile_args)
1449 try:
1450 with open(file_args.argsfile, 'r') as f:
1451 args = json.load(f)
1452 except (IOError, OSError, ValueError) as e:
1453 # We don't need to error out here - "args" is now empty,
1454 # so the call below to parser.parse_args(args) will fail
1455 # and print the full help text.
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +00001456 print('Couldn\'t read arguments: %s' % e, file=sys.stderr)
aludwin7556e0c2016-10-26 08:46:10 -07001457
1458 # Even if we failed to read the args, just call the normal parser now since it
1459 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -07001460 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001461 options, args = parser.parse_args(args)
Ye Kuangfff1e502020-07-13 13:21:57 +00001462 if not isinstance(options.cipd_enabled, (bool, int)):
1463 options.cipd_enabled = distutils.util.strtobool(options.cipd_enabled)
aludwin7556e0c2016-10-26 08:46:10 -07001464 return (parser, options, args)
1465
1466
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001467def _calc_named_cache_hint(named_cache, named_caches):
1468 """Returns the expected size of the missing named caches."""
1469 present = named_cache.available
1470 size = 0
1471 for name, _, hint in named_caches:
1472 if name not in present:
Takuto Ikuta630f99d2020-07-02 12:59:35 +00001473 hint = int(hint)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001474 if hint > 0:
1475 size += hint
1476 return size
1477
1478
aludwin7556e0c2016-10-26 08:46:10 -07001479def main(args):
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -05001480 # Warning: when --argsfile is used, the strings are unicode instances, when
1481 # parsed normally, the strings are str instances.
aludwin7556e0c2016-10-26 08:46:10 -07001482 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -07001483
Takuto Ikutad4be2f12020-05-12 02:15:25 +00001484 if options.report_on_exception and options.isolate_server:
1485 on_error.report_on_exception_exit(options.isolate_server)
1486
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001487 if not file_path.enable_symlink():
Marc-Antoine Ruel5a024272019-01-15 20:11:16 +00001488 logging.warning('Symlink support is not enabled')
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001489
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001490 named_cache = process_named_cache_options(parser, options)
Marc-Antoine Ruel0d8b0f62018-09-10 14:40:35 +00001491 # hint is 0 if there's no named cache.
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001492 hint = _calc_named_cache_hint(named_cache, options.named_caches)
1493 if hint:
1494 # Increase the --min-free-space value by the hint, and recreate the
1495 # NamedCache instance so it gets the updated CachePolicy.
1496 options.min_free_space += hint
1497 named_cache = process_named_cache_options(parser, options)
1498
Takuto Ikuta5c59a842020-01-24 03:05:24 +00001499 # TODO(crbug.com/932396): Remove this.
Takuto Ikuta4a22c2c2020-06-05 02:02:23 +00001500 use_go_isolated = options.cipd_enabled
Takuto Ikuta5c59a842020-01-24 03:05:24 +00001501
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001502 # TODO(maruel): CIPD caches should be defined at an higher level here too, so
1503 # they can be cleaned the same way.
Ye Kuang97849802020-06-29 13:17:09 +00001504 if use_go_isolated and not options.clean:
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +00001505 isolate_cache = None
1506 else:
1507 isolate_cache = isolateserver.process_cache_options(options, trim=False)
1508
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001509 caches = []
1510 if isolate_cache:
1511 caches.append(isolate_cache)
1512 if named_cache:
1513 caches.append(named_cache)
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001514 root = caches[0].cache_dir if caches else six.text_type(os.getcwd())
maruel36a963d2016-04-08 17:15:49 -07001515 if options.clean:
1516 if options.isolated:
1517 parser.error('Can\'t use --isolated with --clean.')
1518 if options.isolate_server:
1519 parser.error('Can\'t use --isolate-server with --clean.')
1520 if options.json:
1521 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -07001522 if options.named_caches:
1523 parser.error('Can\t use --named-cache with --clean.')
Takuto Ikuta9ab28552020-07-31 08:15:45 +00001524
1525 logging.info("initial free space: %d", file_path.get_free_space(root))
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001526 # Trim first, then clean.
1527 local_caching.trim_caches(
1528 caches,
1529 root,
Takuto Ikuta616ce262020-09-07 08:43:48 +00001530 min_free_space=options.min_free_space,
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001531 max_age_secs=MAX_AGE_SECS)
Takuto Ikuta9ab28552020-07-31 08:15:45 +00001532 logging.info("free space after trim: %d", file_path.get_free_space(root))
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001533 for c in caches:
Marc-Antoine Ruel87fc2222018-06-18 13:09:24 +00001534 c.cleanup()
Takuto Ikuta9ab28552020-07-31 08:15:45 +00001535 logging.info("free space after cleanup: %d", file_path.get_free_space(root))
maruel36a963d2016-04-08 17:15:49 -07001536 return 0
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001537
1538 # Trim must still be done for the following case:
1539 # - named-cache was used
1540 # - some entries, with a large hint, where missing
1541 # - --min-free-space was increased accordingly, thus trimming is needed
1542 # Otherwise, this will have no effect, as bot_main calls run_isolated with
1543 # --clean after each task.
1544 if hint:
1545 logging.info('Additional trimming of %d bytes', hint)
Marc-Antoine Ruel0d8b0f62018-09-10 14:40:35 +00001546 local_caching.trim_caches(
1547 caches,
1548 root,
Takuto Ikuta616ce262020-09-07 08:43:48 +00001549 min_free_space=options.min_free_space,
Marc-Antoine Ruel0d8b0f62018-09-10 14:40:35 +00001550 max_age_secs=MAX_AGE_SECS)
maruel36a963d2016-04-08 17:15:49 -07001551
nodir55be77b2016-05-03 09:39:57 -07001552 if not options.isolated and not args:
1553 parser.error('--isolated or command to run is required.')
1554
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001555 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001556
Takuto Ikutaae767b32020-05-11 01:22:19 +00001557 isolateserver.process_isolate_server_options(parser, options, False)
nodir55be77b2016-05-03 09:39:57 -07001558 if not options.isolate_server:
1559 if options.isolated:
1560 parser.error('--isolated requires --isolate-server')
1561 if ISOLATED_OUTDIR_PARAMETER in args:
1562 parser.error(
1563 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001564
nodir90bc8dc2016-06-15 13:35:21 -07001565 if options.root_dir:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001566 options.root_dir = six.text_type(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -07001567 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001568 options.json = six.text_type(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001569
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001570 if any('=' not in i for i in options.env):
1571 parser.error(
1572 '--env required key=value form. value can be skipped to delete '
1573 'the variable')
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001574 options.env = dict(i.split('=', 1) for i in options.env)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001575
1576 prefixes = {}
1577 cwd = os.path.realpath(os.getcwd())
1578 for item in options.env_prefix:
1579 if '=' not in item:
1580 parser.error(
1581 '--env-prefix %r is malformed, must be in the form `VAR=./path`'
1582 % item)
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001583 key, opath = item.split('=', 1)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001584 if os.path.isabs(opath):
1585 parser.error('--env-prefix %r path is bad, must be relative.' % opath)
1586 opath = os.path.normpath(opath)
1587 if not os.path.realpath(os.path.join(cwd, opath)).startswith(cwd):
1588 parser.error(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001589 '--env-prefix %r path is bad, must be relative and not contain `..`.'
1590 % opath)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001591 prefixes.setdefault(key, []).append(opath)
1592 options.env_prefix = prefixes
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001593
nodirbe642ff2016-06-09 15:51:51 -07001594 cipd.validate_cipd_options(parser, options)
1595
vadimsh232f5a82017-01-20 19:23:44 -08001596 install_packages_fn = noop_install_packages
Ye Kuang1d096cb2020-06-26 08:38:21 +00001597 tmp_cipd_cache_dir = None
vadimsh902948e2017-01-20 15:57:32 -08001598 if options.cipd_enabled:
Ye Kuang1d096cb2020-06-26 08:38:21 +00001599 cache_dir = options.cipd_cache
1600 if not cache_dir:
1601 tmp_cipd_cache_dir = six.text_type(tempfile.mkdtemp())
1602 cache_dir = tmp_cipd_cache_dir
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001603 install_packages_fn = (
Ye Kuang1d096cb2020-06-26 08:38:21 +00001604 lambda run_dir, isolated_dir: install_client_and_packages(
1605 run_dir,
1606 cipd.parse_package_args(options.cipd_packages),
1607 options.cipd_server,
1608 options.cipd_client_package,
1609 options.cipd_client_version,
1610 cache_dir=cache_dir,
1611 isolated_dir=isolated_dir))
nodirbe642ff2016-06-09 15:51:51 -07001612
nodird6160682017-02-02 13:03:35 -08001613 @contextlib.contextmanager
nodir0ae98b32017-05-11 13:21:53 -07001614 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001615 # WARNING: this function depends on "options" variable defined in the outer
1616 # function.
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001617 assert six.text_type(run_dir), repr(run_dir)
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001618 assert os.path.isabs(run_dir), run_dir
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001619 named_caches = [(os.path.join(run_dir, six.text_type(relpath)), name)
1620 for name, relpath, _ in options.named_caches]
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001621 for path, name in named_caches:
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001622 named_cache.install(path, name)
nodird6160682017-02-02 13:03:35 -08001623 try:
1624 yield
1625 finally:
dnje289d132017-07-07 11:16:44 -07001626 # Uninstall each named cache, returning it to the cache pool. If an
1627 # uninstall fails for a given cache, it will remain in the task's
1628 # temporary space, get cleaned up by the Swarming bot, and be lost.
1629 #
1630 # If the Swarming bot cannot clean up the cache, it will handle it like
1631 # any other bot file that could not be removed.
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001632 for path, name in reversed(named_caches):
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001633 try:
Marc-Antoine Ruele9558372018-08-03 03:41:22 +00001634 # uninstall() doesn't trim but does call save() implicitly. Trimming
1635 # *must* be done manually via periodic 'run_isolated.py --clean'.
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001636 named_cache.uninstall(path, name)
1637 except local_caching.NamedCacheError:
1638 logging.exception('Error while removing named cache %r at %r. '
1639 'The cache will be lost.', path, name)
nodirf33b8d62016-10-26 22:34:58 -07001640
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001641 extra_args = []
1642 command = []
1643 if options.raw_cmd:
1644 command = args
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001645 if options.relative_cwd:
1646 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1647 if not a.startswith(os.getcwd()):
1648 parser.error(
1649 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001650 else:
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001651 if options.relative_cwd:
1652 parser.error('--relative-cwd requires --raw-cmd')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001653 extra_args = args
1654
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001655 containment_type = subprocess42.Containment.NONE
1656 if options.containment_type == 'AUTO':
1657 containment_type = subprocess42.Containment.AUTO
1658 if options.containment_type == 'JOB_OBJECT':
1659 containment_type = subprocess42.Containment.JOB_OBJECT
1660 containment = subprocess42.Containment(
1661 containment_type=containment_type,
1662 limit_processes=options.limit_processes,
1663 limit_total_committed_memory=options.limit_total_committed_memory)
1664
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001665 data = TaskData(
1666 command=command,
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001667 relative_cwd=options.relative_cwd,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001668 extra_args=extra_args,
1669 isolated_hash=options.isolated,
1670 storage=None,
1671 isolate_cache=isolate_cache,
Junji Watanabe54925c32020-09-08 00:56:18 +00001672 cas_instance=options.cas_instance,
1673 cas_digest=options.cas_digest,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001674 outputs=options.output,
1675 install_named_caches=install_named_caches,
1676 leak_temp_dir=options.leak_temp_dir,
1677 root_dir=_to_unicode(options.root_dir),
1678 hard_timeout=options.hard_timeout,
1679 grace_period=options.grace_period,
1680 bot_file=options.bot_file,
1681 switch_to_account=options.switch_to_account,
1682 install_packages_fn=install_packages_fn,
Takuto Ikuta5c59a842020-01-24 03:05:24 +00001683 use_go_isolated=use_go_isolated,
Takuto Ikuta10cae642020-01-08 08:12:07 +00001684 go_cache_dir=options.cache,
Takuto Ikuta879788c2020-01-10 08:00:26 +00001685 go_cache_policies=local_caching.CachePolicies(
1686 max_cache_size=options.max_cache_size,
1687 min_free_space=options.min_free_space,
1688 max_items=options.max_items,
1689 max_age_secs=None,
1690 ),
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001691 env=options.env,
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +00001692 env_prefix=options.env_prefix,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001693 lower_priority=bool(options.lower_priority),
1694 containment=containment)
nodirbe642ff2016-06-09 15:51:51 -07001695 try:
nodir90bc8dc2016-06-15 13:35:21 -07001696 if options.isolate_server:
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001697 server_ref = isolate_storage.ServerRef(
nodir90bc8dc2016-06-15 13:35:21 -07001698 options.isolate_server, options.namespace)
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001699 storage = isolateserver.get_storage(server_ref)
nodir90bc8dc2016-06-15 13:35:21 -07001700 with storage:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001701 data = data._replace(storage=storage)
nodirf33b8d62016-10-26 22:34:58 -07001702 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001703 assert storage.server_ref.hash_algo == server_ref.hash_algo
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001704 return run_tha_test(data, options.json)
1705 return run_tha_test(data, options.json)
Junji Watanabe38b28b02020-04-23 10:23:30 +00001706 except (cipd.Error, local_caching.NamedCacheError,
1707 local_caching.NoMoreSpace) as ex:
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +00001708 print(ex.message, file=sys.stderr)
nodirbe642ff2016-06-09 15:51:51 -07001709 return 1
Ye Kuang1d096cb2020-06-26 08:38:21 +00001710 finally:
1711 if tmp_cipd_cache_dir is not None:
1712 try:
1713 file_path.rmtree(tmp_cipd_cache_dir)
1714 except OSError:
1715 logging.exception('Remove tmp_cipd_cache_dir=%s failed',
1716 tmp_cipd_cache_dir)
1717 # Best effort clean up. Failed to do so doesn't affect the outcome.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001718
1719
1720if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001721 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001722 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001723 fix_encoding.fix_encoding()
Ye Kuang2dd17442020-04-22 08:45:52 +00001724 net.set_user_agent('run_isolated.py/' + __version__)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001725 sys.exit(main(sys.argv[1:]))