aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMattias Andrée <maandree@operamail.com>2014-12-08 06:21:12 +0100
committerMattias Andrée <maandree@operamail.com>2014-12-08 06:21:12 +0100
commit41a8fcbd23e38c67c039e04c3e4572ac4a891f42 (patch)
tree2a6486a45d9ecc11441776f06e22136ccd241a5f
parentproper multi-home support in ipaddress (diff)
downloadxpybar-41a8fcbd23e38c67c039e04c3e4572ac4a891f42.tar.gz
xpybar-41a8fcbd23e38c67c039e04c3e4572ac4a891f42.tar.bz2
xpybar-41a8fcbd23e38c67c039e04c3e4572ac4a891f42.tar.xz
add ping monitor1.5
Signed-off-by: Mattias Andrée <maandree@operamail.com>
-rw-r--r--Makefile4
-rw-r--r--TODO1
-rw-r--r--examples/plugins/ping53
-rw-r--r--src/plugins/ping.py190
4 files changed, 245 insertions, 3 deletions
diff --git a/Makefile b/Makefile
index 7cf4f12..a7d90e8 100644
--- a/Makefile
+++ b/Makefile
@@ -32,12 +32,12 @@ PLUGINS = chase clock cpuinfo cpuonline cpu df discstats ipaddress \
kmsg leapsec linereader loadavg lunar mem moc network \
pacman snmp snmp6 softirqs solar uname uptime users \
vmstat weather xdisplay xkb alsa dentrystate inodestate \
- files hdparm tzclock ropty
+ files hdparm tzclock ropty ping
PLUGIN_EXAMPLES = chase clock cpu cpuinfo cpuonline df discstats \
ipaddress kmsg loadavg lunar mem moc network \
pacman uname uptime users xdisplay xkb alsa \
- dentrystate inodestate files tzclock ropty
+ dentrystate inodestate files tzclock ropty ping
EXAMPLES = mixed moderate test xmonad
diff --git a/TODO b/TODO
index 98cc5c7..3ce6734 100644
--- a/TODO
+++ b/TODO
@@ -34,7 +34,6 @@ List of plugins to implement:
/proc/net/sockstat6
/proc/net/wireless
/proc/sysvipc
- ping
Demo plugins:
diff --git a/examples/plugins/ping b/examples/plugins/ping
new file mode 100644
index 0000000..9668b4e
--- /dev/null
+++ b/examples/plugins/ping
@@ -0,0 +1,53 @@
+# -*- python -*-
+
+# A xpybar configuration example testing the features of plugins.ping
+
+import time
+import threading
+
+from plugins.ping import Ping
+from plugins.clock import Clock
+
+
+OUTPUT, HEIGHT, YPOS, TOP = 0, 12, 24, True
+
+
+clock = Clock(sync_to = Clock.SECONDS)
+ping_ = Ping(targets = Ping.get_nics(Ping.DEFAULT_GATEWAY), interval = 2)
+
+start_ = start
+def start():
+ start_()
+ async(lambda : clock.continuous_sync(lambda : bar.invalidate()))
+
+
+def colourise_latency(latency):
+ if latency is None:
+ return '????'
+ colour = '31'
+ if latency < 5: colour = '32'
+ elif latency < 10: colour = '00'
+ elif latency < 20: colour = '33'
+ return '\033[%sm%.2f\033[00m' % (colour, latency)
+
+def status(monitor):
+ monitor.semaphore.acquire()
+ try:
+ latency = monitor.get_latency(True)
+ dropped = monitor.dropped(True)
+ droptime = monitor.dropped_time(True)
+ latency = ' '.join(colourise_latency(latency[i]) for i in (1, 2, 4, 8))
+ if dropped == 0:
+ return latency
+ return '%s \033[31m-%i\033[00m(%is)' % (latency, dropped, droptime)
+ finally:
+ monitor.semaphore.release()
+
+
+def redraw():
+ text = ['%s: %s' % (nic, status(ping_.monitors[nic][0])) for nic in ping_.monitors.keys()]
+ text = ' │ '.join(text)
+
+ bar.clear()
+ bar.draw_coloured_text(0, 10, 0, 2, text)
+
diff --git a/src/plugins/ping.py b/src/plugins/ping.py
new file mode 100644
index 0000000..add8254
--- /dev/null
+++ b/src/plugins/ping.py
@@ -0,0 +1,190 @@
+# -*- python -*-
+'''
+xpybar – xmobar replacement written in python
+Copyright © 2014 Mattias Andrée (maandree@member.fsf.org)
+
+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 WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import os
+
+from util import *
+
+from plugins.linereader import LineReader
+
+
+
+class Ping:
+ '''
+ Network connectivity monitor using ICMP echos
+
+ @variable monitors:dict<str, list<Ping.create_monitor.PingMonitor>>
+ Map from network interface to list of network monitors
+ connected against the server specified by the corresponding
+ element in `targets` of `__init__`
+ '''
+
+ DEFAULT_GATEWAY = ...
+ '''
+ Selects the default gateway's IP address
+ '''
+
+ UNITS = { 'ns' : 0.000001, 'us' : 0.001, 'µs' : 0.001, 'ms' : 1.0, 's' : 1000.0 }
+
+
+ def __init__(self, targets = None, interval = 5, arguments = None, buffer_size = 8):
+ '''
+ Constructor
+
+ @param targets:dict<str, itr<str|...>> Map from network interfaces to servers to ping
+ @param interval:float The pinging interval, in seconds
+ @param arguments:itr<str>? Extra arguments for the `ping` command
+ '''
+ if targets is None:
+ targets = Ping.get_nics()
+ gateways = Ping.get_default_gateways()
+ args = ['ping', '-i', str(interval)] + (arguments if arguments is not None else [])
+ def get(nic, target):
+ if target == Ping.DEFAULT_GATEWAY:
+ target = None if nic not in gateways else gateways[nic]
+ if target is None:
+ return None
+ command = args + ['-I', nic, target]
+ return Ping.create_monitor(command, buffer_size, interval)
+ self.monitors = dict((nic, [get(nic, t) for t in targets[nic]]) for nic in targets.keys())
+
+
+ @staticmethod
+ def get_nics(server = ...):
+ '''
+ List all network interfaces with a default gateway
+ and map them to `server`
+
+ @return :dict<str, [`server`]> Dictionary of network interfaces mapped to `server`
+ '''
+ data = spawn_read('ip', 'route').split('\n')
+ def get(fields):
+ device = fields[fields.index('dev') + 1]
+ return (device, [server])
+ return dict(get(line.split(' ')) for line in data if line.startswith('default via '))
+
+
+ @staticmethod
+ def get_default_gateways():
+ '''
+ All network interfaces with a default gateway mapped to their default gateways
+
+ @return :dict<str, str> Map from network interfaces to their default gateways
+ '''
+ data = spawn_read('ip', 'route').split('\n')
+ def get(fields):
+ gateway = fields[2]
+ device = fields[fields.index('dev') + 1]
+ return (device, gateway)
+ return dict(get(line.split(' ')) for line in data if line.startswith('default via '))
+
+
+ @staticmethod
+ def create_monitor(command, buffer_size, interval):
+ import time
+ class PingMonitor:
+ def __init__(self):
+ self.latency_buffer = [None] * buffer_size
+ self.semaphore = threading.Semaphore()
+ self.last_read = time.monotonic()
+ self.last_icmp_seq = 0
+
+ def start(self):
+ with LineReader(spawn(*command)) as reader:
+ reader.next()
+ while True:
+ line = reader.next()
+ self.semaphore.acquire()
+ try:
+ line = line.replace('=', ' ').split(' ')
+ icmp_seq = int(line[line.index('icmp_seq') + 1])
+ ping_time = float(line[line.index('time') + 1])
+ ping_time *= Ping.UNITS[line[line.index('time') + 2]]
+ dropped_pkgs = icmp_seq - self.last_icmp_seq - 1
+ dropped_pkgs = [None] * dropped_pkgs
+ self.last_icmp_seq = icmp_seq
+ self.latency_buffer[:] = ([ping_time] + dropped_pkgs + self.latency_buffer)[:buffer_size]
+ self.last_read = time.monotonic()
+ except:
+ pass
+ finally:
+ self.semaphore.release()
+
+
+ def get_latency(self, acquired = False):
+ '''
+ Get the average responses-time
+
+ @return :list<float?> For index `n` (limited to `buffer_size` inclusively):
+ the average response time for the latest `n - d` received
+ pongs, where `d` is not number of dropped packages of the
+ latest `n` sent pings. If the average response time cannot
+ be calculated (if all packages have been dropped), the
+ value will be `None`. Note that the value at index 0 will
+ always be `None`, because index 0 means no packages.
+ '''
+ if not acquired:
+ self.semaphore.acquire()
+ try:
+ rc = [None]
+ cursum = 0
+ samples = 0
+ for i in range(buffer_size):
+ if self.latency_buffer[i] is not None:
+ cursum += self.latency_buffer[i]
+ samples += 1
+ rc.append(None if samples == 0 else cursum / samples)
+ return rc
+ finally:
+ if not acquired:
+ self.semaphore.release()
+
+ def dropped(self, acquired = False):
+ '''
+ Estimate how many echos have been dropped since the last pong
+
+ @return :int The estimated number of dropped packages
+ '''
+ if not acquired:
+ self.semaphore.acquire()
+ try:
+ return (time.monotonic() - self.last_read) // interval
+ finally:
+ if not acquired:
+ self.semaphore.release()
+
+ def dropped_time(self, acquired = False):
+ '''
+ Estimate for how long packages have been dropping
+
+ @return :float The estimated time packages have been dropping, in seconds
+ '''
+ if not acquired:
+ self.semaphore.acquire()
+ try:
+ time_diff = time.monotonic() - self.last_read
+ return 0 if time_diff < interval else time_diff
+ finally:
+ if not acquired:
+ self.semaphore.release()
+
+ monitor = PingMonitor()
+ async(monitor.start)
+ return monitor
+