blob: 29fecf5178729357324200c7764bd5dd2faa05c0 [file] [log] [blame]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2012 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00005
nodir55be77b2016-05-03 09:39:57 -07006"""Runs a command with optional isolated input/output.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00007
Marc-Antoine Rueleed2f3a2019-03-14 00:00:40 +00008run_isolated takes cares of setting up a temporary environment, running a
9command, and tearing it down.
nodir55be77b2016-05-03 09:39:57 -070010
Marc-Antoine Rueleed2f3a2019-03-14 00:00:40 +000011It handles downloading and uploading isolated files, mapping CIPD packages and
12reusing stateful named caches.
13
14The isolated files, CIPD packages and named caches are kept as a global LRU
15cache.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -050016
Roberto Carrillo71ade6d2018-10-08 22:30:24 +000017Any ${EXECUTABLE_SUFFIX} on the command line or the environment variables passed
18with the --env option will be replaced with ".exe" string on Windows and "" on
19other platforms.
nodirbe642ff2016-06-09 15:51:51 -070020
Roberto Carrillo71ade6d2018-10-08 22:30:24 +000021Any ${ISOLATED_OUTDIR} on the command line or the environment variables passed
22with the --env option will be replaced by the location of a temporary directory
23upon execution of the command specified in the .isolated file. All content
24written to this directory will be uploaded upon termination and the .isolated
25file describing this directory will be printed to stdout.
bpastene447c1992016-06-20 15:21:47 -070026
Marc-Antoine Rueleed2f3a2019-03-14 00:00:40 +000027Any ${SWARMING_BOT_FILE} on the command line or the environment variables passed
28with the --env option will be replaced by the value of the --bot-file parameter.
29This file is used by a swarming bot to communicate state of the host to tasks.
30It is written to by the swarming bot's on_before_task() hook in the swarming
31server's custom bot_config.py.
32
33See
34https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
35for all the variables.
36
37See
38https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/swarming_bot/config/bot_config.py
39for more information about bot_config.py.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000040"""
41
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +000042from __future__ import print_function
43
44__version__ = '1.0.1'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000045
aludwin7556e0c2016-10-26 08:46:10 -070046import argparse
maruel064c0a32016-04-05 11:47:15 -070047import base64
iannucci96fcccc2016-08-30 15:52:22 -070048import collections
vadimsh232f5a82017-01-20 19:23:44 -080049import contextlib
Ye Kuangfff1e502020-07-13 13:21:57 +000050import distutils
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -040051import errno
aludwin7556e0c2016-10-26 08:46:10 -070052import json
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000053import logging
54import optparse
55import os
Takuto Ikuta5c59a842020-01-24 03:05:24 +000056import platform
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -040057import re
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000058import sys
59import tempfile
maruel064c0a32016-04-05 11:47:15 -070060import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000061
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000062from utils import tools
63tools.force_local_third_party()
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000064
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000065# third_party/
66from depot_tools import fix_encoding
Takuto Ikuta6e2ff962019-10-29 12:35:27 +000067import six
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000068
69# pylint: disable=ungrouped-imports
70import auth
71import cipd
72import isolate_storage
73import isolateserver
74import local_caching
75from libs import luci_context
Vadim Shtayura6b555c12014-07-23 16:22:18 -070076from utils import file_path
maruel12e30012015-10-09 11:55:35 -070077from utils import fs
maruel064c0a32016-04-05 11:47:15 -070078from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040079from utils import logging_utils
Ye Kuang2dd17442020-04-22 08:45:52 +000080from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040081from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050082from utils import subprocess42
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000083
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000084
maruele2f2cb82016-07-13 14:41:03 -070085# Magic variables that can be found in the isolate task command line.
86ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
87EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
88SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
89
90
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000091# The name of the log file to use.
92RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
93
maruele2f2cb82016-07-13 14:41:03 -070094
csharp@chromium.orge217f302012-11-22 16:51:53 +000095# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000096RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000097
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000098
maruele2f2cb82016-07-13 14:41:03 -070099# Use short names for temporary directories. This is driven by Windows, which
100# imposes a relatively short maximum path length of 260 characters, often
101# referred to as MAX_PATH. It is relatively easy to create files with longer
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +0000102# path length. A use case is with recursive dependency trees like npm packages.
maruele2f2cb82016-07-13 14:41:03 -0700103#
104# It is recommended to start the script with a `root_dir` as short as
105# possible.
106# - ir stands for isolated_run
107# - io stands for isolated_out
108# - it stands for isolated_tmp
Takuto Ikutab7ce0e32019-11-27 23:26:18 +0000109# - ic stands for isolated_client
maruele2f2cb82016-07-13 14:41:03 -0700110ISOLATED_RUN_DIR = u'ir'
111ISOLATED_OUT_DIR = u'io'
112ISOLATED_TMP_DIR = u'it'
Takuto Ikutab7ce0e32019-11-27 23:26:18 +0000113ISOLATED_CLIENT_DIR = u'ic'
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000114_CAS_CLIENT_DIR = u'cc'
maruele2f2cb82016-07-13 14:41:03 -0700115
Takuto Ikuta02edca22019-11-29 10:04:51 +0000116# TODO(tikuta): take these parameter from luci-config?
Takuto Ikuta9c4eb1d2020-10-05 03:40:14 +0000117# Update tag by `./client/update_go_clients.sh`.
Takuto Ikutac8c92e62020-04-01 07:07:29 +0000118# Or take revision from
Takuto Ikutab7ce0e32019-11-27 23:26:18 +0000119# https://ci.chromium.org/p/infra-internal/g/infra-packagers/console
Takuto Ikuta02edca22019-11-29 10:04:51 +0000120ISOLATED_PACKAGE = 'infra/tools/luci/isolated/${platform}'
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000121_CAS_PACKAGE = 'infra/tools/luci/cas/${platform}'
Takuto Ikuta02714bd2020-10-09 06:19:59 +0000122_LUCI_GO_REVISION = 'git_revision:7939542400b9511d60e67501839275f66bac8d0a'
maruele2f2cb82016-07-13 14:41:03 -0700123
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400124# Keep synced with task_request.py
Lei Leife202df2019-06-11 17:33:34 +0000125CACHE_NAME_RE = re.compile(r'^[a-z0-9_]{1,4096}$')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400126
Takuto Ikutaa010c532020-10-21 05:42:29 +0000127_FREE_SPACE_BUFFER_FOR_GO = 1024 * 1024 * 1024
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -0400128
marueld928c862017-06-08 08:20:04 -0700129OUTLIVING_ZOMBIE_MSG = """\
130*** Swarming tried multiple times to delete the %s directory and failed ***
131*** Hard failing the task ***
132
133Swarming detected that your testing script ran an executable, which may have
134started a child executable, and the main script returned early, leaving the
135children executables playing around unguided.
136
137You don't want to leave children processes outliving the task on the Swarming
138bot, do you? The Swarming bot doesn't.
139
140How to fix?
141- For any process that starts children processes, make sure all children
142 processes terminated properly before each parent process exits. This is
143 especially important in very deep process trees.
144 - This must be done properly both in normal successful task and in case of
145 task failure. Cleanup is very important.
146- The Swarming bot sends a SIGTERM in case of timeout.
147 - You have %s seconds to comply after the signal was sent to the process
148 before the process is forcibly killed.
149- To achieve not leaking children processes in case of signals on timeout, you
150 MUST handle signals in each executable / python script and propagate them to
151 children processes.
152 - When your test script (python or binary) receives a signal like SIGTERM or
153 CTRL_BREAK_EVENT on Windows), send it to all children processes and wait for
154 them to terminate before quitting.
155
156See
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -0400157https://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 -0700158for more information.
159
160*** May the SIGKILL force be with you ***
161"""
162
163
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000164# Currently hardcoded. Eventually could be exposed as a flag once there's value.
165# 3 weeks
166MAX_AGE_SECS = 21*24*60*60
167
Ye Kuang72e6fe82020-08-05 06:30:04 +0000168# TODO(1099655): Enable this once all prod issues are gone.
169_USE_GO_ISOLATED_TO_UPLOAD = False
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000170
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500171TaskData = collections.namedtuple(
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000172 'TaskData',
173 [
Takuto Ikuta9a319502019-11-26 07:40:14 +0000174 # List of strings; the command line to use, independent of what was
175 # specified in the isolated file.
176 'command',
177 # Relative directory to start command into.
178 'relative_cwd',
179 # List of strings; the arguments to add to the command specified in the
180 # isolated file.
181 'extra_args',
182 # Hash of the .isolated file that must be retrieved to recreate the tree
183 # of files to run the target executable. The command specified in the
184 # .isolated is executed. Mutually exclusive with command argument.
185 'isolated_hash',
186 # isolateserver.Storage instance to retrieve remote objects. This object
187 # has a reference to an isolateserver.StorageApi, which does the actual
188 # I/O.
189 'storage',
190 # isolateserver.LocalCache instance to keep from retrieving the same
191 # objects constantly by caching the objects retrieved. Can be on-disk or
192 # in-memory.
193 'isolate_cache',
Junji Watanabe54925c32020-09-08 00:56:18 +0000194 # Digest of the input root on RBE-CAS.
195 'cas_digest',
196 # Full CAS instance name.
197 'cas_instance',
Takuto Ikuta9a319502019-11-26 07:40:14 +0000198 # List of paths relative to root_dir to put into the output isolated
199 # bundle upon task completion (see link_outputs_to_outdir).
200 'outputs',
201 # Function (run_dir) => context manager that installs named caches into
202 # |run_dir|.
203 'install_named_caches',
204 # If True, the temporary directory will be deliberately leaked for later
205 # examination.
206 'leak_temp_dir',
207 # Path to the directory to use to create the temporary directory. If not
208 # specified, a random temporary directory is created.
209 'root_dir',
210 # Kills the process if it lasts more than this amount of seconds.
211 'hard_timeout',
212 # Number of seconds to wait between SIGTERM and SIGKILL.
213 'grace_period',
214 # Path to a file with bot state, used in place of ${SWARMING_BOT_FILE}
215 # task command line argument.
216 'bot_file',
217 # Logical account to switch LUCI_CONTEXT into.
218 'switch_to_account',
219 # Context manager dir => CipdInfo, see install_client_and_packages.
220 'install_packages_fn',
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000221 # Use go isolated client.
222 'use_go_isolated',
Junji Watanabeb03450b2020-09-25 05:09:27 +0000223 # Cache directory for go `isolated` client.
Takuto Ikuta057c5342019-12-03 04:05:05 +0000224 'go_cache_dir',
Junji Watanabeb03450b2020-09-25 05:09:27 +0000225 # Parameters passed to go `isolated` client.
Takuto Ikuta879788c2020-01-10 08:00:26 +0000226 'go_cache_policies',
Junji Watanabeb03450b2020-09-25 05:09:27 +0000227 # Cache directory for `cas` client.
228 'cas_cache_dir',
229 # Parameters passed to `cas` client.
230 'cas_cache_policies',
Takuto Ikuta9a319502019-11-26 07:40:14 +0000231 # Environment variables to set.
232 'env',
233 # Environment variables to mutate with relative directories.
234 # Example: {"ENV_KEY": ['relative', 'paths', 'to', 'prepend']}
235 'env_prefix',
236 # Lowers the task process priority.
237 'lower_priority',
238 # subprocess42.Containment instance. Can be None.
239 'containment',
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000240 ])
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500241
242
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500243def _to_str(s):
244 """Downgrades a unicode instance to str. Pass str through as-is."""
245 if isinstance(s, str):
246 return s
247 # This is technically incorrect, especially on Windows. In theory
248 # sys.getfilesystemencoding() should be used to use the right 'ANSI code
249 # page' on Windows, but that causes other problems, as the character set
250 # is very limited.
251 return s.encode('utf-8')
252
253
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -0500254def _to_unicode(s):
255 """Upgrades a str instance to unicode. Pass unicode through as-is."""
Takuto Ikuta95459dd2019-10-29 12:39:47 +0000256 if isinstance(s, six.text_type) or s is None:
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -0500257 return s
258 return s.decode('utf-8')
259
260
maruel03e11842016-07-14 10:50:16 -0700261def make_temp_dir(prefix, root_dir):
262 """Returns a new unique temporary directory."""
Takuto Ikuta6e2ff962019-10-29 12:35:27 +0000263 return six.text_type(tempfile.mkdtemp(prefix=prefix, dir=root_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000264
265
vadimsh9c54b2c2017-07-25 14:08:29 -0700266@contextlib.contextmanager
267def set_luci_context_account(account, tmp_dir):
268 """Sets LUCI_CONTEXT account to be used by the task.
269
270 If 'account' is None or '', does nothing at all. This happens when
271 run_isolated.py is called without '--switch-to-account' flag. In this case,
272 if run_isolated.py is running in some LUCI_CONTEXT environment, the task will
Takuto Ikuta33e2ff32019-09-30 12:44:03 +0000273 just inherit whatever account is already set. This may happen if users invoke
vadimsh9c54b2c2017-07-25 14:08:29 -0700274 run_isolated.py explicitly from their code.
275
276 If the requested account is not defined in the context, switches to
277 non-authenticated access. This happens for Swarming tasks that don't use
278 'task' service accounts.
279
280 If not using LUCI_CONTEXT-based auth, does nothing.
281 If already running as requested account, does nothing.
282 """
283 if not account:
284 # Not actually switching.
285 yield
286 return
287
288 local_auth = luci_context.read('local_auth')
289 if not local_auth:
290 # Not using LUCI_CONTEXT auth at all.
291 yield
292 return
293
294 # See LUCI_CONTEXT.md for the format of 'local_auth'.
295 if local_auth.get('default_account_id') == account:
296 # Already set, no need to switch.
297 yield
298 return
299
300 available = {a['id'] for a in local_auth.get('accounts') or []}
301 if account in available:
302 logging.info('Switching default LUCI_CONTEXT account to %r', account)
303 local_auth['default_account_id'] = account
304 else:
305 logging.warning(
306 'Requested LUCI_CONTEXT account %r is not available (have only %r), '
307 'disabling authentication', account, sorted(available))
308 local_auth.pop('default_account_id', None)
309
310 with luci_context.write(_tmpdir=tmp_dir, local_auth=local_auth):
311 yield
312
313
nodir90bc8dc2016-06-15 13:35:21 -0700314def process_command(command, out_dir, bot_file):
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000315 """Replaces parameters in a command line.
nodirbe642ff2016-06-09 15:51:51 -0700316
317 Raises:
318 ValueError if a parameter is requested in |command| but its value is not
319 provided.
320 """
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000321 return [replace_parameters(arg, out_dir, bot_file) for arg in command]
322
323
324def replace_parameters(arg, out_dir, bot_file):
325 """Replaces parameter tokens with appropriate values in a string.
326
327 Raises:
328 ValueError if a parameter is requested in |arg| but its value is not
329 provided.
330 """
331 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
332 replace_slash = False
333 if ISOLATED_OUTDIR_PARAMETER in arg:
334 if not out_dir:
335 raise ValueError(
336 'output directory is requested in command or env var, but not '
337 'provided; please specify one')
338 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
339 replace_slash = True
340 if SWARMING_BOT_FILE_PARAMETER in arg:
341 if bot_file:
342 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
nodirbe642ff2016-06-09 15:51:51 -0700343 replace_slash = True
Roberto Carrillo71ade6d2018-10-08 22:30:24 +0000344 else:
345 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command or env '
346 'var, but no bot_file specified. Leaving parameter '
347 'unchanged.')
348 if replace_slash:
349 # Replace slashes only if parameters are present
350 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
351 arg = arg.replace('/', os.sep)
352 return arg
maruela9cfd6f2015-09-15 11:03:15 -0700353
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)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500383 out['PATH'] = '%s%s%s' % (_to_str(bin_dir), os.pathsep, out['PATH'])
384 out['CIPD_CACHE_DIR'] = _to_str(cipd_info.cache_dir)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500385
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +0000386 for key, paths in env_prefixes.items():
Marc-Antoine Ruel9ec1e9f2017-12-20 16:36:54 -0500387 assert isinstance(paths, list), paths
388 paths = [os.path.normpath(os.path.join(run_dir, p)) for p in paths]
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500389 cur = out.get(key)
390 if cur:
391 paths.append(cur)
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -0500392 out[key] = _to_str(os.path.pathsep.join(paths))
vadimsh232f5a82017-01-20 19:23:44 -0800393
Marc-Antoine Ruelefb30b12018-07-25 18:34:36 +0000394 tmp_dir = _to_str(tmp_dir)
395 # pylint: disable=line-too-long
396 # * python respects $TMPDIR, $TEMP, and $TMP in this order, regardless of
397 # platform. So $TMPDIR must be set on all platforms.
398 # https://github.com/python/cpython/blob/2.7/Lib/tempfile.py#L155
399 out['TMPDIR'] = tmp_dir
400 if sys.platform == 'win32':
401 # * chromium's base utils uses GetTempPath().
402 # https://cs.chromium.org/chromium/src/base/files/file_util_win.cc?q=GetTempPath
403 # * Go uses GetTempPath().
404 # * GetTempDir() uses %TMP%, then %TEMP%, then other stuff. So %TMP% must be
405 # set.
406 # https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-gettemppathw
407 out['TMP'] = tmp_dir
408 # https://blogs.msdn.microsoft.com/oldnewthing/20150417-00/?p=44213
409 out['TEMP'] = tmp_dir
410 elif sys.platform == 'darwin':
411 # * Chromium uses an hack on macOS before calling into
412 # NSTemporaryDirectory().
413 # https://cs.chromium.org/chromium/src/base/files/file_util_mac.mm?q=GetTempDir
414 # https://developer.apple.com/documentation/foundation/1409211-nstemporarydirectory
415 out['MAC_CHROMIUM_TMPDIR'] = tmp_dir
416 else:
417 # TMPDIR is specified as the POSIX standard envvar for the temp directory.
418 # http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
419 # * mktemp on linux respects $TMPDIR.
420 # * Chromium respects $TMPDIR on linux.
421 # https://cs.chromium.org/chromium/src/base/files/file_util_posix.cc?q=GetTempDir
422 # * Go uses $TMPDIR.
423 # https://go.googlesource.com/go/+/go1.10.3/src/os/file_unix.go#307
424 pass
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -0500425 return out
vadimsh232f5a82017-01-20 19:23:44 -0800426
427
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000428def run_command(
429 command, cwd, env, hard_timeout, grace_period, lower_priority, containment):
maruel6be7f9e2015-10-01 12:25:30 -0700430 """Runs the command.
431
432 Returns:
433 tuple(process exit code, bool if had a hard timeout)
434 """
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000435 logging.info(
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000436 'run_command(%s, %s, %s, %s, %s, %s)',
437 command, cwd, hard_timeout, grace_period, lower_priority, containment)
marueleb5fbee2015-09-17 13:01:36 -0700438
maruel6be7f9e2015-10-01 12:25:30 -0700439 exit_code = None
440 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700441 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700442 proc = None
443 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700444 try:
maruel6be7f9e2015-10-01 12:25:30 -0700445 # TODO(maruel): This code is imperfect. It doesn't handle well signals
446 # during the download phase and there's short windows were things can go
447 # wrong.
448 def handler(signum, _frame):
449 if proc and not had_signal:
450 logging.info('Received signal %d', signum)
451 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700452 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700453
Marc-Antoine Ruel30b80fe2019-02-08 13:51:31 +0000454 proc = subprocess42.Popen(
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +0000455 command, cwd=cwd, env=env, detached=True, close_fds=True,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +0000456 lower_priority=lower_priority, containment=containment)
maruel6be7f9e2015-10-01 12:25:30 -0700457 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
458 try:
John Budorickc398f092019-06-10 22:49:44 +0000459 exit_code = proc.wait(hard_timeout or None)
maruel6be7f9e2015-10-01 12:25:30 -0700460 except subprocess42.TimeoutExpired:
461 if not had_signal:
462 logging.warning('Hard timeout')
463 had_hard_timeout = True
464 logging.warning('Sending SIGTERM')
465 proc.terminate()
466
Takuto Ikuta684f7912020-09-29 07:49:49 +0000467 kill_sent = False
maruel6be7f9e2015-10-01 12:25:30 -0700468 # Ignore signals in grace period. Forcibly give the grace period to the
469 # child process.
470 if exit_code is None:
471 ignore = lambda *_: None
472 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
473 try:
474 exit_code = proc.wait(grace_period or None)
475 except subprocess42.TimeoutExpired:
476 # Now kill for real. The user can distinguish between the
477 # following states:
478 # - signal but process exited within grace period,
479 # hard_timed_out will be set but the process exit code will be
480 # script provided.
481 # - processed exited late, exit code will be -9 on posix.
482 logging.warning('Grace exhausted; sending SIGKILL')
483 proc.kill()
Takuto Ikuta684f7912020-09-29 07:49:49 +0000484 kill_sent = True
martiniss5c8043e2017-08-01 17:09:43 -0700485 logging.info('Waiting for process exit')
maruel6be7f9e2015-10-01 12:25:30 -0700486 exit_code = proc.wait()
Takuto Ikuta684f7912020-09-29 07:49:49 +0000487
488 # the process group / job object may be dangling so if we didn't kill
489 # it already, give it a poke now.
490 if not kill_sent:
491 proc.kill()
Takuto Ikutaeccf0862020-03-19 03:05:55 +0000492 except OSError as e:
maruela9cfd6f2015-09-15 11:03:15 -0700493 # This is not considered to be an internal error. The executable simply
494 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800495 sys.stderr.write(
tikuta2d678212019-09-23 23:12:08 +0000496 '<The executable does not exist, a dependent library is missing or '
497 'the command line is too long>\n'
498 '<Check for missing .so/.dll in the .isolate or GN file or length of '
499 'command line args>\n'
Takuto Ikutaeccf0862020-03-19 03:05:55 +0000500 '<Command: %s, Exception: %s>\n' % (command, e))
maruela72f46e2016-02-24 11:05:45 -0800501 if os.environ.get('SWARMING_TASK_ID'):
502 # Give an additional hint when running as a swarming task.
503 sys.stderr.write(
504 '<See the task\'s page for commands to help diagnose this issue '
505 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700506 exit_code = 1
507 logging.info(
508 'Command finished with exit code %d (%s)',
509 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700510 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700511
512
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000513def _run_go_cmd_and_wait(cmd):
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000514 """
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000515 Runs an external Go command, `isolated` or `cas`, and wait for its completion.
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000516
517 While this is a generic function to launch a subprocess, it has logic that
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000518 is specific to Go `isolated` and `cas` for waiting and logging.
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000519
520 Returns:
521 The subprocess object
522 """
Ye Kuang3c40e9f2020-07-28 13:15:25 +0000523 cmd_str = ' '.join(cmd)
Ye Kuangc1d800f2020-07-28 10:14:55 +0000524 try:
525 proc = subprocess42.Popen(cmd)
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000526
Ye Kuangc1d800f2020-07-28 10:14:55 +0000527 exceeded_max_timeout = True
528 check_period_sec = 30
529 max_checks = 100
530 # max timeout = max_checks * check_period_sec = 50 minutes
531 for i in range(max_checks):
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000532 # This is to prevent I/O timeout error during setup.
Ye Kuangc1d800f2020-07-28 10:14:55 +0000533 try:
534 retcode = proc.wait(check_period_sec)
535 if retcode != 0:
536 raise ValueError("retcode is not 0: %s (cmd=%s)" % (retcode, cmd_str))
537 exceeded_max_timeout = False
538 break
539 except subprocess42.TimeoutExpired:
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000540 print('still running (after %d seconds)' % ((i + 1) * check_period_sec))
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000541
Ye Kuangc1d800f2020-07-28 10:14:55 +0000542 if exceeded_max_timeout:
543 proc.terminate()
544 try:
545 proc.wait(check_period_sec)
546 except subprocess42.TimeoutExpired:
547 logging.exception(
548 "failed to terminate? timeout happened after %d seconds",
549 check_period_sec)
550 proc.kill()
551 proc.wait()
552 # Raise unconditionally, because |proc| was forcefully terminated.
553 raise ValueError("timedout after %d seconds (cmd=%s)" %
554 (check_period_sec * max_checks, cmd_str))
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000555
Ye Kuangc1d800f2020-07-28 10:14:55 +0000556 return proc
557 except Exception:
558 logging.exception('Failed to run Go cmd %s', cmd_str)
559 raise
Ye Kuangc0cf9ca2020-07-16 08:56:51 +0000560
561
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000562def _fetch_and_map_with_cas(cas_client, digest, instance, output_dir, cache_dir,
563 policies):
564 """
565 Fetches a CAS tree using cas client, create the tree and returns download
566 stats.
567 """
568
Takuto Ikuta34a86c52020-10-13 05:30:57 +0000569 # TODO(crbug.com/chrome-operations/49):
570 # remove this after isolate to RBE-CAS migration.
571 _CAS_EMPTY_DIR_DIGEST = (
572 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/0')
573 if digest == _CAS_EMPTY_DIR_DIGEST:
574 return {
575 'duration': 0.0,
576 'items_cold': '',
577 'items_hot': '',
578 }
579
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000580 start = time.time()
581 result_json_handle, result_json_path = tempfile.mkstemp(
582 prefix=u'fetch-and-map-result-', suffix=u'.json')
583 os.close(result_json_handle)
584 try:
585 cmd = [
586 cas_client,
587 'download',
588 '-digest',
589 digest,
590 '-cas-instance',
591 instance,
592 # flags for cache.
593 '-cache-dir',
594 cache_dir,
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000595 '-cache-max-size',
596 str(policies.max_cache_size),
597 '-cache-min-free-space',
598 str(policies.min_free_space),
599 # flags for output.
600 '-dir',
601 output_dir,
602 '-dump-stats-json',
603 result_json_path,
604 ]
605 _run_go_cmd_and_wait(cmd)
606
607 with open(result_json_path) as json_file:
608 result_json = json.load(json_file)
609
610 return {
611 'duration': time.time() - start,
612 'items_cold': result_json['items_cold'],
613 'items_hot': result_json['items_hot'],
614 }
615 finally:
616 fs.remove(result_json_path)
617
618
619def _fetch_and_map_with_go_isolated(isolated_hash, storage, outdir,
620 go_cache_dir, policies, isolated_client):
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000621 """
622 Fetches an isolated tree using go client, create the tree and returns
623 (bundle, stats).
624 """
625 start = time.time()
626 server_ref = storage.server_ref
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000627 result_json_handle, result_json_path = tempfile.mkstemp(
628 prefix=u'fetch-and-map-result-', suffix=u'.json')
629 os.close(result_json_handle)
630 try:
Ye Kuanga98764c2020-04-09 03:17:37 +0000631 cmd = [
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000632 isolated_client,
633 'download',
634 '-isolate-server',
635 server_ref.url,
636 '-namespace',
637 server_ref.namespace,
638 '-isolated',
639 isolated_hash,
640
641 # flags for cache
642 '-cache-dir',
Takuto Ikuta057c5342019-12-03 04:05:05 +0000643 go_cache_dir,
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000644 '-cache-max-items',
Takuto Ikuta50bc0552019-12-03 03:26:46 +0000645 str(policies.max_items),
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000646 '-cache-max-size',
Takuto Ikuta50bc0552019-12-03 03:26:46 +0000647 str(policies.max_cache_size),
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000648 '-cache-min-free-space',
Takuto Ikuta50bc0552019-12-03 03:26:46 +0000649 str(policies.min_free_space),
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000650
651 # flags for output
652 '-output-dir',
653 outdir,
654 '-fetch-and-map-result-json',
655 result_json_path,
Ye Kuanga98764c2020-04-09 03:17:37 +0000656 ]
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000657 _run_go_cmd_and_wait(cmd)
Takuto Ikuta3153e3b2020-02-18 06:11:47 +0000658
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000659 with open(result_json_path) as json_file:
660 result_json = json.load(json_file)
661
662 isolated = result_json['isolated']
663 bundle = isolateserver.IsolatedBundle(filter_cb=None)
664 # Only following properties are used in caller.
665 bundle.command = isolated.get('command')
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000666 bundle.relative_cwd = isolated.get('relative_cwd')
667
668 return bundle, {
669 'duration': time.time() - start,
670 'items_cold': result_json['items_cold'],
671 'items_hot': result_json['items_hot'],
Ye Kuang65a1de52020-10-16 08:31:16 +0000672 'initial_number_items': result_json['initial_number_items'],
673 'initial_size': result_json['initial_size'],
Takuto Ikutad03ffcc2019-12-02 01:04:23 +0000674 }
675 finally:
676 fs.remove(result_json_path)
677
678
679# TODO(crbug.com/932396): remove this function.
Takuto Ikuta16fac4b2019-12-09 04:57:18 +0000680def fetch_and_map(isolated_hash, storage, cache, outdir):
maruel4409e302016-07-19 14:25:51 -0700681 """Fetches an isolated tree, create the tree and returns (bundle, stats)."""
nodir6f801882016-04-29 14:41:50 -0700682 start = time.time()
683 bundle = isolateserver.fetch_isolated(
684 isolated_hash=isolated_hash,
685 storage=storage,
686 cache=cache,
maruel4409e302016-07-19 14:25:51 -0700687 outdir=outdir,
Takuto Ikuta16fac4b2019-12-09 04:57:18 +0000688 use_symlinks=False)
Takuto Ikuta2b9640e2019-06-19 00:53:23 +0000689 hot = (collections.Counter(cache.used) -
690 collections.Counter(cache.added)).elements()
nodir6f801882016-04-29 14:41:50 -0700691 return bundle, {
Takuto Ikuta630f99d2020-07-02 12:59:35 +0000692 'duration': time.time() - start,
693 'items_cold': base64.b64encode(large.pack(sorted(cache.added))).decode(),
694 'items_hot': base64.b64encode(large.pack(sorted(hot))).decode(),
nodir6f801882016-04-29 14:41:50 -0700695 }
696
697
aludwin0a8e17d2016-10-27 15:57:39 -0700698def link_outputs_to_outdir(run_dir, out_dir, outputs):
699 """Links any named outputs to out_dir so they can be uploaded.
700
701 Raises an error if the file already exists in that directory.
702 """
703 if not outputs:
704 return
705 isolateserver.create_directories(out_dir, outputs)
706 for o in outputs:
Sadaf Matinkhoo10743a62018-03-29 16:28:58 -0400707 copy_recursively(os.path.join(run_dir, o), os.path.join(out_dir, o))
708
709
710def copy_recursively(src, dst):
711 """Efficiently copies a file or directory from src_dir to dst_dir.
712
713 `item` may be a file, directory, or a symlink to a file or directory.
714 All symlinks are replaced with their targets, so the resulting
715 directory structure in dst_dir will never have any symlinks.
716
717 To increase speed, copy_recursively hardlinks individual files into the
718 (newly created) directory structure if possible, unlike Python's
719 shutil.copytree().
720 """
721 orig_src = src
722 try:
723 # Replace symlinks with their final target.
724 while fs.islink(src):
725 res = fs.readlink(src)
726 src = os.path.join(os.path.dirname(src), res)
727 # TODO(sadafm): Explicitly handle cyclic symlinks.
728
729 # Note that fs.isfile (which is a wrapper around os.path.isfile) throws
730 # an exception if src does not exist. A warning will be logged in that case.
731 if fs.isfile(src):
732 file_path.link_file(dst, src, file_path.HARDLINK_WITH_FALLBACK)
733 return
734
735 if not fs.exists(dst):
736 os.makedirs(dst)
737
738 for child in fs.listdir(src):
739 copy_recursively(os.path.join(src, child), os.path.join(dst, child))
740
741 except OSError as e:
742 if e.errno == errno.ENOENT:
743 logging.warning('Path %s does not exist or %s is a broken symlink',
744 src, orig_src)
745 else:
746 logging.info("Couldn't collect output file %s: %s", src, e)
aludwin0a8e17d2016-10-27 15:57:39 -0700747
748
Ye Kuangfb0bad62020-07-28 08:07:25 +0000749def _upload_with_py(storage, out_dir):
750
751 def process_stats(f_st):
752 st = sorted(i.size for i in f_st)
753 return base64.b64encode(large.pack(st)).decode()
754
755 try:
756 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
757 storage, [out_dir], None, verify_push=True)
758
759 isolated = list(results.values())[0]
760 cold = process_stats(f_cold)
761 hot = process_stats(f_hot)
762 return isolated, cold, hot
763
764 except isolateserver.Aborted:
765 # This happens when a signal SIGTERM was received while uploading data.
766 # There is 2 causes:
767 # - The task was too slow and was about to be killed anyway due to
768 # exceeding the hard timeout.
769 # - The amount of data uploaded back is very large and took too much
770 # time to archive.
771 sys.stderr.write('Received SIGTERM while uploading')
772 # Re-raise, so it will be treated as an internal failure.
773 raise
774
775
776def _upload_with_go(storage, outdir, isolated_client):
777 """
778 Uploads results back using the Go `isolated` CLI.
779 """
780 server_ref = storage.server_ref
781 isolated_handle, isolated_path = tempfile.mkstemp(
782 prefix=u'isolated-hash-', suffix=u'.txt')
783 stats_json_handle, stats_json_path = tempfile.mkstemp(
784 prefix=u'dump-stats-', suffix=u'.json')
785 os.close(isolated_handle)
786 os.close(stats_json_handle)
787 try:
788 cmd = [
789 isolated_client,
790 'archive',
791 '-isolate-server',
792 server_ref.url,
793 '-namespace',
794 server_ref.namespace,
795 '-dirs',
796 # Format: <working directory>:<relative path to dir>
797 outdir + ':',
798
799 # output
800 '-dump-hash',
801 isolated_path,
802 '-dump-stats-json',
803 stats_json_path,
Ye Kuangbc4e8402020-07-29 09:54:30 +0000804 '-quiet',
Ye Kuangfb0bad62020-07-28 08:07:25 +0000805 ]
Ye Kuang0023dc52020-08-04 05:28:41 +0000806 # Will do exponential backoff, e.g. 10, 20, 40...
807 # This mitigates https://crbug.com/1094369, where there is a data race on
808 # the uploaded files.
809 backoff = 10
Takuto Ikutae0bfec72020-08-28 02:52:52 +0000810 started = time.time()
Ye Kuang0023dc52020-08-04 05:28:41 +0000811 while True:
812 try:
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000813 _run_go_cmd_and_wait(cmd)
Ye Kuang0023dc52020-08-04 05:28:41 +0000814 break
815 except Exception:
Takuto Ikutae0bfec72020-08-28 02:52:52 +0000816 if time.time() > started + 60 * 2:
817 # This is to not wait task having leaked process long time.
Ye Kuang0023dc52020-08-04 05:28:41 +0000818 raise
819
820 on_error.report('error before %d second backoff' % backoff)
821 logging.exception(
Junji Watanabe4b890ef2020-09-16 01:43:27 +0000822 '_run_go_cmd_and_wait() failed, will retry after %d seconds',
Ye Kuang0023dc52020-08-04 05:28:41 +0000823 backoff)
824 time.sleep(backoff)
825 backoff *= 2
Ye Kuangfb0bad62020-07-28 08:07:25 +0000826
827 with open(isolated_path) as isol_file:
828 isolated = isol_file.read()
829 with open(stats_json_path) as json_file:
830 stats_json = json.load(json_file)
831
832 return isolated, stats_json['items_cold'], stats_json['items_hot']
833 finally:
834 fs.remove(isolated_path)
835 fs.remove(stats_json_path)
836
837
Ye Kuangbc4e8402020-07-29 09:54:30 +0000838def upload_out_dir(storage, out_dir, go_isolated_client):
839 """Uploads the results in |out_dir| back, if there is any.
maruela9cfd6f2015-09-15 11:03:15 -0700840
841 Returns:
Ye Kuangbc4e8402020-07-29 09:54:30 +0000842 tuple(outputs_ref, stats)
maruel064c0a32016-04-05 11:47:15 -0700843 - outputs_ref: a dict referring to the results archived back to the isolated
844 server, if applicable.
nodir6f801882016-04-29 14:41:50 -0700845 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700846 """
maruela9cfd6f2015-09-15 11:03:15 -0700847 # Upload out_dir and generate a .isolated file out of this directory. It is
848 # only done if files were written in the directory.
849 outputs_ref = None
Ye Kuangfb0bad62020-07-28 08:07:25 +0000850 cold = ''
851 hot = ''
nodir6f801882016-04-29 14:41:50 -0700852 start = time.time()
853
maruel12e30012015-10-09 11:55:35 -0700854 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700855 with tools.Profiler('ArchiveOutput'):
Ye Kuangfb0bad62020-07-28 08:07:25 +0000856 isolated = None
Ye Kuang72e6fe82020-08-05 06:30:04 +0000857 if _USE_GO_ISOLATED_TO_UPLOAD and go_isolated_client is not None:
Ye Kuangfb0bad62020-07-28 08:07:25 +0000858 isolated, cold, hot = _upload_with_go(storage, out_dir,
859 go_isolated_client)
Ye Kuang72e6fe82020-08-05 06:30:04 +0000860 else:
861 isolated, cold, hot = _upload_with_py(storage, out_dir)
Ye Kuangfb0bad62020-07-28 08:07:25 +0000862 outputs_ref = {
863 'isolated': isolated,
864 'isolatedserver': storage.server_ref.url,
865 'namespace': storage.server_ref.namespace,
866 }
nodir6f801882016-04-29 14:41:50 -0700867
nodir6f801882016-04-29 14:41:50 -0700868 stats = {
Takuto Ikuta630f99d2020-07-02 12:59:35 +0000869 'duration': time.time() - start,
Ye Kuangfb0bad62020-07-28 08:07:25 +0000870 'items_cold': cold,
871 'items_hot': hot,
nodir6f801882016-04-29 14:41:50 -0700872 }
Ye Kuangbc4e8402020-07-29 09:54:30 +0000873 return outputs_ref, stats
maruela9cfd6f2015-09-15 11:03:15 -0700874
875
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000876def upload_outdir_with_cas(cas_client, cas_instance, outdir):
877 """Uploads the results in |outdir|, if there is any.
878
879 Returns:
880 tuple(root_digest, stats)
881 - root_digest: a digest of the output directory.
882 - stats: uploading stats.
883 """
884 digest_file_handle, digest_path = tempfile.mkstemp(
885 prefix=u'cas-digest', suffix=u'.txt')
886 os.close(digest_file_handle)
887 stats_json_handle, stats_json_path = tempfile.mkstemp(
888 prefix=u'upload-stats', suffix=u'.json')
889 os.close(stats_json_handle)
890
891 try:
892 cmd = [
893 cas_client,
894 'archive',
895 '-cas-instance',
896 cas_instance,
897 '-paths',
898 # Format: <working directory>:<relative path to dir>
899 outdir + ':',
900 # output
901 '-dump-digest',
902 digest_path,
903 '-dump-stats-json',
904 stats_json_path,
905 ]
906
907 start = time.time()
908
909 _run_go_cmd_and_wait(cmd)
910
911 with open(digest_path) as digest_file:
912 digest = digest_file.read()
Junji Watanabec208b302020-09-25 09:18:27 +0000913 h, s = digest.split('/')
914 cas_output_root = {
915 'cas_instance': cas_instance,
916 'digest': {
917 'hash': h,
918 'size_bytes': int(s)
919 }
920 }
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000921 with open(stats_json_path) as stats_file:
922 stats = json.load(stats_file)
923
924 stats['duration'] = time.time() - start
925
Junji Watanabec208b302020-09-25 09:18:27 +0000926 return cas_output_root, stats
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000927 finally:
928 fs.remove(digest_path)
929 fs.remove(stats_json_path)
930
931
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500932def map_and_run(data, constant_run_path):
nodir55be77b2016-05-03 09:39:57 -0700933 """Runs a command with optional isolated input/output.
934
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500935 Arguments:
936 - data: TaskData instance.
937 - constant_run_path: TODO
nodir55be77b2016-05-03 09:39:57 -0700938
939 Returns metadata about the result.
940 """
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +0000941
942 if data.isolate_cache:
943 download_stats = {
944 #'duration': 0.,
945 'initial_number_items': len(data.isolate_cache),
946 'initial_size': data.isolate_cache.total_size,
947 #'items_cold': '<large.pack()>',
948 #'items_hot': '<large.pack()>',
949 }
950 else:
951 # TODO(tikuta): take stats from state.json in this case too.
952 download_stats = {}
953
maruela9cfd6f2015-09-15 11:03:15 -0700954 result = {
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000955 'duration': None,
956 'exit_code': None,
957 'had_hard_timeout': False,
958 'internal_failure': 'run_isolated did not complete properly',
959 'stats': {
960 #'cipd': {
961 # 'duration': 0.,
962 # 'get_client_duration': 0.,
963 #},
964 'isolated': {
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +0000965 'download': download_stats,
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000966 #'upload': {
967 # 'duration': 0.,
968 # 'items_cold': '<large.pack()>',
969 # 'items_hot': '<large.pack()>',
970 #},
971 },
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +0000972 },
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000973 #'cipd_pins': {
974 # 'packages': [
975 # {'package_name': ..., 'version': ..., 'path': ...},
976 # ...
977 # ],
978 # 'client_package': {'package_name': ..., 'version': ...},
979 #},
980 'outputs_ref': None,
Junji Watanabe54925c32020-09-08 00:56:18 +0000981 'cas_output_root': None,
Takuto Ikuta5ed62ad2019-09-26 09:16:00 +0000982 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700983 }
nodirbe642ff2016-06-09 15:51:51 -0700984
Takuto Ikutad46ea762020-10-07 05:43:22 +0000985 assert os.path.isabs(data.root_dir), ("data.root_dir is not abs path: %s" %
986 data.root_dir)
987 file_path.ensure_tree(data.root_dir, 0o700)
988
maruele2f2cb82016-07-13 14:41:03 -0700989 # See comment for these constants.
maruelcffa0542017-04-07 08:39:20 -0700990 # TODO(maruel): This is not obvious. Change this to become an error once we
991 # make the constant_run_path an exposed flag.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500992 if constant_run_path and data.root_dir:
993 run_dir = os.path.join(data.root_dir, ISOLATED_RUN_DIR)
maruel5c4eed82017-05-26 05:33:40 -0700994 if os.path.isdir(run_dir):
995 file_path.rmtree(run_dir)
Lei Leife202df2019-06-11 17:33:34 +0000996 os.mkdir(run_dir, 0o700)
maruelcffa0542017-04-07 08:39:20 -0700997 else:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -0500998 run_dir = make_temp_dir(ISOLATED_RUN_DIR, data.root_dir)
Junji Watanabe1adba7b2020-09-18 07:03:58 +0000999
1000 # True if CAS is used for download/upload files.
1001 use_cas = bool(data.cas_digest)
1002
maruel03e11842016-07-14 10:50:16 -07001003 # storage should be normally set but don't crash if it is not. This can happen
1004 # as Swarming task can run without an isolate server.
Junji Watanabe1adba7b2020-09-18 07:03:58 +00001005 out_dir = None
1006 if data.storage or use_cas:
1007 out_dir = make_temp_dir(ISOLATED_OUT_DIR, data.root_dir)
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001008 tmp_dir = make_temp_dir(ISOLATED_TMP_DIR, data.root_dir)
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001009 isolated_client_dir = make_temp_dir(ISOLATED_CLIENT_DIR, data.root_dir)
nodir55be77b2016-05-03 09:39:57 -07001010 cwd = run_dir
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001011 if data.relative_cwd:
1012 cwd = os.path.normpath(os.path.join(cwd, data.relative_cwd))
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001013 command = data.command
Ye Kuangfb0bad62020-07-28 08:07:25 +00001014 go_isolated_client = None
1015 if data.use_go_isolated:
1016 go_isolated_client = os.path.join(isolated_client_dir,
1017 'isolated' + cipd.EXECUTABLE_SUFFIX)
Junji Watanabe1adba7b2020-09-18 07:03:58 +00001018
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001019 cas_client = None
1020 cas_client_dir = make_temp_dir(_CAS_CLIENT_DIR, data.root_dir)
Junji Watanabe1adba7b2020-09-18 07:03:58 +00001021 if use_cas:
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001022 cas_client = os.path.join(cas_client_dir, 'cas' + cipd.EXECUTABLE_SUFFIX)
1023
nodir55be77b2016-05-03 09:39:57 -07001024 try:
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001025 with data.install_packages_fn(run_dir, isolated_client_dir,
1026 cas_client_dir) as cipd_info:
vadimsh232f5a82017-01-20 19:23:44 -08001027 if cipd_info:
1028 result['stats']['cipd'] = cipd_info.stats
1029 result['cipd_pins'] = cipd_info.pins
nodir90bc8dc2016-06-15 13:35:21 -07001030
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001031 isolated_stats = result['stats'].setdefault('isolated', {})
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001032 if data.isolated_hash:
Takuto Ikutad03ffcc2019-12-02 01:04:23 +00001033 if data.use_go_isolated:
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001034 bundle, stats = _fetch_and_map_with_go_isolated(
Takuto Ikuta90397ca2020-01-08 10:07:55 +00001035 isolated_hash=data.isolated_hash,
1036 storage=data.storage,
Takuto Ikuta90397ca2020-01-08 10:07:55 +00001037 outdir=run_dir,
1038 go_cache_dir=data.go_cache_dir,
Takuto Ikuta879788c2020-01-10 08:00:26 +00001039 policies=data.go_cache_policies,
Ye Kuangfb0bad62020-07-28 08:07:25 +00001040 isolated_client=go_isolated_client)
Takuto Ikuta90397ca2020-01-08 10:07:55 +00001041 else:
Takuto Ikutad03ffcc2019-12-02 01:04:23 +00001042 bundle, stats = fetch_and_map(
1043 isolated_hash=data.isolated_hash,
1044 storage=data.storage,
1045 cache=data.isolate_cache,
Takuto Ikuta16fac4b2019-12-09 04:57:18 +00001046 outdir=run_dir)
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +00001047 isolated_stats['download'].update(stats)
Takuto Ikutab58dbd12020-06-05 09:29:14 +00001048
maruelabec63c2017-04-26 11:53:24 -07001049 # Inject the command
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001050 if not command and bundle.command:
1051 command = bundle.command + data.extra_args
Marc-Antoine Rueld704a1f2017-10-31 10:51:23 -04001052 # Only set the relative directory if the isolated file specified a
1053 # command, and no raw command was specified.
1054 if bundle.relative_cwd:
1055 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
maruelabec63c2017-04-26 11:53:24 -07001056
Junji Watanabe54925c32020-09-08 00:56:18 +00001057 elif data.cas_digest:
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001058 stats = _fetch_and_map_with_cas(
1059 cas_client=cas_client,
1060 digest=data.cas_digest,
1061 instance=data.cas_instance,
1062 output_dir=run_dir,
Junji Watanabeb03450b2020-09-25 05:09:27 +00001063 cache_dir=data.cas_cache_dir,
1064 policies=data.cas_cache_policies)
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001065 isolated_stats['download'].update(stats)
Junji Watanabe54925c32020-09-08 00:56:18 +00001066
maruelabec63c2017-04-26 11:53:24 -07001067 if not command:
1068 # Handle this as a task failure, not an internal failure.
1069 sys.stderr.write(
1070 '<No command was specified!>\n'
1071 '<Please secify a command when triggering your Swarming task>\n')
1072 result['exit_code'] = 1
1073 return result
nodirbe642ff2016-06-09 15:51:51 -07001074
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001075 if not cwd.startswith(run_dir):
1076 # Handle this as a task failure, not an internal failure. This is a
1077 # 'last chance' way to gate against directory escape.
1078 sys.stderr.write('<Relative CWD is outside of run directory!>\n')
1079 result['exit_code'] = 1
1080 return result
1081
1082 if not os.path.isdir(cwd):
1083 # Accepts relative_cwd that does not exist.
Lei Leife202df2019-06-11 17:33:34 +00001084 os.makedirs(cwd, 0o700)
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001085
vadimsh232f5a82017-01-20 19:23:44 -08001086 # If we have an explicit list of files to return, make sure their
1087 # directories exist now.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001088 if data.storage and data.outputs:
1089 isolateserver.create_directories(run_dir, data.outputs)
aludwin0a8e17d2016-10-27 15:57:39 -07001090
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001091 with data.install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001092 sys.stdout.flush()
1093 start = time.time()
1094 try:
vadimsh9c54b2c2017-07-25 14:08:29 -07001095 # Need to switch the default account before 'get_command_env' call,
1096 # so it can grab correct value of LUCI_CONTEXT env var.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001097 with set_luci_context_account(data.switch_to_account, tmp_dir):
1098 env = get_command_env(
Roberto Carrillo71ade6d2018-10-08 22:30:24 +00001099 tmp_dir, cipd_info, run_dir, data.env, data.env_prefix, out_dir,
1100 data.bot_file)
Brian Sheedy7a761172019-08-30 22:55:14 +00001101 command = tools.find_executable(command, env)
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001102 command = process_command(command, out_dir, data.bot_file)
1103 file_path.ensure_command_has_abs_path(command, cwd)
1104
vadimsh9c54b2c2017-07-25 14:08:29 -07001105 result['exit_code'], result['had_hard_timeout'] = run_command(
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +00001106 command, cwd, env, data.hard_timeout, data.grace_period,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001107 data.lower_priority, data.containment)
nodird6160682017-02-02 13:03:35 -08001108 finally:
1109 result['duration'] = max(time.time() - start, 0)
Seth Koehler49139812017-12-19 13:59:33 -05001110
Ye Kuangbc4e8402020-07-29 09:54:30 +00001111 if out_dir:
1112 # Try to link files to the output directory, if specified.
1113 link_outputs_to_outdir(run_dir, out_dir, data.outputs)
1114 isolated_stats = result['stats'].setdefault('isolated', {})
Junji Watanabe1adba7b2020-09-18 07:03:58 +00001115 if use_cas:
1116 result['cas_output_root'], isolated_stats['upload'] = (
1117 upload_outdir_with_cas(cas_client, data.cas_instance, out_dir))
1118 else:
1119 # This could use |go_isolated_client|, so make sure it runs when the
1120 # CIPD package still exists.
1121 result['outputs_ref'], isolated_stats['upload'] = (
1122 upload_out_dir(data.storage, out_dir, go_isolated_client))
Seth Koehler49139812017-12-19 13:59:33 -05001123 # We successfully ran the command, set internal_failure back to
1124 # None (even if the command failed, it's not an internal error).
1125 result['internal_failure'] = None
maruela9cfd6f2015-09-15 11:03:15 -07001126 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -07001127 # An internal error occurred. Report accordingly so the swarming task will
1128 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -07001129 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -07001130 result['internal_failure'] = str(e)
1131 on_error.report(None)
aludwin0a8e17d2016-10-27 15:57:39 -07001132
1133 # Clean up
maruela9cfd6f2015-09-15 11:03:15 -07001134 finally:
1135 try:
Ye Kuangbc4e8402020-07-29 09:54:30 +00001136 success = True
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001137 if data.leak_temp_dir:
nodir32a1ec12016-10-26 18:34:07 -07001138 success = True
maruela9cfd6f2015-09-15 11:03:15 -07001139 logging.warning(
1140 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -07001141 else:
maruel84537cb2015-10-16 14:21:28 -07001142 # On Windows rmtree(run_dir) call above has a synchronization effect: it
1143 # finishes only when all task child processes terminate (since a running
1144 # process locks *.exe file). Examine out_dir only after that call
1145 # completes (since child processes may write to out_dir too and we need
1146 # to wait for them to finish).
Junji Watanabeb03450b2020-09-25 05:09:27 +00001147 dirs_to_remove = [run_dir, tmp_dir, isolated_client_dir, cas_client_dir]
Ye Kuangbc4e8402020-07-29 09:54:30 +00001148 if out_dir:
1149 dirs_to_remove.append(out_dir)
1150 for directory in dirs_to_remove:
Takuto Ikuta69c0d662019-11-27 01:18:08 +00001151 if not fs.isdir(directory):
1152 continue
maruel84537cb2015-10-16 14:21:28 -07001153 try:
Ye Kuangbc4e8402020-07-29 09:54:30 +00001154 success = success and file_path.rmtree(directory)
maruel84537cb2015-10-16 14:21:28 -07001155 except OSError as e:
Takuto Ikuta69c0d662019-11-27 01:18:08 +00001156 logging.error('rmtree(%r) failed: %s', directory, e)
maruel84537cb2015-10-16 14:21:28 -07001157 success = False
1158 if not success:
Takuto Ikuta69c0d662019-11-27 01:18:08 +00001159 sys.stderr.write(
1160 OUTLIVING_ZOMBIE_MSG % (directory, data.grace_period))
Takuto Ikutad7d64e12020-07-31 06:18:45 +00001161 subprocess42.check_call(['tasklist.exe', '/V'], stdout=sys.stderr)
maruel84537cb2015-10-16 14:21:28 -07001162 if result['exit_code'] == 0:
1163 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -07001164
maruela9cfd6f2015-09-15 11:03:15 -07001165 if not success and result['exit_code'] == 0:
1166 result['exit_code'] = 1
1167 except Exception as e:
1168 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -07001169 if out_dir:
1170 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -07001171 result['internal_failure'] = str(e)
Takuto Ikutaa9a907b2020-04-17 08:50:50 +00001172 on_error.report(None)
maruela9cfd6f2015-09-15 11:03:15 -07001173 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -05001174
1175
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001176def run_tha_test(data, result_json):
nodir55be77b2016-05-03 09:39:57 -07001177 """Runs an executable and records execution metadata.
1178
nodir55be77b2016-05-03 09:39:57 -07001179 If isolated_hash is specified, downloads the dependencies in the cache,
1180 hardlinks them into a temporary directory and runs the command specified in
1181 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -05001182
1183 A temporary directory is created to hold the output files. The content inside
1184 this directory will be uploaded back to |storage| packaged as a .isolated
1185 file.
1186
1187 Arguments:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001188 - data: TaskData instance.
1189 - result_json: File path to dump result metadata into. If set, the process
1190 exit code is always 0 unless an internal error occurred.
maruela9cfd6f2015-09-15 11:03:15 -07001191
1192 Returns:
1193 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001194 """
maruela76b9ee2015-12-15 06:18:08 -08001195 if result_json:
1196 # Write a json output file right away in case we get killed.
1197 result = {
Junji Watanabe54925c32020-09-08 00:56:18 +00001198 'exit_code': None,
1199 'had_hard_timeout': False,
1200 'internal_failure': 'Was terminated before completion',
1201 'outputs_ref': None,
1202 'cas_output_root': None,
1203 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -08001204 }
1205 tools.write_json(result_json, result, dense=True)
1206
maruela9cfd6f2015-09-15 11:03:15 -07001207 # run_isolated exit code. Depends on if result_json is used or not.
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001208 result = map_and_run(data, True)
maruela9cfd6f2015-09-15 11:03:15 -07001209 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -07001210
maruela9cfd6f2015-09-15 11:03:15 -07001211 if result_json:
maruel05d5a882015-09-21 13:59:02 -07001212 # We've found tests to delete 'work' when quitting, causing an exception
1213 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -07001214 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -07001215 tools.write_json(result_json, result, dense=True)
1216 # Only return 1 if there was an internal error.
1217 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +00001218
maruela9cfd6f2015-09-15 11:03:15 -07001219 # Marshall into old-style inline output.
1220 if result['outputs_ref']:
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +00001221 # pylint: disable=unsubscriptable-object
maruela9cfd6f2015-09-15 11:03:15 -07001222 data = {
Junji Watanabe38b28b02020-04-23 10:23:30 +00001223 'hash': result['outputs_ref']['isolated'],
1224 'namespace': result['outputs_ref']['namespace'],
1225 'storage': result['outputs_ref']['isolatedserver'],
maruela9cfd6f2015-09-15 11:03:15 -07001226 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -05001227 sys.stdout.flush()
Junji Watanabe38b28b02020-04-23 10:23:30 +00001228 print('[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
1229 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -08001230 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -07001231 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001232
1233
iannuccib58d10d2017-03-18 02:00:25 -07001234# Yielded by 'install_client_and_packages'.
vadimsh232f5a82017-01-20 19:23:44 -08001235CipdInfo = collections.namedtuple('CipdInfo', [
1236 'client', # cipd.CipdClient object
1237 'cache_dir', # absolute path to bot-global cipd tag and instance cache
1238 'stats', # dict with stats to return to the server
1239 'pins', # dict with installed cipd pins to return to the server
1240])
1241
1242
1243@contextlib.contextmanager
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001244def noop_install_packages(_run_dir, _isolated_dir, _cas_dir):
iannuccib58d10d2017-03-18 02:00:25 -07001245 """Placeholder for 'install_client_and_packages' if cipd is disabled."""
vadimsh232f5a82017-01-20 19:23:44 -08001246 yield None
1247
1248
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001249def _install_packages(run_dir, cipd_cache_dir, client, packages):
iannuccib58d10d2017-03-18 02:00:25 -07001250 """Calls 'cipd ensure' for packages.
1251
1252 Args:
1253 run_dir (str): root of installation.
1254 cipd_cache_dir (str): the directory to use for the cipd package cache.
1255 client (CipdClient): the cipd client to use
1256 packages: packages to install, list [(path, package_name, version), ...].
iannuccib58d10d2017-03-18 02:00:25 -07001257
1258 Returns: list of pinned packages. Looks like [
1259 {
1260 'path': 'subdirectory',
1261 'package_name': 'resolved/package/name',
1262 'version': 'deadbeef...',
1263 },
1264 ...
1265 ]
1266 """
1267 package_pins = [None]*len(packages)
1268 def insert_pin(path, name, version, idx):
1269 package_pins[idx] = {
1270 'package_name': name,
1271 # swarming deals with 'root' as '.'
1272 'path': path or '.',
1273 'version': version,
1274 }
1275
1276 by_path = collections.defaultdict(list)
1277 for i, (path, name, version) in enumerate(packages):
1278 # cipd deals with 'root' as ''
1279 if path == '.':
1280 path = ''
1281 by_path[path].append((name, version, i))
1282
1283 pins = client.ensure(
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001284 run_dir,
1285 {
1286 subdir: [(name, vers) for name, vers, _ in pkgs
1287 ] for subdir, pkgs in by_path.items()
1288 },
1289 cache_dir=cipd_cache_dir,
iannuccib58d10d2017-03-18 02:00:25 -07001290 )
1291
Marc-Antoine Ruel04903a32019-10-09 21:09:25 +00001292 for subdir, pin_list in sorted(pins.items()):
iannuccib58d10d2017-03-18 02:00:25 -07001293 this_subdir = by_path[subdir]
1294 for i, (name, version) in enumerate(pin_list):
1295 insert_pin(subdir, name, version, this_subdir[i][2])
1296
Robert Iannucci461b30d2017-12-13 11:34:03 -08001297 assert None not in package_pins, (packages, pins, package_pins)
iannuccib58d10d2017-03-18 02:00:25 -07001298
1299 return package_pins
1300
1301
vadimsh232f5a82017-01-20 19:23:44 -08001302@contextlib.contextmanager
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001303def install_client_and_packages(run_dir, packages, service_url,
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001304 client_package_name, client_version, cache_dir,
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001305 isolated_dir, cas_dir):
vadimsh902948e2017-01-20 15:57:32 -08001306 """Bootstraps CIPD client and installs CIPD packages.
iannucci96fcccc2016-08-30 15:52:22 -07001307
vadimsh232f5a82017-01-20 19:23:44 -08001308 Yields CipdClient, stats, client info and pins (as single CipdInfo object).
1309
1310 Pins and the CIPD client info are in the form of:
iannucci96fcccc2016-08-30 15:52:22 -07001311 [
1312 {
1313 "path": path, "package_name": package_name, "version": version,
1314 },
1315 ...
1316 ]
vadimsh902948e2017-01-20 15:57:32 -08001317 (the CIPD client info is a single dictionary instead of a list)
iannucci96fcccc2016-08-30 15:52:22 -07001318
1319 such that they correspond 1:1 to all input package arguments from the command
1320 line. These dictionaries make their all the way back to swarming, where they
1321 become the arguments of CipdPackage.
nodirbe642ff2016-06-09 15:51:51 -07001322
vadimsh902948e2017-01-20 15:57:32 -08001323 If 'packages' list is empty, will bootstrap CIPD client, but won't install
1324 any packages.
1325
1326 The bootstrapped client (regardless whether 'packages' list is empty or not),
vadimsh232f5a82017-01-20 19:23:44 -08001327 will be made available to the task via $PATH.
vadimsh902948e2017-01-20 15:57:32 -08001328
nodirbe642ff2016-06-09 15:51:51 -07001329 Args:
nodir90bc8dc2016-06-15 13:35:21 -07001330 run_dir (str): root of installation.
vadimsh902948e2017-01-20 15:57:32 -08001331 packages: packages to install, list [(path, package_name, version), ...].
nodirbe642ff2016-06-09 15:51:51 -07001332 service_url (str): CIPD server url, e.g.
1333 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -07001334 client_package_name (str): CIPD package name of CIPD client.
1335 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -07001336 cache_dir (str): where to keep cache of cipd clients, packages and tags.
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001337 isolated_dir (str): where to download isolated client.
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001338 cas_dir (str): where to download cas client.
nodirbe642ff2016-06-09 15:51:51 -07001339 """
1340 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -07001341
nodirbe642ff2016-06-09 15:51:51 -07001342 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -07001343
vadimsh902948e2017-01-20 15:57:32 -08001344 cache_dir = os.path.abspath(cache_dir)
vadimsh232f5a82017-01-20 19:23:44 -08001345 cipd_cache_dir = os.path.join(cache_dir, 'cache') # tag and instance caches
nodir90bc8dc2016-06-15 13:35:21 -07001346 run_dir = os.path.abspath(run_dir)
vadimsh902948e2017-01-20 15:57:32 -08001347 packages = packages or []
nodir90bc8dc2016-06-15 13:35:21 -07001348
nodirbe642ff2016-06-09 15:51:51 -07001349 get_client_start = time.time()
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001350 client_manager = cipd.get_client(cache_dir, service_url, client_package_name,
1351 client_version)
iannucci96fcccc2016-08-30 15:52:22 -07001352
nodirbe642ff2016-06-09 15:51:51 -07001353 with client_manager as client:
1354 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -07001355
iannuccib58d10d2017-03-18 02:00:25 -07001356 package_pins = []
1357 if packages:
Takuto Ikuta2efc7792019-11-27 14:33:34 +00001358 package_pins = _install_packages(run_dir, cipd_cache_dir, client,
1359 packages)
iannuccib58d10d2017-03-18 02:00:25 -07001360
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001361 # Install isolated client to |isolated_dir|.
Takuto Ikuta02edca22019-11-29 10:04:51 +00001362 _install_packages(isolated_dir, cipd_cache_dir, client,
Takuto Ikuta9c4eb1d2020-10-05 03:40:14 +00001363 [('', ISOLATED_PACKAGE, _LUCI_GO_REVISION)])
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001364
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001365 # Install cas client to |cas_dir|.
1366 _install_packages(cas_dir, cipd_cache_dir, client,
Takuto Ikuta9c4eb1d2020-10-05 03:40:14 +00001367 [('', _CAS_PACKAGE, _LUCI_GO_REVISION)])
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001368
iannuccib58d10d2017-03-18 02:00:25 -07001369 file_path.make_tree_files_read_only(run_dir)
nodir90bc8dc2016-06-15 13:35:21 -07001370
vadimsh232f5a82017-01-20 19:23:44 -08001371 total_duration = time.time() - start
Junji Watanabe38b28b02020-04-23 10:23:30 +00001372 logging.info('Installing CIPD client and packages took %d seconds',
1373 total_duration)
nodir90bc8dc2016-06-15 13:35:21 -07001374
vadimsh232f5a82017-01-20 19:23:44 -08001375 yield CipdInfo(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001376 client=client,
1377 cache_dir=cipd_cache_dir,
1378 stats={
1379 'duration': total_duration,
1380 'get_client_duration': get_client_duration,
iannuccib58d10d2017-03-18 02:00:25 -07001381 },
Junji Watanabe38b28b02020-04-23 10:23:30 +00001382 pins={
1383 'client_package': {
1384 'package_name': client.package_name,
1385 'version': client.instance_id,
1386 },
1387 'packages': package_pins,
1388 })
nodirbe642ff2016-06-09 15:51:51 -07001389
1390
1391def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001392 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -07001393 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +00001394 version=__version__,
1395 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -07001396 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001397 '--clean',
1398 action='store_true',
maruel36a963d2016-04-08 17:15:49 -07001399 help='Cleans the cache, trimming it necessary and remove corrupted items '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001400 'and returns without executing anything; use with -v to know what '
1401 'was done')
maruel36a963d2016-04-08 17:15:49 -07001402 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -07001403 '--json',
1404 help='dump output metadata to json file. When used, run_isolated returns '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001405 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -07001406 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -08001407 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -07001408 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001409 '--grace-period',
1410 type='float',
maruel6be7f9e2015-10-01 12:25:30 -07001411 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -07001412 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001413 '--raw-cmd',
1414 action='store_true',
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001415 help='Ignore the isolated command, use the one supplied at the command '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001416 'line')
Marc-Antoine Ruel49e347d2017-10-24 16:52:02 -07001417 parser.add_option(
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001418 '--relative-cwd',
1419 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001420 'requires --raw-cmd')
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001421 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001422 '--env',
1423 default=[],
1424 action='append',
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001425 help='Environment variables to set for the child process')
1426 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001427 '--env-prefix',
1428 default=[],
1429 action='append',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001430 help='Specify a VAR=./path/fragment to put in the environment variable '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001431 'before executing the command. The path fragment must be relative '
1432 'to the isolated run directory, and must not contain a `..` token. '
1433 'The path will be made absolute and prepended to the indicated '
1434 '$VAR using the OS\'s path separator. Multiple items for the same '
1435 '$VAR will be prepended in order.')
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001436 parser.add_option(
bpastene3ae09522016-06-10 17:12:59 -07001437 '--bot-file',
1438 help='Path to a file describing the state of the host. The content is '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001439 'defined by on_before_task() in bot_config.')
aludwin7556e0c2016-10-26 08:46:10 -07001440 parser.add_option(
vadimsh9c54b2c2017-07-25 14:08:29 -07001441 '--switch-to-account',
1442 help='If given, switches LUCI_CONTEXT to given logical service account '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001443 '(e.g. "task" or "system") before launching the isolated process.')
vadimsh9c54b2c2017-07-25 14:08:29 -07001444 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001445 '--output',
1446 action='append',
aludwin0a8e17d2016-10-27 15:57:39 -07001447 help='Specifies an output to return. If no outputs are specified, all '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001448 'files located in $(ISOLATED_OUTDIR) will be returned; '
1449 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those '
1450 'specified by --output option (there can be multiple) will be '
1451 'returned. Note that if a file in OUT_DIR has the same path '
1452 'as an --output option, the --output version will be returned.')
aludwin0a8e17d2016-10-27 15:57:39 -07001453 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001454 '-a',
1455 '--argsfile',
aludwin7556e0c2016-10-26 08:46:10 -07001456 # This is actually handled in parse_args; it's included here purely so it
1457 # can make it into the help text.
1458 help='Specify a file containing a JSON array of arguments to this '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001459 'script. If --argsfile is provided, no other argument may be '
1460 'provided on the command line.')
Takuto Ikutad4be2f12020-05-12 02:15:25 +00001461 parser.add_option(
1462 '--report-on-exception',
1463 action='store_true',
1464 help='Whether report exception during execution to isolate server. '
1465 'This flag should only be used in swarming bot.')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001466
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001467 group = optparse.OptionGroup(parser, 'Data source - Isolate server')
Junji Watanabe54925c32020-09-08 00:56:18 +00001468 # Deprecated. Isoate server is being migrated to RBE-CAS.
1469 # Remove --isolated and isolate server options after migration.
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001470 group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -05001471 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -07001472 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001473 isolateserver.add_isolate_server_options(group)
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001474 parser.add_option_group(group)
1475
1476 group = optparse.OptionGroup(parser,
1477 'Data source - Content Addressed Storage')
Junji Watanabe54925c32020-09-08 00:56:18 +00001478 group.add_option(
1479 '--cas-instance', help='Full CAS instance name for input/output files.')
1480 group.add_option(
1481 '--cas-digest',
1482 help='Digest of the input root on RBE-CAS. The format is '
1483 '`{hash}/{size_bytes}`.')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001484 parser.add_option_group(group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001485
Junji Watanabeb03450b2020-09-25 05:09:27 +00001486 # Cache options.
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -04001487 isolateserver.add_cache_options(parser)
Junji Watanabeb03450b2020-09-25 05:09:27 +00001488 add_cas_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001489
1490 cipd.add_cipd_options(parser)
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001491
1492 group = optparse.OptionGroup(parser, 'Named caches')
1493 group.add_option(
1494 '--named-cache',
1495 dest='named_caches',
1496 action='append',
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001497 nargs=3,
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001498 default=[],
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001499 help='A named cache to request. Accepts 3 arguments: name, path, hint. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001500 'name identifies the cache, must match regex [a-z0-9_]{1,4096}. '
1501 'path is a path relative to the run dir where the cache directory '
1502 'must be put to. '
1503 'This option can be specified more than once.')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001504 group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001505 '--named-cache-root',
1506 default='named_caches',
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001507 help='Cache root directory. Default=%default')
1508 parser.add_option_group(group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001509
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001510 group = optparse.OptionGroup(parser, 'Process containment')
1511 parser.add_option(
1512 '--lower-priority', action='store_true',
1513 help='Lowers the child process priority')
1514 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001515 '--containment-type',
1516 choices=('NONE', 'AUTO', 'JOB_OBJECT'),
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001517 default='NONE',
1518 help='Type of container to use')
1519 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001520 '--limit-processes',
1521 type='int',
1522 default=0,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001523 help='Maximum number of active processes in the containment')
1524 parser.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001525 '--limit-total-committed-memory',
1526 type='int',
1527 default=0,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001528 help='Maximum sum of committed memory in the containment')
1529 parser.add_option_group(group)
1530
1531 group = optparse.OptionGroup(parser, 'Debugging')
1532 group.add_option(
Kenneth Russell61d42352014-09-15 11:41:16 -07001533 '--leak-temp-dir',
1534 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -07001535 help='Deliberately leak isolate\'s temp dir for later examination. '
Junji Watanabe38b28b02020-04-23 10:23:30 +00001536 'Default: %default')
1537 group.add_option('--root-dir', help='Use a directory instead of a random one')
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001538 parser.add_option_group(group)
Kenneth Russell61d42352014-09-15 11:41:16 -07001539
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001540 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -07001541
Ye Kuang1d096cb2020-06-26 08:38:21 +00001542 parser.set_defaults(cache='cache')
nodirbe642ff2016-06-09 15:51:51 -07001543 return parser
1544
1545
Junji Watanabeb03450b2020-09-25 05:09:27 +00001546def add_cas_cache_options(parser):
1547 group = optparse.OptionGroup(parser, 'CAS cache management')
1548 group.add_option(
1549 '--cas-cache',
1550 metavar='DIR',
1551 default='cas-cache',
1552 help='Directory to keep a local cache of the files. Accelerates download '
1553 'by reusing already downloaded files. Default=%default')
1554 parser.add_option_group(group)
1555
1556
1557def process_cas_cache_options(options):
1558 if options.cas_cache:
1559 policies = local_caching.CachePolicies(
1560 max_cache_size=options.max_cache_size,
1561 min_free_space=options.min_free_space,
1562 # max_items isn't used for CAS cache for now.
1563 max_items=None,
1564 max_age_secs=MAX_AGE_SECS)
1565
1566 return local_caching.DiskContentAddressedCache(
1567 six.text_type(os.path.abspath(options.cas_cache)), policies, trim=False)
1568 return local_caching.MemoryContentAddressedCache()
1569
1570
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001571def process_named_cache_options(parser, options, time_fn=None):
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001572 """Validates named cache options and returns a CacheManager."""
1573 if options.named_caches and not options.named_cache_root:
1574 parser.error('--named-cache is specified, but --named-cache-root is empty')
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001575 for name, path, hint in options.named_caches:
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001576 if not CACHE_NAME_RE.match(name):
1577 parser.error(
1578 'cache name %r does not match %r' % (name, CACHE_NAME_RE.pattern))
1579 if not path:
1580 parser.error('cache path cannot be empty')
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001581 try:
Takuto Ikuta630f99d2020-07-02 12:59:35 +00001582 int(hint)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001583 except ValueError:
1584 parser.error('cache hint must be a number')
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001585 if options.named_cache_root:
1586 # Make these configurable later if there is use case but for now it's fairly
1587 # safe values.
1588 # In practice, a fair chunk of bots are already recycled on a daily schedule
1589 # so this code doesn't have any effect to them, unless they are preloaded
1590 # with a really old cache.
1591 policies = local_caching.CachePolicies(
1592 # 1TiB.
1593 max_cache_size=1024*1024*1024*1024,
1594 min_free_space=options.min_free_space,
1595 max_items=50,
Marc-Antoine Ruel5d7606b2018-06-15 19:06:12 +00001596 max_age_secs=MAX_AGE_SECS)
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001597 root_dir = six.text_type(os.path.abspath(options.named_cache_root))
John Budorickc6186972020-02-26 00:58:14 +00001598 cache = local_caching.NamedCache(root_dir, policies, time_fn=time_fn)
1599 # Touch any named caches we're going to use to minimize thrashing
1600 # between tasks that request some (but not all) of the same named caches.
John Budorick0a4dab62020-03-02 22:23:35 +00001601 cache.touch(*[name for name, _, _ in options.named_caches])
John Budorickc6186972020-02-26 00:58:14 +00001602 return cache
Marc-Antoine Ruel8b11dbd2018-05-18 14:31:22 -04001603 return None
1604
1605
aludwin7556e0c2016-10-26 08:46:10 -07001606def parse_args(args):
1607 # Create a fake mini-parser just to get out the "-a" command. Note that
1608 # it's not documented here; instead, it's documented in create_option_parser
1609 # even though that parser will never actually get to parse it. This is
1610 # because --argsfile is exclusive with all other options and arguments.
1611 file_argparse = argparse.ArgumentParser(add_help=False)
1612 file_argparse.add_argument('-a', '--argsfile')
1613 (file_args, nonfile_args) = file_argparse.parse_known_args(args)
1614 if file_args.argsfile:
1615 if nonfile_args:
1616 file_argparse.error('Can\'t specify --argsfile with'
1617 'any other arguments (%s)' % nonfile_args)
1618 try:
1619 with open(file_args.argsfile, 'r') as f:
1620 args = json.load(f)
1621 except (IOError, OSError, ValueError) as e:
1622 # We don't need to error out here - "args" is now empty,
1623 # so the call below to parser.parse_args(args) will fail
1624 # and print the full help text.
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +00001625 print('Couldn\'t read arguments: %s' % e, file=sys.stderr)
aludwin7556e0c2016-10-26 08:46:10 -07001626
1627 # Even if we failed to read the args, just call the normal parser now since it
1628 # will print the correct help message.
nodirbe642ff2016-06-09 15:51:51 -07001629 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001630 options, args = parser.parse_args(args)
Ye Kuangfff1e502020-07-13 13:21:57 +00001631 if not isinstance(options.cipd_enabled, (bool, int)):
1632 options.cipd_enabled = distutils.util.strtobool(options.cipd_enabled)
aludwin7556e0c2016-10-26 08:46:10 -07001633 return (parser, options, args)
1634
1635
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001636def _calc_named_cache_hint(named_cache, named_caches):
1637 """Returns the expected size of the missing named caches."""
1638 present = named_cache.available
1639 size = 0
1640 for name, _, hint in named_caches:
1641 if name not in present:
Takuto Ikuta630f99d2020-07-02 12:59:35 +00001642 hint = int(hint)
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001643 if hint > 0:
1644 size += hint
1645 return size
1646
1647
aludwin7556e0c2016-10-26 08:46:10 -07001648def main(args):
Marc-Antoine Ruelee6ca622017-11-29 11:19:16 -05001649 # Warning: when --argsfile is used, the strings are unicode instances, when
1650 # parsed normally, the strings are str instances.
aludwin7556e0c2016-10-26 08:46:10 -07001651 (parser, options, args) = parse_args(args)
maruel36a963d2016-04-08 17:15:49 -07001652
Takuto Ikutad4be2f12020-05-12 02:15:25 +00001653 if options.report_on_exception and options.isolate_server:
1654 on_error.report_on_exception_exit(options.isolate_server)
1655
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001656 if not file_path.enable_symlink():
Marc-Antoine Ruel5a024272019-01-15 20:11:16 +00001657 logging.warning('Symlink support is not enabled')
Marc-Antoine Ruel5028ba22017-08-25 17:37:51 -04001658
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001659 named_cache = process_named_cache_options(parser, options)
Marc-Antoine Ruel0d8b0f62018-09-10 14:40:35 +00001660 # hint is 0 if there's no named cache.
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001661 hint = _calc_named_cache_hint(named_cache, options.named_caches)
1662 if hint:
1663 # Increase the --min-free-space value by the hint, and recreate the
1664 # NamedCache instance so it gets the updated CachePolicy.
1665 options.min_free_space += hint
1666 named_cache = process_named_cache_options(parser, options)
1667
Takuto Ikuta5c59a842020-01-24 03:05:24 +00001668 # TODO(crbug.com/932396): Remove this.
Takuto Ikuta4a22c2c2020-06-05 02:02:23 +00001669 use_go_isolated = options.cipd_enabled
Takuto Ikuta5c59a842020-01-24 03:05:24 +00001670
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001671 # TODO(maruel): CIPD caches should be defined at an higher level here too, so
1672 # they can be cleaned the same way.
Takuto Ikutaf1c58442020-10-20 09:03:27 +00001673
1674 isolate_cache = isolateserver.process_cache_options(options, trim=False)
1675 cas_cache = process_cas_cache_options(options)
Takuto Ikuta00cf8fc2020-01-14 01:36:00 +00001676
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001677 caches = []
1678 if isolate_cache:
1679 caches.append(isolate_cache)
Junji Watanabeb03450b2020-09-25 05:09:27 +00001680 if cas_cache:
1681 caches.append(cas_cache)
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001682 if named_cache:
1683 caches.append(named_cache)
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001684 root = caches[0].cache_dir if caches else six.text_type(os.getcwd())
maruel36a963d2016-04-08 17:15:49 -07001685 if options.clean:
1686 if options.isolated:
1687 parser.error('Can\'t use --isolated with --clean.')
1688 if options.isolate_server:
1689 parser.error('Can\'t use --isolate-server with --clean.')
1690 if options.json:
1691 parser.error('Can\'t use --json with --clean.')
nodirf33b8d62016-10-26 22:34:58 -07001692 if options.named_caches:
1693 parser.error('Can\t use --named-cache with --clean.')
Junji Watanabeb19f54d2020-09-17 05:54:52 +00001694 if options.cas_instance or options.cas_digest:
1695 parser.error('Can\t use --cas-instance, --cas-digest with --clean.')
Takuto Ikuta9ab28552020-07-31 08:15:45 +00001696
1697 logging.info("initial free space: %d", file_path.get_free_space(root))
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001698 # Trim first, then clean.
1699 local_caching.trim_caches(
1700 caches,
1701 root,
Takuto Ikuta616ce262020-09-07 08:43:48 +00001702 min_free_space=options.min_free_space,
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001703 max_age_secs=MAX_AGE_SECS)
Takuto Ikuta9ab28552020-07-31 08:15:45 +00001704 logging.info("free space after trim: %d", file_path.get_free_space(root))
Marc-Antoine Ruel7139d912018-06-15 20:04:42 +00001705 for c in caches:
Marc-Antoine Ruel87fc2222018-06-18 13:09:24 +00001706 c.cleanup()
Takuto Ikuta9ab28552020-07-31 08:15:45 +00001707 logging.info("free space after cleanup: %d", file_path.get_free_space(root))
maruel36a963d2016-04-08 17:15:49 -07001708 return 0
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001709
1710 # Trim must still be done for the following case:
1711 # - named-cache was used
1712 # - some entries, with a large hint, where missing
1713 # - --min-free-space was increased accordingly, thus trimming is needed
1714 # Otherwise, this will have no effect, as bot_main calls run_isolated with
1715 # --clean after each task.
Takuto Ikutaa010c532020-10-21 05:42:29 +00001716 local_caching.trim_caches(
1717 caches,
1718 root,
1719 # Add 1GB more buffer for Go CLI.
1720 min_free_space=options.min_free_space + _FREE_SPACE_BUFFER_FOR_GO,
1721 max_age_secs=MAX_AGE_SECS)
maruel36a963d2016-04-08 17:15:49 -07001722
Takuto Ikutaf1c58442020-10-20 09:03:27 +00001723 # Save state of isolate/cas cache not to overwrite state from go client.
1724 if use_go_isolated:
1725 isolate_cache.save()
1726 isolate_cache = None
1727 if cas_cache:
1728 cas_cache.save()
1729 cas_cache = None
1730
nodir55be77b2016-05-03 09:39:57 -07001731 if not options.isolated and not args:
1732 parser.error('--isolated or command to run is required.')
1733
Vadim Shtayura5d1efce2014-02-04 10:55:43 -08001734 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -07001735
Takuto Ikutaae767b32020-05-11 01:22:19 +00001736 isolateserver.process_isolate_server_options(parser, options, False)
Junji Watanabeed9ce352020-09-25 12:32:07 +00001737 if ISOLATED_OUTDIR_PARAMETER in args and (not options.isolate_server and
1738 not options.cas_instance):
1739 parser.error('%s in args requires --isolate-server or --cas-instance' %
1740 ISOLATED_OUTDIR_PARAMETER)
1741
1742 if options.isolated and not options.isolate_server:
1743 parser.error('--isolated requires --isolate-server')
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001744
nodir90bc8dc2016-06-15 13:35:21 -07001745 if options.root_dir:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001746 options.root_dir = six.text_type(os.path.abspath(options.root_dir))
Takuto Ikutad46ea762020-10-07 05:43:22 +00001747 else:
1748 options.root_dir = six.text_type(tempfile.mkdtemp(prefix='root'))
maruel12e30012015-10-09 11:55:35 -07001749 if options.json:
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001750 options.json = six.text_type(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -07001751
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001752 if any('=' not in i for i in options.env):
1753 parser.error(
1754 '--env required key=value form. value can be skipped to delete '
1755 'the variable')
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001756 options.env = dict(i.split('=', 1) for i in options.env)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001757
1758 prefixes = {}
1759 cwd = os.path.realpath(os.getcwd())
1760 for item in options.env_prefix:
1761 if '=' not in item:
1762 parser.error(
1763 '--env-prefix %r is malformed, must be in the form `VAR=./path`'
1764 % item)
Marc-Antoine Ruel7a68f712017-12-01 18:45:18 -05001765 key, opath = item.split('=', 1)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001766 if os.path.isabs(opath):
1767 parser.error('--env-prefix %r path is bad, must be relative.' % opath)
1768 opath = os.path.normpath(opath)
1769 if not os.path.realpath(os.path.join(cwd, opath)).startswith(cwd):
1770 parser.error(
Junji Watanabe38b28b02020-04-23 10:23:30 +00001771 '--env-prefix %r path is bad, must be relative and not contain `..`.'
1772 % opath)
Marc-Antoine Ruel19dd8872017-11-28 18:33:39 -05001773 prefixes.setdefault(key, []).append(opath)
1774 options.env_prefix = prefixes
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001775
nodirbe642ff2016-06-09 15:51:51 -07001776 cipd.validate_cipd_options(parser, options)
1777
vadimsh232f5a82017-01-20 19:23:44 -08001778 install_packages_fn = noop_install_packages
Ye Kuang1d096cb2020-06-26 08:38:21 +00001779 tmp_cipd_cache_dir = None
vadimsh902948e2017-01-20 15:57:32 -08001780 if options.cipd_enabled:
Ye Kuang1d096cb2020-06-26 08:38:21 +00001781 cache_dir = options.cipd_cache
1782 if not cache_dir:
1783 tmp_cipd_cache_dir = six.text_type(tempfile.mkdtemp())
1784 cache_dir = tmp_cipd_cache_dir
Takuto Ikutab7ce0e32019-11-27 23:26:18 +00001785 install_packages_fn = (
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001786 lambda run_dir, isolated_dir, cas_dir: install_client_and_packages(
Ye Kuang1d096cb2020-06-26 08:38:21 +00001787 run_dir,
1788 cipd.parse_package_args(options.cipd_packages),
1789 options.cipd_server,
1790 options.cipd_client_package,
1791 options.cipd_client_version,
1792 cache_dir=cache_dir,
Junji Watanabe4b890ef2020-09-16 01:43:27 +00001793 isolated_dir=isolated_dir,
1794 cas_dir=cas_dir,
1795 ))
nodirbe642ff2016-06-09 15:51:51 -07001796
nodird6160682017-02-02 13:03:35 -08001797 @contextlib.contextmanager
nodir0ae98b32017-05-11 13:21:53 -07001798 def install_named_caches(run_dir):
nodird6160682017-02-02 13:03:35 -08001799 # WARNING: this function depends on "options" variable defined in the outer
1800 # function.
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001801 assert six.text_type(run_dir), repr(run_dir)
Marc-Antoine Ruel49f9f8d2018-05-24 15:57:06 -04001802 assert os.path.isabs(run_dir), run_dir
Takuto Ikuta6e2ff962019-10-29 12:35:27 +00001803 named_caches = [(os.path.join(run_dir, six.text_type(relpath)), name)
1804 for name, relpath, _ in options.named_caches]
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001805 for path, name in named_caches:
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001806 named_cache.install(path, name)
nodird6160682017-02-02 13:03:35 -08001807 try:
1808 yield
1809 finally:
dnje289d132017-07-07 11:16:44 -07001810 # Uninstall each named cache, returning it to the cache pool. If an
1811 # uninstall fails for a given cache, it will remain in the task's
1812 # temporary space, get cleaned up by the Swarming bot, and be lost.
1813 #
1814 # If the Swarming bot cannot clean up the cache, it will handle it like
1815 # any other bot file that could not be removed.
Marc-Antoine Ruelc7a704b2018-08-29 19:02:23 +00001816 for path, name in reversed(named_caches):
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001817 try:
Marc-Antoine Ruele9558372018-08-03 03:41:22 +00001818 # uninstall() doesn't trim but does call save() implicitly. Trimming
1819 # *must* be done manually via periodic 'run_isolated.py --clean'.
Marc-Antoine Ruele79ddbf2018-06-13 18:33:07 +00001820 named_cache.uninstall(path, name)
1821 except local_caching.NamedCacheError:
1822 logging.exception('Error while removing named cache %r at %r. '
1823 'The cache will be lost.', path, name)
nodirf33b8d62016-10-26 22:34:58 -07001824
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001825 extra_args = []
1826 command = []
1827 if options.raw_cmd:
1828 command = args
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001829 if options.relative_cwd:
1830 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1831 if not a.startswith(os.getcwd()):
1832 parser.error(
1833 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001834 else:
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001835 if options.relative_cwd:
1836 parser.error('--relative-cwd requires --raw-cmd')
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001837 extra_args = args
1838
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001839 containment_type = subprocess42.Containment.NONE
1840 if options.containment_type == 'AUTO':
1841 containment_type = subprocess42.Containment.AUTO
1842 if options.containment_type == 'JOB_OBJECT':
1843 containment_type = subprocess42.Containment.JOB_OBJECT
1844 containment = subprocess42.Containment(
1845 containment_type=containment_type,
1846 limit_processes=options.limit_processes,
1847 limit_total_committed_memory=options.limit_total_committed_memory)
1848
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001849 data = TaskData(
1850 command=command,
Marc-Antoine Ruel95068cf2017-12-07 21:35:05 -05001851 relative_cwd=options.relative_cwd,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001852 extra_args=extra_args,
1853 isolated_hash=options.isolated,
1854 storage=None,
1855 isolate_cache=isolate_cache,
Junji Watanabe54925c32020-09-08 00:56:18 +00001856 cas_instance=options.cas_instance,
1857 cas_digest=options.cas_digest,
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001858 outputs=options.output,
1859 install_named_caches=install_named_caches,
1860 leak_temp_dir=options.leak_temp_dir,
1861 root_dir=_to_unicode(options.root_dir),
1862 hard_timeout=options.hard_timeout,
1863 grace_period=options.grace_period,
1864 bot_file=options.bot_file,
1865 switch_to_account=options.switch_to_account,
1866 install_packages_fn=install_packages_fn,
Takuto Ikuta5c59a842020-01-24 03:05:24 +00001867 use_go_isolated=use_go_isolated,
Takuto Ikuta10cae642020-01-08 08:12:07 +00001868 go_cache_dir=options.cache,
Takuto Ikuta879788c2020-01-10 08:00:26 +00001869 go_cache_policies=local_caching.CachePolicies(
1870 max_cache_size=options.max_cache_size,
1871 min_free_space=options.min_free_space,
1872 max_items=options.max_items,
1873 max_age_secs=None,
1874 ),
Junji Watanabeb03450b2020-09-25 05:09:27 +00001875 cas_cache_dir=options.cas_cache,
1876 cas_cache_policies=local_caching.CachePolicies(
1877 max_cache_size=options.max_cache_size,
1878 min_free_space=options.min_free_space,
1879 max_items=None,
1880 max_age_secs=None,
1881 ),
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001882 env=options.env,
Marc-Antoine Ruel03c6fd12019-04-30 12:12:55 +00001883 env_prefix=options.env_prefix,
Marc-Antoine Ruel1b65f4e2019-05-02 21:56:58 +00001884 lower_priority=bool(options.lower_priority),
1885 containment=containment)
nodirbe642ff2016-06-09 15:51:51 -07001886 try:
nodir90bc8dc2016-06-15 13:35:21 -07001887 if options.isolate_server:
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001888 server_ref = isolate_storage.ServerRef(
nodir90bc8dc2016-06-15 13:35:21 -07001889 options.isolate_server, options.namespace)
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001890 storage = isolateserver.get_storage(server_ref)
nodir90bc8dc2016-06-15 13:35:21 -07001891 with storage:
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001892 data = data._replace(storage=storage)
nodirf33b8d62016-10-26 22:34:58 -07001893 # Hashing schemes used by |storage| and |isolate_cache| MUST match.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001894 assert storage.server_ref.hash_algo == server_ref.hash_algo
Marc-Antoine Ruel7de52592017-12-07 10:41:12 -05001895 return run_tha_test(data, options.json)
1896 return run_tha_test(data, options.json)
Junji Watanabe38b28b02020-04-23 10:23:30 +00001897 except (cipd.Error, local_caching.NamedCacheError,
1898 local_caching.NoMoreSpace) as ex:
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +00001899 print(ex.message, file=sys.stderr)
nodirbe642ff2016-06-09 15:51:51 -07001900 return 1
Ye Kuang1d096cb2020-06-26 08:38:21 +00001901 finally:
1902 if tmp_cipd_cache_dir is not None:
1903 try:
1904 file_path.rmtree(tmp_cipd_cache_dir)
1905 except OSError:
1906 logging.exception('Remove tmp_cipd_cache_dir=%s failed',
1907 tmp_cipd_cache_dir)
1908 # Best effort clean up. Failed to do so doesn't affect the outcome.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001909
1910
1911if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001912 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +00001913 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +00001914 fix_encoding.fix_encoding()
Ye Kuang2dd17442020-04-22 08:45:52 +00001915 net.set_user_agent('run_isolated.py/' + __version__)
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -05001916 sys.exit(main(sys.argv[1:]))