require 'fileutils'

class Puppet::Util::Pidlock
  attr_reader :lockfile

  def initialize(lockfile)
    @lockfile = lockfile
  end

  def locked?
    clear_if_stale
    return true if File.exists? @lockfile

    # HACK!  There was a temporary change to the lockfile behavior introduced in 2.7.10 and 2.7.11, and reverted
    # in 2.7.12.  We need to pull some chicanery to be backwards-compatible with those versions.  For more info,
    # see the comments on the method... and this hack should be removed for the 3.x series.
    handle_2_7_10_disabled_lockfile
    File.exists? @lockfile
  end

  def mine?
    Process.pid == lock_pid
  end

  def anonymous?
    return false unless File.exists?(@lockfile)
    File.read(@lockfile) == ""
  end

  def lock(opts = {})
    opts = {:anonymous => false}.merge(opts)

    if locked?
      mine?
    else
      if opts[:anonymous]
        File.open(@lockfile, 'w') { |fd| true }
      else
        File.open(@lockfile, "w") { |fd| fd.write(Process.pid) }
      end
      true
    end
  end

  def unlock(opts = {})
    return false unless locked?

    opts = {:anonymous => false}.merge(opts)

    if mine? or (opts[:anonymous] and anonymous?)
      File.unlink(@lockfile)
      true
    else
      false
    end
  end

  private
  def lock_pid
    if File.exists? @lockfile
      File.read(@lockfile).to_i
    else
      nil
    end
  end

  def clear_if_stale
    return if lock_pid.nil?

    errors = [Errno::ESRCH]
    # Process::Error can only happen, and is only defined, on Windows
    errors << Process::Error if defined? Process::Error

    begin
      Process.kill(0, lock_pid)
    rescue *errors
      File.unlink(@lockfile)
    end
  end


  ######################################################################################
  # Backwards compatibility hack
  ######################################################################################
  # A change to lockfile behavior was introduced in 2.7.10 and 2.7.11; basically,
  # instead of using a single lockfile to indicate both administrative disabling of
  # the agent *and* the case where an agent run is already in progress, we started using
  # two separate lockfiles: the 'normal' one for the "run in progress" case, and a
  # separate one with a ".disabled" extension to indicate administrative disabling.
  #
  # This was determined to cause incompatibilities with mcollective, so the behavior
  # was reverted for 2.7.12.  Unfortunately this leaves the possibility that someone
  # may have run "agent --disable" to administratively disable a 2.7.10 or 2.7.11
  # agent, and then upgraded to a newer version.  This method exists only to
  # provide backwards compatibility.  Basically, it just recognizes the 2.7.10/2.7.11
  # ".disabled" lock file, warns, and cleans it up.
  #
  # This should be removed for the 3.x series.
  #
  # For more information, please see tickets #12844, #3757, #4836, and #11057
  #
  # -- cprice 2012-03-01
  #
  def handle_2_7_10_disabled_lockfile
    disabled_lockfile_path = @lockfile + ".disabled"
    if (File.exists?(disabled_lockfile_path))
      Puppet.warning("Found special lockfile '#{disabled_lockfile_path}'; this file was " +
          "generated by a call to 'puppet agent --disable' in puppet 2.7.10 or 2.7.11. " +
          "The expected lockfile path is '#{@lockfile}'; renaming the lock file.")
      File.rename(disabled_lockfile_path, @lockfile)
    end
  end
  private :handle_2_7_10_disabled_lockfile
  ######################################################################################
  # End backwards compatibility hack
  ######################################################################################
end
