| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2012 The LUCI Authors. All rights reserved. | 2 # Copyright 2012 The LUCI Authors. All rights reserved. |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 | 3 # Use of this source code is governed under the Apache License, Version 2.0 |
| 4 # that can be found in the LICENSE file. | 4 # that can be found in the LICENSE file. |
| 5 | 5 |
| 6 """Runs a command with optional isolated input/output. | 6 """Runs a command with optional isolated input/output. |
| 7 | 7 |
| 8 Despite name "run_isolated", can run a generic non-isolated command specified as | 8 Despite name "run_isolated", can run a generic non-isolated command specified as |
| 9 args. | 9 args. |
| 10 | 10 |
| 11 If input isolated hash is provided, fetches it, creates a tree of hard links, | 11 If input isolated hash is provided, fetches it, creates a tree of hard links, |
| 12 appends args to the command in the fetched isolated and runs it. | 12 appends args to the command in the fetched isolated and runs it. |
| 13 To improve performance, keeps a local cache. | 13 To improve performance, keeps a local cache. |
| 14 The local cache can safely be deleted. | 14 The local cache can safely be deleted. |
| 15 | 15 |
| 16 Any ${EXECUTABLE_SUFFIX} on the command line will be replaced with ".exe" string |
| 17 on Windows and "" on other platforms. |
| 18 |
| 19 If at least one CIPD package was specified, any ${CIPD_PATH} on the command line |
| 20 will be replaced by location of a temporary directory that contains installed |
| 21 packages. |
| 22 |
| 16 Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a | 23 Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a |
| 17 temporary directory upon execution of the command specified in the .isolated | 24 temporary directory upon execution of the command specified in the .isolated |
| 18 file. All content written to this directory will be uploaded upon termination | 25 file. All content written to this directory will be uploaded upon termination |
| 19 and the .isolated file describing this directory will be printed to stdout. | 26 and the .isolated file describing this directory will be printed to stdout. |
| 20 """ | 27 """ |
| 21 | 28 |
| 22 __version__ = '0.7.0' | 29 __version__ = '0.8.0' |
| 23 | 30 |
| 24 import base64 | 31 import base64 |
| 32 import contextlib |
| 33 import hashlib |
| 25 import logging | 34 import logging |
| 26 import optparse | 35 import optparse |
| 27 import os | 36 import os |
| 28 import sys | 37 import sys |
| 29 import tempfile | 38 import tempfile |
| 30 import time | 39 import time |
| 31 | 40 |
| 32 from third_party.depot_tools import fix_encoding | 41 from third_party.depot_tools import fix_encoding |
| 33 | 42 |
| 34 from utils import file_path | 43 from utils import file_path |
| 35 from utils import fs | 44 from utils import fs |
| 36 from utils import large | 45 from utils import large |
| 37 from utils import logging_utils | 46 from utils import logging_utils |
| 38 from utils import on_error | 47 from utils import on_error |
| 39 from utils import subprocess42 | 48 from utils import subprocess42 |
| 40 from utils import tools | 49 from utils import tools |
| 41 from utils import zip_package | 50 from utils import zip_package |
| 42 | 51 |
| 43 import auth | 52 import auth |
| 53 import cipd |
| 44 import isolateserver | 54 import isolateserver |
| 45 | 55 |
| 46 | 56 |
| 47 ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}' | 57 ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}' |
| 58 CIPD_PATH_PARAMETER = '${CIPD_PATH}' |
| 59 EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}' |
| 48 | 60 |
| 49 # Absolute path to this file (can be None if running from zip on Mac). | 61 # Absolute path to this file (can be None if running from zip on Mac). |
| 50 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None | 62 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None |
| 51 | 63 |
| 52 # Directory that contains this file (might be inside zip package). | 64 # Directory that contains this file (might be inside zip package). |
| 53 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None | 65 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None |
| 54 | 66 |
| 55 # Directory that contains currently running script file. | 67 # Directory that contains currently running script file. |
| 56 if zip_package.get_main_script_path(): | 68 if zip_package.get_main_script_path(): |
| 57 MAIN_DIR = os.path.dirname( | 69 MAIN_DIR = os.path.dirname( |
| (...skipping 19 matching lines...) Expand all Loading... |
| 77 # Building a zip package when running from another zip package is | 89 # Building a zip package when running from another zip package is |
| 78 # unsupported and probably unneeded. | 90 # unsupported and probably unneeded. |
| 79 assert not zip_package.is_zipped_module(sys.modules[__name__]) | 91 assert not zip_package.is_zipped_module(sys.modules[__name__]) |
| 80 assert THIS_FILE_PATH | 92 assert THIS_FILE_PATH |
| 81 assert BASE_DIR | 93 assert BASE_DIR |
| 82 package = zip_package.ZipPackage(root=BASE_DIR) | 94 package = zip_package.ZipPackage(root=BASE_DIR) |
| 83 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None) | 95 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None) |
| 84 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py')) | 96 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py')) |
| 85 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py')) | 97 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py')) |
| 86 package.add_python_file(os.path.join(BASE_DIR, 'auth.py')) | 98 package.add_python_file(os.path.join(BASE_DIR, 'auth.py')) |
| 99 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py')) |
| 87 package.add_directory(os.path.join(BASE_DIR, 'third_party')) | 100 package.add_directory(os.path.join(BASE_DIR, 'third_party')) |
| 88 package.add_directory(os.path.join(BASE_DIR, 'utils')) | 101 package.add_directory(os.path.join(BASE_DIR, 'utils')) |
| 89 return package | 102 return package |
| 90 | 103 |
| 91 | 104 |
| 92 def make_temp_dir(prefix, root_dir=None): | 105 def make_temp_dir(prefix, root_dir=None): |
| 93 """Returns a temporary directory. | 106 """Returns a temporary directory. |
| 94 | 107 |
| 95 If root_dir is given and /tmp is on same file system as root_dir, uses /tmp. | 108 If root_dir is given and /tmp is on same file system as root_dir, uses /tmp. |
| 96 Otherwise makes a new temp directory under root_dir. | 109 Otherwise makes a new temp directory under root_dir. |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 133 # is not yet changed to verify the hash of the content of the files it is | 146 # is not yet changed to verify the hash of the content of the files it is |
| 134 # looking at, so that if a test modifies an input file, the file must be | 147 # looking at, so that if a test modifies an input file, the file must be |
| 135 # deleted. | 148 # deleted. |
| 136 file_path.make_tree_writeable(rootdir) | 149 file_path.make_tree_writeable(rootdir) |
| 137 else: | 150 else: |
| 138 raise ValueError( | 151 raise ValueError( |
| 139 'change_tree_read_only(%s, %s): Unknown flag %s' % | 152 'change_tree_read_only(%s, %s): Unknown flag %s' % |
| 140 (rootdir, read_only, read_only)) | 153 (rootdir, read_only, read_only)) |
| 141 | 154 |
| 142 | 155 |
| 143 def process_command(command, out_dir): | 156 def process_command(command, out_dir, cipd_path): |
| 144 """Replaces isolated specific variables in a command line.""" | 157 """Replaces variables in a command line. |
| 158 |
| 159 Raises: |
| 160 ValueError if a parameter is requested in |command| but its value is not |
| 161 provided. |
| 162 """ |
| 145 def fix(arg): | 163 def fix(arg): |
| 164 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX) |
| 165 replace_slash = False |
| 166 if CIPD_PATH_PARAMETER in arg: |
| 167 if not cipd_path: |
| 168 raise ValueError('cipd_path is requested in command, but not provided') |
| 169 arg = arg.replace(CIPD_PATH_PARAMETER, cipd_path) |
| 170 replace_slash = True |
| 146 if ISOLATED_OUTDIR_PARAMETER in arg: | 171 if ISOLATED_OUTDIR_PARAMETER in arg: |
| 147 assert out_dir | 172 if not out_dir: |
| 173 raise ValueError('out_dir is requested in command, but not provided') |
| 148 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir) | 174 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir) |
| 149 # Replace slashes only if ISOLATED_OUTDIR_PARAMETER is present | 175 replace_slash = True |
| 176 if replace_slash: |
| 177 # Replace slashes only if parameters are present |
| 150 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar' | 178 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar' |
| 151 arg = arg.replace('/', os.sep) | 179 arg = arg.replace('/', os.sep) |
| 152 return arg | 180 return arg |
| 153 | 181 |
| 154 return [fix(arg) for arg in command] | 182 return [fix(arg) for arg in command] |
| 155 | 183 |
| 156 | 184 |
| 157 def run_command(command, cwd, tmp_dir, hard_timeout, grace_period): | 185 def run_command(command, cwd, tmp_dir, hard_timeout, grace_period): |
| 158 """Runs the command. | 186 """Runs the command. |
| 159 | 187 |
| (...skipping 146 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 306 stats = { | 334 stats = { |
| 307 'duration': time.time() - start, | 335 'duration': time.time() - start, |
| 308 'items_cold': base64.b64encode(large.pack(cold)), | 336 'items_cold': base64.b64encode(large.pack(cold)), |
| 309 'items_hot': base64.b64encode(large.pack(hot)), | 337 'items_hot': base64.b64encode(large.pack(hot)), |
| 310 } | 338 } |
| 311 return outputs_ref, success, stats | 339 return outputs_ref, success, stats |
| 312 | 340 |
| 313 | 341 |
| 314 def map_and_run( | 342 def map_and_run( |
| 315 command, isolated_hash, storage, cache, leak_temp_dir, root_dir, | 343 command, isolated_hash, storage, cache, leak_temp_dir, root_dir, |
| 316 hard_timeout, grace_period, extra_args): | 344 hard_timeout, grace_period, extra_args, cipd_path, cipd_stats): |
| 317 """Runs a command with optional isolated input/output. | 345 """Runs a command with optional isolated input/output. |
| 318 | 346 |
| 319 See run_tha_test for argument documentation. | 347 See run_tha_test for argument documentation. |
| 320 | 348 |
| 321 Returns metadata about the result. | 349 Returns metadata about the result. |
| 322 """ | 350 """ |
| 323 assert bool(command) ^ bool(isolated_hash) | 351 assert bool(command) ^ bool(isolated_hash) |
| 324 result = { | 352 result = { |
| 325 'duration': None, | 353 'duration': None, |
| 326 'exit_code': None, | 354 'exit_code': None, |
| 327 'had_hard_timeout': False, | 355 'had_hard_timeout': False, |
| 328 'internal_failure': None, | 356 'internal_failure': None, |
| 329 'stats': { | 357 'stats': { |
| 330 # 'isolated': { | 358 # 'isolated': { |
| 359 # 'cipd': { |
| 360 # 'duration': 0., |
| 361 # 'get_client_duration': 0., |
| 362 # }, |
| 331 # 'download': { | 363 # 'download': { |
| 332 # 'duration': 0., | 364 # 'duration': 0., |
| 333 # 'initial_number_items': 0, | 365 # 'initial_number_items': 0, |
| 334 # 'initial_size': 0, | 366 # 'initial_size': 0, |
| 335 # 'items_cold': '<large.pack()>', | 367 # 'items_cold': '<large.pack()>', |
| 336 # 'items_hot': '<large.pack()>', | 368 # 'items_hot': '<large.pack()>', |
| 337 # }, | 369 # }, |
| 338 # 'upload': { | 370 # 'upload': { |
| 339 # 'duration': 0., | 371 # 'duration': 0., |
| 340 # 'items_cold': '<large.pack()>', | 372 # 'items_cold': '<large.pack()>', |
| 341 # 'items_hot': '<large.pack()>', | 373 # 'items_hot': '<large.pack()>', |
| 342 # }, | 374 # }, |
| 343 # }, | 375 # }, |
| 344 }, | 376 }, |
| 345 'outputs_ref': None, | 377 'outputs_ref': None, |
| 346 'version': 4, | 378 'version': 5, |
| 347 } | 379 } |
| 380 if cipd_stats: |
| 381 result['stats']['cipd'] = cipd_stats |
| 382 |
| 348 if root_dir: | 383 if root_dir: |
| 349 file_path.ensure_tree(root_dir, 0700) | 384 file_path.ensure_tree(root_dir, 0700) |
| 350 prefix = u'' | |
| 351 else: | 385 else: |
| 352 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None | 386 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None |
| 353 prefix = u'isolated_' | 387 run_dir = make_temp_dir(u'isolated_run', root_dir) |
| 354 run_dir = make_temp_dir(prefix + u'run', root_dir) | 388 out_dir = make_temp_dir(u'isolated_out', root_dir) if storage else None |
| 355 out_dir = make_temp_dir(prefix + u'out', root_dir) if storage else None | 389 tmp_dir = make_temp_dir(u'isolated_tmp', root_dir) |
| 356 tmp_dir = make_temp_dir(prefix + u'tmp', root_dir) | |
| 357 cwd = run_dir | 390 cwd = run_dir |
| 358 | 391 |
| 359 try: | 392 try: |
| 360 if isolated_hash: | 393 if isolated_hash: |
| 361 isolated_stats = result['stats'].setdefault('isolated', {}) | 394 isolated_stats = result['stats'].setdefault('isolated', {}) |
| 362 bundle, isolated_stats['download'] = fetch_and_measure( | 395 bundle, isolated_stats['download'] = fetch_and_measure( |
| 363 isolated_hash=isolated_hash, | 396 isolated_hash=isolated_hash, |
| 364 storage=storage, | 397 storage=storage, |
| 365 cache=cache, | 398 cache=cache, |
| 366 outdir=run_dir) | 399 outdir=run_dir) |
| 367 if not bundle.command: | 400 if not bundle.command: |
| 368 # Handle this as a task failure, not an internal failure. | 401 # Handle this as a task failure, not an internal failure. |
| 369 sys.stderr.write( | 402 sys.stderr.write( |
| 370 '<The .isolated doesn\'t declare any command to run!>\n' | 403 '<The .isolated doesn\'t declare any command to run!>\n' |
| 371 '<Check your .isolate for missing \'command\' variable>\n') | 404 '<Check your .isolate for missing \'command\' variable>\n') |
| 372 if os.environ.get('SWARMING_TASK_ID'): | 405 if os.environ.get('SWARMING_TASK_ID'): |
| 373 # Give an additional hint when running as a swarming task. | 406 # Give an additional hint when running as a swarming task. |
| 374 sys.stderr.write('<This occurs at the \'isolate\' step>\n') | 407 sys.stderr.write('<This occurs at the \'isolate\' step>\n') |
| 375 result['exit_code'] = 1 | 408 result['exit_code'] = 1 |
| 376 return result | 409 return result |
| 377 | 410 |
| 378 change_tree_read_only(run_dir, bundle.read_only) | 411 change_tree_read_only(run_dir, bundle.read_only) |
| 379 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd)) | 412 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd)) |
| 380 command = bundle.command + extra_args | 413 command = bundle.command + extra_args |
| 414 |
| 381 command = tools.fix_python_path(command) | 415 command = tools.fix_python_path(command) |
| 416 command = process_command(command, out_dir, cipd_path) |
| 382 file_path.ensure_command_has_abs_path(command, cwd) | 417 file_path.ensure_command_has_abs_path(command, cwd) |
| 418 |
| 383 sys.stdout.flush() | 419 sys.stdout.flush() |
| 384 start = time.time() | 420 start = time.time() |
| 385 try: | 421 try: |
| 386 result['exit_code'], result['had_hard_timeout'] = run_command( | 422 result['exit_code'], result['had_hard_timeout'] = run_command( |
| 387 process_command(command, out_dir), cwd, tmp_dir, hard_timeout, | 423 command, cwd, tmp_dir, hard_timeout, grace_period) |
| 388 grace_period) | |
| 389 finally: | 424 finally: |
| 390 result['duration'] = max(time.time() - start, 0) | 425 result['duration'] = max(time.time() - start, 0) |
| 391 except Exception as e: | 426 except Exception as e: |
| 392 # An internal error occured. Report accordingly so the swarming task will be | 427 # An internal error occured. Report accordingly so the swarming task will be |
| 393 # retried automatically. | 428 # retried automatically. |
| 394 logging.exception('internal failure: %s', e) | 429 logging.exception('internal failure: %s', e) |
| 395 result['internal_failure'] = str(e) | 430 result['internal_failure'] = str(e) |
| 396 on_error.report(None) | 431 on_error.report(None) |
| 397 finally: | 432 finally: |
| 398 try: | 433 try: |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 444 except Exception as e: | 479 except Exception as e: |
| 445 # Swallow any exception in the main finally clause. | 480 # Swallow any exception in the main finally clause. |
| 446 if out_dir: | 481 if out_dir: |
| 447 logging.exception('Leaking out_dir %s: %s', out_dir, e) | 482 logging.exception('Leaking out_dir %s: %s', out_dir, e) |
| 448 result['internal_failure'] = str(e) | 483 result['internal_failure'] = str(e) |
| 449 return result | 484 return result |
| 450 | 485 |
| 451 | 486 |
| 452 def run_tha_test( | 487 def run_tha_test( |
| 453 command, isolated_hash, storage, cache, leak_temp_dir, result_json, | 488 command, isolated_hash, storage, cache, leak_temp_dir, result_json, |
| 454 root_dir, hard_timeout, grace_period, extra_args): | 489 root_dir, hard_timeout, grace_period, extra_args, cipd_path, cipd_stats): |
| 455 """Runs an executable and records execution metadata. | 490 """Runs an executable and records execution metadata. |
| 456 | 491 |
| 457 Either command or isolated_hash must be specified. | 492 Either command or isolated_hash must be specified. |
| 458 | 493 |
| 459 If isolated_hash is specified, downloads the dependencies in the cache, | 494 If isolated_hash is specified, downloads the dependencies in the cache, |
| 460 hardlinks them into a temporary directory and runs the command specified in | 495 hardlinks them into a temporary directory and runs the command specified in |
| 461 the .isolated. | 496 the .isolated. |
| 462 | 497 |
| 463 A temporary directory is created to hold the output files. The content inside | 498 A temporary directory is created to hold the output files. The content inside |
| 464 this directory will be uploaded back to |storage| packaged as a .isolated | 499 this directory will be uploaded back to |storage| packaged as a .isolated |
| 465 file. | 500 file. |
| 466 | 501 |
| 467 Arguments: | 502 Arguments: |
| 468 command: the command to run, a list of strings. Mutually exclusive with | 503 command: the command to run, a list of strings. Mutually exclusive with |
| 469 isolated_hash. | 504 isolated_hash. |
| 470 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to | 505 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to |
| 471 recreate the tree of files to run the target executable. | 506 recreate the tree of files to run the target executable. |
| 472 The command specified in the .isolated is executed. | 507 The command specified in the .isolated is executed. |
| 473 Mutually exclusive with command argument. | 508 Mutually exclusive with command argument. |
| 474 storage: an isolateserver.Storage object to retrieve remote objects. This | 509 storage: an isolateserver.Storage object to retrieve remote objects. This |
| 475 object has a reference to an isolateserver.StorageApi, which does | 510 object has a reference to an isolateserver.StorageApi, which does |
| 476 the actual I/O. | 511 the actual I/O. |
| 477 cache: an isolateserver.LocalCache to keep from retrieving the same objects | 512 cache: an isolateserver.LocalCache to keep from retrieving the same objects |
| 478 constantly by caching the objects retrieved. Can be on-disk or | 513 constantly by caching the objects retrieved. Can be on-disk or |
| 479 in-memory. | 514 in-memory. |
| 480 leak_temp_dir: if true, the temporary directory will be deliberately leaked | 515 leak_temp_dir: if true, the temporary directory will be deliberately leaked |
| 481 for later examination. | 516 for later examination. |
| 482 result_json: file path to dump result metadata into. If set, the process | 517 result_json: file path to dump result metadata into. If set, the process |
| 483 exit code is always 0 unless an internal error occured. | 518 exit code is always 0 unless an internal error occurred. |
| 484 root_dir: directory to the path to use to create the temporary directory. If | 519 root_dir: directory to the path to use to create the temporary directory. If |
| 485 not specified, a random temporary directory is created. | 520 not specified, a random temporary directory is created. |
| 486 hard_timeout: kills the process if it lasts more than this amount of | 521 hard_timeout: kills the process if it lasts more than this amount of |
| 487 seconds. | 522 seconds. |
| 488 grace_period: number of seconds to wait between SIGTERM and SIGKILL. | 523 grace_period: number of seconds to wait between SIGTERM and SIGKILL. |
| 489 extra_args: optional arguments to add to the command stated in the .isolate | 524 extra_args: optional arguments to add to the command stated in the .isolate |
| 490 file. Ignored if isolate_hash is empty. | 525 file. Ignored if isolate_hash is empty. |
| 526 cipd_path: value for CIPD_PATH_PARAMETER. If empty, command or extra_args |
| 527 must not use CIPD_PATH_PARAMETER. |
| 528 cipd_stats: CIPD stats to include in the metadata written to result_json. |
| 491 | 529 |
| 492 Returns: | 530 Returns: |
| 493 Process exit code that should be used. | 531 Process exit code that should be used. |
| 494 """ | 532 """ |
| 495 assert bool(command) ^ bool(isolated_hash) | 533 assert bool(command) ^ bool(isolated_hash) |
| 496 extra_args = extra_args or [] | 534 extra_args = extra_args or [] |
| 535 |
| 497 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)): | 536 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)): |
| 498 assert storage is not None, 'storage is None although outdir is specified' | 537 assert storage is not None, 'storage is None although outdir is specified' |
| 499 | 538 |
| 500 if result_json: | 539 if result_json: |
| 501 # Write a json output file right away in case we get killed. | 540 # Write a json output file right away in case we get killed. |
| 502 result = { | 541 result = { |
| 503 'exit_code': None, | 542 'exit_code': None, |
| 504 'had_hard_timeout': False, | 543 'had_hard_timeout': False, |
| 505 'internal_failure': 'Was terminated before completion', | 544 'internal_failure': 'Was terminated before completion', |
| 506 'outputs_ref': None, | 545 'outputs_ref': None, |
| 507 'version': 2, | 546 'version': 5, |
| 508 } | 547 } |
| 548 if cipd_stats: |
| 549 result['stats'] = {'cipd': cipd_stats} |
| 509 tools.write_json(result_json, result, dense=True) | 550 tools.write_json(result_json, result, dense=True) |
| 510 | 551 |
| 511 # run_isolated exit code. Depends on if result_json is used or not. | 552 # run_isolated exit code. Depends on if result_json is used or not. |
| 512 result = map_and_run( | 553 result = map_and_run( |
| 513 command, isolated_hash, storage, cache, leak_temp_dir, root_dir, | 554 command, isolated_hash, storage, cache, leak_temp_dir, root_dir, |
| 514 hard_timeout, grace_period, extra_args) | 555 hard_timeout, grace_period, extra_args, cipd_path, cipd_stats) |
| 515 logging.info('Result:\n%s', tools.format_json(result, dense=True)) | 556 logging.info('Result:\n%s', tools.format_json(result, dense=True)) |
| 516 if result_json: | 557 if result_json: |
| 517 # We've found tests to delete 'work' when quitting, causing an exception | 558 # We've found tests to delete 'work' when quitting, causing an exception |
| 518 # here. Try to recreate the directory if necessary. | 559 # here. Try to recreate the directory if necessary. |
| 519 file_path.ensure_tree(os.path.dirname(result_json)) | 560 file_path.ensure_tree(os.path.dirname(result_json)) |
| 520 tools.write_json(result_json, result, dense=True) | 561 tools.write_json(result_json, result, dense=True) |
| 521 # Only return 1 if there was an internal error. | 562 # Only return 1 if there was an internal error. |
| 522 return int(bool(result['internal_failure'])) | 563 return int(bool(result['internal_failure'])) |
| 523 | 564 |
| 524 # Marshall into old-style inline output. | 565 # Marshall into old-style inline output. |
| 525 if result['outputs_ref']: | 566 if result['outputs_ref']: |
| 526 data = { | 567 data = { |
| 527 'hash': result['outputs_ref']['isolated'], | 568 'hash': result['outputs_ref']['isolated'], |
| 528 'namespace': result['outputs_ref']['namespace'], | 569 'namespace': result['outputs_ref']['namespace'], |
| 529 'storage': result['outputs_ref']['isolatedserver'], | 570 'storage': result['outputs_ref']['isolatedserver'], |
| 530 } | 571 } |
| 531 sys.stdout.flush() | 572 sys.stdout.flush() |
| 532 print( | 573 print( |
| 533 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' % | 574 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' % |
| 534 tools.format_json(data, dense=True)) | 575 tools.format_json(data, dense=True)) |
| 535 sys.stdout.flush() | 576 sys.stdout.flush() |
| 536 return result['exit_code'] or int(bool(result['internal_failure'])) | 577 return result['exit_code'] or int(bool(result['internal_failure'])) |
| 537 | 578 |
| 538 | 579 |
| 539 def main(args): | 580 @contextlib.contextmanager |
| 581 def ensure_packages( |
| 582 site_root, packages, service_url, client_package, cache_dir=None, |
| 583 timeout=None): |
| 584 """Returns a context manager that installs packages into site_root. |
| 585 |
| 586 Creates/recreates site_root dir and install packages before yielding. |
| 587 After yielding deletes site_root dir. |
| 588 |
| 589 Args: |
| 590 site_root (str): where to install the packages. |
| 591 packages (list of str): list of package to ensure. |
| 592 Package format is same as for "--cipd-package" option. |
| 593 service_url (str): CIPD server url, e.g. |
| 594 "https://chrome-infra-packages.appspot.com." |
| 595 client_package (str): CIPD package of CIPD client. |
| 596 Format is same as for "--cipd-package" option. |
| 597 cache_dir (str): where to keep cache of cipd clients, packages and tags. |
| 598 timeout: max duration in seconds that this function can take. |
| 599 |
| 600 Yields: |
| 601 CIPD stats as dict. |
| 602 """ |
| 603 assert cache_dir |
| 604 timeoutfn = tools.sliding_timeout(timeout) |
| 605 if not packages: |
| 606 yield |
| 607 return |
| 608 |
| 609 start = time.time() |
| 610 |
| 611 cache_dir = os.path.abspath(cache_dir) |
| 612 |
| 613 # Get CIPD client. |
| 614 client_package_name, client_version = cipd.parse_package(client_package) |
| 615 get_client_start = time.time() |
| 616 client_manager = cipd.get_client( |
| 617 service_url, client_package_name, client_version, cache_dir, |
| 618 timeout=timeoutfn()) |
| 619 with client_manager as client: |
| 620 get_client_duration = time.time() - get_client_start |
| 621 # Create site_root, install packages, yield, delete site_root. |
| 622 if fs.isdir(site_root): |
| 623 file_path.rmtree(site_root) |
| 624 file_path.ensure_tree(site_root, 0770) |
| 625 try: |
| 626 client.ensure( |
| 627 site_root, packages, |
| 628 cache_dir=os.path.join(cache_dir, 'cipd_internal'), |
| 629 timeout=timeoutfn()) |
| 630 |
| 631 total_duration = time.time() - start |
| 632 logging.info( |
| 633 'Installing CIPD client and packages took %d seconds', total_duration) |
| 634 |
| 635 file_path.make_tree_files_read_only(site_root) |
| 636 yield { |
| 637 'duration': total_duration, |
| 638 'get_client_duration': get_client_duration, |
| 639 } |
| 640 finally: |
| 641 file_path.rmtree(site_root) |
| 642 |
| 643 |
| 644 def create_option_parser(): |
| 540 parser = logging_utils.OptionParserWithLogging( | 645 parser = logging_utils.OptionParserWithLogging( |
| 541 usage='%prog <options> [command to run or extra args]', | 646 usage='%prog <options> [command to run or extra args]', |
| 542 version=__version__, | 647 version=__version__, |
| 543 log_file=RUN_ISOLATED_LOG_FILE) | 648 log_file=RUN_ISOLATED_LOG_FILE) |
| 544 parser.add_option( | 649 parser.add_option( |
| 545 '--clean', action='store_true', | 650 '--clean', action='store_true', |
| 546 help='Cleans the cache, trimming it necessary and remove corrupted items ' | 651 help='Cleans the cache, trimming it necessary and remove corrupted items ' |
| 547 'and returns without executing anything; use with -v to know what ' | 652 'and returns without executing anything; use with -v to know what ' |
| 548 'was done') | 653 'was done') |
| 549 parser.add_option( | 654 parser.add_option( |
| 550 '--json', | 655 '--json', |
| 551 help='dump output metadata to json file. When used, run_isolated returns ' | 656 help='dump output metadata to json file. When used, run_isolated returns ' |
| 552 'non-zero only on internal failure') | 657 'non-zero only on internal failure') |
| 553 parser.add_option( | 658 parser.add_option( |
| 554 '--hard-timeout', type='float', help='Enforce hard timeout in execution') | 659 '--hard-timeout', type='float', help='Enforce hard timeout in execution') |
| 555 parser.add_option( | 660 parser.add_option( |
| 556 '--grace-period', type='float', | 661 '--grace-period', type='float', |
| 557 help='Grace period between SIGTERM and SIGKILL') | 662 help='Grace period between SIGTERM and SIGKILL') |
| 558 data_group = optparse.OptionGroup(parser, 'Data source') | 663 data_group = optparse.OptionGroup(parser, 'Data source') |
| 559 data_group.add_option( | 664 data_group.add_option( |
| 560 '-s', '--isolated', | 665 '-s', '--isolated', |
| 561 help='Hash of the .isolated to grab from the isolate server.') | 666 help='Hash of the .isolated to grab from the isolate server.') |
| 562 isolateserver.add_isolate_server_options(data_group) | 667 isolateserver.add_isolate_server_options(data_group) |
| 563 parser.add_option_group(data_group) | 668 parser.add_option_group(data_group) |
| 564 | 669 |
| 565 isolateserver.add_cache_options(parser) | 670 isolateserver.add_cache_options(parser) |
| 566 parser.set_defaults(cache='cache') | 671 |
| 672 cipd.add_cipd_options(parser) |
| 567 | 673 |
| 568 debug_group = optparse.OptionGroup(parser, 'Debugging') | 674 debug_group = optparse.OptionGroup(parser, 'Debugging') |
| 569 debug_group.add_option( | 675 debug_group.add_option( |
| 570 '--leak-temp-dir', | 676 '--leak-temp-dir', |
| 571 action='store_true', | 677 action='store_true', |
| 572 help='Deliberately leak isolate\'s temp dir for later examination ' | 678 help='Deliberately leak isolate\'s temp dir for later examination. ' |
| 573 '[default: %default]') | 679 'Default: %default') |
| 574 debug_group.add_option( | 680 debug_group.add_option( |
| 575 '--root-dir', help='Use a directory instead of a random one') | 681 '--root-dir', help='Use a directory instead of a random one') |
| 576 parser.add_option_group(debug_group) | 682 parser.add_option_group(debug_group) |
| 577 | 683 |
| 578 auth.add_auth_options(parser) | 684 auth.add_auth_options(parser) |
| 685 |
| 686 parser.set_defaults(cache='cache', cipd_cache='cipd_cache') |
| 687 return parser |
| 688 |
| 689 |
| 690 def main(args): |
| 691 parser = create_option_parser() |
| 579 options, args = parser.parse_args(args) | 692 options, args = parser.parse_args(args) |
| 580 | 693 |
| 581 cache = isolateserver.process_cache_options(options) | 694 cache = isolateserver.process_cache_options(options) |
| 582 if options.clean: | 695 if options.clean: |
| 583 if options.isolated: | 696 if options.isolated: |
| 584 parser.error('Can\'t use --isolated with --clean.') | 697 parser.error('Can\'t use --isolated with --clean.') |
| 585 if options.isolate_server: | 698 if options.isolate_server: |
| 586 parser.error('Can\'t use --isolate-server with --clean.') | 699 parser.error('Can\'t use --isolate-server with --clean.') |
| 587 if options.json: | 700 if options.json: |
| 588 parser.error('Can\'t use --json with --clean.') | 701 parser.error('Can\'t use --json with --clean.') |
| 589 cache.cleanup() | 702 cache.cleanup() |
| 590 return 0 | 703 return 0 |
| 591 | 704 |
| 592 if not options.isolated and not args: | 705 if not options.isolated and not args: |
| 593 parser.error('--isolated or command to run is required.') | 706 parser.error('--isolated or command to run is required.') |
| 594 | 707 |
| 595 auth.process_auth_options(parser, options) | 708 auth.process_auth_options(parser, options) |
| 596 | 709 |
| 597 isolateserver.process_isolate_server_options( | 710 isolateserver.process_isolate_server_options( |
| 598 parser, options, True, False) | 711 parser, options, True, False) |
| 599 if not options.isolate_server: | 712 if not options.isolate_server: |
| 600 if options.isolated: | 713 if options.isolated: |
| 601 parser.error('--isolated requires --isolate-server') | 714 parser.error('--isolated requires --isolate-server') |
| 602 if ISOLATED_OUTDIR_PARAMETER in args: | 715 if ISOLATED_OUTDIR_PARAMETER in args: |
| 603 parser.error( | 716 parser.error( |
| 604 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER) | 717 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER) |
| 605 | 718 |
| 606 if options.root_dir: | |
| 607 options.root_dir = unicode(os.path.abspath(options.root_dir)) | |
| 608 if options.json: | 719 if options.json: |
| 609 options.json = unicode(os.path.abspath(options.json)) | 720 options.json = unicode(os.path.abspath(options.json)) |
| 610 | 721 |
| 611 command = [] if options.isolated else args | 722 cipd.validate_cipd_options(parser, options) |
| 612 if options.isolate_server: | 723 |
| 613 storage = isolateserver.get_storage( | 724 root_dir = options.root_dir |
| 614 options.isolate_server, options.namespace) | 725 if root_dir: |
| 615 # Hashing schemes used by |storage| and |cache| MUST match. | 726 root_dir = unicode(os.path.abspath(root_dir)) |
| 616 with storage: | 727 file_path.ensure_tree(root_dir, 0700) |
| 617 assert storage.hash_algo == cache.hash_algo | |
| 618 return run_tha_test( | |
| 619 command, options.isolated, storage, cache, options.leak_temp_dir, | |
| 620 options.json, options.root_dir, options.hard_timeout, | |
| 621 options.grace_period, args) | |
| 622 else: | 728 else: |
| 623 return run_tha_test( | 729 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None |
| 624 command, options.isolated, None, cache, options.leak_temp_dir, | 730 |
| 625 options.json, options.root_dir, options.hard_timeout, | 731 cipd_path = None |
| 626 options.grace_period, args) | 732 if not options.cipd_package: |
| 733 if CIPD_PATH_PARAMETER in args: |
| 734 parser.error('%s in args requires --cipd-package' % CIPD_PATH_PARAMETER) |
| 735 else: |
| 736 cipd_path = make_temp_dir(u'cipd_site_root', root_dir) |
| 737 |
| 738 try: |
| 739 with ensure_packages( |
| 740 cipd_path, options.cipd_package, options.cipd_server, |
| 741 options.cipd_client_package, options.cipd_cache) as cipd_stats: |
| 742 command = [] if options.isolated else args |
| 743 if options.isolate_server: |
| 744 storage = isolateserver.get_storage( |
| 745 options.isolate_server, options.namespace) |
| 746 with storage: |
| 747 # Hashing schemes used by |storage| and |cache| MUST match. |
| 748 assert storage.hash_algo == cache.hash_algo |
| 749 return run_tha_test( |
| 750 command, options.isolated, storage, cache, options.leak_temp_dir, |
| 751 options.json, root_dir, options.hard_timeout, |
| 752 options.grace_period, args, cipd_path, cipd_stats) |
| 753 else: |
| 754 return run_tha_test( |
| 755 command, options.isolated, None, cache, options.leak_temp_dir, |
| 756 options.json, root_dir, options.hard_timeout, |
| 757 options.grace_period, args, cipd_path, cipd_stats) |
| 758 except cipd.Error as ex: |
| 759 print >> sys.stderr, ex.message |
| 760 return 1 |
| 627 | 761 |
| 628 | 762 |
| 629 if __name__ == '__main__': | 763 if __name__ == '__main__': |
| 630 subprocess42.inhibit_os_error_reporting() | 764 subprocess42.inhibit_os_error_reporting() |
| 631 # Ensure that we are always running with the correct encoding. | 765 # Ensure that we are always running with the correct encoding. |
| 632 fix_encoding.fix_encoding() | 766 fix_encoding.fix_encoding() |
| 633 sys.exit(main(sys.argv[1:])) | 767 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |