| 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, | |
| 468 cwd=None, env=None, | 467 cwd=None, env=None, |
| 469 subannotator=None, | 468 allow_subannotations=False, |
| 470 trigger_specs=None, | 469 trigger_specs=None, |
| 471 **kwargs): | 470 **kwargs): |
| 472 """Runs a single step. | 471 """Runs a single step. |
| 473 | 472 |
| 474 Context: | 473 Context: |
| 475 stream: StructuredAnnotationStream to use to emit step | 474 stream: StructuredAnnotationStream to use to emit step |
| 476 step_annotation: optional StructuredAnnotationStep to use instead | |
| 477 of creating one | |
| 478 | 475 |
| 479 Step parameters: | 476 Step parameters: |
| 480 name: name of the step, will appear in buildbots waterfall | 477 name: name of the step, will appear in buildbots waterfall |
| 481 cmd: command to run, list of one or more strings | 478 cmd: command to run, list of one or more strings |
| 482 cwd: absolute path to working directory for the command | 479 cwd: absolute path to working directory for the command |
| 483 env: dict with overrides for environment variables | 480 env: dict with overrides for environment variables |
| 484 subannotator: a callback_implementor class used to parse subannotations | 481 allow_subannotations: if True, lets the step emit its own annotations |
| 485 that the command emits; if None, subannotations will be suppressed. | |
| 486 trigger_specs: a list of trigger specifications, which are dict with keys: | 482 trigger_specs: a list of trigger specifications, which are dict with keys: |
| 487 properties: a dict of properties. | 483 properties: a dict of properties. |
| 488 Buildbot requires buildername property. | 484 Buildbot requires buildername property. |
| 489 | 485 |
| 490 Known kwargs: | 486 Known kwargs: |
| 491 stdout: Path to a file to put step stdout into. If used, stdout won't appear | 487 stdout: Path to a file to put step stdout into. If used, stdout won't appear |
| 492 in annotator's stdout (and |allow_subannotations| is ignored). | 488 in annotator's stdout (and |allow_subannotations| is ignored). |
| 493 stderr: Path to a file to put step stderr into. If used, stderr won't appear | 489 stderr: Path to a file to put step stderr into. If used, stderr won't appear |
| 494 in annotator's stderr. | 490 in annotator's stderr. |
| 495 stdin: Path to a file to read step stdin from. | 491 stdin: Path to a file to read step stdin from. |
| 496 | 492 |
| 497 Returns the returncode of the step. | 493 Returns the returncode of the step. |
| 498 """ | 494 """ |
| 499 if isinstance(cmd, basestring): | 495 if isinstance(cmd, basestring): |
| 500 cmd = (cmd,) | 496 cmd = (cmd,) |
| 501 cmd = map(str, cmd) | 497 cmd = map(str, cmd) |
| 502 | 498 |
| 503 # For error reporting. | 499 # For error reporting. |
| 504 step_dict = kwargs.copy() | 500 step_dict = kwargs.copy() |
| 505 step_dict.update({ | 501 step_dict.update({ |
| 506 'name': name, | 502 'name': name, |
| 507 'cmd': cmd, | 503 'cmd': cmd, |
| 508 'cwd': cwd, | 504 'cwd': cwd, |
| 509 'env': env, | 505 'env': env, |
| 510 'allow_subannotations': subannotator is not None, | 506 'allow_subannotations': allow_subannotations, |
| 511 }) | 507 }) |
| 512 step_env = _merge_envs(os.environ, env) | 508 step_env = _merge_envs(os.environ, env) |
| 513 | 509 |
| 510 step_annotation = stream.step(name) |
| 511 step_annotation.step_started() |
| 512 |
| 514 print_step(step_dict, step_env, stream) | 513 print_step(step_dict, step_env, stream) |
| 515 returncode = 0 | 514 returncode = 0 |
| 516 if cmd: | 515 if cmd: |
| 517 try: | 516 try: |
| 518 # Open file handles for IO redirection based on file names in step_dict. | 517 # Open file handles for IO redirection based on file names in step_dict. |
| 519 fhandles = { | 518 fhandles = { |
| 520 'stdout': subprocess.PIPE, | 519 'stdout': subprocess.PIPE, |
| 521 'stderr': subprocess.PIPE, | 520 'stderr': subprocess.PIPE, |
| 522 'stdin': None, | 521 'stdin': None, |
| 523 } | 522 } |
| (...skipping 27 matching lines...) Expand all Loading... |
| 551 universal_newlines=True, | 550 universal_newlines=True, |
| 552 creationflags=creationflags, | 551 creationflags=creationflags, |
| 553 **fhandles) | 552 **fhandles) |
| 554 | 553 |
| 555 # Safe to close file handles now that subprocess has inherited them. | 554 # Safe to close file handles now that subprocess has inherited them. |
| 556 for handle in fhandles.itervalues(): | 555 for handle in fhandles.itervalues(): |
| 557 if isinstance(handle, file): | 556 if isinstance(handle, file): |
| 558 handle.close() | 557 handle.close() |
| 559 | 558 |
| 560 outlock = threading.Lock() | 559 outlock = threading.Lock() |
| 561 def filter_lines(inhandle, outhandle): | 560 def filter_lines(lock, allow_subannotations, inhandle, outhandle): |
| 562 while True: | 561 while True: |
| 563 line = inhandle.readline() | 562 line = inhandle.readline() |
| 564 if not line: | 563 if not line: |
| 565 break | 564 break |
| 566 with outlock: | 565 lock.acquire() |
| 567 if line.startswith('@@@'): | 566 try: |
| 568 if subannotator: | 567 if not allow_subannotations and line.startswith('@@@'): |
| 569 # The subannotator might write to the handle, thus the lock. | 568 outhandle.write('!') |
| 570 MatchAnnotation(line.strip(), subannotator) | 569 outhandle.write(line) |
| 571 else: | |
| 572 outhandle.write('!') | |
| 573 outhandle.write(line) | |
| 574 else: | |
| 575 outhandle.write(line) | |
| 576 outhandle.flush() | 570 outhandle.flush() |
| 571 finally: |
| 572 lock.release() |
| 577 | 573 |
| 578 # Pump piped stdio through filter_lines. IO going to files on disk is | 574 # Pump piped stdio through filter_lines. IO going to files on disk is |
| 579 # not filtered. | 575 # not filtered. |
| 580 threads = [] | 576 threads = [] |
| 581 for key in ('stdout', 'stderr'): | 577 for key in ('stdout', 'stderr'): |
| 582 if fhandles[key] == subprocess.PIPE: | 578 if fhandles[key] == subprocess.PIPE: |
| 583 inhandle = getattr(proc, key) | 579 inhandle = getattr(proc, key) |
| 584 outhandle = getattr(sys, key) | 580 outhandle = getattr(sys, key) |
| 585 threads.append(threading.Thread( | 581 threads.append(threading.Thread( |
| 586 target=filter_lines, | 582 target=filter_lines, |
| 587 args=(inhandle, outhandle))) | 583 args=(outlock, allow_subannotations, inhandle, outhandle))) |
| 588 | 584 |
| 589 for th in threads: | 585 for th in threads: |
| 590 th.start() | 586 th.start() |
| 591 proc.wait() | 587 proc.wait() |
| 592 for th in threads: | 588 for th in threads: |
| 593 th.join() | 589 th.join() |
| 594 returncode = proc.returncode | 590 returncode = proc.returncode |
| 595 except OSError: | 591 except OSError: |
| 596 # File wasn't found, error will be reported to stream when the exception | 592 # File wasn't found, error will be reported to stream when the exception |
| 597 # crosses the context manager. | 593 # crosses the context manager. |
| 598 step_annotation.step_exception_occured(*sys.exc_info()) | 594 step_annotation.step_exception_occured(*sys.exc_info()) |
| 599 raise | 595 raise |
| 600 | 596 |
| 601 # TODO(martiniss) move logic into own module? | 597 # TODO(martiniss) move logic into own module? |
| 602 if trigger_specs: | 598 if trigger_specs: |
| 603 triggerBuilds(step_annotation, trigger_specs) | 599 triggerBuilds(step_annotation, trigger_specs) |
| 604 | 600 |
| 605 return returncode | 601 return step_annotation, returncode |
| 606 | 602 |
| 607 def update_build_failure(failure, retcode, **_kwargs): | 603 def update_build_failure(failure, retcode, **_kwargs): |
| 608 """Potentially moves failure from False to True, depending on returncode of | 604 """Potentially moves failure from False to True, depending on returncode of |
| 609 the run step and the step's configuration. | 605 the run step and the step's configuration. |
| 610 | 606 |
| 611 can_fail_build: A boolean indicating that a bad retcode for this step should | 607 can_fail_build: A boolean indicating that a bad retcode for this step should |
| 612 be intepreted as a build failure. | 608 be intepreted as a build failure. |
| 613 | 609 |
| 614 Returns new value for failure. | 610 Returns new value for failure. |
| 615 | 611 |
| (...skipping 10 matching lines...) Expand all Loading... |
| 626 sys.exit(1) | 622 sys.exit(1) |
| 627 | 623 |
| 628 stream = StructuredAnnotationStream() | 624 stream = StructuredAnnotationStream() |
| 629 ret_codes = [] | 625 ret_codes = [] |
| 630 build_failure = False | 626 build_failure = False |
| 631 prev_annotation = None | 627 prev_annotation = None |
| 632 for step in steps: | 628 for step in steps: |
| 633 if build_failure and not step.get('always_run', False): | 629 if build_failure and not step.get('always_run', False): |
| 634 ret = None | 630 ret = None |
| 635 else: | 631 else: |
| 636 prev_annotation = stream.step(step['name']) | 632 prev_annotation, ret = run_step(stream, **step) |
| 637 prev_annotation.step_started() | |
| 638 ret = run_step(stream, step_annotation=prev_annotation, **step) | |
| 639 stream = prev_annotation.annotation_stream | 633 stream = prev_annotation.annotation_stream |
| 640 if ret > 0: | 634 if ret > 0: |
| 641 stream.step_cursor(stream.current_step) | 635 stream.step_cursor(stream.current_step) |
| 642 stream.emit('step returned non-zero exit code: %d' % ret) | 636 stream.emit('step returned non-zero exit code: %d' % ret) |
| 643 prev_annotation.step_failure() | 637 prev_annotation.step_failure() |
| 644 | 638 |
| 645 prev_annotation.step_ended() | 639 prev_annotation.step_ended() |
| 646 build_failure = update_build_failure(build_failure, ret) | 640 build_failure = update_build_failure(build_failure, ret) |
| 647 ret_codes.append(ret) | 641 ret_codes.append(ret) |
| 648 if prev_annotation: | 642 if prev_annotation: |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 689 steps.extend(json.load(sys.stdin, object_hook=force_dict_strs)) | 683 steps.extend(json.load(sys.stdin, object_hook=force_dict_strs)) |
| 690 else: | 684 else: |
| 691 with open(args[0], 'rb') as f: | 685 with open(args[0], 'rb') as f: |
| 692 steps.extend(json.load(f, object_hook=force_dict_strs)) | 686 steps.extend(json.load(f, object_hook=force_dict_strs)) |
| 693 | 687 |
| 694 return 1 if run_steps(steps, False)[0] else 0 | 688 return 1 if run_steps(steps, False)[0] else 0 |
| 695 | 689 |
| 696 | 690 |
| 697 if __name__ == '__main__': | 691 if __name__ == '__main__': |
| 698 sys.exit(main()) | 692 sys.exit(main()) |
| OLD | NEW |