├── .gitignore ├── LICENSE ├── README.rdoc ├── Rakefile ├── TODO ├── examples ├── basic.rb ├── run_slowmo.rb └── slowmo.rb ├── lib └── pidfile.rb └── spec └── pidfile_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Samuel Mullen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = PidFile 2 | 3 | http://github.com/samullen/pidfile 4 | 5 | A basic library for creating lockfiles for processes 6 | 7 | This library works with Ruby 1.8 and 1.9 and is licensed under the MIT License. 8 | 9 | == Installation 10 | 11 | The pidfile gem is hosted on RubyGems.org (http://rubygems.org). This 12 | requires that you have http://rubygems.org in your gem sources. 13 | 14 | Install the pidfile gem: 15 | 16 | sudo gem install pidfile 17 | 18 | == Gettings Started 19 | 20 | The pidfile gem is easy to use and only requires the instantiation of the 21 | pidfile, like so: 22 | 23 | pf = PidFile.new 24 | 25 | Of course there is other functionality, but if you are just wanting to keep a 26 | process from running more than once, this is all you need. 27 | 28 | === Arguments 29 | 30 | PidFile creates file to store the process ID (PID). By default, process ID 31 | (PID) files are created in /tmp upon instantiation and are given the same name 32 | as the calling script with a .pid extension. Process ID files are removed when 33 | the creating program exits. 34 | 35 | Each argument, :pidfile and :piddir, can be changed upon instantiation of the 36 | class. 37 | 38 | ==== Example: 39 | 40 | PidFile.new(:piddir => '/var/lock', :pidfile => "awesome.pid") 41 | 42 | == Miscellaneous 43 | 44 | === Why isn't this a singleton? 45 | 46 | My main concern is how it would behave when a process is forked multiple times. 47 | Each new process would get its own PID, but I wasn't sure how using a singleton 48 | would affect this; if the interrelationship of the processes would keep the 49 | object from correctly identifying the correct pidfile. 50 | 51 | Anyway, I like the flexibility that not using a singleton offers, and I guess 52 | I'd rather ask you not to do something bad rather than restraining you. 53 | 54 | == Copyright 55 | 56 | Copyright(c) 2010 Samuel Mullen (samullen). See LICENSE for details 57 | 58 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path("../lib", __FILE__) 2 | 3 | require 'rubygems' 4 | require 'lib/pidfile' 5 | require 'rake/testtask' 6 | require 'rake/gempackagetask' 7 | 8 | lib_dir = File.expand_path('lib') 9 | test_dir = File.expand_path('test') 10 | 11 | gem_spec = Gem::Specification.new do |s| 12 | s.name = "pidfile" 13 | s.version = PidFile::VERSION 14 | s.authors = ["Samuel Mullen"] 15 | s.email = "samullen@gmail.com" 16 | s.homepage = "http://github.com/samullen/pidfile" 17 | s.summary = "A basic library for creating lockfiles for processes" 18 | s.authors = ["Samuel Mullen"] 19 | s.email = "samullen@gmail.com" 20 | s.test_files = Dir['test/**/*.rb'] 21 | s.description = false 22 | s.files = [ 23 | "LICENSE", 24 | "README.rdoc", 25 | "Rakefile", 26 | # "examples/functional.rb", 27 | # "examples/objectoriented.rb", 28 | "lib/pidfile.rb", 29 | ] + s.test_files 30 | end 31 | 32 | Rake::TestTask.new(:test) do |test| 33 | test.libs = [lib_dir, test_dir] 34 | test.pattern = 'test/**/*rb' 35 | test.verbose = true 36 | end 37 | 38 | Rake::GemPackageTask.new(gem_spec) do |pkg| 39 | pkg.need_zip = false 40 | pkg.need_tar = false 41 | end 42 | 43 | desc "Install the gem locally" 44 | task :install => [:test, :gem] do 45 | sh %{gem install pkg/#{gem_spec.name}-#{gem_spec.version}} 46 | end 47 | 48 | desc "Remove the pkg directory and all of its contents." 49 | task :clean => :clobber_package 50 | 51 | task :default => [:test, :gem] 52 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - rework examples 2 | - Cleanup Rakefile. Make sure it all works. 3 | -------------------------------------------------------------------------------- /examples/basic.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require File.join(File.dirname(__FILE__), '..', 'lib','pidfile') 4 | 5 | if PidFile.running? 6 | exit 7 | end 8 | 9 | p = PidFile.new 10 | 11 | puts p.pidfile 12 | puts p.piddir 13 | puts p.pid 14 | puts p.pidpath 15 | -------------------------------------------------------------------------------- /examples/run_slowmo.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | puts "start up a slow process. Starts and sleeps for ten" 4 | system "./slowmo.rb &" 5 | 6 | # give the first process time to create a lockfile 7 | sleep 1 8 | 9 | puts "Start up another slowmo.rb. This one should exit quickly" 10 | system "./slowmo.rb" 11 | puts 'exited' 12 | 13 | -------------------------------------------------------------------------------- /examples/slowmo.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require File.join(File.dirname(__FILE__), '..', 'lib','pidfile') 4 | 5 | p = PidFile.new 6 | sleep 10 7 | -------------------------------------------------------------------------------- /lib/pidfile.rb: -------------------------------------------------------------------------------- 1 | class PidFile 2 | attr_reader :pidfile, :piddir, :pidpath 3 | 4 | class DuplicateProcessError < RuntimeError; end 5 | 6 | VERSION = '0.3.0' 7 | 8 | DEFAULT_OPTIONS = { 9 | :pidfile => File.basename($0, File.extname($0)) + ".pid", 10 | :piddir => '/var/run', 11 | } 12 | 13 | def initialize(*args) 14 | opts = {} 15 | 16 | #----- set options -----# 17 | case 18 | when args.length == 0 then 19 | when args.length == 1 && args[0].class == Hash then 20 | arg = args.shift 21 | 22 | if arg.class == Hash 23 | opts = arg 24 | end 25 | else 26 | raise ArgumentError, "new() expects hash or hashref as argument" 27 | end 28 | 29 | opts = DEFAULT_OPTIONS.merge opts 30 | 31 | @piddir = opts[:piddir] 32 | @pidfile = opts[:pidfile] 33 | @pidpath = File.join(@piddir, @pidfile) 34 | @fh = nil 35 | 36 | #----- Does the pidfile or pid exist? -----# 37 | if self.pidfile_exists? 38 | if self.class.running?(@pidpath) 39 | raise DuplicateProcessError, "Process (#{$0} - #{self.class.pid(@pidpath)}) is already running." 40 | 41 | exit! # exit without removing the existing pidfile 42 | end 43 | 44 | self.release 45 | end 46 | 47 | #----- create the pidfile -----# 48 | create_pidfile 49 | 50 | at_exit { release } 51 | end 52 | 53 | #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=# 54 | # Instance Methods 55 | #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=# 56 | 57 | # Returns the PID, if any, of the instantiating process 58 | def pid 59 | return @pid unless @pid.nil? 60 | 61 | if self.pidfile_exists? 62 | @pid = open(self.pidpath, 'r').read.to_i 63 | else 64 | @pid = nil 65 | end 66 | end 67 | 68 | # Boolean stating whether this process is alive and running 69 | def alive? 70 | return false unless self.pid && (self.pid == Process.pid) 71 | 72 | self.class.process_exists?(self.pid) 73 | end 74 | 75 | # does the pidfile exist? 76 | def pidfile_exists? 77 | self.class.pidfile_exists?(pidpath) 78 | end 79 | 80 | # unlock and remove the pidfile. Sets pid to nil 81 | def release 82 | unless @fh.nil? 83 | @fh.flock(File::LOCK_UN) 84 | remove_pidfile 85 | end 86 | @pid = nil 87 | end 88 | 89 | # returns the modification time of the pidfile 90 | def locktime 91 | File.mtime(self.pidpath) 92 | end 93 | 94 | #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=# 95 | # Class Methods 96 | #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=# 97 | 98 | # Returns the PID, if any, of the instantiating process 99 | def self.pid(path=nil) 100 | if pidfile_exists?(path) 101 | open(path, 'r').read.to_i 102 | end 103 | end 104 | 105 | # class method for determining the existence of pidfile 106 | def self.pidfile_exists?(path=nil) 107 | path ||= File.join(DEFAULT_OPTIONS[:piddir], DEFAULT_OPTIONS[:pidfile]) 108 | 109 | File.exists?(path) 110 | end 111 | 112 | # boolean stating whether the calling program is already running 113 | def self.running?(path=nil) 114 | calling_pid = nil 115 | path ||= File.join(DEFAULT_OPTIONS[:piddir], DEFAULT_OPTIONS[:pidfile]) 116 | 117 | if pidfile_exists?(path) 118 | calling_pid = pid(path) 119 | end 120 | 121 | process_exists?(calling_pid) 122 | end 123 | 124 | private 125 | 126 | # Writes the process ID to the pidfile and defines @pid as such 127 | def create_pidfile 128 | # Once the filehandle is created, we don't release until the process dies. 129 | @fh = open(self.pidpath, "w") 130 | @fh.flock(File::LOCK_EX | File::LOCK_NB) || raise 131 | @pid = Process.pid 132 | @fh.puts @pid 133 | @fh.flush 134 | @fh.rewind 135 | end 136 | 137 | # removes the pidfile. 138 | def remove_pidfile 139 | File.unlink(self.pidpath) if self.pidfile_exists? 140 | end 141 | 142 | def self.process_exists?(process_id) 143 | begin 144 | Process.kill(0, process_id) 145 | true 146 | rescue Errno::ESRCH, TypeError # "PID is NOT running or is zombied 147 | false 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /spec/pidfile_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', 'lib','pidfile') 2 | 3 | describe PidFile do 4 | before(:each) do 5 | @pidfile = PidFile.new(:pidfile => "rspec.pid") 6 | end 7 | 8 | after(:each) do 9 | @pidfile.release 10 | end 11 | 12 | #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=# 13 | # Builder Tests 14 | #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=# 15 | 16 | it "should set defaults upon instantiation" do 17 | @pidfile.pidfile.should == "rspec.pid" 18 | @pidfile.piddir.should == "/tmp" 19 | @pidfile.pidpath.should == "/tmp/rspec.pid" 20 | end 21 | 22 | it "should secure pidfiles left behind and recycle them for itself" do 23 | @pidfile.release 24 | fakepid = 99999999 # absurd number 25 | open("/tmp/foo.pid", "w") {|f| f.puts fakepid } 26 | pf = PidFile.new(:pidfile => "foo.pid") 27 | PidFile.pid(pf.pidpath).should == Process.pid 28 | pf.should be_an_instance_of PidFile 29 | pf.pid.should_not == fakepid 30 | pf.pid.should be_a_kind_of Integer 31 | pf.release 32 | end 33 | 34 | it "should create a pid file upon instantiation" do 35 | File.exists?(@pidfile.pidpath).should be_true 36 | end 37 | 38 | it "should create a pidfile containing same PID as process" do 39 | @pidfile.pid.should == Process.pid 40 | end 41 | 42 | it "should know if pidfile exists or not" do 43 | @pidfile.pidfile_exists?.should be_true 44 | @pidfile.release 45 | @pidfile.pidfile_exists?.should be_false 46 | end 47 | 48 | it "should be able to tell if a process is running" do 49 | @pidfile.alive?.should be_true 50 | end 51 | 52 | it "should remove the pidfile when the calling application exits" do 53 | fork do 54 | exit 55 | end 56 | PidFile.pidfile_exists?.should be_false 57 | end 58 | 59 | it "should raise an error if a pidfile already exists" do 60 | lambda { PidFile.new(:pidfile => "rspec.pid") }.should raise_error 61 | end 62 | 63 | it "should know if a process exists or not - Class method" do 64 | PidFile.running?('/tmp/rspec.pid').should be_true 65 | PidFile.running?('/tmp/foo.pid').should be_false 66 | end 67 | 68 | it "should know if it is running - Class method" do 69 | pf = PidFile.new 70 | PidFile.running?.should be_true 71 | pf.release 72 | PidFile.running?.should be_false 73 | end 74 | 75 | it "should know if it's alive or not" do 76 | @pidfile.alive?.should be_true 77 | @pidfile.release 78 | @pidfile.alive?.should be_false 79 | end 80 | 81 | it "should remove pidfile and set pid to nil when released" do 82 | @pidfile.release 83 | @pidfile.pidfile_exists?.should be_false 84 | @pidfile.pid.should be_nil 85 | end 86 | 87 | it "should give a DateTime value for locktime" do 88 | @pidfile.locktime.should be_an_instance_of Time 89 | end 90 | end 91 | --------------------------------------------------------------------------------