]>
Commit | Line | Data |
---|---|---|
cae70075 AM |
1 | #!/usr/bin/perl -w |
2 | ||
3 | ## eximq | |
4 | ## | |
5 | ## (c) 2003-2004 Piotr Roszatycki <dexter@debian.org>, GPL | |
6 | ## | |
7 | ## $Id$ | |
8 | ||
9 | =head1 NAME | |
10 | ||
11 | eximq - Supervising process for Exim's queue runners. | |
12 | ||
13 | =head1 SYNOPSIS | |
14 | ||
15 | B<eximq> B<-h>|B<--help> | |
16 | ||
17 | B<eximq> [B<--debug-stderr>] [B<--debug-syslog>] | |
18 | [B<--daemon>] S<[B<--pidfile> I<path>]> | |
19 | I<agemin> I<agemax> I<interval> I<processmax> [I<exim_q_command>] | |
20 | ||
21 | =cut | |
22 | ||
23 | use 5.006; | |
24 | use strict; | |
25 | ||
26 | use Getopt::Long qw(:config require_order no_auto_abbrev); | |
27 | use POSIX qw(:sys_wait_h :locale_h setsid); | |
28 | use Pod::Usage; | |
29 | use Unix::Syslog qw(:macros :subs); | |
30 | ||
31 | ||
32 | ############################################################################## | |
33 | ||
34 | ## Constant variables | |
35 | ## | |
36 | ||
37 | ## Program name | |
38 | my $NAME = "eximq"; | |
39 | ||
40 | ## Program version | |
41 | my $VERSION = 0.4; | |
42 | ||
43 | ## Global variables | |
44 | ## | |
45 | ||
46 | ## Spawned command | |
47 | my @eximq_cmd = qw(/usr/sbin/exim -q); | |
48 | ||
49 | ||
50 | ############################################################################## | |
51 | ||
52 | ## Private variables | |
53 | ## | |
54 | ||
55 | ## Count for running spawned processes | |
56 | my $running = 0; | |
57 | ||
58 | ## Process group ID | |
59 | my $pgrp = 0; | |
60 | ||
61 | ## Getopt::Long handler | |
62 | my %opt = ( | |
63 | 'pidfile' => "/var/run/$NAME.pid", | |
64 | ); | |
65 | ||
66 | ## Old value for LC_TIME | |
67 | my $old_lc_time = setlocale(LC_TIME); | |
68 | ||
69 | ||
70 | ############################################################################## | |
71 | ||
72 | ## debug($msg) | |
73 | ## | |
74 | ## Dumps message if debug mode is turned on. | |
75 | ## | |
76 | sub debug(@) { | |
77 | my (@msg) = @_; | |
78 | ||
79 | if (${opt{'debug-syslog'}}) { | |
80 | setlocale(LC_TIME, "C"); | |
81 | openlog($NAME, LOG_PID, LOG_MAIL); | |
82 | syslog(LOG_INFO, "%s", join('', @msg)); | |
83 | closelog; | |
84 | setlocale(LC_TIME, $old_lc_time); | |
85 | } | |
86 | if (${opt{'debug-stderr'}}) { | |
87 | print STDERR "*** @msg\n"; | |
88 | } | |
89 | } | |
90 | ||
91 | ||
92 | ## error($msg) | |
93 | ## | |
94 | ## Dumps error message | |
95 | ## | |
96 | sub error(@) { | |
97 | my (@msg) = @_; | |
98 | ||
99 | setlocale(LC_TIME, "C"); | |
100 | openlog($NAME, LOG_PID, LOG_MAIL); | |
101 | syslog(LOG_ERR, "%s", join('', @_)); | |
102 | closelog; | |
103 | setlocale(LC_TIME, $old_lc_time); | |
104 | print STDERR "*** @_\n"; | |
105 | } | |
106 | ||
107 | ||
108 | ## sig_handler($) | |
109 | ## | |
110 | ## Handler for process signal | |
111 | ## | |
112 | sub sig_handler($) { | |
113 | my($sig) = @_; | |
114 | cleanup(); | |
115 | return unless defined $sig; | |
116 | debug("Got a SIG$sig"); | |
117 | if ($pgrp) { | |
118 | debug("Killing spawned processes for session $pgrp"); | |
119 | $SIG{'TERM'} = 'IGNORE'; | |
120 | kill 'TERM', -$pgrp; | |
121 | } | |
122 | die "Die for SIG$sig\n"; | |
123 | } | |
124 | ||
125 | ||
126 | ## daemonize() | |
127 | ## | |
128 | ## Daemonize main process | |
129 | ## | |
130 | sub daemonize() { | |
131 | chdir '/' or die "Can't chdir to /: $!"; | |
132 | open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; | |
133 | open STDOUT, '>/dev/null' | |
134 | or die "Can't write to /dev/null: $!"; | |
135 | defined(my $pid = fork) or die "Can't fork: $!"; | |
136 | if ($pid) { | |
137 | open PIDFILE, ">$opt{pidfile}" or die "Can't open pidfile $opt{pidfile}: $!"; | |
138 | print PIDFILE "$pid\n" or die "Can't write pidfile $opt{pidfile}: $!"; | |
139 | close PIDFILE; | |
140 | exit; | |
141 | } | |
142 | $pgrp = setsid or die "Can't start a new session: $!"; | |
143 | open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; | |
144 | } | |
145 | ||
146 | ||
147 | ## cleanup() | |
148 | ## | |
149 | ## Clean up before die | |
150 | ## | |
151 | sub cleanup() { | |
152 | unlink $opt{'pidfile'} if $opt{'daemon'} and -f $opt{'pidfile'}; | |
153 | } | |
154 | ||
155 | ||
156 | ## usleep($sec); | |
157 | ## | |
158 | ## Sleep $usec seconds (can be factorized) | |
159 | sub usleep($) { | |
160 | my ($sec) = @_; | |
161 | ||
162 | select(undef, undef, undef, $sec); | |
163 | } | |
164 | ||
165 | ||
166 | ## $n62 = base62($n10); | |
167 | ## | |
168 | ## Convert decimal number to b62 string (part of Exim msgid) | |
169 | ## | |
170 | sub base62($) { | |
171 | my ($n10) = @_; | |
172 | ||
173 | my $BASE = 62; | |
174 | my @CHAR = qw(0 1 2 3 4 5 6 7 8 9 | |
175 | A B C D E F G H I J K L M N O P Q R S T U V W X Y Z | |
176 | a b c d e f g h i j k l m n o p q r s t u v w x y z); | |
177 | ||
178 | my $n62 = ""; | |
179 | ||
180 | while ($n10 > 0) { | |
181 | my $d = $n10 % $BASE; | |
182 | $n62 = $CHAR[$d] . $n62; | |
183 | $n10 = int $n10 / $BASE; | |
184 | } | |
185 | ||
186 | return $n62; | |
187 | } | |
188 | ||
189 | ||
190 | ## $seconds = seconds($time); | |
191 | ## | |
192 | ## Converts time with modifiers [smhd] to seconds | |
193 | ## | |
194 | sub seconds($) { | |
195 | my ($time) = @_; | |
196 | ||
197 | return $time if $time eq "inf"; | |
198 | ||
199 | my $seconds = 0; | |
200 | ||
201 | my $modifier = "[smhd]"; | |
202 | while ($time =~ s/^([\d.]+)($modifier)//) { | |
203 | if ($2 eq "s") { $seconds += $1; $modifier = ""; } | |
204 | if ($2 eq "m") { $seconds += $1 * 60; $modifier = "s"; } | |
205 | if ($2 eq "h") { $seconds += $1 * 60 * 60; $modifier = "[sm]"; } | |
206 | if ($2 eq "d") { $seconds += $1 * 60 * 60 * 24; $modifier = "[smh]"; } | |
207 | } | |
208 | ||
209 | return undef if $time ne ""; | |
210 | ||
211 | return $seconds; | |
212 | } | |
213 | ||
214 | ||
215 | ## eximq_die(@msg) | |
216 | ## | |
217 | ## Die with message | |
218 | ## | |
219 | sub eximq_die(@) { | |
220 | my (@msg) = @_; | |
221 | ||
222 | $SIG{'__DIE__'} = 'DEFAULT'; | |
223 | debug(@msg); | |
224 | die(@msg, "\n"); | |
225 | } | |
226 | ||
227 | ||
228 | ## eximq_exec(@cmd) | |
229 | ## | |
230 | ## Execute command | |
231 | ## | |
232 | sub eximq_exec(@) { | |
233 | my (@cmd) = @_; | |
234 | ||
235 | debug("exec " . join(' ', @cmd)); | |
236 | exec(@cmd); | |
237 | ||
238 | die "Cannot exec: $!"; | |
239 | } | |
240 | ||
241 | ||
242 | ## eximq_spawn($msgid) | |
243 | ## | |
244 | ## Spawn neq eximq process | |
245 | ## | |
246 | sub eximq_spawn(;$$) { | |
247 | my ($msgid_min, $msgid_max) = @_; | |
248 | ||
249 | my $pid; | |
250 | if (!defined($pid = fork)) { | |
251 | error("Cannot fork: $!"); | |
252 | } elsif ($pid) { | |
253 | debug("spawn $pid"); | |
254 | $running++; | |
255 | } else { | |
256 | if (defined $msgid_max) { | |
257 | eximq_exec(@eximq_cmd, "$msgid_min-000000-00", "$msgid_max-zzzzzz-zz"); | |
258 | } elsif (defined $msgid_min) { | |
259 | eximq_exec(@eximq_cmd, "$msgid_min-000000-00"); | |
260 | } else { | |
261 | eximq_exec(@eximq_cmd); | |
262 | } | |
263 | } | |
264 | } | |
265 | ||
266 | ||
267 | ## eximq_reaper() | |
268 | ## | |
269 | ## Harvest spawned eximq processes | |
270 | ## | |
271 | sub eximq_reaper() { | |
272 | ||
273 | while ((my $pid = waitpid(-1,WNOHANG)) > 0) { | |
274 | debug("reap $pid"); | |
275 | $running--; | |
276 | } | |
277 | $SIG{CHLD} = \&eximq_reaper; | |
278 | } | |
279 | ||
280 | ||
281 | ## eximq($agemin, $agemax, $interval, $processmax); | |
282 | ## | |
283 | ## Start Exim queue processing | |
284 | ## | |
285 | sub eximq($$$$) { | |
286 | my ($agemin, $agemax, $interval, $processmax) = @_; | |
287 | my $time_last = time; | |
288 | ||
289 | $SIG{'__DIE__'} = \&eximq_die; | |
290 | $SIG{$_} = 'IGNORE' foreach (qw(HUP PIPE USR1 USR2)); | |
291 | $SIG{$_} = \&sig_handler foreach (qw(INT QUIT TERM)); | |
292 | $SIG{'CHLD'} = \&eximq_reaper; | |
293 | ||
294 | for (;;) { | |
295 | if ($time_last + $interval < time && $running < $processmax) { | |
296 | $time_last = time; | |
297 | if ($agemax > 0 || $agemax eq "inf") { | |
298 | my $msgid_min = "000000"; | |
299 | if ($agemax > 0) { | |
300 | $msgid_min .= base62($time_last - $agemax); | |
301 | $msgid_min =~ s/^.*(.{6})$/$1/; | |
302 | } | |
303 | if ($agemin > 0 || $agemin eq "inf") { | |
304 | my $msgid_max = "000000"; | |
305 | if ($agemin > 0) { | |
306 | $msgid_max .= base62($time_last - $agemin); | |
307 | $msgid_max =~ s/^.*(.{6})$/$1/; | |
308 | } | |
309 | eximq_spawn($msgid_min, $msgid_max); | |
310 | } else { | |
311 | eximq_spawn($msgid_min); | |
312 | } | |
313 | } else { | |
314 | eximq_spawn(); | |
315 | } | |
316 | } | |
317 | sleep(1); | |
318 | } | |
319 | } | |
320 | ||
321 | ||
322 | ## main() | |
323 | ## | |
324 | ## Main subroutine | |
325 | ## | |
326 | sub main() { | |
327 | ||
328 | ## get options | |
329 | my $opt = GetOptions(\%opt, | |
330 | 'help|h|?', | |
331 | 'debug-stderr', | |
332 | 'debug-syslog', | |
333 | 'daemon|d', | |
334 | 'pidfile|p=s', | |
335 | ); | |
336 | ||
337 | pod2usage(2) unless $opt; | |
338 | ||
339 | pod2usage(-verbose=>1, -message=>"$NAME $VERSION\n") if $opt{'help'}; | |
340 | ||
341 | pod2usage(2) if $#ARGV < 3; | |
342 | ||
343 | ## set the process name | |
344 | $0 = join(' ', $0, @ARGV); | |
345 | ||
346 | my ($agemin, $agemax, $interval, $processmax); | |
347 | defined ($agemin = seconds(shift @ARGV)) or die "Bad format for agemin argument\n"; | |
348 | defined ($agemax = seconds(shift @ARGV)) or die "Bad format for agemax argument\n"; | |
349 | ($agemin <= $agemax || $agemax eq "inf") or die "agemin argument cannot be greater than agemax argument\n"; | |
350 | defined ($interval = seconds(shift @ARGV)) or die "Bad format for interval argument\n"; | |
351 | ($processmax = shift @ARGV) =~ /^\d+$/ or die "Bad format for max argument\n"; | |
352 | ||
353 | if (@ARGV) { | |
354 | @eximq_cmd = @ARGV; | |
355 | } | |
356 | ||
357 | daemonize() if $opt{'daemon'}; | |
358 | eximq($agemin, $agemax, $interval, $processmax); | |
359 | } | |
360 | ||
361 | ||
362 | END: { | |
363 | cleanup(); | |
364 | } | |
365 | ||
366 | ||
367 | main(); | |
368 | ||
369 | ||
370 | __END__ | |
371 | ||
372 | =head1 DESCRIPTION | |
373 | ||
374 | The B<eximq> controlls the count of queue runner processes based on | |
375 | messages age. This allows to keep low system load and maximum number of | |
376 | queue runner processes. | |
377 | ||
378 | The B<eximq> can be started as daemon or foregound process. The example | |
379 | init script is available as separate file and it requires F<eximq.args> | |
380 | file which contains arguments for each B<eximq> instances. | |
381 | ||
382 | The example F<eximq.args> file: | |
383 | ||
384 | 0s 1m 5s 10 | |
385 | 1m 2m 15s 10 | |
386 | 2m 5m 30s 10 | |
387 | 5m 15m 1m 5 /usr/sbin/exim -qq | |
388 | 15m 2h 2m 5 /usr/sbin/exim -qq | |
389 | 2h inf 5m 5 /usr/sbin/exim -qq | |
390 | ||
391 | Also, the B<eximq> can be started from F</etc/inittab> file. I.e.: | |
392 | ||
393 | # Exim queue | |
394 | ex01:23:respawn:+/usr/sbin/eximq 0s 1m 5s 10 | |
395 | ex02:23:respawn:+/usr/sbin/eximq 1m 2m 15s 10 | |
396 | ex03:23:respawn:+/usr/sbin/eximq 2m 5m 30s 10 | |
397 | ex04:23:respawn:+/usr/sbin/eximq 5m 15m 1m 5 /usr/sbin/exim -qq | |
398 | ex05:23:respawn:+/usr/sbin/eximq 15m 2h 2m 5 /usr/sbin/exim -qq | |
399 | ex06:23:respawn:+/usr/sbin/eximq 2h inf 5m 5 /usr/sbin/exim -qq | |
400 | ||
401 | In this example the B<eximq> is started three times with different message | |
402 | ages. The first process spawn max 10 queue runners each 5th second for | |
403 | messages not older than 1 minute. The second process spawn max 10 runners | |
404 | each minute for messages not older than 5 minutes and nor newer than 1 minute. | |
405 | The last queue runners work for messages older than 2 hours and are spawned | |
406 | each 5th minute. | |
407 | ||
408 | The queue runner is spawned as B</usr/sbin/exim -q> command as default. | |
409 | Different command can be specified as last argument. | |
410 | ||
411 | For best result you should put | |
412 | ||
413 | queue_only = yes | |
414 | queue_smtp_domains = * | |
415 | ||
416 | in your F</etc/exim/exim.conf> configuration file. | |
417 | ||
418 | =head1 OPTIONS | |
419 | ||
420 | =over 8 | |
421 | ||
422 | =item I<minage> | |
423 | ||
424 | Minimal age of message. The queue runner will be process messages not newer | |
425 | than I<minage>. B<0s> means no limits. | |
426 | ||
427 | =item I<maxage> | |
428 | ||
429 | Maximal age of message. The queue runner will be process messages not older | |
430 | than I<maxage>. B<inf> means no limits. I<maxage> can not be lower than | |
431 | I<minage>. | |
432 | ||
433 | =item I<interval> | |
434 | ||
435 | Interval time between spawning another queue runner. The new runner will be | |
436 | not spawned if the I<interval> time was not reached. | |
437 | ||
438 | =item I<processmax> | |
439 | ||
440 | Maximal number of spawned processes. The new runner will be not spawned if | |
441 | whole number of running processes will be greater than I<processmax>. | |
442 | ||
443 | =item I<exim_q_command> | |
444 | ||
445 | The queue runner command. The default is B</usr/sbin/exim -q>. | |
446 | ||
447 | =item B<--debug-stderr> | |
448 | ||
449 | Turn on debug mode on stderr. | |
450 | ||
451 | =item B<--debug-syslog> | |
452 | ||
453 | Turn on debug mode on syslog (MAIL|INFO). | |
454 | ||
455 | =item B<--daemon> | |
456 | ||
457 | Runs the B<eximq> as daemon. In this mode it creates pidfile and | |
458 | detaches from terminal. | |
459 | ||
460 | =item B<--pidfile> I<path> | |
461 | ||
462 | The I<path> for pidfile which is created if B<eximq> is started in | |
463 | daemon mode. The default I<path> is F</var/run/eximq.pid>. The file is | |
464 | removed after daemon dies. | |
465 | ||
466 | =item B<-h>|B<--help> | |
467 | ||
468 | This help. | |
469 | ||
470 | =back | |
471 | ||
472 | =head1 SIGNALS | |
473 | ||
474 | The B<eximq> ignores B<HUP>, B<PIPE>, B<USR1> and B<USR2> and dies for B<INT>, | |
475 | B<QUIT> and B<TERM>. It kills all spawned processes with B<TERM> after die | |
476 | signal. | |
477 | ||
478 | =head1 SEE ALSO | |
479 | ||
480 | B<exim>(8) | |
481 | ||
482 | =head1 AUTHOR | |
483 | ||
484 | (c) 2003-2004 Piotr Roszatycki E<lt>dexter@debian.orgE<gt> | |
485 | ||
486 | All rights reserved. This program is free software; you can redistribute it | |
487 | and/or modify it under the terms of the GNU General Public License, the | |
488 | latest version. |