#!/usr/bin/perl -w
#
# ***** BEGIN LICENSE BLOCK *****
#
# Portions created by Zimbra are Copyright (C) 2005 Zimbra, Inc.
# All Rights Reserved.
#
# The Original Code is: Zimbra Network
#
# ***** END LICENSE BLOCK *****
#

use strict;

use IO::Socket::INET;
use IO::Select;
use Getopt::Long;

sub usage($);
sub ping();
sub getPids($);
sub sendCmd($$);
sub logMsg(@);
sub logErr(@);
sub exitHandler();

# Set up handler to send "quit" to convertd when zmconvertdmon
# is shut down.
$SIG{QUIT} = 'exitHandler';
$SIG{TERM} = 'exitHandler';

# Flush immediately to avoid logging lag
$| = 1;

my $REGEXP_CONVERTD = "java.*com\\.zimbra\\.cs\\.convertd\\.TransformationServer";
my $REGEXP_KVOOP = "kvoop";
my $PID_FILE = "/opt/zimbra/log/zmconvertdmon.pid";

my $host = "localhost";
my $port = 7047;
my $interval = 15;
my $timeoutSecs = 5;
my $convertdScript;
my $list;
my $help;
my $timedOut = 0;
my $connectionFailed = 0;

# Parse options
GetOptions("host=s" => \$host,
	   "port=i" => \$port,
	   "interval=i" => \$interval,
	   "timeout=i" => \$timeoutSecs,
	   "script|c=s" => \$convertdScript,
	   "list" => \$list,
	   "help" => \$help);
if ($help) {
    usage(0);
}
if ($list) {
    my @pids = getPids($REGEXP_CONVERTD);
    if (scalar(@pids) > 0) {
	print("convertd: " . join(",", @pids) . "\n");
    } else {
	print("No convertd processes detected.\n");
    }

    @pids = getPids($REGEXP_KVOOP);
    if (scalar(@pids) > 0) {
	print("kvoop: " . join(",", @pids) . "\n");
    } else {
	print("No kvoop processes detected.\n");
    }
    exit(0);
}
if (!defined($convertdScript)) {
    print(STDERR "--script option not specified.\n");
    usage(1);
}

# Write pid file
open(PID, ">$PID_FILE")
    or die "Could not write to $PID_FILE: $!";
print(PID $$ . "\n");
close(PID);

# Loop indefinitely, ping convertd, and restart if necessary
logMsg("Monitoring convertd at $host:$port.");
while (1) {
    if (!ping()) {
	my $killFailed = 0;

	# Try killing gracefully
	my @pids = getPids($REGEXP_CONVERTD);
	if (scalar(@pids) > 0) {
	    logMsg("Stopping convertd processes: " . join(",", @pids));
	    kill(15, @pids);
	    sleep(3);
	}

	# Really kill anything that's still hanging around
	@pids = getPids($REGEXP_CONVERTD);
	if (scalar(@pids) > 0) {
	    logMsg("Killing convertd processes: " . join(",", @pids));
	    kill(9, @pids);
	    sleep(3);
	}

	# Make sure convertd died
	@pids = getPids($REGEXP_CONVERTD);
	if (scalar(@pids) > 0) {
	    logErr("Unable to kill convertd processes.");
	    $killFailed = 1;
	}

	# Clean up orphaned kvoop processes
	@pids = getPids($REGEXP_KVOOP);
	if (scalar(@pids) > 0) {
	    logMsg("Stopping kvoop processes: " . join(",", @pids));
	    kill(15, @pids);
	    sleep(3);
	}

	# Really kill orphaned kvoop processes that are still hanging around
	@pids = getPids($REGEXP_KVOOP);
	if (scalar(@pids) > 0) {
	    logMsg("Killing kvoop processes: " . join(",", @pids));
	    kill(9, @pids);
	    sleep(3);
	}

	# Make sure kvoop died
	@pids = getPids($REGEXP_KVOOP);
	if (scalar(@pids) > 0) {
	    logErr("Unable to kill kvoop processes.");
	    $killFailed = 1;
	}

	if ($killFailed) {
	    logErr("Unable to kill processes.  Exiting.");
	    exit(1);
	}

	# Start up new convertd process
	logMsg("Running $convertdScript");
	system("$convertdScript &");
	logMsg("Started convertd.");
    }
    sleep($interval);
}	

##################

sub ping() {
    logMsg("Pinging convertd.");
    my $response = sendCmd("ping", 2);
    if ($connectionFailed) {
	logMsg("Could not connect to convertd.");
	return 0;
    }
    if ($response eq "OK") {
        logMsg("Ping successful.");
        return 1;
    } else {
        logErr("Unexpected response from convertd: '$response'.");
        return 0;
    }
}

sub sendCmd($$) {
    my ($cmd, $numBytesToRead) = @_;

    # Connect to convertd
    $connectionFailed = 0;
    my $socket = IO::Socket::INET->new(PeerHost => $host,
                                       PeerPort => $port,
                                       Proto => "tcp",
                                       Timeout => $timeoutSecs);
    if (!defined($socket)) {
	$connectionFailed = 1;
        logErr("Cannot connect to convertd at $host:$port: $!");
        return "";
    }

    print($socket $cmd);

    my $response;
    if ($numBytesToRead > 0) {
	# Use IO::Select for timing out reads
	my $selector = IO::Select->new();
	$selector->add($socket);

	my @handles = $selector->can_read($timeoutSecs);
	my $timeout = 0;

	if (scalar(@handles) == 0) {
	    $connectionFailed = 1;
	} else {
	    my $handle = $handles[0];
	    $handle->read($response, $numBytesToRead);
	}
    }

    $socket->close();

    if ($connectionFailed) {
        return;
    }

    return $response;
}

sub getPids($) {
    my ($commandRegexp) = @_;
    my @pids;
    my $psCmd = "ps -ww axo pid,command |";
    my $firstLine = 1;

    open(PS, $psCmd)
	or die "Error running $psCmd: $!";
    while (<PS>) {
	if ($firstLine) {
	    # Skip header
	    $firstLine = 0;
	    next;
	}
	if (/^\s*(\d+) (.*)/) {
	    my $pid = $1;
	    my $command = $2;
	    if ($command =~ /$commandRegexp/) {
		push(@pids, $pid);
	    }
	} else {
	    logErr("Line did not match: $_");
	}
    }
    close(PS);
    return @pids;
}

sub exitHandler() {
    logMsg("Sending quit command to convertd.");
    sendCmd("quit", 0);
    exit(0);
}

sub logMsg(@) {
    print scalar(localtime()), ": ", @_, "\n";
}

sub logErr(@) {
    print STDERR scalar(localtime()), ": ", @_, "\n";
}

sub usage($) {
    my ($exitCode) = @_;

    print <<USAGE;
Start and monitor the convertd server.  Periodically ping the
convertd server and restart it if down.
Usage: zmconvertdmon --script <convertd_script> [OPTIONS]
  -h, --host      Host where convertd runs (default localhost).
  -p, --port      convertd port number (default 7047).
  -i, --interval  Ping interval in seconds (default 15).
  -t, --timeout   Ping timeout interval in seconds (default 5).
  -c, --script    Script that starts the convertd server.
  -l, --list      List the convertd and KeyView processes and exit.
  --help          Print this help message and exit.
USAGE

    exit($exitCode);
}
