
module FileLock
   VERSION = 0.2
   REVISION = "$Id: filelock.rb,v 1.8 2000/03/23 10:04:59 cle Exp $"

   require "final"


   class LockError<StandardError; end        # The base of all exceptions
   class AcquireLockError<LockError; end     # File could not be locked
   class AlreadyLockedError<LockError; end   # File already locked 
   class CreateLockError<LockError; end      # Could not create lockfile
   class FrozenLockError<LockError; end      # Lock frozen, no reopen/relock 
   class NotLockedError<LockError; end       # File was not locked
   class RemoveLockError<LockError; end      # Could not remove lockfile
   class StolenLockError<LockError; end      # Lock was stolen by others
   class UnlockError<LockError; end          # Could not unlock file


   class LockedFile

      def LockedFile.open(filename, mode="r", sleep=8, retries=nil,
                          steal=nil, suspend=16, suffix=".lock")
         fp = LockedFile.new(filename, mode, sleep, retries, steal,
                             suspend, suffix)
         if iterator?
            begin
               fp.freeze
               yield fp
            ensure
               fp.close
            end
         else
            fp
         end
      end

      def initialize(filename, mode="r", sleep=8, retries=nil,
                     steal=nil, suspend=16, suffix=".lock")
         @filename = filename
         @frozen = false
         @filep = nil
         @lockf = LockFile.new(filename, sleep, retries, steal, suspend, suffix)
         reopen(mode)
      end

      def close
         return if not @filep
         ObjectSpace.undefine_finalizer(self)
         @lockf.unlock
         closefp
      end

      def freeze
         @frozen = true
         @lockf.freeze
      end

      def reopen(mode="r")
         ObjectSpace.undefine_finalizer(self)
         closefp if @filep
         raise FrozenLockError.new if @frozen 
         @lockf.lock unless @lockf.isLocked?
         @filep = File.open(@filename, mode)
         ObjectSpace.define_finalizer(self, LockedFile::cleanup(@filep,@lockf))
      end

      private

      def LockedFile.cleanup(filep, lockfp)
         lambda do
            begin
               filep.close
               lockfp.unlock
            rescue
            end
         end
      end

      def closefp
         if @filep then @filep.close
            @filep = nil
         end
      end

      def method_missing(msgid, *args)
         @lockf.checkLock
         @filep.send(msgid, *args)
      end
   end


   class LockFile

      def LockFile.create_for(filename, sleep=0, retries=nil, steal=nil,
                     suspend=16, suffix=".lock")
         fp = LockFile.new(filename, sleep, retries, steal, suspend, suffix)
         if iterator?
            begin
               fp.freeze
               yield LockFile.createChecker(fp.locked, fp.lockfilename)
            ensure
               fp.unlock
            end
         else
            fp
         end
      end

      def initialize(filename, sleep=8, retries=nil, steal=nil,
                     suspend=16, suffix=".lock")
         @filename = filename
         @lockfilename = @filename + suffix
         @sleep = sleep
         @retries = retries
         @steal = steal
         @suspend = suspend
         @frozen = false
         @locked = nil
         lock(sleep, retries, steal, suspend)
         callback = LockFile::cleanup(@locked, @lockfilename)
         ObjectSpace.define_finalizer(self, callback)
      end

      attr :locked
      attr :lockfilename

      def checkLock
         isLocked
         mtime = File.mtime(@lockfilename).to_i
         if mtime != @locked 
            ObjectSpace.undefine_finalizer(self)
            raise StolenLockError, @filename
         end
      end

      def freeze 
         @frozen = true
      end

      def isLocked?
         return @locked != nil
      end

      def lock(sleep=nil, retries=nil, steal=nil, suspend=nil)
         raise FrozenLockError.new if @frozen
         isNotLocked
         sleep = @sleep unless sleep
         retries = @retries unless retries
         steal = @steal unless steal
         suspend = @suspend unless suspend
         try_again = callcc { |cc| cc }
         while anotherLock
            raise AcquireLockError, @filename if retries != nil and 
               retries <= 0
            retries -= 1 if retries
            if steal 
               sleep(steal)
               removeLock
               sleep(suspend)
               next
            else
               sleep(sleep)
            end
         end
         try_again.call unless createLock
      end

      def unlock
         checkLock
         begin
            removeLock
         rescue RemoveLockError
            raise UnlockError, $!
         end
      end

      private

      def LockFile.cleanup(lockid, filename)
         lambda do
            mtime = File.mtime(filename).to_i
            if mtime == lockid
               begin File::unlink(filename); 
               rescue
               end
            end
         end
      end

      def LockFile.createChecker(lockid, filename)
         lambda do
            File.mtime(filename).to_i != lockid
         end
      end

      def anotherLock
         return File.exist?(@lockfilename)
      end

   begin
      require "_filelock"
   rescue LoadError
      # As the C extension couldn't be loaded, we define the 
      # necessary method in plain Ruby. This is also ok, only
      # the gap between test of file existence and creation of
      # a file is larger then in the C extension.

      def createLock
         return nil if anotherLock
         begin
            File.open(@lockfilename, "w") do |fd|
                fd.write(Process.uid)
            end
         rescue Errno::EACCES
            if anotherLock
               return nil
            else
               raise CreateLockError, $!.to_s
            end
         rescue Errno::ENOENT
            raise CreateLockError, $!.to_s
         end
         @locked = File.mtime(@lockfilename).to_i
      end
   end

      def isLocked
         raise NotLockedError, @filename unless @locked
      end

      def isNotLocked
         raise AlreadyLockedError, @filename if @locked
      end

      def removeLock
         begin
            File.unlink(@lockfilename)
         rescue SystemCallError
            raise RemoveLockError, $!.to_s
         ensure
            ObjectSpace.undefine_finalizer(self)
            @locked = nil
         end
      end
   end
end