rbg 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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
+