rbg 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/rbg +40 -0
- data/lib/rbg.rb +258 -0
- data/lib/rbg/config.rb +40 -0
- metadata +69 -0
data/bin/rbg
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rbg'
|
3
|
+
|
4
|
+
def die
|
5
|
+
puts "Usage: " + $0 + " run|start|stop|reload -c config_file [-E environment]"
|
6
|
+
Process.exit(1)
|
7
|
+
end
|
8
|
+
|
9
|
+
begin
|
10
|
+
command = nil
|
11
|
+
while arg = ARGV.shift
|
12
|
+
case arg
|
13
|
+
when '-c'
|
14
|
+
config_file = ARGV.shift
|
15
|
+
when '-E'
|
16
|
+
environment = ARGV.shift
|
17
|
+
else
|
18
|
+
command = arg
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
die unless config_file
|
23
|
+
|
24
|
+
case command
|
25
|
+
when 'run'
|
26
|
+
Rbg.start(config_file, {:background => false, :environment => environment})
|
27
|
+
when 'start'
|
28
|
+
Rbg.start(config_file, {:background => true, :environment => environment})
|
29
|
+
when 'stop'
|
30
|
+
Rbg.stop(config_file)
|
31
|
+
when 'reload'
|
32
|
+
Rbg.reload(config_file)
|
33
|
+
else
|
34
|
+
die
|
35
|
+
end
|
36
|
+
rescue Rbg::Error => e
|
37
|
+
$stderr.puts "!!! #{e.message}"
|
38
|
+
Process.exit(1)
|
39
|
+
end
|
40
|
+
|
data/lib/rbg.rb
ADDED
@@ -0,0 +1,258 @@
|
|
1
|
+
require 'rbg/config'
|
2
|
+
|
3
|
+
module Rbg
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
## An array of child PIDs for the current process which have been spawned
|
9
|
+
attr_accessor :child_processes
|
10
|
+
|
11
|
+
## The path to the config file that was specified
|
12
|
+
attr_accessor :config_file
|
13
|
+
|
14
|
+
## Return a configration object for this backgroundable application.
|
15
|
+
def config
|
16
|
+
@config ||= Rbg::Config.new
|
17
|
+
end
|
18
|
+
|
19
|
+
# Creates a 'parent' process. This is responsible for executing 'before_fork'
|
20
|
+
# and then forking the worker processes.
|
21
|
+
def start_parent
|
22
|
+
# Record the PID of this parent in the Master
|
23
|
+
self.child_processes << fork do
|
24
|
+
# Clear the child process list as this fork doesn't have any children yet
|
25
|
+
self.child_processes = Array.new
|
26
|
+
|
27
|
+
# Set the process name (Parent)
|
28
|
+
$0="#{self.config.name}[P]"
|
29
|
+
|
30
|
+
# Debug information
|
31
|
+
puts "New parent process: #{Process.pid}"
|
32
|
+
STDOUT.flush
|
33
|
+
|
34
|
+
# Run the before_fork function
|
35
|
+
self.config.before_fork.call
|
36
|
+
|
37
|
+
# Fork an appropriate number of workers
|
38
|
+
self.fork_workers(self.config.workers)
|
39
|
+
|
40
|
+
# If we get a TERM, send the existing workers a TERM then exit
|
41
|
+
Signal.trap("TERM", proc {
|
42
|
+
# Debug output
|
43
|
+
puts "Parent got a TERM."
|
44
|
+
STDOUT.flush
|
45
|
+
|
46
|
+
# Send TERM to workers
|
47
|
+
kill_child_processes
|
48
|
+
|
49
|
+
# Exit the parent
|
50
|
+
Process.exit(0)
|
51
|
+
})
|
52
|
+
|
53
|
+
# Ending parent processes on INT is not useful or desirable
|
54
|
+
# especially when running in the foreground
|
55
|
+
Signal.trap('INT', proc {})
|
56
|
+
|
57
|
+
# Parent loop, the purpose of this is simply to do nothing until we get a signal
|
58
|
+
# We may add memory management code here in the future
|
59
|
+
loop do
|
60
|
+
sleep 1
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Wrapper to fork multiple workers
|
67
|
+
def fork_workers(n)
|
68
|
+
n.times do |i|
|
69
|
+
self.fork_worker(i)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Fork a single worker
|
74
|
+
def fork_worker(i)
|
75
|
+
pid = fork do
|
76
|
+
# Set process name
|
77
|
+
$0="#{self.config.name}[#{i}]"
|
78
|
+
|
79
|
+
# Ending workers on INT is not useful or desirable
|
80
|
+
Signal.trap('INT', proc {})
|
81
|
+
# Restore normal behaviour
|
82
|
+
Signal.trap('TERM', proc {Process.exit(0)})
|
83
|
+
|
84
|
+
# Execure before_fork code
|
85
|
+
self.config.after_fork.call
|
86
|
+
|
87
|
+
# The actual code to run
|
88
|
+
require self.config.script
|
89
|
+
end
|
90
|
+
|
91
|
+
# Print some debug info and save the pid
|
92
|
+
puts "Spawned '#{self.config.name}[#{i}]' as PID #{pid}"
|
93
|
+
STDOUT.flush
|
94
|
+
|
95
|
+
# Detach to eliminate Zombie processes later
|
96
|
+
Process.detach(pid)
|
97
|
+
|
98
|
+
# Save the worker PID into the Parent's child process list
|
99
|
+
self.child_processes << pid
|
100
|
+
end
|
101
|
+
|
102
|
+
# Kill all child processes
|
103
|
+
def kill_child_processes
|
104
|
+
puts 'Killing child processes...'
|
105
|
+
STDOUT.flush
|
106
|
+
self.child_processes.each do |p|
|
107
|
+
puts "Killing: #{p}"
|
108
|
+
STDOUT.flush
|
109
|
+
begin
|
110
|
+
Process.kill('TERM', p)
|
111
|
+
rescue
|
112
|
+
puts "Process already gone away"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
# Clear the child process list because we just killed them all
|
116
|
+
self.child_processes = Array.new
|
117
|
+
end
|
118
|
+
|
119
|
+
# This is the master process, it spawns some workers then loops
|
120
|
+
def master_process
|
121
|
+
# Log the master PID
|
122
|
+
puts "New master process: #{Process.pid}"
|
123
|
+
STDOUT.flush
|
124
|
+
|
125
|
+
# Set the process name
|
126
|
+
$0="#{self.config.name}[M]"
|
127
|
+
|
128
|
+
# Fork a Parent process
|
129
|
+
# This will load the before_fork in a clean process then fork the script as required
|
130
|
+
self.start_parent
|
131
|
+
|
132
|
+
# If we get a USR1, send the existing workers a TERM before starting some new ones
|
133
|
+
Signal.trap("USR1", proc {
|
134
|
+
puts "Master got a USR1."
|
135
|
+
STDOUT.flush
|
136
|
+
self.kill_child_processes
|
137
|
+
load_config
|
138
|
+
self.start_parent
|
139
|
+
})
|
140
|
+
|
141
|
+
# If we get a TERM, send the existing workers a TERM before bowing out
|
142
|
+
Signal.trap("TERM", proc {
|
143
|
+
puts "Master got a TERM."
|
144
|
+
STDOUT.flush
|
145
|
+
kill_child_processes
|
146
|
+
Process.exit(0)
|
147
|
+
})
|
148
|
+
|
149
|
+
# INT is useful for when we don't want to background
|
150
|
+
Signal.trap("INT", proc {
|
151
|
+
puts "Master got an INT."
|
152
|
+
STDOUT.flush
|
153
|
+
kill_child_processes
|
154
|
+
Process.exit(0)
|
155
|
+
})
|
156
|
+
|
157
|
+
# Main loop, the purpose of this is simply to do nothing until we get a signal
|
158
|
+
loop do
|
159
|
+
sleep 1
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Load or reload the config file defined at startup
|
164
|
+
def load_config
|
165
|
+
@config = nil
|
166
|
+
if File.exist?(self.config_file.to_s)
|
167
|
+
load self.config_file
|
168
|
+
else
|
169
|
+
raise Error, "Configuration file not found at '#{config_file}'"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def start(config_file, options = {})
|
174
|
+
options[:background] ||= false
|
175
|
+
options[:environment] ||= "development"
|
176
|
+
$rbg_env = options[:environment].dup
|
177
|
+
|
178
|
+
# Define the config file then load it
|
179
|
+
self.config_file = config_file
|
180
|
+
self.load_config
|
181
|
+
|
182
|
+
# Initialize child process array
|
183
|
+
self.child_processes = Array.new
|
184
|
+
|
185
|
+
if options[:background]
|
186
|
+
# Fork the master control process and return to a shell
|
187
|
+
master_pid = fork do
|
188
|
+
# Ignore input and log to a file
|
189
|
+
STDIN.reopen('/dev/null')
|
190
|
+
if self.config.log_path
|
191
|
+
STDOUT.reopen(self.config.log_path, 'a')
|
192
|
+
STDERR.reopen(self.config.log_path, 'a')
|
193
|
+
else
|
194
|
+
raise Error, "Log location not specified in '#{config_file}'"
|
195
|
+
end
|
196
|
+
|
197
|
+
self.master_process
|
198
|
+
end
|
199
|
+
|
200
|
+
# Ensure the process is properly backgrounded
|
201
|
+
Process.detach(master_pid)
|
202
|
+
if self.config.pid_path
|
203
|
+
File.open(self.config.pid_path, 'w') {|f| f.write(master_pid) }
|
204
|
+
end
|
205
|
+
|
206
|
+
puts "Master started as PID #{master_pid}"
|
207
|
+
else
|
208
|
+
# Run using existing STDIN / STDOUT
|
209
|
+
self.master_process
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Get the PID from the pidfile defined in the config
|
214
|
+
def pid_from_file
|
215
|
+
raise Error, "PID not defined in '#{config_file}'" unless self.config.pid_path
|
216
|
+
begin
|
217
|
+
pid = File.read(self.config.pid_path).strip.to_i
|
218
|
+
rescue
|
219
|
+
raise Error, "PID file not found"
|
220
|
+
end
|
221
|
+
return pid
|
222
|
+
end
|
223
|
+
|
224
|
+
# Stop the running instance
|
225
|
+
def stop(config_file)
|
226
|
+
# Define the config file then load it
|
227
|
+
self.config_file = config_file
|
228
|
+
self.load_config
|
229
|
+
|
230
|
+
pid = self.pid_from_file
|
231
|
+
|
232
|
+
begin
|
233
|
+
Process.kill('TERM', pid)
|
234
|
+
puts "Sent TERM to PID #{pid}"
|
235
|
+
rescue
|
236
|
+
raise Error, "Process #{pid} not found"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Reload the running instance
|
241
|
+
def reload(config_file)
|
242
|
+
# Define the config file then load it
|
243
|
+
self.config_file = config_file
|
244
|
+
self.load_config
|
245
|
+
|
246
|
+
pid = self.pid_from_file
|
247
|
+
|
248
|
+
begin
|
249
|
+
Process.kill('USR1', pid)
|
250
|
+
puts "Sent USR1 to PID #{pid}"
|
251
|
+
rescue
|
252
|
+
raise Error, "Process #{pid} not found"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
data/lib/rbg/config.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Rbg
|
2
|
+
class Config
|
3
|
+
|
4
|
+
## The name of the application as used in the proclist
|
5
|
+
attr_accessor :name
|
6
|
+
|
7
|
+
## The ruby script which should be backgrounded
|
8
|
+
attr_accessor :script
|
9
|
+
|
10
|
+
## Path to the log file for the master process
|
11
|
+
attr_accessor :log_path
|
12
|
+
|
13
|
+
## Path to the PID file for the master process
|
14
|
+
attr_accessor :pid_path
|
15
|
+
|
16
|
+
## Number of workers to start
|
17
|
+
attr_accessor :workers
|
18
|
+
|
19
|
+
## Block of code to be executed in the master process before the process
|
20
|
+
## has been forked.
|
21
|
+
def before_fork(&block)
|
22
|
+
if block_given?
|
23
|
+
@before_fork = block
|
24
|
+
else
|
25
|
+
@before_fork
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
## Block of code to be executed in the child process after forking has
|
30
|
+
## taken place.
|
31
|
+
def after_fork(&block)
|
32
|
+
if block_given?
|
33
|
+
@after_fork = block
|
34
|
+
else
|
35
|
+
@after_fork
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rbg
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 57
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 9
|
9
|
+
- 1
|
10
|
+
version: 0.9.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Charlie Smurthwaite
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-10-25 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description:
|
23
|
+
email: charlie@atechmedia.com
|
24
|
+
executables:
|
25
|
+
- rbg
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- bin/rbg
|
32
|
+
- lib/rbg/config.rb
|
33
|
+
- lib/rbg.rb
|
34
|
+
has_rdoc: true
|
35
|
+
homepage: http://www.atechmedia.com
|
36
|
+
licenses: []
|
37
|
+
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
hash: 3
|
49
|
+
segments:
|
50
|
+
- 0
|
51
|
+
version: "0"
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.3.7
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Ruby Backgrounder allows multiple copies of ruby scripts to be run in the background and restarted
|
68
|
+
test_files: []
|
69
|
+
|