#/usr/local/bin/perl
#Path: tut.cis.ohio-state.edu!pacific.mps.ohio-state.edu!zaphod.mps.ohio-state.edu!mips!apple!oliveb!orc!mipos3!iwarp.intel.com!news
#From: [email protected] (Randal Schwartz)
#Newsgroups: comp.lang.perl,alt.sources
#Subject: multiple host command launcher (gsh) in Perl
#Message-ID: <[email protected]>
#Date: 7 Mar 90 19:06:33 GMT
#Sender: [email protected]
#Reply-To: [email protected] (Randal Schwartz)
#Organization: Stonehenge; netaccess via Intel, Beaverton, Oregon, USA
#Lines: 279
#Xref: tut.cis.ohio-state.edu comp.lang.perl:607 alt.sources:1534
#
#Here's the 'gsh' I've been using for a while (industrial strength by
#now).  The coding style is not pretty, but it has been roadtested.
#
#Yes, this stuff was inspired by the 'gsh' in the Perl distribution,
#although I've taken it about three steps further.  Mine has parallel
#launching and waiting, a built-in (but overridable/extensible)
#hostlist, and a timeout for those rsh's that launch but "never" come
#back.  You'll want to edit the builtin hostlist, unless you just
#*happen* to have a bunch of systems named 'iwarpa', 'iwarpb', etc.
#etc. :-)
#
#Enjoy.
#
#================================================== snip here
## Copyright (C) 1989, 1990, by Randal L. Schwartz.  All Rights Reserved.
## usage: gsh [options] hostspec [command [arg]...]
## Runs command and args on hosts according to hostspec.  Results are
## sent to STDOUT, with hostname prefix.  A missing command means to just
## echo the computed hostnames on STDOUT. 'hostspec' is one of:
##   hostname, hostattribute, hostspec+hostspec, hostspec-hostspec
## Default hostlist is defined in @HOSTLIST later on.
##
## options:
## -d: don't run any commands on other hosts... but fork anyway.
## -h hostlist: extend the hostlist with the contents of the named file.
## -H hostlist: replace the hostlist with the contents of the named file.
## -i: give STDIN to the processes as STDIN
## -o place: send the outputs to "place$host" instead of STDOUT
## -n procs: run this many processes at a time (default 5).
##           (remember that each rsh is two processes on this host!)
## -v: be noisy about starting and finishing processes.
## -z sec: zap processes after sec seconds (default 300).

## requires 3.0 beta or better
@HOSTLIST = split(/\n/, <<'ENDHOSTLIST');  # comments allowed in here...
decster=decster.uta.edu
ENDHOSTLIST

$| = 1; # don't buffer STDOUT

$the_task_filename = "/tmp/$$.thetask";

$tasks = 0;
$taskmax = 5;
$zapsecs = 300;

sub start {
       local($host) = @_;

       print "starting '$host'...\n" if $verbose;

       while ($tasks > 0 && $tasks >= $taskmax) {
               &finish();
       };
       unless ($pid = fork) {  # child
               open(STDIN, "<$the_task_filename") ||
                       die "Cannot open $the_task_filename as STDIN ($!)";
               open(STDOUT, ">$place$host") ||
                       die "Cannot open $place$host ($!)";
               open(STDERR, ">&STDOUT");
               exec 'cat' if $debug;
               $parent = $$;
               if (fork) { # still the child
                       exec 'rsh', $host, '/bin/sh';
                       die "Cannot exec rsh ($!)";
               }
               # child child
               $zaptime = time + $zapsecs;
               while (time < $zaptime) {
                       sleep 5;
                       exit 0 if getppid == 1;
               }
               kill 9, $parent;
               print "\nTIMED OUT AFTER $zapsecs SECONDS\n";
               exit 0;
       }
       $tasklist{$pid} = $host;
       $tasks++;
}

sub finish {
       return unless $tasks > 0;
       print "waiting on '", join(" ", sort values(tasklist)), "'...\n"
               if $verbose;
       do {
               die "Nothing to wait for??? ($!)" unless ($pid = wait) > 0;
       } until $tasklist{$pid};
       print "finished task on '", delete $tasklist{$pid}, "'.\n"
               if $verbose;
       $tasks--;
}

sub finishall {
       while ($tasks > 0) {
               &finish();
       }
}

sub gethostlist {
       local($f,$replace) = @_;
       open(GETHOSTLIST, "<$f") || die "Cannot open '$f' ($!)";
       @HOSTLIST = () if $replace;
       unshift(@HOSTLIST, <GETHOSTLIST>); # put it at the beginning
       close(GETHOSTLIST);
}

# end initialization... begin code...

while ($ARGV[0] =~ /^-/) {
       $_ = shift;
       $debug++, $verbose++, next if /^-d/;
       $verbose++, next if /^-v/;
       $taskmax = $1, next if /^-n(.+)/;
       $taskmax = shift, next if /^-n/;
       &gethostlist($1, 1), next if /^-H(.+)/;
       &gethostlist(shift, 1), next if /^-H/;
       &gethostlist($1), next if /^-h(.+)/;
       &gethostlist(shift), next if /^-h/;
       $do_stdin++, next if /^-i/;
       $place = $1, next if /^-o(.+)/;
       $place = shift, next if /^-o/;
       $zapsecs = $1, next if /^-z(.+)/;
       $zapsecs = shift, next if /^-z/;
       die "unknown flag $_";
}

$place = "/tmp/$$.", $do_stdout++ unless $place;

unshift(@HOSTLIST,"TARGET=" . shift);

$the_task .= join(" ", @ARGV);
if ($do_stdin) {
       $_ = join("",<STDIN>);
       chop if /\n$/;
       $the_task = "($the_task ;) <<'FoObAr'\n$_\nFoObAr\n";
       # if I got tricky, I could skip the extra shell, but, hey... it works
}

@TARGETS = ();

$attr{'TARGET'} = 1;    # this is what I want.

for $_ (@HOSTLIST) {
       s/\s*\n?$//;    # toss trailing white
       s/^\s*//;       # toss leading white
       next if /^(#.*)?$/; # skip comment lines and blank lines
       if (/^([^-+=]+)=(.*)/) {
               ($name,$repl) = ($1,"+$2");
               next unless $yes = $attr{$name}; # +1 if wanted, -1 if not
               while ($repl =~ s/^([+-])([^-+]+)//) {
                       next if $attr{$2};
                       $attr{$2} = ($1 eq '-') ? - $yes : $yes;
                       print "assigning $attr{$2} to $2\n" if $debug;
               }
       } else {        # must be a terminal node:
               @attr = split;
               $host = $attr[0];
               $wanted = 0;
               for $attr (@attr) {
                       $wanted++, next if $attr{$attr} > 0;
                       $wanted=-1, last if $attr{$attr} < 0;
               }
               push(TARGETS, $host) if $wanted > 0;
       }
}

if ($the_task =~ /^\s*$/) { # no command?  just list the hosts
       print join("\n", @TARGETS), "\n";
       exit 0;
}

open(THE_TASK, ">$the_task_filename") || die "Cannot open THE_TASK ($!)";
print THE_TASK $the_task;
close(THE_TASK);

for $host (@TARGETS) {  # launch'em all, $taskmax at a time
       &start($host);
}

&finishall();           # and hang out while the last $taskmax finish

unlink $the_task_filename; # no need for this anymore

exit 0 unless $do_stdout;

for $host (@TARGETS) {  # show what they said
       open(F,"<$place$host") || die "missing output for $host ($!)";
       if ($_ = join("$host:\t", <F>)) {
               print "$host:\t$_";
               print "\n" unless /\n$/;
       }
       close(F);
       unlink "$place$host";
}
exit 0;