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.
Files changed (4) hide show
  1. data/bin/rbg +40 -0
  2. data/lib/rbg.rb +258 -0
  3. data/lib/rbg/config.rb +40 -0
  4. 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
+
@@ -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
+
@@ -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
+