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()