File Coverage

blib/lib/cPanel/StateFile/FileLocker.pm
Criterion Covered Total %
statement 79 120 65.8
branch 29 62 46.7
condition 2 12 16.6
subroutine 11 13 84.6
pod 3 3 100.0
total 124 210 59.0


line stmt bran cond sub pod time code
1             package cPanel::StateFile::FileLocker;
2             {
3             $cPanel::StateFile::FileLocker::VERSION = '0.606';
4             }
5              
6             # cpanel - cPanel/StateFile/FileLocker.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             #use warnings;
33 37     37   512386 use strict;
  37         113  
  37         1555  
34 37     37   261 use Fcntl ();
  37         117  
  37         60935  
35              
36             sub new {
37 36     36 1 11536 my ( $class, $args_hr ) = @_;
38 36 100       210 $args_hr = {} unless defined $args_hr;
39 36 100       200 die "Argument to new must be a hash reference.\n" unless 'HASH' eq ref $args_hr;
40 35 100       230 die "Required logger argument is missing.\n" unless exists $args_hr->{logger};
41 33         235 my %args = (
42             attempts => 5,
43             max_wait => 300, # five minutes
44             max_age => 300, # five minutes
45             flock_timeout => 60,
46             sleep_secs => 1,
47 33         76 %{$args_hr},
48             );
49 33 100       169 $args{sleep_secs} = 1 if $args{sleep_secs} < 1;
50              
51 33         257 return bless \%args, $class;
52             }
53              
54             sub file_lock {
55 488     488 1 2636 my ( $self, $filename ) = @_;
56 488         1008 my $attempts = $self->{attempts};
57 488         956 my $lockfile = $filename . '.lock';
58 488         1081 $lockfile =~ tr/<>;&|//d;
59              
60             # wait up to the maximum time to hold a lock.
61 488         1247 my $deadline = time + $self->{max_wait};
62             ATTEMPT:
63 488         1417 while ( $attempts-- > 0 ) {
64              
65             # Try to create a lockfile
66 489 100       47894 if ( sysopen( my $fh, $lockfile, &Fcntl::O_WRONLY | &Fcntl::O_EXCL | &Fcntl::O_CREAT ) ) {
67              
68             # success
69 486         2865 my $ex = _flock_timeout( $fh, &Fcntl::LOCK_EX, $self->{flock_timeout} );
70 486 50       1218 if ($ex) {
71 0         0 close $fh;
72 0         0 $self->_throw("Timeout writing lockfile '$lockfile'.");
73             }
74              
75 486         6049 print $fh $$, "\n", $0, "\n", ( time + $self->{max_wait} ), "\n";
76              
77 486         4067270 close $fh;
78 486         3459 return $lockfile;
79             }
80              
81             # Unable to create the lockfile.
82 3         14 $self->_info('Unable to create the lockfile, waiting');
83              
84 3         34 while ( $deadline > time ) {
85 3         11 my ( $pid, $name, $max_time ) = $self->_read_lock_file($lockfile);
86 2 50       8 unless ($pid) {
87              
88             # couldn't read the file. If it doesn't exist, try to create.
89 0 0       0 next ATTEMPT unless -e $lockfile;
90 0         0 sleep $self->{sleep_secs};
91 0         0 next;
92             }
93 2 50       12 if ( time > $max_time ) {
94              
95             # The file says it is expired.
96 0         0 my $expired = time - $max_time;
97 0         0 $self->_info("Stale lock file '$lockfile': lock expired $expired seconds ago, removing...");
98 0         0 unlink $lockfile;
99 0         0 next ATTEMPT;
100             }
101 2 100 66     23 if ( $pid == $$ and $0 eq $name ) {
    50          
    0          
102 1         4 $self->_throw("Attempting to relock '$filename'.");
103             }
104             elsif ( $pid == $$ ) {
105              
106             # Was locked by another process with this PID or $0 changed.
107 1         28 $self->_warn("Inconsistent lock: my PID but process named '$name': removing lock");
108 1         317 unlink $lockfile;
109 1         18 next ATTEMPT;
110             }
111             elsif ( !_pid_alive( $lockfile, $pid ) ) {
112 0 0       0 if ( -e $lockfile ) {
113 0         0 $self->_warn('Removing abandoned lock file.');
114 0         0 unlink $lockfile;
115             }
116 0         0 next ATTEMPT;
117             }
118              
119 0         0 sleep $self->{sleep_secs};
120             }
121             }
122              
123 0         0 $self->_throw("Failed to acquire lock for '$filename'.");
124             }
125              
126             sub file_unlock {
127 487     487 1 2794 my ( $self, $lockfile ) = @_;
128              
129 487 50       1094 $self->_throw('Missing lockfile name.') unless $lockfile;
130 487         825 $lockfile =~ tr/<>;&|//d;
131 487 50       7021 unless ( -e $lockfile ) {
132 0         0 $self->_warn("Lockfile '$lockfile' lost!");
133 0         0 return;
134             }
135 487         1435 my ( $pid, $name, $wait_time ) = $self->_read_lock_file($lockfile);
136 487 50       1350 unless ( defined $pid ) {
137 0         0 $self->_warn("Lockfile '$lockfile' lost!");
138 0         0 return;
139             }
140              
141 487 50       1393 if ( 0 == $pid ) {
142 0         0 $self->_warn('Zero-length lockfile deleted.');
143 0         0 return;
144             }
145 487 100       1369 if ( $$ == $pid ) {
146 486         37898 unlink $lockfile;
147 486         1589 return;
148             }
149             else {
150 1         7 $self->_throw("Attempt to unlock file '$lockfile' locked by another process '$pid'.");
151             }
152              
153             }
154              
155             sub _throw {
156 3     3   6 my $self = shift;
157 3         12 $self->{logger}->throw(@_);
158             }
159              
160             sub _warn {
161 1     1   3 my $self = shift;
162 1         8 return $self->{logger}->warn(@_);
163             }
164              
165             sub _info {
166 3     3   9 my $self = shift;
167 3         16 return $self->{logger}->info(@_);
168             }
169              
170             #
171             # Do flock call with a built in timeout.
172             #
173             # $fh - filehandle to flock
174             # $how - parameter for flock
175             # $when - timeout if it takes this many seconds.
176             #
177             # returns undef on success or "Timeout on flock\n" if it timed out.
178             sub _flock_timeout {
179 976     976   1620 my ( $fh, $how, $when ) = @_;
180 976         1040 my $orig_alarm;
181 976         1249 eval {
182 976     0   16616 local $SIG{'ALRM'} = sub { die "Timeout on flock\n"; };
  0         0  
183 976         5083 $orig_alarm = alarm $when;
184 976         15987 flock $fh, $how;
185             };
186 976         1645 my $ex = $@;
187 976         3669 alarm $orig_alarm;
188 976         3150 return $ex;
189             }
190              
191             # Read information out of a lock file.
192             # Attempts multiple times, locks file while reading, deals with files that vanish, etc.
193             # Returns:
194             # (pid, name) from file if successful.
195             # undef if lock file vanished
196             # (0, 0) if zero-length file and we deleted it.
197             sub _read_lock_file {
198 490     490   718 my ( $self, $lockfile ) = @_;
199              
200 490         777 my $attempts = $self->{attempts};
201 490         1216 while ( $attempts-- > 0 ) {
202 490 50       19909 if ( open( my $fh, '<', $lockfile ) ) {
203 490         1873 my $ex = _flock_timeout( $fh, &Fcntl::LOCK_SH, $self->{flock_timeout} );
204 490 50       1458 $self->_throw("Timeout reading lockfile '$lockfile'.") if $ex;
205              
206             # Provide defaults in case we did not have 3 lines.
207 490         9484 my ( $pid, $name, $wait_time ) = ( <$fh>, '', '', '' );
208              
209 490         5927 close $fh;
210 490 50       1164 unless ($pid) { # retry, we got between open and lock (probably).
211 0         0 sleep $self->{sleep_secs};
212 0         0 next;
213             }
214              
215 490         923 chomp( $pid, $name, $wait_time );
216 490 100       1878 $self->_throw("Invalid lock file: '$pid' is not a PID.") if $pid =~ /\D/;
217 489 50       1113 $name = '' unless length $name;
218 489 50       1288 $wait_time = 0 if $wait_time =~ /\D/;
219 489         2546 return ( $pid, $name, $wait_time );
220             }
221 0 0       0 return unless -e $lockfile; # file vanished, no longer locked.
222              
223 0 0       0 $self->_throw("Cannot open lock file '$lockfile' for reading.") unless -r _;
224 0         0 sleep $self->{sleep_secs};
225             }
226              
227 0         0 my $lock_age = time - ( stat($lockfile) )[9];
228              
229             # not the same as max_timeout, really looking at 5 minutes as old.
230 0 0       0 if ( -z $lockfile ) {
231 0 0       0 if ( $lock_age > $self->{max_age} ) {
232              
233             # the file has existed for some time but still has nothing in it.
234             # kill it.
235 0         0 $self->_info('Old, but empty lock file deleted.');
236 0         0 unlink $lockfile;
237 0         0 return ( 0, 0, 0 );
238             }
239 0         0 return;
240             }
241              
242 0         0 $self->_throw("Unable to read lockfile '$lockfile'");
243             }
244              
245             #
246             # Test the supplied lock and pid to see if the process is still alive.
247             #
248             # $lockfile - file lock we are testing.
249             # $pid - expected owner of the lockfile.
250             #
251             # Return false is the process no longer exists, true if the process exists
252             # or is we can not tell.
253             sub _pid_alive {
254 0     0   0 my ( $lockfile, $pid ) = @_;
255              
256             # if we can use kill to check the pid, it is best choice.
257 0         0 my $fileuid = ( stat($lockfile) )[4];
258 0 0 0     0 if ( $> == 0 || $> == $fileuid ) {
259 0 0 0 8   0 return 0 unless kill( 0, $pid ) or $!{EPERM};
  8         8827  
  8         15530  
  8         752  
260             }
261              
262             # If the proc filesystem is available, it's a good test.
263 0 0 0     0 return -r "/proc/$pid" if -e "/proc/$$" && -r "/proc/$$";
264              
265             # Default to alive, because we can't figure it out.
266 0         0 return 1;
267             }
268              
269             1;
270              
271             __END__