blob: 68a644f33cd723a30310faf6c649ddc64dc6522a [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.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000023"""
24
nodir90bc8dc2016-06-15 13:35:21 -070025__version__ = '0.8.1'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000026
maruel064c0a32016-04-05 11:47:15 -070027import base64
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000028import logging
29import optparse
30import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000031import sys
32import tempfile
maruel064c0a32016-04-05 11:47:15 -070033import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000034
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000035from third_party.depot_tools import fix_encoding
36
Vadim Shtayura6b555c12014-07-23 16:22:18 -070037from utils import file_path
maruel12e30012015-10-09 11:55:35 -070038from utils import fs
maruel064c0a32016-04-05 11:47:15 -070039from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040040from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040041from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050042from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000043from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000044from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000045
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080046import auth
nodirbe642ff2016-06-09 15:51:51 -070047import cipd
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000048import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000049
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000050
nodir55be77b2016-05-03 09:39:57 -070051ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
nodirbe642ff2016-06-09 15:51:51 -070052EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
bpastene3ae09522016-06-10 17:12:59 -070053SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
nodir55be77b2016-05-03 09:39:57 -070054
vadimsh@chromium.org85071062013-08-21 23:37:45 +000055# Absolute path to this file (can be None if running from zip on Mac).
56THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000057
58# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000059BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000060
61# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000062if zip_package.get_main_script_path():
63 MAIN_DIR = os.path.dirname(
64 os.path.abspath(zip_package.get_main_script_path()))
65else:
66 # This happens when 'import run_isolated' is executed at the python
67 # interactive prompt, in that case __file__ is undefined.
68 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000069
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000070# The name of the log file to use.
71RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
72
csharp@chromium.orge217f302012-11-22 16:51:53 +000073# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000074RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000075
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000076
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000077def get_as_zip_package(executable=True):
78 """Returns ZipPackage with this module and all its dependencies.
79
80 If |executable| is True will store run_isolated.py as __main__.py so that
81 zip package is directly executable be python.
82 """
83 # Building a zip package when running from another zip package is
84 # unsupported and probably unneeded.
85 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000086 assert THIS_FILE_PATH
87 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000088 package = zip_package.ZipPackage(root=BASE_DIR)
89 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040090 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000091 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080092 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
nodirbe642ff2016-06-09 15:51:51 -070093 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000094 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
95 package.add_directory(os.path.join(BASE_DIR, 'utils'))
96 return package
97
98
Vadim Shtayuracb0b7432015-07-31 13:26:50 -070099def make_temp_dir(prefix, root_dir=None):
100 """Returns a temporary directory.
101
102 If root_dir is given and /tmp is on same file system as root_dir, uses /tmp.
103 Otherwise makes a new temp directory under root_dir.
maruel79d5e062016-04-08 13:39:57 -0700104
105 Except on OSX, because it's dangerous to create hardlinks in $TMPDIR on OSX!
106 /System/Library/LaunchDaemons/com.apple.bsd.dirhelper.plist runs every day at
107 3:35am and deletes all files older than 3 days in $TMPDIR, but hardlinks do
108 not have the inode modification time updated, so they tend to be old, thus
109 they get deleted.
Vadim Shtayuracb0b7432015-07-31 13:26:50 -0700110 """
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000111 base_temp_dir = None
maruel79d5e062016-04-08 13:39:57 -0700112 real_temp_dir = unicode(tempfile.gettempdir())
113 if sys.platform == 'darwin':
114 # Nope! Nope! Nope!
115 assert root_dir, 'It is unsafe to create hardlinks in $TMPDIR'
116 base_temp_dir = root_dir
117 elif root_dir and not file_path.is_same_filesystem(root_dir, real_temp_dir):
Paweł Hajdan, Jrf7d58722015-04-27 14:54:42 +0200118 base_temp_dir = root_dir
marueleb5fbee2015-09-17 13:01:36 -0700119 return unicode(tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000120
121
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500122def change_tree_read_only(rootdir, read_only):
123 """Changes the tree read-only bits according to the read_only specification.
124
125 The flag can be 0, 1 or 2, which will affect the possibility to modify files
126 and create or delete files.
127 """
128 if read_only == 2:
129 # Files and directories (except on Windows) are marked read only. This
130 # inhibits modifying, creating or deleting files in the test directory,
131 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400132 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500133 elif read_only == 1:
134 # Files are marked read only but not the directories. This inhibits
135 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400136 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500137 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500138 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500139 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
140 # is not yet changed to verify the hash of the content of the files it is
141 # looking at, so that if a test modifies an input file, the file must be
142 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400143 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500144 else:
145 raise ValueError(
146 'change_tree_read_only(%s, %s): Unknown flag %s' %
147 (rootdir, read_only, read_only))
148
149
nodir90bc8dc2016-06-15 13:35:21 -0700150def process_command(command, out_dir, bot_file):
nodirbe642ff2016-06-09 15:51:51 -0700151 """Replaces variables in a command line.
152
153 Raises:
154 ValueError if a parameter is requested in |command| but its value is not
155 provided.
156 """
maruela9cfd6f2015-09-15 11:03:15 -0700157 def fix(arg):
nodirbe642ff2016-06-09 15:51:51 -0700158 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
159 replace_slash = False
nodir55be77b2016-05-03 09:39:57 -0700160 if ISOLATED_OUTDIR_PARAMETER in arg:
nodirbe642ff2016-06-09 15:51:51 -0700161 if not out_dir:
162 raise ValueError('out_dir is requested in command, but not provided')
nodir55be77b2016-05-03 09:39:57 -0700163 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
nodirbe642ff2016-06-09 15:51:51 -0700164 replace_slash = True
nodir90bc8dc2016-06-15 13:35:21 -0700165 if SWARMING_BOT_FILE_PARAMETER in arg:
166 if bot_file:
167 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
168 replace_slash = True
169 else:
170 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command, but no '
171 'bot_file specified. Leaving parameter unchanged.')
nodirbe642ff2016-06-09 15:51:51 -0700172 if replace_slash:
173 # Replace slashes only if parameters are present
nodir55be77b2016-05-03 09:39:57 -0700174 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
175 arg = arg.replace('/', os.sep)
maruela9cfd6f2015-09-15 11:03:15 -0700176 return arg
177
178 return [fix(arg) for arg in command]
179
180
maruel6be7f9e2015-10-01 12:25:30 -0700181def run_command(command, cwd, tmp_dir, hard_timeout, grace_period):
182 """Runs the command.
183
184 Returns:
185 tuple(process exit code, bool if had a hard timeout)
186 """
maruela9cfd6f2015-09-15 11:03:15 -0700187 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700188
189 env = os.environ.copy()
190 if sys.platform == 'darwin':
191 env['TMPDIR'] = tmp_dir.encode('ascii')
192 elif sys.platform == 'win32':
marueldf2329b2016-01-19 15:33:23 -0800193 env['TEMP'] = tmp_dir.encode('ascii')
marueleb5fbee2015-09-17 13:01:36 -0700194 else:
195 env['TMP'] = tmp_dir.encode('ascii')
maruel6be7f9e2015-10-01 12:25:30 -0700196 exit_code = None
197 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700198 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700199 proc = None
200 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700201 try:
maruel6be7f9e2015-10-01 12:25:30 -0700202 # TODO(maruel): This code is imperfect. It doesn't handle well signals
203 # during the download phase and there's short windows were things can go
204 # wrong.
205 def handler(signum, _frame):
206 if proc and not had_signal:
207 logging.info('Received signal %d', signum)
208 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700209 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700210
211 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
212 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
213 try:
214 exit_code = proc.wait(hard_timeout or None)
215 except subprocess42.TimeoutExpired:
216 if not had_signal:
217 logging.warning('Hard timeout')
218 had_hard_timeout = True
219 logging.warning('Sending SIGTERM')
220 proc.terminate()
221
222 # Ignore signals in grace period. Forcibly give the grace period to the
223 # child process.
224 if exit_code is None:
225 ignore = lambda *_: None
226 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
227 try:
228 exit_code = proc.wait(grace_period or None)
229 except subprocess42.TimeoutExpired:
230 # Now kill for real. The user can distinguish between the
231 # following states:
232 # - signal but process exited within grace period,
233 # hard_timed_out will be set but the process exit code will be
234 # script provided.
235 # - processed exited late, exit code will be -9 on posix.
236 logging.warning('Grace exhausted; sending SIGKILL')
237 proc.kill()
238 logging.info('Waiting for proces exit')
239 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700240 except OSError:
241 # This is not considered to be an internal error. The executable simply
242 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800243 sys.stderr.write(
244 '<The executable does not exist or a dependent library is missing>\n'
245 '<Check for missing .so/.dll in the .isolate or GN file>\n'
246 '<Command: %s>\n' % command)
247 if os.environ.get('SWARMING_TASK_ID'):
248 # Give an additional hint when running as a swarming task.
249 sys.stderr.write(
250 '<See the task\'s page for commands to help diagnose this issue '
251 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700252 exit_code = 1
253 logging.info(
254 'Command finished with exit code %d (%s)',
255 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700256 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700257
258
nodir6f801882016-04-29 14:41:50 -0700259def fetch_and_measure(isolated_hash, storage, cache, outdir):
260 """Fetches an isolated and returns (bundle, stats)."""
261 start = time.time()
262 bundle = isolateserver.fetch_isolated(
263 isolated_hash=isolated_hash,
264 storage=storage,
265 cache=cache,
266 outdir=outdir)
267 return bundle, {
268 'duration': time.time() - start,
269 'initial_number_items': cache.initial_number_items,
270 'initial_size': cache.initial_size,
271 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
272 'items_hot': base64.b64encode(
273 large.pack(sorted(set(cache.linked) - set(cache.added)))),
274 }
275
276
maruela9cfd6f2015-09-15 11:03:15 -0700277def delete_and_upload(storage, out_dir, leak_temp_dir):
278 """Deletes the temporary run directory and uploads results back.
279
280 Returns:
nodir6f801882016-04-29 14:41:50 -0700281 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700282 - outputs_ref: a dict referring to the results archived back to the isolated
283 server, if applicable.
284 - success: False if something occurred that means that the task must
285 forcibly be considered a failure, e.g. zombie processes were left
286 behind.
nodir6f801882016-04-29 14:41:50 -0700287 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700288 """
289
290 # Upload out_dir and generate a .isolated file out of this directory. It is
291 # only done if files were written in the directory.
292 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700293 cold = []
294 hot = []
nodir6f801882016-04-29 14:41:50 -0700295 start = time.time()
296
maruel12e30012015-10-09 11:55:35 -0700297 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700298 with tools.Profiler('ArchiveOutput'):
299 try:
maruel064c0a32016-04-05 11:47:15 -0700300 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700301 storage, [out_dir], None)
302 outputs_ref = {
303 'isolated': results[0][0],
304 'isolatedserver': storage.location,
305 'namespace': storage.namespace,
306 }
maruel064c0a32016-04-05 11:47:15 -0700307 cold = sorted(i.size for i in f_cold)
308 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700309 except isolateserver.Aborted:
310 # This happens when a signal SIGTERM was received while uploading data.
311 # There is 2 causes:
312 # - The task was too slow and was about to be killed anyway due to
313 # exceeding the hard timeout.
314 # - The amount of data uploaded back is very large and took too much
315 # time to archive.
316 sys.stderr.write('Received SIGTERM while uploading')
317 # Re-raise, so it will be treated as an internal failure.
318 raise
nodir6f801882016-04-29 14:41:50 -0700319
320 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700321 try:
maruel12e30012015-10-09 11:55:35 -0700322 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700323 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700324 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700325 else:
326 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700327 except OSError as e:
328 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700329 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700330 stats = {
331 'duration': time.time() - start,
332 'items_cold': base64.b64encode(large.pack(cold)),
333 'items_hot': base64.b64encode(large.pack(hot)),
334 }
335 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700336
337
marueleb5fbee2015-09-17 13:01:36 -0700338def map_and_run(
nodir55be77b2016-05-03 09:39:57 -0700339 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
nodir90bc8dc2016-06-15 13:35:21 -0700340 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn):
nodir55be77b2016-05-03 09:39:57 -0700341 """Runs a command with optional isolated input/output.
342
343 See run_tha_test for argument documentation.
344
345 Returns metadata about the result.
346 """
347 assert bool(command) ^ bool(isolated_hash)
maruela9cfd6f2015-09-15 11:03:15 -0700348 result = {
maruel064c0a32016-04-05 11:47:15 -0700349 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700350 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700351 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700352 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700353 'stats': {
nodir55715712016-06-03 12:28:19 -0700354 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700355 # 'cipd': {
356 # 'duration': 0.,
357 # 'get_client_duration': 0.,
358 # },
nodir55715712016-06-03 12:28:19 -0700359 # 'download': {
360 # 'duration': 0.,
361 # 'initial_number_items': 0,
362 # 'initial_size': 0,
363 # 'items_cold': '<large.pack()>',
364 # 'items_hot': '<large.pack()>',
365 # },
366 # 'upload': {
367 # 'duration': 0.,
368 # 'items_cold': '<large.pack()>',
369 # 'items_hot': '<large.pack()>',
370 # },
maruel064c0a32016-04-05 11:47:15 -0700371 # },
372 },
maruela9cfd6f2015-09-15 11:03:15 -0700373 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700374 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700375 }
nodirbe642ff2016-06-09 15:51:51 -0700376
marueleb5fbee2015-09-17 13:01:36 -0700377 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700378 file_path.ensure_tree(root_dir, 0700)
marueleb5fbee2015-09-17 13:01:36 -0700379 else:
380 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
nodirbe642ff2016-06-09 15:51:51 -0700381 run_dir = make_temp_dir(u'isolated_run', root_dir)
382 out_dir = make_temp_dir(u'isolated_out', root_dir) if storage else None
383 tmp_dir = make_temp_dir(u'isolated_tmp', root_dir)
nodir55be77b2016-05-03 09:39:57 -0700384 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700385
nodir55be77b2016-05-03 09:39:57 -0700386 try:
nodir90bc8dc2016-06-15 13:35:21 -0700387 cipd_stats = install_packages_fn(run_dir)
388 if cipd_stats:
389 result['stats']['cipd'] = cipd_stats
390
nodir55be77b2016-05-03 09:39:57 -0700391 if isolated_hash:
nodir55715712016-06-03 12:28:19 -0700392 isolated_stats = result['stats'].setdefault('isolated', {})
393 bundle, isolated_stats['download'] = fetch_and_measure(
nodir55be77b2016-05-03 09:39:57 -0700394 isolated_hash=isolated_hash,
395 storage=storage,
396 cache=cache,
397 outdir=run_dir)
398 if not bundle.command:
399 # Handle this as a task failure, not an internal failure.
400 sys.stderr.write(
401 '<The .isolated doesn\'t declare any command to run!>\n'
402 '<Check your .isolate for missing \'command\' variable>\n')
403 if os.environ.get('SWARMING_TASK_ID'):
404 # Give an additional hint when running as a swarming task.
405 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
406 result['exit_code'] = 1
407 return result
408
409 change_tree_read_only(run_dir, bundle.read_only)
410 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
411 command = bundle.command + extra_args
nodirbe642ff2016-06-09 15:51:51 -0700412
nodir34d673c2016-05-24 09:30:48 -0700413 command = tools.fix_python_path(command)
nodir90bc8dc2016-06-15 13:35:21 -0700414 command = process_command(command, out_dir, bot_file)
maruela9cfd6f2015-09-15 11:03:15 -0700415 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700416
maruel064c0a32016-04-05 11:47:15 -0700417 sys.stdout.flush()
418 start = time.time()
419 try:
420 result['exit_code'], result['had_hard_timeout'] = run_command(
nodirbe642ff2016-06-09 15:51:51 -0700421 command, cwd, tmp_dir, hard_timeout, grace_period)
maruel064c0a32016-04-05 11:47:15 -0700422 finally:
423 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700424 except Exception as e:
nodir90bc8dc2016-06-15 13:35:21 -0700425 # An internal error occurred. Report accordingly so the swarming task will
426 # be retried automatically.
maruel12e30012015-10-09 11:55:35 -0700427 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700428 result['internal_failure'] = str(e)
429 on_error.report(None)
430 finally:
431 try:
432 if leak_temp_dir:
433 logging.warning(
434 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700435 else:
maruel84537cb2015-10-16 14:21:28 -0700436 # On Windows rmtree(run_dir) call above has a synchronization effect: it
437 # finishes only when all task child processes terminate (since a running
438 # process locks *.exe file). Examine out_dir only after that call
439 # completes (since child processes may write to out_dir too and we need
440 # to wait for them to finish).
441 if fs.isdir(run_dir):
442 try:
443 success = file_path.rmtree(run_dir)
444 except OSError as e:
445 logging.error('Failure with %s', e)
446 success = False
447 if not success:
448 print >> sys.stderr, (
449 'Failed to delete the run directory, forcibly failing\n'
450 'the task because of it. No zombie process can outlive a\n'
451 'successful task run and still be marked as successful.\n'
452 'Fix your stuff.')
453 if result['exit_code'] == 0:
454 result['exit_code'] = 1
455 if fs.isdir(tmp_dir):
456 try:
457 success = file_path.rmtree(tmp_dir)
458 except OSError as e:
459 logging.error('Failure with %s', e)
460 success = False
461 if not success:
462 print >> sys.stderr, (
463 'Failed to delete the temporary directory, forcibly failing\n'
464 'the task because of it. No zombie process can outlive a\n'
465 'successful task run and still be marked as successful.\n'
466 'Fix your stuff.')
467 if result['exit_code'] == 0:
468 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700469
marueleb5fbee2015-09-17 13:01:36 -0700470 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700471 if out_dir:
nodir55715712016-06-03 12:28:19 -0700472 isolated_stats = result['stats'].setdefault('isolated', {})
473 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700474 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700475 if not success and result['exit_code'] == 0:
476 result['exit_code'] = 1
477 except Exception as e:
478 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700479 if out_dir:
480 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700481 result['internal_failure'] = str(e)
482 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500483
484
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400485def run_tha_test(
nodir55be77b2016-05-03 09:39:57 -0700486 command, isolated_hash, storage, cache, leak_temp_dir, result_json,
bpastene3ae09522016-06-10 17:12:59 -0700487 root_dir, hard_timeout, grace_period, bot_file, extra_args,
nodir90bc8dc2016-06-15 13:35:21 -0700488 install_packages_fn):
nodir55be77b2016-05-03 09:39:57 -0700489 """Runs an executable and records execution metadata.
490
491 Either command or isolated_hash must be specified.
492
493 If isolated_hash is specified, downloads the dependencies in the cache,
494 hardlinks them into a temporary directory and runs the command specified in
495 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500496
497 A temporary directory is created to hold the output files. The content inside
498 this directory will be uploaded back to |storage| packaged as a .isolated
499 file.
500
501 Arguments:
nodir55be77b2016-05-03 09:39:57 -0700502 command: the command to run, a list of strings. Mutually exclusive with
503 isolated_hash.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500504 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500505 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700506 The command specified in the .isolated is executed.
507 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500508 storage: an isolateserver.Storage object to retrieve remote objects. This
509 object has a reference to an isolateserver.StorageApi, which does
510 the actual I/O.
511 cache: an isolateserver.LocalCache to keep from retrieving the same objects
512 constantly by caching the objects retrieved. Can be on-disk or
513 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700514 leak_temp_dir: if true, the temporary directory will be deliberately leaked
515 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700516 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700517 exit code is always 0 unless an internal error occurred.
nodir90bc8dc2016-06-15 13:35:21 -0700518 root_dir: path to the directory to use to create the temporary directory. If
marueleb5fbee2015-09-17 13:01:36 -0700519 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700520 hard_timeout: kills the process if it lasts more than this amount of
521 seconds.
522 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500523 extra_args: optional arguments to add to the command stated in the .isolate
nodir55be77b2016-05-03 09:39:57 -0700524 file. Ignored if isolate_hash is empty.
nodir90bc8dc2016-06-15 13:35:21 -0700525 install_packages_fn: function (dir) => cipd_stats. Installs packages.
maruela9cfd6f2015-09-15 11:03:15 -0700526
527 Returns:
528 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000529 """
nodir55be77b2016-05-03 09:39:57 -0700530 assert bool(command) ^ bool(isolated_hash)
531 extra_args = extra_args or []
nodirbe642ff2016-06-09 15:51:51 -0700532
nodir55be77b2016-05-03 09:39:57 -0700533 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
534 assert storage is not None, 'storage is None although outdir is specified'
535
maruela76b9ee2015-12-15 06:18:08 -0800536 if result_json:
537 # Write a json output file right away in case we get killed.
538 result = {
539 'exit_code': None,
540 'had_hard_timeout': False,
541 'internal_failure': 'Was terminated before completion',
542 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700543 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800544 }
545 tools.write_json(result_json, result, dense=True)
546
maruela9cfd6f2015-09-15 11:03:15 -0700547 # run_isolated exit code. Depends on if result_json is used or not.
548 result = map_and_run(
nodir55be77b2016-05-03 09:39:57 -0700549 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
nodir90bc8dc2016-06-15 13:35:21 -0700550 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn)
maruela9cfd6f2015-09-15 11:03:15 -0700551 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700552
maruela9cfd6f2015-09-15 11:03:15 -0700553 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700554 # We've found tests to delete 'work' when quitting, causing an exception
555 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700556 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700557 tools.write_json(result_json, result, dense=True)
558 # Only return 1 if there was an internal error.
559 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000560
maruela9cfd6f2015-09-15 11:03:15 -0700561 # Marshall into old-style inline output.
562 if result['outputs_ref']:
563 data = {
564 'hash': result['outputs_ref']['isolated'],
565 'namespace': result['outputs_ref']['namespace'],
566 'storage': result['outputs_ref']['isolatedserver'],
567 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500568 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700569 print(
570 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
571 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800572 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700573 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000574
575
nodir90bc8dc2016-06-15 13:35:21 -0700576def install_packages(
577 run_dir, package_list_file, service_url, client_package_name,
578 client_version, cache_dir=None, timeout=None):
579 """Installs packages. Returns stats.
nodirbe642ff2016-06-09 15:51:51 -0700580
581 Args:
nodir90bc8dc2016-06-15 13:35:21 -0700582 run_dir (str): root of installation.
583 package_list_file (str): path to a file with a list of packages to install.
nodirbe642ff2016-06-09 15:51:51 -0700584 service_url (str): CIPD server url, e.g.
585 "https://chrome-infra-packages.appspot.com."
nodir90bc8dc2016-06-15 13:35:21 -0700586 client_package_name (str): CIPD package name of CIPD client.
587 client_version (str): Version of CIPD client.
nodirbe642ff2016-06-09 15:51:51 -0700588 cache_dir (str): where to keep cache of cipd clients, packages and tags.
589 timeout: max duration in seconds that this function can take.
nodirbe642ff2016-06-09 15:51:51 -0700590 """
591 assert cache_dir
nodir90bc8dc2016-06-15 13:35:21 -0700592 if not package_list_file:
593 return None
594
nodirbe642ff2016-06-09 15:51:51 -0700595 timeoutfn = tools.sliding_timeout(timeout)
nodirbe642ff2016-06-09 15:51:51 -0700596 start = time.time()
nodirbe642ff2016-06-09 15:51:51 -0700597 cache_dir = os.path.abspath(cache_dir)
598
nodir90bc8dc2016-06-15 13:35:21 -0700599 run_dir = os.path.abspath(run_dir)
600 package_list = cipd.parse_package_list_file(package_list_file)
601
nodirbe642ff2016-06-09 15:51:51 -0700602 get_client_start = time.time()
603 client_manager = cipd.get_client(
604 service_url, client_package_name, client_version, cache_dir,
605 timeout=timeoutfn())
606 with client_manager as client:
607 get_client_duration = time.time() - get_client_start
nodir90bc8dc2016-06-15 13:35:21 -0700608 for path, packages in package_list.iteritems():
609 site_root = os.path.abspath(os.path.join(run_dir, path))
610 if not site_root.startswith(run_dir):
611 raise cipd.Error('Invalid CIPD package path "%s"' % path)
612
613 # Do not clean site_root before installation because it may contain other
614 # site roots.
615 file_path.ensure_tree(site_root, 0770)
nodirbe642ff2016-06-09 15:51:51 -0700616 client.ensure(
617 site_root, packages,
618 cache_dir=os.path.join(cache_dir, 'cipd_internal'),
619 timeout=timeoutfn())
nodirbe642ff2016-06-09 15:51:51 -0700620 file_path.make_tree_files_read_only(site_root)
nodir90bc8dc2016-06-15 13:35:21 -0700621
622 total_duration = time.time() - start
623 logging.info(
624 'Installing CIPD client and packages took %d seconds', total_duration)
625
626 return {
627 'duration': total_duration,
628 'get_client_duration': get_client_duration,
629 }
nodirbe642ff2016-06-09 15:51:51 -0700630
631
632def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400633 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700634 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000635 version=__version__,
636 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700637 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700638 '--clean', action='store_true',
639 help='Cleans the cache, trimming it necessary and remove corrupted items '
640 'and returns without executing anything; use with -v to know what '
641 'was done')
642 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700643 '--json',
644 help='dump output metadata to json file. When used, run_isolated returns '
645 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700646 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800647 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700648 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800649 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700650 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700651 parser.add_option(
652 '--bot-file',
653 help='Path to a file describing the state of the host. The content is '
654 'defined by on_before_task() in bot_config.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500655 data_group = optparse.OptionGroup(parser, 'Data source')
656 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500657 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700658 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500659 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500660 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000661
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400662 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700663
664 cipd.add_cipd_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000665
Kenneth Russell61d42352014-09-15 11:41:16 -0700666 debug_group = optparse.OptionGroup(parser, 'Debugging')
667 debug_group.add_option(
668 '--leak-temp-dir',
669 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700670 help='Deliberately leak isolate\'s temp dir for later examination. '
671 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700672 debug_group.add_option(
673 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700674 parser.add_option_group(debug_group)
675
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800676 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700677
678 parser.set_defaults(cache='cache', cipd_cache='cipd_cache')
679 return parser
680
681
682def main(args):
683 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500684 options, args = parser.parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700685
686 cache = isolateserver.process_cache_options(options)
687 if options.clean:
688 if options.isolated:
689 parser.error('Can\'t use --isolated with --clean.')
690 if options.isolate_server:
691 parser.error('Can\'t use --isolate-server with --clean.')
692 if options.json:
693 parser.error('Can\'t use --json with --clean.')
694 cache.cleanup()
695 return 0
696
nodir55be77b2016-05-03 09:39:57 -0700697 if not options.isolated and not args:
698 parser.error('--isolated or command to run is required.')
699
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800700 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700701
702 isolateserver.process_isolate_server_options(
703 parser, options, True, False)
704 if not options.isolate_server:
705 if options.isolated:
706 parser.error('--isolated requires --isolate-server')
707 if ISOLATED_OUTDIR_PARAMETER in args:
708 parser.error(
709 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000710
nodir90bc8dc2016-06-15 13:35:21 -0700711 if options.root_dir:
712 options.root_dir = unicode(os.path.abspath(options.root_dir))
maruel12e30012015-10-09 11:55:35 -0700713 if options.json:
714 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700715
nodirbe642ff2016-06-09 15:51:51 -0700716 cipd.validate_cipd_options(parser, options)
717
nodir90bc8dc2016-06-15 13:35:21 -0700718 install_packages_fn = lambda run_dir: install_packages(
719 run_dir, options.cipd_package_list, options.cipd_server,
720 options.cipd_client_package, options.cipd_client_version,
721 cache_dir=options.cipd_cache)
nodirbe642ff2016-06-09 15:51:51 -0700722
723 try:
nodir90bc8dc2016-06-15 13:35:21 -0700724 command = [] if options.isolated else args
725 if options.isolate_server:
726 storage = isolateserver.get_storage(
727 options.isolate_server, options.namespace)
728 with storage:
729 # Hashing schemes used by |storage| and |cache| MUST match.
730 assert storage.hash_algo == cache.hash_algo
nodirbe642ff2016-06-09 15:51:51 -0700731 return run_tha_test(
nodir90bc8dc2016-06-15 13:35:21 -0700732 command, options.isolated, storage, cache, options.leak_temp_dir,
733 options.json, options.root_dir, options.hard_timeout,
734 options.grace_period, options.bot_file, args, install_packages_fn)
735 else:
736 return run_tha_test(
737 command, options.isolated, None, 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)
nodirbe642ff2016-06-09 15:51:51 -0700740 except cipd.Error as ex:
741 print >> sys.stderr, ex.message
742 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000743
744
745if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -0700746 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000747 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000748 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500749 sys.exit(main(sys.argv[1:]))