#!/usr/bin/perl

#
# multi-rsh version 1.2
# Copyright � 2004,2005 Thomas A. Fine
# Freely redistribute for any purpose, in whole or part, provided this
# copyright/license statement is preserved.
#
# online docs at http://hea-www.harvard.edu/~fine/Tech/multi-rsh.html
#
# email to my last name at head.cfa.harvard.edu with questions and
# comments.
#
#Version 1.0 - 4/26/04 - Initial Version
#Version 1.1 - 5/26/05 - Bug Fix: non-existent host translated to localhost
#Version 1.2 - 12/29/06 - Bug Fix: typo at line 90, wrong var in chop
#

if ($> != 0) {
  print "I have to run as root!\n";
  exit(1);
}

$|=1;

use Socket;

@killlist=(HUP,INT,TERM,KILL);
$SIG{'INT'}=report;

if ($#ARGV < 1) {
  &usage;
  exit(1);
}

$numconns=20;
$timeout=15;
$user=(getpwuid($>))[0];

while ($ARGV[0] =~ /^-/) {
  $opt=shift(@ARGV);
  if ($opt eq "-p") {
    $numconns=shift(@ARGV);
  } elsif ($opt eq "-t") {
    $timeout=shift(@ARGV);
  } elsif ($opt eq "-u") {
    #only allow this if the real user is root.
    if ($< == 0) {
      $user=shift(@ARGV);
    } else {
      print "the -u option requires root priveleges\n";
      exit(1);
    }
  } elsif ($opt eq "-d") {
    $debug=1;
  } elsif ($opt eq "-l") {
    $inputlist=shift(@ARGV);
  } else {
    print "ERROR: unknown option \"$opt\"\n";
    &usage;
    exit(1);
  }
}

if (length($inputlist)) {
  open(LIST,$inputlist);
  while(<LIST>) {
    chop;
    push(@hostlist,$_);
  }
  close(LIST);
  if ($#hostlist == -1) {
    print "ERROR: empty host list\n";
    exit(1);
  }
  $command=join(" ",@ARGV);
} else {
  $command=shift(@ARGV);
  @hostlist=@ARGV;
  #expand a single-arg list into a real list
  if ($#hostlist == 0) {
    @tmp=split(' ',$hostlist[0]);
    if ($#tmp>0) {
      @hostlist=@tmp;
    }
  }
}

$myhost=`hostname`;
chop($myhost);

# make initial connections
for ($contact=0, $i=0; $i<$numconns && $i<=$#hostlist; ++$i, ++$contact) {
  ($handle[$i],$pid[$i])=&myrshinit($hostlist[$contact],$user,$command);
  $hostslot[$i]=$hostlist[$contact];
  $when[$i]=time;
  $killnum[$i]=0;
  ++$numhits;
}

# select loop
while ($numhits) {
  $rin="";
  #set up mask bits
  $num=0;
  for ($i=0; $i<$numconns; ++$i) {
    next if (!defined($handle[$i]));
    vec($rin,fileno($handle[$i]),1)=1;
    ++$num;
  }
  #print "$num filehandles currently open!\n";

  ($nfound,$tym)=select($rout=$rin, undef, undef, 1);

  #check each bit in mask
  for ($i=0; $i<$numconns; ++$i) {
    next if (!defined($handle[$i]));
    if (vec($rout,fileno($handle[$i]),1)) {
      if (eof($handle[$i])) {
	close($handle[$i]);
	--$numhits;
	if ($contact <= $#hostlist) {
	  ($handle[$i],$pid[$i])=&myrshinit($hostlist[$contact],$user,$command);
	  $hostslot[$i]=$hostlist[$contact];
	  $when[$i]=time;
	  $killnum[$i]=0;
	  ++$contact;
	  ++$numhits;
#print "reopened $i\n";
	} else {
	  $handle[$i]=undef;
#print "finshed $i\n";
	}
      } else {
	#$junk=<$handle[$i]>;
	#$input{$hostslot[$i]}.=$junk;
	#print "$hostslot[$i]: $junk";

	if (! read($handle[$i],$buf,8192)) {
	  print "read ERROR: $!\n";
	}
	$buf =~ s/\n/\n$hostslot[$i]: /g;
	$buf =~ s/$hostslot[$i]: $//g;
	print "$hostslot[$i]: $buf";
	#don't timeout if it's already answered
	$when[$i]=0;
      }
    } elsif ($when[$i] && (time-$when[$i] > $timeout)) {
      if ($debug) { print STDERR "timing out $hostslot[$i]\n"; }
      if (! kill(0,$pid[$i])) {
        print "$hostslot[$i]: dead\n";
      } elsif ($killnum[$i]>$#killlist) {
        print "$hostslot[$i]: multi-rsh timeout exceeded, no response to kills\n";
      } else {
        print "$hostslot[$i]: multi-rsh timeout exceeded, attempting kill $killlist[$killnum[$i]] $pid[$i]\n";
        kill($killlist[$killnum[$i]],$pid[$i]);
        ++$killnum[$i];
      }
    }
  }
}

#
# END OF MAIN
#

sub rshinit {
  local($host,$user,$command)=($_[0],$_[1],$_[2]);
  local *FH;

  # DON'T use -n as it causes rsh sockets to exit non-cleanly and temporarily
  # blocks that port -- this is bad because this script can conceivably tie
  # up all free secure ports.
  #$procid=open(FH,"rsh -l $user $host $command 2>&1 |");
  if (! ($procid=open(FH,"-|")) ) {
    setpgrp(0,0);
    close(STDERR);
    open(STDERR,">&STDOUT");
    exec("/usr/ucb/rsh", "-l", $user, $host, $command);
  }
  if ($debug) { print "$host contacted\n"; }
  return (*FH,$procid);
}

sub myrshinit {
  local($host,$user,$command)=($_[0],$_[1],$_[2]);
  local($proto,$iaddr,$there);
  local *FH;

  if (! ($procid=open(FH, "-|")) ) {
    $rshport=514;
    setpgrp(0,0);
    #close(STDERR);
    #open(STDERR, ">&STDOUT");
    select(STDOUT); $|=1;

    $myaddr = inet_aton($myhost);
    $iaddr = inet_aton($host);
    if (length($iaddr) == 0) {
      print "ERROR: Host not found\n";
      exit(1);
    }
    $there = sockaddr_in($rshport, $iaddr);
    $proto = getprotobyname("tcp");
    if (!socket(S,PF_INET,SOCK_STREAM,$proto)) {
      print STDERR "socket: $!\n"; exit(1);
    }
# we don't want this - leads to addr in use errors on connect
# if local and remote host/ports are all the same!
#    if (!setsockopt(S, SOL_SOCKET, SO_REUSEADDR, pack("l", 1))) {
#      print STDERR "setsockopt: $!\n"; exit(1);
#    }
    $done=0;
    $myport=1023;
    while(!$done) {
      $here = sockaddr_in($myport, $myaddr);
      if (bind(S, $here)) {
	#print "using local port $myport\n";
	$done=1;
      }
      if (--$myport == 0) {
	print "$host- ERROR: Can't get a port\n";
	exit(1);
      }
    }
    if (!connect(S,$there)) {
      print STDERR "$host- connect: $!\n";
      exit(1);
    }
    select(S); $|=1; select(STDOUT);
    #send error port (zero means send stderr on same channel as stdout)
    #print S "$errport";
    print S "0\0$user\0$user\0$command\0";
    ##source user
    #print S "$user\0";
    ##destination user
    #print S "$user\0";
    ##command
    #print S "$command\0";

    #rsh verifies the complete connection by returning a null character
    #But we don't want to eat that here; we want to let the main process
    #handle it, so that it can have separate timeouts for the connection
    #and the program.  Well, yeah, but none of that is implemented yet.
    read(S,$buf,1);
    while(read(S,$buf,8192)) {
      print $buf;
    }
    exit(0);
  }
  if ($debug) { print "$host contacted\n"; }
  return (*FH,$procid);
}

sub usage {
  print "Usage: multi-rsh [-p n] [-t n] [-u user] command host [host ...]\n";
  print "       multi-rsh [-p n] [-t n] [-u user] -l inputlist command\n";
  print "       In the first form, command must be a single argument\n";
  print "       In the second form, command can be multiple args\n";
  print "       The second form reads a list of hosts from inputfile\n";
  print "       (- may be used to indicate standard input)\n";
  print "       -p sets number of parallel connections (default 20)\n";
  print "       -t sets timeout in seconds before giving up (default 15)\n";
  print "       -u sets the remote user (defaults to effective uid)\n";
}

sub report {
  local($i);
  for ($i=0; $i<$numconns; ++$i) {
    printf("%2d: %20s %d\n", $i, $hostslot[$i], $when[$i]?time-$when[$i]:-1);
  }
}