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 |