mikehale-daemons 1.0.12.1
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/LICENSE +29 -0
- data/README +232 -0
- data/Rakefile +84 -0
- data/Releases +126 -0
- data/TODO +6 -0
- data/daemons.gemspec +49 -0
- data/lib/daemons.rb +284 -0
- data/lib/daemons/application.rb +387 -0
- data/lib/daemons/application_group.rb +226 -0
- data/lib/daemons/change_privilege.rb +19 -0
- data/lib/daemons/cmdline.rb +117 -0
- data/lib/daemons/controller.rb +134 -0
- data/lib/daemons/daemonize.rb +263 -0
- data/lib/daemons/etc_extension.rb +12 -0
- data/lib/daemons/exceptions.rb +28 -0
- data/lib/daemons/monitor.rb +127 -0
- data/lib/daemons/pid.rb +101 -0
- data/lib/daemons/pidfile.rb +111 -0
- data/lib/daemons/pidmem.rb +10 -0
- metadata +74 -0
@@ -0,0 +1,226 @@
|
|
1
|
+
|
2
|
+
module Daemons
|
3
|
+
class ApplicationGroup
|
4
|
+
|
5
|
+
attr_reader :app_name
|
6
|
+
attr_reader :script
|
7
|
+
|
8
|
+
attr_reader :monitor
|
9
|
+
|
10
|
+
#attr_reader :controller
|
11
|
+
|
12
|
+
attr_reader :options
|
13
|
+
|
14
|
+
attr_reader :applications
|
15
|
+
|
16
|
+
attr_accessor :controller_argv
|
17
|
+
attr_accessor :app_argv
|
18
|
+
|
19
|
+
attr_accessor :dir_mode
|
20
|
+
attr_accessor :dir
|
21
|
+
|
22
|
+
# true if the application is supposed to run in multiple instances
|
23
|
+
attr_reader :multiple
|
24
|
+
|
25
|
+
|
26
|
+
def initialize(app_name, options = {})
|
27
|
+
@app_name = app_name
|
28
|
+
@options = options
|
29
|
+
|
30
|
+
if options[:script]
|
31
|
+
@script = File.expand_path(options[:script])
|
32
|
+
end
|
33
|
+
|
34
|
+
#@controller = controller
|
35
|
+
@monitor = nil
|
36
|
+
|
37
|
+
#options = controller.options
|
38
|
+
|
39
|
+
@multiple = options[:multiple] || false
|
40
|
+
|
41
|
+
@dir_mode = options[:dir_mode] || :script
|
42
|
+
@dir = options[:dir] || ''
|
43
|
+
|
44
|
+
@keep_pid_files = options[:keep_pid_files] || false
|
45
|
+
|
46
|
+
#@applications = find_applications(pidfile_dir())
|
47
|
+
@applications = []
|
48
|
+
end
|
49
|
+
|
50
|
+
# Setup the application group.
|
51
|
+
# Currently this functions calls <tt>find_applications</tt> which finds
|
52
|
+
# all running instances of the application and populates the application array.
|
53
|
+
#
|
54
|
+
def setup
|
55
|
+
@applications = find_applications(pidfile_dir())
|
56
|
+
end
|
57
|
+
|
58
|
+
def pidfile_dir
|
59
|
+
PidFile.dir(@dir_mode, @dir, script)
|
60
|
+
end
|
61
|
+
|
62
|
+
def find_applications(dir)
|
63
|
+
pid_files = PidFile.find_files(dir, app_name, ! @keep_pid_files)
|
64
|
+
|
65
|
+
#pp pid_files
|
66
|
+
|
67
|
+
@monitor = Monitor.find(dir, app_name + '_monitor')
|
68
|
+
|
69
|
+
pid_files.reject! {|f| f =~ /_monitor.pid$/}
|
70
|
+
|
71
|
+
return pid_files.map {|f|
|
72
|
+
app = Application.new(self, {}, PidFile.existing(f))
|
73
|
+
setup_app(app)
|
74
|
+
app
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def new_application(add_options = {})
|
79
|
+
if @applications.size > 0 and not @multiple
|
80
|
+
if options[:force]
|
81
|
+
@applications.delete_if {|a|
|
82
|
+
unless a.running?
|
83
|
+
a.zap
|
84
|
+
true
|
85
|
+
end
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
raise RuntimeException.new('there is already one or more instance(s) of the program running') unless @applications.empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
app = Application.new(self, add_options)
|
93
|
+
|
94
|
+
setup_app(app)
|
95
|
+
|
96
|
+
@applications << app
|
97
|
+
|
98
|
+
return app
|
99
|
+
end
|
100
|
+
|
101
|
+
def setup_app(app)
|
102
|
+
app.controller_argv = @controller_argv
|
103
|
+
app.app_argv = @app_argv
|
104
|
+
end
|
105
|
+
private :setup_app
|
106
|
+
|
107
|
+
def create_monitor(an_app)
|
108
|
+
return if @monitor
|
109
|
+
|
110
|
+
if options[:monitor]
|
111
|
+
@monitor = Monitor.new(an_app)
|
112
|
+
|
113
|
+
@monitor.start(@applications)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def start_all
|
118
|
+
@monitor.stop if @monitor
|
119
|
+
@monitor = nil
|
120
|
+
|
121
|
+
@applications.each {|a|
|
122
|
+
fork {
|
123
|
+
a.start
|
124
|
+
}
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
# Specify :force_kill_wait => (seconds to wait) and this method will
|
129
|
+
# block until the process is dead. It first sends a TERM signal, then
|
130
|
+
# a KILL signal (-9) if the process hasn't died after the wait time.
|
131
|
+
# Note: The force argument is from the original daemons implementation.
|
132
|
+
def stop_all(force = false)
|
133
|
+
@monitor.stop if @monitor
|
134
|
+
|
135
|
+
failed_to_kill = false
|
136
|
+
debug = options[:debug]
|
137
|
+
wait = options[:force_kill_wait].to_i
|
138
|
+
pids = unix_pids
|
139
|
+
if wait > 0 && pids.size > 0
|
140
|
+
puts "[daemons_ext]: Killing #{app_name} with force after #{wait} secs."
|
141
|
+
STDOUT.flush
|
142
|
+
|
143
|
+
# Send term first, don't delete PID files.
|
144
|
+
pids.each {|pid| Process.kill('TERM', pid) rescue Errno::ESRCH}
|
145
|
+
|
146
|
+
begin
|
147
|
+
Timeout::timeout(wait) {block_on_pids(wait, debug, options[:sleepy_time] || 1)}
|
148
|
+
rescue Timeout::Error
|
149
|
+
puts "[daemons_ext]: Time is up! Forcefully killing #{unix_pids.size} #{app_name}(s)..."
|
150
|
+
STDOUT.flush
|
151
|
+
unix_pids.each {|pid| `kill -9 #{pid}`}
|
152
|
+
begin
|
153
|
+
# Give it an extra 30 seconds to kill -9
|
154
|
+
Timeout::timeout(30) {block_on_pids(wait, debug, options[:sleepy_time] || 1)}
|
155
|
+
rescue Timeout::Error
|
156
|
+
failed_to_kill = true
|
157
|
+
puts "[daemons_ext]: #{unix_pids} #{app_name}(s) won't die! Giving up."
|
158
|
+
STDOUT.flush
|
159
|
+
end
|
160
|
+
ensure
|
161
|
+
# Delete Pidfiles
|
162
|
+
@applications.each {|a| a.zap!}
|
163
|
+
end
|
164
|
+
|
165
|
+
puts "[daemons_ext]: All #{app_name}s dead." unless failed_to_kill
|
166
|
+
STDOUT.flush
|
167
|
+
else
|
168
|
+
@applications.each {|a|
|
169
|
+
if force
|
170
|
+
begin; a.stop; rescue ::Exception; end
|
171
|
+
else
|
172
|
+
a.stop
|
173
|
+
end
|
174
|
+
}
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def zap_all
|
179
|
+
@monitor.stop if @monitor
|
180
|
+
|
181
|
+
@applications.each {|a| a.zap}
|
182
|
+
end
|
183
|
+
|
184
|
+
def show_status
|
185
|
+
@applications.each {|a| a.show_status}
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
# Block until all unix_pids are gone (should be wrapped in a timeout)
|
191
|
+
def block_on_pids(wait, debug, sleepy_time = 1)
|
192
|
+
started_at = Time.now
|
193
|
+
num_pids = unix_pids.size
|
194
|
+
while num_pids > 0
|
195
|
+
time_left = wait - (Time.now - started_at)
|
196
|
+
puts "[daemons_ext]: Waiting #{time_left.round} secs on " +
|
197
|
+
"#{num_pids} #{app_name}(s)..."
|
198
|
+
unix_pids.each {|pid| puts "\t#{pid}"} if debug
|
199
|
+
STDOUT.flush
|
200
|
+
sleep sleepy_time
|
201
|
+
num_pids = unix_pids.size
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Find UNIX pids based on app_name. CAUTION: This has only been tested on
|
206
|
+
# Mac OS X and CentOS.
|
207
|
+
def unix_pids
|
208
|
+
pids = []
|
209
|
+
x = `ps auxw | grep -v grep | awk '{print $2, $11}' | grep #{app_name}`
|
210
|
+
if x && x.chomp!
|
211
|
+
processes = x.split(/\n/).compact
|
212
|
+
processes = processes.delete_if do |p|
|
213
|
+
pid, name = p.split(/\s/)
|
214
|
+
# We want to make sure that the first part of the process name matches
|
215
|
+
# so that app_name matches app_name_22
|
216
|
+
app_name != name[0..(app_name.length - 1)]
|
217
|
+
end
|
218
|
+
pids = processes.map {|p| p.split(/\s/)[0].to_i}
|
219
|
+
end
|
220
|
+
|
221
|
+
pids
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'daemons/etc_extension'
|
2
|
+
|
3
|
+
class CurrentProcess
|
4
|
+
def self.change_privilege(user, group=user)
|
5
|
+
puts "Changing process privilege to #{user}:#{group}"
|
6
|
+
|
7
|
+
uid, gid = Process.euid, Process.egid
|
8
|
+
target_uid = Etc.getpwnam(user).uid
|
9
|
+
target_gid = Etc.getgrnam(group).gid
|
10
|
+
|
11
|
+
if uid != target_uid || gid != target_gid
|
12
|
+
Process.initgroups(user, target_gid)
|
13
|
+
Process::GID.change_privilege(target_gid)
|
14
|
+
Process::UID.change_privilege(target_uid)
|
15
|
+
end
|
16
|
+
rescue Errno::EPERM => e
|
17
|
+
raise "Couldn't change user and group to #{user}:#{group}: #{e}"
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
|
2
|
+
module Daemons
|
3
|
+
|
4
|
+
class Optparse
|
5
|
+
|
6
|
+
attr_reader :usage
|
7
|
+
|
8
|
+
def initialize(controller)
|
9
|
+
@controller = controller
|
10
|
+
@options = {}
|
11
|
+
|
12
|
+
@opts = OptionParser.new do |opts|
|
13
|
+
#opts.banner = "Usage: example.rb [options]"
|
14
|
+
opts.banner = ""
|
15
|
+
|
16
|
+
# Boolean switch.
|
17
|
+
# opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
18
|
+
# @options[:verbose] = v
|
19
|
+
# end
|
20
|
+
|
21
|
+
opts.on("-t", "--ontop", "Stay on top (does not daemonize)") do |t|
|
22
|
+
@options[:ontop] = t
|
23
|
+
end
|
24
|
+
|
25
|
+
opts.on("-f", "--force", "Force operation") do |t|
|
26
|
+
@options[:force] = t
|
27
|
+
end
|
28
|
+
|
29
|
+
#opts.separator ""
|
30
|
+
#opts.separator "Specific options:"
|
31
|
+
|
32
|
+
|
33
|
+
opts.separator ""
|
34
|
+
opts.separator "Common options:"
|
35
|
+
|
36
|
+
# No argument, shows at tail. This will print an options summary.
|
37
|
+
# Try it and see!
|
38
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
39
|
+
#puts opts
|
40
|
+
#@usage =
|
41
|
+
controller.print_usage()
|
42
|
+
|
43
|
+
exit
|
44
|
+
end
|
45
|
+
|
46
|
+
# Another typical switch to print the version.
|
47
|
+
opts.on_tail("--version", "Show version") do
|
48
|
+
puts "daemons version #{Daemons::VERSION}"
|
49
|
+
exit
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
@usage = @opts.to_s
|
55
|
+
rescue ::Exception # work around a bug in ruby 1.9
|
56
|
+
@usage = <<END
|
57
|
+
-t, --ontop Stay on top (does not daemonize)
|
58
|
+
-f, --force Force operation
|
59
|
+
|
60
|
+
Common options:
|
61
|
+
-h, --help Show this message
|
62
|
+
--version Show version
|
63
|
+
END
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
#
|
69
|
+
# Return a hash describing the options.
|
70
|
+
#
|
71
|
+
def parse(args)
|
72
|
+
# The options specified on the command line will be collected in *options*.
|
73
|
+
# We set default values here.
|
74
|
+
#options = {}
|
75
|
+
|
76
|
+
|
77
|
+
##pp args
|
78
|
+
@opts.parse(args)
|
79
|
+
|
80
|
+
return @options
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
class Controller
|
87
|
+
|
88
|
+
def print_usage
|
89
|
+
puts "Usage: #{@app_name} <command> <options> -- <application options>"
|
90
|
+
puts
|
91
|
+
puts "* where <command> is one of:"
|
92
|
+
puts " start start an instance of the application"
|
93
|
+
puts " stop stop all instances of the application"
|
94
|
+
puts " restart stop all instances and restart them afterwards"
|
95
|
+
puts " run start the application and stay on top"
|
96
|
+
puts " zap set the application to a stopped state"
|
97
|
+
puts
|
98
|
+
puts "* and where <options> may contain several of the following:"
|
99
|
+
|
100
|
+
puts @optparse.usage
|
101
|
+
end
|
102
|
+
|
103
|
+
def catch_exceptions(&block)
|
104
|
+
begin
|
105
|
+
block.call
|
106
|
+
rescue CmdException, OptionParser::ParseError => e
|
107
|
+
puts "ERROR: #{e.to_s}"
|
108
|
+
puts
|
109
|
+
print_usage()
|
110
|
+
rescue RuntimeException => e
|
111
|
+
puts "ERROR: #{e.to_s}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
|
2
|
+
module Daemons
|
3
|
+
class Controller
|
4
|
+
|
5
|
+
attr_reader :app_name
|
6
|
+
|
7
|
+
attr_reader :group
|
8
|
+
|
9
|
+
attr_reader :options
|
10
|
+
|
11
|
+
|
12
|
+
COMMANDS = [
|
13
|
+
'start',
|
14
|
+
'stop',
|
15
|
+
'restart',
|
16
|
+
'run',
|
17
|
+
'zap',
|
18
|
+
'status'
|
19
|
+
]
|
20
|
+
|
21
|
+
def initialize(options = {}, argv = [])
|
22
|
+
@options = options
|
23
|
+
@argv = argv
|
24
|
+
|
25
|
+
# Allow an app_name to be specified. If not specified use the
|
26
|
+
# basename of the script.
|
27
|
+
@app_name = options[:app_name]
|
28
|
+
|
29
|
+
if options[:script]
|
30
|
+
@script = File.expand_path(options[:script])
|
31
|
+
|
32
|
+
@app_name ||= File.split(@script)[1]
|
33
|
+
end
|
34
|
+
|
35
|
+
@app_name ||= 'unknown_application'
|
36
|
+
|
37
|
+
@command, @controller_part, @app_part = Controller.split_argv(argv)
|
38
|
+
|
39
|
+
#@options[:dir_mode] ||= :script
|
40
|
+
|
41
|
+
@optparse = Optparse.new(self)
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# This function is used to do a final update of the options passed to the application
|
46
|
+
# before they are really used.
|
47
|
+
#
|
48
|
+
# Note that this function should only update <tt>@options</tt> and no other variables.
|
49
|
+
#
|
50
|
+
def setup_options
|
51
|
+
#@options[:ontop] ||= true
|
52
|
+
end
|
53
|
+
|
54
|
+
def run
|
55
|
+
@options.update @optparse.parse(@controller_part).delete_if {|k,v| !v}
|
56
|
+
|
57
|
+
setup_options()
|
58
|
+
|
59
|
+
#pp @options
|
60
|
+
|
61
|
+
@group = ApplicationGroup.new(@app_name, @options)
|
62
|
+
@group.controller_argv = @controller_part
|
63
|
+
@group.app_argv = @app_part
|
64
|
+
|
65
|
+
@group.setup
|
66
|
+
|
67
|
+
case @command
|
68
|
+
when 'start'
|
69
|
+
@group.new_application.start
|
70
|
+
when 'run'
|
71
|
+
@options[:ontop] ||= true
|
72
|
+
@group.new_application.start
|
73
|
+
when 'stop'
|
74
|
+
@group.stop_all
|
75
|
+
when 'restart'
|
76
|
+
unless @group.applications.empty?
|
77
|
+
@group.stop_all
|
78
|
+
sleep 1
|
79
|
+
@group.start_all
|
80
|
+
end
|
81
|
+
when 'zap'
|
82
|
+
@group.zap_all
|
83
|
+
when 'status'
|
84
|
+
unless @group.applications.empty?
|
85
|
+
@group.show_status
|
86
|
+
else
|
87
|
+
puts "#{@group.app_name}: no instances running"
|
88
|
+
end
|
89
|
+
when nil
|
90
|
+
raise CmdException.new('no command given')
|
91
|
+
#puts "ERROR: No command given"; puts
|
92
|
+
|
93
|
+
#print_usage()
|
94
|
+
#raise('usage function not implemented')
|
95
|
+
else
|
96
|
+
raise Error.new("command '#{@command}' not implemented")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
# Split an _argv_ array.
|
102
|
+
# +argv+ is assumed to be in the following format:
|
103
|
+
# ['command', 'controller option 1', 'controller option 2', ..., '--', 'app option 1', ...]
|
104
|
+
#
|
105
|
+
# <tt>command</tt> must be one of the commands listed in <tt>COMMANDS</tt>
|
106
|
+
#
|
107
|
+
# *Returns*: the command as a string, the controller options as an array, the appliation options
|
108
|
+
# as an array
|
109
|
+
#
|
110
|
+
def Controller.split_argv(argv)
|
111
|
+
argv = argv.dup
|
112
|
+
|
113
|
+
command = nil
|
114
|
+
controller_part = []
|
115
|
+
app_part = []
|
116
|
+
|
117
|
+
if COMMANDS.include? argv[0]
|
118
|
+
command = argv.shift
|
119
|
+
end
|
120
|
+
|
121
|
+
if i = argv.index('--')
|
122
|
+
# Handle the case where no controller options are given, just
|
123
|
+
# options after "--" as well (i == 0)
|
124
|
+
controller_part = (i == 0 ? [] : argv[0..i-1])
|
125
|
+
app_part = argv[i+1..-1]
|
126
|
+
else
|
127
|
+
controller_part = argv[0..-1]
|
128
|
+
end
|
129
|
+
|
130
|
+
return command, controller_part, app_part
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|