File Coverage

blib/lib/Net/Spooler.pm
Criterion Covered Total %
statement 27 217 12.4
branch 0 108 0.0
condition 0 20 0.0
subroutine 9 30 30.0
pod 0 17 0.0
total 36 392 9.1


line stmt bran cond sub pod time code
1             # -*- perl -*-
2             #
3             # Net::Spooler - A Perl extension for writing spooling daemons
4             #
5             # Copyright (C) 1999 Jochen Wiedmann
6             # Am Eisteich 9
7             # 72555 Metzingen
8             # Germany
9             #
10             # E-Mail: joe@ispsoft.de
11             #
12             # All rights reserved.
13             #
14             # You may distribute under the terms of either the GNU General Public
15             # License or the Artistic License, as specified in the Perl README file.
16             #
17             ############################################################################
18              
19 2     2   877 use strict;
  2         3  
  2         77  
20              
21 2     2   1218 use Net::Daemon ();
  2         191675  
  2         35  
22 2     2   25 use File::Spec ();
  2         3  
  2         23  
23 2     2   8 use Symbol ();
  2         4  
  2         30  
24 2     2   5670 use Data::Dumper ();
  2         16833  
  2         62  
25 2     2   22 use Fcntl ();
  2         3  
  2         27  
26 2     2   3444 use Safe ();
  2         159169  
  2         144  
27 2     2   23 use Cwd ();
  2         4  
  2         3450  
28              
29             package Net::Spooler;
30              
31             $Net::Spooler::VERSION = '0.02';
32             @Net::Spooler::ISA = qw(Net::Daemon);
33              
34              
35             sub Options ($) {
36 0     0 0   my $opts = shift()->SUPER::Options();
37 0           $opts->{'admin'} = { 'template' => 'admin=s',
38             'description' => '--admin= '
39             . "Admins email address"
40             };
41 0           $opts->{'expiretime'} = { 'template' => 'expiretime=i',
42             'description' => '--expiretime= '
43             . "Spool files expire after seconds"
44             };
45 0           $opts->{'maxsize'} = { 'template' => 'maxsize=s',
46             'description' => '--maxsize= '
47             . "Refuse files larger than bytes"
48             };
49 0           $opts->{'processtimeout'} = { 'template' => 'processtimeout=i',
50             'description' => '--processtimeout= '
51             . "Stop processing files after seconds"
52             };
53 0           $opts->{'spool-dir'} = { 'template' => 'spool-dir=s',
54             'description' => '--spool-dir= '
55             . "Directory for creating spool files"
56             };
57 0           $opts->{'spool-command'} = { 'template' => 'spool-command=s',
58             'description' => '--spool-command= '
59             . "Run for processing spool files"
60             };
61 0           $opts;
62             }
63              
64              
65             =pod
66              
67             =head1 NAME
68              
69             Net::Spooler - A Perl extension for writing spooling daemons
70              
71              
72             =head1 SYNOPSIS
73              
74             # Create a subclass of Net::Spooler
75             use Net::Spooler;
76             package MySpooler;
77             @MySpooler::ISA = qw(Net::Spooler);
78              
79             # Inherit everything, except a single method:
80             sub ProcessFile {
81             my $self = shift; my $file = shift;
82              
83             # Try to process the file here
84             ...
85              
86             # Raise an exception, if something went wrong:
87             die "Failed: $!" unless Something();
88              
89             # Return to indicate sucess.
90             }
91              
92             # Create and run the spooler
93             package main;
94             my $spooler = Net::Spooler->new(
95             'spool-dir' => '/var/myspooler'
96             );
97             $spooler->Bind();
98              
99              
100             =head1 DESCRIPTION
101              
102             This package contains a spooling daemon, in other words a process,
103             that accepts files from an outside source (currently a Unix or
104             TCP/IP socket), stores them in a spooling directory and processes
105             them.
106              
107             The package is implemented as an abstract base class: It is not
108             usefull in itself, but you can get your spooling daemon easily by
109             deriving a concrete subclass from C. In the best case
110             you can inherit everything and overwrite just a single method, the
111             I method, which attempts to process a single file
112             from the spooling directory.
113              
114             C is in turn derived from the C package, thus
115             it borrows class design, in particular methods and attributes, from
116             C. See L for details on this superclass.
117              
118             However, there are a few additions to C:
119              
120              
121             =head2 Attributes
122              
123             Like in C, attributes can be set via the command line,
124             in the config file or as constructor arguments (order descending from
125             most important). And remember, that you can use the C
126             attributes too! See L.
127              
128             =over 8
129              
130             =item I (B<--admin=EemailE)
131              
132             The administrators email address. From time to time it may happen,
133             that the admin receives an email in case of problems.
134              
135             =item I (B<--expiretime=EtimeE>)
136              
137             If processing a file fails repeatedly, the file may finally expire.
138             This means that the file will be removed from the spool directory
139             and a message is sent to the administrator.
140              
141             The default value are 432000 seconds (5 days). A value of 0 means
142             that expiration never happens.
143              
144             Example: Expire after 3 days.
145              
146             --expiretime=259200
147              
148             =item I (B<--maxsize=EmaxsizeE>)
149              
150             By default the maximum size of a file is restricted to 100000 bytes
151             and larger files will be rejected. This option is changing the size,
152             a value of 0 means disabling the limitation.
153              
154             Example: Disable max size
155              
156             --maxsize=0
157              
158             =item I (B<--processtimeout=EtimeoutE>)
159              
160             If processing a single file may result in an endless loop, or simply
161             run too long, then you may specify a timeout. The daemon will raise
162             a signal after the given amount of seconds and stop processing the
163             file, as if the method C raised an exception.
164              
165             The default value is 0 seconds which means that no timeout is used.
166              
167             Example: Use a timeout of 30 seconds.
168              
169             --processtimeout=30
170              
171             =item I (B<--loop-timeout=EtimeE>)
172              
173             If processing a file failed, the spooler will reprocess the file
174             later by forking a child process after the given amount of
175             seconds, by default 300 seconds (5 minutes). This child process
176             will run through all scheduled file
177              
178             =item I (B<--spool-dir=EdirE>)
179              
180             If the daemon accepts files, they are stored in the I.
181             There's no default, you must set this attribute.
182              
183             Example: Use F as a spool directory.
184              
185             --spool-dir=/var/myspooler
186              
187             =item I
188              
189             This attribute is for internal use only. It contains an hash ref, the
190             keys being temporary file names to be removed later.
191              
192             =back
193              
194              
195             =head2 Methods
196              
197             As already said, the C package inherits from C.
198             All methods of the superclass are still valid in C, in
199             particular access control and the like. See L for details.
200              
201             =over 8
202              
203             =item Processing a file
204              
205             $self->ProcessFile($file)
206              
207             (Instance method) Called for processing a single file. This is typically
208             the only method you have to overwrite.
209              
210             The method raises an exception in case of errors. If an exception is
211             raised, the scheduler will later retry to process the file until it
212             expires. See the I and I attributes above.
213              
214             If processing a file exceeds the I (see above), then
215             the scheduler will cancel processing the method and continue as if it
216             raised an exception. (This timeout can be disabled by setting it to 0,
217             the default value.)
218              
219             If the method returns without raising an exception, then the scheduler
220             assumes that the file was processed successfully and remove it from
221             the spool directory.
222              
223             =cut
224              
225 0     0 0   sub StatusOk {
226             }
227 0     0 0   sub StatusError {
228             }
229 0     0 0   sub StatusReject {
230             }
231              
232             sub CommandFile {
233 0     0 0   my($self, $file, $ctrl) = @_;
234              
235 0           my $command = $self->{'spool-command'};
236 0           $command =~ s/\$\@file\$/$file/sg;
237 0           $command =~ s/\$file\$/quotemeta($file)/seg;
  0            
238 0           $command =~ s/\$\@control->([\-\w]+)\$/$ctrl>{$1}/seg;
  0            
239 0           $command =~ s/\$control->([\-\w]+)\$/quotemeta($ctrl->{$1})/seg;
  0            
240 0           $self->Debug("Processing $file: $command");
241 0           my $ph = Symbol::gensym();
242 0 0         open($ph, "$command 2>>errors.log |")
243             or die "Failed to create pipe to command $command: $!";
244 0           my $output;
245             my $line;
246 0           while (defined($line = <$ph>)) {
247 0           $output .= $line;
248 0 0         if ($line =~ /^\s*status\:\s*(.*?)\s*$/i) {
249 0           my $status = lc $1;
250 0 0         if ($status eq 'ok') {
    0          
    0          
251 0           $self->StatusOk($file, $ctrl);
252 0           return 1;
253             } elsif ($status eq 'error') {
254 0           last;
255             } elsif ($status eq 'reject') {
256 0           $self->StatusReject($file, $ctrl);
257 0           while (defined($line = <$ph>)) {
258 0           $output .= $line;
259             }
260 0           close $ph;
261 0 0         open($ph, ">>errors.log") and
262             (print $ph "\n" . localtime() . ", Reject while processing $file:\n$output");
263 0           return 0;
264             }
265             }
266             }
267              
268 0           $self->StatusError($file, $ctrl);
269 0 0         if (defined $line) {
270 0           while (defined($line = <$ph>)) {
271 0           $output .= $line;
272             }
273             }
274 0           close $ph;
275 0 0         open($ph, ">>errors.log") and
276             (print $ph "\n" . localtime() . ", Error while processing $file:\n$output");
277 0           die "Failed to process $file: $output";
278             }
279              
280              
281             =pod
282              
283             =item Choosing file names
284              
285             my $sfile = $self->SequenceFile();
286             my $seq = $self->Sequence($sfile);
287             my $dfile = $self->DataFile($seq);
288             my $cfile = $self->ControlFile($seq);
289              
290              
291             (Instance methods) If the daemon receives a new file, it has to
292             choose a name for it. These names are constructed as follows:
293              
294             First of all, a so-called sequence number is generated by calling
295             the method I. By default these are the numbers 1, 2, 3, ...
296             in 8 hex digits (00000001, 00000002, 00000003, ...). The last
297             generated sequence number is always stored in the sequence file
298             (by default F<$spool-dir/.sequence>, set by calling the I
299             method).
300              
301             Two files are generated for processing the file: The I
302             is the unmodified file, as received by the client. The I
303             contains information used internally by C, for example
304             the time and date of spooling this file. By default the names
305             F<$spool-dir/$seq.dat> and F<$spool-dir/$seq.ctl> are used, generated
306             by calling the methods I and I. Temporary
307             file names are derived by adding the suffix F<.tmp>.
308              
309             Typically you rarely need to overwrite these methods.
310              
311             =back
312              
313             =cut
314              
315             sub SequenceFile {
316 0     0 0   my $self = shift;
317 0           ".sequence";
318             }
319              
320             sub Sequence {
321 0     0 0   my $self = shift; my $file = shift;
  0            
322 0           my $fh = Symbol::gensym();
323 0 0         sysopen($fh, $file, Fcntl::O_RDWR()|Fcntl::O_CREAT(), 0644)
324             or die "Failed to open sequence file $file for append: $!";
325 0 0         flock($fh, Fcntl::LOCK_EX())
326             or die "Failed to lock sequence file $file: $!";
327 0           my $line = <$fh>;
328 0 0 0       my $num = ((defined($line) && $line =~ /(\d+)/) ? $1 : 0) + 1;
329 0 0         seek($fh, 0, 0)
330             or die "Failed to beginning of sequence file $file: $!";
331 0           my $sline = "$num\n";
332 0 0         (print $fh $sline)
333             or die "Failed to write to sequence file $file: $!";
334 0 0         truncate($fh, length($sline))
335             or die "Failed to truncate sequence file $file: $!";
336 0           close($fh); # *No* unlock, this is done automatically as soon as
337             # the destructur of $fh is called!
338 0           $num;
339             }
340              
341             sub DataFile {
342 0     0 0   my $self = shift; my $seq = shift;
  0            
343 0           "$seq.dat";
344             }
345              
346             sub ControlFile {
347 0     0 0   my $self = shift; my $seq = shift;
  0            
348 0           "$seq.ctl";
349             }
350              
351             sub IsControlFile {
352 0     0 0   my $self = shift; my $file = shift;
  0            
353 0 0         return ($file =~ s/\.ctl$/.dat/) ? $file : undef;
354             }
355              
356             =pod
357              
358             =item Accepting a file from the client
359              
360             $self->ReadFile($socket, $fh, $file, $control);
361              
362             (Instance method) This method is actually reading the file $file from
363             the socket $socket. The file is already opened and the method must use
364             the file handle $fh for writing into $file. (The file name is passed
365             for creating error messages only.)
366              
367             The method may store arbitrary data in the hash ref $control: This
368             hash ref is stored in the control file later.
369              
370             The default implementation is accepting a raw file on the socket. You
371             should overwrite the method, if you are accepting structured data,
372             for example 4 bytes of file size and then the raw file. However, if
373             you do overwrite this method, you should consider the I
374             attribute. (See above.)
375              
376             A Perl exception is raised in case of problems.
377              
378             =cut
379              
380             sub ReadFile {
381 0     0 0   my($self, $socket, $fh, $file, $control) = @_;
382 0           my $size = 0;
383 0           my($buf, $len);
384              
385 0           while ($len = read($socket, $buf, 1024)) {
386 0           $size += $len;
387 0 0 0       die "Maximum size of $self->{'maxsize'} exceeded."
388             if ($self->{'maxsize'} and $size > $self->{'maxsize'});
389 0 0         (print $fh $buf)
390             or die "Failed to write into data file $file: $!";
391             }
392 0 0         die "Error while reading from client: $!" unless defined($len);
393             }
394              
395              
396             =pod
397              
398             =item Creating the control file
399              
400             $self->ControlFile($fh, $file, $control);
401              
402             (Instance method) Creates the control file $file by writing the
403             hash ref $control into the open file handle $fh. (The file name
404             $file is passed for use in error messages only.)
405              
406             The default implementation is using the C module for
407             serialization of $control and then writing the dumped hash ref
408             into $fh.
409              
410             A Perl exception is raised in case of problems; nothing is returned
411             otherwise.
412              
413             =cut
414              
415             sub WriteControlFile {
416 0     0 0   my($self, $fh, $file, $control) = @_;
417 0           my $d = Data::Dumper->new([$control], ['control']);
418 0           $d->Indent(1);
419 0 0         (print $fh $d->Dump())
420             or die "Failed to create control file $file: $!";
421             }
422              
423              
424             =pod
425              
426             =item Reading the control file
427              
428             my $ctrl = $self->ReadControlFile($file);
429              
430             (Instance method) This method reads a control file, as created by the
431             I method and creates an instance of I.
432              
433             The default implementation does a simple B (in a Safe compartment
434             for security reasons, see the L package for details) for loading the
435             hash ref from the file. The hash ref is then blessed into the package
436             corresponding to $self: The package name of $self is taken by appending
437             the string B<::Control>.
438              
439             The method returns nothing, a Perl exception is thrown in case of
440             trouble.
441              
442             =cut
443              
444             sub ReadControlFile {
445 0     0 0   my $self = shift; my $file = shift; my $fh = shift;
  0            
  0            
446 0           my $ctrl;
447 0 0         if (ref($file) eq 'HASH') {
448 0           $ctrl = $file;
449             } else {
450 0 0         unless ($fh) {
451 0           $fh = Symbol::gensym();
452 0 0         open($fh, "<$file") or die "Failed to open control file $file: $!";
453             }
454 0           local $/ = undef;
455 0           my $contents = <$fh>;
456 0 0         die "Failed to read control file $file: $!" unless defined($contents);
457 0           my $cpt = Safe->new();
458 0           $ctrl = $cpt->reval($contents);
459 0 0         die $@ if $@;
460 0 0 0       die "Expected hash ref being read from $file"
461             unless defined($ctrl) and ref($ctrl) eq 'HASH';
462             }
463 0           my $class = ref($self) . "::Control";
464 0           my $clisa = $class . "::ISA";
465              
466 2     2   18 no strict 'refs';
  2         5  
  2         2750  
467 0 0         @$clisa = qw(Net::Spooler::Control) unless @$clisa;
468 0           $class->new($ctrl);
469             }
470              
471              
472             ############################################################################
473             #
474             # Name: new
475             #
476             # Purpose: Constructor of the Net::Spooler class; overwrites
477             # Net::Daemon::new
478             #
479             # Inputs: $proto - Class name
480             # $attr - Attributes hash ref
481             # $options - Options array ref
482             #
483             # Returns: New object, dies in case of trouble
484             #
485             ############################################################################
486              
487             sub new {
488 0     0 0   my($proto, $attr, $options) = @_;
489 0 0         $attr->{'loop-timeout'} = 300 unless exists $attr->{'loop-timeout'};
490 0 0         $attr->{'loop-child'} = 1 unless exists $attr->{'loop-child'};
491 0           my $self = $proto->SUPER::new($attr, $options);
492              
493 0 0         my $sdir = $self->{'spool-dir'}
494             or die "Missing spool-dir attribute, use --spool-dir=";
495 0           $sdir = $self->{'spool-dir'} = Cwd::abs_path($sdir);
496 0 0         my $admin = $self->{'admin'}
497             or die "Missing admin email address, use --admin=";
498              
499             # Test whether we have write permissions in the spool directory
500 0           my $fh = Symbol::gensym();
501 0           my $file = File::Spec->catfile($sdir, "WRITETEST");
502 0 0 0       (open($fh, ">$file") and close($fh) and unlink $file)
      0        
503             or die "Write test in $sdir failed, check --spool-dir and permissions";
504              
505 0 0         $self->{'expiretime'} = 432000 unless exists($self->{'expiretime'});
506 0 0         $self->{'processtimeout'} = 0 unless exists($self->{'processtimeout'});
507 0 0         $self->{'queuetime'} = 300 unless exists($self->{'queuetime'});
508 0 0         $self->{'maxsize'} = 100000 unless exists($self->{'maxsize'});
509              
510 0           $self;
511             }
512              
513              
514             ############################################################################
515             #
516             # Name: Loop
517             #
518             # Purpose: In a loop, build the list of currently queued files and
519             # process them.
520             #
521             # Inputs: $self - Instance
522             #
523             # Returns: Nothing; throws a Perl exception in case of errors.
524             #
525             ############################################################################
526              
527             sub Loop {
528 0     0 0   my $self = shift;
529 0           my $dh = Symbol::gensym();
530 0 0         $self->Fatal("Failed to open directory $self->{'spool-dir'}: $!")
531             unless opendir($dh, File::Spec->curdir());
532 0           while (my $cfile = readdir($dh)) {
533 0           my $dfile = $self->IsControlFile($cfile);
534 0 0         next unless defined $dfile;
535 0           my $ctrl = $self->ReadControlFile($cfile);
536 0           $ctrl->Process($self);
537             }
538             }
539              
540             ############################################################################
541             #
542             # Name: Run
543             #
544             # Purpose: Accepts a single file from a client and stores it in the
545             # spool directory
546             #
547             # Inputs: $self - Instance
548             #
549             # Returns: Nothing, dies in case of problems.
550             #
551             ############################################################################
552              
553             sub Run {
554 0     0 0   my $self = shift;
555 0 0         chdir $self->{'spool-dir'}
556             or die "Failed to change directory to $self->{'spool-dir'}: $!";
557              
558             # Create a sequence number. This must not fail, because it may
559             # impact the complete system. That's why we treat it special here.
560 0           my($sfile, $seq);
561 0           eval {
562 0           $sfile = $self->SequenceFile();
563 0           $seq = $self->Sequence($sfile);
564             };
565 0 0         if (!$seq) {
566 0   0       $sfile ||= "the sequence file";
567 0           my $msg = "Creating a sequence number from $sfile failed: $@";
568 0           $self->Mail($msg
569             . "\n\nThis may prevent the system to work."
570             . "\nPlease take immediate action and restore the"
571             . "\nsequence file.");
572 0           $self->Fatal($msg);
573             }
574              
575 0           my $control = {};
576 0           my $cfile = $self->ControlFile($seq);
577 0           my $dfile = $self->DataFile($seq);
578 0           my $time = time;
579 0           $control->{'created'} = "$time (" . localtime($time) . ")";
580 0           $control->{'control'} = $cfile;
581 0           $control->{'data'} = $dfile;
582              
583             # Read the data file from the client
584 0           my $dtfile = "$dfile.tmp";
585 0           my $dtfh = Symbol::gensym();
586 0           my $tmpfiles = $self->{'tmpfiles'} = { $dtfile => 1 };
587 0 0         open($dtfh, ">$dtfile")
588             or die "Failed to open data file $dtfile: $dtfh";
589 0           $self->ReadFile($self->{'socket'}, $dtfh, $dtfile, $control);
590              
591 0           my $ctfile = "$cfile.tmp";
592 0           my $ctfh = Symbol::gensym();
593 0           $tmpfiles->{$ctfile} = 1;
594 0 0         open($ctfh, ">$ctfile")
595             or die "Failed to create temporary file $ctfile: $!";
596 0           $self->WriteControlFile($ctfh, $ctfile, $control);
597              
598 0 0         rename $dtfile, $dfile
599             or die "Failed to rename $dtfile to $dfile: $!";
600 0 0         rename $ctfile, $cfile
601             or die "Failed to rename $ctfile to $cfile: $!";
602 0           my $ctrl = $self->ReadControlFile($control);
603 0           undef $dtfh;
604 0           undef $ctfh;
605 0           delete $self->{'tmpfiles'};
606              
607 0           $ctrl->Process($self);
608             }
609              
610              
611             ############################################################################
612             #
613             # Name: DESTROY
614             #
615             # Purpose: Destructor of the Net::Spooler class; removes temporary files.
616             #
617             # Inputs: $self - Instance
618             #
619             # Returns: Nothing
620             #
621             ############################################################################
622              
623             sub DESTROY {
624 0 0   0     if (my $tf = delete shift()->{'tmpfiles'}) {
625 0           unlink keys %$tf;
626             }
627             }
628              
629             sub Bind {
630 0     0 0   my $self = shift;
631 0 0         chdir $self->{'spool-dir'}
632             or die "Failed to change directory to $self->{'spool-dir'}: $!";
633 0           $self->SUPER::Bind(@_);
634             }
635              
636             package Net::Spooler::Control;
637              
638             sub new {
639 0     0     my $proto = shift; my $hash = shift;
  0            
640 0 0         my $self = $hash ? { %$hash } : {};
641 0   0       bless($self, (ref($proto) || $proto));
642             }
643              
644             sub Process {
645 0     0     my $self = shift; my $spooler = shift;
  0            
646              
647             # Lock the control file
648 0           my $cfh = Symbol::gensym();
649 0           my $cfile = $self->{'control'};
650 0           my $dfile = $self->{'data'};
651 0           $spooler->Debug("Processing file: data=$dfile, control=$cfile");
652 0 0         open($cfh, "<$cfile") or die "Failed to open $cfile for input: $!";
653 0 0         flock($cfh, Fcntl::LOCK_EX()) or die "Failed to lock $cfile: $!";
654              
655             # Set a timeout, if required
656 0           my $result;
657 0           eval {
658 0           my $timeout = $spooler->{'processtimeout'};
659 0 0   0     local $SIG{'ALRM'} = sub { die "Timeout" } if $timeout;
  0            
660 0 0         alarm $timeout if $timeout;
661 0 0         if ($spooler->{'spool-command'}) {
662 0           $result = $spooler->CommandFile($dfile, $self);
663             } else {
664 0           $result = $spooler->ProcessFile($dfile);
665             }
666 0 0         alarm 0 if $timeout;
667             };
668 0 0         if ($@) {
669 0           $spooler->Error("Failed to process $dfile: $@");
670             } else {
671 0 0         $spooler->Log('info', "Processed $dfile, result = %s\n",
672             defined $result ? $result : "undef");
673 0           unlink $cfile, $dfile;
674             }
675             }
676              
677             1;
678              
679              
680             __END__