A simple Python 3 script that parses the output from the getpeerinfo() RPC call. The getpeerinfo() call yields a JSON document that looks like:
[ { "addr" : "200.152.124.103:28333", "services" : "00000001", "lastsend" : 1391707620, "lastrecv" : 1391707647, "bytessent" : 209998, "bytesrecv" : 237591, "conntime" : 1391507320, "version" : 70002, "subver" : "/twisterd:0.9.12/", "inbound" : false, "startingheight" : 22992, "banscore" : 0 }, --- ✂ --- { "addr" : "86.13.244.154:65162", "services" : "00000001", "lastsend" : 1391707647, "lastrecv" : 1391707620, "bytessent" : 78120, "bytesrecv" : 3919, "conntime" : 1391707502, "version" : 70002, "subver" : "/twisterd:0.9.12/", "inbound" : true, "startingheight" : 23302, "banscore" : 0 } ]
(some records have obviously been omitted)
#!/usr/bin/python3.3 # -*- mode: python; coding: utf-8 -*- ############################################################################## # This program is free software; you can redistribute it and/or modify it # # under the terms of the GNU General Public License as published by the Free # # Software Foundation; either version 3 of the License, or (at your option) # # any later version. # # # # This program is distributed in the hope that it will be useful, but with- # # out any warranty; without even the implied warranty of merchantability or # # fitness for a particular purpose. See the GNU General Public License for # # more details. <http://gplv3.fsf.org/> # ############################################################################## __author__ = '@kseistrup (Klaus Alexander Seistrup)' __revision__ = '2014-02-06' __version__ = '0.3.1 (%s)' % __revision__ __copyright__ = """\ twc-peerinfo %s Copyright ©2014 Klaus Alexander Seistrup (@kseistrup) This is free software; see the source for copying conditions. There is no warranty; not even for merchantability or fitness for a particular purpose."""\ % __version__ import sys import os import json import argparse import textwrap # These are native modules and should all be available from sys import (exit, stderr, argv) from datetime import datetime from ipaddress import ip_address from locale import (setlocale, format, LC_ALL) from argparse import ArgumentParser # These, however, need be installed by the user try: from bitcoinrpc.authproxy import AuthServiceProxy from prettytable import PrettyTable except ImportError as error: print(str(error) + '.', file=stderr) exit(1) # end try dummy = setlocale(LC_ALL, '') parser = argparse.ArgumentParser( prog=os.path.basename(argv[0]), formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument( '-v', '--version', action='version', version='%(prog)s/' + __version__, help='print version information and exit' ) parser.add_argument( '-c', '--copying', action='version', version=textwrap.dedent(__copyright__), help='show copying policy and exit' ) args = parser.parse_args() # FIXME: Good for Linux, what about MacOS and Windows? CONF_NAME = os.path.expanduser('~/.twister/twister.conf') conf = dict( connect='localhost', password='pwd', port='28332', ssl='0', user='user' ) def dhms(s): (d, s) = divmod(s+30, 86400) (h, s) = divmod(s, 3600) (m, s) = divmod(s, 60) res = [] if d: res.append('%dd' % d) if h: res.append('%2dh' % h) if m: res.append('%2dm' % m) return ', '.join(res[:2]) or '<1m' def kb(bytes, binary=False): if binary: factor = 1024 else: factor = 1000 return format('%d', int(round(bytes / factor)), grouping=True) try: with open(CONF_NAME, 'r') as fp: for line in fp: # Consider only lines starting with 'rpc' if not line.startswith('rpc'): continue if line.count('=') < 1: continue (key, val) = map(lambda s: s.strip(), line.split('=', 1)) if len(key) < 4 or not key or not val: continue # Skip the 'rpc' part… conf[key[3:]] = val except FileNotFoundError: pass # Use default values if config doesn't exist conf['ssl'] = {'1': 's', 't': 's'}.get(conf.get('ssl', '0'), '') url = 'http%(ssl)s://%(user)s:%(password)s@%(connect)s:%(port)s' % conf twc = AuthServiceProxy(url) info = twc.getpeerinfo() peers = {} for peer in info: # Caveat: untested for IPv6! ip = ip_address((peer['addr'][::-1].split(':', 1)[1][::-1]).strip('[]')) decor = (ip.version, int(ip)) (tx, rx) = (peer['bytessent'], peer['bytesrecv']) lastseen = max(peer['lastsend'], peer['lastrecv']) knownfor = lastseen - peer['conntime'] subver = peer.get('subver', '/???:?.?.?/') mobile = {True: 'M'}.get('android' in subver, ' ') subver = subver.split(':')[1].strip('/') syncnode = {True: '*', False: ' '}[peer.get('syncnode', False)] peers[decor] = ( ip.compressed + syncnode, mobile + subver, {True: 'I', False: 'O'}[peer.get('inbound', False)], format('%d', peer['startingheight'], grouping=True), kb(rx), kb(tx), kb(rx + tx), datetime.fromtimestamp(lastseen).strftime('%H:%M'), dhms(knownfor) ) table = PrettyTable( ['#', 'IP address ', 'Version', 'I/O', 'Height', 'RX kB', 'TX kB', 'Total kB', 'Seen', 'Known'] ) table.align['#'] = 'r' table.align['IP address '] = 'r' table.align['Version'] = 'l' table.align['Height'] = 'r' table.align['RX kB'] = 'r' table.align['TX kB'] = 'r' table.align['Total kB'] = 'r' table.align['Known'] = 'r' seq = 0 for key in sorted(peers.keys()): seq += 1 table.add_row([seq] + list(peers[key])) print(table) exit(0) # eof
The twc-peerinfo script yields an output like this:
+----+------------------+---------+-----+--------+-------+-------+----------+-------+----------+ | # | IP address | Version | I/O | Height | RX kB | TX kB | Total kB | Seen | Known | +----+------------------+---------+-----+--------+-------+-------+----------+-------+----------+ | 1 | 2.220.74.56 | 0.9.13 | I | 23.294 | 8 | 80 | 87 | 18:19 | 48m | | 2 | 5.79.68.137 | 0.9.13 | O | 23.005 | 219 | 198 | 417 | 18:19 | 2d, 4h | | 3 | 24.27.56.215 | 0.9.13 | I | 23.077 | 82 | 216 | 298 | 18:19 | 1d, 13h | | 4 | 24.245.93.223 | 0.9.3 | I | 23.282 | 10 | 80 | 89 | 18:19 | 2h, 52m | | 5 | 37.187.0.83 | 0.9.13 | O | 22.992 | 189 | 140 | 329 | 18:19 | 2d, 7h | | 6 | 37.187.19.215 | 0.9.8 | O | 22.992 | 300 | 171 | 470 | 18:19 | 2d, 7h | | 7 | 46.59.63.57 | 0.9.13 | I | 23.192 | 37 | 135 | 172 | 18:19 | 17h, 40m | | 8 | 46.142.163.186 | 0.9.13 | I | 23.300 | 3 | 76 | 79 | 18:19 | 8m | | 9 | 54.200.158.96 | 0.9.12 | O | 23.096 | 222 | 139 | 360 | 18:19 | 1d, 9h | | 10 | 54.216.111.14 | 0.9.13 | O | 23.109 | 149 | 111 | 260 | 18:19 | 1d, 7h | | 11 | 71.166.37.244 | 0.9.13 | I | 23.194 | 58 | 145 | 203 | 18:19 | 17h, 16m | | 12 | 75.118.24.185 | M0.9.13 | I | 23.165 | 102 | 167 | 269 | 18:19 | 21h, 46m | | 13 | 78.230.218.69 | 0.9.13 | O | 22.992 | 272 | 245 | 517 | 18:19 | 2d, 7h | | 14 | 80.26.96.220 | 0.9.13 | I | 23.238 | 38 | 113 | 152 | 18:19 | 8h, 30m | | 15 | 82.230.220.128 | 0.9.7 | I | 23.187 | 75 | 132 | 207 | 18:19 | 18h, 25m | | 16 | 82.241.251.127 | 0.9.12 | I | 23.229 | 57 | 115 | 173 | 18:19 | 10h, 44m | | 17 | 83.167.48.253 | 0.9.10 | I | 120 | 278 | 151 | 429 | 18:19 | 2d, 6h | | 18 | 84.49.189.86 | 0.9.13 | I | 23.300 | 1 | 76 | 77 | 18:19 | 11m | | 19 | 89.150.170.153 | 0.9.13 | I | 23.182 | 31 | 124 | 155 | 18:19 | 8h, 10m | | 20 | 92.26.222.241 | 0.9.13 | I | 120 | 6 | 78 | 83 | 18:19 | 52m | | 21 | 94.140.52.34 | M0.9.3 | I | 23.285 | 4 | 79 | 83 | 18:19 | 2h, 2m | | 22 | 94.223.87.16 | M0.9.3 | I | 23.300 | 0 | 76 | 76 | 18:19 | 6m | | 23 | 108.247.121.115 | 0.9.13 | I | 23.177 | 100 | 151 | 251 | 18:19 | 19h, 49m | | 24 | 109.69.14.162 | 0.9.9 | I | 23.209 | 57 | 122 | 179 | 18:19 | 13h, 50m | | 25 | 109.77.226.245 | 0.9.13 | I | 23.278 | 5 | 94 | 99 | 18:19 | 4h, 15m | | 26 | 109.139.242.247 | M0.9.13 | I | 23.283 | 8 | 84 | 92 | 18:19 | 2h, 33m | | 27 | 166.137.185.47 | M0.9.13 | I | 120 | 0 | 76 | 76 | 18:19 | 7m | | 28 | 186.26.115.90 | 0.9.13 | I | 23.293 | 2 | 79 | 81 | 18:19 | 22m | | 29 | 188.194.185.165 | 0.9.13 | I | 23.229 | 54 | 97 | 150 | 18:19 | 11h | | 30 | 192.241.235.68 | 0.9.13 | I | 2.000 | 79 | 99 | 178 | 18:19 | 1h, 40m | | 31 | 198.199.87.4 | 0.9.0 | I | 23.039 | 102 | 229 | 332 | 18:19 | 1d, 22h | | 32 | 200.152.124.103* | 0.9.12 | O | 22.992 | 236 | 208 | 444 | 18:19 | 2d, 7h | +----+------------------+---------+-----+--------+-------+-------+----------+-------+----------+
An asterisk behind an IP address shows the current sync node, and an M in front of the version string denotes a mobile client.
PS: The script was put here since I apparently don't have access rights to e.g. “Scripting examples”.