File Coverage

blib/lib/Any/Daemon.pm
Criterion Covered Total %
statement 21 140 15.0
branch 0 62 0.0
condition 0 22 0.0
subroutine 7 20 35.0
pod 3 4 75.0
total 31 248 12.5


line stmt bran cond sub pod time code
1             # Copyrights 2011-2014 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.01.
5 1     1   790 use warnings;
  1         2  
  1         39  
6 1     1   5 use strict;
  1         3  
  1         63  
7              
8             package Any::Daemon;
9             our $VERSION = '0.94';
10              
11              
12 1     1   962 use Log::Report::Optional 'any-daemon';
  1         73186  
  1         7  
13              
14 1     1   1125 use POSIX qw(setsid setuid setgid :sys_wait_h);
  1         14455  
  1         8  
15 1     1   2424 use English qw/$EUID $EGID $PID/;
  1         2295  
  1         7  
16 1     1   267 use File::Spec ();
  1         2  
  1         27  
17              
18             use constant
19 1         1747 { SLEEP_FOR_SOME_TIME => 10
20             , ERROR_RECOVERY_SLEEP => 5
21             , SLOW_WARN_AGAIN_AFTER => 300
22 1     1   5 };
  1         2  
23              
24             # One program can only run one daemon
25             my %childs;
26              
27              
28 0     0 1   sub new(@) {my $class = shift; (bless {}, $class)->init({@_})}
  0            
29              
30             sub init($)
31 0     0 0   { my ($self, $args) = @_;
32              
33 0           $self->{AD_pidfn} = $args->{pid_file};
34              
35 0           my $user = $args->{user};
36 0 0         if(defined $user)
37 0 0         { if($user =~ m/[^0-9]/)
38 0           { my $uid = $self->{AD_uid} = getpwnam $user;
39 0 0         defined $uid
40             or error __x"user {name} does not exist", name => $user;
41             }
42 0           else { $self->{AD_uid} = $user }
43             }
44              
45 0           my $group = $args->{group};
46 0 0         if(defined $group)
47 0 0         { if($group =~ m/[^0-9]/)
48 0           { my $gid = $self->{AD_gid} = getgrnam $group;
49 0 0         defined $gid
50             or error __x"group {name} does not exist", name => $group;
51             }
52             }
53              
54 0           $self->{AD_wd} = $args->{workdir};
55 0           $self;
56             }
57              
58             #--------------------
59              
60 0     0 1   sub workdir() {shift->{AD_wd}}
61              
62             #--------------------
63              
64             sub run(@)
65 0     0 1   { my ($self, %args) = @_;
66              
67 0           my $wd = $self->workdir;
68 0 0         if($wd)
69 0 0 0       { -d $wd or mkdir $wd, 0700
70             or fault __x"cannot create working directory {dir}", dir => $wd;
71              
72 0 0         chdir $wd
73             or fault __x"cannot change to working directory {dir}", dir => $wd;
74             }
75              
76 0 0         my $bg = exists $args{background} ? $args{background} : 1;
77 0 0         if($bg)
78 0           { my $kid = fork;
79 0 0         if($kid)
    0          
80             { # starting parent is ready to leave
81 0           exit 0;
82             }
83             elsif(!defined $kid)
84 0           { fault __x"cannot start the managing daemon";
85             }
86              
87 0 0         dispatcher('list') >= 1
88             or error __x"you need to have a dispatcher to send log to";
89             }
90              
91 0           my $pidfn = $self->{AD_pidfn};
92 0 0         if(defined $pidfn)
93 0           { local *PIDF;
94 0 0         if(open PIDF, '>', $pidfn)
95 0           { print PIDF "$PID\n";
96 0           close PIDF;
97             }
98             }
99              
100 0   0       my $gid = $self->{AD_gid} || $EGID;
101 0   0       my $uid = $self->{AD_uid} || $EUID;
102 0 0 0       if($gid!=$EGID && $uid!=$EUID)
    0          
103 0 0         { chown $uid,$gid, $wd if $wd;
104              
105 0 0         eval { if($] > 5.015007) { setgid $gid; setuid $uid }
  0            
  0            
  0            
106             else
107             { # in old versions of Perl, the uid and gid gets cached
108 0           $EGID = $gid;
109 0           $EUID = $uid;
110             }
111             };
112              
113 0 0         $@ and error __x"cannot switch to user/group to {uid}/{gid}: {err}"
114             , uid => $uid, gid => $gid, err => $@;
115             }
116             elsif($EUID==0)
117 0           { warning __"running daemon as root is dangerous: please specify user";
118             }
119              
120 0           my $sid = setsid;
121              
122 0   0       my $reconfig = $args{reconfig} || \&_reconfig_daemon;
123 0   0       my $kill_childs = $args{kill_childs} || \&_kill_childs;
124 0   0       my $child_died = $args{child_died} || \&_child_died;
125 0   0       my $max_childs = $args{max_childs} || 10;
126 0   0       my $child_task = $args{child_task} || \&_child_task;
127              
128             my $run_child = sub
129             { # re-seed the random number sequence per process
130 0     0     srand;
131              
132             # unhandled errors are to be treated seriously.
133 0           my $rc = try { $child_task->(@_) };
  0            
134 0 0         if(my $e = $@->wasFatal) { $e->throw(reason => 'ALERT'); $rc = 1 }
  0            
  0            
135 0           $rc;
136 0           };
137              
138 0     0     $SIG{CHLD} = sub { $child_died->($max_childs, $run_child) };
  0            
139             $SIG{HUP} = sub
140 0     0     { notice "daemon received signal HUP";
141 0           $reconfig->(keys %childs);
142 0           $child_died->($max_childs, $run_child);
143 0           };
144              
145             $SIG{TERM} = $SIG{INT} = sub
146 0     0     { my $signal = shift;
147 0           notice "daemon terminated by signal $signal";
148              
149 0           $SIG{TERM} = $SIG{CHLD} = 'IGNORE';
150 0           $max_childs = 0;
151 0           $kill_childs->(keys %childs);
152 0           sleep 2; # give childs some time to stop
153 0           kill TERM => -$sid;
154 0 0         unlink $pidfn if $pidfn;
155 0 0         my $intrnr = $signal eq 'INT' ? 2 : 9;
156 0           exit $intrnr+128;
157 0           };
158              
159 0 0         if($bg)
160             { # no standard die and warn output anymore (Log::Report)
161 0           dispatcher close => 'default';
162              
163             # to devnull to avoid write errors in third party modules
164 0           open STDIN, '<', File::Spec->devnull;
165 0           open STDOUT, '>', File::Spec->devnull;
166 0           open STDERR, '>', File::Spec->devnull;
167             }
168              
169 0           info __x"daemon started; proc={proc} uid={uid} gid={gid} childs={max}"
170             , proc => $PID, uid => $EUID, gid => $EGID, max => $max_childs;
171              
172 0           $child_died->($max_childs, $run_child);
173              
174             # child manager will never die
175 0           sleep 60 while 1;
176             }
177              
178             sub _reconfing_daemon(@)
179 0     0     { my @childs = @_;
180 0           notice "HUP: reconfigure deamon not implemented";
181             }
182              
183             sub _child_task()
184 0     0     { notice "No child_task implemented yet. I'll sleep for some time";
185 0           sleep SLEEP_FOR_SOME_TIME;
186             }
187              
188             sub _kill_childs(@)
189 0     0     { my @childs = @_;
190 0           notice "killing ".@childs." children";
191 0           kill TERM => @childs;
192             }
193              
194             # standard implementation for starting new childs.
195             sub _child_died($$)
196 0     0     { my ($max_childs, $run_child) = @_;
197              
198             # Clean-up zombies
199              
200             ZOMBIE:
201 0           while(1)
202 0           { my $kid = waitpid -1, WNOHANG;
203 0 0         last ZOMBIE if $kid <= 0;
204              
205 0 0         if($? != 0)
206 0           { notice "$kid process died with errno $?";
207             # when children start to die, do not respawn too fast,
208             # because usually this means serious troubles with the
209             # server (like database) or implementation.
210 0           sleep ERROR_RECOVERY_SLEEP;
211             }
212              
213 0           delete $childs{$kid};
214             }
215              
216             # Start enough childs
217 0           my $silence_warn = 0;
218              
219             BIRTH:
220 0           while(keys %childs < $max_childs)
221 0           { my $kid = fork;
222 0 0         unless(defined $kid)
223 0 0         { alert "cannot fork new children" unless $silence_warn++;
224 0           sleep 1; # wow, back down! Probably too busy.
225 0 0         $silence_warn = 0 if $silence_warn==SLOW_WARN_AGAIN_AFTER;
226 0           next BIRTH;
227             }
228              
229 0 0         if($kid==0)
230             { # new child
231 0     0     $SIG{HUP} = $SIG{TERM} = $SIG{INT}
232 0           = sub {info 'child says bye'; exit 0};
  0            
233              
234             # I'll not handle my parent's kids!
235 0           $SIG{CHLD} = 'IGNORE';
236 0           %childs = ();
237              
238 0           my $rc = $run_child->();
239 0           exit $rc;
240             }
241              
242             # parent
243 0           $childs{$kid}++;
244             }
245             }
246              
247             1;