rbg 0.9.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/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
|
+
|