blob: 55e44a852aa358dc15736013c6c0fa354f85cbba [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
Joanna Wang4cec0e42021-08-26 00:48:37 +000033Any ${SWARMING_TASK_ID} on the command line will be replaced by the
34SWARMING_TASK_ID value passed with the --env option.
35
Marc-Antoine Rueleed2f3a2019-03-14 00:00:40 +000036See
37https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
38for all the variables.
39
40See
41https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/swarming_bot/config/bot_config.py
42for more information about bot_config.py.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000043"""
44
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +000045from __future__ import print_function
46
47__version__ = '1.0.1'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000048
aludwin7556e0c2016-10-26 08:46:10 -070049import argparse
maruel064c0a32016-04-05 11:47:15 -070050import base64
iannucci96fcccc2016-08-30 15:52:22 -070051import collections
vadimsh232f5a82017-01-20 19:23:44 -080052import contextlib
Ye Kuangfff1e502020-07-13 13:21:57 +000053import distutils
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -040054import errno
aludwin7556e0c2016-10-26 08:46:10 -070055import json
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000056import logging
57import optparse
58import os
Takuto Ikuta5c59a842020-01-24 03:05:24 +000059import platform
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -040060import re
Junji Watanabedc2f89e2021-11-08 08:44:30 +000061import shutil
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000062import sys
63import tempfile
maruel064c0a32016-04-05 11:47:15 -070064import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000065
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000066from utils import tools
67tools.force_local_third_party()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000068
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000069# third_party/
70from depot_tools import fix_encoding
Takuto Ikuta6e2ff962019-10-29 12:35:27 +000071import six
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000072
73# pylint: disable=ungrouped-imports
Takuto Ikutad53d7bd2021-07-16 03:09:33 +000074import DEPS
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000075import auth
76import cipd
77import isolate_storage
78import isolateserver
79import local_caching
80from libs import luci_context
Vadim Shtayura6b555c12014-07-23 16:22:18 -070081from utils import file_path
maruel12e30012015-10-09 11:55:35 -070082from utils import fs
maruel064c0a32016-04-05 11:47:15 -070083from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040084from utils import logging_utils
Ye Kuang2dd17442020-04-22 08:45:52 +000085from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040086from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050087from utils import subprocess42
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000088
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000089
maruele2f2cb82016-07-13 14:41:03 -070090# Magic variables that can be found in the isolate task command line.
91ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
92EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
93SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
Joanna Wang4cec0e42021-08-26 00:48:37 +000094SWARMING_TASK_ID_PARAMETER = '${SWARMING_TASK_ID}'
maruele2f2cb82016-07-13 14:41:03 -070095
96
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000097# The name of the log file to use.
98RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
99
maruele2f2cb82016-07-13 14:41:03 -0700100
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
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +0000104# path length. A use case is with recursive dependency trees like npm packages.
maruele2f2cb82016-07-13 14:41:03 -0700105#
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
Takuto Ikutab7ce0e32019-11-27 23:26:18 +0000111# - ic stands for isolated_client
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +0000112# - ns stands for nsjail
maruele2f2cb82016-07-13 14:41:03 -0700113ISOLATED_RUN_DIR = u'ir'
114ISOLATED_OUT_DIR = u'io'
115ISOLATED_TMP_DIR = u'it'
Takuto Ikutab7ce0e32019-11-27 23:26:18 +0000116ISOLATED_CLIENT_DIR = u'ic'
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000117_CAS_CLIENT_DIR = u'cc'
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +0000118_NSJAIL_DIR = u'ns'
maruele2f2cb82016-07-13 14:41:03 -0700119
Takuto Ikuta02edca22019-11-29 10:04:51 +0000120# TODO(tikuta): take these parameter from luci-config?
Takuto Ikuta02edca22019-11-29 10:04:51 +0000121ISOLATED_PACKAGE = 'infra/tools/luci/isolated/${platform}'
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000122_CAS_PACKAGE = 'infra/tools/luci/cas/${platform}'
Takuto Ikutad53d7bd2021-07-16 03:09:33 +0000123_LUCI_GO_REVISION = DEPS.deps['luci-go']['packages'][0]['version']
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +0000124_NSJAIL_PACKAGE = 'infra/3pp/tools/nsjail/${platform}'
125_NSJAIL_VERSION = DEPS.deps['nsjail']['packages'][0]['version']
maruele2f2cb82016-07-13 14:41:03 -0700126
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400127# Keep synced with task_request.py
Lei Leife202df2019-06-11 17:33:34 +0000128CACHE_NAME_RE = re.compile(r'^[a-z0-9_]{1,4096}$')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400129
Takuto Ikutac9ddff22021-02-18 07:58:39 +0000130_FREE_SPACE_BUFFER_FOR_CIPD_PACKAGES = 2 * 1024 * 1024 * 1024
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400131
marueld928c862017-06-08 08:20:04 -0700132OUTLIVING_ZOMBIE_MSG = """\
133*** Swarming tried multiple times to delete the %s directory and failed ***
134*** Hard failing the task ***
135
136Swarming detected that your testing script ran an executable, which may have
137started a child executable, and the main script returned early, leaving the
138children executables playing around unguided.
139
140You don't want to leave children processes outliving the task on the Swarming
141bot, do you? The Swarming bot doesn't.
142
143How to fix?
144- For any process that starts children processes, make sure all children
145 processes terminated properly before each parent process exits. This is
146 especially important in very deep process trees.
147 - This must be done properly both in normal successful task and in case of
148 task failure. Cleanup is very important.
149- The Swarming bot sends a SIGTERM in case of timeout.
150 - You have %s seconds to comply after the signal was sent to the process
151 before the process is forcibly killed.
152- To achieve not leaking children processes in case of signals on timeout, you
153 MUST handle signals in each executable / python script and propagate them to
154 children processes.
155 - When your test script (python or binary) receives a signal like SIGTERM or
156 CTRL_BREAK_EVENT on Windows), send it to all children processes and wait for
157 them to terminate before quitting.
158
159See
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -0400160https://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 -0700161for more information.
162
163*** May the SIGKILL force be with you ***
164"""
165
166
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000167# Currently hardcoded. Eventually could be exposed as a flag once there's value.
168# 3 weeks
169MAX_AGE_SECS = 21*24*60*60
170
Takuto Ikuta7ff4b242020-12-03 08:07:06 +0000171_CAS_KVS_CACHE_THRESHOLD = 5 * 1024 * 1024 * 1024 # 5 GiB
172
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500173TaskData = collections.namedtuple(
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000174 'TaskData',
175 [
Takuto Ikuta9a319502019-11-26 07:40:14 +0000176 # List of strings; the command line to use, independent of what was
177 # specified in the isolated file.
178 'command',
179 # Relative directory to start command into.
180 'relative_cwd',
Takuto Ikuta9a319502019-11-26 07:40:14 +0000181 # Hash of the .isolated file that must be retrieved to recreate the tree
182 # of files to run the target executable. The command specified in the
183 # .isolated is executed. Mutually exclusive with command argument.
184 'isolated_hash',
185 # isolateserver.Storage instance to retrieve remote objects. This object
186 # has a reference to an isolateserver.StorageApi, which does the actual
187 # I/O.
188 'storage',
189 # isolateserver.LocalCache instance to keep from retrieving the same
190 # objects constantly by caching the objects retrieved. Can be on-disk or
191 # in-memory.
192 'isolate_cache',
Junji Watanabe54925c32020-09-08 00:56:18 +0000193 # Digest of the input root on RBE-CAS.
194 'cas_digest',
195 # Full CAS instance name.
196 'cas_instance',
Takuto Ikuta9a319502019-11-26 07:40:14 +0000197 # List of paths relative to root_dir to put into the output isolated
198 # bundle upon task completion (see link_outputs_to_outdir).
199 'outputs',
200 # Function (run_dir) => context manager that installs named caches into
201 # |run_dir|.
202 'install_named_caches',
203 # If True, the temporary directory will be deliberately leaked for later
204 # examination.
205 'leak_temp_dir',
206 # Path to the directory to use to create the temporary directory. If not
207 # specified, a random temporary directory is created.
208 'root_dir',
209 # Kills the process if it lasts more than this amount of seconds.
210 'hard_timeout',
211 # Number of seconds to wait between SIGTERM and SIGKILL.
212 'grace_period',
213 # Path to a file with bot state, used in place of ${SWARMING_BOT_FILE}
214 # task command line argument.
215 'bot_file',
216 # Logical account to switch LUCI_CONTEXT into.
217 'switch_to_account',
218 # Context manager dir => CipdInfo, see install_client_and_packages.
219 'install_packages_fn',
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000220 # Use go isolated client.
221 'use_go_isolated',
Junji Watanabeb03450b2020-09-25 05:09:27 +0000222 # Cache directory for go `isolated` client.
Takuto Ikuta057c5342019-12-03 04:05:05 +0000223 'go_cache_dir',
Junji Watanabeb03450b2020-09-25 05:09:27 +0000224 # Parameters passed to go `isolated` client.
Takuto Ikuta879788c2020-01-10 08:00:26 +0000225 'go_cache_policies',
Junji Watanabeb03450b2020-09-25 05:09:27 +0000226 # Cache directory for `cas` client.
227 'cas_cache_dir',
228 # Parameters passed to `cas` client.
229 'cas_cache_policies',
Takuto Ikutaae391c52020-12-03 08:43:45 +0000230 # Parameters for kvs file used by `cas` client.
231 'cas_kvs',
Takuto Ikuta9a319502019-11-26 07:40:14 +0000232 # Environment variables to set.
233 'env',
234 # Environment variables to mutate with relative directories.
235 # Example: {"ENV_KEY": ['relative', 'paths', 'to', 'prepend']}
236 'env_prefix',
237 # Lowers the task process priority.
238 'lower_priority',
239 # subprocess42.Containment instance. Can be None.
240 'containment',
Junji Watanabeaee69ad2021-04-28 03:17:34 +0000241 # Function to trim caches before installing cipd packages and
242 # downloading isolated files.
243 'trim_caches_fn',
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000244 ])
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500245
246
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500247def _to_str(s):
248 """Downgrades a unicode instance to str. Pass str through as-is."""
249 if isinstance(s, str):
250 return s
251 # This is technically incorrect, especially on Windows. In theory
252 # sys.getfilesystemencoding() should be used to use the right 'ANSI code
253 # page' on Windows, but that causes other problems, as the character set
254 # is very limited.
255 return s.encode('utf-8')
256
257
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -0500258def _to_unicode(s):
259 """Upgrades a str instance to unicode. Pass unicode through as-is."""
Takuto Ikuta95459dd2019-10-29 12:39:47 +0000260 if isinstance(s, six.text_type) or s is None:
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -0500261 return s
262 return s.decode('utf-8')
263
264
maruel03e11842016-07-14 10:50:16 -0700265def make_temp_dir(prefix, root_dir):
266 """Returns a new unique temporary directory."""
Takuto Ikuta6e2ff962019-10-29 12:35:27 +0000267 return six.text_type(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000268
269
vadimsh9c54b2c2017-07-25 14:08:29 -0700270@contextlib.contextmanager
271def set_luci_context_account(account, tmp_dir):
272 """Sets LUCI_CONTEXT account to be used by the task.
273
274 If 'account' is None or '', does nothing at all. This happens when
275 run_isolated.py is called without '--switch-to-account' flag. In this case,
276 if run_isolated.py is running in some LUCI_CONTEXT environment, the task will
Takuto Ikuta33e2ff32019-09-30 12:44:03 +0000277 just inherit whatever account is already set. This may happen if users invoke
vadimsh9c54b2c2017-07-25 14:08:29 -0700278 run_isolated.py explicitly from their code.
279
280 If the requested account is not defined in the context, switches to
281 non-authenticated access. This happens for Swarming tasks that don't use
282 'task' service accounts.
283
284 If not using LUCI_CONTEXT-based auth, does nothing.
285 If already running as requested account, does nothing.
286 """
287 if not account:
288 # Not actually switching.
289 yield
290 return
291
292 local_auth = luci_context.read('local_auth')
293 if not local_auth:
294 # Not using LUCI_CONTEXT auth at all.
295 yield
296 return
297
298 # See LUCI_CONTEXT.md for the format of 'local_auth'.
299 if local_auth.get('default_account_id') == account:
300 # Already set, no need to switch.
301 yield
302 return
303
304 available = {a['id'] for a in local_auth.get('accounts') or []}
305 if account in available:
306 logging.info('Switching default LUCI_CONTEXT account to %r', account)
307 local_auth['default_account_id'] = account
308 else:
309 logging.warning(
310 'Requested LUCI_CONTEXT account %r is not available (have only %r), '
311 'disabling authentication', account, sorted(available))
312 local_auth.pop('default_account_id', None)
313
314 with luci_context.write(_tmpdir=tmp_dir, local_auth=local_auth):
315 yield
316
317
nodir90bc8dc2016-06-15 13:35:21 -0700318def process_command(command, out_dir, bot_file):
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000319 """Replaces parameters in a command line.
nodirbe642ff2016-06-09 15:51:51 -0700320
321 Raises:
322 ValueError if a parameter is requested in |command| but its value is not
323 provided.
324 """
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000325 return [replace_parameters(arg, out_dir, bot_file) for arg in command]
326
327
328def replace_parameters(arg, out_dir, bot_file):
329 """Replaces parameter tokens with appropriate values in a string.
330
331 Raises:
332 ValueError if a parameter is requested in |arg| but its value is not
333 provided.
334 """
335 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
336 replace_slash = False
337 if ISOLATED_OUTDIR_PARAMETER in arg:
338 if not out_dir:
339 raise ValueError(
340 'output directory is requested in command or env var, but not '
341 'provided; please specify one')
342 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
343 replace_slash = True
344 if SWARMING_BOT_FILE_PARAMETER in arg:
345 if bot_file:
346 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
nodirbe642ff2016-06-09 15:51:51 -0700347 replace_slash = True
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000348 else:
349 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command or env '
350 'var, but no bot_file specified. Leaving parameter '
351 'unchanged.')
Joanna Wang4cec0e42021-08-26 00:48:37 +0000352 if SWARMING_TASK_ID_PARAMETER in arg:
353 task_id = os.environ.get('SWARMING_TASK_ID')
354 if task_id:
355 arg = arg.replace(SWARMING_TASK_ID_PARAMETER, task_id)
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000356 if replace_slash:
357 # Replace slashes only if parameters are present
358 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
359 arg = arg.replace('/', os.sep)
360 return arg
maruela9cfd6f2015-09-15 11:03:15 -0700361
362
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000363def set_temp_dir(env, tmp_dir):
364 """Set temp dir to given env var dictionary"""
365 tmp_dir = _to_str(tmp_dir)
366 # pylint: disable=line-too-long
367 # * python respects $TMPDIR, $TEMP, and $TMP in this order, regardless of
368 # platform. So $TMPDIR must be set on all platforms.
369 # https://github.com/python/cpython/blob/2.7/Lib/tempfile.py#L155
370 env['TMPDIR'] = tmp_dir
371 if sys.platform == 'win32':
372 # * chromium's base utils uses GetTempPath().
373 # https://cs.chromium.org/chromium/src/base/files/file_util_win.cc?q=GetTempPath
374 # * Go uses GetTempPath().
375 # * GetTempDir() uses %TMP%, then %TEMP%, then other stuff. So %TMP% must be
376 # set.
377 # https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-gettemppathw
378 env['TMP'] = tmp_dir
379 # https://blogs.msdn.microsoft.com/oldnewthing/20150417-00/?p=44213
380 env['TEMP'] = tmp_dir
381 elif sys.platform == 'darwin':
382 # * Chromium uses an hack on macOS before calling into
383 # NSTemporaryDirectory().
384 # https://cs.chromium.org/chromium/src/base/files/file_util_mac.mm?q=GetTempDir
385 # https://developer.apple.com/documentation/foundation/1409211-nstemporarydirectory
386 env['MAC_CHROMIUM_TMPDIR'] = tmp_dir
387 else:
388 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
389 # http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
390 # * mktemp on linux respects $TMPDIR.
391 # * Chromium respects $TMPDIR on linux.
392 # https://cs.chromium.org/chromium/src/base/files/file_util_posix.cc?q=GetTempDir
393 # * Go uses $TMPDIR.
394 # https://go.googlesource.com/go/+/go1.10.3/src/os/file_unix.go#307
395 pass
396
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000397
398def get_command_env(tmp_dir, cipd_info, run_dir, env, env_prefixes, out_dir,
399 bot_file):
vadimsh232f5a82017-01-20 19:23:44 -0800400 """Returns full OS environment to run a command in.
401
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800402 Sets up TEMP, puts directory with cipd binary in front of PATH, exposes
403 CIPD_CACHE_DIR env var, and installs all env_prefixes.
vadimsh232f5a82017-01-20 19:23:44 -0800404
405 Args:
406 tmp_dir: temp directory.
407 cipd_info: CipdInfo object is cipd client is used, None if not.
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500408 run_dir: The root directory the isolated tree is mapped in.
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500409 env: environment variables to use
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800410 env_prefixes: {"ENV_KEY": ['cwd', 'relative', 'paths', 'to', 'prepend']}
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000411 out_dir: Isolated output directory. Required to be != None if any of the
412 env vars contain ISOLATED_OUTDIR_PARAMETER.
413 bot_file: Required to be != None if any of the env vars contain
414 SWARMING_BOT_FILE_PARAMETER.
vadimsh232f5a82017-01-20 19:23:44 -0800415 """
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500416 out = os.environ.copy()
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000417 for k, v in env.items():
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500418 if not v:
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500419 out.pop(k, None)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500420 else:
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000421 out[k] = replace_parameters(v, out_dir, bot_file)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500422
423 if cipd_info:
424 bin_dir = os.path.dirname(cipd_info.client.binary_path)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500425 out['PATH'] = '%s%s%s' % (_to_str(bin_dir), os.pathsep, out['PATH'])
426 out['CIPD_CACHE_DIR'] = _to_str(cipd_info.cache_dir)
Takuto Ikuta4ec3e8f2021-04-05 10:21:29 +0000427 cipd_info_path = os.path.join(tmp_dir, 'cipd_info.json')
428 with open(cipd_info_path, 'w') as f:
429 json.dump(cipd_info.pins, f)
430 out['ISOLATED_RESOLVED_PACKAGE_VERSIONS_FILE'] = cipd_info_path
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500431
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000432 for key, paths in env_prefixes.items():
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500433 assert isinstance(paths, list), paths
434 paths = [os.path.normpath(os.path.join(run_dir, p)) for p in paths]
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500435 cur = out.get(key)
436 if cur:
437 paths.append(cur)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500438 out[key] = _to_str(os.path.pathsep.join(paths))
vadimsh232f5a82017-01-20 19:23:44 -0800439
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000440 set_temp_dir(out, tmp_dir)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500441 return out
vadimsh232f5a82017-01-20 19:23:44 -0800442
443
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000444def run_command(
445 command, cwd, env, hard_timeout, grace_period, lower_priority, containment):
maruel6be7f9e2015-10-01 12:25:30 -0700446 """Runs the command.
447
448 Returns:
449 tuple(process exit code, bool if had a hard timeout)
450 """
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000451 logging.info(
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000452 'run_command(%s, %s, %s, %s, %s, %s)',
453 command, cwd, hard_timeout, grace_period, lower_priority, containment)
marueleb5fbee2015-09-17 13:01:36 -0700454
maruel6be7f9e2015-10-01 12:25:30 -0700455 exit_code = None
456 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700457 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700458 proc = None
459 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700460 try:
maruel6be7f9e2015-10-01 12:25:30 -0700461 # TODO(maruel): This code is imperfect. It doesn't handle well signals
462 # during the download phase and there's short windows were things can go
463 # wrong.
464 def handler(signum, _frame):
465 if proc and not had_signal:
466 logging.info('Received signal %d', signum)
467 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700468 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700469
Marc-Antoine Ruel30b80fe2019-02-08 13:51:31 +0000470 proc = subprocess42.Popen(
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000471 command, cwd=cwd, env=env, detached=True, close_fds=True,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000472 lower_priority=lower_priority, containment=containment)
Joanna Wang40959bf2021-08-12 18:10:12 +0000473 logging.info('Subprocess for command started')
maruel6be7f9e2015-10-01 12:25:30 -0700474 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
475 try:
John Budorickc398f092019-06-10 22:49:44 +0000476 exit_code = proc.wait(hard_timeout or None)
Takuto Ikuta6a8f4e12021-11-15 02:33:04 +0000477 logging.info("finished with exit code %d", exit_code)
maruel6be7f9e2015-10-01 12:25:30 -0700478 except subprocess42.TimeoutExpired:
479 if not had_signal:
480 logging.warning('Hard timeout')
481 had_hard_timeout = True
482 logging.warning('Sending SIGTERM')
483 proc.terminate()
484
Takuto Ikuta684f7912020-09-29 07:49:49 +0000485 kill_sent = False
maruel6be7f9e2015-10-01 12:25:30 -0700486 # 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)
Takuto Ikuta6a8f4e12021-11-15 02:33:04 +0000493 logging.info("finished with exit code %d", exit_code)
maruel6be7f9e2015-10-01 12:25:30 -0700494 except subprocess42.TimeoutExpired:
495 # Now kill for real. The user can distinguish between the
496 # following states:
497 # - signal but process exited within grace period,
498 # hard_timed_out will be set but the process exit code will be
499 # script provided.
500 # - processed exited late, exit code will be -9 on posix.
501 logging.warning('Grace exhausted; sending SIGKILL')
502 proc.kill()
Takuto Ikuta684f7912020-09-29 07:49:49 +0000503 kill_sent = True
martiniss5c8043e2017-08-01 17:09:43 -0700504 logging.info('Waiting for process exit')
maruel6be7f9e2015-10-01 12:25:30 -0700505 exit_code = proc.wait()
Takuto Ikuta684f7912020-09-29 07:49:49 +0000506
507 # the process group / job object may be dangling so if we didn't kill
508 # it already, give it a poke now.
509 if not kill_sent:
510 proc.kill()
Takuto Ikutaeccf0862020-03-19 03:05:55 +0000511 except OSError as e:
maruela9cfd6f2015-09-15 11:03:15 -0700512 # This is not considered to be an internal error. The executable simply
513 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800514 sys.stderr.write(
tikuta2d678212019-09-23 23:12:08 +0000515 '<The executable does not exist, a dependent library is missing or '
516 'the command line is too long>\n'
517 '<Check for missing .so/.dll in the .isolate or GN file or length of '
518 'command line args>\n'
Takuto Ikutae900df42021-04-14 04:40:11 +0000519 '<Command: %s>\n'
520 '<Exception: %s>\n' % (command, e))
maruela72f46e2016-02-24 11:05:45 -0800521 if os.environ.get('SWARMING_TASK_ID'):
522 # Give an additional hint when running as a swarming task.
523 sys.stderr.write(
524 '<See the task\'s page for commands to help diagnose this issue '
525 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700526 exit_code = 1
527 logging.info(
528 'Command finished with exit code %d (%s)',
529 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700530 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700531
532
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000533def _run_go_cmd_and_wait(cmd, tmp_dir):
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000534 """
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000535 Runs an external Go command, `isolated` or `cas`, and wait for its completion.
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000536
537 While this is a generic function to launch a subprocess, it has logic that
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000538 is specific to Go `isolated` and `cas` for waiting and logging.
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000539
540 Returns:
541 The subprocess object
542 """
Ye Kuang3c40e9f2020-07-28 13:15:25 +0000543 cmd_str = ' '.join(cmd)
Ye Kuangc1d800f2020-07-28 10:14:55 +0000544 try:
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000545 env = os.environ.copy()
546 set_temp_dir(env, tmp_dir)
547 proc = subprocess42.Popen(cmd, env=env)
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000548
Ye Kuangc1d800f2020-07-28 10:14:55 +0000549 exceeded_max_timeout = True
550 check_period_sec = 30
551 max_checks = 100
552 # max timeout = max_checks * check_period_sec = 50 minutes
553 for i in range(max_checks):
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000554 # This is to prevent I/O timeout error during setup.
Ye Kuangc1d800f2020-07-28 10:14:55 +0000555 try:
556 retcode = proc.wait(check_period_sec)
557 if retcode != 0:
Takuto Ikuta27f4b2f2021-04-26 07:18:55 +0000558 raise subprocess42.CalledProcessError(retcode, cmd=cmd_str)
Ye Kuangc1d800f2020-07-28 10:14:55 +0000559 exceeded_max_timeout = False
560 break
561 except subprocess42.TimeoutExpired:
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000562 print('still running (after %d seconds)' % ((i + 1) * check_period_sec))
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000563
Ye Kuangc1d800f2020-07-28 10:14:55 +0000564 if exceeded_max_timeout:
565 proc.terminate()
566 try:
567 proc.wait(check_period_sec)
568 except subprocess42.TimeoutExpired:
569 logging.exception(
570 "failed to terminate? timeout happened after %d seconds",
571 check_period_sec)
572 proc.kill()
573 proc.wait()
574 # Raise unconditionally, because |proc| was forcefully terminated.
575 raise ValueError("timedout after %d seconds (cmd=%s)" %
576 (check_period_sec * max_checks, cmd_str))
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000577
Ye Kuangc1d800f2020-07-28 10:14:55 +0000578 return proc
579 except Exception:
580 logging.exception('Failed to run Go cmd %s', cmd_str)
581 raise
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000582
583
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000584def _fetch_and_map_with_cas(cas_client, digest, instance, output_dir, cache_dir,
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +0000585 policies, kvs_dir, tmp_dir):
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000586 """
587 Fetches a CAS tree using cas client, create the tree and returns download
588 stats.
589 """
590
591 start = time.time()
592 result_json_handle, result_json_path = tempfile.mkstemp(
593 prefix=u'fetch-and-map-result-', suffix=u'.json')
594 os.close(result_json_handle)
Takuto Ikutad5749ac2021-04-07 06:16:19 +0000595 profile_dir = tempfile.mkdtemp(dir=tmp_dir)
596
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000597 try:
598 cmd = [
599 cas_client,
600 'download',
601 '-digest',
602 digest,
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000603 # flags for cache.
604 '-cache-dir',
605 cache_dir,
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000606 '-cache-max-size',
607 str(policies.max_cache_size),
608 '-cache-min-free-space',
609 str(policies.min_free_space),
610 # flags for output.
611 '-dir',
612 output_dir,
613 '-dump-stats-json',
614 result_json_path,
Takuto Ikuta557025b2021-02-01 08:37:40 +0000615 '-log-level',
Takuto Ikutad5749ac2021-04-07 06:16:19 +0000616 'info',
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000617 ]
Takuto Ikutaae391c52020-12-03 08:43:45 +0000618
Junji Watanabe66d807b2021-11-08 03:20:10 +0000619 # When RUN_ISOLATED_CAS_ADDRESS is set in test mode,
620 # Use it and ignore CAS instance option.
621 cas_addr = os.environ.get('RUN_ISOLATED_CAS_ADDRESS')
622 if cas_addr:
623 cmd.extend([
624 '-cas-addr',
625 cas_addr,
626 ])
627 else:
628 cmd.extend([
629 '-cas-instance',
630 instance
631 ])
632
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +0000633 if kvs_dir:
634 cmd.extend(['-kvs-dir', kvs_dir])
Takuto Ikutaae391c52020-12-03 08:43:45 +0000635
Takuto Ikuta27f4b2f2021-04-26 07:18:55 +0000636 try:
637 _run_go_cmd_and_wait(cmd, tmp_dir)
Takuto Ikuta0909eae2021-04-27 02:54:07 +0000638 except subprocess42.CalledProcessError as ex:
Takuto Ikuta27f4b2f2021-04-26 07:18:55 +0000639 if not kvs_dir:
640 raise
641 logging.exception('Failed to run cas, removing kvs cache dir and retry.')
Takuto Ikuta0909eae2021-04-27 02:54:07 +0000642 on_error.report("Failed to run cas %s" % ex)
Takuto Ikuta27f4b2f2021-04-26 07:18:55 +0000643 file_path.rmtree(kvs_dir)
Takuto Ikutacffabfb2021-11-01 08:05:43 +0000644 file_path.rmtree(output_dir)
Takuto Ikuta27f4b2f2021-04-26 07:18:55 +0000645 _run_go_cmd_and_wait(cmd, tmp_dir)
646
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000647 with open(result_json_path) as json_file:
648 result_json = json.load(json_file)
649
650 return {
651 'duration': time.time() - start,
652 'items_cold': result_json['items_cold'],
653 'items_hot': result_json['items_hot'],
654 }
655 finally:
656 fs.remove(result_json_path)
Takuto Ikutad5749ac2021-04-07 06:16:19 +0000657 file_path.rmtree(profile_dir)
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000658
659
660def _fetch_and_map_with_go_isolated(isolated_hash, storage, outdir,
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000661 go_cache_dir, policies, isolated_client,
662 tmp_dir):
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000663 """
664 Fetches an isolated tree using go client, create the tree and returns
Takuto Ikuta57219f42020-11-02 07:35:36 +0000665 stats.
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000666 """
667 start = time.time()
668 server_ref = storage.server_ref
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000669 result_json_handle, result_json_path = tempfile.mkstemp(
670 prefix=u'fetch-and-map-result-', suffix=u'.json')
671 os.close(result_json_handle)
672 try:
Ye Kuanga98764c2020-04-09 03:17:37 +0000673 cmd = [
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000674 isolated_client,
675 'download',
676 '-isolate-server',
677 server_ref.url,
678 '-namespace',
679 server_ref.namespace,
680 '-isolated',
681 isolated_hash,
682
683 # flags for cache
684 '-cache-dir',
Takuto Ikuta057c5342019-12-03 04:05:05 +0000685 go_cache_dir,
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000686 '-cache-max-items',
Takuto Ikuta50bc0552019-12-03 03:26:46 +0000687 str(policies.max_items),
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000688 '-cache-max-size',
Takuto Ikuta50bc0552019-12-03 03:26:46 +0000689 str(policies.max_cache_size),
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000690 '-cache-min-free-space',
Takuto Ikuta50bc0552019-12-03 03:26:46 +0000691 str(policies.min_free_space),
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000692
693 # flags for output
694 '-output-dir',
695 outdir,
696 '-fetch-and-map-result-json',
697 result_json_path,
Ye Kuanga98764c2020-04-09 03:17:37 +0000698 ]
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000699 _run_go_cmd_and_wait(cmd, tmp_dir)
Takuto Ikuta3153e3b2020-02-18 06:11:47 +0000700
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000701 with open(result_json_path) as json_file:
702 result_json = json.load(json_file)
703
Takuto Ikuta57219f42020-11-02 07:35:36 +0000704 return {
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000705 'duration': time.time() - start,
706 'items_cold': result_json['items_cold'],
707 'items_hot': result_json['items_hot'],
Ye Kuang65a1de52020-10-16 08:31:16 +0000708 'initial_number_items': result_json['initial_number_items'],
709 'initial_size': result_json['initial_size'],
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000710 }
711 finally:
712 fs.remove(result_json_path)
713
714
715# TODO(crbug.com/932396): remove this function.
Takuto Ikuta16fac4b2019-12-09 04:57:18 +0000716def fetch_and_map(isolated_hash, storage, cache, outdir):
Takuto Ikuta57219f42020-11-02 07:35:36 +0000717 """Fetches an isolated tree, create the tree and returns stats."""
nodir6f801882016-04-29 14:41:50 -0700718 start = time.time()
Takuto Ikuta57219f42020-11-02 07:35:36 +0000719 isolateserver.fetch_isolated(
nodir6f801882016-04-29 14:41:50 -0700720 isolated_hash=isolated_hash,
721 storage=storage,
722 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700723 outdir=outdir,
Takuto Ikuta16fac4b2019-12-09 04:57:18 +0000724 use_symlinks=False)
Takuto Ikuta2b9640e2019-06-19 00:53:23 +0000725 hot = (collections.Counter(cache.used) -
726 collections.Counter(cache.added)).elements()
Takuto Ikuta57219f42020-11-02 07:35:36 +0000727 return {
Takuto Ikuta630f99d2020-07-02 12:59:35 +0000728 'duration': time.time() - start,
729 'items_cold': base64.b64encode(large.pack(sorted(cache.added))).decode(),
730 'items_hot': base64.b64encode(large.pack(sorted(hot))).decode(),
nodir6f801882016-04-29 14:41:50 -0700731 }
732
733
aludwin0a8e17d2016-10-27 15:57:39 -0700734def link_outputs_to_outdir(run_dir, out_dir, outputs):
735 """Links any named outputs to out_dir so they can be uploaded.
736
737 Raises an error if the file already exists in that directory.
738 """
739 if not outputs:
740 return
741 isolateserver.create_directories(out_dir, outputs)
742 for o in outputs:
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -0400743 copy_recursively(os.path.join(run_dir, o), os.path.join(out_dir, o))
744
745
746def copy_recursively(src, dst):
747 """Efficiently copies a file or directory from src_dir to dst_dir.
748
749 `item` may be a file, directory, or a symlink to a file or directory.
750 All symlinks are replaced with their targets, so the resulting
751 directory structure in dst_dir will never have any symlinks.
752
753 To increase speed, copy_recursively hardlinks individual files into the
754 (newly created) directory structure if possible, unlike Python's
755 shutil.copytree().
756 """
757 orig_src = src
758 try:
759 # Replace symlinks with their final target.
760 while fs.islink(src):
761 res = fs.readlink(src)
Takuto Ikutaf2ad0a02021-06-24 08:38:40 +0000762 src = os.path.realpath(os.path.join(os.path.dirname(src), res))
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -0400763 # TODO(sadafm): Explicitly handle cyclic symlinks.
764
Takuto Ikutaf2ad0a02021-06-24 08:38:40 +0000765 if not fs.exists(src):
766 logging.warning('Path %s does not exist or %s is a broken symlink', src,
767 orig_src)
768 return
769
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -0400770 if fs.isfile(src):
771 file_path.link_file(dst, src, file_path.HARDLINK_WITH_FALLBACK)
772 return
773
774 if not fs.exists(dst):
775 os.makedirs(dst)
776
777 for child in fs.listdir(src):
778 copy_recursively(os.path.join(src, child), os.path.join(dst, child))
779
780 except OSError as e:
781 if e.errno == errno.ENOENT:
782 logging.warning('Path %s does not exist or %s is a broken symlink',
783 src, orig_src)
784 else:
785 logging.info("Couldn't collect output file %s: %s", src, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700786
787
Ye Kuangfb0bad62020-07-28 08:07:25 +0000788def _upload_with_py(storage, out_dir):
789
790 def process_stats(f_st):
791 st = sorted(i.size for i in f_st)
792 return base64.b64encode(large.pack(st)).decode()
793
794 try:
795 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
796 storage, [out_dir], None, verify_push=True)
797
798 isolated = list(results.values())[0]
799 cold = process_stats(f_cold)
800 hot = process_stats(f_hot)
801 return isolated, cold, hot
802
803 except isolateserver.Aborted:
804 # This happens when a signal SIGTERM was received while uploading data.
805 # There is 2 causes:
806 # - The task was too slow and was about to be killed anyway due to
807 # exceeding the hard timeout.
808 # - The amount of data uploaded back is very large and took too much
809 # time to archive.
810 sys.stderr.write('Received SIGTERM while uploading')
811 # Re-raise, so it will be treated as an internal failure.
812 raise
813
814
Takuto Ikutaf5173872021-05-11 03:18:40 +0000815def upload_out_dir(storage, out_dir):
Ye Kuangbc4e8402020-07-29 09:54:30 +0000816 """Uploads the results in |out_dir| back, if there is any.
maruela9cfd6f2015-09-15 11:03:15 -0700817
818 Returns:
Ye Kuangbc4e8402020-07-29 09:54:30 +0000819 tuple(outputs_ref, stats)
maruel064c0a32016-04-05 11:47:15 -0700820 - outputs_ref: a dict referring to the results archived back to the isolated
821 server, if applicable.
nodir6f801882016-04-29 14:41:50 -0700822 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700823 """
maruela9cfd6f2015-09-15 11:03:15 -0700824 # Upload out_dir and generate a .isolated file out of this directory. It is
825 # only done if files were written in the directory.
826 outputs_ref = None
Ye Kuangfb0bad62020-07-28 08:07:25 +0000827 cold = ''
828 hot = ''
nodir6f801882016-04-29 14:41:50 -0700829 start = time.time()
830
maruel12e30012015-10-09 11:55:35 -0700831 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700832 with tools.Profiler('ArchiveOutput'):
Takuto Ikutaf5173872021-05-11 03:18:40 +0000833 isolated, cold, hot = _upload_with_py(storage, out_dir)
Ye Kuangfb0bad62020-07-28 08:07:25 +0000834 outputs_ref = {
835 'isolated': isolated,
836 'isolatedserver': storage.server_ref.url,
837 'namespace': storage.server_ref.namespace,
838 }
nodir6f801882016-04-29 14:41:50 -0700839
nodir6f801882016-04-29 14:41:50 -0700840 stats = {
Takuto Ikuta630f99d2020-07-02 12:59:35 +0000841 'duration': time.time() - start,
Ye Kuangfb0bad62020-07-28 08:07:25 +0000842 'items_cold': cold,
843 'items_hot': hot,
nodir6f801882016-04-29 14:41:50 -0700844 }
Ye Kuangbc4e8402020-07-29 09:54:30 +0000845 return outputs_ref, stats
maruela9cfd6f2015-09-15 11:03:15 -0700846
847
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000848def upload_outdir_with_cas(cas_client, cas_instance, outdir, tmp_dir):
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000849 """Uploads the results in |outdir|, if there is any.
850
851 Returns:
852 tuple(root_digest, stats)
853 - root_digest: a digest of the output directory.
854 - stats: uploading stats.
855 """
Junji Watanabe15f9e042021-11-12 07:13:50 +0000856 if not fs.listdir(outdir):
857 return None, None
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000858 digest_file_handle, digest_path = tempfile.mkstemp(
859 prefix=u'cas-digest', suffix=u'.txt')
860 os.close(digest_file_handle)
861 stats_json_handle, stats_json_path = tempfile.mkstemp(
862 prefix=u'upload-stats', suffix=u'.json')
863 os.close(stats_json_handle)
864
865 try:
866 cmd = [
867 cas_client,
868 'archive',
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000869 '-paths',
870 # Format: <working directory>:<relative path to dir>
871 outdir + ':',
872 # output
873 '-dump-digest',
874 digest_path,
875 '-dump-stats-json',
876 stats_json_path,
877 ]
878
Junji Watanabe66d807b2021-11-08 03:20:10 +0000879 # When RUN_ISOLATED_CAS_ADDRESS is set in test mode,
880 # Use it and ignore CAS instance option.
881 cas_addr = os.environ.get('RUN_ISOLATED_CAS_ADDRESS')
882 if cas_addr:
883 cmd.extend([
884 '-cas-addr',
885 cas_addr,
886 ])
887 else:
888 cmd.extend([
889 '-cas-instance',
890 cas_instance
891 ])
892
Takuto Ikutabfcef252021-08-25 07:46:19 +0000893 if sys.platform.startswith('linux'):
894 # TODO(crbug.com/1243194): remove this after investigation.
895 cmd.extend(['-log-level', 'debug'])
896
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000897 start = time.time()
898
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000899 _run_go_cmd_and_wait(cmd, tmp_dir)
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000900
901 with open(digest_path) as digest_file:
902 digest = digest_file.read()
Junji Watanabec208b302020-09-25 09:18:27 +0000903 h, s = digest.split('/')
904 cas_output_root = {
905 'cas_instance': cas_instance,
906 'digest': {
907 'hash': h,
908 'size_bytes': int(s)
909 }
910 }
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000911 with open(stats_json_path) as stats_file:
912 stats = json.load(stats_file)
913
914 stats['duration'] = time.time() - start
915
Junji Watanabec208b302020-09-25 09:18:27 +0000916 return cas_output_root, stats
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000917 finally:
918 fs.remove(digest_path)
919 fs.remove(stats_json_path)
920
921
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500922def map_and_run(data, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700923 """Runs a command with optional isolated input/output.
924
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500925 Arguments:
926 - data: TaskData instance.
927 - constant_run_path: TODO
nodir55be77b2016-05-03 09:39:57 -0700928
929 Returns metadata about the result.
930 """
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +0000931
932 if data.isolate_cache:
933 download_stats = {
934 #'duration': 0.,
935 'initial_number_items': len(data.isolate_cache),
936 'initial_size': data.isolate_cache.total_size,
937 #'items_cold': '<large.pack()>',
938 #'items_hot': '<large.pack()>',
939 }
940 else:
941 # TODO(tikuta): take stats from state.json in this case too.
942 download_stats = {}
943
maruela9cfd6f2015-09-15 11:03:15 -0700944 result = {
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000945 'duration': None,
946 'exit_code': None,
947 'had_hard_timeout': False,
948 'internal_failure': 'run_isolated did not complete properly',
949 'stats': {
Junji Watanabeaee69ad2021-04-28 03:17:34 +0000950 'trim_caches': {
951 'duration': 0,
952 },
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000953 #'cipd': {
954 # 'duration': 0.,
955 # 'get_client_duration': 0.,
956 #},
957 'isolated': {
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +0000958 'download': download_stats,
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000959 #'upload': {
960 # 'duration': 0.,
961 # 'items_cold': '<large.pack()>',
962 # 'items_hot': '<large.pack()>',
963 #},
964 },
Junji Watanabeaee69ad2021-04-28 03:17:34 +0000965 'named_caches': {
966 'install': {
967 'duration': 0,
968 },
969 'uninstall': {
970 'duration': 0,
971 },
972 },
973 'cleanup': {
974 'duration': 0,
975 }
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000976 },
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000977 #'cipd_pins': {
978 # 'packages': [
979 # {'package_name': ..., 'version': ..., 'path': ...},
980 # ...
981 # ],
982 # 'client_package': {'package_name': ..., 'version': ...},
983 #},
984 'outputs_ref': None,
Junji Watanabe54925c32020-09-08 00:56:18 +0000985 'cas_output_root': None,
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000986 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700987 }
nodirbe642ff2016-06-09 15:51:51 -0700988
Takuto Ikutad46ea762020-10-07 05:43:22 +0000989 assert os.path.isabs(data.root_dir), ("data.root_dir is not abs path: %s" %
990 data.root_dir)
991 file_path.ensure_tree(data.root_dir, 0o700)
992
maruele2f2cb82016-07-13 14:41:03 -0700993 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700994 # TODO(maruel): This is not obvious. Change this to become an error once we
995 # make the constant_run_path an exposed flag.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500996 if constant_run_path and data.root_dir:
997 run_dir = os.path.join(data.root_dir, ISOLATED_RUN_DIR)
maruel5c4eed82017-05-26 05:33:40 -0700998 if os.path.isdir(run_dir):
999 file_path.rmtree(run_dir)
Lei Leife202df2019-06-11 17:33:34 +00001000 os.mkdir(run_dir, 0o700)
maruelcffa0542017-04-07 08:39:20 -07001001 else:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001002 run_dir = make_temp_dir(ISOLATED_RUN_DIR, data.root_dir)
Junji Watanabe1adba7b2020-09-18 07:03:58 +00001003
1004 # True if CAS is used for download/upload files.
1005 use_cas = bool(data.cas_digest)
1006
maruel03e11842016-07-14 10:50:16 -07001007 # storage should be normally set but don't crash if it is not. This can happen
1008 # as Swarming task can run without an isolate server.
Junji Watanabe1adba7b2020-09-18 07:03:58 +00001009 out_dir = None
1010 if data.storage or use_cas:
1011 out_dir = make_temp_dir(ISOLATED_OUT_DIR, data.root_dir)
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001012 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, data.root_dir)
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001013 isolated_client_dir = make_temp_dir(ISOLATED_CLIENT_DIR, data.root_dir)
nodir55be77b2016-05-03 09:39:57 -07001014 cwd = run_dir
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001015 if data.relative_cwd:
1016 cwd = os.path.normpath(os.path.join(cwd, data.relative_cwd))
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001017 command = data.command
Ye Kuangfb0bad62020-07-28 08:07:25 +00001018 go_isolated_client = None
1019 if data.use_go_isolated:
1020 go_isolated_client = os.path.join(isolated_client_dir,
1021 'isolated' + cipd.EXECUTABLE_SUFFIX)
Junji Watanabe1adba7b2020-09-18 07:03:58 +00001022
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001023 cas_client = None
1024 cas_client_dir = make_temp_dir(_CAS_CLIENT_DIR, data.root_dir)
Junji Watanabe1adba7b2020-09-18 07:03:58 +00001025 if use_cas:
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001026 cas_client = os.path.join(cas_client_dir, 'cas' + cipd.EXECUTABLE_SUFFIX)
1027
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001028 data.trim_caches_fn(result['stats']['trim_caches'])
1029
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +00001030 nsjail_dir = None
1031 if (sys.platform == "linux" and cipd.get_platform() == "amd64" and
1032 data.containment.containment_type == subprocess42.Containment.NSJAIL):
1033 nsjail_dir = make_temp_dir(_NSJAIL_DIR, data.root_dir)
1034
nodir55be77b2016-05-03 09:39:57 -07001035 try:
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +00001036 with data.install_packages_fn(run_dir, isolated_client_dir, cas_client_dir,
1037 nsjail_dir) as cipd_info:
vadimsh232f5a82017-01-20 19:23:44 -08001038 if cipd_info:
1039 result['stats']['cipd'] = cipd_info.stats
1040 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -07001041
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001042 isolated_stats = result['stats'].setdefault('isolated', {})
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001043 if data.isolated_hash:
Takuto Ikutad03ffcc2019-12-02 01:04:23 +00001044 if data.use_go_isolated:
Takuto Ikuta57219f42020-11-02 07:35:36 +00001045 stats = _fetch_and_map_with_go_isolated(
Takuto Ikuta90397ca2020-01-08 10:07:55 +00001046 isolated_hash=data.isolated_hash,
1047 storage=data.storage,
Takuto Ikuta90397ca2020-01-08 10:07:55 +00001048 outdir=run_dir,
1049 go_cache_dir=data.go_cache_dir,
Takuto Ikuta879788c2020-01-10 08:00:26 +00001050 policies=data.go_cache_policies,
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +00001051 isolated_client=go_isolated_client,
1052 tmp_dir=tmp_dir)
Takuto Ikuta90397ca2020-01-08 10:07:55 +00001053 else:
Takuto Ikuta57219f42020-11-02 07:35:36 +00001054 stats = fetch_and_map(
Takuto Ikutad03ffcc2019-12-02 01:04:23 +00001055 isolated_hash=data.isolated_hash,
1056 storage=data.storage,
1057 cache=data.isolate_cache,
Takuto Ikuta16fac4b2019-12-09 04:57:18 +00001058 outdir=run_dir)
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +00001059 isolated_stats['download'].update(stats)
Takuto Ikutab58dbd12020-06-05 09:29:14 +00001060
Junji Watanabe54925c32020-09-08 00:56:18 +00001061 elif data.cas_digest:
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001062 stats = _fetch_and_map_with_cas(
1063 cas_client=cas_client,
1064 digest=data.cas_digest,
1065 instance=data.cas_instance,
1066 output_dir=run_dir,
Junji Watanabeb03450b2020-09-25 05:09:27 +00001067 cache_dir=data.cas_cache_dir,
Takuto Ikutaae391c52020-12-03 08:43:45 +00001068 policies=data.cas_cache_policies,
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +00001069 kvs_dir=data.cas_kvs,
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +00001070 tmp_dir=tmp_dir)
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001071 isolated_stats['download'].update(stats)
Junji Watanabe54925c32020-09-08 00:56:18 +00001072
maruelabec63c2017-04-26 11:53:24 -07001073 if not command:
1074 # Handle this as a task failure, not an internal failure.
1075 sys.stderr.write(
1076 '<No command was specified!>\n'
1077 '<Please secify a command when triggering your Swarming task>\n')
1078 result['exit_code'] = 1
1079 return result
nodirbe642ff2016-06-09 15:51:51 -07001080
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001081 if not cwd.startswith(run_dir):
1082 # Handle this as a task failure, not an internal failure. This is a
1083 # 'last chance' way to gate against directory escape.
1084 sys.stderr.write('<Relative CWD is outside of run directory!>\n')
1085 result['exit_code'] = 1
1086 return result
1087
1088 if not os.path.isdir(cwd):
1089 # Accepts relative_cwd that does not exist.
Lei Leife202df2019-06-11 17:33:34 +00001090 os.makedirs(cwd, 0o700)
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001091
vadimsh232f5a82017-01-20 19:23:44 -08001092 # If we have an explicit list of files to return, make sure their
1093 # directories exist now.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001094 if data.storage and data.outputs:
1095 isolateserver.create_directories(run_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -07001096
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001097 with data.install_named_caches(run_dir, result['stats']['named_caches']):
nodird6160682017-02-02 13:03:35 -08001098 sys.stdout.flush()
1099 start = time.time()
1100 try:
vadimsh9c54b2c2017-07-25 14:08:29 -07001101 # Need to switch the default account before 'get_command_env' call,
1102 # so it can grab correct value of LUCI_CONTEXT env var.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001103 with set_luci_context_account(data.switch_to_account, tmp_dir):
1104 env = get_command_env(
Roberto Carrillo71ade6d2018-10-08 22:30:24 +00001105 tmp_dir, cipd_info, run_dir, data.env, data.env_prefix, out_dir,
1106 data.bot_file)
Brian Sheedy7a761172019-08-30 22:55:14 +00001107 command = tools.find_executable(command, env)
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001108 command = process_command(command, out_dir, data.bot_file)
1109 file_path.ensure_command_has_abs_path(command, cwd)
1110
vadimsh9c54b2c2017-07-25 14:08:29 -07001111 result['exit_code'], result['had_hard_timeout'] = run_command(
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +00001112 command, cwd, env, data.hard_timeout, data.grace_period,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001113 data.lower_priority, data.containment)
nodird6160682017-02-02 13:03:35 -08001114 finally:
1115 result['duration'] = max(time.time() - start, 0)
Seth Koehler49139812017-12-19 13:59:33 -05001116
Ye Kuangbc4e8402020-07-29 09:54:30 +00001117 if out_dir:
1118 # Try to link files to the output directory, if specified.
1119 link_outputs_to_outdir(run_dir, out_dir, data.outputs)
1120 isolated_stats = result['stats'].setdefault('isolated', {})
Junji Watanabe1adba7b2020-09-18 07:03:58 +00001121 if use_cas:
1122 result['cas_output_root'], isolated_stats['upload'] = (
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +00001123 upload_outdir_with_cas(cas_client, data.cas_instance, out_dir,
1124 tmp_dir))
Junji Watanabe1adba7b2020-09-18 07:03:58 +00001125 else:
1126 # This could use |go_isolated_client|, so make sure it runs when the
1127 # CIPD package still exists.
1128 result['outputs_ref'], isolated_stats['upload'] = (
Takuto Ikutaf5173872021-05-11 03:18:40 +00001129 upload_out_dir(data.storage, out_dir))
Seth Koehler49139812017-12-19 13:59:33 -05001130 # We successfully ran the command, set internal_failure back to
1131 # None (even if the command failed, it's not an internal error).
1132 result['internal_failure'] = None
maruela9cfd6f2015-09-15 11:03:15 -07001133 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -07001134 # An internal error occurred. Report accordingly so the swarming task will
1135 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -07001136 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -07001137 result['internal_failure'] = str(e)
1138 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -07001139
1140 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -07001141 finally:
1142 try:
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001143 cleanup_start = time.time()
Ye Kuangbc4e8402020-07-29 09:54:30 +00001144 success = True
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001145 if data.leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -07001146 success = True
maruela9cfd6f2015-09-15 11:03:15 -07001147 logging.warning(
1148 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -07001149 else:
maruel84537cb2015-10-16 14:21:28 -07001150 # On Windows rmtree(run_dir) call above has a synchronization effect: it
1151 # finishes only when all task child processes terminate (since a running
1152 # process locks *.exe file). Examine out_dir only after that call
1153 # completes (since child processes may write to out_dir too and we need
1154 # to wait for them to finish).
Junji Watanabeb03450b2020-09-25 05:09:27 +00001155 dirs_to_remove = [run_dir, tmp_dir, isolated_client_dir, cas_client_dir]
Ye Kuangbc4e8402020-07-29 09:54:30 +00001156 if out_dir:
1157 dirs_to_remove.append(out_dir)
1158 for directory in dirs_to_remove:
Takuto Ikuta69c0d662019-11-27 01:18:08 +00001159 if not fs.isdir(directory):
1160 continue
Junji Watanabe9cdfff52021-01-08 07:20:35 +00001161 start = time.time()
maruel84537cb2015-10-16 14:21:28 -07001162 try:
Junji Watanabecc4eefd2021-01-19 01:46:10 +00001163 file_path.rmtree(directory)
maruel84537cb2015-10-16 14:21:28 -07001164 except OSError as e:
Takuto Ikuta69c0d662019-11-27 01:18:08 +00001165 logging.error('rmtree(%r) failed: %s', directory, e)
maruel84537cb2015-10-16 14:21:28 -07001166 success = False
Junji Watanabe9cdfff52021-01-08 07:20:35 +00001167 finally:
1168 logging.info('Cleanup: rmtree(%r) took %d seconds', directory,
1169 time.time() - start)
maruel84537cb2015-10-16 14:21:28 -07001170 if not success:
Takuto Ikuta69c0d662019-11-27 01:18:08 +00001171 sys.stderr.write(
1172 OUTLIVING_ZOMBIE_MSG % (directory, data.grace_period))
Junji Watanabed952bf12021-05-13 03:15:54 +00001173 if sys.platform == 'win32':
1174 subprocess42.check_call(['tasklist.exe', '/V'], stdout=sys.stderr)
1175 else:
1176 subprocess42.check_call(['ps', 'axu'], stdout=sys.stderr)
maruel84537cb2015-10-16 14:21:28 -07001177 if result['exit_code'] == 0:
1178 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -07001179
maruela9cfd6f2015-09-15 11:03:15 -07001180 if not success and result['exit_code'] == 0:
1181 result['exit_code'] = 1
1182 except Exception as e:
1183 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -07001184 if out_dir:
1185 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -07001186 result['internal_failure'] = str(e)
Takuto Ikutaa9a907b2020-04-17 08:50:50 +00001187 on_error.report(None)
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001188 finally:
1189 cleanup_duration = time.time() - cleanup_start
1190 result['stats']['cleanup']['duration'] = cleanup_duration
1191 logging.info('Cleanup: removing directories took %d seconds',
1192 cleanup_duration)
maruela9cfd6f2015-09-15 11:03:15 -07001193 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -05001194
1195
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001196def run_tha_test(data, result_json):
nodir55be77b2016-05-03 09:39:57 -07001197 """Runs an executable and records execution metadata.
1198
nodir55be77b2016-05-03 09:39:57 -07001199 If isolated_hash is specified, downloads the dependencies in the cache,
1200 hardlinks them into a temporary directory and runs the command specified in
1201 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -05001202
1203 A temporary directory is created to hold the output files. The content inside
1204 this directory will be uploaded back to |storage| packaged as a .isolated
1205 file.
1206
1207 Arguments:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001208 - data: TaskData instance.
1209 - result_json: File path to dump result metadata into. If set, the process
1210 exit code is always 0 unless an internal error occurred.
maruela9cfd6f2015-09-15 11:03:15 -07001211
1212 Returns:
1213 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001214 """
maruela76b9ee2015-12-15 06:18:08 -08001215 if result_json:
1216 # Write a json output file right away in case we get killed.
1217 result = {
Junji Watanabe54925c32020-09-08 00:56:18 +00001218 'exit_code': None,
1219 'had_hard_timeout': False,
1220 'internal_failure': 'Was terminated before completion',
1221 'outputs_ref': None,
1222 'cas_output_root': None,
1223 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -08001224 }
1225 tools.write_json(result_json, result, dense=True)
1226
maruela9cfd6f2015-09-15 11:03:15 -07001227 # run_isolated exit code. Depends on if result_json is used or not.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001228 result = map_and_run(data, True)
maruela9cfd6f2015-09-15 11:03:15 -07001229 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -07001230
maruela9cfd6f2015-09-15 11:03:15 -07001231 if result_json:
maruel05d5a882015-09-21 13:59:02 -07001232 # We've found tests to delete 'work' when quitting, causing an exception
1233 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -07001234 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -07001235 tools.write_json(result_json, result, dense=True)
1236 # Only return 1 if there was an internal error.
1237 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +00001238
maruela9cfd6f2015-09-15 11:03:15 -07001239 # Marshall into old-style inline output.
1240 if result['outputs_ref']:
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001241 # pylint: disable=unsubscriptable-object
maruela9cfd6f2015-09-15 11:03:15 -07001242 data = {
Junji Watanabe38b28b02020-04-23 10:23:30 +00001243 'hash': result['outputs_ref']['isolated'],
1244 'namespace': result['outputs_ref']['namespace'],
1245 'storage': result['outputs_ref']['isolatedserver'],
maruela9cfd6f2015-09-15 11:03:15 -07001246 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -05001247 sys.stdout.flush()
Junji Watanabe38b28b02020-04-23 10:23:30 +00001248 print('[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
1249 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -08001250 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -07001251 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001252
1253
iannuccib58d10d2017-03-18 02:00:25 -07001254# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -08001255CipdInfo = collections.namedtuple('CipdInfo', [
1256 'client', # cipd.CipdClient object
1257 'cache_dir', # absolute path to bot-global cipd tag and instance cache
1258 'stats', # dict with stats to return to the server
1259 'pins', # dict with installed cipd pins to return to the server
1260])
1261
1262
1263@contextlib.contextmanager
Junji Watanabedc2f89e2021-11-08 08:44:30 +00001264def copy_local_packages(_run_dir, _isolated_dir, cas_dir, _nsjail_dir):
1265 """Copies CIPD packages from luci/luci-go dir."""
1266 go_client_dir = os.environ.get('LUCI_GO_CLIENT_DIR')
1267 assert go_client_dir, ('Please set LUCI_GO_CLIENT_DIR env var to install CIPD'
1268 ' packages locally.')
1269 shutil.copy2(os.path.join(go_client_dir, 'cas' + cipd.EXECUTABLE_SUFFIX),
1270 os.path.join(cas_dir, 'cas' + cipd.EXECUTABLE_SUFFIX))
vadimsh232f5a82017-01-20 19:23:44 -08001271 yield None
1272
1273
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001274def _install_packages(run_dir, cipd_cache_dir, client, packages):
iannuccib58d10d2017-03-18 02:00:25 -07001275 """Calls 'cipd ensure' for packages.
1276
1277 Args:
1278 run_dir (str): root of installation.
1279 cipd_cache_dir (str): the directory to use for the cipd package cache.
1280 client (CipdClient): the cipd client to use
1281 packages: packages to install, list [(path, package_name, version), ...].
iannuccib58d10d2017-03-18 02:00:25 -07001282
1283 Returns: list of pinned packages. Looks like [
1284 {
1285 'path': 'subdirectory',
1286 'package_name': 'resolved/package/name',
1287 'version': 'deadbeef...',
1288 },
1289 ...
1290 ]
1291 """
1292 package_pins = [None]*len(packages)
1293 def insert_pin(path, name, version, idx):
1294 package_pins[idx] = {
1295 'package_name': name,
1296 # swarming deals with 'root' as '.'
1297 'path': path or '.',
1298 'version': version,
1299 }
1300
1301 by_path = collections.defaultdict(list)
1302 for i, (path, name, version) in enumerate(packages):
1303 # cipd deals with 'root' as ''
1304 if path == '.':
1305 path = ''
1306 by_path[path].append((name, version, i))
1307
1308 pins = client.ensure(
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001309 run_dir,
1310 {
1311 subdir: [(name, vers) for name, vers, _ in pkgs
1312 ] for subdir, pkgs in by_path.items()
1313 },
1314 cache_dir=cipd_cache_dir,
iannuccib58d10d2017-03-18 02:00:25 -07001315 )
1316
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001317 for subdir, pin_list in sorted(pins.items()):
iannuccib58d10d2017-03-18 02:00:25 -07001318 this_subdir = by_path[subdir]
1319 for i, (name, version) in enumerate(pin_list):
1320 insert_pin(subdir, name, version, this_subdir[i][2])
1321
Robert Iannucci461b30d2017-12-13 11:34:03 -08001322 assert None not in package_pins, (packages, pins, package_pins)
iannuccib58d10d2017-03-18 02:00:25 -07001323
1324 return package_pins
1325
1326
vadimsh232f5a82017-01-20 19:23:44 -08001327@contextlib.contextmanager
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001328def install_client_and_packages(run_dir, packages, service_url,
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001329 client_package_name, client_version, cache_dir,
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +00001330 isolated_dir, cas_dir, nsjail_dir):
vadimsh902948e2017-01-20 15:57:32 -08001331 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -07001332
vadimsh232f5a82017-01-20 19:23:44 -08001333 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
1334
1335 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -07001336 [
1337 {
1338 "path": path, "package_name": package_name, "version": version,
1339 },
1340 ...
1341 ]
vadimsh902948e2017-01-20 15:57:32 -08001342 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -07001343
1344 such that they correspond 1:1 to all input package arguments from the command
1345 line. These dictionaries make their all the way back to swarming, where they
1346 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -07001347
vadimsh902948e2017-01-20 15:57:32 -08001348 If 'packages' list is empty, will bootstrap CIPD client, but won't install
1349 any packages.
1350
1351 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -08001352 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -08001353
nodirbe642ff2016-06-09 15:51:51 -07001354 Args:
nodir90bc8dc2016-06-15 13:35:21 -07001355 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -08001356 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -07001357 service_url (str): CIPD server url, e.g.
1358 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -07001359 client_package_name (str): CIPD package name of CIPD client.
1360 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -07001361 cache_dir (str): where to keep cache of cipd clients, packages and tags.
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001362 isolated_dir (str): where to download isolated client.
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001363 cas_dir (str): where to download cas client.
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +00001364 nsjail_dir (str): where to download nsjail. If set to None, nsjail is not
1365 downloaded.
nodirbe642ff2016-06-09 15:51:51 -07001366 """
1367 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -07001368
nodirbe642ff2016-06-09 15:51:51 -07001369 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -07001370
vadimsh902948e2017-01-20 15:57:32 -08001371 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -08001372 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -07001373 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -08001374 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -07001375
nodirbe642ff2016-06-09 15:51:51 -07001376 get_client_start = time.time()
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001377 client_manager = cipd.get_client(cache_dir, service_url, client_package_name,
1378 client_version)
iannucci96fcccc2016-08-30 15:52:22 -07001379
nodirbe642ff2016-06-09 15:51:51 -07001380 with client_manager as client:
1381 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -07001382
iannuccib58d10d2017-03-18 02:00:25 -07001383 package_pins = []
1384 if packages:
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001385 package_pins = _install_packages(run_dir, cipd_cache_dir, client,
1386 packages)
iannuccib58d10d2017-03-18 02:00:25 -07001387
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001388 # Install isolated client to |isolated_dir|.
Takuto Ikuta02edca22019-11-29 10:04:51 +00001389 _install_packages(isolated_dir, cipd_cache_dir, client,
Takuto Ikuta9c4eb1d2020-10-05 03:40:14 +00001390 [('', ISOLATED_PACKAGE, _LUCI_GO_REVISION)])
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001391
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001392 # Install cas client to |cas_dir|.
1393 _install_packages(cas_dir, cipd_cache_dir, client,
Takuto Ikuta9c4eb1d2020-10-05 03:40:14 +00001394 [('', _CAS_PACKAGE, _LUCI_GO_REVISION)])
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001395
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +00001396 # Install nsjail to |nsjail_dir|.
1397 if nsjail_dir is not None:
1398 _install_packages(nsjail_dir, cipd_cache_dir, client,
1399 [('', _NSJAIL_PACKAGE, _NSJAIL_VERSION)])
1400
iannuccib58d10d2017-03-18 02:00:25 -07001401 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -07001402
vadimsh232f5a82017-01-20 19:23:44 -08001403 total_duration = time.time() - start
Junji Watanabe38b28b02020-04-23 10:23:30 +00001404 logging.info('Installing CIPD client and packages took %d seconds',
1405 total_duration)
nodir90bc8dc2016-06-15 13:35:21 -07001406
vadimsh232f5a82017-01-20 19:23:44 -08001407 yield CipdInfo(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001408 client=client,
1409 cache_dir=cipd_cache_dir,
1410 stats={
1411 'duration': total_duration,
1412 'get_client_duration': get_client_duration,
iannuccib58d10d2017-03-18 02:00:25 -07001413 },
Junji Watanabe38b28b02020-04-23 10:23:30 +00001414 pins={
1415 'client_package': {
1416 'package_name': client.package_name,
1417 'version': client.instance_id,
1418 },
1419 'packages': package_pins,
1420 })
nodirbe642ff2016-06-09 15:51:51 -07001421
1422
1423def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001424 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -07001425 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +00001426 version=__version__,
1427 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -07001428 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001429 '--clean',
1430 action='store_true',
maruel36a963d2016-04-08 17:15:49 -07001431 help='Cleans the cache, trimming it necessary and remove corrupted items '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001432 'and returns without executing anything; use with -v to know what '
1433 'was done')
maruel36a963d2016-04-08 17:15:49 -07001434 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -07001435 '--json',
1436 help='dump output metadata to json file. When used, run_isolated returns '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001437 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -07001438 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001439 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -07001440 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001441 '--grace-period',
1442 type='float',
maruel6be7f9e2015-10-01 12:25:30 -07001443 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -07001444 parser.add_option(
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001445 '--relative-cwd',
Takuto Ikuta18ca29a2020-12-04 07:34:20 +00001446 help='Ignore the isolated \'relative_cwd\' and use this one instead')
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001447 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001448 '--env',
1449 default=[],
1450 action='append',
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001451 help='Environment variables to set for the child process')
1452 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001453 '--env-prefix',
1454 default=[],
1455 action='append',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001456 help='Specify a VAR=./path/fragment to put in the environment variable '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001457 'before executing the command. The path fragment must be relative '
1458 'to the isolated run directory, and must not contain a `..` token. '
1459 'The path will be made absolute and prepended to the indicated '
1460 '$VAR using the OS\'s path separator. Multiple items for the same '
1461 '$VAR will be prepended in order.')
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001462 parser.add_option(
bpastene3ae09522016-06-10 17:12:59 -07001463 '--bot-file',
1464 help='Path to a file describing the state of the host. The content is '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001465 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -07001466 parser.add_option(
vadimsh9c54b2c2017-07-25 14:08:29 -07001467 '--switch-to-account',
1468 help='If given, switches LUCI_CONTEXT to given logical service account '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001469 '(e.g. "task" or "system") before launching the isolated process.')
vadimsh9c54b2c2017-07-25 14:08:29 -07001470 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001471 '--output',
1472 action='append',
aludwin0a8e17d2016-10-27 15:57:39 -07001473 help='Specifies an output to return. If no outputs are specified, all '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001474 'files located in $(ISOLATED_OUTDIR) will be returned; '
1475 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
1476 'specified by --output option (there can be multiple) will be '
1477 'returned. Note that if a file in OUT_DIR has the same path '
1478 'as an --output option, the --output version will be returned.')
aludwin0a8e17d2016-10-27 15:57:39 -07001479 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001480 '-a',
1481 '--argsfile',
aludwin7556e0c2016-10-26 08:46:10 -07001482 # This is actually handled in parse_args; it's included here purely so it
1483 # can make it into the help text.
1484 help='Specify a file containing a JSON array of arguments to this '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001485 'script. If --argsfile is provided, no other argument may be '
1486 'provided on the command line.')
Takuto Ikutad4be2f12020-05-12 02:15:25 +00001487 parser.add_option(
1488 '--report-on-exception',
1489 action='store_true',
1490 help='Whether report exception during execution to isolate server. '
1491 'This flag should only be used in swarming bot.')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001492
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001493 group = optparse.OptionGroup(parser, 'Data source - Isolate server')
Junji Watanabe54925c32020-09-08 00:56:18 +00001494 # Deprecated. Isoate server is being migrated to RBE-CAS.
1495 # Remove --isolated and isolate server options after migration.
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001496 group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001497 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -07001498 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001499 isolateserver.add_isolate_server_options(group)
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001500 parser.add_option_group(group)
1501
1502 group = optparse.OptionGroup(parser,
1503 'Data source - Content Addressed Storage')
Junji Watanabe54925c32020-09-08 00:56:18 +00001504 group.add_option(
1505 '--cas-instance', help='Full CAS instance name for input/output files.')
1506 group.add_option(
1507 '--cas-digest',
1508 help='Digest of the input root on RBE-CAS. The format is '
1509 '`{hash}/{size_bytes}`.')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001510 parser.add_option_group(group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001511
Junji Watanabeb03450b2020-09-25 05:09:27 +00001512 # Cache options.
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -04001513 isolateserver.add_cache_options(parser)
Junji Watanabeb03450b2020-09-25 05:09:27 +00001514 add_cas_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001515
1516 cipd.add_cipd_options(parser)
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001517
1518 group = optparse.OptionGroup(parser, 'Named caches')
1519 group.add_option(
1520 '--named-cache',
1521 dest='named_caches',
1522 action='append',
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001523 nargs=3,
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001524 default=[],
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001525 help='A named cache to request. Accepts 3 arguments: name, path, hint. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001526 'name identifies the cache, must match regex [a-z0-9_]{1,4096}. '
1527 'path is a path relative to the run dir where the cache directory '
1528 'must be put to. '
1529 'This option can be specified more than once.')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001530 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001531 '--named-cache-root',
1532 default='named_caches',
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001533 help='Cache root directory. Default=%default')
1534 parser.add_option_group(group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001535
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001536 group = optparse.OptionGroup(parser, 'Process containment')
1537 parser.add_option(
1538 '--lower-priority', action='store_true',
1539 help='Lowers the child process priority')
1540 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001541 '--containment-type',
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +00001542 choices=('NONE', 'AUTO', 'JOB_OBJECT', 'NSJAIL'),
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001543 default='NONE',
1544 help='Type of container to use')
1545 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001546 '--limit-processes',
1547 type='int',
1548 default=0,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001549 help='Maximum number of active processes in the containment')
1550 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001551 '--limit-total-committed-memory',
1552 type='int',
1553 default=0,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001554 help='Maximum sum of committed memory in the containment')
1555 parser.add_option_group(group)
1556
1557 group = optparse.OptionGroup(parser, 'Debugging')
1558 group.add_option(
Kenneth Russell61d42352014-09-15 11:41:16 -07001559 '--leak-temp-dir',
1560 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -07001561 help='Deliberately leak isolate\'s temp dir for later examination. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001562 'Default: %default')
1563 group.add_option('--root-dir', help='Use a directory instead of a random one')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001564 parser.add_option_group(group)
Kenneth Russell61d42352014-09-15 11:41:16 -07001565
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001566 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001567
Ye Kuang1d096cb2020-06-26 08:38:21 +00001568 parser.set_defaults(cache='cache')
nodirbe642ff2016-06-09 15:51:51 -07001569 return parser
1570
1571
Junji Watanabeb03450b2020-09-25 05:09:27 +00001572def add_cas_cache_options(parser):
1573 group = optparse.OptionGroup(parser, 'CAS cache management')
1574 group.add_option(
1575 '--cas-cache',
1576 metavar='DIR',
1577 default='cas-cache',
1578 help='Directory to keep a local cache of the files. Accelerates download '
1579 'by reusing already downloaded files. Default=%default')
Takuto Ikuta7ff4b242020-12-03 08:07:06 +00001580 group.add_option(
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +00001581 '--kvs-dir',
Takuto Ikuta7ff4b242020-12-03 08:07:06 +00001582 default='',
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +00001583 help='CAS cache dir using kvs for small files. Default=%default')
Junji Watanabeb03450b2020-09-25 05:09:27 +00001584 parser.add_option_group(group)
1585
1586
1587def process_cas_cache_options(options):
1588 if options.cas_cache:
1589 policies = local_caching.CachePolicies(
1590 max_cache_size=options.max_cache_size,
1591 min_free_space=options.min_free_space,
1592 # max_items isn't used for CAS cache for now.
1593 max_items=None,
1594 max_age_secs=MAX_AGE_SECS)
1595
1596 return local_caching.DiskContentAddressedCache(
1597 six.text_type(os.path.abspath(options.cas_cache)), policies, trim=False)
1598 return local_caching.MemoryContentAddressedCache()
1599
1600
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001601def process_named_cache_options(parser, options, time_fn=None):
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001602 """Validates named cache options and returns a CacheManager."""
1603 if options.named_caches and not options.named_cache_root:
1604 parser.error('--named-cache is specified, but --named-cache-root is empty')
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001605 for name, path, hint in options.named_caches:
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001606 if not CACHE_NAME_RE.match(name):
1607 parser.error(
1608 'cache name %r does not match %r' % (name, CACHE_NAME_RE.pattern))
1609 if not path:
1610 parser.error('cache path cannot be empty')
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001611 try:
Takuto Ikuta630f99d2020-07-02 12:59:35 +00001612 int(hint)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001613 except ValueError:
1614 parser.error('cache hint must be a number')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001615 if options.named_cache_root:
1616 # Make these configurable later if there is use case but for now it's fairly
1617 # safe values.
1618 # In practice, a fair chunk of bots are already recycled on a daily schedule
1619 # so this code doesn't have any effect to them, unless they are preloaded
1620 # with a really old cache.
1621 policies = local_caching.CachePolicies(
1622 # 1TiB.
1623 max_cache_size=1024*1024*1024*1024,
1624 min_free_space=options.min_free_space,
1625 max_items=50,
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +00001626 max_age_secs=MAX_AGE_SECS)
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001627 root_dir = six.text_type(os.path.abspath(options.named_cache_root))
John Budorickc6186972020-02-26 00:58:14 +00001628 cache = local_caching.NamedCache(root_dir, policies, time_fn=time_fn)
1629 # Touch any named caches we're going to use to minimize thrashing
1630 # between tasks that request some (but not all) of the same named caches.
John Budorick0a4dab62020-03-02 22:23:35 +00001631 cache.touch(*[name for name, _, _ in options.named_caches])
John Budorickc6186972020-02-26 00:58:14 +00001632 return cache
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001633 return None
1634
1635
aludwin7556e0c2016-10-26 08:46:10 -07001636def parse_args(args):
1637 # Create a fake mini-parser just to get out the "-a" command. Note that
1638 # it's not documented here; instead, it's documented in create_option_parser
1639 # even though that parser will never actually get to parse it. This is
1640 # because --argsfile is exclusive with all other options and arguments.
1641 file_argparse = argparse.ArgumentParser(add_help=False)
1642 file_argparse.add_argument('-a', '--argsfile')
1643 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
1644 if file_args.argsfile:
1645 if nonfile_args:
1646 file_argparse.error('Can\'t specify --argsfile with'
1647 'any other arguments (%s)' % nonfile_args)
1648 try:
1649 with open(file_args.argsfile, 'r') as f:
1650 args = json.load(f)
1651 except (IOError, OSError, ValueError) as e:
1652 # We don't need to error out here - "args" is now empty,
1653 # so the call below to parser.parse_args(args) will fail
1654 # and print the full help text.
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +00001655 print('Couldn\'t read arguments: %s' % e, file=sys.stderr)
aludwin7556e0c2016-10-26 08:46:10 -07001656
1657 # Even if we failed to read the args, just call the normal parser now since it
1658 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -07001659 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001660 options, args = parser.parse_args(args)
Ye Kuangfff1e502020-07-13 13:21:57 +00001661 if not isinstance(options.cipd_enabled, (bool, int)):
1662 options.cipd_enabled = distutils.util.strtobool(options.cipd_enabled)
aludwin7556e0c2016-10-26 08:46:10 -07001663 return (parser, options, args)
1664
1665
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001666def _calc_named_cache_hint(named_cache, named_caches):
1667 """Returns the expected size of the missing named caches."""
1668 present = named_cache.available
1669 size = 0
Takuto Ikutad169bfd2021-08-02 05:45:09 +00001670 logging.info('available named cache %s', present)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001671 for name, _, hint in named_caches:
1672 if name not in present:
Takuto Ikuta630f99d2020-07-02 12:59:35 +00001673 hint = int(hint)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001674 if hint > 0:
Takuto Ikuta74686842021-07-30 04:11:03 +00001675 logging.info("named cache hint: %s, %d", name, hint)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001676 size += hint
Takuto Ikuta74686842021-07-30 04:11:03 +00001677 logging.info("total size of named cache hint: %d", size)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001678 return size
1679
1680
Takuto Ikutaae391c52020-12-03 08:43:45 +00001681def _clean_cmd(parser, options, caches, root):
Takuto Ikuta7ff4b242020-12-03 08:07:06 +00001682 """Cleanup cache dirs/files."""
1683 if options.isolated:
1684 parser.error('Can\'t use --isolated with --clean.')
1685 if options.isolate_server:
1686 parser.error('Can\'t use --isolate-server with --clean.')
1687 if options.json:
1688 parser.error('Can\'t use --json with --clean.')
1689 if options.named_caches:
1690 parser.error('Can\t use --named-cache with --clean.')
1691 if options.cas_instance or options.cas_digest:
1692 parser.error('Can\t use --cas-instance, --cas-digest with --clean.')
1693
1694 logging.info("initial free space: %d", file_path.get_free_space(root))
1695
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +00001696 if options.kvs_dir and fs.isdir(six.text_type(options.kvs_dir)):
Takuto Ikuta7ff4b242020-12-03 08:07:06 +00001697 # Remove kvs file if its size exceeds fixed threshold.
Takuto Ikutab1b70062021-03-22 01:02:41 +00001698 kvs_dir = six.text_type(options.kvs_dir)
1699 size = file_path.get_recursive_size(kvs_dir)
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +00001700 if size >= _CAS_KVS_CACHE_THRESHOLD:
1701 logging.info("remove kvs dir with size: %d", size)
Takuto Ikutab1b70062021-03-22 01:02:41 +00001702 file_path.rmtree(kvs_dir)
Takuto Ikuta7ff4b242020-12-03 08:07:06 +00001703
1704 # Trim first, then clean.
1705 local_caching.trim_caches(
1706 caches,
1707 root,
1708 min_free_space=options.min_free_space,
1709 max_age_secs=MAX_AGE_SECS)
1710 logging.info("free space after trim: %d", file_path.get_free_space(root))
1711 for c in caches:
1712 c.cleanup()
1713 logging.info("free space after cleanup: %d", file_path.get_free_space(root))
1714
1715
aludwin7556e0c2016-10-26 08:46:10 -07001716def main(args):
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -05001717 # Warning: when --argsfile is used, the strings are unicode instances, when
1718 # parsed normally, the strings are str instances.
aludwin7556e0c2016-10-26 08:46:10 -07001719 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -07001720
Joanna Wang40959bf2021-08-12 18:10:12 +00001721 # Must be logged after parse_args(), which eventually calls
1722 # logging_utils.prepare_logging() which expects no logs before its call.
1723 logging.info('Starting run_isolated script')
1724
Junji Watanabe1d83d282021-05-11 05:50:40 +00001725 SWARMING_SERVER = os.environ.get('SWARMING_SERVER')
1726 SWARMING_TASK_ID = os.environ.get('SWARMING_TASK_ID')
1727 if options.report_on_exception and SWARMING_SERVER:
1728 task_url = None
1729 if SWARMING_TASK_ID:
1730 task_url = '%s/task?id=%s' % (SWARMING_SERVER, SWARMING_TASK_ID)
1731 on_error.report_on_exception_exit(SWARMING_SERVER, source=task_url)
Takuto Ikutad4be2f12020-05-12 02:15:25 +00001732
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001733 if not file_path.enable_symlink():
Marc-Antoine Ruel5a024272019-01-15 20:11:16 +00001734 logging.warning('Symlink support is not enabled')
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001735
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001736 named_cache = process_named_cache_options(parser, options)
Marc-Antoine Ruel0d8b0f62018-09-10 14:40:35 +00001737 # hint is 0 if there's no named cache.
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001738 hint = _calc_named_cache_hint(named_cache, options.named_caches)
1739 if hint:
1740 # Increase the --min-free-space value by the hint, and recreate the
1741 # NamedCache instance so it gets the updated CachePolicy.
1742 options.min_free_space += hint
1743 named_cache = process_named_cache_options(parser, options)
1744
Takuto Ikuta5c59a842020-01-24 03:05:24 +00001745 # TODO(crbug.com/932396): Remove this.
Takuto Ikuta4a22c2c2020-06-05 02:02:23 +00001746 use_go_isolated = options.cipd_enabled
Takuto Ikuta5c59a842020-01-24 03:05:24 +00001747
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001748 # TODO(maruel): CIPD caches should be defined at an higher level here too, so
1749 # they can be cleaned the same way.
Takuto Ikutaf1c58442020-10-20 09:03:27 +00001750
1751 isolate_cache = isolateserver.process_cache_options(options, trim=False)
1752 cas_cache = process_cas_cache_options(options)
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +00001753
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001754 caches = []
1755 if isolate_cache:
1756 caches.append(isolate_cache)
Junji Watanabeb03450b2020-09-25 05:09:27 +00001757 if cas_cache:
1758 caches.append(cas_cache)
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001759 if named_cache:
1760 caches.append(named_cache)
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001761 root = caches[0].cache_dir if caches else six.text_type(os.getcwd())
maruel36a963d2016-04-08 17:15:49 -07001762 if options.clean:
Takuto Ikutaae391c52020-12-03 08:43:45 +00001763 _clean_cmd(parser, options, caches, root)
maruel36a963d2016-04-08 17:15:49 -07001764 return 0
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001765
1766 # Trim must still be done for the following case:
1767 # - named-cache was used
1768 # - some entries, with a large hint, where missing
1769 # - --min-free-space was increased accordingly, thus trimming is needed
1770 # Otherwise, this will have no effect, as bot_main calls run_isolated with
1771 # --clean after each task.
Takuto Ikutac9ddff22021-02-18 07:58:39 +00001772 additional_buffer = _FREE_SPACE_BUFFER_FOR_CIPD_PACKAGES
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +00001773 if options.kvs_dir:
Takuto Ikuta7f45c592021-02-09 05:57:05 +00001774 additional_buffer += _CAS_KVS_CACHE_THRESHOLD
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001775 # Add some buffer for Go CLI.
1776 min_free_space = options.min_free_space + additional_buffer
1777
1778 def trim_caches_fn(stats):
1779 start = time.time()
1780 local_caching.trim_caches(
1781 caches, root, min_free_space=min_free_space, max_age_secs=MAX_AGE_SECS)
1782 duration = time.time() - start
1783 stats['duration'] = duration
1784 logging.info('trim_caches: took %d seconds', duration)
maruel36a963d2016-04-08 17:15:49 -07001785
Takuto Ikutaf1c58442020-10-20 09:03:27 +00001786 # Save state of isolate/cas cache not to overwrite state from go client.
1787 if use_go_isolated:
1788 isolate_cache.save()
1789 isolate_cache = None
1790 if cas_cache:
1791 cas_cache.save()
1792 cas_cache = None
1793
Takuto Ikutadc496672021-11-12 05:58:59 +00001794 if not args:
1795 parser.error('command to run is required.')
nodir55be77b2016-05-03 09:39:57 -07001796
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001797 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001798
Takuto Ikutaae767b32020-05-11 01:22:19 +00001799 isolateserver.process_isolate_server_options(parser, options, False)
Junji Watanabeed9ce352020-09-25 12:32:07 +00001800 if ISOLATED_OUTDIR_PARAMETER in args and (not options.isolate_server and
1801 not options.cas_instance):
1802 parser.error('%s in args requires --isolate-server or --cas-instance' %
1803 ISOLATED_OUTDIR_PARAMETER)
1804
1805 if options.isolated and not options.isolate_server:
1806 parser.error('--isolated requires --isolate-server')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001807
nodir90bc8dc2016-06-15 13:35:21 -07001808 if options.root_dir:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001809 options.root_dir = six.text_type(os.path.abspath(options.root_dir))
Takuto Ikutad46ea762020-10-07 05:43:22 +00001810 else:
1811 options.root_dir = six.text_type(tempfile.mkdtemp(prefix='root'))
maruel12e30012015-10-09 11:55:35 -07001812 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001813 options.json = six.text_type(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001814
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001815 if any('=' not in i for i in options.env):
1816 parser.error(
1817 '--env required key=value form. value can be skipped to delete '
1818 'the variable')
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001819 options.env = dict(i.split('=', 1) for i in options.env)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001820
1821 prefixes = {}
1822 cwd = os.path.realpath(os.getcwd())
1823 for item in options.env_prefix:
1824 if '=' not in item:
1825 parser.error(
1826 '--env-prefix %r is malformed, must be in the form `VAR=./path`'
1827 % item)
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001828 key, opath = item.split('=', 1)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001829 if os.path.isabs(opath):
1830 parser.error('--env-prefix %r path is bad, must be relative.' % opath)
1831 opath = os.path.normpath(opath)
1832 if not os.path.realpath(os.path.join(cwd, opath)).startswith(cwd):
1833 parser.error(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001834 '--env-prefix %r path is bad, must be relative and not contain `..`.'
1835 % opath)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001836 prefixes.setdefault(key, []).append(opath)
1837 options.env_prefix = prefixes
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001838
nodirbe642ff2016-06-09 15:51:51 -07001839 cipd.validate_cipd_options(parser, options)
1840
Junji Watanabedc2f89e2021-11-08 08:44:30 +00001841 install_packages_fn = copy_local_packages
Ye Kuang1d096cb2020-06-26 08:38:21 +00001842 tmp_cipd_cache_dir = None
vadimsh902948e2017-01-20 15:57:32 -08001843 if options.cipd_enabled:
Ye Kuang1d096cb2020-06-26 08:38:21 +00001844 cache_dir = options.cipd_cache
1845 if not cache_dir:
1846 tmp_cipd_cache_dir = six.text_type(tempfile.mkdtemp())
1847 cache_dir = tmp_cipd_cache_dir
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +00001848 install_packages_fn = (lambda run_dir, isolated_dir, cas_dir, nsjail_dir:
1849 install_client_and_packages(
1850 run_dir,
1851 cipd.parse_package_args(options.cipd_packages),
1852 options.cipd_server,
1853 options.cipd_client_package,
1854 options.cipd_client_version,
1855 cache_dir=cache_dir,
1856 isolated_dir=isolated_dir,
1857 cas_dir=cas_dir,
1858 nsjail_dir=nsjail_dir,
1859 ))
nodirbe642ff2016-06-09 15:51:51 -07001860
nodird6160682017-02-02 13:03:35 -08001861 @contextlib.contextmanager
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001862 def install_named_caches(run_dir, stats):
nodird6160682017-02-02 13:03:35 -08001863 # WARNING: this function depends on "options" variable defined in the outer
1864 # function.
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001865 assert six.text_type(run_dir), repr(run_dir)
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001866 assert os.path.isabs(run_dir), run_dir
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001867 named_caches = [(os.path.join(run_dir, six.text_type(relpath)), name)
1868 for name, relpath, _ in options.named_caches]
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001869 install_start = time.time()
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001870 for path, name in named_caches:
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001871 named_cache.install(path, name)
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001872 install_duration = time.time() - install_start
1873 stats['install']['duration'] = install_duration
1874 logging.info('named_caches: install took %d seconds', install_duration)
nodird6160682017-02-02 13:03:35 -08001875 try:
1876 yield
1877 finally:
dnje289d132017-07-07 11:16:44 -07001878 # Uninstall each named cache, returning it to the cache pool. If an
1879 # uninstall fails for a given cache, it will remain in the task's
1880 # temporary space, get cleaned up by the Swarming bot, and be lost.
1881 #
1882 # If the Swarming bot cannot clean up the cache, it will handle it like
1883 # any other bot file that could not be removed.
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001884 uninstall_start = time.time()
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001885 for path, name in reversed(named_caches):
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001886 try:
Marc-Antoine Ruele9558372018-08-03 03:41:22 +00001887 # uninstall() doesn't trim but does call save() implicitly. Trimming
1888 # *must* be done manually via periodic 'run_isolated.py --clean'.
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001889 named_cache.uninstall(path, name)
1890 except local_caching.NamedCacheError:
Takuto Ikuta463ecdd2021-03-05 09:35:38 +00001891 if sys.platform == 'win32':
1892 # Show running processes.
1893 sys.stderr.write("running process\n")
1894 subprocess42.check_call(['tasklist.exe', '/V'], stdout=sys.stderr)
1895
Junji Watanabed2ab86b2021-08-13 07:20:23 +00001896 error = (
1897 'Error while removing named cache %r at %r. The cache will be'
1898 ' lost.' % (path, name))
1899 logging.exception(error)
1900 on_error.report(error)
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001901 uninstall_duration = time.time() - uninstall_start
1902 stats['uninstall']['duration'] = uninstall_duration
1903 logging.info('named_caches: uninstall took %d seconds',
1904 uninstall_duration)
nodirf33b8d62016-10-26 22:34:58 -07001905
Takuto Ikutaf3caa9b2020-11-02 05:38:26 +00001906 command = args
1907 if options.relative_cwd:
1908 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1909 if not a.startswith(os.getcwd()):
1910 parser.error(
1911 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001912
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001913 containment_type = subprocess42.Containment.NONE
1914 if options.containment_type == 'AUTO':
1915 containment_type = subprocess42.Containment.AUTO
1916 if options.containment_type == 'JOB_OBJECT':
1917 containment_type = subprocess42.Containment.JOB_OBJECT
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +00001918 if options.containment_type == 'NSJAIL':
1919 containment_type = subprocess42.Containment.NSJAIL
1920 # TODO(https://crbug.com/1227833): This object should eventually contain the
1921 # path to the nsjail binary and the nsjail configuration file.
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001922 containment = subprocess42.Containment(
1923 containment_type=containment_type,
1924 limit_processes=options.limit_processes,
1925 limit_total_committed_memory=options.limit_total_committed_memory)
1926
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001927 data = TaskData(
1928 command=command,
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001929 relative_cwd=options.relative_cwd,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001930 isolated_hash=options.isolated,
1931 storage=None,
1932 isolate_cache=isolate_cache,
Junji Watanabe54925c32020-09-08 00:56:18 +00001933 cas_instance=options.cas_instance,
1934 cas_digest=options.cas_digest,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001935 outputs=options.output,
1936 install_named_caches=install_named_caches,
1937 leak_temp_dir=options.leak_temp_dir,
1938 root_dir=_to_unicode(options.root_dir),
1939 hard_timeout=options.hard_timeout,
1940 grace_period=options.grace_period,
1941 bot_file=options.bot_file,
1942 switch_to_account=options.switch_to_account,
1943 install_packages_fn=install_packages_fn,
Takuto Ikuta5c59a842020-01-24 03:05:24 +00001944 use_go_isolated=use_go_isolated,
Takuto Ikuta10cae642020-01-08 08:12:07 +00001945 go_cache_dir=options.cache,
Takuto Ikuta879788c2020-01-10 08:00:26 +00001946 go_cache_policies=local_caching.CachePolicies(
1947 max_cache_size=options.max_cache_size,
1948 min_free_space=options.min_free_space,
1949 max_items=options.max_items,
1950 max_age_secs=None,
1951 ),
Junji Watanabeb03450b2020-09-25 05:09:27 +00001952 cas_cache_dir=options.cas_cache,
1953 cas_cache_policies=local_caching.CachePolicies(
1954 max_cache_size=options.max_cache_size,
1955 min_free_space=options.min_free_space,
1956 max_items=None,
1957 max_age_secs=None,
1958 ),
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +00001959 cas_kvs=options.kvs_dir,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001960 env=options.env,
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +00001961 env_prefix=options.env_prefix,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001962 lower_priority=bool(options.lower_priority),
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001963 containment=containment,
1964 trim_caches_fn=trim_caches_fn)
nodirbe642ff2016-06-09 15:51:51 -07001965 try:
nodir90bc8dc2016-06-15 13:35:21 -07001966 if options.isolate_server:
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001967 server_ref = isolate_storage.ServerRef(
nodir90bc8dc2016-06-15 13:35:21 -07001968 options.isolate_server, options.namespace)
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001969 storage = isolateserver.get_storage(server_ref)
nodir90bc8dc2016-06-15 13:35:21 -07001970 with storage:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001971 data = data._replace(storage=storage)
nodirf33b8d62016-10-26 22:34:58 -07001972 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001973 assert storage.server_ref.hash_algo == server_ref.hash_algo
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001974 return run_tha_test(data, options.json)
1975 return run_tha_test(data, options.json)
Junji Watanabe38b28b02020-04-23 10:23:30 +00001976 except (cipd.Error, local_caching.NamedCacheError,
1977 local_caching.NoMoreSpace) as ex:
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +00001978 print(ex.message, file=sys.stderr)
Junji Watanabed2ab86b2021-08-13 07:20:23 +00001979 on_error.report(None)
nodirbe642ff2016-06-09 15:51:51 -07001980 return 1
Ye Kuang1d096cb2020-06-26 08:38:21 +00001981 finally:
1982 if tmp_cipd_cache_dir is not None:
1983 try:
1984 file_path.rmtree(tmp_cipd_cache_dir)
1985 except OSError:
1986 logging.exception('Remove tmp_cipd_cache_dir=%s failed',
1987 tmp_cipd_cache_dir)
1988 # Best effort clean up. Failed to do so doesn't affect the outcome.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001989
1990
1991if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001992 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001993 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001994 fix_encoding.fix_encoding()
Ye Kuang2dd17442020-04-22 08:45:52 +00001995 net.set_user_agent('run_isolated.py/' + __version__)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001996 sys.exit(main(sys.argv[1:]))