File Coverage

blib/lib/App/DistSync/LockFile.pm
Criterion Covered Total %
statement 92 114 80.7
branch 28 58 48.2
condition 12 27 44.4
subroutine 20 21 95.2
pod 8 8 100.0
total 160 228 70.1


line stmt bran cond sub pod time code
1             package App::DistSync::LockFile;
2 1     1   88822 use strict;
  1         4  
  1         53  
3 1     1   6 use warnings;
  1         2  
  1         81  
4 1     1   548 use utf8;
  1         251  
  1         4  
5              
6             =encoding utf-8
7              
8             =head1 NAME
9              
10             App::DistSync::LockFile - Lock File management package
11              
12             =head1 SYNOPSIS
13              
14             use App::DistSync::LockFile;
15              
16             my $lf = App::DistSync::LockFile->new (
17             file => '/tmp/file.lock',
18             pid => $$,
19             auto => 0,
20             );
21              
22             if ( my $pid = $lf->check ) {
23             warn $lf->error if $lf->error;
24             die "Already running: $pid";
25             }
26              
27             $lf->lock;
28             die $lf->error if $lf->error;
29              
30             # . . . do stuff . . .
31              
32             $lf->unlock;
33             die $lf->error if $lf->error;
34              
35             ... or with auto-lock and auto-unlock:
36              
37             my $lf = App::DistSync::LockFile->new (
38             file => '/tmp/file.lock',
39             pid => $$,
40             auto => 1,
41             );
42              
43             die $lf->error if $lf->error;
44             die "Already running" if $lf->check;
45              
46             # . . . do stuff . . .
47              
48             =head1 DESCRIPTION
49              
50             This package manages a lock files. It will create a lock file,
51             query the process within to discover if it's still running, and remove
52             the lock file. This module based on L, L,
53             L, L and L.
54              
55             =head1 METHODS
56              
57             This module implements the following methods
58              
59             =head2 new
60              
61             my $lf = new App::DistSync::LockFile(
62             file => '/tmp/file.lock',
63             delay => 60,
64             retries => 5,
65             pid => $$,
66             auto => 1,
67             );
68              
69             This constructor takes several optional attributes:
70              
71             =over 4
72              
73             =item auto
74              
75             auto => 0
76              
77             If this flag specified as true, then
78             will be saved the lock file automatically while instance create and
79             removed the lock file automatically on DESTROY phase. Default: false
80              
81             =item debug
82              
83             debug => 0
84              
85             Print debugging messages to STDERR (0=Off (default), 1=On)
86              
87             =item delay
88              
89             delay => 60
90              
91             Number of seconds to wait between retries to getting a lockfile
92              
93             Default: 60
94              
95             =item file
96              
97             file => '/tmp/test.lock'
98              
99             The name of the lock file to work on. If not specified, a lock
100             file located in current directory will be created that matches F<./basename($0).lock>.
101              
102             =item pid
103              
104             pid => $$
105              
106             The pid to write to a new lockfile. If not specified, C<$$> is
107             used when the lock file doesn't exist. When the lock file does exist, the
108             pid inside it is used.
109              
110              
111             =item retries
112              
113             retries => 5
114              
115             Number of times to retry getting a lockfile
116              
117             Default: 5
118              
119             =back
120              
121             =head2 check
122              
123             if ( my $pid = $lf->check ) {
124             warn $lf->error if $lf->error;
125             die "Already running: $pid";
126             }
127              
128             This method checks the lock file and returns the PID of the process that first acquired the lock.
129             Otherwise returns 0 if no lock file found
130              
131             =head2 error
132              
133             my $error = $lf->error;
134              
135             Returns current error message
136              
137             =head2 file
138              
139             my $file = $lf->file;
140              
141             Accessor for the filename used as the lock file.
142              
143             =head2 lock
144              
145             $self = $self->lock;
146              
147             This method creates a lock file and stores the current PID in it.
148              
149             =head2 own
150              
151             $lf->own(123);
152             my $owner_did = $lf->own;
153              
154             Accessor/mutator for the pid being saved to the lock file.
155              
156             =head2 pid
157              
158             my $pid = $lf->pid;
159              
160             Accessor for the pid being saved to the lock file.
161              
162             =head2 unlock
163              
164             $self = $self->unlock;
165              
166             This method performs unlocking the lock file and removes it
167              
168             =head1 HISTORY
169              
170             See C file
171              
172             =head1 TO DO
173              
174             See C file
175              
176             =head1 SEE ALSO
177              
178             L, L, L,
179             L and L
180              
181             =head1 AUTHOR
182              
183             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
184              
185             =head1 COPYRIGHT
186              
187             Copyright (C) 1998-2026 D&D Corporation
188              
189             =head1 LICENSE
190              
191             This program is distributed under the terms of the Artistic License Version 2.0
192              
193             See the C file or L for details
194              
195             =cut
196              
197 1     1   56 use Carp qw/croak/;
  1         3  
  1         53  
198 1     1   4 use File::Spec;
  1         1  
  1         18  
199 1     1   3 use File::Basename qw/basename/;
  1         1  
  1         70  
200 1     1   359 use IO::File qw//;
  1         9332  
  1         51  
201 1     1   13 use Cwd qw/getcwd/;
  1         9  
  1         91  
202              
203             use constant {
204 1         1456 RETRIES => 5,
205             DELAY => 60,
206 1     1   5 };
  1         1  
207              
208             sub new {
209 2     2 1 146523 my $class = shift;
210 2 50       16 my $args = @_ ? @_ > 1 ? {@_} : {%{$_[0]}} : {};
  0 50       0  
211 2         6 my $self = bless {%$args}, $class;
212 2   50     15 $self->{debug} ||= 0;
213 2         6 $self->{error} = "";
214 2   33     7 $self->{file} //= File::Spec->catfile(getcwd, sprintf("%s.lock", basename($0)));
215 2   33     38 $self->{pid} ||= $$; # Current PID by default
216 2   50     11 $self->{own} ||= 0; # Owner PID
217 2   100     9 $self->{auto} //= 0;
218 2   50     12 $self->{retries} //= RETRIES;
219 2   50     9 $self->{delay} //= DELAY;
220 2         5 $self->{_is_locked} = 0;
221 2 50       14 croak("Incorrect pid attribute") unless $self->{pid} =~ /^[0-9]{1,11}$/;
222 2 50       9 croak("Incorrect retries attribute") unless $self->{retries} =~ /^[0-9]{1,5}$/;
223 2 50       7 croak("Incorrect delay attribute") unless $self->{delay} =~ /^[0-9]{1,5}$/;
224              
225             # Lock file
226 2 100       7 return $self->lock if $self->{auto};
227 1         9 return $self;
228             }
229              
230 19     19 1 444 sub file { shift->{file} }
231 8     8 1 59 sub pid { shift->{pid} }
232             sub own {
233 11     11 1 11 my $self = shift;
234 11 100       23 if (scalar(@_) >= 1) {
235 3         5 $self->{own} = shift;
236 3         4 return $self;
237             }
238 8         54 return $self->{own};
239             }
240             sub error {
241 2     2 1 2 my $self = shift;
242 2 50       6 if (scalar(@_) >= 1) {
243 0         0 $self->{error} = shift;
244 0         0 return $self;
245             }
246 2         18 return $self->{error};
247             }
248             sub lock {
249 3     3 1 14 my $self = shift;
250 3 100       10 if ($self->_is_locked) {
251 1         3 $self->_debug(sprintf("File %s already locked", $self->file));
252 1         4 return $self;
253             }
254              
255             # Signals
256             $SIG{HUP} = $SIG{QUIT} = $SIG{INT} = $SIG{TERM} = sub {
257 0     0   0 $self->_debug( "Caught SIG$_[0]" );
258 0         0 exit;
259 2         50 };
260              
261             # Save temp file
262 2         10 my $tmp_file = sprintf("%s.%d", $self->file, $self->pid);
263 2 50       335 if (open(my $fh, '>', $tmp_file)) {
264 2   33     10 printf $fh "%d\n", $self->pid || $$;
265 2         86 close $fh;
266              
267             # Rename temp file to lock file
268 2         9 for my $try (0 .. $self->{retries}) {
269 2 50       6 unless ($self->check()) { # not exists, ok
270 2 50       5 if (rename($tmp_file, $self->file)) {
271 2         6 $self->{_is_locked} = 1;
272 2         6 $self->_debug("Got lock file");
273 2         16 return $self;
274             }
275             }
276 0 0 0     0 if ($self->{retries} && ($try != $self->{retries})) {
277 0         0 $self->_debug(sprintf("Retrying in %d seconds", $self->{delay}));
278 0 0       0 sleep $self->{delay} unless ($try == $self->{retries});
279             }
280             }
281              
282             } else {
283 0         0 $self->error(sprintf("Could not write to %s: $!", $tmp_file))->_debug($self->error);
284             }
285              
286             # Remove temp file in silent mode
287 0 0       0 unlink $tmp_file if -f $tmp_file;
288              
289             # Ok
290 0         0 return $self;
291             }
292             sub check {
293 5     5 1 10 my $self = shift;
294 5 100       10 return 0 unless -f $self->file;
295              
296             # Load file
297 2 50       11 if (open(my $fh, $self->file)) {
298 2         33 chomp(my $line = <$fh>);
299 2         14 close $fh;
300 2 50 50     24 $self->own(($line || 0) * 1) if $line =~ /^\d+$/;
301 2         5 $self->_debug(sprintf("Found owner PID=%d in %s", $self->own, $self->file));
302              
303             # Check PID and owner PID
304 2 50       5 if ($self->own == $self->pid) {
305 2         4 $self->_debug(sprintf("An attempt to call the check method twice was detected for PID=%d", $self->own));
306 2         3 return $self->own;
307             }
308              
309             # Check owner PID
310 0 0       0 if ( kill(0, $self->own) ) {
311 0         0 $self->_debug(sprintf("Found valid existing lock file for PID=%d", $self->own));
312 0         0 return $self->own;
313             } else {
314 0 0       0 $self->error(sprintf("Could not unlink %s: $!", $self->file))->_debug($self->error)
315             unless unlink $self->file;
316 0 0       0 $self->own(0) unless -f $self->file; # Reset owner PID to 0
317 0         0 $self->_debug("Found and removed stale lock file");
318             }
319             } else {
320 0         0 $self->error(sprintf("Could not read %s: $!", $self->file))->_debug($self->error);
321             }
322              
323 0         0 return 0;
324             }
325             sub unlock {
326 1     1 1 6 my $self = shift;
327              
328             # Remove lock file
329 1 50       3 if ($self->_is_locked) {
330 1 50       3 $self->error(sprintf("Could not unlink %s: $!", $self->file))->_debug($self->error)
331             unless unlink $self->file;
332 1 50       4 $self->own(0) unless -f $self->file; # Reset owner PID to 0
333             } else {
334 0         0 $self->own(0) # Reset owner PID to 0
335             }
336              
337             # Remove temp file in silent mode
338 1         3 my $tmp_file = sprintf("%s.%d", $self->file, $self->pid);
339 1 50       12 unlink $tmp_file if -f $tmp_file;
340              
341 1         9 return $self;
342             }
343              
344             sub _is_locked {
345 4     4   23 my $self = shift;
346 4 100 66     15 return ($self->{_is_locked} && -f $self->file) ? 1 : 0
347             }
348             sub _debug {
349 7     7   8 my $self = shift;
350 7 50       14 warn sprintf("%s: %s\n", ref($self), join("\n", @_)) if $self->{debug};
351             }
352              
353             sub DESTROY {
354 1     1   2 my $self = shift;
355 1 50       6 return unless $self->{auto};
356 0           $self->_debug("Cleaning up...");
357 0           $self->unlock();
358             }
359              
360             1;
361              
362             __END__