views.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. __package__ = 'abx.archivebox'
  2. import os
  3. import inspect
  4. from pathlib import Path
  5. from typing import Any, List, Dict, cast
  6. from benedict import benedict
  7. from django.http import HttpRequest
  8. from django.utils import timezone
  9. from django.utils.html import format_html, mark_safe
  10. from admin_data_views.typing import TableContext, ItemContext
  11. from admin_data_views.utils import render_with_table_view, render_with_item_view, ItemLink
  12. import abx
  13. import archivebox
  14. from archivebox.config import CONSTANTS
  15. from archivebox.misc.util import parse_date
  16. from machine.models import InstalledBinary
  17. def obj_to_yaml(obj: Any, indent: int=0) -> str:
  18. indent_str = " " * indent
  19. if indent == 0:
  20. indent_str = '\n' # put extra newline between top-level entries
  21. if isinstance(obj, dict):
  22. if not obj:
  23. return "{}"
  24. result = "\n"
  25. for key, value in obj.items():
  26. result += f"{indent_str}{key}:{obj_to_yaml(value, indent + 1)}\n"
  27. return result
  28. elif isinstance(obj, list):
  29. if not obj:
  30. return "[]"
  31. result = "\n"
  32. for item in obj:
  33. result += f"{indent_str}- {obj_to_yaml(item, indent + 1).lstrip()}\n"
  34. return result.rstrip()
  35. elif isinstance(obj, str):
  36. if "\n" in obj:
  37. return f" |\n{indent_str} " + obj.replace("\n", f"\n{indent_str} ")
  38. else:
  39. return f" {obj}"
  40. elif isinstance(obj, (int, float, bool)):
  41. return f" {str(obj)}"
  42. elif callable(obj):
  43. source = '\n'.join(
  44. '' if 'def ' in line else line
  45. for line in inspect.getsource(obj).split('\n')
  46. if line.strip()
  47. ).split('lambda: ')[-1].rstrip(',')
  48. return f" {indent_str} " + source.replace("\n", f"\n{indent_str} ")
  49. else:
  50. return f" {str(obj)}"
  51. @render_with_table_view
  52. def binaries_list_view(request: HttpRequest, **kwargs) -> TableContext:
  53. FLAT_CONFIG = archivebox.pm.hook.get_FLAT_CONFIG()
  54. assert request.user.is_superuser, 'Must be a superuser to view configuration settings.'
  55. rows = {
  56. "Binary Name": [],
  57. "Found Version": [],
  58. "From Plugin": [],
  59. "Provided By": [],
  60. "Found Abspath": [],
  61. "Related Configuration": [],
  62. # "Overrides": [],
  63. # "Description": [],
  64. }
  65. relevant_configs = {
  66. key: val
  67. for key, val in FLAT_CONFIG.items()
  68. if '_BINARY' in key or '_VERSION' in key
  69. }
  70. for plugin_id, plugin in abx.get_all_plugins().items():
  71. plugin = benedict(plugin)
  72. if not hasattr(plugin.plugin, 'get_BINARIES'):
  73. continue
  74. for binary in plugin.plugin.get_BINARIES().values():
  75. try:
  76. installed_binary = InstalledBinary.objects.get_from_db_or_cache(binary)
  77. binary = installed_binary.load_from_db()
  78. except Exception as e:
  79. print(e)
  80. rows['Binary Name'].append(ItemLink(binary.name, key=binary.name))
  81. rows['Found Version'].append(f'✅ {binary.loaded_version}' if binary.loaded_version else '❌ missing')
  82. rows['From Plugin'].append(plugin.package)
  83. rows['Provided By'].append(
  84. ', '.join(
  85. f'[{binprovider.name}]' if binprovider.name == getattr(binary.loaded_binprovider, 'name', None) else binprovider.name
  86. for binprovider in binary.binproviders_supported
  87. if binprovider
  88. )
  89. # binary.loaded_binprovider.name
  90. # if binary.loaded_binprovider else
  91. # ', '.join(getattr(provider, 'name', str(provider)) for provider in binary.binproviders_supported)
  92. )
  93. rows['Found Abspath'].append(str(binary.loaded_abspath or '❌ missing'))
  94. rows['Related Configuration'].append(mark_safe(', '.join(
  95. f'<a href="/admin/environment/config/{config_key}/">{config_key}</a>'
  96. for config_key, config_value in relevant_configs.items()
  97. if str(binary.name).lower().replace('-', '').replace('_', '').replace('ytdlp', 'youtubedl') in config_key.lower()
  98. or config_value.lower().endswith(binary.name.lower())
  99. # or binary.name.lower().replace('-', '').replace('_', '') in str(config_value).lower()
  100. )))
  101. # if not binary.overrides:
  102. # import ipdb; ipdb.set_trace()
  103. # rows['Overrides'].append(str(obj_to_yaml(binary.overrides) or str(binary.overrides))[:200])
  104. # rows['Description'].append(binary.description)
  105. return TableContext(
  106. title="Binaries",
  107. table=rows,
  108. )
  109. @render_with_item_view
  110. def binary_detail_view(request: HttpRequest, key: str, **kwargs) -> ItemContext:
  111. assert request.user and request.user.is_superuser, 'Must be a superuser to view configuration settings.'
  112. binary = None
  113. plugin = None
  114. for plugin_id, plugin in abx.get_all_plugins().items():
  115. try:
  116. for loaded_binary in plugin['hooks'].get_BINARIES().values():
  117. if loaded_binary.name == key:
  118. binary = loaded_binary
  119. plugin = plugin
  120. # break # last write wins
  121. except Exception as e:
  122. print(e)
  123. assert plugin and binary, f'Could not find a binary matching the specified name: {key}'
  124. try:
  125. binary = binary.load()
  126. except Exception as e:
  127. print(e)
  128. return ItemContext(
  129. slug=key,
  130. title=key,
  131. data=[
  132. {
  133. "name": binary.name,
  134. "description": binary.abspath,
  135. "fields": {
  136. 'plugin': plugin['package'],
  137. 'binprovider': binary.loaded_binprovider,
  138. 'abspath': binary.loaded_abspath,
  139. 'version': binary.loaded_version,
  140. 'overrides': obj_to_yaml(binary.overrides),
  141. 'providers': obj_to_yaml(binary.binproviders_supported),
  142. },
  143. "help_texts": {
  144. # TODO
  145. },
  146. },
  147. ],
  148. )
  149. @render_with_table_view
  150. def plugins_list_view(request: HttpRequest, **kwargs) -> TableContext:
  151. assert request.user.is_superuser, 'Must be a superuser to view configuration settings.'
  152. rows = {
  153. "Label": [],
  154. "Version": [],
  155. "Author": [],
  156. "Package": [],
  157. "Source Code": [],
  158. "Config": [],
  159. "Binaries": [],
  160. "Package Managers": [],
  161. # "Search Backends": [],
  162. }
  163. config_colors = {
  164. '_BINARY': '#339',
  165. 'USE_': 'green',
  166. 'SAVE_': 'green',
  167. '_ARGS': '#33e',
  168. 'KEY': 'red',
  169. 'COOKIES': 'red',
  170. 'AUTH': 'red',
  171. 'SECRET': 'red',
  172. 'TOKEN': 'red',
  173. 'PASSWORD': 'red',
  174. 'TIMEOUT': '#533',
  175. 'RETRIES': '#533',
  176. 'MAX': '#533',
  177. 'MIN': '#533',
  178. }
  179. def get_color(key):
  180. for pattern, color in config_colors.items():
  181. if pattern in key:
  182. return color
  183. return 'black'
  184. for plugin_id, plugin in abx.get_all_plugins().items():
  185. plugin.hooks.get_BINPROVIDERS = getattr(plugin.plugin, 'get_BINPROVIDERS', lambda: {})
  186. plugin.hooks.get_BINARIES = getattr(plugin.plugin, 'get_BINARIES', lambda: {})
  187. plugin.hooks.get_CONFIG = getattr(plugin.plugin, 'get_CONFIG', lambda: {})
  188. rows['Label'].append(ItemLink(plugin.label, key=plugin.package))
  189. rows['Version'].append(str(plugin.version))
  190. rows['Author'].append(mark_safe(f'<a href="{plugin.homepage}" target="_blank">{plugin.author}</a>'))
  191. rows['Package'].append(ItemLink(plugin.package, key=plugin.package))
  192. rows['Source Code'].append(format_html('<code>{}</code>', str(plugin.source_code).replace(str(Path('~').expanduser()), '~')))
  193. rows['Config'].append(mark_safe(''.join(
  194. f'<a href="/admin/environment/config/{key}/"><b><code style="color: {get_color(key)};">{key}</code></b>=<code>{value}</code></a><br/>'
  195. for configdict in plugin.hooks.get_CONFIG().values()
  196. for key, value in benedict(configdict).items()
  197. )))
  198. rows['Binaries'].append(mark_safe(', '.join(
  199. f'<a href="/admin/environment/binaries/{binary.name}/"><code>{binary.name}</code></a>'
  200. for binary in plugin.hooks.get_BINARIES().values()
  201. )))
  202. rows['Package Managers'].append(mark_safe(', '.join(
  203. f'<a href="/admin/environment/binproviders/{binprovider.name}/"><code>{binprovider.name}</code></a>'
  204. for binprovider in plugin.hooks.get_BINPROVIDERS().values()
  205. )))
  206. # rows['Search Backends'].append(mark_safe(', '.join(
  207. # f'<a href="/admin/environment/searchbackends/{searchbackend.name}/"><code>{searchbackend.name}</code></a>'
  208. # for searchbackend in plugin.SEARCHBACKENDS.values()
  209. # )))
  210. return TableContext(
  211. title="Installed plugins",
  212. table=rows,
  213. )
  214. @render_with_item_view
  215. def plugin_detail_view(request: HttpRequest, key: str, **kwargs) -> ItemContext:
  216. assert request.user.is_superuser, 'Must be a superuser to view configuration settings.'
  217. plugins = abx.get_all_plugins()
  218. plugin_id = None
  219. for check_plugin_id, loaded_plugin in plugins.items():
  220. if check_plugin_id.split('.')[-1] == key.split('.')[-1]:
  221. plugin_id = check_plugin_id
  222. break
  223. assert plugin_id, f'Could not find a plugin matching the specified name: {key}'
  224. plugin = abx.get_plugin(plugin_id)
  225. return ItemContext(
  226. slug=key,
  227. title=key,
  228. data=[
  229. {
  230. "name": plugin.package,
  231. "description": plugin.label,
  232. "fields": {
  233. "id": plugin.id,
  234. "package": plugin.package,
  235. "label": plugin.label,
  236. "version": plugin.version,
  237. "author": plugin.author,
  238. "homepage": plugin.homepage,
  239. "dependencies": getattr(plugin, 'DEPENDENCIES', []),
  240. "source_code": plugin.source_code,
  241. "hooks": plugin.hooks,
  242. },
  243. "help_texts": {
  244. # TODO
  245. },
  246. },
  247. ],
  248. )
  249. @render_with_table_view
  250. def worker_list_view(request: HttpRequest, **kwargs) -> TableContext:
  251. assert request.user.is_superuser, "Must be a superuser to view configuration settings."
  252. rows = {
  253. "Name": [],
  254. "State": [],
  255. "PID": [],
  256. "Started": [],
  257. "Command": [],
  258. "Logfile": [],
  259. "Exit Status": [],
  260. }
  261. from workers.supervisord_util import get_existing_supervisord_process
  262. supervisor = get_existing_supervisord_process()
  263. if supervisor is None:
  264. return TableContext(
  265. title="No running worker processes",
  266. table=rows,
  267. )
  268. all_config_entries = cast(List[Dict[str, Any]], supervisor.getAllConfigInfo() or [])
  269. all_config = {config["name"]: benedict(config) for config in all_config_entries}
  270. # Add top row for supervisord process manager
  271. rows["Name"].append(ItemLink('supervisord', key='supervisord'))
  272. rows["State"].append(supervisor.getState()['statename'])
  273. rows['PID'].append(str(supervisor.getPID()))
  274. rows["Started"].append('-')
  275. rows["Command"].append('supervisord --configuration=tmp/supervisord.conf')
  276. rows["Logfile"].append(
  277. format_html(
  278. '<a href="/admin/environment/logs/{}/">{}</a>',
  279. 'supervisord',
  280. 'logs/supervisord.log',
  281. )
  282. )
  283. rows['Exit Status'].append('0')
  284. # Add a row for each worker process managed by supervisord
  285. for proc in cast(List[Dict[str, Any]], supervisor.getAllProcessInfo()):
  286. proc = benedict(proc)
  287. # {
  288. # "name": "daphne",
  289. # "group": "daphne",
  290. # "start": 1725933056,
  291. # "stop": 0,
  292. # "now": 1725933438,
  293. # "state": 20,
  294. # "statename": "RUNNING",
  295. # "spawnerr": "",
  296. # "exitstatus": 0,
  297. # "logfile": "logs/server.log",
  298. # "stdout_logfile": "logs/server.log",
  299. # "stderr_logfile": "",
  300. # "pid": 33283,
  301. # "description": "pid 33283, uptime 0:06:22",
  302. # }
  303. rows["Name"].append(ItemLink(proc.name, key=proc.name))
  304. rows["State"].append(proc.statename)
  305. rows['PID'].append(proc.description.replace('pid ', ''))
  306. rows["Started"].append(parse_date(proc.start).strftime("%Y-%m-%d %H:%M:%S") if proc.start else '')
  307. rows["Command"].append(all_config[proc.name].command)
  308. rows["Logfile"].append(
  309. format_html(
  310. '<a href="/admin/environment/logs/{}/">{}</a>',
  311. proc.stdout_logfile.split("/")[-1].split('.')[0],
  312. proc.stdout_logfile,
  313. )
  314. )
  315. rows["Exit Status"].append(str(proc.exitstatus))
  316. return TableContext(
  317. title="Running worker processes",
  318. table=rows,
  319. )
  320. @render_with_item_view
  321. def worker_detail_view(request: HttpRequest, key: str, **kwargs) -> ItemContext:
  322. assert request.user.is_superuser, "Must be a superuser to view configuration settings."
  323. from workers.supervisord_util import get_existing_supervisord_process, get_worker, get_sock_file, CONFIG_FILE_NAME
  324. SOCK_FILE = get_sock_file()
  325. CONFIG_FILE = SOCK_FILE.parent / CONFIG_FILE_NAME
  326. supervisor = get_existing_supervisord_process()
  327. if supervisor is None:
  328. return ItemContext(
  329. slug='none',
  330. title='error: No running supervisord process.',
  331. data=[],
  332. )
  333. all_config = cast(List[Dict[str, Any]], supervisor.getAllConfigInfo() or [])
  334. if key == 'supervisord':
  335. relevant_config = CONFIG_FILE.read_text()
  336. relevant_logs = cast(str, supervisor.readLog(0, 10_000_000))
  337. start_ts = [line for line in relevant_logs.split("\n") if "RPC interface 'supervisor' initialized" in line][-1].split(",", 1)[0]
  338. uptime = str(timezone.now() - parse_date(start_ts)).split(".")[0]
  339. proc = benedict(
  340. {
  341. "name": "supervisord",
  342. "pid": supervisor.getPID(),
  343. "statename": supervisor.getState()["statename"],
  344. "start": start_ts,
  345. "stop": None,
  346. "exitstatus": "",
  347. "stdout_logfile": "logs/supervisord.log",
  348. "description": f'pid 000, uptime {uptime}',
  349. }
  350. )
  351. else:
  352. proc = benedict(get_worker(supervisor, key) or {})
  353. relevant_config = [config for config in all_config if config['name'] == key][0]
  354. relevant_logs = supervisor.tailProcessStdoutLog(key, 0, 10_000_000)[0]
  355. return ItemContext(
  356. slug=key,
  357. title=key,
  358. data=[
  359. {
  360. "name": key,
  361. "description": key,
  362. "fields": {
  363. "Command": proc.name,
  364. "PID": proc.pid,
  365. "State": proc.statename,
  366. "Started": parse_date(proc.start).strftime("%Y-%m-%d %H:%M:%S") if proc.start else "",
  367. "Stopped": parse_date(proc.stop).strftime("%Y-%m-%d %H:%M:%S") if proc.stop else "",
  368. "Exit Status": str(proc.exitstatus),
  369. "Logfile": proc.stdout_logfile,
  370. "Uptime": (proc.description or "").split("uptime ", 1)[-1],
  371. "Config": relevant_config,
  372. "Logs": relevant_logs,
  373. },
  374. "help_texts": {"Uptime": "How long the process has been running ([days:]hours:minutes:seconds)"},
  375. },
  376. ],
  377. )
  378. @render_with_table_view
  379. def log_list_view(request: HttpRequest, **kwargs) -> TableContext:
  380. assert request.user.is_superuser, "Must be a superuser to view configuration settings."
  381. log_files = CONSTANTS.LOGS_DIR.glob("*.log")
  382. log_files = sorted(log_files, key=os.path.getmtime)[::-1]
  383. rows = {
  384. "Name": [],
  385. "Last Updated": [],
  386. "Size": [],
  387. "Most Recent Lines": [],
  388. }
  389. # Add a row for each worker process managed by supervisord
  390. for logfile in log_files:
  391. st = logfile.stat()
  392. rows["Name"].append(ItemLink("logs" + str(logfile).rsplit("/logs", 1)[-1], key=logfile.name))
  393. rows["Last Updated"].append(parse_date(st.st_mtime).strftime("%Y-%m-%d %H:%M:%S"))
  394. rows["Size"].append(f'{st.st_size//1000} kb')
  395. with open(logfile, 'rb') as f:
  396. try:
  397. f.seek(-1024, os.SEEK_END)
  398. except OSError:
  399. f.seek(0)
  400. last_lines = f.read().decode('utf-8', errors='replace').split("\n")
  401. non_empty_lines = [line for line in last_lines if line.strip()]
  402. rows["Most Recent Lines"].append(non_empty_lines[-1])
  403. return TableContext(
  404. title="Debug Log files",
  405. table=rows,
  406. )
  407. @render_with_item_view
  408. def log_detail_view(request: HttpRequest, key: str, **kwargs) -> ItemContext:
  409. assert request.user.is_superuser, "Must be a superuser to view configuration settings."
  410. log_file = [logfile for logfile in CONSTANTS.LOGS_DIR.glob('*.log') if key in logfile.name][0]
  411. log_text = log_file.read_text()
  412. log_stat = log_file.stat()
  413. return ItemContext(
  414. slug=key,
  415. title=key,
  416. data=[
  417. {
  418. "name": key,
  419. "description": key,
  420. "fields": {
  421. "Path": str(log_file),
  422. "Size": f"{log_stat.st_size//1000} kb",
  423. "Last Updated": parse_date(log_stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
  424. "Tail": "\n".join(log_text[-10_000:].split("\n")[-20:]),
  425. "Full Log": log_text,
  426. },
  427. },
  428. ],
  429. )