]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/Spawn.pm
spawn: require soft and hard entries in RLIMIT_* handling
[public-inbox.git] / lib / PublicInbox / Spawn.pm
1 # Copyright (C) 2016-2018 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3 #
4 # This allows vfork to be used for spawning subprocesses if
5 # PERL_INLINE_DIRECTORY is explicitly defined in the environment.
6 # Under Linux, vfork can make a big difference in spawning performance
7 # as process size increases (fork still needs to mark pages for CoW use).
8 # Currently, we only use this for code intended for long running
9 # daemons (inside the PSGI code (-httpd) and -nntpd).  The short-lived
10 # scripts (-mda, -index, -learn, -init) either use IPC::run or standard
11 # Perl routines.
12
13 package PublicInbox::Spawn;
14 use strict;
15 use warnings;
16 use base qw(Exporter);
17 use Symbol qw(gensym);
18 use IO::Handle;
19 use PublicInbox::ProcessPipe;
20 our @EXPORT_OK = qw/which spawn popen_rd/;
21 sub RLIMITS () { qw(RLIMIT_CPU RLIMIT_CORE RLIMIT_DATA) }
22
23 my $vfork_spawn = <<'VFORK_SPAWN';
24 #include <sys/types.h>
25 #include <sys/uio.h>
26 #include <sys/time.h>
27 #include <sys/resource.h>
28 #include <unistd.h>
29 #include <alloca.h>
30 #include <signal.h>
31 #include <assert.h>
32
33 #define AV_ALLOCA(av, max) alloca((max = (av_len((av)) + 1)) * sizeof(char *))
34
35 static void av2c_copy(char **dst, AV *src, I32 max)
36 {
37         I32 i;
38
39         for (i = 0; i < max; i++) {
40                 SV **sv = av_fetch(src, i, 0);
41                 dst[i] = sv ? SvPV_nolen(*sv) : 0;
42         }
43         dst[max] = 0;
44 }
45
46 static void *deconst(const char *s)
47 {
48         union { const char *in; void *out; } u;
49         u.in = s;
50         return u.out;
51 }
52
53 /* needs to be safe inside a vfork'ed process */
54 static void xerr(const char *msg)
55 {
56         struct iovec iov[3];
57         const char *err = strerror(errno); /* should be safe in practice */
58
59         iov[0].iov_base = deconst(msg);
60         iov[0].iov_len = strlen(msg);
61         iov[1].iov_base = deconst(err);
62         iov[1].iov_len = strlen(err);
63         iov[2].iov_base = deconst("\n");
64         iov[2].iov_len = 1;
65         writev(2, iov, 3);
66         _exit(1);
67 }
68
69 #define REDIR(var,fd) do { \
70         if (var != fd && dup2(var, fd) < 0) \
71                 xerr("error redirecting std"#var ": "); \
72 } while (0)
73
74 /*
75  * unstable internal API.  This was easy to implement but does not
76  * support arbitrary redirects.  It'll be updated depending on
77  * whatever we'll need in the future.
78  * Be sure to update PublicInbox::SpawnPP if this changes
79  */
80 int pi_fork_exec(int in, int out, int err,
81                         SV *file, SV *cmdref, SV *envref, SV *rlimref)
82 {
83         AV *cmd = (AV *)SvRV(cmdref);
84         AV *env = (AV *)SvRV(envref);
85         AV *rlim = (AV *)SvRV(rlimref);
86         const char *filename = SvPV_nolen(file);
87         pid_t pid;
88         char **argv, **envp;
89         I32 max;
90         sigset_t set, old;
91         int ret, errnum;
92
93         argv = AV_ALLOCA(cmd, max);
94         av2c_copy(argv, cmd, max);
95
96         envp = AV_ALLOCA(env, max);
97         av2c_copy(envp, env, max);
98
99         ret = sigfillset(&set);
100         assert(ret == 0 && "BUG calling sigfillset");
101         ret = sigprocmask(SIG_SETMASK, &set, &old);
102         assert(ret == 0 && "BUG calling sigprocmask to block");
103         pid = vfork();
104         if (pid == 0) {
105                 int sig;
106                 I32 i, max;
107
108                 REDIR(in, 0);
109                 REDIR(out, 1);
110                 REDIR(err, 2);
111                 for (sig = 1; sig < NSIG; sig++)
112                         signal(sig, SIG_DFL); /* ignore errors on signals */
113
114                 max = av_len(rlim);
115                 for (i = 0; i < max; i += 3) {
116                         struct rlimit rl;
117                         SV **res = av_fetch(rlim, i, 0);
118                         SV **soft = av_fetch(rlim, i + 1, 0);
119                         SV **hard = av_fetch(rlim, i + 2, 0);
120
121                         rl.rlim_cur = SvIV(*soft);
122                         rl.rlim_max = SvIV(*hard);
123                         if (setrlimit(SvIV(*res), &rl) < 0)
124                                 xerr("sertlimit");
125                 }
126
127                 /*
128                  * don't bother unblocking, we don't want signals
129                  * to the group taking out a subprocess
130                  */
131                 execve(filename, argv, envp);
132                 xerr("execve failed");
133         }
134         errnum = errno;
135         ret = sigprocmask(SIG_SETMASK, &old, NULL);
136         assert(ret == 0 && "BUG calling sigprocmask to restore");
137         errno = errnum;
138
139         return (int)pid;
140 }
141 VFORK_SPAWN
142
143 my $inline_dir = $ENV{PERL_INLINE_DIRECTORY};
144 $vfork_spawn = undef unless defined $inline_dir && -d $inline_dir && -w _;
145 if (defined $vfork_spawn) {
146         # Inline 0.64 or later has locking in multi-process env,
147         # but we support 0.5 on Debian wheezy
148         use Fcntl qw(:flock);
149         eval {
150                 my $f = "$inline_dir/.public-inbox.lock";
151                 open my $fh, '>', $f or die "failed to open $f: $!\n";
152                 flock($fh, LOCK_EX) or die "LOCK_EX failed on $f: $!\n";
153                 eval 'use Inline C => $vfork_spawn'; #, BUILD_NOISY => 1';
154                 my $err = $@;
155                 flock($fh, LOCK_UN) or die "LOCK_UN failed on $f: $!\n";
156                 die $err if $err;
157         };
158         if ($@) {
159                 warn "Inline::C failed for vfork: $@\n";
160                 $vfork_spawn = undef;
161         }
162 }
163
164 unless (defined $vfork_spawn) {
165         require PublicInbox::SpawnPP;
166         no warnings 'once';
167         *pi_fork_exec = *PublicInbox::SpawnPP::pi_fork_exec
168 }
169
170 sub which ($) {
171         my ($file) = @_;
172         return $file if index($file, '/') == 0;
173         foreach my $p (split(':', $ENV{PATH})) {
174                 $p .= "/$file";
175                 return $p if -x $p;
176         }
177         undef;
178 }
179
180 sub spawn ($;$$) {
181         my ($cmd, $env, $opts) = @_;
182         my $f = which($cmd->[0]);
183         defined $f or die "$cmd->[0]: command not found\n";
184         my @env;
185         $opts ||= {};
186
187         my %env = $opts->{-env} ? () : %ENV;
188         if ($env) {
189                 foreach my $k (keys %$env) {
190                         my $v = $env->{$k};
191                         if (defined $v) {
192                                 $env{$k} = $v;
193                         } else {
194                                 delete $env{$k};
195                         }
196                 }
197         }
198         while (my ($k, $v) = each %env) {
199                 push @env, "$k=$v";
200         }
201         my $in = $opts->{0} || 0;
202         my $out = $opts->{1} || 1;
203         my $err = $opts->{2} || 2;
204         my $rlim = [];
205
206         foreach my $l (RLIMITS()) {
207                 defined(my $v = $opts->{$l}) or next;
208                 my $r = eval "require BSD::Resource; BSD::Resource::$l();";
209                 unless (defined $r) {
210                         warn "$l undefined by BSD::Resource: $@\n";
211                         next;
212                 }
213                 push @$rlim, $r, @$v;
214         }
215         my $pid = pi_fork_exec($in, $out, $err, $f, $cmd, \@env, $rlim);
216         $pid < 0 ? undef : $pid;
217 }
218
219 sub popen_rd {
220         my ($cmd, $env, $opts) = @_;
221         pipe(my ($r, $w)) or die "pipe: $!\n";
222         $opts ||= {};
223         my $blocking = $opts->{Blocking};
224         IO::Handle::blocking($r, $blocking) if defined $blocking;
225         $opts->{1} = fileno($w);
226         my $pid = spawn($cmd, $env, $opts);
227         return unless defined $pid;
228         return ($r, $pid) if wantarray;
229         my $ret = gensym;
230         tie *$ret, 'PublicInbox::ProcessPipe', $pid, $r;
231         $ret;
232 }
233
234 1;