chiark / gitweb /
prefork: fix missing import
[chiark-utils.git] / perl / Prefork.pm
1
2 package Proc::Prefork;
3 require Exporter;
4 our @ISA = qw(Exporter);
5 our @EXPORT = qw(initialisation_complete);
6
7 use Carp;
8 use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK);
9 use IO::FDPass;
10 use POSIX qw(_exit setsid);
11 use Sys::Syslog;
12
13 our $logger;
14
15 our $env_name = 'PREFORK_INTERP';
16
17 our @call_fds;
18 our $socket_path;
19
20 sub fail_log ($) {
21   my ($m) = @_;
22   syslog(LOG_ERR, "$0: prefork [$$]: error: $m");
23   _exit 127;
24 }
25
26 sub server_quit ($) {
27   my ($m) = @_;
28   syslog(LOG_INFO, "$0 prefork [$$]: $m, quitting");
29   _exit(0);
30 }
31
32 # Returns in the executor process
33 sub become_monitor () {
34   my $child = fork // fail("fork executor: $!");
35   if (!$child) {
36     #---- executor ----
37     open ::STDIN , "<& $call_fds[0]" or fail_log("dup for fd0");
38     open ::STDOUT, ">& $call_fds[1]" or fail_log("dup for fd1");
39     open ::STDERR, ">& $call_fds[2]" or fail_log("dup for fd2");
40     close_call_fds();
41     return;
42   }
43
44   #---- monitor [2] ----
45   my $got = waitpid $child, 0 // fail_log("wait for executor: $!");
46   $got == $child or fail_log("wait for esecutor gave $got, expected $child");
47
48   protocol_write(pack "L", $?);
49   _exit(0);
50 }
51
52 sub close_call_fds () {
53   foreach (@call_fds) {
54     next if $_ <= 2;
55     POSIX::close($_);
56   }
57   close CALL;
58 }
59
60 sub protocol_write ($) {
61   my ($d) = @_;
62   return if (print CALL $d and flush CALL);
63   _exit(0) if $!==EPIPE || $!==ECONNRESET;
64   fail_log("protocol write: $!");
65 }
66
67 sub eintr_retry ($) {
68   my ($f) = @_;
69   for (;;) {
70     my $r = $f->();
71     return $r if defined $r;
72     next if $!==EINTR;
73     return $r;
74   }
75 }
76
77 sub protocol_read_fail ($) {
78   my ($what) = @_;
79   _exit(0) if $!==ECONNRESET;
80   die("recv $what: $!");
81 }
82
83 sub protocol_exchange () {
84   protocol_write("\n");
85
86   @call_fds = map {
87     my $r;
88     for (;;) {
89       $! = 0;
90       $r = IO::FDPass::recv(fileno(CALL));
91       last if $r >= 0;
92       _exit(0) if $!==0;
93       protocol_read_fail("fd $_");
94     }
95     $r;
96   } 0..2;
97
98   my $len;
99   $r = read(CALL, $len, 4) // protocol_read_fail("message length");
100   $r == 4 or _exit(0);
101
102   $len = unpack "L", $len;
103   my $data;
104   $r = read(CALL, $data, $len) // protocol_read_fail("message data ($len)");
105   $r == $len or _exit(0);
106
107   @ARGV = split /\0/, $data;
108   @ARGV >= 2 or die("message data has too few strings");
109   length(pop(@ARGV)) and die("message data missing trailing nul");
110   %ENV = ();
111   while (my $s = shift @ARGV) {
112     last if !length $s;
113     $s =~ m/=/ or die("message data env var missing equals");
114     $ENV{$`} = $';
115   }
116 }
117
118 sub initialisation_complete {
119   my %opts = @_;
120
121   # if env var not set, we're not running under prefork-interp
122   my @env_params = split /,/, ($ENV{$env_name} // return), 5;
123   croak "$env_name has too few entries" unless @env_params == 5;
124
125   $num_servers = $opts{max_servers} // 4;
126
127   #---- setup (pm) [1] ----
128
129   $socket_path = pop @env_params;
130   foreach (@env_params) {
131     $_ eq ($_+0) or croak "$env_name contains $_, not a number";
132   }
133   open LISTEN, "+>&=$env_params[0]" or croak "listen fd: $!";
134   open CALL,   "+>&=$env_params[1]" or croak "listen fd: $!";
135   @call_fds = ($env_params[2], $env_params[3], 2);
136
137   if (!$opts{no_openlog}) {
138     openlog("prefork-interp $0", 'ndelay,nofatal,pid',
139             $opts{log_facility} // 'log_user');
140   }
141
142   open NULL, "+>/dev/null" or croak "open /dev/null: $!";
143
144   #---- fork for server ----
145
146   my $child = fork // croak "first fork failed: $!";
147   if ($child) {
148     #---- setup (pm) [2], exits ----
149     _exit(0);
150   }
151
152   #---- server(pm) [1] ----
153
154   $child = fork // croak "second fork failed: $!";
155   if (!$child) {
156     # we are the child, i.e. the one fa-monitor
157     return become_monitor();
158   }
159
160   our %children;
161   $children{$child} = 1;
162   
163   # --- server(pm) [2] ----
164
165   setsid() > 0 or fail_log("setsid: $!");
166   open STDIN, "<&NULL" or fail_log("dup null onto stdin: $!");
167   open STDOUT, ">&NULL" or fail_log("dup null onto stdout: $!");
168   open STDERR, ">&NULL" or fail_log("dup null onto stderr: $!");
169   close NULL;
170
171   close_call_fds();
172
173   my $flags = fcntl(LISTEN, F_GETFL, 0)
174     or fail_log("F_GETFL listen socket: $!");
175   $flags |= O_NONBLOCK;
176   fcntl(LISTEN, F_SETFL, $flags)
177     or fail_log("F_SETFL listen socket: $!");
178
179   my $errcount = 0;
180
181   for (;;) {
182     # reap children
183     if (%children) {
184       my $full = %children >= $num_servers;
185       my $got = waitpid -1, ($full ? 0 : WNOHANG);
186       if ($?) {
187         syslog(LOG_WARNING,
188  "$0 prefork [$$]: monitor process [$got] failed with wait status $?");
189       }
190       if (!exists $children{$got}) {
191         syslog(LOG_WARNING,
192  "$0 prefork [$$]: monitor process [$got] wasn't one of ours?!");
193       }
194       delete $children{$got};
195       next;
196     }
197
198     # select for accepting or housekeeping timeout
199     my $rbits = '';
200     vec($rbits, fileno(LISTEN), 1) = 1;
201     my $ebits = $rbits;
202     my $nfound = select($rbits, '', $ebits, ($opts{check_interval} // 86400));
203
204     if ($nfound) {
205       if (accept(CALL, LISTEN)) {
206         $child = fork // fail_log("fork for accepted call failed: $!");
207         if (!$child) {
208           #---- monitor [1] ----
209           close LISTEN;
210           eval { protocol_exchange(); 1; }
211             or fail_log("protocol exchange failed: $@");
212           return become_monitor();
213         }
214         close(CALL);
215         $errcount = 0;
216         $children{$child} = 1;
217       } elsif ($! == EINTR || $! == EAGAIN || $! == EWOULDBLOCK) {
218       } else {
219         syslog(LOG_WARNING, "$0 prefork [$$]: accept failed: $!");
220         if ($errcount > ($opts{max_errors} // 100)) {
221           syslog(LOG_ERR,
222                  "$0 prefork [$$]: too many accept failures, quitting");
223           _exit(16);
224         }
225       }
226       next;
227     }
228
229     # are we still live?
230     my @st_listen = stat(LISTEN) // fail_log("fstat listening socket: $!");
231     my @st_socket = stat($socket_path) // do {
232       if ($! == ENOENT) { server_quit("socket $socket is ENOENT"); }
233       fail_log("stat socket $socket: $!");
234     };
235     if ("@st_listen[0..2]" ne "@st_socket[0..2]") {
236       server_quit("socket $socket is no longer ours");
237     }
238   }
239 }
240
241 1;
242
243 __END__