diff --git a/FU.pm b/FU.pm
index 30ae4b4..cd8bc41 100644
--- a/FU.pm
+++ b/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("
Hello, $who!
");
+ };
+
+ 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 module is a web framework. The C 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 - JSON parsing & formatting.
+
+=item * L - 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 to read running configuration from environment
+variables and command-line arguments during early startup, see L"Running the
+Site"> below.
+
+I
+
+=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.
+
+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), 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 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, I,
+I, I, I, I.
+
+=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 can be an C
+combination to listen on TCP, or a path (optionally prefixed with C) 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 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.
+
+=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