FU: Implement --monitor, add some docs; FU::Util: add fdpass functions
This commit is contained in:
parent
09fe50d2a2
commit
3e84a4f4d3
8 changed files with 480 additions and 99 deletions
317
FU.pm
317
FU.pm
|
|
@ -3,6 +3,7 @@ use v5.36;
|
|||
use Carp 'confess';
|
||||
use IO::Socket;
|
||||
use POSIX;
|
||||
use FU::Util;
|
||||
|
||||
|
||||
sub import($pkg, @opt) {
|
||||
|
|
@ -21,10 +22,10 @@ our $fu = bless {}, 'FU::obj'; # App request-local data
|
|||
sub fu() { $fu }
|
||||
|
||||
|
||||
sub debug :lvalue () { state $v = 0 }
|
||||
sub log_slow_pages :lvalue () { state $v = 0 }
|
||||
sub log_queries :lvalue () { state $v = 0 }
|
||||
sub max_request_body :lvalue () { state $v = 10*1024*1024 }
|
||||
sub debug { state $v = 0; $v = $_[0] if @_; $v }
|
||||
sub log_slow_pages { state $v = 0; $v = $_[0] if @_; $v }
|
||||
sub log_queries { state $v = 0; $v = $_[0] if @_; $v }
|
||||
sub max_request_body { state $v = 10*1024*1024; $v = $_[0] if @_; $v }
|
||||
|
||||
sub mime_types() { state $v = {qw{
|
||||
7z application/x-7z-compressed
|
||||
|
|
@ -151,6 +152,29 @@ my %onerr = (
|
|||
sub on_error :prototype($&) { $onerr{$_[0]} = $_[1] }
|
||||
|
||||
|
||||
my($monitor_check, @monitor_paths);
|
||||
sub monitor_path { push @monitor_paths, @_ }
|
||||
sub monitor_check :prototype(&) { $monitor_check = $_[0] }
|
||||
|
||||
sub _monitor {
|
||||
state %data;
|
||||
return 1 if $monitor_check && $monitor_check->();
|
||||
|
||||
require File::Find;
|
||||
eval {
|
||||
File::Find::find({
|
||||
wanted => sub {
|
||||
my $m = (stat)[9];
|
||||
$data{$_} //= $m;
|
||||
die if $m > $data{$_};
|
||||
},
|
||||
no_chdir => 1
|
||||
}, $0, values %INC, @monitor_paths);
|
||||
0
|
||||
} // 1;
|
||||
}
|
||||
|
||||
|
||||
sub _decode_utf8 {
|
||||
fu->error(400, 'Invalid UTF-8 in request') if !utf8::decode($_[0]);
|
||||
# Disallow any control codes, except for x09 (tab), x0a (newline) and x0d (carriage return)
|
||||
|
|
@ -211,7 +235,12 @@ sub _log_err($e) {
|
|||
}
|
||||
|
||||
sub _do_req($c) {
|
||||
# TODO: check for changes if $c->{monitor}
|
||||
if ($c->{monitor} && _monitor) {
|
||||
warn "File change detected, restarting process.\n" if debug;
|
||||
FU::Util::fdpass_send(fileno($c->{supervisor_sock}), fileno($c->{client_sock}), 'f0000');
|
||||
exit;
|
||||
}
|
||||
|
||||
local $REQ = {};
|
||||
local $fu = bless {}, 'FU::obj';
|
||||
|
||||
|
|
@ -284,16 +313,14 @@ sub _supervisor($c) {
|
|||
fcntl $c->{listen_sock}, Fcntl::F_SETFD(), 0;
|
||||
fcntl $wsock, Fcntl::F_SETFD(), 0;
|
||||
|
||||
my @child_cmd = (
|
||||
$^X, (map "-I$_", @INC), $0,
|
||||
$c->{monitor} ? '--monitor' : '--no-monitor',
|
||||
$c->{max_reqs} ? "--max-reqs=$c->{max_reqs}" : (),
|
||||
debug ? '--debug' : '--no-debug',
|
||||
'--supervisor-fd='.fileno($wsock),
|
||||
'--listen-fd='.fileno($c->{listen_sock}),
|
||||
);
|
||||
$ENV{FU_MONITOR} = $c->{monitor};
|
||||
$ENV{FU_MAX_REQS} = $c->{max_reqs};
|
||||
$ENV{FU_DEBUG} = debug;
|
||||
$ENV{FU_SUPERVISOR_FD} = fileno $wsock;
|
||||
$ENV{FU_LISTEN_FD} = fileno $c->{listen_sock};
|
||||
|
||||
my $err = 0;
|
||||
my @client_fd;
|
||||
while (1) {
|
||||
while ((my $pid = waitpid(-1, POSIX::WNOHANG())) > 0) {
|
||||
$err = 1 if POSIX::WIFEXITED($?) && POSIX::WEXITSTATUS($?) != 0;
|
||||
|
|
@ -307,72 +334,74 @@ sub _supervisor($c) {
|
|||
# Don't bother spawning more than 1 at a time while in error state
|
||||
my $spawn = !$err ? $c->{proc} - keys %childs : (grep $_ == 1, values %childs) ? 0 : 1;
|
||||
for (1..$spawn) {
|
||||
my $client = shift @client_fd;
|
||||
my $pid = fork;
|
||||
die $! if !defined $pid;
|
||||
if (!$pid) { # child
|
||||
$SIG{CHLD} = $SIG{HUP} = $SIG{INT} = $SIG{TERM} = undef;
|
||||
# In error state, wait with loading the script until we've received a request.
|
||||
# Otherwise we'll end up in an infinite spawning loop if the script doesn't start properly.
|
||||
my $sock;
|
||||
if ($err) {
|
||||
$sock = $c->{listen_sock}->accept() or die $!;
|
||||
if ($client) {
|
||||
$ENV{FU_CLIENT_FD} = $client;
|
||||
} elsif ($err) {
|
||||
# In error state, wait with loading the script until we've received a request.
|
||||
# Otherwise we'll end up in an infinite spawning loop if the script doesn't start properly.
|
||||
my $sock = $c->{listen_sock}->accept() or die $!;
|
||||
fcntl $sock, Fcntl::F_SETFD, 0 if $sock;
|
||||
$ENV{FU_CLIENT_FD} = fileno $sock;
|
||||
}
|
||||
exec @child_cmd, $sock ? '--client-fd='.fileno($sock) : ();
|
||||
exec $^X, (map "-I$_", @INC), $0;
|
||||
exit 1;
|
||||
}
|
||||
$client && IO::Socket->new_from_fd($client, 'r'); # close() the fd if we have one
|
||||
$childs{$pid} = 1;
|
||||
}
|
||||
|
||||
next if ($rsock->sysread(my $cmd, 5)//0) != 5;
|
||||
next if $cmd eq 'c0000'; # child died
|
||||
# Assumption: we never get short reads.
|
||||
my ($fd, $msg) = FU::Util::fdpass_recv(fileno($rsock), 5);
|
||||
push @client_fd, $fd if $fd;
|
||||
next if !$msg;
|
||||
next if $msg eq 'c0000'; # child died
|
||||
next if $msg eq 'f0000'; # child is about to exit and passed a client fd to us
|
||||
|
||||
if ($cmd =~ /^r/) { # child ready
|
||||
my $pid = unpack 'V', substr $cmd, 1;
|
||||
if ($msg =~ /^r/) { # child ready
|
||||
my $pid = unpack 'V', substr $msg, 1;
|
||||
$childs{$pid} = 2 if $childs{$pid};
|
||||
$err = 0;
|
||||
}
|
||||
|
||||
# TODO: Socket passing thing for autoreloading childs
|
||||
}
|
||||
}
|
||||
|
||||
sub _spawn {
|
||||
state %c = (
|
||||
listen_sock => undef,
|
||||
client_sock => undef,
|
||||
supervisor_sock => undef,
|
||||
init => 0,
|
||||
);
|
||||
return if $c{init} && !@_; # already checked if we need to spawn
|
||||
state %c;
|
||||
return if keys %c && !@_; # already checked if we need to spawn
|
||||
|
||||
%c = (%c, @_, init => 1) if @_ && defined $_[0];
|
||||
if (!$c{init}++) {
|
||||
$c{http} = $ENV{FU_HTTP} // '127.0.0.1:3000';
|
||||
$c{fcgi} = $ENV{FU_FCGI};
|
||||
$c{proc} = $ENV{FU_PROC} // 1;
|
||||
$c{monitor} = $ENV{FU_MONITOR};
|
||||
$c{max_reqs} = $ENV{FU_MAX_REQS};
|
||||
debug = 1 if $ENV{FU_DEBUG};
|
||||
if (!keys %c) {
|
||||
%c = (
|
||||
http => $ENV{FU_HTTP} // '127.0.0.1:3000',
|
||||
fcgi => $ENV{FU_FCGI},
|
||||
proc => $ENV{FU_PROC} // 1,
|
||||
monitor => $ENV{FU_MONITOR} // 0,
|
||||
max_reqs => $ENV{FU_MAX_REQS} // 0,
|
||||
listen_sock => $ENV{FU_LISTEN_FD} && IO::Socket->new_from_fd($ENV{FU_LISTEN_FD}, 'r'),
|
||||
client_sock => $ENV{FU_CLIENT_FD} && IO::Socket->new_from_fd($ENV{FU_CLIENT_FD}, 'r+'),
|
||||
supervisor_sock => $ENV{FU_SUPERVISOR_FD} && IO::Socket->new_from_fd($ENV{FU_SUPERVISOR_FD}, 'w'),
|
||||
!$ENV{FU_SUPERVISOR_FD} && @_ && defined $_[0] ? @_ : (),
|
||||
);
|
||||
debug $ENV{FU_DEBUG} if exists $ENV{FU_DEBUG};
|
||||
|
||||
for (@ARGV) {
|
||||
for (@_ ? () : @ARGV) {
|
||||
$c{http} = $1 if /^--http=(.+)$/;
|
||||
$c{fcgi} = $1 if /^--fcgi=(.+)$/;
|
||||
$c{proc} = $1 if /^--proc=([0-9]+)$/;
|
||||
$c{monitor} = 1 if /^--monitor$/;
|
||||
$c{monitor} = 0 if /^--no-monitor$/;
|
||||
$c{max_reqs} = $1 if /^--max-reqs=([0-9]+)$/;
|
||||
debug = 1 if /^--debug$/;
|
||||
debug = 0 if /^--no-debug$/;
|
||||
$c{listen_sock} = IO::Socket->new_from_fd($1, 'r') if /^--listen-fd=([0-9]+)$/;
|
||||
$c{client_sock} = IO::Handle->new_from_fd($1, 'r+') if /^--client-fd=([0-9]+)$/;
|
||||
$c{supervisor_sock} = IO::Handle->new_from_fd($1, 'w') if /^--supervisor-fd=([0-9]+)$/;
|
||||
debug 1 if /^--debug$/;
|
||||
debug 0 if /^--no-debug$/;
|
||||
}
|
||||
};
|
||||
|
||||
# Single process, no need for a supervisor
|
||||
my $need_supervisor = !$c{supervisor_sock} && !$c{client_sock}
|
||||
&& ($c{proc} > 1 || $c{monitor} || $c{max_reqs});
|
||||
my $need_supervisor = !$c{supervisor_sock} && !$c{client_sock} && ($c{proc} > 1 || $c{monitor} || $c{max_reqs});
|
||||
return if !@_ && !$need_supervisor;
|
||||
|
||||
if (!$c{listen_sock}) {
|
||||
|
|
@ -423,7 +452,7 @@ package FU::obj;
|
|||
use Carp 'confess';
|
||||
|
||||
sub fu() { $FU::fu }
|
||||
sub debug :lvalue { FU::debug }
|
||||
sub debug { FU::debug }
|
||||
|
||||
sub db_conn { $FU::DB || FU::_connect_db }
|
||||
|
||||
|
|
@ -553,14 +582,194 @@ __END__
|
|||
|
||||
=head1 NAME
|
||||
|
||||
FU - A collection of awesome modules plus a lean and efficient web framework.
|
||||
FU - Framework Ultimatum: A Lean and Efficient Zero-Dependency Web Framework.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use v5.36;
|
||||
use FU -spawn;
|
||||
|
||||
FU::get qr{/hello/(.+)}, sub($who) {
|
||||
fu->set_body("<h1>Hello, $who!</h1>");
|
||||
};
|
||||
|
||||
FU::run;
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
=head2 Properties
|
||||
=head2 Distribution Overview
|
||||
|
||||
- Requires a moderately recent Perl (>= 5.36).
|
||||
- Only works on 64-bit Linux (and possibly *BSD).
|
||||
- Assumes that no threading is used; not all modules are thread-safe.
|
||||
This top-level C<FU> module is a web framework. The C<FU> distribution also
|
||||
includes a bunch of modules that the framework depends on or which are
|
||||
otherwise useful when building web backends. These modules are standalone and
|
||||
can be used independently of the framework:
|
||||
|
||||
=over
|
||||
|
||||
=item * L<FU::Util> - JSON parsing & formatting.
|
||||
|
||||
=item * L<FU::Pg> - PostgreSQL client.
|
||||
|
||||
=back
|
||||
|
||||
Note that everything in this distribution requires a moderately recent version
|
||||
of Perl (5.36+), a C compiler and a 64-bit POSIXy system (not Windows, that
|
||||
is).
|
||||
|
||||
=head2 Framework Overview
|
||||
|
||||
=head2 Importing FU
|
||||
|
||||
You'll usually want to add the following statement somewhere near the top of
|
||||
your script:
|
||||
|
||||
use FU -spawn;
|
||||
|
||||
The C<-spawn> option tells C<FU> to read running configuration from environment
|
||||
variables and command-line arguments during early startup, see L</"Running the
|
||||
Site"> below.
|
||||
|
||||
I<TODO: more import options>
|
||||
|
||||
=head2 Framework Configuration
|
||||
|
||||
=over
|
||||
|
||||
=item FU::monitor_path(@paths)
|
||||
|
||||
Add filesystem paths to be monitored for changes when running in monitor mode
|
||||
(see C<--monitor> in L</"Running the Site">). When given a directory, all files
|
||||
under the directory are recursively checked. The given paths do not actually
|
||||
have to exist, errors are silently discarded. Relative paths are resolved to
|
||||
the current working directory at the time that the paths are checked for
|
||||
changes, so you may want to pass absolute paths if you ever call C<chdir()>.
|
||||
|
||||
You do not have to add the current script or files in C<%INC>, these are
|
||||
monitored by default.
|
||||
|
||||
=item FU::monitor_check($sub)
|
||||
|
||||
Register a subroutine to be called in monitor mode. The subroutine should
|
||||
return a true value to signal that something has changed and the process should
|
||||
reload, false otherwise. The subroutine is called before any filesystem paths
|
||||
are checked (as in C<FU::monitor_path>), so if you run any build system things
|
||||
here, file modifications are properly detected and trigger a reload.
|
||||
|
||||
Only one subroutine can be registered at a time. Be careful to ensure that the
|
||||
subroutine returns a false value at some point, otherwise you may end up in a
|
||||
restart loop.
|
||||
|
||||
=back
|
||||
|
||||
=head2 Handlers & Routing
|
||||
|
||||
=head2 The 'fu' Object
|
||||
|
||||
=head2 Request Information
|
||||
|
||||
=head2 Generating Responses
|
||||
|
||||
=head2 Running the Site
|
||||
|
||||
When your script is done setting L</"Framework Configuration"> and registering
|
||||
L</"Handlers & Routing">, it should call C<FU::run> to actually start serving
|
||||
the website:
|
||||
|
||||
=over
|
||||
|
||||
=item FU::run(%options)
|
||||
|
||||
In normal circumstances, this function does not return.
|
||||
|
||||
When FU has been loaded with the C<-spawn> flag, C<%options> are read from the
|
||||
environment variables or command line arguments documented below. Otherwise,
|
||||
the following corresponding options can be passed instead: I<http>, I<fcgi>,
|
||||
I<proc>, I<monitor>, I<max_reqs>, I<listen_sock>.
|
||||
|
||||
=back
|
||||
|
||||
Command-line options are read only when FU has been loaded with C<-spawn>, the
|
||||
environment variables are always read.
|
||||
|
||||
=over
|
||||
|
||||
=item FU_HTTP=addr
|
||||
|
||||
=item --http=addr
|
||||
|
||||
Start a local web server on the given address. I<addr> can be an C<ip:port>
|
||||
combination to listen on TCP, or a path (optionally prefixed with C<unix:>) to
|
||||
listen on a UNIX socket. E.g.
|
||||
|
||||
./your-script.pl --http=127.0.0.1:8000
|
||||
./your-script.pl --http=unix:/path/to/socket
|
||||
|
||||
B<WARNING:> The built-in HTTP server is only intended for local development
|
||||
setups, it is NOT suitable for production deployments in its current form. It
|
||||
does not enforce a limit on request header size, does not support HTTPS and has
|
||||
no provisions for extracting the client IP address when behind a reverse proxy.
|
||||
Please use FastCGI instead for internet-facing deployments.
|
||||
|
||||
=item FU_FCGI=addr
|
||||
|
||||
=item --fcgi=addr
|
||||
|
||||
Like the HTTP counterpart above, but listen on a FastCGI socket instead. If
|
||||
this option is set, it takes precedence over the HTTP option.
|
||||
|
||||
=item FU_PROC=n
|
||||
|
||||
=item --proc=n
|
||||
|
||||
How many worker processes to spawn, defaults to 1.
|
||||
|
||||
=item FU_MONITOR=0/1
|
||||
|
||||
=item --monitor or --no-monitor
|
||||
|
||||
When enabled, worker processes will monitor for file changes and automatically
|
||||
restart on changes. This is immensely useful during development, but comes at a
|
||||
significant cost in performance - better not enable this in production.
|
||||
|
||||
=item FU_MAX_REQS=n
|
||||
|
||||
=item --max-reqs=n
|
||||
|
||||
Worker processes can automatically restart after handling a number of requests.
|
||||
Set to 0 (the default) to disable this feature. This option can be useful when
|
||||
your worker processes keep accumulating memory over time. A little pruning here
|
||||
and there can never hurt.
|
||||
|
||||
=item FU_DEBUG=0/1
|
||||
|
||||
=item --debug or --no-debug
|
||||
|
||||
Set the initial value for C<FU::debug>.
|
||||
|
||||
=item LISTEN_FD=num
|
||||
|
||||
Listen for incoming connections on the given file descriptor instead of
|
||||
creating a new listen socket. This is mainly useful if you are using an
|
||||
external process manager.
|
||||
|
||||
=back
|
||||
|
||||
When C<--monitor> or C<--max-reqs> are set or C<<--proc>> is larger than 1, FU
|
||||
starts a supervisor process to ensure the requested number of worker processes
|
||||
are running and that they are restarted when necessary. When FU has been loaded
|
||||
with the C<-spawn> flag, this supervisor process runs directly from the context
|
||||
of the C<use FU> statement - that is, before the rest of your script has even
|
||||
loaded. This saves valuable resources: the supervisor has no need of your
|
||||
website code nor does it need an active connection to your database to do its
|
||||
job. Without the C<-spawn> flag, the supervisor has to run from C<FU::run>,
|
||||
which is less efficient but does allow for more flexible configuration from
|
||||
within your script.
|
||||
|
||||
When not running in supervisor mode, no separate worker processes are started
|
||||
and requests are instead handled directly in the starting process.
|
||||
|
||||
In supervisor mode, sending C<SIGHUP> causes all worker processes to reload
|
||||
their code. In both modes, C<SIGTERM> or C<SIGINT> can be used to trigger a
|
||||
clean shutdown.
|
||||
|
||||
I<TODO:> Alternate FastCGI spawning options & server config examples.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue