#!/usr/misc/bin/perl5
# ------------------------------------------------------------------
# | Copyright Unpublished, Atria Software, Inc.  All Rights        |
# | Reserved.  This software contains proprietary and confidential |
# | information of Atria and its suppliers.  Use, disclosure or    |
# | reproduction is prohibited without the prior express written   |
# | consent of Atria.                                              |
# ------------------------------------------------------------------


# ck_all_cntrs.pl (the VOB is implicitly the current directory's VOB)
#
# check all of the elements in the the current VOB
# and detect missing (and corrupt text_file_delta) 
# source containers.
# *** need to be in a view (any view will do) ***
# *** need to be cd'd to the VOB to check ***
#
# % setview <any view>
# % cd <vob root dir>
# % ct lock -vob .
# % /usr/atria/bin/Perl ck_all_cntrs.pl | tee ck_all_cntrs.out
# ... sit back ...
# % ct unlock -vob .
#
# Note: actually, you need only lock the source pools, not the
# entire VOB.  That way folks can still do everything except
# make branches, checkin and checkout (if doing so involves making a branch).
#
# Warning: this script will take a long time for a large VOB.
# Sorry, I can't give you a size / time factor. FYI, the
# check consists of a "find -all" and *for each element found*,
# a "ct desc" and a "ct dump".  If a non-delta element is found,
# there is a "lsvtree" and a "ct dump" for each version of the element.
# 
# It isn't necessary to lock the VOB being checked, however,
# not locking it may result in "false" errors (in the unlikely
# event that an element is changing -e.g., checkin, mkbranch-
# while it is being checked).
# 
# Oh, while the name of the script is misleading, only source
# containers are checked.
#
# Find each element in the VOB and check it:
# For delta managed elements check for the single container.
# For non-delta managed elements, check for a container for each version.
#   maintain a table of visited containers so that we only check
#   them once.
#
# If a missing container is found and it's a delta managed element,
# check for / report other containers that are for that element.
# (actually, this is NYI).
#
# If the container is a text_file_delta container, check to see that no NULLs
# are present in the file (if this has been enabled; see below).
# NYI: do likewise if the container is a z_text_file_delta container.

$ct='cleartool';

$SINCE_DATE='03-Jan-1970';
$BACKUP_DATETIME='15-Oct-1997.02:00:00';

if (-d 'C:/') {
    # NT
    chop($cwd = `cd`);
    $DEV_NULL='NUL';
    $CREATED_SINCE="created_since($SINCE_DATE)";
} else {
    # UNIX
    chop($cwd = `pwd`);
    $DEV_NULL='/dev/null';
    $CREATED_SINCE="'created_since($SINCE_DATE)'";
}

# check_for_nulls
#
# comment this out to DISABLE checking for nulls in delta containers
#$check_for_nulls=1;

# force non-buffered output (so we see progress even when redirecting
# output to a file).
$| = 1;

# cleartool command
# maintains global file handles CT_WRITE and CT_READ
#
sub cleartool 
{
    local($cmd) = @_;
    local($_);
    local($cmd_terminator) = ('ThisIsABadCommand');
    local(@response);

    # Spawn a cleartool process if we haven't yet done so
    if (! $spawned_ct) {
	pipe(CT_READ, CT_P_WRITE) || die("can't create pipe\n");
	local($oldfh) = select(CT_P_WRITE); $| = 1; select($oldfh);

	# redirect STDOUT to CT_P_WRITE, ditto for STDERR
	open(SAVED_STDOUT, ">&STDOUT") || die("can't dup stdout\n");
	close(STDOUT);
	open(STDOUT, ">&CT_P_WRITE") || die("can't redirect STDERR\n");

	open(SAVED_STDERR, ">&STDERR") || die("can't dup stderr\n");
	close(STDERR);
	open(STDERR, ">&CT_P_WRITE") || die("can't redirect STDERR\n");

	# spawn the cleatool and get a WRITE fh for its input.
	# Its output / errout is inherited from our (redirected) STD{OUT,ERR}
	open(CT_WRITE, "| $ct") || die("can't spawn $ct\n");
        $oldfh = select(CT_WRITE); $| = 1; select($oldfh);

	close(CT_P_WRITE);   # this isn't our side of the pipe

	# redirect STD{OUT,ERR} back to their original state
	close(STDOUT);
	open(STDOUT, ">&SAVED_STDOUT");
	close(SAVED_STDOUT);

	close(STDERR);
	open(STDERR, ">&SAVED_STDERR");
	close(SAVED_STDERR);

        $spawned_ct = "yes";
    }

    # Send the command.
    print CT_WRITE  $cmd, "\n";

    # Send a special piece of swill so we can recognize the end of the response
    print CT_WRITE  "$cmd_terminator", "\n";

    # Get the response
    while (<CT_READ>) {
	chop;
	last if (/$cmd_terminator/);
	push(@response, $_);
    }
    @response;
}

# ver_get_cntr <ver_xpn>
#
sub ver_get_cntr 
{
    local($ver_xpn) = @_;
    local($_, $cntr);

    ($cntr) = grep(/^source cont=/, &cleartool("dump $ver_xpn"));
    return 0 if (!$cntr);    # return nil for ndata versions
    $cntr =~ /^source cont="(.*)"/;
    $cntr = $1;
}

# elem_get_mgr <elem_xpn>
#
sub elem_get_mgr 
{
    local($elem_xpn) = @_;
    local($_, $eltype, $typemgr);

    ($eltype) = grep(/element type:/, &cleartool("desc $elem_xpn"));
    $eltype =~ /element type:\s+([^\s]+)/;
    $eltype = $1;

    $typemgr = $eltype_mgrs{$eltype};
    if (! $typemgr) {
	($typemgr) = grep (/type manager:/, &cleartool("desc -type $eltype"));
	$typemgr =~ /type manager:\s+([^\s]+)/;
	$typemgr = $1;
	$eltype_mgrs{$eltype} = $typemgr;
    }
    $typemgr || die("no typemgr for pname '$elem_xpn'\n");
    $typemgr;
}

# ck_elem <$elem_xpn>
#
sub ck_elem 
{
    local($elem_xpn) = @_;
    local($_, $typemgr, $eltype, $cntr, $ver_xpn, $elem_error, $err_str);

    $elem_error = 0;

    # determine the element's type mgr
    #
    $typemgr = &elem_get_mgr($elem_xpn);

    # if it's a delta manager, 
    #    only need to access one of the elements versions
    #    if missing, see if there are candidate containers
    # otherwise
    #    need to access each of the element's versions
    #
    if ($typemgr =~ /text_file_delta/) {
	$NCNTRS++;
        $cntr = &ver_get_cntr("$elem_xpn/main/0");
        print LOG_FH "element $elem_xpn\n" .
                     "   {\n" .
                     "      ELEMENT => '$elem_xpn' ,\n" .
                     "      TYPEMGR => '$typemgr' ,\n" .
                     "      SRCCONT => '$cntr' ,\n" .
                     "   }\n" ;
        $err_str = &ck_tfd_cntr($cntr, $typemgr);
        if ($err_str) {
            $elem_error=1;
            print LOG_FH "##### elem *** ERROR ***: $elem_xpn\n" .
                               "  $err_str\n";
        } else {
            print LOG_FH "##### elem OK: $elem_xpn\n";
        }
    } elsif ($typemgr !~ /directory/) {
        # really need to check all versions
        #
        print LOG_FH "##### checking each non-ndata version of '$elem_xpn' (manager '$typemgr')...\n";
        foreach (`$ct find $elem_xpn -ver $CREATED_SINCE -print`) {
	    chop;
            $ver_xpn = $_;
            next if ($ver_xpn =~ /CHECKEDOUT/);
            $cntr = &ver_get_cntr($ver_xpn);
            print LOG_FH "version $ver_xpn\n " .
                         "   {\n" .
                         "      VERSION => '$ver_xpn' ,\n" .
                         "      TYPEMGR => '$typemgr' ,\n" .
                         "      SRCCONT => '$cntr' ,\n" .
                         "   }\n" ;
	    next if $visited_cntrs{$cntr};  # skip cntrs we've already checked
	    $visited_cntrs{$cntr} = 1;      # add it to the table
	    $ver_xpn =~ /.*\@\@(.*)/;
	    $ver_id_str = $1;
            if ($cntr) { # ignore ndata versions
		$NCNTRS++;
                if (! -f $cntr) {
                    print LOG_FH "##### ver *** ERROR ***: $ver_id_str\n";
                    print LOG_FH "----- missing container: $cntr\n";
                    $elem_error=1;
                } else {
                    print LOG_FH "##### ver OK: $ver_id_str\n";
                }
            }
        }
        if ($elem_error != 0) {
            print LOG_FH "##### elem *** ERROR ***: $elem_xpn\n";
            print LOG_FH "----- missing container: $cntr\n";
        } else {
            print LOG_FH "##### elem OK: $elem_xpn\n";
        }
    }
    $elem_error;
}

# get_elem_history($elem_xpn)
#   Return the (ls)history of this element since the backup
#
sub get_elem_history
{
   local $elem_xpn = shift;
   $elem_xpn .= '@@' unless ($elem_xpn =~ /\@\@$/);
   my @args = ("-long", "-directory", "-since", $BACKUP_DATETIME, $elem_xpn);
   my $cmd  = "lshistory " . join(" ",@args);
   my @history = &cleartool($cmd);
   return join("\n", @history);
}

## $CHARMAP{$ch}
## Map a non-printable ascii character to a plaintext sequence
%CHARMAP = (
   "\000"  =>  'NUL;^@'  ,
   "\001"  =>  'SOH;^A'  ,
   "\002"  =>  'STX;^B'  ,
   "\003"  =>  'ETX;^C'  ,
   "\004"  =>  'EOT;^D'  ,
   "\005"  =>  'ENQ;^E'  ,
   "\006"  =>  'ACK;^F'  ,
   "\007"  =>  'BEL;^G;\a'  ,
   "\010"  =>  'BS;^H;\b'  ,
   ##"\011"  =>  'TAB;^I;\t'  ,
   "\011"  =>  "\t"  ,
   ##"\012"  =>  'LF;^J;\n'  ,
   "\012"  =>  "\n"  ,
   "\013"  =>  'VT;^K;\v'  ,
   "\014"  =>  'FF;^L;\f'  ,
   "\015"  =>  'CR;^M;\r'  ,
   "\016"  =>  'SO;^N'  ,
   "\017"  =>  'SI;^O'  ,
   "\020"  =>  'DLE;^P'  ,
   "\021"  =>  'DC1;^Q'  ,
   "\022"  =>  'DC2;^R'  ,
   "\023"  =>  'DC3;^S'  ,
   "\024"  =>  'DC4;^T'  ,
   "\025"  =>  'NAK;^U'  ,
   "\026"  =>  'SYN;^V'  ,
   "\027"  =>  'ETB;^W'  ,
   "\031"  =>  'CAN;^X'  ,
   "\030"  =>  'EM;^Y'  ,
   "\032"  =>  'SUB;^Z'  ,
   "\033"  =>  'ESC;^['  ,
   "\034"  =>  "FS;^\\"  ,
   "\035"  =>  'GS;^]'  ,
   "\036"  =>  'RS;^^'  ,
   "\037"  =>  'US;^_'  ,
   "\127"  =>  'DEL'  ,
);

## charmap($ch)
## Map a non-printable ascii character to a plaintext sequence
sub charmap {
   local $char = shift;
   local $_ = $char;
   if (($char ne "\n") && ($char ne "\t")) {
      $_ = sprintf("\\%03o:0x%X", ord($char), ord($char));
      $_ .= ";$CHARMAP{$char}"  if (exists $CHARMAP{$char});
   }
   return $_;
}

# escape_chars($str)
## "Escape" non-printables in a string
sub escape_chars {
   local($_) = @_;
   tr/\007\010\013\014\015/\\a\\b\\v\\f\\r/;
   s/([\000-\037\177-\377])/sprintf("<%s>", &charmap($1))/ge;
   return $_;
}

# has_binary_chars($str)
## Return TRUE if the given string contains no non-printable characters;
## Return FALSE otherwise.
sub has_binary_chars {
   local($_) = @_;
   ## \000 --> \006 are usually *bad* news.
   ## \200 --> \377 are suspect as well, but make special exceptions for
   ##               \200, \205, \221, \222, and \240
   return /[\000-\006\201-\204\206-\220\223-\237\241-\377]/ ? 0 : 1;
}

# An associative array of error messages:
#
%TFD_ERRORS =
(
   'BAD_TABLE_START',  "Invalid 1st line (should be \"^S <id1> <id2>\")." ,
   'BAD_TABLE_ENTRY',  "Invalid characters in delta table entry."         ,
   'UNEXPECTED_DELTA', "'^I' or '^D' encountered before '^E'."            ,
   'BAD_DELTA_HEADER', "Invalid characters in delta table header"         ,
   'UNEXPECTED_ENTRY', "^V' or '^B' encountered after '^E'."              ,
   'EXTRA_TABLE_END',  "More than one '^E' encountered."                  ,
   'BAD_DELTA_ENTRY',  "Invalid characters in delta."                     ,
   'NO_TABLE_END',     "No '^E' encountered to end delta table."
);

# tfd_errstr($err_index, $line_num, $textline)
#
#    Return the errors string corresponding the given index
#    (which should be one of the keys of the associative array
#    used below)
#
sub tfd_errstr
{
   my ($err_index, $line_num, $textline) = @_;
   local $_ = ($line_num) ? "[line $line_num]" : "";
   $_ .= "$TFD_ERRORS{$err_index}\n";
   $_ .= "\t\"$textline\"\n"  if ($textline);
   return  $_;
}

# ck_tfd_cntr($tfd_cntr, $typemgr)
#   Return "" if the given source data container seems to be in
#   proper format. Else return a string which describes the first
#   problem encountered.
#
sub ck_tfd_cntr
{
  my ($tfd_cntr, $typemgr) = @_;
  return "$tfd_cntr does not exist"     unless (-e $tfd_cntr);
  return "$tfd_cntr is not a file"      unless (-f _);
  return "$tfd_cntr is not readable"    unless (-r _);
  if ($typemgr eq 'z_text_file_delta') {
     open(CK_TFD_CNTR_FH, "zcat - <$tfd_cntr |") ||
        return "can't run zcat(1) on $tfd_cntr: $!";
  } else {
     return "$tfd_cntr is not a text file" unless (-T _);
     open(CK_TFD_CNTR_FH, "<$tfd_cntr") ||
        return "$tfd_cntr can't be opened for reading: $!";
  }
  ##---------------------------------------------------------------
  ## From what I can ascertain, the format of a text data storage
  ## container is as follows:
  ##
  ## There are two sections of the file:
  ##   (1) the delta table, which contains information identifying
  ##       the name (oid) and sequencing information for each version
  ##       and branch of the element.
  ##
  ##   (2) the deltas themselves, which contain information about
  ##       inserted/deleted lines for a given delta between two
  ##       element revisions. The deltas are in *interleaved*
  ##       format (similar to SCCS) as opposed to forward or reverse
  ##       delta format.
  ##
  ## Special control/command sequences are indicated by a leading
  ## token on the line which begins with a '^' and is followed
  ## by un uppercase character (one of [SVB#EID]). Its not clear to me
  ## how a source line which contains a leading '^' is handled, I assume
  ## it is escaped in some manner.
  ##
  ## In the following descriptions, all "<id>"s and "line-counts" seem
  ## to be hexadecimal numbers. All <<identifiers> are some string of
  ## non-whitespace characters whose meaning isnt obvious to me.
  ##
  ## The format of the file should be:
  ##   ^S <db-id> <identifier>         <== Start of the delta table
  ##   ... delta table entries ...
  ##   ^E <identifier>                 <== End of the delta table
  ##   ... interleaved delta entries ...
  ##
  ## A delta *table* entry looks like one of the following:
  ##   ^V <oid> <seq-num1> <seq-num2> <identifier>
  ##   ^B <oid> <br-num1> <br-num2> <identifier>
  ##   ^#^V <oid> <seq-num1> <seq-num2> <identifier>
  ##   ^#^B <oid> <br-num1> <br-num2> <identifier>
  ##
  ## My best guess to the meaning of these entries is that:
  ##   1. A leading '^V' indicates info for an element Version
  ##   2. A leading '^B' indicates info for an element Branch
  ##   3. A leading '^#^V' indicates info for an element Version
  ##      that has since been *removed*
  ##   4. A leading '^#^B' indicates info for an element Branch
  ##      that has since been *removed*
  ##
  ## A delta entry looks like one of the following:
  ##   ^I <seq-num1> <seq-num2> <num-lines-inserted>\n ... inserted lines ...
  ##   ^D <seq-num1> <seq-num2> <num-lines-deleted>\n ... deleted lines ...
  ##
  ## The '^I' corresponds to lines that have been Inserted and
  ## the '^D' corresponds to lines that have been Deleted.
  ##
  ## As indicated above, each insert/delete control-sequence line
  ## is immediately followed by one or more "source lines" which
  ## make up the corresponding set of inserted/deleted lines for
  ## some portion of a corresponding version of the textfile.
  ##
  ## However, due to the "nested" nature of interleaved deltas, it is
  ## also possible that an insert/delete control-sequence line is
  ## followed by one or more other insert/delete control sequence lines
  ## before eventually referring to the source lines in question.
  ##---------------------------------------------------------------

  local($_);
  my $in_deltas = 0;
  ## read first line
  chomp($_ = <CK_TFD_CNTR_FH>);
  my $text = &escape_chars($_);
  unless (/^\^S \S+ \S+$/) {
     close(CK_TFD_CNTR_FH);
     return &tfd_errstr('BADSTART');
  }
  my $lines   = 1;
  my $err_str = '';
  while (<CK_TFD_CNTR_FH>) {
     ++$lines;
     chomp;
     $text = &escape_chars($_);
     if (! $in_deltas) {
        if (/^\^E \S+$/) {
           ## end of delta table, now enter the interleaved deltas
           ++$in_deltas;
           next;
        } elsif (/^(\^\#)?\^([VB]) (\S+ \S+ \S+ \S+)$/) {
           ## looks like a valid table entry, check for unprintables
           unless ( &has_binary_chars($3) ) {
              $err_str = &tfd_errstr('BAD_TABLE_START', $lines, $text);
              last;
           }
        } elsif (/^\^([ID]) (\S+ \S+ \S+)$/) {
           $err_str = &tfd_errstr('UNEXPECTED_DELTA', $lines, $text);
           last;
        } else {
           ## looks like an invalid table entry
           $err_str = &tfd_errstr('BAD_TABLE_ENTRY', $lines, $text);
           last;
        }
     } else {
        if (/^\^([ID]) (\S+ \S+ \S+)$/) {
           ## looks like a valid delta entry
           unless ( &has_binary_chars($3) ) {
              $err_str = &tfd_errstr('BAD_DELTA_HEADER', $lines, $text);
              last;
           }
        } elsif (/^(\^\#)?\^([VB]) /) {
           close(CK_TFD_CNTR_FH);
           $err_str = &tfd_errstr('UNEXPECTED_ENTRY', $lines, $text);
           last;
        } elsif (/^\^E \S+$/) {
           $err_str = &tfd_errstr('EXTRA_TABLE_END', $lines, $text);
        } else {
           ## must be a line from a delta, check for unprintables
           unless ( &has_binary_chars($_) ) {
              $err_str = &tfd_errstr('BAD_DELTA_ENTRY', $lines, $text);
              last;
           }
        }
     }
  }
  close(CK_TFD_CNTR_FH);
  $err_str = $err_str || (($in_deltas) ? "" : &tfd_errstr('NO_TABLE_END'));
  return $err_str;
}

# tfd_has_nulls (pname, leafname)
# check (and report) if the leafname file has any nulls in it.
# Also report problems if container is empty or doesn't look like tfd container.
# return TRUE if container has any problems.
#
sub tfd_has_nulls 
{
    local($pname, $cntr) = @_;
    local($buf, $bcnt, $problems, $fh, @stat);

    # Report a problem if the container is empty.
    @stat = stat($cntr);
    if ($stat[7] <= 0) {
	print "\nERROR: tfd container has 0 size: $pname\n";
	return 1;
    }

    $fh = TFD_HAS_NULLS_FH;
    open($fh, $cntr) || (print "\nERROR: Can't open $pname\n" && return 1);

    # Report if 1st line doesn't have TFD 'control record'
    if ($bcnt = read ($fh, $buf, 25)) {
	if ($buf !~ /^\^S db1 /) {
	    print "\nERROR: missing tfd control record at byte 0 in $pname\n";
	    $problems++;
	}
    }

    # Report any blocks of NULLs
    $problems += &fh_file_has_nulls ($fh, $pname, $cntr);

    close($fh);
    $problems > 0;  # return TRUE if any problems.
}

# fh_file_has_nulls (fh, pname, leafname)
# check (and report) if the file associated with filehandle 'fh'
# has any nulls in it.
# return TRUE if container has any problems.
#
sub fh_file_has_nulls 
{
    local($fh, $pname, $file) = @_;
    local($buf, $bcnt, $bufsize, $bufbaseindx);
    local($num_null_blocks, $problems);
    local($pos, $first_null_indx, $last_null_indx);

    # reset things 
    seek($fh, 0, 0);
    binmode($fh);

    $bufsize=16384;

    $bufbaseindx=0;
    $last_null_indx = -2;  # must be < -1 to ensure 1st NULL (at loc 0)
			   # looks like a new block

    # Track / report blocks of NULLs (deal with blocks spanning buffers)
    while ($bcnt = read ($fh, $buf, $bufsize)) {
	$pos = $[;
	while (($pos = index($buf, "\000", $pos)) >= $[) {
	    # Found a NULL.
	    if ($pos - 1 + $bufbaseindx == $last_null_indx) {
		# Consecutive, just keep going.
		$last_null_indx++;
	    } else {
		# Begin new block (report end of previous block if any)
		print $last_null_indx - $first_null_indx + 1,
		      " consecutive NULL bytes\n" if ($last_null_indx >= 0);

		# begin new block
		$first_null_indx = $pos + $bufbaseindx;
		$last_null_indx = $first_null_indx;
		print "\nERROR: found NULL at byte $first_null_indx in $pname\n";
		$problems++;
		$num_null_blocks++;
	    }
	    $pos++;
	}
	$bufbaseindx += $bcnt;
    }
    # Report the end of the last NULL block (if any)
    if ($last_null_indx >= 0) {
        if ($first_null_indx == 0 && $last_null_indx + 1 == $bufbaseindx) {
            print $last_null_indx - $first_null_indx + 1,
                " consecutive NULL bytes - FILE IS ALL NULLS\n";
        } else {
            print $last_null_indx - $first_null_indx + 1,
                " consecutive NULL bytes\n";
        }
    }

    $problems > 0;  # return TRUE if any problems.
}

###########
# main
###########

$NELEMS = 0;
$NCNTRS = 0;
$ELEM_ERRORS = 0;
$elem_hist = '';

$Usage = "$0 [-help] [-base NAME] [-log FILE] [PNAME-IN-VOB]";
use Getopt::Long;

$opt_help    = 0;
$opt_basename = 'ck_tfd_cntrs';
$opt_logfile = ".";
$rc = GetOptions("help", "basename=s", "logfile=s");

if (! $rc) {
   print STDERR "$Usage\n";
   exit(2);
}
if ($opt_help) {
   print STDOUT "$Usage\n";
   exit(1);
}

## Permit directories to be specified instead of file paths.
$opt_logfile = "${opt_logfile}/$opt_basename.$$.LOG"   if (-d $opt_logfile);

## Open output files
open(LOG_FH, ">$opt_logfile") || die "Can't open $opt_logfile for writing: $!";

## Make sure they are line buffered
select((select(LOG_FH), $| = 1)[0]);

print "
##### checking all source containers for VOB at '$cwd' ...


***** IF THIS VOB ISN'T LOCKED, YOU CAN GET FALSE ERROR DETECTIONS *****

";
print "\n";

# find each element in the VOB and check it
#
@ARGV = ('-all') if (@ARGV == 0); 

for $pname (@ARGV) {
   print "+ executing $ct find $pname -print | ...\n";
   open(FINDH, "$ct find $pname -print |") || die("can't spawn 'ct find'\n");
   foreach (<FINDH>) {
       chop;
       next unless /\@\@$/;
       print "+ checking element $_\n";
       $NELEMS++;
       ## $elem_hist = &get_elem_history($_);
       ## if ($elem_hist) {
       ##    $elem_hist =~ s/^/  /mg;
       ##    print HISTORY_FH "$_\n$elem_hist\n\n";
       ## }
       $ELEM_ERRORS += &ck_elem($_);
   }
   close(FINDH);
}

print LOG_FH "\n##### checked $NELEMS elements with $NCNTRS (unique) containers checked\n\n";
if ($ELEM_ERRORS != 0) {
    print LOG_FH "***************************\n";
    print LOG_FH "***** ERRORS DETECTED *****\n";
    print LOG_FH "***************************\n";
} else {
    print LOG_FH "******************\n";
    print LOG_FH "***** ALL OK *****\n";
    print LOG_FH "******************\n";
}

close(LOG_FH);
exit $ELEM_ERRORS;

