File Coverage

blib/lib/cPanel/StateFile.pm
Criterion Covered Total %
statement 197 211 93.3
branch 75 96 78.1
condition 15 23 65.2
subroutine 32 35 91.4
pod 6 6 100.0
total 325 371 87.6


line stmt bran cond sub pod time code
1             package cPanel::StateFile;
2             {
3             $cPanel::StateFile::VERSION = '0.606';
4             }
5              
6             # cpanel - cPanel/StateFile.pm Copyright(c) 2014 cPanel, Inc.
7             # All rights Reserved.
8             # copyright@cpanel.net http://cpanel.net
9             #
10             # Redistribution and use in source and binary forms, with or without
11             # modification, are permitted provided that the following conditions are met:
12             # * Redistributions of source code must retain the above copyright
13             # notice, this list of conditions and the following disclaimer.
14             # * Redistributions in binary form must reproduce the above copyright
15             # notice, this list of conditions and the following disclaimer in the
16             # documentation and/or other materials provided with the distribution.
17             # * Neither the name of the owner nor the names of its contributors may
18             # be used to endorse or promote products derived from this software
19             # without specific prior written permission.
20             #
21             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22             # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23             # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24             # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
25             # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26             # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27             # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
28             # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29             # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30             # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31              
32 40     40   193686 use strict;
  40         399  
  40         1466  
33              
34             #use warnings;
35              
36 40     40   294 use Fcntl ();
  40         73  
  40         601  
37 40     40   215 use File::Path ();
  40         73  
  40         794  
38 40     40   226 use Scalar::Util ();
  40         85  
  40         159953  
39              
40             my $the_logger;
41             my $the_locker;
42              
43             # Simplifies having both classes in the module use the same package.
44             my $pkg = __PACKAGE__;
45              
46             # -----------------------------------------------------------------------------
47             # Policy code: The following allows the calling code to supply two objects that
48             # specify the behavior for logging and file locking. This approach was applied
49             # to allow these policies to be changed without requiring the overhead of the
50             # default implementations.
51              
52             # The default logging code is simple enough that I am including it inline
53             # instead of making a separate class file. All methods are forwarded to the
54             # CORE C and C functions.
55             {
56              
57             package DefaultLogger;
58             {
59             $DefaultLogger::VERSION = '0.606';
60             }
61              
62             sub new {
63 23     23   71 my ($class) = @_;
64 23         81 return bless {}, $class;
65             }
66              
67             sub throw {
68 52     52   67 my $self = shift;
69 52         676 die @_;
70             }
71              
72             sub warn {
73 1     1   2 my $self = shift;
74 1         7 CORE::warn @_;
75 1         6 return;
76             }
77              
78             sub info {
79 1     1   1 my $self = shift;
80 1         5 CORE::warn @_;
81 1         6 return;
82             }
83              
84             sub notify {
85 0     0   0 my $self = shift;
86 0         0 return;
87             }
88             }
89              
90             sub _throw {
91 36     36   68 my $self = shift;
92 36         97 _get_logger()->throw(@_);
93             }
94              
95             sub _warn {
96 16     16   27 my $self = shift;
97 16         31 _get_logger()->warn(@_);
98             }
99              
100             sub _notify {
101 8     8   15 my $self = shift;
102 8         20 _get_logger()->notify(@_);
103             }
104              
105             # We never use _info, so remove it
106              
107             sub _get_logger {
108 150 100   150   452 unless ( defined $the_logger ) {
109 23         170 $the_logger = DefaultLogger->new();
110             }
111 150         753 return $the_logger;
112             }
113              
114             sub _get_locker {
115 61 100   61   224 unless ( defined $the_locker ) {
116 29     34   2266 eval 'use cPanel::StateFile::FileLocker;'; ## no critic (ProhibitStringyEval)
  34         29926  
  33         212  
  33         694  
117 29 50       164 $pkg->_throw(@_) if $@;
118 29         201 $the_locker = cPanel::StateFile::FileLocker->new( { logger => _get_logger() } );
119             }
120 61         341 return $the_locker;
121             }
122              
123             my $are_policies_set = 0;
124              
125             #
126             # This method allows changing the policies for logging and locking.
127             sub import {
128 17     17   3497 my $class = shift;
129 17 100       95 die 'Not an even number of arguments to the $pkg module' if @_ % 2;
130 16 50       105 die 'Policies already set elsewhere' if $are_policies_set;
131 16 100       2106 return 1 unless @_; # Don't set the policies flag.
132              
133 12         45 while (@_) {
134 12         40 my ( $policy, $object ) = splice( @_, 0, 2 );
135 12 50       45 next unless defined $object;
136 12 100       42 if ( '-logger' eq $policy ) {
    100          
137 9 100       34 unless ( ref $object ) {
138 5     1   1297 eval "use $object;"; ## no critic (ProhibitStringyEval)
  1         599  
  0            
  0            
139 5 100       31 die $@ if $@;
140              
141             # convert module into an object.
142 4         17 $object = $object->new;
143             }
144 8 100       62 die 'Supplied logger object does not support the correct interface.'
145             unless _valid_logger($object);
146 7         29 $the_logger = $object;
147             }
148             elsif ( '-filelock' eq $policy ) {
149 2 100       10 unless ( ref $object ) {
150 1         91 eval "use $object;"; ## no critic (ProhibitStringyEval)
151 1 50       10 die $@ if $@;
152              
153             # convert module into an object.
154 0         0 $object = $object->new;
155             }
156 1 50       7 die 'Supplied filelock object does not support the correct interface.'
157             unless _valid_file_locker($object);
158 0         0 $the_locker = $object;
159             }
160             else {
161 1         10 die "Unrecognized policy '$policy'";
162             }
163             }
164 7         14 $are_policies_set = 1;
165 7         2173 return 1;
166             }
167              
168             {
169              
170             #
171             # Nested class to handle locking cleanly.
172             #
173             # Locks the file on creation, throwing on failure. Unlocks the file when
174             # the object is destroyed.
175             {
176              
177             package cPanel::StateFile::Guard;
178             {
179             $cPanel::StateFile::Guard::VERSION = '0.606';
180             }
181              
182             sub new {
183 464     464   961 my ( $class, $args_ref ) = @_;
184 464 50       1368 $pkg->throw('Args parameter must be a hash reference.') unless 'HASH' eq ref $args_ref;
185 464 50       1101 $pkg->throw('No StateFile.') unless exists $args_ref->{state};
186              
187 464         1693 my $self = bless { state_file => $args_ref->{state} }, $class;
188              
189 464         1117 $self->_lock();
190              
191 464         873 return $self;
192             }
193              
194             sub DESTROY {
195 464     464   1947 my ($self) = @_;
196              
197             # make certain that an exception here cannot escape and won't effect
198             # exceptions currently in progress.
199 464         658 local $@;
200 464         943 eval { $self->_unlock(); };
  464         1027  
201 464         5203 return;
202             }
203              
204             sub _lock {
205 484     484   771 my $self = shift;
206 484         1036 my $state_file = $self->{state_file};
207              
208 484         1028 my $filename = $state_file->{file_name};
209 484         2494 $self->{lock_file} = $state_file->{locker}->file_lock($filename);
210 484 50       1805 $state_file->throw("Unable to acquire file lock for '$filename'.") unless $self->{lock_file};
211 484         811 return;
212             }
213              
214             sub _unlock {
215 489     489   677 my $self = shift;
216 489         732 my $state_file = $self->{state_file};
217 489 100       1098 return unless $self->{lock_file};
218              
219 484 100       1188 if ( $state_file->{file_handle} ) {
220              
221             # TODO probably need to check for failure, but then what do I do?
222 244         322 eval {
223 244     0   3889 local $SIG{'ALRM'} = sub { die "flock 8 timeout\n"; };
  0         0  
224 244         1322 my $orig_alarm = alarm $state_file->{flock_timeout};
225 244         1518 flock $state_file->{file_handle}, 8;
226 244         3181 alarm $orig_alarm;
227             };
228 244         3470 close $state_file->{file_handle};
229 244         449 $state_file->{file_handle} = undef;
230              
231             # Update size and timestamp after close.
232 244         4726 @{$state_file}{qw(file_size file_mtime)} = ( stat( $state_file->{file_name} ) )[ 7, 9 ];
  244         686  
233             }
234 484         2227 $state_file->{locker}->file_unlock( $self->{lock_file} );
235 484         933 $self->{lock_file} = undef;
236 484         898 return;
237             }
238              
239             sub call_unlocked {
240 28     28   1674 my ( $self, $code ) = @_;
241 28         66 my $state_file = $self->{state_file};
242 28 100       99 $state_file->throw('Cannot nest call_unlocked calls.') unless defined $self->{lock_file};
243 27 100       118 $state_file->throw('Missing coderef to call_unlocked.') unless 'CODE' eq ref $code;
244              
245             # unlock for the duration of the code execution
246 25         80 $self->_unlock();
247 25         44 eval { $code->(); };
  25         76  
248 20         1407 my $ex = $@;
249              
250             # relock even if exception.
251 20         544 $self->_lock();
252              
253             # probably should resync if necessary.
254 20         528 $state_file->_resynch($self);
255              
256 20 100       91 $pkg->_throw($ex) if $ex;
257              
258 19         94 return;
259             }
260              
261             sub _open {
262 205     205   559 my ( $self, $mode ) = @_;
263 205         328 my $state_file = $self->{state_file};
264 205 50       556 $state_file->throw('Cannot open state file inside a call_unlocked call.') unless defined $self->{lock_file};
265              
266 205 50       9060 open my $fh, $mode, $state_file->{file_name}
267             or $state_file->throw("Unable to open state file '$state_file->{file_name}': $!");
268             eval {
269 205     0   3544 local $SIG{'ALRM'} = sub { die "flock 2 timeout\n"; };
  0         0  
270 205         1099 my $orig_alarm = alarm $state_file->{flock_timeout};
271 205         1239 flock $fh, 2;
272 205         761 alarm $orig_alarm;
273 205         2516 1;
274 205 50       320 } or do {
275 0         0 close($fh);
276 0 0       0 if ( $@ eq "flock 2 timeout\n" ) {
277 0         0 $state_file->throw('Guard timed out trying to open state file.');
278             }
279             else {
280 0         0 $state_file->throw($@);
281             }
282             };
283 205         629 $state_file->{file_handle} = $fh;
284             }
285              
286             sub update_file {
287 226     226   1058 my ($self) = @_;
288 226         406 my $state_file = $self->{state_file};
289 226 100       641 $state_file->throw('Cannot update_file inside a call_unlocked call.') unless defined $self->{lock_file};
290              
291 225 50       530 if ( !$state_file->{file_handle} ) {
292 225 100       3409 if ( -e $state_file->{file_name} ) {
293 186         535 $self->_open('+<');
294             }
295             else {
296 39 50       2591 sysopen( my $fh, $state_file->{file_name}, &Fcntl::O_CREAT | &Fcntl::O_EXCL | &Fcntl::O_RDWR )
297             or $state_file->throw("Cannot create state file '$state_file->{file_name}': $!");
298 39         127 $state_file->{file_handle} = $fh;
299             }
300             }
301 225         1448 seek( $state_file->{file_handle}, 0, 0 );
302 225 50       9802 truncate( $state_file->{file_handle}, 0 )
303             or $state_file->throw("Unable to truncate the state file: $!");
304              
305 225         1721 $state_file->{data_object}->save_to_cache( $state_file->{file_handle} );
306 225         30738 $state_file->{file_mtime} = ( stat( $state_file->{file_handle} ) )[9];
307              
308             # Make certain we are at end of file.
309 225 50       9954589 seek( $state_file->{file_handle}, 0, 2 )
310             or $state_file->throw("Unable to go to end of file: $!");
311 225         644 $state_file->{file_size} = tell( $state_file->{file_handle} );
312 225         616 return;
313             }
314              
315             }
316              
317             # Back to StateFile
318              
319             sub new {
320 62     62 1 5254 my ( $class, $args_ref ) = @_;
321 62         257 my $self = bless {}, $class;
322 62 100 100     361 if ( exists $args_ref->{logger} && _valid_logger( $args_ref->{logger} ) ) {
323 1         4 $self->{logger} = $args_ref->{logger};
324             }
325             else {
326 61         391 $self->{logger} = $pkg->_get_logger();
327 61 100       366 if ( exists $args_ref->{logger} ) {
328 1         4 $self->throw('Supplied logger does not support required methods.');
329             }
330             }
331 61 50 66     287 if ( exists $args_ref->{locker} && _valid_file_locker( $args_ref->{locker} ) ) {
332 0         0 $self->{locker} = $args_ref->{locker};
333             }
334             else {
335 61         310 $self->{locker} = $pkg->_get_locker();
336 61 100       259 if ( exists $args_ref->{locker} ) {
337 1         4 $self->throw('Supplied locker does not support required methods.');
338             }
339             }
340 60 50 0     202 $args_ref->{state_file} ||= $args_ref->{cache_file} if exists $args_ref->{cache_file};
341 60 100       252 $self->throw('No state filename supplied.') unless exists $args_ref->{state_file};
342 57 100       252 $self->throw('No data object supplied.') unless exists $args_ref->{data_obj};
343 56         223 my $data_obj = $args_ref->{data_obj};
344             $self->throw('Data object does not have required interface.')
345 56         606 unless eval { $data_obj->can('load_from_cache') }
346 56 100 66     108 and eval { $data_obj->can('save_to_cache') };
  54         416  
347              
348 54         561 my ( $dirname, $file ) = ( $args_ref->{state_file} =~ m{^(.*)/([^/]*)$}g );
349 54         223 $dirname =~ s{[^/]+/\.\./}{/}g; # resolve parent references
350 54         119 $dirname =~ s{[^/]+/\.\.$}{};
351 54         104 $dirname =~ s{/\./}{/}g; # resolve self references
352 54         108 $dirname =~ s{/\.$}{};
353 54 100       1390 if ( !-d $dirname ) {
354 11 50       28569 File::Path::mkpath($dirname)
355             or $self->throw("Unable to create Cache directory ('$dirname').");
356             }
357 54         244 $self->{file_name} = "$dirname/$file";
358              
359 54         122 $self->{data_object} = $data_obj;
360 54         129 $self->{file_mtime} = -1;
361 54         121 $self->{file_size} = -1;
362 54         128 $self->{file_handle} = undef;
363 54   50     484 $self->{flock_timeout} = $args_ref->{timeout} || 60;
364 54         300 Scalar::Util::weaken( $self->{data_object} );
365              
366 54         216 $self->synch();
367              
368 46         276 return $self;
369             }
370              
371             #
372             # Return true if the supplied logger object implements the correct
373             # interface, false otherwise.
374             sub _valid_logger {
375 10     10   22 my ($logger) = @_;
376              
377 10         29 foreach my $method (qw/throw warn info notify/) {
378 34 100       41 return unless eval { $logger->can($method) };
  34         215  
379             }
380              
381 8         40 return 1;
382             }
383              
384             #
385             # Return true if the supplied file locker object implements the correct
386             # interface, false otherwise.
387             sub _valid_file_locker {
388 2     2   6 my ($locker) = @_;
389              
390 2         6 foreach my $method (qw/file_lock file_unlock/) {
391 2 50       5 return unless eval { $locker->can($method) };
  2         33  
392             }
393              
394 0         0 return 1;
395             }
396              
397             sub synch {
398 464     464 1 3724 my ($self) = @_;
399              
400             # need to set the lock asap to avoid any concurrency problem
401 464         4497 my $guard = cPanel::StateFile::Guard->new( { state => $self } );
402              
403 464 100 100     11557 if ( !-e $self->{file_name} or -z _ ) {
404              
405             # File doesn't exist or is empty, initialize it.
406 40         178 $guard->update_file();
407             }
408             else {
409 424         1282 $self->_resynch($guard);
410             }
411              
412             # if not assigned anywhere, let the guard die.
413 456 100       1541 return unless defined wantarray;
414              
415             # Otherwise return it.
416 187         593 return $guard;
417             }
418              
419             sub _resynch {
420 444     444   717 my ( $self, $guard ) = @_;
421              
422 444         6564 my ( $mtime, $size ) = ( stat( $self->{file_name} ) )[ 9, 7 ];
423 444 100 100     3070 if ( $self->{file_mtime} < $mtime || $self->{file_size} != $size ) {
424              
425             # File is newer or a different size
426 19   33     67 $guard ||= cPanel::StateFile::Guard->new( { state => $self } );
427 19         76 $guard->_open('+<');
428 19         121 $self->{data_object}->load_from_cache( $self->{file_handle} );
429 11         141 ( $self->{file_mtime}, $self->{file_size} ) = ( stat( $self->{file_handle} ) )[ 9, 7 ];
430             }
431              
432 436         822 return $guard;
433             }
434              
435 22     22 1 172 sub get_logger { return $_[0]->{logger}; }
436              
437             sub throw {
438 28     28 1 41 my $self = shift;
439 28         96 return $self->{logger}->throw(@_);
440             }
441              
442             sub warn {
443 2     2 1 1751 my $self = shift;
444 2         16 return $self->{logger}->warn(@_);
445             }
446              
447             sub info {
448 3     3 1 367 my $self = shift;
449 3         18 return $self->{logger}->info(@_);
450             }
451             }
452              
453             1;
454              
455             __END__