#! /usr/bin/env python
#
# Copyright (C) 2005 BULL SA.
# Written by Guillaume Thouvenin <guillaume.thouvenin@bull.net>
#
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#


"""
This program reads a file written by 'acct' and/or by 'jobd'. Depending
of the options, it dumps information about accounting, jobs or per-group
of processes. Currently you can do only one action by one but it's for 
readability
"""

import getopt
import os
import sys
import struct

__revision__ = "$Revision: 1.18 $"

class ElsaParser:
    """
    This class implements a method to go through files used in the
    Enhanced Linux System Accounting project and store informations
    in a dictionary that is returned.
    """
   
    # Constructor
    def __init__(self, filename, size, fmt, key):
        """
        Method invokes for the newly-created class instance.

	@self: instance of the class
	@filename: name of the file that contains data
	@size: size of the structure
	@fmt: format of the structure
	@key: number of the field in the strucutre used as index for 
	      the dictionary.

	Example for a struct with an integer and a char {int, char} on 
	IA-32 architecture, ssize=8 (because of alignement) and fmt='ic'.
	If the integer is the dictionary's key, key=0 (1st field of the 
	structure).
	"""

        self.filename = filename
        self.ssize    = size
        self.fmt      = fmt
        self.key      = key

    # Public Method
    def parse(self):
        """
        Parse a file and return a dictionary. The key is the process ID
        and the value is the job ID.
        """

        dico = {}

        try:
            ifile = open(self.filename, 'rb')
        except IOError:
            print "Cannot open", self.filename
            return dico

        while 1:
            try:
                buf = ifile.read(self.ssize)
                tmp = struct.unpack(self.fmt, buf)
                dico[tmp[self.key]] = tmp
            except (IOError, KeyboardInterrupt):
                break
            except struct.error:
                #print "Unable to parse", self.filename, "(check file format)"
                break
        
        ifile.close()
        return dico


def print_help(name):
    """
    Display help about 'name'

    @name: The name of the program
    """

    print """
    SYNOPSIS
        %s [ACTIONS] [OPTIONS] [-h]

    DESCRIPTION
        %s reads two different kind of files. The first one is an accounting 
	file. It must contains per process accounting data obtained with the 
	BSD process accounting version 3. The second one is a job file that 
	must contains information about groups of processes provided by the job 
	daemon.

	Depending of chosen OPTIONS you can simply dump the contents of a 
	given file on the standard output or you can compute per-groups of
	processes accounting information. Currently, informations display are 
	the number of processes in a job, the elapsed, user and system time, 
	the minor and major page faults and also the number of pages swapped.

    ACTIONS
        -d    Dump information about process accounting or groups of processes.

        -x    Generate accounting data about per-groups of processes. You must
	      provide an accounting file AND a job file.

    OPTIONS
        -a    Set the accounting file (must be BSD accounting v3).

        -j    Set the job file provided by the job daemon.

    SEE ALSO
        accton(8), jobmng(1)

    AUTHOR
        Originally written by Guillaume Thouvenin
        <guillaume.thouvenin@bull.net>

    """ % (name, name)


def valid_args(action, file1, file2):
    """
    Check if arguments are coherent. Depending of the action, arguments
    should or shouldn't be set. 

    @action: action to perform (1:analysis, 2:dump and 3:both)
    @file1: a file (accounting or job file)
    @file2: a file (accounting or job file)
    """
    # action must be 1, 2 or 3.
    if action != 1 and action != 2 and action != 3:
        print "Error: you must select an action."
        return False

    # action == 1 or 3 means run analysis and thus, file1 
    # and file2 must be defined
    if (action == 1 or action == 3) and (file1 == "" or file2 == ""):
        print "Error: you choose to do an analysis so you must"
	print "       select an accounting file AND a job file."
        return False
    else:
        # Everything looks good at this point
        return True

    # If a dump is asked, at least one file must be defined
    if (action == 2 or action == 3) and (file1 == "" and file2 == ""):
        print "Error: you choose to dump the content of a file on the screen"
	print "       so you must select an accounting file AND/OR a job file."
        return False
    else:
        # Everything looks good at this point
        return True



def dump_job_file(info):
    """
    Dump information about group of processes called 'job'.
    
    The structure used by the job daemon is (unsigned int, pid_t, 
    unsigned int) thus, sizeof_jobd_msg is 12 (3*4 bytes for IA-32). It's 
    architecture dependent.

    @info: a dictionary. The key is a pid. It contains tuple that are
           (req, pid, jid) where req is a netlink request (see jobd).
    """

    print "++ Job information ++"
    print

    try:
        for pid, value in info.iteritems():
            print "[ PID#", pid , " - JOB#" , value[2], "]"
    except (KeyboardInterrupt, IOError):
        pass 

    print


def dump_acct_file(info):
    """
    Dump per-process accounting information.

    The structure used by acct is acct_v3. In linux/acct.h we found the 
    following definition

    struct acct_v3
    {
        0 :   char      ac_flag;            /* Flags */
        1 :   char      ac_version;         /* Always set to ACCT_VERSION */
        2 :   __u16     ac_tty;             /* Control Terminal */
        3 :   __u32     ac_exitcode;        /* Exitcode */
        4 :   __u32     ac_uid;             /* Real User ID */
        5 :   __u32     ac_gid;             /* Real Group ID */
        6 :   __u32     ac_pid;             /* Process ID */
        7 :   __u32     ac_ppid;            /* Parent Process ID */
        8 :   __u32     ac_btime;           /* Process Creation Time */
            #ifdef __KERNEL_
        9 :   __u32     ac_etime;           /* Elapsed Time */
            #else
        9 :   float     ac_etime;           /* Elapsed Time */
            #endif
        10:   comp_t    ac_utime;           /* User Time */
        11:   comp_t    ac_stime;           /* System Time */
        12:   comp_t    ac_mem;             /* Average Memory Usage */
        13:   comp_t    ac_io;              /* Chars Transferred */
        14:   comp_t    ac_rw;              /* Blocks Read or Written */
        15:   comp_t    ac_minflt;          /* Minor Pagefaults */
        16:   comp_t    ac_majflt;          /* Major Pagefaults */
        17:   comp_t    ac_swaps;           /* Number of Swaps */
        18:   char      ac_comm[ACCT_COMM]; /* Command Name */
    };
   
    ACCT_COM = 16 bytes
    sizeof(struct acct_v3) = 64 bytes
    
    @info: a dictionary. The key is a pid. It contains tuples that are represented
           the above structure.
    """

    print "++ BSD process accounting version 3 ++"
    print

    try:
        for pid, value in info.iteritems():
            print "command [" , pid , "] = " , value[18]
            print "\tElapsed Time:", value[9]
            print "\tUser Time   :", value[10]
            print "\tSystem Time :", value[11]
            print "\tMem Usage   :", value[12]
            print "\tMinor Fault :", value[15]
            print "\tMajor Fault :", value[16]
            print
    except (KeyboardInterrupt, IOError):
        pass

    print


def run_analysis(acct_dico, job_dico):
    """
    Compute per-groups of processes accounting and display the result on the 
    standard output.

    @acct_dico: per-process accounting information.
    @job_dico: information about groups of processes.
    """
    
    elsa_dico = {}
    for pid, value in job_dico.iteritems():
        try:
            # value[2] == jobID
            jid = value[2]
            # retrieve accounting information about process #PID
            process_info = acct_dico[pid]
        except KeyError:
            print "ERROR during analysis"
            break
        except (IOError, KeyboardInterrupt, struct.error):
            break

        try:
            elsa_dico[jid] = ( elsa_dico[jid][0] + 1, 
                               elsa_dico[jid][1] + process_info[9],
                               elsa_dico[jid][2] + process_info[10],
                               elsa_dico[jid][3] + process_info[11],
                               elsa_dico[jid][4] + process_info[15],
                               elsa_dico[jid][5] + process_info[16],
                               elsa_dico[jid][6] + process_info[17])
        except KeyError:
            # Create a new entry in elsa_dico
            # We use 0.0 because we want to use float.
            elsa_dico[jid] = ( 1,
                               0.0 + process_info[9],
                               0.0 + process_info[10],
                               0.0 + process_info[11],
                               process_info[15],
                               process_info[16],
                               process_info[17])

        except (IOError, KeyboardInterrupt, struct.error):
            break

    # display results with some colors :)
    # Attribute for an xterm can be:  
    #   \033: escape sequence start
    #   1/22 -> bold on/off
    #   4/24 -> underline on/off
    #   5/25 -> blink on/off
    #   7/27 -> inverse on/off
    #   30..37 -> foreground color
    #             30: Black
    #             31: Red
    #             32: Green
    #             33: Yellow\Orange
    #             34: Blue
    #             35: Magenta
    #             36: Cyan
    #             37: Light Gray\Black
    #   40..47 -> background color
    #   m   : escape sequence stop
    use_color = '\033[1;4;30;47m'
    use_nocolor = '\033[0m'
    print ""
    for jid, res in elsa_dico.iteritems():
        print use_color, "Results for job ID", jid, use_nocolor, "\n"
        print "   Nbr of processes :", res[0]
        print "   Elapsed Time     :", res[1], "AHZ"
        print "   User Time        :", res[2], "AHZ"
        print "   System Time      :", res[3], "AHZ"
        print "   Minor Pagefaults :", res[4], "page(s)"
        print "   Major Pagefaults :", res[5], "page(s)"
        print "   Swaps            :", res[6], "page(s)"
        print ""


def parse_cmd_line(progname, cmd_line):
    """
    Parse the arguments given in command line.

    @progname: The executable's name
    @cmd_line: list of all arguments (without the executable's name)
    """

    # Initialize some variables
    action = 0  # 1: perform an analysis
                # 2: dump the given file
                # 3: both 
    acct_file = ""
    job_file  = ""

    # Parse the command line 
    # We parse start options
    try:
        opts, args = getopt.getopt(cmd_line, "hxda:j:")
    except getopt.GetoptError:
        # print help and exit:
        print_help(progname)
        sys.exit(2)

    if not args == []:
        print "Warning: Argument(s)", args, "are not needed and useless"

    for option, argument in opts:
        if option == "-h":
            print_help(progname)
            sys.exit()
        if option == '-x':
            action |= 1 
        if option == '-d':
            action |= 2 
        if option == '-a':
            acct_file = argument
        if option == '-j':
            job_file = argument

    return action, acct_file, job_file


def main():
    """
    The main routine
    """
    
    # Initialize some variables
    progname = os.path.abspath(sys.argv[0]).split(os.sep)[-1]
    acct_dico = {}
    job_dico  = {}

    action, acct_file, job_file = parse_cmd_line(progname, sys.argv[1:])
   
    # Check the validity of arguments
    if not valid_args(action, acct_file, job_file):
        print "Try", progname, "-h for more information"
        sys.exit(2)

    # see dump_acct_info() to know why we pass such parameters 
    if acct_file:
        elsa_parser = ElsaParser(acct_file, 64, 'ccHIIIIIIfHHHHHHHH16s', 6)
        acct_dico = elsa_parser.parse()

    # see dump_job_info() to know why we pass such parameters 
    if job_file:
        elsa_parser = ElsaParser(job_file, 12, 'iiI', 1)
        job_dico = elsa_parser.parse()

    # dump is done if action != 1
    if action != 1:
        if acct_dico:
            dump_acct_file(acct_dico)
        if job_dico:
            dump_job_file(job_dico)
   
    # run the analysis if action != 2
    if action != 2:
    	run_analysis(acct_dico, job_dico)

    
if __name__ == "__main__":
    main()
