#!perl
use strict;
package PPerlServer;
use IO::Socket;
use IO::File;
use Fcntl ':flock', 'F_GETFL';
use POSIX qw(:signal_h setsid WNOHANG);
use Carp 'croak','cluck';
use PPerl qw( recv_fd read_int );
use vars qw(%CHILDREN $spid $data_at %FILEHANDLES $logfile);
$spid = $$; # stash it as we change to be someone else later
$data_at = tell main::DATA
if fileno main::DATA; # strangely it moves!
BEGIN {
$logfile = '/tmp/pperl.log';
$PPERL::SOCKET_NAME = $ARGV[0];
$PPERL::MAX_SERVERS = $ARGV[1] || 3;
$PPERL::MAX_REQUESTS = $ARGV[2] || 100;
$PPERL::WIDE_OPEN = $ARGV[3] || 0;
$PPERL::NO_CLEANUP = $ARGV[4] || 0;
$0 = $ARGV[5];
}
sub log_error {
local *FH;
open(FH, ">>$logfile")
or die "Can't open logfile: '$logfile' $!";
print FH @_;
}
my $____self;
my $should_exit = 0;
my $exit_code = 0;
BEGIN {
*CORE::GLOBAL::exit = sub {
my $retval = shift || 0;
my $package = (caller)[0];
log_error("exit $package $retval $should_exit\n");
if ($package eq 'PPerlServer' || $should_exit) {
exit($retval);
}
$exit_code = $retval;
goto __PPerl_exit;
};
*CORE::GLOBAL::fork = sub {
my $package = (caller)[0];
my $ret = fork();
if (defined $ret && !$ret && $package ne 'PPerlServer') {
# forked children should just exit
$should_exit = 1;
}
return $ret;
};
*CORE::GLOBAL::exec = sub {
# This is so horribly wrong, overloading exec() to be
# system();exit() - but there's no other way to make this
# work. See the documentation in t/19invoke_djbish.t for
# further info on this issue.
my $package = (caller)[0];
log_error("exec $package $should_exit\n");
if ($package eq 'PPerlServer' || $should_exit) {
exec(@_);
}
my $code = system(@_);
$exit_code = $code >> 8;
goto __PPerl_exit;
};
}
sub barf {
print $____self "XX @_";
exit 1;
}
use vars qw( $pid $pidfile $saved_dir );
# lifted from B::walksymtable - mild tweaks to avoid all that slow
# slow recursion
my ($count, $closed);
sub find_open_filehandles {
my ($symref, $prefix) = @_;
return if $PPERL::NO_CLEANUP;
my $sym;
my $ref;
no strict 'vars';
$prefix = '' unless defined $prefix;
push @packages, [ $symref, $prefix ];
local(*GLOB);
my %fhs;
my $foo;
while ($foo = shift @packages) {
($symref, $prefix) = @$foo;
log_error("Examining $prefix\n");
while (($sym, $ref) = each %$symref) {
$count++;
*GLOB = "*main::".$prefix.$sym;
log_error("Processing: $sym\n");
if ($sym =~ /::$/) {
$sym = $prefix . $sym;
if ($sym ne "main::" && $sym ne "<none>::") {
push @packages, [ \%GLOB, $sym ];
}
}
else {
next if $sym eq 'DATA';
if (defined *GLOB{IO} and fileno(*GLOB{IO})) {
log_error("Adding: *main::".$prefix.$sym. " to FILEHANDLES\n");
$fhs{"*main::".$prefix.$sym} = *GLOB{IO};
}
}
}
}
return %fhs;
}
sub close_over {
return if $PPERL::NO_CLEANUP;
log_error("closing over\n");
my %fhs = find_open_filehandles(@_);
foreach my $key (keys %fhs) {
next if exists $FILEHANDLES{$key};
my $fh = $fhs{$key};
log_error("closing $key\n");
$closed++;
close $fh;
}
}
sub init_server {
($pidfile) = @_;
my $fh = open_pid_file($pidfile);
become_daemon();
log_error("became daemon with pid $$\n");
print $fh $$;
close $fh;
return $pid = $$;
}
sub safe_fork {
### block signal for fork
my $sigset = POSIX::SigSet->new(SIGINT);
POSIX::sigprocmask(SIG_BLOCK, $sigset)
or die "Can't block SIGINT for fork: [$!]\n";
### fork off a child
my $pid = fork;
unless( defined $pid ) {
die "Couldn't fork: [$!]\n";
}
### make SIGINT kill us as it did before
$SIG{INT} = 'DEFAULT';
### put back to normal
POSIX::sigprocmask(SIG_UNBLOCK, $sigset)
or die "Can't unblock SIGINT for fork: [$!]\n";
return $pid;
}
sub become_daemon {
my $child = fork;
die "Can't fork: $!" unless defined($child);
exit(0) if $child; # parent dies;
POSIX::setsid(); # become session leader
open(STDIN,"</dev/null");
open(STDOUT,">/dev/null");
open(STDERR, '>&STDOUT');
chdir '/'; # change working directory
umask(0); # forget file mode creation mask
$ENV{PATH} = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
$SIG{CHLD} = \&reap_child;
# signal handler for child die events
$SIG{TERM} = $SIG{INT} = \&do_term;
$SIG{HUP} = \&do_hup;
}
sub launch_child {
my $callback = shift;
my $signals = POSIX::SigSet->new(SIGINT,SIGCHLD,SIGTERM,SIGHUP);
sigprocmask(SIG_BLOCK,$signals); # block inconvenient signals
die("Can't fork: $!") unless defined (my $child = fork());
# my $child = safe_fork();
if ($child) {
$CHILDREN{$child} = $callback || 1;
$SIG{CHLD} = \&reap_child;
}
else {
$SIG{HUP} = $SIG{INT} = $SIG{CHLD} = $SIG{TERM} = 'DEFAULT';
$< = $>; # set real UID to effective UID
}
sigprocmask(SIG_UNBLOCK,$signals); # unblock signals
return $child;
}
sub reap_child {
log_error("$$ reap_child\n");
while ( (my $child = waitpid(-1,WNOHANG)) > 0) {
log_error("$$ reaped $child\n");
$CHILDREN{$child}->($child) if ref $CHILDREN{$child} eq 'CODE';
delete $CHILDREN{$child};
}
$SIG{CHLD} = \&sig_chld;
}
sub kill_children {
log_error("Killing children: ". join(', ', keys(%CHILDREN)). "\n");
kill INT => keys %CHILDREN;
# wait until all the children die
# sleep while %CHILDREN;
}
sub open_pid_file {
my $file = shift;
if (-e $file) { # oops. pid file already exists
my $fh = IO::File->new($file) || return;
my $pid = <$fh>;
if ($pid != $$) {
croak "Invalid PID file" unless $pid =~ /^(\d+)$/;
croak "Server already running with PID $1" if kill 0 => $1;
croak "Can't unlink PID file $file" unless -w $file && unlink $file;
}
}
return IO::File->new($file,O_WRONLY|O_CREAT|O_EXCL,($PPERL::WIDE_OPEN ? 0666 : 0644))
or die "Can't create $file: $!\n";
}
END {
$> = $<; # regain privileges
if (defined $pid and $$ == $pid) {
unlink $pidfile;
unlink $PPERL::SOCKET_NAME;
}
}
my $CHILD_COUNT = 0; # number of children
my $DONE = 0; # set flag to true when server done
# $SIG{INT} = $SIG{TERM} = sub { $DONE++ };
$PPERL::SOCKET_NAME =~ m~^([a-z0-9/_-]+)$~i
or die "unclean socket name '$PPERL::SOCKET_NAME'";
$PPERL::SOCKET_NAME = $1;
if (-e $PPERL::SOCKET_NAME) {
if (-e "${PPERL::SOCKET_NAME}.pid") {
die "socket and pid file both exist - possible error state. Delete both and retry";
}
unlink($PPERL::SOCKET_NAME);
}
my $socket = IO::Socket::UNIX->new( Local => $PPERL::SOCKET_NAME,
Type => SOCK_STREAM,
Listen => 1,
Reuse => 1,
) or croak("Can't create listen socket: $!");
if ($PPERL::WIDE_OPEN) {
chmod(0777, $PPERL::SOCKET_NAME);
}
# create PID file, initialize logging, and go into background
init_server("${PPERL::SOCKET_NAME}.pid");
sub cleanup_child {
my $child = shift;
$CHILD_COUNT--;
}
sub do_term {
log_error("TERM signal received, terminating children...\n");
close($socket);
kill_children();
exit(0);
}
sub do_hup {
log_error("HUP signal received by $$\n");
close($socket);
kill_children();
exit(0);
}
sub setup_chunk {
my $sd = $_[0];
$sd->autoflush(1);
$spid = $$;
my $sock = $sd->fileno();
log_error("$$ starting handshake\n");
# potentially flaky magic number - certainly pid gets more than 10
# decimal digits on my dev machines - richardc
my $pid = sprintf('%010d', $$);
POSIX::write($sock, $pid, 10);
log_error("$$ duping fds from $sock\n");
my $target;
while ((my $target = read_int($sock)) >= 0) {
log_error("$$ targeting $target\n");
if ( $target == $sock ) {
log_error("$$ performing the Dick Van Dyke shuffle\n");
# Mary Poppins, step in time
my $newsock = POSIX::dup($sock);
unless (defined $newsock) {
log_error("$$ gor blimey Mary Poppins, I can't move ($!)\n");
barf("$$ Dick Van Dyke exception! ($!)\n");
}
log_error("$$ dup sock: $sock newsock: $newsock\n");
POSIX::close($sock);
$sock = $newsock;
unless ($sd = $_[0] = IO::Socket->new_from_fd( $sock, "+<" )) {
log_error("$$ failed to step in time ($!)\n");
barf("$$ Dick Van Dyke shuffle failed! ($!)\n");
}
}
my $fd = recv_fd( $sock );
if ($fd < 0) {
my $err = "$$ recv_fd for $target returned -1: $!\n";
log_error($err);
barf($err);
}
log_error("$$ $fd wants to be $target\n");
if ($fd != $target) {
my $ret = POSIX::dup2($fd, $target);
log_error("$$ dup2 returns $ret\n");
barf("$$ dup2($fd, $target) failed") unless $ret;
$ret = POSIX::close($fd);
log_error("$$ close of $fd returns $ret\n");
barf("$$ close($fd) failed after dup2($fd, $target)") unless $ret;
}
}
local $/ = "\0";
while (<$sd>) {
chomp;
if ($_ eq '[PID]') {
chomp( my $pid = <$sd> );
log_error("$$ is talking to $pid\n");
$pid =~ /^(\d+)$/ or barf("$$ pid is mangled\n");
if ($] > 5.006001) {
PPerl::setreadonly('$', $1); # ');
}
else {
$$ = $1;
}
next;
}
if ($_ eq '[CWD]') {
chomp ( my $cwd = <$sd> );
log_error("$$ cwd $cwd\n");
# we are bad people
$cwd =~ /^(.*)$/
or barf("cwd '$cwd' unclean");
$cwd = $1;
chdir $cwd or barf("couldn't chdir '$cwd': '$!'");
next;
}
if ($_ eq '[ENV]') {
log_error("$$ setting env\n");
chomp( my $env = <$sd> );
for (1..$env) {
local $_ = <$sd>;
chomp;
my ($key, $value) = split(/\s*=\s*/, $_, 2);
$ENV{$key} = $value;
}
next;
}
if ($_ eq '[ARGV]') {
chomp( my $args = <$sd> );
log_error("$$ setting $args args\n");
for (1..$args) {
chomp( $_ = <$sd> );
log_error("$$ Read arg: $_\n");
push @ARGV, $_;
}
next;
}
if ($_ eq '[DONE]') {
return;
}
log_error("got unexpected '$_'\n");
barf("what's this then? '$_'");
}
}
use vars qw($cycles);
while (!$DONE) {
while ($CHILD_COUNT < $PPERL::MAX_SERVERS) {
my $child = launch_child(\&cleanup_child);
if ($child) { # child > 0, so we are the parent
log_error("$$ launched child $child\n");
$CHILD_COUNT++;
}
else {
$cycles = $PPERL::MAX_REQUESTS;
while ($cycles-- && !$should_exit) {
log_error("$$ waiting for $cycles more new jobs\n");
$____self = $socket->accept;
unless ($____self) {
log_error("$$ accept failed: $!\n");
last;
}
%ENV = ();
@ARGV = ();
setup_chunk($____self);
local *ARGV if !@ARGV; # mumble, grumble, scoping
if (!@ARGV) { # magic for <ARGV>
log_error("$$ Set argv magic\n");
open(ARGV, "<&STDIN");
}
log_error("$$ sending OK message\n");
POSIX::write($____self->fileno, "OK\n", 3);
log_error("$$ sent\n");
# reset bogus '<STDIN>, line 99' count
$. = 0;
$? = $! = $^E = 0;
select STDOUT;
'' =~ m/^$/; # should reset match variables
srand (time ^ $$);
goto ____PPerlCode;
____PPerlBackAgain:
if ($@) {
print STDERR $@;
$exit_code = 255;
}
__PPerl_exit:
log_error("$$ disabling alarms\n");
alarm 0;
log_error("$$ closing everything\n");
$closed = $count = 0;
close_over(\%::);
log_error("$$ considered $count things, closed $closed\n");
open(STDIN,"</dev/null");
open(STDOUT,">/dev/null");
open(STDERR, '>&STDOUT');
if ( fileno main::DATA && $data_at ) {
my $ret = seek(main::DATA, $data_at, 0);
log_error("seek returned $ret\n");
}
log_error("$$ run done - exit code $exit_code\n");
print $____self $exit_code;
log_error("$$ closing down socket\n");
close($____self);
}
log_error("$$ child is done (should_exit: $should_exit, cycles: $cycles)\n");
exit 0; # child is done
}
}
sleep; # wait for a signal
}
PPerl::kill_children();
log_error("normal termination\n");
exit 0;
#putting it here avoids accidental closures
____PPerlCode:
eval {
package main;
no strict;
# should you change the line below change write_pperl_h too
#### Your Code Here ####
};
goto ____PPerlBackAgain;
BEGIN {
log_error("finding open filehandles ($PPERL::NO_CLEANUP)\n");
%FILEHANDLES = find_open_filehandles(\%::);
}
syntax highlighted by Code2HTML, v. 0.9.1