File Coverage

blib/lib/Directory/Queue.pm
Criterion Covered Total %
statement 135 149 90.6
branch 39 62 62.9
condition 6 17 35.2
subroutine 24 24 100.0
pod 6 6 100.0
total 210 258 81.4


line stmt bran cond sub pod time code
1             #+##############################################################################
2             # #
3             # File: Directory/Queue.pm #
4             # #
5             # Description: object oriented interface to a directory based queue #
6             # #
7             #-##############################################################################
8              
9             #
10             # module definition
11             #
12              
13             package Directory::Queue;
14 7     7   1233 use strict;
  7         26  
  7         211  
15 7     7   35 use warnings;
  7         13  
  7         610  
16             our $VERSION = "2.1";
17             our $REVISION = sprintf("%d.%02d", q$Revision: 1.52 $ =~ /(\d+)\.(\d+)/);
18              
19             #
20             # used modules
21             #
22              
23 7     7   3449 use No::Worries::Die qw(dief);
  7         139795  
  7         50  
24 7     7   897 use No::Worries::Export qw(export_control);
  7         17  
  7         43  
25 7     7   4774 use No::Worries::Stat qw(ST_DEV ST_INO ST_NLINK ST_SIZE ST_MTIME);
  7         27333  
  7         56  
26 7     7   5750 use POSIX qw(:errno_h :fcntl_h);
  7         58031  
  7         47  
27 7     7   18748 use Time::HiRes qw();
  7         10385  
  7         371  
28              
29             #
30             # global variables
31             #
32              
33             our(
34             %_LoadedModule, # hash of successfully loaded modules
35             );
36              
37             #+++############################################################################
38             # #
39             # Constants #
40             # #
41             #---############################################################################
42              
43             #
44             # reasonable buffer size for file I/O operations
45             #
46              
47 7     7   84 use constant SYSBUFSIZE => 1_048_576; # 1MB
  7         21  
  7         15108  
48              
49             #
50             # regular expressions
51             #
52              
53             our(
54             $_DirectoryRegexp, # regexp matching an intermediate directory
55             $_ElementRegexp, # regexp matching an element
56             );
57              
58             $_DirectoryRegexp = qr/[0-9a-f]{8}/;
59             $_ElementRegexp = qr/[0-9a-f]{14}/;
60              
61             #+++############################################################################
62             # #
63             # Common Code #
64             # #
65             #---############################################################################
66              
67             #
68             # make sure a module is loaded
69             #
70              
71             sub _require ($) {
72 19     19   37 my($module) = @_;
73              
74 19 100       61 return if $_LoadedModule{$module};
75 6         556 eval("require $module"); ## no critic 'ProhibitStringyEval'
76 6 50       37 if ($@) {
77 0         0 $@ =~ s/\s+at\s.+?\sline\s+\d+\.?$//;
78 0         0 dief("failed to load %s: %s", $module, $@);
79             } else {
80 6         25 $_LoadedModule{$module} = 1;
81             }
82             }
83              
84             #
85             # return the name of a new element to (try to) use with:
86             # - 8 hexadecimal digits for the number of seconds since the Epoch
87             # - 5 hexadecimal digits for the microseconds part
88             # - 1 hexadecimal digit from the caller to further reduce name collisions
89             #
90             # properties:
91             # - fixed size (14 hexadecimal digits)
92             # - likely to be unique (with very high-probability)
93             # - can be lexically sorted
94             # - ever increasing (for a given process)
95             # - reasonably compact
96             # - matching $_ElementRegexp
97             #
98              
99             sub _name ($) {
100 103     103   715 return(sprintf("%08x%05x%01x", Time::HiRes::gettimeofday(), $_[0]));
101             }
102              
103             #
104             # create a directory in adversary conditions:
105             # - return true on success
106             # - return false if the directory already exists
107             # - die in case of any other error
108             # - handle an optional umask
109             #
110              
111             sub _special_mkdir ($$) {
112 49     49   196 my($path, $umask) = @_;
113 49         96 my($oldumask, $success);
114              
115 49 50       124 if (defined($umask)) {
116 0         0 $oldumask = umask($umask);
117 0         0 $success = mkdir($path);
118 0         0 umask($oldumask);
119             } else {
120 49         6780 $success = mkdir($path);
121             }
122 49 50       473 return(1) if $success;
123 0 0 0     0 dief("cannot mkdir(%s): %s", $path, $!) unless $! == EEXIST and -d $path;
124             # RACE: someone else may have created it at the the same time
125 0         0 return(0);
126             }
127              
128             #
129             # delete a directory in adversary conditions:
130             # - return true on success
131             # - return false if the path does not exist (anymore)
132             # - die in case of any other error
133             #
134              
135             sub _special_rmdir ($) {
136 1     1   3 my($path) = @_;
137              
138 1 50       48 return(1) if rmdir($path);
139 0 0       0 dief("cannot rmdir(%s): %s", $path, $!) unless $! == ENOENT;
140             # RACE: someone else may have deleted it at the the same time
141 0         0 return(0);
142             }
143              
144             #
145             # get the contents of a directory in adversary conditions:
146             # - return the list of names without . and ..
147             # - return an empty list if the directory does not exist (anymore),
148             # unless the optional second argument is true
149             # - die in case of any other error
150             #
151              
152             sub _special_getdir ($;$) {
153 109     109   308 my($path, $strict) = @_;
154 109         173 my($dh, @list);
155              
156 109 50       3209 if (opendir($dh, $path)) {
157 109         3351 @list = grep($_ !~ /^\.\.?$/, readdir($dh));
158 109 50       1324 closedir($dh) or dief("cannot closedir(%s): %s", $path, $!);
159 109         944 return(@list);
160             }
161 0 0 0     0 dief("cannot opendir(%s): %s", $path, $!)
162             unless $! == ENOENT and not $strict;
163             # RACE: someone else may have deleted it at the the same time
164 0         0 return();
165             }
166              
167             #
168             # create a file:
169             # - return the file handle on success
170             # - tolerate some errors unless the optional third argument is true
171             # - die in case of any other error
172             # - handle an optional umask
173             #
174              
175             sub _create ($$;$) {
176 49     49   169 my($path, $umask, $strict) = @_;
177 49         93 my($fh, $oldumask, $success);
178              
179 49 50       109 if (defined($umask)) {
180 0         0 $oldumask = umask($umask);
181 0         0 $success = sysopen($fh, $path, O_WRONLY|O_CREAT|O_EXCL);
182 0         0 umask($oldumask);
183             } else {
184 49         3529 $success = sysopen($fh, $path, O_WRONLY|O_CREAT|O_EXCL);
185             }
186 49 100       425 return($fh) if $success;
187 2 50 33     38 dief("cannot sysopen(%s, O_WRONLY|O_CREAT|O_EXCL): %s", $path, $!)
      33        
188             unless ($! == EEXIST or $! == ENOENT) and not $strict;
189             # RACE: someone else may have created the file (EEXIST)
190             # RACE: the containing directory may be mising (ENOENT)
191 2         13 return(0);
192             }
193              
194             #
195             # "touch" a file or directory
196             #
197              
198             sub _touch ($) {
199 1     1   4 my($path) = @_;
200 1         2 my($time);
201              
202 1         2 $time = time();
203 1 50       20 utime($time, $time, $path)
204             or dief("cannot utime(%d, %d, %s): %s", $time, $time, $path, $!);
205             }
206              
207             #+++############################################################################
208             # #
209             # Base Class #
210             # #
211             #---############################################################################
212              
213             #
214             # helper to compute an "id" from the given path
215             #
216              
217             sub _path2id ($) {
218 23     23   72 my($path) = @_;
219 23         76 my(@stat);
220              
221             # on some operating systems, we cannot rely on inode numbers :-(
222 23 50       174 return($path) if $^O =~ /^(cygwin|dos|MSWin32)$/;
223             # on others, we can: device number plus inode number should be unique
224 23         292 @stat = stat($path);
225 23 50       87 dief("cannot stat(%s): %s", $path, $!) unless @stat;
226 23         158 return($stat[ST_DEV] . ":" . $stat[ST_INO]);
227             }
228              
229             #
230             # object creator (wrapper)
231             #
232              
233             sub new : method {
234 19     19 1 11261 my($class, %option) = @_;
235 19         45 my($subclass);
236              
237 19   100     70 $option{"type"} ||= "Simple";
238 19         54 $subclass = $class . "::" . $option{"type"};
239 19         58 _require($subclass);
240 19         41 delete($option{"type"});
241 19         99 return($subclass->new(%option));
242             }
243              
244             #
245             # object creator (inherited)
246             #
247              
248             sub _new : method {
249 23     23   72 my($class, %option) = @_;
250 23         44 my($self, $path);
251              
252             # path is mandatory
253 23 50       76 dief("missing option: path") unless defined($option{"path"});
254             dief("not a directory: %s", $option{"path"})
255 23 50 66     506 if -e $option{"path"} and not -d _;
256             # build the object
257             $self = {
258 23         141 "path" => $option{"path"}, # toplevel path
259             "dirs" => [], # cached list of intermediate directories
260             "elts" => [], # cached list of elements
261             };
262             # check the integer options
263 23         71 foreach my $name (qw(maxlock maxtemp rndhex umask)) {
264 92 100       207 next unless defined($option{$name});
265             dief("invalid %s: %s", $name, $option{$name})
266 8 50       65 unless $option{$name} =~ /^\d+$/;
267             }
268             # handle the maxlock option
269 23 100       70 if (defined($option{"maxlock"})) {
270 2         5 $self->{"maxlock"} = $option{"maxlock"};
271             } else {
272 21         51 $self->{"maxlock"} = 600;
273             }
274             # handle the maxtemp option
275 23 100       59 if (defined($option{"maxtemp"})) {
276 2         6 $self->{"maxtemp"} = $option{"maxtemp"};
277             } else {
278 21         45 $self->{"maxtemp"} = 300;
279             }
280             # handle the rndhex option
281 23 100       51 if (defined($option{"rndhex"})) {
282             dief("invalid rndhex: %s", $option{"rndhex"})
283 2 50       9 unless $option{"rndhex"} < 16;
284 2         6 $self->{"rndhex"} = $option{"rndhex"};
285             } else {
286 21         86 $self->{"rndhex"} = int(rand(16));
287             }
288             # handle the umask option
289 23 100       62 if (defined($option{"umask"})) {
290             dief("invalid umask: %s", $option{"umask"})
291 2 50       9 unless $option{"umask"} < 512;
292 2         6 $self->{"umask"} = $option{"umask"};
293             }
294             # create the toplevel directory if needed
295 23         45 $path = "";
296 23         174 foreach my $name (split(/\/+/, $self->{"path"})) {
297 81         198 $path .= $name . "/";
298 81 100       816 _special_mkdir($path, $self->{"umask"}) unless -d $path;
299             }
300             # store the unique queue identifier
301 23         122 $self->{"id"} = _path2id($self->{"path"});
302             # that's it!
303 23         59 bless($self, $class);
304 23         91 return($self);
305             }
306              
307             #
308             # copy/clone the object
309             #
310             # note:
311             # - the main purpose is to copy/clone the iterator cached state
312             # - the other attributes are _not_ cloned but this is not a problem
313             # since they should not change
314             #
315              
316             sub copy : method {
317 4     4 1 8 my($self) = @_;
318 4         6 my($copy);
319              
320 4         7 $copy = { %{ $self } };
  4         27  
321 4         10 $copy->{"dirs"} = [];
322 4         6 $copy->{"elts"} = [];
323 4         13 bless($copy, ref($self));
324 4         14 return($copy);
325             }
326              
327             #
328             # return the toplevel path of the queue
329             #
330              
331             sub path : method {
332 4     4 1 31 my($self) = @_;
333              
334 4         30 return($self->{"path"});
335             }
336              
337             #
338             # return a unique identifier for the queue
339             #
340              
341             sub id : method {
342 12     12 1 25 my($self) = @_;
343              
344 12         42 return($self->{"id"});
345             }
346              
347             #
348             # return the name of the next element in the queue, using cached information
349             #
350              
351             sub next : method { ## no critic 'ProhibitBuiltinHomonyms'
352 59     59 1 501 my($self) = @_;
353 59         84 my($dir, @list);
354              
355 59 100       75 return(shift(@{ $self->{"elts"} })) if @{ $self->{"elts"} };
  32         142  
  59         155  
356 27         46 while (@{ $self->{"dirs"} }) {
  28         68  
357 19         25 $dir = shift(@{ $self->{"dirs"} });
  19         47  
358 19         62 foreach my $name (_special_getdir($self->{"path"} . "/" . $dir)) {
359 61 50       326 push(@list, $1) if $name =~ /^($_ElementRegexp)$/o; # untaint
360             }
361 19 100       56 next unless @list;
362 18         158 $self->{"elts"} = [ map("$dir/$_", sort(@list)) ];
363 18         36 return(shift(@{ $self->{"elts"} }));
  18         95  
364             }
365 9         39 return("");
366             }
367              
368             #
369             # return the first element in the queue and cache information about the next ones
370             #
371              
372             sub first : method {
373 16     16 1 1654 my($self) = @_;
374 16         57 my(@list);
375              
376 16         42 foreach my $name (_special_getdir($self->{"path"}, "strict")) {
377 44 100       282 push(@list, $1) if $name =~ /^($_DirectoryRegexp)$/o; # untaint
378             }
379 16         76 $self->{"dirs"} = [ sort(@list) ];
380 16         35 $self->{"elts"} = [];
381 16         73 return($self->next());
382             }
383              
384             #
385             # export control
386             #
387              
388             sub import : method {
389 15     15   115 my($pkg, %exported);
390              
391 15         44 $pkg = shift(@_);
392 15         54 foreach my $name (
393             qw(SYSBUFSIZE _name $_DirectoryRegexp $_ElementRegexp
394             _special_getdir _special_mkdir _special_rmdir _create _touch)) {
395 135         291 $exported{$name}++;
396             }
397 15         125 export_control(scalar(caller()), $pkg, \%exported, @_);
398             }
399              
400             1;
401              
402             __END__