#!/opt/bin/perl

##########################################################################
## All portions of this code are copyright (c) 2015,2016 nethype GmbH   ##
##########################################################################
## Using, reading, modifying or copying this code requires a LICENSE    ##
## from nethype GmbH, Franz-Werfel-Str. 11, 74078 Heilbronn,            ##
## Germany. If you happen to have questions, feel free to contact us at ##
## license@nethype.de.                                                  ##
##########################################################################

# modules we want in our template
use common::sense;
use EV;
use AnyEvent ();
use AnyEvent::Socket ();
use AnyEvent::Fork ();

# create master template
use AnyEvent::Fork::Early ();

# modules we only expect to need in the parent
use Getopt::Long ();

our $VERSION    = 0.1;

our $LISTEN;
our $VERBOSE      = 0;
our $MAX_WORKERS  = 8;
our $MAX_REQUESTS = 1000;
our $REFRESH      = 60; # call agni_refresh every 60s
our $RESTART      = 92707.71;
our @MOUNT;

sub usage {
   print <<EOF;
Usage: $0 [opts...]
       -h | --help                  this list
       -v | --verbose               be more verbose
       -q | --quiet                 be very quiet
       -V | --version               show version

       -l | --listen /unix/domain/socket
       -l | --listen ip:port        bind on given port

       --preload-mod module         preload a perl module
       --preload-bag PATH/GID       preload objects from container
       --preload-obj PATH/GID       preload the given object

       -r | --refresh seconds       agni refresh interval ($REFRESH)

       --workers integer            maximum number of workers ($MAX_WORKERS)
       --max-requests integer       kill workers after this many requests ($MAX_REQUESTS)

       --restart integer            recreate master after this many seconds ($RESTART)

       --mount PATH/GID /LOCATION   mount an agni application

EOF
   exit $_[0];
}

@ARGV or usage 0;

our @PRELOAD_MOD;
our @PRELOAD_OBJ;
our @PRELOAD_BAG;

Getopt::Long::Configure "no_ignore_case";
Getopt::Long::GetOptions
      "help|h"     => sub { usage 0 },
      "verbose|v"  => sub { ++$VERBOSE },
      "quiet|q"    => sub { --$VERBOSE },
      "version|V"  => sub { print "$VERSION\n"; exit 0 },

      "listen|l=s"      => \$LISTEN,
      "preload-mod=s"   => \@PRELOAD_MOD,
      "preload-bag=s"   => \@PRELOAD_BAG,
      "preload-obj=s"   => \@PRELOAD_OBJ,

      "workers|w=i"     => \$MAX_WORKERS,
      "refresh=i"       => \$REFRESH,
      "max-requests=i"  => \$MAX_REQUESTS,
      "restart=i"       => \$RESTART,
      "mount=s{2}"      => \@MOUNT,
;

my $listenguard;

{
   my ($host, $port) = AnyEvent::Socket::parse_hostport $LISTEN
      or die "$LISTEN: cannot parse bind specification (try host:port or /unix/socket)\n";

   $listenguard = AnyEvent::Socket::tcp_bind $host, $port, my $cv = AE::cv;

   $LISTEN = $cv->recv;
}

my $template;
my $refresh_timer = EV::periodic 0, $REFRESH, 0, sub {
   $template->eval ('&PApp::SCGI::Worker::refresh');
};

sub new_template {
   $template = AnyEvent::Fork
         ->new
         ->require ("PApp::SCGI::Worker")
         ->send_fh ($LISTEN)
         ->eval ("&PApp::SCGI::Worker::init", $VERBOSE, $REFRESH, $MAX_REQUESTS)
         ->eval ("&PApp::SCGI::Worker::preload_mod", @PRELOAD_MOD)
         ->eval ("&PApp::SCGI::Worker::preload_bag", @PRELOAD_BAG)
         ->eval ("&PApp::SCGI::Worker::preload_obj", @PRELOAD_OBJ)
         ->eval ("&PApp::SCGI::Worker::mount" , @MOUNT)
         ->eval ("&PApp::SCGI::Worker::post_init");
}

new_template;
my $restart_timer = AE::timer $RESTART, $RESTART, sub {
   new_template;
};

my $workers;
my $idle;
my $listenw;

sub check;

sub add_worker {
   ++$workers;
   ++$idle;

   print "[$$] adding worker (workers $workers, idle $idle).\n" if $VERBOSE >= 2;

   my $state = "0";

   $template->fork->run ("PApp::SCGI::Worker::run", sub {
      my $fh = shift
         or die "$!: unable to start worker.\n";

      my $rw; $rw = AE::io $fh, 0, sub {
         if (defined sysread $fh, my $newstate, 1) {
            if ($state ne $newstate) {
               if ($newstate eq "0") {
                  ++$idle;
                  undef $listenw;
               } elsif ($newstate eq "1") {
                  --$idle;
                  check;
               } else {
                  undef $rw;
                  --$workers;
                  --$idle if $state eq "0";
                  print "[$$] removing worker (state $state) (workers $workers, idle $idle).\n" if $VERBOSE >= 2;
                  check;
               }
               $state = $newstate;
            }

         } elsif ($! != Errno::EINTR) {
            warn "[$$] $fh worker error: $!\n";

            undef $rw;
            --$workers;
            --$idle if $state eq "0";
            check;
         }
      };
   });
}

sub check {
   if (!$idle && $workers < $MAX_WORKERS) {
      $listenw ||= AE::io $LISTEN, 0, sub {
         add_worker
            if $workers < $MAX_WORKERS;
         undef $listenw;
         check;
      };
   }
}

check;
EV::run;


