File Coverage

blib/lib/Schedule/At.pm
Criterion Covered Total %
statement 19 187 10.1
branch 2 48 4.1
condition 0 18 0.0
subroutine 4 35 11.4
pod 4 17 23.5
total 29 305 9.5


line stmt bran cond sub pod time code
1             package Schedule::At;
2              
3             require 5.004;
4              
5             # Copyright (c) 1997-2012 Jose A. Rodriguez. All rights reserved.
6             # This program is free software; you can redistribute it and/or modify it
7             # under the same terms as Perl itself.
8              
9 1     1   751 use vars qw($VERSION @ISA $TIME_FORMAT $SHELL);
  1         2  
  1         89  
10              
11             $VERSION = '1.15';
12              
13             $SHELL = '';
14              
15              
16             ###############################################################################
17             # Load configuration for this OS
18             ###############################################################################
19              
20 1     1   6 use Config;
  1         1  
  1         3027  
21              
22             my @configs = split (/\./, "$Config{'osname'}");
23             while (@configs) {
24             my $subName = 'AtCfg_' . join('_', @configs);
25             $subName =~ s/[^\w\d]/_/g;
26              
27             eval "&$subName"; # Call configuration subroutine
28             last if !$@;
29              
30             pop @configs;
31             }
32              
33             &AtCfg if $@; # Default configuration
34              
35             ###############################################################################
36             # Public subroutines
37             ###############################################################################
38              
39             $TIME_FORMAT = '%Q%H%M'; # Format for Date::Manip::DateUnix subroutine
40              
41             $TAGID = '##### Please, do not remove this Schedule::At TAG: ';
42              
43             sub add {
44 0     0 1 0 my %params = @_;
45              
46 0 0       0 my $command = $AT{($params{FILE} ? 'addFile' : 'add')};
47 0 0       0 return &$command($params{JOBID}) if ref($command) eq 'CODE';
48              
49 0         0 my $atTime = _std2atTime($params{TIME});
50            
51 0         0 $command =~ s/%TIME%/$atTime/g;
52 0         0 $command =~ s/%FILE%/$params{FILE}/g;
53              
54 0 0       0 if ($SHELL) {
55 0         0 $command = "SHELL=$SHELL $command";
56             }
57              
58 0 0       0 if ($params{FILE}) {
59 0         0 return (system($command) / 256);
60             } else {
61             # Ignore signal to die in case at commands fails
62 0         0 local $SIG{'PIPE'} = 'IGNORE';
63              
64 0         0 open (ATCMD, "| $command");
65 0 0       0 print ATCMD "$TAGID$params{TAG}\n" if $params{TAG};
66              
67 0         0 print ATCMD ref($params{COMMAND}) eq "ARRAY" ?
68 0 0       0 join("\n", @{$params{COMMAND}}) : $params{COMMAND};
69            
70 0         0 close (ATCMD);
71 0         0 return $?;
72             }
73              
74 0         0 0;
75             }
76              
77             sub remove {
78 0     0 1 0 my %params = @_;
79              
80 0 0       0 if ($params{JOBID}) {
81 0         0 my $command = $AT{'remove'};
82 0 0       0 return &$command(@_) if ref($command) eq 'CODE';
83              
84 0         0 $command =~ s/%JOBID%/$params{JOBID}/g;
85              
86 0         0 system($command) >> 8;
87             } else {
88 0 0       0 return if !defined $params{TAG};
89              
90 0         0 my %jobs = getJobs();
91 0         0 my %return;
92              
93 0         0 foreach my $job (values %jobs) {
94 0 0 0     0 next if !defined($job->{JOBID}) ||
95             !defined($job->{TAG});
96              
97 0 0 0     0 if ($job->{JOBID} && $params{TAG} eq $job->{TAG}) {
98 0         0 $return{$job->{JOBID}} =
99             remove(JOBID => "$job->{JOBID}")
100             }
101             }
102              
103 0         0 return \%return
104             }
105             }
106              
107             sub getJobs {
108 1     1 1 600 my %param = @_;
109              
110 1         2 my %jobs;
111            
112 1         2 my $command = $AT{'getJobs'};
113 1 50       4 return &$command(@_) if ref($command) eq 'CODE';
114              
115 1 50       3675 open (ATCMD, "$command |")
116             or die "Schedule::At: Can't exec getJobs command: $!\n";
117 0         0 line: while (defined (my $atLine = )) {
118 0 0       0 if (defined $AT{'headings'}) {
119 0         0 foreach my $head (@{$AT{'headings'}}) {
  0         0  
120 0 0       0 next line if $atLine =~ /$head/;
121             }
122             }
123              
124 0         0 chomp $atLine;
125              
126 0         0 my %atJob;
127 0         0 ($atJob{JOBID}, $atJob{TIME})
128 0         0 = &{$AT{'parseJobList'}}($atLine);
129 0         0 $atJob{TAG} = _getTag(JOBID => $atJob{JOBID});
130 0 0 0     0 next if $param{TAG} &&
      0        
131             (!$atJob{TAG} || $atJob{TAG} ne $param{TAG});
132 0 0 0     0 next if $param{JOBID} &&
      0        
133             (!$atJob{JOBID} || $atJob{JOBID} ne $param{JOBID});
134 0         0 $jobs{$atJob{JOBID}} = \%atJob;
135             }
136 0         0 close (ATCMD);
137              
138 0         0 %jobs;
139             }
140              
141             sub readJobs {
142 0     0 1 0 my %jobs = getJobs(@_);
143              
144 0         0 my @job_ids = map { $_->{JOBID} } values %jobs;
  0         0  
145              
146 0         0 my %content;
147 0         0 foreach my $jobid (@job_ids) {
148 0         0 $content{$jobid} = _readJob(JOBID => $jobid);
149             }
150              
151             %content
152 0         0 }
153              
154             ###############################################################################
155             # Private subroutines
156             ###############################################################################
157              
158             sub _readJob {
159 0     0   0 my %params = @_;
160              
161 0         0 my $command = $AT{'getCommand'};
162 0 0       0 $command = &$command($params{JOBID}) if ref($command) eq 'CODE';
163              
164 0         0 $command =~ s/%JOBID%/$params{JOBID}/g;
165              
166 0         0 local $/ = undef; # slurp mode
167 0 0       0 open (JOB, "$command")
168             or die "Can't open $command: $!\n";
169 0         0 my $job = ;
170 0         0 close (JOB);
171              
172 0         0 $job
173             }
174              
175             sub _getTag {
176 0     0   0 my %params = @_;
177              
178 0         0 my $job = _readJob(@_);
179 0         0 $job =~ /$TAGID(.*)$/m;
180 0         0 return $1;
181              
182 0         0 my @job = split("\n", _readJob(@_));
183 0         0 foreach my $commandLine (@job) {
184 0 0       0 return $1 if $commandLine =~ /$TAGID(.*)$/;
185             }
186              
187 0         0 undef;
188             }
189              
190             sub _std2atTime {
191 0     0   0 my ($stdTime) = @_;
192              
193             # StdTime: YYYYMMDDHHMM
194 0         0 my ($year, $month, $day, $hour, $mins) =
195             $stdTime =~ /(....)(..)(..)(..)(..)/;
196              
197 0         0 my $timeFormat = $AT{'timeFormat'};
198 0 0       0 return &$timeFormat($year, $month, $day, $hour, $mins)
199             if ref($timeFormat) eq 'CODE';
200              
201 0         0 $timeFormat =~ s/%YEAR%/$year/g;
202 0         0 $timeFormat =~ s/%MONTH%/$month/g;
203 0         0 $timeFormat =~ s/%DAY%/$day/g;
204 0         0 $timeFormat =~ s/%HOUR%/$hour/g;
205 0         0 $timeFormat =~ s/%MINS%/$mins/g;
206              
207 0         0 $timeFormat;
208             }
209              
210             =head1 NAME
211              
212             Schedule::At - OS independent interface to the Unix 'at' command
213              
214             =head1 SYNOPSIS
215              
216             require Schedule::At;
217              
218             Schedule::At::add(TIME => $string, COMMAND => $string [, TAG =>$string]);
219             Schedule::At::add(TIME => $string, COMMAND => \@array [, TAG =>$string]);
220             Schedule::At::add(TIME => $string, FILE => $string)
221              
222             %jobs = Schedule::At::getJobs();
223             %jobs = Schedule::At::getJobs(JOBID => $string);
224             %jobs = Schedule::At::getJobs(TAG => $string);
225              
226             Schedule::At::readJobs(JOBID => $string);
227             Schedule::At::readJobs(TAG => $string);
228              
229             Schedule::At::remove(JOBID => $string);
230             Schedule::At::remove(TAG => $string);
231              
232             =head1 DESCRIPTION
233              
234             This modules provides an OS independent interface to 'at', the Unix
235             command that allows you to execute commands at a specified time.
236              
237             =over 4
238              
239             =item Schedule::At::add
240              
241             Adds a new job to the at queue.
242              
243             You have to specify a B
244             a common format: YYYYMMDDHHmm where B is the year (4 digits), B
245             the month (01-12), B
is the day (01-31), B the hour (00-23) and
246             B the minutes.
247              
248             The command is passed with the B or the B parameter.
249             B can be used to pass the command as an string, or an array of
250             commands, and B to read the commands from a file.
251              
252             The optional parameter B serves as an application specific way to
253             identify a job or a set of jobs.
254              
255             Returns 0 on success or a value != 0 if an error occurred.
256              
257             =item Schedule::At::readJobs
258              
259             Read the job content identified by the B or B parameters.
260              
261             Returns a hash of JOBID => $string where $string is the the job
262             content. As the operating systems usually add a few environment settings,
263             the content is longer than the command provided when adding the job.
264              
265             =item Schedule::At::remove
266              
267             Remove an at job.
268              
269             You identify the job to be deleted using the B parameter (an
270             opaque string returned by the getJobs subroutine). You can also specify
271             a job or a set of jobs to delete with the B parameter, removing
272             all the jobs that have the same tag (as specified with the add subroutine).
273              
274             Used with JOBID, returns 0 on success or a value != 0 if an error occurred.
275             Used with TAG, returns a hash reference where the keys are the JOBID of
276             the jobs found and the values indicate the success of the remove operation.
277              
278             =item Schedule::At::getJobs
279              
280             Called with no params returns a hash with all the current jobs or
281             dies if an error has occurred.
282             It's possible to specify the B or B parameters so only matching
283             jobs are returned.
284             For each job the key is a JOBID (an OS dependent string that shouldn't be
285             interpreted), and the value is a hash reference.
286              
287             This hash reference points to a hash with the keys:
288              
289             =over 4
290              
291             =item TIME
292              
293             An OS dependent string specifying the time to execute the command
294              
295             =item TAG
296              
297             The tag specified in the Schedule::At::add subroutine
298              
299             =back
300              
301             =back
302              
303             =head1 Configuration Variables
304              
305             =over 4
306              
307             =item *
308              
309             $Schedule::At::SHELL
310              
311             This variable can be used to specify shell for execution of the scheduled command.
312             Can be useful for example when scheduling from CGI script and the account of the user under which httpd runs
313             is locked by using '/bin/false' or similar as a shell.
314              
315             =back
316              
317              
318             =head1 EXAMPLES
319              
320             use Schedule::At;
321              
322             # 1
323             Schedule::At::add (TIME => '199801181530', COMMAND => 'ls',
324             TAG => 'ScheduleAt');
325             # 2
326             @cmdlist = ("ls", "echo hello world");
327              
328             Schedule::At::add (TIME => '199801181630', COMMAND => \@cmdlist,
329             TAG => 'ScheduleAt');
330             # 3
331             Schedule::At::add (TIME => '199801181730', COMMAND => 'df');
332              
333             # This will remove #1 and #2 but no #3
334             Schedule::At::remove (TAG => 'ScheduleAt');
335              
336             my %atJobs = Schedule::At::getJobs();
337             foreach my $job (values %atJobs) {
338             print "\t", $job->{JOBID}, "\t", $job->{TIME}, ' ',
339             ($job->{TAG} || ''), "\n";
340             }
341              
342             =head1 AUTHOR
343              
344             Jose A. Rodriguez (jose AT rodriguez.jp)
345              
346             =cut
347              
348             ###############################################################################
349             # OS dependent code
350             ###############################################################################
351              
352             sub AtCfg {
353             # Currently the default configuration just aborts
354 0     0 0 0 die "SORRY! There is no config for this OS.\n";
355             }
356              
357             sub AtCfg_solaris {
358 0     0 0 0 $AT{'add'} = 'at %TIME% 2> /dev/null';
359 0         0 $AT{'addFile'} = 'at -f %FILE% %TIME% 2> /dev/null';
360             $AT{'timeFormat'} = sub {
361 0     0   0 my ($year, $month, $day, $hour, $mins) = @_;
362              
363 0         0 my @months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
364             'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
365              
366 0         0 "$hour:$mins " . $months[$month-1] . " $day, $year";
367 0         0 };
368 0         0 $AT{'remove'} = 'at -r %JOBID%';
369 0         0 $AT{'getJobs'} = 'at -l';
370 0         0 $AT{'headings'} = [];
371 0         0 $AT{'getCommand'} = '/usr/spool/cron/atjobs/%JOBID%';
372             # Ignore "user = xxx" when executed by root
373 0     0   0 $AT{'parseJobList'} = sub { $_[0] =~ /^.*(\d{10}.a)\s+(.*)$/ };
  0         0  
374             }
375              
376             sub AtCfg_sunos {
377 0     0 0 0 &AtCfg_solaris;
378             $AT{'getCommand'} = sub {
379 0     0   0 my ($jobid) = @_;
380              
381 0         0 for my $filename (glob('/usr/spool/cron/atjobs/*')) {
382 0 0       0 return $filename if (stat($filename))[1] == $jobid;
383             }
384              
385 0         0 undef;
386             }
387 0         0 }
388              
389             sub AtCfg_dec_osf {
390 0     0 0 0 &AtCfg_solaris;
391             # josear.1137594600.a Wed Jan 18 15:30:00 2006
392 0     0   0 $AT{'parseJobList'} = sub { $_[0] =~ /^(\S+)\s+(.*)$/ };
  0         0  
393             }
394              
395             sub AtCfg_hpux {
396 0     0 0 0 $AT{'add'} = 'at %TIME% 2> /dev/null';
397 0         0 $AT{'addFile'} = 'at -f %FILE% %TIME% 2> /dev/null';
398 0         0 $AT{'timeFormat'} = '%HOUR%:%MINS% %MONTH%/%DAY%/%YEAR%';
399 0         0 $AT{'remove'} = 'at -r %JOBID%';
400 0         0 $AT{'getJobs'} = 'at -l';
401 0         0 $AT{'headings'} = [];
402 0         0 $AT{'getCommand'} = '/usr/spool/cron/atjobs/%JOBID%';
403 0     0   0 $AT{'parseJobList'} = sub { $_[0] =~ /^(\S+)\s+(.*)$/ };
  0         0  
404             }
405              
406             sub AtCfg_linux {
407 1     1 0 2 $AT{'add'} = 'at %TIME% 2> /dev/null';
408 1         3 $AT{'addFile'} = 'at -f %FILE% %TIME% 2> /dev/null';
409 1         2 $AT{'timeFormat'} = '%HOUR%:%MINS% %MONTH%/%DAY%/%YEAR%';
410 1         2 $AT{'remove'} = 'atrm %JOBID%';
411 1         2 $AT{'getJobs'} = 'atq';
412 1         2 $AT{'headings'} = ['Date'];
413 1         3 $AT{'getCommand'} = 'at -c %JOBID% |';
414             # 1 2003-01-18 15:30 a josear
415             # 10 Tue Jan 31 10:00:00 2012 a josear (debian)
416             $AT{'parseJobList'} = sub {
417 0     0     my @fields = split("\t", $_[0]);
418 0 0         my $date = substr($fields[1], 0,
419             ($fields[1] =~ /^d/) ? 16 : 24);
420 0           ($fields[0], $date)
421 1         38 };
422             }
423              
424             sub AtCfg_aix {
425 0     0 0   $AT{'add'} = 'at -t %TIME% 2> /dev/null';
426 0           $AT{'addFile'} = 'at -f %FILE% %TIME% 2> /dev/null';
427 0           $AT{'timeFormat'} = '%YEAR%%MONTH%%DAY%%HOUR%%MINS%';
428 0           $AT{'remove'} = 'at -r %JOBID%';
429 0           $AT{'getJobs'} = 'at -l';
430 0           $AT{'headings'} = [];
431              
432             # Only for privileged users (group system), so use alternate command
433             #$AT{'getCommand'} = '/usr/spool/cron/atjobs/%JOBID%';
434 0           $AT{'getCommand'} = 'at -lv %JOBID% |tail +4 |';
435              
436 0     0     $AT{'parseJobList'} = sub { $_[0] =~ /^(\S+)\s+(.*)$/ };
  0            
437             }
438              
439             sub AtCfg_dynixptx {
440 0     0 0   $AT{'add'} = 'at %TIME% 2> /dev/null';
441 0           $AT{'addFile'} = 'at -f %FILE% %TIME% 2> /dev/null';
442             $AT{'timeFormat'} = sub {
443 0     0     my ($year, $month, $day, $hour, $mins) = @_;
444              
445 0           my @months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
446             'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
447              
448 0           "$hour:$mins " . $months[$month-1] . " $day, $year";
449 0           };
450 0           $AT{'remove'} = 'at -r %JOBID%';
451 0           $AT{'getJobs'} = 'at -l';
452 0           $AT{'headings'} = [];
453 0           $AT{'getCommand'} = '/usr/spool/cron/atjobs/%JOBID%';
454             $AT{'parseJobList'} = sub {
455 0     0     my $user = scalar getpwuid $<;
456 0 0         if ($user eq 'root') {
457 0           $_[0] =~ /^\s*\S+\s*\S+\s*\S+\s*(\S+)\s+(.*)$/
458             }
459             else {
460 0           $_[0] =~ /(\S+)\s+(.*)$/
461             }
462 0           };
463             }
464              
465             sub AtCfg_freebsd {
466 0     0 0   $AT{'add'} = 'at %TIME% 2> /dev/null';
467 0           $AT{'addFile'} = 'at -f %FILE% %TIME% 2> /dev/null';
468             $AT{'timeFormat'} = sub {
469 0     0     my ($year, $month, $day, $hour, $mins) = @_;
470              
471 0           my @months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
472             'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
473              
474 0           "$hour:$mins " . $months[$month-1] . " $day $year";
475 0           };
476 0           $AT{'remove'} = 'atrm %JOBID%';
477 0           $AT{'getJobs'} = 'at -l';
478 0           $AT{'headings'} = ['Date', 'Owner', 'Queue', 'Job'];
479 0           $AT{'getCommand'} = 'at -c %JOBID% | ';
480 0     0     $AT{'parseJobList'} = sub { $_[0] =~ s/^\s*(.+)\s+\S+\s+\S+\s+(\d+)$/$2_$1/; $_[0] =~ /^(.+)_(.+)$/ };
  0            
  0            
481             }
482              
483             sub AtCfg_netbsd {
484 0     0 0   &AtCfg_freebsd;
485             }
486              
487             sub AtCfg_dragonfly {
488 0     0 0   &AtCfg_freebsd;
489             }
490              
491             sub AtCfg_openbsd {
492 0     0 0   &AtCfg_freebsd;
493 0           $AT{'headings'} = [];
494 0     0     $AT{'parseJobList'} = sub { $_[0] =~ /^.*(\d{10}.c)\s+(.*)$/ };
  0            
495             }
496              
497             # Mac OS X (darwin, tiger)
498             sub AtCfg_darwin {
499 0     0 0   $AT{'add'} = 'at %TIME% 2> /dev/null';
500 0           $AT{'addFile'} = 'at -f %FILE% %TIME% 2> /dev/null';
501 0           $AT{'timeFormat'} = '%HOUR%:%MINS% %MONTH%/%DAY%/%YEAR%';
502 0           $AT{'remove'} = 'atrm %JOBID%';
503 0           $AT{'getJobs'} = 'atq';
504 0           $AT{'headings'} = ['Job','Date'];
505 0           $AT{'getCommand'} = 'at -c %JOBID% | ';
506             # 74 Wed Jan 18 15:32:00 2006
507             $AT{'parseJobList'} = sub {
508 0     0     my @fields = split("\t", $_[0]);
509 0           ($fields[0], substr($fields[1], 0, 16))
510 0           };
511             }