225 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
		
		
			
		
	
	
			225 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
| 
								 | 
							
								#!/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');
							 | 
						||
| 
								 | 
							
								}
							 |