FU
view release on metacpan or search on metacpan
package FU 1.4;
use v5.36;
use Carp 'confess', 'croak';
use IO::Socket;
use POSIX ();
use Time::HiRes 'time', 'clock_gettime', 'CLOCK_MONOTONIC';
use FU::Log 'log_write';
use FU::Util;
use FU::Validate;
my $procname;
my $scriptpath = $0;
sub import($pkg, @opt) {
my $c = caller;
no strict 'refs';
*{$c.'::fu'} = \&fu;
my $spawn;
for (@opt) {
if (ref $procname eq 'FU::ARG') { $procname = $_ }
elsif ($_ eq '-procname') { $procname = bless {}, 'FU::ARG' }
elsif ($_ eq '-spawn') { $spawn = 1; }
else { croak "Unknown import option: '$_'" }
}
croak "Missing argument for -procname option" if ref $procname eq 'FU::ARG';
_spawn() if $spawn;
}
our $REQ = {}; # Internal request-local data
our $fu = bless {}, 'FU::obj'; # App request-local data
sub fu() { $fu }
FU::Log::capture_warn(1);
FU::Log::set_fmt(sub($msg) {
FU::Log::default_fmt($msg,
fu->path && fu->method ? fu->method.' '.fu->path.(fu->query?'?'.fu->query:'') : '[global]',
);
});
sub debug { state $v = 0; $v = $_[0] if @_; $v }
sub log_slow_reqs { state $v = 0; $v = $_[0] if @_; $v }
sub mime_types() { state $v = {qw{
7z application/x-7z-compressed
aac audio/aac
atom application/atom+xml
avi video/x-msvideo
avif image/avif
bin application/octet-stream
bmp image/bmp
bz2 application/x-bzip2
css text/css
csv text/csv
gif image/gif
htm text/html
html text/html
ico image/x-icon
jpeg image/jpeg
jpg image/jpeg
js application/javascript
json application/json
jxl image/jxl
mjs application/javascript
mp3 audio/mpeg
mp4 video/mp4
mp4v video/mp4
mpg4 video/mp4
mpg video/mpeg
mpeg video/mpeg
oga audio/ogg
ogg audio/ogg
ogv video/ogg
otf font/otf
pdf application/pdf
png image/png
rar application/x-rar-compressed
rss application/rss+xml
svg image/svg+xml
tar application/x-tar
tiff image/tiff
ttf font/ttf
txt text/plain
webp image/webp
webm video/webm
xhtml text/html
xml application/xml
xsd application/xml
xsl application/xml
zip application/zip
zst application/zstd
}} }
# Single process, no need for a supervisor
my $need_supervisor = !$c{supervisor_sock} && !$c{client_sock} && ($c{proc} > 1 || $c{monitor} || $c{max_reqs});
return if !@_ && !$need_supervisor;
if (!$c{http} && !$c{fcgi} && !$c{listen_sock}) {
# When spawned under FastCGI, stdin is our listen socket
local $_ = getpeername \*STDIN;
if ($!{ENOTCONN}) {
$c{listen_sock} = IO::Socket->new_from_fd(0, 'r');
$c{listen_proto} = 'fcgi';
}
};
$c{http} //= '127.0.0.1:3000';
if (!$c{listen_sock}) {
$c{listen_proto} //= $c{fcgi} ? 'fcgi' : 'http';
my $addr = $c{$c{listen_proto}};
$c{listen_sock} = IO::Socket->new(
Listen => 10 * $c{proc},
Type => IO::Socket::SOCK_STREAM(),
$addr =~ m{^(unix:|/)(.+)$} ? do {
my $path = ($1 eq '/' ? '/' : '').$2;
unlink $path if -S $path;
+(Domain => IO::Socket::AF_UNIX(), Local => $path)
} : (
Domain => IO::Socket::AF_INET(),
ReuseAddr => 1,
Proto => 'tcp',
LocalAddr => $addr,
)
) or die "Unable to create listen socket: $!\n";
log_write "Listening on $addr\n" if debug;
}
if ($need_supervisor) {
_supervisor \%c;
} else {
$c{supervisor_sock}->syswrite('r'.pack 'V', $$) if $c{supervisor_sock};
$c{max_reqs} = $1 >= $2 ? $1 : $1 + int rand $2-$1 if $c{max_reqs} =~ /^([0-9]+):([0-9]+)$/;
_run_loop \%c;
}
}
sub run(%conf) {
confess "FU::run() called with configuration options, but FU has already been loaded with -spawn" if keys %conf;
# Clean up any state we may have accumulated during initialization.
$REQ = {};
$fu = bless {}, 'FU::obj';
_spawn(keys %conf ? \%conf : undef);
}
package FU::obj;
use v5.36;
use Carp 'confess';
sub fu() { $FU::fu }
sub debug { FU::debug }
sub db_conn { $FU::DB || FU::_connect_db }
sub db {
$REQ->{txn} ||= do {
my $txn = eval { fu->db_conn->txn };
if (!$txn) {
# Can't start a transaction? We might be screwed, try to reconnect.
FU::_connect_db;
$txn = fu->db_conn->txn; # Let this error if it also fails
}
$txn
};
}
sub sql { shift->db->sql(@_) }
sub SQL { shift->db->SQL(@_) }
sub _fmt_section($s) { $s =~ s/^\s*/ /r =~ s/\s+$//r =~ s/\n/\n /rg }
sub log_verbose($,$msg) {
my $r = $FU::REQ;
return FU::Log::log_write($msg) if $r->{log_verbose}++;
FU::Log::log_write(join "\n",
'IP: '.($r->{ip}||'-'),
'Headers:', (map " $_: $r->{hdr}{$_}", sort keys $r->{hdr}->%*),
$r->{multipart} ? ('Body (multipart):', _fmt_section join "\n", map $_->describe, $r->{multipart}->@*) :
$r->{json} ? ('Body (JSON):', _fmt_section FU::Util::json_format($r->{json}, pretty => 1, canonical => 1)) :
$r->{formdata} ? ('Body (formdata):', _fmt_section FU::Util::json_format($r->{formdata}, pretty => 1, canonical => 1)) :
length $r->{body} ? do {
my $b = substr $r->{body}, 0, 4096;
my $trunc = length $r->{body} > 4096 ? ', truncated' : '';
utf8::decode($b) ? ("Body (utf8$trunc):", _fmt_section($b =~ s/\r//rg =~ s/\n{4,}/\n[..]\n/rg))
: ("Body (hex$trunc):", _fmt_section(unpack('H*', $b) =~ s/(.{128})/$1\n/rg))
} : (),
'Message:', _fmt_section $msg
);
}
# Request information methods
sub path { $FU::REQ->{path} }
sub method { $FU::REQ->{method} }
sub header($, $h) { $FU::REQ->{hdr}{ lc $h } }
sub headers { $FU::REQ->{hdr} }
sub ip { $FU::REQ->{ip} }
sub _getfield($data, @a) {
if (@a == 1 && !ref $a[0]) {
fu->error(400, "Expected top-level to be a hash") if ref $data ne 'HASH';
return $data->{$a[0]};
}
my $schema = FU::Validate->compile(@a > 1 ? { keys => {@a} } : $a[0]);
my $res = $schema->validate($data);
return @a == 2 ? $res->{$a[0]} : $res;
}
( run in 2.086 seconds using v1.01-cache-2.11-cpan-364913b4093 )