#!/bin/sh

# DISH - distributed shell
# Copyright (C) 1998, 1999 Frederick W. Wheeler
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
# Fred Wheeler
# fww@ieee.org

# The GNU General Public License can be found at
# http://www.gnu.org/copyleft/gpl.html

# DISH reads a list of commands from its standard input, one command
# per line.  The commands are executed on a user specified set of
# remote hosts in parallel.  Each remote host runs one command at a
# time.  Whenever a remote host finishes a command, it is assigned the
# next command from the input list.  DISH exits when all commands have
# been run.
#
# The set of remote hosts is specified with the --hosts command line
# option or via the HOSTS environment variable.  The command line
# setting takes precedence over the environment variable.  These hosts
# will usually need to run the same operating system and mount the
# same file server, but that depends on what commands are being run.
#
# With many jobs running in parallel, the standard output of DISH is
# quite messy.
#
# The seq command from the GNU sh-utils package is very handy with DISH.
#
# Example 1: Simple test
#   HOSTS='monet renoir'
#   echo "sleep 2\nsleep 5\nsleep 2\nsleep 2" | dish
#
# Example 2: Use seq and make to organize many jobs
#   seq -f 'nice +20 make %g.run' 100 | dish --hosts='monet renoir'
#
# Example 3: Each command changes directory and saves output
#   HOSTS='monet renoir cezanne'
#   seq 100 | xargs -i echo 'cd ~/workdir; make {}.run > {}.out' | dish
#
# If a host is listed N times in the set of remote hosts, N job slots
# are assigned to that host.  This is handy for machines with multiple
# CPUs.
#
# IMPORTANT CAUTION: don't put and ampersand (&) at the end of
# commands sent to DISH!  If you do this DISH will lose track of
# commands run on the remote hosts, think that the last command
# finished and run another.  This will repeat until you have a big
# mess.
#
# Please contact the author with any suggestions or bug reports.

# Used GNU updatedb version 4.1 from findutils-4.1 as a shell
# programming technique and style guide when writing this script.

# Keywords: dish distributed shell parallel cluster remote execution
#           script GNU GPL Fred Wheeler

# Change Log
#
# Jan 13, 1999: Used vector trick and changed `let' to `expr' to
# convert DISH to a Bourne shell script.  Added check for slot_pid ==
# x so script does not rely on portability of `kill -0 x'.  Changed
# some comments.  Changed version to 0.4.
#
# Sep 21, 2000: The remote shell command is now found by checking in
# order: command line --rsh-command option, DISH_RSH environment
# variable, RSH environment variable, default "remsh".

usage="\
Usage: dish [--hosts='host1 host2...'] [--rsh-command=cmd]
            [--verbose={yes,no}] [--version] [--help]"

verbose=yes
for arg
do
  opt=`echo $arg|sed 's/^\([^=]*\).*/\1/'`
  val=`echo $arg|sed 's/^[^=]*=\(.*\)/\1/'`
  case "$opt" in
    --hosts) HOSTS="$val" ;;
    --rsh-command) DISH_RSH="$val" ;;
    --verbose) verbose="$val" ;;
    --version) echo "dish version 0.4"; exit 0 ;;
    --help) echo "$usage"; exit 0 ;;
    *) echo "dish: invalid option $opt
$usage" >&2
       exit 1 ;;
  esac
done

# if variables are not set by command line or environment, use defaults
: ${HOSTS="localhost"}
# if DISH_RSH is not set use RSH, if RSH is not set use "remsh"
: ${RSH:="remsh"} ${DISH_RSH:="$RSH"}

index="0"
index_list=""
for host in $HOSTS; do
  eval "slot_host$index=\$host"
  eval "slot_pid$index=x"
  index_list="$index_list $index"
  index=`expr $index + 1`
done

# read one command line at a time from standard input
while read cmd; do
  # infinite loop for this command until it gets run in a free job slot
  while true; do
    # loop through each job slot index
    for index in $index_list; do
      # get the PID for this job slot
      eval "pid=\$slot_pid$index"
      # either no PID yet or use SIGNULL to see if job slot has process running
      test $pid != x && kill -0 $pid 1> /dev/null 2>&1
      # if no process is running, start the job in the slot
      if test $? -ne 0; then
        # get the hostname for this slot
        eval "host=\$slot_host$index"
        # if verbose, print the job slot host and command
        if test $verbose = yes; then
          echo "$host: $cmd"
        fi
        # run the command on the remote host
        # the -n switch to rsh/remsh makes it disconnect and run in background
        $DISH_RSH $host -n "$cmd" &
        # record the pid of the remote shell command
        eval "slot_pid$index=$!"
        # break out of enclosing for and while loops to start next command
        break 2
      fi
    done
    # after trying all hosts wait a second before trying again
    sleep 1
  done
done

# wait for all jobs to finish
wait

exit 0
