procodile 0.0.2 → 1.0.0
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 +19 -10
- data/lib/procodile/cli.rb +8 -9
- data/lib/procodile/config.rb +2 -4
- data/lib/procodile/control_session.rb +1 -1
- data/lib/procodile/instance.rb +32 -18
- data/lib/procodile/logger.rb +1 -3
- data/lib/procodile/process.rb +10 -1
- data/lib/procodile/signal_handler.rb +23 -9
- data/lib/procodile/supervisor.rb +64 -28
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b644b27235abc8fcae9bfd9d7ca9bbd87cd1c0d8
|
4
|
+
data.tar.gz: 82428a6110171a2b783f0a249b8bf6661e02c6e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b4cbe1b3445fbb4fab8afd3cab8e1a90aa03b0e4c24de0daddd9823cdc756831ee096647d5a424e363e36808fb43709c73d5be763b09de5d6f805d7869275e26
|
7
|
+
data.tar.gz: 6363da66bd85396ffbd5bde9ad4ff64464535fd73f1f5b9c29b1a3d32b42ed4d2bf2f0343743d005438db85c730081c0a2827a9b5cc8a216ab9dd5d57a4880a6
|
data/bin/procodile
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'optparse'
|
3
|
+
require 'fileutils'
|
4
|
+
$:.unshift(File.expand_path('../../lib', __FILE__))
|
5
|
+
require 'procodile'
|
3
6
|
|
4
7
|
command = ARGV[0]
|
5
8
|
|
@@ -11,6 +14,12 @@ begin
|
|
11
14
|
options[:root] = root
|
12
15
|
end
|
13
16
|
|
17
|
+
if ['start', 'stop', 'restart'].include?(command)
|
18
|
+
opts.on("-p", "--processes a,b,c", "Only #{command} the listed processes or process types") do |processes|
|
19
|
+
options[:processes] = processes
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
14
23
|
if command == 'start'
|
15
24
|
opts.on("-f", "--foreground", "Run the supervisor in the foreground") do
|
16
25
|
options[:foreground] = true
|
@@ -19,23 +28,23 @@ begin
|
|
19
28
|
opts.on("--clean", "Remove all previous pid and sock files before starting") do
|
20
29
|
options[:clean] = true
|
21
30
|
end
|
22
|
-
end
|
23
31
|
|
24
|
-
|
25
|
-
|
26
|
-
|
32
|
+
opts.on("-b", "--brittle", "Kill everything when one process exits") do
|
33
|
+
options[:brittle] = true
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on("-d", "--dev", "Run in development mode") do
|
37
|
+
options[:development] = true
|
38
|
+
options[:brittle] = true
|
39
|
+
options[:foreground] = true
|
27
40
|
end
|
28
41
|
end
|
29
42
|
end.parse!
|
30
43
|
rescue OptionParser::InvalidOption => e
|
31
|
-
$stderr.puts "
|
44
|
+
$stderr.puts "Error: #{e.message}".color(31)
|
32
45
|
exit 1
|
33
46
|
end
|
34
47
|
|
35
|
-
$:.unshift(File.expand_path('../../lib', __FILE__))
|
36
|
-
|
37
|
-
require 'fileutils'
|
38
|
-
require 'procodile'
|
39
48
|
require 'procodile/error'
|
40
49
|
require 'procodile/config'
|
41
50
|
require 'procodile/cli'
|
@@ -46,6 +55,6 @@ begin
|
|
46
55
|
cli = Procodile::CLI.new(config, options)
|
47
56
|
cli.run(command)
|
48
57
|
rescue Procodile::Error => e
|
49
|
-
$stderr.puts "
|
58
|
+
$stderr.puts "Error: #{e.message}".color(31)
|
50
59
|
exit 1
|
51
60
|
end
|
data/lib/procodile/cli.rb
CHANGED
@@ -33,6 +33,9 @@ module Procodile
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
+
run_options = {}
|
37
|
+
run_options[:brittle] = @cli_options[:brittle]
|
38
|
+
|
36
39
|
processes = process_names_from_cli_option
|
37
40
|
|
38
41
|
if @cli_options[:clean]
|
@@ -43,15 +46,15 @@ module Procodile
|
|
43
46
|
|
44
47
|
if @cli_options[:foreground]
|
45
48
|
File.open(pid_path, 'w') { |f| f.write(::Process.pid) }
|
46
|
-
Supervisor.new(@config).start(:processes => processes)
|
49
|
+
Supervisor.new(@config, run_options).start(:processes => processes)
|
47
50
|
else
|
48
51
|
FileUtils.rm_f(File.join(@config.pid_root, "*.pid"))
|
49
52
|
pid = fork do
|
50
|
-
STDOUT.reopen(log_path, 'a')
|
53
|
+
STDOUT.reopen(@config.log_path, 'a')
|
51
54
|
STDOUT.sync = true
|
52
|
-
STDERR.reopen(log_path, 'a')
|
55
|
+
STDERR.reopen(@config.log_path, 'a')
|
53
56
|
STDERR.sync = true
|
54
|
-
Supervisor.new(@config).start(:processes => processes)
|
57
|
+
Supervisor.new(@config, run_options).start(:processes => processes)
|
55
58
|
end
|
56
59
|
::Process.detach(pid)
|
57
60
|
File.open(pid_path, 'w') { |f| f.write(pid) }
|
@@ -178,11 +181,7 @@ module Procodile
|
|
178
181
|
end
|
179
182
|
|
180
183
|
def pid_path
|
181
|
-
File.join(@config.pid_root, '
|
182
|
-
end
|
183
|
-
|
184
|
-
def log_path
|
185
|
-
File.join(@config.log_root, 'supervisor.log')
|
184
|
+
File.join(@config.pid_root, 'procodile.pid')
|
186
185
|
end
|
187
186
|
|
188
187
|
def process_names_from_cli_option
|
data/lib/procodile/config.rb
CHANGED
@@ -14,9 +14,7 @@ module Procodile
|
|
14
14
|
unless File.exist?(procfile_path)
|
15
15
|
raise Error, "Procfile not found at #{procfile_path}"
|
16
16
|
end
|
17
|
-
|
18
17
|
FileUtils.mkdir_p(pid_root)
|
19
|
-
FileUtils.mkdir_p(log_root)
|
20
18
|
end
|
21
19
|
|
22
20
|
def reload
|
@@ -73,8 +71,8 @@ module Procodile
|
|
73
71
|
@pid_root ||= File.expand_path(options['pid_root'] || 'pids', @root)
|
74
72
|
end
|
75
73
|
|
76
|
-
def
|
77
|
-
@
|
74
|
+
def log_path
|
75
|
+
@log_path ||= File.expand_path(options['log_path'] || 'procodile.log', @root)
|
78
76
|
end
|
79
77
|
|
80
78
|
def sock_path
|
@@ -16,7 +16,7 @@ module Procodile
|
|
16
16
|
Procodile.log nil, 'control', "Received #{command} command"
|
17
17
|
public_send(command, options)
|
18
18
|
rescue Procodile::Error => e
|
19
|
-
Procodile.log nil, 'control', "
|
19
|
+
Procodile.log nil, 'control', "Error: #{e.message}".color(31)
|
20
20
|
"500 #{e.message}"
|
21
21
|
end
|
22
22
|
else
|
data/lib/procodile/instance.rb
CHANGED
@@ -6,11 +6,13 @@ module Procodile
|
|
6
6
|
attr_accessor :pid
|
7
7
|
attr_reader :id
|
8
8
|
attr_accessor :process
|
9
|
+
attr_accessor :respawnable
|
9
10
|
|
10
11
|
def initialize(process, id)
|
11
12
|
@process = process
|
12
13
|
@id = id
|
13
14
|
@respawns = 0
|
15
|
+
@respawnable = true
|
14
16
|
end
|
15
17
|
|
16
18
|
#
|
@@ -34,13 +36,6 @@ module Procodile
|
|
34
36
|
File.join(@process.config.pid_root, "#{description}.pid")
|
35
37
|
end
|
36
38
|
|
37
|
-
#
|
38
|
-
# Return the path to this instance's log file
|
39
|
-
#
|
40
|
-
def log_file_path
|
41
|
-
File.join(@process.config.log_root, "#{description}.log")
|
42
|
-
end
|
43
|
-
|
44
39
|
#
|
45
40
|
# Return the PID that is in the instances process PID file
|
46
41
|
#
|
@@ -77,13 +72,24 @@ module Procodile
|
|
77
72
|
# to monitor this process rather than spawning a new one.
|
78
73
|
@pid = existing_pid
|
79
74
|
Procodile.log(@process.log_color, description, "Already running with PID #{@pid}")
|
75
|
+
nil
|
80
76
|
else
|
81
|
-
|
77
|
+
if self.process.log_path
|
78
|
+
log_destination = File.open(self.process.log_path, 'a')
|
79
|
+
return_value = nil
|
80
|
+
else
|
81
|
+
reader, writer = IO.pipe
|
82
|
+
log_destination = writer
|
83
|
+
return_value = reader
|
84
|
+
end
|
85
|
+
|
82
86
|
Dir.chdir(@process.config.root)
|
83
|
-
@pid = ::Process.spawn({'PID_FILE' => pid_file_path}, @process.command, :out =>
|
87
|
+
@pid = ::Process.spawn({'PID_FILE' => pid_file_path}, @process.command, :out => log_destination, :err => log_destination, :pgroup => true)
|
84
88
|
Procodile.log(@process.log_color, description, "Started with PID #{@pid}")
|
85
89
|
File.open(pid_file_path, 'w') { |f| f.write(@pid.to_s + "\n") }
|
86
90
|
::Process.detach(@pid)
|
91
|
+
|
92
|
+
return_value
|
87
93
|
end
|
88
94
|
end
|
89
95
|
|
@@ -164,7 +170,7 @@ module Procodile
|
|
164
170
|
#
|
165
171
|
def update_pid
|
166
172
|
pid_from_file = self.pid_from_file
|
167
|
-
if pid_from_file != @pid
|
173
|
+
if pid_from_file && pid_from_file != @pid
|
168
174
|
@pid = pid_from_file
|
169
175
|
Procodile.log(@process.log_color, description, "PID file changed. Updated pid to #{@pid}")
|
170
176
|
true
|
@@ -174,26 +180,34 @@ module Procodile
|
|
174
180
|
end
|
175
181
|
|
176
182
|
#
|
177
|
-
# Check the status of this process and handle as appropriate
|
183
|
+
# Check the status of this process and handle as appropriate.
|
178
184
|
#
|
179
185
|
def check
|
180
186
|
# Don't do any checking if we're in the midst of a restart
|
181
187
|
return if @restarting
|
188
|
+
return if unmonitored?
|
182
189
|
|
183
190
|
if self.running?
|
184
191
|
# Everything is OK. The process is running.
|
192
|
+
true
|
185
193
|
else
|
186
194
|
# If the process isn't running any more, update the PID in our memory from
|
187
195
|
# the file in case the process has changed itself.
|
188
196
|
return check if update_pid
|
189
197
|
|
190
|
-
if
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
198
|
+
if @respawnable
|
199
|
+
if can_respawn?
|
200
|
+
Procodile.log(@process.log_color, description, "Process has stopped. Respawning...")
|
201
|
+
start
|
202
|
+
add_respawn
|
203
|
+
elsif respawns >= @process.max_respawns
|
204
|
+
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")
|
205
|
+
Procodile.log(@process.log_color, description, "It will not be respawned automatically any longer and will no longer be managed.").color(31)
|
206
|
+
tidy
|
207
|
+
unmonitor
|
208
|
+
end
|
209
|
+
else
|
210
|
+
Procodile.log(@process.log_color, description, "Process has stopped. Respawning not available.")
|
197
211
|
tidy
|
198
212
|
unmonitor
|
199
213
|
end
|
data/lib/procodile/logger.rb
CHANGED
@@ -9,9 +9,7 @@ module Procodile
|
|
9
9
|
mutex.synchronize do
|
10
10
|
text.to_s.lines.map(&:chomp).each do |message|
|
11
11
|
output = ""
|
12
|
-
output += "
|
13
|
-
output += "#{Time.now.strftime("%H:%M:%S")} #{name.ljust(15, ' ')} |"
|
14
|
-
output += "\e[0m "
|
12
|
+
output += "#{Time.now.strftime("%H:%M:%S")} #{name.ljust(15, ' ')} | ".color(color)
|
15
13
|
output += message
|
16
14
|
$stdout.puts output
|
17
15
|
$stdout.flush
|
data/lib/procodile/process.rb
CHANGED
@@ -38,6 +38,14 @@ module Procodile
|
|
38
38
|
@options['respawn_window'] ? @options['respawn_window'].to_i : 3600
|
39
39
|
end
|
40
40
|
|
41
|
+
#
|
42
|
+
# Return the path where log output for this process should be written to. If
|
43
|
+
# none, output will be written to the supervisor log.
|
44
|
+
#
|
45
|
+
def log_path
|
46
|
+
@options['log_path'] ? File.expand_path(@options['log_path'], @config.root) : nil
|
47
|
+
end
|
48
|
+
|
41
49
|
#
|
42
50
|
# Defines how this process should be restarted
|
43
51
|
#
|
@@ -68,7 +76,8 @@ module Procodile
|
|
68
76
|
:max_respawns => self.max_respawns,
|
69
77
|
:respawn_window => self.respawn_window,
|
70
78
|
:command => self.command,
|
71
|
-
:restart_mode => self.restart_mode
|
79
|
+
:restart_mode => self.restart_mode,
|
80
|
+
:log_path => self.log_path
|
72
81
|
}
|
73
82
|
end
|
74
83
|
|
@@ -1,26 +1,28 @@
|
|
1
1
|
module Procodile
|
2
2
|
class SignalHandler
|
3
3
|
|
4
|
+
attr_reader :pipe
|
5
|
+
|
4
6
|
def self.queue
|
5
7
|
Thread.main[:signal_queue] ||= []
|
6
8
|
end
|
7
9
|
|
8
10
|
def initialize(*signals)
|
9
11
|
@handlers = {}
|
12
|
+
reader, writer = IO.pipe
|
13
|
+
@pipe = {:reader => reader, :writer => writer}
|
14
|
+
signals.each do |sig|
|
15
|
+
Signal.trap(sig, proc { SignalHandler.queue << sig ; notice })
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def start
|
10
20
|
Thread.new do
|
11
21
|
loop do
|
12
|
-
|
13
|
-
if @handlers[signal]
|
14
|
-
@handlers[signal].each(&:call)
|
15
|
-
end
|
16
|
-
end
|
22
|
+
handle
|
17
23
|
sleep 1
|
18
24
|
end
|
19
25
|
end
|
20
|
-
|
21
|
-
signals.each do |sig|
|
22
|
-
Signal.trap(sig, proc { SignalHandler.queue << sig })
|
23
|
-
end
|
24
26
|
end
|
25
27
|
|
26
28
|
def register(name, &block)
|
@@ -28,5 +30,17 @@ module Procodile
|
|
28
30
|
@handlers[name] << block
|
29
31
|
end
|
30
32
|
|
33
|
+
def notice
|
34
|
+
@pipe[:writer].write_nonblock('.')
|
35
|
+
end
|
36
|
+
|
37
|
+
def handle
|
38
|
+
if signal = self.class.queue.shift
|
39
|
+
if @handlers[signal]
|
40
|
+
@handlers[signal].each(&:call)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
31
45
|
end
|
32
46
|
end
|
data/lib/procodile/supervisor.rb
CHANGED
@@ -6,27 +6,31 @@ module Procodile
|
|
6
6
|
attr_reader :config
|
7
7
|
attr_reader :processes
|
8
8
|
|
9
|
-
|
10
|
-
# processes that have been provided.
|
11
|
-
def initialize(config)
|
9
|
+
def initialize(config, run_options = {})
|
12
10
|
@config = config
|
11
|
+
@run_options = run_options
|
13
12
|
@processes = {}
|
14
|
-
|
15
|
-
signal_handler.
|
16
|
-
signal_handler.register('
|
17
|
-
signal_handler.register('
|
18
|
-
signal_handler.register('
|
19
|
-
signal_handler.register('
|
13
|
+
@readers = {}
|
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 }
|
20
20
|
end
|
21
21
|
|
22
22
|
def start(options = {})
|
23
23
|
Procodile.log nil, "system", "#{@config.app_name} supervisor started with PID #{::Process.pid}"
|
24
|
+
if @run_options[:brittle]
|
25
|
+
Procodile.log nil, "system", "Running in brittle mode"
|
26
|
+
end
|
24
27
|
Thread.new do
|
25
28
|
socket = ControlServer.new(self)
|
26
29
|
socket.listen
|
27
30
|
end
|
28
31
|
start_processes(options[:processes])
|
29
|
-
|
32
|
+
watch_for_output
|
33
|
+
loop { supervise; sleep 3 }
|
30
34
|
end
|
31
35
|
|
32
36
|
def start_processes(types = [])
|
@@ -92,26 +96,30 @@ module Procodile
|
|
92
96
|
end
|
93
97
|
|
94
98
|
def supervise
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
remove_dead_instances
|
99
|
-
|
100
|
-
# Remove processes that have been stopped
|
101
|
-
remove_stopped_instances
|
99
|
+
# Tidy up any instances that we no longer wish to be managed. They will
|
100
|
+
# be removed from the list.
|
101
|
+
remove_unmonitored_instances
|
102
102
|
|
103
|
-
|
104
|
-
|
105
|
-
instances.each(&:check)
|
106
|
-
end
|
103
|
+
# Remove processes that have been stopped
|
104
|
+
remove_stopped_instances
|
107
105
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
106
|
+
# Check all instances that we manage and let them do their things.
|
107
|
+
@processes.each do |_, instances|
|
108
|
+
instances.each do |instance|
|
109
|
+
instance.check
|
110
|
+
if instance.unmonitored?
|
111
|
+
if @run_options[:brittle]
|
112
|
+
Procodile.log nil, "system", "Stopping everything because a process has died in brittle mode."
|
113
|
+
return stop
|
114
|
+
end
|
115
|
+
end
|
112
116
|
end
|
117
|
+
end
|
113
118
|
|
114
|
-
|
119
|
+
# If the processes go away, we can stop the supervisor now
|
120
|
+
if @processes.size == 0
|
121
|
+
Procodile.log nil, "system", "All processes have stopped"
|
122
|
+
stop_supervisor
|
115
123
|
end
|
116
124
|
end
|
117
125
|
|
@@ -123,6 +131,30 @@ module Procodile
|
|
123
131
|
|
124
132
|
private
|
125
133
|
|
134
|
+
def watch_for_output
|
135
|
+
Thread.new do
|
136
|
+
loop do
|
137
|
+
io = IO.select([@signal_handler.pipe[:reader]] + @readers.keys, nil, nil, 30)
|
138
|
+
@signal_handler.handle
|
139
|
+
if io
|
140
|
+
io.first.each do |reader|
|
141
|
+
next if reader == @signal_handler.pipe[:reader]
|
142
|
+
if reader.eof?
|
143
|
+
@readers.delete(reader)
|
144
|
+
else
|
145
|
+
data = reader.gets
|
146
|
+
if instance = @readers[reader]
|
147
|
+
Procodile.log instance.process.log_color, instance.description, "=> ".color(instance.process.log_color) + data
|
148
|
+
else
|
149
|
+
Procodile.log nil, 'unknown', data
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
126
158
|
def check_instance_quantities
|
127
159
|
@processes.each do |process, instances|
|
128
160
|
if instances.size > process.quantity
|
@@ -140,13 +172,17 @@ module Procodile
|
|
140
172
|
|
141
173
|
def start_instances(instances)
|
142
174
|
instances.each do |instance|
|
143
|
-
|
175
|
+
if @run_options[:brittle]
|
176
|
+
instance.respawnable = false
|
177
|
+
end
|
178
|
+
io = instance.start
|
179
|
+
@readers[io] = instance if io
|
144
180
|
@processes[instance.process] ||= []
|
145
181
|
@processes[instance.process] << instance
|
146
182
|
end
|
147
183
|
end
|
148
184
|
|
149
|
-
def
|
185
|
+
def remove_unmonitored_instances
|
150
186
|
@processes.each do |_, instances|
|
151
187
|
instances.reject!(&:unmonitored?)
|
152
188
|
end.reject! { |_, instances| instances.empty? }
|