#!/usr/bin/env perl
#
#$Id: serialmgrd,v 1.25 2009/06/27 18:24:33 staffcvs Exp $
#

use Data::Dumper;
use Error qw(:try);
use POSIX;
use Sys::Syslog;

my $stuff = [];
my $hosts = [];
my $hup   = 0;

my $consolidate_exe = "/usr/bin/consolidate";
my $sympathy_exe    = "/usr/bin/sympathy";

my @keys = qw(host portcode baud task user password name options);

my $labeling = {
    shedu      => { offsets => [ 0, 7, 3, 11, 15 ], },
    phoenix    => { offsets => [ 0, 3, 11, 15 ], },
    leprechaun => { offsets => [ 0, 3 ], },
    imp        => { offsets => [ 0, 3 ], },
    pixie      => { offsets => [ 0, 3, 11 ], },
    basilisk   => { offsets => [ 0, 3, 11, 7, 15 ], },
    wyvern     => { offsets => [ 0, 3, 11 ], },
    goblin     => { offsets => [ 0, 3 ], },
    naga       => { offsets => [ 0, 5 ], },
    dragon     => { offsets => [ 0, 3, 11 ], },
    woking     => { offsets => [ 0, 3, 11 ], },
};

sub port_decode($$) {
    my ( $host, $code ) = @_;

    return "/dev/ttyS" . $code if ( $code =~ /^\d+$/ );
    return "/dev/ttyUSB" . $1 if ( $code =~ /^u.(\d+)$/ );
    return "/dev/ttyS" . ( $labeling->{$host}->{offsets}->[$1] + $2 )
      if ( $code =~ /^(\d+)\.(\d+)$/ );

    return $code;
}

sub quit() {
    sleep(10);
    exit(1);
}

sub read_file() {
    my $lines = [];
    my $c;
    open FILE, "<" . "/etc/serialmgr/config";

    while (<FILE>) {
        next if ( $_ =~ /^#/ );

        chomp;
        @_ = split(':');
        $c = 0;
        push @$lines, { map { $keys[ $c++ ] => $_ } @_ };
    }
    close FILE;
    return $lines;
}

sub hostname() {
    my ( $sysname, $nodename, $release, $version, $machine ) = POSIX::uname();
    return $1 if ( $nodename =~ /([^.]+)\./ );
    return $nodename;
}

sub check_users($) {
    my $c = shift;

    for my $l (@$c) {
        my $shell;
        if ( $l->{task} eq "sympathy" ) {
            $shell = "/usr/bin/run_sympathy";
        }
        else {
            $shell = "/usr/bin/run_conclient";
        }
        if ( not defined getpwnam( $l->{user} ) ) {
            syslog( LOG_ERR, "creating an account for user " . $l->{user} );
            mkdir "/export/home";
            mkdir "/export/home/colo";
            my @cmd = ();
            push @cmd, "useradd";
            push @cmd, "-d", "/export/home/colo/" . $l->{user};
            if ( length( $l->{password} ) > 2 ) {
                push @cmd, "-p", $l->{password};
            }
            push @cmd, "-s", $shell;
            push @cmd, "-m";
            push @cmd, $l->{user};

            #print join( ' ', @cmd ), "\n";
            system @cmd;
        }
        if ( not defined getgrnam( $l->{user} ) ) {
            syslog( LOG_ERR, "need to create a group for " . $l->{user} );
            quit();
        }
        my $current_shell = ( getpwnam( $l->{user} ) )[8];
        my $uid =  getpwnam( $l->{user} ) ;

        if (( $uid > 0 ) and ( $current_shell ne $shell )) {
            syslog( LOG_ERR,
                "changing shell for user " . $l->{user} . " to " . $shell );
            my @cmd = ();
            push @cmd, "usermod";
            push @cmd, "-s", $shell;
            push @cmd, $l->{user};
            system @cmd;

        }

        if ( length( $l->{password} ) > 2 ) {
            my $pwd = ( getpwnam( $l->{user} ) )[1];
            if ( $l->{password} ne $pwd ) {
                syslog( LOG_ERR, "changing password for user " . $l->{user} );
                my @cmd = ();
                push @cmd, "usermod";
                push @cmd, "-p", $l->{password};
                push @cmd, $l->{user};
                system @cmd;

            }
        }

        if ( $l->{user} eq 'root' ) {
        }
        elsif ( ! -d "/export/home/colo/".$l->{user} ) {
            syslog( LOG_ERR, 
                 "need to create a home directory for " . $l->{user} );
            quit();         
        }
        elsif ( ( getpwnam( $l->{user} ) )[2] != ( stat _ )[4] ||
                  ( getpwnam( $l->{user} ) )[3] != ( stat _ )[5] ) {
            syslog( LOG_ERR, 
                "changing home directory ownership for user " . $l->{user} );
            my @cmd = (); 
            push @cmd, "chown", "-R";
            push @cmd, $l->{user}.".".$l->{user};
            push @cmd, "/export/home/colo/".$l->{user};
            system @cmd;
        }
    }

}

sub resolve_users($) {
    my $c = shift;
    my $ret=[];
    my $host  = hostname();

    for my $l (@$c) {
	next if ( $l->{host} ne $host );

        my $uid = $l->{user};
        my $gid = $l->{user};

        if ( $uid =~ /[^\d]/ ) {
            $uid = getpwnam($uid);
            if ( !defined($uid) ) {
                syslog( LOG_ERR, "unknown user " . $l->{user} );
                quit();
            }
        }

        if ( $gid =~ /[^\d]/ ) {
            $gid = getgrnam($gid);
            if ( !defined($gid) ) {
                syslog( LOG_ERR, "unknown group " . $l->{user} );
                quit();
            }
        }
        $l->{uid}       = $uid;
        $l->{gid}       = $gid;
        $l->{backoff}   = 0;
        $l->{badstarts} = 0;
        $l->{running}   = 0;

        if ( $l->{uid} > 0 ) {
            $l->{socket}     = "/export/home/colo/" . $l->{user} . "/console-socket";
            $l->{socket_dir} = "/export/home/colo/" . $l->{user};
            $l->{log}        = "/export/home/colo/" . $l->{user} . "/logs/console";
            $l->{log_dir}    = "/export/home/colo/" . $l->{user} . "/logs";
        }
        else {
            if ( $l->{task} eq "sympathy" ) {
                $l->{socket}     = "/root/sympathy/" . $l->{name};
                $l->{socket_dir} = "/root/sympathy";
                $l->{log}        = "/root/sympathy/" . $l->{name} . ".log";
                $l->{log_dir}    = "/root/sympathy";
            }
            else {
                $l->{socket}     = "/root/consoles/" . $l->{name};
                $l->{socket_dir} = "/root/consoles";
                $l->{log}        = "/root/consoles/" . $l->{name} . ".log";
                $l->{log_dir}    = "/root/consoles";
            }
        }
	push @$ret,$l;
    }

return $ret;
}

sub get_config() {
    my $cf    = read_file();
    my $stuff = [];
#    my $host  = hostname();

    for $line (@$cf) {
#        if ( $line->{host} eq $host ) {
            $line->{device} = port_decode( $line->{host}, $line->{portcode} );
            push @$stuff, $line;
#        }
    }

    return $stuff;
}

sub task_exe_args ($) {
    my ($t) = @_;
    my @args;
    my $exe;

    if ( $t->{task} eq "sympathy" ) {

        push @args, "-S";
        push @args, "-s";
        push @args, "-F";
        push @args, "-d";
        push @args, $t->{device};
        push @args, "-b";
        push @args, $t->{baud};
        push @args, "-K";
        push @args, "-k";
        push @args, $t->{socket};
        push @args, "-L";
        push @args, $t->{log};
        push @args, "-R";
	push @args, $t->{options} if $t->{options};

        $exe = $sympathy_exe;
    }
    else {
        push @args, "-d";
        push @args, "-e";
        push @args, "-s";
        push @args, $t->{baud} . ",n,8";
        push @args, "-f";
        push @args, "none";
        push @args, "-l";
        push @args, "1048576";
        push @args, $t->{device};
        push @args, $t->{log};
        push @args, $t->{socket};
	push @args, $t->{options} if $t->{options};

        $exe = $consolidate_exe;

    }
    return ($exe, @args);
}

sub task_info_string ($) {
    my ($t) = @_;
    return join "\0", $t->{uid}, $t->{gid}, task_exe_args($t);
}

sub start($) {
    my $t = shift;

    $t->{started} = time();

    my $pid = fork();
    if ( !defined($pid) ) {
        syslog( LOG_ERR, "fork failed" );
        return undef;
    }
    elsif ( $pid != 0 ) {
        $t->{pid}     = $pid;
        $t->{running} = 1;
        syslog( LOG_ERR,
                "Started  "
              . $t->{task} . " for "
              . $t->{name} . " on "
              . $t->{device} . " ("
              . $t->{portcode}
              . ") pid "
              . $t->{pid} );
        return $pid;
    }

    # use sympathy to clean up any old lock files whilst we're still root
    system( $sympathy_exe, "-S", "-C", "-d", $t->{device} );

    chown $t->{gid}, $t->{uid}, $t->{device};
    $( = $) = $t->{gid} if ( $t->{gid} != 0 );
    $< = $> = $t->{uid} if ( $t->{uid} != 0 );

    mkdir $t->{socket_dir};
    mkdir $t->{log_dir};

    my ($exe, @args) = task_exe_args($t);

    syslog(LOG_ERR, $exe. " ". join( ' ', @args ));

    exec( $exe, @args );

    exit(1);

}

sub taskinfo ($) {
    my ($t) = @_;
    return $t->{task} . " for "
         . $t->{name} . " on "
         . $t->{device} . " ("
         . $t->{portcode}
         . ") pid "
         . $t->{pid};
}

sub died($) {
    my $t = shift;

    my $ranfor = $t->{ended} - $t->{started};

    syslog( LOG_ERR,
            "Finished "
          . $t->{task} . " for "
          . $t->{name} . " on "
          . $t->{device} . " ("
          . $t->{portcode}
          . ") pid "
          . $t->{pid}
          . " lasted "
          . $ranfor
          . "s" );

    if ( $ranfor < 20 ) {
        $t->{badstarts}++;
    }

    if ( $t->{badstarts} > 5 ) {
        syslog( LOG_ERR,
                "Looping  " . taskinfo( $t ) . " suspended" );
        $t->{backoff}   = time() + 5 * 60;
        $t->{badstarts} = 0;
    }
    $t->{pid} = undef;

}

sub hup() {
    $hup++;
}

sub medea($) {
    my ($killstuff) = @_;

    for $t (@$killstuff) {
        kill POSIX::SIGTERM, $t->{pid} if ( defined( $t->{pid} ) );
    }
}

sub terminate() {
    syslog( LOG_ERR, "shutting down on signal" );
    medea($stuff);
    exit(0);
}

sub sigchld() {
    for (;;) {
        my $got = waitpid -1, WNOHANG;
        last if $got<=0;
        my ($t) = grep { $_->{running} and $_->{pid} == $got } @$stuff;
        if ($t) {
            syslog( LOG_INFO, "Terminated " . taskinfo( $t ). ": $?" );
            $t->{ended}   = time();
            $t->{running} = 0;
        } else {
            syslog( LOG_INFO, "Terminated unknown pid $got: $?" );
        }
    }
}

openlog( "serialmgrd", "pid,console", LOG_LOCAL0 );

syslog( LOG_ERR, "starts" );

$stuff = get_config();
check_users($stuff);
$stuff=resolve_users($stuff);

$SIG{HUP}  = \&hup;
$SIG{INT}  = $SIG{TERM} = \&terminate;
$SIG{CHLD} = \&sigchld;

while (1) {
    my $now = time();

    sigchld();

    for $t (@$stuff) {
        if ( defined $t->{pid} ) {
            if ( ( $t->{running} ) and ( kill( 0, $t->{pid} ) == 0 ) ) {
                $t->{running} = 0;
                $t->{ended}   = time();
            }

            if ( not $t->{running} ) {
                died($t);
            }
        }

        if ( ( not defined( $t->{pid} ) ) and ( $t->{backoff} < $now ) ) {
            start($t);

        }
    }


    if ( scalar(@$stuff) < 1 ) {
        syslog( LOG_ERR, "Nothing to do - sleeping 3600s" );
        sleep(3600);
    }
    if ($hup) {
        $hup = 0;
        syslog( LOG_ERR, "SIGHUP rereading config" );

        my $oldstuff = $stuff;
        my %oldmap;
        foreach my $t (@$stuff) {
            $oldmap{ task_info_string $t } = $t;
        }

        $stuff = get_config();
        check_users($stuff);
        $stuff=resolve_users($stuff);

        foreach my $t (@$stuff) {
            my $old = $oldmap{ task_info_string $t };
            next unless $old && $old->{running};
            foreach my $key (qw(running started ended pid)) {
                $t->{$key} = $old->{$key};
                $old->{$key} = undef;
            }
            syslog( LOG_INFO, "Keeping " . taskinfo($t) );
        }

        medea($oldstuff);
    }

    sleep(10);
}
