diff --git a/lib/puppet/util/lockfile.rb b/lib/puppet/util/lockfile.rb index 9771f389e..21a1c2d9b 100644 --- a/lib/puppet/util/lockfile.rb +++ b/lib/puppet/util/lockfile.rb @@ -1,66 +1,66 @@ # This class provides a simple API for managing a lock file # whose contents are an (optional) String. In addition # to querying the basic state (#locked?) of the lock, managing # the lock (#lock, #unlock), the contents can be retrieved at # any time while the lock is held (#lock_data). This can be # used to store pids, messages, etc. # # @see Puppet::Util::JsonLockfile class Puppet::Util::Lockfile attr_reader :file_path def initialize(file_path) @file_path = file_path end # Lock the lockfile. You may optionally pass a data object, which will be # retrievable for the duration of time during which the file is locked. # # @param [String] lock_data an optional String data object to associate # with the lock. This may be used to store pids, descriptive messages, # etc. The data may be retrieved at any time while the lock is held by # calling the #lock_data method. # @return [boolean] true if lock is successfully acquired, false otherwise. def lock(lock_data = nil) begin Puppet::FileSystem.exclusive_create(@file_path, nil) do |fd| fd.print(lock_data) end true rescue Errno::EEXIST false end end def unlock if locked? Puppet::FileSystem.unlink(@file_path) true else false end end def locked? # delegate logic to a more explicit private method file_locked? end # Retrieve the (optional) lock data that was specified at the time the file # was locked. # @return [String] the data object. def lock_data return File.read(@file_path) if file_locked? end # Private, internal utility method for encapsulating the logic about # whether or not the file is locked. This method can be called # by other methods in this class without as much risk of accidentally # being overridden by child classes. # @return [boolean] true if the file is locked, false if it is not. - def file_locked?() + def file_locked? Puppet::FileSystem.exist? @file_path end private :file_locked? end diff --git a/lib/puppet/util/pidlock.rb b/lib/puppet/util/pidlock.rb index 35e4ad431..3e8389e23 100644 --- a/lib/puppet/util/pidlock.rb +++ b/lib/puppet/util/pidlock.rb @@ -1,56 +1,61 @@ require 'fileutils' require 'puppet/util/lockfile' class Puppet::Util::Pidlock def initialize(lockfile) @lockfile = Puppet::Util::Lockfile.new(lockfile) end def locked? clear_if_stale @lockfile.locked? end def mine? Process.pid == lock_pid end def lock return mine? if locked? @lockfile.lock(Process.pid) end - def unlock() + def unlock if mine? return @lockfile.unlock else false end end def lock_pid - @lockfile.lock_data.to_i + pid = @lockfile.lock_data + begin + Integer(pid) + rescue ArgumentError, TypeError + nil + end end def file_path @lockfile.file_path end def clear_if_stale - return if lock_pid.nil? + return @lockfile.unlock 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 @lockfile.unlock end end private :clear_if_stale end diff --git a/spec/unit/util/pidlock_spec.rb b/spec/unit/util/pidlock_spec.rb index 2ebe7dec8..fcef7aa31 100644 --- a/spec/unit/util/pidlock_spec.rb +++ b/spec/unit/util/pidlock_spec.rb @@ -1,182 +1,218 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/util/pidlock' describe Puppet::Util::Pidlock do require 'puppet_spec/files' include PuppetSpec::Files before(:each) do @lockfile = tmpfile("lock") @lock = Puppet::Util::Pidlock.new(@lockfile) end describe "#lock" do it "should not be locked at start" do @lock.should_not be_locked end it "should not be mine at start" do @lock.should_not be_mine end it "should become locked" do @lock.lock @lock.should be_locked end it "should become mine" do @lock.lock @lock.should be_mine end it "should be possible to lock multiple times" do @lock.lock lambda { @lock.lock }.should_not raise_error end it "should return true when locking" do @lock.lock.should be_true end it "should return true if locked by me" do @lock.lock @lock.lock.should be_true end it "should create a lock file" do @lock.lock Puppet::FileSystem.exist?(@lockfile).should be_true end + it 'should create an empty lock file even when pid is missing' do + Process.stubs(:pid).returns('') + @lock.lock + Puppet::FileSystem.exist?(@lock.file_path).should be_true + Puppet::FileSystem.read(@lock.file_path).should be_empty + end + + it 'should replace an existing empty lockfile with a pid, given a subsequent lock call made against a valid pid' do + # empty pid results in empty lockfile + Process.stubs(:pid).returns('') + @lock.lock + Puppet::FileSystem.exist?(@lock.file_path).should be_true + + # next lock call with valid pid kills existing empty lockfile + Process.stubs(:pid).returns(1234) + @lock.lock + Puppet::FileSystem.exist?(@lock.file_path).should be_true + Puppet::FileSystem.read(@lock.file_path).should == '1234' + end + it "should expose the lock file_path" do @lock.file_path.should == @lockfile end end describe "#unlock" do it "should not be locked anymore" do @lock.lock @lock.unlock @lock.should_not be_locked end it "should return false if not locked" do @lock.unlock.should be_false end it "should return true if properly unlocked" do @lock.lock @lock.unlock.should be_true end it "should get rid of the lock file" do @lock.lock @lock.unlock Puppet::FileSystem.exist?(@lockfile).should be_false end end describe "#locked?" do it "should return true if locked" do @lock.lock @lock.should be_locked end + + it "should remove the lockfile when pid is missing" do + Process.stubs(:pid).returns('') + @lock.lock + @lock.locked?.should be_false + Puppet::FileSystem.exist?(@lock.file_path).should be_false + end + end + + describe '#lock_pid' do + it 'should return nil if the pid is empty' do + # fake pid to get empty lockfile + Process.stubs(:pid).returns('') + @lock.lock + @lock.lock_pid.should == nil + end end describe "with a stale lock" do before(:each) do # fake our pid to be 1234 Process.stubs(:pid).returns(1234) # lock the file @lock.lock # fake our pid to be a different pid, to simulate someone else # holding the lock Process.stubs(:pid).returns(6789) Process.stubs(:kill).with(0, 6789) Process.stubs(:kill).with(0, 1234).raises(Errno::ESRCH) end it "should not be locked" do @lock.should_not be_locked end describe "#lock" do it "should clear stale locks" do - @lock.locked? + @lock.locked?.should be_false Puppet::FileSystem.exist?(@lockfile).should be_false end it "should replace with new locks" do @lock.lock Puppet::FileSystem.exist?(@lockfile).should be_true @lock.lock_pid.should == 6789 @lock.should be_mine @lock.should be_locked end end describe "#unlock" do it "should not be allowed" do @lock.unlock.should be_false end it "should not remove the lock file" do @lock.unlock Puppet::FileSystem.exist?(@lockfile).should be_true end end end describe "with another process lock" do before(:each) do # fake our pid to be 1234 Process.stubs(:pid).returns(1234) # lock the file @lock.lock # fake our pid to be a different pid, to simulate someone else # holding the lock Process.stubs(:pid).returns(6789) Process.stubs(:kill).with(0, 6789) Process.stubs(:kill).with(0, 1234) end it "should be locked" do @lock.should be_locked end it "should not be mine" do @lock.should_not be_mine end describe "#lock" do it "should not be possible" do @lock.lock.should be_false end it "should not overwrite the lock" do @lock.lock @lock.should_not be_mine end end describe "#unlock" do it "should not be possible" do @lock.unlock.should be_false end it "should not remove the lock file" do @lock.unlock Puppet::FileSystem.exist?(@lockfile).should be_true end it "should still not be our lock" do @lock.unlock @lock.should_not be_mine end end end end