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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a67424151ecf1b1b6fc80cdd93d4989b3bc1f0ba
4
- data.tar.gz: 5df9bfac9a606afe8987a6400fa097f0091d99a3
3
+ metadata.gz: b644b27235abc8fcae9bfd9d7ca9bbd87cd1c0d8
4
+ data.tar.gz: 82428a6110171a2b783f0a249b8bf6661e02c6e4
5
5
  SHA512:
6
- metadata.gz: 85cc30d458aa91a9ec06c25412bfced78860f7f2a2f2a74d674e790c811d2d7005d1cfe8812d6fd027013a21919c8126f7d5930bb219db3c6c4e1b09a06e8ad7
7
- data.tar.gz: 8d80992dcfb3a8dafda5da5feae9cc4798f6225862583eb520be96c979f29dbd45cab4bc8070274b0a88e5662f27ef4c93fed4248488259454e2624c626a30a3
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
- 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
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 "\e[31merror: #{e.message}\e[0m"
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 "\e[31merror: #{e.message}\e[0m"
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, 'supervisor.pid')
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
@@ -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 log_root
77
- @log_root ||= File.expand_path(options['log_root'] || 'log', @root)
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', "\e[31mError: #{e.message}\e[0m"
19
+ Procodile.log nil, 'control', "Error: #{e.message}".color(31)
20
20
  "500 #{e.message}"
21
21
  end
22
22
  else
@@ -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
- log_file = File.open(self.log_file_path, 'a')
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 => log_file, :err => log_file, :pgroup => true)
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 can_respawn?
191
- Procodile.log(@process.log_color, description, "Process has stopped. Respawning...")
192
- start
193
- add_respawn
194
- elsif respawns >= @process.max_respawns
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")
196
- Procodile.log(@process.log_color, description, "\e[31mIt will not be respawned automatically any longer and will no longer be managed.\e[0m")
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
@@ -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 += "\e[#{color}m" if color
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
@@ -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
- if signal = self.class.queue.shift
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
@@ -6,27 +6,31 @@ module Procodile
6
6
  attr_reader :config
7
7
  attr_reader :processes
8
8
 
9
- # Create a new supervisor instance that will be monitoring the
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
- 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 }
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
- supervise
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
- loop do
96
- # Tidy up any instances that we no longer wish to be managed. They will
97
- # be removed from the list.
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
- # Check all instances that we manage and let them do their things.
104
- @processes.each do |_, instances|
105
- instances.each(&:check)
106
- end
103
+ # Remove processes that have been stopped
104
+ remove_stopped_instances
107
105
 
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
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
- sleep 5
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
- instance.start
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 remove_dead_instances
185
+ def remove_unmonitored_instances
150
186
  @processes.each do |_, instances|
151
187
  instances.reject!(&:unmonitored?)
152
188
  end.reject! { |_, instances| instances.empty? }
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.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Cooke