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 |
| 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 ${EXE_SUFFIX} on the command line will be replaced with ".exe" string on | |
|
M-A Ruel
2016/06/06 23:34:51
The isolate client uses EXECUTABLE_SUFFIX. It thin
nodir
2016/06/07 18:46:35
Done
| |
| 17 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 EXE_SUFFIX_PARAMETER = '${EXE_SUFFIX}' | |
| 60 | |
| 61 # .exe on Windows. | |
| 62 EXE_SUFFIX = '.exe' if sys.platform == 'win32' else '' | |
| 63 | |
| 48 | 64 |
| 49 # Absolute path to this file (can be None if running from zip on Mac). | 65 # 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 | 66 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None |
| 51 | 67 |
| 52 # Directory that contains this file (might be inside zip package). | 68 # Directory that contains this file (might be inside zip package). |
| 53 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None | 69 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None |
| 54 | 70 |
| 55 # Directory that contains currently running script file. | 71 # Directory that contains currently running script file. |
| 56 if zip_package.get_main_script_path(): | 72 if zip_package.get_main_script_path(): |
| 57 MAIN_DIR = os.path.dirname( | 73 MAIN_DIR = os.path.dirname( |
| (...skipping 19 matching lines...) Expand all Loading... | |
| 77 # Building a zip package when running from another zip package is | 93 # Building a zip package when running from another zip package is |
| 78 # unsupported and probably unneeded. | 94 # unsupported and probably unneeded. |
| 79 assert not zip_package.is_zipped_module(sys.modules[__name__]) | 95 assert not zip_package.is_zipped_module(sys.modules[__name__]) |
| 80 assert THIS_FILE_PATH | 96 assert THIS_FILE_PATH |
| 81 assert BASE_DIR | 97 assert BASE_DIR |
| 82 package = zip_package.ZipPackage(root=BASE_DIR) | 98 package = zip_package.ZipPackage(root=BASE_DIR) |
| 83 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None) | 99 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')) | 100 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py')) |
| 85 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py')) | 101 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py')) |
| 86 package.add_python_file(os.path.join(BASE_DIR, 'auth.py')) | 102 package.add_python_file(os.path.join(BASE_DIR, 'auth.py')) |
| 103 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py')) | |
| 87 package.add_directory(os.path.join(BASE_DIR, 'third_party')) | 104 package.add_directory(os.path.join(BASE_DIR, 'third_party')) |
| 88 package.add_directory(os.path.join(BASE_DIR, 'utils')) | 105 package.add_directory(os.path.join(BASE_DIR, 'utils')) |
| 89 return package | 106 return package |
| 90 | 107 |
| 91 | 108 |
| 92 def make_temp_dir(prefix, root_dir=None): | 109 def make_temp_dir(prefix, root_dir=None): |
| 93 """Returns a temporary directory. | 110 """Returns a temporary directory. |
| 94 | 111 |
| 95 If root_dir is given and /tmp is on same file system as root_dir, uses /tmp. | 112 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. | 113 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 | 150 # 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 | 151 # looking at, so that if a test modifies an input file, the file must be |
| 135 # deleted. | 152 # deleted. |
| 136 file_path.make_tree_writeable(rootdir) | 153 file_path.make_tree_writeable(rootdir) |
| 137 else: | 154 else: |
| 138 raise ValueError( | 155 raise ValueError( |
| 139 'change_tree_read_only(%s, %s): Unknown flag %s' % | 156 'change_tree_read_only(%s, %s): Unknown flag %s' % |
| 140 (rootdir, read_only, read_only)) | 157 (rootdir, read_only, read_only)) |
| 141 | 158 |
| 142 | 159 |
| 143 def process_command(command, out_dir): | 160 def process_command(command, out_dir, cipd_path): |
| 144 """Replaces isolated specific variables in a command line.""" | 161 """Replaces variables in a command line. |
| 162 | |
| 163 Raises: | |
| 164 AssertionError if a parameter is requested in |command| but its value is not | |
|
Vadim Sh.
2016/06/06 21:32:13
ValueError
assertion errors must not happen in co
nodir
2016/06/07 18:46:35
the code is correct. The cmdline is checked before
| |
| 165 provided. | |
| 166 """ | |
| 145 def fix(arg): | 167 def fix(arg): |
| 168 arg = arg.replace(EXE_SUFFIX_PARAMETER, EXE_SUFFIX) | |
| 169 replace_slash = False | |
| 170 if CIPD_PATH_PARAMETER in arg: | |
| 171 assert cipd_path, ( | |
| 172 'cipd_path is requested in command %r, ' | |
| 173 'but its value is not provided is empty' % command) | |
| 174 arg = arg.replace(CIPD_PATH_PARAMETER, cipd_path) | |
| 175 replace_slash = True | |
| 146 if ISOLATED_OUTDIR_PARAMETER in arg: | 176 if ISOLATED_OUTDIR_PARAMETER in arg: |
| 147 assert out_dir | 177 assert out_dir |
| 148 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir) | 178 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir) |
| 149 # Replace slashes only if ISOLATED_OUTDIR_PARAMETER is present | 179 replace_slash = True |
| 180 if replace_slash: | |
| 181 # Replace slashes only if parameters are present | |
| 150 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar' | 182 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar' |
| 151 arg = arg.replace('/', os.sep) | 183 arg = arg.replace('/', os.sep) |
| 152 return arg | 184 return arg |
| 153 | 185 |
| 154 return [fix(arg) for arg in command] | 186 return [fix(arg) for arg in command] |
| 155 | 187 |
| 156 | 188 |
| 157 def run_command(command, cwd, tmp_dir, hard_timeout, grace_period): | 189 def run_command(command, cwd, tmp_dir, hard_timeout, grace_period): |
| 158 """Runs the command. | 190 """Runs the command. |
| 159 | 191 |
| (...skipping 146 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 306 stats = { | 338 stats = { |
| 307 'duration': time.time() - start, | 339 'duration': time.time() - start, |
| 308 'items_cold': base64.b64encode(large.pack(cold)), | 340 'items_cold': base64.b64encode(large.pack(cold)), |
| 309 'items_hot': base64.b64encode(large.pack(hot)), | 341 'items_hot': base64.b64encode(large.pack(hot)), |
| 310 } | 342 } |
| 311 return outputs_ref, success, stats | 343 return outputs_ref, success, stats |
| 312 | 344 |
| 313 | 345 |
| 314 def map_and_run( | 346 def map_and_run( |
| 315 command, isolated_hash, storage, cache, leak_temp_dir, root_dir, | 347 command, isolated_hash, storage, cache, leak_temp_dir, root_dir, |
| 316 hard_timeout, grace_period, extra_args): | 348 hard_timeout, grace_period, extra_args, cipd_path, cipd_stats): |
| 317 """Runs a command with optional isolated input/output. | 349 """Runs a command with optional isolated input/output. |
| 318 | 350 |
| 319 See run_tha_test for argument documentation. | 351 See run_tha_test for argument documentation. |
| 320 | 352 |
| 321 Returns metadata about the result. | 353 Returns metadata about the result. |
| 322 """ | 354 """ |
| 323 assert bool(command) ^ bool(isolated_hash) | 355 assert bool(command) ^ bool(isolated_hash) |
| 324 result = { | 356 result = { |
| 325 'duration': None, | 357 'duration': None, |
| 326 'exit_code': None, | 358 'exit_code': None, |
| 327 'had_hard_timeout': False, | 359 'had_hard_timeout': False, |
| 328 'internal_failure': None, | 360 'internal_failure': None, |
| 329 'stats': { | 361 'stats': { |
| 330 # 'isolated': { | 362 # 'isolated': { |
| 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 # }, |
|
M-A Ruel
2016/06/06 23:34:51
Add expectation about:
'cipd': {
...
},
nodir
2016/06/07 18:46:35
Done.
| |
| 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, |
|
M-A Ruel
2016/06/06 23:34:51
oops!
| |
| 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 client_cache=None, 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 client_cache (isolatedserver.DiskCache): if not None, overrides client | |
| 599 cache derived from |cache_dir|. | |
| 600 timeout: max duration in seconds that this function can take. | |
| 601 | |
| 602 Yields: | |
| 603 CIPD stats as dict. | |
| 604 """ | |
| 605 assert cache_dir | |
| 606 timeouter = tools.Timeouter(timeout) | |
| 607 assert not client_cache or isinstance(client_cache, isolateserver.DiskCache) | |
| 608 if not packages: | |
| 609 yield | |
| 610 return | |
| 611 | |
| 612 start = time.time() | |
| 613 | |
| 614 # Prepare caches. | |
| 615 cache_dir = os.path.abspath(cache_dir) | |
| 616 # version_cache is {version_digest -> instance id} mapping. | |
| 617 # It does not take a lot of disk space. | |
| 618 version_cache = isolateserver.DiskCache( | |
| 619 unicode(os.path.join(cache_dir, 'versions')), | |
| 620 isolateserver.CachePolicies(0, 0, 300), | |
| 621 hashlib.sha1) | |
| 622 # client_cache is {instance_id -> client binary} mapping. | |
| 623 # It is bounded by 5 client versions. | |
| 624 client_cache = client_cache or isolateserver.DiskCache( | |
| 625 unicode(os.path.join(cache_dir, 'clients')), | |
| 626 isolateserver.CachePolicies(0, 0, 5), | |
| 627 hashlib.sha1) | |
| 628 | |
| 629 # Get CIPD client. | |
| 630 client_package_name, client_version = cipd.parse_package(client_package) | |
| 631 get_client_start = time.time() | |
| 632 client = cipd.get_client( | |
| 633 service_url, client_package_name, client_version, | |
| 634 version_cache=version_cache, client_cache=client_cache, | |
| 635 timeout=timeouter.left()) | |
| 636 get_client_duration = time.time() - get_client_start | |
| 637 | |
| 638 # Create site_root, install packages, yield, delete site_root. | |
| 639 if fs.isdir(site_root): | |
| 640 file_path.rmtree(site_root) | |
| 641 file_path.ensure_tree(site_root, 0777) | |
| 642 file_path.make_tree_writeable(site_root) | |
| 643 try: | |
| 644 client.ensure( | |
| 645 site_root, packages, | |
| 646 cache_dir=os.path.join(cache_dir, 'cipd_internal'), | |
| 647 timeout=timeouter.left()) | |
| 648 | |
| 649 total_duration = time.time() - start | |
| 650 logging.info( | |
| 651 'Installing CIPD client and packages took %d seconds', total_duration) | |
| 652 | |
| 653 file_path.make_tree_files_read_only(site_root) | |
| 654 yield { | |
| 655 'duration': total_duration, | |
| 656 'get_client_duration': get_client_duration, | |
| 657 } | |
| 658 finally: | |
| 659 file_path.rmtree(site_root) | |
| 660 | |
| 661 | |
| 662 def create_option_parser(): | |
| 540 parser = logging_utils.OptionParserWithLogging( | 663 parser = logging_utils.OptionParserWithLogging( |
| 541 usage='%prog <options> [command to run or extra args]', | 664 usage='%prog <options> [command to run or extra args]', |
| 542 version=__version__, | 665 version=__version__, |
| 543 log_file=RUN_ISOLATED_LOG_FILE) | 666 log_file=RUN_ISOLATED_LOG_FILE) |
| 544 parser.add_option( | 667 parser.add_option( |
| 545 '--clean', action='store_true', | 668 '--clean', action='store_true', |
| 546 help='Cleans the cache, trimming it necessary and remove corrupted items ' | 669 help='Cleans the cache, trimming it necessary and remove corrupted items ' |
| 547 'and returns without executing anything; use with -v to know what ' | 670 'and returns without executing anything; use with -v to know what ' |
| 548 'was done') | 671 'was done') |
| 549 parser.add_option( | 672 parser.add_option( |
| 550 '--json', | 673 '--json', |
| 551 help='dump output metadata to json file. When used, run_isolated returns ' | 674 help='dump output metadata to json file. When used, run_isolated returns ' |
| 552 'non-zero only on internal failure') | 675 'non-zero only on internal failure') |
| 553 parser.add_option( | 676 parser.add_option( |
| 554 '--hard-timeout', type='float', help='Enforce hard timeout in execution') | 677 '--hard-timeout', type='float', help='Enforce hard timeout in execution') |
| 555 parser.add_option( | 678 parser.add_option( |
| 556 '--grace-period', type='float', | 679 '--grace-period', type='float', |
| 557 help='Grace period between SIGTERM and SIGKILL') | 680 help='Grace period between SIGTERM and SIGKILL') |
| 558 data_group = optparse.OptionGroup(parser, 'Data source') | 681 data_group = optparse.OptionGroup(parser, 'Data source') |
| 559 data_group.add_option( | 682 data_group.add_option( |
| 560 '-s', '--isolated', | 683 '-s', '--isolated', |
| 561 help='Hash of the .isolated to grab from the isolate server.') | 684 help='Hash of the .isolated to grab from the isolate server.') |
| 562 isolateserver.add_isolate_server_options(data_group) | 685 isolateserver.add_isolate_server_options(data_group) |
| 563 parser.add_option_group(data_group) | 686 parser.add_option_group(data_group) |
| 564 | 687 |
| 565 isolateserver.add_cache_options(parser) | 688 isolateserver.add_cache_options(parser) |
| 566 parser.set_defaults(cache='cache') | 689 |
| 690 cipd.add_cipd_options(parser) | |
| 567 | 691 |
| 568 debug_group = optparse.OptionGroup(parser, 'Debugging') | 692 debug_group = optparse.OptionGroup(parser, 'Debugging') |
| 569 debug_group.add_option( | 693 debug_group.add_option( |
| 570 '--leak-temp-dir', | 694 '--leak-temp-dir', |
| 571 action='store_true', | 695 action='store_true', |
| 572 help='Deliberately leak isolate\'s temp dir for later examination ' | 696 help='Deliberately leak isolate\'s temp dir for later examination. ' |
| 573 '[default: %default]') | 697 'Default: %default') |
| 574 debug_group.add_option( | 698 debug_group.add_option( |
| 575 '--root-dir', help='Use a directory instead of a random one') | 699 '--root-dir', help='Use a directory instead of a random one') |
| 576 parser.add_option_group(debug_group) | 700 parser.add_option_group(debug_group) |
| 577 | 701 |
| 578 auth.add_auth_options(parser) | 702 auth.add_auth_options(parser) |
| 703 | |
| 704 parser.set_defaults(cache='isolate_cache', cipd_cache='cipd_cache') | |
|
Vadim Sh.
2016/06/06 21:32:14
I believe this will "leak" all existing isolate ca
nodir
2016/06/07 18:46:35
Done.
| |
| 705 return parser | |
| 706 | |
| 707 | |
| 708 def main(args): | |
| 709 parser = create_option_parser() | |
| 579 options, args = parser.parse_args(args) | 710 options, args = parser.parse_args(args) |
| 580 | 711 |
| 581 cache = isolateserver.process_cache_options(options) | 712 cache = isolateserver.process_cache_options(options) |
| 582 if options.clean: | 713 if options.clean: |
| 583 if options.isolated: | 714 if options.isolated: |
| 584 parser.error('Can\'t use --isolated with --clean.') | 715 parser.error('Can\'t use --isolated with --clean.') |
| 585 if options.isolate_server: | 716 if options.isolate_server: |
| 586 parser.error('Can\'t use --isolate-server with --clean.') | 717 parser.error('Can\'t use --isolate-server with --clean.') |
| 587 if options.json: | 718 if options.json: |
| 588 parser.error('Can\'t use --json with --clean.') | 719 parser.error('Can\'t use --json with --clean.') |
| 589 cache.cleanup() | 720 cache.cleanup() |
| 590 return 0 | 721 return 0 |
| 591 | 722 |
| 592 if not options.isolated and not args: | 723 if not options.isolated and not args: |
| 593 parser.error('--isolated or command to run is required.') | 724 parser.error('--isolated or command to run is required.') |
| 594 | 725 |
| 595 auth.process_auth_options(parser, options) | 726 auth.process_auth_options(parser, options) |
| 596 | 727 |
| 597 isolateserver.process_isolate_server_options( | 728 isolateserver.process_isolate_server_options( |
| 598 parser, options, True, False) | 729 parser, options, True, False) |
| 599 if not options.isolate_server: | 730 if not options.isolate_server: |
| 600 if options.isolated: | 731 if options.isolated: |
| 601 parser.error('--isolated requires --isolate-server') | 732 parser.error('--isolated requires --isolate-server') |
| 602 if ISOLATED_OUTDIR_PARAMETER in args: | 733 if ISOLATED_OUTDIR_PARAMETER in args: |
| 603 parser.error( | 734 parser.error( |
| 604 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER) | 735 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER) |
| 605 | 736 |
| 606 if options.root_dir: | 737 root_dir = options.root_dir |
| 607 options.root_dir = unicode(os.path.abspath(options.root_dir)) | 738 if root_dir: |
| 739 root_dir = unicode(os.path.abspath(root_dir)) | |
| 740 file_path.ensure_tree(root_dir, 0700) | |
|
Vadim Sh.
2016/06/06 21:32:14
nit: do it after all options are validated
nodir
2016/06/07 18:46:35
Done.
| |
| 741 else: | |
| 742 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None | |
| 743 | |
| 608 if options.json: | 744 if options.json: |
| 609 options.json = unicode(os.path.abspath(options.json)) | 745 options.json = unicode(os.path.abspath(options.json)) |
| 610 | 746 |
| 611 command = [] if options.isolated else args | 747 cipd.validate_cipd_options(parser, options) |
| 612 if options.isolate_server: | 748 cipd_path = None |
| 613 storage = isolateserver.get_storage( | 749 if not options.cipd_package: |
| 614 options.isolate_server, options.namespace) | 750 if CIPD_PATH_PARAMETER in args: |
| 615 # Hashing schemes used by |storage| and |cache| MUST match. | 751 parser.error('%s in args requires --cipd-package' % CIPD_PATH_PARAMETER) |
| 616 with storage: | |
| 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: | 752 else: |
| 623 return run_tha_test( | 753 cipd_path = make_temp_dir(u'cipd_site_root', root_dir) |
| 624 command, options.isolated, None, cache, options.leak_temp_dir, | 754 |
| 625 options.json, options.root_dir, options.hard_timeout, | 755 try: |
| 626 options.grace_period, args) | 756 with ensure_packages( |
| 757 cipd_path, options.cipd_package, options.cipd_server, | |
| 758 options.cipd_client_package, options.cipd_cache) as cipd_stats: | |
| 759 command = [] if options.isolated else args | |
| 760 if options.isolate_server: | |
| 761 storage = isolateserver.get_storage( | |
| 762 options.isolate_server, options.namespace) | |
| 763 with storage: | |
| 764 # Hashing schemes used by |storage| and |cache| MUST match. | |
| 765 assert storage.hash_algo == cache.hash_algo | |
| 766 return run_tha_test( | |
| 767 command, options.isolated, storage, cache, options.leak_temp_dir, | |
| 768 options.json, root_dir, options.hard_timeout, | |
| 769 options.grace_period, args, cipd_path, cipd_stats) | |
| 770 else: | |
| 771 return run_tha_test( | |
| 772 command, options.isolated, None, cache, options.leak_temp_dir, | |
| 773 options.json, root_dir, options.hard_timeout, | |
| 774 options.grace_period, args, cipd_path, cipd_stats) | |
| 775 except cipd.Error as ex: | |
| 776 print ex.message | |
| 777 return 1 | |
| 627 | 778 |
| 628 | 779 |
| 629 if __name__ == '__main__': | 780 if __name__ == '__main__': |
| 630 subprocess42.inhibit_os_error_reporting() | 781 subprocess42.inhibit_os_error_reporting() |
| 631 # Ensure that we are always running with the correct encoding. | 782 # Ensure that we are always running with the correct encoding. |
| 632 fix_encoding.fix_encoding() | 783 fix_encoding.fix_encoding() |
| 633 sys.exit(main(sys.argv[1:])) | 784 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |