#! /usr/bin/perl -w
##**************************************************************
##
## Copyright (C) 1990-2007, Condor Team, Computer Sciences Department,
## University of Wisconsin-Madison, WI.
## 
## Licensed under the Apache License, Version 2.0 (the "License"); you
## may not use this file except in compliance with the License.  You may
## obtain a copy of the License at
## 
##    http://www.apache.org/licenses/LICENSE-2.0
## 
## Unless required by applicable law or agreed to in writing, software
## distributed under the License is distributed on an "AS IS" BASIS,
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
## See the License for the specific language governing permissions and
## limitations under the License.
##
##**************************************************************

use strict;

# Update the module include path
BEGIN
{
    my $Dir = $0;
    if ( $Dir =~ /(.*)\/.*/ )
    {
	push @INC, "$1";
    }
}
use HawkeyePublish;
use HawkeyeLib;

# Prototypes
sub Init( );
sub MainLoop( );
sub Configure( );
sub RunIt( );
sub ProcessData( $ );
sub FixString( $ );

# Hawkeye data
my $Hawkeye;

my %Config =
    (
     Debug	=> 0,
     TopInterval	=> 30,
     RunOnce	=> 0,
     MaxLoops	=> 1000,
     TopOpt	=> {
		    Batch => "-b ",
		    Delay => "-d ",
		    BlankSep => 0,
		   },
    );

# Valid field list:
my %TopFields =
    (
     pid =>
     {
      enabled => 0,
      type => HawkeyePublish::TypeNumber,
      l => [ "PID", ],
     },
     user =>
     {
      enabled => 0,
      type => HawkeyePublish::TypeString,
      l => [ "USER", "USERNAME", ],
     },
     priority =>
     {
      enabled => 0,
      type => HawkeyePublish::TypeString,
      l => [ "PRI", ],
     },
     thread =>
     {
      enabled => 0,
      type => HawkeyePublish::TypeNumber,
      l => [ "THR", ],
     },
     nice =>
     {
      enabled => 0,
      type => HawkeyePublish::TypeNumber,
      l => [ "NI", "NICE", ],
     },
     size =>
     {
      enabled => 0,
      type => HawkeyePublish::TypeNumber,
      l => [ "SIZE", ],
     },
     rss =>
     {
      enabled => 0,
      type => HawkeyePublish::TypeNumber,
      l => [ "RSS", "RES" ],
     },
     share =>
     {
      enabled => 0,
      type => HawkeyePublish::TypeNumber,
      l => [ "SHARE", ],
     },
     state =>
     {
      enabled => 0,
      type => HawkeyePublish::TypeString,
      l => [ "STAT", "STATE", ],
     },
     lib =>
     {
      enabled => 0,
      type => HawkeyePublish::TypeNumber,
      l => [ "LIB", ],
     },
     cpu =>
     {
      enabled => 0,
      type => "%",
      l => [ "%CPU", "CPU" ],
     },
     cpunum =>
     {
      enabled => 0,
      type => HawkeyePublish::TypeNumber,
       l => [ "CPU", ],
     },
     mem =>
     {
      enabled => 0,
      type => "%",
      l => [ "%MEM", ],
     },
     time =>
     {
      enabled => 0,
      type => "t",
      l => [ "TIME", ],
     },
     command =>
     {
      enabled => 0,
      type => HawkeyePublish::TypeString,
      l => [ "COMMAND", ],
     },
    );
my %TopTypes =
    (

     HawkeyePublish::TypeString() =>
     {
      pubtype => HawkeyePublish::TypeString,
      prefix => "",
     },

     HawkeyePublish::TypeNumber() =>
     {
      pubtype => HawkeyePublish::TypeNumber,
      prefix => "",
     },

     "t" =>
     {
      pubtype => HawkeyePublish::TypeNumber,
      prefix => "",
     },

     "%" =>
     {
      pubtype => HawkeyePublish::TypeNumber,
      prefix => "pct_",
     },

    );
my %SortOut;

# Main logic
$| = 1;
print STDERR "My pid = $$\n";

# Global state variables
my $Reconfig = 0;
my $AllDone = 0;
my $AlarmPending = 0;
my $TopPid = -1;
my @TopHeader;
my $TopVersion = "";

# Initialize signal handlers
$SIG{HUP} = sub {
    print STDERR "Got HUP\n";
    $Reconfig = 1;
};
$SIG{TERM} = sub {
    if ( $TopPid > 0 ) {
	print STDERR "Got SIGTERM; killing top (PID = $TopPid)\n";
	kill( "TERM", $TopPid ) || print STDERR "Can't kill $TopPid\n";
    } else {
	print STDERR "Got SIGTERM (no top to kill)\n";
    }
    $AllDone++;
};
$SIG{INT} = sub {
    if ( $TopPid > 0 ) {
	print STDERR "Got SIGINT; killing top (PID = $TopPid)\n";
	kill( "INT", $TopPid ) || print STDERR "Can't kill $TopPid\n";
    } else {
	print STDERR "Got SIGINT (no top to kill)\n";
    }
    $AllDone++;
};
$SIG{CHLD} = sub {
    my $Pid = wait ();
    if ( ( $Pid > 0 ) && ( $Pid == $TopPid ) ) {
	print STDERR "\"top\" process $Pid died\n";
    }
    $TopPid = -1;
};
$SIG{ALRM} = sub {
    if ( $TopPid > 0 ) {
	print STDERR "Got SIGALRM; killing top (PID = $TopPid)\n";
	kill( "TERM", $TopPid ) || print STDERR "Can't kill $TopPid\n";
    } else {
	print STDERR "Got SIGALRM (no top to kill)\n";
    }
    if ( $AlarmPending ) {
	print STDERR "Previous alarm pending; bailing out\n";
	exit 1;
    }
    alarm( $Config{TopInterval} * 4 );
    $AllDone++;
    $AlarmPending++;
};

# Initialize & Run
Init( );
MainLoop( );

# Loop 'til we're done
sub MainLoop( )
{
    my $LastTime = -1;
    my $FastCount = 0;
    my $LastSleepTime = 0;
    do
    {
	# Timing logic
	my $CurTime = time();
	my $TimeDiff = $CurTime - $LastTime - $LastSleepTime;
	$LastSleepTime = 0;
	if ( ( $LastTime > 0 ) && ( $TimeDiff < 10 ) ) {
	    print STDERR "\"top\" died after $TimeDiff seconds\n";
	    if ( ++$FastCount > 1 ) {
		my $SleepTime = $FastCount * 10;
		print STDERR "Sleeping for $SleepTime seconds\n";
		sleep( $SleepTime );
		$LastSleepTime = $SleepTime;
	    }
	} else {
	    $FastCount-- if ( $FastCount > 0 );
	}
	$LastTime = $CurTime;

	# Check configuration
	if ( $Reconfig ) {
	    Configure( );
	    $Reconfig = 0;
	}

	# Run the test
	my $Exit = RunIt( );

	# If we're in run once mode, we're "all done"
	if ( $Exit ) {
	    $AllDone++;
	} else {
	    sleep( 5 );
	}
    }
    while( ! $AllDone );

    # Done
    exit 0;
}

# Initialization logic
sub Init( )
{

    # Do the initial configuration stuff
    HawkeyeLib::DoConfig( );

    # Parse command line args
    foreach my $Arg ( @ARGV )
    {
	if ( $Arg =~ /^-1$/ ) {
	    $Config{RunOnce} = 1;
	} elsif ( $Arg =~ /^-c$/ ) {
	    HawkeyeLib::HardConfig( "_sort" );
	    HawkeyeLib::HardConfig( "_fields" );
	    HawkeyeLib::HardConfig( "_data_period" );
	}
	elsif ( $Arg =~ /^-d$/ ) {
	    $Config{Debug}++;
	} else {
	    print STDERR "Unknown argument '$Arg'\n";
	}
    }

    # Setup the hawkeye stuff
    $Hawkeye = HawkeyePublish->new;
    $Hawkeye->Quiet( 1 );
    $Hawkeye->AutoIndexSet( 0 );

    # Pull out the top 'header' info
    $_ =`top -b -n 1 |grep COMMAND`;
    chomp;
    s/^\s+//;
    @TopHeader = split ( /\s+/, $_ );

    # Run top, and look at it's version...
    my $Cmd = "top -v";
    if ( open( TOP, "$Cmd 2>&1 |" ) )
    {
	$_ = <TOP>; chomp;
	print STDERR "Top version string: '$_'\n";
	$TopVersion = $_;
	if ( /procps/ )
	{
	    $Config{TopOpt}{Batch} = "-b ";
	    $Config{TopOpt}{Delay} = "-d ";
	    $Config{TopOpt}{BlankSep} = 0;
	}
	elsif ( /version 3\.5/ )
	{
	    $Config{TopOpt}{Batch} = "-b -d999999";
	    $Config{TopOpt}{Delay} = "-s";
	    $Config{TopOpt}{BlankSep} = 1;
	}
	close( TOP );
    }

    # Do an initial configuration
    Configure( );
}

# Configuration logic
sub Configure( )
{
    ### Valid fields are:
    ###   pid, user, priority, nice, size, rss, share, status, lib,
    ###    cpu,mem,time,command
    ###
    ### Examples
    ###
    ###  10:pid,command
    ###  15:*

    #ReadConfig();
    my $CfgSort = HawkeyeLib::ReadConfig( "_sort", ":5" );
    my $CfgFields = HawkeyeLib::ReadConfig( "_fields", "*" );
    my $CfgInterval = HawkeyeLib::ReadConfig( "_interval", "" );
    if ( $CfgInterval eq "" )
    {
	$CfgInterval = HawkeyeLib::ReadConfig( "_period", "30s" );
    }

    # Max loops
    $Config{MaxLoops} = HawkeyeLib::ReadConfig( "_maxloops", "1000" );
    $Config{MaxLoops} = 1 if ( $Config{RunOnce} );

    # Check the config period string
    if ( $CfgInterval =~ /(\d+)([sSmMhH]?)/ )
    {
	$Config{TopInterval} = $1;
	my $Modifier = ( defined $2 ? uc( $2 ) : "" );
	if ( $Modifier eq "S" )
	{
	    # Do nothing
	}
	elsif ( $Modifier eq "M" )
	{
	    $Config{TopInterval} *= 60;
	}
	elsif ( $Modifier eq "H" )
	{
	    $Config{TopInterval} *= ( 60 * 60 );
	}
	elsif ( $Modifier ne "" )
	{
	    print STDERR "Ignoring unknown period modifier '$2'\n";
	}
    }
    else
    {
	print STDERR "Ignoring top config time '$CfgInterval'\n";
    }

    # Examine the field names that we got at startup
    foreach my $Key ( keys %TopFields )
    {
	$TopFields{$Key}{offset} = -1;
    }
    my $Offset = 0;
    foreach my $tf ( @TopHeader )
    {
	foreach my $Key ( keys %TopFields )
	{
	    foreach my $Label ( @{$TopFields{$Key}{l}} )
	    {
		next if ( $TopFields{$Key}{offset} >= 0 );
		if ( $Label eq $tf )
		{
		    $TopFields{$Key}{offset} = $Offset;
		}
	    }
	}
	$Offset++;
    }

    # Build the "published" names & types
    foreach my $Name ( keys %TopFields )
    {
	my $Pre = "";
	my $PubType = $TopFields{$Name}{type};
	if ( $PubType eq "%" )
	{
	    $Pre = "ptc_";
	    $PubType = HawkeyePublish::TypeString;
	}
	$TopFields{$Name}{pubname} = $Pre . $Name;
	$TopFields{$Name}{pubtype} = $PubType;
    }

    # Extract the sort info
    {
	$CfgSort =~ s/\s+/,/g;
	my ( @List ) = split ( /,+/, $CfgSort );
	my $String;
	foreach $String ( @List )
	{
	    my ( $Name, $Count ) = split ( /:/, $String );
	    $Count = 1
		if ( ( ! defined $Count ) || ( $Count eq "" ) );

	    # "*" and "" mean use top's sort
	    if (  ($Name eq "*" ) || ( $Name eq "" )  )
	    {
		$SortOut{"*"} = $Count;
	    }
	    elsif ( exists $TopFields{$Name} )
	    {
		$SortOut{$Name} = $Count;
	    }
	    else
	    {
		print STDERR "Warning: Ignoring sort key '$Name': $Count\n";
	    }
	}
    }

    # Extract the display fields
    {
	my $Fields = "";
	my $Name;
	my ( @List ) = split ( /[\s\,]+/, $CfgFields );
	print STDERR "Enable list: " . join( " ", @List ) . "\n"
	    if ( $Config{Debug} );
	if ( $List[0] eq "*" )
	{
	    print STDERR "Enabling: " if ( $Config{Debug} );
	    foreach $Name ( keys %TopFields )
	    {
		next if ( $TopFields{$Name}{offset} < 0 );
		$TopFields{$Name}{enabled} = 1;
		my $Type = $TopFields{$Name}{type};
		$Fields = $Fields . $TopTypes{$Type}{prefix} . $Name . " ";
		print STDERR "$Name " if ( $Config{Debug} );
	    }
	    print STDERR "\n" if ( $Config{Debug} );
	}
	else
	{
	    print STDERR "Enabling: " if ( $Config{Debug} );
	    foreach $Name ( @List )
	    {
		chomp $Name;
		if ( ! exists $TopFields{$Name} )
		{
		    print STDERR
			"Warning: Invalid field '$Name' in Top list\n";
		    next;
		}
		elsif ( $TopFields{$Name}{offset} < 0 )
		{
		    print STDERR
			"Warning: Field name '$Name' not found in" .
			    " top output\n";
		    next;
		}
		$TopFields{$Name}{enabled} = 1;
		my $Type = $TopFields{$Name}{type};
		$Fields .= $TopTypes{$Type}{prefix} . $Name . " ";
		print STDERR "$Name " if ( $Config{Debug} );
	    }
	    print STDERR "\n" if ( $Config{Debug} );
	}
	$Hawkeye->Store( "FIELDS" , $Fields );
    }
}


###
### Get top output for the top n processes
###
sub RunIt( )
{
    # Build the command & start it
    my $Cmd = "nice -20 top " . $Config{TopOpt}{Batch} . " " .
	$Config{TopOpt}{Delay} . "$Config{TopInterval}";
    $TopPid = open ( TOP, "$Cmd|" );
    Carp::confess( "Can't run top '$Cmd'" ) if ( ! $TopPid );
    print STDERR "$Cmd => $TopPid\n";

    # Make stdout unbufferred
    $| = 1;

    # Flag to skip the first input line
    my $SkipFirst = 0;

    # Are we done yet (yes if in 'run once' mode)
    my $Done = 0;
    my $Loops = 0;
    my $Exit = 0;

    # Timing info
    my $LastTime = -1;
    my $FastCount = 0;

    # Loop til top dies..
    while( ! $Done )
    {
	# Fire off an alarm if we get hung up...
	$AlarmPending = 0;
	alarm( $Config{TopInterval} * 4 );

	# Throw away the first line
	$_ = <TOP> if ( $SkipFirst );
	last if ( eof TOP );

	# Store the # of processes that we should read
	my $NumProcesses = 0;

	# Hash of the summary info
	my $Summary = HawkeyeHash->new( \$Hawkeye, "Summary_" );
	$Summary->Add( "TOP_VERSION",
		       HawkeyePublish::TypeString,
		       $TopVersion );


	# Read the header
      READHEADER:
	my @Buffer;
	while ( ( $_ = shift @Buffer ) or ( $_ = <TOP> ) )
	{
	    # Uptime line?
	    if ( /.*up.*load average:/ )
	    {
		my %Uptime;

		# Parse the uptime line
		HawkeyeLib::ParseUptime( $_, \%Uptime );

		# And, add it to our summary
		foreach my $Attr ( keys %Uptime )
		{
		    my $Type = ( ( $Attr =~ /string/i ) ?
				 HawkeyePublish::TypeString :
				 HawkeyePublish::TypeNumber );
		    $Summary->Add( $Attr, $Type, $Uptime{$Attr} );
		}
	    }

	    # Older tops just dump a "load" line
	    elsif ( /^load averages:\s+(.*)/ )
	    {
		chomp;

		# Hack out the commas
		my @LoadInfo = split( /[\s\,]+/, $1 );
		if ( $#LoadInfo == 3 )
		{
		    my %Load;

		    # Parse the uptime line
		    $Load{Load01} = shift( @LoadInfo );
		    $Load{Load05} = shift( @LoadInfo );
		    $Load{Load15} = shift( @LoadInfo );

		    my $TimeStr = shift( @LoadInfo );
		    if ( $TimeStr =~ /(\d+):(\d+)([ap])m/ )
		    {
			my $Time = ( $1 * 60 ) + $2;
			$Time += (12 * 60 ) if ( $3 =~ /p/ );
			$Load{Time} = $Time;
			$Load{TimeString} = $TimeStr;
		    }
		    elsif ( $TimeStr =~ /(\d+):(\d+):(\d+)/ )
		    {
			$Load{Time} = ( $1 * 60 ) + $2;
			$Load{TimeString} = $TimeStr;
		    }

		    # And, add it to our summary
		    foreach my $Attr ( keys %Load )
		    {
			my $Type = ( ( $Attr =~ /string/i ) ?
				     HawkeyePublish::TypeString :
				     HawkeyePublish::TypeNumber );
			$Summary->Add( $Attr, $Type, $Load{$Attr} );
		    }
		}
	    }

	    # Processes summary information?
	    elsif ( /^(\d+)\s*(.+):(.*)/ )
	    {
		my $Num = $1;
		my $Name = $2;
		my $Rest = $3;
		$Name =~ s/\s+/_/g;

		# Openning summary #?
		if ( $Num ne "" )
		{
		    $Summary->Add( $Name, HawkeyePublish::TypeNumber, $Num );
		    $NumProcesses = $Num if ( $Name eq "processes" );
		}

		# Split the rest of the line...
		$Rest =~ s/\s{2,}/,/g;
		my @Fields = split ( /,/, $Rest );
		my $Field;
		foreach $Field ( @Fields )
		{
		    if ( $Field =~ /\s*(\S+)(.*)/ )
		    {
			my $Value = $1;
			my $Var = $2;
			$Var =~ s/^\s+//;
			my $Type = FixString( \$Value );
			my $What =
			    $Name . "_" . $TopTypes{$Type}{prefix} . $Var;
			$Summary->Add( $What,
				       $TopTypes{$Type}{pubtype},
				       $Value );
		    }
		}
	    }

	    # CPU Summary
	    elsif ( /CPU(\d*) states:(.+)/ )
	    {
		my $Cpu = $1;
		$Cpu = 0 if ( $Cpu eq "" );
		my $Rest = $2;
		$Rest =~ s/^\s+//;
		my @Stuff = split( /\s+/, $Rest );
		for( my $i = 0;  $i+1 < $#Stuff;  $i += 2 )
		{
		    my $Value = $Stuff[$i];
		    my $Type = FixString( \$Value );
		    my $Var = $Stuff[$i+1];
		    my $Name = "CPU_" . $Cpu . "_" .
			$TopTypes{$Type}{prefix} . $Var;
		    $Summary->Add( $Name,
				   $TopTypes{$Type}{pubtype},
				   $Value );
		}
	    }

	    # Memory info
	    elsif ( /^(Mem|Swap):(.+)/ )
	    {
		my $MemSwap = $1;
		my $Rest = $2;
		$Rest =~ s/^\s+//;

		# Maybe add the next line to it
		$_ = <TOP>;
		if ( /^\s+(\d+)/ )
		{
		    $Rest =~ s/\s+$//;
		    $Rest .= ",";
		    $Rest .= $_;
		}
		else
		{
		    push( @Buffer, $_ );
		}

		# Now, parse it out...
		foreach my $Field ( split( /\s*,\s*/, $Rest ) )
		{
		    my ( $Value, $What ) = split( /\s+/, $Field );
		    $Summary->Add( $MemSwap . "_" . $What,
				   HawkeyePublish::TypeAuto,
				   $Value
				 );
		}
	    }

	    # Header line?
	    elsif ( /\s+PID\s+USER/ )
	    {
		while( <TOP> )
		{
		    chomp;
		    next if ( ! /\s*\d+/ );
		    push( @Buffer, $_ );
		    last;
		}
		last;
	    }
	}
	last if ( eof TOP );

	# Ok, summary is done...
	$Summary->Store( "Summary_" );


	# Read the processes themselves
	my $Proc = 0;
	my %Processes;
      READPROC:
	while ( ( $_ = shift @Buffer ) || ( $_ = <TOP> ) )
	{
	    chomp;

	    # Ooops; did we go to far???
	    last if ( $Config{TopOpt}{BlankSep} && ( /^\s*$/ ) );
	    if ( /\s+load average:/ )
	    {
		push( @Buffer, $_ );
		last;
	    }

	    # Verify that we start with a process ID type thingy,
	    # then parse it..
	    next if ( ! /\s*\d+/ );
	    my @Process = split;
	    my $Pid = $Process[0];

	    # Unfortunately, the "State" field can be space separated, so
	    # split screws it up (bad split, bad bad).
	    # Here, we combine, say, ("N" "R") into "N R"...
	    # Note: This assumes that the next "valid" field cannot be a
	    #   single non-digit
	    my $State = $TopFields{state}{offset};
	    my $n = 0;
	    while ( ( ( $State + 1 ) < $#Process ) &&
		    ( $Process[$State+1] =~ /^(\D)$/ ) )
	    {
		$Process[$State] .= " $1";
		splice( @Process, $State+1, 1 );
		if ( ++$n > 2 )
		{
		    print STDERR "It looks like the NR combining got lost in\n";
		    print STDERR "'$_'\n";
		    last;
		}
	    }

	    # Clean up numbers where required
	    foreach my $n ( 0 .. $#Process )
	    {
		FixString( \$Process[$n] );
	    }
	    $Processes{$Proc} = \@Process;
	    my $p = $Processes{$Proc};
	    $Proc++;

	    # We can't read the last line 'cause it doesn't have a
	    # line terminator on it (thanks, top!), so <TOP> blocks, which
	    # really sucks, so we ignore that last line for now.
	    # Perhaps we'll do better later
	    if ( $Proc == ( $NumProcesses - 1 ) )
	    {
		$SkipFirst = 1;
		last;
	    }
	}

	# Count a run
	$Loops++;

	# Ok, process the collected data now
	if ( $Proc < 3 )
	{
	    print STDERR "No process lines found\n";
	    next;
	}
	ProcessData( \%Processes );
	%Processes = ();

	# Timing logic
	my $CurTime = time();
	my $TimeDiff = $CurTime - $LastTime;
	my $MaxTime = $Config{TopInterval} * 1.5;
	my $MinTime = $Config{TopInterval} * 0.5;
	if ( ( $LastTime > 0 ) && ( $TimeDiff < $MinTime ) )
	{
	    print STDERR
		"output looped in $TimeDiff seconds ($Proc lines)!\n";
	    $Done++ if ( ++$FastCount > 1 );
	}
	else
	{
	    $FastCount-- if ( $FastCount > 0 );
	}
	$LastTime = $CurTime;

	# Check for reconfig
	$Done++ if ( $Reconfig );

	# Check for max runs
	if ( $Loops >= $Config{MaxLoops} )
	{
	    $Done++;
	    $Exit++;
	}
    }

    # All done reading!
    sleep( 1 );
    if ( $TopPid >= 0 )
    {
	print STDERR "Killing top (PID = $TopPid )\n";
	kill( "TERM", $TopPid )
    }
    close( TOP );

    return $Exit;
}

###
### Process the collected data....
###
sub ProcessData( $ )
{
    my $ProcessHash = shift;

    my $Publish = HawkeyeHash->new( \$Hawkeye, "" );
    #my %Publish;
    my $SortKey;
    my $ProcNum = 0;
    foreach $SortKey ( keys %SortOut )
    {
	my $Count = $SortOut{$SortKey};
	print STDERR "Sorting on $SortKey ($Count)\n" if ( $Config{Debug} );

	my @SortedProcs;
	my $SortOk = 1;
	if ( $SortKey ne "*" )
	{
	    my $SO = undef;
	    if ( ! exists $TopFields{$SortKey} )
	    {
		print STDERR "WARNING: Bad sort order key '$SortKey'\n";
		$SortOk = 0;
	    }
	    else
	    {
		$SO = $TopFields{$SortKey}{offset};
		if ( ! defined $SO )
		{
		    print STDERR "WARNING: Sort order undefined";
		    $SortOk = 0;
		}
		elsif ( $SO =~ /^\D/ )
		{
		    print STDERR "WARNING: Bad sort order offset '$SO'";
		    $SortOk = 0;
		}
	    }

	    # Sort OK?
	    if ( $SortOk )
	    {
		my @Keys = keys %{$ProcessHash};
		@SortedProcs = sort
		{ @{$ProcessHash->{$a}}[$SO] <=> @{$ProcessHash->{$b}}[$SO] }
		    keys %{$ProcessHash};
	    }
	    else
	    {
		print STDERR "WARNING: Reverting to default sort\n";
	    }
	}

	# Use default sort
	if ( ( $SortKey eq "*" ) || ( ! $SortOk ) )
	{
	    @SortedProcs = sort keys %{$ProcessHash};
	}

	# Walk through the process list...
	my $Proc;
	#foreach $Proc ( sort $SortFunc ( keys %{$ProcessHash} ) )
	foreach $Proc ( @SortedProcs )
	{
	    my $Process = $ProcessHash->{$Proc};
	    my $Pid = \@{$Process}[0];

	    # Should we publish this one?
	    last if ( $Count-- <= 0 );	# No more for this sort?
	    next if ( $$Pid < 0 );	# Already published?

	    # Publish it
	    my $Key;
	    foreach $Key ( keys %TopFields )
	    {
		next if ( $TopFields{$Key}{offset} < 0 );
		if ( $TopFields{$Key}{enabled} )
		{
		    my $Type = $TopFields{$Key}{type};
		    my $Name =
			$ProcNum . "_" . $TopTypes{$Type}{prefix} . $Key;
		    my $Value = @{$Process}[ $TopFields{$Key}{offset} ];
		    $Publish->Add( $Name, $TopTypes{$Type}{pubtype}, $Value );
		}
	    }
	    $Hawkeye->StoreIndex( $ProcNum );
	    $ProcNum++;
	    $$Pid = -1;
	}
    }

    # Store off the data
    $Publish->Store( );

    # Finally, publish it!
    $Hawkeye->Publish( );
}

sub FixString( $ )
{
    my $String = shift;

    # Convert Megs to * 10^6, etc
    if ( $$String =~ /^(\d+(?:\.\d+)?)([KMG])$/ )
    {
	my $Value = $1;
	my $Modifier = $2;
	$$String = $Value * 1e3 if ( $Modifier =~ /k/i );
	$$String = $Value * 1e6 if ( $Modifier =~ /m/i );
	$$String = $Value * 1e9 if ( $Modifier =~ /g/i );
	return HawkeyePublish::TypeNumber;
    }
    # Clean up hrs:minutes
    elsif ( $$String =~ /^(\d+):(\d+)$/ )
    {
	$$String = $1 * 60 + $2;
	return "t";
    }
    # Clean up nnnnm (ie 12345m = 12345 minutes)
    elsif ( $$String =~ /^(\d+)m$/ )
    {
	$$String = $1;
	return HawkeyePublish::TypeNumber;
    }
    # Clean up nnnnH (ie 12345H = 12345 hours)
    elsif ( $$String =~ /^([\d\.]+)H$/ )
    {
	$$String = $1 * 60;
	return HawkeyePublish::TypeNumber;
    }
    # Percentages (just return the number portion, strip the %)
    elsif ( $$String =~ /^(\d+\.?\d*)\%$/ )
    {
	$$String = $1;
	return "%";
    }
    # Just a plain 'ol number?
    elsif ( $$String =~ /^(\d+)$/ )
    {
	return HawkeyePublish::TypeNumber;
    }
    else
    {
	return HawkeyePublish::TypeString;
    }
}
