#!/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