daemonz 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.textile +68 -0
- data/Rakefile +22 -0
- data/config_template.yml +87 -0
- data/daemonz.gemspec +57 -0
- data/init.rb +6 -0
- data/lib/daemonz/config.rb +163 -0
- data/lib/daemonz/killer.rb +64 -0
- data/lib/daemonz/logging.rb +19 -0
- data/lib/daemonz/manage.rb +69 -0
- data/lib/daemonz/master.rb +59 -0
- data/lib/daemonz/process.rb +74 -0
- data/lib/daemonz.rb +10 -0
- data/test/daemonz_test.rb +8 -0
- metadata +97 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Victor Costan
|
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.
|
data/README.textile
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
h1. Daemonz
|
2
|
+
|
3
|
+
bq. Automatically starts and stops the daemons in a Rails application.
|
4
|
+
|
5
|
+
h2. Installation
|
6
|
+
|
7
|
+
Install with:
|
8
|
+
<pre>
|
9
|
+
script/plugin install git://github.com/costan/daemonz.git
|
10
|
+
</pre>
|
11
|
+
|
12
|
+
You don't need to do anything in your code for daemonz to work. It will start
|
13
|
+
the daemons when your server starts, and stop them when your server exits. It
|
14
|
+
does work with multiple servers.
|
15
|
+
|
16
|
+
h2. Configuration
|
17
|
+
|
18
|
+
The main configuration file is config/daemonz.yml. Daemons are configured by
|
19
|
+
individual files in the config/daemonz directory. Configuration files are ran
|
20
|
+
through Erb, so you can go crazy.
|
21
|
+
|
22
|
+
Daemonz comes with a few configuration examples that can also be used as they
|
23
|
+
are, by removing the <code>:disabled: true</code> line. Please contribute your
|
24
|
+
configuration if you think others could use it.
|
25
|
+
|
26
|
+
h2. Daemon Generator
|
27
|
+
|
28
|
+
Daemonz includes a generator for a daemon intended to do background processing
|
29
|
+
inside a Rails environment. If you're writing your first daemon, give it a try.
|
30
|
+
The scaffolded code includes the configuration file, a daemon skeleton using the
|
31
|
+
<code>simple-daemon</code> gem, and an integration test skeleton.
|
32
|
+
|
33
|
+
<pre>
|
34
|
+
script/generate daemon YourDaemonName
|
35
|
+
</pre>
|
36
|
+
|
37
|
+
h2. Testing
|
38
|
+
|
39
|
+
You can test your daemonz configuration with rake, as shown below. Keep in mind
|
40
|
+
that the Rake tasks are only provided for testing, and you should not use them
|
41
|
+
while your Rails application is running. Nothing bad should happen if you run
|
42
|
+
the tasks by mistake, but things may go astray if you mix the Rake tasks with
|
43
|
+
application starts and stops.
|
44
|
+
<pre>
|
45
|
+
rake daemons:start # Starts your daemons (for testing only)
|
46
|
+
rake daemons:stop # Stops your daemons (for testing only)
|
47
|
+
</pre>
|
48
|
+
|
49
|
+
Using the default configuration, daemons are not running during tests. The
|
50
|
+
snippet below shows how you can have daemons running during a specific test.
|
51
|
+
<notextile>
|
52
|
+
<pre>
|
53
|
+
class DaemonsTest < ActionController::IntegrationTest
|
54
|
+
# If this isn't here, Rails runs the entire test in a transaction, so daemons'
|
55
|
+
# database changes aren't visible. Also, if you share the database with
|
56
|
+
# daemons, sqlite won't cut it.
|
57
|
+
self.use_transactional_fixtures = false
|
58
|
+
|
59
|
+
test "something needing daemons" do
|
60
|
+
Daemonz.with_daemons do
|
61
|
+
# daemons will be alive while this code is executed
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
</pre>
|
66
|
+
</notextile>
|
67
|
+
|
68
|
+
p. Copyright (c) 2008 Victor Costan, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the daemonz plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.pattern = 'test/**/*_test.rb'
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'Generate documentation for the daemonz plugin.'
|
16
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
18
|
+
rdoc.title = 'Daemonz'
|
19
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
20
|
+
rdoc.rdoc_files.include('README')
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
22
|
+
end
|
data/config_template.yml
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# Configuration file for the daemonz plugin.
|
2
|
+
#
|
3
|
+
# For documentation and examples, refer to the original template in
|
4
|
+
# vendor/daemonz/config_template.yml
|
5
|
+
|
6
|
+
---
|
7
|
+
# Descriptions of the daemons to be managed by daemonz.
|
8
|
+
#
|
9
|
+
# daemonz starts the daemons according to the alphabetic order of their names.
|
10
|
+
# start_order can be used to override this order. Daemons will be stored in the
|
11
|
+
# reverse order of their starting order.
|
12
|
+
#
|
13
|
+
# daemonz likes to ensure that multiple instances of a daemon don't run at the
|
14
|
+
# same time, as this can be fatal for daemons with on-disk state, like ferret.
|
15
|
+
# So daemonz ensures that a daemon is dead before starting it, and right after
|
16
|
+
# stopping it. This is achieved using the following means:
|
17
|
+
# * Stop commands: first, daemonz uses the stop command supplied in the daemon
|
18
|
+
# configuration
|
19
|
+
# * PID files: if the daemon has .pid files, daemonz tries to read the file
|
20
|
+
# and find the corresponding processes, then tree-kills them
|
21
|
+
# * Process table: if possible, daemonz dumps the process table, looks for the
|
22
|
+
# processes that look like the daemon, and tree-kills them
|
23
|
+
# * Pattern matching: processes whose command lines have the same arguments as
|
24
|
+
# those given to daemon "look like" that daemon
|
25
|
+
# * Tree killing: a daemon is killed by killing its main process, together
|
26
|
+
# with all processes descending from that process; a process
|
27
|
+
# is first sent SIGTERM and, if it's still alive after a
|
28
|
+
# couple of seconds, it's sent a SIGKILL
|
29
|
+
:daemons:
|
30
|
+
# Example: configuring ferret.
|
31
|
+
ferret:
|
32
|
+
# Ferret uses different binaries to be started and stopped.
|
33
|
+
:start_binary: script/ferret_server
|
34
|
+
:stop_binary: script/ferret_server
|
35
|
+
# The arguments to be given to the start and stop commands.
|
36
|
+
# Note that this file is processed with Erb, like your views.
|
37
|
+
:start_args: -e <%= RAILS_ENV %> start
|
38
|
+
:stop_args: -e <%= RAILS_ENV %> stop
|
39
|
+
# Time to wait after sending the stop command, before killing the daemon.
|
40
|
+
:delay_before_kill: 0.35
|
41
|
+
# Pattern for the PID file(s) used by the daemon.
|
42
|
+
:pids: tmp/pids/ferret*.pid
|
43
|
+
# daemonz will ignore this daemon configuration when this flag is true
|
44
|
+
:disabled: true
|
45
|
+
starling:
|
46
|
+
# The same binary is used to start and stop starling.
|
47
|
+
:binary: starling
|
48
|
+
# The binary name will not be merged with root_path.
|
49
|
+
:absolute_binary: true
|
50
|
+
:start_args: -d -h 127.0.0.1 -p 16020 -P <%= RAILS_ROOT %>/tmp/pids/starling.pid -q <%= RAILS_ROOT %>/tmp -L <%= RAILS_ROOT %>/log/starling.log
|
51
|
+
# No arguments are needed to stop starling.
|
52
|
+
:stop_args: ''
|
53
|
+
# Pattern for the PID file(s) used by the daemon.
|
54
|
+
:pids: tmp/pids/starling*.pid
|
55
|
+
# Override for the patterns used to identify the daemon's processes.
|
56
|
+
:kill_patterns: <%= RAILS_ROOT %>/log/starling.log
|
57
|
+
# Time to wait after sending the stop command, before killing the daemon.
|
58
|
+
:delay_before_kill: 0.2
|
59
|
+
# Override the daemon startup order. Starling consumer daemons should have
|
60
|
+
# their start_order set to 2, so starling is running when they start.
|
61
|
+
:start_order: 1
|
62
|
+
# daemonz will ignore this daemon configuration when this flag is true
|
63
|
+
:disabled: true
|
64
|
+
|
65
|
+
# The base path for daemon binaries specified in binary, start_binary and
|
66
|
+
# stop_binary.
|
67
|
+
:root_path: <%= RAILS_ROOT %>
|
68
|
+
|
69
|
+
# Where daemonz should log - set to stdout, stderr, or rails.
|
70
|
+
:logger: stdout
|
71
|
+
|
72
|
+
# Set to true to completely disable daemonz, and not load any plugins.
|
73
|
+
:disabled: false
|
74
|
+
|
75
|
+
# daemonz is loaded every time the Rails framework is loaded. Sometimes
|
76
|
+
# (e.g. when performing migrations) daemons aren't required, so we shouldn't
|
77
|
+
# have to wait for the few seconds it takes to start / stop daemons.
|
78
|
+
|
79
|
+
# Daemons will not be started / stopped when the name of the binary that's
|
80
|
+
# loading Rails ($0) is one of the following.
|
81
|
+
:disabled_for:
|
82
|
+
- 'rake'
|
83
|
+
- 'script/generate'
|
84
|
+
|
85
|
+
# Daemons will not be started for the following environments.
|
86
|
+
:disabled_in:
|
87
|
+
- 'test'
|
data/daemonz.gemspec
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{daemonz}
|
8
|
+
s.version = "0.3.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Victor Costan"]
|
12
|
+
s.date = %q{2010-04-18}
|
13
|
+
s.email = %q{victor@costan.us}
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"README.textile"
|
16
|
+
]
|
17
|
+
s.files = [
|
18
|
+
"MIT-LICENSE",
|
19
|
+
"README.textile",
|
20
|
+
"Rakefile",
|
21
|
+
"config_template.yml",
|
22
|
+
"daemonz.gemspec",
|
23
|
+
"init.rb",
|
24
|
+
"lib/daemonz.rb",
|
25
|
+
"lib/daemonz/config.rb",
|
26
|
+
"lib/daemonz/killer.rb",
|
27
|
+
"lib/daemonz/logging.rb",
|
28
|
+
"lib/daemonz/manage.rb",
|
29
|
+
"lib/daemonz/master.rb",
|
30
|
+
"lib/daemonz/process.rb"
|
31
|
+
]
|
32
|
+
s.homepage = %q{http://github.com/costan/daemonz}
|
33
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
34
|
+
s.require_paths = ["lib"]
|
35
|
+
s.rubygems_version = %q{1.3.6}
|
36
|
+
s.summary = %q{Automatically starts and stops the daemons in a Rails application}
|
37
|
+
s.test_files = [
|
38
|
+
"test/daemonz_test.rb"
|
39
|
+
]
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
43
|
+
s.specification_version = 3
|
44
|
+
|
45
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
46
|
+
s.add_runtime_dependency(%q<simple-daemon>, [">= 0"])
|
47
|
+
s.add_runtime_dependency(%q<zerg_support>, [">= 0"])
|
48
|
+
else
|
49
|
+
s.add_dependency(%q<simple-daemon>, [">= 0"])
|
50
|
+
s.add_dependency(%q<zerg_support>, [">= 0"])
|
51
|
+
end
|
52
|
+
else
|
53
|
+
s.add_dependency(%q<simple-daemon>, [">= 0"])
|
54
|
+
s.add_dependency(%q<zerg_support>, [">= 0"])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
data/init.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Daemonz
|
5
|
+
class << self
|
6
|
+
attr_reader :config
|
7
|
+
|
8
|
+
# Set by the rake tasks.
|
9
|
+
attr_accessor :keep_daemons_at_exit
|
10
|
+
end
|
11
|
+
|
12
|
+
# compute whether daemonz should be enabled or not
|
13
|
+
def self.disabled?
|
14
|
+
return config[:cached_disabled] if config.has_key? :cached_disabled
|
15
|
+
config[:cached_disabled] = disabled_without_cache!
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.disabled_without_cache!
|
19
|
+
return true if config[:disabled]
|
20
|
+
return true if config[:disabled_in].include? RAILS_ENV
|
21
|
+
config[:disabled_for].any? do |suffix|
|
22
|
+
suffix == $0[-suffix.length, suffix.length]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# figure out the plugin's configuration
|
27
|
+
def self.configure(config_file, options = {})
|
28
|
+
load_configuration config_file
|
29
|
+
|
30
|
+
config[:root_path] ||= RAILS_ROOT
|
31
|
+
if options[:force_enabled]
|
32
|
+
config[:disabled] = false
|
33
|
+
config[:disabled_for] = []
|
34
|
+
config[:disabled_in] = []
|
35
|
+
else
|
36
|
+
config[:disabled] ||= false
|
37
|
+
config[:disabled_for] ||= ['rake', 'script/generate']
|
38
|
+
config[:disabled_in] ||= ['test']
|
39
|
+
end
|
40
|
+
config[:disabled] = false if config[:disabled] == 'false'
|
41
|
+
config[:master_file] ||= File.join RAILS_ROOT, "tmp", "pids", "daemonz.master.pid"
|
42
|
+
|
43
|
+
config[:logger] &&= options[:override_logger]
|
44
|
+
self.configure_logger
|
45
|
+
|
46
|
+
if self.disabled?
|
47
|
+
config[:is_master] = false
|
48
|
+
else
|
49
|
+
config[:is_master] = Daemonz.claim_master
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# load and parse the config file
|
54
|
+
def self.load_configuration(config_file)
|
55
|
+
if File.exist? config_file
|
56
|
+
file_contents = File.read config_file
|
57
|
+
erb_result = ERB.new(file_contents).result
|
58
|
+
@config = YAML.load erb_result
|
59
|
+
@config[:daemons] ||= {}
|
60
|
+
|
61
|
+
config_dir = File.join(File.dirname(config_file), 'daemonz')
|
62
|
+
if File.exist? config_dir
|
63
|
+
Dir.entries(config_dir).each do |entry|
|
64
|
+
daemons_file = File.join(config_dir, entry)
|
65
|
+
next unless File.file? daemons_file
|
66
|
+
|
67
|
+
file_contents = File.read daemons_file
|
68
|
+
erb_result = ERB.new(file_contents).result
|
69
|
+
daemons = YAML.load erb_result
|
70
|
+
daemons.keys.each do |daemon|
|
71
|
+
if @config[:daemons].has_key? daemon
|
72
|
+
logger.warn "Daemonz daemon file #{entry} overwrites daemon #{daemon} defined in daemonz.yml"
|
73
|
+
end
|
74
|
+
@config[:daemons][daemon] = daemons[daemon]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
else
|
79
|
+
logger.warn "Daemonz configuration not found - #{config_file}"
|
80
|
+
@config = {}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class << self
|
85
|
+
attr_reader :daemons
|
86
|
+
end
|
87
|
+
|
88
|
+
# process the daemon configuration
|
89
|
+
def self.configure_daemons
|
90
|
+
@daemons = []
|
91
|
+
config[:daemons].each do |name, daemon_config|
|
92
|
+
next if daemon_config[:disabled]
|
93
|
+
daemon = { :name => name }
|
94
|
+
|
95
|
+
# compute the daemon startup / stop commands
|
96
|
+
['start', 'stop'].each do |command|
|
97
|
+
daemon_binary = daemon_config[:binary] || daemon_config["#{command}_binary".to_sym]
|
98
|
+
if daemon_config[:absolute_binary]
|
99
|
+
daemon_path = `which #{daemon_binary}`.strip
|
100
|
+
unless daemon_config[:kill_patterns]
|
101
|
+
logger.error "Daemonz ignoring #{name}; using an absolute binary path but no custom process kill patterns"
|
102
|
+
break
|
103
|
+
end
|
104
|
+
else
|
105
|
+
daemon_path = File.join config[:root_path], daemon_binary || ''
|
106
|
+
end
|
107
|
+
unless daemon_binary and File.exists? daemon_path
|
108
|
+
logger.error "Daemonz ignoring #{name}; the #{command} file is missing"
|
109
|
+
break
|
110
|
+
end
|
111
|
+
|
112
|
+
unless daemon_config[:absolute_binary]
|
113
|
+
begin
|
114
|
+
binary_perms = File.stat(daemon_path).mode
|
115
|
+
if binary_perms != (binary_perms | 0111)
|
116
|
+
File.chmod(binary_perms | 0111, daemon_path)
|
117
|
+
end
|
118
|
+
rescue Exception => e
|
119
|
+
# chmod might fail due to lack of permissions
|
120
|
+
logger.error "Daemonz failed to make #{name} binary executable - #{e.class.name}: #{e}\n"
|
121
|
+
logger.info e.backtrace.join("\n") + "\n"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
daemon_args = daemon_config[:args] || daemon_config["#{command}_args".to_sym]
|
126
|
+
daemon_cmdline = "#{daemon_path} #{daemon_args}"
|
127
|
+
daemon[command.to_sym] = {:path => daemon_path, :cmdline => daemon_cmdline}
|
128
|
+
end
|
129
|
+
next unless daemon[:stop]
|
130
|
+
|
131
|
+
# kill patterns
|
132
|
+
daemon[:kill_patterns] = daemon_config[:kill_patterns] || [daemon[:start][:path]]
|
133
|
+
|
134
|
+
# pass-through params
|
135
|
+
daemon[:pids] = daemon_config[:pids]
|
136
|
+
unless daemon[:pids]
|
137
|
+
logger.error "Daemonz ignoring #{name}; no pid file pattern specified"
|
138
|
+
next
|
139
|
+
end
|
140
|
+
daemon[:delay_before_kill] = daemon_config[:delay_before_kill] || 0.2
|
141
|
+
daemon[:start_order] = daemon_config[:start_order]
|
142
|
+
|
143
|
+
@daemons << daemon
|
144
|
+
end
|
145
|
+
|
146
|
+
# sort by start_order, then by name
|
147
|
+
@daemons.sort! do |a, b|
|
148
|
+
if a[:start_order]
|
149
|
+
if b[:start_order]
|
150
|
+
if a[:start_order] != b[:start_order]
|
151
|
+
next a[:start_order] <=> b[:start_order]
|
152
|
+
else
|
153
|
+
next a[:name] <=> b[:name]
|
154
|
+
end
|
155
|
+
else
|
156
|
+
next 1
|
157
|
+
end
|
158
|
+
else
|
159
|
+
next a[:name] <=> b[:name]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'English'
|
2
|
+
|
3
|
+
module Daemonz
|
4
|
+
# Complex procedure for killing a process or a bunch of process replicas
|
5
|
+
# kill_command is the script that's supposed to kill the process / processes (tried first)
|
6
|
+
# pid_patters are globs identifying PID files (a file can match any of the patterns)
|
7
|
+
# process_patterns are strings that should show on a command line (a process must match all)
|
8
|
+
# options:
|
9
|
+
# :verbose - log what gets killed
|
10
|
+
# :script_delay - the amount of seconds to sleep after launching the kill script
|
11
|
+
# :force_script - the kill script is executed even if there are no PID files
|
12
|
+
def self.kill_process_set(kill_script, pid_patterns, process_patterns, options = {})
|
13
|
+
# Phase 1: kill order (only if there's a PID file)
|
14
|
+
pid_patterns = [pid_patterns] unless pid_patterns.kind_of? Enumerable
|
15
|
+
unless options[:force_script]
|
16
|
+
pid_files = pid_patterns.map { |pattern| Dir.glob(pattern) }.flatten
|
17
|
+
end
|
18
|
+
if options[:force_script] or !(pid_files.empty? or kill_script.nil?)
|
19
|
+
logger.info "Issuing kill order: #{kill_script}\n" if options[:verbose]
|
20
|
+
success = Kernel.system kill_script unless kill_script.nil?
|
21
|
+
if !success and options[:verbose]
|
22
|
+
logger.warn "Kill order failed with exit code #{$CHILD_STATUS.exitstatus}"
|
23
|
+
end
|
24
|
+
|
25
|
+
deadline_time = Time.now + (options[:script_delay] || 0.5)
|
26
|
+
while Time.now < deadline_time
|
27
|
+
pid_files = pid_patterns.map { |pattern| Dir.glob(pattern) }.flatten
|
28
|
+
break if pid_files.empty?
|
29
|
+
sleep 0.05
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Phase 2: look through PID files and issue kill orders
|
34
|
+
pinfo = process_info()
|
35
|
+
pid_files = pid_patterns.map { |pattern| Dir.glob(pattern) }.flatten
|
36
|
+
pid_files.each do |fname|
|
37
|
+
begin
|
38
|
+
pid = File.open(fname, 'r') { |f| f.read.strip! }
|
39
|
+
process_cmdline = pinfo[pid][:cmdline]
|
40
|
+
# avoid killing innocent victims
|
41
|
+
if pinfo[pid].nil? or process_patterns.all? { |pattern| process_cmdline.index pattern }
|
42
|
+
logger.warn "Killing #{pid}: #{process_cmdline}" if options[:verbose]
|
43
|
+
Process.kill 'TERM', pid.to_i
|
44
|
+
end
|
45
|
+
rescue
|
46
|
+
# just in case the file gets wiped before we see it
|
47
|
+
end
|
48
|
+
begin
|
49
|
+
logger.warn "Deleting #{fname}" if options[:verbose]
|
50
|
+
File.delete fname if File.exists? fname
|
51
|
+
rescue
|
52
|
+
# prevents crashing if the file is wiped after we call exists?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Phase 3: look through the process table and kill anything that looks good
|
57
|
+
pinfo = process_info()
|
58
|
+
pinfo.each do |pid, info|
|
59
|
+
next unless process_patterns.all? { |pattern| info[:cmdline].index pattern }
|
60
|
+
logger.warn "Killing #{pid}: #{pinfo[pid][:cmdline]}" if options[:verbose]
|
61
|
+
Process.kill 'TERM', pid.to_i
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Daemonz
|
2
|
+
@logger = RAILS_DEFAULT_LOGGER
|
3
|
+
class <<self
|
4
|
+
attr_reader :logger
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.configure_logger
|
8
|
+
case config[:logger]
|
9
|
+
when 'stdout'
|
10
|
+
@logger = Logger.new(STDOUT)
|
11
|
+
@logger.level = Logger::DEBUG
|
12
|
+
when 'stderr'
|
13
|
+
@logger = Logger.new(STDERR)
|
14
|
+
@logger.level = Logger::DEBUG
|
15
|
+
when 'rails'
|
16
|
+
@logger = RAILS_DEFAULT_LOGGER
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'English'
|
2
|
+
|
3
|
+
module Daemonz
|
4
|
+
# Starts daemons, yields, stops daemons. Intended for tests.
|
5
|
+
def self.with_daemons(logger = 'rails')
|
6
|
+
begin
|
7
|
+
safe_start :force_enabled => true, :override_logger => logger
|
8
|
+
yield
|
9
|
+
ensure
|
10
|
+
safe_stop :force_enabled => true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Complete startup used by rake:start and at Rails plug-in startup.
|
15
|
+
def self.safe_start(options = {})
|
16
|
+
daemonz_config = File.join(RAILS_ROOT, 'config', 'daemonz.yml')
|
17
|
+
Daemonz.configure daemonz_config, options
|
18
|
+
|
19
|
+
if Daemonz.config[:is_master]
|
20
|
+
Daemonz.configure_daemons
|
21
|
+
Daemonz.start_daemons!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Complete shutdown used by rake:start and at Rails application exit.
|
26
|
+
def self.safe_stop(options = {})
|
27
|
+
if options[:configure]
|
28
|
+
daemonz_config = File.join(RAILS_ROOT, 'config', 'daemonz.yml')
|
29
|
+
Daemonz.configure daemonz_config, options
|
30
|
+
end
|
31
|
+
if Daemonz.config[:is_master]
|
32
|
+
if options[:configure]
|
33
|
+
Daemonz.configure_daemons
|
34
|
+
end
|
35
|
+
Daemonz.stop_daemons!
|
36
|
+
Daemonz.release_master_lock
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.start_daemons!
|
41
|
+
@daemons.each { |daemon| start_daemon! daemon }
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.stop_daemons!
|
45
|
+
@daemons.reverse.each { |daemon| stop_daemon! daemon }
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.start_daemon!(daemon)
|
49
|
+
# cleanup before we start
|
50
|
+
kill_process_set daemon[:stop][:cmdline], daemon[:pids],
|
51
|
+
daemon[:kill_patterns],
|
52
|
+
:script_delay => daemon[:delay_before_kill],
|
53
|
+
:verbose => true, :force_script => false
|
54
|
+
|
55
|
+
logger.info "Daemonz starting #{daemon[:name]}: #{daemon[:start][:cmdline]}"
|
56
|
+
success = Kernel.system daemon[:start][:cmdline]
|
57
|
+
unless success
|
58
|
+
logger.warn "Daemonz start script for #{daemon[:name]} failed " +
|
59
|
+
"with code #{$CHILD_STATUS.exitstatus}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.stop_daemon!(daemon)
|
64
|
+
kill_process_set daemon[:stop][:cmdline], daemon[:pids],
|
65
|
+
daemon[:kill_patterns],
|
66
|
+
:script_delay => daemon[:delay_before_kill],
|
67
|
+
:verbose => true, :force_script => true
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'English'
|
2
|
+
|
3
|
+
module Daemonz
|
4
|
+
def self.release_master_lock
|
5
|
+
if File.exist? config[:master_file]
|
6
|
+
File.delete config[:master_file]
|
7
|
+
else
|
8
|
+
logger.warn "Master lock removed by someone else"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.grab_master_lock
|
13
|
+
loop do
|
14
|
+
File.open(config[:master_file], File::CREAT | File::RDWR) do |f|
|
15
|
+
if f.flock File::LOCK_EX
|
16
|
+
lock_data = f.read
|
17
|
+
lock_data = lock_data[lock_data.index(/\d/), lock_data.length] if lock_data.index /\d/
|
18
|
+
master = lock_data.split("\n", 2)
|
19
|
+
|
20
|
+
if master.length == 2
|
21
|
+
master_pid = master[0].to_i
|
22
|
+
master_cmdline = master[1]
|
23
|
+
if master_pid != 0
|
24
|
+
master_pinfo = process_info(master_pid)
|
25
|
+
return master_pid if master_pinfo and master_pinfo[:cmdline] == master_cmdline
|
26
|
+
|
27
|
+
logger.info "Old master (PID #{master_pid}) died; breaking master lock"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
f.truncate 0
|
32
|
+
f.write "#{$PID}\n#{process_info($PID)[:cmdline]}"
|
33
|
+
f.flush
|
34
|
+
return nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# attempts to claim the master lock
|
41
|
+
def self.claim_master
|
42
|
+
loop do
|
43
|
+
begin
|
44
|
+
# try to grab that lock
|
45
|
+
master_pid = grab_master_lock
|
46
|
+
if master_pid
|
47
|
+
logger.info "Daemonz in slave mode; PID #{master_pid} has master lock"
|
48
|
+
return false
|
49
|
+
else
|
50
|
+
logger.info "Daemonz grabbed master lock"
|
51
|
+
return true
|
52
|
+
end
|
53
|
+
#rescue Exception => e
|
54
|
+
# logger.warn "Daemonz mastering failed: #{e.class.name} - #{e}"
|
55
|
+
# logger.info "Retrying daemonz mastering"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# Mocks the sys-proctable gem using ps.
|
2
|
+
#
|
3
|
+
# This is useful even if sys-proctable is available, because it may fail for
|
4
|
+
# random reasons.
|
5
|
+
module Daemonz::ProcTable
|
6
|
+
class ProcInfo
|
7
|
+
def initialize(pid, cmdline)
|
8
|
+
@pid = pid
|
9
|
+
@cmdline = cmdline
|
10
|
+
end
|
11
|
+
attr_reader :pid, :cmdline
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.ps_emulation
|
15
|
+
retval = []
|
16
|
+
ps_output = `ps ax`
|
17
|
+
ps_output.each_line do |pline|
|
18
|
+
pdata = pline.split(nil, 5)
|
19
|
+
pinfo = ProcInfo.new(pdata[0].strip, pdata[4].strip)
|
20
|
+
retval << pinfo
|
21
|
+
end
|
22
|
+
return retval
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'sys/proctable'
|
28
|
+
if Sys::ProcTable::VERSION == '0.7.6'
|
29
|
+
raise LoadError, 'Buggy sys/proctable, emulate'
|
30
|
+
end
|
31
|
+
|
32
|
+
module Daemonz::ProcTable
|
33
|
+
def self.ps
|
34
|
+
# We don't use ps_emulation all the time because sys-proctable is
|
35
|
+
# faster. We only pay the performance penalty when sys-proctable fails.
|
36
|
+
begin
|
37
|
+
Sys::ProcTable.ps
|
38
|
+
rescue Exception
|
39
|
+
self.ps_emulation
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
rescue LoadError
|
44
|
+
# The accelerated version is not available, use the slow version all the time.
|
45
|
+
|
46
|
+
module Daemonz::ProcTable
|
47
|
+
def self.ps
|
48
|
+
self.ps_emulation
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module Daemonz
|
54
|
+
# returns information about a process or all the running processes
|
55
|
+
def self.process_info(pid = nil)
|
56
|
+
info = Hash.new
|
57
|
+
|
58
|
+
Daemonz::ProcTable.ps.each do |process|
|
59
|
+
item = { :cmdline => process.cmdline, :pid => process.pid.to_s }
|
60
|
+
|
61
|
+
if pid.nil?
|
62
|
+
info[process.pid.to_s] = item
|
63
|
+
else
|
64
|
+
return item if item[:pid].to_s == pid.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
if pid.nil?
|
69
|
+
return info
|
70
|
+
else
|
71
|
+
return nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/daemonz.rb
ADDED
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: daemonz
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 3
|
8
|
+
- 0
|
9
|
+
version: 0.3.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Victor Costan
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-04-18 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: simple-daemon
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :runtime
|
31
|
+
version_requirements: *id001
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: zerg_support
|
34
|
+
prerelease: false
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
segments:
|
40
|
+
- 0
|
41
|
+
version: "0"
|
42
|
+
type: :runtime
|
43
|
+
version_requirements: *id002
|
44
|
+
description:
|
45
|
+
email: victor@costan.us
|
46
|
+
executables: []
|
47
|
+
|
48
|
+
extensions: []
|
49
|
+
|
50
|
+
extra_rdoc_files:
|
51
|
+
- README.textile
|
52
|
+
files:
|
53
|
+
- MIT-LICENSE
|
54
|
+
- README.textile
|
55
|
+
- Rakefile
|
56
|
+
- config_template.yml
|
57
|
+
- daemonz.gemspec
|
58
|
+
- init.rb
|
59
|
+
- lib/daemonz.rb
|
60
|
+
- lib/daemonz/config.rb
|
61
|
+
- lib/daemonz/killer.rb
|
62
|
+
- lib/daemonz/logging.rb
|
63
|
+
- lib/daemonz/manage.rb
|
64
|
+
- lib/daemonz/master.rb
|
65
|
+
- lib/daemonz/process.rb
|
66
|
+
has_rdoc: true
|
67
|
+
homepage: http://github.com/costan/daemonz
|
68
|
+
licenses: []
|
69
|
+
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options:
|
72
|
+
- --charset=UTF-8
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
segments:
|
80
|
+
- 0
|
81
|
+
version: "0"
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
requirements: []
|
90
|
+
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 1.3.6
|
93
|
+
signing_key:
|
94
|
+
specification_version: 3
|
95
|
+
summary: Automatically starts and stops the daemons in a Rails application
|
96
|
+
test_files:
|
97
|
+
- test/daemonz_test.rb
|