Skip to content
Navigation Menu
Toggle navigation
Sign in
In this repository
All GitHub Enterprise
↵
Jump to
↵
No suggested jump to results
In this repository
All GitHub Enterprise
↵
Jump to
↵
In this organization
All GitHub Enterprise
↵
Jump to
↵
In this repository
All GitHub Enterprise
↵
Jump to
↵
Sign in
Reseting focus
You signed in with another tab or window.
Reload
to refresh your session.
You signed out in another tab or window.
Reload
to refresh your session.
You switched accounts on another tab or window.
Reload
to refresh your session.
Dismiss alert
{{ message }}
mariux64
/
mxtools
Public
Notifications
You must be signed in to change notification settings
Fork
0
Star
1
Code
Issues
15
Pull requests
4
Actions
Projects
0
Security
Insights
Additional navigation options
Code
Issues
Pull requests
Actions
Projects
Security
Insights
Files
6795406
applications-defaults
blink
checktrust
clusterd
crashkernel
cronwrap
desktop-files
etc
fon
forensics
forensics
forensicsd
forensicsd.service
forensicsd.timer
mxvmem
hostconfig
kill-exuser
kvm_monitor
libexec_cron
libexec_startup
logrotate
make-automaps
mkmotd
mozilla-launcher
mxgrub
mxmirror
mxmount
mxnetctl
mxproxmox
mxqi
mxraid
mxrouter
mxshadow
mxsnoop
mxstartup
mxvlan
net_qdisc_nfs_low
netlog
nfsdtop
nvidiactl
pdist
pkgadmin
pmirror
prun
put_websafe
serial-log
slowio
unbound
usrlocalpkg
uvpn
vmcontrol
wakeonlan
.gitignore
Makefile
README.md
install.sh
Breadcrumbs
mxtools
/
forensics
/
mxvmem
Blame
Blame
Latest commit
History
History
executable file
·
534 lines (434 loc) · 14.7 KB
Breadcrumbs
mxtools
/
forensics
/
mxvmem
Top
File metadata and controls
Code
Blame
executable file
·
534 lines (434 loc) · 14.7 KB
Raw
#! /usr/bin/env python3 ''' show memory consumption per user The script analyses actual or recorded memory usage on the host where it is run. The default is to read the information live from the /proc directory. When called with the '-c' option, /var/log is searched for the most recent forensics dump. Setting the FORENSICS_STORE environment variable overrides the default directory where the forensics logs are searched. What to do if too much memory is burned on a given host? #> ps -u $USER -o pid,state,lstart,vsz,cmd This helps finding old processes, or one may just end _all_ own sessions on this host: #> killall -u $USER History: 13.10.2022, created, Kreitler 08.05.2024, default to reading the state 'live' ''' import os import re import pwd import sys import time import argparse # # Debug helper # try: # from dumper import dump # except ModuleNotFoundError: # from pprint import pprint as dump # ------------------------------------------ group/summarize data in a tree class Classifier(): ''' Use to group/summarize values into a tree structure. ''' def __init__(self): self.cnt = 0 self.fld = dict() self.val = 0 self._ik = None def __iter__(self): # this is either expensive, stupid, or already present elsewhere in python self._ik = iter(sorted (self.fld.keys())) return self def __next__(self): n = next(self._ik) return (n, self.fld[n]) def cfy(self, fields, val=0): self.cnt += 1 self.val += val if len(fields) > 0: f = fields.pop(0) if f not in self.fld: self.fld[f] = Classifier() self.fld[f].cfy(fields, val) return val def get(self, key): return self.fld[key] def getval(self): return self.val def dump_simple(self, lbl='.', ind=0): print('# %s %-8s %6d ,'%(' '*ind,lbl+':', self.cnt), self.val) ks = sorted(self.fld.keys()) for k in ks: self.fld[k].dump_simple(k, ind+2) pass # ------------------------------------ access to proc data (stored or live) class ProcBuffer(): ''' Reads from /proc, provides an iterator. Keeps ProcStream simple, allows handling /proc the same way as a 'forensics' log. Example usage: pb = ProcBuffer() print(pb.read()) for line in pb: print(line) ''' def __init__(self, pdir='/proc'): self.pdir = pdir self.buf = [] self.bufit = iter(self.buf) def __iter__(self): return self def __next__(self): return next(self.bufit) def readfile(self, fn): lines = 0 try: f = open(fn, errors='backslashreplace') except (FileNotFoundError, PermissionError): # do nothing return 0 for line in f: self.buf.append(fn + ' ' + line.strip()) lines += 1 f.close() return lines def read(self): # get machine info, return number of items for pf in ('uptime', 'meminfo'): self.readfile(self.pdir + '/' + pf) # go for the PIDs for pd in os.scandir(self.pdir): if pd.name.isnumeric() and pd.is_dir(): # isdigit(), isdecimal() -- which one ? for pf in ('stat', 'status'): self.readfile(self.pdir + '/' + pd.name + '/' + pf) return len(self.buf) def get_iter(self): self.bufit = iter(self.buf) return self.bufit class ProcInfoBase(): def __iter__(self): return self # today nobody closes files, but I like to ... def close(self): pass class ProcInfo(ProcInfoBase): ''' Read from /proc .''' def __init__(self, pdir='/proc'): pb = ProcBuffer(pdir) pb.read() self.pbi = pb.get_iter() self.source = pdir def __next__(self): return next(self.pbi) class ProcInfoSaved(ProcInfoBase): # aka forensics ''' Read from forensics file. ''' def __init__(self, logfile): self.file = open(logfile, errors='backslashreplace') self.source = logfile def __next__(self): line = self.file.readline() if not line: self.close() raise StopIteration return line.strip() def close(self): if self.file: self.file.close() self.file = None # ------------------------------------------------------- collect proc data class ProcStreamParser(): ''' state: R is running, S is sleeping, D is sleeping in an uninterruptible wait, Z is zombie, T is traced or stopped, I is idle, and not in the docs ? ''' def __init__(self,procfshandler): self.pfh = procfshandler self.pidrex = re.compile('/proc/(\d+)/(\S+)\s+(.*)') # :xxx: hardcoded '/proc' def parse(self, line): m = self.pidrex.match(line) if not m: if line.startswith('/proc/uptime '): wall_clock = float(line.split()[1]) self.pfh.set_uptime(wall_clock) self.pfh.report_append( '# uptime: %.2f s, %.2f d' % (wall_clock, wall_clock/3600.0/24.0) ) elif line.startswith('/proc/meminfo MemTotal'): memtotal = int(line.split()[2]) self.pfh.set_memtotal(memtotal) self.pfh.report_append( '# memtotal: %d kB, %.2f GB' % (memtotal, memtotal/1024/1024) ) else: pid = int(m.group(1)) pfile = m.group(2) entry = m.group(3) if pfile == 'stat': fields = entry.split() state = fields[2] start_time = float(fields[21])/100.0 # seconds please ... self.pfh.set_info(pid, 'start_time', start_time) elif pfile == 'status': if entry.startswith('VmData:'): val = int(entry.split()[1]) self.pfh.set_info(pid, 'vmdata', val) elif entry.startswith('State:'): # redundant val = entry.split()[1] self.pfh.set_info(pid, 'state', val) elif entry.startswith('Uid:'): val = int(entry.split()[1]) self.pfh.set_info(pid, 'uid', val) # --------------------------------------------------------------- workhorse class ProcFsHandler(): def __init__(self, classifier = Classifier(), age_thresh = 2): self.Cfy = classifier self.store = {} self.usermap = {} self.report = [] self.uptime = -1 self.memtotal = -1 self.age_threshold = age_thresh * 60*60*24 # days self.supr_sys = False # used to suppress system accounts def set_uptime(self, t): self.uptime = t def set_memtotal(self, m): self.memtotal = m def suppress_system_acc(self): self.supr_sys = True def report_append(self, s): self.report.append(s) # fill hash def set_info(self, pid, key, val): if pid not in self.store: self.store[pid] = {} self.store[pid][key] = val def analyze(self): for p in self.store: # --------- resolve_user_names name = '_unknown_' vmdata = 0 state = '' uid = self.store[p]['uid'] if self.supr_sys and (uid < 100 or uid >= 65533): continue if not uid in self.usermap: try: name = pwd.getpwuid(uid).pw_name except KeyError: name = str(uid) self.usermap[uid] = name name = self.usermap[uid] # ------------------- classify state = self.store[p]['state'] if state in 'DZT': state = 'DZT' if 'vmdata' in self.store[p]: vmdata = int(self.store[p]['vmdata']) if (self.uptime - self.store[p]['start_time']) < self.age_threshold: # 'young' ones get a lower case letter state = state.lower() else: continue # nothing to sum up self.Cfy.cfy( [name, state], vmdata) # -------------------------- memory classifier that 'reports' formated bars class ProcMemClassifier(Classifier): keys = ('DZT','S','R','dzt','s','r') def maxMem(self): m = max(k[1].val for k in self) return m def barStrings(self, scmax = 100, width = 80): ''' returns a string array for display: string has 3 components: 'user |X-----| memory' -- name bar amount ''' # 10 for the user, 12 is default width for the number, # 2 for fillers and such, 2 for the border bar_width = width - 10 - 12 - 2 - 2 ret = list() for k in self: user = k[0] mtot = k[1].val bar = ' %-10s' % str(user)[:9] + '|' nused = 0 for key in self.keys: if key in k[1].fld: ks = k[1].fld[key] n = int(ks.val/scmax * bar_width) if n > 0: # todo: mind max clipping! bar += key[0] + '-' * (n-1) nused += n remain = bar_width - nused if remain <= 0: bar += '|' else: bar += '|' + ' ' * (remain-1) + '.' bar += sep_fmt12(k[1].val) ret.append((user, mtot, bar)) return ret def summaryStrings(self): ''' returns a string array for condensed display: string has 3 components: 'user memory states' ''' ret = list() for k in self: states = '' user = k[0] mtot = k[1].val line = ' %-10s' % str(user)[:9] + ':' line += sep_fmt12(k[1].val) for key in self.keys: if key in k[1].fld: states += key[0] line += ' : [' + states + ']' ret.append((user, mtot, line)) return ret def collectusage(self, keys= '', memlimit=0): ret = list() for k in self: memused = 0 for key in keys: if key == 'D': key = 'DZT' # ugly if key == 'd': key = 'dzt' if key in k[1].fld: if k[1].fld[key].val > memlimit: memused += k[1].fld[key].val if memused: ret.append((k[0], memused)) return ret # ------------------------------------------------------------------- tools def register_logs(logdir): ''' Returns forensics-logs ordered by age, newest first. ''' tmp_logrec = [] for log in '00 10 20 30 40 50'.split(): logfile = os.path.join(logdir, 'forensics-%sth_min.log' % log) if os.access(logfile, os.R_OK): mt = os.stat(logfile).st_mtime tmp_logrec.append((logfile,mt)) logs = sorted (tmp_logrec, key=lambda L: L[1], reverse=True) return logs def sep_fmt12(n = 0.0): ''' Gives readable positive numbers of width 12. ''' ret = '%12s' % '{0:,}'.format(n) # new in C: printf("%'.2f", 1234567.89); ??? ret = ret.replace(',','.') if len(ret) > 12: ret = '%12.4g' % n return ret def get_term_size(): ''' Say 'no' to shutil. ''' cols_lines = (80, 24) try: cols_lines = os.get_terminal_size() except (ValueError, OSError): pass return cols_lines def chk_term_size_usable(): ''' Make sure terminal area is large enough, tell user. ''' bail = False min_w = 40 min_h = 12 cols, lines = get_term_size() if cols < min_w: print('# Fatal: Terminal window is not wide enough. Columns needed:', min_w, '( got', cols, ')', file=sys.stderr ) bail = True if lines < min_h: print('# Fatal: Terminal window is not high enough. Lines needed:', min_h, '( got', lines, ')', file=sys.stderr ) bail = True if bail: print('# Will end now ...', file=sys.stderr) return False return True # ---------------------------------------------------------- user interface def handle_args(): ''' Guess? ''' ap = argparse.ArgumentParser(allow_abbrev=True, description = 'Read conserved or live information from the /proc directory, ' + 'and shows memory consumption per user.' ) ap.add_argument('-H', dest='pydoc', help="show documentation", action='store_true') ap.add_argument("-a", dest='allentries', help='print all entries, makes you scroll, helps when piping', action='store_true') ap.add_argument('-c', dest='readconserved', help='read data from a conserved forensics dump', action='store_true') ap.add_argument("-d", dest='logdir', metavar='dir', default=None, help='location of forensics logs (/var/log)') ap.add_argument('-m', dest='memthresh', metavar='percent' , help='threshold for memory usage report (10%%)', default=10.0, type=float) ap.add_argument('-o', dest='no_sysacc', help='omit system accounts from being reported', action='store_true', default=False) ap.add_argument('-q', dest='query', metavar='query' , help='report memory usage for given categories (eg. \'SD\')', default='') ap.add_argument('-s', dest='summary', help='print short summary', action='store_true') ap.add_argument('-t', dest='durationthresh', metavar='days' , help='time in days when a job is considered as old', default=2) ap.add_argument('-v', dest='verbose', help='be a bit more verbose', action='store_true') ap.add_argument('forensicsfile', metavar='file', nargs='?', help='forensics file (defaults to most recent log found)') return ap.parse_args() # ------------------------------------------------------------- ab die post if __name__ == '__main__': args = handle_args() if args.pydoc: import subprocess subprocess.run(('pydoc3', sys.argv[0])) quit() if not chk_term_size_usable(): quit() proc = None if args.forensicsfile: if os.access(args.forensicsfile, os.R_OK): proc = ProcInfoSaved(args.forensicsfile) else: print('# Error: can not read', args.forensicsfile, file=sys.stderr) quit() elif args.readconserved: default_logdir = '/var/log' logdir = None if 'FORENSICS_STORE' in os.environ: logdir = os.environ['FORENSICS_STORE'] if args.logdir: logdir = args.logdir if not logdir: logdir = default_logdir if not os.access(logdir, os.R_OK): print('# Error: can not access', logdir, file=sys.stderr) quit() logs = register_logs(logdir) if not len(logs): print('# Fatal: no logs found in', logdir, file=sys.stderr) quit() proc = ProcInfoSaved(logs[0][0]) else: proc = ProcInfo() if args.verbose: print(' Reading:', "'%s'" % proc.source, '...') pmc = ProcMemClassifier() whs = ProcFsHandler(pmc, float(args.durationthresh)) if args.no_sysacc: whs.suppress_system_acc() psp = ProcStreamParser(whs) for line in proc: psp.parse(line) proc.close() whs.analyze() if args.query: memthresh = args.memthresh keys = args.query result = pmc.collectusage(keys, whs.memtotal*(memthresh/100)) for r in result: print('%s %.1f' % (r[0], r[1]/1e6)) quit() print(' Memory: %.1f Gb available, %.1f Gb in use (%.1f %%)\n' % ( whs.memtotal/1024**2, pmc.getval()/1024**2, 100 * pmc.getval()/whs.memtotal ) ) if args.summary: for i in sorted(pmc.summaryStrings(), key=lambda L: L[1], reverse=True): print(i[2]) else: cols, lines = get_term_size() maxmem_used = pmc.maxMem() print(' USER ', '*** OLD/young processes ***'.center(cols-23) , 'Amount [kb]') mem_bars = pmc.barStrings(maxmem_used, cols) limit = lines-11 if not args.allentries else len(mem_bars) for i in sorted(mem_bars, key=lambda L: L[1], reverse=True)[:limit]: print(i[2]) print('*** R = running, S = sleeping, D = deep sleep, zombie, or debugged ***'.center(cols)) print()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
You can’t perform that action at this time.