User:AnomieBOT/source/bot-instance.pl

#!/usr/bin/perl -w

use strict;

# binmodes
$|=1;
binmode STDOUT, ':utf8';
binmode STDERR, ':utf8';

die "Cannot run as root\n" if $<==0;

my $scriptname=$0;
my @args=@ARGV;
my $botnum = shift or die "USAGE: $0 botnum task-dir\n";
my $dir = shift or die "USAGE: $0 botnum task-dir\n";
$dir.='/' unless substr($dir,-1) eq '/';

use Cwd;
use File::Basename;
use lib File::Basename::dirname( Cwd::realpath( __FILE__ ) );
use AnomieBOT::API;
use Sys::Hostname;
use POSIX ':sys_wait_h';
use sort 'stable';

my @tasks=();
my @tasknames=();
my %jobstatuses=();

chdir($AnomieBOT::API::basedir);

sub warnTS {
    warn POSIX::strftime('[%F %T] ', localtime), @_;
}

sub die2 {
    warnTS @_;
    exit(0);
}

my $api=AnomieBOT::API->new('conf.ini', $botnum);
$api->read_command();
$api->reopen_logs();

opendir(D, $dir) or die2 "($botnum) Could not open task directory: $!\n";
while(my $file=readdir(D)){
    next if -d $dir.$file;
    next if substr($file,0,1) eq '.';
    my $task='';
    if(!open(X, '<:utf8', $dir.$file)){
        warnTS "($botnum) Could not open task file $file: $!\n";
        next;
    }
    while(<X>){
        $task=$1 if /^package (.*);$/;
    }
    close(X);
    if($task eq ''){
        warnTS "($botnum) Invalid task file $file\n";
        next;
    }
    AnomieBOT::API::load($dir.$file);
    my $t=$task->new();
    next unless $t->approved == $botnum;
    push @tasks, $t;
    $task=~s/.*:://;
    push @tasknames, $task;
}
closedir(D);
unless(@tasks){
    warnTS "($botnum) No tasks\n";
    exit(0);
}

# Replace spaces with NBSP, gridengine doesn't handle spaces in $0 well.
($0=$api->user." ($botnum): ".join(' ', @tasknames))=~s/\s/\N{U+00A0}/g;
warnTS "Bot instance $botnum starting, pid $$ on " . hostname . "\n";

$SIG{QUIT}=sub { warnTS "($botnum) QUIT received!\n"; $api->halting('term'); };
$SIG{TERM}=sub { warnTS "($botnum) TERM received!\n"; $api->halting('term'); };
$SIG{INT}=sub { warnTS "($botnum) INT received!\n"; $api->halting('term'); };
$SIG{HUP}=sub { warnTS "($botnum) HUP received!\n"; $api->halting('restart'); };
$SIG{USR1}=sub { warnTS "($botnum) USR1 received! (ignoring)\n"; };
$SIG{USR2}=sub { warnTS "($botnum) (M) USR2 received!\n"; $api->halting('term'); };

# We need to somehow sort both @tasks and @tasknames. This does it by sorting
# indices first and then taking slices of both arrays.
my @i = sort { $tasks[$a]->{'order'} <=> $tasks[$b]->{'order'} } ( 0 .. $#tasks );
@tasks = @tasks[@i];
@tasknames = @tasknames[@i];
my @next=map { 0 } @tasks;

for my $taskname (@tasknames) {
    $jobstatuses{$taskname} = $api->cache->get( "status:$taskname" ) // { lastrun => 0, nextrun => 0 };
    $jobstatuses{$taskname}{'hostname'} = hostname;
    $jobstatuses{$taskname}{'botnum'} = $botnum;
    $jobstatuses{$taskname}{'status'} = 'job starting';
    $api->cache->set( "status:$taskname", $jobstatuses{$taskname} );
}

$api->onpause( sub {
    my $flag = shift;

    for(my $i=0; $i<@tasks; $i++){
        my $taskname = $tasknames[$i];
        next if $next[$i]<0;
        if ( $flag ) {
            next if $jobstatuses{$taskname}{'status'} eq 'paused';
            $jobstatuses{$taskname}{'pause saved status'} = $jobstatuses{$taskname}{'status'};
            $jobstatuses{$taskname}{'status'} = 'paused';
        } elsif ( exists( $jobstatuses{$taskname}{'pause saved status'} ) ) {
            $jobstatuses{$taskname}{'status'} = $jobstatuses{$taskname}{'pause saved status'};
            delete $jobstatuses{$taskname}{'pause saved status'};
        }
        $api->cache->set( "status:$taskname", $jobstatuses{$taskname} );
    }
} );

$api->login();
while(!$api->halting){
    while ( !$api->halting ) {
        my ($statuslist, $cas) = $api->cache->gets( 'joblist' );
        my $any = 0;
        for my $taskname (keys %jobstatuses) {
            $api->cache->add( "status:$taskname", $jobstatuses{$taskname} );
            unless ( grep $_ eq $taskname, @$statuslist ) {
                $any = 1;
                push @$statuslist, $taskname;
            }
        }
        last if !$any;
        warnTS "($botnum) Updating joblist\n";
        last if $api->cache->cas( 'joblist', $statuslist, $cas );
        sleep 1;
    }
    last if $api->halting;

    my $wait=60; # maximum wait time
    my $realwait=1e100;
    for(my $i=0; $i<@tasks; $i++){
        my $taskname = $tasknames[$i];
        next if($next[$i]<0 || $next[$i]>time());
        warnTS "($botnum) Starting task $taskname (".ref($tasks[$i]).")\n" if($api->DEBUG & 1);
        $jobstatuses{$taskname}{'status'} = 'running';
        $jobstatuses{$taskname}{'lastrun'} = time();
        $api->cache->set( "status:$taskname", $jobstatuses{$taskname} );
        my $w;
        my $terminated = 0;
        eval { $w=$tasks[$i]->run($api); };
        if($@){
            warnTS "($botnum) Caught error from task $taskname: $@\n";
            $jobstatuses{$taskname}{'status'} = 'error';
            $jobstatuses{$taskname}{'nextrun'} = 0;
            $terminated = 1;
        } elsif(!defined($w)){
            warnTS "($botnum) Task $taskname returned undef\n" if($api->DEBUG & 1);
            $jobstatuses{$taskname}{'status'} = 'ended';
            $jobstatuses{$taskname}{'nextrun'} = 0;
            $terminated = 1;
        } else {
            warnTS "($botnum) Task $taskname returned $w\n" if($api->DEBUG & 1);
            $next[$i]=time()+$w;
            $wait=$w if $w<$wait;
            $realwait=$w if $w<$realwait;
            $jobstatuses{$taskname}{'status'} = $tasks[$i]->status;
            $jobstatuses{$taskname}{'nextrun'} = $next[$i];
        }

        # Update job status
        $api->cache->set( "status:$taskname", $jobstatuses{$taskname} );
        if ( $terminated ) {
            delete $tasks[$i];
            # Indicate ended task in $0
            $tasknames[$i] = '(' . $tasknames[$i] . ')';
            ($0=$api->user." ($botnum): ".join(' ', @tasknames))=~s/\s/\N{U+00A0}/g;
            $next[$i]=-1;
        }

        last if $api->halting;
    }
    die2 "($botnum) No tasks" unless @tasks;
    warnTS "($botnum) Sleeping for $wait seconds\n" if($wait>0 && ($api->DEBUG & 1));
    last if $api->halting;
    $api->drop_connections() if $realwait>300;
    sleep($wait) if $wait>0;
}

for(my $i=0; $i<@tasks; $i++){
    my $taskname = $tasknames[$i];
    next if $next[$i]<0;
    $jobstatuses{$taskname}{'status'} = 'job ended';
    $jobstatuses{$taskname}{'nextrun'} = 0;
    $api->cache->set( "status:$taskname", $jobstatuses{$taskname} );
}

my $haltcode = $api->halting;

if($haltcode eq 'term'){
    warnTS "($botnum) Exiting now...\n";
    $api->DESTROY;
    warnTS "($botnum) Exited!\n";
    exit(0);
} elsif($haltcode =~ /^restart/){
    warnTS "($botnum) Restarting bot...\n";
    $api->DESTROY;
    warnTS "($botnum) Exited!\n";
    exec {$scriptname} ($scriptname,@args);
    warn "($botnum) Could not re-exec $scriptname: $!\n";
    exit(1);
} else {
    warnTS "($botnum) Exiting for unknown halt code $haltcode...\n";
    $api->DESTROY;
    warnTS "($botnum) Exited!\n";
    exit(2);
}