procodile 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 12da3acd72adc0bf302ddad40c02133f308cf168
4
+ data.tar.gz: e13faabcbbadce0fd6ac91885c3b5ba937f398db
5
+ SHA512:
6
+ metadata.gz: 8b61d0f1fb351bf1f05eac2ea0d22accd8ff2ebbe51fd551397ac5c8ee17693ffa36f6a7f3d2e9729774cf1dabac99358930de70c45c0aa2cd95b1d9359ffb06
7
+ data.tar.gz: b2f481642d8e7015808946d07cf6b7d0c027e1d2b8956518b27a61bd670e851e690fbf98c79cd3ec02b042d6c176c275794abe6ee2139effc234fc60152b65d9
data/bin/procodile ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ 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!
10
+
11
+ $:.unshift(File.expand_path('../../lib', __FILE__))
12
+
13
+ require 'fileutils'
14
+ require 'procodile/error'
15
+ require 'procodile/config'
16
+ require 'procodile/cli'
17
+
18
+ Thread.abort_on_exception = true
19
+ begin
20
+
21
+ config = Procodile::Config.new(options[:root] ? File.expand_path(options[:root]) : FileUtils.pwd)
22
+ command = ARGV[0]
23
+
24
+ cli = Procodile::CLI.new(config)
25
+ cli.run(command)
26
+
27
+ rescue Procodile::Error => e
28
+ $stderr.puts "\e[31merror: #{e.message}\e[0m"
29
+ exit 1
30
+ end
data/lib/procodile.rb ADDED
@@ -0,0 +1,2 @@
1
+ module Procodile
2
+ end
@@ -0,0 +1,131 @@
1
+ require 'fileutils'
2
+ require 'procodile/error'
3
+ require 'procodile/supervisor'
4
+ require 'procodile/signal_handler'
5
+
6
+ module Procodile
7
+ class CLI
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ end
12
+
13
+ def run(command)
14
+ if self.class.instance_methods(false).include?(command.to_sym) && command != 'run'
15
+ public_send(command)
16
+ else
17
+ raise Error, "Invalid command '#{command}'"
18
+ end
19
+ end
20
+
21
+ def start
22
+ if running?
23
+ raise Error, "#{@config.app_name} already running (PID: #{current_pid})"
24
+ 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
38
+ 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
+ end
43
+
44
+ def stop
45
+ if running?
46
+ ::Process.kill('TERM', current_pid)
47
+ puts "Stopping #{@config.app_name} processes & supervisor..."
48
+ else
49
+ raise Error, "#{@config.app_name} supervisor isn't running"
50
+ end
51
+ end
52
+
53
+ def stop_supervisor
54
+ 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."
60
+ else
61
+ puts "OK. That's fine. You can just run `stop` to stop processes too."
62
+ end
63
+
64
+ else
65
+ raise Error, "#{@config.app_name} supervisor isn't running"
66
+ end
67
+ end
68
+
69
+ def restart
70
+ if running?
71
+ ::Process.kill('USR1', current_pid)
72
+ puts "Restarting #{@config.app_name}"
73
+ else
74
+ raise Error, "#{@config.app_name} supervisor isn't running"
75
+ end
76
+ end
77
+
78
+ def status
79
+ 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}"
83
+ else
84
+ puts "#{@config.app_name} supervisor not running"
85
+ end
86
+ end
87
+
88
+ def kill
89
+ Dir[File.join(@config.pid_root, '*.pid')].each do |pid_path|
90
+ name = pid_path.split('/').last.gsub(/\.pid\z/, '')
91
+ pid = File.read(pid_path).to_i
92
+ begin
93
+ ::Process.kill('KILL', pid)
94
+ puts "Sent KILL to #{pid} (#{name})"
95
+ rescue Errno::ESRCH
96
+ end
97
+ FileUtils.rm(pid_path)
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def running?
104
+ if pid = current_pid
105
+ ::Process.getpgid(pid) ? true : false
106
+ else
107
+ false
108
+ end
109
+ rescue Errno::ESRCH
110
+ false
111
+ end
112
+
113
+ def current_pid
114
+ if File.exist?(pid_path)
115
+ pid_file = File.read(pid_path).strip
116
+ pid_file.length > 0 ? pid_file.to_i : nil
117
+ else
118
+ nil
119
+ end
120
+ end
121
+
122
+ def pid_path
123
+ File.join(@config.pid_root, 'supervisor.pid')
124
+ end
125
+
126
+ def log_path
127
+ File.join(@config.log_root, 'supervisor.log')
128
+ end
129
+
130
+ end
131
+ end
@@ -0,0 +1,64 @@
1
+ require 'yaml'
2
+ require 'procodile/error'
3
+ require 'procodile/process'
4
+
5
+ module Procodile
6
+ class Config
7
+
8
+ COLORS = [35, 31, 36, 32, 33, 34]
9
+
10
+ attr_reader :root
11
+
12
+ def initialize(root)
13
+ @root = root
14
+ unless File.exist?(procfile_path)
15
+ raise Error, "Procfile not found at #{procfile_path}"
16
+ end
17
+
18
+ FileUtils.mkdir_p(pid_root)
19
+ FileUtils.mkdir_p(log_root)
20
+ end
21
+
22
+ def app_name
23
+ options['app_name'] || 'Procodile'
24
+ end
25
+
26
+ 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)
30
+ end
31
+ end
32
+
33
+ def process_list
34
+ @processes ||= YAML.load_file(procfile_path)
35
+ end
36
+
37
+ def options
38
+ @options ||= File.exist?(options_path) ? YAML.load_file(options_path) : {}
39
+ end
40
+
41
+ def process_options
42
+ @process_options ||= options['processes'] || {}
43
+ end
44
+
45
+ def pid_root
46
+ File.expand_path(options['pid_root'] || 'pids', @root)
47
+ end
48
+
49
+ def log_root
50
+ File.expand_path(options['log_root'] || 'log', @root)
51
+ end
52
+
53
+ private
54
+
55
+ def procfile_path
56
+ File.join(@root, 'Procfile')
57
+ end
58
+
59
+ def options_path
60
+ File.join(@root, 'Procfile.options')
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,4 @@
1
+ module Procodile
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,204 @@
1
+ require 'procodile/logger'
2
+
3
+ module Procodile
4
+ class Instance
5
+
6
+ attr_accessor :pid
7
+
8
+ def initialize(process, id)
9
+ @process = process
10
+ @id = id
11
+ @respawns = 0
12
+ end
13
+
14
+ def description
15
+ "#{@process.name}.#{@id}"
16
+ end
17
+
18
+ def dead?
19
+ @dead || false
20
+ end
21
+
22
+ def pid_file_path
23
+ File.join(@process.config.pid_root, "#{description}.pid")
24
+ end
25
+
26
+ def log_file_path
27
+ File.join(@process.config.log_root, "#{description}.log")
28
+ end
29
+
30
+ def pid_from_file
31
+ if File.exist?(pid_file_path)
32
+ pid = File.read(pid_file_path)
33
+ pid.length > 0 ? pid.strip.to_i : nil
34
+ else
35
+ nil
36
+ end
37
+ end
38
+
39
+ def running?(force_pid = nil)
40
+ if force_pid || @pid
41
+ ::Process.getpgid(force_pid || @pid) ? true : false
42
+ else
43
+ false
44
+ end
45
+ rescue Errno::ESRCH
46
+ false
47
+ end
48
+
49
+ def start
50
+ @stopping = false
51
+ Dir.chdir(@process.config.root) do
52
+ 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)
54
+ Procodile.log(@process.log_color, description, "Started with PID #{@pid}")
55
+ File.open(pid_file_path, 'w') { |f| f.write(@pid.to_s + "\n") }
56
+ ::Process.detach(@pid)
57
+ end
58
+ end
59
+
60
+ #
61
+ # Is this instance supposed to be stopping/be stopped?
62
+ #
63
+ def stopping?
64
+ @stopping || false
65
+ end
66
+
67
+ #
68
+ # Send this signal the signal to stop and mark the instance in a state that
69
+ # tells us that we want it to be stopped.
70
+ #
71
+ def stop
72
+ @stopping = true
73
+ update_pid
74
+ if self.running?
75
+ pid = self.pid_from_file
76
+ Procodile.log(@process.log_color, description, "Sending TERM to #{pid}")
77
+ ::Process.kill('TERM', pid)
78
+ else
79
+ Procodile.log(@process.log_color, description, "Process already stopped")
80
+ end
81
+ end
82
+
83
+ #
84
+ # A method that will be called when this instance has been stopped and it isn't going to be
85
+ # started again
86
+ #
87
+ def on_stop
88
+ FileUtils.rm_f(self.pid_file_path)
89
+ Procodile.log(@process.log_color, description, "Removed PID file")
90
+ end
91
+
92
+ #
93
+ # Retarts the process using the appropriate method from the process configuraiton
94
+ #
95
+ def restart
96
+ Procodile.log(@process.log_color, description, "Restarting using #{@process.restart_mode} mode")
97
+ @restarting = true
98
+ update_pid
99
+ case @process.restart_mode
100
+ when 'usr1', 'usr2'
101
+ if running?
102
+ ::Process.kill(@process.restart_mode.upcase, @pid)
103
+ Procodile.log(@process.log_color, description, "Sent #{@process.restart_mode.upcase} signal to process #{@pid}")
104
+ else
105
+ Procodile.log(@process.log_color, description, "Process not running already. Starting it.")
106
+ start
107
+ end
108
+ when 'start-term'
109
+ old_process_pid = @pid
110
+ start
111
+ Procodile.log(@process.log_color, description, "Sent TERM signal to old PID #{old_process_pid} (forgetting now)")
112
+ ::Process.kill('TERM', old_process_pid)
113
+ when 'term-start'
114
+ stop
115
+ Thread.new do
116
+ # Wait for this process to stop and when it has, run it.
117
+ sleep 0.5 while running?
118
+ start
119
+ end
120
+ end
121
+ ensure
122
+ @restarting = false
123
+ end
124
+
125
+ #
126
+ # Update the locally cached PID from that stored on the file system.
127
+ #
128
+ def update_pid
129
+ pid_from_file = self.pid_from_file
130
+ if pid_from_file != @pid
131
+ @pid = pid_from_file
132
+ Procodile.log(@process.log_color, description, "PID file changed. Updated pid to #{@pid}")
133
+ true
134
+ else
135
+ false
136
+ end
137
+ end
138
+
139
+ #
140
+ # Check the status of this process and handle as appropriate
141
+ #
142
+ def check
143
+ # Don't do any checking if we're in the midst of a restart
144
+ return if @restarting
145
+
146
+ if self.running?
147
+ # Everything is OK. The process is running.
148
+ else
149
+ # If the process isn't running any more, update the PID in our memory from
150
+ # the file in case the process has changed itself.
151
+ return check if update_pid
152
+
153
+ if can_respawn?
154
+ Procodile.log(@process.log_color, description, "Process has stopped. Respawning...")
155
+ start
156
+ add_respawn
157
+ elsif respawns >= @process.max_respawns
158
+ 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
+ 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
161
+ end
162
+ end
163
+ end
164
+
165
+ #
166
+ # Mark this process as dead and tidy up after it
167
+ #
168
+ def die
169
+ on_stop
170
+ @dead = true
171
+ end
172
+
173
+ #
174
+ # Can this process be respawned if needed?
175
+ #
176
+ def can_respawn?
177
+ !stopping? && (respawns + 1) <= @process.max_respawns
178
+ end
179
+
180
+ #
181
+ # Return the number of times this process has been respawned in the last hour
182
+ #
183
+ def respawns
184
+ if @respawns.nil? || @last_respawn.nil? || @last_respawn < (Time.now - @process.respawn_window)
185
+ 0
186
+ else
187
+ @respawns
188
+ end
189
+ end
190
+
191
+ #
192
+ # Increment the counter of respawns for this process
193
+ #
194
+ def add_respawn
195
+ if @last_respawn && @last_respawn < (Time.now - @process.respawn_window)
196
+ @respawns = 1
197
+ else
198
+ @last_respawn = Time.now
199
+ @respawns += 1
200
+ end
201
+ end
202
+
203
+ end
204
+ end
@@ -0,0 +1,22 @@
1
+ require 'logger'
2
+ module Procodile
3
+
4
+ def self.mutex
5
+ @mutex ||= Mutex.new
6
+ end
7
+
8
+ def self.log(color, name, text)
9
+ mutex.synchronize do
10
+ text.to_s.lines.map(&:chomp).each do |message|
11
+ output = ""
12
+ output += "\e[#{color}m" if color
13
+ output += "#{Time.now.strftime("%H:%M:%S")} #{name.ljust(15, ' ')} |"
14
+ output += "\e[0m "
15
+ output += message
16
+ $stdout.puts output
17
+ $stdout.flush
18
+ end
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,65 @@
1
+ require 'procodile/instance'
2
+
3
+ module Procodile
4
+ class Process
5
+
6
+ attr_reader :name
7
+ attr_reader :command
8
+ attr_reader :config
9
+
10
+ def initialize(config, name, command, options = {})
11
+ @config = config
12
+ @name = name
13
+ @command = command
14
+ @options = options
15
+ end
16
+
17
+ #
18
+ # Return the color for this process
19
+ #
20
+ def log_color
21
+ @options['log_color'] || 0
22
+ end
23
+
24
+ #
25
+ # How many instances of this process should be started
26
+ #
27
+ def quantity
28
+ @options['quantity'] || 1
29
+ end
30
+
31
+ #
32
+ # The maximum number of times this process can be respawned in the given period
33
+ #
34
+ def max_respawns
35
+ @options['max_respawns'] ? @options['max_respawns'].to_i : 5
36
+ end
37
+
38
+ #
39
+ # The respawn window. One hour by default.
40
+ #
41
+ def respawn_window
42
+ @options['respawn_window'] ? @options['respawn_window'].to_i : 3600
43
+ end
44
+
45
+ #
46
+ # Defines how this process should be restarted
47
+ #
48
+ # start-term = start new instances and send term to children
49
+ # usr1 = just send a usr1 signal to the current instance
50
+ # usr2 = just send a usr2 signal to the current instance
51
+ # term-start = stop the old instances, when no longer running, start a new one
52
+ #
53
+ def restart_mode
54
+ @options['restart_mode'] || 'term-start'
55
+ end
56
+
57
+ #
58
+ # Generate an array of new instances for this process (based on its quantity)
59
+ #
60
+ def generate_instances
61
+ quantity.times.map { |i| Instance.new(self, i + 1) }
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,32 @@
1
+ module Procodile
2
+ class SignalHandler
3
+
4
+ def self.queue
5
+ Thread.main[:signal_queue] ||= []
6
+ end
7
+
8
+ def initialize(*signals)
9
+ @handlers = {}
10
+ Thread.new do
11
+ loop do
12
+ if signal = self.class.queue.shift
13
+ if @handlers[signal]
14
+ @handlers[signal].each(&:call)
15
+ end
16
+ end
17
+ sleep 1
18
+ end
19
+ end
20
+
21
+ signals.each do |sig|
22
+ Signal.trap(sig, proc { SignalHandler.queue << sig })
23
+ end
24
+ end
25
+
26
+ def register(name, &block)
27
+ @handlers[name] ||= []
28
+ @handlers[name] << block
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,96 @@
1
+ module Procodile
2
+ class Supervisor
3
+
4
+ # Create a new supervisor instance that will be monitoring the
5
+ # processes that have been provided.
6
+ def initialize(config)
7
+ @config = config
8
+ @instances = []
9
+ end
10
+
11
+ def start
12
+ 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
18
+ end
19
+ supervise
20
+ end
21
+
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
33
+ end
34
+
35
+ def restart
36
+ Procodile.log nil, 'system', "Restarting all #{@config.app_name} processes"
37
+ @instances.each(&:restart)
38
+ end
39
+
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)"
45
+ else
46
+ Procodile.log '37;44', 'status', "#{instance.description} is STOPPED"
47
+ end
48
+ end
49
+ end
50
+
51
+ def supervise
52
+ loop do
53
+ # Tidy up any instances that we no longer wish to be managed. They will
54
+ # be removed from the list.
55
+ remove_dead_instances
56
+
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
61
+
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)
72
+ end
73
+
74
+ sleep 5
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def remove_dead_instances
81
+ @instances.reject!(&:dead?)
82
+ end
83
+
84
+ def remove_stopped_instances
85
+ @instances.reject! do |instance|
86
+ if instance.running?
87
+ false
88
+ else
89
+ instance.on_stop
90
+ true
91
+ end
92
+ end
93
+ end
94
+
95
+ end
96
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: procodile
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Adam Cooke
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Run Ruby/Rails processes in the background on Linux servers with ease.
14
+ email:
15
+ - me@adamcooke.io
16
+ executables:
17
+ - procodile
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - bin/procodile
22
+ - lib/procodile.rb
23
+ - lib/procodile/cli.rb
24
+ - lib/procodile/config.rb
25
+ - lib/procodile/error.rb
26
+ - lib/procodile/instance.rb
27
+ - lib/procodile/logger.rb
28
+ - lib/procodile/process.rb
29
+ - lib/procodile/signal_handler.rb
30
+ - lib/procodile/supervisor.rb
31
+ homepage: https://github.com/adamcooke/procodile
32
+ licenses:
33
+ - MIT
34
+ metadata: {}
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 2.5.1
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: This gem will help you run Ruby processes from a Procfile on Linux servers
55
+ in the background.
56
+ test_files: []