#!/usr/bin/perl -w

use strict;
use warnings;
use File::Basename;
use Cwd qw(abs_path);
use Getopt::Long qw(:config no_ignore_case gnu_compat);

# for debug only, can be removed.
#use Data::Dumper;

# set current path of this command
my $curpath = dirname(Cwd::abs_path(__FILE__));

my $CONFFILE = "/etc/osw2csv.conf";
if (-f "$curpath/".basename($CONFFILE)) {
	require "$curpath/".basename($CONFFILE);
} elsif (-f $CONFFILE) {
	require $CONFFILE;
}

require "$curpath/osw2csv_functions";


sub printUsageAndExit {
	error(@_);
	print STDERR <<"END_OF_USAGE";
usage:	# cat psfile | $0 [option] > csvfile.csv
description:
	This script generate csv file regarding processes, which is from "ps"
	output provided by OSWatcher or ExaWatcher.
	The ps output is read from STDIN, and cvs is provided onto STDOUT.
        option:
	-a <NAME>, --analyze <NAME>
		each process analysis by NAME, like "VSS", "RSS", "%CPU"...
		"NUM" is special, which analyzes process number, needs -w.
	-w, --wrapup
		wrap-up mode, sum data of processes which has same name.
		This needs -a together.
END_OF_USAGE
	exit 1;
}


# belows are defined in $COMMONFUNCTIONS
our $zzz_second;  # set by "zzz ... " line. 0 means "there are no zzz line"


# ps header array(ex: USER,PID,%CPU,%MEM,VSZ,RSS,TTY,STAT,START,TIME,COMMAND)
my @psheader;
# cmd, pid and designated elm index
my ($cmdidx, $pididx, $elmidx) = (-1, -1, -1);


# get Ps header, synchronized for analyzePsLine();
sub getPsHeader {
	return join(',', @psheader);
}


# check whether $line is valid ps header
# using $zzz_second and $zzz_timezone to decide the actual time.
sub checkPsHeader {
	my ($line, $elmname) = @_;
	# remove head-spaces
	$line =~ s/^\s+//;
	my @ary = split(/\s+/, $line);

	if (!grep ($_ eq "PID" || $_ eq "COMMAND" || $_ eq "CMD", @ary) ||
            $line !~ /^[%A-Z_\s\+-]+$/) {
# comment out to suppress unnecessary message
#		warning("Failed to analyze ps header:", "$line");
		return 0;
	}
	if ($#psheader >= 0) {
		# psheader has been set;
		my @newheader = split(/\s+/,$line);
# consider whether this check is necessary, as this takes time.
		if (join(',', @newheader) ne join(',', @psheader)) {
			warning("Invalid ps header, skip:", "$line");
			return 0;
		}
	} else {
		# this is the 1st time to get ps header, need to store data.
		my ($tmppididx) = grep { $ary[$_] eq "PID" } 0..$#ary;
		my ($tmpcmdidx) = grep { $ary[$_] eq "CMD" || $ary[$_] eq "COMMAND" } 0..$#ary;
		my ($tmpelmidx) = grep { $ary[$_] eq $elmname } 0..$#ary;
		if (!defined($tmpelmidx)) {
			error_exit("elmname($elmname) cannot be found in:\n".
				   join(',', @ary));
		}
		if (!defined($tmppididx) || !defined($tmpcmdidx)) {
			warning("Failed to analyze ps header, skip:",
			        "  line:$line",
			        "  pididx=$tmppididx, cmdidx=$tmpcmdidx");
			return 0;
		}
		$pididx = $tmppididx;	
		$cmdidx = $tmpcmdidx;	
		$elmidx = $tmpelmidx;	
		@psheader = @ary;
	} 
	return 1;
}


sub printPsCSVHeader {
	print getPsHeader()."\n";
}


# analyze and return cvs-ed process line
# ex.
# ($pid, $cmd, @line)
# "root         1  0.0  0.0  19420  1160 ?        Ss    2016   0:01 /sbin/init"
# -> (1, /sbin/init, <data_of_elmidx>)
sub analyzePsProcessLine {
	my ($line) = @_;

	# below lines are omitted for efficient/speedly processing
	# the last element could be "cmd arg1 arg2...", so remove all args
	# my @retary = (split(/\s+/, $line, $#psheader+2))[0..$#psheader];
	# return if ($#retary != $#psheader);
	# return ($retary[$pididx], $retary[$cmdidx], $retary[$elmidx]);
	
	# Note various format "00:11:22", "Mar 3" or "2016" in START/STARTED column.
	# This just removes a space in date string, ex. "Mar 11" -> "Mar11"
	# to handle it as 1 continuous string.
	$line =~ s/ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{1,2}) / $1$2 /g;

	# remove below line as it does not work correctly on CentOS7
	# return (split(/\s+/, $line, $#psheader+2))[$pididx, $cmdidx, $elmidx];

	my @ary = split(/\s+/, $line, $#psheader+2);
	return @ary[$pididx, $cmdidx, $elmidx];
}


# readlines from STDIN and construct %phash
sub getPsProcessHash {
	my $elmname = $_[0];
	# read/check the first line
	return if (!defined($_ = readNextLineFromSTDIN()));

	my %phash = (
		"prockey" => {},
		"procdata" => {}
	);
	initPrintProcessing();
	while (defined($_)) {
		printProcessing(", time:".getDateStringFromSeconds($zzz_second));

		# set current time if there is "zzz ***" line
		if (setZZZLine($_)) {
			$_ = readNextLineFromSTDIN();
			next;
		}

		# ignore if the line is not ps header
		if (!checkPsHeader($_, $elmname)) {
			$_ = readNextLineFromSTDIN();
			next;
		}

		$_ = readNextLineFromSTDIN();

		# analyze process lines
		while (defined($_) && !isZZZLine($_)) {
			my ($pid, $cmd, $elmdat) = analyzePsProcessLine($_);
			last if (!defined($pid) || !defined($cmd));
			my $keystr = "$cmd:$pid";

			# $phash{prockey} was array but now hash because it is
			# twice faster than array.
			$phash{prockey}->{$keystr} = 1;
			# to reduce memory size, save only the necessary info.
			$phash{procdata}->{$zzz_second}->{$keystr} = $elmdat;
			$_ = readNextLineFromSTDIN();
		}
	}
	endPrintProcessing();
	return \%phash;
}



######################################################################
# Main routine

# check the command option which is -p and/or -w whether it intends
# which information should be printed.

my %opts;
GetOptions(\%opts, 'help|h', "analyze|a=s", "wrapup|w");

if (!$opts{analyze}) {
	printUsageAndExit("-a needs to be specified.");
}
if ($opts{help}) {
	printUsageAndExit("help eoption specified.");
}
if (!-p STDIN) {
	printUsageAndExit("run as piped command.");
}
if ($opts{wrapup} && !$opts{analyze}) {
	printUsageAndExit("-w needs -a option.");
}
if (!$opts{wrapup} && $opts{analyze} eq "NUM") {
	printUsageAndExit("-a NUM needs -w option.");
}

my $elmname = $opts{analyze};

# $phash = process hash
# Note that NUM is special, temporally change to "PID" here
my $phash = getPsProcessHash($elmname eq "NUM" ? "PID" : $elmname);
# $phash = { 
#     # Note: prockey is a hash as it's faster than array to exclusive the keys
#     prockey   => { "init:1" => 1, "oracle:5562" => 1, ... ];
#     procdata  => { 2364349349(=second of "2016/07/08 10:50:01") => {
#			"init:1" => 1002,
#				# selected from VIRT,RES,SHR,%CPU,%MEM
#			"oracle:5562" => 61488
#				:
#		   },
#		   { 2364349359(=second of "2016/07/08 10:50:11") => {
#				:
#		   },
#				:
# }

if (!$opts{wrapup}) {
	printProcessCSV($phash);
} elsif ($elmname eq "NUM") {
	printWrapupedProcessCSV($phash, 1);
} else {
	printWrapupedProcessCSV($phash, 0);
}
exit 0;

