PeerInfo

A simple Python 3 script that parses the output from the getpeerinfo() RPC call. The getpeerinfo() call yields a JSON document that looks like:

getpeerinfo.json
[
    {
        "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)

twc-peerinfo.py
#!/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:

peerinfo.txt
+----+------------------+---------+-----+--------+-------+-------+----------+-------+----------+
|  # |      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”.