procodile 0.0.1 → 0.0.2

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: 12da3acd72adc0bf302ddad40c02133f308cf168
4
- data.tar.gz: e13faabcbbadce0fd6ac91885c3b5ba937f398db
3
+ metadata.gz: a67424151ecf1b1b6fc80cdd93d4989b3bc1f0ba
4
+ data.tar.gz: 5df9bfac9a606afe8987a6400fa097f0091d99a3
5
5
  SHA512:
6
- metadata.gz: 8b61d0f1fb351bf1f05eac2ea0d22accd8ff2ebbe51fd551397ac5c8ee17693ffa36f6a7f3d2e9729774cf1dabac99358930de70c45c0aa2cd95b1d9359ffb06
7
- data.tar.gz: b2f481642d8e7015808946d07cf6b7d0c027e1d2b8956518b27a61bd670e851e690fbf98c79cd3ec02b042d6c176c275794abe6ee2139effc234fc60152b65d9
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
- 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!
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
- command = ARGV[0]
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
@@ -1,2 +1,8 @@
1
1
  module Procodile
2
2
  end
3
+
4
+ class String
5
+ def color(color)
6
+ "\e[#{color}m#{self}\e[0m"
7
+ end
8
+ end
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
- raise Error, "#{@config.app_name} already running (PID: #{current_pid})"
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
- 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
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
- ::Process.kill('TERM', current_pid)
47
- puts "Stopping #{@config.app_name} processes & supervisor..."
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 stop_supervisor
78
+ def restart
54
79
  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."
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
- puts "OK. That's fine. You can just run `stop` to stop processes too."
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 restart
103
+ def reload_config
70
104
  if running?
71
- ::Process.kill('USR1', current_pid)
72
- puts "Restarting #{@config.app_name}"
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
- puts "#{@config.app_name} running (PID: #{current_pid})"
81
- ::Process.kill('USR2', current_pid)
82
- puts "Instance status details added to #{log_path}"
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
@@ -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
- options = {'log_color' => COLORS[index.divmod(COLORS.size)[1]]}.merge(process_options[name] || {})
29
- hash[name] = Process.new(self, name, command, options)
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
- @processes ||= YAML.load_file(procfile_path)
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
@@ -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
- def dead?
19
- @dead || false
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
- Dir.chdir(@process.config.root) do
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
- @pid = ::Process.spawn({'PID_FILE' => pid_file_path}, @process.command, :out => log_file, :err => log_file)
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
- pid = self.pid_from_file
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
- die
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 die
169
- on_stop
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
@@ -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
- end
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 + 1) }
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
@@ -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
- @instances = []
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
- @config.processes.each do |name, process|
14
- process.generate_instances.each do |instance|
15
- instance.start
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 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
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 restart
36
- Procodile.log nil, 'system', "Restarting all #{@config.app_name} processes"
37
- @instances.each(&:restart)
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 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)"
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
- Procodile.log '37;44', 'status', "#{instance.description} is STOPPED"
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
- 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
100
+ # Remove processes that have been stopped
101
+ remove_stopped_instances
61
102
 
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)
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.reject!(&:dead?)
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
- @instances.reject! do |instance|
86
- if instance.running?
87
- false
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
- instance.on_stop
90
- true
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.1
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