File Coverage

blib/lib/Any/Daemon.pm
Criterion Covered Total %
statement 24 180 13.3
branch 0 90 0.0
condition 0 25 0.0
subroutine 8 28 28.5
pod 4 8 50.0
total 36 331 10.8


line stmt bran cond sub pod time code
1             # Copyrights 2011-2018 by [Mark Overmeer].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             # This code is part of distribution Any-Daemon. Meta-POD processed with
6             # OODoc into POD and HTML manual-pages. See README.md
7             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
8              
9             package Any::Daemon;
10 1     1   722 use vars '$VERSION';
  1         2  
  1         56  
11             $VERSION = '0.96';
12              
13              
14 1     1   21 use warnings;
  1         2  
  1         28  
15 1     1   5 use strict;
  1         2  
  1         21  
16              
17 1     1   433 use Log::Report::Optional 'any-daemon';
  1         82539  
  1         7  
18              
19 1     1   285 use POSIX qw(setsid setuid setgid :sys_wait_h);
  1         2  
  1         9  
20 1     1   730 use English qw/$EUID $EGID $PID/;
  1         1590  
  1         6  
21 1     1   251 use File::Spec ();
  1         2  
  1         25  
22              
23             use constant
24 1         2085 { SLEEP_FOR_SOME_TIME => 10
25             , ERROR_RECOVERY_SLEEP => 5
26             , SLOW_WARN_AGAIN_AFTER => 300
27 1     1   6 };
  1         1  
28              
29             # One program can only run one daemon
30             my %childs;
31              
32              
33 0     0 1   sub new(@) {my $class = shift; (bless {}, $class)->init({@_})}
  0            
34              
35             sub init($)
36 0     0 0   { my ($self, $args) = @_;
37 0           $self->{AD_pidfn} = $args->{pid_file};
38              
39 0           my $user = $args->{user};
40 0 0         if(defined $user)
    0          
41 0 0         { if($user =~ m/[^0-9]/)
42 0           { my $uid = $self->{AD_uid} = getpwnam $user;
43 0 0         defined $uid
44             or error __x"user {name} does not exist", name => $user;
45             }
46 0           else { $self->{AD_uid} = $user }
47             }
48             elsif($EUID==0)
49 0           { warning __"running daemon as root is dangerous: please specify user";
50             }
51              
52 0           my $group = $args->{group};
53 0 0         if(defined $group)
54 0 0         { if($group =~ m/[^0-9]/)
55 0           { my $gid = $self->{AD_gid} = getgrnam $group;
56 0 0         defined $gid
57             or error __x"group {name} does not exist", name => $group;
58             }
59             }
60              
61 0           $self->{AD_wd} = $args->{workdir};
62 0           $self;
63             }
64              
65             #--------------------
66              
67 0     0 1   sub workdir() {shift->{AD_wd}}
68 0     0 1   sub pidFilename() { shift->{AD_pidfn} }
69              
70             #--------------------
71              
72             sub _mkcall($)
73 0 0   0     { return $_[1] if ref $_[1] eq 'CODE';
74 0           my ($self, $what) = @_;
75 0     0     sub { $self->$what(@_) };
  0            
76             }
77              
78             sub run(@)
79 0     0 1   { my ($self, %args) = @_;
80              
81 0           my $wd = $self->workdir;
82 0 0         if($wd)
83 0 0 0       { -d $wd or mkdir $wd, 0700
84             or fault __x"cannot create working directory {dir}", dir => $wd;
85              
86 0 0         chdir $wd
87             or fault __x"cannot change to working directory {dir}", dir => $wd;
88             }
89              
90 0 0         my $bg = exists $args{background} ? $args{background} : 1;
91 0 0         if($bg)
92 0           { trace "backgrounding managing daemon";
93              
94 0           my $kid = fork;
95 0 0         if($kid)
    0          
96             { # starting parent is ready to leave
97 0           exit 0;
98             }
99             elsif(!defined $kid)
100 0           { fault __x"cannot start the managing daemon";
101             }
102              
103 0 0         dispatcher('list') >= 1
104             or error __x"you need to have a dispatcher to send log to";
105             }
106              
107 0           my $pidfn = $self->pidFilename;
108 0 0         if(defined $pidfn)
109 0           { local *PIDF;
110 0 0         if(open PIDF, '>', $pidfn)
111 0           { print PIDF "$PID\n";
112 0           close PIDF;
113             }
114             }
115              
116 0   0       my $gid = $self->{AD_gid} || $EGID;
117 0   0       my $uid = $self->{AD_uid} || $EUID;
118              
119 0 0         chown $uid,$gid, $wd if $wd; # don't check success: user may have plan
120              
121 0 0         if($gid != $EGID)
122 0 0         { if($] > 5.015007)
123 0 0         { setgid $gid or fault __x"cannot change to group {gid}", gid => $gid;
124             }
125             else # in old versions of Perl, the uid and gid gets cached
126 0           { eval { $EGID = $gid };
  0            
127 0 0         $@ and error __x"cannot switch to group {gid}: {err}"
128             , gid => $gid, err => $@;
129             }
130             }
131              
132 0 0         if($uid != $EUID)
133 0 0         { if($] > 5.015007)
134 0 0         { setuid $uid or fault __x"cannot change to user {uid}", uid => $uid;
135             }
136             else
137 0           { eval { $EUID = $uid };
  0            
138 0 0         $@ and error __x"cannot switch to user {uid}: {err}"
139             , uid => $uid, err => $@;
140             }
141             }
142              
143 0           setsid;
144              
145 0           my $child_task = $self->_mkcall($args{child_task});
146 0           my $own_task = $self->_mkcall($args{run_task});
147              
148 0 0 0       $child_task || $own_task
149             or panic __x"you have to run with either child_task or run_task";
150              
151 0 0 0       $child_task && $own_task
152             or panic __x"run with only one of child_task and run_task";
153              
154 0 0         if($bg)
155             { # no standard die and warn output anymore (Log::Report)
156 0           dispatcher close => 'default';
157              
158             # to devnull to avoid write errors in third party modules
159 0           open STDIN, '<', File::Spec->devnull;
160 0           open STDOUT, '>', File::Spec->devnull;
161 0           open STDERR, '>', File::Spec->devnull;
162             }
163              
164 0 0         if($child_task)
165 0           { $self->_run_with_childs($child_task, %args) }
166 0           else { $self->_run_without_childs($own_task, %args) }
167             }
168              
169             sub _run_with_childs($%) {
170 0     0     my ($self, $child_task, %args) = @_;
171 0   0       my $reconfig = $self->_mkcall($args{reconfig} || 'reconfigDaemon');
172 0   0       my $kill_childs = $self->_mkcall($args{kill_childs} || 'killChilds');
173 0   0       my $child_died = $self->_mkcall($args{child_died} || 'childDied');
174 0   0       my $max_childs = $args{max_childs} || 10;
175              
176             my $run_child = sub
177             { # re-seed the random number sequence per process
178 0     0     srand(time+$$);
179              
180             # unhandled errors are to be treated seriously.
181 0           my $rc = try { $child_task->(@_) };
  0            
182 0 0         if(my $e = $@->wasFatal) { $e->throw(reason => 'ALERT'); $rc = 1 }
  0            
  0            
183 0           $rc;
184 0           };
185              
186 0     0     $SIG{CHLD} = sub { $child_died->($max_childs, $run_child) };
  0            
187             $SIG{HUP} = sub
188 0     0     { notice "daemon received signal HUP";
189 0           $reconfig->(keys %childs);
190 0           $child_died->($max_childs, $run_child);
191 0           };
192              
193             $SIG{TERM} = $SIG{INT} = sub
194 0     0     { my $signal = shift;
195 0           notice "daemon terminated by signal $signal";
196              
197 0           $SIG{TERM} = $SIG{CHLD} = 'IGNORE';
198 0           $max_childs = 0;
199 0           $kill_childs->(keys %childs);
200 0           sleep 2; # give childs some time to stop
201 0           kill TERM => 0; # terminate the whole process group
202              
203 0           my $pidfn = $self->pidFilename;
204 0 0         unlink $pidfn if $pidfn;
205              
206 0 0         my $intrnr = $signal eq 'INT' ? 2 : 9;
207 0           exit $intrnr+128;
208 0           };
209              
210 0           notice __x"daemon started; proc={proc} uid={uid} gid={gid} childs={max}"
211             , proc => $PID, uid => $EUID, gid => $EGID, max => $max_childs;
212              
213 0           $child_died->($max_childs, $run_child);
214              
215             # child manager will never die
216 0           sleep 60 while 1;
217             }
218              
219             sub _run_without_childs($%) {
220 0     0     my ($self, $run_task, %args) = @_;
221 0   0       my $reconfig = $self->_mkcall($args{reconfig} || 'reconfigDaemon');
222              
223             # unhandled errors are to be treated seriously.
224 0     0     my $rc = try { $run_task->(@_) };
  0            
225 0 0         if(my $e = $@->wasFatal) { $e->throw(reason => 'ALERT'); $rc = 1 }
  0            
  0            
226              
227             $SIG{HUP} = sub
228 0     0     { notice "daemon received signal HUP";
229 0           $reconfig->(keys %childs);
230 0           };
231              
232             $SIG{TERM} = $SIG{INT} = sub
233 0     0     { my $signal = shift;
234 0           notice "daemon terminated by signal $signal";
235              
236 0           my $pidfn = $self->pidFilename;
237 0 0         unlink $pidfn if $pidfn;
238              
239 0 0         my $intrnr = $signal eq 'INT' ? 2 : 9;
240 0           exit $intrnr+128;
241 0           };
242              
243 0           notice __x"daemon started; proc={proc} uid={uid} gid={gid}"
244             , proc => $PID, uid => $EUID, gid => $EGID;
245              
246 0           $run_task->();
247             }
248              
249             sub reconfigDaemon(@)
250 0     0 0   { my ($self, @childs) = @_;
251 0           notice "HUP: reconfigure deamon not implemented";
252             }
253              
254             sub killChilds(@)
255 0     0 0   { my ($self, @childs) = @_;
256 0 0         @childs or return;
257              
258 0           notice "killing ".@childs." children";
259 0           kill TERM => @childs;
260             }
261              
262             # standard implementation for starting new childs.
263             sub childDied($$)
264 0     0 0   { my ($self, $max_childs, $run_child) = @_;
265              
266             # Clean-up zombies
267              
268             ZOMBIE:
269 0           while(1)
270 0           { my $kid = waitpid -1, WNOHANG;
271 0 0         last ZOMBIE if $kid <= 0;
272              
273 0 0         if($? != 0)
274 0 0         { my $err = WIFEXITED($?) ? "errno ".WEXITSTATUS($?) : "sig $?";
275 0           notice "$kid process terminated with $err";
276             # when children start to die, do not respawn too fast,
277             # because usually this means serious troubles with the
278             # server (like database) or implementation.
279 0           sleep ERROR_RECOVERY_SLEEP;
280             }
281              
282 0           delete $childs{$kid};
283             }
284              
285             # Start enough childs
286 0           my $silence_warn = 0;
287              
288             BIRTH:
289 0           while(keys %childs < $max_childs)
290 0           { my $kid = fork;
291 0 0         unless(defined $kid)
292 0 0         { alert "cannot fork new children" unless $silence_warn++;
293 0           sleep 1; # wow, back down! Probably too busy.
294 0 0         $silence_warn = 0 if $silence_warn==SLOW_WARN_AGAIN_AFTER;
295 0           next BIRTH;
296             }
297              
298 0 0         if($kid==0)
299             { # new child
300             $SIG{HUP} = $SIG{TERM} = $SIG{INT}
301 0     0     = sub {info 'child says bye'; exit 0};
  0            
  0            
302              
303             # I'll not handle my parent's kids!
304 0           $SIG{CHLD} = 'IGNORE';
305 0           %childs = ();
306              
307 0           my $rc = $run_child->();
308 0           exit $rc;
309             }
310              
311             # parent
312 0           $childs{$kid}++;
313             }
314             }
315              
316             1;