| 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 273 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 284 return bundle, { | 284 return bundle, { |
| 285 'duration': time.time() - start, | 285 'duration': time.time() - start, |
| 286 'initial_number_items': cache.initial_number_items, | 286 'initial_number_items': cache.initial_number_items, |
| 287 'initial_size': cache.initial_size, | 287 'initial_size': cache.initial_size, |
| 288 'items_cold': base64.b64encode(large.pack(sorted(cache.added))), | 288 'items_cold': base64.b64encode(large.pack(sorted(cache.added))), |
| 289 'items_hot': base64.b64encode( | 289 'items_hot': base64.b64encode( |
| 290 large.pack(sorted(set(cache.used) - set(cache.added)))), | 290 large.pack(sorted(set(cache.used) - set(cache.added)))), |
| 291 } | 291 } |
| 292 | 292 |
| 293 | 293 |
| 294 def link_outputs_to_outdir(run_dir, out_dir, outputs): |
| 295 """Links any named outputs to out_dir so they can be uploaded. |
| 296 |
| 297 Raises an error if the file already exists in that directory. |
| 298 """ |
| 299 if not outputs: |
| 300 return |
| 301 isolateserver.create_directories(out_dir, outputs) |
| 302 for o in outputs: |
| 303 try: |
| 304 file_path.link_file( |
| 305 os.path.join(out_dir, o), |
| 306 os.path.join(run_dir, o), |
| 307 file_path.HARDLINK_WITH_FALLBACK) |
| 308 except OSError as e: |
| 309 # TODO(aludwin): surface this error |
| 310 sys.stderr.write('<Could not return file %s: %s>' % (o, e)) |
| 311 |
| 312 |
| 294 def delete_and_upload(storage, out_dir, leak_temp_dir): | 313 def delete_and_upload(storage, out_dir, leak_temp_dir): |
| 295 """Deletes the temporary run directory and uploads results back. | 314 """Deletes the temporary run directory and uploads results back. |
| 296 | 315 |
| 297 Returns: | 316 Returns: |
| 298 tuple(outputs_ref, success, stats) | 317 tuple(outputs_ref, success, stats) |
| 299 - outputs_ref: a dict referring to the results archived back to the isolated | 318 - outputs_ref: a dict referring to the results archived back to the isolated |
| 300 server, if applicable. | 319 server, if applicable. |
| 301 - success: False if something occurred that means that the task must | 320 - success: False if something occurred that means that the task must |
| 302 forcibly be considered a failure, e.g. zombie processes were left | 321 forcibly be considered a failure, e.g. zombie processes were left |
| 303 behind. | 322 behind. |
| 304 - stats: uploading stats. | 323 - stats: uploading stats. |
| 305 """ | 324 """ |
| 306 | |
| 307 # Upload out_dir and generate a .isolated file out of this directory. It is | 325 # Upload out_dir and generate a .isolated file out of this directory. It is |
| 308 # only done if files were written in the directory. | 326 # only done if files were written in the directory. |
| 309 outputs_ref = None | 327 outputs_ref = None |
| 310 cold = [] | 328 cold = [] |
| 311 hot = [] | 329 hot = [] |
| 312 start = time.time() | 330 start = time.time() |
| 313 | 331 |
| 314 if fs.isdir(out_dir) and fs.listdir(out_dir): | 332 if fs.isdir(out_dir) and fs.listdir(out_dir): |
| 315 with tools.Profiler('ArchiveOutput'): | 333 with tools.Profiler('ArchiveOutput'): |
| 316 try: | 334 try: |
| (...skipping 29 matching lines...) Expand all Loading... |
| 346 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e) | 364 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e) |
| 347 stats = { | 365 stats = { |
| 348 'duration': time.time() - start, | 366 'duration': time.time() - start, |
| 349 'items_cold': base64.b64encode(large.pack(cold)), | 367 'items_cold': base64.b64encode(large.pack(cold)), |
| 350 'items_hot': base64.b64encode(large.pack(hot)), | 368 'items_hot': base64.b64encode(large.pack(hot)), |
| 351 } | 369 } |
| 352 return outputs_ref, success, stats | 370 return outputs_ref, success, stats |
| 353 | 371 |
| 354 | 372 |
| 355 def map_and_run( | 373 def map_and_run( |
| 356 command, isolated_hash, storage, isolate_cache, init_name_caches, | 374 command, isolated_hash, storage, isolate_cache, outputs, init_name_caches, |
| 357 leak_temp_dir, root_dir, hard_timeout, grace_period, bot_file, extra_args, | 375 leak_temp_dir, root_dir, hard_timeout, grace_period, bot_file, extra_args, |
| 358 install_packages_fn, use_symlinks): | 376 install_packages_fn, use_symlinks): |
| 359 """Runs a command with optional isolated input/output. | 377 """Runs a command with optional isolated input/output. |
| 360 | 378 |
| 361 See run_tha_test for argument documentation. | 379 See run_tha_test for argument documentation. |
| 362 | 380 |
| 363 Returns metadata about the result. | 381 Returns metadata about the result. |
| 364 """ | 382 """ |
| 365 assert root_dir or root_dir is None | 383 assert root_dir or root_dir is None |
| 366 assert bool(command) ^ bool(isolated_hash) | 384 assert bool(command) ^ bool(isolated_hash) |
| (...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 434 if os.environ.get('SWARMING_TASK_ID'): | 452 if os.environ.get('SWARMING_TASK_ID'): |
| 435 # Give an additional hint when running as a swarming task. | 453 # Give an additional hint when running as a swarming task. |
| 436 sys.stderr.write('<This occurs at the \'isolate\' step>\n') | 454 sys.stderr.write('<This occurs at the \'isolate\' step>\n') |
| 437 result['exit_code'] = 1 | 455 result['exit_code'] = 1 |
| 438 return result | 456 return result |
| 439 | 457 |
| 440 change_tree_read_only(run_dir, bundle.read_only) | 458 change_tree_read_only(run_dir, bundle.read_only) |
| 441 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd)) | 459 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd)) |
| 442 command = bundle.command + extra_args | 460 command = bundle.command + extra_args |
| 443 | 461 |
| 462 # If we have an explicit list of files to return, make sure their |
| 463 # directories exist now. |
| 464 if storage and outputs: |
| 465 isolateserver.create_directories(run_dir, outputs) |
| 466 |
| 444 command = tools.fix_python_path(command) | 467 command = tools.fix_python_path(command) |
| 445 command = process_command(command, out_dir, bot_file) | 468 command = process_command(command, out_dir, bot_file) |
| 446 file_path.ensure_command_has_abs_path(command, cwd) | 469 file_path.ensure_command_has_abs_path(command, cwd) |
| 447 | 470 |
| 448 init_name_caches(run_dir) | 471 init_name_caches(run_dir) |
| 449 | 472 |
| 450 sys.stdout.flush() | 473 sys.stdout.flush() |
| 451 start = time.time() | 474 start = time.time() |
| 452 try: | 475 try: |
| 453 result['exit_code'], result['had_hard_timeout'] = run_command( | 476 result['exit_code'], result['had_hard_timeout'] = run_command( |
| 454 command, cwd, tmp_dir, hard_timeout, grace_period) | 477 command, cwd, tmp_dir, hard_timeout, grace_period) |
| 455 finally: | 478 finally: |
| 456 result['duration'] = max(time.time() - start, 0) | 479 result['duration'] = max(time.time() - start, 0) |
| 457 except Exception as e: | 480 except Exception as e: |
| 458 # An internal error occurred. Report accordingly so the swarming task will | 481 # An internal error occurred. Report accordingly so the swarming task will |
| 459 # be retried automatically. | 482 # be retried automatically. |
| 460 logging.exception('internal failure: %s', e) | 483 logging.exception('internal failure: %s', e) |
| 461 result['internal_failure'] = str(e) | 484 result['internal_failure'] = str(e) |
| 462 on_error.report(None) | 485 on_error.report(None) |
| 486 |
| 487 # Clean up |
| 463 finally: | 488 finally: |
| 464 try: | 489 try: |
| 490 # Try to link files to the output directory, if specified. |
| 491 if out_dir: |
| 492 link_outputs_to_outdir(run_dir, out_dir, outputs) |
| 493 |
| 465 success = False | 494 success = False |
| 466 if leak_temp_dir: | 495 if leak_temp_dir: |
| 467 success = True | 496 success = True |
| 468 logging.warning( | 497 logging.warning( |
| 469 'Deliberately leaking %s for later examination', run_dir) | 498 'Deliberately leaking %s for later examination', run_dir) |
| 470 else: | 499 else: |
| 471 # On Windows rmtree(run_dir) call above has a synchronization effect: it | 500 # On Windows rmtree(run_dir) call above has a synchronization effect: it |
| 472 # finishes only when all task child processes terminate (since a running | 501 # finishes only when all task child processes terminate (since a running |
| 473 # process locks *.exe file). Examine out_dir only after that call | 502 # process locks *.exe file). Examine out_dir only after that call |
| 474 # completes (since child processes may write to out_dir too and we need | 503 # 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... |
| 511 result['exit_code'] = 1 | 540 result['exit_code'] = 1 |
| 512 except Exception as e: | 541 except Exception as e: |
| 513 # Swallow any exception in the main finally clause. | 542 # Swallow any exception in the main finally clause. |
| 514 if out_dir: | 543 if out_dir: |
| 515 logging.exception('Leaking out_dir %s: %s', out_dir, e) | 544 logging.exception('Leaking out_dir %s: %s', out_dir, e) |
| 516 result['internal_failure'] = str(e) | 545 result['internal_failure'] = str(e) |
| 517 return result | 546 return result |
| 518 | 547 |
| 519 | 548 |
| 520 def run_tha_test( | 549 def run_tha_test( |
| 521 command, isolated_hash, storage, isolate_cache, init_name_caches, | 550 command, isolated_hash, storage, isolate_cache, outputs, init_name_caches, |
| 522 leak_temp_dir, result_json, root_dir, hard_timeout, grace_period, bot_file, | 551 leak_temp_dir, result_json, root_dir, hard_timeout, grace_period, bot_file, |
| 523 extra_args, install_packages_fn, use_symlinks): | 552 extra_args, install_packages_fn, use_symlinks): |
| 524 """Runs an executable and records execution metadata. | 553 """Runs an executable and records execution metadata. |
| 525 | 554 |
| 526 Either command or isolated_hash must be specified. | 555 Either command or isolated_hash must be specified. |
| 527 | 556 |
| 528 If isolated_hash is specified, downloads the dependencies in the cache, | 557 If isolated_hash is specified, downloads the dependencies in the cache, |
| 529 hardlinks them into a temporary directory and runs the command specified in | 558 hardlinks them into a temporary directory and runs the command specified in |
| 530 the .isolated. | 559 the .isolated. |
| 531 | 560 |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 578 'exit_code': None, | 607 'exit_code': None, |
| 579 'had_hard_timeout': False, | 608 'had_hard_timeout': False, |
| 580 'internal_failure': 'Was terminated before completion', | 609 'internal_failure': 'Was terminated before completion', |
| 581 'outputs_ref': None, | 610 'outputs_ref': None, |
| 582 'version': 5, | 611 'version': 5, |
| 583 } | 612 } |
| 584 tools.write_json(result_json, result, dense=True) | 613 tools.write_json(result_json, result, dense=True) |
| 585 | 614 |
| 586 # run_isolated exit code. Depends on if result_json is used or not. | 615 # run_isolated exit code. Depends on if result_json is used or not. |
| 587 result = map_and_run( | 616 result = map_and_run( |
| 588 command, isolated_hash, storage, isolate_cache, init_name_caches, | 617 command, isolated_hash, storage, isolate_cache, outputs, init_name_caches, |
| 589 leak_temp_dir, root_dir, hard_timeout, grace_period, bot_file, extra_args, | 618 leak_temp_dir, root_dir, hard_timeout, grace_period, bot_file, extra_args, |
| 590 install_packages_fn, use_symlinks) | 619 install_packages_fn, use_symlinks) |
| 591 logging.info('Result:\n%s', tools.format_json(result, dense=True)) | 620 logging.info('Result:\n%s', tools.format_json(result, dense=True)) |
| 592 | 621 |
| 593 if result_json: | 622 if result_json: |
| 594 # We've found tests to delete 'work' when quitting, causing an exception | 623 # We've found tests to delete 'work' when quitting, causing an exception |
| 595 # here. Try to recreate the directory if necessary. | 624 # here. Try to recreate the directory if necessary. |
| 596 file_path.ensure_tree(os.path.dirname(result_json)) | 625 file_path.ensure_tree(os.path.dirname(result_json)) |
| 597 tools.write_json(result_json, result, dense=True) | 626 tools.write_json(result_json, result, dense=True) |
| 598 # Only return 1 if there was an internal error. | 627 # Only return 1 if there was an internal error. |
| (...skipping 158 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 757 parser.add_option( | 786 parser.add_option( |
| 758 '--hard-timeout', type='float', help='Enforce hard timeout in execution') | 787 '--hard-timeout', type='float', help='Enforce hard timeout in execution') |
| 759 parser.add_option( | 788 parser.add_option( |
| 760 '--grace-period', type='float', | 789 '--grace-period', type='float', |
| 761 help='Grace period between SIGTERM and SIGKILL') | 790 help='Grace period between SIGTERM and SIGKILL') |
| 762 parser.add_option( | 791 parser.add_option( |
| 763 '--bot-file', | 792 '--bot-file', |
| 764 help='Path to a file describing the state of the host. The content is ' | 793 help='Path to a file describing the state of the host. The content is ' |
| 765 'defined by on_before_task() in bot_config.') | 794 'defined by on_before_task() in bot_config.') |
| 766 parser.add_option( | 795 parser.add_option( |
| 796 '--output', action='append', |
| 797 help='Specifies an output to return. If no outputs are specified, all ' |
| 798 'files located in $(ISOLATED_OUTDIR) will be returned; ' |
| 799 'otherwise, outputs in both $(ISOLATED_OUTDIR) and those ' |
| 800 'specified by --output option (there can be multiple) will be ' |
| 801 'returned. Note that if a file in OUT_DIR has the same path ' |
| 802 'as an --output option, the --output version will be returned.') |
| 803 parser.add_option( |
| 767 '-a', '--argsfile', | 804 '-a', '--argsfile', |
| 768 # This is actually handled in parse_args; it's included here purely so it | 805 # This is actually handled in parse_args; it's included here purely so it |
| 769 # can make it into the help text. | 806 # can make it into the help text. |
| 770 help='Specify a file containing a JSON array of arguments to this ' | 807 help='Specify a file containing a JSON array of arguments to this ' |
| 771 'script. If --argsfile is provided, no other argument may be ' | 808 'script. If --argsfile is provided, no other argument may be ' |
| 772 'provided on the command line.') | 809 'provided on the command line.') |
| 773 data_group = optparse.OptionGroup(parser, 'Data source') | 810 data_group = optparse.OptionGroup(parser, 'Data source') |
| 774 data_group.add_option( | 811 data_group.add_option( |
| 775 '-s', '--isolated', | 812 '-s', '--isolated', |
| 776 help='Hash of the .isolated to grab from the isolate server.') | 813 help='Hash of the .isolated to grab from the isolate server.') |
| (...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 885 storage = isolateserver.get_storage( | 922 storage = isolateserver.get_storage( |
| 886 options.isolate_server, options.namespace) | 923 options.isolate_server, options.namespace) |
| 887 with storage: | 924 with storage: |
| 888 # Hashing schemes used by |storage| and |isolate_cache| MUST match. | 925 # Hashing schemes used by |storage| and |isolate_cache| MUST match. |
| 889 assert storage.hash_algo == isolate_cache.hash_algo | 926 assert storage.hash_algo == isolate_cache.hash_algo |
| 890 return run_tha_test( | 927 return run_tha_test( |
| 891 command, | 928 command, |
| 892 options.isolated, | 929 options.isolated, |
| 893 storage, | 930 storage, |
| 894 isolate_cache, | 931 isolate_cache, |
| 932 options.output, |
| 895 init_named_caches, | 933 init_named_caches, |
| 896 options.leak_temp_dir, | 934 options.leak_temp_dir, |
| 897 options.json, options.root_dir, | 935 options.json, options.root_dir, |
| 898 options.hard_timeout, | 936 options.hard_timeout, |
| 899 options.grace_period, | 937 options.grace_period, |
| 900 options.bot_file, args, | 938 options.bot_file, args, |
| 901 install_packages_fn, | 939 install_packages_fn, |
| 902 options.use_symlinks) | 940 options.use_symlinks) |
| 903 return run_tha_test( | 941 return run_tha_test( |
| 904 command, | 942 command, |
| 905 options.isolated, | 943 options.isolated, |
| 906 None, | 944 None, |
| 907 isolate_cache, | 945 isolate_cache, |
| 946 options.output, |
| 908 init_named_caches, | 947 init_named_caches, |
| 909 options.leak_temp_dir, | 948 options.leak_temp_dir, |
| 910 options.json, | 949 options.json, |
| 911 options.root_dir, | 950 options.root_dir, |
| 912 options.hard_timeout, | 951 options.hard_timeout, |
| 913 options.grace_period, | 952 options.grace_period, |
| 914 options.bot_file, args, | 953 options.bot_file, args, |
| 915 install_packages_fn, | 954 install_packages_fn, |
| 916 options.use_symlinks) | 955 options.use_symlinks) |
| 917 except (cipd.Error, named_cache.Error) as ex: | 956 except (cipd.Error, named_cache.Error) as ex: |
| 918 print >> sys.stderr, ex.message | 957 print >> sys.stderr, ex.message |
| 919 return 1 | 958 return 1 |
| 920 | 959 |
| 921 | 960 |
| 922 if __name__ == '__main__': | 961 if __name__ == '__main__': |
| 923 subprocess42.inhibit_os_error_reporting() | 962 subprocess42.inhibit_os_error_reporting() |
| 924 # Ensure that we are always running with the correct encoding. | 963 # Ensure that we are always running with the correct encoding. |
| 925 fix_encoding.fix_encoding() | 964 fix_encoding.fix_encoding() |
| 926 file_path.enable_symlink() | 965 file_path.enable_symlink() |
| 927 | 966 |
| 928 sys.exit(main(sys.argv[1:])) | 967 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |