2
0

execute.py 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. import logging
  2. import subprocess
  3. logger = logging.getLogger(__name__)
  4. ERROR_OUTPUT_MAX_LINE_COUNT = 25
  5. BORG_ERROR_EXIT_CODE = 2
  6. def execute_and_log_output(full_command, output_log_level, shell):
  7. last_lines = []
  8. process = subprocess.Popen(
  9. full_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell
  10. )
  11. while process.poll() is None:
  12. line = process.stdout.readline().rstrip().decode()
  13. if not line:
  14. continue
  15. # Keep the last few lines of output in case the command errors, and we need the output for
  16. # the exception below.
  17. last_lines.append(line)
  18. if len(last_lines) > ERROR_OUTPUT_MAX_LINE_COUNT:
  19. last_lines.pop(0)
  20. logger.log(output_log_level, line)
  21. remaining_output = process.stdout.read().rstrip().decode()
  22. if remaining_output: # pragma: no cover
  23. logger.log(output_log_level, remaining_output)
  24. exit_code = process.poll()
  25. # If shell is True, assume we're running something other than Borg and should treat all non-zero
  26. # exit codes as errors.
  27. error = bool(exit_code != 0) if shell else bool(exit_code >= BORG_ERROR_EXIT_CODE)
  28. if error:
  29. # If an error occurs, include its output in the raised exception so that we don't
  30. # inadvertently hide error output.
  31. if len(last_lines) == ERROR_OUTPUT_MAX_LINE_COUNT:
  32. last_lines.insert(0, '...')
  33. raise subprocess.CalledProcessError(
  34. exit_code, ' '.join(full_command), '\n'.join(last_lines)
  35. )
  36. def execute_command(full_command, output_log_level=logging.INFO, shell=False):
  37. '''
  38. Execute the given command (a sequence of command/argument strings) and log its output at the
  39. given log level. If output log level is None, instead capture and return the output. If
  40. shell is True, execute the command within a shell.
  41. Raise subprocesses.CalledProcessError if an error occurs while running the command.
  42. '''
  43. logger.debug(' '.join(full_command))
  44. if output_log_level is None:
  45. output = subprocess.check_output(full_command, shell=shell)
  46. return output.decode() if output is not None else None
  47. else:
  48. execute_and_log_output(full_command, output_log_level, shell=shell)
  49. def execute_command_without_capture(full_command):
  50. '''
  51. Execute the given command (a sequence of command/argument strings), but don't capture or log its
  52. output in any way. This is necessary for commands that monkey with the terminal (e.g. progress
  53. display) or provide interactive prompts.
  54. '''
  55. logger.debug(' '.join(full_command))
  56. try:
  57. subprocess.check_call(full_command)
  58. except subprocess.CalledProcessError as error:
  59. if error.returncode >= BORG_ERROR_EXIT_CODE:
  60. raise