File Coverage

blib/lib/File/CacheDir.pm
Criterion Covered Total %
statement 147 203 72.4
branch 57 114 50.0
condition 7 18 38.8
subroutine 17 19 89.4
pod 0 12 0.0
total 228 366 62.3


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2              
3             package File::CacheDir;
4              
5 5     5   33487 use strict;
  5         89  
  5         242  
6 5     5   30 use vars qw(@ISA @EXPORT_OK $VERSION %EXTANT_DIR);
  5         9  
  5         445  
7 5     5   31 use Exporter;
  5         12  
  5         209  
8 5     5   13984 use CGI qw();
  5         116910  
  5         179  
9 5     5   55 use File::Path qw(mkpath);
  5         8  
  5         413  
10 5     5   4914 use IO::Dir;
  5         137459  
  5         292  
11              
12 5     5   4875 use POSIX qw( setsid _exit );
  5         32035  
  5         43  
13              
14             @ISA = ('Exporter');
15             @EXPORT_OK = qw( cache_dir );
16             $VERSION = "1.30";
17              
18             %EXTANT_DIR = ();
19              
20             sub new {
21 12     12 0 44021675 my $type = shift;
22 12         48 my $hash_ref = $_[0];
23 12 100       122 my @PASSED_ARGS = (ref $hash_ref eq 'HASH') ? %{$_[0]} : @_;
  11         121  
24 12         37 my $cache_object;
25 12         245 my @DEFAULT_ARGS = (
26             base_dir => "/tmp/cache_dir",
27             cache_stats => 1,
28             carry_forward => 1,
29             cleanup_fork => 1,
30             # percentage of time to attempt cleanup run
31             cleanup_frequency => 100,
32             cleanup_length => 3600,
33             cleanup_suffix => ".cleanup.lock",
34             content_typed => 0,
35             cookie_brick_over => 0,
36             cookie_name => "cache_dir",
37             cookie_path => '/',
38             filename => time . $$,
39             periods_to_keep => 2,
40             set_cookie => 0,
41             ttl => "1 day",
42             );
43 12         270 my %ARGS = (@DEFAULT_ARGS, @PASSED_ARGS);
44 12         54 $cache_object = bless \%ARGS, $type;
45              
46             # clean up a few blank things in the object
47 12 50       103 unless($cache_object->{set_cookie}) {
48 12         52 foreach(qw(set_cookie cookie_name cookie_path)) {
49 36         129 delete $cache_object->{$_};
50             }
51             }
52 12         42 foreach(qw(carry_forward)) {
53 12 50       74 delete $cache_object->{$_} unless($cache_object->{$_});
54             }
55              
56 12         107 return $cache_object;
57             }
58              
59             sub ttl_mkpath {
60 4     4 0 16 my $self = shift;
61 4         9 my $_ttl_dir = shift;
62 4         1903 mkpath $_ttl_dir;
63 4 50       20 die "couldn't mkpath '$_ttl_dir': $!" unless($self->dash_d($_ttl_dir));
64             }
65              
66             sub expired_check {
67 8     8 0 21 my $self = shift;
68 8         21 my $_sub_dir = shift;
69 8         31 my $diff = $self->{int_time} - $_sub_dir;
70 8 50       33 if($diff > $self->{periods_to_keep}) {
71 8         43 return 1;
72             } else {
73 0         0 return 0;
74             }
75             }
76              
77             ### want to be able to easily track -d checks, and want to have
78             ### all the cache_stats code in one place
79             sub dash_d {
80 52     52 0 129 my $self = shift;
81 52         95 my $_dir = shift;
82 52         89 my $return = 0;
83 52 100 66     404 if($self->{cache_stats} && $EXTANT_DIR{$_dir}) {
84 18         48 $return = 1;
85             } else {
86 34 100       5040 if(-d $_dir) {
87 18         38 $return = 1;
88 18         96 $EXTANT_DIR{$_dir} = 1;
89             }
90             }
91 52         382 return $return;
92             }
93              
94             ### sub to wrap the cleanup code... this way we can control
95             ### when cleanup should occur, even if somebody overides
96             ### the cleanup logic
97              
98             sub perhaps_cleanup {
99 8     8 0 19 my $self = shift;
100 8         21 my $_dir = shift;
101              
102             ### let's do this each time a cleanup might happen
103             ### might be a little much, but shouldn't be anywhere
104             ### near as bad as the pre-cache days
105 8         62 foreach my $this_dir (keys %EXTANT_DIR) {
106 12 50       170 delete $EXTANT_DIR{$this_dir} unless(-d $EXTANT_DIR{$this_dir});
107             }
108              
109             ### might want to do cleanup only a portion of the time
110 8 50       49 return undef if( rand(100) >= $self->{cleanup_frequency} );
111              
112             ### Do quick lock file checking... (only one process should handle a cleanup)
113 8         28 my $file = "$_dir$self->{cleanup_suffix}";
114 8         385 my @stat = stat $file;
115              
116 8 50 33     46 if( @stat && time - $stat[9] < $self->{cleanup_length} ){
117 0         0 return undef;
118             }
119              
120             ## all checks passed... (we're still here)
121             ## make the lockfile, do the cleanup, and whatnot
122              
123             ### realize this won't work over nfs, but better than nothing
124 8         1146 open(FILE,">$file");
125 8 50       110 flock(FILE, 6) or return undef;
126              
127 8 100       30 if($self->{cleanup_fork}) {
128 4 50       16 return if strong_fork();
129             }
130 4         16 $self->cleanup( $_dir );
131 4         405 close(FILE);
132 4         288 unlink $file;
133 4 50       54 child_exit() if($self->{cleanup_fork});
134             }
135              
136             sub cleanup {
137 14     14 0 26003141 my $self = shift;
138 14         36 my $root = shift;
139 14 50       144 return if($root =~ /$self->{cleanup_suffix}$/);
140 14 100 66     1288 return unlink $root if
141             -l $root || !-d _;
142 10 50       114 if (my $dir = IO::Dir->new($root)) {
143 10         3136 while (defined(my $node = $dir->read)) {
144 28 100       1830 next if $node =~ /^\.\.?$/;
145 8 50       71 $node = $1 if $node =~ /(.+)/;
146 8         69 $self->cleanup("$root/$node");
147             }
148             }
149 10         1282 return rmdir $root;
150             }
151              
152             sub handle_ttl {
153 12     12 0 30 my $self = shift;
154              
155 12 50       710 if($self->{ttl} =~ /^\d+$/) {
    50          
156             # do nothing
157             } elsif($self->{ttl} =~ s/^(\d+)\s*(\D+)$/$1/) {
158 12 50       70 $self->{ttl} = $1 if defined $1;
159 12 50       77 my $units = (defined $2) ? $2 : '';
160 12 100 66     123 if(($units =~ /^s/i) || (!$units)) {
    50          
    50          
    0          
    0          
161 10         54 $self->{ttl} = $self->{ttl};
162             } elsif ($units =~ /^m/i) {
163 0         0 $self->{ttl} *= 60;
164             } elsif ($units =~ /^h/i) {
165 2         7 $self->{ttl} *= 3600;
166             } elsif ($units =~ /^d/i) {
167 0         0 $self->{ttl} *= 86400;
168             } elsif ($units =~ /^w/i) {
169 0         0 $self->{ttl} *= 604800;
170             } else {
171 0         0 die "invalid ttl '$self->{ttl}', bad units '$units'";
172             }
173             } else {
174 0         0 die "invalid ttl '$self->{ttl}', not just number and couldn't find units";
175             }
176             }
177              
178             sub sub_mkdir {
179 12     12 0 30 my $self = shift;
180 12         34 my $_dir = shift;
181 12         10376 mkdir $_dir, 0755;
182 12 50       74 die "couldn't mkpath '$_dir': $!" unless($self->dash_d($_dir));
183             }
184              
185             sub cache_dir {
186 12     12 0 247 my $self = $_[0];
187 12 100       108 unless(UNIVERSAL::isa($self, __PACKAGE__)) {
188 2         21 $self = File::CacheDir->new(@_);
189             }
190              
191 12         36 delete $self->{carried_forward};
192              
193 12         61 $self->handle_ttl;
194              
195 12         46 $self->{base_dir} =~ s@/$@@;
196 12         58 my $ttl_dir = "$self->{base_dir}/$self->{ttl}/";
197              
198 12 100       56 unless($self->dash_d($ttl_dir)) {
199 4         25 $self->ttl_mkpath($ttl_dir);
200             }
201              
202 12         84 $self->{int_time} = (int(time/$self->{ttl}));
203 12         83 $self->{full_dir} = "$ttl_dir$self->{int_time}/";
204 12         55 $self->{last_int_time} = $self->{int_time} - 1;
205 12         88 $self->{last_dir} = "$ttl_dir$self->{last_int_time}/";
206              
207 12 50       51 if($self->{carry_forward}) {
208 12         37 $self->{last_int_time} = $self->{int_time} - 1;
209 12         51 $self->{last_int_dir} = "$ttl_dir$self->{last_int_time}/";
210 12         44 $self->{carry_forward_filename} = "$self->{last_int_dir}$self->{filename}";
211 12 100       1378 if(-e $self->{carry_forward_filename}) {
212 4 50       20 unless($self->dash_d($self->{full_dir})) {
213 4         30 $self->sub_mkdir($self->{full_dir});
214 4 50       14 die "couldn't mkpath '$self->{full_dir}': $!" unless($self->dash_d($self->{full_dir}));
215             }
216              
217 4         19 $self->{full_path} = "$self->{full_dir}$self->{filename}";
218              
219 4         494 rename $self->{carry_forward_filename}, $self->{full_path};
220 4 50       702 die "couldn't rename $self->{carry_forward_filename}, $self->{full_path}: $!" unless(-e $self->{full_path});
221              
222 4         19 $self->{carried_forward} = 1;
223              
224 4 50       17 if($self->{set_cookie}) {
225 0         0 ($self->{cookie_value}) = $self->{full_path} =~ /^$self->{base_dir}(.+)/;
226 0         0 $self->set_cookie;
227             }
228 4         64 return $self->{full_path};
229              
230             }
231             }
232              
233 8 50       35 if($self->dash_d($self->{full_dir})) {
234 0         0 $self->{full_path} = "$self->{full_dir}$self->{filename}";
235 0 0       0 if($self->{set_cookie}) {
236 0         0 ($self->{cookie_value}) = $self->{full_path} =~ /^$self->{base_dir}(.+)/;
237 0         0 $self->set_cookie;
238             }
239 0         0 return $self->{full_path};
240             } else {
241 8 50       237 if( rand(100) < $self->{cleanup_frequency}) {
242 8         384 opendir(DIR, $ttl_dir);
243 8         326 while (my $sub_dir = readdir(DIR)) {
244 24 100       368 next if($sub_dir =~ /^\.\.?$/);
245 8 50       102 next if($sub_dir =~ /$self->{cleanup_suffix}/);
246 8 50       123 $sub_dir = $1 if $sub_dir =~ /(.+)/;
247 8 50       43 if($self->expired_check($sub_dir)) {
248 8         58 $self->perhaps_cleanup("$ttl_dir$sub_dir");
249             }
250             }
251 8         160 closedir(DIR);
252             }
253 8         241 $self->sub_mkdir($self->{full_dir});
254 8 50       36 die "couldn't mkpath '$self->{full_dir}': $!" unless($self->dash_d($self->{full_dir}));
255 8         71 $self->{full_path} = "$self->{full_dir}$self->{filename}";
256 8 50       266 if($self->{set_cookie}) {
257 0         0 ($self->{cookie_value}) = $self->{full_path} =~ /^$self->{base_dir}(.+)/;
258 0         0 $self->set_cookie;
259             }
260 8         300 return $self->{full_path};
261             }
262             }
263              
264             sub set_cookie {
265 0     0 0 0 my $self = shift;
266 0 0       0 return unless($self->{set_cookie});
267 0         0 my $old_cookie = CGI::cookie( -name => $self->{cookie_name} );
268 0 0 0     0 if(!$self->{cookie_brick_over} && defined $old_cookie) {
269 0         0 $self->{cookie_value} = $old_cookie;
270 0         0 return $old_cookie;
271             }
272 0         0 $self->{cookie_value} =~ m@$self->{base_dir}(.+)@;
273 0         0 my $new_cookie = CGI::cookie
274             (-name => $self->{cookie_name},
275             -value => $1,
276             -path => $self->{cookie_path},
277             );
278 0 0       0 if ($self->{content_typed}) {
279 0         0 print qq{\n};
280             } else {
281 0         0 print "Set-Cookie: $new_cookie\n";
282             }
283 0         0 return;
284             }
285              
286             sub strong_fork {
287             # Grab anonymous subroutine CODEREF or pointer to subroutine
288             # if a CODEREF is not passed - the routine will behave like fork.
289 4     4 0 7 my $routine=shift;
290             # STDOUT buffer must be clean before fork() because
291             # IO buffers are also replicated to the child process.
292             # We can't have the data already sent to STDOUT to be sent
293             # again by the child process!
294 4         42 $| = 1;
295             # Print nothing to STDOUT to force buffer to flush before fork() is called.
296             # This causes even a 'tie'd STDOUT (FastCGI or mod_perl) to actually flush.
297 4         22 print STDOUT "";
298             # Create a pipe for the grandchild pid slide through
299 4         117 pipe (*RPID, *WPID);
300             # Fork is necessary to hide from apache's touch of death.
301 4         4681 my $child = fork;
302             # Don't abort CGI if fork fails.
303 4 50       135 if (!defined $child) {
304 0         0 return undef;
305             }
306             # Parent should continue ASAP
307 4 50       133 if ($child) {
308             # I'm not going to put anything down this pipe.
309 4         101 close WPID;
310 4         55 my $grandchild_pid = '';
311             # Waiting for Child to send the Grandchild pid
312 4         22504 sysread(*RPID, $grandchild_pid, 10);
313             # It should be a number if the child fork()ed successfully.
314 4         26 $grandchild_pid += 0;
315             # Done with the pipe
316 4         232 close RPID;
317 4 50       2318 if (!defined $grandchild_pid) {
318 0         0 return undef;
319             }
320              
321             # Intermediate Child should hopefully terminate very soon
322             # (if it hasn't already). Clean off the zombie that it
323             # just became or wait until it dies.
324 4         106 waitpid($child, 0);
325              
326 4         509 return $grandchild_pid;
327             }
328             # This is the Child process.
329              
330             # I'm not going to look at anything in this pipe.
331 0           close RPID;
332              
333             # Double fork makes cleaning zombies very easy.
334             # A defunct process always vanishes from existence
335             # when its parent is terminated. It is better than
336             # SIGCHLD handlers because, unfortunately, under
337             # mod_perl, the handler may get blown away by future
338             # requests that also use it.
339 0           my $grandchild_pid = fork;
340             # If second fork fails, pretend like I am the grandchild and
341             # make the parent process block until the subroutine completes.
342 0 0         if (!defined $grandchild_pid) { # Fork failed
    0          
343 0           print STDERR "Second process [$$] failed to fork for [$0]! [$!]\n";
344 0           close WPID;
345 0           _exit(0);
346             } elsif ($grandchild_pid) { # Intermediate Child process
347             # Stuff the magic grandchild id down the pipe
348             # so grandpa knows who it is. Intermediate process
349             # does the stuffing so the grandchild process can
350             # get to work as soon as possible.
351 0           print WPID $grandchild_pid;
352 0           close WPID;
353             # This releases the waitpid() block above without
354             # calling any DESTROY nor END blocks while the detach
355             # variables are still within scope. It also slams all
356             # FD_CLOEXEC handles shut abruptly.
357 0           _exit(0);
358             }
359             # This is the Grandchild process running.
360 0           close WPID;
361              
362             # Setsid is necessary to escape Apache touch of death
363             # when it is shutdown. We do not want a TERM signal to
364             # be sent to this strong_fork'ed child process just because
365             # the root web server gets shutdown.
366 0           setsid();
367              
368             # Untie is necessary to buttwag around those perl wrappers
369             # that do not implement a CLOSE method correctly (mod_perl
370             # and/or FastCGI) and in case STDOUT is tied and not a real
371             # handle.
372 0 0         untie *STDOUT if tied *STDOUT;
373              
374             # Shutdown STDOUT handle to trick apache into releasing
375             # connection to client as soon as parent CGI finishes.
376             # (The STDERR should still be redirected to the error_log.)
377 0           close STDOUT;
378              
379             # look to see if we want to keep this process going
380             # this makes us function more like the child process of a fork
381             # this allows for safe fork
382 0 0 0       if (! $routine || ! ref $routine) {
383 0           open STDOUT, '>/dev/null' || die "Can't write /dev/null [$!]";
384 0           open STDIN, '
385 0           return 0; # this is what fork returns on a child
386             }
387              
388             # Finally play the CODEREF that was passed.
389 0           $@ = ""; {
390             # Eval to catch the "die" calls from causing Apache::exit
391             # from being triggered leaving nasty dettached httpd
392             # processes under mod_perl
393 0           local $SIG{__DIE__} = 'DEFAULT';
  0            
394 0           eval {
395 0           &$routine;
396             };
397             # If an error occurred, log it and continue
398 0 0         if ($@) {
399 0           warn "strong_fork: $@";
400             }
401             }
402             # Program must terminate to avoid duplicate execution
403             # of code following this function in the caller program,
404             # but the DESTROY methods and END blocks should be run.
405             # Apache::exit should not be called since this is the
406             # strong_fork'd child process.
407 0           &child_exit();
408             }
409              
410             sub child_exit {
411 0     0 0   _exit(0);
412             }
413              
414             1;
415              
416             __END__