# <@LICENSE>
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to you under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at:
# 
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# </@LICENSE>

package Mail::SpamAssassin::Locker::Flock;

use strict;
use warnings;
# use bytes;
use re 'taint';

use Mail::SpamAssassin;
use Mail::SpamAssassin::Locker;
use Mail::SpamAssassin::Util;
use Mail::SpamAssassin::Logger;
use File::Spec;
use IO::File;
use Fcntl qw(:DEFAULT :flock);

our @ISA = qw(Mail::SpamAssassin::Locker);

###########################################################################

sub new {
  my $class = shift;
  my $self = $class->SUPER::new(@_);
  $self;
}

###########################################################################
# Attempt to create a file lock, using NFS-UNsafe locking techniques.

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 $mode");

  my $lock_file = "$path.mutex";
  my $umask = umask(~$mode);
  my $fh = IO::File->new;

  if (!$fh->open ($lock_file, O_RDWR|O_CREAT)) {
      umask $umask; # just in case
      die "locker: safe_lock: cannot create lockfile $lock_file: $!\n";
  }
  umask $umask; # we've created the file, so reset umask

  dbg("locker: safe_lock: created $lock_file");

  my $unalarmed = 0;
  my $oldalarm = 0;

  # use a SIGALRM-based timer -- more efficient than second-by-second
  # sleeps
  my $eval_stat;
  eval {
    local $SIG{ALRM} = sub { die "alarm\n" };
    dbg("locker: safe_lock: trying to get lock on $path with $max_retries timeout");

    # max_retries is basically seconds! so use it for the timeout
    $oldalarm = alarm $max_retries;

    # HELLO!?! IO::File doesn't have a flock() method?!
    if (!flock($fh, LOCK_EX)) {
      warn "locker: safe_lock: cannot obtain a lock on log file: $!";
    } else {
      alarm $oldalarm;
      $unalarmed = 1; # avoid calling alarm(0) twice

      dbg("locker: safe_lock: link to $lock_file: link ok");
      $is_locked = 1;

      # just to be nice: let people know when it was locked
      $fh->print("$$\n")  or die "error writing to lock file: $!";
      $fh->flush  or die "cannot flush lock file: $!";

      # keep the FD around - we need to keep the lockfile open or the lock
      # is unlocked!
      $self->{lock_fhs} ||= { };
      $self->{lock_fhs}->{$path} = $fh;
    }
    1;
  } or do {
    $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
  };

  $unalarmed or alarm $oldalarm; # if we die'd above, need to reset here

  if (defined $eval_stat) {
    if ($eval_stat =~ /alarm/) {
      dbg("locker: safe_lock: timed out after $max_retries seconds");
    } else {
      die "locker: safe_lock: $eval_stat\n";
    }
  }

  return $is_locked;
}

###########################################################################

sub safe_unlock {
  my ($self, $path) = @_;

  if (!exists $self->{lock_fhs} || !defined $self->{lock_fhs}->{$path}) {
    dbg("locker: safe_unlock: no lock handle for $path - already unlocked?");
    return;
  }

  my $fh = $self->{lock_fhs}->{$path};
  delete $self->{lock_fhs}->{$path};

  flock($fh, LOCK_UN)  or die "cannot unlock a log file: $!";
  $fh->close  or die "error closing a lock file: $!";

  dbg("locker: safe_unlock: unlocked $path.mutex");

  # do NOT unlink! this would open a race, whereby:
  #
  # procA: ....unlock                           (unlocked lockfile)
  # procB:            lock                      (gets lock on lockfile)
  # procA:                 unlink               (deletes lockfile)
  # (procB's lock is now deleted as well!)
  # procC:                        create, lock  (gets lock on new file)
  #
  # both procB and procC would then think they had locks, and both
  # would write to the database file.  this is bad.
  #
  # unlink ("$path.mutex"); 
  #
  # side-effect: we leave a .mutex file around. but hey!
}

###########################################################################

sub refresh_lock {
  my($self, $path) = @_;

  return unless $path;

  if (!exists $self->{lock_fhs} || !defined $self->{lock_fhs}->{$path}) {
    warn "locker: refresh_lock: no lock handle for $path\n";
    return;
  }

  my $fh = $self->{lock_fhs}->{$path};
  $fh->print("$$\n")  or die "error writing to lock file: $!";
  $fh->flush  or die "cannot flush lock file: $!";

  dbg("locker: refresh_lock: refresh $path.mutex");
}

###########################################################################

1;
