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.

214 lines
6.8 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. # Undocumented option to write one line for each artefact filename produced by this process to FD 3.
  124. parser.add_argument('--write-artefacts-fd-3', dest = 'writeArtefactsFd3', action = 'store_true', help = argparse.SUPPRESS)
  125. parser.add_argument('url', help = 'Target URL')
  126. args = parser.parse_args()
  127. return args
  128. def setup_logging():
  129. logging.setLoggerClass(Logger)
  130. global _logger
  131. _logger = logging.getLogger(__name__)
  132. def configure_logging(verbosity, dumpLocals_):
  133. global dumpLocals
  134. dumpLocals = dumpLocals_
  135. rootLogger = logging.getLogger()
  136. # Set level
  137. if verbosity > 0:
  138. level = logging.INFO if verbosity == 1 else logging.DEBUG
  139. rootLogger.setLevel(level)
  140. for handler in rootLogger.handlers:
  141. handler.setLevel(level)
  142. # Create formatter
  143. formatter = logging.Formatter('{asctime}.{msecs:03.0f} {levelname} {name} {message}', datefmt = '%Y-%m-%d %H:%M:%S', style = '{')
  144. # Remove existing handlers
  145. for handler in rootLogger.handlers:
  146. rootLogger.removeHandler(handler)
  147. # Add stream handler
  148. handler = logging.StreamHandler()
  149. handler.setFormatter(formatter)
  150. rootLogger.addHandler(handler)
  151. def main():
  152. setup_logging()
  153. args = parse_args()
  154. configure_logging(args.verbosity, args.dumpLocals)
  155. import codearchiver.core
  156. import codearchiver.modules
  157. import codearchiver.storage
  158. with _dump_locals_on_exception():
  159. inputUrl = codearchiver.core.InputURL(args.url)
  160. if args.writeArtefactsFd3:
  161. artefactsFd = os.fdopen(3, 'w')
  162. storage = codearchiver.storage.DirectoryStorage(os.getcwd())
  163. module = codearchiver.core.get_module_instance(inputUrl, storage = storage)
  164. with tempfile.TemporaryDirectory(prefix = 'tmp.codearchiver.', dir = os.getcwd()) as td:
  165. _logger.debug(f'Running in {td}')
  166. os.chdir(td)
  167. try:
  168. result = module.process()
  169. finally:
  170. os.chdir('..')
  171. if args.writeArtefactsFd3:
  172. with storage.lock():
  173. artefacts = storage.list_new_files()
  174. for filename in artefacts:
  175. print(filename, file = artefactsFd)
  176. if __name__ == '__main__':
  177. main()