#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# apache-top
# Copyright (C) 2006 Carles Amigó
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Modified 2012-SEP-17 by jacouh@gmail.com
#
from HTMLParser import HTMLParser
from htmlentitydefs import name2codepoint
import operator
import sys
import os
import re
import signal
import urllib2
import socket
import curses
import traceback
import getopt
import time
#
# minimal screen height and width in characters:
#
glngScreenHeightMin = 11
glngScreenWidthMin = 25
#
# key indicating if to fetch hostname using name servers:
#
gblnIp2Hostname = False
#
# star sign process index from 0:
#
glngStarProcessIndex = 0
#
# PID traced:
#
glngPidTraced = -1
#
# IP traced:
#
gstrIpTraced = ""
#
# URL filter regex:
#
gstrUrlFilter = ""
#
# main exit message:
#
gstrExitMessage = ""
#
# shown processes PID and IP lists:
#
glstPidShown = []
glstIpShown = []
class ApacheStatusParser(HTMLParser):
"""
Clase que parseja la sortida del handler server-status de apache
"""
performance_info = 2
scoreboard = 3
proceses = 4
status = 0
store = False # defineix si el contingut s'ha de guardar o no
append = False # defineix si els seguents caracters s'han d'afegir o posar en un altre camp
performance_info_data = []
scoreboard_data = []
proceses_data = []
def __init__(self):
HTMLParser.__init__(self)
self.performance_info_data = []
self.scoreboard_data = []
self.proceses_data = []
self.store = False
self.append = False
self.status = 1
def handle_starttag(self, tag, attrs):
if tag == "b":
return
self.store = False
if self.status <= self.performance_info:
if tag == "dt":
self.store = True
elif self.status <= self.scoreboard:
if tag == "pre":
self.store = True
elif self.status <= self.proceses:
if tag == "tr":
#if len(self.proceses_data[-1]) != 0:
if len (self.proceses_data) == 0:
self.proceses_data.append([])
else:
if len(self.proceses_data[-1]) > 0:
self.proceses_data.append([])
elif tag == "td":
self.store = True
def handle_endtag(self, tag):
if tag == "b":
return
self.store = False
self.append = False
if self.status <= self.performance_info and tag == "dl":
self.status += 1
elif self.status <= self.scoreboard and tag == "pre":
self.status += 1
elif self.status <= self.proceses and tag == "table":
self.status += 1
def handle_data(self,data):
if self.store and data != "\n":
if self.status <= self.performance_info:
self.performance_info_data.append(data.replace("\n",""))
elif self.status <= self.scoreboard:
self.scoreboard_data.append(data.replace("\n",""))
elif self.status <= self.proceses:
if not self.append:
self.proceses_data[-1].append(data.replace("\n",""))
else:
self.proceses_data[-1][-1] += data.replace("\n","")
def handle_charref(self, ref):
self.append = True
self.handle_data("%s;" % ref)
def handle_entityref(self, ref):
self.append = True
#self.handle_data("&%s;" % ref)
self.handle_data(unichr(name2codepoint[ref]))
def eval_data(self):
for process in self.proceses_data:
# PID
try:
process[1] = eval(process[1])
except:
process[1] = 0
# Acc Number of accesses this connection / this child / this slot
process[2] = process[2].split("/")
process[2][0] = eval(process[2][0])
process[2][1] = eval(process[2][1])
process[2][2] = eval(process[2][2])
# M Mode of operation
#pass
# CPU CPU , number of seconds
process[4] = eval(process[4])
# SS Seconds since beginning of most recent request
process[5] = eval(process[5])
# Req Milliseconds required to process most recent request
process[6] = eval(process[6])
# Conn Kilobytes transferred this connection
process[7] = eval(process[7])
# Child Megabytes transferred this child
process[8] = eval(process[8])
# Slot Total megabytes transferred this slot
process[9] = eval(process[9])
def usage(exit = 1):
print main.__doc__
sys.exit(exit)
def set_screen_size_canonical():
height = 24
width = 80
curses.resizeterm(height, width)
return 1
def check_terminal_size():
height = curses.LINES
width = curses.COLS
b2do = False
if(height <= glngScreenHeightMin):
height = 24
b2do = True
if(width <= glngScreenWidthMin):
width = 80
b2do = True
if(b2do):
curses.resizeterm(height, width)
return 1
def clear_screen_below(screen, y):
(height, width) = screen.getmaxyx()
if(y < height):
for i in range(y, height - 1):
screen.addstr(i, 0, width * " ")
return (height - y)
def clear_screen_below_cursor(screen):
(y, x) = screen.getyx()
return clear_screen_below(screen, y)
def getkey_wait(screen):
screen.nodelay(0)
#curses.curs_set(1)
curses.noecho()
screen.keypad(0)
c = screen.getkey()
#curses.noecho()
#curses.curs_set(0)
screen.nodelay(1)
screen.keypad(1)
return c
def getline_wait(screen, y, x, buffer_length):
screen.nodelay(0)
#curses.curs_set(1)
curses.echo()
screen.keypad(0)
strInput = screen.getstr(y, x, buffer_length)
curses.noecho()
#curses.curs_set(0)
screen.nodelay(1)
screen.keypad(1)
return strInput
def print_help(screen, yio):
(height, width) = screen.getmaxyx()
#
# this may cause error if too many lines:
#
topics = [
"\ta\tSwitch between show all processes and show only active processes (default)",
"\tC\tSort by CPU usage",
"\td\tChange interval delay in s",
"\tf\tFilter/Unfilter URL Regex",
"\th or ?\tToggle this help window",
"\tI\tSort by IP",
"\tk\tKill a process to be input by the user on the keyboard",
"\tM\tSort by Mode of operation",
"\tn\tToggle key to use name servers to show visiting IPs or Hostnames",
"\tP\tSort by PID",
"\tp\tPause/Unpause display (freeze/free screen updates)",
"\tq\tExit",
"\tR\tSort by Request",
"\tr\tReverse sort",
"\tS\tSort by Seconds since beginning of most recent request",
"\tt\tTrace/untrace a single PID or IP to be input by the user on the keyboard",
"\tV\tSort by VirtualHost",
"",
"Use DOWN and UP arrow keys to move the star * sign. LEFT to trace the active PID, RIGHT its IP."
]
if(glngPidTraced >= 0):
topics.append("Currently tracing PID " + str(glngPidTraced) + ", press the key t to untrace it.")
elif(gstrIpTraced != ""):
topics.append("Currently tracing IP " + gstrIpTraced + ", press the key t to untrace it.")
elif(gstrUrlFilter != ""):
topics.append("Current URL Filter Regex " + gstrUrlFilter + ", press the key f to cancel the filter.")
y = yio
ymax = height - 1
for topic in topics:
y = y + 1
if(y >= ymax):
break;
screen.addstr(y, 0, topic)
if(y < ymax - 1):
y = y + 2
elif(y < ymax):
y = y + 1
if(y <= ymax):
screen.addstr(y, 0, "Press any key to continue", curses.A_REVERSE)
return 1
def print_status(screen, status):
(height, width) = screen.getmaxyx()
x = len(status)
if(x < width):
x = width - x
else:
x = 0
screen.addstr(2, x, status)
return 1
def print_status_trace(screen):
if(glngPidTraced >= 0):
return print_status(screen, "Tracing PID " + str(glngPidTraced))
elif(gstrIpTraced != ""):
return print_status(screen, "Tracing IP " + gstrIpTraced)
elif(gstrUrlFilter != ""):
return print_status(screen, "URL Filter Regex " + gstrUrlFilter)
else:
return 0
def print_star_char(screen, y, strChar):
if(strChar == " "):
opts = curses.A_NORMAL
else:
opts = curses.A_BOLD
screen.addstr(y, 35, strChar, opts)
return 1
def print_star(screen, y):
return print_star_char(screen, y, "*")
def clear_star(screen, y):
return print_star_char(screen, y, " ")
#
# require_input:
# 0: input not required
# 1: input refreshing delay
# 2: input any key during help
# 3: input PID to kill
# 4: input PID/IP to trace
# 5: URL Filter Regex
#
def print_screen(screen, url, lngInterval):
global gblnIp2Hostname, glngStarProcessIndex, glngPidTraced,\
gstrIpTraced, gstrUrlFilter, gstrExitMessage
global glstPidShown, glstIpShown
screen = stdscr.subwin(0, 0)
screen.nodelay(1)
screen.keypad(1)
end = False
paused = False
sort = 5
message = ""
reverse = True
show_only_active = True
require_input = 0
interval = lngInterval
urlchecked = False
#
# input c is effective during both the help waiting
# and timer looping.
#
while not end:
help_sreen_on = False
keycode = -1
c = ""
try:
data = ApacheStatusParser()
mysocket = urllib2.urlopen(url)
if(not urlchecked):
urlchecked = True
statusdata = mysocket.read()
data.feed(statusdata)
data.eval_data()
#width = curses.tigetnum('cols') or 80
#height = curses.tigetnum('lines') or 24
(height, width) = screen.getmaxyx()
#
# we clear screen only if the user has no time to read during keyboard I/O:
#
if(require_input == 0) or (require_input == 2):
screen.clear()
screen_cleared = True
else:
screen_cleared = False
# imprimim el header
screen.addstr(0 ,0, data.performance_info_data[5].replace("Server uptime: ","Uptime:").replace(" days","d").replace(" day","d").replace(" hours","h").replace(" hour","h").replace(" minutes","m").replace(" minute","m").replace(" seconds","s").replace("second","s") + ", " + data.performance_info_data[3])
screen.addstr(1, 0, data.performance_info_data[7])
screen.addstr(2, 0, data.performance_info_data[8].replace("request","req").replace("second","sec") + ", Active/Idle: " + data.performance_info_data[9].split()[0] + "/" + data.performance_info_data[9].split()[5])
# imprimim el scoreboard
for num in range(0, len(data.scoreboard_data[0]), width):
screen.addstr(4+num/width, 0, data.scoreboard_data[0][num:num+width])
lngmsg = len(message)
# here the screen position to do user trigger I/O:
yio = 5 + num/width
yprocess0 = yio + 2
iprocessmax = height - yprocess0 - 1
if lngmsg > 0:
if(lngmsg < width):
screen.addstr(yio, lngmsg, (width - lngmsg) * " ")
screen.addstr(yio, 0, message, curses.A_BOLD | curses.A_REVERSE)
screen.refresh()
#
# change interval:
#
if require_input == 1:
strInput = getline_wait(screen, yio, lngmsg+1, 5)
if(strInput != ""):
interval = float(strInput)
if interval < 0: interval = lngInterval
#
# send help topics:
#
elif require_input == 2:
print_help(screen, yio)
c = getkey_wait(screen)
clear_screen_below(screen, yio)
lngmsg = 0
help_sreen_on = True
#
# URL Filter Regex:
#
elif require_input == 5:
strInput = getline_wait(screen, yio, lngmsg+1, 50)
gstrUrlFilter = strInput
if(gstrUrlFilter != ""):
try:
re.compile(gstrUrlFilter)
glngPidTraced = -1
gstrIpTraced = ""
show_only_active = False
except:
message = "Ignoring Error Regex: " + gstrUrlFilter
lng = len(message)
if(width > lng):
screen.addstr(yio, lng, (width - lng) * " ")
screen.addstr(yio, 0, message, curses.A_BOLD)
message = ""
gstrUrlFilter = ""
show_only_active = True
pass
else:
show_only_active = True
#
# kill PID:
#
elif require_input == 3:
strInput = getline_wait(screen, yio, lngmsg+1, 5)
if(strInput != ""):
pid = int(strInput)
if pid > 0:
os.kill(pid, signal.SIGKILL)
#
# trace PID or IP:
#
elif require_input == 4:
strInput = getline_wait(screen, yio, lngmsg+1, 30)
if(strInput != ""):
if(strInput.find(".") >= 0):
glngPidTraced = -1
gstrIpTraced = strInput
gstrUrlFilter = ""
show_only_active = False
else:
glngPidTraced = int(strInput)
gstrIpTraced = ""
gstrUrlFilter = ""
if(glngPidTraced >= 0):
show_only_active = False
message = ""
require_input = 0
print_status_trace(screen)
glstPidShown = []
glstIpShown = []
print_proceses(yio + 1, 0, screen, data.proceses_data, columns=[ 1, 3, 5, 4, 11, 10, 12 ], sort=sort, reverse=reverse, width=width, show_only_active=show_only_active )
nprocesses = len(glstPidShown)
if(glngStarProcessIndex >= nprocesses):
glngStarProcessIndex = nprocesses - 1
if(glngStarProcessIndex < 0):
glngStarProcessIndex = 0
if(not screen_cleared):
clear_screen_below_cursor(screen)
print_star(screen, yprocess0 + glngStarProcessIndex)
screen.move(yio, lngmsg)
#screen.hline(2, 1, curses.ACS_HLINE, 77)
#screen.refresh()
#time.sleep(interval)
time_start = time.time()
while True:
#c = ""
if(c == ""):
try:
#c = screen.getkey()
keycode = screen.getch()
c = chr(keycode)
#message = str(keycode) + "=>" + c
except:
pass
#
# function keys:
#
bhasfunckey = False
lngStarProcessIndex0 = glngStarProcessIndex
#
if(keycode == 27):
sys.exit()
elif(keycode == curses.KEY_UP):
bhasfunckey = True
if glngStarProcessIndex > 0:
glngStarProcessIndex = glngStarProcessIndex -1
elif(keycode == curses.KEY_DOWN):
bhasfunckey = True
if glngStarProcessIndex < iprocessmax:
if glngStarProcessIndex < nprocesses - 1:
glngStarProcessIndex = glngStarProcessIndex + 1
else:
glngStarProcessIndex = iprocessmax
elif(keycode == curses.KEY_LEFT):
bhasfunckey = True
if(glngStarProcessIndex < nprocesses):
glngPidTraced = glstPidShown[glngStarProcessIndex]
gstrIpTraced = ""
gstrUrlFilter = ""
message = "Tracing PID " + str(glngPidTraced) + ", press t to untrace it."
show_only_active = True
else:
glngPidTraced = -1
gstrIpTraced = ""
gstrUrlFilter = ""
break
elif(keycode == curses.KEY_RIGHT):
bhasfunckey = True
if(glngStarProcessIndex < nprocesses):
glngPidTraced = -1
gstrIpTraced = glstIpShown[glngStarProcessIndex]
gstrUrlFilter = ""
message = "Tracing IP " + gstrIpTraced + ", press t to untrace it."
show_only_active = True
else:
glngPidTraced = -1
gstrIpTraced = ""
gstrUrlFilter = ""
break
if(bhasfunckey):
c = ""
clear_star(screen, yprocess0 + lngStarProcessIndex0)
print_star(screen, yprocess0 + glngStarProcessIndex)
screen.move(yio, lngmsg)
#
# keyboad normal key:
#
if c == "q":
# Exit
end = True
elif c == "P":
# Sort by PID
sort = 1
message = "Sort by PID"
elif c == "p":
# Paused
if(paused):
paused = False
message = ""
else:
paused = True
if(width < 6):
screen.addstr(yio, 6, (width - 6) * " ")
screen.addstr(yio, 0, "Paused", curses.A_BOLD | curses.A_REVERSE)
elif c == "C":
# Sort by cpu
sort = 4
message = "Sort by CPU usage: " + c
elif c == "S":
# Sort by SS"
sort = 5
message = "Sort by Seconds since beginning of most recent request"
elif c == "V":
# Sort by vhost
sort = 11
message = "Sort by VirtualHost"
elif c == "M":
# Sort by Mode of operation
sort = 3
message = "Sort by Mode of operation"
elif c == "n":
# IP to hostname:
if(gblnIp2Hostname):
gblnIp2Hostname = False
message = "Show IPs"
else:
gblnIp2Hostname = True
message = "Show hostnames"
elif c == "R":
# Sort by request
sort = 12
message = "Sort by Request"
elif c == "I":
# Sort by ip
sort = 10
message = "Sort by IP"
elif c == "d":
# change interval delay in s:
message = "Set new interval delay in s, currently = " + str(interval) + ":"
require_input = 1
elif c == "h" or c == "?":
# send help:
if( not help_sreen_on):
message = "Help for Interactive Commands:"
require_input = 2
elif c == "f":
# URL Filter Regex:
if(gstrUrlFilter != ""):
message = "Current URL Filter Regex: " + gstrUrlFilter + ", type New Filter or [RETURN] to cancel it:"
else:
message = "URL Filter Regex:"
require_input = 5
elif c == "k":
# kill pid:
message = "PID to kill:"
require_input = 3
elif c == "t":
# trace PID/IP:
if(glngPidTraced >= 0):
glngPidTraced = -1
show_only_active = True
elif(gstrIpTraced != ""):
gstrIpTraced = ""
show_only_active = True
else:
message = "PID like 12345 or IP like 192.168.1.1 to trace:"
require_input = 4
elif c == "a":
# Show only active
glngPidTraced = -1
if show_only_active:
show_only_active = False
message = "Show all processes"
else:
show_only_active = True
message = "Show only active processes"
elif c == "r":
# reverse sort
if reverse:
reverse = False
message = "Reversed sorting"
else:
reverse = True
message = "Normal sorting"
time.sleep(0.1)
if(paused):
if(c != ""):
if(c != "p"):
paused = False
c = ""
break
message = ""
require_input = 0
end = False
c = ""
else:
if c != "": break
c = ""
elapsed_time = time.time()-time_start
if elapsed_time > interval: break
except IndexError:
#raise
pass
except (KeyboardInterrupt, SystemExit):
gstrExitMessage = ""
raise
except urllib2.URLError:
if(urlchecked):
pass
else:
gstrExitMessage = "Cannot open URL " + url + "."
raise
except curses.error:
#gstrExitMessage = "Your screen is too small: minimal height: "\
# + str(glngScreenHeightMin) + " raws, width " + str(glngScreenWidthMin) + " columns."
#raise
set_screen_size_canonical()
pass
except:
pass
#raise
def print_proceses(y, x, screen, proceses, columns, sort, reverse, width, show_only_active = True):
header = "PID M SS CPU VHost IP Request"
screen.addstr(y, x, header + (width-len(header)) * " ", curses.A_REVERSE)
n = 1
if sort != None:
for process in sorted(proceses, key=operator.itemgetter(sort), reverse=reverse):
n += print_process(y+n,x,screen,process,columns,show_only_active,width)
else:
for process in proceses:
n += print_process(y+n,x,screen,process,columns,show_only_active,width)
try:
screen.addstr(y+n, x, width * " ")
except:
pass
return n
def print_process(y, x, screen, process, columns, show_only_active, width):
global glstPidShown, glstIpShown
if(glngPidTraced >= 0):
if(process[1] != glngPidTraced):
return 0
elif(gstrIpTraced != ""):
if(process[columns[5]] != gstrIpTraced):
return 0
elif(gstrUrlFilter != ""):
if(not re.search(gstrUrlFilter, process[columns[6]], re.M|re.I)):
return 0
hostname = process[columns[5]]
if(gblnIp2Hostname):
try:
(hostname, aliaslist, ipaddrlist) = socket.gethostbyaddr(hostname)
except:
pass
if not show_only_active or (process[3] != "." and process[3] != "_"):
try:
screen.addstr(y, x, width * " ")
n = x;
screen.addstr(y, n, str(process[columns[0]])) # SS
n = n+ 6
screen.addstr(y, n, process[columns[1]]) # M
n = n+ 2
screen.addstr(y, n, str(process[columns[2]])) # PID
n = n+ 6
cpu = str(process[columns[3]])
if len(cpu.split('.')[1]) < 2:
cpu = cpu + "0"*(2-len(cpu.split('.')[1]))
screen.addstr(y, n+(4-len(cpu)), cpu) # CPU
n = n+ 6
screen.addstr(y, n, str(process[columns[4]])) # VHOST
n = n+ 16
screen.addstr(y, n, hostname) # IP
n = n+ 15
screen.addstr(y, n, " " + str(process[columns[6]])) # REQUEST
glstPidShown.append(process[1])
glstIpShown.append(process[columns[5]])
return 1
except:
return 1
else:
return 0
def main(url, stdscr):
"""Shows the actual status of the Apache web server using the server-status
url. It needs the ExtendedStatus flag
Usage: apache-top.py [-d delay] -u url
-u url Url where apache-status is located
Example: apache-top.py -u http://www.domain.com/server-status
-d delay Refreshing delay in s
Interactive keys:
a Switch between show all processes and show only active processes (default)
C Sort by CPU usage
d Change interval delay in s
f Filter/Unfilter URL
h or ? Toggle this help window
I Sort by IP
k Kill a process to be input by the user on the keyboard
M Sort by Mode of operation
n Toggle key to use name servers to show visiting IPs or Hostnames
P Sort by PID
p Pause/Unpause display (freeze/free screen updates)
q Exit
R Sort by Request
r Reverse sort
S Sort by Seconds since beginning of most recent request
t Trace/untrace a single PID or IP to be input by the user on the keyboard
V Sort by VirtualHost
Use DOWN and UP arrow keys to move the star * sign. LEFT to trace the active PID, RIGHT its IP.
"""
cols = {
"srv": 0,
"pid": 1,
"acc": 2,
"m": 3,
"cpu": 4,
"ss": 5,
"req": 6,
"conn": 7,
"child": 8,
"slot": 9,
"client": 10,
"vhost": 11,
"request": 12
}
try:
print_screen(stdscr,url, lngInterval)
except:
raise
if __name__ == "__main__":
url = None
lngTimeout = 30
lngInterval = 2
socket.setdefaulttimeout(lngTimeout)
try:
opt_list = getopt.getopt(sys.argv[1:], "d:hu:")
except:
usage()
for opt in opt_list[0]:
if opt[0]=="-h":
usage(0)
elif opt[0]=="-u":
url = opt[1]
elif opt[0]=="-d":
lngInterval = eval(opt[1])
else:
usage
if url == None:
print "*** ERROR: Url missing\n"
usage()
try:
# Initialize curses
stdscr=curses.initscr()
# check termial size to be large enough:
check_terminal_size()
# Turn off echoing of keys, and enter cbreak mode,
# where no buffering is performed on keyboard input
curses.noecho()
curses.cbreak()
#curses.curs_set(0)
# In keypad mode, escape sequences for special keys
# (like the cursor keys) will be interpreted and
# a special value like curses.KEY_LEFT will be returned
stdscr.keypad(1)
try:
main(url,stdscr) # Enter the main loop
except:
raise
# Set everything back to normal
#curses.curs_set(1)
stdscr.keypad(0)
curses.echo()
curses.nocbreak()
curses.endwin() # Terminate curses
except:
# In event of error, restore terminal to sane state.
stdscr.keypad(0)
curses.echo()
curses.nocbreak()
curses.endwin()
#traceback.print_exc() # Print the exception
#print "ERROR parsing the data. Please, make sure you are alowed to read the server-status page and you have ExtendedStatus flag activated"
if gstrExitMessage != "":
print gstrExitMessage