execute.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import logging
  2. import os
  3. import subprocess
  4. logger = logging.getLogger(__name__)
  5. ERROR_OUTPUT_MAX_LINE_COUNT = 25
  6. BORG_ERROR_EXIT_CODE = 2
  7. def exit_code_indicates_error(command, exit_code, error_on_warnings=False):
  8. '''
  9. Return True if the given exit code from running the command corresponds to an error.
  10. '''
  11. # If we're running something other than Borg, treat all non-zero exit codes as errors.
  12. if 'borg' in command[0] and not error_on_warnings:
  13. return bool(exit_code >= BORG_ERROR_EXIT_CODE)
  14. return bool(exit_code != 0)
  15. def log_output(command, process, output_buffer, output_log_level, error_on_warnings):
  16. '''
  17. Given a command already executed, its process opened by subprocess.Popen(), and the process'
  18. relevant output buffer (stderr or stdout), log its output with the requested log level.
  19. Additionally, raise a CalledProcessException if the process exits with an error (or a warning,
  20. if error on warnings is True).
  21. '''
  22. last_lines = []
  23. while process.poll() is None:
  24. line = output_buffer.readline().rstrip().decode()
  25. if not line:
  26. continue
  27. # Keep the last few lines of output in case the command errors, and we need the output for
  28. # the exception below.
  29. last_lines.append(line)
  30. if len(last_lines) > ERROR_OUTPUT_MAX_LINE_COUNT:
  31. last_lines.pop(0)
  32. logger.log(output_log_level, line)
  33. remaining_output = output_buffer.read().rstrip().decode()
  34. if remaining_output: # pragma: no cover
  35. logger.log(output_log_level, remaining_output)
  36. exit_code = process.poll()
  37. if exit_code_indicates_error(command, exit_code, error_on_warnings):
  38. # If an error occurs, include its output in the raised exception so that we don't
  39. # inadvertently hide error output.
  40. if len(last_lines) == ERROR_OUTPUT_MAX_LINE_COUNT:
  41. last_lines.insert(0, '...')
  42. raise subprocess.CalledProcessError(exit_code, ' '.join(command), '\n'.join(last_lines))
  43. def execute_command(
  44. full_command,
  45. output_log_level=logging.INFO,
  46. output_file=None,
  47. input_file=None,
  48. shell=False,
  49. extra_environment=None,
  50. working_directory=None,
  51. error_on_warnings=False,
  52. ):
  53. '''
  54. Execute the given command (a sequence of command/argument strings) and log its output at the
  55. given log level. If output log level is None, instead capture and return the output. If an
  56. open output file object is given, then write stdout to the file and only log stderr (but only
  57. if an output log level is set). If an open input file object is given, then read stdin from the
  58. file. If shell is True, execute the command within a shell. If an extra environment dict is
  59. given, then use it to augment the current environment, and pass the result into the command. If
  60. a working directory is given, use that as the present working directory when running the
  61. command.
  62. Raise subprocesses.CalledProcessError if an error occurs while running the command.
  63. '''
  64. logger.debug(
  65. ' '.join(full_command)
  66. + (' < {}'.format(input_file.name) if input_file else '')
  67. + (' > {}'.format(output_file.name) if output_file else '')
  68. )
  69. environment = {**os.environ, **extra_environment} if extra_environment else None
  70. if output_log_level is None:
  71. output = subprocess.check_output(
  72. full_command, shell=shell, env=environment, cwd=working_directory
  73. )
  74. return output.decode() if output is not None else None
  75. else:
  76. process = subprocess.Popen(
  77. full_command,
  78. stdin=input_file,
  79. stdout=output_file or subprocess.PIPE,
  80. stderr=subprocess.PIPE if output_file else subprocess.STDOUT,
  81. shell=shell,
  82. env=environment,
  83. cwd=working_directory,
  84. )
  85. log_output(
  86. full_command,
  87. process,
  88. process.stderr if output_file else process.stdout,
  89. output_log_level,
  90. error_on_warnings,
  91. )
  92. def execute_command_without_capture(full_command, working_directory=None, error_on_warnings=False):
  93. '''
  94. Execute the given command (a sequence of command/argument strings), but don't capture or log its
  95. output in any way. This is necessary for commands that monkey with the terminal (e.g. progress
  96. display) or provide interactive prompts.
  97. If a working directory is given, use that as the present working directory when running the
  98. command.
  99. '''
  100. logger.debug(' '.join(full_command))
  101. try:
  102. subprocess.check_call(full_command, cwd=working_directory)
  103. except subprocess.CalledProcessError as error:
  104. if exit_code_indicates_error(full_command, error.returncode, error_on_warnings):
  105. raise