#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 
# apache-top
# Copyright (C) 2006  Carles Amigó
# Modified in 2022 by @3G
# Python3 compatibility by @3G
#
# 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.
# 

from html.parser import HTMLParser
import operator
import sys
import urllib.request
import curses
import traceback
import getopt
import time


class ApacheStatusParser(HTMLParser):

    performance_info = 2
    scoreboard = 3
    processes = 4

    status = 0
    hcol = 0

    store = False
    hstore = False
    append = False

    performance_info_data = {}
    scoreboard_data = []
    processes_data = []
    processes_header = {}

    def __init__(self):
        HTMLParser.__init__(self)
        self.performance_info_data = {}
        self.scoreboard_data = []
        self.processes_data = []
        self.processes_header = {}
        self.store = False
        self.hstore = False
        self.hcol = 0
        self.append = False
        self.status = 1

        self.performance_info_data['server_uptime']=""
        self.performance_info_data['restart_time']=""
        self.performance_info_data['cpu_usage']=""
        self.performance_info_data['rps']=""
        self.performance_info_data['active']=""


    def handle_starttag(self, tag, attrs):
        if tag == "b":
            return
        self.store = False
        self.hstore = 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.processes:
            if tag == "tr":
                #if len(self.processes_data[-1]) != 0:
                if len (self.processes_data) == 0:
                    self.processes_data.append([])
                else:
                    if len(self.processes_data[-1]) > 0:
                        self.processes_data.append([])

            elif tag == "td":
                self.store = True

                if self.status <= self.processes:
                    self.processes_data[-1].append("")

            elif tag == "th":
                self.hstore = True

    def handle_endtag(self, tag):
        if tag == "b":
            return
        self.store = False
        self.hstore = 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.processes 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",""))
                if data.lower().find('server uptime: ') != -1:
                    self.performance_info_data['server_uptime']=data.replace("\n","")
                elif data.lower().find('restart time: ') != -1:
                    self.performance_info_data['restart_time']=data.replace("\n","")
                elif data.lower().find('cpu usage: ') != -1:
                    self.performance_info_data['cpu_usage'] = data.replace("\n","")
                elif data.lower().find('requests/sec') != -1:
                    self.performance_info_data['rps'] = data.replace("\n","")
                elif data.lower().find('requests currently being processed') != -1:
                    self.performance_info_data['active'] = data.replace("\n","")
            elif self.status <= self.scoreboard:
                self.scoreboard_data.append(data.replace("\n",""))
            elif self.status <= self.processes:
                if not self.append:
#                    self.processes_data[-1].append(data.replace("\n",""))
                    self.processes_data[-1][-1] = data.replace("\n","")
                else:
                    self.processes_data[-1][-1] += data.replace("\n","")
        elif self.hstore and data != "\n" and data != "":
            if self.status <= self.processes:
                self.processes_header[data.lower().replace("\n","")]=self.hcol
                self.hcol += 1

    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)

    def eval_data(self):
        for process in self.processes_data:
            # 1 PID
            try:
                process[1] = eval(process[1])
            except:
                process[1] = 0
            # 2 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])
            # 3 M Mode of operation
            #pass
            # 4 CPU CPU usage, number of seconds
            process[4] = eval(process[4])
            # 5 SS Seconds since beginning of most recent request
            process[5] = eval(process[5])
#            # 6 Req Milliseconds required to process most recent request
#            process[6] = eval(process[6])
#            # 7,8 Conn Kilobytes transferred this connection
#            process[7] = eval(process[7])
#            # 8,9 Child Megabytes transferred this child
#            process[8] = eval(process[8])
#            # 9,10 Slot Total megabytes transferred this slot
#            process[9] = eval(process[9])

def usage(exit = 1):
    print (main.__doc__)
    sys.exit(exit)


def print_screen(screen, url):

    screen = stdscr.subwin(0, 0)
    screen.nodelay(1)

    end = False
    sort = 5
    message = ""
    reverse = True
    show_only_active = True
    c = ""

    while not end:
        try:
            data = ApacheStatusParser()
            #statusdata = urllib.urlopen(url).read()
            with urllib.request.urlopen(url) as uresponse:
                statusdata = uresponse.read().decode('utf8')
            data.feed(statusdata)
            #data.eval_data()
            #width = curses.tigetnum('cols') or 80
            #height = curses.tigetnum('lines') or 24
            (height, width) = screen.getmaxyx()
            screen.clear()

#            hheader = "Q-exit P-PID C-CPU S-ss V-vhost M-mode R-request I-ip A-all/active"
#            screen.addstr(0,0,hheader + " "*(width-len(hheader)), curses.A_REVERSE)

            screen.addstr(0,0,data.performance_info_data['server_uptime'].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['restart_time'].replace("Restart Time:","Restart:"))
            screen.addstr(1,0,data.performance_info_data['cpu_usage'])
            screen.addstr(2,0,data.performance_info_data['rps'].replace("request","req").replace("second","sec") + ", Active/Idle: " + data.performance_info_data['active'].split()[0] + "/" + data.performance_info_data['active'].split()[5])

            # imprimim el scoreboard
            for num in range(0,len(data.scoreboard_data[0]),width):
                 screen.addstr(4+int(num/width),0, data.scoreboard_data[0][num:num+width])

            if len(message) > 0:
                screen.addstr(5+int(num/width),0,message, curses.A_BOLD | curses.A_REVERSE)
                message = ""

            # PID, M, SS, CPU, Vhost, Client, Request
            print_processes(6+int(num/width),0,screen, data.processes_data, columns=[ data.processes_header["pid"], data.processes_header["m"], data.processes_header["ss"], data.processes_header["cpu"], data.processes_header["vhost"], data.processes_header["client"], data.processes_header["request"] ], sort=int(sort), reverse=reverse, width=int(width), show_only_active=show_only_active )

            #screen.hline(2, 1, curses.ACS_HLINE, 77)
            screen.refresh()
#            time.sleep(2)


            for xw in range(1,20):
                try:
                    c = screen.getkey()
                except:
                    pass
                if (c): break
                time.sleep(0.1)

            if c == "q":
                # quit
                end = True
            elif c == "p":
                sort = data.processes_header["pid"]
                reverse = True
                message = "Sort by PID"
            elif c == "P":
                sort = data.processes_header["pid"]
                reverse = False
                message = "Sort by PID, reverse"
            elif c == "c":
                sort = data.processes_header["cpu"]
                reverse = True
                message = "Sort by CPU usage"
            elif c == "C":
                sort = data.processes_header["cpu"]
                reverse = False
                message = "Sort by CPU usage, reverse"
            elif c == "s":
                sort = data.processes_header["ss"]
                reverse = True
                message = "Sort by Seconds since beginning of most recent request"
            elif c == "S":
                sort = data.processes_header["ss"]
                reverse = False
                message = "Sort by Seconds since beginning of most recent request, reverse"
            elif c == "v":
                sort = data.processes_header["vhost"]
                reverse = False
                message = "Sort by VirtualHost"
            elif c == "V":
                sort = data.processes_header["vhost"]
                reverse = True
                message = "Sort by VirtualHost, reverse"
            elif c == "m":
                sort = data.processes_header["m"]
                reverse = False
                message = "Sort by Mode of operation"
            elif c == "M":
                sort = data.processes_header["m"]
                reverse = True
                message = "Sort by Mode of operation, reverse"
            elif c == "r":
                sort = data.processes_header["request"]
                reverse = False
                message = "Sort by Request"
            elif c == "R":
                sort = data.processes_header["request"]
                reverse = True
                message = "Sort by Request, reverse"
            elif c == "i":
                sort = data.processes_header["client"]
                reverse = False
                message = "Sort by IP"
            elif c == "I":
                sort = data.processes_header["client"]
                reverse = True
                message = "Sort by IP, reverse"
            elif c == "a":
                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":
#                if reverse:
#                    reverse = False
#                    message = "Reversed sorting"
#                else:
#                    reverse = True
#                    message = "Normal sorting"

            c = ""

        except IndexError:
            raise
        except:
            pass


def print_processes(y,x,screen, processes, 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:
        processes.sort(key=operator.itemgetter(sort), reverse=reverse)

#        for process in sorted(processes, key=operator.itemgetter(sort), reverse=reverse):
#            n += print_process(y+n,x,screen,process,columns,show_only_active,width)
#    else:
#        for process in processes:
#            n += print_process(y+n,x,screen,process,columns,show_only_active,width)

    for process in processes:
        n += print_process(y+n,x,screen,process,columns,show_only_active,width)

    try:
        screen.addstr(y+n,x, " "*int(width))
    except:
        pass

def print_process(y,x,screen,process,columns,show_only_active,width):
    if not show_only_active or (process[columns[1]] != "." and process[columns[1]] != "_"):
        try:
            screen.addstr(y,x, " "*width)
            n = x;
            screen.addstr(y,n, str(process[columns[0]])) # PID
            n = n+ 8
            screen.addstr(y,n, process[columns[1]]) # M
            n = n+ 2
            screen.addstr(y,n, str(process[columns[2]])) # SS
            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+ 32
            screen.addstr(y,n, str(process[columns[5]])) # IP

            n = n+ 15
            screen.addstr(y,n, " " + str(process[columns[6]])) # REQUEST
            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 -u url
        -u url    Url where apache-status is located
		  Example: apache-top -u http://www.domain.com/server-status


    Interactive keys:
	Q	Exit
	P	Sort by PID
	C	Sort by CPU usage
	S	Sort by Seconds since beginning of most recent request
	V	Sort by VirtualHost
	M	Sort by Mopde of operation
	R	Sort by Request
	I	Sort by IP
	A	Switch between show all processes and show only active processes (default)

    Reverse sort using <SHIFT> + sort key

    """



    try:
        print_screen(stdscr,url)
    except:
        raise


if __name__ == "__main__":

    url = None

    try:
        opt_list = getopt.getopt(sys.argv[1:], "u:h")
    except:
        usage()

    for opt in opt_list[0]:
        if opt[0]=="-h":
            usage(0)
        elif opt[0]=="-u":
            url = opt[1]
        else:
            usage

    if url == None:
        print ("*** ERROR: URL missing\n")
        usage()

    try:
        # Initialize curses
        stdscr=curses.initscr()
        # 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.
        curses.curs_set(1)
        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")

