File Coverage

blib/lib/Schedule/Cron.pm
Criterion Covered Total %
statement 437 611 71.5
branch 200 346 57.8
condition 101 181 55.8
subroutine 36 42 85.7
pod 12 14 85.7
total 786 1194 65.8


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2              
3             =encoding utf8
4              
5             =head1 NAME
6              
7             Cron - cron-like scheduler for Perl subroutines
8              
9             =head1 SYNOPSIS
10              
11             use Schedule::Cron;
12              
13             # Subroutines to be called
14             sub dispatcher {
15             print "ID: ",shift,"\n";
16             print "Args: ","@_","\n";
17             }
18              
19             sub check_links {
20             # do something...
21             }
22              
23             # Create new object with default dispatcher
24             my $cron = new Schedule::Cron(\&dispatcher);
25              
26             # Load a crontab file
27             $cron->load_crontab("/var/spool/cron/perl");
28              
29             # Add dynamically crontab entries
30             $cron->add_entry("3 4 * * *",ROTATE => "apache","sendmail");
31             $cron->add_entry("0 11 * * Mon-Fri",\&check_links);
32              
33             # Run scheduler
34             $cron->run(detach=>1);
35            
36              
37             =head1 DESCRIPTION
38              
39             This module provides a simple but complete cron like scheduler. I.e this
40             module can be used for periodically executing Perl subroutines. The dates and
41             parameters for the subroutines to be called are specified with a format known
42             as crontab entry (see L<"METHODS">, C and L)
43              
44             The philosophy behind C is to call subroutines periodically
45             from within one single Perl program instead of letting C trigger several
46             (possibly different) Perl scripts. Everything under one roof. Furthermore,
47             C provides mechanism to create crontab entries dynamically,
48             which isn't that easy with C.
49              
50             C knows about all extensions (well, at least all extensions I'm
51             aware of, i.e those of the so called "Vixie" cron) for crontab entries like
52             ranges including 'steps', specification of month and days of the week by name,
53             or coexistence of lists and ranges in the same field. It even supports a bit
54             more (like lists and ranges with symbolic names).
55              
56             =head1 METHODS
57              
58             =over 4
59              
60             =cut
61              
62             #'
63              
64             package Schedule::Cron;
65              
66 17     17   756006 use Time::ParseDate;
  17         192880  
  17         1069  
67 17     17   10975 use Data::Dumper;
  17         118235  
  17         1005  
68              
69 17     17   132 use strict;
  17         52  
  17         3862  
70 17     17   4884 use vars qw($VERSION $DEBUG);
  17         39  
  17         842  
71 17     17   10244 use subs qw(dbg);
  17         433  
  17         97  
72              
73             my $HAS_POSIX;
74              
75             BEGIN {
76 17     17   1313 eval {
77 17         8187 require POSIX;
78 17         107520 import POSIX ":sys_wait_h";
79             };
80 17 50       29127 $HAS_POSIX = $@ ? 0 : 1;
81             }
82              
83              
84             $VERSION = "1.05";
85              
86             our $DEBUG = 0;
87             my %STARTEDCHILD = ();
88              
89             my @WDAYS = qw(
90             Sunday
91             Monday
92             Tuesday
93             Wednesday
94             Thursday
95             Friday
96             Saturday
97             Sunday
98             );
99              
100             my @ALPHACONV = (
101             { },
102             { },
103             { },
104             { qw(jan 1 feb 2 mar 3 apr 4 may 5 jun 6 jul 7 aug 8
105             sep 9 oct 10 nov 11 dec 12) },
106             { qw(sun 0 mon 1 tue 2 wed 3 thu 4 fri 5 sat 6)},
107             { }
108             );
109             my @RANGES = (
110             [ 0,59 ],
111             [ 0,23 ],
112             [ 0,31 ],
113             [ 0,12 ],
114             [ 0,7 ],
115             [ 0,59 ]
116             );
117              
118             my @LOWMAP = (
119             {},
120             {},
121             { 0 => 1},
122             { 0 => 1},
123             { 7 => 0},
124             {},
125             );
126              
127              
128             # Currently, there are two ways for reaping. One, which only waits explicitly
129             # on PIDs it forked on its own, and one which waits on all PIDs (even on those
130             # it doesn't forked itself). The later has been proved to work on Win32 with
131             # the 64 threads limit (RT #56926), but not when one creates forks on ones
132             # own. The specific reaper works for RT #55741.
133              
134             # It tend to use the specific one, if it also resolves RT #56926. Both are left
135             # here for reference until a decision has been done for 1.01
136              
137             sub REAPER {
138 5     5 0 66 &_reaper_all();
139             }
140              
141             # Specific reaper
142             sub _reaper_specific {
143 17     17   8425 local ($!,%!,$?);
  17     0   22944  
  17         145  
  0         0  
144 0 0       0 if ($HAS_POSIX)
145             {
146 0         0 foreach my $pid (keys %STARTEDCHILD) {
147 0 0       0 if ($STARTEDCHILD{$pid}) {
148 0 0       0 my $res = $HAS_POSIX ? waitpid($pid, WNOHANG) : waitpid($pid,0);
149 0 0       0 if ($res > 0) {
150             # We reaped a truly running process
151 0         0 $STARTEDCHILD{$pid} = 0;
152 0 0       0 dbg "Reaped child $res" if $DEBUG;
153             }
154             }
155             }
156             }
157             else
158             {
159 0         0 my $waitedpid = 0;
160 0         0 while($waitedpid != -1) {
161 0         0 $waitedpid = wait;
162             }
163             }
164             }
165              
166             # Catch all reaper
167             sub _reaper_all {
168             #local ($!,%!,$?,${^CHILD_ERROR_NATIVE});
169              
170             # Localizing ${^CHILD_ERROR_NATIVE} breaks signalhander.t which checks that
171             # chained SIGCHLD handlers are called. I don't know why, though, hence I
172             # leave it out for now. See #69916 for some discussion why this handler
173             # might be needed.
174 5     5   348 local ($!,%!,$?);
175 5         48 my $kid;
176             do
177 5   66     17 {
178             # Only on POSIX systems the wait will return immediately
179             # if there are no finished child processes. Simple 'wait'
180             # waits blocking on childs.
181 6 50       151 $kid = $HAS_POSIX ? waitpid(-1, WNOHANG) : wait;
182 6 50       36 dbg "Kid: $kid" if $DEBUG;
183 6 50 66     151 if ($kid != 0 && $kid != -1 && defined $STARTEDCHILD{$kid})
      66        
184             {
185             # We don't delete the hash entry here to avoid an issue
186             # when modifying global hash from multiple threads
187 1         8 $STARTEDCHILD{$kid} = 0;
188 1 50       13 dbg "Reaped child $kid" if $DEBUG;
189             }
190             } while ($kid != 0 && $kid != -1);
191              
192             # Note to myself: Is the %STARTEDCHILD hash really necessary if we use -1
193             # for waiting (i.e. for waiting on any child ?). In the current
194             # implementation, %STARTEDCHILD is not used at all. It would be only
195             # needed if we iterate over it to wait on pids specifically.
196             }
197              
198             # Cleaning is done in extra method called from the main
199             # process in order to avoid event handlers modifying this
200             # global hash which can lead to memory errors.
201             # See RT #55741 for more details on this.
202             # This method is called in strategic places.
203             sub _cleanup_process_list
204             {
205 14     14   99 my ($self, $cfg) = @_;
206            
207             # Cleanup processes even on those systems, where the SIGCHLD is not
208             # propagated. Only do this for POSIX, otherwise this call would block
209             # until all child processes would have been finished.
210             # See RT #56926 for more details.
211              
212             # Do not cleanup if nofork because jobs that fork will do their own reaping.
213 14 100 66     368 &REAPER() if $HAS_POSIX && !$cfg->{nofork};
214              
215             # Delete entries from this global hash only from within the main
216             # thread/process. Hence, this method must not be called from within
217             # a signalhandler
218 14         130 for my $k (keys %STARTEDCHILD)
219             {
220 5 50       54 delete $STARTEDCHILD{$k} unless $STARTEDCHILD{$k};
221             }
222             }
223              
224             =item $cron = new Schedule::Cron($dispatcher,[extra args])
225              
226             Creates a new C object. C<$dispatcher> is a reference to a subroutine,
227             which will be called by default. C<$dispatcher> will be invoked with the
228             arguments parameter provided in the crontab entry if no other subroutine is
229             specified. This can be either a single argument containing the argument
230             parameter literally has string (default behavior) or a list of arguments when
231             using the C option described below.
232              
233             The date specifications must be either provided via a crontab like file or
234             added explicitly with C (L<"add_entry">).
235              
236             I can be a hash or hash reference for additional arguments. The
237             following parameters are recognized:
238              
239             =over
240              
241             =item file =>
242              
243              
244             Load the crontab entries from
245              
246             =item eval => 1
247              
248             Eval the argument parameter in a crontab entry before calling the subroutine
249             (instead of literally calling the dispatcher with the argument parameter as
250             string)
251              
252             =item nofork => 1
253              
254             Don't fork when starting the scheduler. Instead, the jobs are executed within
255             current process. In your executed jobs, you have full access to the global
256             variables of your script and hence might influence other jobs running at a
257             different time. This behavior is fundamentally different to the 'fork' mode,
258             where each jobs gets its own process and hence a B of the process space,
259             independent of each other job and the main process. This is due to the nature
260             of the C system call.
261              
262             =item nostatus => 1
263              
264             Do not update status in $0. Set this if you don't want ps to reveal the internals
265             of your application, including job argument lists. Default is 0 (update status).
266              
267             =item skip => 1
268              
269             Skip any pending jobs whose time has passed. This option is only useful in
270             combination with C where a job might block the execution of the
271             following jobs for quite some time. By default, any pending job is executed
272             even if its scheduled execution time has already passed. With this option set
273             to true all pending which would have been started in the meantime are skipped.
274              
275             =item catch => 1
276              
277             Catch any exception raised by a job. This is especially useful in combination with
278             the C option to avoid stopping the main process when a job raises an
279             exception (dies).
280              
281             =item after_job => \&after_sub
282              
283             Call a subroutine after a job has been run. The first argument is the return
284             value of the dispatched job, the reminding arguments are the arguments with
285             which the dispatched job has been called.
286              
287             Example:
288              
289             my $cron = new Schedule::Cron(..., after_job => sub {
290             my ($ret,@args) = @_;
291             print "Return value: ",$ret," - job arguments: (",join ":",@args,")\n";
292             });
293              
294             =item log => \&log_sub
295              
296             Install a logging subroutine. The given subroutine is called for several events
297             during the lifetime of a job. This method is called with two arguments: A log
298             level of 0 (info),1 (warning) or 2 (error) depending on the importance of the
299             message and the message itself.
300              
301             For example, you could use I (L) for logging
302             purposes for example like in the following code snippet:
303              
304             use Log::Log4perl;
305             use Log::Log4perl::Level;
306              
307             my $log_method = sub {
308             my ($level,$msg) = @_;
309             my $DBG_MAP = { 0 => $INFO, 1 => $WARN, 2 => $ERROR };
310              
311             my $logger = Log::Log4perl->get_logger("My::Package");
312             $logger->log($DBG_MAP->{$level},$msg);
313             }
314            
315             my $cron = new Schedule::Cron(.... , log => $log_method);
316              
317             =item loglevel => <-1,0,1,2>
318              
319             Restricts logging to the specified severity level or below. Use 0 to have all
320             messages generated, 1 for only warnings and errors and 2 for errors only.
321             Default is 0 (all messages). A loglevel of -1 (debug) will include job
322             argument lists (also in $0) in the job start message logged with a level of 0
323             or above. You may have security concerns with this. Unless you are debugging,
324             use 0 or higher. A value larger than 2 will disable logging completely.
325              
326             Although you can filter in your log routine, generating the messages can be
327             expensive, for example if you pass arguments pointing to large hashes. Specifying
328             a loglevel avoids formatting data that your routine would discard.
329              
330             =item processprefix =>
331              
332             Cron::Schedule sets the process' name (i.e. C<$0>) to contain some informative
333             messages like when the next job executes or with which arguments a job is
334             called. By default, the prefix for this labels is C. With this
335             option you can set it to something different. You can e.g. use C<$0> to include
336             the original process name. You can inhibit this with the C option, and
337             prevent the argument display by setting C to zero or higher.
338              
339             =item processname =>
340              
341             Set the process name (i.e. C<$0>) to a literal string. Using this setting
342             overrides C and C.
343              
344             =item sleep => \&hook
345              
346             If specified, &hook will be called instead of sleep(), with the time to sleep
347             in seconds as first argument and the Schedule::Cron object as second. This hook
348             allows you to use select() instead of sleep, so that you can handle IO, for
349             example job requests from a network connection.
350              
351             e.g.
352              
353             $cron->run( { sleep => \&sleep_hook, nofork => 1 } );
354              
355             sub sleep_hook {
356             my ($time, $cron) = @_;
357              
358             my ($rin, $win, $ein) = ('','','');
359             my ($rout, $wout, $eout);
360             vec($rin, fileno(STDIN), 1) = 1;
361             my ($nfound, $ttg) = select($rout=$rin, $wout=$win, $eout=$ein, $time);
362             if ($nfound) {
363             handle_io($rout, $wout, $eout);
364             }
365             return;
366             }
367              
368             =back
369              
370             =cut
371              
372             sub new
373             {
374 27     27 1 38561 my $class = shift;
375 27   50     349 my $dispatcher = shift || die "No dispatching sub provided";
376 27 50       158 die "Dispatcher not a ref to a subroutine" unless ref($dispatcher) eq "CODE";
377 27 100       150 my $cfg = ref($_[0]) eq "HASH" ? $_[0] : { @_ };
378 27 100       135 $cfg->{processprefix} = "Schedule::Cron" unless $cfg->{processprefix};
379 27   100     161 my $timeshift = $cfg->{timeshift} || 0;
380 27         207 my $self = {
381             cfg => $cfg,
382             dispatcher => $dispatcher,
383             timeshift => $timeshift,
384             queue => [ ],
385             map => { }
386             };
387 27   33     154 bless $self,(ref($class) || $class);
388            
389 27 100       119 $self->load_crontab if $cfg->{file};
390 27         289 $self;
391             }
392              
393             =item $cron->load_crontab($file)
394              
395             =item $cron->load_crontab(file=>$file,[eval=>1])
396              
397             Loads and parses the crontab file C<$file>. The entries found in this file will
398             be B to the current time table with C<$cron-Eadd_entry>.
399              
400             The format of the file consists of cron commands containing of lines with at
401             least 5 columns, whereas the first 5 columns specify the date. The rest of the
402             line (i.e columns 6 and greater) contains the argument with which the
403             dispatcher subroutine will be called. By default, the dispatcher will be
404             called with one single string argument containing the rest of the line
405             literally. Alternatively, if you call this method with the optional argument
406             C1> (you must then use the second format shown above), the rest of
407             the line will be evaled before used as argument for the dispatcher.
408              
409             For the format of the first 5 columns, please see L<"add_entry">.
410              
411             Blank lines and lines starting with a C<#> will be ignored.
412              
413             There's no way to specify another subroutine within the crontab file. All
414             calls will be made to the dispatcher provided at construction time.
415              
416             If you want to start up fresh, you should call
417             C<$cron-Eclean_timetable()> before.
418              
419             Example of a crontab fiqw(le:)
420              
421             # The following line runs on every Monday at 2:34 am
422             34 2 * * Mon "make_stats"
423             # The next line should be best read in with an eval=>1 argument
424             * * 1 1 * { NEW_YEAR => '1',HEADACHE => 'on' }
425              
426             =cut
427              
428             #'
429              
430             sub load_crontab
431             {
432 5     5 1 22 my $self = shift;
433 5         10 my $cfg = shift;
434              
435 5 100       14 if ($cfg)
436             {
437 4 100       14 if (@_)
    100          
438             {
439 1 50       7 $cfg = ref($cfg) eq "HASH" ? $cfg : { $cfg,@_ };
440             }
441             elsif (!ref($cfg))
442             {
443 2         5 my $new_cfg = { };
444 2         3 $new_cfg->{file} = $cfg;
445 2         5 $cfg = $new_cfg;
446             }
447             }
448            
449 5   50     20 my $file = $cfg->{file} || $self->{cfg}->{file} || die "No filename provided";
450 5   100     32 my $eval = $cfg->{eval} || $self->{cfg}->{eval};
451            
452 5 50       190 open(F,$file) || die "Cannot open schedule $file : $!";
453 5         17 my $line = 0;
454 5         184 while ()
455             {
456 345         492 $line++;
457             # Strip off trailing comments and ignore empty
458             # or pure comments lines:
459 345         633 s/#.*$//;
460 345 100       1118 next if /^\s*$/;
461 185 50       324 next if /^\s*#/;
462 185         283 chomp;
463 185         825 s/\s*(.*)\s*$/$1/;
464 185         1008 my ($min,$hour,$dmon,$month,$dweek,$rest) = split (/\s+/,$_,6);
465            
466 185         505 my $time = [ $min,$hour,$dmon,$month,$dweek ];
467              
468             # Try to check, whether an optional 6th column specifying seconds
469             # exists:
470 185         244 my $args;
471 185 50       334 if ($rest)
472             {
473 185         511 my ($col6,$more_args) = split(/\s+/,$rest,2);
474 185 100       480 if ($col6 =~ /^[\d\-\*\,\/]+$/)
475             {
476 15         32 push @$time,$col6;
477 15         47 dbg "M: $more_args";
478 15         29 $args = $more_args;
479             }
480             else
481             {
482 170         258 $args = $rest;
483             }
484             }
485 185         506 $self->add_entry($time,{ 'args' => $args, 'eval' => $eval});
486             }
487 5         72 close F;
488             }
489              
490             =item $cron->add_entry($timespec,[arguments])
491              
492             Adds a new entry to the list of scheduled cron jobs.
493              
494             B
495              
496             C<$timespec> is the specification of the scheduled time in crontab format
497             (L) which contains five mandatory time and date fields and an
498             optional 6th column. C<$timespec> can be either a plain string, which contains
499             a whitespace separated time and date specification. Alternatively,
500             C<$timespec> can be a reference to an array containing the five elements for
501             the date fields.
502              
503             The time and date fields are (taken mostly from L, "Vixie" cron):
504              
505             field values
506             ===== ======
507             minute 0-59
508             hour 0-23
509             day of month 1-31
510             month 1-12 (or as names)
511             day of week 0-7 (0 or 7 is Sunday, or as names)
512             seconds 0-59 (optional)
513              
514             A field may be an asterisk (*), which always stands for
515             ``first-last''.
516              
517             Ranges of numbers are allowed. Ranges are two numbers
518             separated with a hyphen. The specified range is
519             inclusive. For example, 8-11 for an ``hours'' entry
520             specifies execution at hours 8, 9, 10 and 11.
521              
522             Lists are allowed. A list is a set of numbers (or
523             ranges) separated by commas. Examples: ``1,2,5,9'',
524             ``0-4,8-12''.
525              
526             Step values can be used in conjunction with ranges.
527             Following a range with ``/'' specifies skips of
528             the numbers value through the range. For example,
529             ``0-23/2'' can be used in the hours field to specify
530             command execution every other hour (the alternative in
531             the V7 standard is ``0,2,4,6,8,10,12,14,16,18,20,22'').
532             Steps are also permitted after an asterisk, so if you
533             want to say ``every two hours'', just use ``*/2''.
534              
535             Names can also be used for the ``month'' and ``day of
536             week'' fields. Use the first three letters of the
537             particular day or month (case doesn't matter).
538              
539             Note: The day of a command's execution can be specified
540             by two fields -- day of month, and day of week.
541             If both fields are restricted (ie, aren't *), the
542             command will be run when either field matches the
543             current time. For example, ``30 4 1,15 * 5''
544             would cause a command to be run at 4:30 am on the
545             1st and 15th of each month, plus every Friday
546              
547             Examples:
548              
549             "8 0 * * *" ==> 8 minutes after midnight, every day
550             "5 11 * * Sat,Sun" ==> at 11:05 on each Saturday and Sunday
551             "0-59/5 * * * *" ==> every five minutes
552             "42 12 3 Feb Sat" ==> at 12:42 on 3rd of February and on
553             each Saturday in February
554             "32 11 * * * 0-30/2" ==> 11:32:00, 11:32:02, ... 11:32:30 every
555             day
556              
557             In addition, ranges or lists of names are allowed.
558              
559             An optional sixth column can be used to specify the seconds within the
560             minute. If not present, it is implicitly set to "0".
561              
562             B
563              
564             The subroutine to be executed when the C<$timespec> matches can be
565             specified in several ways.
566              
567             First, if the optional C are lacking, the default dispatching
568             subroutine provided at construction time will be called without arguments.
569              
570             If the second parameter to this method is a reference to a subroutine, this
571             subroutine will be used instead of the dispatcher.
572              
573             Any additional parameters will be given as arguments to the subroutine to be
574             executed. You can also specify a reference to an array instead of a list of
575             parameters.
576              
577             You can also use a named parameter list provided as an hashref. The named
578             parameters recognized are:
579              
580             =over
581              
582             =item subroutine
583              
584             =item sub
585              
586             Reference to subroutine to be executed
587              
588             =item arguments
589              
590             =item args
591              
592             Reference to array containing arguments to be use when calling the subroutine
593              
594             =item eval
595              
596             If true, use the evaled string provided with the C parameter. The
597             evaluation will take place immediately (not when the subroutine is going to be
598             called)
599              
600             =back
601              
602             Examples:
603              
604             $cron->add_entry("* * * * *");
605             $cron->add_entry("* * * * *","doit");
606             $cron->add_entry("* * * * *",\&dispatch,"first",2,"third");
607             $cron->add_entry("* * * * *",{'subroutine' => \&dispatch,
608             'arguments' => [ "first",2,"third" ]});
609             $cron->add_entry("* * * * *",{'subroutine' => \&dispatch,
610             'arguments' => '[ "first",2,"third" ]',
611             'eval' => 1});
612              
613             =cut
614              
615             sub add_entry
616             {
617 219     219 1 1512 my $self = shift;
618 219         310 my $time = shift;
619 219   100     476 my $args = shift || [];
620 219         305 my $dispatch;
621            
622             # dbg "Args: ",Dumper($time,$args);
623            
624 219 100       537 if (ref($args) eq "HASH")
    100          
625             {
626 193         252 my $cfg = $args;
627 193         277 $args = undef;
628 193   66     553 $dispatch = $cfg->{subroutine} || $cfg->{sub};
629 193   100     620 $args = $cfg->{arguments} || $cfg->{args} || [];
630 193 100 66     471 if ($cfg->{eval} && $cfg)
631             {
632 112 100       198 die "You have to provide a simple scalar if using eval" if (ref($args));
633 111         130 my $orig_args = $args;
634 111 50       207 dbg "Evaled args ",Dumper($args) if $DEBUG;
635 111         4480 $args = [ eval $args ];
636 111 50       374 die "Cannot evaluate args (\"$orig_args\")"
637             if $@;
638             }
639             }
640             elsif (ref($args) eq "CODE")
641             {
642 4         10 $dispatch = $args;
643 4   50     23 $args = shift || [];
644             }
645 218 100       490 if (ref($args) ne "ARRAY")
646             {
647 81         155 $args = [ $args,@_ ];
648             }
649              
650 218   66     882 $dispatch ||= $self->{dispatcher};
651              
652              
653 218 100       734 my $time_array = ref($time) ? $time : [ split(/\s+/,$time) ];
654 218 100 100     663 die "Invalid number of columns in time entry (5 or 6)\n"
655             if ($#$time_array != 4 && $#$time_array !=5);
656 217         641 $time = join ' ',@$time_array;
657              
658             # dbg "Adding ",Dumper($time);
659 217         302 push @{$self->{time_table}},
  217         767  
660             {
661             time => $time,
662             dispatcher => $dispatch,
663             args => $args
664             };
665            
666 217         402 $self->{entries_changed} = 1;
667             # dbg "Added Args ",Dumper($self->{args});
668            
669 217         291 my $index = $#{$self->{time_table}};
  217         385  
670 217         341 my $id = $args->[0];
671 217 100       531 $self->{map}->{$id} = $index if $id;
672            
673 217         287 return $#{$self->{time_table}};
  217         1118  
674             }
675              
676             =item @entries = $cron->list_entries()
677              
678             Return a list of cron entries. Each entry is a hash reference of the following
679             form:
680              
681             $entry = {
682             time => $timespec,
683             dispatch => $dispatcher,
684             args => $args_ref
685             }
686              
687             Here C<$timespec> is the specified time in crontab format as provided to
688             C, C<$dispatcher> is a reference to the dispatcher for this entry
689             and C<$args_ref> is a reference to an array holding additional arguments (which
690             can be an empty array reference). For further explanation of this arguments
691             refer to the documentation of the method C.
692              
693             The order index of each entry can be used within C, C
694             and C. But be aware, when you are deleting an entry, that you
695             have to re-fetch the list, since the order will have changed.
696              
697             Note that these entries are returned by value and were obtained from the
698             internal list by a deep copy. I.e. you are free to modify it, but this won't
699             influence the original entries. Instead use C if you need to
700             modify an existing crontab entry.
701              
702             =cut
703              
704             sub list_entries
705             {
706 5     5 1 562 my ($self) = shift;
707            
708 5         7 my @ret;
709 5         7 foreach my $entry (@{$self->{time_table}})
  5         11  
710             {
711             # Deep copy $entry
712 7         16 push @ret,$self->_deep_copy_entry($entry);
713             }
714 5         37 return @ret;
715             }
716              
717              
718             =item $entry = $cron->get_entry($idx)
719              
720             Get a single entry. C<$entry> is either a hashref with the possible keys
721             C
722             with the given index C<$idx> exists.
723              
724             =cut
725              
726             sub get_entry
727             {
728 92     92 1 1408 my ($self,$idx) = @_;
729              
730 92         307 my $entry = $self->{time_table}->[$idx];
731 92 100       315 if ($entry)
732             {
733 91         362 return $self->_deep_copy_entry($entry);
734             }
735             else
736             {
737 1         6 return undef;
738             }
739             }
740              
741             =item $cron->delete_entry($idx)
742              
743             Delete the entry at index C<$idx>. Returns the deleted entry on success,
744             C otherwise.
745              
746             =cut
747              
748             sub delete_entry
749             {
750 3     3 1 1124 my ($self,$idx) = @_;
751              
752 3 50       5 if ($idx <= $#{$self->{time_table}})
  3         20  
753             {
754 3         7 $self->{entries_changed} = 1;
755              
756             # Remove entry from $self->{map} which
757             # remembers the index in the timetable by name (==id)
758             # and update all larger indexes appropriately
759             # Fix for #54692
760 3         6 my $map = $self->{map};
761 3         5 foreach my $key (keys %{$map}) {
  3         10  
762 6 100       21 if ($map->{$key} > $idx) {
    100          
763 2         5 $map->{$key}--;
764             } elsif ($map->{$key} == $idx) {
765 2         5 delete $map->{$key};
766             }
767             }
768 3         6 return splice @{$self->{time_table}},$idx,1;
  3         13  
769             }
770             else
771             {
772 0         0 return undef;
773             }
774             }
775              
776             =item $cron->update_entry($idx,$entry)
777              
778             Updates the entry with index C<$idx>. C<$entry> is a hash ref as described in
779             C and must contain at least a value C<$entry-E{time}>. If no
780             C<$entry-E{dispatcher}> is given, then the default dispatcher is used. This
781             method returns the old entry on success, C otherwise.
782              
783             =cut
784              
785             sub update_entry
786             {
787 1     1 1 3 my ($self,$idx,$entry) = @_;
788              
789 1 50       4 die "No update entry given" unless $entry;
790 1 50       4 die "No time specification given" unless $entry->{time};
791            
792 1 50       2 if ($idx <= $#{$self->{time_table}})
  1         5  
793             {
794 1         3 my $new_entry = $self->_deep_copy_entry($entry);
795             $new_entry->{dispatcher} = $self->{dispatcher}
796 1 50       7 unless $new_entry->{dispatcher};
797             $new_entry->{args} = []
798 1 50       5 unless $new_entry->{args};
799 1         2 return splice @{$self->{time_table}},$idx,1,$new_entry;
  1         5  
800             }
801             else
802             {
803 0         0 return undef;
804             }
805             }
806              
807             =item $cron->run([options])
808              
809             This method starts the scheduler.
810              
811             When called without options, this method will never return and executes the
812             scheduled subroutine calls as needed.
813              
814             Alternatively, you can detach the main scheduler loop from the current process
815             (daemon mode). In this case, the pid of the forked scheduler process will be
816             returned.
817              
818             The C parameter specifies the running mode of C. It
819             can be either a plain list which will be interpreted as a hash or it can be a
820             reference to a hash. The following named parameters (keys of the provided hash)
821             are recognized:
822              
823             =over
824              
825             =item detach
826              
827             If set to a true value the scheduler process is detached from the current
828             process (UNIX only).
829              
830             =item pid_file
831              
832             If running in daemon mode, name the optional file, in which the process id of
833             the scheduler process should be written. By default, no PID File will be
834             created.
835              
836             =item nofork, skip, catch, log, loglevel, nostatus, sleep
837              
838             See C for a description of these configuration parameters, which can be
839             provided here as well. Note, that the options given here overrides those of the
840             constructor.
841              
842             =back
843              
844              
845             Examples:
846              
847             # Start scheduler, detach from current process and
848             # write the PID of the forked scheduler to the
849             # specified file
850             $cron->run(detach=>1,pid_file=>"/var/run/scheduler.pid");
851              
852             # Start scheduler and wait forever.
853             $cron->run();
854              
855             =cut
856              
857             sub run
858             {
859 18     18 1 211 my $self = shift;
860 18 100       98 my $cfg = ref($_[0]) eq "HASH" ? $_[0] : { @_ };
861 18         73 $cfg = { %{$self->{cfg}}, %$cfg }; # Merge in global config;
  18         128  
862              
863 18         64 my $log = $cfg->{log};
864 18         49 my $loglevel = $cfg->{loglevel};
865 18 50       89 $loglevel = 0 unless defined $loglevel;
866 18         41 my $sleeper = $cfg->{sleep};
867              
868 18         104 $self->_rebuild_queue;
869 18         58 delete $self->{entries_changed};
870 18 50       39 die "Nothing in schedule queue" unless @{$self->{queue}};
  18         123  
871            
872             # Install reaper now.
873 18 100       74 unless ($cfg->{nofork}) {
874 5         30 my $old_child_handler = $SIG{'CHLD'};
875             $SIG{'CHLD'} = sub {
876 1 50   1   36 dbg "Calling reaper" if $DEBUG;
877 1         22 &REAPER();
878 1 50 33     26 if ($old_child_handler && ref $old_child_handler eq 'CODE')
879             {
880 1 50       5 dbg "Calling old child handler" if $DEBUG;
881             #use B::Deparse ();
882             #my $deparse = B::Deparse->new;
883             #print 'sub ', $deparse->coderef2text($old_child_handler), "\n";
884 1         28 &$old_child_handler();
885             }
886 5         90 };
887             }
888            
889 18 100       81 if (my $name = $cfg->{processname}) {
890 2         17 $0 = $name
891             }
892              
893             my $mainloop = sub {
894             MAIN:
895 18     18   56 while (42)
896             {
897 32 50       61 unless (@{$self->{queue}}) # Queue length
  32         136  
898             {
899             # Last job deleted itself, or we were run with no entries.
900             # We can't return, so throw an exception - perhaps someone will catch.
901 0         0 die "No more jobs to run\n";
902             }
903 32         113 my ($indexes,$time) = $self->_get_next_jobs();
904 32 50       137 dbg "Jobs for $time : ",join(",",@$indexes) if $DEBUG;
905 32         106 my $now = $self->_now();
906 32         68 my $sleep = 0;
907 32 100       110 if ($time < $now)
908             {
909 1 50       5 if ($cfg->{skip})
910             {
911 1         3 for my $index (@$indexes) {
912 1 50 33     11 $log->(0,"Schedule::Cron - Skipping job $index")
913             if $log && $loglevel <= 0;
914 1         42 $self->_update_queue($index);
915             }
916 1         4 next;
917             }
918             # At least a safety airbag
919 0         0 $sleep = 1;
920             }
921             else
922             {
923 31         105 $sleep = $time - $now;
924             }
925              
926 31 100 100     219 unless ($cfg->{processname} || $cfg->{nostatus}) {
927 27         100 $0 = $self->_get_process_prefix()." MainLoop - next: ".scalar(localtime($time));
928             }
929              
930 31 50       161 if (!$time) {
931 0 0       0 die "Internal: No time found, self: ",$self->{queue},"\n" unless $time;
932             }
933              
934 31 50       100 dbg "R: sleep = $sleep | ",scalar(localtime($time))," (",scalar(localtime($now)),")" if $DEBUG;
935              
936 31         98 while ($sleep > 0)
937             {
938 31 50       178 if ($sleeper)
939             {
940 0         0 $sleeper->($sleep,$self);
941 0 0       0 if ($self->{entries_changed})
942             {
943 0         0 $self->_rebuild_queue;
944 0         0 delete $self->{entries_changed};
945 0         0 redo MAIN;
946             }
947             } else {
948 31         37535332 sleep($sleep);
949             }
950 31         1058 $sleep = $time - $self->_now();
951             }
952              
953 29         229 for my $index (@$indexes) {
954 36         266 $self->_execute($index,$cfg);
955             # If "skip" is set and the job takes longer than a second, then
956             # the remaining jobs are skipped.
957 21 100 66     189 last if $cfg->{skip} && $time < $self->_now();
958             }
959 14         257 $self->_cleanup_process_list($cfg);
960              
961 14 100       77 if ($self->{entries_changed}) {
962 3 50       14 dbg "rebuilding queue" if $DEBUG;
963 3         14 $self->_rebuild_queue;
964 3         15 delete $self->{entries_changed};
965             } else {
966 11         68 for my $index (@$indexes) {
967 15         141 $self->_update_queue($index);
968             }
969             }
970             }
971 18         145 };
972              
973 18 50       65 if ($cfg->{detach})
974             {
975 0 0       0 defined(my $pid = fork) or die "Can't fork: $!";
976 0 0       0 if ($pid)
977             {
978             # Parent:
979 0 0       0 if ($cfg->{pid_file})
980             {
981 0 0       0 if (open(P,">".$cfg->{pid_file}))
982             {
983 0         0 print P $pid,"\n";
984 0         0 close P;
985             }
986             else
987             {
988 0         0 warn "Warning: Cannot open ",$cfg->{pid_file}," : $!\n";
989             }
990            
991             }
992 0         0 return $pid;
993             }
994             else
995             {
996             # Child:
997             # Try to detach from terminal:
998 0         0 chdir '/';
999 0 0       0 open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
1000 0 0       0 open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
1001            
1002 0         0 eval { require POSIX; };
  0         0  
1003 0 0       0 if ($@)
1004             {
1005             # if (1) {
1006 0 0       0 if (open(T,"/dev/tty"))
1007             {
1008 0         0 dbg "No setsid found, trying ioctl() (Error: $@)";
1009 0         0 eval { require 'ioctl.ph'; };
  0         0  
1010 0 0       0 if ($@)
1011             {
1012 0         0 eval { require 'sys/ioctl.ph'; };
  0         0  
1013 0 0       0 if ($@)
1014             {
1015 0         0 die "No 'ioctl.ph'. Probably you have to run h2ph (Error: $@)";
1016             }
1017             }
1018 0         0 my $notty = &TIOCNOTTY;
1019 0 0 0     0 die "No TIOCNOTTY !" if $@ || !$notty;
1020 0 0       0 ioctl(T,$notty,0) || die "Cannot issue ioctl(..,TIOCNOTTY) : $!";
1021 0         0 close(T);
1022             };
1023             }
1024             else
1025             {
1026 0 0       0 &POSIX::setsid() || die "Can't start a new session: $!";
1027             }
1028 0 0       0 open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
1029            
1030 0 0 0     0 unless ($cfg->{processname} || $cfg->{nostatus}) {
1031 0         0 $0 = $self->_get_process_prefix()." MainLoop";
1032             }
1033              
1034 0         0 &$mainloop();
1035             }
1036             }
1037             else
1038             {
1039 18         61 &$mainloop();
1040             }
1041             }
1042              
1043              
1044             =item $cron->clean_timetable()
1045              
1046             Remove all scheduled entries
1047              
1048             =cut
1049              
1050             sub clean_timetable
1051             {
1052 2     2 1 124 my $self = shift;
1053 2         7 $self->{entries_changed} = 1;
1054 2         11 $self->{time_table} = [];
1055             }
1056              
1057              
1058             =item $cron->check_entry($id)
1059              
1060             Check, whether the given ID is already registered in the timetable.
1061             A ID is the first argument in the argument parameter of the
1062             a crontab entry.
1063              
1064             Returns (one of) the index in the timetable (can be 0, too) if the ID
1065             could be found or C otherwise.
1066              
1067             Example:
1068              
1069             $cron->add_entry("* * * * *","ROTATE");
1070             .
1071             .
1072             defined($cron->check_entry("ROTATE")) || die "No ROTATE entry !"
1073              
1074             =cut
1075              
1076             sub check_entry
1077             {
1078 4     4 1 1347 my $self = shift;
1079 4         9 my $id = shift;
1080 4         9 return $self->{map}->{$id};
1081             }
1082              
1083              
1084             =item $cron->get_next_execution_time($cron_entry,[$ref_time])
1085              
1086             Well, this is mostly an internal method, but it might be useful on
1087             its own.
1088              
1089             The purpose of this method is to calculate the next execution time
1090             from a specified crontab entry
1091              
1092             Parameters:
1093              
1094             =over
1095              
1096             =item $cron_entry
1097              
1098             The crontab entry as specified in L<"add_entry">
1099              
1100             =item $ref_time
1101              
1102             The reference time for which the next time should be searched which matches
1103             C<$cron_entry>. By default, take the current time
1104              
1105             =back
1106              
1107             This method returns the number of epoch-seconds of the next matched
1108             date for C<$cron_entry>.
1109              
1110             Since I suspect, that this calculation of the next execution time might
1111             fail in some circumstances (bugs are lurking everywhere ;-) an
1112             additional interactive method C is provided for checking
1113             crontab entries against your expected output. Refer to the
1114             top-level README for additional usage information for this method.
1115              
1116             =cut
1117              
1118             sub get_next_execution_time
1119             {
1120 102     102 1 20946 my $self = shift;
1121 102         214 my $cron_entry = shift;
1122 102         163 my $time = shift;
1123            
1124 102 100       907 $cron_entry = [ split /\s+/,$cron_entry ] unless ref($cron_entry);
1125              
1126             # Expand and check entry:
1127             # =======================
1128 102 50 66     549 die "Exactly 5 or 6 columns has to be specified for a crontab entry ! (not ",
1129             scalar(@$cron_entry),")"
1130             if ($#$cron_entry != 4 && $#$cron_entry != 5);
1131            
1132 102         219 my @expanded;
1133             my $w;
1134            
1135 102         389 for my $i (0..$#$cron_entry)
1136             {
1137 560         1453 my @e = split /,/,$cron_entry->[$i];
1138 560         898 my @res;
1139             my $t;
1140 559         1252 while (defined($t = shift @e)) {
1141             # Subst "*/5" -> "0-59/5"
1142 1392         2319 $t =~ s|^\*(/.+)$|$RANGES[$i][0]."-".$RANGES[$i][1].$1|e;
  6         43  
1143            
1144 1392 100       2451 if ($t =~ m|^([^-]+)-([^-/]+)(/(.*))?$|)
1145             {
1146 34         202 my ($low,$high,$step) = ($1,$2,$4);
1147 34 100       128 $step = 1 unless $step;
1148 34 100       145 if ($low !~ /^(\d+)/)
1149             {
1150 7         22 $low = $ALPHACONV[$i]{lc $low};
1151             }
1152 34 100       124 if ($high !~ /^(\d+)/)
1153             {
1154 7         15 $high = $ALPHACONV[$i]{lc $high};
1155             }
1156 34 50 33     439 if (! defined($low) || !defined($high) || $low > $high || $step !~ /^\d+$/)
      33        
      33        
1157             {
1158 0         0 die "Invalid cronentry '",$cron_entry->[$i],"'";
1159             }
1160 34         74 my $j;
1161 34         129 for ($j = $low; $j <= $high; $j += $step)
1162             {
1163 783         1619 push @e,$j;
1164             }
1165             }
1166             else
1167             {
1168 1358 100       4209 $t = $ALPHACONV[$i]{lc $t} if $t !~ /^(\d+|\*)$/;
1169 1358 100 66     4266 $t = $LOWMAP[$i]{$t} if( defined $t && exists($LOWMAP[$i]{$t}) );
1170            
1171 1358 50 33     5586 die "Invalid cronentry '",$cron_entry->[$i],"'"
      66        
      33        
1172             if (!defined($t) || ($t ne '*' && ($t < $RANGES[$i][0] || $t > $RANGES[$i][1])));
1173 1358         3543 push @res,$t;
1174             }
1175             }
1176 559 100 100     2581 push @expanded, ($#res == 0 && $res[0] eq '*') ? [ "*" ] : [ sort {$a <=> $b} @res];
  979         1663  
1177             }
1178            
1179             # Check for strange bug
1180 101         509 $self->_verify_expanded_cron_entry($cron_entry,\@expanded);
1181              
1182             # Calculating time:
1183             # =================
1184 101   66     377 my $now = $time || time;
1185              
1186 101 100 100     395 if ($expanded[2]->[0] ne '*' && $expanded[4]->[0] ne '*')
1187             {
1188             # Special check for which time is lower (Month-day or Week-day spec):
1189 4         7 my @bak = @{$expanded[4]};
  4         12  
1190 4         10 $expanded[4] = [ '*' ];
1191 4         10 my $t1 = $self->_calc_time($now,\@expanded);
1192 4         11 $expanded[4] = \@bak;
1193 4         11 $expanded[2] = [ '*' ];
1194 4         12 my $t2 = $self->_calc_time($now,\@expanded);
1195 4 50       11 dbg "MDay : ",scalar(localtime($t1))," -- WDay : ",scalar(localtime($t2)) if $DEBUG;
1196 4 100       22 return $t1 < $t2 ? $t1 : $t2;
1197             }
1198             else
1199             {
1200             # No conflicts possible:
1201 97         336 return $self->_calc_time($now,\@expanded);
1202             }
1203             }
1204              
1205             =item $cron->set_timeshift($ts)
1206              
1207             Modify global time shift for all timetable. The timeshift is subbed from localtime
1208             to calculate next execution time for all scheduled jobs.
1209              
1210             ts parameter must be in seconds. Default value is 0. Negative values are allowed to
1211             shift time in the past.
1212              
1213             Returns actual timeshift in seconds.
1214              
1215             Example:
1216              
1217             $cron->set_timeshift(120);
1218              
1219             Will delay all jobs 2 minutes in the future.
1220              
1221             =cut
1222              
1223             sub set_timeshift
1224             {
1225 0     0 1 0 my $self = shift;
1226 0   0     0 my $value = shift || 0;
1227              
1228 0         0 $self->{timeshift} = $value;
1229 0         0 return $self->{timeshift};
1230             }
1231              
1232             # ==================================================
1233             # PRIVATE METHODS:
1234             # ==================================================
1235              
1236             # Build up executing queue and delete any
1237             # existing entries
1238             sub _rebuild_queue
1239             {
1240 21     21   54 my $self = shift;
1241 21         79 $self->{queue} = [ ];
1242             #dbg "TT: ",$#{$self->{time_table}};
1243 21         52 for my $id (0..$#{$self->{time_table}})
  21         111  
1244             {
1245 33         115 $self->_update_queue($id);
1246             }
1247             }
1248              
1249             # deeply copy an entry in the time table
1250             sub _deep_copy_entry
1251             {
1252 99     99   228 my ($self,$entry) = @_;
1253              
1254 99         181 my $args = [ @{$entry->{args}} ];
  99         352  
1255 99         909 my $copied_entry = { %$entry };
1256 99         273 $copied_entry->{args} = $args;
1257 99         433 return $copied_entry;
1258             }
1259              
1260             # Return an array with an arrayref of entry index and the time which should be
1261             # executed now
1262             sub _get_next_jobs {
1263 32     32   62 my $self = shift;
1264 32         60 my ($index,$time) = @{shift @{$self->{queue}}};
  32         60  
  32         114  
1265 32         138 my $indexes = [ $index ];
1266 32   100     81 while (@{$self->{queue}} && $self->{queue}->[0]->[1] == $time) {
  40         238  
1267 8         18 my $index = @{shift @{$self->{queue}}}[0];
  8         11  
  8         22  
1268 8         19 push @$indexes,$index;
1269             }
1270 32         132 return $indexes,$time;
1271             }
1272              
1273             # Execute a subroutine whose time has come
1274             sub _execute
1275             {
1276 36     36   110 my $self = shift;
1277 36         94 my $index = shift;
1278 36   33     182 my $cfg = shift || $self->{cfg};
1279 36   50     229 my $entry = $self->get_entry($index)
1280             || die "Internal: No entry with index $index found in ",Dumper([$self->list_entries()]);
1281              
1282 36         101 my $pid;
1283              
1284              
1285 36         117 my $log = $cfg->{log};
1286 36   50     402 my $loglevel = $cfg->{loglevel} || 0;
1287              
1288 36 100       295 unless ($cfg->{nofork})
1289             {
1290 7 100       8650 if ($pid = fork)
1291             {
1292             # Parent
1293 4 50 33     255 $log->(0,"Schedule::Cron - Forking child PID $pid") if $log && $loglevel <= 0;
1294             # Register PID
1295 4         216 $STARTEDCHILD{$pid} = 1;
1296 4         435 return;
1297             }
1298             }
1299            
1300             # Child
1301 32         356 my $dispatch = $entry->{dispatcher};
1302 32 50       361 die "No subroutine provided with $dispatch"
1303             unless ref($dispatch) eq "CODE";
1304 32         157 my $args = $entry->{args};
1305            
1306 32         115 my @args = ();
1307 32 100 66     393 if (defined($args) && defined($args->[0]))
1308             {
1309 3         50 push @args,@$args;
1310             }
1311              
1312              
1313 32 100 66     489 if ($log && $loglevel <= 0 || !$cfg->{nofork} && !$cfg->{processname} && !$cfg->{nostatus}) {
      33        
      100        
1314 7 50 66     210 my $args_label = (@args && $loglevel <= -1) ? " with (".join(",",$self->_format_args(@args)).")" : "";
1315             $0 = $self->_get_process_prefix()." Dispatched job $index$args_label"
1316 7 50 66     245 unless $cfg->{nofork} || $cfg->{processname} || $cfg->{nostatus};
      33        
1317 7 100 66     169 $log->(0,"Schedule::Cron - Starting job $index$args_label")
1318             if $log && $loglevel <= 0;
1319             }
1320 32         381 my $dispatch_result;
1321 32 100       154 if ($cfg->{catch})
1322             {
1323             # Evaluate dispatcher
1324             eval
1325 2         13 {
1326 2         11 $dispatch_result = &$dispatch(@args);
1327             };
1328 2 50       53 if ($@)
1329             {
1330 2 50 33     22 $log->(2,"Schedule::Cron - Error within job $index: $@")
1331             if $log && $loglevel <= 2;
1332             }
1333             }
1334             else
1335             {
1336             # Let dispatcher die if needed.
1337 30         365 $dispatch_result = &$dispatch(@args);
1338             }
1339            
1340 20 100       7001735 if($cfg->{after_job}) {
1341 1         3 my $job = $cfg->{after_job};
1342 1 50       5 if (ref($job) eq "CODE") {
1343             eval
1344 1         4 {
1345 1         5 &$job($dispatch_result,@args);
1346             };
1347 1 50       1756 if ($@)
1348             {
1349 0 0 0     0 $log->(2,"Schedule::Cron - Error while calling after_job callback with retval = $dispatch_result: $@")
1350             if $log && $loglevel <= 2;
1351             }
1352             } else {
1353 0 0 0     0 $log->(2,"Schedule::Cron - Invalid after_job callback, it's not a code ref (but ",$job,")")
1354             if $log && $loglevel <= 2;
1355             }
1356             }
1357              
1358 20 100 66     175 $log->(0,"Schedule::Cron - Finished job $index") if $log && $loglevel <= 0;
1359 20 100       2091 exit unless $cfg->{nofork};
1360             }
1361              
1362             # Update the scheduler queue with a new entry
1363             sub _update_queue
1364             {
1365 49     49   114 my $self = shift;
1366 49         90 my $index = shift;
1367 49         166 my $entry = $self->get_entry($index);
1368            
1369 49         266 my $new_time = $self->get_next_execution_time($entry->{time});
1370             # Check, whether next execution time is *smaller* than the current time.
1371             # This can happen during DST backflip:
1372 48         171 my $now = $self->_now();
1373 48 50       186 if ($new_time <= $now) {
1374 0 0       0 dbg "Adjusting time calculation because of DST back flip (new_time - now = ",$new_time - $now,")" if $DEBUG;
1375             # We are adding hours as long as our target time is in the future
1376 0         0 while ($new_time <= $now) {
1377 0         0 $new_time += 3600;
1378             }
1379             }
1380              
1381 48 50       160 dbg "Updating Queue: ",scalar(localtime($new_time)) if $DEBUG;
1382 48         116 $self->{queue} = [ sort { $a->[1] <=> $b->[1] } @{$self->{queue}},[$index,$new_time] ];
  51         198  
  48         373  
1383             #dbg "Queue now: ",Dumper($self->{queue});
1384             }
1385              
1386              
1387             # Out "now" which can be shifted if as argument
1388             sub _now {
1389 110     110   409 my $self = shift;
1390 110         843 return time + $self->{timeshift};
1391             }
1392              
1393             # The heart of the module.
1394             # calculate the next concrete date
1395             # for execution from a crontab entry
1396             sub _calc_time
1397             {
1398 105     105   193 my $self = shift;
1399 105         170 my $now = shift;
1400 105         152 my $expanded = shift;
1401              
1402 105 100       326 my $offset = ($expanded->[5] ? 1 : 60) + $self->{timeshift};
1403 105         3465 my ($now_sec,$now_min,$now_hour,$now_mday,$now_mon,$now_wday,$now_year) =
1404             (localtime($now+$offset))[0,1,2,3,4,6,5];
1405 105         352 $now_mon++;
1406 105         231 $now_year += 1900;
1407              
1408             # Notes on variables set:
1409             # $now_... : the current date, fixed at call time
1410             # $dest_...: date used for backtracking. At the end, it contains
1411             # the desired lowest matching date
1412              
1413 105         291 my ($dest_mon,$dest_mday,$dest_wday,$dest_hour,$dest_min,$dest_sec,$dest_year) =
1414             ($now_mon,$now_mday,$now_wday,$now_hour,$now_min,$now_sec,$now_year);
1415              
1416             # dbg Dumper($expanded);
1417              
1418             # Airbag...
1419 105         307 while ($dest_year <= $now_year + 1)
1420             {
1421 131 50       315 dbg "Parsing $dest_hour:$dest_min:$dest_sec $dest_year/$dest_mon/$dest_mday" if $DEBUG;
1422            
1423             # Check month:
1424 131 100       352 if ($expanded->[3]->[0] ne '*')
1425             {
1426 21 100       56 unless (defined ($dest_mon = $self->_get_nearest($dest_mon,$expanded->[3])))
1427             {
1428 8         15 $dest_mon = $expanded->[3]->[0];
1429 8         12 $dest_year++;
1430             }
1431             }
1432            
1433             # Check for day of month:
1434 131 100       336 if ($expanded->[2]->[0] ne '*')
1435             {
1436 28 100       48 if ($dest_mon != $now_mon)
1437             {
1438 12         22 $dest_mday = $expanded->[2]->[0];
1439             }
1440             else
1441             {
1442 16 100       44 unless (defined ($dest_mday = $self->_get_nearest($dest_mday,$expanded->[2])))
1443             {
1444             # Next day matched is within the next month. ==> redo it
1445 5         11 $dest_mday = $expanded->[2]->[0];
1446 5         8 $dest_mon++;
1447 5 50       12 if ($dest_mon > 12)
1448             {
1449 5         7 $dest_mon = 1;
1450 5         8 $dest_year++;
1451             }
1452 5 50       10 dbg "Backtrack mday: $dest_mday/$dest_mon/$dest_year" if $DEBUG;
1453 5         34 next;
1454             }
1455             }
1456             }
1457             else
1458             {
1459 103 100       309 $dest_mday = ($dest_mon == $now_mon ? $dest_mday : 1);
1460             }
1461            
1462             # Check for day of week:
1463 126 100       293 if ($expanded->[4]->[0] ne '*')
1464             {
1465 17         47 $dest_wday = $self->_get_nearest($dest_wday,$expanded->[4]);
1466 17 100       41 $dest_wday = $expanded->[4]->[0] unless $dest_wday;
1467            
1468 17         26 my ($mon,$mday,$year);
1469             # dbg "M: $dest_mon MD: $dest_mday WD: $dest_wday Y:$dest_year";
1470 17 100       37 $dest_mday = 1 if $dest_mon != $now_mon;
1471 17         85 my $t = parsedate(sprintf("%4.4d/%2.2d/%2.2d",$dest_year,$dest_mon,$dest_mday));
1472 17         3935 ($mon,$mday,$year) =
1473             (localtime(parsedate("$WDAYS[$dest_wday]",PREFER_FUTURE=>1,NOW=>$t-1)))[4,3,5];
1474 17         5008 $mon++;
1475 17         34 $year += 1900;
1476            
1477 17 50       44 dbg "Calculated $mday/$mon/$year for weekday ",$WDAYS[$dest_wday] if $DEBUG;
1478 17 100 66     73 if ($mon != $dest_mon || $year != $dest_year) {
1479 3 50       6 dbg "backtracking" if $DEBUG;
1480 3         7 $dest_mon = $mon;
1481 3         6 $dest_year = $year;
1482 3         5 $dest_mday = 1;
1483 3         18 $dest_wday = (localtime(parsedate(sprintf("%4.4d/%2.2d/%2.2d",
1484             $dest_year,$dest_mon,$dest_mday))))[6];
1485 3         707 next;
1486             }
1487            
1488 14         26 $dest_mday = $mday;
1489             }
1490             else
1491             {
1492 109 50       345 unless ($dest_mday)
1493             {
1494 0 0       0 $dest_mday = ($dest_mon == $now_mon ? $dest_mday : 1);
1495             }
1496             }
1497              
1498            
1499             # Check for hour
1500 123 100       334 if ($expanded->[1]->[0] ne '*')
1501             {
1502 46 100 100     181 if ($dest_mday != $now_mday || $dest_mon != $now_mon || $dest_year != $now_year)
      66        
1503             {
1504 27         42 $dest_hour = $expanded->[1]->[0];
1505             }
1506             else
1507             {
1508             #dbg "Checking for next hour $dest_hour";
1509 19 100       47 unless (defined ($dest_hour = $self->_get_nearest($dest_hour,$expanded->[1])))
1510             {
1511             # Hour to match is at the next day ==> redo it
1512 8         11 $dest_hour = $expanded->[1]->[0];
1513 8         49 my $t = parsedate(sprintf("%2.2d:%2.2d:%2.2d %4.4d/%2.2d/%2.2d",
1514             $dest_hour,$dest_min,$dest_sec,$dest_year,$dest_mon,$dest_mday));
1515 8         2302 ($dest_mday,$dest_mon,$dest_year,$dest_wday) =
1516             (localtime(parsedate("+ 1 day",NOW=>$t)))[3,4,5,6];
1517 8         2410 $dest_mon++;
1518 8         16 $dest_year += 1900;
1519 8         27 next;
1520             }
1521             }
1522             }
1523             else
1524             {
1525 77 100       181 $dest_hour = ($dest_mday == $now_mday ? $dest_hour : 0);
1526             }
1527             # Check for minute
1528 115 100       280 if ($expanded->[0]->[0] ne '*')
1529             {
1530 40 100 100     155 if ($dest_hour != $now_hour || $dest_mday != $now_mday || $dest_mon != $dest_mon || $dest_year != $now_year)
      66        
      66        
1531             {
1532 30         50 $dest_min = $expanded->[0]->[0];
1533             }
1534             else
1535             {
1536 10 100       30 unless (defined ($dest_min = $self->_get_nearest($dest_min,$expanded->[0])))
1537             {
1538             # Minute to match is at the next hour ==> redo it
1539 2         6 $dest_min = $expanded->[0]->[0];
1540 2         20 my $t = parsedate(sprintf("%2.2d:%2.2d:%2.2d %4.4d/%2.2d/%2.2d",
1541             $dest_hour,$dest_min,$dest_sec,$dest_year,$dest_mon,$dest_mday));
1542 2         1825 ($dest_hour,$dest_mday,$dest_mon,$dest_year,$dest_wday) =
1543             (localtime(parsedate(" + 1 hour",NOW=>$t))) [2,3,4,5,6];
1544 2         1071 $dest_mon++;
1545 2         7 $dest_year += 1900;
1546 2         10 next;
1547             }
1548             }
1549             }
1550             else
1551             {
1552 75 100 100     551 if ($dest_hour != $now_hour ||
      66        
1553             $dest_mday != $now_mday ||
1554             $dest_year != $now_year) {
1555 9         15 $dest_min = 0;
1556             }
1557             }
1558             # Check for seconds
1559 113 100       284 if ($expanded->[5])
1560             {
1561 61 100       159 if ($expanded->[5]->[0] ne '*')
1562             {
1563 45 100       111 if ($dest_min != $now_min)
1564             {
1565 7         16 $dest_sec = $expanded->[5]->[0];
1566             }
1567             else
1568             {
1569 38 100       141 unless (defined ($dest_sec = $self->_get_nearest($dest_sec,$expanded->[5])))
1570             {
1571             # Second to match is at the next minute ==> redo it
1572 7         26 $dest_sec = $expanded->[5]->[0];
1573 7         60 my $t = parsedate(sprintf("%2.2d:%2.2d:%2.2d %4.4d/%2.2d/%2.2d",
1574             $dest_hour,$dest_min,$dest_sec,
1575             $dest_year,$dest_mon,$dest_mday));
1576 7         2211 ($dest_min,$dest_hour,$dest_mday,$dest_mon,$dest_year,$dest_wday) =
1577             (localtime(parsedate(" + 1 minute",NOW=>$t))) [1,2,3,4,5,6];
1578 7         3473 $dest_mon++;
1579 7         17 $dest_year += 1900;
1580 7         28 next;
1581             }
1582             }
1583             }
1584             else
1585             {
1586 16 50       69 $dest_sec = ($dest_min == $now_min ? $dest_sec : 0);
1587             }
1588             }
1589             else
1590             {
1591 52         68 $dest_sec = 0;
1592             }
1593            
1594             # We did it !!
1595 106         711 my $date = sprintf("%2.2d:%2.2d:%2.2d %4.4d/%2.2d/%2.2d",
1596             $dest_hour,$dest_min,$dest_sec,$dest_year,$dest_mon,$dest_mday);
1597 106 50       278 dbg "Next execution time: $date ",$WDAYS[$dest_wday] if $DEBUG;
1598 106         555 my $result = parsedate($date, VALIDATE => 1);
1599             # Check for a valid date
1600 106 100       61859 if ($result)
1601             {
1602             # Valid date... return it!
1603 105         689 return $result;
1604             }
1605             else
1606             {
1607             # Invalid date i.e. (02/30/2008). Retry it with another, possibly
1608             # valid date
1609 1         4 my $t = parsedate($date); # print scalar(localtime($t)),"\n";
1610 1         266 ($dest_hour,$dest_mday,$dest_mon,$dest_year,$dest_wday) =
1611             (localtime(parsedate(" + 1 second",NOW=>$t))) [2,3,4,5,6];
1612 1         261 $dest_mon++;
1613 1         3 $dest_year += 1900;
1614 1         4 next;
1615             }
1616             }
1617              
1618             # Die with an error because we couldn't find a next execution entry
1619 0         0 my $dumper = new Data::Dumper($expanded);
1620 0         0 $dumper->Terse(1);
1621 0         0 $dumper->Indent(0);
1622              
1623 0         0 die "No suitable next execution time found for ",$dumper->Dump(),", now == ",scalar(localtime($now)),"\n";
1624             }
1625              
1626             # get next entry in list or
1627             # undef if is the highest entry found
1628             sub _get_nearest
1629             {
1630 121     121   206 my $self = shift;
1631 121         217 my $x = shift;
1632 121         165 my $to_check = shift;
1633 121         365 foreach my $i (0 .. $#$to_check)
1634             {
1635 482 100       979 if ($$to_check[$i] >= $x)
1636             {
1637 84         378 return $$to_check[$i] ;
1638             }
1639             }
1640 37         102 return undef;
1641             }
1642              
1643              
1644             # prepare a list of object for pretty printing e.g. in the process list
1645             sub _format_args {
1646 1     1   92 my $self = shift;
1647 1         11 my @args = @_;
1648 1         15 my $dumper = new Data::Dumper(\@args);
1649 1         51 $dumper->Terse(1);
1650 1         18 $dumper->Maxdepth(2);
1651 1         11 $dumper->Indent(0);
1652 1         21 return $dumper->Dump();
1653             }
1654              
1655             # get the prefix to use when setting $0
1656             sub _get_process_prefix {
1657 30     30   94 my $self = shift;
1658 30   50     165 my $prefix = $self->{cfg}->{processprefix} || "Schedule::Cron";
1659 30         1339 return $prefix;
1660             }
1661              
1662             # our very own debugging routine
1663             # ('guess everybody has its own style ;-)
1664             # Callers check $DEBUG on the critical path to save the computes
1665             # used to produce expensive arguments. Omitting those would be
1666             # functionally correct, but rather wasteful.
1667             sub dbg
1668             {
1669 15 50   15   31 if ($DEBUG)
1670             {
1671 0   0     0 my $args = join('',@_) || "";
1672 0         0 my $caller = (caller(1))[0];
1673 0         0 my $line = (caller(0))[2];
1674 0   0     0 $caller ||= $0;
1675 0 0       0 if (length $caller > 22)
1676             {
1677 0         0 $caller = substr($caller,0,10)."..".substr($caller,-10,10);
1678             }
1679 0         0 print STDERR sprintf ("%02d:%02d:%02d [%22.22s %4.4s] %s\n",
1680             (localtime)[2,1,0],$caller,$line,$args);
1681             }
1682             }
1683              
1684             # Helper method for reporting bugs concerning calculation
1685             # of execution bug:
1686             *bug = \&report_exectime_bug; # Shortcut
1687             sub report_exectime_bug
1688             {
1689 0     0 0 0 my $self = shift;
1690 0         0 my $endless = shift;
1691 0         0 my $time = time;
1692 0         0 my $inp;
1693 0         0 my $now = $self->_time_as_string($time);
1694 0         0 my $email;
1695              
1696             do
1697 0         0 {
1698 0         0 while (1)
1699             {
1700 0         0 $inp = $self->_get_input("Reference time\n(default: $now) : ");
1701 0 0       0 if ($inp)
1702             {
1703 0 0       0 parsedate($inp) || (print "Couldn't parse \"$inp\"\n",next);
1704 0         0 $now = $inp;
1705             }
1706 0         0 last;
1707             }
1708 0         0 my $now_time = parsedate($now);
1709            
1710 0         0 my ($next_time,$next);
1711 0         0 my @entries;
1712 0         0 while (1)
1713             {
1714 0         0 $inp = $self->_get_input("Crontab time (5 columns) : ");
1715 0         0 @entries = split (/\s+/,$inp);
1716 0 0       0 if (@entries != 5)
1717             {
1718 0         0 print "Invalid crontab entry \"$inp\"\n";
1719 0         0 next;
1720             }
1721             eval
1722 0         0 {
1723 0     0   0 local $SIG{ALRM} = sub { die "TIMEOUT" };
  0         0  
1724 0         0 alarm(60);
1725 0         0 $next_time = Schedule::Cron->get_next_execution_time(\@entries,$now_time);
1726 0         0 alarm(0);
1727             };
1728 0 0       0 if ($@)
1729             {
1730 0         0 alarm(0);
1731 0 0       0 if ($@ eq "TIMEOUT")
1732             {
1733 0         0 $next_time = -1;
1734             } else
1735             {
1736 0         0 print "Invalid crontab entry \"$inp\" ($@)\n";
1737 0         0 next;
1738             }
1739             }
1740            
1741 0 0       0 if ($next_time > 0)
1742             {
1743 0         0 $next = $self->_time_as_string($next_time);
1744             } else
1745             {
1746 0         0 $next = "Run into infinite loop !!";
1747             }
1748 0         0 last;
1749             }
1750            
1751 0         0 my ($expected,$expected_time);
1752 0         0 while (1)
1753             {
1754 0         0 $inp = $self->_get_input("Expected time : ");
1755 0 0       0 unless ($expected_time = parsedate($inp))
1756             {
1757 0         0 print "Couldn't parse \"$inp\"\n";
1758 0         0 next;
1759             }
1760 0         0 $expected = $self->_time_as_string($expected_time);
1761 0         0 last;
1762             }
1763            
1764             # Print out bug report:
1765 0 0       0 if ($expected eq $next)
1766             {
1767 0         0 print "\nHmm, seems that everything's ok, or ?\n\n";
1768 0         0 print "Calculated time: ",$next,"\n";
1769 0         0 print "Expected time : ",$expected,"\n";
1770             } else
1771             {
1772 0         0 print <
1773             Congratulation, you hit a bug.
1774              
1775             EOT
1776 0 0       0 $email = $self->_get_input("Your E-Mail Address (if available) : ")
1777             unless defined($email);
1778 0 0       0 $email = "" unless defined($email);
1779            
1780 0         0 print "\n","=" x 80,"\n";
1781 0         0 print <
1782             Please report the following lines
1783             to roland\@cpan.org
1784              
1785             EOT
1786 0         0 print "# ","-" x 78,"\n";
1787 0         0 print "Reftime: ",$now,"\n";
1788 0 0       0 print "# Reported by : ",$email,"\n" if $email;
1789 0         0 printf "%8s %8s %8s %8s %8s %s\n",@entries,$expected;
1790 0         0 print "# Calculated : \n";
1791 0         0 printf "# %8s %8s %8s %8s %8s %s\n",@entries,$next;
1792 0 0       0 unless ($endless)
1793             {
1794 0         0 require Config;
1795 0   0     0 my $vers = `uname -r 2>/dev/null` || $Config::Config{'osvers'} ;
1796 0         0 chomp $vers;
1797 0   0     0 my $osname = `uname -s 2>/dev/null` || $Config::Config{'osname'};
1798 0         0 chomp $osname;
1799 0         0 print "# OS: $osname ($vers)\n";
1800 0         0 print "# Perl-Version: $]\n";
1801 0         0 print "# Time::ParseDate-Version: ",$Time::ParseDate::VERSION,"\n";
1802             }
1803 0         0 print "# ","-" x 78,"\n";
1804             }
1805            
1806 0         0 print "\n","=" x 80,"\n";
1807             } while ($endless);
1808             }
1809              
1810             my ($input_initialized,$term);
1811             sub _get_input
1812             {
1813 0     0   0 my $self = shift;
1814 0         0 my $prompt = shift;
1815 17     17   112157 use vars qw($term);
  17         41  
  17         8871  
1816              
1817 0 0       0 unless (defined($input_initialized))
1818             {
1819 0         0 eval { require Term::ReadLine; };
  0         0  
1820            
1821 0 0       0 $input_initialized = $@ ? 0 : 1;
1822 0 0       0 if ($input_initialized)
1823             {
1824 0         0 $term = new Term::ReadLine;
1825 0         0 $term->ornaments(0);
1826             }
1827             }
1828            
1829 0 0       0 unless ($input_initialized)
1830             {
1831 0         0 print $prompt;
1832 0         0 my $inp = ;
1833 0         0 chomp $inp;
1834 0         0 return $inp;
1835             }
1836             else
1837             {
1838 0         0 chomp $prompt;
1839 0         0 my @prompt = split /\n/s,$prompt;
1840 0 0       0 if ($#prompt > 0)
1841             {
1842 0         0 print join "\n",@prompt[0..$#prompt-1],"\n";
1843             }
1844 0         0 my $inp = $term->readline($prompt[$#prompt]);
1845 0         0 return $inp;
1846             }
1847             }
1848              
1849             sub _time_as_string
1850             {
1851 0     0   0 my $self = shift;
1852 0         0 my $time = shift;
1853              
1854 0         0 my ($min,$hour,$mday,$month,$year,$wday) = (localtime($time))[1..6];
1855 0         0 $month++;
1856 0         0 $year += 1900;
1857 0         0 $wday = $WDAYS[$wday];
1858 0         0 return sprintf("%2.2d:%2.2d %2.2d/%2.2d/%4.4d %s",
1859             $hour,$min,$mday,$month,$year,$wday);
1860             }
1861              
1862              
1863             # As reported by RT Ticket #24712 sometimes,
1864             # the expanded version of the cron entry is flaky.
1865             # However, this occurs only very rarely and randomly.
1866             # So, we need to provide good diagnostics when this
1867             # happens
1868             sub _verify_expanded_cron_entry {
1869 101     101   169 my $self = shift;
1870 101         180 my $original = shift;
1871 101         176 my $entry = shift;
1872 101 50       388 die "Internal: Not an array ref. Orig: ",Dumper($original), ", expanded: ",Dumper($entry)," (self = ",Dumper($self),")"
1873             unless ref($entry) eq "ARRAY";
1874            
1875 101         158 for my $i (0 .. $#{$entry}) {
  101         306  
1876 559 50       1136 die "Internal: Part $i of entry is not an array ref. Original: ",
1877             Dumper($original),", expanded: ",Dumper($entry)," (self=",Dumper($self),")",
1878             unless ref($entry->[$i]) eq "ARRAY";
1879             }
1880             }
1881              
1882             =back
1883              
1884             =head1 DST ISSUES
1885              
1886             Daylight saving occurs typically twice a year: In the first switch, one hour is
1887             skipped. Any job which triggers in this skipped hour will be fired in the
1888             next hour. So, when the DST switch goes from 2:00 to 3:00 a job which is
1889             scheduled for 2:43 will be executed at 3:43.
1890              
1891             For the reverse backwards switch later in the year, the behavior is
1892             undefined. Two possible behaviors can occur: For jobs triggered in short
1893             intervals, where the next execution time would fire in the extra hour as well,
1894             the job could be executed again or skipped in this extra hour. Currently,
1895             running C in C would skip the extra job, in C it
1896             would execute a second time. The reason is the way how L
1897             calculates epoch times for dates given like C<02:50:00 2009/10/25>. Should it
1898             return the seconds since 1970 for this time happening 'first', or for this time
1899             in the extra hour ? As it turns out, L returns the epoch time
1900             of the first occurrence for C and for C it returns the second
1901             occurrence. Unfortunately, there is no way to specify I entry
1902             L should pick (until now). Of course, after all, this is
1903             obviously not L's fault, since a simple date specification
1904             within the DST back-switch period B ambiguous. However, it would be nice if
1905             the parsing behavior of L would be consistent across time
1906             zones (a ticket has be raised for fixing this). Then L's
1907             behavior within a DST backward switch would be consistent as well.
1908              
1909             Since changing the internal algorithm which worked now for over ten years would
1910             be too risky and I don't see any simple solution for this right now, it is
1911             likely that this I behavior will exist for some time. Maybe some
1912             hero is coming along and will fix this, but this is probably not me ;-)
1913              
1914             Sorry for that.
1915              
1916             =head1 AUTHORS
1917              
1918             Roland Huß
1919              
1920             Currently maintained by Nicholas Hubbard
1921              
1922             =head1 CONTRIBUTORS
1923              
1924             =over 4
1925              
1926             =item *
1927              
1928             Alexandr Ciornii
1929              
1930             =item *
1931              
1932             Andrew Danforth
1933              
1934             =item *
1935              
1936             Andy Ford
1937              
1938             =item *
1939              
1940             Bray Jones
1941              
1942             =item *
1943              
1944             Clinton Gormley
1945              
1946             =item *
1947              
1948             Eric Wilhelm
1949              
1950             =item *
1951              
1952             Frank Mayer
1953              
1954             =item *
1955              
1956             Jamie McCarthy
1957              
1958             =item *
1959              
1960             Loic Paillotin
1961              
1962             =item *
1963              
1964             Nicholas Hubbard
1965              
1966             =item *
1967              
1968             Peter Vary
1969              
1970             =item *
1971              
1972             Philippe Verdret
1973              
1974             =back
1975              
1976             =head1 COPYRIGHT AND LICENSE
1977              
1978             Copyright (c) 1999-2013 Roland Huß.
1979              
1980             Copyright (c) 2022-2023 Nicholas Hubbard.
1981              
1982             This library is free software; you can redistribute it and/or
1983             modify it under the same terms as Perl itself.
1984              
1985             =cut
1986              
1987             1;