blob: cbfae61721827bdd85c5c9a160ae6949238df942 [file] [log] [blame]
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2012 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00005
nodir55be77b2016-05-03 09:39:57 -07006"""Runs a command with optional isolated input/output.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +00007
nodir55be77b2016-05-03 09:39:57 -07008Despite name "run_isolated", can run a generic non-isolated command specified as
9args.
10
11If input isolated hash is provided, fetches it, creates a tree of hard links,
12appends args to the command in the fetched isolated and runs it.
13To improve performance, keeps a local cache.
14The local cache can safely be deleted.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -050015
nodirbe642ff2016-06-09 15:51:51 -070016Any ${EXECUTABLE_SUFFIX} on the command line will be replaced with ".exe" string
17on Windows and "" on other platforms.
18
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -050019Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
20temporary directory upon execution of the command specified in the .isolated
21file. All content written to this directory will be uploaded upon termination
22and the .isolated file describing this directory will be printed to stdout.
bpastene447c1992016-06-20 15:21:47 -070023
24Any ${SWARMING_BOT_FILE} on the command line will be replaced by the value of
25the --bot-file parameter. This file is used by a swarming bot to communicate
26state of the host to tasks. It is written to by the swarming bot's
27on_before_task() hook in the swarming server's custom bot_config.py.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000028"""
29
nodir90bc8dc2016-06-15 13:35:21 -070030__version__ = '0.8.1'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000031
maruel064c0a32016-04-05 11:47:15 -070032import base64
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000033import logging
34import optparse
35import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000036import sys
37import tempfile
maruel064c0a32016-04-05 11:47:15 -070038import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000039
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000040from third_party.depot_tools import fix_encoding
41
Vadim Shtayura6b555c12014-07-23 16:22:18 -070042from utils import file_path
maruel12e30012015-10-09 11:55:35 -070043from utils import fs
maruel064c0a32016-04-05 11:47:15 -070044from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040045from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040046from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050047from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000048from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000049from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000050
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080051import auth
nodirbe642ff2016-06-09 15:51:51 -070052import cipd
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000053import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000054
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000055
nodir55be77b2016-05-03 09:39:57 -070056ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
nodirbe642ff2016-06-09 15:51:51 -070057EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
bpastene3ae09522016-06-10 17:12:59 -070058SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
nodir55be77b2016-05-03 09:39:57 -070059
vadimsh@chromium.org85071062013-08-21 23:37:45 +000060# Absolute path to this file (can be None if running from zip on Mac).
61THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000062
63# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000064BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000065
66# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000067if zip_package.get_main_script_path():
68 MAIN_DIR = os.path.dirname(
69 os.path.abspath(zip_package.get_main_script_path()))
70else:
71 # This happens when 'import run_isolated' is executed at the python
72 # interactive prompt, in that case __file__ is undefined.
73 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000074
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000075# The name of the log file to use.
76RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
77
csharp@chromium.orge217f302012-11-22 16:51:53 +000078# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000079RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000080
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000081
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000082def get_as_zip_package(executable=True):
83 """Returns ZipPackage with this module and all its dependencies.
84
85 If |executable| is True will store run_isolated.py as __main__.py so that
86 zip package is directly executable be python.
87 """
88 # Building a zip package when running from another zip package is
89 # unsupported and probably unneeded.
90 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000091 assert THIS_FILE_PATH
92 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000093 package = zip_package.ZipPackage(root=BASE_DIR)
94 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040095 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000096 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080097 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
nodirbe642ff2016-06-09 15:51:51 -070098 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000099 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
100 package.add_directory(os.path.join(BASE_DIR, 'utils'))
101 return package
102
103
Vadim Shtayuracb0b7432015-07-31 13:26:50 -0700104def make_temp_dir(prefix, root_dir=None):
105 """Returns a temporary directory.
106
107 If root_dir is given and /tmp is on same file system as root_dir, uses /tmp.
108 Otherwise makes a new temp directory under root_dir.
maruel79d5e062016-04-08 13:39:57 -0700109
110 Except on OSX, because it's dangerous to create hardlinks in $TMPDIR on OSX!
111 /System/Library/LaunchDaemons/com.apple.bsd.dirhelper.plist runs every day at
112 3:35am and deletes all files older than 3 days in $TMPDIR, but hardlinks do
113 not have the inode modification time updated, so they tend to be old, thus
114 they get deleted.
Vadim Shtayuracb0b7432015-07-31 13:26:50 -0700115 """
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000116 base_temp_dir = None
maruel79d5e062016-04-08 13:39:57 -0700117 real_temp_dir = unicode(tempfile.gettempdir())
118 if sys.platform == 'darwin':
119 # Nope! Nope! Nope!
120 assert root_dir, 'It is unsafe to create hardlinks in $TMPDIR'
121 base_temp_dir = root_dir
122 elif root_dir and not file_path.is_same_filesystem(root_dir, real_temp_dir):
Paweł Hajdan, Jrf7d58722015-04-27 14:54:42 +0200123 base_temp_dir = root_dir
marueleb5fbee2015-09-17 13:01:36 -0700124 return unicode(tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000125
126
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500127def change_tree_read_only(rootdir, read_only):
128 """Changes the tree read-only bits according to the read_only specification.
129
130 The flag can be 0, 1 or 2, which will affect the possibility to modify files
131 and create or delete files.
132 """
133 if read_only == 2:
134 # Files and directories (except on Windows) are marked read only. This
135 # inhibits modifying, creating or deleting files in the test directory,
136 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400137 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500138 elif read_only == 1:
139 # Files are marked read only but not the directories. This inhibits
140 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400141 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500142 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500143 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500144 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
145 # is not yet changed to verify the hash of the content of the files it is
146 # looking at, so that if a test modifies an input file, the file must be
147 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400148 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500149 else:
150 raise ValueError(
151 'change_tree_read_only(%s, %s): Unknown flag %s' %
152 (rootdir, read_only, read_only))
153
154
nodir90bc8dc2016-06-15 13:35:21 -0700155def process_command(command, out_dir, bot_file):
nodirbe642ff2016-06-09 15:51:51 -0700156 """Replaces variables in a command line.
157
158 Raises:
159 ValueError if a parameter is requested in |command| but its value is not
160 provided.
161 """
maruela9cfd6f2015-09-15 11:03:15 -0700162 def fix(arg):
nodirbe642ff2016-06-09 15:51:51 -0700163 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
164 replace_slash = False
nodir55be77b2016-05-03 09:39:57 -0700165 if ISOLATED_OUTDIR_PARAMETER in arg:
nodirbe642ff2016-06-09 15:51:51 -0700166 if not out_dir:
167 raise ValueError('out_dir is requested in command, but not provided')
nodir55be77b2016-05-03 09:39:57 -0700168 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
nodirbe642ff2016-06-09 15:51:51 -0700169 replace_slash = True
nodir90bc8dc2016-06-15 13:35:21 -0700170 if SWARMING_BOT_FILE_PARAMETER in arg:
171 if bot_file:
172 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
173 replace_slash = True
174 else:
175 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command, but no '
176 'bot_file specified. Leaving parameter unchanged.')
nodirbe642ff2016-06-09 15:51:51 -0700177 if replace_slash:
178 # Replace slashes only if parameters are present
nodir55be77b2016-05-03 09:39:57 -0700179 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
180 arg = arg.replace('/', os.sep)
maruela9cfd6f2015-09-15 11:03:15 -0700181 return arg
182
183 return [fix(arg) for arg in command]
184
185
maruel6be7f9e2015-10-01 12:25:30 -0700186def run_command(command, cwd, tmp_dir, hard_timeout, grace_period):
187 """Runs the command.
188
189 Returns:
190 tuple(process exit code, bool if had a hard timeout)
191 """
maruela9cfd6f2015-09-15 11:03:15 -0700192 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700193
194 env = os.environ.copy()
195 if sys.platform == 'darwin':
196 env['TMPDIR'] = tmp_dir.encode('ascii')
197 elif sys.platform == 'win32':
marueldf2329b2016-01-19 15:33:23 -0800198 env['TEMP'] = tmp_dir.encode('ascii')
marueleb5fbee2015-09-17 13:01:36 -0700199 else:
200 env['TMP'] = tmp_dir.encode('ascii')
maruel6be7f9e2015-10-01 12:25:30 -0700201 exit_code = None
202 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700203 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700204 proc = None
205 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700206 try:
maruel6be7f9e2015-10-01 12:25:30 -0700207 # TODO(maruel): This code is imperfect. It doesn't handle well signals
208 # during the download phase and there's short windows were things can go
209 # wrong.
210 def handler(signum, _frame):
211 if proc and not had_signal:
212 logging.info('Received signal %d', signum)
213 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700214 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700215
216 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
217 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
218 try:
219 exit_code = proc.wait(hard_timeout or None)
220 except subprocess42.TimeoutExpired:
221 if not had_signal:
222 logging.warning('Hard timeout')
223 had_hard_timeout = True
224 logging.warning('Sending SIGTERM')
225 proc.terminate()
226
227 # Ignore signals in grace period. Forcibly give the grace period to the
228 # child process.
229 if exit_code is None:
230 ignore = lambda *_: None
231 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
232 try:
233 exit_code = proc.wait(grace_period or None)
234 except subprocess42.TimeoutExpired:
235 # Now kill for real. The user can distinguish between the
236 # following states:
237 # - signal but process exited within grace period,
238 # hard_timed_out will be set but the process exit code will be
239 # script provided.
240 # - processed exited late, exit code will be -9 on posix.
241 logging.warning('Grace exhausted; sending SIGKILL')
242 proc.kill()
243 logging.info('Waiting for proces exit')
244 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700245 except OSError:
246 # This is not considered to be an internal error. The executable simply
247 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800248 sys.stderr.write(
249 '<The executable does not exist or a dependent library is missing>\n'
250 '<Check for missing .so/.dll in the .isolate or GN file>\n'
251 '<Command: %s>\n' % command)
252 if os.environ.get('SWARMING_TASK_ID'):
253 # Give an additional hint when running as a swarming task.
254 sys.stderr.write(
255 '<See the task\'s page for commands to help diagnose this issue '
256 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700257 exit_code = 1
258 logging.info(
259 'Command finished with exit code %d (%s)',
260 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700261 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700262
263
nodir6f801882016-04-29 14:41:50 -0700264def fetch_and_measure(isolated_hash, storage, cache, outdir):
265 """Fetches an isolated and returns (bundle, stats)."""
266 start = time.time()
267 bundle = isolateserver.fetch_isolated(
268 isolated_hash=isolated_hash,
269 storage=storage,
270 cache=cache,
271 outdir=outdir)
272 return bundle, {
273 'duration': time.time() - start,
274 'initial_number_items': cache.initial_number_items,
275 'initial_size': cache.initial_size,
276 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
277 'items_hot': base64.b64encode(
278 large.pack(sorted(set(cache.linked) - set(cache.added)))),
279 }
280
281
maruela9cfd6f2015-09-15 11:03:15 -0700282def delete_and_upload(storage, out_dir, leak_temp_dir):
283 """Deletes the temporary run directory and uploads results back.
284
285 Returns:
nodir6f801882016-04-29 14:41:50 -0700286 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700287 - outputs_ref: a dict referring to the results archived back to the isolated
288 server, if applicable.
289 - success: False if something occurred that means that the task must
290 forcibly be considered a failure, e.g. zombie processes were left
291 behind.
nodir6f801882016-04-29 14:41:50 -0700292 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700293 """
294
295 # Upload out_dir and generate a .isolated file out of this directory. It is
296 # only done if files were written in the directory.
297 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700298 cold = []
299 hot = []
nodir6f801882016-04-29 14:41:50 -0700300 start = time.time()
301
maruel12e30012015-10-09 11:55:35 -0700302 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700303 with tools.Profiler('ArchiveOutput'):
304 try:
maruel064c0a32016-04-05 11:47:15 -0700305 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700306 storage, [out_dir], None)
307 outputs_ref = {
308 'isolated': results[0][0],
309 'isolatedserver': storage.location,
310 'namespace': storage.namespace,
311 }
maruel064c0a32016-04-05 11:47:15 -0700312 cold = sorted(i.size for i in f_cold)
313 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700314 except isolateserver.Aborted:
315 # This happens when a signal SIGTERM was received while uploading data.
316 # There is 2 causes:
317 # - The task was too slow and was about to be killed anyway due to
318 # exceeding the hard timeout.
319 # - The amount of data uploaded back is very large and took too much
320 # time to archive.
321 sys.stderr.write('Received SIGTERM while uploading')
322 # Re-raise, so it will be treated as an internal failure.
323 raise
nodir6f801882016-04-29 14:41:50 -0700324
325 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700326 try:
maruel12e30012015-10-09 11:55:35 -0700327 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700328 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700329 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700330 else:
331 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700332 except OSError as e:
333 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700334 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700335 stats = {
336 'duration': time.time() - start,
337 'items_cold': base64.b64encode(large.pack(cold)),
338 'items_hot': base64.b64encode(large.pack(hot)),
339 }
340 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700341
342
marueleb5fbee2015-09-17 13:01:36 -0700343def map_and_run(
nodir55be77b2016-05-03 09:39:57 -0700344 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
nodir90bc8dc2016-06-15 13:35:21 -0700345 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn):
nodir55be77b2016-05-03 09:39:57 -0700346 """Runs a command with optional isolated input/output.
347
348 See run_tha_test for argument documentation.
349
350 Returns metadata about the result.
351 """
352 assert bool(command) ^ bool(isolated_hash)
maruela9cfd6f2015-09-15 11:03:15 -0700353 result = {
maruel064c0a32016-04-05 11:47:15 -0700354 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700355 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700356 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700357 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700358 'stats': {
nodir55715712016-06-03 12:28:19 -0700359 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700360 # 'cipd': {
361 # 'duration': 0.,
362 # 'get_client_duration': 0.,
363 # },
nodir55715712016-06-03 12:28:19 -0700364 # 'download': {
365 # 'duration': 0.,
366 # 'initial_number_items': 0,
367 # 'initial_size': 0,
368 # 'items_cold': '<large.pack()>',
369 # 'items_hot': '<large.pack()>',
370 # },
371 # 'upload': {
372 # 'duration': 0.,
373 # 'items_cold': '<large.pack()>',
374 # 'items_hot': '<large.pack()>',
375 # },
maruel064c0a32016-04-05 11:47:15 -0700376 # },
377 },
maruela9cfd6f2015-09-15 11:03:15 -0700378 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700379 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700380 }
nodirbe642ff2016-06-09 15:51:51 -0700381
marueleb5fbee2015-09-17 13:01:36 -0700382 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700383 file_path.ensure_tree(root_dir, 0700)
marueleb5fbee2015-09-17 13:01:36 -0700384 else:
385 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
nodirbe642ff2016-06-09 15:51:51 -0700386 run_dir = make_temp_dir(u'isolated_run', root_dir)
387 out_dir = make_temp_dir(u'isolated_out', root_dir) if storage else None
388 tmp_dir = make_temp_dir(u'isolated_tmp', root_dir)
nodir55be77b2016-05-03 09:39:57 -0700389 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700390
nodir55be77b2016-05-03 09:39:57 -0700391 try:
nodir90bc8dc2016-06-15 13:35:21 -0700392 cipd_stats = install_packages_fn(run_dir)
393 if cipd_stats:
394 result['stats']['cipd'] = cipd_stats
395
nodir55be77b2016-05-03 09:39:57 -0700396 if isolated_hash:
nodir55715712016-06-03 12:28:19 -0700397 isolated_stats = result['stats'].setdefault('isolated', {})
398 bundle, isolated_stats['download'] = fetch_and_measure(
nodir55be77b2016-05-03 09:39:57 -0700399 isolated_hash=isolated_hash,
400 storage=storage,
401 cache=cache,
402 outdir=run_dir)
403 if not bundle.command:
404 # Handle this as a task failure, not an internal failure.
405 sys.stderr.write(
406 '<The .isolated doesn\'t declare any command to run!>\n'
407 '<Check your .isolate for missing \'command\' variable>\n')
408 if os.environ.get('SWARMING_TASK_ID'):
409 # Give an additional hint when running as a swarming task.
410 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
411 result['exit_code'] = 1
412 return result
413
414 change_tree_read_only(run_dir, bundle.read_only)
415 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
416 command = bundle.command + extra_args
nodirbe642ff2016-06-09 15:51:51 -0700417
nodir34d673c2016-05-24 09:30:48 -0700418 command = tools.fix_python_path(command)
nodir90bc8dc2016-06-15 13:35:21 -0700419 command = process_command(command, out_dir, bot_file)
maruela9cfd6f2015-09-15 11:03:15 -0700420 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700421
maruel064c0a32016-04-05 11:47:15 -0700422 sys.stdout.flush()
423 start = time.time()
424 try:
425 result['exit_code'], result['had_hard_timeout'] = run_command(
nodirbe642ff2016-06-09 15:51:51 -0700426 command, cwd, tmp_dir, hard_timeout, grace_period)
maruel064c0a32016-04-05 11:47:15 -0700427 finally:
428 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700429 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700430 # An internal error occurred. Report accordingly so the swarming task will
431 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700432 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700433 result['internal_failure'] = str(e)
434 on_error.report(None)
435 finally:
436 try:
437 if leak_temp_dir:
438 logging.warning(
439 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700440 else:
maruel84537cb2015-10-16 14:21:28 -0700441 # On Windows rmtree(run_dir) call above has a synchronization effect: it
442 # finishes only when all task child processes terminate (since a running
443 # process locks *.exe file). Examine out_dir only after that call
444 # completes (since child processes may write to out_dir too and we need
445 # to wait for them to finish).
446 if fs.isdir(run_dir):
447 try:
448 success = file_path.rmtree(run_dir)
449 except OSError as e:
450 logging.error('Failure with %s', e)
451 success = False
452 if not success:
453 print >> sys.stderr, (
454 'Failed to delete the run directory, forcibly failing\n'
455 'the task because of it. No zombie process can outlive a\n'
456 'successful task run and still be marked as successful.\n'
457 'Fix your stuff.')
458 if result['exit_code'] == 0:
459 result['exit_code'] = 1
460 if fs.isdir(tmp_dir):
461 try:
462 success = file_path.rmtree(tmp_dir)
463 except OSError as e:
464 logging.error('Failure with %s', e)
465 success = False
466 if not success:
467 print >> sys.stderr, (
468 'Failed to delete the temporary directory, forcibly failing\n'
469 'the task because of it. No zombie process can outlive a\n'
470 'successful task run and still be marked as successful.\n'
471 'Fix your stuff.')
472 if result['exit_code'] == 0:
473 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700474
marueleb5fbee2015-09-17 13:01:36 -0700475 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700476 if out_dir:
nodir55715712016-06-03 12:28:19 -0700477 isolated_stats = result['stats'].setdefault('isolated', {})
478 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700479 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700480 if not success and result['exit_code'] == 0:
481 result['exit_code'] = 1
482 except Exception as e:
483 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700484 if out_dir:
485 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700486 result['internal_failure'] = str(e)
487 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500488
489
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400490def run_tha_test(
nodir55be77b2016-05-03 09:39:57 -0700491 command, isolated_hash, storage, cache, leak_temp_dir, result_json,
bpastene3ae09522016-06-10 17:12:59 -0700492 root_dir, hard_timeout, grace_period, bot_file, extra_args,
nodir90bc8dc2016-06-15 13:35:21 -0700493 install_packages_fn):
nodir55be77b2016-05-03 09:39:57 -0700494 """Runs an executable and records execution metadata.
495
496 Either command or isolated_hash must be specified.
497
498 If isolated_hash is specified, downloads the dependencies in the cache,
499 hardlinks them into a temporary directory and runs the command specified in
500 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500501
502 A temporary directory is created to hold the output files. The content inside
503 this directory will be uploaded back to |storage| packaged as a .isolated
504 file.
505
506 Arguments:
nodir55be77b2016-05-03 09:39:57 -0700507 command: the command to run, a list of strings. Mutually exclusive with
508 isolated_hash.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500509 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500510 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700511 The command specified in the .isolated is executed.
512 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500513 storage: an isolateserver.Storage object to retrieve remote objects. This
514 object has a reference to an isolateserver.StorageApi, which does
515 the actual I/O.
516 cache: an isolateserver.LocalCache to keep from retrieving the same objects
517 constantly by caching the objects retrieved. Can be on-disk or
518 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700519 leak_temp_dir: if true, the temporary directory will be deliberately leaked
520 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700521 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700522 exit code is always 0 unless an internal error occurred.
nodir90bc8dc2016-06-15 13:35:21 -0700523 root_dir: path to the directory to use to create the temporary directory. If
marueleb5fbee2015-09-17 13:01:36 -0700524 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700525 hard_timeout: kills the process if it lasts more than this amount of
526 seconds.
527 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500528 extra_args: optional arguments to add to the command stated in the .isolate
nodir55be77b2016-05-03 09:39:57 -0700529 file. Ignored if isolate_hash is empty.
nodir90bc8dc2016-06-15 13:35:21 -0700530 install_packages_fn: function (dir) => cipd_stats. Installs packages.
maruela9cfd6f2015-09-15 11:03:15 -0700531
532 Returns:
533 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000534 """
nodir55be77b2016-05-03 09:39:57 -0700535 assert bool(command) ^ bool(isolated_hash)
536 extra_args = extra_args or []
nodirbe642ff2016-06-09 15:51:51 -0700537
nodir55be77b2016-05-03 09:39:57 -0700538 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
539 assert storage is not None, 'storage is None although outdir is specified'
540
maruela76b9ee2015-12-15 06:18:08 -0800541 if result_json:
542 # Write a json output file right away in case we get killed.
543 result = {
544 'exit_code': None,
545 'had_hard_timeout': False,
546 'internal_failure': 'Was terminated before completion',
547 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700548 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800549 }
550 tools.write_json(result_json, result, dense=True)
551
maruela9cfd6f2015-09-15 11:03:15 -0700552 # run_isolated exit code. Depends on if result_json is used or not.
553 result = map_and_run(
nodir55be77b2016-05-03 09:39:57 -0700554 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
nodir90bc8dc2016-06-15 13:35:21 -0700555 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn)
maruela9cfd6f2015-09-15 11:03:15 -0700556 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700557
maruela9cfd6f2015-09-15 11:03:15 -0700558 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700559 # We've found tests to delete 'work' when quitting, causing an exception
560 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700561 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700562 tools.write_json(result_json, result, dense=True)
563 # Only return 1 if there was an internal error.
564 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000565
maruela9cfd6f2015-09-15 11:03:15 -0700566 # Marshall into old-style inline output.
567 if result['outputs_ref']:
568 data = {
569 'hash': result['outputs_ref']['isolated'],
570 'namespace': result['outputs_ref']['namespace'],
571 'storage': result['outputs_ref']['isolatedserver'],
572 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500573 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700574 print(
575 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
576 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800577 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700578 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000579
580
nodir90bc8dc2016-06-15 13:35:21 -0700581def install_packages(
582 run_dir, package_list_file, service_url, client_package_name,
583 client_version, cache_dir=None, timeout=None):
584 """Installs packages. Returns stats.
nodirbe642ff2016-06-09 15:51:51 -0700585
586 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700587 run_dir (str): root of installation.
588 package_list_file (str): path to a file with a list of packages to install.
nodirbe642ff2016-06-09 15:51:51 -0700589 service_url (str): CIPD server url, e.g.
590 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700591 client_package_name (str): CIPD package name of CIPD client.
592 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700593 cache_dir (str): where to keep cache of cipd clients, packages and tags.
594 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700595 """
596 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700597 if not package_list_file:
598 return None
599
nodirbe642ff2016-06-09 15:51:51 -0700600 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700601 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700602 cache_dir = os.path.abspath(cache_dir)
603
nodir90bc8dc2016-06-15 13:35:21 -0700604 run_dir = os.path.abspath(run_dir)
605 package_list = cipd.parse_package_list_file(package_list_file)
606
nodirbe642ff2016-06-09 15:51:51 -0700607 get_client_start = time.time()
608 client_manager = cipd.get_client(
609 service_url, client_package_name, client_version, cache_dir,
610 timeout=timeoutfn())
611 with client_manager as client:
612 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700613 for path, packages in package_list.iteritems():
614 site_root = os.path.abspath(os.path.join(run_dir, path))
615 if not site_root.startswith(run_dir):
616 raise cipd.Error('Invalid CIPD package path "%s"' % path)
617
618 # Do not clean site_root before installation because it may contain other
619 # site roots.
620 file_path.ensure_tree(site_root, 0770)
nodirbe642ff2016-06-09 15:51:51 -0700621 client.ensure(
622 site_root, packages,
623 cache_dir=os.path.join(cache_dir, 'cipd_internal'),
624 timeout=timeoutfn())
nodirbe642ff2016-06-09 15:51:51 -0700625 file_path.make_tree_files_read_only(site_root)
nodir90bc8dc2016-06-15 13:35:21 -0700626
627 total_duration = time.time() - start
628 logging.info(
629 'Installing CIPD client and packages took %d seconds', total_duration)
630
631 return {
632 'duration': total_duration,
633 'get_client_duration': get_client_duration,
634 }
nodirbe642ff2016-06-09 15:51:51 -0700635
636
637def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400638 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700639 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000640 version=__version__,
641 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700642 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700643 '--clean', action='store_true',
644 help='Cleans the cache, trimming it necessary and remove corrupted items '
645 'and returns without executing anything; use with -v to know what '
646 'was done')
647 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700648 '--json',
649 help='dump output metadata to json file. When used, run_isolated returns '
650 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700651 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800652 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700653 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800654 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700655 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700656 parser.add_option(
657 '--bot-file',
658 help='Path to a file describing the state of the host. The content is '
659 'defined by on_before_task() in bot_config.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500660 data_group = optparse.OptionGroup(parser, 'Data source')
661 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500662 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700663 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500664 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500665 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000666
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400667 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700668
669 cipd.add_cipd_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000670
Kenneth Russell61d42352014-09-15 11:41:16 -0700671 debug_group = optparse.OptionGroup(parser, 'Debugging')
672 debug_group.add_option(
673 '--leak-temp-dir',
674 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700675 help='Deliberately leak isolate\'s temp dir for later examination. '
676 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700677 debug_group.add_option(
678 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700679 parser.add_option_group(debug_group)
680
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800681 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700682
683 parser.set_defaults(cache='cache', cipd_cache='cipd_cache')
684 return parser
685
686
687def main(args):
688 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500689 options, args = parser.parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700690
691 cache = isolateserver.process_cache_options(options)
692 if options.clean:
693 if options.isolated:
694 parser.error('Can\'t use --isolated with --clean.')
695 if options.isolate_server:
696 parser.error('Can\'t use --isolate-server with --clean.')
697 if options.json:
698 parser.error('Can\'t use --json with --clean.')
699 cache.cleanup()
700 return 0
701
nodir55be77b2016-05-03 09:39:57 -0700702 if not options.isolated and not args:
703 parser.error('--isolated or command to run is required.')
704
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800705 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700706
707 isolateserver.process_isolate_server_options(
708 parser, options, True, False)
709 if not options.isolate_server:
710 if options.isolated:
711 parser.error('--isolated requires --isolate-server')
712 if ISOLATED_OUTDIR_PARAMETER in args:
713 parser.error(
714 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000715
nodir90bc8dc2016-06-15 13:35:21 -0700716 if options.root_dir:
717 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -0700718 if options.json:
719 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700720
nodirbe642ff2016-06-09 15:51:51 -0700721 cipd.validate_cipd_options(parser, options)
722
nodir90bc8dc2016-06-15 13:35:21 -0700723 install_packages_fn = lambda run_dir: install_packages(
724 run_dir, options.cipd_package_list, options.cipd_server,
725 options.cipd_client_package, options.cipd_client_version,
726 cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -0700727
728 try:
nodir90bc8dc2016-06-15 13:35:21 -0700729 command = [] if options.isolated else args
730 if options.isolate_server:
731 storage = isolateserver.get_storage(
732 options.isolate_server, options.namespace)
733 with storage:
734 # Hashing schemes used by |storage| and |cache| MUST match.
735 assert storage.hash_algo == cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -0700736 return run_tha_test(
nodir90bc8dc2016-06-15 13:35:21 -0700737 command, options.isolated, storage, cache, options.leak_temp_dir,
738 options.json, options.root_dir, options.hard_timeout,
739 options.grace_period, options.bot_file, args, install_packages_fn)
740 else:
741 return run_tha_test(
742 command, options.isolated, None, cache, options.leak_temp_dir,
743 options.json, options.root_dir, options.hard_timeout,
744 options.grace_period, options.bot_file, args, install_packages_fn)
nodirbe642ff2016-06-09 15:51:51 -0700745 except cipd.Error as ex:
746 print >> sys.stderr, ex.message
747 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000748
749
750if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -0700751 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000752 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000753 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500754 sys.exit(main(sys.argv[1:]))