| 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 | 16 Any ${EXECUTABLE_SUFFIX} on the command line will be replaced with ".exe" string |
| 17 on Windows and "" on other platforms. | 17 on Windows and "" on other platforms. |
| 18 | 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 | |
| 23 Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a | 19 Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a |
| 24 temporary directory upon execution of the command specified in the .isolated | 20 temporary directory upon execution of the command specified in the .isolated |
| 25 file. All content written to this directory will be uploaded upon termination | 21 file. All content written to this directory will be uploaded upon termination |
| 26 and the .isolated file describing this directory will be printed to stdout. | 22 and the .isolated file describing this directory will be printed to stdout. |
| 27 """ | 23 """ |
| 28 | 24 |
| 29 __version__ = '0.8.0' | 25 __version__ = '0.8.1' |
| 30 | 26 |
| 31 import base64 | 27 import base64 |
| 32 import contextlib | |
| 33 import hashlib | |
| 34 import logging | 28 import logging |
| 35 import optparse | 29 import optparse |
| 36 import os | 30 import os |
| 37 import sys | 31 import sys |
| 38 import tempfile | 32 import tempfile |
| 39 import time | 33 import time |
| 40 | 34 |
| 41 from third_party.depot_tools import fix_encoding | 35 from third_party.depot_tools import fix_encoding |
| 42 | 36 |
| 43 from utils import file_path | 37 from utils import file_path |
| 44 from utils import fs | 38 from utils import fs |
| 45 from utils import large | 39 from utils import large |
| 46 from utils import logging_utils | 40 from utils import logging_utils |
| 47 from utils import on_error | 41 from utils import on_error |
| 48 from utils import subprocess42 | 42 from utils import subprocess42 |
| 49 from utils import tools | 43 from utils import tools |
| 50 from utils import zip_package | 44 from utils import zip_package |
| 51 | 45 |
| 52 import auth | 46 import auth |
| 53 import cipd | 47 import cipd |
| 54 import isolateserver | 48 import isolateserver |
| 55 | 49 |
| 56 | 50 |
| 57 ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}' | 51 ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}' |
| 58 CIPD_PATH_PARAMETER = '${CIPD_PATH}' | |
| 59 EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}' | 52 EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}' |
| 60 SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}' | 53 SWARMING_BOT_FILE_PARAMETER = '${SWARMING_BOT_FILE}' |
| 61 | 54 |
| 62 # Absolute path to this file (can be None if running from zip on Mac). | 55 # Absolute path to this file (can be None if running from zip on Mac). |
| 63 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None | 56 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None |
| 64 | 57 |
| 65 # Directory that contains this file (might be inside zip package). | 58 # Directory that contains this file (might be inside zip package). |
| 66 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None | 59 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None |
| 67 | 60 |
| 68 # Directory that contains currently running script file. | 61 # Directory that contains currently running script file. |
| (...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 147 # is not yet changed to verify the hash of the content of the files it is | 140 # 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 | 141 # looking at, so that if a test modifies an input file, the file must be |
| 149 # deleted. | 142 # deleted. |
| 150 file_path.make_tree_writeable(rootdir) | 143 file_path.make_tree_writeable(rootdir) |
| 151 else: | 144 else: |
| 152 raise ValueError( | 145 raise ValueError( |
| 153 'change_tree_read_only(%s, %s): Unknown flag %s' % | 146 'change_tree_read_only(%s, %s): Unknown flag %s' % |
| 154 (rootdir, read_only, read_only)) | 147 (rootdir, read_only, read_only)) |
| 155 | 148 |
| 156 | 149 |
| 157 def process_command(command, out_dir, cipd_path, bot_file): | 150 def process_command(command, out_dir, bot_file): |
| 158 """Replaces variables in a command line. | 151 """Replaces variables in a command line. |
| 159 | 152 |
| 160 Raises: | 153 Raises: |
| 161 ValueError if a parameter is requested in |command| but its value is not | 154 ValueError if a parameter is requested in |command| but its value is not |
| 162 provided. | 155 provided. |
| 163 """ | 156 """ |
| 164 def fix(arg): | 157 def fix(arg): |
| 165 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX) | 158 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX) |
| 166 replace_slash = False | 159 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 | |
| 172 if ISOLATED_OUTDIR_PARAMETER in arg: | 160 if ISOLATED_OUTDIR_PARAMETER in arg: |
| 173 if not out_dir: | 161 if not out_dir: |
| 174 raise ValueError('out_dir is requested in command, but not provided') | 162 raise ValueError('out_dir is requested in command, but not provided') |
| 175 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir) | 163 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir) |
| 176 replace_slash = True | 164 replace_slash = True |
| 165 if SWARMING_BOT_FILE_PARAMETER in arg: |
| 166 if bot_file: |
| 167 arg = arg.replace(SWARMING_BOT_FILE_PARAMETER, bot_file) |
| 168 replace_slash = True |
| 169 else: |
| 170 logging.warning('SWARMING_BOT_FILE_PARAMETER found in command, but no ' |
| 171 'bot_file specified. Leaving parameter unchanged.') |
| 177 if replace_slash: | 172 if replace_slash: |
| 178 # Replace slashes only if parameters are present | 173 # Replace slashes only if parameters are present |
| 179 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar' | 174 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar' |
| 180 arg = arg.replace('/', os.sep) | 175 arg = arg.replace('/', os.sep) |
| 181 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.') | |
| 188 return arg | 176 return arg |
| 189 | 177 |
| 190 return [fix(arg) for arg in command] | 178 return [fix(arg) for arg in command] |
| 191 | 179 |
| 192 | 180 |
| 193 def run_command(command, cwd, tmp_dir, hard_timeout, grace_period): | 181 def run_command(command, cwd, tmp_dir, hard_timeout, grace_period): |
| 194 """Runs the command. | 182 """Runs the command. |
| 195 | 183 |
| 196 Returns: | 184 Returns: |
| 197 tuple(process exit code, bool if had a hard timeout) | 185 tuple(process exit code, bool if had a hard timeout) |
| (...skipping 144 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 342 stats = { | 330 stats = { |
| 343 'duration': time.time() - start, | 331 'duration': time.time() - start, |
| 344 'items_cold': base64.b64encode(large.pack(cold)), | 332 'items_cold': base64.b64encode(large.pack(cold)), |
| 345 'items_hot': base64.b64encode(large.pack(hot)), | 333 'items_hot': base64.b64encode(large.pack(hot)), |
| 346 } | 334 } |
| 347 return outputs_ref, success, stats | 335 return outputs_ref, success, stats |
| 348 | 336 |
| 349 | 337 |
| 350 def map_and_run( | 338 def map_and_run( |
| 351 command, isolated_hash, storage, cache, leak_temp_dir, root_dir, | 339 command, isolated_hash, storage, cache, leak_temp_dir, root_dir, |
| 352 hard_timeout, grace_period, bot_file, extra_args, cipd_path, cipd_stats): | 340 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn): |
| 353 """Runs a command with optional isolated input/output. | 341 """Runs a command with optional isolated input/output. |
| 354 | 342 |
| 355 See run_tha_test for argument documentation. | 343 See run_tha_test for argument documentation. |
| 356 | 344 |
| 357 Returns metadata about the result. | 345 Returns metadata about the result. |
| 358 """ | 346 """ |
| 359 assert bool(command) ^ bool(isolated_hash) | 347 assert bool(command) ^ bool(isolated_hash) |
| 360 result = { | 348 result = { |
| 361 'duration': None, | 349 'duration': None, |
| 362 'exit_code': None, | 350 'exit_code': None, |
| (...skipping 15 matching lines...) Expand all Loading... |
| 378 # 'upload': { | 366 # 'upload': { |
| 379 # 'duration': 0., | 367 # 'duration': 0., |
| 380 # 'items_cold': '<large.pack()>', | 368 # 'items_cold': '<large.pack()>', |
| 381 # 'items_hot': '<large.pack()>', | 369 # 'items_hot': '<large.pack()>', |
| 382 # }, | 370 # }, |
| 383 # }, | 371 # }, |
| 384 }, | 372 }, |
| 385 'outputs_ref': None, | 373 'outputs_ref': None, |
| 386 'version': 5, | 374 'version': 5, |
| 387 } | 375 } |
| 388 if cipd_stats: | |
| 389 result['stats']['cipd'] = cipd_stats | |
| 390 | 376 |
| 391 if root_dir: | 377 if root_dir: |
| 392 file_path.ensure_tree(root_dir, 0700) | 378 file_path.ensure_tree(root_dir, 0700) |
| 393 else: | 379 else: |
| 394 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None | 380 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None |
| 395 run_dir = make_temp_dir(u'isolated_run', root_dir) | 381 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 | 382 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) | 383 tmp_dir = make_temp_dir(u'isolated_tmp', root_dir) |
| 398 cwd = run_dir | 384 cwd = run_dir |
| 399 | 385 |
| 400 try: | 386 try: |
| 387 cipd_stats = install_packages_fn(run_dir) |
| 388 if cipd_stats: |
| 389 result['stats']['cipd'] = cipd_stats |
| 390 |
| 401 if isolated_hash: | 391 if isolated_hash: |
| 402 isolated_stats = result['stats'].setdefault('isolated', {}) | 392 isolated_stats = result['stats'].setdefault('isolated', {}) |
| 403 bundle, isolated_stats['download'] = fetch_and_measure( | 393 bundle, isolated_stats['download'] = fetch_and_measure( |
| 404 isolated_hash=isolated_hash, | 394 isolated_hash=isolated_hash, |
| 405 storage=storage, | 395 storage=storage, |
| 406 cache=cache, | 396 cache=cache, |
| 407 outdir=run_dir) | 397 outdir=run_dir) |
| 408 if not bundle.command: | 398 if not bundle.command: |
| 409 # Handle this as a task failure, not an internal failure. | 399 # Handle this as a task failure, not an internal failure. |
| 410 sys.stderr.write( | 400 sys.stderr.write( |
| 411 '<The .isolated doesn\'t declare any command to run!>\n' | 401 '<The .isolated doesn\'t declare any command to run!>\n' |
| 412 '<Check your .isolate for missing \'command\' variable>\n') | 402 '<Check your .isolate for missing \'command\' variable>\n') |
| 413 if os.environ.get('SWARMING_TASK_ID'): | 403 if os.environ.get('SWARMING_TASK_ID'): |
| 414 # Give an additional hint when running as a swarming task. | 404 # Give an additional hint when running as a swarming task. |
| 415 sys.stderr.write('<This occurs at the \'isolate\' step>\n') | 405 sys.stderr.write('<This occurs at the \'isolate\' step>\n') |
| 416 result['exit_code'] = 1 | 406 result['exit_code'] = 1 |
| 417 return result | 407 return result |
| 418 | 408 |
| 419 change_tree_read_only(run_dir, bundle.read_only) | 409 change_tree_read_only(run_dir, bundle.read_only) |
| 420 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd)) | 410 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd)) |
| 421 command = bundle.command + extra_args | 411 command = bundle.command + extra_args |
| 422 | 412 |
| 423 command = tools.fix_python_path(command) | 413 command = tools.fix_python_path(command) |
| 424 command = process_command(command, out_dir, cipd_path, bot_file) | 414 command = process_command(command, out_dir, bot_file) |
| 425 file_path.ensure_command_has_abs_path(command, cwd) | 415 file_path.ensure_command_has_abs_path(command, cwd) |
| 426 | 416 |
| 427 sys.stdout.flush() | 417 sys.stdout.flush() |
| 428 start = time.time() | 418 start = time.time() |
| 429 try: | 419 try: |
| 430 result['exit_code'], result['had_hard_timeout'] = run_command( | 420 result['exit_code'], result['had_hard_timeout'] = run_command( |
| 431 command, cwd, tmp_dir, hard_timeout, grace_period) | 421 command, cwd, tmp_dir, hard_timeout, grace_period) |
| 432 finally: | 422 finally: |
| 433 result['duration'] = max(time.time() - start, 0) | 423 result['duration'] = max(time.time() - start, 0) |
| 434 except Exception as e: | 424 except Exception as e: |
| 435 # An internal error occured. Report accordingly so the swarming task will be | 425 # An internal error occurred. Report accordingly so the swarming task will |
| 436 # retried automatically. | 426 # be retried automatically. |
| 437 logging.exception('internal failure: %s', e) | 427 logging.exception('internal failure: %s', e) |
| 438 result['internal_failure'] = str(e) | 428 result['internal_failure'] = str(e) |
| 439 on_error.report(None) | 429 on_error.report(None) |
| 440 finally: | 430 finally: |
| 441 try: | 431 try: |
| 442 if leak_temp_dir: | 432 if leak_temp_dir: |
| 443 logging.warning( | 433 logging.warning( |
| 444 'Deliberately leaking %s for later examination', run_dir) | 434 'Deliberately leaking %s for later examination', run_dir) |
| 445 else: | 435 else: |
| 446 # On Windows rmtree(run_dir) call above has a synchronization effect: it | 436 # On Windows rmtree(run_dir) call above has a synchronization effect: it |
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 488 # Swallow any exception in the main finally clause. | 478 # Swallow any exception in the main finally clause. |
| 489 if out_dir: | 479 if out_dir: |
| 490 logging.exception('Leaking out_dir %s: %s', out_dir, e) | 480 logging.exception('Leaking out_dir %s: %s', out_dir, e) |
| 491 result['internal_failure'] = str(e) | 481 result['internal_failure'] = str(e) |
| 492 return result | 482 return result |
| 493 | 483 |
| 494 | 484 |
| 495 def run_tha_test( | 485 def run_tha_test( |
| 496 command, isolated_hash, storage, cache, leak_temp_dir, result_json, | 486 command, isolated_hash, storage, cache, leak_temp_dir, result_json, |
| 497 root_dir, hard_timeout, grace_period, bot_file, extra_args, | 487 root_dir, hard_timeout, grace_period, bot_file, extra_args, |
| 498 cipd_path, cipd_stats): | 488 install_packages_fn): |
| 499 """Runs an executable and records execution metadata. | 489 """Runs an executable and records execution metadata. |
| 500 | 490 |
| 501 Either command or isolated_hash must be specified. | 491 Either command or isolated_hash must be specified. |
| 502 | 492 |
| 503 If isolated_hash is specified, downloads the dependencies in the cache, | 493 If isolated_hash is specified, downloads the dependencies in the cache, |
| 504 hardlinks them into a temporary directory and runs the command specified in | 494 hardlinks them into a temporary directory and runs the command specified in |
| 505 the .isolated. | 495 the .isolated. |
| 506 | 496 |
| 507 A temporary directory is created to hold the output files. The content inside | 497 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 | 498 this directory will be uploaded back to |storage| packaged as a .isolated |
| 509 file. | 499 file. |
| 510 | 500 |
| 511 Arguments: | 501 Arguments: |
| 512 command: the command to run, a list of strings. Mutually exclusive with | 502 command: the command to run, a list of strings. Mutually exclusive with |
| 513 isolated_hash. | 503 isolated_hash. |
| 514 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to | 504 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to |
| 515 recreate the tree of files to run the target executable. | 505 recreate the tree of files to run the target executable. |
| 516 The command specified in the .isolated is executed. | 506 The command specified in the .isolated is executed. |
| 517 Mutually exclusive with command argument. | 507 Mutually exclusive with command argument. |
| 518 storage: an isolateserver.Storage object to retrieve remote objects. This | 508 storage: an isolateserver.Storage object to retrieve remote objects. This |
| 519 object has a reference to an isolateserver.StorageApi, which does | 509 object has a reference to an isolateserver.StorageApi, which does |
| 520 the actual I/O. | 510 the actual I/O. |
| 521 cache: an isolateserver.LocalCache to keep from retrieving the same objects | 511 cache: an isolateserver.LocalCache to keep from retrieving the same objects |
| 522 constantly by caching the objects retrieved. Can be on-disk or | 512 constantly by caching the objects retrieved. Can be on-disk or |
| 523 in-memory. | 513 in-memory. |
| 524 leak_temp_dir: if true, the temporary directory will be deliberately leaked | 514 leak_temp_dir: if true, the temporary directory will be deliberately leaked |
| 525 for later examination. | 515 for later examination. |
| 526 result_json: file path to dump result metadata into. If set, the process | 516 result_json: file path to dump result metadata into. If set, the process |
| 527 exit code is always 0 unless an internal error occurred. | 517 exit code is always 0 unless an internal error occurred. |
| 528 root_dir: directory to the path to use to create the temporary directory. If | 518 root_dir: path to the directory to use to create the temporary directory. If |
| 529 not specified, a random temporary directory is created. | 519 not specified, a random temporary directory is created. |
| 530 hard_timeout: kills the process if it lasts more than this amount of | 520 hard_timeout: kills the process if it lasts more than this amount of |
| 531 seconds. | 521 seconds. |
| 532 grace_period: number of seconds to wait between SIGTERM and SIGKILL. | 522 grace_period: number of seconds to wait between SIGTERM and SIGKILL. |
| 533 extra_args: optional arguments to add to the command stated in the .isolate | 523 extra_args: optional arguments to add to the command stated in the .isolate |
| 534 file. Ignored if isolate_hash is empty. | 524 file. Ignored if isolate_hash is empty. |
| 535 cipd_path: value for CIPD_PATH_PARAMETER. If empty, command or extra_args | 525 install_packages_fn: function (dir) => cipd_stats. Installs packages. |
| 536 must not use CIPD_PATH_PARAMETER. | |
| 537 cipd_stats: CIPD stats to include in the metadata written to result_json. | |
| 538 | 526 |
| 539 Returns: | 527 Returns: |
| 540 Process exit code that should be used. | 528 Process exit code that should be used. |
| 541 """ | 529 """ |
| 542 assert bool(command) ^ bool(isolated_hash) | 530 assert bool(command) ^ bool(isolated_hash) |
| 543 extra_args = extra_args or [] | 531 extra_args = extra_args or [] |
| 544 | 532 |
| 545 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)): | 533 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' | 534 assert storage is not None, 'storage is None although outdir is specified' |
| 547 | 535 |
| 548 if result_json: | 536 if result_json: |
| 549 # Write a json output file right away in case we get killed. | 537 # Write a json output file right away in case we get killed. |
| 550 result = { | 538 result = { |
| 551 'exit_code': None, | 539 'exit_code': None, |
| 552 'had_hard_timeout': False, | 540 'had_hard_timeout': False, |
| 553 'internal_failure': 'Was terminated before completion', | 541 'internal_failure': 'Was terminated before completion', |
| 554 'outputs_ref': None, | 542 'outputs_ref': None, |
| 555 'version': 5, | 543 'version': 5, |
| 556 } | 544 } |
| 557 if cipd_stats: | |
| 558 result['stats'] = {'cipd': cipd_stats} | |
| 559 tools.write_json(result_json, result, dense=True) | 545 tools.write_json(result_json, result, dense=True) |
| 560 | 546 |
| 561 # run_isolated exit code. Depends on if result_json is used or not. | 547 # run_isolated exit code. Depends on if result_json is used or not. |
| 562 result = map_and_run( | 548 result = map_and_run( |
| 563 command, isolated_hash, storage, cache, leak_temp_dir, root_dir, | 549 command, isolated_hash, storage, cache, leak_temp_dir, root_dir, |
| 564 hard_timeout, grace_period, bot_file, extra_args, cipd_path, cipd_stats) | 550 hard_timeout, grace_period, bot_file, extra_args, install_packages_fn) |
| 565 logging.info('Result:\n%s', tools.format_json(result, dense=True)) | 551 logging.info('Result:\n%s', tools.format_json(result, dense=True)) |
| 566 | 552 |
| 567 if result_json: | 553 if result_json: |
| 568 # We've found tests to delete 'work' when quitting, causing an exception | 554 # We've found tests to delete 'work' when quitting, causing an exception |
| 569 # here. Try to recreate the directory if necessary. | 555 # here. Try to recreate the directory if necessary. |
| 570 file_path.ensure_tree(os.path.dirname(result_json)) | 556 file_path.ensure_tree(os.path.dirname(result_json)) |
| 571 tools.write_json(result_json, result, dense=True) | 557 tools.write_json(result_json, result, dense=True) |
| 572 # Only return 1 if there was an internal error. | 558 # Only return 1 if there was an internal error. |
| 573 return int(bool(result['internal_failure'])) | 559 return int(bool(result['internal_failure'])) |
| 574 | 560 |
| 575 # Marshall into old-style inline output. | 561 # Marshall into old-style inline output. |
| 576 if result['outputs_ref']: | 562 if result['outputs_ref']: |
| 577 data = { | 563 data = { |
| 578 'hash': result['outputs_ref']['isolated'], | 564 'hash': result['outputs_ref']['isolated'], |
| 579 'namespace': result['outputs_ref']['namespace'], | 565 'namespace': result['outputs_ref']['namespace'], |
| 580 'storage': result['outputs_ref']['isolatedserver'], | 566 'storage': result['outputs_ref']['isolatedserver'], |
| 581 } | 567 } |
| 582 sys.stdout.flush() | 568 sys.stdout.flush() |
| 583 print( | 569 print( |
| 584 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' % | 570 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' % |
| 585 tools.format_json(data, dense=True)) | 571 tools.format_json(data, dense=True)) |
| 586 sys.stdout.flush() | 572 sys.stdout.flush() |
| 587 return result['exit_code'] or int(bool(result['internal_failure'])) | 573 return result['exit_code'] or int(bool(result['internal_failure'])) |
| 588 | 574 |
| 589 | 575 |
| 590 @contextlib.contextmanager | 576 def install_packages( |
| 591 def ensure_packages( | 577 run_dir, package_list_file, service_url, client_package_name, |
| 592 site_root, packages, service_url, client_package, cache_dir=None, | 578 client_version, cache_dir=None, timeout=None): |
| 593 timeout=None): | 579 """Installs packages. Returns stats. |
| 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 | 580 |
| 599 Args: | 581 Args: |
| 600 site_root (str): where to install the packages. | 582 run_dir (str): root of installation. |
| 601 packages (list of str): list of package to ensure. | 583 package_list_file (str): path to a file with a list of packages to install. |
| 602 Package format is same as for "--cipd-package" option. | |
| 603 service_url (str): CIPD server url, e.g. | 584 service_url (str): CIPD server url, e.g. |
| 604 "https://chrome-infra-packages.appspot.com." | 585 "https://chrome-infra-packages.appspot.com." |
| 605 client_package (str): CIPD package of CIPD client. | 586 client_package_name (str): CIPD package name of CIPD client. |
| 606 Format is same as for "--cipd-package" option. | 587 client_version (str): Version of CIPD client. |
| 607 cache_dir (str): where to keep cache of cipd clients, packages and tags. | 588 cache_dir (str): where to keep cache of cipd clients, packages and tags. |
| 608 timeout: max duration in seconds that this function can take. | 589 timeout: max duration in seconds that this function can take. |
| 609 | |
| 610 Yields: | |
| 611 CIPD stats as dict. | |
| 612 """ | 590 """ |
| 613 assert cache_dir | 591 assert cache_dir |
| 592 if not package_list_file: |
| 593 return None |
| 594 |
| 614 timeoutfn = tools.sliding_timeout(timeout) | 595 timeoutfn = tools.sliding_timeout(timeout) |
| 615 if not packages: | |
| 616 yield | |
| 617 return | |
| 618 | |
| 619 start = time.time() | 596 start = time.time() |
| 620 | |
| 621 cache_dir = os.path.abspath(cache_dir) | 597 cache_dir = os.path.abspath(cache_dir) |
| 622 | 598 |
| 623 # Get CIPD client. | 599 run_dir = os.path.abspath(run_dir) |
| 624 client_package_name, client_version = cipd.parse_package(client_package) | 600 package_list = cipd.parse_package_list_file(package_list_file) |
| 601 |
| 625 get_client_start = time.time() | 602 get_client_start = time.time() |
| 626 client_manager = cipd.get_client( | 603 client_manager = cipd.get_client( |
| 627 service_url, client_package_name, client_version, cache_dir, | 604 service_url, client_package_name, client_version, cache_dir, |
| 628 timeout=timeoutfn()) | 605 timeout=timeoutfn()) |
| 629 with client_manager as client: | 606 with client_manager as client: |
| 630 get_client_duration = time.time() - get_client_start | 607 get_client_duration = time.time() - get_client_start |
| 631 # Create site_root, install packages, yield, delete site_root. | 608 for path, packages in package_list.iteritems(): |
| 632 if fs.isdir(site_root): | 609 site_root = os.path.abspath(os.path.join(run_dir, path)) |
| 633 file_path.rmtree(site_root) | 610 if not site_root.startswith(run_dir): |
| 634 file_path.ensure_tree(site_root, 0770) | 611 raise cipd.Error('Invalid CIPD package path "%s"' % path) |
| 635 try: | 612 |
| 613 # Do not clean site_root before installation because it may contain other |
| 614 # site roots. |
| 615 file_path.ensure_tree(site_root, 0770) |
| 636 client.ensure( | 616 client.ensure( |
| 637 site_root, packages, | 617 site_root, packages, |
| 638 cache_dir=os.path.join(cache_dir, 'cipd_internal'), | 618 cache_dir=os.path.join(cache_dir, 'cipd_internal'), |
| 639 timeout=timeoutfn()) | 619 timeout=timeoutfn()) |
| 620 file_path.make_tree_files_read_only(site_root) |
| 640 | 621 |
| 641 total_duration = time.time() - start | 622 total_duration = time.time() - start |
| 642 logging.info( | 623 logging.info( |
| 643 'Installing CIPD client and packages took %d seconds', total_duration) | 624 'Installing CIPD client and packages took %d seconds', total_duration) |
| 644 | 625 |
| 645 file_path.make_tree_files_read_only(site_root) | 626 return { |
| 646 yield { | 627 'duration': total_duration, |
| 647 'duration': total_duration, | 628 'get_client_duration': get_client_duration, |
| 648 'get_client_duration': get_client_duration, | 629 } |
| 649 } | |
| 650 finally: | |
| 651 file_path.rmtree(site_root) | |
| 652 | 630 |
| 653 | 631 |
| 654 def create_option_parser(): | 632 def create_option_parser(): |
| 655 parser = logging_utils.OptionParserWithLogging( | 633 parser = logging_utils.OptionParserWithLogging( |
| 656 usage='%prog <options> [command to run or extra args]', | 634 usage='%prog <options> [command to run or extra args]', |
| 657 version=__version__, | 635 version=__version__, |
| 658 log_file=RUN_ISOLATED_LOG_FILE) | 636 log_file=RUN_ISOLATED_LOG_FILE) |
| 659 parser.add_option( | 637 parser.add_option( |
| 660 '--clean', action='store_true', | 638 '--clean', action='store_true', |
| 661 help='Cleans the cache, trimming it necessary and remove corrupted items ' | 639 help='Cleans the cache, trimming it necessary and remove corrupted items ' |
| (...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 723 | 701 |
| 724 isolateserver.process_isolate_server_options( | 702 isolateserver.process_isolate_server_options( |
| 725 parser, options, True, False) | 703 parser, options, True, False) |
| 726 if not options.isolate_server: | 704 if not options.isolate_server: |
| 727 if options.isolated: | 705 if options.isolated: |
| 728 parser.error('--isolated requires --isolate-server') | 706 parser.error('--isolated requires --isolate-server') |
| 729 if ISOLATED_OUTDIR_PARAMETER in args: | 707 if ISOLATED_OUTDIR_PARAMETER in args: |
| 730 parser.error( | 708 parser.error( |
| 731 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER) | 709 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER) |
| 732 | 710 |
| 711 if options.root_dir: |
| 712 options.root_dir = unicode(os.path.abspath(options.root_dir)) |
| 733 if options.json: | 713 if options.json: |
| 734 options.json = unicode(os.path.abspath(options.json)) | 714 options.json = unicode(os.path.abspath(options.json)) |
| 735 | 715 |
| 736 cipd.validate_cipd_options(parser, options) | 716 cipd.validate_cipd_options(parser, options) |
| 737 | 717 |
| 738 root_dir = options.root_dir | 718 install_packages_fn = lambda run_dir: install_packages( |
| 739 if root_dir: | 719 run_dir, options.cipd_package_list, options.cipd_server, |
| 740 root_dir = unicode(os.path.abspath(root_dir)) | 720 options.cipd_client_package, options.cipd_client_version, |
| 741 file_path.ensure_tree(root_dir, 0700) | 721 cache_dir=options.cipd_cache) |
| 742 else: | |
| 743 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 | 722 |
| 752 try: | 723 try: |
| 753 with ensure_packages( | 724 command = [] if options.isolated else args |
| 754 cipd_path, options.cipd_package, options.cipd_server, | 725 if options.isolate_server: |
| 755 options.cipd_client_package, options.cipd_cache) as cipd_stats: | 726 storage = isolateserver.get_storage( |
| 756 command = [] if options.isolated else args | 727 options.isolate_server, options.namespace) |
| 757 if options.isolate_server: | 728 with storage: |
| 758 storage = isolateserver.get_storage( | 729 # Hashing schemes used by |storage| and |cache| MUST match. |
| 759 options.isolate_server, options.namespace) | 730 assert storage.hash_algo == cache.hash_algo |
| 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, | |
| 766 options.grace_period, options.bot_file, args, | |
| 767 cipd_path, cipd_stats) | |
| 768 else: | |
| 769 return run_tha_test( | 731 return run_tha_test( |
| 770 command, options.isolated, None, cache, options.leak_temp_dir, | 732 command, options.isolated, storage, cache, options.leak_temp_dir, |
| 771 options.json, root_dir, options.hard_timeout, | 733 options.json, options.root_dir, options.hard_timeout, |
| 772 options.grace_period, options.bot_file, args, | 734 options.grace_period, options.bot_file, args, install_packages_fn) |
| 773 cipd_path, cipd_stats) | 735 else: |
| 736 return run_tha_test( |
| 737 command, options.isolated, None, cache, options.leak_temp_dir, |
| 738 options.json, options.root_dir, options.hard_timeout, |
| 739 options.grace_period, options.bot_file, args, install_packages_fn) |
| 774 except cipd.Error as ex: | 740 except cipd.Error as ex: |
| 775 print >> sys.stderr, ex.message | 741 print >> sys.stderr, ex.message |
| 776 return 1 | 742 return 1 |
| 777 | 743 |
| 778 | 744 |
| 779 if __name__ == '__main__': | 745 if __name__ == '__main__': |
| 780 subprocess42.inhibit_os_error_reporting() | 746 subprocess42.inhibit_os_error_reporting() |
| 781 # Ensure that we are always running with the correct encoding. | 747 # Ensure that we are always running with the correct encoding. |
| 782 fix_encoding.fix_encoding() | 748 fix_encoding.fix_encoding() |
| 783 sys.exit(main(sys.argv[1:])) | 749 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |