mitogen/examples/mitop.py

180 lines
4.4 KiB
Python

import curses
import subprocess
import sys
import time
import mitogen.core
import mitogen.master
import mitogen.utils
class Host(object):
name = None
context = None
recv = None
def __init__(self):
self.procs = {} #: pid -> Process()
class Process(object):
host = None
user = None
pid = None
ppid = None
pgid = None
command = None
rss = None
pcpu = None
rss = None
@mitogen.core.takes_router
def remote_main(context_id, handle, delay, router):
context = mitogen.core.Context(router, context_id)
sender = mitogen.core.Sender(context, handle)
args = ['ps', '-axwwo', 'user,pid,ppid,pgid,%cpu,rss,command']
while True:
sender.put(subprocess.check_output(args))
time.sleep(delay)
def parse_output(host, s):
prev_pids = set(host.procs)
for line in s.splitlines()[1:]:
bits = line.split(None, 6)
pid = int(bits[1])
new = pid not in prev_pids
prev_pids.discard(pid)
try:
proc = host.procs[pid]
except KeyError:
host.procs[pid] = proc = Process()
proc.hostname = host.name
proc.new = new
proc.user = bits[0]
proc.pid = pid
proc.ppid = int(bits[2])
proc.pgid = int(bits[3])
proc.pcpu = float(bits[4])
proc.rss = int(bits[5]) / 1024
proc.command = bits[6]
# These PIDs had no update, so probably they are dead now.
for pid in prev_pids:
del host.procs[pid]
class Painter(object):
def __init__(self, hosts):
self.stdscr = curses.initscr()
curses.start_color()
self.height, self.width = self.stdscr.getmaxyx()
curses.cbreak()
curses.noecho()
self.stdscr.keypad(1)
self.hosts = hosts
self.format = (
'%(hostname)10.10s '
'%(pid)7.7s '
'%(ppid)7.7s '
'%(pcpu)6.6s '
'%(rss)5.5s '
'%(command)20s'
)
def close(self):
curses.endwin()
def paint(self):
self.stdscr.erase()
self.stdscr.addstr(0, 0, time.ctime())
all_procs = []
for host in self.hosts:
all_procs.extend(host.procs.itervalues())
all_procs.sort(key=(lambda proc: -proc.pcpu))
self.stdscr.addstr(1, 0, self.format % {
'hostname': 'HOST',
'pid': 'PID',
'ppid': 'PPID',
'pcpu': '%CPU',
'rss': 'RSS',
'command': 'COMMAND',
})
for i, proc in enumerate(all_procs):
if (i+3) >= self.height:
break
if proc.new:
self.stdscr.attron(curses.A_BOLD)
else:
self.stdscr.attroff(curses.A_BOLD)
self.stdscr.addstr(2+i, 0, self.format % dict(
vars(proc),
command=proc.command[:self.width-36]
))
self.stdscr.refresh()
def local_main(painter, router, select, delay):
next_paint = 0
while True:
recv, (msg, data) = select.get()
parse_output(recv.host, data)
if next_paint < time.time():
next_paint = time.time() + delay
painter.paint()
def main(router, argv):
mitogen.utils.log_to_file()
if not len(argv):
print 'mitop: Need a list of SSH hosts to connect to.'
sys.exit(1)
delay = 2.0
select = mitogen.master.Select(oneshot=False)
hosts = []
for hostname in argv:
print 'Starting on', hostname
host = Host()
host.name = hostname
if host.name == 'localhost':
host.context = router.local()
else:
host.context = router.ssh(hostname=host.name)
host.recv = mitogen.core.Receiver(router)
host.recv.host = host
select.add(host.recv)
call_recv = host.context.call_async(remote_main,
mitogen.context_id, host.recv.handle, delay)
# Adding call_recv to the select will cause CallError to be thrown by
# .get() if startup in the context fails, halt local_main() and cause
# the exception to be printed.
select.add(call_recv)
hosts.append(host)
painter = Painter(hosts)
try:
try:
local_main(painter, router, select, delay)
except KeyboardInterrupt:
pass
finally:
painter.close()
if __name__ == '__main__':
mitogen.utils.run_with_router(main, sys.argv[1:])