| """Build step class for executing a simple shell command.""" |
| |
| from __future__ import absolute_import |
| import logging |
| import time |
| |
| import six |
| from helpers import retry_utils |
| from helpers import signal_utils |
| from slave import base_step |
| |
| class ShellStep(base_step.BaseStep): |
| """Build step class for executing a shell command.""" |
| |
| class ShellCommand(object): |
| """A simple object wrapping a shell command to execute.""" |
| |
| def __init__(self, command, validator=None, cwd=None, env=None, |
| num_retries=0, setup_for_retry=None, halt_on_failure=False, |
| retry_if=None): |
| """Constructor for a ShellCommand object. |
| |
| Args: |
| command: The command to be run, as a list of strings (required). |
| validator: A function that takes 3 arguments: returncode, stdout, stderr |
| from the run command and returns a boolean value indicating if the |
| command succeeded (True) or failed (False). |
| (Default checks if returncode == 0) |
| cwd: The directory from which this command is to be run. This will |
| override the current directory of the step. |
| env: A dictionary mapping environment variables to their values. |
| num_retries: The number of times to retry this step if it fails. |
| (Defaults to 0) |
| retry_if: A function that takes 3 arguments: returncode, stdout, stderr |
| from the run command and returns whether to retry the command (True) |
| or to skip retrying (False). |
| setup_for_retry: A function to run before attempting to retry this |
| command. Can be used to do work that may help the command |
| succeed upon retry. Only used if num_retries > 0. |
| halt_on_failure: Boolean if we should stop running subsequent steps on |
| failure. (Defaults to False) |
| """ |
| assert isinstance(command, list) |
| assert not cwd or isinstance(cwd, six.string_types) |
| assert not env or isinstance(env, dict) |
| |
| self.command = command |
| self._validator = validator |
| self.cwd = cwd |
| self.env = env or {} |
| self._num_retries = num_retries |
| self._retry_if = retry_if |
| self._setup_for_retry = setup_for_retry |
| self._halt_on_failure = halt_on_failure |
| |
| @property |
| def num_retries(self): |
| """Number of retries to run.""" |
| return self._num_retries |
| |
| @property |
| def halt_on_failure(self): |
| """Boolean if we should halt on a failure.""" |
| return self._halt_on_failure |
| |
| def exec_retry_setup(self, returncode, stdout, stderr): |
| """Run this before attempting to retry this command. |
| |
| If command has failed from a recoverable issue, then this can be used |
| to recover so that the next retry attempt of |self.command| will succeed. |
| |
| Args: |
| returncode: returncode of |self.command| that failed and is about to |
| be retried. |
| stdout: stdout of |self.command| that failed and is about to be retried. |
| stderr: stderr of |self.command| that failed and is about to be retried. |
| """ |
| if self._setup_for_retry: |
| self._setup_for_retry(returncode, stdout, stderr) |
| |
| def validate(self, returncode, stdout, stderr): |
| if self._validator: |
| return self._validator(returncode, stdout, stderr) |
| return returncode == 0 |
| |
| def retry_if(self, returncode, stdout, stderr): |
| """If command fails validator, check if it should be retried.""" |
| if self._retry_if: |
| return self._retry_if(returncode, stdout, stderr) |
| return returncode != 0 |
| |
| def __init__(self, name=None, command=None, validator=None, |
| properties_handler=None, force_success=False, num_retries=0, |
| retry_if=None, cwd=None, env=None, **kwargs): |
| """Creates a ShellStep instance. |
| |
| Args: |
| name: name of this step |
| command: shell command to run, as a list (argv). |
| validator: A function that takes 3 arguments: returncode, stdout, stderr |
| from the run command and returns a boolean value indicating if the |
| command succeeded (True) or failed (False). |
| (Default checks if returncode == 0) |
| properties_handler: if provided, method for providing properties from the |
| command output. |
| force_success: Force this step to return success, even if the shell |
| command returns non-zero. (Defaults to False) |
| num_retries: The number of times to retry this step if it fails. |
| (Defaults to 0) |
| retry_if: A function that takes 3 arguments: returncode, stdout, stderr |
| from the run command and returns whether to retry the command (True) |
| or to skip retrying (False). |
| cwd: The directory from which the ShellCommands will be run if cwd is |
| not set on the ShellCommand. This will override the current directory |
| for the step also. |
| env: A dictionary mapping environment variables to their values to be |
| used if no env is set on the ShelLCommands. |
| **kwargs: Any additional args to pass to BaseStep. |
| """ |
| base_step.BaseStep.__init__(self, name=name, **kwargs) |
| |
| self._command = command |
| self._validator = validator |
| self._properties_handler = properties_handler |
| self._force_success = force_success |
| self._num_retries = num_retries |
| self._retry_if = retry_if |
| self._cwd = cwd |
| self._env = env |
| |
| def _execute_cmd(self, cmd): |
| """Executes a ShellCommand. |
| |
| Args: |
| cmd: A ShellCommand instance. |
| |
| Returns: |
| A tuple of: |
| Boolean if the command was successful, returncode, stdout, stderr |
| """ |
| directory = cmd.cwd or self._cwd or self.directory or None |
| environment = cmd.env or self._env or {} |
| retry = True |
| attempt = 0 |
| success = True |
| while retry: |
| returncode, stdout, stderr = self.exec_subprocess( |
| cmd.command, cwd=directory, env=environment) |
| success = cmd.validate(returncode, stdout, stderr) |
| retry = attempt < cmd.num_retries and not success |
| if retry and cmd.retry_if(returncode, stdout, stderr): |
| delay = retry_utils.retry_delay(attempt) |
| attempt += 1 |
| logging.info( |
| 'Retrying command in %s seconds. Attempt %s of %s.', |
| delay, attempt, cmd.num_retries) |
| time.sleep(delay) |
| cmd.exec_retry_setup(returncode, stdout, stderr) |
| return success, returncode, stdout, stderr |
| |
| def run(self): |
| """Executes this step by running all commands found in get_commands(). |
| |
| Returns: |
| True iff the return code of every command was 0. |
| """ |
| retry, success = True, True |
| returncode, stdout, stderr = 0, '', '' |
| attempt = 0 |
| |
| while retry: |
| try: |
| success = True # Reset success for each retry |
| for shell_command in self.get_commands(): |
| cmd_success, returncode, stdout, stderr = self._execute_cmd( |
| shell_command |
| ) |
| |
| success = self._force_success or cmd_success |
| if shell_command.halt_on_failure and not success: |
| break |
| except signal_utils.SigtermException: |
| # We want to skip retries in case of a Sigterm because this means we |
| # got a signal from outside to stop current execution. |
| raise |
| except Exception: # pylint: disable=broad-except |
| # Absorb all exceptions until there are no retries left. |
| if attempt < self._num_retries: |
| logging.exception(('Step exception during [%s] ignored because there ' |
| 'are still retries(%d) remainining.'), |
| self.get_name(), |
| self._num_retries - attempt) |
| success = False |
| else: |
| logging.exception('Step exception during [%s] with no retries left.', |
| self.get_name(),) |
| return False |
| |
| finally: |
| retry = attempt < self._num_retries and not success |
| |
| if retry: |
| delay = retry_utils.retry_delay(attempt) |
| attempt += 1 |
| logging.info( |
| 'Retrying [%s] in %s seconds. Attempt %s of %s.', |
| self.get_name(), delay, attempt, self._num_retries) |
| time.sleep(delay) |
| |
| # Only set properties from the final run, otherwise properties from |
| # failed runs and successful runs could be mixed together. |
| if self._properties_handler: |
| new_properties = self._properties_handler(returncode, stdout, stderr) |
| for property_name in new_properties: |
| if property_name == 'review': |
| self.add_review(new_properties[property_name]) |
| else: |
| self.set_build_property(property_name, new_properties[property_name]) |
| |
| return success |
| |
| def get_commands(self): |
| """Returns which commands to be run. Can be overridden by a subclass. |
| |
| Returns: |
| List of ShellCommand objects to execute. |
| """ |
| assert isinstance(self._command, list) |
| return [self.ShellCommand(self._command, validator=self._validator, |
| retry_if=self._retry_if)] |