PeerBlocks.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. mmgen_node_tools.PeerBlocks: List blocks in flight, disconnect stalling nodes
  12. """
  13. import asyncio
  14. from collections import namedtuple
  15. from mmgen.util import msg,msg_r,is_int
  16. from mmgen.term import get_term,get_terminal_size,get_char
  17. from mmgen.ui import line_input
  18. from .PollDisplay import PollDisplay
  19. RED,RESET = ('\033[31m','\033[0m')
  20. COLORS = ['\033[38;5;%s;1m' % c for c in list(range(247,256)) + [231]]
  21. ERASE_ALL,CUR_HOME = ('\033[J','\033[H')
  22. CUR_HIDE,CUR_SHOW = ('\033[?25l','\033[?25h')
  23. term = None
  24. class Display(PollDisplay):
  25. poll_secs = 2
  26. def __init__(self,cfg):
  27. super().__init__(cfg)
  28. global term,term_width
  29. if not term:
  30. term = get_term()
  31. term.init(noecho=True)
  32. term_width = self.cfg.columns or get_terminal_size().width
  33. msg_r(CUR_HOME+ERASE_ALL+CUR_HOME)
  34. async def get_info(self,rpc):
  35. return await rpc.call('getpeerinfo')
  36. def display(self,count):
  37. msg_r(
  38. CUR_HOME
  39. + (ERASE_ALL if count == 1 else '')
  40. + 'CONNECTED PEERS ({a}) {b} - poll {c}'.format(
  41. a = len(self.info),
  42. b = self.desc,
  43. c = count ).ljust(term_width)[:term_width]
  44. + '\n'
  45. + ('\n'.join(self.gen_display()) + '\n' if self.info else '')
  46. + ERASE_ALL
  47. + f"Type a peer number to disconnect, 'q' to quit, or any other key for {self.other_desc} display:"
  48. + '\b' )
  49. async def disconnect_node(self,rpc,addr):
  50. return await rpc.call('disconnectnode',addr)
  51. def get_input(self):
  52. s = get_char(immed_chars='q0123456789',prehold_protect=False,num_bytes=1)
  53. if not is_int(s):
  54. return s
  55. with self.info_lock:
  56. msg('')
  57. term.reset()
  58. # readline required for correct operation here; without it, user must re-type first digit
  59. ret = line_input( self.cfg, 'peer number> ', insert_txt=s, hold_protect=False )
  60. term.init(noecho=True)
  61. self.enable_display = False # prevent display from updating before process_input()
  62. return ret
  63. async def process_input(self,rpc):
  64. ids = tuple(str(i['id']) for i in self.info)
  65. ret = False
  66. msg_r(CUR_HIDE)
  67. if self.input in ids:
  68. from mmgen.exception import RPCFailure
  69. addr = self.info[ids.index(self.input)]['addr']
  70. try:
  71. await self.disconnect_node(rpc,addr)
  72. except RPCFailure:
  73. msg_r(f'Unable to disconnect peer {self.input} ({addr})')
  74. else:
  75. msg_r(f'Disconnecting peer {self.input} ({addr})')
  76. await asyncio.sleep(1)
  77. elif self.input and is_int(self.input[0]):
  78. msg_r(f'{self.input}: invalid peer number ')
  79. await asyncio.sleep(0.5)
  80. else:
  81. ret = True
  82. msg_r(CUR_SHOW)
  83. return ret
  84. class BlocksDisplay(Display):
  85. desc = 'Blocks in Flight'
  86. other_desc = 'address'
  87. def gen_display(self):
  88. pd = namedtuple('peer_data',['id','blks_data','blks_width'])
  89. bd = namedtuple('block_datum',['num','disp'])
  90. def gen_block_data():
  91. global min_height
  92. min_height = None
  93. for d in self.info:
  94. if d.get('inflight'):
  95. blocks = d['inflight']
  96. min_height = min(blocks) if not min_height else min(min_height,min(blocks))
  97. line = ' '.join(map(str,blocks))[:blks_field_width]
  98. blocks_disp = line.split()
  99. yield pd(
  100. d['id'],
  101. [bd(blocks[i],blocks_disp[i]) for i in range(len(blocks_disp))],
  102. len(line) )
  103. else:
  104. yield pd(d['id'],[],0)
  105. def gen_line(peer_data):
  106. for blk in peer_data.blks_data:
  107. yield (RED if blk.num == min_height else COLORS[blk.num % 10]) + blk.disp + RESET
  108. id_width = max(2, max(len(str(i['id'])) for i in self.info))
  109. blks_field_width = term_width - 2 - id_width
  110. fs = '{:>%s}: {}' % id_width
  111. # we must iterate through all data to get 'min_height' before calling gen_line():
  112. for peer_data in tuple(gen_block_data()):
  113. yield fs.format(
  114. peer_data.id,
  115. ' '.join(gen_line(peer_data)) + ' ' * (blks_field_width - peer_data.blks_width) )
  116. class PeersDisplay(Display):
  117. desc = 'Address Menu'
  118. other_desc = 'blocks'
  119. def gen_display(self):
  120. id_width = max(2, max(len(str(i['id'])) for i in self.info))
  121. addr_width = max(len(str(i['addr'])) for i in self.info)
  122. for d in self.info:
  123. yield '{a:>{A}}: {b:{B}} {c}'.format(
  124. a = d['id'],
  125. A = id_width,
  126. b = d['addr'],
  127. B = addr_width,
  128. c = d['subver']
  129. ).ljust(term_width)[:term_width]