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

Side by Side Diff: client/run_isolated.py

Issue 1932143003: run_isolated: support non-isolated commands (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/luci-py@run-isolated-download-stats
Patch Set: addressed comments Created 4 years, 7 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
« no previous file with comments | « client/isolateserver.py ('k') | client/swarming.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 by the Apache v2.0 license that can be 3 # Use of this source code is governed by the Apache v2.0 license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Reads a .isolated, creates a tree of hardlinks and runs the test. 6 """Runs a command with optional isolated input/output.
7 7
8 To improve performance, it keeps a local cache. The local cache can safely be 8 Despite name "run_isolated", can run a generic non-isolated command specified as
9 deleted. 9 args.
10
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.
13 To improve performance, keeps a local cache.
14 The local cache can safely be deleted.
10 15
11 Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a 16 Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
12 temporary directory upon execution of the command specified in the .isolated 17 temporary directory upon execution of the command specified in the .isolated
13 file. All content written to this directory will be uploaded upon termination 18 file. All content written to this directory will be uploaded upon termination
14 and the .isolated file describing this directory will be printed to stdout. 19 and the .isolated file describing this directory will be printed to stdout.
15 """ 20 """
16 21
17 __version__ = '0.6.1' 22 __version__ = '0.7.0'
18 23
19 import base64 24 import base64
20 import logging 25 import logging
21 import optparse 26 import optparse
22 import os 27 import os
23 import sys 28 import sys
24 import tempfile 29 import tempfile
25 import time 30 import time
26 31
27 from third_party.depot_tools import fix_encoding 32 from third_party.depot_tools import fix_encoding
28 33
29 from utils import file_path 34 from utils import file_path
30 from utils import fs 35 from utils import fs
31 from utils import large 36 from utils import large
32 from utils import logging_utils 37 from utils import logging_utils
33 from utils import on_error 38 from utils import on_error
34 from utils import subprocess42 39 from utils import subprocess42
35 from utils import tools 40 from utils import tools
36 from utils import zip_package 41 from utils import zip_package
37 42
38 import auth 43 import auth
39 import isolated_format
40 import isolateserver 44 import isolateserver
41 45
42 46
47 ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
48
43 # Absolute path to this file (can be None if running from zip on Mac). 49 # Absolute path to this file (can be None if running from zip on Mac).
44 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None 50 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
45 51
46 # Directory that contains this file (might be inside zip package). 52 # Directory that contains this file (might be inside zip package).
47 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None 53 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
48 54
49 # Directory that contains currently running script file. 55 # Directory that contains currently running script file.
50 if zip_package.get_main_script_path(): 56 if zip_package.get_main_script_path():
51 MAIN_DIR = os.path.dirname( 57 MAIN_DIR = os.path.dirname(
52 os.path.abspath(zip_package.get_main_script_path())) 58 os.path.abspath(zip_package.get_main_script_path()))
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after
130 file_path.make_tree_writeable(rootdir) 136 file_path.make_tree_writeable(rootdir)
131 else: 137 else:
132 raise ValueError( 138 raise ValueError(
133 'change_tree_read_only(%s, %s): Unknown flag %s' % 139 'change_tree_read_only(%s, %s): Unknown flag %s' %
134 (rootdir, read_only, read_only)) 140 (rootdir, read_only, read_only))
135 141
136 142
137 def process_command(command, out_dir): 143 def process_command(command, out_dir):
138 """Replaces isolated specific variables in a command line.""" 144 """Replaces isolated specific variables in a command line."""
139 def fix(arg): 145 def fix(arg):
140 if '${ISOLATED_OUTDIR}' in arg: 146 if ISOLATED_OUTDIR_PARAMETER in arg:
141 return arg.replace('${ISOLATED_OUTDIR}', out_dir).replace('/', os.sep) 147 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
148 # Replace slashes only if ISOLATED_OUTDIR_PARAMETER is present
149 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
150 arg = arg.replace('/', os.sep)
142 return arg 151 return arg
143 152
144 return [fix(arg) for arg in command] 153 return [fix(arg) for arg in command]
145 154
146 155
147 def run_command(command, cwd, tmp_dir, hard_timeout, grace_period): 156 def run_command(command, cwd, tmp_dir, hard_timeout, grace_period):
148 """Runs the command. 157 """Runs the command.
149 158
150 Returns: 159 Returns:
151 tuple(process exit code, bool if had a hard timeout) 160 tuple(process exit code, bool if had a hard timeout)
(...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after
295 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e) 304 logging.exception('Had difficulties removing out_dir %s: %s', out_dir, e)
296 stats = { 305 stats = {
297 'duration': time.time() - start, 306 'duration': time.time() - start,
298 'items_cold': base64.b64encode(large.pack(cold)), 307 'items_cold': base64.b64encode(large.pack(cold)),
299 'items_hot': base64.b64encode(large.pack(hot)), 308 'items_hot': base64.b64encode(large.pack(hot)),
300 } 309 }
301 return outputs_ref, success, stats 310 return outputs_ref, success, stats
302 311
303 312
304 def map_and_run( 313 def map_and_run(
305 isolated_hash, storage, cache, leak_temp_dir, root_dir, hard_timeout, 314 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
306 grace_period, extra_args): 315 hard_timeout, grace_period, extra_args):
307 """Maps and run the command. Returns metadata about the result.""" 316 """Runs a command with optional isolated input/output.
317
318 See run_tha_test for argument documentation.
319
320 Returns metadata about the result.
321 """
322 assert bool(command) ^ bool(isolated_hash)
308 result = { 323 result = {
309 'duration': None, 324 'duration': None,
310 'exit_code': None, 325 'exit_code': None,
311 'had_hard_timeout': False, 326 'had_hard_timeout': False,
312 'internal_failure': None, 327 'internal_failure': None,
313 'stats': { 328 'stats': {
314 # 'download': { 329 # 'download': {
315 # 'duration': 0., 330 # 'duration': 0.,
316 # 'initial_number_items': 0, 331 # 'initial_number_items': 0,
317 # 'initial_size': 0, 332 # 'initial_size': 0,
(...skipping 11 matching lines...) Expand all
329 } 344 }
330 if root_dir: 345 if root_dir:
331 file_path.ensure_tree(root_dir, 0700) 346 file_path.ensure_tree(root_dir, 0700)
332 prefix = u'' 347 prefix = u''
333 else: 348 else:
334 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None 349 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
335 prefix = u'isolated_' 350 prefix = u'isolated_'
336 run_dir = make_temp_dir(prefix + u'run', root_dir) 351 run_dir = make_temp_dir(prefix + u'run', root_dir)
337 out_dir = make_temp_dir(prefix + u'out', root_dir) 352 out_dir = make_temp_dir(prefix + u'out', root_dir)
338 tmp_dir = make_temp_dir(prefix + u'tmp', root_dir) 353 tmp_dir = make_temp_dir(prefix + u'tmp', root_dir)
354 cwd = run_dir
355
339 try: 356 try:
340 bundle, result['stats']['download'] = fetch_and_measure( 357 if isolated_hash:
341 isolated_hash=isolated_hash, 358 bundle, result['stats']['download'] = fetch_and_measure(
342 storage=storage, 359 isolated_hash=isolated_hash,
343 cache=cache, 360 storage=storage,
344 outdir=run_dir) 361 cache=cache,
345 if not bundle.command: 362 outdir=run_dir)
346 # Handle this as a task failure, not an internal failure. 363 if not bundle.command:
347 sys.stderr.write( 364 # Handle this as a task failure, not an internal failure.
348 '<The .isolated doesn\'t declare any command to run!>\n' 365 sys.stderr.write(
349 '<Check your .isolate for missing \'command\' variable>\n') 366 '<The .isolated doesn\'t declare any command to run!>\n'
350 if os.environ.get('SWARMING_TASK_ID'): 367 '<Check your .isolate for missing \'command\' variable>\n')
351 # Give an additional hint when running as a swarming task. 368 if os.environ.get('SWARMING_TASK_ID'):
352 sys.stderr.write('<This occurs at the \'isolate\' step>\n') 369 # Give an additional hint when running as a swarming task.
353 result['exit_code'] = 1 370 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
354 return result 371 result['exit_code'] = 1
372 return result
355 373
356 change_tree_read_only(run_dir, bundle.read_only) 374 change_tree_read_only(run_dir, bundle.read_only)
357 cwd = os.path.normpath(os.path.join(run_dir, bundle.relative_cwd)) 375 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
358 command = bundle.command + extra_args 376 command = bundle.command + extra_args
359 file_path.ensure_command_has_abs_path(command, cwd) 377 file_path.ensure_command_has_abs_path(command, cwd)
360 sys.stdout.flush() 378 sys.stdout.flush()
361 start = time.time() 379 start = time.time()
362 try: 380 try:
363 result['exit_code'], result['had_hard_timeout'] = run_command( 381 result['exit_code'], result['had_hard_timeout'] = run_command(
364 process_command(command, out_dir), cwd, tmp_dir, hard_timeout, 382 process_command(command, out_dir), cwd, tmp_dir, hard_timeout,
365 grace_period) 383 grace_period)
366 finally: 384 finally:
367 result['duration'] = max(time.time() - start, 0) 385 result['duration'] = max(time.time() - start, 0)
368 except Exception as e: 386 except Exception as e:
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
417 if not success and result['exit_code'] == 0: 435 if not success and result['exit_code'] == 0:
418 result['exit_code'] = 1 436 result['exit_code'] = 1
419 except Exception as e: 437 except Exception as e:
420 # Swallow any exception in the main finally clause. 438 # Swallow any exception in the main finally clause.
421 logging.exception('Leaking out_dir %s: %s', out_dir, e) 439 logging.exception('Leaking out_dir %s: %s', out_dir, e)
422 result['internal_failure'] = str(e) 440 result['internal_failure'] = str(e)
423 return result 441 return result
424 442
425 443
426 def run_tha_test( 444 def run_tha_test(
427 isolated_hash, storage, cache, leak_temp_dir, result_json, root_dir, 445 command, isolated_hash, storage, cache, leak_temp_dir, result_json, root_dir ,
M-A Ruel 2016/05/03 13:55:23 80 cols
428 hard_timeout, grace_period, extra_args): 446 hard_timeout, grace_period, extra_args):
429 """Downloads the dependencies in the cache, hardlinks them into a temporary 447 """Runs an executable and records execution metadata.
430 directory and runs the executable from there. 448
449 Either command or isolated_hash must be specified.
450
451 If isolated_hash is specified, downloads the dependencies in the cache,
452 hardlinks them into a temporary directory and runs the command specified in
453 the .isolated.
431 454
432 A temporary directory is created to hold the output files. The content inside 455 A temporary directory is created to hold the output files. The content inside
433 this directory will be uploaded back to |storage| packaged as a .isolated 456 this directory will be uploaded back to |storage| packaged as a .isolated
434 file. 457 file.
435 458
436 Arguments: 459 Arguments:
460 command: the command to run, a list of strings. Mutually exclusive with
461 isolated_hash.
437 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to 462 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
438 recreate the tree of files to run the target executable. 463 recreate the tree of files to run the target executable.
464 The command specified in the .isolated is executed.
465 Mutually exclusive with command argument.
439 storage: an isolateserver.Storage object to retrieve remote objects. This 466 storage: an isolateserver.Storage object to retrieve remote objects. This
440 object has a reference to an isolateserver.StorageApi, which does 467 object has a reference to an isolateserver.StorageApi, which does
441 the actual I/O. 468 the actual I/O.
442 cache: an isolateserver.LocalCache to keep from retrieving the same objects 469 cache: an isolateserver.LocalCache to keep from retrieving the same objects
443 constantly by caching the objects retrieved. Can be on-disk or 470 constantly by caching the objects retrieved. Can be on-disk or
444 in-memory. 471 in-memory.
445 leak_temp_dir: if true, the temporary directory will be deliberately leaked 472 leak_temp_dir: if true, the temporary directory will be deliberately leaked
446 for later examination. 473 for later examination.
447 result_json: file path to dump result metadata into. If set, the process 474 result_json: file path to dump result metadata into. If set, the process
448 exit code is always 0 unless an internal error occured. 475 exit code is always 0 unless an internal error occured.
449 root_dir: directory to the path to use to create the temporary directory. If 476 root_dir: directory to the path to use to create the temporary directory. If
450 not specified, a random temporary directory is created. 477 not specified, a random temporary directory is created.
451 hard_timeout: kills the process if it lasts more than this amount of 478 hard_timeout: kills the process if it lasts more than this amount of
452 seconds. 479 seconds.
453 grace_period: number of seconds to wait between SIGTERM and SIGKILL. 480 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
454 extra_args: optional arguments to add to the command stated in the .isolate 481 extra_args: optional arguments to add to the command stated in the .isolate
455 file. 482 file. Ignored if isolate_hash is empty.
456 483
457 Returns: 484 Returns:
458 Process exit code that should be used. 485 Process exit code that should be used.
459 """ 486 """
487 assert bool(command) ^ bool(isolated_hash)
488 extra_args = extra_args or []
489 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
490 assert storage is not None, 'storage is None although outdir is specified'
491
460 if result_json: 492 if result_json:
461 # Write a json output file right away in case we get killed. 493 # Write a json output file right away in case we get killed.
462 result = { 494 result = {
463 'exit_code': None, 495 'exit_code': None,
464 'had_hard_timeout': False, 496 'had_hard_timeout': False,
465 'internal_failure': 'Was terminated before completion', 497 'internal_failure': 'Was terminated before completion',
466 'outputs_ref': None, 498 'outputs_ref': None,
467 'version': 2, 499 'version': 2,
468 } 500 }
469 tools.write_json(result_json, result, dense=True) 501 tools.write_json(result_json, result, dense=True)
470 502
471 # run_isolated exit code. Depends on if result_json is used or not. 503 # run_isolated exit code. Depends on if result_json is used or not.
472 result = map_and_run( 504 result = map_and_run(
473 isolated_hash, storage, cache, leak_temp_dir, root_dir, hard_timeout, 505 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
474 grace_period, extra_args) 506 hard_timeout, grace_period, extra_args)
475 logging.info('Result:\n%s', tools.format_json(result, dense=True)) 507 logging.info('Result:\n%s', tools.format_json(result, dense=True))
476 if result_json: 508 if result_json:
477 # We've found tests to delete 'work' when quitting, causing an exception 509 # We've found tests to delete 'work' when quitting, causing an exception
478 # here. Try to recreate the directory if necessary. 510 # here. Try to recreate the directory if necessary.
479 file_path.ensure_tree(os.path.dirname(result_json)) 511 file_path.ensure_tree(os.path.dirname(result_json))
480 tools.write_json(result_json, result, dense=True) 512 tools.write_json(result_json, result, dense=True)
481 # Only return 1 if there was an internal error. 513 # Only return 1 if there was an internal error.
482 return int(bool(result['internal_failure'])) 514 return int(bool(result['internal_failure']))
483 515
484 # Marshall into old-style inline output. 516 # Marshall into old-style inline output.
485 if result['outputs_ref']: 517 if result['outputs_ref']:
486 data = { 518 data = {
487 'hash': result['outputs_ref']['isolated'], 519 'hash': result['outputs_ref']['isolated'],
488 'namespace': result['outputs_ref']['namespace'], 520 'namespace': result['outputs_ref']['namespace'],
489 'storage': result['outputs_ref']['isolatedserver'], 521 'storage': result['outputs_ref']['isolatedserver'],
490 } 522 }
491 sys.stdout.flush() 523 sys.stdout.flush()
492 print( 524 print(
493 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' % 525 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
494 tools.format_json(data, dense=True)) 526 tools.format_json(data, dense=True))
495 sys.stdout.flush() 527 sys.stdout.flush()
496 return result['exit_code'] or int(bool(result['internal_failure'])) 528 return result['exit_code'] or int(bool(result['internal_failure']))
497 529
498 530
499 def main(args): 531 def main(args):
500 parser = logging_utils.OptionParserWithLogging( 532 parser = logging_utils.OptionParserWithLogging(
501 usage='%prog <options>', 533 usage='%prog <options> [command to run or extra args]',
502 version=__version__, 534 version=__version__,
503 log_file=RUN_ISOLATED_LOG_FILE) 535 log_file=RUN_ISOLATED_LOG_FILE)
504 parser.add_option( 536 parser.add_option(
505 '--clean', action='store_true', 537 '--clean', action='store_true',
506 help='Cleans the cache, trimming it necessary and remove corrupted items ' 538 help='Cleans the cache, trimming it necessary and remove corrupted items '
507 'and returns without executing anything; use with -v to know what ' 539 'and returns without executing anything; use with -v to know what '
508 'was done') 540 'was done')
509 parser.add_option( 541 parser.add_option(
510 '--json', 542 '--json',
511 help='dump output metadata to json file. When used, run_isolated returns ' 543 help='dump output metadata to json file. When used, run_isolated returns '
512 'non-zero only on internal failure') 544 'non-zero only on internal failure')
513 parser.add_option( 545 parser.add_option(
514 '--hard-timeout', type='float', help='Enforce hard timeout in execution') 546 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
515 parser.add_option( 547 parser.add_option(
516 '--grace-period', type='float', 548 '--grace-period', type='float',
517 help='Grace period between SIGTERM and SIGKILL') 549 help='Grace period between SIGTERM and SIGKILL')
518 data_group = optparse.OptionGroup(parser, 'Data source') 550 data_group = optparse.OptionGroup(parser, 'Data source')
519 data_group.add_option( 551 data_group.add_option(
520 '-s', '--isolated', 552 '-s', '--isolated',
521 help='Hash of the .isolated to grab from the isolate server') 553 help='Hash of the .isolated to grab from the isolate server.')
522 isolateserver.add_isolate_server_options(data_group) 554 isolateserver.add_isolate_server_options(data_group)
523 parser.add_option_group(data_group) 555 parser.add_option_group(data_group)
524 556
525 isolateserver.add_cache_options(parser) 557 isolateserver.add_cache_options(parser)
526 parser.set_defaults(cache='cache') 558 parser.set_defaults(cache='cache')
527 559
528 debug_group = optparse.OptionGroup(parser, 'Debugging') 560 debug_group = optparse.OptionGroup(parser, 'Debugging')
529 debug_group.add_option( 561 debug_group.add_option(
530 '--leak-temp-dir', 562 '--leak-temp-dir',
531 action='store_true', 563 action='store_true',
532 help='Deliberately leak isolate\'s temp dir for later examination ' 564 help='Deliberately leak isolate\'s temp dir for later examination '
533 '[default: %default]') 565 '[default: %default]')
534 debug_group.add_option( 566 debug_group.add_option(
535 '--root-dir', help='Use a directory instead of a random one') 567 '--root-dir', help='Use a directory instead of a random one')
536 parser.add_option_group(debug_group) 568 parser.add_option_group(debug_group)
537 569
538 auth.add_auth_options(parser) 570 auth.add_auth_options(parser)
539 options, args = parser.parse_args(args) 571 options, args = parser.parse_args(args)
540 572
541 cache = isolateserver.process_cache_options(options) 573 cache = isolateserver.process_cache_options(options)
542 if options.clean: 574 if options.clean:
543 if options.isolated: 575 if options.isolated:
544 parser.error('Can\'t use --isolated with --clean.') 576 parser.error('Can\'t use --isolated with --clean.')
545 if options.isolate_server: 577 if options.isolate_server:
546 parser.error('Can\'t use --isolate-server with --clean.') 578 parser.error('Can\'t use --isolate-server with --clean.')
547 if options.json: 579 if options.json:
548 parser.error('Can\'t use --json with --clean.') 580 parser.error('Can\'t use --json with --clean.')
549 cache.cleanup() 581 cache.cleanup()
550 return 0 582 return 0
551 583
584 if not options.isolated and not args:
585 parser.error('--isolated or command to run is required.')
586
552 auth.process_auth_options(parser, options) 587 auth.process_auth_options(parser, options)
553 isolateserver.process_isolate_server_options(parser, options, True) 588
589 isolateserver.process_isolate_server_options(
590 parser, options, True, False)
591 if not options.isolate_server:
592 if options.isolated:
593 parser.error('--isolated requires --isolate-server')
594 if ISOLATED_OUTDIR_PARAMETER in args:
595 parser.error(
596 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
554 597
555 if options.root_dir: 598 if options.root_dir:
556 options.root_dir = unicode(os.path.abspath(options.root_dir)) 599 options.root_dir = unicode(os.path.abspath(options.root_dir))
557 if options.json: 600 if options.json:
558 options.json = unicode(os.path.abspath(options.json)) 601 options.json = unicode(os.path.abspath(options.json))
559 if not options.isolated: 602
560 parser.error('--isolated is required.') 603 command = [] if options.isolated else args
561 with isolateserver.get_storage( 604 if options.isolate_server:
562 options.isolate_server, options.namespace) as storage: 605 storage = isolateserver.get_storage(
606 options.isolate_server, options.namespace)
563 # Hashing schemes used by |storage| and |cache| MUST match. 607 # Hashing schemes used by |storage| and |cache| MUST match.
564 assert storage.hash_algo == cache.hash_algo 608 assert storage.hash_algo == cache.hash_algo
565 return run_tha_test( 609 return run_tha_test(
566 options.isolated, storage, cache, options.leak_temp_dir, options.json, 610 command, options.isolated, storage, cache, options.leak_temp_dir,
567 options.root_dir, options.hard_timeout, options.grace_period, args) 611 options.json, options.root_dir, options.hard_timeout,
612 options.grace_period, args)
613 else:
614 return run_tha_test(
615 command, options.isolated, None, cache, options.leak_temp_dir,
616 options.json, options.root_dir, options.hard_timeout,
617 options.grace_period, args)
568 618
569 619
570 if __name__ == '__main__': 620 if __name__ == '__main__':
571 # Ensure that we are always running with the correct encoding. 621 # Ensure that we are always running with the correct encoding.
572 fix_encoding.fix_encoding() 622 fix_encoding.fix_encoding()
573 sys.exit(main(sys.argv[1:])) 623 sys.exit(main(sys.argv[1:]))
OLDNEW
« no previous file with comments | « client/isolateserver.py ('k') | client/swarming.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698