-# Copyright (C) 2016-2020 all contributors <meta@public-inbox.org>
+# Copyright (C) 2016-2021 all contributors <meta@public-inbox.org>
# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
#
# This allows vfork to be used for spawning subprocesses if
# daemons (inside the PSGI code (-httpd) and -nntpd). The short-lived
# scripts (-mda, -index, -learn, -init) either use IPC::run or standard
# Perl routines.
+#
+# There'll probably be more OS-level C stuff here, down the line.
+# We don't want too many DSOs: https://udrepper.livejournal.com/8790.html
package PublicInbox::Spawn;
use strict;
-use warnings;
-use base qw(Exporter);
+use parent qw(Exporter);
use Symbol qw(gensym);
use PublicInbox::ProcessPipe;
-our @EXPORT_OK = qw/which spawn popen_rd/;
-sub RLIMITS () { qw(RLIMIT_CPU RLIMIT_CORE RLIMIT_DATA) }
+our @EXPORT_OK = qw(which spawn popen_rd run_die nodatacow_dir);
+our @RLIMITS = qw(RLIMIT_CPU RLIMIT_CORE RLIMIT_DATA);
my $vfork_spawn = <<'VFORK_SPAWN';
#include <sys/types.h>
-#include <sys/uio.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <unistd.h>
#include <stdlib.h>
+#include <errno.h>
/* some platforms need alloca.h, but some don't */
#if defined(__GNUC__) && !defined(alloca)
}
VFORK_SPAWN
+# btrfs on Linux is copy-on-write (COW) by default. As of Linux 5.7,
+# this still leads to fragmentation for SQLite and Xapian files where
+# random I/O happens, so we disable COW just for SQLite files and Xapian
+# directories. Disabling COW disables checksumming, so we only do this
+# for regeneratable files, and not canonical git storage (git doesn't
+# checksum refs, only data under $GIT_DIR/objects).
+my $set_nodatacow = $^O eq 'linux' ? <<'SET_NODATACOW' : '';
+#include <sys/ioctl.h>
+#include <sys/vfs.h>
+#include <linux/magic.h>
+#include <linux/fs.h>
+#include <dirent.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+void nodatacow_fd(int fd)
+{
+ struct statfs buf;
+ int val = 0;
+
+ if (fstatfs(fd, &buf) < 0) {
+ fprintf(stderr, "fstatfs: %s\\n", strerror(errno));
+ return;
+ }
+
+ /* only btrfs is known to have this problem, so skip for non-btrfs */
+ if (buf.f_type != BTRFS_SUPER_MAGIC)
+ return;
+
+ if (ioctl(fd, FS_IOC_GETFLAGS, &val) < 0) {
+ fprintf(stderr, "FS_IOC_GET_FLAGS: %s\\n", strerror(errno));
+ return;
+ }
+ val |= FS_NOCOW_FL;
+ if (ioctl(fd, FS_IOC_SETFLAGS, &val) < 0)
+ fprintf(stderr, "FS_IOC_SET_FLAGS: %s\\n", strerror(errno));
+}
+
+void nodatacow_dir(const char *dir)
+{
+ DIR *dh = opendir(dir);
+ int fd;
+
+ if (!dh) croak("opendir(%s): %s", dir, strerror(errno));
+ fd = dirfd(dh);
+ if (fd >= 0)
+ nodatacow_fd(fd);
+ /* ENOTSUP probably won't happen under Linux... */
+ closedir(dh);
+}
+SET_NODATACOW
+
+# last choice for script/lei, 1st choice for lei internals
+# compatible with PublicInbox::CmdIPC4
+my $fdpass = <<'FDPASS';
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <sys/socket.h>
+
+#if defined(CMSG_SPACE) && defined(CMSG_LEN)
+#define SEND_FD_CAPA 6
+#define SEND_FD_SPACE (SEND_FD_CAPA * sizeof(int))
+union my_cmsg {
+ struct cmsghdr hdr;
+ char pad[sizeof(struct cmsghdr) + 16 + SEND_FD_SPACE];
+};
+
+SV *send_cmd4(PerlIO *s, SV *svfds, SV *data, int flags)
+{
+ struct msghdr msg = { 0 };
+ union my_cmsg cmsg = { 0 };
+ STRLEN dlen = 0;
+ struct iovec iov;
+ ssize_t sent;
+ AV *fds = (AV *)SvRV(svfds);
+ I32 i, nfds = av_len(fds) + 1;
+ int *fdp;
+
+ if (SvOK(data)) {
+ iov.iov_base = SvPV(data, dlen);
+ iov.iov_len = dlen;
+ }
+ if (!dlen) { /* must be non-zero */
+ iov.iov_base = &msg.msg_namelen; /* whatever */
+ iov.iov_len = 1;
+ }
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+ if (nfds) {
+ if (nfds > SEND_FD_CAPA) {
+ fprintf(stderr, "FIXME: bump SEND_FD_CAPA=%d\n", nfds);
+ nfds = SEND_FD_CAPA;
+ }
+ msg.msg_control = &cmsg.hdr;
+ msg.msg_controllen = CMSG_SPACE(nfds * sizeof(int));
+ cmsg.hdr.cmsg_level = SOL_SOCKET;
+ cmsg.hdr.cmsg_type = SCM_RIGHTS;
+ cmsg.hdr.cmsg_len = CMSG_LEN(nfds * sizeof(int));
+ fdp = (int *)CMSG_DATA(&cmsg.hdr);
+ for (i = 0; i < nfds; i++) {
+ SV **fd = av_fetch(fds, i, 0);
+ *fdp++ = SvIV(*fd);
+ }
+ }
+ sent = sendmsg(PerlIO_fileno(s), &msg, flags);
+ return sent >= 0 ? newSViv(sent) : &PL_sv_undef;
+}
+
+void recv_cmd4(PerlIO *s, SV *buf, STRLEN n)
+{
+ union my_cmsg cmsg = { 0 };
+ struct msghdr msg = { 0 };
+ struct iovec iov;
+ ssize_t i;
+ Inline_Stack_Vars;
+ Inline_Stack_Reset;
+
+ if (!SvOK(buf))
+ sv_setpvn(buf, "", 0);
+ iov.iov_base = SvGROW(buf, n + 1);
+ iov.iov_len = n;
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+ msg.msg_control = &cmsg.hdr;
+ msg.msg_controllen = CMSG_SPACE(SEND_FD_SPACE);
+
+ i = recvmsg(PerlIO_fileno(s), &msg, 0);
+ if (i < 0)
+ Inline_Stack_Push(&PL_sv_undef);
+ else
+ SvCUR_set(buf, i);
+ if (i > 0 && cmsg.hdr.cmsg_level == SOL_SOCKET &&
+ cmsg.hdr.cmsg_type == SCM_RIGHTS) {
+ size_t len = cmsg.hdr.cmsg_len;
+ int *fdp = (int *)CMSG_DATA(&cmsg.hdr);
+ for (i = 0; CMSG_LEN((i + 1) * sizeof(int)) <= len; i++)
+ Inline_Stack_Push(sv_2mortal(newSViv(*fdp++)));
+ }
+ Inline_Stack_Done;
+}
+#endif /* defined(CMSG_SPACE) && defined(CMSG_LEN) */
+FDPASS
+
my $inline_dir = $ENV{PERL_INLINE_DIRECTORY} //= (
$ENV{XDG_CACHE_HOME} //
( ($ENV{HOME} // '/nonexistent').'/.cache' )
).'/public-inbox/inline-c';
-$vfork_spawn = undef unless -d $inline_dir && -w _;
+$set_nodatacow = $vfork_spawn = $fdpass = undef unless -d $inline_dir && -w _;
if (defined $vfork_spawn) {
# Inline 0.64 or later has locking in multi-process env,
# but we support 0.5 on Debian wheezy
my $f = "$inline_dir/.public-inbox.lock";
open my $fh, '>', $f or die "failed to open $f: $!\n";
flock($fh, LOCK_EX) or die "LOCK_EX failed on $f: $!\n";
- eval 'use Inline C => $vfork_spawn';
+ eval 'use Inline C => $vfork_spawn.$fdpass.$set_nodatacow';
+ # . ', BUILD_NOISY => 1';
my $err = $@;
+ my $ndc_err;
+ if ($err && $set_nodatacow) { # missing Linux kernel headers
+ $ndc_err = $err;
+ undef $set_nodatacow;
+ eval 'use Inline C => $vfork_spawn . $fdpass';
+ }
flock($fh, LOCK_UN) or die "LOCK_UN failed on $f: $!\n";
die $err if $err;
+ warn $ndc_err if $ndc_err;
};
if ($@) {
warn "Inline::C failed for vfork: $@\n";
- $vfork_spawn = undef;
+ $set_nodatacow = $vfork_spawn = $fdpass = undef;
}
}
require PublicInbox::SpawnPP;
*pi_fork_exec = \&PublicInbox::SpawnPP::pi_fork_exec
}
+unless ($set_nodatacow) {
+ require PublicInbox::NDC_PP;
+ no warnings 'once';
+ *nodatacow_fd = \&PublicInbox::NDC_PP::nodatacow_fd;
+ *nodatacow_dir = \&PublicInbox::NDC_PP::nodatacow_dir;
+}
+
+undef $set_nodatacow;
+undef $vfork_spawn;
+undef $fdpass;
sub which ($) {
my ($file) = @_;
return $file if index($file, '/') >= 0;
- foreach my $p (split(':', $ENV{PATH})) {
+ for my $p (split(/:/, $ENV{PATH})) {
$p .= "/$file";
return $p if -x $p;
}
}
my $rlim = [];
- foreach my $l (RLIMITS()) {
+ foreach my $l (@RLIMITS) {
defined(my $v = $opts->{$l}) or next;
my $r = eval "require BSD::Resource; BSD::Resource::$l();";
unless (defined $r) {
}
my $cd = $opts->{'-C'} // ''; # undef => NULL mapping doesn't work?
my $pid = pi_fork_exec($redir, $f, $cmd, \@env, $rlim, $cd);
- die "fork_exec failed: $!\n" unless $pid > 0;
+ die "fork_exec @$cmd failed: $!\n" unless $pid > 0;
$pid;
}
sub popen_rd {
- my ($cmd, $env, $opts) = @_;
+ my ($cmd, $env, $opt) = @_;
pipe(my ($r, $w)) or die "pipe: $!\n";
- $opts ||= {};
- $opts->{1} = fileno($w);
- my $pid = spawn($cmd, $env, $opts);
+ $opt ||= {};
+ $opt->{1} = fileno($w);
+ my $pid = spawn($cmd, $env, $opt);
return ($r, $pid) if wantarray;
my $ret = gensym;
- tie *$ret, 'PublicInbox::ProcessPipe', $pid, $r;
+ tie *$ret, 'PublicInbox::ProcessPipe', $pid, $r, @$opt{qw(cb arg)};
$ret;
}
+sub run_die ($;$$) {
+ my ($cmd, $env, $rdr) = @_;
+ my $pid = spawn($cmd, $env, $rdr);
+ waitpid($pid, 0) == $pid or die "@$cmd did not finish";
+ $? == 0 or die "@$cmd failed: \$?=$?\n";
+}
+
1;