File Coverage

blib/lib/File/Lockfile/Emacs.pm
Criterion Covered Total %
statement 56 91 61.5
branch 16 54 29.6
condition 5 15 33.3
subroutine 8 11 72.7
pod 4 4 100.0
total 89 175 50.8


line stmt bran cond sub pod time code
1             package File::Lockfile::Emacs;
2              
3 2     2   568471 use 5.010001;
  2         8  
4 2     2   13 use strict;
  2         4  
  2         65  
5 2     2   11 use warnings;
  2         8  
  2         120  
6 2     2   4255 use Log::ger;
  2         126  
  2         15  
7              
8 2     2   628 use Exporter qw(import);
  2         4  
  2         3738  
9              
10             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
11             our $DATE = '2025-03-13'; # DATE
12             our $DIST = 'File-Lockfile-Emacs'; # DIST
13             our $VERSION = '0.002'; # VERSION
14              
15             our @EXPORT_OK = qw(
16             emacs_lockfile_get
17             emacs_lockfile_lock
18             emacs_lockfile_locked
19             emacs_lockfile_unlock
20             );
21              
22             our %SPEC;
23              
24             our %argspec0_target_file = (
25             target_file => {
26             summary => 'Target file',
27             schema => 'filename*',
28             req => 1,
29             pos => 0,
30             },
31             );
32              
33             our %argspecopt_force = (
34             force => {
35             schema => 'bool*',
36             cmdline_aliases => {f=>{}},
37             },
38             );
39              
40             sub _lockfile_path {
41 6     6   12 my ($target_path) = @_;
42 6         11 my $lockfile_path = $target_path;
43 6 50       18 if ($lockfile_path =~ m!/!) {
44 0         0 $lockfile_path =~ s!(.*/)(.*)!"$1#$2"!
45             } else {
46 6         10 $lockfile_path = ".#$target_path";
47             }
48 6         29 log_trace "Lockfile path: %s", $lockfile_path;
49 6         16 $lockfile_path;
50             }
51              
52             sub _read_lockfile {
53 2     2   6 my ($target_path) = @_;
54              
55 2         3 my $lockfile_path = _lockfile_path($target_path);
56 2         24 my $is_symlink = -l $lockfile_path;
57 2         10 my $res = {exists=>0, path=>$lockfile_path};
58 2         2 my $content;
59 2 50       5 if ($is_symlink) {
60 2         4 $res->{exists} = 1;
61 2         45 $content = readlink($lockfile_path);
62 2 50       7 if (!defined($content)) {
63 0         0 $res->{error} = "Can't read link: $!";
64 0         0 return $res;
65             }
66             } else {
67             open my($fh), "<", $lockfile_path
68 0 0       0 or do { $res->{error} = "Can't read file: $!"; return $res };
  0         0  
  0         0  
69 0         0 { local $/ = undef; $content = <$fh>; close $fh }
  0         0  
  0         0  
  0         0  
70             }
71 2         7 log_trace "Lockfile content: %s", $content;
72             $content =~ /\A(.+)\@(.+)\.(\d+)(?::(\d+))?\R?\z/s
73 2 50       18 or do { $res->{error} = "Bad syntax in lock file content, does not match user\@host.pid:boot"; return $res };
  0         0  
  0         0  
74              
75 2         6 $res->{user} = $1;
76 2         5 $res->{host} = $2;
77 2         6 $res->{pid} = $3;
78 2 50       7 $res->{boot} = $4 if $4;
79              
80 2         4 $res;
81             }
82              
83             $SPEC{emacs_lockfile_get} = {
84             v => 1.1,
85             summary => "Get information on an Emacs lockfile of a target file",
86             args => {
87             %argspec0_target_file,
88             },
89             description => <<'MARKDOWN',
90              
91             MARKDOWN
92             };
93             sub emacs_lockfile_get {
94 0     0 1 0 my %args = @_;
95 0 0       0 defined(my $target_file = $args{target_file}) or return [400, "Please specify target_file"];
96              
97 0         0 my $lockinfo = _read_lockfile($target_file);
98              
99 0 0       0 return [500, $lockinfo->{error}] if $lockinfo->{error};
100 0         0 [200, "OK", $lockinfo];
101             }
102              
103             $SPEC{emacs_lockfile_lock} = {
104             v => 1.1,
105             summary => "Lock a file using Emacs-style lockfile",
106             args => {
107             %argspec0_target_file,
108             %argspecopt_force,
109             },
110             description => <<'MARKDOWN',
111              
112             Will return 412 if target file does not exist (unless `force` option is set to
113             true, in which case we proceed to locking anyway).
114              
115             Will return 304 if target file is already locked using Emacs-style lockfile by
116             the same process as us.
117              
118             Will return 409 if target file is already locked using Emacs-style lockfile by
119             another process (unless when `force` option is set to true, in which case will
120             take over the lock). Note that there are race conditions when using the `force`
121             option (between checking that the lockfile, unlinking it, and creating our own).
122             It is not recommended to use the `force` option.
123              
124             Will return 500 if there's an error in reading the lockfile.
125              
126             Will return 412 if we are not the same process that locks the file (unless
127             `force` option is set to true, in which case we proceed to unlocking anyway).
128              
129             Will return 500 if there's an error in removing the lockfile.
130              
131             Will return 200 if everything goes ok.
132              
133             MARKDOWN
134             };
135             sub emacs_lockfile_lock {
136 5     5 1 475600 my %args = @_;
137 5 50       25 defined(my $target_file = $args{target_file}) or return [400, "Please specify target_file"];
138 5         14 my $force = $args{force};
139              
140 5 100 100     94 return [412, "Target file does not exist"] if !$force && !(-f $target_file);
141              
142             my $new_lockinfo = {
143             user => $ENV{USERNAME} // $ENV{USER} // $ENV{LOGNAME},
144 4         1265 host => do { require Sys::Hostname; Sys::Hostname::hostname() },
  4         1865  
145             pid => $$,
146 4   33     36 boot => do { require Unix::Uptime; time() - Unix::Uptime->uptime },
  4   33     722  
  4         2102  
147             };
148 4         523 my $lockfile_path = _lockfile_path($target_file);
149              
150             L1:
151              
152             # try creating a lock
153 5 100       920 if (symlink "$new_lockinfo->{user}\@$new_lockinfo->{host}.$new_lockinfo->{pid}:$new_lockinfo->{boot}", $lockfile_path) {
154 3         40 return [200, "Locked"];
155             }
156              
157             # file is probably already locked
158 2         27 my $old_lockinfo = _read_lockfile($target_file);
159              
160             return [500, "Couldn't create lockfile but lockfile doesn't exist, probably permission problem"]
161 2 50       5 unless $old_lockinfo->{exists};
162              
163             return [500, "Can't get lockfile information: $old_lockinfo->{error}"]
164 2 50       6 if $old_lockinfo->{error};
165              
166 2 50       7 if ($new_lockinfo->{pid} != $old_lockinfo->{pid}) {
167 2 100       5 if ($force) {
168             # note that there are race conditions between checking existing lock
169             # above, unlinking it, and creating the new lock. Thus it's not
170             # really recommended to use the `force` option.
171              
172             # unlock this old lockfile
173 1 50       88 unlink $lockfile_path or return [500, "Can't remove old lockfile '$lockfile_path': $!"];
174 1         9 goto L1;
175             }
176              
177 1         11 return [412, "Target file was not locked by us (pid $$) but by pid $old_lockinfo->{pid}"];
178             }
179              
180 0           return [304, "File was already locked"];
181             }
182              
183             $SPEC{emacs_lockfile_locked} = {
184             v => 1.1,
185             summary => "Check whether a target file is locked using Emacs-style lockfile",
186             args => {
187             %argspec0_target_file,
188             by_us => {
189             summary => 'If set to true, only return true when lockfile is created by us; if false, then will only return true when lockfile is created by others',
190             schema => 'bool',
191             },
192             },
193             };
194             sub emacs_lockfile_locked {
195 0     0 1   my %args = @_;
196 0 0         defined(my $target_file = $args{target_file}) or return [400, "Please specify target_file"];
197              
198 0           my $lockinfo = _read_lockfile($target_file);
199              
200 0 0         return [500, $lockinfo->{error}] if $lockinfo->{error};
201 0 0         return [200, "OK", 0] unless $lockinfo->{exists};
202 0 0         return [200, "OK", 1] unless defined $args{by_us};
203 0 0         return [200, "OK", $args{by_us} ? ($$ == $lockinfo->{pid}) : ($$ != $lockinfo->{pid})];
204             }
205              
206             $SPEC{emacs_lockfile_unlock} = {
207             v => 1.1,
208             summary => "Unlock a file locked with Emacs-style lockfile",
209             args => {
210             %argspec0_target_file,
211             %argspecopt_force,
212             },
213             description => <<'MARKDOWN',
214              
215             Note that there is a race condition between reading the lockfile and unlinking
216             it.
217              
218             Will return 412 if target file does not exist (unless `force` option is set to
219             true, in which case we proceed to unlocking anyway).
220              
221             Will return 304 if target file is not currently locked using Emacs-style
222             lockfile.
223              
224             Will return 500 if there's an error in reading the lockfile.
225              
226             Will return 412 if we are not the same process that locks the file (unless
227             `force` option is set to true, in which case we proceed to unlocking anyway).
228              
229             Will return 500 if there's an error in removing the lockfile.
230              
231             Will return 200 if everything goes ok.
232              
233             MARKDOWN
234             };
235             sub emacs_lockfile_unlock {
236 0     0 1   my %args = @_;
237 0 0         defined(my $target_file = $args{target_file}) or return [400, "Please specify target_file"];
238 0           my $force = $args{force};
239              
240 0 0 0       return [412, "Target file does not exist"] if !$force && !(-f $target_file);
241 0           my $lockinfo = _read_lockfile($target_file);
242              
243 0 0         return [304, "Target file was not unlocked"] unless $lockinfo->{exists};
244              
245 0 0         return [500, $lockinfo->{error}] if $lockinfo->{error};
246              
247             return [412, "Target file was not locked by us (pid $$) but by pid $lockinfo->{pid}"]
248 0 0 0       if !$force && ($$ != $lockinfo->{pid});
249              
250             unlink $lockinfo->{path}
251 0 0         or return [500, "Can't unlink lockfile '$lockinfo->{path}': $!"];
252              
253 0           [200, "Unlocked"];
254             }
255              
256             1;
257             # ABSTRACT: Create/check/delete Emacs-style lockfiles
258              
259             __END__