#!/usr/bin/perl -w

# Copyright 2008-2014 Cumulus Systems Incorporated.
# All Rights Reserved.

# Listen as an xinet daemon. Read file name, read data, send acknowledgement to the sender on remote host.

# Switch on warnings. It is used so that we can switch-off warning in Getopts() call.
$^W = 1;

# It is used for strict compilation.
use strict;

# This is used to create folder recursively.
use File::Path qw(mkpath);

# Create or remove directory trees.
use File::Path;

# Supply object methods for I/O handles.
use IO::Handle;

# POSIX - Perl interface to IEEE Std 1003.1.
use POSIX "strftime";

# It is used to get the current date and time.
use Time::HiRes qw(gettimeofday);

# It is used to indicate the path where common module is placed.
use lib "/usr/local/megha/lib/common/perl";

# It is required for logging information.
use commonLogModule;

# Flush output after every write. Unbuffered output is usually desired.
$| = 1;

# This variable stores commonLogModule instance.
my $logObj = "";

# This variable stores the probe type.
my $probeType = "Linux";

# Application home directory.
my $HOME_DIR = "/usr/local/megha";
#my $HOME_DIR = "/usr/local/megha";

# Time out in seconds for a complete file transfer.
my $TIMEOUT = 60;

# Size of read buffer in bytes (1 MB).
my $BUFSIZE = 1024 * 1024;

# Probe directory.
my $DEST_HOME_DIR = "$HOME_DIR/db/probe";

# Handle to stdin.
my $stdin;

# Handle to stdout.
my $stdout;

# Used to get data from receivedData() function.
my $data = "";

# Used to get checksum value from file meta-data.
my $checksum = "";

# Used to store file size.
my $fileSize = -1;

# Used to store length of file name.
my $fileNameLength = -1;

# Used to store file name.
my $fileName = "";

# Folder where we will save the data file from remote machine.
my $probeRawDataFolder = "";

# Initialize the environment.
my $status = init();
if (0 != $status)
{
    $logObj->error("Call to init() failed.");
    goto EXIT;
}

$status = receiveFile();
if (0 != $status)
{
    $logObj->error("Call to receiveFile() failed.");
    goto EXIT;
}

EXIT:

exit(1);

# This function is used to initialize the environment to run the Perl script correctly.
#
# @affected global variables :
#   $propFilePath
#   $logFile
#   $LOG_HANDLE
#
# @return :
#   Nothing
sub init
{
    my $retval = 0;
    
    # This variable stores Log folder path.
    my $logFolder = $HOME_DIR."/logs";

    # If log folder does not exist then create it.
    if (! (-e $logFolder))
    {
        mkpath($logFolder);
    }

    # Get commonLogModule instance.
    $logObj = commonLogModule->getInstance($logFolder, $probeType, "All");
    
    # Make STDIN and STDOUT into scalar variables.
    $stdin = IO::Handle->new();
    $stdin = *STDIN;

    $stdout = IO::Handle->new();
    $stdout = *STDOUT;

    return $retval;
}

# This function reads the file data which has been sent from remote host.
#
# @param:
#    None.
#
# @return:
#    0 on success. 1 on failure.
sub receiveFile
{
    # This variable is used to store the return value of function calls.
    my $retVal = 0;

    # Read the file data.
    eval
    {
        # If there is time out in file read operation then eval will set $@ with "alarm".
        local $SIG{ALRM} = sub { die "alarm\n" };

        # Set the time out for file send operation.
        alarm($TIMEOUT);

        # Receive file meta-data.
        $retVal = getMetaDataOfFile();
        if (0 != $retVal)
        {
            $logObj->error("[receiveFile]: Call to getMetaDataOfFile() failed.");
            goto EXIT;
        }

        # Receive actual file data.
        $retVal = getDataFile();
        if (0 != $retVal)
        {
            $logObj->error("[receiveFile]: Call to getDataFile() failed.");
            goto EXIT;
        }
    };

EXIT:

    # Cancel the alarm.
    alarm(0);

    # $@ gives error message from the last eval command.
    if ($@)
    {
        if ($@ ne "alarm\n")
        {
            $logObj->error("[receiveFile]: Error during file read operation for file: [$fileName], Error: [$@].");
        }
        else
        {
            $logObj->error("[receiveFile]: Time out in file read operation, file name : [$fileName].");
        }
    }

    return $retVal;
}

# This function reads the meta information of file received from network.
# First 33 chars are used to specify file meta data.
#
# Format is:
#   fileNameLength => 10 decimal digits
#   blank          => 1 character
#   checksum       => 10 decimal digits
#   blank          => 1 character
#   fileSize       => 10 decimal digits
#   blank          => 1 character
#   fileName       => variable length
#   newline        => 1 character
#
# @affected global variables :
#   $fileNameLength
#   $checksum
#   $fileSize
#   $probeRawDataFolder
#
# @param:
#    None.
#
# @return:
#    0 on success;
#    1 on failure.
sub getMetaDataOfFile
{
    # This variable is used to store the return value of function calls.
    my $retVal = 0;

    # Used to store status of receivedData() function.
    my $msg = "";

    # Read meta-data string.
    $msg = &receiveData($stdin, \$data, 33);

    if (defined($msg))
    {
        $logObj->error("[getMetaDataOfFile]: receiveData returned error with error: [$msg].");
        $retVal = 1;

        goto EXIT;
    }

    # If read data size is not correct then error out.
    my $length = length($data);
    if ($length < 33)
    {
        $logObj->error("[getMetaDataOfFile]: Length of received data is less than 33. Read size: [$length].");
        $retVal = 1;

        goto EXIT;
    }

    # Get 33 chars of meta data and remove that data from the $data string.
    my $metadata = substr($data, 0, 33, '');

    $logObj->debug("[getMetaDataOfFile]: Meta-data: [$metadata].");
    if ($metadata =~ /^(\d{10}) (\d{10}) (\d{10}) $/)
    {
        ($fileNameLength, $checksum, $fileSize) = ($1, $2, $3);
    }
    else
    {
        $logObj->error("[getMetaDataOfFile]: Network read error. Found corrupted metadata: [$metadata].");
        $retVal = 1;

        goto EXIT;
    }

    # Read file name. It could be the case that we have received more than 33 char while reading the meta-data. So some part of file name could be in
    # $data string. So read only remaining part.
    my $remainingFileNameSize = $fileNameLength + 1 - length($data);
    if ($remainingFileNameSize > 0)
    {
        my $buf = "";

        $msg = &receiveData($stdin, \$buf, $remainingFileNameSize);
        if (defined($msg))
        {
            $logObj->error("[getMetaDataOfFile]: Network read error: [$msg].");
            $retVal = 1;

            goto EXIT;
        }

        $data .= $buf;
    }

    # Now we have file name in $data. Get the file name from $data and also remove it from $data string.
    $fileName = substr($data, 0, $fileNameLength, '');

    # Verify the length of file name.
    if (length($fileName) != $fileNameLength)
    {
        $logObj->error("[getMetaDataOfFile]: File name [$fileName] lenght not equal to attribute fileNameLength [$fileNameLength].");
        $retVal = 1;

        goto EXIT;
    }

    # Replace slashes and blanks with underscore.
    $fileName =~ s|/|_|g;
    $fileName =~ s|\s|_|g;

    # Get the host name, time and other properties from the file name.
    my $temp = $fileName;

    # Strip off leading "localhost_" or "localhost.localdoamin_" string.
    $temp =~ s/^localhost_//g;
    $temp =~ s/^localhost\.localdomain_//g;

    my ($hostName, $ymdhms, $ymd, $hms, $os, $type, $interval, $samples, $siteName, $probeName);

    # File name looks like 192.168.156.254_20140711_125510_linux_iostat_1_60_cumulus_cumulus
    ($hostName, $ymd, $hms, $os, $type, $interval, $samples, $siteName, $probeName) = split(/_/, $temp);

    if ($hostName eq "" || $ymd eq "" || $hms eq "" || $os eq "" || $type eq "" || $interval eq "" || $samples eq "" || $siteName eq "" ||
        $probeName eq "")
    {
        $logObj->dubug("[getMetaDataOfFile]: Host name: [$hostName]. YMD: [$ymd]. HMS: [$hms]. OS: [$os]. Type: [$type]. Interval: [$interval]. Samples: [$samples]. Site name: [$siteName]. Probe name: [$probeName].");
        $logObj->warn("[getMetaDataOfFile]: File name not in proper format: [$fileName].");
        
        $retVal = 1;
        goto EXIT;
    }

    $probeRawDataFolder = $DEST_HOME_DIR . "/" . $siteName . "/Linux_". $probeName . "/rawdata";

    # Create the folder for this Linux Probe in the probe folder.
    unless (defined eval {mkpath($probeRawDataFolder)})
    {
        $logObj->error("[getMetaDataOfFile]: Not able to create folder: [$probeRawDataFolder].");
        $retVal = 1;

        goto EXIT;
    }

    $logObj->info("[getMetaDataOfFile]: [$probeRawDataFolder].");
    if (! chdir($probeRawDataFolder))
    {
        $logObj->error("[getMetaDataOfFile]: Can't cd into [$probeRawDataFolder], error: [$!].");
        $retVal = 1;

        goto EXIT;
    }

EXIT:

    return $retVal;
}

# It read the file data over network and dump the data in a file at location specific to a Linux Probe.
#
# @param:
#    None.
#
# @return:
#    Nothing.
sub getDataFile
{
    # Used to get file data.
    my $buf = "";

    # Used to store total data received so far.
    my $total_count = 0;

    # Used to store error code.
    my $err = "";

    # This variable is used to store the return value of function calls.
    my $retVal = 0;

    # Create a temporary file to write the file data received from network.
    if (! open(OUTF, '>', "tmp.$fileName"))
    {
        $logObj->error("[getDataFile]: Failed to create temporary file [tmp.$fileName].");
        $retVal = 1;

        goto EXIT;
    }

    # Read all file data and write in the temporary file.
P1: for ( ; ; )
    {
        my $count = $stdin->sysread($buf, $BUFSIZE);
        if (! defined($count))
        {
            $err = "NET";
            last P1;
        }

        # Zero means EOF.
        if ($count == 0)
        {
            $err = "EOF";
            last P1;
        }

        $total_count += $count;

        # Write data in temporary file.
        print OUTF $buf;

        if ($total_count >= $fileSize)
        {
            last P1;
        }
    }

    # Close the temporary file.
    if (close(OUTF))
    {
        # If there is some error while writing data to file.
        if ($err)
        {
            if ($err eq "EOF")
            {
                $logObj->error("[getDataFile]: EOF on input. Needed [$fileSize] bytes while got [$total_count].");
                $retVal = 1;

                goto EXIT;
            }
            elsif ($err eq "NET")
            {
                $logObj->error("[getDataFile]: Error while reading data from network.");
                $retVal = 1;

                goto EXIT;
            }
            else
            {
                $logObj->error("[getDataFile]: Failed while closing file [tmp.$fileName], error [$!]");
                $retVal = 1;

                goto EXIT;
            }
        }
        # If there is no problem in writing the data and send acknowledgement to remote machine.
        else
        {
            $logObj->info("[getDataFile]: Saved file [$fileName].");

            my $msg = &transmitData($stdout, "ok\n", 3);
            if (defined($msg))
            {
                $logObj->error("[getDataFile]: Error sending ok to remote: [$msg].");
                $retVal = 1;

                goto EXIT;
            }
        }
    }
    else
    {
        $logObj->error("[getDataFile] : Failed to close the file [tmp.$fileName].");

        # We don't want to lose data, so even on a write error, keep the file. If it's zero-length, then we could delete it.
        if (-e "tmp.$fileName" && -z "tmp.$fileName")
        {
            $logObj->info("[getDataFile]: Zero-length file, deleting it. [tmp.$fileName].");
            unlink("tmp.$fileName");
        }

        $retVal = 1;
        goto EXIT;
    }

    # Make an END block, to do the final rename at the end.
    if (defined($fileName))
    {
        # TODO: What is the use of "eval q"
        eval q(
        END
        {
            $logObj->info("[getDataFile]: Renaming file [tmp.$fileName].");
            rename("tmp.$fileName", $fileName);
        };)
    }

EXIT:

    return $retVal;
}

# Read file data in a given buffer using sysread() API.
#
# @param:
#    $_[0]: [In] File handle.
#    $_[1]: [Out] Buffer.
#    $_[2]: [In] Data size.
#
# @return:
#    Return value is undef on success. Else error message.
sub receiveData
{
    my ($handle, $buf, $count) = @_;

    my $offset = 0;

    for ( ; ; )
    {
        my $got = $handle->sysread($$buf, $count, $offset);
        if (! defined($got))
        {
            return $!;
        }

        $count -= $got;

        if ($count <= 0)
        {
            return undef;
        }

        $offset += $got;
    }
}

# Send file data to remote machine using syswrite() API.
#
# @param:
#    $_[0]: [In] File handle.
#    $_[1]: [In] Buffer.
#    $_[2]: [In] Data size.
#
# @return:
#    Return undef on success. Else error string.
sub transmitData
{
    my ($handle, $buf, $count) = @_;

    my $offset = 0;

    for ( ; ; )
    {
        $logObj->debug("Buffer: [$buf], Count: [$count], Offset: [$offset].");
        my $written = $handle->syswrite($buf, $count, $offset);

        if (! defined($written))
        {
            return $!;
        }

        $count -= $written;

        if ($count <= 0)
        {
            last;
        }

        $offset += $written;
    }

    return undef;
}