FU: Implement --monitor, add some docs; FU::Util: add fdpass functions

This commit is contained in:
Yorhel 2025-02-15 14:58:00 +01:00
parent 09fe50d2a2
commit 3e84a4f4d3
8 changed files with 480 additions and 99 deletions

317
FU.pm
View file

@ -3,6 +3,7 @@ use v5.36;
use Carp 'confess'; use Carp 'confess';
use IO::Socket; use IO::Socket;
use POSIX; use POSIX;
use FU::Util;
sub import($pkg, @opt) { sub import($pkg, @opt) {
@ -21,10 +22,10 @@ our $fu = bless {}, 'FU::obj'; # App request-local data
sub fu() { $fu } sub fu() { $fu }
sub debug :lvalue () { state $v = 0 } sub debug { state $v = 0; $v = $_[0] if @_; $v }
sub log_slow_pages :lvalue () { state $v = 0 } sub log_slow_pages { state $v = 0; $v = $_[0] if @_; $v }
sub log_queries :lvalue () { state $v = 0 } sub log_queries { state $v = 0; $v = $_[0] if @_; $v }
sub max_request_body :lvalue () { state $v = 10*1024*1024 } sub max_request_body { state $v = 10*1024*1024; $v = $_[0] if @_; $v }
sub mime_types() { state $v = {qw{ sub mime_types() { state $v = {qw{
7z application/x-7z-compressed 7z application/x-7z-compressed
@ -151,6 +152,29 @@ my %onerr = (
sub on_error :prototype($&) { $onerr{$_[0]} = $_[1] } 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 { sub _decode_utf8 {
fu->error(400, 'Invalid UTF-8 in request') if !utf8::decode($_[0]); 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) # 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) { 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 $REQ = {};
local $fu = bless {}, 'FU::obj'; local $fu = bless {}, 'FU::obj';
@ -284,16 +313,14 @@ sub _supervisor($c) {
fcntl $c->{listen_sock}, Fcntl::F_SETFD(), 0; fcntl $c->{listen_sock}, Fcntl::F_SETFD(), 0;
fcntl $wsock, Fcntl::F_SETFD(), 0; fcntl $wsock, Fcntl::F_SETFD(), 0;
my @child_cmd = ( $ENV{FU_MONITOR} = $c->{monitor};
$^X, (map "-I$_", @INC), $0, $ENV{FU_MAX_REQS} = $c->{max_reqs};
$c->{monitor} ? '--monitor' : '--no-monitor', $ENV{FU_DEBUG} = debug;
$c->{max_reqs} ? "--max-reqs=$c->{max_reqs}" : (), $ENV{FU_SUPERVISOR_FD} = fileno $wsock;
debug ? '--debug' : '--no-debug', $ENV{FU_LISTEN_FD} = fileno $c->{listen_sock};
'--supervisor-fd='.fileno($wsock),
'--listen-fd='.fileno($c->{listen_sock}),
);
my $err = 0; my $err = 0;
my @client_fd;
while (1) { while (1) {
while ((my $pid = waitpid(-1, POSIX::WNOHANG())) > 0) { while ((my $pid = waitpid(-1, POSIX::WNOHANG())) > 0) {
$err = 1 if POSIX::WIFEXITED($?) && POSIX::WEXITSTATUS($?) != 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 # 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; my $spawn = !$err ? $c->{proc} - keys %childs : (grep $_ == 1, values %childs) ? 0 : 1;
for (1..$spawn) { for (1..$spawn) {
my $client = shift @client_fd;
my $pid = fork; my $pid = fork;
die $! if !defined $pid; die $! if !defined $pid;
if (!$pid) { # child if (!$pid) { # child
$SIG{CHLD} = $SIG{HUP} = $SIG{INT} = $SIG{TERM} = undef; $SIG{CHLD} = $SIG{HUP} = $SIG{INT} = $SIG{TERM} = undef;
# In error state, wait with loading the script until we've received a request. if ($client) {
# Otherwise we'll end up in an infinite spawning loop if the script doesn't start properly. $ENV{FU_CLIENT_FD} = $client;
my $sock; } elsif ($err) {
if ($err) { # In error state, wait with loading the script until we've received a request.
$sock = $c->{listen_sock}->accept() or die $!; # 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; 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; exit 1;
} }
$client && IO::Socket->new_from_fd($client, 'r'); # close() the fd if we have one
$childs{$pid} = 1; $childs{$pid} = 1;
} }
next if ($rsock->sysread(my $cmd, 5)//0) != 5; # Assumption: we never get short reads.
next if $cmd eq 'c0000'; # child died 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 if ($msg =~ /^r/) { # child ready
my $pid = unpack 'V', substr $cmd, 1; my $pid = unpack 'V', substr $msg, 1;
$childs{$pid} = 2 if $childs{$pid}; $childs{$pid} = 2 if $childs{$pid};
$err = 0; $err = 0;
} }
# TODO: Socket passing thing for autoreloading childs
} }
} }
sub _spawn { sub _spawn {
state %c = ( state %c;
listen_sock => undef, return if keys %c && !@_; # already checked if we need to spawn
client_sock => undef,
supervisor_sock => undef,
init => 0,
);
return if $c{init} && !@_; # already checked if we need to spawn
%c = (%c, @_, init => 1) if @_ && defined $_[0]; if (!keys %c) {
if (!$c{init}++) { %c = (
$c{http} = $ENV{FU_HTTP} // '127.0.0.1:3000'; http => $ENV{FU_HTTP} // '127.0.0.1:3000',
$c{fcgi} = $ENV{FU_FCGI}; fcgi => $ENV{FU_FCGI},
$c{proc} = $ENV{FU_PROC} // 1; proc => $ENV{FU_PROC} // 1,
$c{monitor} = $ENV{FU_MONITOR}; monitor => $ENV{FU_MONITOR} // 0,
$c{max_reqs} = $ENV{FU_MAX_REQS}; max_reqs => $ENV{FU_MAX_REQS} // 0,
debug = 1 if $ENV{FU_DEBUG}; 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{http} = $1 if /^--http=(.+)$/;
$c{fcgi} = $1 if /^--fcgi=(.+)$/; $c{fcgi} = $1 if /^--fcgi=(.+)$/;
$c{proc} = $1 if /^--proc=([0-9]+)$/; $c{proc} = $1 if /^--proc=([0-9]+)$/;
$c{monitor} = 1 if /^--monitor$/; $c{monitor} = 1 if /^--monitor$/;
$c{monitor} = 0 if /^--no-monitor$/; $c{monitor} = 0 if /^--no-monitor$/;
$c{max_reqs} = $1 if /^--max-reqs=([0-9]+)$/; $c{max_reqs} = $1 if /^--max-reqs=([0-9]+)$/;
debug = 1 if /^--debug$/; debug 1 if /^--debug$/;
debug = 0 if /^--no-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]+)$/;
} }
}; };
# Single process, no need for a supervisor # Single process, no need for a supervisor
my $need_supervisor = !$c{supervisor_sock} && !$c{client_sock} my $need_supervisor = !$c{supervisor_sock} && !$c{client_sock} && ($c{proc} > 1 || $c{monitor} || $c{max_reqs});
&& ($c{proc} > 1 || $c{monitor} || $c{max_reqs});
return if !@_ && !$need_supervisor; return if !@_ && !$need_supervisor;
if (!$c{listen_sock}) { if (!$c{listen_sock}) {
@ -423,7 +452,7 @@ package FU::obj;
use Carp 'confess'; use Carp 'confess';
sub fu() { $FU::fu } sub fu() { $FU::fu }
sub debug :lvalue { FU::debug } sub debug { FU::debug }
sub db_conn { $FU::DB || FU::_connect_db } sub db_conn { $FU::DB || FU::_connect_db }
@ -553,14 +582,194 @@ __END__
=head1 NAME =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 =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 =head1 DESCRIPTION
=head2 Properties =head2 Distribution Overview
- Requires a moderately recent Perl (>= 5.36). This top-level C<FU> module is a web framework. The C<FU> distribution also
- Only works on 64-bit Linux (and possibly *BSD). includes a bunch of modules that the framework depends on or which are
- Assumes that no threading is used; not all modules are thread-safe. 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.

19
FU.xs
View file

@ -1,8 +1,11 @@
#include <stdio.h> #include <stdio.h>
#include <errno.h> #include <errno.h>
#include <string.h> #include <string.h> /* strerror() */
#include <arpa/inet.h> #include <arpa/inet.h> /* inet_ntop(), inet_ntoa() */
#include <dlfcn.h> #include <sys/socket.h> /* fd passing */
#include <sys/un.h> /* fd passing */
#include <dlfcn.h> /* dlopen() etc */
#undef PERL_IMPLICIT_SYS #undef PERL_IMPLICIT_SYS
#define PERL_NO_GET_CONTEXT #define PERL_NO_GET_CONTEXT
@ -21,6 +24,7 @@
#include "c/common.c" #include "c/common.c"
#include "c/jsonfmt.c" #include "c/jsonfmt.c"
#include "c/jsonparse.c" #include "c/jsonparse.c"
#include "c/fdpass.c"
#include "c/libpq.h" #include "c/libpq.h"
#include "c/pgtypes.c" #include "c/pgtypes.c"
#include "c/pgconn.c" #include "c/pgconn.c"
@ -81,6 +85,15 @@ void json_parse(SV *val, ...)
CODE: CODE:
ST(0) = fujson_parse_xs(aTHX_ ax, items, val); ST(0) = fujson_parse_xs(aTHX_ ax, items, val);
void fdpass_send(int socket, int fd, SV *data)
CODE:
STRLEN buflen;
const char *buf = SvPVbyte(data, buflen);
ST(0) = sv_2mortal(newSViv(fufdpass_send(socket, fd, buf, buflen)));
void fdpass_recv(int socket, UV len)
CODE:
XSRETURN(fufdpass_recv(aTHX_ ax, socket, len));
MODULE = FU PACKAGE = FU::Pg MODULE = FU PACKAGE = FU::Pg

View file

@ -42,7 +42,7 @@ directly with C<libpq>.
=over =over
=item B<< FU::Pg->connect($string) >> =item FU::Pg->connect($string)
Connect to the PostgreSQL server and return a new C<FU::Pg::conn> object. Connect to the PostgreSQL server and return a new C<FU::Pg::conn> object.
C<$string> can either be in key=value format or a URI, refer to L<the C<$string> can either be in key=value format or a URI, refer to L<the
@ -52,17 +52,17 @@ for the full list of supported formats and options. You may also pass an empty
string and leave the configuration up L<environment string and leave the configuration up L<environment
variables|https://www.postgresql.org/docs/current/libpq-envars.html>. variables|https://www.postgresql.org/docs/current/libpq-envars.html>.
=item B<< $conn->server_version >> =item $conn->server_version
Returns the version of the PostgreSQL server as an integer in the format of Returns the version of the PostgreSQL server as an integer in the format of
C<$major * 10000 + $minor>. For example, returns 170002 for PostgreSQL 17.2. C<$major * 10000 + $minor>. For example, returns 170002 for PostgreSQL 17.2.
=item B<< $conn->lib_version >> =item $conn->lib_version
Returns the libpq version in the same format as the C<server_version> method. Returns the libpq version in the same format as the C<server_version> method.
Also available directly as C<FU::Pg::lib_version()>. Also available directly as C<FU::Pg::lib_version()>.
=item B<< $conn->status >> =item $conn->status
Returns a string indicating the status of the connection. Note that this method Returns a string indicating the status of the connection. Note that this method
does not verify that the connection is still alive, the status is updated after does not verify that the connection is still alive, the status is updated after
@ -95,17 +95,17 @@ Connection is dead or otherwise unusable.
=back =back
=item B<< $conn->cache($enable) >> =item $conn->cache($enable)
=item B<< $conn->text_params($enable) >> =item $conn->text_params($enable)
=item B<< $conn->text_results($enable) >> =item $conn->text_results($enable)
=item B<< $conn->text($enable) >> =item $conn->text($enable)
Set the default settings for new statements created with B<< $conn->q() >>. Set the default settings for new statements created with B<< $conn->q() >>.
=item B<< $conn->cache_size($num) >> =item $conn->cache_size($num)
Set the number of prepared statements to keep in the cache. Defaults to 256. Set the number of prepared statements to keep in the cache. Defaults to 256.
@ -114,7 +114,7 @@ Prepared statements that still have an active C<$st> object are not counted
towards this number. The cache works as an LRU: when it's full, the statement towards this number. The cache works as an LRU: when it's full, the statement
that hasn't been used for the longest time is reclaimed. that hasn't been used for the longest time is reclaimed.
=item B<< $conn->disconnect >> =item $conn->disconnect
Close the connection. Any active transactions are rolled back and any further Close the connection. Any active transactions are rolled back and any further
attempts to use C<$conn> throw an error. attempts to use C<$conn> throw an error.
@ -125,13 +125,13 @@ attempts to use C<$conn> throw an error.
=over =over
=item B<< $conn->exec($sql) >> =item $conn->exec($sql)
Execute one or more SQL commands, separated by a semicolon. Returns the number Execute one or more SQL commands, separated by a semicolon. Returns the number
of rows affected by the last statement or I<undef> if that information is not of rows affected by the last statement or I<undef> if that information is not
available for the given command (like with C<CREATE TABLE>). available for the given command (like with C<CREATE TABLE>).
=item B<< $conn->q($sql, @params) >> =item $conn->q($sql, @params)
Create a new SQL statement with the given C<$sql> string and an optional list Create a new SQL statement with the given C<$sql> string and an optional list
of bind parameters. C<$sql> can only hold a single statement. of bind parameters. C<$sql> can only hold a single statement.
@ -154,19 +154,19 @@ configuration parameters:
=over =over
=item B<< $st->cache($enable) >> =item $st->cache($enable)
Enable or disable caching of the prepared statement for this particular query. Enable or disable caching of the prepared statement for this particular query.
=item B<< $st->text_params($enable) >> =item $st->text_params($enable)
Enable or disable sending bind parameters in the text format. Enable or disable sending bind parameters in the text format.
=item B<< $st->text_results($enable) >> =item $st->text_results($enable)
Enable or disable receiving query results in the text format. Enable or disable receiving query results in the text format.
=item B<< $st->text($enable) >> =item $st->text($enable)
Shorthand for setting C<text_params> and C<text_results> at the same time. Shorthand for setting C<text_params> and C<text_results> at the same time.
@ -176,7 +176,7 @@ Statement objects can be inspected with the following two methods:
=over =over
=item B<< $st->param_types >> =item $st->param_types
Returns an arrayref of integers indicating the type (as I<oid>) of each Returns an arrayref of integers indicating the type (as I<oid>) of each
parameter in the given C<$sql> string. Example: parameter in the given C<$sql> string. Example:
@ -187,7 +187,7 @@ parameter in the given C<$sql> string. Example:
my $oids = $conn->q('SELECT id FROM books')->params; my $oids = $conn->q('SELECT id FROM books')->params;
# $oids = [] # $oids = []
=item B<< $st->columns >> =item $st->columns
Returns an arrayref of hashrefs describing each column that the statement Returns an arrayref of hashrefs describing each column that the statement
returns. returns.
@ -206,7 +206,7 @@ how you'd like to obtain the results:
=over =over
=item B<< $st->exec >> =item $st->exec
Execute the query and return the number of rows affected. Similar to C<< Execute the query and return the number of rows affected. Similar to C<<
$conn->exec >>. $conn->exec >>.
@ -214,7 +214,7 @@ $conn->exec >>.
my $v = $conn->q('UPDATE books SET read = true WHERE id = 1')->exec; my $v = $conn->q('UPDATE books SET read = true WHERE id = 1')->exec;
# $v = 1 # $v = 1
=item B<< $st->val >> =item $st->val
Return the first column of the first row. Throws an error if the query does not Return the first column of the first row. Throws an error if the query does not
return exactly one column, or if multiple rows are returned. Returns I<undef> return exactly one column, or if multiple rows are returned. Returns I<undef>
@ -223,7 +223,7 @@ if no rows are returned or if its value is I<NULL>.
my $v = $conn->q('SELECT COUNT(*) FROM books')->val; my $v = $conn->q('SELECT COUNT(*) FROM books')->val;
# $v = 2 # $v = 2
=item B<< $st->rowl >> =item $st->rowl
Return the first row as a list. Throws an error if the query does not return Return the first row as a list. Throws an error if the query does not return
exactly one row. exactly one row.
@ -231,7 +231,7 @@ exactly one row.
my($id, $title) = $conn->q('SELECT id, title FROM books LIMIT 1')->rowl; my($id, $title) = $conn->q('SELECT id, title FROM books LIMIT 1')->rowl;
# ($id, $title) = (1, 'Revelation Space'); # ($id, $title) = (1, 'Revelation Space');
=item B<< $st->rowa >> =item $st->rowa
Return the first row as an arrayref, equivalent to C<< [$st->rowl] >> but Return the first row as an arrayref, equivalent to C<< [$st->rowl] >> but
might be slightly more efficient. might be slightly more efficient.
@ -239,7 +239,7 @@ might be slightly more efficient.
my $row = $conn->q('SELECT id, title FROM books LIMIT 1')->rowa; my $row = $conn->q('SELECT id, title FROM books LIMIT 1')->rowa;
# $row = [1, 'Revelation Space']; # $row = [1, 'Revelation Space'];
=item B<< $st->rowh >> =item $st->rowh
Return the first row as a hashref. Also throws an error if the query returns Return the first row as a hashref. Also throws an error if the query returns
multiple columns with the same name. multiple columns with the same name.
@ -247,7 +247,7 @@ multiple columns with the same name.
my $row = $conn->q('SELECT id, title FROM books LIMIT 1')->rowh; my $row = $conn->q('SELECT id, title FROM books LIMIT 1')->rowh;
# $row = { id => 1, title => 'Revelation Space' }; # $row = { id => 1, title => 'Revelation Space' };
=item B<< $st->alla >> =item $st->alla
Return all rows as an arrayref of arrayrefs. Return all rows as an arrayref of arrayrefs.
@ -257,7 +257,7 @@ Return all rows as an arrayref of arrayrefs.
# [ 2, 'The Invincible' ], # [ 2, 'The Invincible' ],
# ]; # ];
=item B<< $st->allh >> =item $st->allh
Return all rows as an arrayref of hashrefs. Throws an error if the query Return all rows as an arrayref of hashrefs. Throws an error if the query
returns multiple columns with the same name. returns multiple columns with the same name.
@ -268,7 +268,7 @@ returns multiple columns with the same name.
# { id => 2, title => 'The Invincible' }, # { id => 2, title => 'The Invincible' },
# ]; # ];
=item B<< $st->flat >> =item $st->flat
Return an arrayref with all rows flattened. Return an arrayref with all rows flattened.
@ -278,7 +278,7 @@ Return an arrayref with all rows flattened.
# 2, 'The Invincible', # 2, 'The Invincible',
# ]; # ];
=item B<< $st->kvv >> =item $st->kvv
Return a hashref where the first result column is used as key and the second Return a hashref where the first result column is used as key and the second
column as value. If the query only returns a single column, C<true> is used as column as value. If the query only returns a single column, C<true> is used as
@ -290,7 +290,7 @@ value instead. An error is thrown if the query returns 3 or more columns.
# 2 => 'The Invincible', # 2 => 'The Invincible',
# }; # };
=item B<< $st->kva >> =item $st->kva
Return a hashref where the first result column is used as key and the remaining Return a hashref where the first result column is used as key and the remaining
columns are stored as arrayref. columns are stored as arrayref.
@ -301,7 +301,7 @@ columns are stored as arrayref.
# 2 => [ 'The Invincible', false ], # 2 => [ 'The Invincible', false ],
# }; # };
=item B<< $st->kvh >> =item $st->kvh
Return a hashref where the first result column is used as key and the remaining Return a hashref where the first result column is used as key and the remaining
columns are stored as hashref. columns are stored as hashref.
@ -355,16 +355,16 @@ Transaction methods:
=over =over
=item B<< $txn->exec(..) >> =item $txn->exec(..)
=item B<< $txn->q(..) >> =item $txn->q(..)
Run a query inside the transaction. These work the same as the respective Run a query inside the transaction. These work the same as the respective
methods on the parent C<$conn> object. methods on the parent C<$conn> object.
=item B<< $txn->commit >> =item $txn->commit
=item B<< $txn->rollback >> =item $txn->rollback
Commit or abort the transaction. Any attempts to run queries on this Commit or abort the transaction. Any attempts to run queries on this
transaction object after this call will throw an error. transaction object after this call will throw an error.
@ -372,13 +372,13 @@ transaction object after this call will throw an error.
Calling C<rollback> is optional, the transaction is automatically rolled back Calling C<rollback> is optional, the transaction is automatically rolled back
when the object goes out of scope. when the object goes out of scope.
=item B<< $txn->cache($enable) >> =item $txn->cache($enable)
=item B<< $txn->text_params($enable) >> =item $txn->text_params($enable)
=item B<< $txn->text_results($enable) >> =item $txn->text_results($enable)
=item B<< $txn->text($enable) >> =item $txn->text($enable)
Set the default settings for new statements created with B<< $txn->q() >>. Set the default settings for new statements created with B<< $txn->q() >>.
@ -387,13 +387,13 @@ created. Subtransactions inherit these settings from their parent transaction.
Changing these settings within a transaction does not affect the main Changing these settings within a transaction does not affect the main
connection or any already existing subtransactions. connection or any already existing subtransactions.
=item B<< $txn->txn >> =item $txn->txn
Create a subtransaction within the current transaction. A subtransaction works Create a subtransaction within the current transaction. A subtransaction works
exactly the same as a top-level transaction, except any changes remain exactly the same as a top-level transaction, except any changes remain
invisible to other sessions until the top-level transaction has been committed. invisible to other sessions until the top-level transaction has been committed.
=item B<< $txn->status >> =item $txn->status
Like C<< $conn->status >>, but with the following status codes: Like C<< $conn->status >>, but with the following status codes:

View file

@ -4,7 +4,10 @@ use v5.36;
use FU::XS; use FU::XS;
use Exporter 'import'; use Exporter 'import';
our @EXPORT_OK = qw/json_format json_parse/; our @EXPORT_OK = qw/
json_format json_parse
fdpass_send fdpass_recv
/;
1; 1;
__END__ __END__
@ -150,3 +153,45 @@ unnecessary features and C<#ifdef>s to support ancient perls and esoteric
configurations. Still, if you need anything not provided by these functions, configurations. Still, if you need anything not provided by these functions,
L<JSON::PP> and L<Cpanel::JSON::XS> are perfectly fine alternatives. L<JSON::PP> and L<Cpanel::JSON::XS> are perfectly fine alternatives.
L<JSON::SIMD> and L<Mojo::JSON> also look like good and maintained candidates.) L<JSON::SIMD> and L<Mojo::JSON> also look like good and maintained candidates.)
=head2 File Descriptor Passing
UNIX sockets (see L<IO::Socket::UNIX>) have the fancy property of letting you
send file descriptors over them, allowing you to pass, for example, a socket
from one process to another. This is a pretty low-level operation and not
something you'll often need, but two functions to use that feature are provided
here anyway because the L<FU> supervisor uses them:
=over
=item fdpass_send($send_fd, $pass_fd, $message)
Send a message and a file descriptor (C<$pass_fd>) over the given socket
(<$send_fd>). C<$message> must not be empty, even if you don't intend to do
anything with it on receipt. Both C<$send_fd> and C<$pass_fd> must be numeric
file descriptors, as obtained by C<fileno()>.
=item ($fd, $message) = fdpass_recv($recv_fd, $max_message_len)
Read a file descriptor and message from the given C<$recv_fd>, which must be
the numeric file descriptor of a socket. This function can be used as a
replacement for C<sysread()>: the returned C<$fd> is undef if no file
descriptor was received. The returned C<$message> is undef on error or an empty
string on EOF.
Like regular socket I/O, a single C<fdpass_send()> message may be split across
multiple C<fdpass_recv()> calls; in that case the C<$fd> will only be received
on the first call.
Don't use this function if the sender may include multiple file descriptors in
a single message, weird things can happen. File descriptors received this way
do not have the C<CLOEXEC> flag and will thus survive a call to C<exec()>.
Refer to L<this wonderful
discussion|https://gist.github.com/kentonv/bc7592af98c68ba2738f4436920868dc>
for more weirdness and edge cases.
=back
See also L<IO::FDPass> for a more portable solution, although that one does not
support passing along regular data.

View file

@ -17,7 +17,6 @@ make install
Things that may or may not happen: Things that may or may not happen:
- FU - The website framework, taking inspiration from TUWF. - FU - The website framework, taking inspiration from TUWF.
- FU::HTTPServer / FU::FastCGI - Minimal libs to support the web framework.
- FU::JSON - JSON::{XS,PP,etc}-compatible wrapper around FU::Util's JSON functions? I prolly won't need this myself, but could be handy. - FU::JSON - JSON::{XS,PP,etc}-compatible wrapper around FU::Util's JSON functions? I prolly won't need this myself, but could be handy.
- FU::Log - Basic logger. - FU::Log - Basic logger.
- FU::Util additions: `uri_escape`, `VNDB::Util::query_encode`, `scrypt`, `urandom`. - FU::Util additions: `uri_escape`, `VNDB::Util::query_encode`, `scrypt`, `urandom`.

76
c/fdpass.c Normal file
View file

@ -0,0 +1,76 @@
/* File descriptor passing based on
* https://manned.org/man.c2c6968a/cmsg.3 */
static ssize_t fufdpass_send(int socket, int fd, const char *buf, size_t buflen) {
union {
char buf[CMSG_SPACE(sizeof(int))];
struct cmsghdr align;
} cmsgbuf = {};
struct iovec iov;
iov.iov_base = (char *)buf;
iov.iov_len = buflen;
struct msghdr msg;
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsgbuf.buf;
msg.msg_controllen = sizeof(cmsgbuf.buf);
msg.msg_flags = 0;
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
return sendmsg(socket, &msg, 0);
}
static int fufdpass_recv(pTHX_ I32 ax, int socket, size_t len) {
if (GIMME_V != G_LIST)
fu_confess("Invalid use of fdpass_recv() in scalar context");
union {
char buf[CMSG_SPACE(sizeof(int))];
struct cmsghdr align;
} cmsgbuf;
SV *buf = sv_2mortal(newSV(len));
SvPOK_only(buf);
struct iovec iov;
iov.iov_base = SvPVX(buf);
iov.iov_len = len;
struct msghdr msg;
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsgbuf.buf;
msg.msg_controllen = sizeof(cmsgbuf.buf);
msg.msg_flags = 0;
ssize_t r = recvmsg(socket, &msg, 0);
if (r < 0) {
ST(0) = &PL_sv_undef;
ST(1) = &PL_sv_undef;
return 2;
}
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
if (cmsg == NULL || cmsg->cmsg_level != SOL_SOCKET
|| cmsg->cmsg_type != SCM_RIGHTS || cmsg->cmsg_len != CMSG_LEN(sizeof(int))) {
ST(0) = &PL_sv_undef;
} else {
int fd;
memcpy(&fd, CMSG_DATA(cmsg), sizeof(int));
ST(0) = sv_2mortal(newSViv(fd));
}
SvCUR_set(buf, r);
ST(1) = buf;
return 2;
}

View file

@ -16,7 +16,7 @@ struct fupg_prep {
}; };
#define fupg_prep_hash(p) ((p)->hash) #define fupg_prep_hash(p) ((p)->hash)
#define fupg_prep_eq(a, b) (strcmp((a)->query, (b)->query) == 0) #define fupg_prep_eq(a, b) ((a)->hash == (b)->hash && strcmp((a)->query, (b)->query) == 0)
KHASHL_SET_INIT(KH_LOCAL, fupg_prepared, fupg_prepared, fupg_prep *, fupg_prep_hash, fupg_prep_eq); KHASHL_SET_INIT(KH_LOCAL, fupg_prepared, fupg_prepared, fupg_prep *, fupg_prep_hash, fupg_prep_eq);
static void fupg_prep_destroy(fupg_prep *p) { static void fupg_prep_destroy(fupg_prep *p) {

39
t/fdpass.t Normal file
View file

@ -0,0 +1,39 @@
use v5.36;
use Test::More;
use IO::Socket qw/AF_UNIX SOCK_STREAM PF_UNSPEC/;
BEGIN { use_ok 'FU::Util', qw/fdpass_send fdpass_recv/ }
my ($rd, $wr) = IO::Socket->socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC);
is $wr->syswrite("abc", 3), 3;
my ($fd, $buf) = fdpass_recv fileno($rd), 10;
ok !defined $fd;
is $buf, 'abc';
is fdpass_send(fileno($wr), fileno($wr), 'def'), 3;
($fd, $buf) = fdpass_recv fileno($rd), 50;
ok $fd > 0;
is $buf, 'def';
# Check that $fd is indeed an alias for $wr
my $nwr = IO::Socket->new_from_fd($fd, 'w');
is $nwr->syswrite('hij'), 3;
is $rd->sysread($buf, 20), 3;
is $buf, 'hij';
$nwr->close;
$wr->close;
($fd, $buf) = fdpass_recv fileno($rd), 10;
ok !defined $fd;
is $buf, '';
($fd, $buf) = fdpass_recv -1, 10;
ok !defined $fd;
ok !defined $buf;
is fdpass_send(-1, 3, 'x'), -1;
done_testing;