Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Contains generating and parsing systems of the Chromium Buildbot Annotator. | 6 """Contains generating and parsing systems of the Chromium Buildbot Annotator. |
| 7 | 7 |
| 8 When executed as a script, this reads step name / command pairs from a file and | 8 When executed as a script, this reads step name / command pairs from a file and |
| 9 executes those lines while annotating the output. The input is json: | 9 executes those lines while annotating the output. The input is json: |
| 10 | 10 |
| (...skipping 446 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 457 | 457 |
| 458 step.step_trigger(json.dumps({ | 458 step.step_trigger(json.dumps({ |
| 459 'builderNames': [builder_name], | 459 'builderNames': [builder_name], |
| 460 'bucket': trig.get('bucket'), | 460 'bucket': trig.get('bucket'), |
| 461 'changes': changes, | 461 'changes': changes, |
| 462 'properties': trig.get('properties'), | 462 'properties': trig.get('properties'), |
| 463 }, sort_keys=True)) | 463 }, sort_keys=True)) |
| 464 | 464 |
| 465 | 465 |
| 466 def run_step(stream, name, cmd, | 466 def run_step(stream, name, cmd, |
| 467 step_annotation, | |
| 467 cwd=None, env=None, | 468 cwd=None, env=None, |
| 468 allow_subannotations=False, | 469 subannotator=None, |
| 469 trigger_specs=None, | 470 trigger_specs=None, |
| 470 **kwargs): | 471 **kwargs): |
| 471 """Runs a single step. | 472 """Runs a single step. |
| 472 | 473 |
| 473 Context: | 474 Context: |
| 474 stream: StructuredAnnotationStream to use to emit step | 475 stream: StructuredAnnotationStream to use to emit step |
| 476 step_annotation: optional StructuredAnnotationStep to use instead | |
| 477 of creating one | |
| 475 | 478 |
| 476 Step parameters: | 479 Step parameters: |
| 477 name: name of the step, will appear in buildbots waterfall | 480 name: name of the step, will appear in buildbots waterfall |
| 478 cmd: command to run, list of one or more strings | 481 cmd: command to run, list of one or more strings |
| 479 cwd: absolute path to working directory for the command | 482 cwd: absolute path to working directory for the command |
| 480 env: dict with overrides for environment variables | 483 env: dict with overrides for environment variables |
| 481 allow_subannotations: if True, lets the step emit its own annotations | 484 subannotator: a callback_implementor class used to parse subannotations |
| 485 that the command emits; if None, subannotations will be suppressed. | |
| 482 trigger_specs: a list of trigger specifications, which are dict with keys: | 486 trigger_specs: a list of trigger specifications, which are dict with keys: |
| 483 properties: a dict of properties. | 487 properties: a dict of properties. |
| 484 Buildbot requires buildername property. | 488 Buildbot requires buildername property. |
| 485 | 489 |
| 486 Known kwargs: | 490 Known kwargs: |
| 487 stdout: Path to a file to put step stdout into. If used, stdout won't appear | 491 stdout: Path to a file to put step stdout into. If used, stdout won't appear |
| 488 in annotator's stdout (and |allow_subannotations| is ignored). | 492 in annotator's stdout (and |allow_subannotations| is ignored). |
| 489 stderr: Path to a file to put step stderr into. If used, stderr won't appear | 493 stderr: Path to a file to put step stderr into. If used, stderr won't appear |
| 490 in annotator's stderr. | 494 in annotator's stderr. |
| 491 stdin: Path to a file to read step stdin from. | 495 stdin: Path to a file to read step stdin from. |
| 492 | 496 |
| 493 Returns the returncode of the step. | 497 Returns the returncode of the step. |
| 494 """ | 498 """ |
| 495 if isinstance(cmd, basestring): | 499 if isinstance(cmd, basestring): |
| 496 cmd = (cmd,) | 500 cmd = (cmd,) |
| 497 cmd = map(str, cmd) | 501 cmd = map(str, cmd) |
| 498 | 502 |
| 499 # For error reporting. | 503 # For error reporting. |
| 500 step_dict = kwargs.copy() | 504 step_dict = kwargs.copy() |
| 501 step_dict.update({ | 505 step_dict.update({ |
| 502 'name': name, | 506 'name': name, |
| 503 'cmd': cmd, | 507 'cmd': cmd, |
| 504 'cwd': cwd, | 508 'cwd': cwd, |
| 505 'env': env, | 509 'env': env, |
| 506 'allow_subannotations': allow_subannotations, | 510 'allow_subannotations': subannotator is not None, |
| 507 }) | 511 }) |
| 508 step_env = _merge_envs(os.environ, env) | 512 step_env = _merge_envs(os.environ, env) |
| 509 | 513 |
| 510 step_annotation = stream.step(name) | |
| 511 step_annotation.step_started() | |
| 512 | |
| 513 print_step(step_dict, step_env, stream) | 514 print_step(step_dict, step_env, stream) |
| 514 returncode = 0 | 515 returncode = 0 |
| 515 if cmd: | 516 if cmd: |
| 516 try: | 517 try: |
| 517 # Open file handles for IO redirection based on file names in step_dict. | 518 # Open file handles for IO redirection based on file names in step_dict. |
| 518 fhandles = { | 519 fhandles = { |
| 519 'stdout': subprocess.PIPE, | 520 'stdout': subprocess.PIPE, |
| 520 'stderr': subprocess.PIPE, | 521 'stderr': subprocess.PIPE, |
| 521 'stdin': None, | 522 'stdin': None, |
| 522 } | 523 } |
| 523 for key in fhandles: | 524 for key in fhandles: |
| 524 if key in step_dict: | 525 if key in step_dict: |
| 525 fhandles[key] = open(step_dict[key], | 526 fhandles[key] = open(step_dict[key], |
| 526 'rb' if key == 'stdin' else 'wb') | 527 'rb' if key == 'stdin' else 'wb') |
| 527 | 528 |
| 528 with modify_lookup_path(step_env.get('PATH')): | 529 with modify_lookup_path(step_env.get('PATH')): |
| 529 proc = subprocess.Popen( | 530 proc = subprocess.Popen( |
| 530 cmd, | 531 cmd, |
| 531 env=step_env, | 532 env=step_env, |
| 532 cwd=cwd, | 533 cwd=cwd, |
| 533 universal_newlines=True, | 534 universal_newlines=True, |
| 534 **fhandles) | 535 **fhandles) |
| 535 | 536 |
| 536 # Safe to close file handles now that subprocess has inherited them. | 537 # Safe to close file handles now that subprocess has inherited them. |
| 537 for handle in fhandles.itervalues(): | 538 for handle in fhandles.itervalues(): |
| 538 if isinstance(handle, file): | 539 if isinstance(handle, file): |
| 539 handle.close() | 540 handle.close() |
| 540 | 541 |
| 541 outlock = threading.Lock() | 542 outlock = threading.Lock() |
| 542 def filter_lines(lock, allow_subannotations, inhandle, outhandle): | 543 def filter_lines(inhandle, outhandle): |
|
iannucci
2015/04/03 20:45:59
yay closures
luqui
2015/04/07 23:26:53
Acknowledged.
| |
| 543 while True: | 544 while True: |
| 544 line = inhandle.readline() | 545 line = inhandle.readline() |
| 545 if not line: | 546 if not line: |
| 546 break | 547 break |
| 547 lock.acquire() | 548 with outlock: |
| 548 try: | 549 if line.startswith('@@@'): |
| 549 if not allow_subannotations and line.startswith('@@@'): | 550 if subannotator: |
| 550 outhandle.write('!') | 551 # The subannotator might write to the handle, thus the lock. |
|
iannucci
2015/04/03 20:45:59
where does subannotator get bound to outhandle?
luqui
2015/04/07 23:26:54
outhandle can be sys.stdout, which is written to b
| |
| 551 outhandle.write(line) | 552 MatchAnnotation(line.strip(), subannotator) |
| 553 else: | |
| 554 outhandle.write('!') | |
| 555 outhandle.write(line) | |
| 556 else: | |
| 557 outhandle.write(line) | |
| 552 outhandle.flush() | 558 outhandle.flush() |
| 553 finally: | |
| 554 lock.release() | |
| 555 | 559 |
| 556 # Pump piped stdio through filter_lines. IO going to files on disk is | 560 # Pump piped stdio through filter_lines. IO going to files on disk is |
| 557 # not filtered. | 561 # not filtered. |
| 558 threads = [] | 562 threads = [] |
| 559 for key in ('stdout', 'stderr'): | 563 for key in ('stdout', 'stderr'): |
| 560 if fhandles[key] == subprocess.PIPE: | 564 if fhandles[key] == subprocess.PIPE: |
| 561 inhandle = getattr(proc, key) | 565 inhandle = getattr(proc, key) |
| 562 outhandle = getattr(sys, key) | 566 outhandle = getattr(sys, key) |
| 563 threads.append(threading.Thread( | 567 threads.append(threading.Thread( |
| 564 target=filter_lines, | 568 target=filter_lines, |
| 565 args=(outlock, allow_subannotations, inhandle, outhandle))) | 569 args=(inhandle, outhandle))) |
| 566 | 570 |
| 567 for th in threads: | 571 for th in threads: |
| 568 th.start() | 572 th.start() |
| 569 proc.wait() | 573 proc.wait() |
| 570 for th in threads: | 574 for th in threads: |
| 571 th.join() | 575 th.join() |
| 572 returncode = proc.returncode | 576 returncode = proc.returncode |
| 573 except OSError: | 577 except OSError: |
| 574 # File wasn't found, error will be reported to stream when the exception | 578 # File wasn't found, error will be reported to stream when the exception |
| 575 # crosses the context manager. | 579 # crosses the context manager. |
| 576 step_annotation.step_exception_occured(*sys.exc_info()) | 580 step_annotation.step_exception_occured(*sys.exc_info()) |
| 577 raise | 581 raise |
| 578 | 582 |
| 579 # TODO(martiniss) move logic into own module? | 583 # TODO(martiniss) move logic into own module? |
| 580 if trigger_specs: | 584 if trigger_specs: |
| 581 triggerBuilds(step_annotation, trigger_specs) | 585 triggerBuilds(step_annotation, trigger_specs) |
| 582 | 586 |
| 583 return step_annotation, returncode | 587 return returncode |
| 584 | 588 |
| 585 def update_build_failure(failure, retcode, **_kwargs): | 589 def update_build_failure(failure, retcode, **_kwargs): |
| 586 """Potentially moves failure from False to True, depending on returncode of | 590 """Potentially moves failure from False to True, depending on returncode of |
| 587 the run step and the step's configuration. | 591 the run step and the step's configuration. |
| 588 | 592 |
| 589 can_fail_build: A boolean indicating that a bad retcode for this step should | 593 can_fail_build: A boolean indicating that a bad retcode for this step should |
| 590 be intepreted as a build failure. | 594 be intepreted as a build failure. |
| 591 | 595 |
| 592 Returns new value for failure. | 596 Returns new value for failure. |
| 593 | 597 |
| 594 Called externally from annotated_run, which is why it's a separate function. | 598 Called externally from annotated_run, which is why it's a separate function. |
| 595 """ | 599 """ |
| 596 # TODO(iannucci): Allow step to specify "OK" return values besides 0? | 600 # TODO(iannucci): Allow step to specify "OK" return values besides 0? |
| 597 return failure or retcode | 601 return failure or retcode |
| 598 | 602 |
| 599 def run_steps(steps, build_failure): | 603 def run_steps(steps, build_failure): |
| 600 for step in steps: | 604 for step in steps: |
| 601 error = _validate_step(step) | 605 error = _validate_step(step) |
| 602 if error: | 606 if error: |
| 603 print 'Invalid step - %s\n%s' % (error, json.dumps(step, indent=2)) | 607 print 'Invalid step - %s\n%s' % (error, json.dumps(step, indent=2)) |
| 604 sys.exit(1) | 608 sys.exit(1) |
| 605 | 609 |
| 606 stream = StructuredAnnotationStream() | 610 stream = StructuredAnnotationStream() |
| 607 ret_codes = [] | 611 ret_codes = [] |
| 608 build_failure = False | 612 build_failure = False |
| 609 prev_annotation = None | 613 prev_annotation = None |
| 610 for step in steps: | 614 for step in steps: |
| 611 if build_failure and not step.get('always_run', False): | 615 if build_failure and not step.get('always_run', False): |
|
iannucci
2015/04/03 20:45:59
this function isn't used for anything... it should
luqui
2015/04/07 23:26:54
A bit of code removal and unittest changes to do t
| |
| 612 ret = None | 616 ret = None |
| 613 else: | 617 else: |
| 614 prev_annotation, ret = run_step(stream, **step) | 618 prev_annotation = stream.step(step['name']) |
| 619 prev_annotation.step_started() | |
| 620 ret = run_step(stream, step_annotation=prev_annotation, **step) | |
| 615 stream = prev_annotation.annotation_stream | 621 stream = prev_annotation.annotation_stream |
| 616 if ret > 0: | 622 if ret > 0: |
| 617 stream.step_cursor(stream.current_step) | 623 stream.step_cursor(stream.current_step) |
| 618 stream.emit('step returned non-zero exit code: %d' % ret) | 624 stream.emit('step returned non-zero exit code: %d' % ret) |
| 619 prev_annotation.step_failure() | 625 prev_annotation.step_failure() |
| 620 | 626 |
| 621 prev_annotation.step_ended() | 627 prev_annotation.step_ended() |
| 622 build_failure = update_build_failure(build_failure, ret) | 628 build_failure = update_build_failure(build_failure, ret) |
| 623 ret_codes.append(ret) | 629 ret_codes.append(ret) |
| 624 if prev_annotation: | 630 if prev_annotation: |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 665 steps.extend(json.load(sys.stdin, object_hook=force_dict_strs)) | 671 steps.extend(json.load(sys.stdin, object_hook=force_dict_strs)) |
| 666 else: | 672 else: |
| 667 with open(args[0], 'rb') as f: | 673 with open(args[0], 'rb') as f: |
| 668 steps.extend(json.load(f, object_hook=force_dict_strs)) | 674 steps.extend(json.load(f, object_hook=force_dict_strs)) |
| 669 | 675 |
| 670 return 1 if run_steps(steps, False)[0] else 0 | 676 return 1 if run_steps(steps, False)[0] else 0 |
| 671 | 677 |
| 672 | 678 |
| 673 if __name__ == '__main__': | 679 if __name__ == '__main__': |
| 674 sys.exit(main()) | 680 sys.exit(main()) |
| OLD | NEW |