use
Fcntl
qw(:DEFAULT :flock)
;
our
@ISA
=
qw(Mail::SpamAssassin::Locker)
;
sub
new {
my
$class
=
shift
;
my
$self
=
$class
->SUPER::new(
@_
);
$self
;
}
sub
safe_lock {
my
(
$self
,
$path
,
$max_retries
,
$mode
) =
@_
;
my
$is_locked
= 0;
my
@stat
;
$max_retries
||= 30;
$mode
||=
"0700"
;
$mode
= (
oct
$mode
) & 0666;
dbg (
"locker: mode is %03o"
,
$mode
);
my
$lock_file
=
"$path.lock"
;
my
$hname
= Mail::SpamAssassin::Util::fq_hostname();
my
$lock_tmp
= Mail::SpamAssassin::Util::untaint_file_path
(
$path
.
".lock."
.
$hname
.
"."
.$$);
$self
->{lock_tmp} =
$lock_tmp
;
my
$umask
=
umask
(~
$mode
);
if
(!
open
(LTMP,
">$lock_tmp"
)) {
umask
$umask
;
die
"locker: safe_lock: cannot create tmp lockfile $lock_tmp for $lock_file: $!\n"
;
}
umask
$umask
;
LTMP->autoflush(1);
dbg(
"locker: safe_lock: created $lock_tmp"
);
for
(
my
$retries
= 0;
$retries
<
$max_retries
* 2;
$retries
++) {
if
(
$retries
> 0) {
$self
->jittery_half_second_sleep(); }
print
LTMP
"$hname.$$\n"
or
warn
"Error writing to $lock_tmp: $!"
;
dbg(
"locker: safe_lock: trying to get lock on $path with $retries retries"
);
if
(
link
(
$lock_tmp
,
$lock_file
)) {
dbg(
"locker: safe_lock: link to $lock_file: link ok"
);
$is_locked
= 1;
last
;
}
unless
($!{EEXIST}) {
warn
"locker: creating link $lock_file to $lock_tmp failed: '$!'"
;
}
@stat
=
lstat
(
$lock_tmp
);
@stat
or
warn
"locker: error accessing $lock_tmp: $!"
;
if
(
defined
$stat
[3] &&
$stat
[3] > 1) {
dbg(
"locker: safe_lock: link to $lock_file: stat ok"
);
$is_locked
= 1;
last
;
}
my
$now
= (
$#stat
< 11 ?
undef
:
$stat
[10]);
@stat
=
lstat
(
$lock_file
);
@stat
or
warn
"locker: error accessing $lock_file: $!"
;
my
$lock_age
= (
$#stat
< 11 ?
undef
:
$stat
[10]);
if
(
defined
(
$lock_age
) &&
defined
(
$now
) && (
$now
-
$lock_age
) > LOCK_MAX_AGE)
{
dbg(
"locker: safe_lock: breaking stale $lock_file: age="
.
(
defined
$lock_age
?
$lock_age
:
"undef"
) .
" now=$now"
);
unlink
(
$lock_file
)
or
warn
"locker: safe_lock: unlink of lock file $lock_file failed: $!\n"
;
}
}
close
LTMP or
die
"error closing $lock_tmp: $!"
;
unlink
(
$lock_tmp
)
or
warn
"locker: safe_lock: unlink of temp lock $lock_tmp failed: $!\n"
;
if
(
$is_locked
) {
@stat
=
lstat
(
$lock_file
);
@stat
or
warn
"locker: error accessing $lock_file: $!"
;
my
$lock_ctime
= (
$#stat
< 11 ?
undef
:
$stat
[10]);
$self
->{lock_ctimes} ||= { };
$self
->{lock_ctimes}->{
$path
} =
$lock_ctime
;
}
return
$is_locked
;
}
sub
safe_unlock {
my
(
$self
,
$path
) =
@_
;
my
$lock_file
=
"$path.lock"
;
my
$lock_tmp
=
$self
->{lock_tmp};
if
(!
$lock_tmp
) {
dbg(
"locker: safe_unlock: $path.lock never locked"
);
return
;
}
my
@stat_ourtmp
;
if
(!
defined
sysopen
(LTMP,
$lock_tmp
, O_CREAT|O_WRONLY|O_EXCL, 0700)) {
warn
"locker: safe_unlock: failed to create lock tmpfile $lock_tmp: $!"
;
return
;
}
else
{
LTMP->autoflush(1);
print
LTMP
"\n"
or
warn
"Error writing to $lock_tmp: $!"
;
if
(!(
@stat_ourtmp
=
stat
(LTMP)) || (
scalar
(
@stat_ourtmp
) < 11)) {
@stat_ourtmp
or
warn
"locker: error accessing $lock_tmp: $!"
;
warn
"locker: safe_unlock: failed to create lock tmpfile $lock_tmp"
;
close
LTMP or
die
"error closing $lock_tmp: $!"
;
unlink
(
$lock_tmp
)
or
warn
"locker: safe_lock: unlink of lock file $lock_tmp failed: $!\n"
;
return
;
}
}
my
$ourtmp_ctime
=
$stat_ourtmp
[10];
if
(!
defined
$ourtmp_ctime
) {
die
"locker: safe_unlock: stat failed on $lock_tmp"
;
}
close
LTMP or
die
"error closing $lock_tmp: $!"
;
unlink
(
$lock_tmp
)
or
warn
"locker: safe_lock: unlink of lock file $lock_tmp failed: $!\n"
;
my
$lock_ctime
=
$self
->{lock_ctimes}->{
$path
};
if
(!
defined
$lock_ctime
) {
warn
"locker: safe_unlock: no ctime recorded for $lock_file"
;
return
;
}
my
@stat_lock
=
lstat
(
$lock_file
);
@stat_lock
or
warn
"locker: error accessing $lock_file: $!"
;
my
$now_ctime
=
$stat_lock
[10];
if
(
defined
$now_ctime
&&
$now_ctime
==
$lock_ctime
)
{
unlink
(
$lock_file
)
or
warn
"locker: safe_unlock: unlinking $lock_file failed: $!\n"
;
dbg(
"locker: safe_unlock: unlink $lock_file"
);
if
(
$ourtmp_ctime
>=
$lock_ctime
+ LOCK_MAX_AGE) {
dbg(
"locker: safe_unlock: lock expired on $lock_file expired safely; sleeping"
);
my
$i
;
for
(
$i
= 0;
$i
< 5;
$i
++) {
$self
->jittery_one_second_sleep();
}
}
return
;
}
if
(
$ourtmp_ctime
<
$lock_ctime
+ LOCK_MAX_AGE) {
warn
"locker: safe_unlock: lock on $lock_file was stolen"
;
}
else
{
warn
"locker: safe_unlock: lock on $lock_file was lost due to expiry"
;
}
}
sub
refresh_lock {
my
(
$self
,
$path
) =
@_
;
return
unless
$path
;
my
$lock_file
=
"$path.lock"
;
utime
time
,
time
,
$lock_file
;
my
@stat
=
lstat
(
$lock_file
);
@stat
or
warn
"locker: error accessing $lock_file: $!"
;
my
$lock_ctime
= (
$#stat
< 11 ?
undef
:
$stat
[10]);
$self
->{lock_ctimes}->{
$path
} =
$lock_ctime
;
dbg(
"locker: refresh_lock: refresh $path.lock"
);
}
1;