procodile 0.0.2 → 1.0.0

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