#!/usr/bin/perl --
# @(#) File: tkping - Machine Status Tool
# @(#) $Source: ~/tkping-0.1.1/tkping

use strict qw(vars subs);	# prevent stupid mistakes
use vars qw($NAME $THISDIR $VERSION $SYNOPSIS $ARGUMENTS $DESCRIPTION);

## Get basename and dirname of program path
($NAME, $THISDIR) = ($0 =~ m|^(.*)/([^/]+)$|o) ? ($2, $1) : ($0, '.');

$VERSION = "0.1.1";

$SYNOPSIS =
   "$NAME [options]";

$ARGUMENTS = "
 -r/ows {nbr}       Set the {nbr} of rows to be displayed
 -c/olumns {nbr}    Set the {nbr} of columns to be displayed
 -w/intitle {titleStr}  Set window title to {titleStr}
 -i/gnore           Do not ping grid automatically, wait for manual rqst

 -p/ackets {nbr}    {nbr} of packets to send to each node (default 5)
 -s/leep {secs}     {secs} between traversals of the host list (default 120)
 -t/imeout {mSecs}  How long to wait for packet before assuming node no longer
                     responding (default 500 milliseconds)

 -x/defs {fSpec}    Load default values from {fSpec} instead of default
                     (default /etc/tkping/tkping.conf)
 -n/odes {fSpec}    Load host (node) list from {fSpec} instead of default
                     (default ~/.tkpingrc)
 -l/ogfile {fSpec}  Log error/fail events to {fSpec}
 
 -h/elp        Display (h)elp (this screen)
 -d/ebug       (d)ebug: display DEBUG messages   (for use by developer)
 -v/erbose     (v)erbose: show more detailed info regarding progress

NOTE: options can be in any order.
   
RETURN CODES:
 0 - successful (or help was requested)
 2 - invalid parms, internal error, etc.\n
";


$DESCRIPTION = "
$NAME - Ver$VERSION Machine Status Tool
   
by Stephen Moraco, stephen\@debian.org
Copyright (c) 2001, Stephen M Moraco.\n
";

### =====================================================================
###                     Hook to external pieces parts
### ---------------------------------------------------------------------
### 

### !!Uncomment the following line to help in debugging use statements!!
# BEGIN { $Exporter::Verbose = 1; }   # good Package debugging technique...

require 5.004;

use English;
use lib "/usr/share/tkping";
use lib ".";

require "newgetopt.pl";

use Tk;
use Tk qw(:eventtypes);
use Tk::widgets qw(Dialog Button Label Entry Menubar);
require Tk::ErrorDialog;
use Tk::After;

use tkpingLib 1.01;	#  import helper routines... 
use tkpingLib qw(
   $NOTSET_STR
);

## Force a flush after every write or print on the currently selected output
## filehandle.
use FileHandle;
STDOUT->autoflush;
STDERR->autoflush;


###
### ---------------------------------------------------------------------
###                  End of external pieces parts hooks
### =====================================================================

my $glbNbrRows = 5;			# default
my $glbNbrColumns = 1;		# default
my $glbTimeoutMillSecs = 500;	# default
my $glbSleepSecs = 120;		# default
my $glbNbrPackets = 5;		# default
my $glbTitleStr = $NOTSET_STR;
my $glbLogFspec = $NOTSET_STR;
my $glbHostsFspec = $NOTSET_STR;
my $glbConfFspec = $NOTSET_STR;

my %glbConfigDataHs = ();

$newgetopt::ignorecase = 0;   # force case-sensitivity!

my @origArgsAr = @ARGV; # Capture before polluted by interpretation
my $origArgsStr = join " ", @origArgsAr;

Usage( 0 ) if( $ARGV[0] eq "\-\?" );

my $rc = NGetOpt("debug", "help", "verbose", "ignore", 
              "rows:i", \$glbNbrRows, "columns:i", \$glbNbrColumns,
              "timeout:i", \$glbTimeoutMillSecs, "sleep:i", \$glbSleepSecs, 
              "wintitle:s", \$glbTitleStr, "logfile:s", \$glbLogFspec,
              "nodes:s", \$glbHostsFspec, "packets:i", \$glbNbrPackets,
              "xdefs:s", \$glbConfFspec);

use vars qw($opt_debug $opt_help $opt_verbose $opt_ignore 
            $opt_rows $opt_columns $opt_timeout $opt_sleep $opt_wintitle
			$opt_logfile $opt_nodes $opt_packets $opt_xdefs);

$^W = 1;	# I want to see warnings from here to end-of-file!

### User asked for help...  This should always be _fast_ so is here before
### further validation!
Usage(0) if ($opt_help);	# exit with success (0) return code!

Usage(2) if (!$rc);			# exit with error (2) if option parse failure

###
###  Validate environment
###
###   [Here we validate that key directories, key central files exist, etc.]
###
DebugMsg("Checking environment");

my $LOGFILE = "$NAME.log";
SetLog($LOGFILE);

my ($realId,$effId) = GetUserNames();

my $FPING_CMD = "/usr/bin/fping";

###
###  Validate/internalize parms
###
###   [Here we validate parm combinations, presence and translate for later use]
###
###
DebugMsg("Evaluating parms");

###
###  Get and deep-validate/xlate parms
###

if(@ARGV > 0) {
	Msg(-err,-nl,"Too many parms! [$origArgsStr]\007");
	$rc = 0;
}

$rc =0 if(!RequireParm("wintitle", $glbTitleStr, "{titleStr}"));
$rc =0 if(!RequireParm("nodes", $glbHostsFspec, "{fspec}"));
$rc =0 if(!RequireParm("xdefs", $glbConfFspec, "{fspec}"));
$rc =0 if(!RequireParm("logfile", $glbLogFspec, "{fspec}"));


if(! $rc) {
	Usage(2);
}

###  determine if any options require command line only MODE!

###
###  Initialize Global Variables
###
my $NOTSET_FLAG = "** NOT set **";

###  This is the list of supported parameters.
my %glbValidParametersHs = ();
$glbValidParametersHs{'allPacketsBackColor'} = 1;
$glbValidParametersHs{'somePacketsBackColor'} = 1;
$glbValidParametersHs{'noPacketsBackColor'} = 1;
$glbValidParametersHs{'ignoredColor'} = 1;
$glbValidParametersHs{'changedColor'} = 1;
$glbValidParametersHs{'errorColor'} = 1;
$glbValidParametersHs{'Rows'} = 1;
$glbValidParametersHs{'Columns'} = 1;
$glbValidParametersHs{'Packets'} = 1;
$glbValidParametersHs{'Sleep'} = 1;
$glbValidParametersHs{'Timeout'} = 1;
$glbValidParametersHs{'WinTitle'} = 1;
$glbValidParametersHs{'Ignore'} = 1;
$glbValidParametersHs{'Logfile'} = 1;

###  Now declare text-color and active-text-color settings to be valid
foreach my $colorKey (grep /Color$/, (keys %glbValidParametersHs)) {
	my $reasonPrefix = $colorKey;
	$reasonPrefix =~ s/Color$//;
	$glbValidParametersHs{"${reasonPrefix}TextColor"} = 1;
	$glbValidParametersHs{"${reasonPrefix}ActiveTextColor"} = 1;
}


###
###  !LIVE! variables!!! (write these and the main window updates!
###
my $glbGeneralStatusMsg = "";
my $glbEvaluationStatusMsg = "";


###
###  Locate/identify the files we are about to load
###
if(!defined $opt_nodes) {
	my $HOMEDIR = $ENV{'HOME'};
	$glbHostsFspec = "$HOMEDIR/.tkpingrc";
	my $TEST_HOSTS = "./test.hosts";
	if(-f $TEST_HOSTS) {
		$glbHostsFspec = $TEST_HOSTS;  ###  Overide with test file if present
	}
}
DebugMsg("glbHostsFspec=[$glbHostsFspec]");

if(!defined $opt_xdefs) {
	$glbConfFspec = "/etc/tkping/tkping.conf";
	my $TEST_CONF = "./test.conf";
	if(-f $TEST_CONF) {
		$glbConfFspec = $TEST_CONF;
	}
}
DebugMsg("glbConfFspec=[$glbConfFspec]");

LogMsg("  by $effId as: $NAME $origArgsStr");


my $MW;		# our main window object

my $glbModeCommandLineOnly = 0;

### ---------------------------------------------------------------------
###  Status tracking variables, used in INFO dialog
###
my $glbApplicationStartTimeStr = `/bin/date`;
chomp $glbApplicationStartTimeStr;

my $glbRcvdAllPktsSinceTimeStr = $glbApplicationStartTimeStr;
my $glbCurrentStatusStr = "Returning all packets";

###
###  track average response time for each host
###
my %glbRunningAverageForLastTraversalsByHostHs = ();
###  then for each host we have
$glbRunningAverageForLastTraversalsByHostHs{5} = 0;
$glbRunningAverageForLastTraversalsByHostHs{50} = 0;
$glbRunningAverageForLastTraversalsByHostHs{100} = 0;
$glbRunningAverageForLastTraversalsByHostHs{200} = 0;

###
###  track quality of response for each traversal of each host
###
my %glbTraversalQualityCountByHostHs = ();
$glbTraversalQualityCountByHostHs{'none'} = 0;
$glbTraversalQualityCountByHostHs{'some'} = 0;
$glbTraversalQualityCountByHostHs{'all'} = 0;



### =====================================================================
###                        Begin Subroutines
### ---------------------------------------------------------------------
###  onQuit()  - give user  chance to make final saves before exiting
###       (This procedure is called when the user clicks on 'file->quit')
###
sub onQuit()
{
	DebugMsg("onQuit() - entry");
	return;

	my $dlgTxt = "You've modifed the shiplist but have not saved it." .
	             "\nDo you wish to save these changes?";
	
	my $DIALOG_SAVE_CHANGED_LIST = $MW->Dialog(
					-title     => "Save modified list?",
					-bitmap         => 'question',
					-default_button => 'CANCEL',
					-buttons        => ['CANCEL','SAVE & EXIT','EXIT'],
					-justify        => 'center',
					-text           => $dlgTxt,
	);
	my $buttonPressed = $DIALOG_SAVE_CHANGED_LIST->Show('-global');
	foreach ($buttonPressed) {
		/^CANCEL$/ || /^EXIT$/ and do {
			if($_ =~ /^EXIT$/) {
				LogMsg(" ** NOT Saving changes");
			}
			last;
		};
		/^SAVE \& EXIT$/ and do {
			last;
		};
		FatalMsg("(INTERNAL) failed to recognize button from saveList dialog!");
	}
	if($buttonPressed =~ /EXIT/) {
		doExit(0,"exit with or without saving");
	}
	#  return cause user cancelled the from the modifed-need-to-save dialog
	DebugMsg("Cancelled quit!");
}
	


### ---------------------------------------------------------------------
###  doExit()  - do final wrapup...
###
sub doExit($$)
{
	my $retCode = shift;
	my $exitMsg = shift;
	
	###  shut down our ping-ing loop....
	killPingLoop();
	
	###
	###  We're Done!!!
	###
	DebugMsg("Done");
	if($retCode) {
		LogMsg("Exit RC=$retCode: $exitMsg");
	}
	EndLog();
	exit($retCode);	# exit with or without saving
}


### ---------------------------------------------------------------------
###  beep - make noise when called!
###
sub beep()
{
	$MW->bell;
}


my $OUR_MARKER = "-- NO MORE HOSTS AFTER HERE --";

### 
###  If we need a GUI, generate/display it!   xxCOLORxx
###
my %glbButtonObjByHostNameHs = ();   # button-OBJ by hostname (full set)
my %glbDownedHostsHs = ();		     # button-OBJ by hostname (deactivated subset!)
my %glbSelectedHostsHs = ();	     # button-OBJ by hostname (selected subset!)
my %glbPriorHostColorHs = ();
my $glbDefaultButtonColor = "";

my $currentRightMouseHost = "";

my %glbSumPingReturnedByHostHs = ();
my %glbTotalPingsByHostHs = ();

sub trackHostResults($$)
{
	my $hostNm = shift;
	my $returnedPingCt = shift;

	$glbSumPingReturnedByHostHs{$hostNm} += $returnedPingCt;
	$glbTotalPingsByHostHs{$hostNm} += $glbNbrPackets;

	my $reasonId = "";

	if($glbSumPingReturnedByHostHs{$hostNm} ==
	   $glbTotalPingsByHostHs{$hostNm}) {
		$reasonId = "AllPacketsBack";	###  GREEN
	} elsif($returnedPingCt == $glbNbrPackets) {
		$reasonId = "Changed";			###  GREY
	} elsif($returnedPingCt > 0) {
		$reasonId = "SomePacketsBack";	###  YELLOW     
	} elsif($returnedPingCt < 1) {
		$reasonId = "NoPacketsBack";   	###  RED
	} else {
		$reasonId = "????";     
	}

	my $isCurrAll = ($returnedPingCt == $glbNbrPackets) ? 1 : 0;
	my $isCurrSome = ($returnedPingCt > 0) ? 1 : 0;
	my $isCurrNone = ($returnedPingCt < 1) ? 1 : 0;
}


### ---------------------------------------------------------------------
###  accumulateStatsFromFpingData - process data returned from fping(1)
###
sub accumulateStatsFromFpingData(@)
{
	my @rawFPingDataAr = @_;
	my %hostListHs = ();
	my $hostListCt = ();

	###  NOTE: $glbNbrPackets is our required number of 
	###    responses for each host
	my @summaryResultsAr = ();

	my %deadHostsHs = ();
	my %unreachableHostsHs = ();

	my %pingReturnCtrsByHostHs = ();

	###  NOTE: host list is passed first (then sep) then fping output

	my $inHostList = 1;
	foreach my $line (@rawFPingDataAr) {
		if($inHostList) {
			###  have new host entry (we should have ping results for this host!
			if($line ne $OUR_MARKER) {
				$hostListHs{$line} = 1;	# add new host
			} else {
				$inHostList = 0;
				$hostListCt = (keys %hostListHs);
				DebugMsg("* Pinging $hostListCt hosts in list");
				map { $pingReturnCtrsByHostHs{$_} = 0; } (keys %hostListHs);
				next;
			}
		} else {
			###  we have the full hostlist, now let's process real
			###   fping results data
			if($line =~ /^\s*$/) {
				next;	#  skip blank lines
			} elsif($line =~ /Host Unreachable.*sent\sto\s(\w+)\s/i) {
				###  host names to mark as partially avail...
				my $deadHost = $1;
				$deadHostsHs{$deadHost} = 1;
				DebugMsg("Dead host[$deadHost] = [$line]");
			} elsif($line =~ /^(\w+)\saddress not found/i) {
				###  host names to turn black (bad names)
				my $unkHost = $1;
				$unreachableHostsHs{$unkHost} = 1;
				DebugMsg("Unk host[$unkHost] = [$line]");
			} elsif($line =~ /^.*:\s+\[.*bytes.*\(.*$/) {
    			# host-ok : [0], 64 bytes, 0.81 ms (0.81 avg, 0% loss)
				###  is ping results for one ping
				###  \$1 is hostname
				my $hostNm = $line;
				$hostNm =~ s/\s+:\s+.*$//;
				$pingReturnCtrsByHostHs{$hostNm}++;
				###  \$2 is ping attempt nbr
				my $attemptNbr = $line;
				$attemptNbr =~ s/^[^\[]+\[//;
				$attemptNbr =~ s/\],.*$//;
				###  \$3 is response time ex:(0.03 ms)
				my $responseTime = $line;
				$responseTime =~ s/^.*bytes,\s+//;
				$responseTime =~ s/\s\(.*$//;
				DebugMsg("Rlst: #${attemptNbr} host=[${hostNm}],dur=[${responseTime}]\n\t - line=[$line]");

			} elsif($line =~ /^.*:.*$/) {
				###  summary line, parse 2 determine 'all', 'none', 'one' pings
				#  save summaries for now...
				if($line =~ /(\w+)\s:\s[-\s]+$/) {
					my $justWentDownHostNm = $1;
					$deadHostsHs{$justWentDownHostNm} = 1;
					DebugMsg("Missed All pings!: [$line]");
				} else {
					push @summaryResultsAr, $line;
					DebugMsg("Ignoring: [$line]");
				}
			} else {
				DebugMsg("??? line=[$line]");
			}
		}
	}
	foreach my $hostNm (keys %pingReturnCtrsByHostHs) {
		my $returnCt = $pingReturnCtrsByHostHs{$hostNm};
		if($returnCt == $glbNbrPackets) {
			###  set host GOOD (GREEN)
		 	setHostColor($glbButtonObjByHostNameHs{$hostNm},
			             $glbConfigDataHs{'allpacketsbackcolor'});
		} elsif($returnCt > 0)  {
			###  set host PARTIAL (???)
		 	setHostColor($glbButtonObjByHostNameHs{$hostNm},
			             $glbConfigDataHs{'somepacketsbackcolor'});
		} else {
			###  Hmmm no reponse... is it unreachable or dead?
			if(exists $deadHostsHs{$hostNm}) {
				###  set host UNRESPONSIVE (RED)
		 		setHostColor($glbButtonObjByHostNameHs{$hostNm},
				             $glbConfigDataHs{'nopacketsbackcolor'});
			} elsif(exists $unreachableHostsHs{$hostNm}) {
				###  set host UNKNOWN (BLACK)
		 		setHostColor($glbButtonObjByHostNameHs{$hostNm},
				             $glbConfigDataHs{'errorcolor'});
			} else {
				Msg(-err,"[CODE] not sure about host=[$hostNm]");
			}
		}
	}

    ### ---------------------------------------------------------------------
    ###  Example fping(8) v2.2b2-3 (at time of this writing) output
    ### ---------------------------------------------------------------------
    # $ fping -C 5 -b 36 host-ok host-off
    # RET_CODE=1
    # --- stdout ---
    # host-ok : [0], 64 bytes, 0.81 ms (0.81 avg, 0% loss)
    # host-ok : [1], 64 bytes, 0.43 ms (0.62 avg, 0% loss)
    # host-ok : [2], 64 bytes, 0.42 ms (0.55 avg, 0% loss)
    # host-ok : [3], 64 bytes, 0.44 ms (0.52 avg, 0% loss)
    # host-ok : [4], 64 bytes, 0.43 ms (0.50 avg, 0% loss)
    # --------------
    # --- stderr ---
    # ICMP Host Unreachable from 10.0.0.4 for ICMP Echo sent to host-off (10.0.0.6)
    # ICMP Host Unreachable from 10.0.0.4 for ICMP Echo sent to host-off (10.0.0.6)
    # ICMP Host Unreachable from 10.0.0.4 for ICMP Echo sent to host-off (10.0.0.6)
    #  
    # host-ok : 0.1 0.3 0.2 0.4 0.3
    # host-off : - - - - -
    # --------------
    ### ---------------------------------------------------------------------
    # $ fping -C 5 -b 36 host-ok host-off host-bad 
    # RET_CODE=2
    # --- stdout ---
    # host-ok : [0], 64 bytes, 0.48 ms (0.48 avg, 0% loss) 
    # host-ok : [1], 64 bytes, 0.42 ms (0.45 avg, 0% loss) 
    # host-ok : [2], 64 bytes, 0.46 ms (0.45 avg, 0% loss) 
    # host-ok : [3], 64 bytes, 0.44 ms (0.45 avg, 0% loss) 
    # host-ok : [4], 64 bytes, 0.44 ms (0.44 avg, 0% loss) 
    # -------------- 
    # --- stderr --- 
    # host-bad address not found 
    # ICMP Host Unreachable from 10.0.0.4 for ICMP Echo sent to host-off (10.0.0.6) 
    # ICMP Host Unreachable from 10.0.0.4 for ICMP Echo sent to host-off (10.0.0.6) 
    # ICMP Host Unreachable from 10.0.0.4 for ICMP Echo sent to host-off (10.0.0.6)  
    # host-ok : 0.8 0.2 0.6 0.4 0.4 
    # host-off : - - - - - 
    # -------------- 
    #
    ### ---------------------------------------------------------------------
    # # $ fping -C 5 -b 36 host-ok 
    # RET_CODE=0 
    # --- stdout --- 
    # host-ok : [0], 64 bytes, 0.44 ms (0.44 avg, 0% loss) 
    # host-ok : [1], 64 bytes, 0.44 ms (0.44 avg, 0% loss) 
    # host-ok : [2], 64 bytes, 0.45 ms (0.44 avg, 0% loss) 
    # host-ok : [3], 64 bytes, 0.44 ms (0.44 avg, 0% loss) 
    # host-ok : [4], 64 bytes, 0.59 ms (0.47 avg, 0% loss) 
    # -------------- 
    # --- stderr ---  
    # host-ok : 0.4 0.4 0.5 0.4 0.9 
    # --------------
    ### ---------------------------------------------------------------------
    # # $ fping -C 100 -b 36 host-c  # (host appears after ping starts)
    # RET_CODE=1 
    # --- stdout --- 
    #    STATS NOT SHOWN
    # --------------
    # --- stderr ---  
    #    ERRORS NOT SHOWN
    # host-c : - - - - - - - - - - - - - - - - - - - - - - - - 1092.3 134.1 1.6 \\
    # 1.5 1.9 1.4 1.6 1.5 1.2 1.0 1.5 1.6 1.3 1.7 1.4 1.4 1.2 1.6 1.3 1.0 1.1 1.3 \\
    # 1.2 3.6 1.8 1.5 1.9 1.7 1.1 1.7 1.9 1.0 1.9 1.9 1.1 1.2 1.0 1.6 1.1 1.9 \\
    # 1.9 1.7 1.0 1.7 1.0 1.3 1.2 1.9 1.1 1.0 1.3 1.8 1.4 1.7 1.3
    # --------------
    ### ---------------------------------------------------------------------
}

### ---------------------------------------------------------------------
###  pingHostsAndAccumulateStats - ping one or more hosts and hand-off 
###                                data to be processed
###
sub pingHostsAndAccumulateStats(@)
{
	my @hostListAr = @_;

	my $hostLst = join " ",@hostListAr;

	if(@hostListAr < 1) {
		DebugMsg("Aborting ping, no active hosts!");
		return;	###  No hosts to ping!
	}

	###  force display update (makes it harder to catch button pressed...)
	$glbEvaluationStatusMsg	= "Ping";
	
	$MW->idletasks();   ###  force display update
	
	#
	#my $cmdStr = "$FPING_CMD -c 5 host host host host host, etc."
	#   NOTE: 5 is a setting, and host.. is list of current buttons...
	#
	my @stdoutAr = ();
	my @stderrAr = ();
	my $cmdStr = "$FPING_CMD -C $glbNbrPackets -t $glbTimeoutMillSecs $hostLst";
	my $retCode = DoCmdRetOutput($cmdStr,\@stdoutAr,\@stderrAr);
	#  NOTE: fping returns the following error codes which are OK
	#          rc=0: all OK, 
	#          rc=1: some unreachable & some ok, 
	#          rc=2: some not found & some not reachable & some ok
	#   Also: summaries arrive in STDERR output
	if($retCode < 0 || $retCode > 2) {
		Msg(-err,"$FPING_CMD failed retCode=[$retCode]");
		map { print STDERR "$NAME(ERR): $_\n";  } @stderrAr;
		map { print STDERR "$NAME(OUT): $_\n";  } @stdoutAr;
		FatalMsg("Aborted");
	}
	#
	#  We have good fping(1) data let's process it
	#
	accumulateStatsFromFpingData(@hostListAr, $OUR_MARKER, 
	                             @stdoutAr, @stderrAr);

	$glbEvaluationStatusMsg	= "IDLE";
	###  force display update (makes it harder to catch button pressed...)
	$MW->idletasks();   ###  force display update
}

my $glbButtonGrid;
my $glbMenuBar;
my %glbActiveHostsHs = ();		     # button-OBJ by hostname (active subset!)

### ---------------------------------------------------------------------
###  pingEmAll - iterate over hosts pinging each and gathering stats
###
sub pingEmAll()
{
	#  account for any late color changes
	$MW->idletasks();   ###  force display update

	#  enter ping mode
	$glbEvaluationStatusMsg	= "Ping";

	$glbButtonGrid->Busy(-recurse => 1);
	$glbMenuBar->Busy();
	$MW->idletasks();   ###  force display update
	
	###  issue the ping command (currently active hosts)
	###  and process ping-returned data
	pingHostsAndAccumulateStats((keys %glbActiveHostsHs));

	#  return to normal user interaction
	$glbMenuBar->Unbusy();
	$glbButtonGrid->Unbusy();
	$glbEvaluationStatusMsg	= "IDLE";
	$MW->idletasks();   ###  force display update
}


my $glbPingLoopId = 0;

### ---------------------------------------------------------------------
###  setupPingLoop - we're starting (or restarting) ping-loop
###
sub setupPingLoop()
{
	my $LOOP_INTERVAL_120MS = (2 * 60  * 1000);	# 2 minutes...
	my $LOOP_INTERVAL_50MS = (50 * 1000);	# 50 seconds...
	
	$glbPingLoopId = $MW->repeat($LOOP_INTERVAL_50MS,\&pingEmAll);
}


### ---------------------------------------------------------------------
###  killPingLoop - we're shutting down (or we've stopped loop), kill ping loop
###
sub killPingLoop()
{
	if($glbPingLoopId) {
		$MW->afterCancel($glbPingLoopId);
		$glbPingLoopId = 0;
	} else {
		DebugMsg("killPingLoop() - already killed!");
	}
}




### --------------------------------------------------------------------------
###  setHostColor - given host name get object and inform it of new color
###                 change, return the color before the change to the caller
###
sub setHostColor($$;$)
{
	my $btnObj = shift;
	my $colorValue = shift;

	my $colorReason = shift;

#	DebugMsg("btnObj=[$btnObj], colorValue=[$colorValue]");

	my $priorBGcolor = $btnObj->cget('-background');
	$btnObj->configure(-background => $colorValue);
	$btnObj->configure(-activebackground => $colorValue);
	if($colorValue eq "black") {
		$btnObj->configure(-foreground => 'white');
		$btnObj->configure(-activeforeground => 'red');
	} else {
		$btnObj->configure(-foreground => 'black');
		$btnObj->configure(-activeforeground => 'white');
	}
	return $priorBGcolor;
}


### --------------------------------------------------------------------------
###  mvDown2Active - 
###
sub mvDown2Active($)
{
	my $hostNm = shift;

	if(exists $glbDownedHostsHs{$hostNm}) {
		DebugMsg(" - moving host [$hostNm] from down to active");
		$glbActiveHostsHs{$hostNm} = $glbDownedHostsHs{$hostNm};
		delete $glbDownedHostsHs{$hostNm};
		###  now reset to default color!
	 	setHostColor($glbActiveHostsHs{$hostNm},
		             $glbDefaultButtonColor);
	} else {
		Msg(-war,"Attempt to activate host [$hostNm] 2nd time!");
	}
}


### --------------------------------------------------------------------------
###  mvActive2Down - 
###
sub mvActive2Down($)
{
	my $hostNm = shift;

	if(exists $glbActiveHostsHs{$hostNm}) {
		DebugMsg(" - moving host [$hostNm] from active to down");
		$glbDownedHostsHs{$hostNm} = $glbActiveHostsHs{$hostNm};
		delete $glbActiveHostsHs{$hostNm};
		###  now color as down!
 		setHostColor($glbDownedHostsHs{$hostNm},
		             $glbConfigDataHs{'ignoredcolor'});
	} else {
		Msg(-war,"Attempt to down host [$hostNm] 2nd time!");
	}
}



### --------------------------------------------------------------------------
###  markSelectedHostsDown - user clicked on host buttons, then picked
###                          File->Down.  This sets selected hosts to IGNORE
###                          (moves them to down list from active!)
### 
sub markSelectedHostsDown(;$)
{
	DebugMsg("markSelectedHostsDown() - ENTRY");
	use vars qw( %glbConfigDataHs );

	my $singleHostNm = shift;

	my %selectedHostsHs = ();

	if(defined $singleHostNm) {
		$selectedHostsHs{$singleHostNm} = 
		        $glbButtonObjByHostNameHs{$singleHostNm};
	} else {
		%selectedHostsHs = %glbSelectedHostsHs;
	}

	###  iff we have entries selected, do...
	if((keys %selectedHostsHs) > 0) {
		foreach my $hostNm (keys %selectedHostsHs) {
			###  If really selected... (not by popup menu...)
			if(exists $glbSelectedHostsHs{$hostNm}) {
				###  toggle selection state, removing from the list, too
				toggleHostSelection($hostNm);	#  Use toggle to un-select!
			}
			###  move to down status, if not already down
			if(!exists $glbDownedHostsHs{$hostNm}) {
				mvActive2Down($hostNm);
			}
		}
	}
}


### --------------------------------------------------------------------------
###  markSingleHostDown - user right-clicked on a single host button then
###                       picked down.  This deactivates future pings for
###                       this host.
sub markSingleHostDown() 
{
	DebugMsg("markSingleHostsDown() - ENTRY");

	markSelectedHostsDown($currentRightMouseHost);	#  Now, mark host down...
}


### --------------------------------------------------------------------------
###  clearAllHosts - user picked 'Edit->Clear Selection'.  So... we unselect 
###                    all selected hosts!
sub clearAllHosts()
{
	foreach my $hostNm (keys %glbSelectedHostsHs) {
		###  toggle selection state, removing from the list, too
		toggleHostSelection($hostNm);	#  Use toggle to un-select!
	}
}


### --------------------------------------------------------------------------
###  selectAllHosts - user picked 'Edit->Select All'.  So... we move all hosts
###                    to selected list and post color changes accordingly
sub selectAllHosts()
{
	### foreach host 
	foreach my $hostNm (keys %glbButtonObjByHostNameHs) {
		###	if not already selected...
		if(!exists $glbSelectedHostsHs{$hostNm}) {
			###  toggle selection state, adding to list, too
			toggleHostSelection($hostNm);	#  Hey! use toggle to un-select!
		}
	}
}


### --------------------------------------------------------------------------
###  recheckAllHosts - user picked 'File->Recheck All'.  So... we move any from
###                    down list back to active status!  We also unselect all
###                    selected!
sub recheckAllHosts()
{
	###  unselect selected-hosts before resetting selection list!
	if((keys %glbSelectedHostsHs) > 0) {
		###  for each selected host...
		foreach my $hostNm (keys %glbSelectedHostsHs) {
			toggleHostSelection($hostNm);	#  Hey! use toggle to un-select!
		}
	}
	###  iff we have down-entries, do...
	if((keys %glbDownedHostsHs) > 0) {
		foreach my $hostNm (keys %glbDownedHostsHs) {
			mvDown2Active($hostNm);
		}
		%glbDownedHostsHs = ();		# clear our downed list
	}

	###  issue the ping command (all hosts, after making all active again)
	###  and process ping-returned data
	pingHostsAndAccumulateStats((keys %glbActiveHostsHs));
}


### --------------------------------------------------------------------------
###  recheckSelectedHosts - user clicked on host buttons, then picked
###                         File->Recheck.  This reactivates selected 
###                         hosts if they were downed!
###                          (moves them to active list from down list!)
### 
sub recheckSelectedHosts()
{
	DebugMsg("recheckSelectedHosts() - ENTRY");
	my @hostsToPingAr = ();

	###  iff we have entries, do...
	if((keys %glbSelectedHostsHs) > 0) {
		###  for each selected host...
		foreach my $hostNm (keys %glbSelectedHostsHs) {
			###  Mark this host as needing ping
			push @hostsToPingAr, $hostNm;
			###  unselect... our button
			toggleHostSelection($hostNm);	#  let's use toggle to un-select!
			###  if was marked down, awaken it
			if(exists $glbDownedHostsHs{$hostNm}) {
				###  add to active list
				mvDown2Active($hostNm);
			}
		}

		###  issue the ping command (selected hosts) 
		###  and process ping-returned data
		pingHostsAndAccumulateStats(@hostsToPingAr);
	}
}


### --------------------------------------------------------------------------
###  recheckSingleHost - user right-clicked on a host button then
###                      picked recheck.  This reactivates the selected
###                      host if it was downed!
###                       (moves it to the active list from down list!)
sub recheckSingleHost()
{
	DebugMsg("recheckSingleHost() - ENTRY");
	my $singleHost = $currentRightMouseHost;

	#  capture prior selections
	my %holdSelectedHostsHs = %glbSelectedHostsHs;

	%glbSelectedHostsHs = ();		#  empty it
	$glbSelectedHostsHs{$singleHost} = $glbButtonObjByHostNameHs{$singleHost};

	recheckSelectedHosts();		#  Now, recheck selected...

	#  restore prior selections
	%glbSelectedHostsHs = %holdSelectedHostsHs;
}


### --------------------------------------------------------------------------
###  displayInfoFor1stSelectedHost - user clicked on one or more host buttons
###                       then picked File->Info.  This displays a status 
###                       dialog showing ping history for only the first host 
###                       in the list and app runtime info.
###
sub displayInfoFor1stSelectedHost()
{
#	if((keys %glbSelectedHostsHs) > 0) {
#	}
	beep();
}


### --------------------------------------------------------------------------
###  displayInfoForSingleHost - user right-clicked on a single host button then
###                       picked info.  This displays a status dialog showing
###                       ping history for this host and app runtime info.
sub displayInfoForSingleHost()
{
	my $singleHost = $currentRightMouseHost;

	#  capture prior selections
	my %holdSelectedHostsHs = %glbSelectedHostsHs;

	%glbSelectedHostsHs = ();		#  empty it
	$glbSelectedHostsHs{$singleHost} = $glbButtonObjByHostNameHs{$singleHost};

	displayInfoFor1stSelectedHost();

	#  restore prior selections
	%glbSelectedHostsHs = %holdSelectedHostsHs;
}


### --------------------------------------------------------------------------
###  toggleHostSelection - [button press] invert selection color, if selects
###                        add to the list of selections, else remove from the
###                        list.
###
sub toggleHostSelection($)
{
#	my $btnObj = shift;
	my $hostNm = shift;

	#  if the code is broken (inactive buttons get pressed)
	#  we may come thru here with "" hostnames!
	if(!defined $hostNm || $hostNm eq "") {
		return;		# exit without doing anything
	}

	###  if already selected...
	if(exists $glbSelectedHostsHs{$hostNm}) {
		###  decolor host (revert to prior color)!
		setHostColor($glbSelectedHostsHs{$hostNm}, 
		             $glbPriorHostColorHs{$hostNm});
		###  deselect host!
		delete $glbSelectedHostsHs{$hostNm};
	} else {
		###  select host!
		if(!exists $glbButtonObjByHostNameHs{$hostNm}) {
			FatalMsg("[CODE] hostNm[$hostNm] NOT found in master button list!");
		}
		#  record our hostname and button object as selected
		$glbSelectedHostsHs{$hostNm} = $glbButtonObjByHostNameHs{$hostNm};
		###  color host as selected!
		$glbPriorHostColorHs{$hostNm} =
		   setHostColor($glbSelectedHostsHs{$hostNm},"#00FFFF");
		#  if never been done, capture default color!
		#   NOTE: this happens on first button ever colored
		if($glbDefaultButtonColor eq "") {
			$glbDefaultButtonColor = $glbPriorHostColorHs{$hostNm};
		}
	}
}


### ---------------------------------------------------------------------
###  loadParmData - load our configuration data including color, rates and
###                 durations, etc.
###
sub loadParmData($)
{
	my $fSpec = shift;

	my @rawParmsDataAr = File2Array($fSpec);
	my %actualParmDataHs = ();

	my %lcParmListHs = ();
	foreach my $parmId (keys %glbValidParametersHs) {
		my $parm = lc $parmId;
		$lcParmListHs{$parm} = 1;
	}
	
	foreach (@rawParmsDataAr) {
		$_ =~ s/#.*$//g;	#  remove comments
		$_ =~ s/\s+$//g;	#  remove trailing white-space
		$_ =~ s/^\s+//g;	#  remove leading white-space
		if($_ =~ /^\s*$|^$/) {
			next;	# skip blank lines
		}
		if($_ =~ /^[^:\s]+:\s+/) {
			my ($parmNm, $value) = split /:\s+/,$_,2;
			my $lcParmNm = lc $parmNm;	#  force to lc
			if(!exists $lcParmListHs{$lcParmNm}) {
				Msg(-err,"Ignoring BAD(unknown) parm [$parmNm]=[$value]");
				next;
			}
			$value = lc $value;	#  force to lc
			$value =~ s/0x/#/;	#  put back the # color-value lead-in
			$actualParmDataHs{$lcParmNm} = $value;
			DebugMsg("parmLoader() $lcParmNm=[$value]");
			next;   # skip paramter overrides, too
		}
		DebugMsg("parmLoader() skipping host data [$_]");	
	}
	###  Now declare text-color and active-text-color settings 
	###   iff not given in file just loaded
	my @colorKeyAr = (grep /Color$/i, (keys %actualParmDataHs));
	foreach my $colorKey (grep !/TextColor$/i, @colorKeyAr) {
		my $reasonPrefix = $colorKey;
		$reasonPrefix =~ s/Color$//i;
		my $color = $actualParmDataHs{$colorKey};
		my ($nml, $actv) = ($color =~ /black|#000000/i) ? ("white","red") : ("black", "white");
		my $keyNm = lc "${reasonPrefix}TextColor";
		if(!exists $actualParmDataHs{$keyNm}) {
			$actualParmDataHs{$keyNm} = $nml;
			DebugMsg("+$keyNm: $nml");
		}
		$keyNm = lc "${reasonPrefix}ActiveTextColor";
		if(!exists $actualParmDataHs{$keyNm}) {
			$actualParmDataHs{$keyNm} = $actv;
			DebugMsg("+$keyNm: $actv");
		}
	}
	return %actualParmDataHs;
}


### ---------------------------------------------------------------------
###  loadHostData - load list of hosts to be monitored, maybe even placement
###                 and labellinbg data as well
###
sub loadHostData($)
{
	my $fSpec = shift;

	my @rawHostsDataAr = File2Array($fSpec);
	my @actualHostDataAr = ();
	
	foreach (@rawHostsDataAr) {
		$_ =~ s/#.*$//g;	#  remove comments
		$_ =~ s/\s+$//g;	#  remove trailing white-space
		$_ =~ s/^\s+//g;	#  remove leading white-space
		if($_ =~ /^\s*$|^$/) {
			next;	# skip blank lines
		}
		if($_ =~ /^[^:\s]+:\s+/) {
			DebugMsg("hostLoader() skipping parm [$_]");	
			next;   # skip paramter overrides, too
		}
		push @actualHostDataAr, $_;
	}
	return @actualHostDataAr;
}


###
### ---------------------------------------------------------------------
###                        End of Subroutines
### =====================================================================


DebugMsg("Acting on request");

###
###  load our data file
###
my @rawHostsDataAr = loadHostData($glbHostsFspec);
my $rawHostsDataCt = @rawHostsDataAr;

if($rawHostsDataCt < 1) {
	Msg(-err,"No hosts given");
	Msg(-inf,"Please add hosts to ~/.tkpingrc or provide -nodes option");
	FatalMsg("Aborting...");
}

%glbConfigDataHs = loadParmData($glbConfFspec);

my $colOverrideCt = (grep /<nextcolumn>/i,@rawHostsDataAr);
if($colOverrideCt > 0) { 
	$colOverrideCt++;	#  make one-relative (vs. zero)
	$glbNbrColumns = $colOverrideCt;	#  force use of desired column count
	#  calculate desired nbr rows since columns are specified!
	my $maxRowCt = 0;
	my $currRowCt = 0;
	foreach (@rawHostsDataAr) {
		/<nextcolumn>/i and do {
			if($currRowCt > $maxRowCt) {
				$maxRowCt = $currRowCt;
			}
			$currRowCt = 0;
			next;
		};
		$currRowCt++
	}
	if($currRowCt > 0) {
		if($currRowCt > $maxRowCt) {
			$maxRowCt = $currRowCt;
		}
	}
	#  now set desired row count
	$glbNbrRows = $maxRowCt + 1;	###  BUG +1 fixes draw problem!
} else {
	if($glbNbrColumns < 2) {
		$glbNbrRows = $rawHostsDataCt + 1;  #  always leave last blank line
		$glbNbrColumns = 1;	# always at least one column
	} else {
#		$glbNbrRows = int(($rawHostsDataCt / $glbNbrColumns) + 1);
	}
}

if(!$glbModeCommandLineOnly) {

	my $topWindowWidth = ($glbNbrColumns * 72) + 20;
	### ---------------------------------------------------------------------
	###  () setup the GUI (top window...)
	###
	#$MW = MainWindow->new(-width => $topWindowWidth);
	$MW = MainWindow->new();
	$MW->title("$NAME - Ver $VERSION");
	my $FONT = '-*-Helvetica-Medium-B-Normal--*-140-*-*-*-*-*-*';
	

	# ------------------------------------------------------------
	#  Create the Menu Pane  
	#
	$glbMenuBar = $MW->Frame(-relief => 'raised', -borderwidth => 1);
	                         
	$glbMenuBar->grid(-sticky => 'nsew')->pack(-fill => 'x');

	my $glbOnHostMenu = $MW->Menu(
	    qw/-tearoff 0 -menuitems/ =>
	    [
	     [Button => '~Recheck', -command => \&recheckSingleHost],
	     [Button => '~Info', -command => \&displayInfoForSingleHost],
	     [Button => '~Down', -command => \&markSingleHostDown],
		]);

	sub OnBtn3HostBtn
	{
		my ($button, $hostName) = @_;
		$currentRightMouseHost = $hostName;
#		print STDOUT "hostname=[$hostName]\n";
		$glbOnHostMenu->Popup(-popover => $button, -popanchor => "sw");
	}
	

	my $glbFileMenu = $glbMenuBar->Menubutton(
	    qw/-text File -tearoff 0 -underline 0 -menuitems/ =>
	    [
	     [Button => '~Recheck', -command => \&recheckSelectedHosts],
	     [Button => 'Recheck ~All', -command => \&recheckAllHosts],
	     [Button => '~Info', -command => \&displayInfoFor1stSelectedHost],
	     [Button => '~Down', -command => \&markSelectedHostsDown],
	     [Separator => ''],
	     [Button    => '~Quit', -command => \&onQuit],
	    ])->grid(qw/-row 0 -column 0 -sticky w/)->pack(-side => 'left');

	my $glbEditMenu = $glbMenuBar->Menubutton(
	    qw/-text Edit -tearoff 0 -underline 0 -menuitems/ =>
	    [
	     [Button => '~Select All', -command => \&selectAllHosts],
	     [Button => '~Clear Selection', -command => \&clearAllHosts],
	    ])->grid(qw/-row 0 -column 0 -sticky w/)->pack(-side => 'left');

	my $helpMenu = $glbMenuBar->Menubutton(
	    qw/-text Help -tearoff 0 -underline 0 -menuitems/ =>
	    [
	     [Button    => "~About"],
	    ])->grid(qw/-row 0 -column 2/)->pack(-side => 'right'); 
	
	                                                
	my $DIALOG_ABOUT = $MW->Dialog(
	    -title          => "About $NAME",
	    -bitmap         => 'info',
	    -default_button => 'OK',
	    -buttons        => ['OK'],
	    -justify        => 'center',
	    -text           => $DESCRIPTION
	);
	
	$helpMenu->entryconfigure("About", 
						      -command => [$DIALOG_ABOUT => 'Show']);
						   
	
	# ------------------------------------------------------------
	#  Create the StatusText Pane (bottom row of window)
	#
	#$glbGeneralStatusMsg = "";
	$glbEvaluationStatusMsg = "OK";
	
	my $statusBar = $MW->Frame(
	                       -relief => 'flat',
	                       -borderwidth => 2,
	                       -height => 20,
	                     )->pack(-expand => 'no', 
	                             -fill => 'x',
								 -anchor => 's', 
								 -side => 'bottom',
								 -padx => 0,
								 -pady => 1);
	my $statusFrame = $statusBar->Frame(-relief => 'sunken', 
								 -borderwidth => 1)->pack(-expand => 'yes',
								                          -anchor => 's', 
								                          -side => 'left',
														  -fill => 'x', 
														  -pady => '0', 
														  -padx => '1');
														  
	my $status2Frame = $statusBar->Frame(-relief => 'sunken', 
								 -borderwidth => 1)->pack(-expand => 'no', 
								                          -anchor => 's', 
								                          -side => 'right',
														  -fill => 'none', 
														  -pady => '0', 
														  -padx => '1');
														  
	my $generalStatus = $statusFrame->Label(-textvariable => \$glbGeneralStatusMsg,
									 -width => '15',
								     -anchor => 'w')->grid(-sticky => 'ew')->pack(-side => 'left');
							
	my $evalStatus = $status2Frame->Label(-textvariable => \$glbEvaluationStatusMsg,
									 -width => '5',
								     -anchor => 'c')->grid(-sticky => 'ew')->pack(-side => 'right');
	
	# ------------------------------------------------------------
	#  Create the button grid 
	#
	$glbNbrRows--;
my %glbCanvasBtnIdByBtnObjHs = ();

    #$glbButtonGrid = $MW->Frame->pack(-expand => 0, -fill => 'none');
    $glbButtonGrid = $MW->Scrolled('Canvas',
	                               -scrollbars => 'osoe',
								   -height => ($glbNbrRows * 35) + 19,
								   -width => ($glbNbrColumns * 135) + 5,
	                       )->pack(-expand => 'yes', -fill => 'both');

	my $btnCanvas = $glbButtonGrid->Subwidget('canvas');

	my $rawDataSlotNbr = 0;
	my $waitingForColumnChange = 0;
	for(my $column = 0; $column < $glbNbrColumns; $column++) {
	    for(my $row = 0; $row <= $glbNbrRows; $row++) {
			#  get data for this button
			my ($hostOrType, $labelText);
			if($rawDataSlotNbr + 1 > $rawHostsDataCt) {
				$hostOrType = "<blank>";	###  ran out! force empty extras!
			} else {
				($hostOrType, $labelText) = split /\s+/,$rawHostsDataAr[$rawDataSlotNbr],2;
			}
			my $styleChoice = "";
			my $foregroundColor = "black";
			my $activeForegroundColor = "white";
			my $state = "active";
			foreach ($hostOrType) {
				/<blank>/i and do {
					$styleChoice = "flat";
					$state = "disabled";
					$labelText = "";	#  nope no hostname here!
					last;
				};
				/<nextcolumn>/i and do {
					$waitingForColumnChange = 1;
					$rawDataSlotNbr++;
					last;
				};
				/<message>/i and do {
					$styleChoice = "flat";
					$state = "disabled";
					$foregroundColor = "white";
					$activeForegroundColor = "white";
					last;
				};
				# have hostname
				$styleChoice = "raised";
			}
			if($waitingForColumnChange) { 
				$styleChoice = "flat";
				$state = "disabled";
				$labelText = "";	#  nope no hostname here, either!
			}
			
			#  create new buttons, one for each row,col position
			my $button = $btnCanvas->Button(
			               -text		=> "$labelText",
						   -relief		=> "$styleChoice",
						   -foreground	=> "$foregroundColor",
						   -activeforeground	=> "$activeForegroundColor",
						   -disabledforeground 	=> "$foregroundColor",
						   -state		=> "$state",
						   -height		=> 1,
						   -width		=> 14,
						   -default		=> 'normal',
						   -command		=> [ \&toggleHostSelection, $labelText ]
						   );

			#  place button on canvas at specific coords
			$glbCanvasBtnIdByBtnObjHs{$button} = 
				$btnCanvas->createWindow(($column * 135) + 70,
				                         ($row * 35) + 25, 
										 -window => $button);


			#  if is host button (not label/spacer) then...
			if($styleChoice eq "raised") {
				#  bind a popup menu via right-mouse to this button
            	if($state ne "disabled") {
					$button->bind('<Button-3>', [\&OnBtn3HostBtn, $labelText]);
				}

				#  keep track of our button ID's
				$glbButtonObjByHostNameHs{$labelText} = $button;

				#  setup our list of active hosts (have button-id ready)
				$glbActiveHostsHs{$labelText} = $button;
			}

			$rawDataSlotNbr++ if(!$waitingForColumnChange);
	  	}
	  	$waitingForColumnChange = 0;
	}

	#  tell scrolled canvas how big the drawn areas is so it
	#   can setup scroll bars appropriately...
	$glbButtonGrid->configure(-scrollregion => [ $btnCanvas->bbox("all") ] );
	
	$glbGeneralStatusMsg = sprintf("oc=%d, c=%d, r=%d",
	                               $colOverrideCt,
								   $glbNbrColumns,
								   $glbNbrRows);

} ##  end if($glbModeCommandLineOnly)


#LogMsg("EVAL Result: $evalResult");

###  need fork? of task at 120 sec periods to run fping(8) to 
###    get host status data...
#
#my $cmdStr = "$FPING_CMD -c 5 host host host host host, etc."
#   NOTE: 5 is a setting, and host.. is list of current buttons...
#
#my $retCode = DoCmdRetOutput($cmdStr,\@stdoutAr,\@stderrAr);
#


# ------------------------------------------------------------
#  do work, GUI or NOTgui
#
if(!$glbModeCommandLineOnly) {
	#  display the initial status message
	if($glbGeneralStatusMsg eq "") {
		$glbGeneralStatusMsg = "Ping loop enabled";
	}
	
	# start our ping loop...
	setupPingLoop();

	#  allow user interaction!!!
	MainLoop();
}


# 
#  We're done!!!
#
doExit(0,"Normal exit at end");

### ---------------------------------------------------------------------
# These lines must be at the end of the file.  They help emacs users to
# prevent tabs being introduced into our source files. (tbc, 08/14/98)
#
# GNU emacs magic
# Local Variables:
# local-write-file-hooks:((lambda () (untabify (point-min) (point-max)) nil))
# tab-width:4
# tab-stop-list:(4 8 12 16 20 24 28 32 36)
# End:
##-----------------------------------------------------------------------
