#!/usr/bin/python3 # coding: utf-8 # # A simple indicator applet displaying cpu and memory information # # Author: Alex Eftimie <alex@eftimie.ro> # Fork Author: fossfreedom <foss.freedom@gmail.com> # Original Homepage: http://launchpad.net/indicator-sysmonitor # Fork Homepage: https://github.com/fossfreedom/indicator-sysmonitor # License: GPL v3 import json import time from threading import Thread from threading import Event import subprocess import copy import logging import re import os import platform from gettext import gettext as _ from gi.repository import GLib import psutil as ps ps_v1_api = int(ps.__version__.split('.')[0]) <= 1 B_UNITS = ['', 'KB', 'MB', 'GB', 'TB'] cpu_load = [] def bytes_to_human(num, suffix='B'): for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: if abs(num) < 1024.0: return "%3.2f %s%s" % (num, unit, suffix) num /= 1024.0 return "%.2f %s%s" % (num, 'Yi', suffix) class ISMError(Exception): """General exception.""" def __init__(self, msg): Exception.__init__(self, msg) class SensorManager(object): """Singleton""" _instance = None SETTINGS_FILE = os.getenv("HOME") + '/.indicator-sysmonitor.json' digit_regex = re.compile(r'''\d+''') class __impl: settings = { 'custom_text': 'cpu: {cpu} mem: {mem}', 'interval': 2, 'on_startup': False, 'sensors': { # 'name' => (desc, cmd) } } supported_sensors = None def __init__(self): self.sensor_instances = [CPUSensor(), NvGPUSensor(), MemSensor(), NetSensor(), BatSensor(), FSSensor(), SwapSensor(), UporDownSensor(), PublicIPSensor(), CPUTemp(), NvGPUTemp()] for sensor in self.sensor_instances: self.settings['sensors'][sensor.name] = (sensor.desc, sensor.cmd) self._last_net_usage = [0, 0] # (up, down) self._fetcher = None # @staticmethod @classmethod def update_regex(self, names=None): if names is None: names = list(self.settings["sensors"].keys()) reg = '|'.join(names) reg = "\A({})\Z".format(reg) # global supported_sensors self.supported_sensors = re.compile("{}".format(reg)) def get(self, name): """ :param name: of the sensor :return: the sensor instance """ for sensor in self.sensor_instances: if sensor.check(name) is not None: return sensor return None # @staticmethod def exists(self, name): """Checks if the sensor name exists""" return bool(self.supported_sensors.match(name)) # @staticmethod def check(self, sensor_string): for sensor in self.sensor_instances: sensor.check(sensor_string) def add(self, name, desc, cmd): """Adds a custom sensors.""" if self.exists(name): raise ISMError(_("Sensor name already in use.")) self.settings["sensors"][name] = (desc, cmd) self.update_regex() def delete(self, name): """Deletes a custom sensors.""" sensors = self.settings['sensors'] names = list(sensors.keys()) if name not in names: raise ISMError(_("Sensor is not defined.")) _desc, default = sensors[name] if default is True: raise ISMError(_("Can not delete default sensors.")) del sensors[name] self.update_regex() def edit(self, name, newname, desc, cmd): """Edits a custom sensors.""" try: sensors = self.settings['sensors'] _desc, default = sensors[name] except KeyError: raise ISMError(_("Sensor does not exists.")) if default is True: raise ISMError(_("Can not edit default sensors.")) if newname != name: if newname in list(sensors.keys()): raise ISMError(_("Sensor name already in use.")) sensors[newname] = (desc, cmd) del sensors[name] self.settings["custom_text"] = self.settings["custom_text"].replace( name, newname) self.update_regex() def load_settings(self): """It gets the settings from the config file and sets them to the correct vars""" try: with open(SensorManager.SETTINGS_FILE, 'r') as f: cfg = json.load(f) if cfg['custom_text'] is not None: self.settings['custom_text'] = cfg['custom_text'] if cfg['interval'] is not None: self.settings['interval'] = cfg['interval'] if cfg['on_startup'] is not None: self.settings['on_startup'] = cfg['on_startup'] if cfg['sensors'] is not None: # need to merge our current list of sensors with what was previously saved newcopy = self.settings['sensors'] newcopy.update(cfg['sensors']) self.settings['sensors'] = newcopy self.update_regex() except Exception as ex: logging.exception(ex) logging.error('Reading settings failed') def save_settings(self): """It stores the current settings to the config file.""" # TODO: use gsettings try: with open(SensorManager.SETTINGS_FILE, 'w') as f: f.write(json.dumps(self.settings)) except Exception as ex: logging.exception(ex) logging.error('Writing settings failed') def get_guide(self): """Updates the label guide from appindicator.""" # foss - I'm doubtful any of this guide stuff works - this needs to be recoded # each sensor needs a sensor guide data = self._fetcher.fetch() for key in data: if key.startswith('fs'): data[key] = '000gB' break data['mem'] = data['cpu'] = data['bat'] = '000%' data['net'] = '↓666kB/s ↑666kB/s' self.settings['custom_text'].format(**data) return self.settings['custom_text'].format(**data) def get_label(self, data): """It updates the appindicator text with the the values from data""" try: label = self.settings["custom_text"].format(**data) if len(data) \ else _("(no output)") except KeyError as ex: label = _("Invalid Sensor: {}").format(ex) except Exception as ex: logging.exception(ex) label = _("Unknown error: ").format(ex) return label def initiate_fetcher(self, parent): if self._fetcher is not None: self._fetcher.stop() self._fetcher = StatusFetcher(parent) self._fetcher.start() logging.info("Fetcher started") def fill_liststore(self, list_store): sensors = self.settings['sensors'] for name in list(sensors.keys()): list_store.append([name, sensors[name][0]]) def get_command(self, name): cmd = self.settings["sensors"][name][1] return cmd def set_custom_text(self, custom_text): self.settings["custom_text"] = custom_text def get_custom_text(self): return self.settings["custom_text"] def set_interval(self, interval): self.settings["interval"] = interval def get_interval(self): return self.settings["interval"] def get_results(self): """Return a dict whose element are the sensors and their values""" res = {} from preferences import Preferences # We call this only once per update global cpu_load cpu_load = ps.cpu_percent(interval=0, percpu=True) # print (self.settings["custom_text"]) custom_text is the full visible string seen in Preferences edit field for sensor in Preferences.sensors_regex.findall( self.settings["custom_text"]): sensor = sensor[1:-1] instance = self.get(sensor) if instance: value = instance.get_value(sensor) if value: res[sensor] = value else: # custom sensor res[sensor] = BaseSensor.script_exec(self.settings["sensors"][sensor][1]) return res def __init__(self): if SensorManager._instance is None: SensorManager._instance = SensorManager.__impl() # Store instance reference as the only member in the handle self.__dict__['_SensorManager__instance'] = SensorManager._instance def __getattr__(self, attr): """ Delegate access to implementation """ return getattr(self.__instance, attr) def __setattr__(self, attr, value): """ Delegate access to implementation """ return setattr(self.__instance, attr, value) class BaseSensor(object): name = '' desc = '' cmd = True def check(self, sensor): ''' checks to see if the sensor string passed in valid :param sensor: string representation of the sensor :return: True if the sensor is understood and passes the check or an Exception if the format of the sensor string is wrong None is returned if the sensor string is nothing to-do with the Sensor name ''' if sensor == self.name: return True def get_value(self, sensor_data): return None @staticmethod def script_exec(command): """Execute a custom command.""" try: output = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True).communicate()[0].strip() except: output = _("Error") logging.error(_("Error running: {}").format(command)) return output.decode('utf-8') if output else _("(no output)") class NvGPUSensor(BaseSensor): name = 'nvgpu' desc = _('Nvidia GPU utilization') def get_value(self, sensor): if sensor == 'nvgpu': return "{:02.0f}%".format(self._fetch_gpu()) def _fetch_gpu(self, percpu=False): result = subprocess.check_output(['nvidia-smi', '--query-gpu=utilization.gpu', '--format=csv']) perc = result.splitlines()[1] perc = perc[:-2] return int(perc) class NvGPUTemp(BaseSensor): """Return GPU temperature expressed in Celsius """ name = 'nvgputemp' desc = _('Nvidia GPU Temperature') def get_value(self, sensor): # degrees symbol is unicode U+00B0 return "{}\u00B0C".format(self._fetch_gputemp()) def _fetch_gputemp(self): result = subprocess.check_output(['nvidia-smi', '--query-gpu=temperature.gpu', '--format=csv']) perc = result.splitlines()[1] return int(perc) class CPUSensor(BaseSensor): name = 'cpu\d*' desc = _('Average CPU usage') cpus = re.compile("\Acpu\d*\Z") last = None if ps_v1_api: cpu_count = ps.NUM_CPUS else: cpu_count = ps.cpu_count() def check(self, sensor): if self.cpus.match(sensor): if len(sensor) == 3: nber = 0 else: nber = int(sensor[3:]) if len(sensor) > 3 else 999 if nber >= self.cpu_count: print(sensor) print(self.cpu_count) print(len(sensor)) raise ISMError(_("Invalid number of CPUs.")) return True def get_value(self, sensor): if sensor == 'cpu': return "{:02.0f}%".format(self._fetch_cpu()) elif CPUSensor.cpus.match(sensor): cpus = self._fetch_cpu(percpu=True) return "{:02.0f}%".format(cpus[int(sensor[3:])]) return None def _fetch_cpu(self, percpu=False): if percpu: return cpu_load r = 0.0; for i in cpu_load: r += i r /= self.cpu_count return r class MemSensor(BaseSensor): name = 'mem' desc = _('Physical memory in use.') def get_value(self, sensor_data): return '{:02.0f}%'.format(self._fetch_mem()) def _fetch_mem(self): """It gets the total memory info and return the used in percent.""" def grep(pattern, word_list): expr = re.compile(pattern) arr = [elem for elem in word_list if expr.match(elem)] return arr[0] with open('/proc/meminfo') as meminfofile: meminfo = meminfofile.readlines() total = SensorManager.digit_regex.findall(grep("MemTotal", meminfo))[0] release = re.split('\.', platform.release()) major_version = int(release[0]) minor_version = int(re.search(r'\d+', release[1]).group()) if (minor_version >= 16 and major_version == 3) or (major_version > 3): available = SensorManager.digit_regex.findall( grep("MemAvailable", meminfo))[0] return 100 - 100 * int(available) / float(total) else: free = SensorManager.digit_regex.findall( grep("MemFree", meminfo))[0] cached = SensorManager.digit_regex.findall( grep("Cached", meminfo))[0] free = int(free) + int(cached) return 100 - 100 * free / float(total) class NetSensor(BaseSensor): name = 'net' desc = _('Network activity.') _last_net_usage = [0, 0] # (up, down) def get_value(self, sensor_data): return self._fetch_net() def _fetch_net(self): """It returns the bytes sent and received in bytes/second""" current = [0, 0] for _, iostat in list(ps.net_io_counters(pernic=True).items()): current[0] += iostat.bytes_recv current[1] += iostat.bytes_sent dummy = copy.deepcopy(current) current[0] -= self._last_net_usage[0] current[1] -= self._last_net_usage[1] self._last_net_usage = dummy mgr = SensorManager() current[0] /= mgr.get_interval() current[1] /= mgr.get_interval() return '↓ {:>9s}/s ↑ {:>9s}/s'.format(bytes_to_human(current[0]), bytes_to_human(current[1])) class BatSensor(BaseSensor): name = 'bat\d*' desc = _('Battery capacity.') bat = re.compile("\Abat\d*\Z") def check(self, sensor): if self.bat.match(sensor): bat_id = int(sensor[3:]) if len(sensor) > 3 else 0 if not os.path.exists("/sys/class/power_supply/BAT{}".format(bat_id)): raise ISMError(_("Invalid number returned for the Battery sensor.")) return True def get_value(self, sensor): if BatSensor.bat.match(sensor): bat_id = int(sensor[3:]) if len(sensor) > 3 else 0 return '{:02.0f}%'.format(self._fetch_bat(bat_id)) return None def _fetch_bat(self, batid): """Fetch the the amount of remaining battery""" capacity = 0 try: with open("/sys/class/power_supply/BAT{}/capacity".format(batid)) as state: while True: capacity = int(state.readline()) break except IOError: return "N/A" return capacity class FSSensor(BaseSensor): name = 'fs//.+' desc = _('Available space in file system.') def check(self, sensor): if sensor.startswith("fs//"): path = sensor.split("//")[1] if not os.path.exists(path): raise ISMError(_("Path: {} doesn't exists.").format(path)) return True def get_value(self, sensor): if sensor.startswith('fs//'): parts = sensor.split('//') return self._fetch_fs(parts[1]) return None def _fetch_fs(self, mount_point): """It returns the amount of bytes available in the fs in a human-readble format.""" if not os.access(mount_point, os.F_OK): return None stat = os.statvfs(mount_point) bytes_ = stat.f_bavail * stat.f_frsize for unit in B_UNITS: if bytes_ < 1024: return "{} {}".format(round(bytes_, 2), unit) bytes_ /= 1024 class SwapSensor(BaseSensor): name = 'swap' desc = _("Average swap usage") def get_value(self, sensor): return '{:02.0f}%'.format(self._fetch_swap()) def _fetch_swap(self): """Return the swap usage in percent""" usage = 0 total = 0 try: with open("/proc/swaps") as swaps: swaps.readline() for line in swaps.readlines(): dummy, dummy, total_, usage_, dummy = line.split() total += int(total_) usage += int(usage_) if total == 0: return 0 else: return usage * 100 / total except IOError: return "N/A" class UporDownSensor(BaseSensor): name = 'upordown' desc = _("Display if your internet connection is up or down") command = 'if wget -qO /dev/null google.com > /dev/null; then echo "☺"; else echo "☹"; fi' current_val = "" lasttime = 0 # we refresh this every 10 seconds def get_value(self, sensor): if self.current_val == "" or self.lasttime == 0 or (time.time() - self.lasttime) > 10: self.current_val = self.script_exec(self.command) self.lasttime = time.time() return self.current_val class PublicIPSensor(BaseSensor): name = 'publicip' desc = _("Display your public IP address") command = 'curl ipv4.icanhazip.com' current_ip = "" lasttime = 0 # we refresh this every 10 minutes def get_value(self, sensor): if self.current_ip == "" or self.lasttime == 0 or (time.time() - self.lasttime) > 600: self.current_ip = self.script_exec(self.command) self.lasttime = time.time() return self.current_ip class CPUTemp(BaseSensor): """Return CPU temperature expressed in Celsius """ name = 'cputemp' desc = _('CPU temperature') def get_value(self, sensor): # degrees symbol is unicode U+00B0 return "{:02.0f}\u00B0C".format(self._fetch_cputemp()) def _fetch_cputemp(self): # http://www.mjmwired.net/kernel/Documentation/hwmon/sysfs-interface # first try the following sys file # /sys/class/thermal/thermal_zone0/temp # if that fails try various hwmon files cat = lambda file: open(file, 'r').read().strip() ret = None zone = "/sys/class/thermal/thermal_zone0/" try: ret = int(cat(os.path.join(zone, 'temp'))) / 1000 except: pass if ret: return ret base = '/sys/class/hwmon/' ls = sorted(os.listdir(base)) assert ls, "%r is empty" % base for hwmon in ls: hwmon = os.path.join(base, hwmon) try: ret = int(cat(os.path.join(hwmon, 'temp1_input'))) / 1000 break except: pass # if fahrenheit: # digits = [(x * 1.8) + 32 for x in digits] return ret class StatusFetcher(Thread): """It recollects the info about the sensors.""" def __init__(self, parent): Thread.__init__(self) self._parent = parent self.mgr = SensorManager() self.alive = Event() self.alive.set() GLib.timeout_add_seconds(self.mgr.get_interval(), self.run) def fetch(self): return self.mgr.get_results() def stop(self): self.alive.clear() def run(self): data = self.fetch() self._parent.update(data) if self.alive.isSet(): return True