#!/usr/bin/perl

# prun - parallel run processes
#
#   This software is Public Domain. Please maintain version history.
#   Report all bugs to Greg Ercolano (erco@3dsite.com).
#
#     1.00 02/??/98 erco@3dsite.com  perl4 version
#     x.xx xx/xx/xx -                -
#

local($G_verbose) = 1;          # verbose flag
local($G_tmpdir)  = "/var/tmp"; # tmp output files
local(@G_run);                  # array of commands to run loaded from stdin

sub HelpAndExit
{
    print STDERR <<"EOF";
prun - parallel-run commands on stdin

USAGE
    prun [options] < cmds

OPTIONS
    -M #  - sets maximum # forks at a given time (default=$maxcount)
    -q    - quiet (disables RUN/DONE messages)

EXAMPLE
    % cat file
    rdist -c file sun1:/usr/local/bin/file
    rdist -c file sun2:/usr/local/bin/file
    rdist -c file sun3:/usr/local/bin/file
    rdist -c file sun4:/usr/local/bin/file
    % prun < file

DESCRIPTION
    Forks off all commands on stdin in parallel.
    Warning: garbage in = disaster out. Be careful.
    Blank lines and lines that start with # are ignored.

    The -M flag controls the maximum number of parallel 
    processes that will be running at any given time.

    All commands have stdin redirected from /dev/null, and
    stdout/stderr redirected to a log file that is printed
    to the screen when each process finishes.

    Hitting the interrupt key will remove any leftover log 
    files, and sends SIGKILL to all children still running.
EOF
    exit(1);
}

# POST-INTERRUPT CLEANUP
sub Cleanup
{
    local ($pid);
    local ($tmp);

    # REMOVE TMP FILES FOR ANY RUNNING CHILDREN
    print "(Cleanup..)\n";
    foreach $pid ( keys ( %children ) )
    {
        $tmp = "$G_tmpdir/prun.$pid";

        # KILL RUNNING CHILDREN
        if ( $G_verbose )
            { print "CLEANUP: killing $pid\n"; }
        kill('INT', $pid);

        # REMOVE TMPFILE
        if ( ! -e $tmp )
            { next; }
        if ( $G_verbose )
            { print "CLEANUP: Removing $tmp..\n"; }
        unless ( unlink($tmp) )
            { print STDERR "prun: ERROR: unlink($tmp): $!\n"; }
    }
    exit(1);
}

# ANNOUNCE START OR COMPLETION
sub Started
{
    local ($i, $count, $pid) = @_;
    if ( $G_verbose )
    {
        print "RUN  " . sprintf("%5d",$pid) . 
              ", $count busy, " . ($#G_run - $i) . 
              " to go: $children{$pid}\n";
    }
}

# A PROCESS FINISHED
sub Finish
{
    local($i, $count, $pid) = @_;
    local($tmp) = "$G_tmpdir/prun.$pid";

    # A CHILD FINISHED
    if ( $G_verbose )
    {
        print "DONE " . sprintf("%5d",$pid) . 
              ", $count busy, " . ($#G_run - $i) .
              " to go: $children{$pid}\n";
    }

    # PRINT AND REMOVE OUTPUT FILE
    if ( -e $tmp )
    {
        print "--- $children{$pid}\n";
        open(OUT, "<$tmp");
        while ( <OUT> )
            { print "$_"; }
        close(OUT);
        unless ( unlink($tmp) )
            { print STDERR "prun: ERROR: unlink($tmp): $!\n"; }
    }
    delete($children{$pid});
}

# MAIN
{
    local ($i);
    local ($pid);
    local ($cmd);
    local (%children);  # hash of children mapped by pid
    local ($count)    = 0;
    local ($maxcount) = 4;

    # PARSE ARGS
    for ( $i=0; $i<=$#ARGV; $i++ )
    {
        if ( $ARGV[$i] =~ /^-h/ )
            { &HelpAndExit(); }
        elsif ( $ARGV[$i] =~ /^-q/ )
            { $G_verbose = 0; }
        elsif ( $ARGV[$i] =~ /^-M/ )
            { $i++; $maxcount = $ARGV[$i]; }
        else
            { print STDERR "prun: '$ARGV[$i]' unknown flag\n"; exit(1); }
    }

    # LOAD ALL COMMANDS FROM STDIN
    while ( <STDIN> )
    {
        $cmd = $_;
        chop($cmd);

        # SKIP COMMENTS AND BLANK LINES
        if ( $cmd =~ /^#/ || $cmd =~ /^[\s]*$/ )
            { next; }

        push(@G_run, $cmd);
    }

    if ( $G_verbose )
        { print "$#G_run commands, batching $maxcount at a time\n"; }

    # SETUP AN INTERRUPT TRAP
    $SIG{'INT'} = 'Cleanup';

    for ( $i=0; $i<=$#G_run; $i++ )
    {
        $cmd = $G_run[$i];

        # HIT MAXIMUM? WAIT FOR A CHILD TO FINISH
        if ( $count >= $maxcount )
        {
            # WAIT FOR A CHILD
            if ( ($pid = wait()) != -1 )
            {
                $count--;
                &Finish($i, $count, $pid);
            }
        }

        # START A NEW CHILD
        if ( ( $pid = fork()) == 0 )
        {
            # CHILD
            local ($tmp) = "$G_tmpdir/prun.$$";
            exec("$cmd > $tmp 2>&1 < /dev/null");
            print STDERR "exec($cmd): $!\n";
            exit(1);
        }
        else
        {
            # PARENT
            $children{$pid} = $cmd;
            &Started($i, $count, $pid);
            $count++;
        }
    }

    # NO MORE COMMANDS TO RUN? WAIT FOR CHILDREN
    while ( ( $pid = wait()) != -1)
    {
        $count--;
        &Finish($#G_run, $count, $pid);
    }
    exit(0);
}

