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   903 use strict;
  7         18  
  7         163  
15 7     7   27 use warnings;
  7         15  
  7         494  
16             our $VERSION = "2.2";
17             our $REVISION = sprintf("%d.%02d", q$Revision: 1.52 $ =~ /(\d+)\.(\d+)/);
18              
19             #
20             # used modules
21             #
22              
23 7     7   2735 use No::Worries::Die qw(dief);
  7         108838  
  7         35  
24 7     7   658 use No::Worries::Export qw(export_control);
  7         14  
  7         32  
25 7     7   3400 use No::Worries::Stat qw(ST_DEV ST_INO ST_NLINK ST_SIZE ST_MTIME);
  7         21490  
  7         38  
26 7     7   4503 use POSIX qw(:errno_h :fcntl_h);
  7         47843  
  7         33  
27 7     7   14533 use Time::HiRes qw();
  7         8190  
  7         291  
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   68 use constant SYSBUFSIZE => 1_048_576; # 1MB
  7         14  
  7         12084  
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   24 my($module) = @_;
73              
74 19 100       47 return if $_LoadedModule{$module};
75 6         393 eval("require $module"); ## no critic 'ProhibitStringyEval'
76 6 50       26 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         16 $_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   538 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   160 my($path, $umask) = @_;
113 49         68 my($oldumask, $success);
114              
115 49 50       90 if (defined($umask)) {
116 0         0 $oldumask = umask($umask);
117 0         0 $success = mkdir($path);
118 0         0 umask($oldumask);
119             } else {
120 49         2354 $success = mkdir($path);
121             }
122 49 50       348 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   5 my($path) = @_;
137              
138 1 50       41 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 149     149   255 my($path, $strict) = @_;
154 149         179 my($dh, @list);
155              
156 149 50       3312 if (opendir($dh, $path)) {
157 149         3261 @list = grep($_ !~ /^\.\.?$/, readdir($dh));
158 149 50       1318 closedir($dh) or dief("cannot closedir(%s): %s", $path, $!);
159 149         1378 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   129 my($path, $umask, $strict) = @_;
177 49         73 my($fh, $oldumask, $success);
178              
179 49 50       88 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         3023 $success = sysopen($fh, $path, O_WRONLY|O_CREAT|O_EXCL);
185             }
186 49 100       334 return($fh) if $success;
187 2 50 33     39 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         15 return(0);
192             }
193              
194             #
195             # "touch" a file or directory
196             #
197              
198             sub _touch ($) {
199 1     1   3 my($path) = @_;
200 1         1 my($time);
201              
202 1         2 $time = time();
203 1 50       16 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   45 my($path) = @_;
219 23         31 my(@stat);
220              
221             # on some operating systems, we cannot rely on inode numbers :-(
222 23 50       155 return($path) if $^O =~ /^(cygwin|dos|MSWin32)$/;
223             # on others, we can: device number plus inode number should be unique
224 23         257 @stat = stat($path);
225 23 50       80 dief("cannot stat(%s): %s", $path, $!) unless @stat;
226 23         132 return($stat[ST_DEV] . ":" . $stat[ST_INO]);
227             }
228              
229             #
230             # object creator (wrapper)
231             #
232              
233             sub new : method {
234 19     19 1 8840 my($class, %option) = @_;
235 19         31 my($subclass);
236              
237 19   100     80 $option{"type"} ||= "Simple";
238 19         40 $subclass = $class . "::" . $option{"type"};
239 19         47 _require($subclass);
240 19         31 delete($option{"type"});
241 19         69 return($subclass->new(%option));
242             }
243              
244             #
245             # object creator (inherited)
246             #
247              
248             sub _new : method {
249 23     23   58 my($class, %option) = @_;
250 23         35 my($self, $path);
251              
252             # path is mandatory
253 23 50       66 dief("missing option: path") unless defined($option{"path"});
254             dief("not a directory: %s", $option{"path"})
255 23 50 66     391 if -e $option{"path"} and not -d _;
256             # build the object
257             $self = {
258 23         121 "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         50 foreach my $name (qw(maxlock maxtemp rndhex umask)) {
264 92 100       164 next unless defined($option{$name});
265             dief("invalid %s: %s", $name, $option{$name})
266 8 50       43 unless $option{$name} =~ /^\d+$/;
267             }
268             # handle the maxlock option
269 23 100       56 if (defined($option{"maxlock"})) {
270 2         4 $self->{"maxlock"} = $option{"maxlock"};
271             } else {
272 21         40 $self->{"maxlock"} = 600;
273             }
274             # handle the maxtemp option
275 23 100       45 if (defined($option{"maxtemp"})) {
276 2         3 $self->{"maxtemp"} = $option{"maxtemp"};
277             } else {
278 21         34 $self->{"maxtemp"} = 300;
279             }
280             # handle the rndhex option
281 23 100       41 if (defined($option{"rndhex"})) {
282             dief("invalid rndhex: %s", $option{"rndhex"})
283 2 50       5 unless $option{"rndhex"} < 16;
284 2         3 $self->{"rndhex"} = $option{"rndhex"};
285             } else {
286 21         60 $self->{"rndhex"} = int(rand(16));
287             }
288             # handle the umask option
289 23 100       52 if (defined($option{"umask"})) {
290             dief("invalid umask: %s", $option{"umask"})
291 2 50       6 unless $option{"umask"} < 512;
292 2         3 $self->{"umask"} = $option{"umask"};
293             }
294             # create the toplevel directory if needed
295 23         39 $path = "";
296 23         126 foreach my $name (split(/\/+/, $self->{"path"})) {
297 81         172 $path .= $name . "/";
298 81 100       638 _special_mkdir($path, $self->{"umask"}) unless -d $path;
299             }
300             # store the unique queue identifier
301 23         98 $self->{"id"} = _path2id($self->{"path"});
302             # that's it!
303 23         45 bless($self, $class);
304 23         72 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 7 my($self) = @_;
318 4         4 my($copy);
319              
320 4         5 $copy = { %{ $self } };
  4         22  
321 4         10 $copy->{"dirs"} = [];
322 4         5 $copy->{"elts"} = [];
323 4         9 bless($copy, ref($self));
324 4         11 return($copy);
325             }
326              
327             #
328             # return the toplevel path of the queue
329             #
330              
331             sub path : method {
332 4     4 1 30 my($self) = @_;
333              
334 4         23 return($self->{"path"});
335             }
336              
337             #
338             # return a unique identifier for the queue
339             #
340              
341             sub id : method {
342 12     12 1 22 my($self) = @_;
343              
344 12         37 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 415 my($self) = @_;
353 59         68 my($dir, @list);
354              
355 59 100       59 return(shift(@{ $self->{"elts"} })) if @{ $self->{"elts"} };
  32         92  
  59         138  
356 27         34 while (@{ $self->{"dirs"} }) {
  28         57  
357 19         23 $dir = shift(@{ $self->{"dirs"} });
  19         33  
358 19         54 foreach my $name (_special_getdir($self->{"path"} . "/" . $dir)) {
359 61 50       286 push(@list, $1) if $name =~ /^($_ElementRegexp)$/o; # untaint
360             }
361 19 100       46 next unless @list;
362 18         131 $self->{"elts"} = [ map("$dir/$_", sort(@list)) ];
363 18         33 return(shift(@{ $self->{"elts"} }));
  18         81  
364             }
365 9         29 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 1391 my($self) = @_;
374 16         34 my(@list);
375              
376 16         36 foreach my $name (_special_getdir($self->{"path"}, "strict")) {
377 44 100       237 push(@list, $1) if $name =~ /^($_DirectoryRegexp)$/o; # untaint
378             }
379 16         63 $self->{"dirs"} = [ sort(@list) ];
380 16         30 $self->{"elts"} = [];
381 16         110 return($self->next());
382             }
383              
384             #
385             # export control
386             #
387              
388             sub import : method {
389 15     15   73 my($pkg, %exported);
390              
391 15         27 $pkg = shift(@_);
392 15         32 foreach my $name (
393             qw(SYSBUFSIZE _name $_DirectoryRegexp $_ElementRegexp
394             _special_getdir _special_mkdir _special_rmdir _create _touch)) {
395 135         218 $exported{$name}++;
396             }
397 15         106 export_control(scalar(caller()), $pkg, \%exported, @_);
398             }
399              
400             1;
401              
402             __END__