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 |