A VCS repository archival tool
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

205 рядки
6.4 KiB

  1. import argparse
  2. import contextlib
  3. import datetime
  4. import inspect
  5. import logging
  6. import os
  7. import requests.models
  8. # Imported in parse_args() and main() after setting up the logger:
  9. #import codearchiver.core
  10. #import codearchiver.modules
  11. #import codearchiver.storage
  12. #import codearchiver.version
  13. import tempfile
  14. ## Logging
  15. dumpLocals = False
  16. _logger = logging # Replaced below after setting the logger class
  17. class Logger(logging.Logger):
  18. def _log_with_stack(self, level, *args, **kwargs):
  19. super().log(level, *args, **kwargs)
  20. if dumpLocals:
  21. stack = inspect.stack()
  22. if len(stack) >= 3:
  23. name = _dump_stack_and_locals(stack[2:][::-1])
  24. super().log(level, f'Dumped stack and locals to {name}')
  25. def warning(self, *args, **kwargs):
  26. self._log_with_stack(logging.WARNING, *args, **kwargs)
  27. def error(self, *args, **kwargs):
  28. self._log_with_stack(logging.ERROR, *args, **kwargs)
  29. def critical(self, *args, **kwargs):
  30. self._log_with_stack(logging.CRITICAL, *args, **kwargs)
  31. def log(self, level, *args, **kwargs):
  32. if level >= logging.WARNING:
  33. self._log_with_stack(level, *args, **kwargs)
  34. else:
  35. super().log(level, *args, **kwargs)
  36. def _requests_preparedrequest_repr(name, request):
  37. ret = []
  38. ret.append(repr(request))
  39. ret.append(f'\n {name}.method = {request.method}')
  40. ret.append(f'\n {name}.url = {request.url}')
  41. ret.append(f'\n {name}.headers = \\')
  42. for field in request.headers:
  43. ret.append(f'\n {field} = {_repr("_", request.headers[field])}')
  44. if request.body:
  45. ret.append(f'\n {name}.body = ')
  46. ret.append(_repr('_', request.body).replace('\n', '\n '))
  47. return ''.join(ret)
  48. def _requests_response_repr(name, response, withHistory = True):
  49. ret = []
  50. ret.append(repr(response))
  51. ret.append(f'\n {name}.url = {response.url}')
  52. ret.append(f'\n {name}.request = ')
  53. ret.append(_repr('_', response.request).replace('\n', '\n '))
  54. if withHistory and response.history:
  55. ret.append(f'\n {name}.history = [')
  56. for previousResponse in response.history:
  57. ret.append(f'\n ')
  58. ret.append(_requests_response_repr('_', previousResponse, withHistory = False).replace('\n', '\n '))
  59. ret.append('\n ]')
  60. ret.append(f'\n {name}.status_code = {response.status_code}')
  61. ret.append(f'\n {name}.headers = \\')
  62. for field in response.headers:
  63. ret.append(f'\n {field} = {_repr("_", response.headers[field])}')
  64. ret.append(f'\n {name}.content = {_repr("_", response.content)}')
  65. return ''.join(ret)
  66. def _repr(name, value):
  67. if type(value) is requests.models.Response:
  68. return _requests_response_repr(name, value)
  69. if type(value) is requests.models.PreparedRequest:
  70. return _requests_preparedrequest_repr(name, value)
  71. valueRepr = repr(value)
  72. if '\n' in valueRepr:
  73. return ''.join(['\\\n ', valueRepr.replace('\n', '\n ')])
  74. return valueRepr
  75. @contextlib.contextmanager
  76. def _dump_locals_on_exception():
  77. try:
  78. yield
  79. except Exception as e:
  80. trace = inspect.trace()
  81. if len(trace) >= 2:
  82. name = _dump_stack_and_locals(trace[1:], exc = e)
  83. _logger.fatal(f'Dumped stack and locals to {name}')
  84. raise
  85. def _dump_stack_and_locals(trace, exc = None):
  86. with tempfile.NamedTemporaryFile('w', prefix = 'codearchiver_locals_', delete = False) as fp:
  87. if exc is not None:
  88. fp.write('Exception:\n')
  89. fp.write(f' {type(exc).__module__}.{type(exc).__name__}: {exc!s}\n')
  90. fp.write(f' args: {exc.args!r}\n')
  91. fp.write('\n')
  92. fp.write('Stack:\n')
  93. for frameRecord in trace:
  94. fp.write(f' File "{frameRecord.filename}", line {frameRecord.lineno}, in {frameRecord.function}\n')
  95. for line in frameRecord.code_context:
  96. fp.write(f' {line.strip()}\n')
  97. fp.write('\n')
  98. for frameRecord in trace:
  99. module = inspect.getmodule(frameRecord[0])
  100. if not module.__name__.startswith('codearchiver.') and module.__name__ != 'codearchiver':
  101. continue
  102. locals_ = frameRecord[0].f_locals
  103. fp.write(f'Locals from file "{frameRecord.filename}", line {frameRecord.lineno}, in {frameRecord.function}:\n')
  104. for variableName in locals_:
  105. variable = locals_[variableName]
  106. varRepr = _repr(variableName, variable)
  107. fp.write(f' {variableName} {type(variable)} = ')
  108. fp.write(varRepr.replace('\n', '\n '))
  109. fp.write('\n')
  110. fp.write('\n')
  111. if 'self' in locals_ and hasattr(locals_['self'], '__dict__'):
  112. fp.write(f'Object dict:\n')
  113. fp.write(repr(locals_['self'].__dict__))
  114. fp.write('\n\n')
  115. name = fp.name
  116. return name
  117. def parse_args():
  118. import codearchiver.version
  119. parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter)
  120. parser.add_argument('--version', action = 'version', version = f'codearchiver {codearchiver.version.__version__}')
  121. parser.add_argument('-v', '--verbose', '--verbosity', dest = 'verbosity', action = 'count', default = 0, help = 'Increase output verbosity')
  122. parser.add_argument('--dump-locals', dest = 'dumpLocals', action = 'store_true', default = False, help = 'Dump local variables on serious log messages (warnings or higher)')
  123. parser.add_argument('url', help = 'Target URL')
  124. args = parser.parse_args()
  125. return args
  126. def setup_logging():
  127. logging.setLoggerClass(Logger)
  128. global _logger
  129. _logger = logging.getLogger(__name__)
  130. def configure_logging(verbosity, dumpLocals_):
  131. global dumpLocals
  132. dumpLocals = dumpLocals_
  133. rootLogger = logging.getLogger()
  134. # Set level
  135. if verbosity > 0:
  136. level = logging.INFO if verbosity == 1 else logging.DEBUG
  137. rootLogger.setLevel(level)
  138. for handler in rootLogger.handlers:
  139. handler.setLevel(level)
  140. # Create formatter
  141. formatter = logging.Formatter('{asctime}.{msecs:03.0f} {levelname} {name} {message}', datefmt = '%Y-%m-%d %H:%M:%S', style = '{')
  142. # Remove existing handlers
  143. for handler in rootLogger.handlers:
  144. rootLogger.removeHandler(handler)
  145. # Add stream handler
  146. handler = logging.StreamHandler()
  147. handler.setFormatter(formatter)
  148. rootLogger.addHandler(handler)
  149. def main():
  150. setup_logging()
  151. args = parse_args()
  152. configure_logging(args.verbosity, args.dumpLocals)
  153. import codearchiver.core
  154. import codearchiver.modules
  155. import codearchiver.storage
  156. with _dump_locals_on_exception():
  157. inputUrl = codearchiver.core.InputURL(args.url)
  158. storage = codearchiver.storage.DirectoryStorage(os.getcwd())
  159. module = codearchiver.core.get_module_instance(inputUrl, storage = storage)
  160. with tempfile.TemporaryDirectory(prefix = 'tmp.codearchiver.', dir = os.getcwd()) as td:
  161. _logger.debug(f'Running in {td}')
  162. os.chdir(td)
  163. try:
  164. result = module.process()
  165. finally:
  166. os.chdir('..')
  167. if __name__ == '__main__':
  168. main()