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