A VCS repository archival tool
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

196 lines
6.1 KiB

  1. import argparse
  2. import contextlib
  3. import datetime
  4. import inspect
  5. import logging
  6. import requests.models
  7. # Imported in parse_args() and main() after setting up the logger:
  8. #import codearchiver.core
  9. #import codearchiver.modules
  10. #import codearchiver.version
  11. import tempfile
  12. ## Logging
  13. dumpLocals = False
  14. logger = logging # Replaced below after setting the logger class
  15. class Logger(logging.Logger):
  16. def _log_with_stack(self, level, *args, **kwargs):
  17. super().log(level, *args, **kwargs)
  18. if dumpLocals:
  19. stack = inspect.stack()
  20. if len(stack) >= 3:
  21. name = _dump_stack_and_locals(stack[2:][::-1])
  22. super().log(level, f'Dumped stack and locals to {name}')
  23. def warning(self, *args, **kwargs):
  24. self._log_with_stack(logging.WARNING, *args, **kwargs)
  25. def error(self, *args, **kwargs):
  26. self._log_with_stack(logging.ERROR, *args, **kwargs)
  27. def critical(self, *args, **kwargs):
  28. self._log_with_stack(logging.CRITICAL, *args, **kwargs)
  29. def log(self, level, *args, **kwargs):
  30. if level >= logging.WARNING:
  31. self._log_with_stack(level, *args, **kwargs)
  32. else:
  33. super().log(level, *args, **kwargs)
  34. def _requests_preparedrequest_repr(name, request):
  35. ret = []
  36. ret.append(repr(request))
  37. ret.append(f'\n {name}.method = {request.method}')
  38. ret.append(f'\n {name}.url = {request.url}')
  39. ret.append(f'\n {name}.headers = \\')
  40. for field in request.headers:
  41. ret.append(f'\n {field} = {_repr("_", request.headers[field])}')
  42. if request.body:
  43. ret.append(f'\n {name}.body = ')
  44. ret.append(_repr('_', request.body).replace('\n', '\n '))
  45. return ''.join(ret)
  46. def _requests_response_repr(name, response, withHistory = True):
  47. ret = []
  48. ret.append(repr(response))
  49. ret.append(f'\n {name}.url = {response.url}')
  50. ret.append(f'\n {name}.request = ')
  51. ret.append(_repr('_', response.request).replace('\n', '\n '))
  52. if withHistory and response.history:
  53. ret.append(f'\n {name}.history = [')
  54. for previousResponse in response.history:
  55. ret.append(f'\n ')
  56. ret.append(_requests_response_repr('_', previousResponse, withHistory = False).replace('\n', '\n '))
  57. ret.append('\n ]')
  58. ret.append(f'\n {name}.status_code = {response.status_code}')
  59. ret.append(f'\n {name}.headers = \\')
  60. for field in response.headers:
  61. ret.append(f'\n {field} = {_repr("_", response.headers[field])}')
  62. ret.append(f'\n {name}.content = {_repr("_", response.content)}')
  63. return ''.join(ret)
  64. def _repr(name, value):
  65. if type(value) is requests.models.Response:
  66. return _requests_response_repr(name, value)
  67. if type(value) is requests.models.PreparedRequest:
  68. return _requests_preparedrequest_repr(name, value)
  69. valueRepr = repr(value)
  70. if '\n' in valueRepr:
  71. return ''.join(['\\\n ', valueRepr.replace('\n', '\n ')])
  72. return valueRepr
  73. @contextlib.contextmanager
  74. def _dump_locals_on_exception():
  75. try:
  76. yield
  77. except Exception as e:
  78. trace = inspect.trace()
  79. if len(trace) >= 2:
  80. name = _dump_stack_and_locals(trace[1:], exc = e)
  81. logger.fatal(f'Dumped stack and locals to {name}')
  82. raise
  83. def _dump_stack_and_locals(trace, exc = None):
  84. with tempfile.NamedTemporaryFile('w', prefix = 'codearchiver_locals_', delete = False) as fp:
  85. if exc is not None:
  86. fp.write('Exception:\n')
  87. fp.write(f' {type(exc).__module__}.{type(exc).__name__}: {exc!s}\n')
  88. fp.write(f' args: {exc.args!r}\n')
  89. fp.write('\n')
  90. fp.write('Stack:\n')
  91. for frameRecord in trace:
  92. fp.write(f' File "{frameRecord.filename}", line {frameRecord.lineno}, in {frameRecord.function}\n')
  93. for line in frameRecord.code_context:
  94. fp.write(f' {line.strip()}\n')
  95. fp.write('\n')
  96. for frameRecord in trace:
  97. module = inspect.getmodule(frameRecord[0])
  98. if not module.__name__.startswith('codearchiver.') and module.__name__ != 'codearchiver':
  99. continue
  100. locals_ = frameRecord[0].f_locals
  101. fp.write(f'Locals from file "{frameRecord.filename}", line {frameRecord.lineno}, in {frameRecord.function}:\n')
  102. for variableName in locals_:
  103. variable = locals_[variableName]
  104. varRepr = _repr(variableName, variable)
  105. fp.write(f' {variableName} {type(variable)} = ')
  106. fp.write(varRepr.replace('\n', '\n '))
  107. fp.write('\n')
  108. fp.write('\n')
  109. if 'self' in locals_ and hasattr(locals_['self'], '__dict__'):
  110. fp.write(f'Object dict:\n')
  111. fp.write(repr(locals_['self'].__dict__))
  112. fp.write('\n\n')
  113. name = fp.name
  114. return name
  115. def parse_args():
  116. import codearchiver.core
  117. import codearchiver.modules
  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. with _dump_locals_on_exception():
  155. inputUrl = codearchiver.core.InputURL(args.url)
  156. module = codearchiver.core.get_module_instance(inputUrl)
  157. module.process()
  158. if __name__ == '__main__':
  159. main()