The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl -w

=pod

=head1 NAME

thetop - A 'top' utility for the schwartz

=head1 SYNOPSIS

  thetop [--func FORMAT] [--arg FORMAT] [--sort ARGS] [--delay SECS] [--score-dir DIR]

=head1 DESCRIPTION


=cut

#--------------------------------------#
# Dependencies

use strict;

use Getopt::Long;
use Term::Cap;
use POSIX;

#--------------------------------------#
# Global Variables

use vars qw( $OSPEED );

BEGIN {
    my $termios = POSIX::Termios->new;
    $termios->getattr;
    $OSPEED = $termios->getospeed || 9600;
};

our $TERM = Term::Cap->Tgetent({OSPEED=>$OSPEED});

#--------------------------------------#
# Main Program

my ($score_dir, $delay, $func_col, @arg_col, $sort);

GetOptions('score-dir=s' => \$score_dir,
           'delay|d=s'   => \$delay,
           'func=s'      => \$func_col,
           'arg=s'       => \@arg_col,
           'sort|s=s'    => \$sort,
          );

# Make sure we know where to find the scoreboard files
unless ($score_dir) {
    foreach my $d (qw(/var/run /dev/shm /tmp)) {
        if (-e "$d/theschwartz") {
            $score_dir = "$d/theschwartz";
            last;
        }
    }

    die "Can't find scoreboard directory.  Use '--score-dir'\n"
      unless $score_dir;
}

# If we got some formatting instructions for the arg column, parse it out
my %arg_col_by_func;
if (@arg_col) {
    foreach my $a (@arg_col) {
        if ($a =~ /=/) {
            my ($func, $fmt) = split('=', $a);
            $arg_col_by_func{$func} = $fmt;
        } else {
            $arg_col_by_func{'__ALL__'} = $a;
        }
    }
}

# Make sure to give a reasonable default for delay
$delay ||= 3;

# Start reporting
clr_screen();
while (1) {
    report($score_dir, $func_col, \%arg_col_by_func, $sort);
    sleep($delay);
    clr_screen();
}

################################################################################

sub report {
    my ($dir, $func_col, $arg_col_by_func, $sort) = @_;

    # Find the files available
    opendir(SD, $dir) or die "Can't read directory '$dir': $!\n";
    my @files = map { $dir."/$_" } readdir(SD);
    closedir(SD);

    # Grab the data out of them
    my @data;
    foreach my $f (@files) {
        next unless $f =~ /scoreboard\.[0-9]+$/;
        open(SF, '<', $f) or die "Can't open score file '$f': $!\n";
        my %dat = map { chomp; split('=') } <SF>;
        close(SF);

        $dat{arg_array} = [split(',', $dat{arg}||'')];
        push @data, \%dat;
    }

    my $num = scalar(@data);
    my $width = 80-17-$num;
    printf("Workers: %d total %${width}s\n\n", $num, scalar localtime);
    printf("% 5s % 20s % 2s % 7s % 41s\n", 'PID', 'FUNC', 'S', 'TIME', 'ARGS');
    foreach my $d (sort { order_by($sort, $a, $b) } @data) {
        my $func_str = fmt_func($d, $func_col);

        printf("% 5s % 20s % 2s % 7s % 41s\n",
               $d->{pid},
               $func_str,
               ($d->{done} ? 'S' : 'R'),
               fmt_time($d),
               fmt_arg($d, $arg_col_by_func, $func_str),
              );
    }
}

sub order_by {
    my ($sort, $a, $b) = @_;

    if ($sort) {

    } else {
        # Default to push running tasks to the top
        return ($a->{done}||0) <=> ($b->{done}||0) ||
               ($a->{started}||0) <=> ($b->{started}||0);
    }
}

sub fmt_func {
    my ($d, $fmt) = @_;
    my $val = $d->{funcname};

    if ($fmt) {
        if ($fmt eq 'trim') {
            $val =~ s/^.+:://g;
        } else {
            $val =~ /($fmt)/;
            $val = $1;
        }
    }

    return substr($val, 0, 20),
}

sub fmt_time {
    my ($d) = @_;
    my $secs = ($d->{done}||time) - $d->{started};

    if ($secs < 60) {
        return sprintf("%02d:%02d", 0, $secs);
    } elsif ($secs < 3600) {
        my $min = int($secs/60);
        $secs = $secs%60;
        return sprintf("%02d:%02d", $min, $secs);
    } else {
        my $hr  = int($secs/60/60);
        my $min = int($secs/60%60);
        $secs = $secs%60;
        return sprintf("%d:%02d:%02d", $hr, $min, $secs);
    }
}

## Format the arguments by interpreting the args as either a hash or an array
## and printing out the appropriate element.

sub fmt_arg {
    my ($d, $arg_col_by_func, $func_str) = @_;
    my $val = $d->{arg};
    my $func_orig = $d->{funcname};

    if ($arg_col_by_func) {
        my $fmt = ($arg_col_by_func{$func_str}  ||
                   $arg_col_by_func{$func_orig} ||
                   $arg_col_by_func{'__ALL__'});
        if ($fmt) {
            my $arg_array = $d->{arg_array};

            # If its a number treat the args as an array
            if ($fmt =~ /^[0-9]+$/) {
                $val = $arg_array->[$fmt];
            }
            # otherwise, treat the args as a hash
            else {
                # Compensate for odd numbers of args
                push @$arg_array, undef if scalar(@$arg_array) % 2;

                my %h = @$arg_array;
                $val = $h{$fmt};
            }
        }
    }

    return substr($val||'', 0, 41),
}

sub clr_screen {
    $TERM->Tputs('cl', 1, \*STDOUT);
}