Cron.pm 57 KB

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