procodile 1.0.2 → 1.0.3

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: 29cc9799aaaaa76c72574fc9189d026ed3a16aa1
4
- data.tar.gz: 506ce012eb6c52c749701d43a6909d6ef41f670e
3
+ metadata.gz: 812b0f566074632d4c15caddfd02e6b1ddf8ee88
4
+ data.tar.gz: 2acf4f380545089801e036b22aebbd1937d5be6b
5
5
  SHA512:
6
- metadata.gz: 3678666e81aaec26e1991378cd20dc8bf8d285a52ed054948d91abe99db5f82299764ac5fc724a80f0ad14129ca8a45e06c6e1860fe9d0506d36c471f6ee1ed9
7
- data.tar.gz: 09323ed2d5bbd94d7213ceb6d544450e78e9d1898597e44728bf5d0c74e9fc2795f013dc2c62f5e82af577104354850931f61c48d41c2dde9825416a838042e9
6
+ metadata.gz: 38ee04794ce4b93d537f20704cae020dd3f03e5af610f181a3a2c11a1657e5fc48dc486ad759d74cd60187e8859869176dd8d19df1f3ef6bcd7d01ca2e8bd11e
7
+ data.tar.gz: 57ff3aef3f9e8dcbbe3c0871f68e15e82b1fa18c10e035d891042774340388bf2de595bc1de95e6343e3b5f743e9ebd13a126c9db105efa925ddaaa8c034e1ba
data/bin/procodile CHANGED
@@ -1,43 +1,29 @@
1
1
  #!/usr/bin/env ruby
2
+ $:.unshift(File.expand_path('../../lib', __FILE__))
3
+
2
4
  require 'optparse'
3
5
  require 'fileutils'
4
- $:.unshift(File.expand_path('../../lib', __FILE__))
5
6
  require 'procodile'
7
+ require 'procodile/version'
8
+ require 'procodile/error'
9
+ require 'procodile/config'
10
+ require 'procodile/cli'
11
+
12
+ Thread.abort_on_exception = true
6
13
 
7
- command = ARGV[0]
14
+ command = ARGV[0] || 'help'
15
+ cli = Procodile::CLI.new
8
16
 
9
17
  options = {}
10
18
  begin
11
19
  OptionParser.new do |opts|
12
- opts.banner = "Usage: procodile [command] [options]"
20
+ opts.version = Procodile::VERSION
21
+ opts.banner = "Usage: procodile #{command} [options]"
13
22
  opts.on("-r", "--root PATH", "The path to the root of your application") do |root|
14
23
  options[:root] = root
15
24
  end
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
-
23
- if command == 'start'
24
- opts.on("-f", "--foreground", "Run the supervisor in the foreground") do
25
- options[:foreground] = true
26
- end
27
-
28
- opts.on("--clean", "Remove all previous pid and sock files before starting") do
29
- options[:clean] = true
30
- end
31
-
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
40
- end
25
+ if cli.class.commands[command.to_sym] && option_block = cli.class.commands[command.to_sym][:options]
26
+ option_block.call(opts, cli)
41
27
  end
42
28
  end.parse!
43
29
  rescue OptionParser::InvalidOption => e
@@ -45,14 +31,10 @@ rescue OptionParser::InvalidOption => e
45
31
  exit 1
46
32
  end
47
33
 
48
- require 'procodile/error'
49
- require 'procodile/config'
50
- require 'procodile/cli'
51
-
52
- Thread.abort_on_exception = true
53
34
  begin
54
- config = Procodile::Config.new(options[:root] ? File.expand_path(options[:root]) : FileUtils.pwd)
55
- cli = Procodile::CLI.new(config, options)
35
+ if command != 'help'
36
+ cli.config = Procodile::Config.new(options[:root] ? File.expand_path(options[:root]) : FileUtils.pwd)
37
+ end
56
38
  cli.run(command)
57
39
  rescue Procodile::Error => e
58
40
  $stderr.puts "Error: #{e.message}".color(31)
data/lib/procodile/cli.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'fileutils'
2
+ require 'procodile/version'
2
3
  require 'procodile/error'
3
4
  require 'procodile/supervisor'
4
5
  require 'procodile/signal_handler'
@@ -7,20 +8,93 @@ require 'procodile/control_client'
7
8
  module Procodile
8
9
  class CLI
9
10
 
10
- def initialize(config, cli_options = {})
11
- @config = config
12
- @cli_options = cli_options
11
+ def self.commands
12
+ @commands ||= {}
13
+ end
14
+
15
+ def self.desc(description)
16
+ @description = description
17
+ end
18
+
19
+ def self.options(&block)
20
+ @options = block
21
+ end
22
+
23
+ def self.command(name)
24
+ commands[name] = {:name => name, :description => @description, :options => @options}
25
+ @description = nil
26
+ @options = nil
27
+ end
28
+
29
+ attr_accessor :options
30
+ attr_accessor :config
31
+
32
+ def initialize
33
+ @options = {}
13
34
  end
14
35
 
15
36
  def run(command)
16
- if self.class.instance_methods(false).include?(command.to_sym) && command != 'run'
37
+ if self.class.commands.keys.include?(command.to_sym)
17
38
  public_send(command)
18
39
  else
19
40
  raise Error, "Invalid command '#{command}'"
20
41
  end
21
42
  end
22
43
 
23
- def start
44
+ #
45
+ # Help
46
+ #
47
+
48
+ desc "Shows this help output"
49
+ command def help
50
+ puts "\e[45;37mWelcome to Procodile v#{Procodile::VERSION}\e[0m"
51
+ puts "For documentation see https://adam.ac/procodile."
52
+ puts
53
+
54
+ puts "The following commands are supported:"
55
+ puts
56
+ self.class.commands.each do |method, options|
57
+ puts " \e[34m#{method.to_s.ljust(18, ' ')}\e[0m #{options[:description]}"
58
+ end
59
+ puts
60
+ puts "For details for the options available for each command, use the --help option."
61
+ puts "For example 'procodile start --help'."
62
+ end
63
+
64
+ #
65
+ # Start
66
+ #
67
+
68
+ desc "Starts processes and/or the supervisor"
69
+ options do |opts, cli|
70
+ opts.on("-p", "--processes a,b,c", "Only start the listed processes or process types") do |processes|
71
+ cli.options[:processes] = processes
72
+ end
73
+
74
+ opts.on("-f", "--foreground", "Run the supervisor in the foreground") do
75
+ cli.options[:foreground] = true
76
+ end
77
+
78
+ opts.on("--clean", "Remove all previous pid and sock files before starting") do
79
+ cli.options[:clean] = true
80
+ end
81
+
82
+ opts.on("-b", "--brittle", "Kill everything when one process exits") do
83
+ cli.options[:brittle] = true
84
+ end
85
+
86
+ opts.on("--stop-when-none", "Stop the supervisor when all processes are stopped") do
87
+ cli.options[:stop_when_none] = true
88
+ end
89
+
90
+ opts.on("-d", "--dev", "Run in development mode") do
91
+ cli.options[:development] = true
92
+ cli.options[:brittle] = true
93
+ cli.options[:foreground] = true
94
+ cli.options[:stop_when_none] = true
95
+ end
96
+ end
97
+ command def start
24
98
  if running?
25
99
  instances = ControlClient.run(@config.sock_path, 'start_processes', :processes => process_names_from_cli_option)
26
100
  if instances.empty?
@@ -34,17 +108,18 @@ module Procodile
34
108
  end
35
109
 
36
110
  run_options = {}
37
- run_options[:brittle] = @cli_options[:brittle]
111
+ run_options[:brittle] = @options[:brittle]
112
+ run_options[:stop_when_none] = @options[:stop_when_none]
38
113
 
39
114
  processes = process_names_from_cli_option
40
115
 
41
- if @cli_options[:clean]
116
+ if @options[:clean]
42
117
  FileUtils.rm_f(File.join(@config.pid_root, '*.pid'))
43
118
  FileUtils.rm_f(File.join(@config.pid_root, '*.sock'))
44
119
  puts "Removed all old pid & sock files"
45
120
  end
46
121
 
47
- if @cli_options[:foreground]
122
+ if @options[:foreground]
48
123
  File.open(pid_path, 'w') { |f| f.write(::Process.pid) }
49
124
  Supervisor.new(@config, run_options).start(:processes => processes)
50
125
  else
@@ -62,10 +137,24 @@ module Procodile
62
137
  end
63
138
  end
64
139
 
65
- def stop
140
+ #
141
+ # Stop
142
+ #
143
+
144
+ desc "Stops processes and/or the supervisor"
145
+ options do |opts, cli|
146
+ opts.on("-p", "--processes a,b,c", "Only stop the listed processes or process types") do |processes|
147
+ cli.options[:processes] = processes
148
+ end
149
+
150
+ opts.on("-s", "--stop-supervisor", "Stop the ") do
151
+ cli.options[:stop_supervisor] = true
152
+ end
153
+ end
154
+ command def stop
66
155
  if running?
67
156
  options = {}
68
- instances = ControlClient.run(@config.sock_path, 'stop', :processes => process_names_from_cli_option)
157
+ instances = ControlClient.run(@config.sock_path, 'stop', :processes => process_names_from_cli_option, :stop_supervisor => @options[:stop_supervisor])
69
158
  if instances.empty?
70
159
  puts "There are no processes to stop."
71
160
  else
@@ -78,7 +167,17 @@ module Procodile
78
167
  end
79
168
  end
80
169
 
81
- def restart
170
+ #
171
+ # Restart
172
+ #
173
+
174
+ desc "Restart processes"
175
+ options do |opts, cli|
176
+ opts.on("-p", "--processes a,b,c", "Only restart the listed processes or process types") do |processes|
177
+ cli.options[:processes] = processes
178
+ end
179
+ end
180
+ command def restart
82
181
  if running?
83
182
  options = {}
84
183
  instances = ControlClient.run(@config.sock_path, 'restart', :processes => process_names_from_cli_option)
@@ -94,7 +193,12 @@ module Procodile
94
193
  end
95
194
  end
96
195
 
97
- def stop_supervisor
196
+ #
197
+ # Stop Supervisor
198
+ #
199
+
200
+ desc "Stop the supervisor without stopping processes"
201
+ command def stop_supervisor
98
202
  if running?
99
203
  ::Process.kill('TERM', current_pid)
100
204
  puts "Supervisor will be stopped in a moment."
@@ -103,49 +207,79 @@ module Procodile
103
207
  end
104
208
  end
105
209
 
106
- def reload_config
210
+ #
211
+ # Reload Config
212
+ #
213
+
214
+ desc "Reload Procodile configuration"
215
+ command def reload
107
216
  if running?
108
217
  ControlClient.run(@config.sock_path, 'reload_config')
109
- puts "Reloading config for #{@config.app_name}"
218
+ puts "Reloaded config for #{@config.app_name}"
110
219
  else
111
220
  raise Error, "#{@config.app_name} supervisor isn't running"
112
221
  end
113
222
  end
114
223
 
115
- def status
224
+ #
225
+ # Check process concurrency
226
+ #
227
+
228
+ desc "Check process concurrency"
229
+ options do |opts, cli|
230
+ opts.on("--no-reload", "Do not reload the configuration before checking") do |processes|
231
+ cli.options[:reload] = false
232
+ end
233
+ end
234
+ command def check_concurrency
116
235
  if running?
117
- stats = ControlClient.run(@config.sock_path, 'status')
118
- stats['processes'].each_with_index do |process, index|
119
- puts unless index == 0
120
- puts "|| ".color(process['log_color']) + process['name'].color(process['log_color'])
121
- puts "||".color(process['log_color']) + " Quantity " + process['quantity'].to_s
122
- puts "||".color(process['log_color']) + " Command " + process['command']
123
- puts "||".color(process['log_color']) + " Respawning " + "#{process['max_respawns']} every #{process['respawn_window']} seconds"
124
- puts "||".color(process['log_color']) + " Restart mode " + process['restart_mode']
125
- puts "||".color(process['log_color']) + " Log path " + (process['log_path'] || "none specified")
126
- instances = stats['instances'][process['name']]
127
- if instances.empty?
128
- puts "||".color(process['log_color']) + " No processes running."
129
- else
130
- instances.each do |instance|
131
- print "|| ".color(process['log_color']) + instance['description'].to_s.ljust(20, ' ').color(process['log_color'])
132
- if instance['running']
133
- print ' Running '.color("42;37")
134
- else
135
- print ' Stopped '.color("41;37")
136
- end
137
- print " pid " + instance['pid'].to_s.ljust(12, ' ')
138
- print instance['respawns'].to_s + " respawns"
139
- puts
140
- end
236
+ reply = ControlClient.run(@config.sock_path, 'check_concurrency', :reload => @options[:reload])
237
+ if reply['started'].empty? && reply['stopped'].empty?
238
+ puts "Everything looks good!"
239
+ else
240
+ reply['started'].each do |instance|
241
+ puts "Started #{instance['description']}".color(32)
242
+ end
243
+
244
+ reply['stopped'].each do |instance|
245
+ puts "Stopped #{instance['description']}".color(31)
141
246
  end
142
247
  end
248
+ else
249
+ raise Error, "#{@config.app_name} supervisor isn't running"
250
+ end
251
+ end
252
+
253
+ #
254
+ # Status
255
+ #
256
+
257
+ desc "Show the current status of processes"
258
+ options do |opts, cli|
259
+ opts.on("--json", "Return the status as a JSON hash") do
260
+ cli.options[:json] = true
261
+ end
262
+ end
263
+ command def status
264
+ if running?
265
+ status = ControlClient.run(@config.sock_path, 'status')
266
+ if @options[:json]
267
+ puts status.to_json
268
+ else
269
+ require 'procodile/status_cli_output'
270
+ StatusCLIOutput.new(status).print_all
271
+ end
143
272
  else
144
273
  puts "#{@config.app_name} supervisor not running"
145
274
  end
146
275
  end
147
276
 
148
- def kill
277
+ #
278
+ # Kill
279
+ #
280
+
281
+ desc "Forcefully kill all known processes"
282
+ command def kill
149
283
  Dir[File.join(@config.pid_root, '*.pid')].each do |pid_path|
150
284
  name = pid_path.split('/').last.gsub(/\.pid\z/, '')
151
285
  pid = File.read(pid_path).to_i
@@ -195,8 +329,8 @@ module Procodile
195
329
  end
196
330
 
197
331
  def process_names_from_cli_option
198
- if @cli_options[:processes]
199
- processes = @cli_options[:processes].split(',')
332
+ if @options[:processes]
333
+ processes = @options[:processes].split(',')
200
334
  if processes.empty?
201
335
  raise Error, "No process names provided"
202
336
  end
@@ -21,37 +21,33 @@ module Procodile
21
21
  @process_list = nil
22
22
  @options = nil
23
23
  @process_options = nil
24
-
25
- process_list.each do |name, command|
26
- if process = @processes[name]
27
- # This command is already in our list. Add it.
28
- if process.command != command
29
- process.command = command
30
- Procodile.log nil, 'system', "#{name} command has changed. Updated."
31
- end
32
-
33
- if process_options[name].is_a?(Hash)
34
- process.options = process_options[name]
24
+ @local_options = nil
25
+ @local_process_options = nil
26
+
27
+ if @processes
28
+ process_list.each do |name, command|
29
+ if process = @processes[name]
30
+ # This command is already in our list. Add it.
31
+ if process.command != command
32
+ process.command = command
33
+ Procodile.log nil, 'system', "#{name} command has changed. Updated."
34
+ end
35
+ process.options = options_for_process(name)
35
36
  else
36
- process.options = {}
37
+ Procodile.log nil, 'system', "#{name} has been added to the Procfile. Adding it."
38
+ @processes[name] = create_process(name, command, COLORS[@processes.size.divmod(COLORS.size)[1]])
37
39
  end
38
- else
39
- Procodile.log nil, 'system', "#{name} has been added to the Procfile. Adding it."
40
- @processes[name] = Process.new(self, name, command, process_options[name] || {})
41
- @processes[name].log_color = COLORS[@processes.size.divmod(COLORS.size)[1]]
42
40
  end
43
41
  end
44
-
45
42
  end
46
43
 
47
44
  def app_name
48
- @app_name ||= options['app_name'] || 'Procodile'
45
+ @app_name ||= local_options['app_name'] || options['app_name'] || 'Procodile'
49
46
  end
50
47
 
51
48
  def processes
52
49
  @processes ||= process_list.each_with_index.each_with_object({}) do |((name, command), index), hash|
53
- hash[name] = Process.new(self, name, command, process_options[name] || {})
54
- hash[name].log_color = COLORS[index.divmod(COLORS.size)[1]]
50
+ hash[name] = create_process(name, command, COLORS[index.divmod(COLORS.size)[1]])
55
51
  end
56
52
  end
57
53
 
@@ -67,12 +63,32 @@ module Procodile
67
63
  @process_options ||= options['processes'] || {}
68
64
  end
69
65
 
66
+ def local_options
67
+ @local_options ||= File.exist?(local_options_path) ? YAML.load_file(local_options_path) : {}
68
+ end
69
+
70
+ def local_process_options
71
+ @local_process_options ||= local_options['processes'] || {}
72
+ end
73
+
74
+ def options_for_process(name)
75
+ (process_options[name] || {}).merge(local_process_options[name] || {})
76
+ end
77
+
78
+ def environment_variables
79
+ (options['env'] || {}).merge(local_options['env'] || {})
80
+ end
81
+
82
+ def local_environment_variables
83
+ @local_environment_variables ||= local_options['env'] || {}
84
+ end
85
+
70
86
  def pid_root
71
- @pid_root ||= File.expand_path(options['pid_root'] || 'pids', @root)
87
+ @pid_root ||= File.expand_path(local_options['pid_root'] || options['pid_root'] || 'pids', @root)
72
88
  end
73
89
 
74
90
  def log_path
75
- @log_path ||= File.expand_path(options['log_path'] || 'procodile.log', @root)
91
+ @log_path ||= File.expand_path(local_options['log_path'] || options['log_path'] || 'procodile.log', @root)
76
92
  end
77
93
 
78
94
  def sock_path
@@ -89,5 +105,15 @@ module Procodile
89
105
  File.join(@root, 'Procfile.options')
90
106
  end
91
107
 
108
+ def local_options_path
109
+ File.join(@root, 'Procfile.local')
110
+ end
111
+
112
+ def create_process(name, command, log_color)
113
+ process = Process.new(self, name, command, options_for_process(name))
114
+ process.log_color = log_color
115
+ process
116
+ end
117
+
92
118
  end
93
119
  end
@@ -1,4 +1,5 @@
1
1
  require 'json'
2
+ require 'procodile/version'
2
3
 
3
4
  module Procodile
4
5
  class ControlSession
@@ -30,7 +31,7 @@ module Procodile
30
31
  end
31
32
 
32
33
  def stop(options)
33
- instances = @supervisor.stop(:processes => options['processes'])
34
+ instances = @supervisor.stop(:processes => options['processes'], :stop_supervisor => options['stop_supervisor'])
34
35
  "200 " + instances.map(&:to_hash).to_json
35
36
  end
36
37
 
@@ -44,23 +45,32 @@ module Procodile
44
45
  "200"
45
46
  end
46
47
 
48
+ def check_concurrency(options)
49
+ result = @supervisor.check_concurrency(:reload => options['reload'])
50
+ result = result.each_with_object({}) { |(type, instances), hash| hash[type] = instances.map(&:to_hash) }
51
+ "200 #{result.to_json}"
52
+ end
53
+
54
+
47
55
  def status(options)
48
56
  instances = {}
49
57
  @supervisor.processes.each do |process, process_instances|
50
58
  instances[process.name] = []
51
59
  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
- }
60
+ instances[process.name] << instance.to_hash
59
61
  end
60
62
  end
61
63
 
62
64
  processes = @supervisor.processes.keys.map(&:to_hash)
63
- result = {:instances => instances, :processes => processes}
65
+ result = {
66
+ :version => Procodile::VERSION,
67
+ :root => @supervisor.config.root,
68
+ :app_name => @supervisor.config.app_name,
69
+ :supervisor => @supervisor.to_hash,
70
+ :instances => instances,
71
+ :processes => processes,
72
+ :environment_variables => @supervisor.config.environment_variables
73
+ }
64
74
  "200 #{result.to_json}"
65
75
  end
66
76
 
@@ -13,6 +13,7 @@ module Procodile
13
13
  @id = id
14
14
  @respawns = 0
15
15
  @respawnable = true
16
+ @started_at = nil
16
17
  end
17
18
 
18
19
  #
@@ -22,6 +23,16 @@ module Procodile
22
23
  "#{@process.name}.#{@id}"
23
24
  end
24
25
 
26
+ #
27
+ # Return an array of environment variables that should be set
28
+ #
29
+ def environment_variables
30
+ @process.config.environment_variables.merge({
31
+ 'PID_FILE' => self.pid_file_path,
32
+ 'APP_ROOT' => @process.config.root
33
+ })
34
+ end
35
+
25
36
  #
26
37
  # Should this instance still be monitored by the supervisor?
27
38
  #
@@ -64,7 +75,7 @@ module Procodile
64
75
  #
65
76
  # Start a new instance of this process
66
77
  #
67
- def start
78
+ def start(&block)
68
79
  @stopping = false
69
80
  existing_pid = self.pid_from_file
70
81
  if running?(existing_pid)
@@ -72,6 +83,7 @@ module Procodile
72
83
  # to monitor this process rather than spawning a new one.
73
84
  @pid = existing_pid
74
85
  Procodile.log(@process.log_color, description, "Already running with PID #{@pid}")
86
+ @started_at = File.mtime(self.pid_file_path)
75
87
  nil
76
88
  else
77
89
  if self.process.log_path
@@ -84,10 +96,15 @@ module Procodile
84
96
  end
85
97
 
86
98
  Dir.chdir(@process.config.root)
87
- @pid = ::Process.spawn({'PID_FILE' => pid_file_path}, @process.command, :out => log_destination, :err => log_destination, :pgroup => true)
99
+ @pid = ::Process.spawn(environment_variables, @process.command, :out => log_destination, :err => log_destination, :pgroup => true)
88
100
  Procodile.log(@process.log_color, description, "Started with PID #{@pid}")
89
101
  File.open(pid_file_path, 'w') { |f| f.write(@pid.to_s + "\n") }
90
102
  ::Process.detach(@pid)
103
+ @started_at = Time.now
104
+
105
+ if block_given?
106
+ block.call(self, return_value)
107
+ end
91
108
 
92
109
  return_value
93
110
  end
@@ -120,6 +137,7 @@ module Procodile
120
137
  # started again
121
138
  #
122
139
  def on_stop
140
+ @started_at = nil
123
141
  tidy
124
142
  unmonitor
125
143
  end
@@ -135,7 +153,7 @@ module Procodile
135
153
  #
136
154
  # Retarts the process using the appropriate method from the process configuraiton
137
155
  #
138
- def restart
156
+ def restart(&block)
139
157
  Procodile.log(@process.log_color, description, "Restarting using #{@process.restart_mode} mode")
140
158
  @restarting = true
141
159
  update_pid
@@ -146,11 +164,11 @@ module Procodile
146
164
  Procodile.log(@process.log_color, description, "Sent #{@process.restart_mode.upcase} signal to process #{@pid}")
147
165
  else
148
166
  Procodile.log(@process.log_color, description, "Process not running already. Starting it.")
149
- start
167
+ start(&block)
150
168
  end
151
169
  when 'start-term'
152
170
  old_process_pid = @pid
153
- start
171
+ start(&block)
154
172
  Procodile.log(@process.log_color, description, "Sent #{@process.term_signal} signal to old PID #{old_process_pid} (forgetting now)")
155
173
  ::Process.kill(@process.term_signal, old_process_pid)
156
174
  when 'term-start'
@@ -158,7 +176,7 @@ module Procodile
158
176
  Thread.new do
159
177
  # Wait for this process to stop and when it has, run it.
160
178
  sleep 0.5 while running?
161
- start
179
+ start(&block)
162
180
  end
163
181
  end
164
182
  ensure
@@ -259,7 +277,8 @@ module Procodile
259
277
  :description => self.description,
260
278
  :pid => self.pid,
261
279
  :respawns => self.respawns,
262
- :running => self.running?
280
+ :running => self.running?,
281
+ :started_at => @started_at ? @started_at.to_i : nil
263
282
  }
264
283
  end
265
284
 
@@ -0,0 +1,77 @@
1
+ module Procodile
2
+ class StatusCLIOutput
3
+
4
+ def initialize(status)
5
+ @status = status
6
+ end
7
+
8
+ def print_all
9
+ print_header
10
+ print_processes
11
+ end
12
+
13
+ def print_header
14
+ puts "Procodile Version " + @status['version'].to_s.color(34)
15
+ puts "Application Root " + "#{@status['root']}".color(34)
16
+ puts "Supervisor PID " + "#{@status['supervisor']['pid']}".color(34)
17
+ if time = @status['supervisor']['started_at']
18
+ time = Time.at(time)
19
+ puts "Started " + "#{time.to_s}".color(34)
20
+ end
21
+ if @status['environment_variables'] && !@status['environment_variables'].empty?
22
+ @status['environment_variables'].each_with_index do |(key, value), index|
23
+ if index == 0
24
+ print "Environment Vars "
25
+ else
26
+ print " "
27
+ end
28
+ print key.color(34)
29
+ puts " " + value.to_s
30
+ end
31
+ end
32
+ end
33
+
34
+ def print_processes
35
+ puts
36
+ @status['processes'].each_with_index do |process, index|
37
+ puts unless index == 0
38
+ puts "|| ".color(process['log_color']) + process['name'].color(process['log_color'])
39
+ puts "||".color(process['log_color']) + " Quantity " + process['quantity'].to_s
40
+ puts "||".color(process['log_color']) + " Command " + process['command']
41
+ puts "||".color(process['log_color']) + " Respawning " + "#{process['max_respawns']} every #{process['respawn_window']} seconds"
42
+ puts "||".color(process['log_color']) + " Restart mode " + process['restart_mode']
43
+ puts "||".color(process['log_color']) + " Log path " + (process['log_path'] || "none specified")
44
+ instances = @status['instances'][process['name']]
45
+ if instances.empty?
46
+ puts "||".color(process['log_color']) + " No processes running."
47
+ else
48
+ instances.each do |instance|
49
+ print "|| => ".color(process['log_color']) + instance['description'].to_s.ljust(17, ' ').color(process['log_color'])
50
+ if instance['running']
51
+ print 'Running'.color("32")
52
+ else
53
+ print 'Stopped'.color("31")
54
+ end
55
+ print " " + formatted_timestamp(instance['started_at']).ljust(10, ' ')
56
+ print " pid: " + instance['pid'].to_s.ljust(7, ' ')
57
+ print " respawns: " + instance['respawns'].to_s.ljust(7, ' ')
58
+ puts
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def formatted_timestamp(timestamp)
67
+ return '' if timestamp.nil?
68
+ timestamp = Time.at(timestamp)
69
+ if timestamp > (Time.now - (60 * 60 * 24))
70
+ timestamp.strftime("%H:%M")
71
+ else
72
+ timestamp.strftime("%d/%m/%Y")
73
+ end
74
+ end
75
+
76
+ end
77
+ end
@@ -5,6 +5,7 @@ module Procodile
5
5
 
6
6
  attr_reader :config
7
7
  attr_reader :processes
8
+ attr_reader :started_at
8
9
 
9
10
  def initialize(config, run_options = {})
10
11
  @config = config
@@ -13,7 +14,7 @@ module Procodile
13
14
  @readers = {}
14
15
  @signal_handler = SignalHandler.new('TERM', 'USR1', 'USR2', 'INT', 'HUP')
15
16
  @signal_handler.register('TERM') { stop_supervisor }
16
- @signal_handler.register('INT') { stop }
17
+ @signal_handler.register('INT') { stop(:stop_supervisor => true) }
17
18
  @signal_handler.register('USR1') { restart }
18
19
  @signal_handler.register('USR2') { status }
19
20
  @signal_handler.register('HUP') { reload_config }
@@ -30,10 +31,12 @@ module Procodile
30
31
  end
31
32
  start_processes(options[:processes])
32
33
  watch_for_output
34
+ @started_at = Time.now
33
35
  loop { supervise; sleep 3 }
34
36
  end
35
37
 
36
38
  def start_processes(types = [])
39
+ reload_config
37
40
  Array.new.tap do |instances_started|
38
41
  @config.processes.each do |name, process|
39
42
  next if types && !types.include?(name.to_s) # Not a process we want
@@ -45,17 +48,19 @@ module Procodile
45
48
  end
46
49
 
47
50
  def stop(options = {})
51
+ if options[:stop_supervisor]
52
+ @run_options[:stop_when_none] = true
53
+ end
54
+
48
55
  Array.new.tap do |instances_stopped|
49
56
  if options[:processes].nil?
50
- return if @stopping
51
- @stopping = true
52
57
  Procodile.log nil, "system", "Stopping all #{@config.app_name} processes"
53
- @processes.each do |_, instances|
54
- instances.each do |instance|
55
- instance.stop
56
- instances_stopped << instance
57
- end
58
+ @processes.each do |_, instances|
59
+ instances.each do |instance|
60
+ instance.stop
61
+ instances_stopped << instance
58
62
  end
63
+ end
59
64
  else
60
65
  instances = process_names_to_instances(options[:processes])
61
66
  Procodile.log nil, "system", "Stopping #{instances.size} process(es)"
@@ -68,23 +73,25 @@ module Procodile
68
73
  end
69
74
 
70
75
  def restart(options = {})
71
- @config.reload
76
+ reload_config
72
77
  Array.new.tap do |instances_restarted|
73
78
  if options[:processes].nil?
74
79
  Procodile.log nil, "system", "Restarting all #{@config.app_name} processes"
75
80
  @processes.each do |_, instances|
76
81
  instances.each do |instance|
77
- instance.restart
82
+ instance.restart { |_, io| add_reader(instance, io) }
78
83
  instances_restarted << instance
79
84
  end
80
85
  end
86
+ instances_restarted.push(*check_instance_quantities[:started])
81
87
  else
82
88
  instances = process_names_to_instances(options[:processes])
83
89
  Procodile.log nil, "system", "Restarting #{instances.size} process(es)"
84
90
  instances.each do |instance|
85
- instance.restart
91
+ instance.restart { |_, io| add_reader(instance, io) }
86
92
  instances_restarted << instance
87
93
  end
94
+ instances_restarted.push(*check_instance_quantities(options[:processes])[:started])
88
95
  end
89
96
  end
90
97
  end
@@ -116,29 +123,66 @@ module Procodile
116
123
  end
117
124
  end
118
125
 
119
- # If the processes go away, we can stop the supervisor now
120
- if @processes.all? { |_,instances| instances.size == 0 }
121
- Procodile.log nil, "system", "All processes have stopped"
122
- stop_supervisor
126
+ if @run_options[:stop_when_none]
127
+ # If the processes go away, we can stop the supervisor now
128
+ if @processes.all? { |_,instances| instances.size == 0 }
129
+ Procodile.log nil, "system", "All processes have stopped"
130
+ stop_supervisor
131
+ end
123
132
  end
124
133
  end
125
134
 
126
135
  def reload_config
127
136
  Procodile.log nil, "system", "Reloading configuration"
128
137
  @config.reload
129
- check_instance_quantities
138
+ end
139
+
140
+ def check_concurrency(options = {})
141
+ Procodile.log nil, "system", "Checking process concurrency"
142
+ reload_config unless options[:reload] == false
143
+ result = check_instance_quantities
144
+ if result[:started].empty? && result[:stopped].empty?
145
+ Procodile.log nil, "system", "Process concurrency looks good"
146
+ else
147
+ unless result[:started].empty?
148
+ Procodile.log nil, "system", "Concurrency check started #{result[:started].map(&:description).join(', ')}"
149
+ end
150
+
151
+ unless result[:stopped].empty?
152
+ Procodile.log nil, "system", "Concurrency check stopped #{result[:stopped].map(&:description).join(', ')}"
153
+ end
154
+ end
155
+ result
156
+ end
157
+
158
+ def to_hash
159
+ {
160
+ :started_at => @started_at ? @started_at.to_i : nil,
161
+ :pid => ::Process.pid
162
+ }
130
163
  end
131
164
 
132
165
  private
133
166
 
167
+ def add_reader(instance, io)
168
+ return unless io
169
+ @readers[io] = instance
170
+ @signal_handler.notice
171
+ end
172
+
134
173
  def watch_for_output
135
174
  Thread.new do
136
175
  loop do
137
176
  io = IO.select([@signal_handler.pipe[:reader]] + @readers.keys, nil, nil, 30)
138
177
  @signal_handler.handle
178
+
139
179
  if io
140
180
  io.first.each do |reader|
141
- next if reader == @signal_handler.pipe[:reader]
181
+ if reader == @signal_handler.pipe[:reader]
182
+ @signal_handler.pipe[:reader].read_nonblock(999) rescue nil
183
+ next
184
+ end
185
+
142
186
  if reader.eof?
143
187
  @readers.delete(reader)
144
188
  else
@@ -155,17 +199,20 @@ module Procodile
155
199
  end
156
200
  end
157
201
 
158
- def check_instance_quantities
159
- @processes.each do |process, instances|
160
- if instances.size > process.quantity
161
- quantity_to_stop = instances.size - process.quantity
162
- Procodile.log nil, "system", "Stopping #{quantity_to_stop} #{process.name} process(es)"
163
- instances.last(quantity_to_stop).each(&:stop)
164
- elsif instances.size < process.quantity
165
- quantity_needed = process.quantity - instances.size
166
- start_id = instances.last ? instances.last.id + 1 : 1
167
- Procodile.log nil, "system", "Starting #{quantity_needed} more #{process.name} process(es) (start with #{start_id})"
168
- start_instances(process.generate_instances(quantity_needed, start_id))
202
+ def check_instance_quantities(processes = nil)
203
+ {:started => [], :stopped => []}.tap do |status|
204
+ @processes.each do |process, instances|
205
+ next if processes && !processes.include?(process.name)
206
+ if instances.size > process.quantity
207
+ quantity_to_stop = instances.size - process.quantity
208
+ Procodile.log nil, "system", "Stopping #{quantity_to_stop} #{process.name} process(es)"
209
+ status[:stopped] = instances.last(quantity_to_stop).each(&:stop)
210
+ elsif instances.size < process.quantity
211
+ quantity_needed = process.quantity - instances.size
212
+ start_id = instances.last ? instances.last.id + 1 : 1
213
+ Procodile.log nil, "system", "Starting #{quantity_needed} more #{process.name} process(es) (start with #{start_id})"
214
+ status[:started] = start_instances(process.generate_instances(quantity_needed, start_id))
215
+ end
169
216
  end
170
217
  end
171
218
  end
@@ -175,8 +222,7 @@ module Procodile
175
222
  if @run_options[:brittle]
176
223
  instance.respawnable = false
177
224
  end
178
- io = instance.start
179
- @readers[io] = instance if io
225
+ instance.start { |_, io| add_reader(instance, io) }
180
226
  @processes[instance.process] ||= []
181
227
  @processes[instance.process] << instance
182
228
  end
@@ -0,0 +1,3 @@
1
+ module Procodile
2
+ VERSION = '1.0.3'
3
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: procodile
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Cooke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-17 00:00:00.000000000 Z
11
+ date: 2016-10-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -34,8 +34,6 @@ extra_rdoc_files: []
34
34
  files:
35
35
  - bin/procodile
36
36
  - lib/procodile.rb
37
- - lib/procodile/capistrano2.rb
38
- - lib/procodile/capistrano3.rb
39
37
  - lib/procodile/cli.rb
40
38
  - lib/procodile/config.rb
41
39
  - lib/procodile/control_client.rb
@@ -46,7 +44,9 @@ files:
46
44
  - lib/procodile/logger.rb
47
45
  - lib/procodile/process.rb
48
46
  - lib/procodile/signal_handler.rb
47
+ - lib/procodile/status_cli_output.rb
49
48
  - lib/procodile/supervisor.rb
49
+ - lib/procodile/version.rb
50
50
  homepage: https://github.com/adamcooke/procodile
51
51
  licenses:
52
52
  - MIT
@@ -1,39 +0,0 @@
1
- Capistrano::Configuration.instance(:must_exist).load do
2
-
3
- namespace :procodile do
4
-
5
- task :start, :roles => fetch(:procodile_roles, [:app]) do
6
- run procodile_command('start')
7
- end
8
-
9
- task :stop, :roles => fetch(:procodile_roles, [:app]) do
10
- run procodile_command('stop')
11
- end
12
-
13
- task :restart, :roles => fetch(:procodile_roles, [:app]) do
14
- run procodile_command('restart')
15
- end
16
-
17
- task :status, :roles => fetch(:procodile_roles, [:app]) do
18
- run procodile_command('status')
19
- end
20
-
21
- after 'deploy:start', 'procodile:start'
22
- after 'deploy:stop', 'procodile:stop'
23
- after 'deploy:restart', 'procodile:restart'
24
-
25
- def procodile_command(command, options = "")
26
- binary = fetch(:procodile_binary, 'procodile')
27
- if processes = fetch(:processes, nil)
28
- options = "-p #{processes} " + options
29
- end
30
- command = "#{binary} #{command} -r #{current_path} #{options}"
31
- if user = fetch(:procodile_user, nil)
32
- "sudo -u #{user} #{command}"
33
- else
34
- command
35
- end
36
- end
37
-
38
- end
39
- end
@@ -1,40 +0,0 @@
1
- namespace :procodile do
2
- desc 'Start procodile processes'
3
- task :start do
4
- on roles(fetch(:procodile_roles, [:app])) do
5
- execute procodile_command(:start)
6
- end
7
- end
8
-
9
- desc 'Stop procodile processes'
10
- task :stop do
11
- on roles(fetch(:procodile_roles, [:app])) do
12
- execute procodile_command(:stop)
13
- end
14
- end
15
-
16
- desc 'Restart procodile processes'
17
- task :restart do
18
- on roles(fetch(:procodile_roles, [:app])) do
19
- execute procodile_command(:restart)
20
- end
21
- end
22
-
23
- after 'deploy:start', "procodile:start"
24
- after 'deploy:stop', "procodile:stop"
25
- after 'deploy:restart', "procodile:restart"
26
-
27
- def procodile_command(command, options = "")
28
- binary = fetch(:procodile_binary, 'procodile')
29
- if processes = fetch(:processes, nil)
30
- options = "-p #{processes} " + options
31
- end
32
- command = "#{binary} #{command} -r #{current_path} #{options}"
33
- if user = fetch(:procodile_user, nil)
34
- "sudo -u #{user} #{command}"
35
- else
36
- command
37
- end
38
- end
39
-
40
- end