view.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. # This file is part of DarkFi (https://dark.fi)
  2. #
  3. # Copyright (C) 2020-2024 Dyne.org foundation
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU Affero General Public License as
  7. # published by the Free Software Foundation, either version 3 of the
  8. # License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU Affero General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Affero General Public License
  16. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. import urwid
  18. import logging
  19. import asyncio
  20. import datetime as dt
  21. from enum import Enum
  22. from src.model import Model
  23. class DnetWidget(urwid.WidgetWrap):
  24. def __init__(self, name, kind):
  25. self.name = name
  26. self.kind = kind
  27. def selectable(self):
  28. return True
  29. def keypress(self, size, key):
  30. return key
  31. def update(self, txt):
  32. super().__init__(txt)
  33. self._w = urwid.AttrWrap(self._w, None)
  34. self._w.focus_attr = 'line'
  35. class NodeState(Enum):
  36. ON = 0
  37. OFF = 1
  38. class Node(DnetWidget):
  39. def __init__(self, name, kind, state):
  40. self.state = state
  41. super().__init__(name, kind)
  42. def set_txt(self):
  43. if self.state == NodeState.OFF:
  44. txt = urwid.Text(f"{self.name} (offline)")
  45. super().update(txt)
  46. else:
  47. txt = urwid.Text(f"{self.name}")
  48. super().update(txt)
  49. class Session(DnetWidget):
  50. def set_txt(self):
  51. txt = urwid.Text(f" {self.kind}")
  52. super().update(txt)
  53. class Slot(DnetWidget):
  54. def set_txt(self, i, addr):
  55. self.i = i
  56. match self.kind:
  57. case "outbound-slot":
  58. self.addr = addr[0]
  59. self.id = addr[1]
  60. txt = urwid.Text(f" {self.i}: {self.addr}")
  61. super().update(txt)
  62. case "spawn-slot":
  63. self.id = addr
  64. txt = urwid.Text(f" {addr}")
  65. super().update(txt)
  66. case "manual-slot" | "seed-slot" | "inbound-slot":
  67. self.addr = addr
  68. txt = urwid.Text(f" {self.addr}")
  69. super().update(txt)
  70. class View():
  71. palette = [
  72. ('body','light gray','default', 'standout'),
  73. ('line','dark cyan','default','standout'),
  74. ]
  75. def __init__(self, model):
  76. self.model = model
  77. self.pile = urwid.Pile([urwid.Text("")])
  78. scroll = urwid.ScrollBar(urwid.Scrollable(self.pile))
  79. rightbox = urwid.LineBox(scroll)
  80. leftbox = urwid.LineBox(scroll)
  81. self.listwalker = urwid.SimpleListWalker([])
  82. self.list = urwid.ListBox(self.listwalker)
  83. leftbox = urwid.LineBox(self.list)
  84. columns = urwid.Columns([leftbox, rightbox], focus_column=0)
  85. self.ui = urwid.Frame(urwid.AttrWrap(columns, 'body'))
  86. self.sessions = set()
  87. self.nodes = set()
  88. self.refresh_needed = False
  89. def add_node(self, name, info, state):
  90. logging.debug(f"Adding node: {name} {info} {state}")
  91. node = Node(name, "node", state)
  92. node.set_txt()
  93. self.nodes.add(name)
  94. self.listwalker.append(node)
  95. self.add_sessions(name, info)
  96. def add_sessions(self, name, info):
  97. for session in ['outbound', 'inbound', 'manual', 'seed']:
  98. if session in info and info[session]:
  99. session_widget = Session(name, session)
  100. session_widget.set_txt()
  101. self.listwalker.append(session_widget)
  102. self.add_slots(name, session, info[session])
  103. def add_slots(self, name, session, slots):
  104. for i, addr in slots.items():
  105. slot = Slot(name, f"{session}-slot")
  106. slot.set_txt(i, addr)
  107. self.listwalker.append(slot)
  108. match session:
  109. case "outbound":
  110. if addr[1] > 0:
  111. self.sessions.add(addr[1])
  112. case "inbound" | "manual " | "seed":
  113. self.sessions.add(i)
  114. def add_lilith(self, name, info, state):
  115. logging.debug(f"Adding lilith: {name} {info} {state}")
  116. node = Node(name, "lilith-node", state)
  117. node.set_txt()
  118. self.nodes.add(name)
  119. self.listwalker.append(node)
  120. if state == NodeState.OFF:
  121. return
  122. else:
  123. for (i, key) in enumerate(info['spawns'].keys()):
  124. slot = Slot(name, "spawn-slot")
  125. slot.set_txt(i, key)
  126. self.listwalker.append(slot)
  127. def update_lilith(self, name, info):
  128. for index, widget in enumerate(self.listwalker):
  129. if isinstance(widget, Node) and widget.name == name:
  130. # Offline node has come online
  131. if widget.state == NodeState.OFF and info:
  132. self.refresh_needed = True
  133. # Online node has gone offline
  134. elif widget.state == NodeState.ON and not info:
  135. self.refresh_needed = True
  136. def update_node(self, name, info):
  137. for index, widget in enumerate(self.listwalker):
  138. if isinstance(widget, Node) and widget.name == name:
  139. # Offline node has come online
  140. if widget.state == NodeState.OFF and info:
  141. self.refresh_needed = True
  142. # Online node has gone offline
  143. elif widget.state == NodeState.ON and not info:
  144. self.refresh_needed = True
  145. else:
  146. widget.set_txt()
  147. return index + 1
  148. return None
  149. def update_slots(self, name, info):
  150. if info is None:
  151. return
  152. for session in ['outbound', 'inbound', 'manual', 'seed']:
  153. if session in info and info[session]:
  154. for i, addr in info[session].items():
  155. self.update_slot(name, session, i, addr)
  156. def update_slot(self, name, session, i, addr):
  157. for index, widget in enumerate(self.listwalker):
  158. if isinstance(widget, Slot) and \
  159. widget.name == name and \
  160. widget.kind == f"{session}-slot" and \
  161. widget.i == i:
  162. key = (f"{widget.name}", f"{widget.i}")
  163. if key in self.model.nodes[widget.name]['event']:
  164. info = self.model.nodes[widget.name]['event'].get(key)
  165. widget.set_txt(i, info)
  166. self.listwalker[index] = widget
  167. break
  168. #-----------------------------------------------------------------
  169. # Render dnet.subscribe_events() RPC call
  170. # Right hand menu only
  171. #-----------------------------------------------------------------
  172. def update_right_box(self):
  173. self.pile.contents.clear()
  174. focus_w = self.list.get_focus()
  175. if focus_w[0] is None:
  176. return
  177. kind = focus_w[0].kind
  178. match kind:
  179. case "outbound":
  180. key = (focus_w[0].name, "outbound")
  181. info = self.model.nodes.get(focus_w[0].name)
  182. if key in info['event']:
  183. ev = info['event'].get(key)
  184. self.pile.contents.append((
  185. urwid.Text(f" {ev}"),
  186. self.pile.options()))
  187. case "outbound-slot" | "inbound-slot" | \
  188. "manual-slot" | "seed-slot":
  189. addr = focus_w[0].addr
  190. name = focus_w[0].name
  191. info = self.model.nodes.get(name)
  192. if addr in info['msgs']:
  193. msg = info['msgs'].get(addr)
  194. for m in msg:
  195. time = m[0]
  196. event = m[1]
  197. msg = m[2]
  198. self.pile.contents.append((urwid.Text(
  199. f"{time}: {event}: {msg}"),
  200. self.pile.options()))
  201. case "spawn-slot":
  202. name = focus_w[0].name
  203. spawn_name = focus_w[0].id
  204. lilith = self.model.liliths.get(name)
  205. spawns = lilith.get('spawns')
  206. info = spawns.get(spawn_name)
  207. if info['urls']:
  208. urls = info['urls']
  209. self.pile.contents.append((urwid.Text(
  210. f"Accept addrs:"),
  211. self.pile.options()))
  212. for url in urls:
  213. self.pile.contents.append((urwid.Text(
  214. f" {url}"),
  215. self.pile.options()))
  216. if info['whitelist']:
  217. whitelist = info['whitelist']
  218. self.pile.contents.append((urwid.Text(
  219. f"Whitelist:"),
  220. self.pile.options()))
  221. for host in whitelist:
  222. self.pile.contents.append((urwid.Text(
  223. f" {host}"),
  224. self.pile.options()))
  225. if info['greylist']:
  226. greylist = info['greylist']
  227. self.pile.contents.append((urwid.Text(
  228. f"Greylist:"),
  229. self.pile.options()))
  230. for host in greylist:
  231. self.pile.contents.append((urwid.Text(
  232. f" {host}"),
  233. self.pile.options()))
  234. if info['goldlist']:
  235. goldlist = info['goldlist']
  236. self.pile.contents.append((urwid.Text(
  237. f"Goldlist:"),
  238. self.pile.options()))
  239. for host in goldlist:
  240. self.pile.contents.append((urwid.Text(
  241. f" {host}"),
  242. self.pile.options()))
  243. def update_node_state(self, info):
  244. if info:
  245. logging.debug(f"update_node_state(): Returning {NodeState.ON}")
  246. return NodeState.ON
  247. else:
  248. logging.debug(f"update_node_state(): Returning {NodeState.OFF}")
  249. return NodeState.OFF
  250. def refresh(self):
  251. logging.debug("Refresh initiated.")
  252. self.listwalker.clear()
  253. self.sessions.clear()
  254. self.nodes.clear()
  255. # Repopulate
  256. for name, info in self.model.nodes.items():
  257. logging.debug(f"refresh nodes(): (nodes) updating node state")
  258. state = self.update_node_state(info)
  259. logging.debug(f"refresh nodes(): {name}, {info}, {state}")
  260. self.add_node(name, info, state)
  261. for name, info in self.model.liliths.items():
  262. logging.debug(f"refresh nodes(): (lilith) updating node state")
  263. state = self.update_node_state(info)
  264. logging.debug(f"refresh liliths(): {name}, {info}, {state}")
  265. self.add_lilith(name, info, state)
  266. logging.debug("Refresh complete.")
  267. async def update_view(self, evloop: asyncio.AbstractEventLoop,
  268. loop: urwid.MainLoop):
  269. while True:
  270. await asyncio.sleep(0.1)
  271. if self.refresh_needed:
  272. self.refresh()
  273. self.refresh_needed = False
  274. else:
  275. for name, info in self.model.nodes.items():
  276. #logging.debug(f"update_view(): found {name}, {info}")
  277. # Check for new nodes or update existing slots.
  278. if name not in self.nodes:
  279. logging.debug(f"update_view(): (node) updating node state")
  280. state = self.update_node_state(info)
  281. self.add_node(name, info, state)
  282. else:
  283. start_index = self.update_node(name, info)
  284. if start_index is not None:
  285. self.update_slots(name, info)
  286. # Check for outbound or inbound connections coming
  287. # online or going offline, which requires a redraw.
  288. if 'outbound' in info:
  289. for i, (addr, id) in info['outbound'].items():
  290. if id > 0 and id not in self.sessions:
  291. logging.debug(f"Outbound {id}, {addr} came online.")
  292. self.refresh_needed = True
  293. break
  294. if 'inbound' in info:
  295. for key, addr in info['inbound'].items():
  296. if key in self.sessions and not addr:
  297. logging.debug(f"Inbound {key} went offline.")
  298. # Delete this key from the model.
  299. del(info['inbound'][f'{key}'])
  300. self.refresh_needed = True
  301. break
  302. if key not in self.sessions:
  303. logging.debug(f"Inbound {key}, {addr} came online.")
  304. self.refresh_needed = True
  305. break
  306. # Check for new lilith nodes.
  307. for name, info in self.model.liliths.items():
  308. if name not in self.nodes:
  309. logging.debug(f"update_view(): (lilith) updating node state")
  310. state = self.update_node_state(info)
  311. self.add_lilith(name, info, state)
  312. else:
  313. self.update_lilith(name, info)
  314. self.update_right_box()
  315. evloop.call_soon(loop.draw_screen)