| OLD | NEW |
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import calendar | 5 import calendar |
| 6 import collections | 6 import collections |
| 7 import contextlib | 7 import contextlib |
| 8 import datetime | 8 import datetime |
| 9 import itertools |
| 9 import json | 10 import json |
| 10 import os | 11 import os |
| 11 import re | 12 import re |
| 12 import StringIO | 13 import StringIO |
| 13 import sys | 14 import sys |
| 14 import tempfile | 15 import tempfile |
| 15 import time | 16 import time |
| 16 import traceback | 17 import traceback |
| 17 | 18 |
| 18 from . import recipe_api | 19 from . import recipe_api |
| (...skipping 150 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 169 def stream(inner): | 170 def stream(inner): |
| 170 return step_stream | 171 return step_stream |
| 171 | 172 |
| 172 return EmptyOpenStep() | 173 return EmptyOpenStep() |
| 173 | 174 |
| 174 rendered_step = render_step( | 175 rendered_step = render_step( |
| 175 step_config, recipe_test_api.DisabledTestData() | 176 step_config, recipe_test_api.DisabledTestData() |
| 176 ) | 177 ) |
| 177 step_config = None # Make sure we use rendered step config. | 178 step_config = None # Make sure we use rendered step config. |
| 178 | 179 |
| 179 rendered_step = rendered_step._replace( | |
| 180 config=rendered_step.config._replace( | |
| 181 cmd=map(str, rendered_step.config.cmd), | |
| 182 ), | |
| 183 ) | |
| 184 | |
| 185 step_env = _merge_envs(os.environ, (rendered_step.config.env or {})) | 180 step_env = _merge_envs(os.environ, (rendered_step.config.env or {})) |
| 181 # Now that the step's environment is all sorted, evaluate PATH on windows |
| 182 # to find the actual intended executable. |
| 183 rendered_step = _hunt_path(rendered_step, step_env) |
| 186 self._print_step(step_stream, rendered_step, step_env) | 184 self._print_step(step_stream, rendered_step, step_env) |
| 187 | 185 |
| 188 class ReturnOpenStep(OpenStep): | 186 class ReturnOpenStep(OpenStep): |
| 189 def run(inner): | 187 def run(inner): |
| 190 step_config = rendered_step.config | 188 step_config = rendered_step.config |
| 191 try: | 189 try: |
| 192 # Open file handles for IO redirection based on file names in | 190 # Open file handles for IO redirection based on file names in |
| 193 # step_config. | 191 # step_config. |
| 194 handles = { | 192 handles = { |
| 195 'stdout': step_stream, | 193 'stdout': step_stream, |
| (...skipping 337 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 533 # This assert ensures that: | 531 # This assert ensures that: |
| 534 # no two placeholders have the same name | 532 # no two placeholders have the same name |
| 535 # at most one placeholder has the default name | 533 # at most one placeholder has the default name |
| 536 assert item.name not in output_phs[module_name][placeholder_name], ( | 534 assert item.name not in output_phs[module_name][placeholder_name], ( |
| 537 'Step "%s" has multiple output placeholders of %s.%s. Please ' | 535 'Step "%s" has multiple output placeholders of %s.%s. Please ' |
| 538 'specify explicit and different names for them.' % ( | 536 'specify explicit and different names for them.' % ( |
| 539 step_config.name, module_name, placeholder_name)) | 537 step_config.name, module_name, placeholder_name)) |
| 540 output_phs[module_name][placeholder_name][item.name] = (item, tdata) | 538 output_phs[module_name][placeholder_name][item.name] = (item, tdata) |
| 541 else: | 539 else: |
| 542 new_cmd.append(item) | 540 new_cmd.append(item) |
| 543 step_config = step_config._replace(cmd=new_cmd) | 541 step_config = step_config._replace(cmd=map(str, new_cmd)) |
| 544 | 542 |
| 545 # Process 'stdout', 'stderr' and 'stdin' placeholders, if given. | 543 # Process 'stdout', 'stderr' and 'stdin' placeholders, if given. |
| 546 stdio_placeholders = {} | 544 stdio_placeholders = {} |
| 547 for key in ('stdout', 'stderr', 'stdin'): | 545 for key in ('stdout', 'stderr', 'stdin'): |
| 548 placeholder = getattr(step_config, key) | 546 placeholder = getattr(step_config, key) |
| 549 tdata = None | 547 tdata = None |
| 550 if placeholder: | 548 if placeholder: |
| 551 if key == 'stdin': | 549 if key == 'stdin': |
| 552 assert isinstance(placeholder, util.InputPlaceholder), ( | 550 assert isinstance(placeholder, util.InputPlaceholder), ( |
| 553 '%s(%r) should be an InputPlaceholder.' % (key, placeholder)) | 551 '%s(%r) should be an InputPlaceholder.' % (key, placeholder)) |
| (...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 644 return result | 642 return result |
| 645 for k, v in override.items(): | 643 for k, v in override.items(): |
| 646 if v is None: | 644 if v is None: |
| 647 if k in result: | 645 if k in result: |
| 648 del result[k] | 646 del result[k] |
| 649 else: | 647 else: |
| 650 result[str(k)] = str(v) % original | 648 result[str(k)] = str(v) % original |
| 651 return result | 649 return result |
| 652 | 650 |
| 653 | 651 |
| 652 if sys.platform == "win32": |
| 653 _hunt_path_exts = ('.exe', '.bat') |
| 654 def _hunt_path(rendered_step, step_env): |
| 655 """This takes the lazy cross-product of PATH and ('.exe', '.bat') to find |
| 656 what cmd.exe would have found for the command if we used shell=True. |
| 657 |
| 658 This must be called on the render_step AFTER _merge_envs has produced |
| 659 step_env to pick up any changes to PATH. |
| 660 |
| 661 If it succeeds, it returns a new rendered_step. If it fails, it returns the |
| 662 same rendered_step, and subprocess will run as normal (and likely fail). |
| 663 |
| 664 This will not attempt to do any evaluations for commands where the program |
| 665 already has an explicit extension (.exe, .bat, etc.), and it will not |
| 666 attempt to do any evaluations for commands where the program is an absolute |
| 667 path. |
| 668 |
| 669 This DOES NOT use PATHEXT to keep the recipe engine's behavior as |
| 670 predictable as possible. We don't currently rely on any other runnable |
| 671 extensions besides these two, and when we could, we choose to explicitly |
| 672 invoke the interpreter (e.g. python.exe, cscript.exe, etc.). |
| 673 """ |
| 674 cmd = rendered_step.config.cmd |
| 675 cmd0 = cmd[0] |
| 676 if '.' in cmd0: # something.ext will work fine with subprocess. |
| 677 return rendered_step |
| 678 if os.path.isabs(cmd0): # PATH isn't even used |
| 679 return rendered_step |
| 680 |
| 681 # begin the hunt |
| 682 paths = step_env.get('PATH', '').split(os.pathsep) |
| 683 if not paths: |
| 684 return rendered_step |
| 685 |
| 686 # try every extension for each path |
| 687 for path, ext in itertools.product(paths, _hunt_path_exts): |
| 688 candidate = os.path.join(path, cmd0+ext) |
| 689 if os.path.isfile(candidate): |
| 690 return rendered_step._replace( |
| 691 config=rendered_step.config._replace( |
| 692 cmd=[candidate]+cmd[1:], |
| 693 ), |
| 694 ) |
| 695 return rendered_step |
| 696 else: |
| 697 def _hunt_path(rendered_step, _step_env): |
| 698 return rendered_step |
| 699 |
| 700 |
| 654 def _shell_quote(arg): | 701 def _shell_quote(arg): |
| 655 """Shell-quotes a string with minimal noise. | 702 """Shell-quotes a string with minimal noise. |
| 656 | 703 |
| 657 Such that it is still reproduced exactly in a bash/zsh shell. | 704 Such that it is still reproduced exactly in a bash/zsh shell. |
| 658 """ | 705 """ |
| 659 | 706 |
| 660 arg = arg.encode('utf-8') | 707 arg = arg.encode('utf-8') |
| 661 | 708 |
| 662 if arg == '': | 709 if arg == '': |
| 663 return "''" | 710 return "''" |
| (...skipping 15 matching lines...) Expand all Loading... |
| 679 supplied command, and only uses the |env| kwarg for modifying the environment | 726 supplied command, and only uses the |env| kwarg for modifying the environment |
| 680 of the child process. | 727 of the child process. |
| 681 """ | 728 """ |
| 682 saved_path = os.environ['PATH'] | 729 saved_path = os.environ['PATH'] |
| 683 try: | 730 try: |
| 684 if path is not None: | 731 if path is not None: |
| 685 os.environ['PATH'] = path | 732 os.environ['PATH'] = path |
| 686 yield | 733 yield |
| 687 finally: | 734 finally: |
| 688 os.environ['PATH'] = saved_path | 735 os.environ['PATH'] = saved_path |
| OLD | NEW |