| OLD | NEW |
| 1 # Copyright 2013 The LUCI Authors. All rights reserved. | 1 # Copyright 2013 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 """Runs a Swarming task. | 5 """Runs a Swarming task. |
| 6 | 6 |
| 7 Downloads all the necessary files to run the task, executes the command and | 7 Downloads all the necessary files to run the task, executes the command and |
| 8 streams results back to the Swarming server. | 8 streams results back to the Swarming server. |
| 9 | 9 |
| 10 The process exit code is 0 when the task was executed, even if the task itself | 10 The process exit code is 0 when the task was executed, even if the task itself |
| 11 failed. If there's any failure in the setup or teardown, like invalid packet | 11 failed. If there's any failure in the setup or teardown, like invalid packet |
| 12 response, failure to contact the server, etc, a non zero exit code is used. It's | 12 response, failure to contact the server, etc, a non zero exit code is used. It's |
| 13 up to the calling process (bot_main.py) to signal that there was an internal | 13 up to the calling process (bot_main.py) to signal that there was an internal |
| 14 failure and to cancel this task run and ask the server to retry it. | 14 failure and to cancel this task run and ask the server to retry it. |
| 15 """ | 15 """ |
| 16 | 16 |
| 17 import base64 | 17 import base64 |
| 18 import json | 18 import json |
| 19 import logging | 19 import logging |
| 20 import optparse | 20 import optparse |
| 21 import os | 21 import os |
| 22 import signal | 22 import signal |
| 23 import sys | 23 import sys |
| 24 import time | 24 import time |
| 25 | 25 |
| 26 from utils import file_path |
| 26 from utils import net | 27 from utils import net |
| 27 from utils import on_error | 28 from utils import on_error |
| 28 from utils import subprocess42 | 29 from utils import subprocess42 |
| 29 from utils import zip_package | 30 from utils import zip_package |
| 30 | 31 |
| 31 import bot_auth | 32 import bot_auth |
| 32 import file_reader | 33 import file_reader |
| 33 | 34 |
| 34 | 35 |
| 35 # Path to this file or the zip containing this file. | 36 # Path to this file or the zip containing this file. |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 74 | 75 |
| 75 def get_run_isolated(): | 76 def get_run_isolated(): |
| 76 """Returns the path to itself to run run_isolated. | 77 """Returns the path to itself to run run_isolated. |
| 77 | 78 |
| 78 Mocked in test to point to the real run_isolated.py script. | 79 Mocked in test to point to the real run_isolated.py script. |
| 79 """ | 80 """ |
| 80 return [sys.executable, THIS_FILE, 'run_isolated'] | 81 return [sys.executable, THIS_FILE, 'run_isolated'] |
| 81 | 82 |
| 82 | 83 |
| 83 def get_isolated_cmd( | 84 def get_isolated_cmd( |
| 84 work_dir, task_details, isolated_result, min_free_space, bot_file): | 85 work_dir, task_details, isolated_result, bot_file, package_list, |
| 86 min_free_space): |
| 85 """Returns the command to call run_isolated. Mocked in tests.""" | 87 """Returns the command to call run_isolated. Mocked in tests.""" |
| 86 assert (bool(task_details.command) != | 88 assert (bool(task_details.command) != |
| 87 bool(task_details.isolated and task_details.isolated.get('input'))) | 89 bool(task_details.isolated and task_details.isolated.get('input'))) |
| 88 bot_dir = os.path.dirname(work_dir) | 90 bot_dir = os.path.dirname(work_dir) |
| 89 if os.path.isfile(isolated_result): | 91 if os.path.isfile(isolated_result): |
| 90 os.remove(isolated_result) | 92 os.remove(isolated_result) |
| 91 cmd = get_run_isolated() | 93 cmd = get_run_isolated() |
| 92 | 94 |
| 93 if task_details.isolated: | 95 if task_details.isolated: |
| 94 cmd.extend( | 96 cmd.extend( |
| 95 [ | 97 [ |
| 96 '-I', task_details.isolated['server'].encode('utf-8'), | 98 '-I', task_details.isolated['server'].encode('utf-8'), |
| 97 '--namespace', task_details.isolated['namespace'].encode('utf-8'), | 99 '--namespace', task_details.isolated['namespace'].encode('utf-8'), |
| 98 ]) | 100 ]) |
| 99 isolated_input = task_details.isolated.get('input') | 101 isolated_input = task_details.isolated.get('input') |
| 100 if isolated_input: | 102 if isolated_input: |
| 101 cmd.extend( | 103 cmd.extend( |
| 102 [ | 104 [ |
| 103 '--isolated', isolated_input, | 105 '--isolated', isolated_input, |
| 104 ]) | 106 ]) |
| 105 | 107 |
| 106 if task_details.cipd_input and task_details.cipd_input.get('packages'): | 108 if task_details.cipd_input and task_details.cipd_input.get('packages'): |
| 107 to_pkg = lambda p: '%s:%s' % (p['package_name'], p['version']) | 109 package_json = { |
| 110 # cipd_input and run_isolated.py use the same format for 'packages' |
| 111 # property. It is a list of package JSON objects. |
| 112 'packages': task_details.cipd_input['packages'], |
| 113 } |
| 114 with open(package_list, 'wb') as f: |
| 115 json.dump(package_json, f) |
| 108 cmd.extend( | 116 cmd.extend( |
| 109 [ | 117 [ |
| 110 '--cipd-cache', os.path.join(bot_dir, 'cipd_cache'), | 118 '--cipd-cache', os.path.join(bot_dir, 'cipd_cache'), |
| 111 '--cipd-client-package', | 119 '--cipd-client-package', |
| 112 to_pkg(task_details.cipd_input.get('client_package')), | 120 task_details.cipd_input['client_package']['package_name'], |
| 121 '--cipd-client-version', |
| 122 task_details.cipd_input['client_package']['version'], |
| 123 '--cipd-package-list', package_list, |
| 113 '--cipd-server', task_details.cipd_input.get('server'), | 124 '--cipd-server', task_details.cipd_input.get('server'), |
| 114 ]) | 125 ]) |
| 115 for p in task_details.cipd_input['packages']: | |
| 116 cmd.extend(['--cipd-package', to_pkg(p)]) | |
| 117 | 126 |
| 118 cmd.extend( | 127 cmd.extend( |
| 119 [ | 128 [ |
| 120 '--json', isolated_result, | 129 '--json', isolated_result, |
| 121 '--log-file', os.path.join(bot_dir, 'logs', 'run_isolated.log'), | 130 '--log-file', os.path.join(bot_dir, 'logs', 'run_isolated.log'), |
| 122 '--cache', os.path.join(bot_dir, 'isolated_cache'), | 131 '--cache', os.path.join(bot_dir, 'isolated_cache'), |
| 123 '--root-dir', os.path.join(work_dir, 'isolated'), | 132 '--root-dir', os.path.join(work_dir, 'isolated'), |
| 124 ]) | 133 ]) |
| 125 if min_free_space: | 134 if min_free_space: |
| 126 cmd.extend(('--min-free-space', str(min_free_space))) | 135 cmd.extend(('--min-free-space', str(min_free_space))) |
| (...skipping 235 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 362 # Signal the command is about to be started. | 371 # Signal the command is about to be started. |
| 363 last_packet = start = now = monotonic_time() | 372 last_packet = start = now = monotonic_time() |
| 364 params = { | 373 params = { |
| 365 'cost_usd': cost_usd_hour * (now - task_start) / 60. / 60., | 374 'cost_usd': cost_usd_hour * (now - task_start) / 60. / 60., |
| 366 'id': task_details.bot_id, | 375 'id': task_details.bot_id, |
| 367 'task_id': task_details.task_id, | 376 'task_id': task_details.task_id, |
| 368 } | 377 } |
| 369 post_update(swarming_server, headers_cb(), params, None, '', 0) | 378 post_update(swarming_server, headers_cb(), params, None, '', 0) |
| 370 | 379 |
| 371 isolated_result = os.path.join(work_dir, 'isolated_result.json') | 380 isolated_result = os.path.join(work_dir, 'isolated_result.json') |
| 381 package_list = os.path.join(work_dir, 'package_list.json') |
| 372 cmd = get_isolated_cmd( | 382 cmd = get_isolated_cmd( |
| 373 work_dir, task_details, isolated_result, min_free_space, bot_file) | 383 work_dir, task_details, isolated_result, bot_file, package_list, |
| 384 min_free_space) |
| 374 # Hard timeout enforcement is deferred to run_isolated. Grace is doubled to | 385 # Hard timeout enforcement is deferred to run_isolated. Grace is doubled to |
| 375 # give one 'grace_period' slot to the child process and one slot to upload | 386 # give one 'grace_period' slot to the child process and one slot to upload |
| 376 # the results back. | 387 # the results back. |
| 377 task_details.hard_timeout = 0 | 388 task_details.hard_timeout = 0 |
| 378 if task_details.grace_period: | 389 if task_details.grace_period: |
| 379 task_details.grace_period *= 2 | 390 task_details.grace_period *= 2 |
| 380 | 391 |
| 381 try: | 392 try: |
| 382 # TODO(maruel): Support both channels independently and display stderr in | 393 # TODO(maruel): Support both channels independently and display stderr in |
| 383 # red. | 394 # red. |
| (...skipping 173 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 557 swarming_server, headers_cb(), params, exit_code, | 568 swarming_server, headers_cb(), params, exit_code, |
| 558 stdout, output_chunk_start) | 569 stdout, output_chunk_start) |
| 559 return { | 570 return { |
| 560 u'exit_code': exit_code, | 571 u'exit_code': exit_code, |
| 561 u'hard_timeout': had_hard_timeout, | 572 u'hard_timeout': had_hard_timeout, |
| 562 u'io_timeout': had_io_timeout, | 573 u'io_timeout': had_io_timeout, |
| 563 u'must_signal_internal_failure': must_signal_internal_failure, | 574 u'must_signal_internal_failure': must_signal_internal_failure, |
| 564 u'version': OUT_VERSION, | 575 u'version': OUT_VERSION, |
| 565 } | 576 } |
| 566 finally: | 577 finally: |
| 567 try: | 578 file_path.try_remove(unicode(isolated_result)) |
| 568 os.remove(isolated_result) | 579 file_path.try_remove(unicode(package_list)) |
| 569 except OSError: | |
| 570 pass | |
| 571 stop_headers_reader() | 580 stop_headers_reader() |
| 572 | 581 |
| 573 | 582 |
| 574 def main(args): | 583 def main(args): |
| 575 subprocess42.inhibit_os_error_reporting() | 584 subprocess42.inhibit_os_error_reporting() |
| 576 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__) | 585 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__) |
| 577 parser.add_option('--in-file', help='Name of the request file') | 586 parser.add_option('--in-file', help='Name of the request file') |
| 578 parser.add_option( | 587 parser.add_option( |
| 579 '--out-file', help='Name of the JSON file to write a task summary to') | 588 '--out-file', help='Name of the JSON file to write a task summary to') |
| 580 parser.add_option( | 589 parser.add_option( |
| (...skipping 19 matching lines...) Expand all Loading... |
| 600 options.start = now | 609 options.start = now |
| 601 | 610 |
| 602 try: | 611 try: |
| 603 load_and_run( | 612 load_and_run( |
| 604 options.in_file, options.swarming_server, options.cost_usd_hour, | 613 options.in_file, options.swarming_server, options.cost_usd_hour, |
| 605 options.start, options.out_file, options.min_free_space, | 614 options.start, options.out_file, options.min_free_space, |
| 606 options.bot_file) | 615 options.bot_file) |
| 607 return 0 | 616 return 0 |
| 608 finally: | 617 finally: |
| 609 logging.info('quitting') | 618 logging.info('quitting') |
| OLD | NEW |