From 41a8fcbd23e38c67c039e04c3e4572ac4a891f42 Mon Sep 17 00:00:00 2001 From: Mattias Andrée Date: Mon, 8 Dec 2014 06:21:12 +0100 Subject: add ping monitor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Andrée --- Makefile | 4 +- TODO | 1 - examples/plugins/ping | 53 ++++++++++++++ src/plugins/ping.py | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 examples/plugins/ping create mode 100644 src/plugins/ping.py 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 . +''' + +import os + +from util import * + +from plugins.linereader import LineReader + + + +class Ping: + ''' + Network connectivity monitor using ICMP echos + + @variable monitors:dict> + 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> Map from network interfaces to servers to ping + @param interval:float The pinging interval, in seconds + @param arguments:itr? 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 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 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 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 + -- cgit v1.2.3-70-g09d2