procodile 1.0.2 → 1.0.3

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