procodile 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/procodile +30 -0
- data/lib/procodile.rb +2 -0
- data/lib/procodile/cli.rb +131 -0
- data/lib/procodile/config.rb +64 -0
- data/lib/procodile/error.rb +4 -0
- data/lib/procodile/instance.rb +204 -0
- data/lib/procodile/logger.rb +22 -0
- data/lib/procodile/process.rb +65 -0
- data/lib/procodile/signal_handler.rb +32 -0
- data/lib/procodile/supervisor.rb +96 -0
- metadata +56 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 12da3acd72adc0bf302ddad40c02133f308cf168
|
4
|
+
data.tar.gz: e13faabcbbadce0fd6ac91885c3b5ba937f398db
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8b61d0f1fb351bf1f05eac2ea0d22accd8ff2ebbe51fd551397ac5c8ee17693ffa36f6a7f3d2e9729774cf1dabac99358930de70c45c0aa2cd95b1d9359ffb06
|
7
|
+
data.tar.gz: b2f481642d8e7015808946d07cf6b7d0c027e1d2b8956518b27a61bd670e851e690fbf98c79cd3ec02b042d6c176c275794abe6ee2139effc234fc60152b65d9
|
data/bin/procodile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
options = {}
|
4
|
+
OptionParser.new do |opts|
|
5
|
+
opts.banner = "Usage: procodile [options]"
|
6
|
+
opts.on("-r", "--root PATH", "The path to the root of your application") do |root|
|
7
|
+
options[:root] = root
|
8
|
+
end
|
9
|
+
end.parse!
|
10
|
+
|
11
|
+
$:.unshift(File.expand_path('../../lib', __FILE__))
|
12
|
+
|
13
|
+
require 'fileutils'
|
14
|
+
require 'procodile/error'
|
15
|
+
require 'procodile/config'
|
16
|
+
require 'procodile/cli'
|
17
|
+
|
18
|
+
Thread.abort_on_exception = true
|
19
|
+
begin
|
20
|
+
|
21
|
+
config = Procodile::Config.new(options[:root] ? File.expand_path(options[:root]) : FileUtils.pwd)
|
22
|
+
command = ARGV[0]
|
23
|
+
|
24
|
+
cli = Procodile::CLI.new(config)
|
25
|
+
cli.run(command)
|
26
|
+
|
27
|
+
rescue Procodile::Error => e
|
28
|
+
$stderr.puts "\e[31merror: #{e.message}\e[0m"
|
29
|
+
exit 1
|
30
|
+
end
|
data/lib/procodile.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'procodile/error'
|
3
|
+
require 'procodile/supervisor'
|
4
|
+
require 'procodile/signal_handler'
|
5
|
+
|
6
|
+
module Procodile
|
7
|
+
class CLI
|
8
|
+
|
9
|
+
def initialize(config)
|
10
|
+
@config = config
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(command)
|
14
|
+
if self.class.instance_methods(false).include?(command.to_sym) && command != 'run'
|
15
|
+
public_send(command)
|
16
|
+
else
|
17
|
+
raise Error, "Invalid command '#{command}'"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
if running?
|
23
|
+
raise Error, "#{@config.app_name} already running (PID: #{current_pid})"
|
24
|
+
end
|
25
|
+
FileUtils.rm_f(File.join(@config.pid_root, "*.pid"))
|
26
|
+
pid = fork do
|
27
|
+
STDOUT.reopen(log_path, 'a')
|
28
|
+
STDOUT.sync = true
|
29
|
+
STDERR.reopen(log_path, 'a')
|
30
|
+
STDERR.sync = true
|
31
|
+
supervisor = Supervisor.new(@config)
|
32
|
+
signal_handler = SignalHandler.new('TERM', 'USR1', 'USR2', 'INT', 'HUP')
|
33
|
+
signal_handler.register('TERM') { supervisor.stop }
|
34
|
+
signal_handler.register('USR1') { supervisor.restart }
|
35
|
+
signal_handler.register('USR2') { supervisor.status }
|
36
|
+
signal_handler.register('INT') { supervisor.stop_supervisor }
|
37
|
+
supervisor.start
|
38
|
+
end
|
39
|
+
::Process.detach(pid)
|
40
|
+
File.open(pid_path, 'w') { |f| f.write(pid) }
|
41
|
+
puts "Started #{@config.app_name} supervisor with PID #{pid}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop
|
45
|
+
if running?
|
46
|
+
::Process.kill('TERM', current_pid)
|
47
|
+
puts "Stopping #{@config.app_name} processes & supervisor..."
|
48
|
+
else
|
49
|
+
raise Error, "#{@config.app_name} supervisor isn't running"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def stop_supervisor
|
54
|
+
if running?
|
55
|
+
puts "This will stop the supervisor only. Any processes that it started will no longer be managed."
|
56
|
+
puts "They will need to be stopped manually. \e[34mDo you wish to continue? (yes/NO)\e[0m"
|
57
|
+
if ['y', 'yes'].include?($stdin.gets.to_s.strip.downcase)
|
58
|
+
::Process.kill('INT', current_pid)
|
59
|
+
puts "We've asked it to stop. It'll probably be done in a moment."
|
60
|
+
else
|
61
|
+
puts "OK. That's fine. You can just run `stop` to stop processes too."
|
62
|
+
end
|
63
|
+
|
64
|
+
else
|
65
|
+
raise Error, "#{@config.app_name} supervisor isn't running"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def restart
|
70
|
+
if running?
|
71
|
+
::Process.kill('USR1', current_pid)
|
72
|
+
puts "Restarting #{@config.app_name}"
|
73
|
+
else
|
74
|
+
raise Error, "#{@config.app_name} supervisor isn't running"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def status
|
79
|
+
if running?
|
80
|
+
puts "#{@config.app_name} running (PID: #{current_pid})"
|
81
|
+
::Process.kill('USR2', current_pid)
|
82
|
+
puts "Instance status details added to #{log_path}"
|
83
|
+
else
|
84
|
+
puts "#{@config.app_name} supervisor not running"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def kill
|
89
|
+
Dir[File.join(@config.pid_root, '*.pid')].each do |pid_path|
|
90
|
+
name = pid_path.split('/').last.gsub(/\.pid\z/, '')
|
91
|
+
pid = File.read(pid_path).to_i
|
92
|
+
begin
|
93
|
+
::Process.kill('KILL', pid)
|
94
|
+
puts "Sent KILL to #{pid} (#{name})"
|
95
|
+
rescue Errno::ESRCH
|
96
|
+
end
|
97
|
+
FileUtils.rm(pid_path)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def running?
|
104
|
+
if pid = current_pid
|
105
|
+
::Process.getpgid(pid) ? true : false
|
106
|
+
else
|
107
|
+
false
|
108
|
+
end
|
109
|
+
rescue Errno::ESRCH
|
110
|
+
false
|
111
|
+
end
|
112
|
+
|
113
|
+
def current_pid
|
114
|
+
if File.exist?(pid_path)
|
115
|
+
pid_file = File.read(pid_path).strip
|
116
|
+
pid_file.length > 0 ? pid_file.to_i : nil
|
117
|
+
else
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def pid_path
|
123
|
+
File.join(@config.pid_root, 'supervisor.pid')
|
124
|
+
end
|
125
|
+
|
126
|
+
def log_path
|
127
|
+
File.join(@config.log_root, 'supervisor.log')
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'procodile/error'
|
3
|
+
require 'procodile/process'
|
4
|
+
|
5
|
+
module Procodile
|
6
|
+
class Config
|
7
|
+
|
8
|
+
COLORS = [35, 31, 36, 32, 33, 34]
|
9
|
+
|
10
|
+
attr_reader :root
|
11
|
+
|
12
|
+
def initialize(root)
|
13
|
+
@root = root
|
14
|
+
unless File.exist?(procfile_path)
|
15
|
+
raise Error, "Procfile not found at #{procfile_path}"
|
16
|
+
end
|
17
|
+
|
18
|
+
FileUtils.mkdir_p(pid_root)
|
19
|
+
FileUtils.mkdir_p(log_root)
|
20
|
+
end
|
21
|
+
|
22
|
+
def app_name
|
23
|
+
options['app_name'] || 'Procodile'
|
24
|
+
end
|
25
|
+
|
26
|
+
def processes
|
27
|
+
process_list.each_with_index.each_with_object({}) do |((name, command), index), hash|
|
28
|
+
options = {'log_color' => COLORS[index.divmod(COLORS.size)[1]]}.merge(process_options[name] || {})
|
29
|
+
hash[name] = Process.new(self, name, command, options)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def process_list
|
34
|
+
@processes ||= YAML.load_file(procfile_path)
|
35
|
+
end
|
36
|
+
|
37
|
+
def options
|
38
|
+
@options ||= File.exist?(options_path) ? YAML.load_file(options_path) : {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def process_options
|
42
|
+
@process_options ||= options['processes'] || {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def pid_root
|
46
|
+
File.expand_path(options['pid_root'] || 'pids', @root)
|
47
|
+
end
|
48
|
+
|
49
|
+
def log_root
|
50
|
+
File.expand_path(options['log_root'] || 'log', @root)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def procfile_path
|
56
|
+
File.join(@root, 'Procfile')
|
57
|
+
end
|
58
|
+
|
59
|
+
def options_path
|
60
|
+
File.join(@root, 'Procfile.options')
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'procodile/logger'
|
2
|
+
|
3
|
+
module Procodile
|
4
|
+
class Instance
|
5
|
+
|
6
|
+
attr_accessor :pid
|
7
|
+
|
8
|
+
def initialize(process, id)
|
9
|
+
@process = process
|
10
|
+
@id = id
|
11
|
+
@respawns = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
"#{@process.name}.#{@id}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def dead?
|
19
|
+
@dead || false
|
20
|
+
end
|
21
|
+
|
22
|
+
def pid_file_path
|
23
|
+
File.join(@process.config.pid_root, "#{description}.pid")
|
24
|
+
end
|
25
|
+
|
26
|
+
def log_file_path
|
27
|
+
File.join(@process.config.log_root, "#{description}.log")
|
28
|
+
end
|
29
|
+
|
30
|
+
def pid_from_file
|
31
|
+
if File.exist?(pid_file_path)
|
32
|
+
pid = File.read(pid_file_path)
|
33
|
+
pid.length > 0 ? pid.strip.to_i : nil
|
34
|
+
else
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def running?(force_pid = nil)
|
40
|
+
if force_pid || @pid
|
41
|
+
::Process.getpgid(force_pid || @pid) ? true : false
|
42
|
+
else
|
43
|
+
false
|
44
|
+
end
|
45
|
+
rescue Errno::ESRCH
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
def start
|
50
|
+
@stopping = false
|
51
|
+
Dir.chdir(@process.config.root) do
|
52
|
+
log_file = File.open(self.log_file_path, 'a')
|
53
|
+
@pid = ::Process.spawn({'PID_FILE' => pid_file_path}, @process.command, :out => log_file, :err => log_file)
|
54
|
+
Procodile.log(@process.log_color, description, "Started with PID #{@pid}")
|
55
|
+
File.open(pid_file_path, 'w') { |f| f.write(@pid.to_s + "\n") }
|
56
|
+
::Process.detach(@pid)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Is this instance supposed to be stopping/be stopped?
|
62
|
+
#
|
63
|
+
def stopping?
|
64
|
+
@stopping || false
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Send this signal the signal to stop and mark the instance in a state that
|
69
|
+
# tells us that we want it to be stopped.
|
70
|
+
#
|
71
|
+
def stop
|
72
|
+
@stopping = true
|
73
|
+
update_pid
|
74
|
+
if self.running?
|
75
|
+
pid = self.pid_from_file
|
76
|
+
Procodile.log(@process.log_color, description, "Sending TERM to #{pid}")
|
77
|
+
::Process.kill('TERM', pid)
|
78
|
+
else
|
79
|
+
Procodile.log(@process.log_color, description, "Process already stopped")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# A method that will be called when this instance has been stopped and it isn't going to be
|
85
|
+
# started again
|
86
|
+
#
|
87
|
+
def on_stop
|
88
|
+
FileUtils.rm_f(self.pid_file_path)
|
89
|
+
Procodile.log(@process.log_color, description, "Removed PID file")
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Retarts the process using the appropriate method from the process configuraiton
|
94
|
+
#
|
95
|
+
def restart
|
96
|
+
Procodile.log(@process.log_color, description, "Restarting using #{@process.restart_mode} mode")
|
97
|
+
@restarting = true
|
98
|
+
update_pid
|
99
|
+
case @process.restart_mode
|
100
|
+
when 'usr1', 'usr2'
|
101
|
+
if running?
|
102
|
+
::Process.kill(@process.restart_mode.upcase, @pid)
|
103
|
+
Procodile.log(@process.log_color, description, "Sent #{@process.restart_mode.upcase} signal to process #{@pid}")
|
104
|
+
else
|
105
|
+
Procodile.log(@process.log_color, description, "Process not running already. Starting it.")
|
106
|
+
start
|
107
|
+
end
|
108
|
+
when 'start-term'
|
109
|
+
old_process_pid = @pid
|
110
|
+
start
|
111
|
+
Procodile.log(@process.log_color, description, "Sent TERM signal to old PID #{old_process_pid} (forgetting now)")
|
112
|
+
::Process.kill('TERM', old_process_pid)
|
113
|
+
when 'term-start'
|
114
|
+
stop
|
115
|
+
Thread.new do
|
116
|
+
# Wait for this process to stop and when it has, run it.
|
117
|
+
sleep 0.5 while running?
|
118
|
+
start
|
119
|
+
end
|
120
|
+
end
|
121
|
+
ensure
|
122
|
+
@restarting = false
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Update the locally cached PID from that stored on the file system.
|
127
|
+
#
|
128
|
+
def update_pid
|
129
|
+
pid_from_file = self.pid_from_file
|
130
|
+
if pid_from_file != @pid
|
131
|
+
@pid = pid_from_file
|
132
|
+
Procodile.log(@process.log_color, description, "PID file changed. Updated pid to #{@pid}")
|
133
|
+
true
|
134
|
+
else
|
135
|
+
false
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Check the status of this process and handle as appropriate
|
141
|
+
#
|
142
|
+
def check
|
143
|
+
# Don't do any checking if we're in the midst of a restart
|
144
|
+
return if @restarting
|
145
|
+
|
146
|
+
if self.running?
|
147
|
+
# Everything is OK. The process is running.
|
148
|
+
else
|
149
|
+
# If the process isn't running any more, update the PID in our memory from
|
150
|
+
# the file in case the process has changed itself.
|
151
|
+
return check if update_pid
|
152
|
+
|
153
|
+
if can_respawn?
|
154
|
+
Procodile.log(@process.log_color, description, "Process has stopped. Respawning...")
|
155
|
+
start
|
156
|
+
add_respawn
|
157
|
+
elsif respawns >= @process.max_respawns
|
158
|
+
Procodile.log(@process.log_color, description, "\e[41;37mWarning:\e[0m\e[31m this process has been respawned #{respawns} times and keeps dying.\e[0m")
|
159
|
+
Procodile.log(@process.log_color, description, "\e[31mIt will not be respawned automatically any longer and will no longer be managed.\e[0m")
|
160
|
+
die
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
#
|
166
|
+
# Mark this process as dead and tidy up after it
|
167
|
+
#
|
168
|
+
def die
|
169
|
+
on_stop
|
170
|
+
@dead = true
|
171
|
+
end
|
172
|
+
|
173
|
+
#
|
174
|
+
# Can this process be respawned if needed?
|
175
|
+
#
|
176
|
+
def can_respawn?
|
177
|
+
!stopping? && (respawns + 1) <= @process.max_respawns
|
178
|
+
end
|
179
|
+
|
180
|
+
#
|
181
|
+
# Return the number of times this process has been respawned in the last hour
|
182
|
+
#
|
183
|
+
def respawns
|
184
|
+
if @respawns.nil? || @last_respawn.nil? || @last_respawn < (Time.now - @process.respawn_window)
|
185
|
+
0
|
186
|
+
else
|
187
|
+
@respawns
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
#
|
192
|
+
# Increment the counter of respawns for this process
|
193
|
+
#
|
194
|
+
def add_respawn
|
195
|
+
if @last_respawn && @last_respawn < (Time.now - @process.respawn_window)
|
196
|
+
@respawns = 1
|
197
|
+
else
|
198
|
+
@last_respawn = Time.now
|
199
|
+
@respawns += 1
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'logger'
|
2
|
+
module Procodile
|
3
|
+
|
4
|
+
def self.mutex
|
5
|
+
@mutex ||= Mutex.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.log(color, name, text)
|
9
|
+
mutex.synchronize do
|
10
|
+
text.to_s.lines.map(&:chomp).each do |message|
|
11
|
+
output = ""
|
12
|
+
output += "\e[#{color}m" if color
|
13
|
+
output += "#{Time.now.strftime("%H:%M:%S")} #{name.ljust(15, ' ')} |"
|
14
|
+
output += "\e[0m "
|
15
|
+
output += message
|
16
|
+
$stdout.puts output
|
17
|
+
$stdout.flush
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'procodile/instance'
|
2
|
+
|
3
|
+
module Procodile
|
4
|
+
class Process
|
5
|
+
|
6
|
+
attr_reader :name
|
7
|
+
attr_reader :command
|
8
|
+
attr_reader :config
|
9
|
+
|
10
|
+
def initialize(config, name, command, options = {})
|
11
|
+
@config = config
|
12
|
+
@name = name
|
13
|
+
@command = command
|
14
|
+
@options = options
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Return the color for this process
|
19
|
+
#
|
20
|
+
def log_color
|
21
|
+
@options['log_color'] || 0
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# How many instances of this process should be started
|
26
|
+
#
|
27
|
+
def quantity
|
28
|
+
@options['quantity'] || 1
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# The maximum number of times this process can be respawned in the given period
|
33
|
+
#
|
34
|
+
def max_respawns
|
35
|
+
@options['max_respawns'] ? @options['max_respawns'].to_i : 5
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# The respawn window. One hour by default.
|
40
|
+
#
|
41
|
+
def respawn_window
|
42
|
+
@options['respawn_window'] ? @options['respawn_window'].to_i : 3600
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Defines how this process should be restarted
|
47
|
+
#
|
48
|
+
# start-term = start new instances and send term to children
|
49
|
+
# usr1 = just send a usr1 signal to the current instance
|
50
|
+
# usr2 = just send a usr2 signal to the current instance
|
51
|
+
# term-start = stop the old instances, when no longer running, start a new one
|
52
|
+
#
|
53
|
+
def restart_mode
|
54
|
+
@options['restart_mode'] || 'term-start'
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Generate an array of new instances for this process (based on its quantity)
|
59
|
+
#
|
60
|
+
def generate_instances
|
61
|
+
quantity.times.map { |i| Instance.new(self, i + 1) }
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Procodile
|
2
|
+
class SignalHandler
|
3
|
+
|
4
|
+
def self.queue
|
5
|
+
Thread.main[:signal_queue] ||= []
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(*signals)
|
9
|
+
@handlers = {}
|
10
|
+
Thread.new do
|
11
|
+
loop do
|
12
|
+
if signal = self.class.queue.shift
|
13
|
+
if @handlers[signal]
|
14
|
+
@handlers[signal].each(&:call)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
sleep 1
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
signals.each do |sig|
|
22
|
+
Signal.trap(sig, proc { SignalHandler.queue << sig })
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def register(name, &block)
|
27
|
+
@handlers[name] ||= []
|
28
|
+
@handlers[name] << block
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Procodile
|
2
|
+
class Supervisor
|
3
|
+
|
4
|
+
# Create a new supervisor instance that will be monitoring the
|
5
|
+
# processes that have been provided.
|
6
|
+
def initialize(config)
|
7
|
+
@config = config
|
8
|
+
@instances = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def start
|
12
|
+
Procodile.log nil, "system", "#{@config.app_name} supervisor started with PID #{::Process.pid}"
|
13
|
+
@config.processes.each do |name, process|
|
14
|
+
process.generate_instances.each do |instance|
|
15
|
+
instance.start
|
16
|
+
@instances << instance
|
17
|
+
end
|
18
|
+
end
|
19
|
+
supervise
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop
|
23
|
+
return if @stopping
|
24
|
+
@stopping = true
|
25
|
+
Procodile.log nil, "system", "Stopping all #{@config.app_name} processes"
|
26
|
+
@instances.each(&:stop)
|
27
|
+
end
|
28
|
+
|
29
|
+
def stop_supervisor
|
30
|
+
Procodile.log nil, 'system', "Stopping #{@config.app_name} supervisor"
|
31
|
+
FileUtils.rm_f(File.join(@config.pid_root, 'supervisor.pid'))
|
32
|
+
::Process.exit 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def restart
|
36
|
+
Procodile.log nil, 'system', "Restarting all #{@config.app_name} processes"
|
37
|
+
@instances.each(&:restart)
|
38
|
+
end
|
39
|
+
|
40
|
+
def status
|
41
|
+
Procodile.log '37;44', 'status', "Status as at: #{Time.now.utc.to_s}"
|
42
|
+
@instances.each do |instance|
|
43
|
+
if instance.running?
|
44
|
+
Procodile.log '37;44', 'status', "#{instance.description} is RUNNING (pid #{instance.pid}). Respawned #{instance.respawns} time(s)"
|
45
|
+
else
|
46
|
+
Procodile.log '37;44', 'status', "#{instance.description} is STOPPED"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def supervise
|
52
|
+
loop do
|
53
|
+
# Tidy up any instances that we no longer wish to be managed. They will
|
54
|
+
# be removed from the list.
|
55
|
+
remove_dead_instances
|
56
|
+
|
57
|
+
if @stopping
|
58
|
+
# If the system is stopping, we'll remove any instances that have already
|
59
|
+
# stopped and trigger their on_stop callback.
|
60
|
+
remove_stopped_instances
|
61
|
+
|
62
|
+
# When all the instances we manage have gone away, we can stop ourself.
|
63
|
+
if @instances.size > 0
|
64
|
+
Procodile.log nil, "system", "Waiting for #{@instances.size} processes to stop"
|
65
|
+
else
|
66
|
+
Procodile.log nil, "system", "All processes have stopped"
|
67
|
+
stop_supervisor
|
68
|
+
end
|
69
|
+
else
|
70
|
+
# Check all instances that we manage and let them do their things.
|
71
|
+
@instances.each(&:check)
|
72
|
+
end
|
73
|
+
|
74
|
+
sleep 5
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def remove_dead_instances
|
81
|
+
@instances.reject!(&:dead?)
|
82
|
+
end
|
83
|
+
|
84
|
+
def remove_stopped_instances
|
85
|
+
@instances.reject! do |instance|
|
86
|
+
if instance.running?
|
87
|
+
false
|
88
|
+
else
|
89
|
+
instance.on_stop
|
90
|
+
true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
metadata
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: procodile
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Cooke
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-10-16 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Run Ruby/Rails processes in the background on Linux servers with ease.
|
14
|
+
email:
|
15
|
+
- me@adamcooke.io
|
16
|
+
executables:
|
17
|
+
- procodile
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- bin/procodile
|
22
|
+
- lib/procodile.rb
|
23
|
+
- lib/procodile/cli.rb
|
24
|
+
- lib/procodile/config.rb
|
25
|
+
- lib/procodile/error.rb
|
26
|
+
- lib/procodile/instance.rb
|
27
|
+
- lib/procodile/logger.rb
|
28
|
+
- lib/procodile/process.rb
|
29
|
+
- lib/procodile/signal_handler.rb
|
30
|
+
- lib/procodile/supervisor.rb
|
31
|
+
homepage: https://github.com/adamcooke/procodile
|
32
|
+
licenses:
|
33
|
+
- MIT
|
34
|
+
metadata: {}
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
requirements: []
|
50
|
+
rubyforge_project:
|
51
|
+
rubygems_version: 2.5.1
|
52
|
+
signing_key:
|
53
|
+
specification_version: 4
|
54
|
+
summary: This gem will help you run Ruby processes from a Procfile on Linux servers
|
55
|
+
in the background.
|
56
|
+
test_files: []
|