blob: 57452e8c4316b29f49f93ab1f518f2c60eaf3ea9 [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
71
72# pylint: disable=ungrouped-imports
Takuto Ikutad53d7bd2021-07-16 03:09:33 +000073import DEPS
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000074import auth
75import cipd
Justin Luong97eda6f2022-08-23 01:29:16 +000076import errors
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000077import local_caching
78from libs import luci_context
Vadim Shtayura6b555c12014-07-23 16:22:18 -070079from utils import file_path
maruel12e30012015-10-09 11:55:35 -070080from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040081from utils import logging_utils
Ye Kuang2dd17442020-04-22 08:45:52 +000082from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040083from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050084from utils import subprocess42
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000085
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000086
maruele2f2cb82016-07-13 14:41:03 -070087# Magic variables that can be found in the isolate task command line.
88ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
89EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
90SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
Joanna Wang4cec0e42021-08-26 00:48:37 +000091SWARMING_TASK_ID_PARAMETER = '${SWARMING_TASK_ID}'
maruele2f2cb82016-07-13 14:41:03 -070092
93
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000094# The name of the log file to use.
95RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
96
maruele2f2cb82016-07-13 14:41:03 -070097
maruele2f2cb82016-07-13 14:41:03 -070098# Use short names for temporary directories. This is driven by Windows, which
99# imposes a relatively short maximum path length of 260 characters, often
100# referred to as MAX_PATH. It is relatively easy to create files with longer
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +0000101# path length. A use case is with recursive dependency trees like npm packages.
maruele2f2cb82016-07-13 14:41:03 -0700102#
103# It is recommended to start the script with a `root_dir` as short as
104# possible.
105# - ir stands for isolated_run
106# - io stands for isolated_out
107# - it stands for isolated_tmp
Takuto Ikutab7ce0e32019-11-27 23:26:18 +0000108# - ic stands for isolated_client
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +0000109# - ns stands for nsjail
Junji Watanabe53d31882022-01-13 07:58:00 +0000110ISOLATED_RUN_DIR = 'ir'
111ISOLATED_OUT_DIR = 'io'
112ISOLATED_TMP_DIR = 'it'
113ISOLATED_CLIENT_DIR = 'ic'
114_CAS_CLIENT_DIR = 'cc'
115_NSJAIL_DIR = 'ns'
maruele2f2cb82016-07-13 14:41:03 -0700116
Takuto Ikuta02edca22019-11-29 10:04:51 +0000117# TODO(tikuta): take these parameter from luci-config?
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000118_CAS_PACKAGE = 'infra/tools/luci/cas/${platform}'
Takuto Ikutad53d7bd2021-07-16 03:09:33 +0000119_LUCI_GO_REVISION = DEPS.deps['luci-go']['packages'][0]['version']
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +0000120_NSJAIL_PACKAGE = 'infra/3pp/tools/nsjail/${platform}'
121_NSJAIL_VERSION = DEPS.deps['nsjail']['packages'][0]['version']
maruele2f2cb82016-07-13 14:41:03 -0700122
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400123# Keep synced with task_request.py
Lei Leife202df2019-06-11 17:33:34 +0000124CACHE_NAME_RE = re.compile(r'^[a-z0-9_]{1,4096}$')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400125
Takuto Ikutac9ddff22021-02-18 07:58:39 +0000126_FREE_SPACE_BUFFER_FOR_CIPD_PACKAGES = 2 * 1024 * 1024 * 1024
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400127
marueld928c862017-06-08 08:20:04 -0700128OUTLIVING_ZOMBIE_MSG = """\
129*** Swarming tried multiple times to delete the %s directory and failed ***
130*** Hard failing the task ***
131
132Swarming detected that your testing script ran an executable, which may have
133started a child executable, and the main script returned early, leaving the
134children executables playing around unguided.
135
136You don't want to leave children processes outliving the task on the Swarming
137bot, do you? The Swarming bot doesn't.
138
139How to fix?
140- For any process that starts children processes, make sure all children
141 processes terminated properly before each parent process exits. This is
142 especially important in very deep process trees.
143 - This must be done properly both in normal successful task and in case of
144 task failure. Cleanup is very important.
145- The Swarming bot sends a SIGTERM in case of timeout.
146 - You have %s seconds to comply after the signal was sent to the process
147 before the process is forcibly killed.
148- To achieve not leaking children processes in case of signals on timeout, you
149 MUST handle signals in each executable / python script and propagate them to
150 children processes.
151 - When your test script (python or binary) receives a signal like SIGTERM or
152 CTRL_BREAK_EVENT on Windows), send it to all children processes and wait for
153 them to terminate before quitting.
154
155See
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -0400156https://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 -0700157for more information.
158
159*** May the SIGKILL force be with you ***
160"""
161
162
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000163# Currently hardcoded. Eventually could be exposed as a flag once there's value.
164# 3 weeks
165MAX_AGE_SECS = 21*24*60*60
166
Takuto Ikuta7ff4b242020-12-03 08:07:06 +0000167_CAS_KVS_CACHE_THRESHOLD = 5 * 1024 * 1024 * 1024 # 5 GiB
168
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500169TaskData = collections.namedtuple(
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000170 'TaskData',
171 [
Takuto Ikuta9a319502019-11-26 07:40:14 +0000172 # List of strings; the command line to use, independent of what was
173 # specified in the isolated file.
174 'command',
175 # Relative directory to start command into.
176 'relative_cwd',
Junji Watanabe54925c32020-09-08 00:56:18 +0000177 # Digest of the input root on RBE-CAS.
178 'cas_digest',
179 # Full CAS instance name.
180 'cas_instance',
Takuto Ikuta9a319502019-11-26 07:40:14 +0000181 # List of paths relative to root_dir to put into the output isolated
182 # bundle upon task completion (see link_outputs_to_outdir).
183 'outputs',
184 # Function (run_dir) => context manager that installs named caches into
185 # |run_dir|.
186 'install_named_caches',
187 # If True, the temporary directory will be deliberately leaked for later
188 # examination.
189 'leak_temp_dir',
190 # Path to the directory to use to create the temporary directory. If not
191 # specified, a random temporary directory is created.
192 'root_dir',
193 # Kills the process if it lasts more than this amount of seconds.
194 'hard_timeout',
195 # Number of seconds to wait between SIGTERM and SIGKILL.
196 'grace_period',
197 # Path to a file with bot state, used in place of ${SWARMING_BOT_FILE}
198 # task command line argument.
199 'bot_file',
200 # Logical account to switch LUCI_CONTEXT into.
201 'switch_to_account',
202 # Context manager dir => CipdInfo, see install_client_and_packages.
203 'install_packages_fn',
Junji Watanabeb03450b2020-09-25 05:09:27 +0000204 # Cache directory for `cas` client.
205 'cas_cache_dir',
206 # Parameters passed to `cas` client.
207 'cas_cache_policies',
Takuto Ikutaae391c52020-12-03 08:43:45 +0000208 # Parameters for kvs file used by `cas` client.
209 'cas_kvs',
Takuto Ikuta9a319502019-11-26 07:40:14 +0000210 # Environment variables to set.
211 'env',
212 # Environment variables to mutate with relative directories.
213 # Example: {"ENV_KEY": ['relative', 'paths', 'to', 'prepend']}
214 'env_prefix',
215 # Lowers the task process priority.
216 'lower_priority',
217 # subprocess42.Containment instance. Can be None.
218 'containment',
Junji Watanabeaee69ad2021-04-28 03:17:34 +0000219 # Function to trim caches before installing cipd packages and
220 # downloading isolated files.
221 'trim_caches_fn',
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000222 ])
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500223
maruel03e11842016-07-14 10:50:16 -0700224def make_temp_dir(prefix, root_dir):
225 """Returns a new unique temporary directory."""
Junji Watanabe7a631b02022-01-13 02:30:29 +0000226 return tempfile.mkdtemp(prefix=prefix, dir=root_dir)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000227
228
vadimsh9c54b2c2017-07-25 14:08:29 -0700229@contextlib.contextmanager
230def set_luci_context_account(account, tmp_dir):
231 """Sets LUCI_CONTEXT account to be used by the task.
232
233 If 'account' is None or '', does nothing at all. This happens when
234 run_isolated.py is called without '--switch-to-account' flag. In this case,
235 if run_isolated.py is running in some LUCI_CONTEXT environment, the task will
Takuto Ikuta33e2ff32019-09-30 12:44:03 +0000236 just inherit whatever account is already set. This may happen if users invoke
vadimsh9c54b2c2017-07-25 14:08:29 -0700237 run_isolated.py explicitly from their code.
238
239 If the requested account is not defined in the context, switches to
240 non-authenticated access. This happens for Swarming tasks that don't use
241 'task' service accounts.
242
243 If not using LUCI_CONTEXT-based auth, does nothing.
244 If already running as requested account, does nothing.
245 """
246 if not account:
247 # Not actually switching.
248 yield
249 return
250
251 local_auth = luci_context.read('local_auth')
252 if not local_auth:
253 # Not using LUCI_CONTEXT auth at all.
254 yield
255 return
256
257 # See LUCI_CONTEXT.md for the format of 'local_auth'.
258 if local_auth.get('default_account_id') == account:
259 # Already set, no need to switch.
260 yield
261 return
262
263 available = {a['id'] for a in local_auth.get('accounts') or []}
264 if account in available:
265 logging.info('Switching default LUCI_CONTEXT account to %r', account)
266 local_auth['default_account_id'] = account
267 else:
268 logging.warning(
269 'Requested LUCI_CONTEXT account %r is not available (have only %r), '
270 'disabling authentication', account, sorted(available))
271 local_auth.pop('default_account_id', None)
272
273 with luci_context.write(_tmpdir=tmp_dir, local_auth=local_auth):
274 yield
275
276
nodir90bc8dc2016-06-15 13:35:21 -0700277def process_command(command, out_dir, bot_file):
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000278 """Replaces parameters in a command line.
nodirbe642ff2016-06-09 15:51:51 -0700279
280 Raises:
281 ValueError if a parameter is requested in |command| but its value is not
282 provided.
283 """
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000284 return [replace_parameters(arg, out_dir, bot_file) for arg in command]
285
286
287def replace_parameters(arg, out_dir, bot_file):
288 """Replaces parameter tokens with appropriate values in a string.
289
290 Raises:
291 ValueError if a parameter is requested in |arg| but its value is not
292 provided.
293 """
294 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
295 replace_slash = False
296 if ISOLATED_OUTDIR_PARAMETER in arg:
297 if not out_dir:
298 raise ValueError(
299 'output directory is requested in command or env var, but not '
300 'provided; please specify one')
301 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
302 replace_slash = True
303 if SWARMING_BOT_FILE_PARAMETER in arg:
304 if bot_file:
305 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
nodirbe642ff2016-06-09 15:51:51 -0700306 replace_slash = True
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000307 else:
308 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command or env '
309 'var, but no bot_file specified. Leaving parameter '
310 'unchanged.')
Joanna Wang4cec0e42021-08-26 00:48:37 +0000311 if SWARMING_TASK_ID_PARAMETER in arg:
312 task_id = os.environ.get('SWARMING_TASK_ID')
313 if task_id:
314 arg = arg.replace(SWARMING_TASK_ID_PARAMETER, task_id)
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000315 if replace_slash:
316 # Replace slashes only if parameters are present
317 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
318 arg = arg.replace('/', os.sep)
319 return arg
maruela9cfd6f2015-09-15 11:03:15 -0700320
321
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000322def set_temp_dir(env, tmp_dir):
323 """Set temp dir to given env var dictionary"""
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000324 # pylint: disable=line-too-long
325 # * python respects $TMPDIR, $TEMP, and $TMP in this order, regardless of
326 # platform. So $TMPDIR must be set on all platforms.
327 # https://github.com/python/cpython/blob/2.7/Lib/tempfile.py#L155
328 env['TMPDIR'] = tmp_dir
329 if sys.platform == 'win32':
330 # * chromium's base utils uses GetTempPath().
331 # https://cs.chromium.org/chromium/src/base/files/file_util_win.cc?q=GetTempPath
332 # * Go uses GetTempPath().
333 # * GetTempDir() uses %TMP%, then %TEMP%, then other stuff. So %TMP% must be
334 # set.
335 # https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-gettemppathw
336 env['TMP'] = tmp_dir
337 # https://blogs.msdn.microsoft.com/oldnewthing/20150417-00/?p=44213
338 env['TEMP'] = tmp_dir
339 elif sys.platform == 'darwin':
340 # * Chromium uses an hack on macOS before calling into
341 # NSTemporaryDirectory().
342 # https://cs.chromium.org/chromium/src/base/files/file_util_mac.mm?q=GetTempDir
343 # https://developer.apple.com/documentation/foundation/1409211-nstemporarydirectory
344 env['MAC_CHROMIUM_TMPDIR'] = tmp_dir
345 else:
346 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
347 # http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
348 # * mktemp on linux respects $TMPDIR.
349 # * Chromium respects $TMPDIR on linux.
350 # https://cs.chromium.org/chromium/src/base/files/file_util_posix.cc?q=GetTempDir
351 # * Go uses $TMPDIR.
352 # https://go.googlesource.com/go/+/go1.10.3/src/os/file_unix.go#307
353 pass
354
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000355
356def get_command_env(tmp_dir, cipd_info, run_dir, env, env_prefixes, out_dir,
357 bot_file):
vadimsh232f5a82017-01-20 19:23:44 -0800358 """Returns full OS environment to run a command in.
359
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800360 Sets up TEMP, puts directory with cipd binary in front of PATH, exposes
361 CIPD_CACHE_DIR env var, and installs all env_prefixes.
vadimsh232f5a82017-01-20 19:23:44 -0800362
363 Args:
364 tmp_dir: temp directory.
365 cipd_info: CipdInfo object is cipd client is used, None if not.
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500366 run_dir: The root directory the isolated tree is mapped in.
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500367 env: environment variables to use
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800368 env_prefixes: {"ENV_KEY": ['cwd', 'relative', 'paths', 'to', 'prepend']}
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000369 out_dir: Isolated output directory. Required to be != None if any of the
370 env vars contain ISOLATED_OUTDIR_PARAMETER.
371 bot_file: Required to be != None if any of the env vars contain
372 SWARMING_BOT_FILE_PARAMETER.
vadimsh232f5a82017-01-20 19:23:44 -0800373 """
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500374 out = os.environ.copy()
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000375 for k, v in env.items():
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500376 if not v:
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500377 out.pop(k, None)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500378 else:
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000379 out[k] = replace_parameters(v, out_dir, bot_file)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500380
381 if cipd_info:
382 bin_dir = os.path.dirname(cipd_info.client.binary_path)
Junji Watanabe7a631b02022-01-13 02:30:29 +0000383 out['PATH'] = '%s%s%s' % (bin_dir, os.pathsep, out['PATH'])
384 out['CIPD_CACHE_DIR'] = cipd_info.cache_dir
Takuto Ikuta4ec3e8f2021-04-05 10:21:29 +0000385 cipd_info_path = os.path.join(tmp_dir, 'cipd_info.json')
386 with open(cipd_info_path, 'w') as f:
387 json.dump(cipd_info.pins, f)
388 out['ISOLATED_RESOLVED_PACKAGE_VERSIONS_FILE'] = cipd_info_path
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500389
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000390 for key, paths in env_prefixes.items():
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500391 assert isinstance(paths, list), paths
392 paths = [os.path.normpath(os.path.join(run_dir, p)) for p in paths]
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500393 cur = out.get(key)
394 if cur:
395 paths.append(cur)
Junji Watanabe7a631b02022-01-13 02:30:29 +0000396 out[key] = os.path.pathsep.join(paths)
vadimsh232f5a82017-01-20 19:23:44 -0800397
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000398 set_temp_dir(out, tmp_dir)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500399 return out
vadimsh232f5a82017-01-20 19:23:44 -0800400
401
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000402def run_command(
403 command, cwd, env, hard_timeout, grace_period, lower_priority, containment):
maruel6be7f9e2015-10-01 12:25:30 -0700404 """Runs the command.
405
406 Returns:
407 tuple(process exit code, bool if had a hard timeout)
408 """
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000409 logging_utils.user_logs('run_command(%s, %s, %s, %s, %s, %s)', command, cwd,
410 hard_timeout, grace_period, lower_priority,
411 containment)
marueleb5fbee2015-09-17 13:01:36 -0700412
maruel6be7f9e2015-10-01 12:25:30 -0700413 exit_code = None
414 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700415 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700416 proc = None
417 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700418 try:
maruel6be7f9e2015-10-01 12:25:30 -0700419 # TODO(maruel): This code is imperfect. It doesn't handle well signals
420 # during the download phase and there's short windows were things can go
421 # wrong.
422 def handler(signum, _frame):
423 if proc and not had_signal:
424 logging.info('Received signal %d', signum)
425 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700426 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700427
Marc-Antoine Ruel30b80fe2019-02-08 13:51:31 +0000428 proc = subprocess42.Popen(
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000429 command, cwd=cwd, env=env, detached=True, close_fds=True,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000430 lower_priority=lower_priority, containment=containment)
Joanna Wang40959bf2021-08-12 18:10:12 +0000431 logging.info('Subprocess for command started')
maruel6be7f9e2015-10-01 12:25:30 -0700432 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
433 try:
John Budorickc398f092019-06-10 22:49:44 +0000434 exit_code = proc.wait(hard_timeout or None)
Takuto Ikuta88382c82022-02-03 08:46:17 +0000435 logging.info("finished with exit code %d after hard_timeout %s",
436 exit_code, hard_timeout)
maruel6be7f9e2015-10-01 12:25:30 -0700437 except subprocess42.TimeoutExpired:
438 if not had_signal:
439 logging.warning('Hard timeout')
440 had_hard_timeout = True
441 logging.warning('Sending SIGTERM')
442 proc.terminate()
443
Takuto Ikuta684f7912020-09-29 07:49:49 +0000444 kill_sent = False
maruel6be7f9e2015-10-01 12:25:30 -0700445 # Ignore signals in grace period. Forcibly give the grace period to the
446 # child process.
447 if exit_code is None:
448 ignore = lambda *_: None
449 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
450 try:
451 exit_code = proc.wait(grace_period or None)
Takuto Ikuta88382c82022-02-03 08:46:17 +0000452 logging.info("finished with exit code %d after grace_period %s",
453 exit_code, grace_period)
maruel6be7f9e2015-10-01 12:25:30 -0700454 except subprocess42.TimeoutExpired:
455 # Now kill for real. The user can distinguish between the
456 # following states:
457 # - signal but process exited within grace period,
458 # hard_timed_out will be set but the process exit code will be
459 # script provided.
460 # - processed exited late, exit code will be -9 on posix.
461 logging.warning('Grace exhausted; sending SIGKILL')
462 proc.kill()
Takuto Ikuta684f7912020-09-29 07:49:49 +0000463 kill_sent = True
martiniss5c8043e2017-08-01 17:09:43 -0700464 logging.info('Waiting for process exit')
maruel6be7f9e2015-10-01 12:25:30 -0700465 exit_code = proc.wait()
Takuto Ikuta684f7912020-09-29 07:49:49 +0000466
467 # the process group / job object may be dangling so if we didn't kill
468 # it already, give it a poke now.
469 if not kill_sent:
470 proc.kill()
Takuto Ikutaeccf0862020-03-19 03:05:55 +0000471 except OSError as e:
maruela9cfd6f2015-09-15 11:03:15 -0700472 # This is not considered to be an internal error. The executable simply
473 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800474 sys.stderr.write(
tikuta2d678212019-09-23 23:12:08 +0000475 '<The executable does not exist, a dependent library is missing or '
476 'the command line is too long>\n'
477 '<Check for missing .so/.dll in the .isolate or GN file or length of '
478 'command line args>\n'
Takuto Ikutae900df42021-04-14 04:40:11 +0000479 '<Command: %s>\n'
480 '<Exception: %s>\n' % (command, e))
maruela72f46e2016-02-24 11:05:45 -0800481 if os.environ.get('SWARMING_TASK_ID'):
482 # Give an additional hint when running as a swarming task.
483 sys.stderr.write(
484 '<See the task\'s page for commands to help diagnose this issue '
485 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700486 exit_code = 1
487 logging.info(
488 'Command finished with exit code %d (%s)',
489 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700490 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700491
492
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000493def _run_go_cmd_and_wait(cmd, tmp_dir):
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000494 """
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000495 Runs an external Go command, `isolated` or `cas`, and wait for its completion.
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000496
497 While this is a generic function to launch a subprocess, it has logic that
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000498 is specific to Go `isolated` and `cas` for waiting and logging.
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000499
500 Returns:
501 The subprocess object
502 """
Ye Kuang3c40e9f2020-07-28 13:15:25 +0000503 cmd_str = ' '.join(cmd)
Ye Kuangc1d800f2020-07-28 10:14:55 +0000504 try:
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000505 env = os.environ.copy()
506 set_temp_dir(env, tmp_dir)
507 proc = subprocess42.Popen(cmd, env=env)
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000508
Ye Kuangc1d800f2020-07-28 10:14:55 +0000509 exceeded_max_timeout = True
510 check_period_sec = 30
511 max_checks = 100
512 # max timeout = max_checks * check_period_sec = 50 minutes
513 for i in range(max_checks):
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000514 # This is to prevent I/O timeout error during setup.
Ye Kuangc1d800f2020-07-28 10:14:55 +0000515 try:
516 retcode = proc.wait(check_period_sec)
517 if retcode != 0:
Takuto Ikuta27f4b2f2021-04-26 07:18:55 +0000518 raise subprocess42.CalledProcessError(retcode, cmd=cmd_str)
Ye Kuangc1d800f2020-07-28 10:14:55 +0000519 exceeded_max_timeout = False
520 break
521 except subprocess42.TimeoutExpired:
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000522 print('still running (after %d seconds)' % ((i + 1) * check_period_sec))
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000523
Ye Kuangc1d800f2020-07-28 10:14:55 +0000524 if exceeded_max_timeout:
525 proc.terminate()
526 try:
527 proc.wait(check_period_sec)
528 except subprocess42.TimeoutExpired:
529 logging.exception(
530 "failed to terminate? timeout happened after %d seconds",
531 check_period_sec)
532 proc.kill()
533 proc.wait()
534 # Raise unconditionally, because |proc| was forcefully terminated.
535 raise ValueError("timedout after %d seconds (cmd=%s)" %
536 (check_period_sec * max_checks, cmd_str))
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000537
Ye Kuangc1d800f2020-07-28 10:14:55 +0000538 return proc
539 except Exception:
540 logging.exception('Failed to run Go cmd %s', cmd_str)
541 raise
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000542
543
Takuto Ikutacd68ef52021-11-18 04:11:45 +0000544def _fetch_and_map(cas_client, digest, instance, output_dir, cache_dir,
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +0000545 policies, kvs_dir, tmp_dir):
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000546 """
547 Fetches a CAS tree using cas client, create the tree and returns download
548 stats.
549 """
550
551 start = time.time()
552 result_json_handle, result_json_path = tempfile.mkstemp(
Junji Watanabe53d31882022-01-13 07:58:00 +0000553 prefix='fetch-and-map-result-', suffix='.json')
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000554 os.close(result_json_handle)
Takuto Ikutad5749ac2021-04-07 06:16:19 +0000555 profile_dir = tempfile.mkdtemp(dir=tmp_dir)
556
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000557 try:
558 cmd = [
559 cas_client,
560 'download',
561 '-digest',
562 digest,
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000563 # flags for cache.
564 '-cache-dir',
565 cache_dir,
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000566 '-cache-max-size',
567 str(policies.max_cache_size),
568 '-cache-min-free-space',
569 str(policies.min_free_space),
570 # flags for output.
571 '-dir',
572 output_dir,
Justin Luong54fa9592022-08-11 03:44:40 +0000573 '-dump-json',
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000574 result_json_path,
Takuto Ikuta557025b2021-02-01 08:37:40 +0000575 '-log-level',
Takuto Ikutad5749ac2021-04-07 06:16:19 +0000576 'info',
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000577 ]
Takuto Ikutaae391c52020-12-03 08:43:45 +0000578
Junji Watanabe66d807b2021-11-08 03:20:10 +0000579 # When RUN_ISOLATED_CAS_ADDRESS is set in test mode,
580 # Use it and ignore CAS instance option.
581 cas_addr = os.environ.get('RUN_ISOLATED_CAS_ADDRESS')
582 if cas_addr:
583 cmd.extend([
584 '-cas-addr',
585 cas_addr,
586 ])
587 else:
588 cmd.extend([
589 '-cas-instance',
590 instance
591 ])
592
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +0000593 if kvs_dir:
594 cmd.extend(['-kvs-dir', kvs_dir])
Takuto Ikutaae391c52020-12-03 08:43:45 +0000595
Justin Luong54fa9592022-08-11 03:44:40 +0000596 def open_json_and_check(result_json_path, cleanup_dirs):
597 cas_error = False
598 result_json = {}
599 try:
600 with open(result_json_path) as json_file:
601 result_json = json.load(json_file)
602 cas_error = result_json.get('result') in ('digest_invalid',
603 'authentication_error',
604 'arguments_invalid')
605 except (IOError, ValueError):
606 logging.error('Failed to read json file: %s', result_json_path)
607 raise
608 finally:
609 if cleanup_dirs:
610 file_path.rmtree(kvs_dir)
611 file_path.rmtree(output_dir)
612 if cas_error:
Justin Luong97eda6f2022-08-23 01:29:16 +0000613 raise errors.NonRecoverableCasException(result_json['result'], digest,
614 instance)
Justin Luong54fa9592022-08-11 03:44:40 +0000615 return result_json
616
Takuto Ikuta27f4b2f2021-04-26 07:18:55 +0000617 try:
618 _run_go_cmd_and_wait(cmd, tmp_dir)
Takuto Ikuta0909eae2021-04-27 02:54:07 +0000619 except subprocess42.CalledProcessError as ex:
Takuto Ikuta27f4b2f2021-04-26 07:18:55 +0000620 if not kvs_dir:
Justin Luong54fa9592022-08-11 03:44:40 +0000621 open_json_and_check(result_json_path, False)
Takuto Ikuta27f4b2f2021-04-26 07:18:55 +0000622 raise
Justin Luong54fa9592022-08-11 03:44:40 +0000623 open_json_and_check(result_json_path, True)
Takuto Ikuta27f4b2f2021-04-26 07:18:55 +0000624 logging.exception('Failed to run cas, removing kvs cache dir and retry.')
Takuto Ikuta0909eae2021-04-27 02:54:07 +0000625 on_error.report("Failed to run cas %s" % ex)
Takuto Ikuta27f4b2f2021-04-26 07:18:55 +0000626 _run_go_cmd_and_wait(cmd, tmp_dir)
627
Justin Luong54fa9592022-08-11 03:44:40 +0000628 result_json = open_json_and_check(result_json_path, False)
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000629
630 return {
631 'duration': time.time() - start,
632 'items_cold': result_json['items_cold'],
633 'items_hot': result_json['items_hot'],
634 }
635 finally:
636 fs.remove(result_json_path)
Takuto Ikutad5749ac2021-04-07 06:16:19 +0000637 file_path.rmtree(profile_dir)
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000638
639
aludwin0a8e17d2016-10-27 15:57:39 -0700640def link_outputs_to_outdir(run_dir, out_dir, outputs):
641 """Links any named outputs to out_dir so they can be uploaded.
642
643 Raises an error if the file already exists in that directory.
644 """
645 if not outputs:
646 return
Takuto Ikutae0dce462021-11-16 08:49:46 +0000647 file_path.create_directories(out_dir, outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700648 for o in outputs:
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -0400649 copy_recursively(os.path.join(run_dir, o), os.path.join(out_dir, o))
650
651
652def copy_recursively(src, dst):
653 """Efficiently copies a file or directory from src_dir to dst_dir.
654
655 `item` may be a file, directory, or a symlink to a file or directory.
656 All symlinks are replaced with their targets, so the resulting
657 directory structure in dst_dir will never have any symlinks.
658
659 To increase speed, copy_recursively hardlinks individual files into the
660 (newly created) directory structure if possible, unlike Python's
661 shutil.copytree().
662 """
663 orig_src = src
664 try:
665 # Replace symlinks with their final target.
666 while fs.islink(src):
667 res = fs.readlink(src)
Takuto Ikutaf2ad0a02021-06-24 08:38:40 +0000668 src = os.path.realpath(os.path.join(os.path.dirname(src), res))
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -0400669 # TODO(sadafm): Explicitly handle cyclic symlinks.
670
Takuto Ikutaf2ad0a02021-06-24 08:38:40 +0000671 if not fs.exists(src):
672 logging.warning('Path %s does not exist or %s is a broken symlink', src,
673 orig_src)
674 return
675
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -0400676 if fs.isfile(src):
677 file_path.link_file(dst, src, file_path.HARDLINK_WITH_FALLBACK)
678 return
679
680 if not fs.exists(dst):
681 os.makedirs(dst)
682
683 for child in fs.listdir(src):
684 copy_recursively(os.path.join(src, child), os.path.join(dst, child))
685
686 except OSError as e:
687 if e.errno == errno.ENOENT:
688 logging.warning('Path %s does not exist or %s is a broken symlink',
689 src, orig_src)
690 else:
691 logging.info("Couldn't collect output file %s: %s", src, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700692
693
Takuto Ikutacd68ef52021-11-18 04:11:45 +0000694def upload_outdir(cas_client, cas_instance, outdir, tmp_dir):
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000695 """Uploads the results in |outdir|, if there is any.
696
697 Returns:
698 tuple(root_digest, stats)
699 - root_digest: a digest of the output directory.
700 - stats: uploading stats.
701 """
Junji Watanabe15f9e042021-11-12 07:13:50 +0000702 if not fs.listdir(outdir):
703 return None, None
Junji Watanabe53d31882022-01-13 07:58:00 +0000704 digest_file_handle, digest_path = tempfile.mkstemp(prefix='cas-digest',
705 suffix='.txt')
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000706 os.close(digest_file_handle)
Junji Watanabe53d31882022-01-13 07:58:00 +0000707 stats_json_handle, stats_json_path = tempfile.mkstemp(prefix='upload-stats',
708 suffix='.json')
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000709 os.close(stats_json_handle)
710
711 try:
712 cmd = [
713 cas_client,
714 'archive',
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000715 '-paths',
716 # Format: <working directory>:<relative path to dir>
717 outdir + ':',
718 # output
719 '-dump-digest',
720 digest_path,
Justin Luong54fa9592022-08-11 03:44:40 +0000721 '-dump-json',
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000722 stats_json_path,
723 ]
724
Junji Watanabe66d807b2021-11-08 03:20:10 +0000725 # When RUN_ISOLATED_CAS_ADDRESS is set in test mode,
726 # Use it and ignore CAS instance option.
727 cas_addr = os.environ.get('RUN_ISOLATED_CAS_ADDRESS')
728 if cas_addr:
729 cmd.extend([
730 '-cas-addr',
731 cas_addr,
732 ])
733 else:
734 cmd.extend([
735 '-cas-instance',
736 cas_instance
737 ])
738
Takuto Ikuta23388f52022-02-01 01:39:00 +0000739 if sys.platform == 'linux':
Takuto Ikutabfcef252021-08-25 07:46:19 +0000740 # TODO(crbug.com/1243194): remove this after investigation.
741 cmd.extend(['-log-level', 'debug'])
742
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000743 start = time.time()
744
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000745 _run_go_cmd_and_wait(cmd, tmp_dir)
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000746
747 with open(digest_path) as digest_file:
748 digest = digest_file.read()
Junji Watanabec208b302020-09-25 09:18:27 +0000749 h, s = digest.split('/')
750 cas_output_root = {
751 'cas_instance': cas_instance,
752 'digest': {
753 'hash': h,
754 'size_bytes': int(s)
755 }
756 }
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000757 with open(stats_json_path) as stats_file:
758 stats = json.load(stats_file)
759
760 stats['duration'] = time.time() - start
761
Junji Watanabec208b302020-09-25 09:18:27 +0000762 return cas_output_root, stats
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000763 finally:
764 fs.remove(digest_path)
765 fs.remove(stats_json_path)
766
767
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500768def map_and_run(data, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700769 """Runs a command with optional isolated input/output.
770
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500771 Arguments:
772 - data: TaskData instance.
773 - constant_run_path: TODO
nodir55be77b2016-05-03 09:39:57 -0700774
775 Returns metadata about the result.
776 """
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +0000777
Takuto Ikutaa71c6562021-11-18 06:07:55 +0000778 # TODO(tikuta): take stats from state.json in this case too.
779 download_stats = {
780 # 'duration': 0.,
781 # 'initial_number_items': len(data.cas_cache),
782 # 'initial_size': data.cas_cache.total_size,
783 # 'items_cold': '<large.pack()>',
784 # 'items_hot': '<large.pack()>',
785 }
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +0000786
maruela9cfd6f2015-09-15 11:03:15 -0700787 result = {
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000788 'duration': None,
789 'exit_code': None,
790 'had_hard_timeout': False,
791 'internal_failure': 'run_isolated did not complete properly',
792 'stats': {
Junji Watanabeaee69ad2021-04-28 03:17:34 +0000793 'trim_caches': {
794 'duration': 0,
795 },
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000796 #'cipd': {
797 # 'duration': 0.,
798 # 'get_client_duration': 0.,
799 #},
800 'isolated': {
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +0000801 'download': download_stats,
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000802 #'upload': {
803 # 'duration': 0.,
804 # 'items_cold': '<large.pack()>',
805 # 'items_hot': '<large.pack()>',
806 #},
807 },
Junji Watanabeaee69ad2021-04-28 03:17:34 +0000808 'named_caches': {
809 'install': {
810 'duration': 0,
811 },
812 'uninstall': {
813 'duration': 0,
814 },
815 },
816 'cleanup': {
817 'duration': 0,
818 }
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000819 },
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000820 #'cipd_pins': {
821 # 'packages': [
822 # {'package_name': ..., 'version': ..., 'path': ...},
823 # ...
824 # ],
825 # 'client_package': {'package_name': ..., 'version': ...},
826 #},
827 'outputs_ref': None,
Junji Watanabe54925c32020-09-08 00:56:18 +0000828 'cas_output_root': None,
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000829 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700830 }
nodirbe642ff2016-06-09 15:51:51 -0700831
Takuto Ikutad46ea762020-10-07 05:43:22 +0000832 assert os.path.isabs(data.root_dir), ("data.root_dir is not abs path: %s" %
833 data.root_dir)
834 file_path.ensure_tree(data.root_dir, 0o700)
835
maruele2f2cb82016-07-13 14:41:03 -0700836 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700837 # TODO(maruel): This is not obvious. Change this to become an error once we
838 # make the constant_run_path an exposed flag.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500839 if constant_run_path and data.root_dir:
840 run_dir = os.path.join(data.root_dir, ISOLATED_RUN_DIR)
maruel5c4eed82017-05-26 05:33:40 -0700841 if os.path.isdir(run_dir):
842 file_path.rmtree(run_dir)
Lei Leife202df2019-06-11 17:33:34 +0000843 os.mkdir(run_dir, 0o700)
maruelcffa0542017-04-07 08:39:20 -0700844 else:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500845 run_dir = make_temp_dir(ISOLATED_RUN_DIR, data.root_dir)
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000846
maruel03e11842016-07-14 10:50:16 -0700847 # storage should be normally set but don't crash if it is not. This can happen
848 # as Swarming task can run without an isolate server.
Takuto Ikuta417388f2021-11-18 07:39:52 +0000849 out_dir = make_temp_dir(ISOLATED_OUT_DIR, data.root_dir)
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500850 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, data.root_dir)
nodir55be77b2016-05-03 09:39:57 -0700851 cwd = run_dir
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500852 if data.relative_cwd:
853 cwd = os.path.normpath(os.path.join(cwd, data.relative_cwd))
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500854 command = data.command
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000855
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000856 cas_client_dir = make_temp_dir(_CAS_CLIENT_DIR, data.root_dir)
Takuto Ikuta417388f2021-11-18 07:39:52 +0000857 cas_client = os.path.join(cas_client_dir, 'cas' + cipd.EXECUTABLE_SUFFIX)
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000858
Junji Watanabeaee69ad2021-04-28 03:17:34 +0000859 data.trim_caches_fn(result['stats']['trim_caches'])
860
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +0000861 nsjail_dir = None
862 if (sys.platform == "linux" and cipd.get_platform() == "amd64" and
863 data.containment.containment_type == subprocess42.Containment.NSJAIL):
864 nsjail_dir = make_temp_dir(_NSJAIL_DIR, data.root_dir)
865
nodir55be77b2016-05-03 09:39:57 -0700866 try:
Takuto Ikuta1ce61362021-11-16 05:44:17 +0000867 with data.install_packages_fn(run_dir, cas_client_dir,
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +0000868 nsjail_dir) as cipd_info:
vadimsh232f5a82017-01-20 19:23:44 -0800869 if cipd_info:
870 result['stats']['cipd'] = cipd_info.stats
871 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -0700872
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000873 isolated_stats = result['stats'].setdefault('isolated', {})
Takuto Ikutab58dbd12020-06-05 09:29:14 +0000874
Takuto Ikutacd68ef52021-11-18 04:11:45 +0000875 if data.cas_digest:
876 stats = _fetch_and_map(
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000877 cas_client=cas_client,
878 digest=data.cas_digest,
879 instance=data.cas_instance,
880 output_dir=run_dir,
Junji Watanabeb03450b2020-09-25 05:09:27 +0000881 cache_dir=data.cas_cache_dir,
Takuto Ikutaae391c52020-12-03 08:43:45 +0000882 policies=data.cas_cache_policies,
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +0000883 kvs_dir=data.cas_kvs,
Takuto Ikuta0f8a19c2021-03-02 00:50:38 +0000884 tmp_dir=tmp_dir)
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000885 isolated_stats['download'].update(stats)
Junji Watanabe54925c32020-09-08 00:56:18 +0000886
maruelabec63c2017-04-26 11:53:24 -0700887 if not command:
888 # Handle this as a task failure, not an internal failure.
889 sys.stderr.write(
890 '<No command was specified!>\n'
891 '<Please secify a command when triggering your Swarming task>\n')
892 result['exit_code'] = 1
893 return result
nodirbe642ff2016-06-09 15:51:51 -0700894
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500895 if not cwd.startswith(run_dir):
896 # Handle this as a task failure, not an internal failure. This is a
897 # 'last chance' way to gate against directory escape.
898 sys.stderr.write('<Relative CWD is outside of run directory!>\n')
899 result['exit_code'] = 1
900 return result
901
902 if not os.path.isdir(cwd):
903 # Accepts relative_cwd that does not exist.
Lei Leife202df2019-06-11 17:33:34 +0000904 os.makedirs(cwd, 0o700)
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -0500905
vadimsh232f5a82017-01-20 19:23:44 -0800906 # If we have an explicit list of files to return, make sure their
907 # directories exist now.
Takuto Ikutaab8d0232021-11-16 12:12:09 +0000908 if data.outputs:
Takuto Ikutae0dce462021-11-16 08:49:46 +0000909 file_path.create_directories(run_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -0700910
Junji Watanabeaee69ad2021-04-28 03:17:34 +0000911 with data.install_named_caches(run_dir, result['stats']['named_caches']):
nodird6160682017-02-02 13:03:35 -0800912 sys.stdout.flush()
913 start = time.time()
914 try:
vadimsh9c54b2c2017-07-25 14:08:29 -0700915 # Need to switch the default account before 'get_command_env' call,
916 # so it can grab correct value of LUCI_CONTEXT env var.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500917 with set_luci_context_account(data.switch_to_account, tmp_dir):
918 env = get_command_env(
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000919 tmp_dir, cipd_info, run_dir, data.env, data.env_prefix, out_dir,
920 data.bot_file)
Brian Sheedy7a761172019-08-30 22:55:14 +0000921 command = tools.find_executable(command, env)
Robert Iannucci24ae76a2018-02-26 12:51:18 -0800922 command = process_command(command, out_dir, data.bot_file)
923 file_path.ensure_command_has_abs_path(command, cwd)
924
vadimsh9c54b2c2017-07-25 14:08:29 -0700925 result['exit_code'], result['had_hard_timeout'] = run_command(
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000926 command, cwd, env, data.hard_timeout, data.grace_period,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000927 data.lower_priority, data.containment)
nodird6160682017-02-02 13:03:35 -0800928 finally:
929 result['duration'] = max(time.time() - start, 0)
Seth Koehler49139812017-12-19 13:59:33 -0500930
Takuto Ikuta417388f2021-11-18 07:39:52 +0000931 # Try to link files to the output directory, if specified.
932 link_outputs_to_outdir(run_dir, out_dir, data.outputs)
933 isolated_stats = result['stats'].setdefault('isolated', {})
934 result['cas_output_root'], upload_stats = upload_outdir(
935 cas_client, data.cas_instance, out_dir, tmp_dir)
936 if upload_stats:
937 isolated_stats['upload'] = upload_stats
Takuto Ikutacd68ef52021-11-18 04:11:45 +0000938
Seth Koehler49139812017-12-19 13:59:33 -0500939 # We successfully ran the command, set internal_failure back to
940 # None (even if the command failed, it's not an internal error).
941 result['internal_failure'] = None
Justin Luong97eda6f2022-08-23 01:29:16 +0000942 except errors.NonRecoverableCasException as e:
Justin Luong54fa9592022-08-11 03:44:40 +0000943 # We could not find the CAS package. The swarming task should not
944 # be retried automatically
945 result['missing_cas'] = e.to_dict()
946 logging.exception('internal failure: %s', e)
947 result['internal_failure'] = str(e)
948 on_error.report(None)
949
Justin Luong97eda6f2022-08-23 01:29:16 +0000950 except errors.NonRecoverableCipdException as e:
Justin Luong54fa9592022-08-11 03:44:40 +0000951 # We could not find the CIPD package. The swarming task should not
952 # be retried automatically
953 result['missing_cipd'] = [e.to_dict()]
954 logging.exception('internal failure: %s', e)
955 result['internal_failure'] = str(e)
956 on_error.report(None)
maruela9cfd6f2015-09-15 11:03:15 -0700957 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700958 # An internal error occurred. Report accordingly so the swarming task will
959 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700960 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700961 result['internal_failure'] = str(e)
962 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -0700963
964 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -0700965 finally:
966 try:
Junji Watanabeaee69ad2021-04-28 03:17:34 +0000967 cleanup_start = time.time()
Ye Kuangbc4e8402020-07-29 09:54:30 +0000968 success = True
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500969 if data.leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -0700970 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700971 logging.warning(
972 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700973 else:
maruel84537cb2015-10-16 14:21:28 -0700974 # On Windows rmtree(run_dir) call above has a synchronization effect: it
975 # finishes only when all task child processes terminate (since a running
976 # process locks *.exe file). Examine out_dir only after that call
977 # completes (since child processes may write to out_dir too and we need
978 # to wait for them to finish).
Takuto Ikuta1ce61362021-11-16 05:44:17 +0000979 dirs_to_remove = [run_dir, tmp_dir, cas_client_dir]
Ye Kuangbc4e8402020-07-29 09:54:30 +0000980 if out_dir:
981 dirs_to_remove.append(out_dir)
982 for directory in dirs_to_remove:
Takuto Ikuta69c0d662019-11-27 01:18:08 +0000983 if not fs.isdir(directory):
984 continue
Junji Watanabe9cdfff52021-01-08 07:20:35 +0000985 start = time.time()
maruel84537cb2015-10-16 14:21:28 -0700986 try:
Junji Watanabecc4eefd2021-01-19 01:46:10 +0000987 file_path.rmtree(directory)
maruel84537cb2015-10-16 14:21:28 -0700988 except OSError as e:
Takuto Ikuta69c0d662019-11-27 01:18:08 +0000989 logging.error('rmtree(%r) failed: %s', directory, e)
maruel84537cb2015-10-16 14:21:28 -0700990 success = False
Junji Watanabe9cdfff52021-01-08 07:20:35 +0000991 finally:
992 logging.info('Cleanup: rmtree(%r) took %d seconds', directory,
993 time.time() - start)
maruel84537cb2015-10-16 14:21:28 -0700994 if not success:
Takuto Ikuta69c0d662019-11-27 01:18:08 +0000995 sys.stderr.write(
996 OUTLIVING_ZOMBIE_MSG % (directory, data.grace_period))
Junji Watanabed952bf12021-05-13 03:15:54 +0000997 if sys.platform == 'win32':
998 subprocess42.check_call(['tasklist.exe', '/V'], stdout=sys.stderr)
999 else:
1000 subprocess42.check_call(['ps', 'axu'], stdout=sys.stderr)
maruel84537cb2015-10-16 14:21:28 -07001001 if result['exit_code'] == 0:
1002 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -07001003
maruela9cfd6f2015-09-15 11:03:15 -07001004 if not success and result['exit_code'] == 0:
1005 result['exit_code'] = 1
1006 except Exception as e:
1007 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -07001008 if out_dir:
1009 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -07001010 result['internal_failure'] = str(e)
Takuto Ikutaa9a907b2020-04-17 08:50:50 +00001011 on_error.report(None)
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001012 finally:
1013 cleanup_duration = time.time() - cleanup_start
1014 result['stats']['cleanup']['duration'] = cleanup_duration
1015 logging.info('Cleanup: removing directories took %d seconds',
1016 cleanup_duration)
maruela9cfd6f2015-09-15 11:03:15 -07001017 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -05001018
1019
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001020def run_tha_test(data, result_json):
nodir55be77b2016-05-03 09:39:57 -07001021 """Runs an executable and records execution metadata.
1022
nodir55be77b2016-05-03 09:39:57 -07001023 If isolated_hash is specified, downloads the dependencies in the cache,
1024 hardlinks them into a temporary directory and runs the command specified in
1025 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -05001026
1027 A temporary directory is created to hold the output files. The content inside
1028 this directory will be uploaded back to |storage| packaged as a .isolated
1029 file.
1030
1031 Arguments:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001032 - data: TaskData instance.
1033 - result_json: File path to dump result metadata into. If set, the process
1034 exit code is always 0 unless an internal error occurred.
maruela9cfd6f2015-09-15 11:03:15 -07001035
1036 Returns:
1037 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001038 """
maruela76b9ee2015-12-15 06:18:08 -08001039 if result_json:
1040 # Write a json output file right away in case we get killed.
1041 result = {
Junji Watanabe54925c32020-09-08 00:56:18 +00001042 'exit_code': None,
1043 'had_hard_timeout': False,
1044 'internal_failure': 'Was terminated before completion',
1045 'outputs_ref': None,
1046 'cas_output_root': None,
1047 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -08001048 }
1049 tools.write_json(result_json, result, dense=True)
1050
maruela9cfd6f2015-09-15 11:03:15 -07001051 # run_isolated exit code. Depends on if result_json is used or not.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001052 result = map_and_run(data, True)
maruela9cfd6f2015-09-15 11:03:15 -07001053 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -07001054
maruela9cfd6f2015-09-15 11:03:15 -07001055 if result_json:
maruel05d5a882015-09-21 13:59:02 -07001056 # We've found tests to delete 'work' when quitting, causing an exception
1057 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -07001058 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -07001059 tools.write_json(result_json, result, dense=True)
1060 # Only return 1 if there was an internal error.
1061 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +00001062
maruela9cfd6f2015-09-15 11:03:15 -07001063 # Marshall into old-style inline output.
1064 if result['outputs_ref']:
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001065 # pylint: disable=unsubscriptable-object
maruela9cfd6f2015-09-15 11:03:15 -07001066 data = {
Junji Watanabe38b28b02020-04-23 10:23:30 +00001067 'hash': result['outputs_ref']['isolated'],
1068 'namespace': result['outputs_ref']['namespace'],
1069 'storage': result['outputs_ref']['isolatedserver'],
maruela9cfd6f2015-09-15 11:03:15 -07001070 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -05001071 sys.stdout.flush()
Junji Watanabe38b28b02020-04-23 10:23:30 +00001072 print('[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
1073 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -08001074 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -07001075 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001076
1077
iannuccib58d10d2017-03-18 02:00:25 -07001078# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -08001079CipdInfo = collections.namedtuple('CipdInfo', [
1080 'client', # cipd.CipdClient object
1081 'cache_dir', # absolute path to bot-global cipd tag and instance cache
1082 'stats', # dict with stats to return to the server
1083 'pins', # dict with installed cipd pins to return to the server
1084])
1085
1086
1087@contextlib.contextmanager
Takuto Ikuta1ce61362021-11-16 05:44:17 +00001088def copy_local_packages(_run_dir, cas_dir, _nsjail_dir):
Junji Watanabedc2f89e2021-11-08 08:44:30 +00001089 """Copies CIPD packages from luci/luci-go dir."""
1090 go_client_dir = os.environ.get('LUCI_GO_CLIENT_DIR')
1091 assert go_client_dir, ('Please set LUCI_GO_CLIENT_DIR env var to install CIPD'
1092 ' packages locally.')
1093 shutil.copy2(os.path.join(go_client_dir, 'cas' + cipd.EXECUTABLE_SUFFIX),
1094 os.path.join(cas_dir, 'cas' + cipd.EXECUTABLE_SUFFIX))
vadimsh232f5a82017-01-20 19:23:44 -08001095 yield None
1096
1097
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001098def _install_packages(run_dir, cipd_cache_dir, client, packages):
iannuccib58d10d2017-03-18 02:00:25 -07001099 """Calls 'cipd ensure' for packages.
1100
1101 Args:
1102 run_dir (str): root of installation.
1103 cipd_cache_dir (str): the directory to use for the cipd package cache.
1104 client (CipdClient): the cipd client to use
1105 packages: packages to install, list [(path, package_name, version), ...].
iannuccib58d10d2017-03-18 02:00:25 -07001106
1107 Returns: list of pinned packages. Looks like [
1108 {
1109 'path': 'subdirectory',
1110 'package_name': 'resolved/package/name',
1111 'version': 'deadbeef...',
1112 },
1113 ...
1114 ]
1115 """
1116 package_pins = [None]*len(packages)
1117 def insert_pin(path, name, version, idx):
1118 package_pins[idx] = {
1119 'package_name': name,
1120 # swarming deals with 'root' as '.'
1121 'path': path or '.',
1122 'version': version,
1123 }
1124
1125 by_path = collections.defaultdict(list)
1126 for i, (path, name, version) in enumerate(packages):
1127 # cipd deals with 'root' as ''
1128 if path == '.':
1129 path = ''
1130 by_path[path].append((name, version, i))
1131
1132 pins = client.ensure(
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001133 run_dir,
1134 {
1135 subdir: [(name, vers) for name, vers, _ in pkgs
1136 ] for subdir, pkgs in by_path.items()
1137 },
1138 cache_dir=cipd_cache_dir,
iannuccib58d10d2017-03-18 02:00:25 -07001139 )
1140
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001141 for subdir, pin_list in sorted(pins.items()):
iannuccib58d10d2017-03-18 02:00:25 -07001142 this_subdir = by_path[subdir]
1143 for i, (name, version) in enumerate(pin_list):
1144 insert_pin(subdir, name, version, this_subdir[i][2])
1145
Robert Iannucci461b30d2017-12-13 11:34:03 -08001146 assert None not in package_pins, (packages, pins, package_pins)
iannuccib58d10d2017-03-18 02:00:25 -07001147
1148 return package_pins
1149
1150
vadimsh232f5a82017-01-20 19:23:44 -08001151@contextlib.contextmanager
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001152def install_client_and_packages(run_dir, packages, service_url,
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001153 client_package_name, client_version, cache_dir,
Takuto Ikuta1ce61362021-11-16 05:44:17 +00001154 cas_dir, nsjail_dir):
vadimsh902948e2017-01-20 15:57:32 -08001155 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -07001156
vadimsh232f5a82017-01-20 19:23:44 -08001157 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
1158
1159 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -07001160 [
1161 {
1162 "path": path, "package_name": package_name, "version": version,
1163 },
1164 ...
1165 ]
vadimsh902948e2017-01-20 15:57:32 -08001166 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -07001167
1168 such that they correspond 1:1 to all input package arguments from the command
1169 line. These dictionaries make their all the way back to swarming, where they
1170 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -07001171
vadimsh902948e2017-01-20 15:57:32 -08001172 If 'packages' list is empty, will bootstrap CIPD client, but won't install
1173 any packages.
1174
1175 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -08001176 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -08001177
nodirbe642ff2016-06-09 15:51:51 -07001178 Args:
nodir90bc8dc2016-06-15 13:35:21 -07001179 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -08001180 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -07001181 service_url (str): CIPD server url, e.g.
1182 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -07001183 client_package_name (str): CIPD package name of CIPD client.
1184 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -07001185 cache_dir (str): where to keep cache of cipd clients, packages and tags.
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001186 cas_dir (str): where to download cas client.
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +00001187 nsjail_dir (str): where to download nsjail. If set to None, nsjail is not
1188 downloaded.
nodirbe642ff2016-06-09 15:51:51 -07001189 """
1190 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -07001191
nodirbe642ff2016-06-09 15:51:51 -07001192 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -07001193
vadimsh902948e2017-01-20 15:57:32 -08001194 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -08001195 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -07001196 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -08001197 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -07001198
nodirbe642ff2016-06-09 15:51:51 -07001199 get_client_start = time.time()
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001200 client_manager = cipd.get_client(cache_dir, service_url, client_package_name,
1201 client_version)
iannucci96fcccc2016-08-30 15:52:22 -07001202
nodirbe642ff2016-06-09 15:51:51 -07001203 with client_manager as client:
1204 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -07001205
iannuccib58d10d2017-03-18 02:00:25 -07001206 package_pins = []
1207 if packages:
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001208 package_pins = _install_packages(run_dir, cipd_cache_dir, client,
1209 packages)
iannuccib58d10d2017-03-18 02:00:25 -07001210
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001211 # Install cas client to |cas_dir|.
1212 _install_packages(cas_dir, cipd_cache_dir, client,
Takuto Ikuta9c4eb1d2020-10-05 03:40:14 +00001213 [('', _CAS_PACKAGE, _LUCI_GO_REVISION)])
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001214
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +00001215 # Install nsjail to |nsjail_dir|.
1216 if nsjail_dir is not None:
1217 _install_packages(nsjail_dir, cipd_cache_dir, client,
1218 [('', _NSJAIL_PACKAGE, _NSJAIL_VERSION)])
1219
iannuccib58d10d2017-03-18 02:00:25 -07001220 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -07001221
vadimsh232f5a82017-01-20 19:23:44 -08001222 total_duration = time.time() - start
Junji Watanabe38b28b02020-04-23 10:23:30 +00001223 logging.info('Installing CIPD client and packages took %d seconds',
1224 total_duration)
nodir90bc8dc2016-06-15 13:35:21 -07001225
vadimsh232f5a82017-01-20 19:23:44 -08001226 yield CipdInfo(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001227 client=client,
1228 cache_dir=cipd_cache_dir,
1229 stats={
1230 'duration': total_duration,
1231 'get_client_duration': get_client_duration,
iannuccib58d10d2017-03-18 02:00:25 -07001232 },
Junji Watanabe38b28b02020-04-23 10:23:30 +00001233 pins={
1234 'client_package': {
1235 'package_name': client.package_name,
1236 'version': client.instance_id,
1237 },
1238 'packages': package_pins,
1239 })
nodirbe642ff2016-06-09 15:51:51 -07001240
1241
1242def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001243 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -07001244 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +00001245 version=__version__,
1246 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -07001247 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001248 '--clean',
1249 action='store_true',
maruel36a963d2016-04-08 17:15:49 -07001250 help='Cleans the cache, trimming it necessary and remove corrupted items '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001251 'and returns without executing anything; use with -v to know what '
1252 'was done')
maruel36a963d2016-04-08 17:15:49 -07001253 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -07001254 '--json',
1255 help='dump output metadata to json file. When used, run_isolated returns '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001256 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -07001257 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001258 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -07001259 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001260 '--grace-period',
1261 type='float',
maruel6be7f9e2015-10-01 12:25:30 -07001262 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -07001263 parser.add_option(
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001264 '--relative-cwd',
Takuto Ikuta18ca29a2020-12-04 07:34:20 +00001265 help='Ignore the isolated \'relative_cwd\' and use this one instead')
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001266 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001267 '--env',
1268 default=[],
1269 action='append',
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001270 help='Environment variables to set for the child process')
1271 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001272 '--env-prefix',
1273 default=[],
1274 action='append',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001275 help='Specify a VAR=./path/fragment to put in the environment variable '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001276 'before executing the command. The path fragment must be relative '
1277 'to the isolated run directory, and must not contain a `..` token. '
1278 'The path will be made absolute and prepended to the indicated '
1279 '$VAR using the OS\'s path separator. Multiple items for the same '
1280 '$VAR will be prepended in order.')
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001281 parser.add_option(
bpastene3ae09522016-06-10 17:12:59 -07001282 '--bot-file',
1283 help='Path to a file describing the state of the host. The content is '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001284 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -07001285 parser.add_option(
vadimsh9c54b2c2017-07-25 14:08:29 -07001286 '--switch-to-account',
1287 help='If given, switches LUCI_CONTEXT to given logical service account '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001288 '(e.g. "task" or "system") before launching the isolated process.')
vadimsh9c54b2c2017-07-25 14:08:29 -07001289 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001290 '--output',
1291 action='append',
aludwin0a8e17d2016-10-27 15:57:39 -07001292 help='Specifies an output to return. If no outputs are specified, all '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001293 'files located in $(ISOLATED_OUTDIR) will be returned; '
1294 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
1295 'specified by --output option (there can be multiple) will be '
1296 'returned. Note that if a file in OUT_DIR has the same path '
1297 'as an --output option, the --output version will be returned.')
aludwin0a8e17d2016-10-27 15:57:39 -07001298 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001299 '-a',
1300 '--argsfile',
aludwin7556e0c2016-10-26 08:46:10 -07001301 # This is actually handled in parse_args; it's included here purely so it
1302 # can make it into the help text.
1303 help='Specify a file containing a JSON array of arguments to this '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001304 'script. If --argsfile is provided, no other argument may be '
1305 'provided on the command line.')
Takuto Ikutad4be2f12020-05-12 02:15:25 +00001306 parser.add_option(
1307 '--report-on-exception',
1308 action='store_true',
1309 help='Whether report exception during execution to isolate server. '
1310 'This flag should only be used in swarming bot.')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001311
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001312 group = optparse.OptionGroup(parser,
1313 'Data source - Content Addressed Storage')
Junji Watanabe54925c32020-09-08 00:56:18 +00001314 group.add_option(
1315 '--cas-instance', help='Full CAS instance name for input/output files.')
1316 group.add_option(
1317 '--cas-digest',
1318 help='Digest of the input root on RBE-CAS. The format is '
1319 '`{hash}/{size_bytes}`.')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001320 parser.add_option_group(group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001321
Junji Watanabeb03450b2020-09-25 05:09:27 +00001322 # Cache options.
Junji Watanabeb03450b2020-09-25 05:09:27 +00001323 add_cas_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001324
1325 cipd.add_cipd_options(parser)
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001326
1327 group = optparse.OptionGroup(parser, 'Named caches')
1328 group.add_option(
1329 '--named-cache',
1330 dest='named_caches',
1331 action='append',
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001332 nargs=3,
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001333 default=[],
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001334 help='A named cache to request. Accepts 3 arguments: name, path, hint. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001335 'name identifies the cache, must match regex [a-z0-9_]{1,4096}. '
1336 'path is a path relative to the run dir where the cache directory '
1337 'must be put to. '
1338 'This option can be specified more than once.')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001339 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001340 '--named-cache-root',
1341 default='named_caches',
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001342 help='Cache root directory. Default=%default')
1343 parser.add_option_group(group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001344
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001345 group = optparse.OptionGroup(parser, 'Process containment')
1346 parser.add_option(
1347 '--lower-priority', action='store_true',
1348 help='Lowers the child process priority')
1349 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001350 '--containment-type',
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +00001351 choices=('NONE', 'AUTO', 'JOB_OBJECT', 'NSJAIL'),
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001352 default='NONE',
1353 help='Type of container to use')
1354 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001355 '--limit-processes',
1356 type='int',
1357 default=0,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001358 help='Maximum number of active processes in the containment')
1359 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001360 '--limit-total-committed-memory',
1361 type='int',
1362 default=0,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001363 help='Maximum sum of committed memory in the containment')
1364 parser.add_option_group(group)
1365
1366 group = optparse.OptionGroup(parser, 'Debugging')
1367 group.add_option(
Kenneth Russell61d42352014-09-15 11:41:16 -07001368 '--leak-temp-dir',
1369 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -07001370 help='Deliberately leak isolate\'s temp dir for later examination. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001371 'Default: %default')
1372 group.add_option('--root-dir', help='Use a directory instead of a random one')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001373 parser.add_option_group(group)
Kenneth Russell61d42352014-09-15 11:41:16 -07001374
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001375 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001376
Ye Kuang1d096cb2020-06-26 08:38:21 +00001377 parser.set_defaults(cache='cache')
nodirbe642ff2016-06-09 15:51:51 -07001378 return parser
1379
1380
Junji Watanabeb03450b2020-09-25 05:09:27 +00001381def add_cas_cache_options(parser):
1382 group = optparse.OptionGroup(parser, 'CAS cache management')
1383 group.add_option(
1384 '--cas-cache',
1385 metavar='DIR',
1386 default='cas-cache',
1387 help='Directory to keep a local cache of the files. Accelerates download '
1388 'by reusing already downloaded files. Default=%default')
Takuto Ikuta7ff4b242020-12-03 08:07:06 +00001389 group.add_option(
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +00001390 '--kvs-dir',
Takuto Ikuta7ff4b242020-12-03 08:07:06 +00001391 default='',
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +00001392 help='CAS cache dir using kvs for small files. Default=%default')
Takuto Ikutaa71c6562021-11-18 06:07:55 +00001393 group.add_option(
1394 '--max-cache-size',
1395 type='int',
1396 metavar='NNN',
1397 default=50 * 1024 * 1024 * 1024,
1398 help='Trim if the cache gets larger than this value, default=%default')
1399 group.add_option(
1400 '--min-free-space',
1401 type='int',
1402 metavar='NNN',
1403 default=2 * 1024 * 1024 * 1024,
1404 help='Trim if disk free space becomes lower than this value, '
1405 'default=%default')
Junji Watanabeb03450b2020-09-25 05:09:27 +00001406 parser.add_option_group(group)
1407
1408
1409def process_cas_cache_options(options):
1410 if options.cas_cache:
1411 policies = local_caching.CachePolicies(
1412 max_cache_size=options.max_cache_size,
1413 min_free_space=options.min_free_space,
1414 # max_items isn't used for CAS cache for now.
1415 max_items=None,
1416 max_age_secs=MAX_AGE_SECS)
1417
Junji Watanabe7a631b02022-01-13 02:30:29 +00001418 return local_caching.DiskContentAddressedCache(os.path.abspath(
1419 options.cas_cache),
1420 policies,
1421 trim=False)
Junji Watanabeb03450b2020-09-25 05:09:27 +00001422 return local_caching.MemoryContentAddressedCache()
1423
1424
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001425def process_named_cache_options(parser, options, time_fn=None):
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001426 """Validates named cache options and returns a CacheManager."""
1427 if options.named_caches and not options.named_cache_root:
1428 parser.error('--named-cache is specified, but --named-cache-root is empty')
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001429 for name, path, hint in options.named_caches:
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001430 if not CACHE_NAME_RE.match(name):
1431 parser.error(
1432 'cache name %r does not match %r' % (name, CACHE_NAME_RE.pattern))
1433 if not path:
1434 parser.error('cache path cannot be empty')
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001435 try:
Takuto Ikuta630f99d2020-07-02 12:59:35 +00001436 int(hint)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001437 except ValueError:
1438 parser.error('cache hint must be a number')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001439 if options.named_cache_root:
1440 # Make these configurable later if there is use case but for now it's fairly
1441 # safe values.
1442 # In practice, a fair chunk of bots are already recycled on a daily schedule
1443 # so this code doesn't have any effect to them, unless they are preloaded
1444 # with a really old cache.
1445 policies = local_caching.CachePolicies(
1446 # 1TiB.
1447 max_cache_size=1024*1024*1024*1024,
1448 min_free_space=options.min_free_space,
1449 max_items=50,
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +00001450 max_age_secs=MAX_AGE_SECS)
Junji Watanabe7a631b02022-01-13 02:30:29 +00001451 root_dir = os.path.abspath(options.named_cache_root)
John Budorickc6186972020-02-26 00:58:14 +00001452 cache = local_caching.NamedCache(root_dir, policies, time_fn=time_fn)
1453 # Touch any named caches we're going to use to minimize thrashing
1454 # between tasks that request some (but not all) of the same named caches.
John Budorick0a4dab62020-03-02 22:23:35 +00001455 cache.touch(*[name for name, _, _ in options.named_caches])
John Budorickc6186972020-02-26 00:58:14 +00001456 return cache
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001457 return None
1458
1459
aludwin7556e0c2016-10-26 08:46:10 -07001460def parse_args(args):
1461 # Create a fake mini-parser just to get out the "-a" command. Note that
1462 # it's not documented here; instead, it's documented in create_option_parser
1463 # even though that parser will never actually get to parse it. This is
1464 # because --argsfile is exclusive with all other options and arguments.
1465 file_argparse = argparse.ArgumentParser(add_help=False)
1466 file_argparse.add_argument('-a', '--argsfile')
1467 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
1468 if file_args.argsfile:
1469 if nonfile_args:
1470 file_argparse.error('Can\'t specify --argsfile with'
1471 'any other arguments (%s)' % nonfile_args)
1472 try:
1473 with open(file_args.argsfile, 'r') as f:
1474 args = json.load(f)
1475 except (IOError, OSError, ValueError) as e:
1476 # We don't need to error out here - "args" is now empty,
1477 # so the call below to parser.parse_args(args) will fail
1478 # and print the full help text.
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +00001479 print('Couldn\'t read arguments: %s' % e, file=sys.stderr)
aludwin7556e0c2016-10-26 08:46:10 -07001480
1481 # Even if we failed to read the args, just call the normal parser now since it
1482 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -07001483 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001484 options, args = parser.parse_args(args)
Ye Kuangfff1e502020-07-13 13:21:57 +00001485 if not isinstance(options.cipd_enabled, (bool, int)):
1486 options.cipd_enabled = distutils.util.strtobool(options.cipd_enabled)
aludwin7556e0c2016-10-26 08:46:10 -07001487 return (parser, options, args)
1488
1489
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001490def _calc_named_cache_hint(named_cache, named_caches):
1491 """Returns the expected size of the missing named caches."""
1492 present = named_cache.available
1493 size = 0
Takuto Ikutad169bfd2021-08-02 05:45:09 +00001494 logging.info('available named cache %s', present)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001495 for name, _, hint in named_caches:
1496 if name not in present:
Takuto Ikuta630f99d2020-07-02 12:59:35 +00001497 hint = int(hint)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001498 if hint > 0:
Takuto Ikuta74686842021-07-30 04:11:03 +00001499 logging.info("named cache hint: %s, %d", name, hint)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001500 size += hint
Takuto Ikuta74686842021-07-30 04:11:03 +00001501 logging.info("total size of named cache hint: %d", size)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001502 return size
1503
1504
Takuto Ikutaae391c52020-12-03 08:43:45 +00001505def _clean_cmd(parser, options, caches, root):
Takuto Ikuta7ff4b242020-12-03 08:07:06 +00001506 """Cleanup cache dirs/files."""
Takuto Ikuta7ff4b242020-12-03 08:07:06 +00001507 if options.json:
1508 parser.error('Can\'t use --json with --clean.')
1509 if options.named_caches:
1510 parser.error('Can\t use --named-cache with --clean.')
1511 if options.cas_instance or options.cas_digest:
1512 parser.error('Can\t use --cas-instance, --cas-digest with --clean.')
1513
1514 logging.info("initial free space: %d", file_path.get_free_space(root))
1515
Junji Watanabe7a631b02022-01-13 02:30:29 +00001516 if options.kvs_dir and fs.isdir(options.kvs_dir):
Takuto Ikuta7ff4b242020-12-03 08:07:06 +00001517 # Remove kvs file if its size exceeds fixed threshold.
Junji Watanabe7a631b02022-01-13 02:30:29 +00001518 kvs_dir = options.kvs_dir
Takuto Ikutab1b70062021-03-22 01:02:41 +00001519 size = file_path.get_recursive_size(kvs_dir)
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +00001520 if size >= _CAS_KVS_CACHE_THRESHOLD:
1521 logging.info("remove kvs dir with size: %d", size)
Takuto Ikutab1b70062021-03-22 01:02:41 +00001522 file_path.rmtree(kvs_dir)
Takuto Ikuta7ff4b242020-12-03 08:07:06 +00001523
1524 # Trim first, then clean.
1525 local_caching.trim_caches(
1526 caches,
1527 root,
1528 min_free_space=options.min_free_space,
1529 max_age_secs=MAX_AGE_SECS)
1530 logging.info("free space after trim: %d", file_path.get_free_space(root))
1531 for c in caches:
1532 c.cleanup()
1533 logging.info("free space after cleanup: %d", file_path.get_free_space(root))
1534
1535
aludwin7556e0c2016-10-26 08:46:10 -07001536def main(args):
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -05001537 # Warning: when --argsfile is used, the strings are unicode instances, when
1538 # parsed normally, the strings are str instances.
aludwin7556e0c2016-10-26 08:46:10 -07001539 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -07001540
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +00001541 # adds another log level for logs which are directed to standard output
1542 # these logs will be uploaded to cloudstorage
1543 logging_utils.set_user_level_logging()
1544
Joanna Wang40959bf2021-08-12 18:10:12 +00001545 # Must be logged after parse_args(), which eventually calls
1546 # logging_utils.prepare_logging() which expects no logs before its call.
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +00001547 logging_utils.user_logs('Starting run_isolated script')
Joanna Wang40959bf2021-08-12 18:10:12 +00001548
Junji Watanabe1d83d282021-05-11 05:50:40 +00001549 SWARMING_SERVER = os.environ.get('SWARMING_SERVER')
1550 SWARMING_TASK_ID = os.environ.get('SWARMING_TASK_ID')
1551 if options.report_on_exception and SWARMING_SERVER:
1552 task_url = None
1553 if SWARMING_TASK_ID:
1554 task_url = '%s/task?id=%s' % (SWARMING_SERVER, SWARMING_TASK_ID)
1555 on_error.report_on_exception_exit(SWARMING_SERVER, source=task_url)
Takuto Ikutad4be2f12020-05-12 02:15:25 +00001556
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001557 if not file_path.enable_symlink():
Marc-Antoine Ruel5a024272019-01-15 20:11:16 +00001558 logging.warning('Symlink support is not enabled')
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001559
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001560 named_cache = process_named_cache_options(parser, options)
Marc-Antoine Ruel0d8b0f62018-09-10 14:40:35 +00001561 # hint is 0 if there's no named cache.
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001562 hint = _calc_named_cache_hint(named_cache, options.named_caches)
1563 if hint:
1564 # Increase the --min-free-space value by the hint, and recreate the
1565 # NamedCache instance so it gets the updated CachePolicy.
1566 options.min_free_space += hint
1567 named_cache = process_named_cache_options(parser, options)
1568
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001569 # TODO(maruel): CIPD caches should be defined at an higher level here too, so
1570 # they can be cleaned the same way.
Takuto Ikutaf1c58442020-10-20 09:03:27 +00001571
Takuto Ikutaf1c58442020-10-20 09:03:27 +00001572 cas_cache = process_cas_cache_options(options)
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +00001573
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001574 caches = []
Junji Watanabeb03450b2020-09-25 05:09:27 +00001575 if cas_cache:
1576 caches.append(cas_cache)
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001577 if named_cache:
1578 caches.append(named_cache)
Junji Watanabe7a631b02022-01-13 02:30:29 +00001579 root = caches[0].cache_dir if caches else os.getcwd()
maruel36a963d2016-04-08 17:15:49 -07001580 if options.clean:
Takuto Ikutaae391c52020-12-03 08:43:45 +00001581 _clean_cmd(parser, options, caches, root)
maruel36a963d2016-04-08 17:15:49 -07001582 return 0
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001583
1584 # Trim must still be done for the following case:
1585 # - named-cache was used
1586 # - some entries, with a large hint, where missing
1587 # - --min-free-space was increased accordingly, thus trimming is needed
1588 # Otherwise, this will have no effect, as bot_main calls run_isolated with
1589 # --clean after each task.
Takuto Ikutac9ddff22021-02-18 07:58:39 +00001590 additional_buffer = _FREE_SPACE_BUFFER_FOR_CIPD_PACKAGES
Takuto Ikuta91cb5ca2021-03-17 07:19:30 +00001591 if options.kvs_dir:
Takuto Ikuta7f45c592021-02-09 05:57:05 +00001592 additional_buffer += _CAS_KVS_CACHE_THRESHOLD
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001593 # Add some buffer for Go CLI.
1594 min_free_space = options.min_free_space + additional_buffer
1595
1596 def trim_caches_fn(stats):
1597 start = time.time()
1598 local_caching.trim_caches(
1599 caches, root, min_free_space=min_free_space, max_age_secs=MAX_AGE_SECS)
1600 duration = time.time() - start
1601 stats['duration'] = duration
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +00001602 logging_utils.user_logs('trim_caches: took %d seconds', duration)
maruel36a963d2016-04-08 17:15:49 -07001603
Takuto Ikuta1ce61362021-11-16 05:44:17 +00001604 # Save state of cas cache not to overwrite state from go client.
Takuto Ikutaf1c58442020-10-20 09:03:27 +00001605 if cas_cache:
1606 cas_cache.save()
1607 cas_cache = None
1608
Takuto Ikutadc496672021-11-12 05:58:59 +00001609 if not args:
1610 parser.error('command to run is required.')
nodir55be77b2016-05-03 09:39:57 -07001611
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001612 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001613
Takuto Ikutaa71c6562021-11-18 06:07:55 +00001614 if ISOLATED_OUTDIR_PARAMETER in args and not options.cas_instance:
1615 parser.error('%s in args requires --cas-instance' %
Junji Watanabeed9ce352020-09-25 12:32:07 +00001616 ISOLATED_OUTDIR_PARAMETER)
1617
nodir90bc8dc2016-06-15 13:35:21 -07001618 if options.root_dir:
Junji Watanabe7a631b02022-01-13 02:30:29 +00001619 options.root_dir = os.path.abspath(options.root_dir)
Takuto Ikutad46ea762020-10-07 05:43:22 +00001620 else:
Junji Watanabe7a631b02022-01-13 02:30:29 +00001621 options.root_dir = tempfile.mkdtemp(prefix='root')
maruel12e30012015-10-09 11:55:35 -07001622 if options.json:
Junji Watanabe7a631b02022-01-13 02:30:29 +00001623 options.json = os.path.abspath(options.json)
nodir55be77b2016-05-03 09:39:57 -07001624
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001625 if any('=' not in i for i in options.env):
1626 parser.error(
1627 '--env required key=value form. value can be skipped to delete '
1628 'the variable')
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001629 options.env = dict(i.split('=', 1) for i in options.env)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001630
1631 prefixes = {}
1632 cwd = os.path.realpath(os.getcwd())
1633 for item in options.env_prefix:
1634 if '=' not in item:
1635 parser.error(
1636 '--env-prefix %r is malformed, must be in the form `VAR=./path`'
1637 % item)
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001638 key, opath = item.split('=', 1)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001639 if os.path.isabs(opath):
1640 parser.error('--env-prefix %r path is bad, must be relative.' % opath)
1641 opath = os.path.normpath(opath)
1642 if not os.path.realpath(os.path.join(cwd, opath)).startswith(cwd):
1643 parser.error(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001644 '--env-prefix %r path is bad, must be relative and not contain `..`.'
1645 % opath)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001646 prefixes.setdefault(key, []).append(opath)
1647 options.env_prefix = prefixes
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001648
nodirbe642ff2016-06-09 15:51:51 -07001649 cipd.validate_cipd_options(parser, options)
1650
Junji Watanabedc2f89e2021-11-08 08:44:30 +00001651 install_packages_fn = copy_local_packages
Ye Kuang1d096cb2020-06-26 08:38:21 +00001652 tmp_cipd_cache_dir = None
vadimsh902948e2017-01-20 15:57:32 -08001653 if options.cipd_enabled:
Ye Kuang1d096cb2020-06-26 08:38:21 +00001654 cache_dir = options.cipd_cache
1655 if not cache_dir:
Junji Watanabe7a631b02022-01-13 02:30:29 +00001656 tmp_cipd_cache_dir = tempfile.mkdtemp()
Ye Kuang1d096cb2020-06-26 08:38:21 +00001657 cache_dir = tmp_cipd_cache_dir
Takuto Ikuta1ce61362021-11-16 05:44:17 +00001658 install_packages_fn = (
1659 lambda run_dir, cas_dir, nsjail_dir: install_client_and_packages(
1660 run_dir,
1661 cipd.parse_package_args(options.cipd_packages),
1662 options.cipd_server,
1663 options.cipd_client_package,
1664 options.cipd_client_version,
1665 cache_dir=cache_dir,
1666 cas_dir=cas_dir,
1667 nsjail_dir=nsjail_dir,
1668 ))
nodirbe642ff2016-06-09 15:51:51 -07001669
nodird6160682017-02-02 13:03:35 -08001670 @contextlib.contextmanager
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001671 def install_named_caches(run_dir, stats):
nodird6160682017-02-02 13:03:35 -08001672 # WARNING: this function depends on "options" variable defined in the outer
1673 # function.
Junji Watanabe7a631b02022-01-13 02:30:29 +00001674 assert str(run_dir), repr(run_dir)
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001675 assert os.path.isabs(run_dir), run_dir
Junji Watanabe7a631b02022-01-13 02:30:29 +00001676 named_caches = [(os.path.join(run_dir, str(relpath)), name)
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001677 for name, relpath, _ in options.named_caches]
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001678 install_start = time.time()
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001679 for path, name in named_caches:
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001680 named_cache.install(path, name)
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001681 install_duration = time.time() - install_start
1682 stats['install']['duration'] = install_duration
1683 logging.info('named_caches: install took %d seconds', install_duration)
nodird6160682017-02-02 13:03:35 -08001684 try:
1685 yield
1686 finally:
dnje289d132017-07-07 11:16:44 -07001687 # Uninstall each named cache, returning it to the cache pool. If an
1688 # uninstall fails for a given cache, it will remain in the task's
1689 # temporary space, get cleaned up by the Swarming bot, and be lost.
1690 #
1691 # If the Swarming bot cannot clean up the cache, it will handle it like
1692 # any other bot file that could not be removed.
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001693 uninstall_start = time.time()
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001694 for path, name in reversed(named_caches):
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001695 try:
Marc-Antoine Ruele9558372018-08-03 03:41:22 +00001696 # uninstall() doesn't trim but does call save() implicitly. Trimming
1697 # *must* be done manually via periodic 'run_isolated.py --clean'.
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001698 named_cache.uninstall(path, name)
1699 except local_caching.NamedCacheError:
Takuto Ikuta463ecdd2021-03-05 09:35:38 +00001700 if sys.platform == 'win32':
1701 # Show running processes.
1702 sys.stderr.write("running process\n")
1703 subprocess42.check_call(['tasklist.exe', '/V'], stdout=sys.stderr)
1704
Junji Watanabed2ab86b2021-08-13 07:20:23 +00001705 error = (
1706 'Error while removing named cache %r at %r. The cache will be'
1707 ' lost.' % (path, name))
1708 logging.exception(error)
1709 on_error.report(error)
Junji Watanabeaee69ad2021-04-28 03:17:34 +00001710 uninstall_duration = time.time() - uninstall_start
1711 stats['uninstall']['duration'] = uninstall_duration
1712 logging.info('named_caches: uninstall took %d seconds',
1713 uninstall_duration)
nodirf33b8d62016-10-26 22:34:58 -07001714
Takuto Ikutaf3caa9b2020-11-02 05:38:26 +00001715 command = args
1716 if options.relative_cwd:
1717 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1718 if not a.startswith(os.getcwd()):
1719 parser.error(
1720 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001721
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001722 containment_type = subprocess42.Containment.NONE
1723 if options.containment_type == 'AUTO':
1724 containment_type = subprocess42.Containment.AUTO
1725 if options.containment_type == 'JOB_OBJECT':
1726 containment_type = subprocess42.Containment.JOB_OBJECT
Anirudh Mathukumilli92d57b62021-08-04 23:21:57 +00001727 if options.containment_type == 'NSJAIL':
1728 containment_type = subprocess42.Containment.NSJAIL
1729 # TODO(https://crbug.com/1227833): This object should eventually contain the
1730 # path to the nsjail binary and the nsjail configuration file.
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001731 containment = subprocess42.Containment(
1732 containment_type=containment_type,
1733 limit_processes=options.limit_processes,
1734 limit_total_committed_memory=options.limit_total_committed_memory)
1735
Junji Watanabe7a631b02022-01-13 02:30:29 +00001736 data = TaskData(command=command,
1737 relative_cwd=options.relative_cwd,
1738 cas_instance=options.cas_instance,
1739 cas_digest=options.cas_digest,
1740 outputs=options.output,
1741 install_named_caches=install_named_caches,
1742 leak_temp_dir=options.leak_temp_dir,
1743 root_dir=options.root_dir,
1744 hard_timeout=options.hard_timeout,
1745 grace_period=options.grace_period,
1746 bot_file=options.bot_file,
1747 switch_to_account=options.switch_to_account,
1748 install_packages_fn=install_packages_fn,
1749 cas_cache_dir=options.cas_cache,
1750 cas_cache_policies=local_caching.CachePolicies(
1751 max_cache_size=options.max_cache_size,
1752 min_free_space=options.min_free_space,
1753 max_items=None,
1754 max_age_secs=None,
1755 ),
1756 cas_kvs=options.kvs_dir,
1757 env=options.env,
1758 env_prefix=options.env_prefix,
1759 lower_priority=bool(options.lower_priority),
1760 containment=containment,
1761 trim_caches_fn=trim_caches_fn)
nodirbe642ff2016-06-09 15:51:51 -07001762 try:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001763 return run_tha_test(data, options.json)
Justin Luong97eda6f2022-08-23 01:29:16 +00001764 except (cipd.Error, local_caching.NamedCacheError, local_caching.NoMoreSpace,
1765 errors.NonRecoverableCipdException) as ex:
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +00001766 print(ex.message, file=sys.stderr)
Junji Watanabed2ab86b2021-08-13 07:20:23 +00001767 on_error.report(None)
nodirbe642ff2016-06-09 15:51:51 -07001768 return 1
Ye Kuang1d096cb2020-06-26 08:38:21 +00001769 finally:
1770 if tmp_cipd_cache_dir is not None:
1771 try:
1772 file_path.rmtree(tmp_cipd_cache_dir)
1773 except OSError:
1774 logging.exception('Remove tmp_cipd_cache_dir=%s failed',
1775 tmp_cipd_cache_dir)
1776 # Best effort clean up. Failed to do so doesn't affect the outcome.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001777
1778
1779if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001780 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001781 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001782 fix_encoding.fix_encoding()
Ye Kuang2dd17442020-04-22 08:45:52 +00001783 net.set_user_agent('run_isolated.py/' + __version__)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001784 sys.exit(main(sys.argv[1:]))