blob: 20454ea97f5ef3128d206a8734064b9d9d552d84 [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
19If at least one CIPD package was specified, any ${CIPD_PATH} on the command line
20will be replaced by location of a temporary directory that contains installed
21packages.
22
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -050023Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
24temporary directory upon execution of the command specified in the .isolated
25file. All content written to this directory will be uploaded upon termination
26and the .isolated file describing this directory will be printed to stdout.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000027"""
28
nodirbe642ff2016-06-09 15:51:51 -070029__version__ = '0.8.0'
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000030
maruel064c0a32016-04-05 11:47:15 -070031import base64
nodirbe642ff2016-06-09 15:51:51 -070032import contextlib
33import hashlib
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000034import logging
35import optparse
36import os
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000037import sys
38import tempfile
maruel064c0a32016-04-05 11:47:15 -070039import time
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +000040
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000041from third_party.depot_tools import fix_encoding
42
Vadim Shtayura6b555c12014-07-23 16:22:18 -070043from utils import file_path
maruel12e30012015-10-09 11:55:35 -070044from utils import fs
maruel064c0a32016-04-05 11:47:15 -070045from utils import large
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040046from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040047from utils import on_error
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -050048from utils import subprocess42
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000049from utils import tools
vadimsh@chromium.org3e97deb2013-08-24 00:56:44 +000050from utils import zip_package
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000051
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080052import auth
nodirbe642ff2016-06-09 15:51:51 -070053import cipd
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000054import isolateserver
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000055
vadimsh@chromium.orga4326472013-08-24 02:05:41 +000056
nodir55be77b2016-05-03 09:39:57 -070057ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
nodirbe642ff2016-06-09 15:51:51 -070058CIPD_PATH_PARAMETER = '${CIPD_PATH}'
59EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
bpastene3ae09522016-06-10 17:12:59 -070060SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}'
nodir55be77b2016-05-03 09:39:57 -070061
vadimsh@chromium.org85071062013-08-21 23:37:45 +000062# Absolute path to this file (can be None if running from zip on Mac).
63THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000064
65# Directory that contains this file (might be inside zip package).
vadimsh@chromium.org85071062013-08-21 23:37:45 +000066BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000067
68# Directory that contains currently running script file.
maruel@chromium.org814d23f2013-10-01 19:08:00 +000069if zip_package.get_main_script_path():
70 MAIN_DIR = os.path.dirname(
71 os.path.abspath(zip_package.get_main_script_path()))
72else:
73 # This happens when 'import run_isolated' is executed at the python
74 # interactive prompt, in that case __file__ is undefined.
75 MAIN_DIR = None
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000076
csharp@chromium.orgff2a4662012-11-21 20:49:32 +000077# The name of the log file to use.
78RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
79
csharp@chromium.orge217f302012-11-22 16:51:53 +000080# The name of the log to use for the run_test_cases.py command
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000081RUN_TEST_CASES_LOG = 'run_test_cases.log'
csharp@chromium.orge217f302012-11-22 16:51:53 +000082
vadimsh@chromium.org87d63262013-04-04 19:34:21 +000083
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000084def get_as_zip_package(executable=True):
85 """Returns ZipPackage with this module and all its dependencies.
86
87 If |executable| is True will store run_isolated.py as __main__.py so that
88 zip package is directly executable be python.
89 """
90 # Building a zip package when running from another zip package is
91 # unsupported and probably unneeded.
92 assert not zip_package.is_zipped_module(sys.modules[__name__])
vadimsh@chromium.org85071062013-08-21 23:37:45 +000093 assert THIS_FILE_PATH
94 assert BASE_DIR
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +000095 package = zip_package.ZipPackage(root=BASE_DIR)
96 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040097 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
maruel@chromium.orgdedbf492013-09-12 20:42:11 +000098 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080099 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
nodirbe642ff2016-06-09 15:51:51 -0700100 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
vadimsh@chromium.org8b9d56b2013-08-21 22:24:35 +0000101 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
102 package.add_directory(os.path.join(BASE_DIR, 'utils'))
103 return package
104
105
Vadim Shtayuracb0b7432015-07-31 13:26:50 -0700106def make_temp_dir(prefix, root_dir=None):
107 """Returns a temporary directory.
108
109 If root_dir is given and /tmp is on same file system as root_dir, uses /tmp.
110 Otherwise makes a new temp directory under root_dir.
maruel79d5e062016-04-08 13:39:57 -0700111
112 Except on OSX, because it's dangerous to create hardlinks in $TMPDIR on OSX!
113 /System/Library/LaunchDaemons/com.apple.bsd.dirhelper.plist runs every day at
114 3:35am and deletes all files older than 3 days in $TMPDIR, but hardlinks do
115 not have the inode modification time updated, so they tend to be old, thus
116 they get deleted.
Vadim Shtayuracb0b7432015-07-31 13:26:50 -0700117 """
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000118 base_temp_dir = None
maruel79d5e062016-04-08 13:39:57 -0700119 real_temp_dir = unicode(tempfile.gettempdir())
120 if sys.platform == 'darwin':
121 # Nope! Nope! Nope!
122 assert root_dir, 'It is unsafe to create hardlinks in $TMPDIR'
123 base_temp_dir = root_dir
124 elif root_dir and not file_path.is_same_filesystem(root_dir, real_temp_dir):
Paweł Hajdan, Jrf7d58722015-04-27 14:54:42 +0200125 base_temp_dir = root_dir
marueleb5fbee2015-09-17 13:01:36 -0700126 return unicode(tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000127
128
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500129def change_tree_read_only(rootdir, read_only):
130 """Changes the tree read-only bits according to the read_only specification.
131
132 The flag can be 0, 1 or 2, which will affect the possibility to modify files
133 and create or delete files.
134 """
135 if read_only == 2:
136 # Files and directories (except on Windows) are marked read only. This
137 # inhibits modifying, creating or deleting files in the test directory,
138 # except on Windows where creating and deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400139 file_path.make_tree_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500140 elif read_only == 1:
141 # Files are marked read only but not the directories. This inhibits
142 # modifying files but creating or deleting files is still possible.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400143 file_path.make_tree_files_read_only(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500144 elif read_only in (0, None):
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500145 # Anything can be modified.
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500146 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
147 # is not yet changed to verify the hash of the content of the files it is
148 # looking at, so that if a test modifies an input file, the file must be
149 # deleted.
Marc-Antoine Ruele4ad07e2014-10-15 20:22:29 -0400150 file_path.make_tree_writeable(rootdir)
Marc-Antoine Ruel7124e392014-01-09 11:49:21 -0500151 else:
152 raise ValueError(
153 'change_tree_read_only(%s, %s): Unknown flag %s' %
154 (rootdir, read_only, read_only))
155
156
bpastene3ae09522016-06-10 17:12:59 -0700157def process_command(command, out_dir, cipd_path, bot_file):
nodirbe642ff2016-06-09 15:51:51 -0700158 """Replaces variables in a command line.
159
160 Raises:
161 ValueError if a parameter is requested in |command| but its value is not
162 provided.
163 """
maruela9cfd6f2015-09-15 11:03:15 -0700164 def fix(arg):
nodirbe642ff2016-06-09 15:51:51 -0700165 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
166 replace_slash = False
167 if CIPD_PATH_PARAMETER in arg:
168 if not cipd_path:
169 raise ValueError('cipd_path is requested in command, but not provided')
170 arg = arg.replace(CIPD_PATH_PARAMETER, cipd_path)
171 replace_slash = True
nodir55be77b2016-05-03 09:39:57 -0700172 if ISOLATED_OUTDIR_PARAMETER in arg:
nodirbe642ff2016-06-09 15:51:51 -0700173 if not out_dir:
174 raise ValueError('out_dir is requested in command, but not provided')
nodir55be77b2016-05-03 09:39:57 -0700175 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
nodirbe642ff2016-06-09 15:51:51 -0700176 replace_slash = True
177 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)
bpastene3ae09522016-06-10 17:12:59 -0700181 if SWARMING_BOT_FILE_PARAMETER in arg:
182 if bot_file:
183 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file)
184 arg = arg.replace('/', os.sep)
185 else:
186 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command, but no '
187 'bot_file specified. Leaving paramater unchanged.')
maruela9cfd6f2015-09-15 11:03:15 -0700188 return arg
189
190 return [fix(arg) for arg in command]
191
192
maruel6be7f9e2015-10-01 12:25:30 -0700193def run_command(command, cwd, tmp_dir, hard_timeout, grace_period):
194 """Runs the command.
195
196 Returns:
197 tuple(process exit code, bool if had a hard timeout)
198 """
maruela9cfd6f2015-09-15 11:03:15 -0700199 logging.info('run_command(%s, %s)' % (command, cwd))
marueleb5fbee2015-09-17 13:01:36 -0700200
201 env = os.environ.copy()
202 if sys.platform == 'darwin':
203 env['TMPDIR'] = tmp_dir.encode('ascii')
204 elif sys.platform == 'win32':
marueldf2329b2016-01-19 15:33:23 -0800205 env['TEMP'] = tmp_dir.encode('ascii')
marueleb5fbee2015-09-17 13:01:36 -0700206 else:
207 env['TMP'] = tmp_dir.encode('ascii')
maruel6be7f9e2015-10-01 12:25:30 -0700208 exit_code = None
209 had_hard_timeout = False
maruela9cfd6f2015-09-15 11:03:15 -0700210 with tools.Profiler('RunTest'):
maruel6be7f9e2015-10-01 12:25:30 -0700211 proc = None
212 had_signal = []
maruela9cfd6f2015-09-15 11:03:15 -0700213 try:
maruel6be7f9e2015-10-01 12:25:30 -0700214 # TODO(maruel): This code is imperfect. It doesn't handle well signals
215 # during the download phase and there's short windows were things can go
216 # wrong.
217 def handler(signum, _frame):
218 if proc and not had_signal:
219 logging.info('Received signal %d', signum)
220 had_signal.append(True)
maruel556d9052015-10-05 11:12:44 -0700221 raise subprocess42.TimeoutExpired(command, None)
maruel6be7f9e2015-10-01 12:25:30 -0700222
223 proc = subprocess42.Popen(command, cwd=cwd, env=env, detached=True)
224 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, handler):
225 try:
226 exit_code = proc.wait(hard_timeout or None)
227 except subprocess42.TimeoutExpired:
228 if not had_signal:
229 logging.warning('Hard timeout')
230 had_hard_timeout = True
231 logging.warning('Sending SIGTERM')
232 proc.terminate()
233
234 # Ignore signals in grace period. Forcibly give the grace period to the
235 # child process.
236 if exit_code is None:
237 ignore = lambda *_: None
238 with subprocess42.set_signal_handler(subprocess42.STOP_SIGNALS, ignore):
239 try:
240 exit_code = proc.wait(grace_period or None)
241 except subprocess42.TimeoutExpired:
242 # Now kill for real. The user can distinguish between the
243 # following states:
244 # - signal but process exited within grace period,
245 # hard_timed_out will be set but the process exit code will be
246 # script provided.
247 # - processed exited late, exit code will be -9 on posix.
248 logging.warning('Grace exhausted; sending SIGKILL')
249 proc.kill()
250 logging.info('Waiting for proces exit')
251 exit_code = proc.wait()
maruela9cfd6f2015-09-15 11:03:15 -0700252 except OSError:
253 # This is not considered to be an internal error. The executable simply
254 # does not exit.
maruela72f46e2016-02-24 11:05:45 -0800255 sys.stderr.write(
256 '<The executable does not exist or a dependent library is missing>\n'
257 '<Check for missing .so/.dll in the .isolate or GN file>\n'
258 '<Command: %s>\n' % command)
259 if os.environ.get('SWARMING_TASK_ID'):
260 # Give an additional hint when running as a swarming task.
261 sys.stderr.write(
262 '<See the task\'s page for commands to help diagnose this issue '
263 'by reproducing the task locally>\n')
maruela9cfd6f2015-09-15 11:03:15 -0700264 exit_code = 1
265 logging.info(
266 'Command finished with exit code %d (%s)',
267 exit_code, hex(0xffffffff & exit_code))
maruel6be7f9e2015-10-01 12:25:30 -0700268 return exit_code, had_hard_timeout
maruela9cfd6f2015-09-15 11:03:15 -0700269
270
nodir6f801882016-04-29 14:41:50 -0700271def fetch_and_measure(isolated_hash, storage, cache, outdir):
272 """Fetches an isolated and returns (bundle, stats)."""
273 start = time.time()
274 bundle = isolateserver.fetch_isolated(
275 isolated_hash=isolated_hash,
276 storage=storage,
277 cache=cache,
278 outdir=outdir)
279 return bundle, {
280 'duration': time.time() - start,
281 'initial_number_items': cache.initial_number_items,
282 'initial_size': cache.initial_size,
283 'items_cold': base64.b64encode(large.pack(sorted(cache.added))),
284 'items_hot': base64.b64encode(
285 large.pack(sorted(set(cache.linked) - set(cache.added)))),
286 }
287
288
maruela9cfd6f2015-09-15 11:03:15 -0700289def delete_and_upload(storage, out_dir, leak_temp_dir):
290 """Deletes the temporary run directory and uploads results back.
291
292 Returns:
nodir6f801882016-04-29 14:41:50 -0700293 tuple(outputs_ref, success, stats)
maruel064c0a32016-04-05 11:47:15 -0700294 - outputs_ref: a dict referring to the results archived back to the isolated
295 server, if applicable.
296 - success: False if something occurred that means that the task must
297 forcibly be considered a failure, e.g. zombie processes were left
298 behind.
nodir6f801882016-04-29 14:41:50 -0700299 - stats: uploading stats.
maruela9cfd6f2015-09-15 11:03:15 -0700300 """
301
302 # Upload out_dir and generate a .isolated file out of this directory. It is
303 # only done if files were written in the directory.
304 outputs_ref = None
maruel064c0a32016-04-05 11:47:15 -0700305 cold = []
306 hot = []
nodir6f801882016-04-29 14:41:50 -0700307 start = time.time()
308
maruel12e30012015-10-09 11:55:35 -0700309 if fs.isdir(out_dir) and fs.listdir(out_dir):
maruela9cfd6f2015-09-15 11:03:15 -0700310 with tools.Profiler('ArchiveOutput'):
311 try:
maruel064c0a32016-04-05 11:47:15 -0700312 results, f_cold, f_hot = isolateserver.archive_files_to_storage(
maruela9cfd6f2015-09-15 11:03:15 -0700313 storage, [out_dir], None)
314 outputs_ref = {
315 'isolated': results[0][0],
316 'isolatedserver': storage.location,
317 'namespace': storage.namespace,
318 }
maruel064c0a32016-04-05 11:47:15 -0700319 cold = sorted(i.size for i in f_cold)
320 hot = sorted(i.size for i in f_hot)
maruela9cfd6f2015-09-15 11:03:15 -0700321 except isolateserver.Aborted:
322 # This happens when a signal SIGTERM was received while uploading data.
323 # There is 2 causes:
324 # - The task was too slow and was about to be killed anyway due to
325 # exceeding the hard timeout.
326 # - The amount of data uploaded back is very large and took too much
327 # time to archive.
328 sys.stderr.write('Received SIGTERM while uploading')
329 # Re-raise, so it will be treated as an internal failure.
330 raise
nodir6f801882016-04-29 14:41:50 -0700331
332 success = False
maruela9cfd6f2015-09-15 11:03:15 -0700333 try:
maruel12e30012015-10-09 11:55:35 -0700334 if (not leak_temp_dir and fs.isdir(out_dir) and
maruel6eeea7d2015-09-16 12:17:42 -0700335 not file_path.rmtree(out_dir)):
maruela9cfd6f2015-09-15 11:03:15 -0700336 logging.error('Had difficulties removing out_dir %s', out_dir)
nodir6f801882016-04-29 14:41:50 -0700337 else:
338 success = True
maruela9cfd6f2015-09-15 11:03:15 -0700339 except OSError as e:
340 # When this happens, it means there's a process error.
maruel12e30012015-10-09 11:55:35 -0700341 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
nodir6f801882016-04-29 14:41:50 -0700342 stats = {
343 'duration': time.time() - start,
344 'items_cold': base64.b64encode(large.pack(cold)),
345 'items_hot': base64.b64encode(large.pack(hot)),
346 }
347 return outputs_ref, success, stats
maruela9cfd6f2015-09-15 11:03:15 -0700348
349
marueleb5fbee2015-09-17 13:01:36 -0700350def map_and_run(
nodir55be77b2016-05-03 09:39:57 -0700351 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
bpastene3ae09522016-06-10 17:12:59 -0700352 hard_timeout, grace_period, bot_file, extra_args, cipd_path, cipd_stats):
nodir55be77b2016-05-03 09:39:57 -0700353 """Runs a command with optional isolated input/output.
354
355 See run_tha_test for argument documentation.
356
357 Returns metadata about the result.
358 """
359 assert bool(command) ^ bool(isolated_hash)
maruela9cfd6f2015-09-15 11:03:15 -0700360 result = {
maruel064c0a32016-04-05 11:47:15 -0700361 'duration': None,
maruela9cfd6f2015-09-15 11:03:15 -0700362 'exit_code': None,
maruel6be7f9e2015-10-01 12:25:30 -0700363 'had_hard_timeout': False,
maruela9cfd6f2015-09-15 11:03:15 -0700364 'internal_failure': None,
maruel064c0a32016-04-05 11:47:15 -0700365 'stats': {
nodir55715712016-06-03 12:28:19 -0700366 # 'isolated': {
nodirbe642ff2016-06-09 15:51:51 -0700367 # 'cipd': {
368 # 'duration': 0.,
369 # 'get_client_duration': 0.,
370 # },
nodir55715712016-06-03 12:28:19 -0700371 # 'download': {
372 # 'duration': 0.,
373 # 'initial_number_items': 0,
374 # 'initial_size': 0,
375 # 'items_cold': '<large.pack()>',
376 # 'items_hot': '<large.pack()>',
377 # },
378 # 'upload': {
379 # 'duration': 0.,
380 # 'items_cold': '<large.pack()>',
381 # 'items_hot': '<large.pack()>',
382 # },
maruel064c0a32016-04-05 11:47:15 -0700383 # },
384 },
maruela9cfd6f2015-09-15 11:03:15 -0700385 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700386 'version': 5,
maruela9cfd6f2015-09-15 11:03:15 -0700387 }
nodirbe642ff2016-06-09 15:51:51 -0700388 if cipd_stats:
389 result['stats']['cipd'] = cipd_stats
390
marueleb5fbee2015-09-17 13:01:36 -0700391 if root_dir:
nodire5028a92016-04-29 14:38:21 -0700392 file_path.ensure_tree(root_dir, 0700)
marueleb5fbee2015-09-17 13:01:36 -0700393 else:
394 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
nodirbe642ff2016-06-09 15:51:51 -0700395 run_dir = make_temp_dir(u'isolated_run', root_dir)
396 out_dir = make_temp_dir(u'isolated_out', root_dir) if storage else None
397 tmp_dir = make_temp_dir(u'isolated_tmp', root_dir)
nodir55be77b2016-05-03 09:39:57 -0700398 cwd = run_dir
maruela9cfd6f2015-09-15 11:03:15 -0700399
nodir55be77b2016-05-03 09:39:57 -0700400 try:
401 if isolated_hash:
nodir55715712016-06-03 12:28:19 -0700402 isolated_stats = result['stats'].setdefault('isolated', {})
403 bundle, isolated_stats['download'] = fetch_and_measure(
nodir55be77b2016-05-03 09:39:57 -0700404 isolated_hash=isolated_hash,
405 storage=storage,
406 cache=cache,
407 outdir=run_dir)
408 if not bundle.command:
409 # Handle this as a task failure, not an internal failure.
410 sys.stderr.write(
411 '<The .isolated doesn\'t declare any command to run!>\n'
412 '<Check your .isolate for missing \'command\' variable>\n')
413 if os.environ.get('SWARMING_TASK_ID'):
414 # Give an additional hint when running as a swarming task.
415 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
416 result['exit_code'] = 1
417 return result
418
419 change_tree_read_only(run_dir, bundle.read_only)
420 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
421 command = bundle.command + extra_args
nodirbe642ff2016-06-09 15:51:51 -0700422
nodir34d673c2016-05-24 09:30:48 -0700423 command = tools.fix_python_path(command)
bpastene3ae09522016-06-10 17:12:59 -0700424 command = process_command(command, out_dir, cipd_path, bot_file)
maruela9cfd6f2015-09-15 11:03:15 -0700425 file_path.ensure_command_has_abs_path(command, cwd)
nodirbe642ff2016-06-09 15:51:51 -0700426
maruel064c0a32016-04-05 11:47:15 -0700427 sys.stdout.flush()
428 start = time.time()
429 try:
430 result['exit_code'], result['had_hard_timeout'] = run_command(
nodirbe642ff2016-06-09 15:51:51 -0700431 command, cwd, tmp_dir, hard_timeout, grace_period)
maruel064c0a32016-04-05 11:47:15 -0700432 finally:
433 result['duration'] = max(time.time() - start, 0)
maruela9cfd6f2015-09-15 11:03:15 -0700434 except Exception as e:
435 # An internal error occured. Report accordingly so the swarming task will be
436 # retried automatically.
maruel12e30012015-10-09 11:55:35 -0700437 logging.exception('internal failure: %s', e)
maruela9cfd6f2015-09-15 11:03:15 -0700438 result['internal_failure'] = str(e)
439 on_error.report(None)
440 finally:
441 try:
442 if leak_temp_dir:
443 logging.warning(
444 'Deliberately leaking %s for later examination', run_dir)
marueleb5fbee2015-09-17 13:01:36 -0700445 else:
maruel84537cb2015-10-16 14:21:28 -0700446 # On Windows rmtree(run_dir) call above has a synchronization effect: it
447 # finishes only when all task child processes terminate (since a running
448 # process locks *.exe file). Examine out_dir only after that call
449 # completes (since child processes may write to out_dir too and we need
450 # to wait for them to finish).
451 if fs.isdir(run_dir):
452 try:
453 success = file_path.rmtree(run_dir)
454 except OSError as e:
455 logging.error('Failure with %s', e)
456 success = False
457 if not success:
458 print >> sys.stderr, (
459 'Failed to delete the run directory, forcibly failing\n'
460 'the task because of it. No zombie process can outlive a\n'
461 'successful task run and still be marked as successful.\n'
462 'Fix your stuff.')
463 if result['exit_code'] == 0:
464 result['exit_code'] = 1
465 if fs.isdir(tmp_dir):
466 try:
467 success = file_path.rmtree(tmp_dir)
468 except OSError as e:
469 logging.error('Failure with %s', e)
470 success = False
471 if not success:
472 print >> sys.stderr, (
473 'Failed to delete the temporary directory, forcibly failing\n'
474 'the task because of it. No zombie process can outlive a\n'
475 'successful task run and still be marked as successful.\n'
476 'Fix your stuff.')
477 if result['exit_code'] == 0:
478 result['exit_code'] = 1
maruela9cfd6f2015-09-15 11:03:15 -0700479
marueleb5fbee2015-09-17 13:01:36 -0700480 # This deletes out_dir if leak_temp_dir is not set.
nodir9130f072016-05-27 13:59:08 -0700481 if out_dir:
nodir55715712016-06-03 12:28:19 -0700482 isolated_stats = result['stats'].setdefault('isolated', {})
483 result['outputs_ref'], success, isolated_stats['upload'] = (
nodir9130f072016-05-27 13:59:08 -0700484 delete_and_upload(storage, out_dir, leak_temp_dir))
maruela9cfd6f2015-09-15 11:03:15 -0700485 if not success and result['exit_code'] == 0:
486 result['exit_code'] = 1
487 except Exception as e:
488 # Swallow any exception in the main finally clause.
nodir9130f072016-05-27 13:59:08 -0700489 if out_dir:
490 logging.exception('Leaking out_dir %s: %s', out_dir, e)
maruela9cfd6f2015-09-15 11:03:15 -0700491 result['internal_failure'] = str(e)
492 return result
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500493
494
Marc-Antoine Ruel0ec868b2015-08-12 14:12:46 -0400495def run_tha_test(
nodir55be77b2016-05-03 09:39:57 -0700496 command, isolated_hash, storage, cache, leak_temp_dir, result_json,
bpastene3ae09522016-06-10 17:12:59 -0700497 root_dir, hard_timeout, grace_period, bot_file, extra_args,
498 cipd_path, cipd_stats):
nodir55be77b2016-05-03 09:39:57 -0700499 """Runs an executable and records execution metadata.
500
501 Either command or isolated_hash must be specified.
502
503 If isolated_hash is specified, downloads the dependencies in the cache,
504 hardlinks them into a temporary directory and runs the command specified in
505 the .isolated.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500506
507 A temporary directory is created to hold the output files. The content inside
508 this directory will be uploaded back to |storage| packaged as a .isolated
509 file.
510
511 Arguments:
nodir55be77b2016-05-03 09:39:57 -0700512 command: the command to run, a list of strings. Mutually exclusive with
513 isolated_hash.
Marc-Antoine Ruel35b58432014-12-08 17:40:40 -0500514 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500515 recreate the tree of files to run the target executable.
nodir55be77b2016-05-03 09:39:57 -0700516 The command specified in the .isolated is executed.
517 Mutually exclusive with command argument.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500518 storage: an isolateserver.Storage object to retrieve remote objects. This
519 object has a reference to an isolateserver.StorageApi, which does
520 the actual I/O.
521 cache: an isolateserver.LocalCache to keep from retrieving the same objects
522 constantly by caching the objects retrieved. Can be on-disk or
523 in-memory.
Kenneth Russell61d42352014-09-15 11:41:16 -0700524 leak_temp_dir: if true, the temporary directory will be deliberately leaked
525 for later examination.
maruela9cfd6f2015-09-15 11:03:15 -0700526 result_json: file path to dump result metadata into. If set, the process
nodirbe642ff2016-06-09 15:51:51 -0700527 exit code is always 0 unless an internal error occurred.
marueleb5fbee2015-09-17 13:01:36 -0700528 root_dir: directory to the path to use to create the temporary directory. If
529 not specified, a random temporary directory is created.
maruel6be7f9e2015-10-01 12:25:30 -0700530 hard_timeout: kills the process if it lasts more than this amount of
531 seconds.
532 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
Marc-Antoine Ruel2283ad12014-02-09 11:14:57 -0500533 extra_args: optional arguments to add to the command stated in the .isolate
nodir55be77b2016-05-03 09:39:57 -0700534 file. Ignored if isolate_hash is empty.
nodirbe642ff2016-06-09 15:51:51 -0700535 cipd_path: value for CIPD_PATH_PARAMETER. If empty, command or extra_args
536 must not use CIPD_PATH_PARAMETER.
537 cipd_stats: CIPD stats to include in the metadata written to result_json.
maruela9cfd6f2015-09-15 11:03:15 -0700538
539 Returns:
540 Process exit code that should be used.
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000541 """
nodir55be77b2016-05-03 09:39:57 -0700542 assert bool(command) ^ bool(isolated_hash)
543 extra_args = extra_args or []
nodirbe642ff2016-06-09 15:51:51 -0700544
nodir55be77b2016-05-03 09:39:57 -0700545 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
546 assert storage is not None, 'storage is None although outdir is specified'
547
maruela76b9ee2015-12-15 06:18:08 -0800548 if result_json:
549 # Write a json output file right away in case we get killed.
550 result = {
551 'exit_code': None,
552 'had_hard_timeout': False,
553 'internal_failure': 'Was terminated before completion',
554 'outputs_ref': None,
nodirbe642ff2016-06-09 15:51:51 -0700555 'version': 5,
maruela76b9ee2015-12-15 06:18:08 -0800556 }
nodirbe642ff2016-06-09 15:51:51 -0700557 if cipd_stats:
558 result['stats'] = {'cipd': cipd_stats}
maruela76b9ee2015-12-15 06:18:08 -0800559 tools.write_json(result_json, result, dense=True)
560
maruela9cfd6f2015-09-15 11:03:15 -0700561 # run_isolated exit code. Depends on if result_json is used or not.
562 result = map_and_run(
nodir55be77b2016-05-03 09:39:57 -0700563 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
bpastene3ae09522016-06-10 17:12:59 -0700564 hard_timeout, grace_period, bot_file, extra_args, cipd_path, cipd_stats)
maruela9cfd6f2015-09-15 11:03:15 -0700565 logging.info('Result:\n%s', tools.format_json(result, dense=True))
bpastene3ae09522016-06-10 17:12:59 -0700566
maruela9cfd6f2015-09-15 11:03:15 -0700567 if result_json:
maruel05d5a882015-09-21 13:59:02 -0700568 # We've found tests to delete 'work' when quitting, causing an exception
569 # here. Try to recreate the directory if necessary.
nodire5028a92016-04-29 14:38:21 -0700570 file_path.ensure_tree(os.path.dirname(result_json))
maruela9cfd6f2015-09-15 11:03:15 -0700571 tools.write_json(result_json, result, dense=True)
572 # Only return 1 if there was an internal error.
573 return int(bool(result['internal_failure']))
maruel@chromium.org781ccf62013-09-17 19:39:47 +0000574
maruela9cfd6f2015-09-15 11:03:15 -0700575 # Marshall into old-style inline output.
576 if result['outputs_ref']:
577 data = {
578 'hash': result['outputs_ref']['isolated'],
579 'namespace': result['outputs_ref']['namespace'],
580 'storage': result['outputs_ref']['isolatedserver'],
581 }
Marc-Antoine Ruelc44f5722015-01-08 16:10:01 -0500582 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700583 print(
584 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
585 tools.format_json(data, dense=True))
maruelb76604c2015-11-11 11:53:44 -0800586 sys.stdout.flush()
maruela9cfd6f2015-09-15 11:03:15 -0700587 return result['exit_code'] or int(bool(result['internal_failure']))
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000588
589
nodirbe642ff2016-06-09 15:51:51 -0700590@contextlib.contextmanager
591def ensure_packages(
592 site_root, packages, service_url, client_package, cache_dir=None,
593 timeout=None):
594 """Returns a context manager that installs packages into site_root.
595
596 Creates/recreates site_root dir and install packages before yielding.
597 After yielding deletes site_root dir.
598
599 Args:
600 site_root (str): where to install the packages.
601 packages (list of str): list of package to ensure.
602 Package format is same as for "--cipd-package" option.
603 service_url (str): CIPD server url, e.g.
604 "https://chrome-infra-packages.appspot.com."
605 client_package (str): CIPD package of CIPD client.
606 Format is same as for "--cipd-package" option.
607 cache_dir (str): where to keep cache of cipd clients, packages and tags.
608 timeout: max duration in seconds that this function can take.
609
610 Yields:
611 CIPD stats as dict.
612 """
613 assert cache_dir
614 timeoutfn = tools.sliding_timeout(timeout)
615 if not packages:
616 yield
617 return
618
619 start = time.time()
620
621 cache_dir = os.path.abspath(cache_dir)
622
623 # Get CIPD client.
624 client_package_name, client_version = cipd.parse_package(client_package)
625 get_client_start = time.time()
626 client_manager = cipd.get_client(
627 service_url, client_package_name, client_version, cache_dir,
628 timeout=timeoutfn())
629 with client_manager as client:
630 get_client_duration = time.time() - get_client_start
631 # Create site_root, install packages, yield, delete site_root.
632 if fs.isdir(site_root):
633 file_path.rmtree(site_root)
634 file_path.ensure_tree(site_root, 0770)
635 try:
636 client.ensure(
637 site_root, packages,
638 cache_dir=os.path.join(cache_dir, 'cipd_internal'),
639 timeout=timeoutfn())
640
641 total_duration = time.time() - start
642 logging.info(
643 'Installing CIPD client and packages took %d seconds', total_duration)
644
645 file_path.make_tree_files_read_only(site_root)
646 yield {
647 'duration': total_duration,
648 'get_client_duration': get_client_duration,
649 }
650 finally:
651 file_path.rmtree(site_root)
652
653
654def create_option_parser():
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400655 parser = logging_utils.OptionParserWithLogging(
nodir55be77b2016-05-03 09:39:57 -0700656 usage='%prog <options> [command to run or extra args]',
maruel@chromium.orgdedbf492013-09-12 20:42:11 +0000657 version=__version__,
658 log_file=RUN_ISOLATED_LOG_FILE)
maruela9cfd6f2015-09-15 11:03:15 -0700659 parser.add_option(
maruel36a963d2016-04-08 17:15:49 -0700660 '--clean', action='store_true',
661 help='Cleans the cache, trimming it necessary and remove corrupted items '
662 'and returns without executing anything; use with -v to know what '
663 'was done')
664 parser.add_option(
maruela9cfd6f2015-09-15 11:03:15 -0700665 '--json',
666 help='dump output metadata to json file. When used, run_isolated returns '
667 'non-zero only on internal failure')
maruel6be7f9e2015-10-01 12:25:30 -0700668 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800669 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
maruel6be7f9e2015-10-01 12:25:30 -0700670 parser.add_option(
maruel5c9e47b2015-12-18 13:02:30 -0800671 '--grace-period', type='float',
maruel6be7f9e2015-10-01 12:25:30 -0700672 help='Grace period between SIGTERM and SIGKILL')
bpastene3ae09522016-06-10 17:12:59 -0700673 parser.add_option(
674 '--bot-file',
675 help='Path to a file describing the state of the host. The content is '
676 'defined by on_before_task() in bot_config.')
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500677 data_group = optparse.OptionGroup(parser, 'Data source')
678 data_group.add_option(
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500679 '-s', '--isolated',
nodir55be77b2016-05-03 09:39:57 -0700680 help='Hash of the .isolated to grab from the isolate server.')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500681 isolateserver.add_isolate_server_options(data_group)
Marc-Antoine Ruel1687b5e2014-02-06 17:47:53 -0500682 parser.add_option_group(data_group)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000683
Marc-Antoine Ruela57d7db2014-10-15 20:31:19 -0400684 isolateserver.add_cache_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700685
686 cipd.add_cipd_options(parser)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000687
Kenneth Russell61d42352014-09-15 11:41:16 -0700688 debug_group = optparse.OptionGroup(parser, 'Debugging')
689 debug_group.add_option(
690 '--leak-temp-dir',
691 action='store_true',
nodirbe642ff2016-06-09 15:51:51 -0700692 help='Deliberately leak isolate\'s temp dir for later examination. '
693 'Default: %default')
marueleb5fbee2015-09-17 13:01:36 -0700694 debug_group.add_option(
695 '--root-dir', help='Use a directory instead of a random one')
Kenneth Russell61d42352014-09-15 11:41:16 -0700696 parser.add_option_group(debug_group)
697
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800698 auth.add_auth_options(parser)
nodirbe642ff2016-06-09 15:51:51 -0700699
700 parser.set_defaults(cache='cache', cipd_cache='cipd_cache')
701 return parser
702
703
704def main(args):
705 parser = create_option_parser()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500706 options, args = parser.parse_args(args)
maruel36a963d2016-04-08 17:15:49 -0700707
708 cache = isolateserver.process_cache_options(options)
709 if options.clean:
710 if options.isolated:
711 parser.error('Can\'t use --isolated with --clean.')
712 if options.isolate_server:
713 parser.error('Can\'t use --isolate-server with --clean.')
714 if options.json:
715 parser.error('Can\'t use --json with --clean.')
716 cache.cleanup()
717 return 0
718
nodir55be77b2016-05-03 09:39:57 -0700719 if not options.isolated and not args:
720 parser.error('--isolated or command to run is required.')
721
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800722 auth.process_auth_options(parser, options)
nodir55be77b2016-05-03 09:39:57 -0700723
724 isolateserver.process_isolate_server_options(
725 parser, options, True, False)
726 if not options.isolate_server:
727 if options.isolated:
728 parser.error('--isolated requires --isolate-server')
729 if ISOLATED_OUTDIR_PARAMETER in args:
730 parser.error(
731 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000732
maruel12e30012015-10-09 11:55:35 -0700733 if options.json:
734 options.json = unicode(os.path.abspath(options.json))
nodir55be77b2016-05-03 09:39:57 -0700735
nodirbe642ff2016-06-09 15:51:51 -0700736 cipd.validate_cipd_options(parser, options)
737
738 root_dir = options.root_dir
739 if root_dir:
740 root_dir = unicode(os.path.abspath(root_dir))
741 file_path.ensure_tree(root_dir, 0700)
nodir55be77b2016-05-03 09:39:57 -0700742 else:
nodirbe642ff2016-06-09 15:51:51 -0700743 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
744
745 cipd_path = None
746 if not options.cipd_package:
747 if CIPD_PATH_PARAMETER in args:
748 parser.error('%s in args requires --cipd-package' % CIPD_PATH_PARAMETER)
749 else:
750 cipd_path = make_temp_dir(u'cipd_site_root', root_dir)
751
752 try:
753 with ensure_packages(
754 cipd_path, options.cipd_package, options.cipd_server,
755 options.cipd_client_package, options.cipd_cache) as cipd_stats:
756 command = [] if options.isolated else args
757 if options.isolate_server:
758 storage = isolateserver.get_storage(
759 options.isolate_server, options.namespace)
760 with storage:
761 # Hashing schemes used by |storage| and |cache| MUST match.
762 assert storage.hash_algo == cache.hash_algo
763 return run_tha_test(
764 command, options.isolated, storage, cache, options.leak_temp_dir,
765 options.json, root_dir, options.hard_timeout,
bpastene3ae09522016-06-10 17:12:59 -0700766 options.grace_period, options.bot_file, args,
767 cipd_path, cipd_stats)
nodirbe642ff2016-06-09 15:51:51 -0700768 else:
769 return run_tha_test(
770 command, options.isolated, None, cache, options.leak_temp_dir,
771 options.json, root_dir, options.hard_timeout,
bpastene3ae09522016-06-10 17:12:59 -0700772 options.grace_period, options.bot_file, args,
773 cipd_path, cipd_stats)
nodirbe642ff2016-06-09 15:51:51 -0700774 except cipd.Error as ex:
775 print >> sys.stderr, ex.message
776 return 1
maruel@chromium.org9c72d4e2012-09-28 19:20:25 +0000777
778
779if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -0700780 subprocess42.inhibit_os_error_reporting()
csharp@chromium.orgbfb98742013-03-26 20:28:36 +0000781 # Ensure that we are always running with the correct encoding.
vadimsh@chromium.orga4326472013-08-24 02:05:41 +0000782 fix_encoding.fix_encoding()
Marc-Antoine Ruel90c98162013-12-18 15:11:57 -0500783 sys.exit(main(sys.argv[1:]))