File Coverage

blib/lib/POE/Wheel/Run/DaemonHelper.pm
Criterion Covered Total %
statement 64 186 34.4
branch 14 60 23.3
condition 5 15 33.3
subroutine 12 23 52.1
pod 10 15 66.6
total 105 299 35.1


line stmt bran cond sub pod time code
1             package POE::Wheel::Run::DaemonHelper;
2              
3 2     2   389482 use 5.006;
  2         10  
4 2     2   12 use strict;
  2         3  
  2         71  
5 2     2   14 use warnings;
  2         3  
  2         161  
6 2     2   972 use POE qw( Wheel::Run );
  2         58893  
  2         19  
7 2     2   145830 use base 'Error::Helper';
  2         4  
  2         1461  
8 2     2   6202 use Algorithm::Backoff::Exponential;
  2         7737  
  2         79  
9 2     2   1396 use Sys::Syslog;
  2         23452  
  2         193  
10 2     2   1526 use File::Slurp qw(append_file read_file);
  2         43966  
  2         5388  
11              
12             =head1 NAME
13              
14             POE::Wheel::Run::DaemonHelper - Helper for the POE::Wheel::Run for easy controlling logging of stdout/err as well as restarting with backoff.
15              
16             =head1 VERSION
17              
18             Version 0.1.0
19              
20             =cut
21              
22             our $VERSION = '0.1.0';
23              
24             =head1 SYNOPSIS
25              
26             use strict;
27             use warnings;
28             use POE::Wheel::Run::DaemonHelper;
29             use POE;
30              
31             my $program = 'sleep 1; echo test; derp derp derp';
32              
33             my $dh = POE::Wheel::Run::DaemonHelper->new(
34             program => $program,
35             status_syslog => 1,
36             restart_ctl => 1,
37             status_print => 1,
38             status_print_warn => 1,
39             # this one will be ignored as it will already be warning for print
40             status_syslog_warn => 1,
41             );
42              
43             $dh->create_session;
44              
45             POE::Kernel->run();
46              
47             =head1 METHODS
48              
49             =head2 new
50              
51             Required args as below.
52              
53             - program :: The program to execute. Either a string or array.
54             Default :: undef
55              
56             - restart_ctl :: Control if it will be restarted if it dies.
57             Default :: 1
58              
59             Optional args are as below.
60              
61             - syslog_name :: The name to use when sending stuff to syslog.
62             Default :: DaemonHelper
63              
64             - pid_file :: The file to check for additional PIDs. Used for for
65             with the $dh->pids and $dh->pid_from_pid_file.
66             Default :: undef
67              
68             - default_kill_signal :: The default signal to use for kill.
69             Default :: TERM
70              
71             The following optional args control the backoff. Backoff is handled by
72             L with consider_actual_delay and delay_on_success
73             set to true. The following are passed to it.
74              
75             - max_delay :: Max backoff delay in seconds when a program exits quickly.
76             Default :: 90
77              
78             - initial_delay :: Initial backoff amount.
79             Default :: 2
80              
81             The following optional args control the how the log_message method behaves.
82              
83             - syslog_facility :: The syslog facility to log to.
84             Default :: daemon
85              
86             - stdout_prepend :: What to prepend to STDOUT lines sent for status logging.
87             Default :: Out:
88              
89             - stderr_prepend :: What to prepend to STDERR lines sent to status logging.
90             Default :: Err:
91              
92             - status_print :: Print statuses messages to stdout.
93             Default :: 0
94              
95             - status_print_warn :: For when error is true, use warn.
96             Default :: 0
97              
98             - status_syslog :: Send status messages to syslog
99             Default :: 1
100              
101             - status_syslog_warn :: Warn for error messages going to syslog. Warn will only be used once.
102             Default :: 0
103              
104             =cut
105              
106             sub new {
107 1     1 1 261430 my ( $blank, %opts ) = @_;
108              
109 1         42 my $self = {
110             perror => undef,
111             error => undef,
112             errorLine => undef,
113             errorFilename => undef,
114             errorString => "",
115             errorExtra => {
116             all_errors_fatal => 1,
117             flags => {
118             1 => 'invalidProgram',
119             2 => 'optsBadRef',
120             3 => 'optsNotInt',
121             4 => 'readPidFileFailed',
122             5 => 'killFailed',
123             },
124             fatal_flags => {},
125             perror_not_fatal => 0,
126             },
127             args => {
128             ints => {
129             'max_delay' => 1,
130             'initial_delay' => 1,
131             },
132             args => [
133             'syslog_name', 'syslog_facility', 'stdout_prepend', 'stderr_prepend',
134             'max_delay', 'initial_delay', 'status_syslog', 'status_print',
135             'status_print_warn', 'status_syslog_warn', 'restart_ctl', 'pid_file',
136             'default_kill_signal',
137             ],
138             },
139             program => undef,
140             syslog_name => 'DaemonHelper',
141             syslog_facility => 'daemon',
142             stdout_prepend => 'Out: ',
143             stderr_prepend => 'Err: ',
144             max_delay => 90,
145             initial_delay => 2,
146             session_created => 0,
147             started => undef,
148             started_at => undef,
149             restart_ctl => 1,
150             backoff => undef,
151             pid => undef,
152             status_syslog => 1,
153             status_syslog_warn => 0,
154             status_print => 0,
155             status_print_warn => 0,
156             append_pid => 0,
157             pid_prepend => 1,
158             pid_file => undef,
159             default_kill_signal => 'TERM',
160             };
161 1         4 bless $self;
162              
163 1 50 33     13 if ( !defined( $opts{program} ) ) {
    50          
164 0         0 $self->{perror} = 1;
165 0         0 $self->{error} = 1;
166 0         0 $self->{errorString} = 'program is defined';
167 0         0 $self->warn;
168 0         0 return;
169             } elsif ( ref( $opts{program} ) ne '' && ref( $opts{program} ) ne 'ARRAY' ) {
170 0         0 $self->{perror} = 1;
171 0         0 $self->{error} = 1;
172 0         0 $self->{errorString} = 'ref for program is ' . ref( $opts{program} ) . ', but should be either "" or ARRAY';
173 0         0 $self->warn;
174 0         0 return;
175             }
176 1         14 $self->{program} = $opts{program};
177              
178 1         3 foreach my $arg ( @{ $self->{args}{args} } ) {
  1         4  
179 13 100       92 if ( defined( $opts{$arg} ) ) {
180 11 50       28 if ( ref( $opts{$arg} ) ne '' ) {
181 0         0 $self->{perror} = 1;
182 0         0 $self->{error} = 2;
183 0         0 $self->{errorString} = 'ref for ' . $arg . ' is ' . ref( $opts{$arg} ) . ', but should be ""';
184 0         0 $self->warn;
185 0         0 return;
186             }
187              
188 11 50 66     45 if ( $self->{args}{ints}{$arg} && $opts{$arg} !~ /^[0-9]+$/ ) {
189 0         0 $self->{perror} = 1;
190 0         0 $self->{error} = 3;
191 0         0 $self->{errorString} = $arg . ' is "' . $opts{$arg} . '" and does not match /^[0-9]+$/';
192 0         0 $self->warn;
193 0         0 return;
194             }
195              
196 11         41 $self->{$arg} = $opts{$arg};
197             } ## end if ( defined( $opts{$arg} ) )
198             } ## end foreach my $arg ( @{ $self->{args}{args} } )
199              
200 1         4 eval {
201             $self->{backoff} = Algorithm::Backoff::Exponential->new(
202             initial_delay => $self->{initial_delay},
203             max_delay => $self->{max_delay},
204 1         41 consider_actual_delay => 1,
205             delay_on_success => 1,
206             );
207             };
208 1 50       104 if ($@) {
209 0         0 die($@);
210             }
211              
212 1         8 return $self;
213             } ## end sub new
214              
215             =head2 create_session
216              
217             This creates the new POE session that will handle this.
218              
219             $dh->create_session;
220              
221             =cut
222              
223             sub create_session {
224 1     1 1 49 my ( $self, %opts ) = @_;
225              
226 1         33 $self->errorblank;
227              
228 1         52 POE::Session->create(
229             inline_states => {
230             _start => \&on_start,
231             got_child_stdout => \&on_child_stdout,
232             got_child_stderr => \&on_child_stderr,
233             got_child_close => \&on_child_close,
234             got_child_signal => \&on_child_signal,
235             },
236             heap => { self => $self },
237             );
238              
239 1         340 return;
240             } ## end sub create_session
241              
242             =head2 log_message
243              
244             Logs a message. Printing to stdout or sending to syslog is controlled via
245             the status_syslog and status_print values passed to new.
246              
247              
248              
249             - status :: What to log.
250             Default :: undef
251              
252             - error :: If true, this will set the log level from info to err.
253             Default :: 0
254              
255             =cut
256              
257             sub log_message {
258 4     4 1 49 my ( $self, %opts ) = @_;
259              
260 4         24 $self->errorblank;
261              
262 4 50       122 if ( !defined( $opts{status} ) ) {
263 0         0 return;
264             }
265              
266 4         13 my $level = 'info';
267 4 100       13 if ( $opts{error} ) {
268 1         7 $level = 'err';
269             }
270              
271             # used for making sure we only use warn once.
272 4         7 my $warned = 0;
273              
274 4 50       18 if ( $self->{status_print} ) {
275 4 100 66     33 if ( $self->{status_print_warn} && $opts{error} ) {
276 1         59 warn( $self->{syslog_name} . '[' . $$ . '] ' . $opts{status} );
277 1         5 $warned = 1;
278             } else {
279 3         97 print $self->{syslog_name} . '[' . $$ . '] ' . $opts{status} . "\n";
280             }
281             }
282              
283 4 50       25 if ( $self->{status_syslog} ) {
284 0 0 0     0 if ( $self->{status_syslog_warn} && $opts{error} && !$warned ) {
      0        
285 0         0 warn( $self->{syslog_name} . '[' . $$ . '] ' . $opts{status} );
286             }
287 0         0 eval {
288 0         0 openlog( $self->{syslog_name}, '', $self->{syslog_facility} );
289 0         0 syslog( $level, $opts{status} );
290 0         0 closelog();
291             };
292 0 0       0 if ($@) {
293 0         0 warn( 'Errored logging message... ' . $@ );
294             }
295             } ## end if ( $self->{status_syslog} )
296             } ## end sub log_message
297              
298             =head2 kill
299              
300             Sends the specified signal to the PIDs.
301              
302             Returns undef if there are no PIDs, meaning it is not running.
303              
304             If the signal is not supported, the error 5, killFailed, is set.
305              
306             For understanding the return value, see the docs for the Perl
307             function kill.
308              
309             If you want to see the available signals,
310             check L and $Config{sig_name}.
311              
312             - signal :: The signal to send. The default is conntrolled
313             by the setting of the default_kill_signal setting.
314              
315             # send the default signal
316             my $count=$dh->kill;
317              
318             # send the KILL signal
319             my $count;
320             eval{ $count=$dh->kill(signal=>'KILL'); };
321             if ($@ && $Error::Helper::errorFlag eq 'killFailed') {
322             die('Unkown kill signal used');
323             } elsif ($@) {
324             die($@);
325             } elsif ( $count < 1 ) {
326             die('Failed to kill any of the procs');
327             }
328             print $count . " procs signaled\n";
329              
330             =cut
331              
332             sub kill {
333 0     0 1 0 my ( $self, %opts ) = @_;
334              
335 0         0 $self->errorblank;
336              
337 0 0       0 if ( !defined( $opts{signal} ) ) {
338 0         0 $opts{signal} = $self->{default_kill_signal};
339             }
340              
341 0         0 my @pids = $self->pids;
342              
343 0 0       0 if ( !defined( $pids[0] ) ) {
344 0         0 return undef;
345             }
346              
347 0         0 my $count;
348 0         0 eval { $count = kill $opts{signal}, @pids; };
  0         0  
349 0 0       0 if ($@) {
350 0         0 $self->{error} = 5;
351             $self->{errorString}
352 0         0 = 'Died trying to send kill signal "' . $opts{signal} . '" to pids ' . join( ',', @pids ) . ' ... ' . $@;
353 0         0 $self->warn;
354 0         0 return undef;
355             }
356              
357 0         0 return $count;
358             } ## end sub kill
359              
360             =head2 pid
361              
362             Returns the PID of the process or undef if it
363             has not been started.
364              
365             This just return the child PID. Will not return
366             the PID from the PID file if one is set.
367              
368             my $pid = $dh->pid;
369             if ($pid){
370             print 'PID is '.$started_at."\n";
371             }
372              
373             =cut
374              
375             sub pid {
376 0     0 1 0 my ( $self, %opts ) = @_;
377              
378 0         0 $self->errorblank;
379              
380 0         0 return $self->{pid};
381             }
382              
383             =head2 pids
384              
385             Returns the child PID and PID from the PID file if one is specified.
386              
387             This calls pid_from_pid_file via eval and ignores if it fails. If you
388             want to check to see if that errored or not, check to see if error 4,
389             readPidFileFailed, was set or use both pid and pid_from_pid_file.
390              
391             my @pids = $dh->pids;
392             print 'PIDs are ' . join(', ', @pids) . "\n";
393              
394             =cut
395              
396             sub pids {
397 0     0 1 0 my ($self) = @_;
398              
399 0         0 $self->errorblank;
400              
401 0         0 my @pids;
402              
403 0 0       0 if ( defined( $self->{pid} ) ) {
404 0         0 push( @pids, $self->{pid} );
405             }
406              
407 0         0 my $pid_from_pid_file;
408 0         0 eval { $pid_from_pid_file = $self->pid_from_pid_file; };
  0         0  
409 0 0       0 if ( defined($pid_from_pid_file) ) {
410 0         0 push( @pids, $pid_from_pid_file );
411             }
412              
413 0         0 return @pids;
414             } ## end sub pids
415              
416             =head2 pid_from_pid_file
417              
418             Reads the PID from the PID file.
419              
420             If one was not specified or the file does not exist, it returns undef.
421              
422             Will throw error 4, readPidFileFailed, if it could not read it.
423              
424             After reading it, it will return the first integer.
425              
426             my $pid;
427             eval{ $pid = $dh->pid_from_pid_file; };
428             if ($@) {
429             print "Could not read PID file\n";
430             } elsif (defined ($pid)) {
431             print 'PID: ' . $pid . "\n";
432             }
433              
434             =cut
435              
436             sub pid_from_pid_file {
437 0     0 1 0 my ($self) = @_;
438              
439 0         0 $self->errorblank;
440              
441 0 0       0 if ( !defined( $self->{pid_file} ) ) {
    0          
442 0         0 return undef;
443             } elsif ( !-f $self->{pid_file} ) {
444 0         0 return undef;
445             }
446              
447 0         0 my $raw_pid_file;
448 0         0 eval { $raw_pid_file = read_file( $self->{pid_file} ); };
  0         0  
449 0 0       0 if ($@) {
450 0         0 $self->{error} = 4;
451 0         0 $self->{errorString} = 'Failed to read PID file, "' . $self->{pid_file} . '" ... ' . $@;
452 0         0 $self->warn;
453 0         0 return;
454             }
455              
456 0         0 my @raw_pid_file_split = split( /\n/, $raw_pid_file );
457 0         0 foreach my $line (@raw_pid_file_split) {
458 0 0       0 if ( $line =~ /^[0-9]+$/ ) {
459 0         0 return $line;
460             }
461             }
462              
463 0         0 return undef;
464             } ## end sub pid_from_pid_file
465              
466             =head2 restart_ctl
467              
468             Controls if the process will be restarted when it exits or not.
469              
470             - restart_ctl :: A Perl boolean that if true the process will
471             be restarted when it exits.
472             Default :: undef
473              
474             # next time it exits, it won't be restarted
475             $dh->restart_ctl(restart_ctl=>0);
476              
477             If restart_ctl is undef, the current value is returned.
478              
479             my $restart_ctl = $dh->restart_ctl;
480             if ($restart_ctl) {
481             print "Will be restarted when it dies.\n";
482             } else {
483             print "Will NOT be restarted when it dies.\n";
484             }
485              
486             =cut
487              
488             sub restart_ctl {
489 0     0 1 0 my ( $self, %opts ) = @_;
490              
491 0         0 $self->errorblank;
492              
493 0 0       0 if ( !defined( $opts{restart_ctl} ) ) {
494 0         0 return $self->{restart_ctl};
495             }
496              
497 0         0 $self->{restart_ctl} = $opts{restart_ctl};
498             } ## end sub restart_ctl
499              
500             =head2 started
501              
502             Returns a Perl boolean for if it has been started or not.
503              
504             my $started=$dh->started;
505             if ($started){
506             print 'started as '.$dh->pid."\n";
507             }
508              
509             =cut
510              
511             sub started {
512 0     0 1 0 my ( $self, %opts ) = @_;
513              
514 0         0 $self->errorblank;
515              
516 0         0 return $self->{started};
517             }
518              
519             =head2 started_at
520              
521             Returns the unix time it was (re)started at or undef if it has not
522             been started.
523              
524             my $started_at = $dh->started;
525             if ($started_at){
526             print 'started at '.$started_at."\n";
527             }
528              
529             =cut
530              
531             sub started_at {
532 0     0 1 0 my ( $self, %opts ) = @_;
533              
534 0         0 $self->errorblank;
535              
536 0         0 return $self->{started_at};
537             }
538              
539             sub on_start {
540              
541             my $child = POE::Wheel::Run->new(
542             StdioFilter => POE::Filter::Line->new(),
543             StderrFilter => POE::Filter::Line->new(),
544             Program => $_[HEAP]{self}->{program},
545 1     1 0 1228 StdoutEvent => "got_child_stdout",
546             StderrEvent => "got_child_stderr",
547             CloseEvent => "got_child_close",
548             );
549              
550 1         7592 $_[KERNEL]->sig_child( $child->PID, "got_child_signal" );
551              
552             # Wheel events include the wheel's ID.
553 1         291 $_[HEAP]{children_by_wid}{ $child->ID } = $child;
554              
555             # Signal events include the process ID.
556 1         14 $_[HEAP]{children_by_pid}{ $child->PID } = $child;
557              
558 1         34 $_[HEAP]{self}->log_message( status => 'Starting... ' . $_[HEAP]{self}->{program} );
559              
560 1         12 $_[HEAP]{self}->log_message( status => 'Child pid ' . $child->PID . ' started' );
561              
562 1         4 $_[HEAP]{self}{started} = 1;
563 1         9 $_[HEAP]{self}{pid} = $child->PID;
564 1         27 $_[HEAP]{self}{started_at} = time;
565             } ## end sub on_start
566              
567             sub on_child_stdout {
568 0     0 0   my ( $stdout_line, $wheel_id ) = @_[ ARG0, ARG1 ];
569 0           my $child = $_[HEAP]{children_by_wid}{$wheel_id};
570              
571 0           my $prepend = $_[HEAP]{self}->{stdout_prepend};
572 0 0         if ( $_[HEAP]{self}->{pid_prepend} ) {
573 0           $prepend = $_[HEAP]{self}->{pid} . ' ' . $prepend;
574             }
575              
576 0           $_[HEAP]{self}->log_message( status => $prepend . $stdout_line );
577             } ## end sub on_child_stdout
578              
579             sub on_child_stderr {
580 0     0 0   my ( $stderr_line, $wheel_id ) = @_[ ARG0, ARG1 ];
581 0           my $child = $_[HEAP]{children_by_wid}{$wheel_id};
582              
583 0           my $prepend = $_[HEAP]{self}->{stderr_prepend};
584 0 0         if ( $_[HEAP]{self}->{pid_prepend} ) {
585 0           $prepend = $_[HEAP]{self}->{pid} . ' ' . $prepend;
586             }
587              
588 0           $_[HEAP]{self}->log_message( error => 1, status => $prepend . $stderr_line );
589             } ## end sub on_child_stderr
590              
591             sub on_child_close {
592 0     0 0   my $wheel_id = $_[ARG0];
593 0           my $child = delete $_[HEAP]{children_by_wid}{$wheel_id};
594              
595             # May have been reaped by on_child_signal().
596 0 0         unless ( defined $child ) {
597 0           return;
598             }
599 0           $_[HEAP]{self}->log_message( status => $child->PID . ' closed all pipes.' );
600 0           delete $_[HEAP]{children_by_pid}{ $child->PID };
601             } ## end sub on_child_close
602              
603             sub on_child_signal {
604 0     0 0   my $error = 0;
605 0 0         if ( $_[ARG2] ne '0' ) {
606 0           $error = 1,;
607             }
608              
609 0           my $child = delete $_[HEAP]{children_by_pid}{ $_[ARG1] };
610              
611 0           $_[HEAP]{self}->log_message( error => $error, status => $_[ARG1] . ' exited with ' . $_[ARG2] );
612              
613 0 0         if ( defined($child) ) {
614 0           delete $_[HEAP]{children_by_wid}{ $child->ID };
615             }
616              
617 0           my $secs;
618 0 0         if ( !$error ) {
619 0           $secs = $_[HEAP]{self}{backoff}->success;
620             } else {
621 0           $secs = $_[HEAP]{self}{backoff}->failure;
622             }
623              
624 0 0         if ( $_[HEAP]{self}->{restart_ctl} ) {
625 0           $_[HEAP]{self}->log_message( status => 'restarting in ' . $secs . ' seconds' );
626              
627 0           $_[KERNEL]->delay( _start => 3 );
628             } else {
629 0           $_[HEAP]{self}->log_message( status => 'restart_ctl false... not restarting' );
630             }
631             } ## end sub on_child_signal
632              
633             =head1 ERROR CODES / FLAGS
634              
635             =head2 1, invalidProgram
636              
637             No program is specified.
638              
639             =head2 2, optsBadRef
640              
641             The opts has a invlaid ref.
642              
643             =head2 3, optsNotInt
644              
645             The opts in question should be a int.
646              
647             =head2 4, readPidFileFailed
648              
649             Failed to read the PID file.
650              
651             =head2 5, killFailed
652              
653             Failed to run kill. This in general means a improper signal was specified.
654              
655             If you want to see the available signals, check L and $Config{sig_name}.
656              
657             =head1 AUTHOR
658              
659             Zane C. Bowers-Hadley, C<< >>
660              
661             =head1 BUGS
662              
663             Please report any bugs or feature requests to C, or through
664             the web interface at L. I will be notified, and then you'll
665             automatically be notified of progress on your bug as I make changes.
666              
667              
668              
669              
670             =head1 SUPPORT
671              
672             You can find documentation for this module with the perldoc command.
673              
674             perldoc POE::Wheel::Run::DaemonHelper
675              
676              
677             You can also look for information at:
678              
679             =over 4
680              
681             =item * RT: CPAN's request tracker (report bugs here)
682              
683             L
684              
685             =item * Search CPAN
686              
687             L
688              
689             =back
690              
691              
692             =head1 ACKNOWLEDGEMENTS
693              
694              
695             =head1 LICENSE AND COPYRIGHT
696              
697             This software is Copyright (c) 2024 by Zane C. Bowers-Hadley.
698              
699             This is free software, licensed under:
700              
701             The GNU Lesser General Public License, Version 2.1, February 1999
702              
703              
704             =cut
705              
706             1; # End of POE::Wheel::Run::DaemonHelper