#!/usr/bin/perl
#
# Danga's Statistics Server Replicator
# Very lightweight program that replicates data from one statistic server (subscribe *)
# to another one.  Very, very simple and lightweight.  Uses open3() to use the ssh command
# to connect to a remote statistics server.
#
# Command line options:
#
#    -c STRING  set what config file to use for options
#    -D         if present, tell the server to daemonize
#
# Configuration file format:
#
#    server = STRING          location of statistics server
#    port = INT               port number server listens on
#    ssh_host = STRING        host of remote SSH to tunnel through
#    ssh_port = INT           port SSH daemon is on
#    ssh_key = STRING         filename to use for our private key
#    ssh_user = STRING        username to identify as to SSH server
#
# Copyright 2004, Danga Interactive
#
# Authors:
#   Mark Smith <marksmith@danga.com>
#
# License:
#   undecided.
#

# uses
use strict;
use IO::Socket;
use IO::Select;
use IPC::Open3;
use Getopt::Long;
use Carp;
use POSIX ":sys_wait_h";
use Socket qw(PF_INET IPPROTO_TCP SOCK_STREAM);
use Danga::Socket;

# Command-line options will override
my ($daemonize, $conf_file);
Getopt::Long::Configure( "bundling" );
Getopt::Long::GetOptions(
    'D|daemon'   => \$daemonize,
    'c|config=s' => \$conf_file,
);
die "You must at least specify --config=FILENAME for me to work.\n" unless $conf_file;
die "File '$conf_file' doesn't exist for configuration.\n" unless -e $conf_file;

# parse the config file
my %config;
open FILE, "<$conf_file"
    or die "Unable to open config file: $!\n";
while (my $line = <FILE>) {
    if ($line =~ /^\s*([^#].*)\s*=\s*(.*)\s*/) {
        my ($l, $r) = (trim($1), trim($2));
        if ($l eq 'server') { $config{to}{host} = $r; }
        elsif ($l eq 'port') { $config{to}{port} = $r+0; }
        elsif ($l eq 'ssh_key') { $config{from}{key} = $r; }
        elsif ($l eq 'ssh_host') { $config{from}{host} = $r; }
        elsif ($l eq 'ssh_port') { $config{from}{port} = $r+0; }
        elsif ($l eq 'ssh_user') { $config{from}{username} = $r; };
    }
}
close FILE;

# daemonize?
daemonize() if $daemonize;

# connect to stats server we're replicating TO.  messy.
my $dsock = new_connection()
    or die "Can't get initial socket... exiting.\n";

# now setup our ssh open3 sockets
my ($reader, $writer);
my $sshpid = open3($writer, $reader, $reader, 'ssh', '-C', '-tt', '-i', $config{from}{key},
                   '-p', $config{from}{port}, '-l', $config{from}{username}, $config{from}{host});
print $writer "sub *\n";

# always kill off our SSH connection
$SIG{TERM} = sub { kill 15, $sshpid; exit 0; };
$SIG{INT} = sub { kill 15, $sshpid; exit 0; };

# variables used later
my $readbuf;

# this is our main reading loop
my $sobj = new IO::Select;
$sobj->add($reader);

# post event loop
my $postloop = sub {
    # check if somehow ssh died on us? :-/
    my $pid = waitpid -1, WNOHANG;
    if ($pid == $sshpid) {
        # sleep a few seconds and try to spawn a new one
        $sshpid = 0;
        while (!$sshpid) {
            print "Lost SSH connection... sleeping 5 seconds.\n";
            sleep 5;
            $sshpid = open3($writer, $reader, $reader, 'ssh', '-C', '-tt', '-i', $config{from}{key},
                            '-p', $config{from}{port}, '-l', $config{from}{username}, $config{from}{host});
            print $writer "sub *\n";
        }
    }

    # if our parent socket is closed...
    unless ($dsock && !$dsock->{closed}) {
        # create a new one if we can
        print "Lost SPUD connection... reconnecting...\n";
        $dsock = new_connection();
        unless ($dsock) {
            print "\tUnable to connect... pausing a second.\n";
            sleep 1;
            return 1;
        }
    }

    # see if we can read from our socket yet
    my @ready = $sobj->can_read(0.1);
    return 1 unless @ready;

    # must be ready to read
    my $bytes = sysread $reader, $readbuf, 1024, length $readbuf;
    while ($readbuf =~ s/(.+?)\r?\n//) {
        my $line = $1;
        next unless $line =~ /^set/i;
        $dsock->write("Q$line\r\n");
    }
    return 1;
};

# now configure the client
Client->SetLoopTimeout(100); # 100 milliseconds timeout
Client->SetPostLoopCallback($postloop); # have it call us

# now run the event loop
Client->EventLoop();

# kill off our child too
kill 15, $sshpid;
print "replicator terminating\n";

# daemonizer routine
sub daemonize {
    my($pid, $sess_id, $i);

    ## Fork and exit parent
    if ($pid = fork) { exit 0; }

    ## Detach ourselves from the terminal
    croak "Cannot detach from controlling terminal"
        unless $sess_id = POSIX::setsid();

    ## Prevent possibility of acquiring a controling terminal
    $SIG{'HUP'} = 'IGNORE';
    if ($pid = fork) { exit 0; }

    ## Change working directory
    chdir "/";

    ## Clear file creation mask
    umask 0;

    ## Close open file descriptors
    close(STDIN);
    close(STDOUT);
    close(STDERR);

    ## Reopen stderr, stdout, stdin to /dev/null
    open(STDIN,  "+>/dev/null");
    open(STDOUT, "+>&STDIN");
    open(STDERR, "+>&STDIN");
}

# little trimming sub
sub trim {
    my $res = shift;
    $res =~ s/^\s+//;
    $res =~ s/\s+$//;
    return $res;
}

# connect anew to the SPUD server we're replicating to
# NOTE: can return undef if we can't get to the server!
sub new_connection {
    my $sock;
    socket $sock, PF_INET, SOCK_STREAM, IPPROTO_TCP;
    die "error: can't make socket\n"
        unless $sock && defined fileno($sock);
    IO::Handle::blocking($sock, 0);
    connect $sock, Socket::sockaddr_in($config{to}{port}, Socket::inet_aton($config{to}{host}));
    my $dsock = Client->new($sock)
        or return undef;
    $dsock->watch_write(1);
    $dsock->watch_read(1);
    return $dsock;
}

###########################################################################
### Client class for use in processing input/output
package Client;

use base "Danga::Socket";

sub event_read {
    # read and toss, we don't care about input from the user here
    my Client $self = $_[0];
    my $bref = $self->read;
}

sub event_err {
    # connection died?
    my Client $self = $_[0];
    $self->close('event_err');
}

sub event_hup {
    # connection to server died...
    my Client $self = $_[0];
    $self->close('event_hup');
}
