#!/usr/bin/perl -w

use strict;
use warnings;

use Getopt::Long;
use Unix::PID;
use vars qw($k);
GetOptions (
    'kill|k'  => \$k,
);
## kill the process
if($k){
    $0 =~/(.*)\/(.*\.pl)/;
    my $pname = '/'.$2;
    my @pids = `ps ax | grep $pname | grep -v grep | awk {'print \$1'}`;
    foreach my $pid (@pids) {
        `kill -9 $pid` unless ($pid == $$);
    }
    exit;
}

$ENV{PATH} = '/bin:/usr/bin';
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
delete @ENV{qw(HOME LOGNAME MAIL USER USERNAME)};

package SyncServer;
use IPC::Run qw( run timeout );
use vars qw($VERSION);
$VERSION = '2.0';

# the user and group under which the regular operations
# (sources checkout, sources export and configuration, interogation)
# are made
use constant SYNC_USER             => 'root'; 
use constant SYNC_GROUP            => 'root';
use constant SYNC_GROUP_ADDITIONAL => 'root';
use constant GET_SYNC_USER_HOME    => sub {
    '/var/www';
};

# svn binaries
use constant SVNVERSION_PATH  => '/usr/bin/svnversion';
use constant SVNCLIENT_PATH   => '/usr/bin/svn';
use constant PERL_PATH   => '/usr/bin/perl';
use constant KILL_PATH   => '/bin/kill';
my %CONF = (
    besget_servers => {
    checked_out_dir      => 'co_besget',
    exported_dir         => 'export_besget',
    restartable_services => [{
        name      => 'Apache2',
        ctl_path  => '/etc/init.d/apache2',
        pid_file  => '/var/run/apache2.pid',
        wait_time => 30, # seconds
                }],
    },
    dorics_servers => {
    checked_out_dir      => 'dorics',
    exported_dir         => 'export_dorics',
    restartable_services => [{
        name      => 'Apache2',
        ctl_path  => '/etc/init.d/apache2',
        pid_file  => '/var/run/apache2.pid',
        wait_time => 30, # seconds
                }],
    },
);
$CONF{webmail_servers} = $CONF{web_servers};


my @saved_uid = ($<, $>);
my @saved_gid = ($(, $));

sub new {
    bless { 'server' => $RPC_SyncServer_SVN::server }, shift;
}

sub lose_privileges {
    my $uid = getpwnam(SYNC_USER);
    my $gid = getgrnam(SYNC_GROUP);
    my $gid_additional = join(" ", map {
    scalar getgrnam($_);
    } split(/\s+/, SYNC_GROUP_ADDITIONAL));
    $gid_additional = $gid if $gid_additional =~ /^\s*$/;
    $( = $gid;
    $) = "$gid $gid_additional";
    $> = $< = $uid;
}

sub restore_privileges {
    ($<, $>) = @saved_uid;
    ($(, $)) = @saved_gid;
}

sub get_head_revision {
    my ($self, $conf_type) = @_;
    $self->{server}->Debug('get_head_revision');

    my $co_dir = $CONF{$conf_type}->{checked_out_dir};
    return (undef, "No 'checked_out_dir' defined for conf_type '$conf_type'")
    unless $co_dir;

    lose_privileges();

    # svn status -u ~svn-sync/co/empty-for-svnversion
    my ($out, $err);
    local $SIG{PIPE} = 'IGNORE';
    run [ SVNCLIENT_PATH, 'status', '-u',
      join('/', GET_SYNC_USER_HOME->(), $co_dir, 'empty-for-svnversion') ],
        \undef, \$out, \$err;

    restore_privileges();

    if ($out =~ /^Status against revision:\s*(\d+)\s*$/) {
    $out = $1;
    }
    return ($out, $err);
}

sub get_production_revision {
    my ($self, $conf_type) = @_;
    $self->{server}->Debug('get_production_revision');

    my $exported_dir = $CONF{$conf_type}->{exported_dir};
    return (undef, "No 'exported_dir' defined for conf_type '$conf_type'")
    unless $exported_dir;

    lose_privileges();

    # ~svn-sync/export/production is symlink to ~svn-sync/export/r-revision
    my $revision;
    my $path = join('/', GET_SYNC_USER_HOME->(), $exported_dir, 'production');
    if (-l $path) {
    $revision = readlink $path;
    $revision =~ s/^r-(\d+)$/$1/;
    undef $revision if $revision !~ /^\d+$/;
    }

    restore_privileges();

    return $revision;
}

sub get_last_changed_revision {
    my ($self, $conf_type) = @_;
    $self->{server}->Debug('get_last_changed_revision');

    my $co_dir = $CONF{$conf_type}->{checked_out_dir};
    return (undef, "No 'checked_out_dir' defined for conf_type '$conf_type'")
    unless $co_dir;

    lose_privileges();

    # svnversion -c ~svn-sync/co
    my ($out, $err, $revision);
    local $SIG{PIPE} = 'IGNORE';
    run [ SVNVERSION_PATH, '-cn', join('/', GET_SYNC_USER_HOME->(), $co_dir) ],
        \undef, \$out, \$err;
    ($revision) = ($out =~ /^\d+:(\d+)/);

    restore_privileges();

    return ($revision, $err);
}

sub get_checked_out_revision {
    my ($self, $conf_type) = @_;
    $self->{server}->Debug('get_checked_out_revision');

    my $co_dir = $CONF{$conf_type}->{checked_out_dir};
    return (undef, "No 'checked_out_dir' defined for conf_type '$conf_type'")
    unless $co_dir;

    lose_privileges();

    # svnversion ~svn-sync/co/empty-for-svnversion
    my ($out, $err, $revision);
    local $SIG{PIPE} = 'IGNORE';
    run [ SVNVERSION_PATH, '-n', join('/', GET_SYNC_USER_HOME->(),
                      $co_dir, 'empty-for-svnversion') ],
        \undef, \$out, \$err;
    ($revision) = ($out =~ /^(\d+)/);

    restore_privileges();

    return ($revision, $err);
}

sub check_out_sources {
    my ($self, $revision, $conf_type) = @_;
    $self->{server}->Debug('check_out_sources');

    my $co_dir = $CONF{$conf_type}->{checked_out_dir};
    return (undef, "No 'checked_out_dir' defined for conf_type '$conf_type'")
    unless $co_dir;

    lose_privileges();
    my $orig_umask = umask 0002;

    # svn update [-r revision] ~svn-sync/co
    my @cmd = (SVNCLIENT_PATH, 'update');
    push @cmd, ('-r', $1)
    if defined $revision and $revision =~ /^\s*(\d+)\s*$/;
    push @cmd, GET_SYNC_USER_HOME->() . '/' . $co_dir;
    my ($out, $err);
    local $SIG{PIPE} = 'IGNORE';
    run \@cmd, \undef, \$out, \$err;

    umask $orig_umask;
    restore_privileges();

    if ($out =~ /revision (\d+)\./) {
    $self->{server}->Log(notice => "Checked out revision $1");
    } else {
    $self->{server}->Log(notice => "Tried to check out sources, but apparently if failed with this error output:\n$err");
    }
    return ($out, $err);
}

sub export_sources {
    my ($self, $conf_type) = @_;
    $self->{server}->Debug('export_sources');

    my $co_dir = $CONF{$conf_type}->{checked_out_dir};
    return (undef, "No 'checked_out_dir' defined for conf_type '$conf_type'")
    unless $co_dir;

    my $exported_dir = $CONF{$conf_type}->{exported_dir};
    return (undef, "No 'exported_dir' defined for conf_type '$conf_type'")
    unless $exported_dir;

    lose_privileges();
    my $orig_umask = umask 0002;

    # ~svn-sync/co/scripts/export.sh
    my ($out, $err);
    local $SIG{PIPE} = 'IGNORE';
    run [ join('/', GET_SYNC_USER_HOME->(), $co_dir, 'scripts', 'export.sh'),
          $co_dir, $exported_dir ], \undef, \$out, \$err;
    # $err = join('/', GET_SYNC_USER_HOME->(), $co_dir, 'scripts', 'export.sh');
    umask $orig_umask;
    restore_privileges();

    return ($out, $err);
}

sub restart_services {
    my ($self, $conf_type) = @_;

    return (undef, "No 'restartable_services' defined for conf_type '$conf_type'")
    unless exists $CONF{$conf_type}->{restartable_services};

    my ($all_out_stop, $all_err_stop, $all_out_start, $all_err_start);

    foreach my $service (@{ $CONF{$conf_type}->{restartable_services} }) {
    local $SIG{PIPE} = 'IGNORE';

    my ($out_stop, $err_stop, $out_start, $err_start);
    run [ $service->{ctl_path}, 'stop' ], \undef, \$out_stop, \$err_stop;
    $self->{server}->Log(notice => "Stopped $service->{name}");

    eval {
        local $SIG{ALRM} = sub { die "alarm" };
        alarm $service->{wait_time};
        while (-e $service->{pid_file}) {
        select(undef, undef, undef, 0.1);
        }
        alarm 0;
    };
    if ($@ and $@ !~ /^alarm/) {
        $self->{server}->Log(crit => "Error stopping $service->{name}: $@");
    } else {
        local $SIG{PIPE} = 'IGNORE';
        if (run([ $service->{ctl_path}, 'start' ], \undef, \$out_start, \$err_start)) {
        $self->{server}->Log(notice => "Started $service->{name}");
        } else {
        $self->{server}->Log(notice => "Failed to start $service->{name}: $?");
        }
    }

    $all_out_stop .= "$service->{name}: $out_stop\n" if length $out_stop;
    $all_err_stop .= "$service->{name}: $err_stop\n" if length $err_stop;
    $all_out_start .= "$service->{name}: $out_start\n" if length $out_start;
    $all_err_start .= "$service->{name}: $err_start\n" if length $err_start;
    }

    return ($all_out_stop, $all_err_stop, $all_out_start, $all_err_start);
}

sub do_export {
    my ($self, $params) = @_;
    my (@output, $out, $err);

    unless (defined $params and exists $params->{conf_type}) {
    return ({
        method => 'do_export',
        out => '',
        err => "Missing 'conf_type' parameter",
    });
    }

    my $revision = (exists($params->{revision}) and $params->{revision} =~ /^\d+$/) ? $params->{revision} : undef;
    ($out, $err) = $self->check_out_sources($revision, $params->{conf_type});
    push @output, {
    method => 'check_out_sources',
    out => $out,
    err => $err,
    };

    my $isdev = exists($params->{isdev}) ? $params->{isdev} : undef;

    unless ($isdev) {
        ($out, $err) = $self->export_sources($params->{conf_type});
        push @output, {
        method => 'export_sources',
        out => $out,
        err => $err,
        };
    }

    if (exists $params->{restart_services} and $params->{restart_services}) {

    my ($out_stop, $err_stop,
        $out_start, $err_start) = $self->restart_services($params->{conf_type});
    push @output, {
        method => 'stop_services',
        out => $out_stop,
        err => $err_stop,
    };
    push @output, {
        method => 'start_services',
        out => $out_start,
        err => $err_start,
    };
    }
    return @output;
}

sub do_update_vost_dns{
    my ($self, $params) = @_;
    $self->{server}->Log(notice => "Updating vost DNS setting...");
    my $msg = run "perl /usr/local/svn-sync/co/scripts/cron/vost/update_dns.pl" ;
    $self->{server}->Log(notice => "Updated vost DNS setting successful.") if ($msg=~/OK/ig);
    return [$msg];
}

package RPC_SyncServer_SVN;
require RPC::PlServer;
use IO::File;
use POSIX qw(setsid);
use vars qw($VERSION @ISA $server);
$VERSION = '1.0';
@ISA = qw(RPC::PlServer);

# rpc server
use constant PID_FILE        => '/var/run/syncserver_svn.pid';
use constant LOG_FILE        => '/var/log/syncserver_svn';
use constant PORT            => 28;

sub daemonize {
    chdir '/'                 or die "Can't chdir to /: $!";
    open STDIN, '/dev/null'   or die "Can't read /dev/null: $!";
    open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
    defined(my $pid = fork)   or die "Can't fork: $!";
    exit if $pid;
    setsid                    or die "Can't start a new session: $!";
    open STDERR, '>&STDOUT'   or die "Can't dup stdout: $!";
}

eval {
    daemonize();
    my $logfile = IO::File->new(LOG_FILE, 'a')
    or die "Couldn't set logfile: $!";
    $logfile->autoflush(1);

    $server = RPC_SyncServer_SVN->new({
    'pidfile'    => PID_FILE,
    'logfile'    => $logfile,
    'debug'      => 1,
    'user'       => 0, # root, but due to a bug in Net::Daemon 0.37 the
                       # server dies with an error when the name is used
    'group'      => 0, # root
    'localport'  => PORT,
    'mode'       => 'single', # only one connection at a time
    'clients'    => [
             {
                 'mask'   => '^127\.0\.0\.1$',
                 'accept' => 1,
             },
             {
                 'mask'   => '^192\.168\.\d+\.\d+$',
                 'accept' => 1,
             },
             # Deny everything else
             {
                 'mask'   => '.*',
                 'accept' => 1,
             },
             ],
    'methods'    => {
        'RPC_SyncServer_SVN' => {
        'ClientObject' => 1,
        'CallMethod'   => 1,
        'NewHandle'    => 1,
        },
        'SyncServer' => {
        'new'                       => 1,
        'get_head_revision'         => 1,
        'get_production_revision'   => 1,
        'get_checked_out_revision'  => 1,
        'get_last_changed_revision' => 1,
        'check_out_sources'         => 1,
        'export_sources'            => 1,
        'restart_services'          => 1,
        'do_export'                 => 1,
        'do_update_vost_dns'        => 0,
        },
    },
    });
    $SIG{PIPE} = 'IGNORE';
    $server->Bind();
};
if ($@) {
    print STDERR "Couldn't create RPC_SyncServer_SVN instance: $@"; # 'emacs
}

