Chromium Code Reviews| 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 |
| (...skipping 18 matching lines...) Expand all Loading... | |
| 29 | 29 |
| 30 __version__ = '0.8.6' | 30 __version__ = '0.8.6' |
| 31 | 31 |
| 32 import argparse | 32 import argparse |
| 33 import base64 | 33 import base64 |
| 34 import collections | 34 import collections |
| 35 import json | 35 import json |
| 36 import logging | 36 import logging |
| 37 import optparse | 37 import optparse |
| 38 import os | 38 import os |
| 39 import shutil | |
| 39 import sys | 40 import sys |
| 40 import tempfile | 41 import tempfile |
| 41 import time | 42 import time |
| 42 | 43 |
| 43 from third_party.depot_tools import fix_encoding | 44 from third_party.depot_tools import fix_encoding |
| 44 | 45 |
| 45 from utils import file_path | 46 from utils import file_path |
| 46 from utils import fs | 47 from utils import fs |
| 47 from utils import large | 48 from utils import large |
| 48 from utils import logging_utils | 49 from utils import logging_utils |
| (...skipping 233 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 282 return bundle, { | 283 return bundle, { |
| 283 'duration': time.time() - start, | 284 'duration': time.time() - start, |
| 284 'initial_number_items': cache.initial_number_items, | 285 'initial_number_items': cache.initial_number_items, |
| 285 'initial_size': cache.initial_size, | 286 'initial_size': cache.initial_size, |
| 286 'items_cold': base64.b64encode(large.pack(sorted(cache.added))), | 287 'items_cold': base64.b64encode(large.pack(sorted(cache.added))), |
| 287 'items_hot': base64.b64encode( | 288 'items_hot': base64.b64encode( |
| 288 large.pack(sorted(set(cache.used) - set(cache.added)))), | 289 large.pack(sorted(set(cache.used) - set(cache.added)))), |
| 289 } | 290 } |
| 290 | 291 |
| 291 | 292 |
| 293 def move_outputs_to_outdir(run_dir, out_dir, outputs): | |
| 294 """ Moves/overwrites any named outputs to out_dir so they can be uploaded.""" | |
|
M-A Ruel
2016/10/26 15:43:40
"""Moves/overwrites ...
aludwin
2016/10/26 17:32:14
Done.
| |
| 295 if outputs == None: | |
|
M-A Ruel
2016/10/26 15:43:40
if not outputs:
aludwin
2016/10/26 17:32:14
Done.
| |
| 296 return | |
| 297 isolateserver.create_directories(out_dir, outputs) | |
| 298 for o in outputs: | |
| 299 shutil.copyfile( | |
|
M-A Ruel
2016/10/26 15:43:40
this doesn't fit a line?
please use instead:
file
aludwin
2016/10/26 17:32:14
Done.
| |
| 300 os.path.join(run_dir, o), | |
| 301 os.path.join(out_dir, o)) | |
| 302 | |
| 303 | |
| 292 def delete_and_upload(storage, out_dir, leak_temp_dir): | 304 def delete_and_upload(storage, out_dir, leak_temp_dir): |
| 293 """Deletes the temporary run directory and uploads results back. | 305 """Deletes the temporary run directory and uploads results back. |
| 294 | 306 |
| 295 Returns: | 307 Returns: |
| 296 tuple(outputs_ref, success, stats) | 308 tuple(outputs_ref, success, stats) |
| 297 - outputs_ref: a dict referring to the results archived back to the isolated | 309 - outputs_ref: a dict referring to the results archived back to the isolated |
| 298 server, if applicable. | 310 server, if applicable. |
| 299 - success: False if something occurred that means that the task must | 311 - success: False if something occurred that means that the task must |
| 300 forcibly be considered a failure, e.g. zombie processes were left | 312 forcibly be considered a failure, e.g. zombie processes were left |
| 301 behind. | 313 behind. |
| 302 - stats: uploading stats. | 314 - stats: uploading stats. |
| 303 """ | 315 """ |
| 304 | |
| 305 # Upload out_dir and generate a .isolated file out of this directory. It is | 316 # Upload out_dir and generate a .isolated file out of this directory. It is |
| 306 # only done if files were written in the directory. | 317 # only done if files were written in the directory. |
| 307 outputs_ref = None | 318 outputs_ref = None |
| 308 cold = [] | 319 cold = [] |
| 309 hot = [] | 320 hot = [] |
| 310 start = time.time() | 321 start = time.time() |
| 311 | 322 |
| 312 if fs.isdir(out_dir) and fs.listdir(out_dir): | 323 if fs.isdir(out_dir) and fs.listdir(out_dir): |
| 313 with tools.Profiler('ArchiveOutput'): | 324 with tools.Profiler('ArchiveOutput'): |
| 314 try: | 325 try: |
| (...skipping 29 matching lines...) Expand all Loading... | |
| 344 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e) | 355 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e) |
| 345 stats = { | 356 stats = { |
| 346 'duration': time.time() - start, | 357 'duration': time.time() - start, |
| 347 'items_cold': base64.b64encode(large.pack(cold)), | 358 'items_cold': base64.b64encode(large.pack(cold)), |
| 348 'items_hot': base64.b64encode(large.pack(hot)), | 359 'items_hot': base64.b64encode(large.pack(hot)), |
| 349 } | 360 } |
| 350 return outputs_ref, success, stats | 361 return outputs_ref, success, stats |
| 351 | 362 |
| 352 | 363 |
| 353 def map_and_run( | 364 def map_and_run( |
| 354 command, isolated_hash, storage, isolate_cache, leak_temp_dir, root_dir, | 365 command, isolated_hash, storage, isolate_cache, outputs, leak_temp_dir, |
| 355 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn, | 366 root_dir, hard_timeout, grace_period, bot_file, extra_args, |
| 356 use_symlinks): | 367 install_packages_fn, use_symlinks): |
| 357 """Runs a command with optional isolated input/output. | 368 """Runs a command with optional isolated input/output. |
| 358 | 369 |
| 359 See run_tha_test for argument documentation. | 370 See run_tha_test for argument documentation. |
| 360 | 371 |
| 361 Returns metadata about the result. | 372 Returns metadata about the result. |
| 362 """ | 373 """ |
| 363 assert root_dir or root_dir is None | 374 assert root_dir or root_dir is None |
| 364 assert bool(command) ^ bool(isolated_hash) | 375 assert bool(command) ^ bool(isolated_hash) |
| 365 result = { | 376 result = { |
| 366 'duration': None, | 377 'duration': None, |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 432 if os.environ.get('SWARMING_TASK_ID'): | 443 if os.environ.get('SWARMING_TASK_ID'): |
| 433 # Give an additional hint when running as a swarming task. | 444 # Give an additional hint when running as a swarming task. |
| 434 sys.stderr.write('<This occurs at the \'isolate\' step>\n') | 445 sys.stderr.write('<This occurs at the \'isolate\' step>\n') |
| 435 result['exit_code'] = 1 | 446 result['exit_code'] = 1 |
| 436 return result | 447 return result |
| 437 | 448 |
| 438 change_tree_read_only(run_dir, bundle.read_only) | 449 change_tree_read_only(run_dir, bundle.read_only) |
| 439 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd)) | 450 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd)) |
| 440 command = bundle.command + extra_args | 451 command = bundle.command + extra_args |
| 441 | 452 |
| 453 # If we have an explicit list of files to return, make sure their | |
| 454 # directories exist now | |
|
M-A Ruel
2016/10/26 15:43:39
add period.
aludwin
2016/10/26 17:32:14
Done.
| |
| 455 if storage and outputs != None: | |
|
M-A Ruel
2016/10/26 15:43:39
if storage and outputs:
aludwin
2016/10/26 17:32:14
Done.
| |
| 456 isolateserver.create_directories(run_dir, outputs) | |
| 457 | |
| 442 command = tools.fix_python_path(command) | 458 command = tools.fix_python_path(command) |
| 443 command = process_command(command, out_dir, bot_file) | 459 command = process_command(command, out_dir, bot_file) |
| 444 file_path.ensure_command_has_abs_path(command, cwd) | 460 file_path.ensure_command_has_abs_path(command, cwd) |
| 445 | 461 |
| 446 sys.stdout.flush() | 462 sys.stdout.flush() |
| 447 start = time.time() | 463 start = time.time() |
| 448 try: | 464 try: |
| 449 result['exit_code'], result['had_hard_timeout'] = run_command( | 465 result['exit_code'], result['had_hard_timeout'] = run_command( |
| 450 command, cwd, tmp_dir, hard_timeout, grace_period) | 466 command, cwd, tmp_dir, hard_timeout, grace_period) |
| 451 finally: | 467 finally: |
| 452 result['duration'] = max(time.time() - start, 0) | 468 result['duration'] = max(time.time() - start, 0) |
| 453 except Exception as e: | 469 except Exception as e: |
| 454 # An internal error occurred. Report accordingly so the swarming task will | 470 # An internal error occurred. Report accordingly so the swarming task will |
| 455 # be retried automatically. | 471 # be retried automatically. |
| 456 logging.exception('internal failure: %s', e) | 472 logging.exception('internal failure: %s', e) |
| 457 result['internal_failure'] = str(e) | 473 result['internal_failure'] = str(e) |
| 458 on_error.report(None) | 474 on_error.report(None) |
| 475 | |
| 476 # Try to copy files to the output directory, if specified. They must *all* | |
| 477 # exist; we'll treat this as a task failure if we can't copy the files for | |
|
M-A Ruel
2016/10/26 15:43:40
can the outputs be directories? I feel this should
aludwin
2016/10/26 17:32:14
Currently, Bazel doesn't support this - but it wil
| |
| 478 # some reason. | |
| 479 try: | |
| 480 if out_dir: | |
| 481 move_outputs_to_outdir(run_dir, out_dir, outputs) | |
| 482 except Exception as e: | |
|
M-A Ruel
2016/10/26 15:43:40
Please catch the exceptions at move_outputs_to_out
aludwin
2016/10/26 17:32:14
Done.
| |
| 483 sys.stderr.write('<Could not return requested files: %s>' % e) | |
| 484 result['exit_code'] = 1 | |
| 485 return result | |
| 486 | |
| 487 # Clean up | |
| 459 finally: | 488 finally: |
| 460 try: | 489 try: |
| 461 if leak_temp_dir: | 490 if leak_temp_dir: |
| 462 logging.warning( | 491 logging.warning( |
| 463 'Deliberately leaking %s for later examination', run_dir) | 492 'Deliberately leaking %s for later examination', run_dir) |
| 464 else: | 493 else: |
| 465 # On Windows rmtree(run_dir) call above has a synchronization effect: it | 494 # On Windows rmtree(run_dir) call above has a synchronization effect: it |
| 466 # finishes only when all task child processes terminate (since a running | 495 # finishes only when all task child processes terminate (since a running |
| 467 # process locks *.exe file). Examine out_dir only after that call | 496 # process locks *.exe file). Examine out_dir only after that call |
| 468 # completes (since child processes may write to out_dir too and we need | 497 # completes (since child processes may write to out_dir too and we need |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 505 result['exit_code'] = 1 | 534 result['exit_code'] = 1 |
| 506 except Exception as e: | 535 except Exception as e: |
| 507 # Swallow any exception in the main finally clause. | 536 # Swallow any exception in the main finally clause. |
| 508 if out_dir: | 537 if out_dir: |
| 509 logging.exception('Leaking out_dir %s: %s', out_dir, e) | 538 logging.exception('Leaking out_dir %s: %s', out_dir, e) |
| 510 result['internal_failure'] = str(e) | 539 result['internal_failure'] = str(e) |
| 511 return result | 540 return result |
| 512 | 541 |
| 513 | 542 |
| 514 def run_tha_test( | 543 def run_tha_test( |
| 515 command, isolated_hash, storage, isolate_cache, leak_temp_dir, result_json, | 544 command, isolated_hash, storage, isolate_cache, outputs, |
| 516 root_dir, hard_timeout, grace_period, bot_file, extra_args, | 545 leak_temp_dir, result_json, root_dir, hard_timeout, grace_period, |
| 517 install_packages_fn, use_symlinks): | 546 bot_file, extra_args, install_packages_fn, use_symlinks): |
| 518 """Runs an executable and records execution metadata. | 547 """Runs an executable and records execution metadata. |
| 519 | 548 |
| 520 Either command or isolated_hash must be specified. | 549 Either command or isolated_hash must be specified. |
| 521 | 550 |
| 522 If isolated_hash is specified, downloads the dependencies in the cache, | 551 If isolated_hash is specified, downloads the dependencies in the cache, |
| 523 hardlinks them into a temporary directory and runs the command specified in | 552 hardlinks them into a temporary directory and runs the command specified in |
| 524 the .isolated. | 553 the .isolated. |
| 525 | 554 |
| 526 A temporary directory is created to hold the output files. The content inside | 555 A temporary directory is created to hold the output files. The content inside |
| 527 this directory will be uploaded back to |storage| packaged as a .isolated | 556 this directory will be uploaded back to |storage| packaged as a .isolated |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 570 'exit_code': None, | 599 'exit_code': None, |
| 571 'had_hard_timeout': False, | 600 'had_hard_timeout': False, |
| 572 'internal_failure': 'Was terminated before completion', | 601 'internal_failure': 'Was terminated before completion', |
| 573 'outputs_ref': None, | 602 'outputs_ref': None, |
| 574 'version': 5, | 603 'version': 5, |
| 575 } | 604 } |
| 576 tools.write_json(result_json, result, dense=True) | 605 tools.write_json(result_json, result, dense=True) |
| 577 | 606 |
| 578 # run_isolated exit code. Depends on if result_json is used or not. | 607 # run_isolated exit code. Depends on if result_json is used or not. |
| 579 result = map_and_run( | 608 result = map_and_run( |
| 580 command, isolated_hash, storage, isolate_cache, leak_temp_dir, root_dir, | 609 command, isolated_hash, storage, isolate_cache, outputs, |
| 581 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn, | 610 leak_temp_dir, root_dir, hard_timeout, grace_period, bot_file, |
| 582 use_symlinks) | 611 extra_args, install_packages_fn, use_symlinks) |
| 583 logging.info('Result:\n%s', tools.format_json(result, dense=True)) | 612 logging.info('Result:\n%s', tools.format_json(result, dense=True)) |
| 584 | 613 |
| 585 if result_json: | 614 if result_json: |
| 586 # We've found tests to delete 'work' when quitting, causing an exception | 615 # We've found tests to delete 'work' when quitting, causing an exception |
| 587 # here. Try to recreate the directory if necessary. | 616 # here. Try to recreate the directory if necessary. |
| 588 file_path.ensure_tree(os.path.dirname(result_json)) | 617 file_path.ensure_tree(os.path.dirname(result_json)) |
| 589 tools.write_json(result_json, result, dense=True) | 618 tools.write_json(result_json, result, dense=True) |
| 590 # Only return 1 if there was an internal error. | 619 # Only return 1 if there was an internal error. |
| 591 return int(bool(result['internal_failure'])) | 620 return int(bool(result['internal_failure'])) |
| 592 | 621 |
| (...skipping 134 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 727 parser.add_option( | 756 parser.add_option( |
| 728 '--hard-timeout', type='float', help='Enforce hard timeout in execution') | 757 '--hard-timeout', type='float', help='Enforce hard timeout in execution') |
| 729 parser.add_option( | 758 parser.add_option( |
| 730 '--grace-period', type='float', | 759 '--grace-period', type='float', |
| 731 help='Grace period between SIGTERM and SIGKILL') | 760 help='Grace period between SIGTERM and SIGKILL') |
| 732 parser.add_option( | 761 parser.add_option( |
| 733 '--bot-file', | 762 '--bot-file', |
| 734 help='Path to a file describing the state of the host. The content is ' | 763 help='Path to a file describing the state of the host. The content is ' |
| 735 'defined by on_before_task() in bot_config.') | 764 'defined by on_before_task() in bot_config.') |
| 736 parser.add_option( | 765 parser.add_option( |
| 766 '--output', action='append', | |
| 767 help='Specifies an output to return. If no outputs are specified, all ' | |
| 768 'files located in $(ISOLATED_OUT_DIR) will be returned; ' | |
| 769 'otherwise, outputs in both $(ISOLATED_OUT_DIR) and those ' | |
| 770 'specified by --output option (there can be multiple) will be ' | |
| 771 'returned. Note that if a file in OUT_DIR has the same path ' | |
| 772 'as an --output option, the --output version will be returned.') | |
| 773 parser.add_option( | |
| 737 '-a', '--argsfile', | 774 '-a', '--argsfile', |
| 738 # This is actually handled in parse_args; it's included here purely so it | 775 # This is actually handled in parse_args; it's included here purely so it |
| 739 # can make it into the help text. | 776 # can make it into the help text. |
| 740 help='Specify a file containing a JSON array of arguments to this ' | 777 help='Specify a file containing a JSON array of arguments to this ' |
| 741 'script. If --argsfile is provided, no other argument may be ' | 778 'script. If --argsfile is provided, no other argument may be ' |
| 742 'provided on the command line.') | 779 'provided on the command line.') |
| 743 data_group = optparse.OptionGroup(parser, 'Data source') | 780 data_group = optparse.OptionGroup(parser, 'Data source') |
| 744 data_group.add_option( | 781 data_group.add_option( |
| 745 '-s', '--isolated', | 782 '-s', '--isolated', |
| 746 help='Hash of the .isolated to grab from the isolate server.') | 783 help='Hash of the .isolated to grab from the isolate server.') |
| (...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 839 | 876 |
| 840 try: | 877 try: |
| 841 command = [] if options.isolated else args | 878 command = [] if options.isolated else args |
| 842 if options.isolate_server: | 879 if options.isolate_server: |
| 843 storage = isolateserver.get_storage( | 880 storage = isolateserver.get_storage( |
| 844 options.isolate_server, options.namespace) | 881 options.isolate_server, options.namespace) |
| 845 with storage: | 882 with storage: |
| 846 # Hashing schemes used by |storage| and |isolated_cache| MUST match. | 883 # Hashing schemes used by |storage| and |isolated_cache| MUST match. |
| 847 assert storage.hash_algo == isolated_cache.hash_algo | 884 assert storage.hash_algo == isolated_cache.hash_algo |
| 848 return run_tha_test( | 885 return run_tha_test( |
| 849 command, options.isolated, storage, isolated_cache, | 886 command, options.isolated, storage, isolated_cache, options.output, |
| 850 options.leak_temp_dir, options.json, options.root_dir, | 887 options.leak_temp_dir, options.json, options.root_dir, |
| 851 options.hard_timeout, options.grace_period, options.bot_file, args, | 888 options.hard_timeout, options.grace_period, options.bot_file, args, |
| 852 install_packages_fn, options.use_symlinks) | 889 install_packages_fn, options.use_symlinks) |
| 853 return run_tha_test( | 890 return run_tha_test( |
| 854 command, options.isolated, None, isolated_cache, options.leak_temp_dir, | 891 command, options.isolated, None, isolated_cache, options.output, |
| 855 options.json, options.root_dir, options.hard_timeout, | 892 options.leak_temp_dir, options.json, options.root_dir, |
| 856 options.grace_period, options.bot_file, args, install_packages_fn, | 893 options.hard_timeout, options.grace_period, options.bot_file, |
| 857 options.use_symlinks) | 894 args, install_packages_fn, options.use_symlinks) |
| 858 except cipd.Error as ex: | 895 except cipd.Error as ex: |
| 859 print >> sys.stderr, ex.message | 896 print >> sys.stderr, ex.message |
| 860 return 1 | 897 return 1 |
| 861 | 898 |
| 862 | 899 |
| 863 if __name__ == '__main__': | 900 if __name__ == '__main__': |
| 864 subprocess42.inhibit_os_error_reporting() | 901 subprocess42.inhibit_os_error_reporting() |
| 865 # Ensure that we are always running with the correct encoding. | 902 # Ensure that we are always running with the correct encoding. |
| 866 fix_encoding.fix_encoding() | 903 fix_encoding.fix_encoding() |
| 867 file_path.enable_symlink() | 904 file_path.enable_symlink() |
| 868 | 905 |
| 869 sys.exit(main(sys.argv[1:])) | 906 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |