daemonz 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|