cpu_mon.py - sites - public wiki contents of suckless.org
 (HTM) git clone git://git.suckless.org/sites
 (DIR) Log
 (DIR) Files
 (DIR) Refs
       ---
       cpu_mon.py (8598B)
       ---
            1 #!/usr/bin/python
            2 # coding: utf-8
            3 """
            4 GENMON APPLET USAGE EXAMPLE
            5 
            6 A Custom CPU monitor.
            7 
            8 Challenge: We want a bit of history and cpu monitoring anyway needs some delta t.
            9 
           10 So we run this as daemon.
           11 It keeps writing cpu infos to file -> start me in autostart.sh
           12 
           13 Hightlights:
           14 - cpu per core
           15 - top cpu eating process, customized with own config (not colliding with yours of ~/.config/procps)
           16 - arbitray long symbol lists, will pick per percent
           17 - output colorized using pango
           18 
           19 Lowlights:
           20 Linux Only. Not hard to rewrite, but now it's just Linux, looking into /proc/stat
           21 For BSD check the suckless' slstatus regarding how to derive load.
           22 
           23 Usage:
           24 Start this tool e.g. in autostart.sh and leave running.
           25 In genmon use `cat $HOME/.dwm/out.cpu_mon` for the single shot tray icon command
           26 """
           27 
           28 import os, sys, psutil, time, subprocess as sp
           29 import time
           30 
           31 here = os.path.abspath((os.path.dirname(__file__)))
           32 
           33 # ------------------------------------------------------------------------------ config
           34 col_norm = '#a093c7'
           35 col_high = '#bf568b'
           36 
           37 # we run top whenever a core if over this:
           38 th_cpu_min_to_snapshot_top = 20
           39 # we show proc names whenever its utilizaition is over this:
           40 th_cpu_min_to_show_procs = 80
           41 # 0: show always (>0: no space taken for core)
           42 th_min_cpu_show_core = 0
           43 show_max_procs = 3
           44 th_color_high_cpu = 80
           45 top_output_max_lines = 20
           46 
           47 top_rc_dir = here + '/.config/procps'
           48 top = 'HOME="%s" top -b -1 -n 1 -w 56 | head -n %s' % (here, top_output_max_lines)
           49 Sensors = ['cpu']
           50 # Sensors = ['time']
           51 bars = ' ▁▂▃▄▅▆▇'
           52 # -------------------------------------------------------------------------- end config
           53 
           54 
           55 # configure the panel item to cat this file:
           56 fn_out = here + '/out.cpu_mon'
           57 
           58 
           59 # maybe we want arrows stuff some day:
           60 Traffic100 = 1024  # bytes
           61 # arr_downs = ' 🢓↓⬇ﰬ🡇'
           62 arr_downs = ' ↓⬇ﰬ🡇'
           63 arr_ups = ' ↑⬆🡅'
           64 
           65 s = []
           66 CPUs = psutil.cpu_count()
           67 # normal way to read load: read /proc/stat
           68 ctx = {'proc_stat': [0 for i in range(CPUs)], 'traffic': [0, 0], 'fifo': None}
           69 
           70 
           71 bar_intv = 100.0 / len(bars)
           72 arr_downs_intv = 100.0 / len(arr_downs)
           73 arr_ups_intv = 100.0 / len(arr_ups)
           74 arrows = [[arr_downs, arr_downs_intv], [arr_ups, arr_ups_intv]]
           75 
           76 # delivers the *cummulated* load values - per cpu.
           77 # A difference of 100 within 1 sec means: fully loaded
           78 proc_stat = '/proc/stat'
           79 
           80 
           81 run_top = lambda: os.popen(top).read()
           82 
           83 
           84 def cmd_colmn():
           85     # cache the position of the COMMAND column, we need it all the time
           86     n = 'cpu_top_cmd_col'
           87     c = ctx.get(n)
           88     if not c:
           89         t = ctx['cpu_top']
           90         c = ctx[n] = len(t.split(' COMMAND', 1)[0].rsplit('\n', 1)[1])
           91     return c
           92 
           93 
           94 def add_top_cpu_eaters(r, count, cpu):
           95     """Get the <count> top most cpu eating procs names as shown by top"""
           96     # TODO: import re would not hurt
           97     t = ctx['cpu_top']
           98     p = t.split('COMMAND', 1)[1].split('\n', 1 + count)
           99     colmn = cmd_colmn()
          100     for i in range(count, 0, -1):
          101         if cpu[i - 1] < th_cpu_min_to_show_procs:
          102             continue
          103         # P =  p)[nr].split()[11:])
          104         r.insert(0, '%s ' % p[i][colmn:].replace(' ', '')[:10])
          105 
          106 
          107 class sensors:
          108     def cpu():
          109         r = []
          110         l = ctx.pop('cpu_top', 0)
          111         if l:
          112             ctx['cpu_top_old'] = l
          113             ctx['cpu_top_old_ts'] = time.time()
          114         with open(proc_stat) as fd:
          115             t = fd.read()
          116         o = ctx['proc_stat']
          117         h = []
          118         for i in range(CPUs):
          119             v, t = t.split('cpu%s ' % i, 1)[1].split('\n', 1)
          120             v = int(v.split(' ', 1)[0])
          121             d = min(v - o[i], 99.9)
          122             o[i] = v
          123             # print(i, d, file=sys.stderr)
          124             h.append(d)
          125         h = list(reversed(sorted(h)))
          126         # show top process:
          127         if h[0] > th_cpu_min_to_snapshot_top:  # 20
          128             ctx['cpu_top'] = run_top()  # for hover tip - only when there is activity
          129             if h[0] > th_cpu_min_to_show_procs:
          130                 add_top_cpu_eaters(r, show_max_procs, h)  # for status bar
          131         ctx['col_cpu'] = col_high if h[0] > th_color_high_cpu else col_norm
          132         v = lambda d: '' if d < th_min_cpu_show_core else bars[int(d / bar_intv)]
          133         [r.append(v(d)) for d in h]
          134         return ''.join(r)
          135 
          136 
          137 # These would be other sensors - but for those we take the original ones from XFCE4:
          138 #     def time():
          139 #         t = time.ctime().split()
          140 #         t.pop(1)  #  month
          141 #         t.pop()
          142 #         return ' '.join(t)
          143 
          144 #     def mem():
          145 #         return '%s' % psutil.virtual_memory().percent
          146 
          147 #     def traffic():
          148 #         r = []
          149 #         o = ctx['traffic']
          150 #         h = psutil.net_io_counters(pernic=False)
          151 #         v = [h.bytes_sent, h.bytes_recv]
          152 #         print('')
          153 #         for i in range(2):
          154 #             d = 100 * (min((v[i] - o[i]), Traffic100 - 1) / Traffic100)
          155 #             # print('%s\t%s' % (v[i] - o[i], d))
          156 #             o[i] = v[i]
          157 #             arrs, arr_int = arrows[i]
          158 #             col = '\x04' if i == 0 else '\x03'
          159 #             s = arrs[int(d / arr_int)]
          160 #             r.append('%s%s' % (col, s))
          161 #         return ''.join(r)
          162 
          163 #     def battery():
          164 #         B = ''
          165 #         P = 'ﮤ'
          166 #         d = psutil.sensors_battery()
          167 #         d, pp = int(d.percent), d.power_plugged
          168 #         p = '\x02' + P[0] if pp else '\x04' + P[1]
          169 #         s = B[int(min(d, 99) / (100 / len(B)))]
          170 #         if d < 30:
          171 #             s = '\x04' + s
          172 #         if d < 60:
          173 #             s = '\x03' + s
          174 #         else:
          175 #             s = '\x02' + s
          176 #         if d > 90 and pp:
          177 #             return ''
          178 #         return s + ' ' + p + ' '
          179 
          180 
          181 # for dwm's status bar (old version, caused high cpu):
          182 # def xsetroot(sl):
          183 #     if os.system('xsetroot -name "%s"' % sl):
          184 #         print('exitting status.py')
          185 #         sys.exit(1)
          186 
          187 
          188 def to_stdout(sl):
          189     sl = '<txt><span fgcolor="%s">%s</span></txt>' % (ctx['col_cpu'], sl)
          190     t = ctx.get('cpu_top')
          191     if not t:
          192         t = ctx.get('cpu_top_old')
          193         if t:
          194             t = '%s Seconds Ago:\n' % (int(time.time() - ctx['cpu_top_old_ts'])) + t
          195 
          196     if t:
          197         sl += '<tool><span font_family="monospace">%s</span></tool>' % t
          198     print(sl)
          199     fd = ctx['fd_out']
          200     fd.seek(0)
          201     fd.write(sl)
          202     fd.flush()
          203 
          204 
          205 def main():
          206     ctx['fd_out'] = open(fn_out, 'w')
          207     out = to_stdout
          208     while True:
          209         s.clear()
          210         for w in Sensors:
          211             k = getattr(sensors, w)()
          212             s.append('%s ' % k)
          213         sl = ''.join(s)
          214         r = os.popen('ls -lta --color=always').read()
          215         out(sl)
          216         time.sleep(1)  # other values: load calc must be adapted.
          217 
          218 
          219 # Follows the top config - you cant use CLI flags for this.
          220 # Created by: `HOME=~/.dwm top` -> F (select fields) -> W (write toprc)
          221 # then:
          222 # cat .dwm/.config/procps/toprc | base64  >> $HOME/bin/cpu_mon.py (is binary)
          223 top_cfg = '''
          224 dG9wJ3MgQ29uZmlnIEZpbGUgKExpbnV4IHByb2Nlc3NlcyB3aXRoIHdpbmRvd3MpCklkOmosIE1v
          225 ZGVfYWx0c2NyPTAsIE1vZGVfaXJpeHBzPTEsIERlbGF5X3RpbWU9My4wLCBDdXJ3aW49MApEZWYJ
          226 ZmllbGRzY3VyPaWmqDO0Oz1AxLe6OcUnKSorLC0uLzAxMjU2ODw+P0FCQ0ZHSElKS0xNTk9QUVJT
          227 VFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6Cgl3aW5mbGFncz0xNjEwNzYs
          228 IHNvcnRpbmR4PTE4LCBtYXh0YXNrcz0wLCBncmFwaF9jcHVzPTAsIGdyYXBoX21lbXM9MCwgZG91
          229 YmxlX3VwPTAsIGNvbWJpbmVfY3B1cz0wCglzdW1tY2xyPTEsIG1zZ3NjbHI9MSwgaGVhZGNscj0z
          230 LCB0YXNrY2xyPTEKSm9iCWZpZWxkc2N1cj2lprm3uiiztMS7vUA8p8UpKissLS4vMDEyNTY4Pj9B
          231 QkNGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5egoJ
          232 d2luZmxhZ3M9MTkzODQ0LCBzb3J0aW5keD0wLCBtYXh0YXNrcz0wLCBncmFwaF9jcHVzPTAsIGdy
          233 YXBoX21lbXM9MCwgZG91YmxlX3VwPTAsIGNvbWJpbmVfY3B1cz0wCglzdW1tY2xyPTYsIG1zZ3Nj
          234 bHI9NiwgaGVhZGNscj03LCB0YXNrY2xyPTYKTWVtCWZpZWxkc2N1cj2lurs8vb6/wMFNQk7DRDM0
          235 t8UmJygpKissLS4vMDEyNTY4OUZHSElKS0xPUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xt
          236 bm9wcXJzdHV2d3h5egoJd2luZmxhZ3M9MTkzODQ0LCBzb3J0aW5keD0yMSwgbWF4dGFza3M9MCwg
          237 Z3JhcGhfY3B1cz0wLCBncmFwaF9tZW1zPTAsIGRvdWJsZV91cD0wLCBjb21iaW5lX2NwdXM9MAoJ
          238 c3VtbWNscj01LCBtc2dzY2xyPTUsIGhlYWRjbHI9NCwgdGFza2Nscj01ClVzcglmaWVsZHNjdXI9
          239 paanqKqwube6xMUpKywtLi8xMjM0NTY4Ozw9Pj9AQUJDRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xd
          240 Xl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoKCXdpbmZsYWdzPTE5Mzg0NCwgc29ydGluZHg9
          241 MywgbWF4dGFza3M9MCwgZ3JhcGhfY3B1cz0wLCBncmFwaF9tZW1zPTAsIGRvdWJsZV91cD0wLCBj
          242 b21iaW5lX2NwdXM9MAoJc3VtbWNscj0zLCBtc2dzY2xyPTMsIGhlYWRjbHI9MiwgdGFza2Nscj0z
          243 CkZpeGVkX3dpZGVzdD0wLCBTdW1tX21zY2FsZT0xLCBUYXNrX21zY2FsZT0wLCBaZXJvX3N1cHBy
          244 ZXNzPTAK
          245 '''.strip()
          246 
          247 
          248 import base64
          249 
          250 
          251 def write_top_cfg():
          252     os.makedirs(top_rc_dir, exist_ok=True)
          253     with open(top_rc_dir + '/toprc', 'wb') as fd:
          254         fd.write(base64.standard_b64decode(top_cfg))
          255 
          256 
          257 if __name__ == '__main__':
          258     write_top_cfg()
          259     try:
          260         main()
          261     finally:
          262         ctx['fd_out'].close()