procodile 0.0.1 → 0.0.2
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.
- checksums.yaml +4 -4
- data/bin/procodile +32 -11
- data/lib/procodile.rb +6 -0
- data/lib/procodile/cli.rb +109 -33
- data/lib/procodile/config.rb +38 -7
- data/lib/procodile/control_client.rb +50 -0
- data/lib/procodile/control_server.rb +29 -0
- data/lib/procodile/control_session.rb +68 -0
- data/lib/procodile/instance.rb +59 -10
- data/lib/procodile/process.rb +22 -11
- data/lib/procodile/supervisor.rb +140 -47
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a67424151ecf1b1b6fc80cdd93d4989b3bc1f0ba
|
4
|
+
data.tar.gz: 5df9bfac9a606afe8987a6400fa097f0091d99a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85cc30d458aa91a9ec06c25412bfced78860f7f2a2f2a74d674e790c811d2d7005d1cfe8812d6fd027013a21919c8126f7d5930bb219db3c6c4e1b09a06e8ad7
|
7
|
+
data.tar.gz: 8d80992dcfb3a8dafda5da5feae9cc4798f6225862583eb520be96c979f29dbd45cab4bc8070274b0a88e5662f27ef4c93fed4248488259454e2624c626a30a3
|
data/bin/procodile
CHANGED
@@ -1,29 +1,50 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'optparse'
|
3
|
+
|
4
|
+
command = ARGV[0]
|
5
|
+
|
3
6
|
options = {}
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
7
|
+
begin
|
8
|
+
OptionParser.new do |opts|
|
9
|
+
opts.banner = "Usage: procodile [command] [options]"
|
10
|
+
opts.on("-r", "--root PATH", "The path to the root of your application") do |root|
|
11
|
+
options[:root] = root
|
12
|
+
end
|
13
|
+
|
14
|
+
if command == 'start'
|
15
|
+
opts.on("-f", "--foreground", "Run the supervisor in the foreground") do
|
16
|
+
options[:foreground] = true
|
17
|
+
end
|
18
|
+
|
19
|
+
opts.on("--clean", "Remove all previous pid and sock files before starting") do
|
20
|
+
options[:clean] = true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
if ['start', 'stop', 'restart'].include?(command)
|
25
|
+
opts.on("-p", "--processes a,b,c", "Only #{command} the listed processes or process types") do |processes|
|
26
|
+
options[:processes] = processes
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end.parse!
|
30
|
+
rescue OptionParser::InvalidOption => e
|
31
|
+
$stderr.puts "\e[31merror: #{e.message}\e[0m"
|
32
|
+
exit 1
|
33
|
+
end
|
10
34
|
|
11
35
|
$:.unshift(File.expand_path('../../lib', __FILE__))
|
12
36
|
|
13
37
|
require 'fileutils'
|
38
|
+
require 'procodile'
|
14
39
|
require 'procodile/error'
|
15
40
|
require 'procodile/config'
|
16
41
|
require 'procodile/cli'
|
17
42
|
|
18
43
|
Thread.abort_on_exception = true
|
19
44
|
begin
|
20
|
-
|
21
45
|
config = Procodile::Config.new(options[:root] ? File.expand_path(options[:root]) : FileUtils.pwd)
|
22
|
-
|
23
|
-
|
24
|
-
cli = Procodile::CLI.new(config)
|
46
|
+
cli = Procodile::CLI.new(config, options)
|
25
47
|
cli.run(command)
|
26
|
-
|
27
48
|
rescue Procodile::Error => e
|
28
49
|
$stderr.puts "\e[31merror: #{e.message}\e[0m"
|
29
50
|
exit 1
|
data/lib/procodile.rb
CHANGED
data/lib/procodile/cli.rb
CHANGED
@@ -2,12 +2,14 @@ require 'fileutils'
|
|
2
2
|
require 'procodile/error'
|
3
3
|
require 'procodile/supervisor'
|
4
4
|
require 'procodile/signal_handler'
|
5
|
+
require 'procodile/control_client'
|
5
6
|
|
6
7
|
module Procodile
|
7
8
|
class CLI
|
8
9
|
|
9
|
-
def initialize(config)
|
10
|
+
def initialize(config, cli_options = {})
|
10
11
|
@config = config
|
12
|
+
@cli_options = cli_options
|
11
13
|
end
|
12
14
|
|
13
15
|
def run(command)
|
@@ -20,56 +22,88 @@ module Procodile
|
|
20
22
|
|
21
23
|
def start
|
22
24
|
if running?
|
23
|
-
|
25
|
+
instances = ControlClient.run(@config.sock_path, 'start_processes', :processes => process_names_from_cli_option)
|
26
|
+
if instances.empty?
|
27
|
+
raise Error, "No processes were started. The type you entered might already be running or isn't defined."
|
28
|
+
else
|
29
|
+
instances.each do |instance|
|
30
|
+
puts "Started #{instance['description']} (PID: #{instance['pid']})"
|
31
|
+
end
|
32
|
+
return
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
processes = process_names_from_cli_option
|
37
|
+
|
38
|
+
if @cli_options[:clean]
|
39
|
+
FileUtils.rm_f(File.join(@config.pid_root, '*.pid'))
|
40
|
+
FileUtils.rm_f(File.join(@config.pid_root, '*.sock'))
|
41
|
+
puts "Removed all old pid & sock files"
|
24
42
|
end
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
43
|
+
|
44
|
+
if @cli_options[:foreground]
|
45
|
+
File.open(pid_path, 'w') { |f| f.write(::Process.pid) }
|
46
|
+
Supervisor.new(@config).start(:processes => processes)
|
47
|
+
else
|
48
|
+
FileUtils.rm_f(File.join(@config.pid_root, "*.pid"))
|
49
|
+
pid = fork do
|
50
|
+
STDOUT.reopen(log_path, 'a')
|
51
|
+
STDOUT.sync = true
|
52
|
+
STDERR.reopen(log_path, 'a')
|
53
|
+
STDERR.sync = true
|
54
|
+
Supervisor.new(@config).start(:processes => processes)
|
55
|
+
end
|
56
|
+
::Process.detach(pid)
|
57
|
+
File.open(pid_path, 'w') { |f| f.write(pid) }
|
58
|
+
puts "Started #{@config.app_name} supervisor with PID #{pid}"
|
38
59
|
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
60
|
end
|
43
61
|
|
44
62
|
def stop
|
45
63
|
if running?
|
46
|
-
|
47
|
-
|
64
|
+
options = {}
|
65
|
+
instances = ControlClient.run(@config.sock_path, 'stop', :processes => process_names_from_cli_option)
|
66
|
+
if instances.empty?
|
67
|
+
puts "There are no processes to stop."
|
68
|
+
else
|
69
|
+
instances.each do |instance|
|
70
|
+
puts "Stopping #{instance['description']} (PID: #{instance['pid']})"
|
71
|
+
end
|
72
|
+
end
|
48
73
|
else
|
49
74
|
raise Error, "#{@config.app_name} supervisor isn't running"
|
50
75
|
end
|
51
76
|
end
|
52
77
|
|
53
|
-
def
|
78
|
+
def restart
|
54
79
|
if running?
|
55
|
-
|
56
|
-
|
57
|
-
if
|
58
|
-
|
59
|
-
puts "We've asked it to stop. It'll probably be done in a moment."
|
80
|
+
options = {}
|
81
|
+
instances = ControlClient.run(@config.sock_path, 'restart', :processes => process_names_from_cli_option)
|
82
|
+
if instances.empty?
|
83
|
+
puts "There are no processes to restart."
|
60
84
|
else
|
61
|
-
|
85
|
+
instances.each do |instance|
|
86
|
+
puts "Restarting #{instance['description']} (PID: #{instance['pid']})"
|
87
|
+
end
|
62
88
|
end
|
89
|
+
else
|
90
|
+
raise Error, "#{@config.app_name} supervisor isn't running"
|
91
|
+
end
|
92
|
+
end
|
63
93
|
|
94
|
+
def stop_supervisor
|
95
|
+
if running?
|
96
|
+
::Process.kill('TERM', current_pid)
|
97
|
+
puts "Supervisor will be stopped in a moment."
|
64
98
|
else
|
65
99
|
raise Error, "#{@config.app_name} supervisor isn't running"
|
66
100
|
end
|
67
101
|
end
|
68
102
|
|
69
|
-
def
|
103
|
+
def reload_config
|
70
104
|
if running?
|
71
|
-
|
72
|
-
puts "
|
105
|
+
ControlClient.run(@config.sock_path, 'reload_config')
|
106
|
+
puts "Reloading config for #{@config.app_name}"
|
73
107
|
else
|
74
108
|
raise Error, "#{@config.app_name} supervisor isn't running"
|
75
109
|
end
|
@@ -77,9 +111,22 @@ module Procodile
|
|
77
111
|
|
78
112
|
def status
|
79
113
|
if running?
|
80
|
-
|
81
|
-
|
82
|
-
|
114
|
+
stats = ControlClient.run(@config.sock_path, 'status')
|
115
|
+
stats['processes'].each_with_index do |process, index|
|
116
|
+
puts unless index == 0
|
117
|
+
puts "|| ".color(process['log_color']) + process['name'].color(process['log_color'])
|
118
|
+
puts "||".color(process['log_color']) + " Quantity " + process['quantity'].to_s
|
119
|
+
puts "||".color(process['log_color']) + " Command " + process['command']
|
120
|
+
puts "||".color(process['log_color']) + " Respawning " + "#{process['max_respawns']} every #{process['respawn_window']} seconds"
|
121
|
+
puts "||".color(process['log_color']) + " Restart mode " + process['restart_mode']
|
122
|
+
stats['instances'][process['name']].each do |instance|
|
123
|
+
print "|| ".color(process['log_color']) + instance['description'].to_s.ljust(20, ' ').color(process['log_color'])
|
124
|
+
print "pid " + instance['pid'].to_s.ljust(12, ' ')
|
125
|
+
print (instance['running'] ? 'Running' : 'Stopped').to_s.ljust(15, ' ')
|
126
|
+
print instance['respawns'].to_s + " respawns"
|
127
|
+
puts
|
128
|
+
end
|
129
|
+
end
|
83
130
|
else
|
84
131
|
puts "#{@config.app_name} supervisor not running"
|
85
132
|
end
|
@@ -100,6 +147,17 @@ module Procodile
|
|
100
147
|
|
101
148
|
private
|
102
149
|
|
150
|
+
def send_to_socket(command, options = {})
|
151
|
+
|
152
|
+
socket = UNIXSocket.new(@config.sock_path)
|
153
|
+
# Get the connection confirmation
|
154
|
+
connection = socket.gets
|
155
|
+
return false unless connection == 'READY'
|
156
|
+
# Send a command.
|
157
|
+
ensure
|
158
|
+
socket.close rescue nil
|
159
|
+
end
|
160
|
+
|
103
161
|
def running?
|
104
162
|
if pid = current_pid
|
105
163
|
::Process.getpgid(pid) ? true : false
|
@@ -127,5 +185,23 @@ module Procodile
|
|
127
185
|
File.join(@config.log_root, 'supervisor.log')
|
128
186
|
end
|
129
187
|
|
188
|
+
def process_names_from_cli_option
|
189
|
+
if @cli_options[:processes]
|
190
|
+
processes = @cli_options[:processes].split(',')
|
191
|
+
if processes.empty?
|
192
|
+
raise Error, "No process names provided"
|
193
|
+
end
|
194
|
+
processes.each do |process|
|
195
|
+
process_name, _ = process.split('.', 2)
|
196
|
+
unless @config.process_list.keys.include?(process_name.to_s)
|
197
|
+
raise Error, "Process '#{process_name}' is not configured. You may need to reload your config."
|
198
|
+
end
|
199
|
+
end
|
200
|
+
processes
|
201
|
+
else
|
202
|
+
nil
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
130
206
|
end
|
131
207
|
end
|
data/lib/procodile/config.rb
CHANGED
@@ -19,19 +19,46 @@ module Procodile
|
|
19
19
|
FileUtils.mkdir_p(log_root)
|
20
20
|
end
|
21
21
|
|
22
|
+
def reload
|
23
|
+
@process_list = nil
|
24
|
+
@options = nil
|
25
|
+
@process_options = nil
|
26
|
+
|
27
|
+
process_list.each do |name, command|
|
28
|
+
if process = @processes[name]
|
29
|
+
# This command is already in our list. Add it.
|
30
|
+
if process.command != command
|
31
|
+
process.command = command
|
32
|
+
Procodile.log nil, 'system', "#{name} command has changed. Updated."
|
33
|
+
end
|
34
|
+
|
35
|
+
if process_options[name].is_a?(Hash)
|
36
|
+
process.options = process_options[name]
|
37
|
+
else
|
38
|
+
process.options = {}
|
39
|
+
end
|
40
|
+
else
|
41
|
+
Procodile.log nil, 'system', "#{name} has been added to the Procfile. Adding it."
|
42
|
+
@processes[name] = Process.new(self, name, command, process_options[name] || {})
|
43
|
+
@processes[name].log_color = COLORS[@processes.size.divmod(COLORS.size)[1]]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
22
49
|
def app_name
|
23
|
-
options['app_name'] || 'Procodile'
|
50
|
+
@app_name ||= options['app_name'] || 'Procodile'
|
24
51
|
end
|
25
52
|
|
26
53
|
def processes
|
27
|
-
process_list.each_with_index.each_with_object({}) do |((name, command), index), hash|
|
28
|
-
|
29
|
-
hash[name] =
|
54
|
+
@processes ||= process_list.each_with_index.each_with_object({}) do |((name, command), index), hash|
|
55
|
+
hash[name] = Process.new(self, name, command, process_options[name] || {})
|
56
|
+
hash[name].log_color = COLORS[index.divmod(COLORS.size)[1]]
|
30
57
|
end
|
31
58
|
end
|
32
59
|
|
33
60
|
def process_list
|
34
|
-
@
|
61
|
+
@process_list ||= YAML.load_file(procfile_path)
|
35
62
|
end
|
36
63
|
|
37
64
|
def options
|
@@ -43,11 +70,15 @@ module Procodile
|
|
43
70
|
end
|
44
71
|
|
45
72
|
def pid_root
|
46
|
-
File.expand_path(options['pid_root'] || 'pids', @root)
|
73
|
+
@pid_root ||= File.expand_path(options['pid_root'] || 'pids', @root)
|
47
74
|
end
|
48
75
|
|
49
76
|
def log_root
|
50
|
-
File.expand_path(options['log_root'] || 'log', @root)
|
77
|
+
@log_root ||= File.expand_path(options['log_root'] || 'log', @root)
|
78
|
+
end
|
79
|
+
|
80
|
+
def sock_path
|
81
|
+
File.join(pid_root, 'supervisor.sock')
|
51
82
|
end
|
52
83
|
|
53
84
|
private
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module Procodile
|
5
|
+
class ControlClient
|
6
|
+
|
7
|
+
def initialize(sock_path, &block)
|
8
|
+
@socket = UNIXSocket.new(sock_path)
|
9
|
+
if block_given?
|
10
|
+
begin
|
11
|
+
block.call(self)
|
12
|
+
ensure
|
13
|
+
disconnect
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.run(sock_path, command, options = {})
|
19
|
+
socket = self.new(sock_path)
|
20
|
+
socket.run(command, options)
|
21
|
+
ensure
|
22
|
+
socket.disconnect rescue nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def run(command, options = {})
|
26
|
+
@socket.puts("#{command} #{options.to_json}")
|
27
|
+
code, reply = @socket.gets.strip.split(/\s+/, 2)
|
28
|
+
if code.to_i == 200
|
29
|
+
if reply && reply.length > 0
|
30
|
+
JSON.parse(reply)
|
31
|
+
else
|
32
|
+
true
|
33
|
+
end
|
34
|
+
else
|
35
|
+
raise Error, "Error from control server: #{code} (#{reply.inspect})"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def disconnect
|
40
|
+
@socket.close rescue nil
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def parse_response(data)
|
46
|
+
code, message = data.split(/\s+/, 2)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'procodile/control_session'
|
3
|
+
|
4
|
+
module Procodile
|
5
|
+
class ControlServer
|
6
|
+
|
7
|
+
def initialize(supervisor)
|
8
|
+
@supervisor = supervisor
|
9
|
+
end
|
10
|
+
|
11
|
+
def listen
|
12
|
+
socket = UNIXServer.new(@supervisor.config.sock_path)
|
13
|
+
Procodile.log nil, 'control', "Listening at #{@supervisor.config.sock_path}"
|
14
|
+
loop do
|
15
|
+
client = socket.accept
|
16
|
+
session = ControlSession.new(@supervisor, client)
|
17
|
+
while line = client.gets
|
18
|
+
if response = session.receive_data(line.strip)
|
19
|
+
client.puts response
|
20
|
+
end
|
21
|
+
end
|
22
|
+
client.close
|
23
|
+
end
|
24
|
+
ensure
|
25
|
+
FileUtils.rm_f(@supervisor.config.sock_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Procodile
|
4
|
+
class ControlSession
|
5
|
+
|
6
|
+
def initialize(supervisor, client)
|
7
|
+
@supervisor = supervisor
|
8
|
+
@client = client
|
9
|
+
end
|
10
|
+
|
11
|
+
def receive_data(data)
|
12
|
+
command, options = data.split(/\s+/, 2)
|
13
|
+
options = JSON.parse(options)
|
14
|
+
if self.class.instance_methods(false).include?(command.to_sym) && command != 'receive_data'
|
15
|
+
begin
|
16
|
+
Procodile.log nil, 'control', "Received #{command} command"
|
17
|
+
public_send(command, options)
|
18
|
+
rescue Procodile::Error => e
|
19
|
+
Procodile.log nil, 'control', "\e[31mError: #{e.message}\e[0m"
|
20
|
+
"500 #{e.message}"
|
21
|
+
end
|
22
|
+
else
|
23
|
+
"404 Invaid command"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def start_processes(options)
|
28
|
+
instances = @supervisor.start_processes(options['processes'])
|
29
|
+
"200 " + instances.map(&:to_hash).to_json
|
30
|
+
end
|
31
|
+
|
32
|
+
def stop(options)
|
33
|
+
instances = @supervisor.stop(:processes => options['processes'])
|
34
|
+
"200 " + instances.map(&:to_hash).to_json
|
35
|
+
end
|
36
|
+
|
37
|
+
def restart(options)
|
38
|
+
instances = @supervisor.restart(:processes => options['processes'])
|
39
|
+
"200 " + instances.map(&:to_hash).to_json
|
40
|
+
end
|
41
|
+
|
42
|
+
def reload_config(options)
|
43
|
+
@supervisor.reload_config
|
44
|
+
"200"
|
45
|
+
end
|
46
|
+
|
47
|
+
def status(options)
|
48
|
+
instances = {}
|
49
|
+
@supervisor.processes.each do |process, process_instances|
|
50
|
+
instances[process.name] = []
|
51
|
+
for instance in process_instances
|
52
|
+
instances[process.name] << {
|
53
|
+
:description => instance.description,
|
54
|
+
:pid => instance.pid,
|
55
|
+
:running => instance.running?,
|
56
|
+
:respawns => instance.respawns,
|
57
|
+
:command => instance.process.command
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
processes = @supervisor.processes.keys.map(&:to_hash)
|
63
|
+
result = {:instances => instances, :processes => processes}
|
64
|
+
"200 #{result.to_json}"
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
data/lib/procodile/instance.rb
CHANGED
@@ -4,6 +4,8 @@ module Procodile
|
|
4
4
|
class Instance
|
5
5
|
|
6
6
|
attr_accessor :pid
|
7
|
+
attr_reader :id
|
8
|
+
attr_accessor :process
|
7
9
|
|
8
10
|
def initialize(process, id)
|
9
11
|
@process = process
|
@@ -11,22 +13,37 @@ module Procodile
|
|
11
13
|
@respawns = 0
|
12
14
|
end
|
13
15
|
|
16
|
+
#
|
17
|
+
# Return a description for this instance
|
18
|
+
#
|
14
19
|
def description
|
15
20
|
"#{@process.name}.#{@id}"
|
16
21
|
end
|
17
22
|
|
18
|
-
|
19
|
-
|
23
|
+
#
|
24
|
+
# Should this instance still be monitored by the supervisor?
|
25
|
+
#
|
26
|
+
def unmonitored?
|
27
|
+
@monitored == false
|
20
28
|
end
|
21
29
|
|
30
|
+
#
|
31
|
+
# Return the path to this instance's PID file
|
32
|
+
#
|
22
33
|
def pid_file_path
|
23
34
|
File.join(@process.config.pid_root, "#{description}.pid")
|
24
35
|
end
|
25
36
|
|
37
|
+
#
|
38
|
+
# Return the path to this instance's log file
|
39
|
+
#
|
26
40
|
def log_file_path
|
27
41
|
File.join(@process.config.log_root, "#{description}.log")
|
28
42
|
end
|
29
43
|
|
44
|
+
#
|
45
|
+
# Return the PID that is in the instances process PID file
|
46
|
+
#
|
30
47
|
def pid_from_file
|
31
48
|
if File.exist?(pid_file_path)
|
32
49
|
pid = File.read(pid_file_path)
|
@@ -36,6 +53,9 @@ module Procodile
|
|
36
53
|
end
|
37
54
|
end
|
38
55
|
|
56
|
+
#
|
57
|
+
# Is this process running? Pass an option to check the given PID instead of the instance
|
58
|
+
#
|
39
59
|
def running?(force_pid = nil)
|
40
60
|
if force_pid || @pid
|
41
61
|
::Process.getpgid(force_pid || @pid) ? true : false
|
@@ -46,11 +66,21 @@ module Procodile
|
|
46
66
|
false
|
47
67
|
end
|
48
68
|
|
69
|
+
#
|
70
|
+
# Start a new instance of this process
|
71
|
+
#
|
49
72
|
def start
|
50
73
|
@stopping = false
|
51
|
-
|
74
|
+
existing_pid = self.pid_from_file
|
75
|
+
if running?(existing_pid)
|
76
|
+
# If the PID in the file is already running, we should just just continue
|
77
|
+
# to monitor this process rather than spawning a new one.
|
78
|
+
@pid = existing_pid
|
79
|
+
Procodile.log(@process.log_color, description, "Already running with PID #{@pid}")
|
80
|
+
else
|
52
81
|
log_file = File.open(self.log_file_path, 'a')
|
53
|
-
|
82
|
+
Dir.chdir(@process.config.root)
|
83
|
+
@pid = ::Process.spawn({'PID_FILE' => pid_file_path}, @process.command, :out => log_file, :err => log_file, :pgroup => true)
|
54
84
|
Procodile.log(@process.log_color, description, "Started with PID #{@pid}")
|
55
85
|
File.open(pid_file_path, 'w') { |f| f.write(@pid.to_s + "\n") }
|
56
86
|
::Process.detach(@pid)
|
@@ -72,8 +102,7 @@ module Procodile
|
|
72
102
|
@stopping = true
|
73
103
|
update_pid
|
74
104
|
if self.running?
|
75
|
-
|
76
|
-
Procodile.log(@process.log_color, description, "Sending TERM to #{pid}")
|
105
|
+
Procodile.log(@process.log_color, description, "Sending TERM to #{@pid}")
|
77
106
|
::Process.kill('TERM', pid)
|
78
107
|
else
|
79
108
|
Procodile.log(@process.log_color, description, "Process already stopped")
|
@@ -85,6 +114,14 @@ module Procodile
|
|
85
114
|
# started again
|
86
115
|
#
|
87
116
|
def on_stop
|
117
|
+
tidy
|
118
|
+
unmonitor
|
119
|
+
end
|
120
|
+
|
121
|
+
#
|
122
|
+
# Tidy up when this process isn't needed any more
|
123
|
+
#
|
124
|
+
def tidy
|
88
125
|
FileUtils.rm_f(self.pid_file_path)
|
89
126
|
Procodile.log(@process.log_color, description, "Removed PID file")
|
90
127
|
end
|
@@ -157,7 +194,8 @@ module Procodile
|
|
157
194
|
elsif respawns >= @process.max_respawns
|
158
195
|
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
196
|
Procodile.log(@process.log_color, description, "\e[31mIt will not be respawned automatically any longer and will no longer be managed.\e[0m")
|
160
|
-
|
197
|
+
tidy
|
198
|
+
unmonitor
|
161
199
|
end
|
162
200
|
end
|
163
201
|
end
|
@@ -165,9 +203,8 @@ module Procodile
|
|
165
203
|
#
|
166
204
|
# Mark this process as dead and tidy up after it
|
167
205
|
#
|
168
|
-
def
|
169
|
-
|
170
|
-
@dead = true
|
206
|
+
def unmonitor
|
207
|
+
@monitored = false
|
171
208
|
end
|
172
209
|
|
173
210
|
#
|
@@ -200,5 +237,17 @@ module Procodile
|
|
200
237
|
end
|
201
238
|
end
|
202
239
|
|
240
|
+
#
|
241
|
+
# Return this instance as a hash
|
242
|
+
#
|
243
|
+
def to_hash
|
244
|
+
{
|
245
|
+
:description => self.description,
|
246
|
+
:pid => self.pid,
|
247
|
+
:respawns => self.respawns,
|
248
|
+
:running => self.running?
|
249
|
+
}
|
250
|
+
end
|
251
|
+
|
203
252
|
end
|
204
253
|
end
|
data/lib/procodile/process.rb
CHANGED
@@ -3,22 +3,18 @@ require 'procodile/instance'
|
|
3
3
|
module Procodile
|
4
4
|
class Process
|
5
5
|
|
6
|
-
attr_reader :name
|
7
|
-
attr_reader :command
|
8
6
|
attr_reader :config
|
7
|
+
attr_reader :name
|
8
|
+
attr_accessor :command
|
9
|
+
attr_accessor :options
|
10
|
+
attr_accessor :log_color
|
9
11
|
|
10
12
|
def initialize(config, name, command, options = {})
|
11
13
|
@config = config
|
12
14
|
@name = name
|
13
15
|
@command = command
|
14
16
|
@options = options
|
15
|
-
|
16
|
-
|
17
|
-
#
|
18
|
-
# Return the color for this process
|
19
|
-
#
|
20
|
-
def log_color
|
21
|
-
@options['log_color'] || 0
|
17
|
+
@log_color = 0
|
22
18
|
end
|
23
19
|
|
24
20
|
#
|
@@ -57,8 +53,23 @@ module Procodile
|
|
57
53
|
#
|
58
54
|
# Generate an array of new instances for this process (based on its quantity)
|
59
55
|
#
|
60
|
-
def generate_instances
|
61
|
-
quantity.times.map { |i| Instance.new(self, i +
|
56
|
+
def generate_instances(quantity = self.quantity, start_number = 1)
|
57
|
+
quantity.times.map { |i| Instance.new(self, i + start_number) }
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Return a hash
|
62
|
+
#
|
63
|
+
def to_hash
|
64
|
+
{
|
65
|
+
:name => self.name,
|
66
|
+
:log_color => self.log_color,
|
67
|
+
:quantity => self.quantity,
|
68
|
+
:max_respawns => self.max_respawns,
|
69
|
+
:respawn_window => self.respawn_window,
|
70
|
+
:command => self.command,
|
71
|
+
:restart_mode => self.restart_mode
|
72
|
+
}
|
62
73
|
end
|
63
74
|
|
64
75
|
end
|
data/lib/procodile/supervisor.rb
CHANGED
@@ -1,93 +1,186 @@
|
|
1
|
+
require 'procodile/control_server'
|
2
|
+
|
1
3
|
module Procodile
|
2
4
|
class Supervisor
|
3
5
|
|
6
|
+
attr_reader :config
|
7
|
+
attr_reader :processes
|
8
|
+
|
4
9
|
# Create a new supervisor instance that will be monitoring the
|
5
10
|
# processes that have been provided.
|
6
11
|
def initialize(config)
|
7
12
|
@config = config
|
8
|
-
@
|
13
|
+
@processes = {}
|
14
|
+
signal_handler = SignalHandler.new('TERM', 'USR1', 'USR2', 'INT', 'HUP')
|
15
|
+
signal_handler.register('TERM') { stop_supervisor }
|
16
|
+
signal_handler.register('INT') { stop }
|
17
|
+
signal_handler.register('USR1') { restart }
|
18
|
+
signal_handler.register('USR2') { status }
|
19
|
+
signal_handler.register('HUP') { reload_config }
|
9
20
|
end
|
10
21
|
|
11
|
-
def start
|
22
|
+
def start(options = {})
|
12
23
|
Procodile.log nil, "system", "#{@config.app_name} supervisor started with PID #{::Process.pid}"
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
@instances << instance
|
17
|
-
end
|
24
|
+
Thread.new do
|
25
|
+
socket = ControlServer.new(self)
|
26
|
+
socket.listen
|
18
27
|
end
|
28
|
+
start_processes(options[:processes])
|
19
29
|
supervise
|
20
30
|
end
|
21
31
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
FileUtils.rm_f(File.join(@config.pid_root, 'supervisor.pid'))
|
32
|
-
::Process.exit 0
|
32
|
+
def start_processes(types = [])
|
33
|
+
Array.new.tap do |instances_started|
|
34
|
+
@config.processes.each do |name, process|
|
35
|
+
next if types && !types.include?(name.to_s) # Not a process we want
|
36
|
+
next if @processes.keys.include?(process) # Process type already running
|
37
|
+
instances = start_instances(process.generate_instances)
|
38
|
+
instances_started.push(*instances)
|
39
|
+
end
|
40
|
+
end
|
33
41
|
end
|
34
42
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
43
|
+
def stop(options = {})
|
44
|
+
Array.new.tap do |instances_stopped|
|
45
|
+
if options[:processes].nil?
|
46
|
+
return if @stopping
|
47
|
+
@stopping = true
|
48
|
+
Procodile.log nil, "system", "Stopping all #{@config.app_name} processes"
|
49
|
+
@processes.each do |_, instances|
|
50
|
+
instances.each do |instance|
|
51
|
+
instance.stop
|
52
|
+
instances_stopped << instance
|
53
|
+
end
|
54
|
+
end
|
55
|
+
else
|
56
|
+
instances = process_names_to_instances(options[:processes])
|
57
|
+
Procodile.log nil, "system", "Stopping #{instances.size} process(es)"
|
58
|
+
instances.each do |instance|
|
59
|
+
instance.stop
|
60
|
+
instances_stopped << instance
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
38
64
|
end
|
39
65
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
if
|
44
|
-
Procodile.log
|
66
|
+
def restart(options = {})
|
67
|
+
@config.reload
|
68
|
+
Array.new.tap do |instances_restarted|
|
69
|
+
if options[:processes].nil?
|
70
|
+
Procodile.log nil, "system", "Restarting all #{@config.app_name} processes"
|
71
|
+
@processes.each do |_, instances|
|
72
|
+
instances.each do |instance|
|
73
|
+
instance.restart
|
74
|
+
instances_restarted << instance
|
75
|
+
end
|
76
|
+
end
|
45
77
|
else
|
46
|
-
|
78
|
+
instances = process_names_to_instances(options[:processes])
|
79
|
+
Procodile.log nil, "system", "Restarting #{instances.size} process(es)"
|
80
|
+
instances.each do |instance|
|
81
|
+
instance.restart
|
82
|
+
instances_restarted << instance
|
83
|
+
end
|
47
84
|
end
|
48
85
|
end
|
49
86
|
end
|
50
87
|
|
88
|
+
def stop_supervisor
|
89
|
+
Procodile.log nil, 'system', "Stopping #{@config.app_name} supervisor"
|
90
|
+
FileUtils.rm_f(File.join(@config.pid_root, 'supervisor.pid'))
|
91
|
+
::Process.exit 0
|
92
|
+
end
|
93
|
+
|
51
94
|
def supervise
|
52
95
|
loop do
|
53
96
|
# Tidy up any instances that we no longer wish to be managed. They will
|
54
97
|
# be removed from the list.
|
55
98
|
remove_dead_instances
|
56
99
|
|
57
|
-
|
58
|
-
|
59
|
-
# stopped and trigger their on_stop callback.
|
60
|
-
remove_stopped_instances
|
100
|
+
# Remove processes that have been stopped
|
101
|
+
remove_stopped_instances
|
61
102
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
@instances.each(&:check)
|
103
|
+
# Check all instances that we manage and let them do their things.
|
104
|
+
@processes.each do |_, instances|
|
105
|
+
instances.each(&:check)
|
106
|
+
end
|
107
|
+
|
108
|
+
# If the processes go away, we can stop the supervisor now
|
109
|
+
if @processes.size == 0
|
110
|
+
Procodile.log nil, "system", "All processes have stopped"
|
111
|
+
stop_supervisor
|
72
112
|
end
|
73
113
|
|
74
114
|
sleep 5
|
75
115
|
end
|
76
116
|
end
|
77
117
|
|
118
|
+
def reload_config
|
119
|
+
Procodile.log nil, "system", "Reloading configuration"
|
120
|
+
@config.reload
|
121
|
+
check_instance_quantities
|
122
|
+
end
|
123
|
+
|
78
124
|
private
|
79
125
|
|
126
|
+
def check_instance_quantities
|
127
|
+
@processes.each do |process, instances|
|
128
|
+
if instances.size > process.quantity
|
129
|
+
quantity_to_stop = instances.size - process.quantity
|
130
|
+
Procodile.log nil, "system", "Stopping #{quantity_to_stop} #{process.name} process(es)"
|
131
|
+
instances.last(quantity_to_stop).each(&:stop)
|
132
|
+
elsif instances.size < process.quantity
|
133
|
+
quantity_needed = process.quantity - instances.size
|
134
|
+
start_id = instances.last ? instances.last.id + 1 : 1
|
135
|
+
Procodile.log nil, "system", "Starting #{quantity_needed} more #{process.name} process(es) (start with #{start_id})"
|
136
|
+
start_instances(process.generate_instances(quantity_needed, start_id))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def start_instances(instances)
|
142
|
+
instances.each do |instance|
|
143
|
+
instance.start
|
144
|
+
@processes[instance.process] ||= []
|
145
|
+
@processes[instance.process] << instance
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
80
149
|
def remove_dead_instances
|
81
|
-
@instances
|
150
|
+
@processes.each do |_, instances|
|
151
|
+
instances.reject!(&:unmonitored?)
|
152
|
+
end.reject! { |_, instances| instances.empty? }
|
82
153
|
end
|
83
154
|
|
84
155
|
def remove_stopped_instances
|
85
|
-
@
|
86
|
-
|
87
|
-
|
156
|
+
@processes.each do |_, instances|
|
157
|
+
instances.reject! do |instance|
|
158
|
+
if !instance.running? && instance.stopping?
|
159
|
+
instance.on_stop
|
160
|
+
true
|
161
|
+
else
|
162
|
+
false
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end.reject! { |_, instances| instances.empty? }
|
166
|
+
end
|
167
|
+
|
168
|
+
def process_names_to_instances(names)
|
169
|
+
names.each_with_object([]) do |name, array|
|
170
|
+
if name =~ /\A(.*)\.(\d+)\z/
|
171
|
+
process_name, id = $1, $2
|
172
|
+
@processes.each do |process, instances|
|
173
|
+
next unless process.name == process_name
|
174
|
+
instances.each do |instance|
|
175
|
+
next unless instance.id == id.to_i
|
176
|
+
array << instance
|
177
|
+
end
|
178
|
+
end
|
88
179
|
else
|
89
|
-
|
90
|
-
|
180
|
+
@processes.each do |process, instances|
|
181
|
+
next unless process.name == name
|
182
|
+
instances.each { |i| array << i}
|
183
|
+
end
|
91
184
|
end
|
92
185
|
end
|
93
186
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: procodile
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adam Cooke
|
@@ -9,7 +9,21 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
date: 2016-10-16 00:00:00.000000000 Z
|
12
|
-
dependencies:
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
description: Run Ruby/Rails processes in the background on Linux servers with ease.
|
14
28
|
email:
|
15
29
|
- me@adamcooke.io
|
@@ -22,6 +36,9 @@ files:
|
|
22
36
|
- lib/procodile.rb
|
23
37
|
- lib/procodile/cli.rb
|
24
38
|
- lib/procodile/config.rb
|
39
|
+
- lib/procodile/control_client.rb
|
40
|
+
- lib/procodile/control_server.rb
|
41
|
+
- lib/procodile/control_session.rb
|
25
42
|
- lib/procodile/error.rb
|
26
43
|
- lib/procodile/instance.rb
|
27
44
|
- lib/procodile/logger.rb
|