File Coverage

blib/lib/Schedule/Cron.pm
Criterion Covered Total %
statement 432 611 70.7
branch 197 346 56.9
condition 98 178 55.0
subroutine 36 42 85.7
pod 12 14 85.7
total 775 1191 65.0


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