A VCS repository archival tool
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.

214 righe
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()