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 } |
(...skipping 27 matching lines...) Expand all Loading... |
550 universal_newlines=True, | 551 universal_newlines=True, |
551 creationflags=creationflags, | 552 creationflags=creationflags, |
552 **fhandles) | 553 **fhandles) |
553 | 554 |
554 # Safe to close file handles now that subprocess has inherited them. | 555 # Safe to close file handles now that subprocess has inherited them. |
555 for handle in fhandles.itervalues(): | 556 for handle in fhandles.itervalues(): |
556 if isinstance(handle, file): | 557 if isinstance(handle, file): |
557 handle.close() | 558 handle.close() |
558 | 559 |
559 outlock = threading.Lock() | 560 outlock = threading.Lock() |
560 def filter_lines(lock, allow_subannotations, inhandle, outhandle): | 561 def filter_lines(inhandle, outhandle): |
561 while True: | 562 while True: |
562 line = inhandle.readline() | 563 line = inhandle.readline() |
563 if not line: | 564 if not line: |
564 break | 565 break |
565 lock.acquire() | 566 with outlock: |
566 try: | 567 if line.startswith('@@@'): |
567 if not allow_subannotations and line.startswith('@@@'): | 568 if subannotator: |
568 outhandle.write('!') | 569 # The subannotator might write to the handle, thus the lock. |
569 outhandle.write(line) | 570 MatchAnnotation(line.strip(), subannotator) |
| 571 else: |
| 572 outhandle.write('!') |
| 573 outhandle.write(line) |
| 574 else: |
| 575 outhandle.write(line) |
570 outhandle.flush() | 576 outhandle.flush() |
571 finally: | |
572 lock.release() | |
573 | 577 |
574 # Pump piped stdio through filter_lines. IO going to files on disk is | 578 # Pump piped stdio through filter_lines. IO going to files on disk is |
575 # not filtered. | 579 # not filtered. |
576 threads = [] | 580 threads = [] |
577 for key in ('stdout', 'stderr'): | 581 for key in ('stdout', 'stderr'): |
578 if fhandles[key] == subprocess.PIPE: | 582 if fhandles[key] == subprocess.PIPE: |
579 inhandle = getattr(proc, key) | 583 inhandle = getattr(proc, key) |
580 outhandle = getattr(sys, key) | 584 outhandle = getattr(sys, key) |
581 threads.append(threading.Thread( | 585 threads.append(threading.Thread( |
582 target=filter_lines, | 586 target=filter_lines, |
583 args=(outlock, allow_subannotations, inhandle, outhandle))) | 587 args=(inhandle, outhandle))) |
584 | 588 |
585 for th in threads: | 589 for th in threads: |
586 th.start() | 590 th.start() |
587 proc.wait() | 591 proc.wait() |
588 for th in threads: | 592 for th in threads: |
589 th.join() | 593 th.join() |
590 returncode = proc.returncode | 594 returncode = proc.returncode |
591 except OSError: | 595 except OSError: |
592 # File wasn't found, error will be reported to stream when the exception | 596 # File wasn't found, error will be reported to stream when the exception |
593 # crosses the context manager. | 597 # crosses the context manager. |
594 step_annotation.step_exception_occured(*sys.exc_info()) | 598 step_annotation.step_exception_occured(*sys.exc_info()) |
595 raise | 599 raise |
596 | 600 |
597 # TODO(martiniss) move logic into own module? | 601 # TODO(martiniss) move logic into own module? |
598 if trigger_specs: | 602 if trigger_specs: |
599 triggerBuilds(step_annotation, trigger_specs) | 603 triggerBuilds(step_annotation, trigger_specs) |
600 | 604 |
601 return step_annotation, returncode | 605 return returncode |
602 | 606 |
603 def update_build_failure(failure, retcode, **_kwargs): | 607 def update_build_failure(failure, retcode, **_kwargs): |
604 """Potentially moves failure from False to True, depending on returncode of | 608 """Potentially moves failure from False to True, depending on returncode of |
605 the run step and the step's configuration. | 609 the run step and the step's configuration. |
606 | 610 |
607 can_fail_build: A boolean indicating that a bad retcode for this step should | 611 can_fail_build: A boolean indicating that a bad retcode for this step should |
608 be intepreted as a build failure. | 612 be intepreted as a build failure. |
609 | 613 |
610 Returns new value for failure. | 614 Returns new value for failure. |
611 | 615 |
(...skipping 10 matching lines...) Expand all Loading... |
622 sys.exit(1) | 626 sys.exit(1) |
623 | 627 |
624 stream = StructuredAnnotationStream() | 628 stream = StructuredAnnotationStream() |
625 ret_codes = [] | 629 ret_codes = [] |
626 build_failure = False | 630 build_failure = False |
627 prev_annotation = None | 631 prev_annotation = None |
628 for step in steps: | 632 for step in steps: |
629 if build_failure and not step.get('always_run', False): | 633 if build_failure and not step.get('always_run', False): |
630 ret = None | 634 ret = None |
631 else: | 635 else: |
632 prev_annotation, ret = run_step(stream, **step) | 636 prev_annotation = stream.step(step['name']) |
| 637 prev_annotation.step_started() |
| 638 ret = run_step(stream, step_annotation=prev_annotation, **step) |
633 stream = prev_annotation.annotation_stream | 639 stream = prev_annotation.annotation_stream |
634 if ret > 0: | 640 if ret > 0: |
635 stream.step_cursor(stream.current_step) | 641 stream.step_cursor(stream.current_step) |
636 stream.emit('step returned non-zero exit code: %d' % ret) | 642 stream.emit('step returned non-zero exit code: %d' % ret) |
637 prev_annotation.step_failure() | 643 prev_annotation.step_failure() |
638 | 644 |
639 prev_annotation.step_ended() | 645 prev_annotation.step_ended() |
640 build_failure = update_build_failure(build_failure, ret) | 646 build_failure = update_build_failure(build_failure, ret) |
641 ret_codes.append(ret) | 647 ret_codes.append(ret) |
642 if prev_annotation: | 648 if prev_annotation: |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
683 steps.extend(json.load(sys.stdin, object_hook=force_dict_strs)) | 689 steps.extend(json.load(sys.stdin, object_hook=force_dict_strs)) |
684 else: | 690 else: |
685 with open(args[0], 'rb') as f: | 691 with open(args[0], 'rb') as f: |
686 steps.extend(json.load(f, object_hook=force_dict_strs)) | 692 steps.extend(json.load(f, object_hook=force_dict_strs)) |
687 | 693 |
688 return 1 if run_steps(steps, False)[0] else 0 | 694 return 1 if run_steps(steps, False)[0] else 0 |
689 | 695 |
690 | 696 |
691 if __name__ == '__main__': | 697 if __name__ == '__main__': |
692 sys.exit(main()) | 698 sys.exit(main()) |
OLD | NEW |