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 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