Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(112)

Side by Side Diff: client/run_isolated.py

Issue 2037253002: run_isolated.py: install CIPD packages (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/luci-py@master
Patch Set: Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
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
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:]))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698