invoker 0.1.2 → 1.0.0
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 +14 -6
- data/.gitignore +3 -0
- data/Gemfile +0 -1
- data/TODO +5 -0
- data/bin/invoker +0 -3
- data/invoker.gemspec +6 -1
- data/lib/invoker.rb +26 -1
- data/lib/invoker/command_listener/client.rb +1 -1
- data/lib/invoker/command_worker.rb +2 -2
- data/lib/invoker/commander.rb +34 -18
- data/lib/invoker/errors.rb +1 -0
- data/lib/invoker/parsers/config.rb +68 -4
- data/lib/invoker/parsers/option_parser.rb +18 -5
- data/lib/invoker/power.rb +6 -0
- data/lib/invoker/power/balancer.rb +118 -0
- data/lib/invoker/power/config.rb +53 -0
- data/lib/invoker/power/dns.rb +37 -0
- data/lib/invoker/power/port_finder.rb +49 -0
- data/lib/invoker/power/powerup.rb +26 -0
- data/lib/invoker/power/setup.rb +134 -0
- data/lib/invoker/runner.rb +8 -2
- data/lib/invoker/version.rb +1 -1
- data/readme.md +29 -0
- data/spec/invoker/commander_spec.rb +2 -2
- data/spec/invoker/config_spec.rb +106 -2
- data/spec/invoker/invoker_spec.rb +53 -0
- data/spec/invoker/power/config_spec.rb +24 -0
- data/spec/invoker/power/port_finder_spec.rb +16 -0
- data/spec/invoker/power/setup_spec.rb +87 -0
- metadata +104 -18
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YWUzNjRlNGZlNTI4Njg4NjJmNmRiYWQ3NzdkZGRhMDM0MzFjMGYxMg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
N2NkYjJiMGM4YmZjZmM4MzQyYTM4YzgzODg5MWMxN2FiMWI3OTQ5MA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YWRhNDI1YTNkMDczMDk0YzQ4MzM1NzczN2U1ZGQzNDdiZWRjY2Y4MDQyMGZk
|
10
|
+
NjUyY2M0ZDUzMzgzZmJhOWIxY2ZlYmNkNjY0ZTFkYTllMmE1OTQyNWU0OTIz
|
11
|
+
ZTA4YzNlNzUyYmViNWMyMzBiMDc3NzFjZDY4ODE4ZTE2NjBlZGU=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YzgzYzQ4Zjk4ZGMzZTY4ZDFjOGI5N2MxMWMyMGYwMWMwOGZlZWMwZjYxNDAy
|
14
|
+
OWI1MzQ2ZGZkMmQ0ZGFjNzk2Zjc4YWM3ZDhkYTQ3YzY0NGMyYzgwMTM0Mzky
|
15
|
+
ZTJmNTQwZWEyNjM5ZWU0YzU1NjMyYzEwZTI1MjhmYmZjN2Q1NzY=
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/TODO
ADDED
data/bin/invoker
CHANGED
data/invoker.gemspec
CHANGED
@@ -28,8 +28,13 @@ Gem::Specification.new do |s|
|
|
28
28
|
s.summary = %q{Something small for Process management}
|
29
29
|
s.add_dependency("slop", "~> 3.4.6")
|
30
30
|
s.add_dependency("iniparse", "~> 1.1.6")
|
31
|
-
s.add_dependency("colored", "~> 1.2.0")
|
32
31
|
s.add_dependency("formatador", "~> 0.2.4")
|
32
|
+
s.add_dependency("eventmachine", "~> 1.0.3")
|
33
|
+
s.add_dependency("em-proxy", "~> 0.1.8")
|
34
|
+
s.add_dependency("rubydns", "~> 0.6.5")
|
35
|
+
s.add_dependency("uuid", "~> 2.3.7")
|
36
|
+
s.add_dependency("highline", "~> 1.6.19")
|
37
|
+
s.add_dependency("http-parser-lite", "~> 0.6.0")
|
33
38
|
s.add_development_dependency("bacon")
|
34
39
|
s.add_development_dependency("mocha")
|
35
40
|
s.add_development_dependency("mocha-on-bacon")
|
data/lib/invoker.rb
CHANGED
@@ -1,13 +1,18 @@
|
|
1
1
|
$: << File.dirname(__FILE__) unless $:.include?(File.expand_path(File.dirname(__FILE__)))
|
2
2
|
|
3
|
-
require "colored"
|
4
3
|
require "formatador"
|
4
|
+
require 'rubydns'
|
5
|
+
require 'em-proxy'
|
6
|
+
require 'http-parser'
|
5
7
|
require "ostruct"
|
8
|
+
require "uuid"
|
9
|
+
require "highline"
|
6
10
|
require "invoker/version"
|
7
11
|
require "invoker/logger"
|
8
12
|
require "invoker/runner"
|
9
13
|
require "invoker/command_listener/server"
|
10
14
|
require "invoker/command_listener/client"
|
15
|
+
require "invoker/power"
|
11
16
|
require "invoker/errors"
|
12
17
|
require "invoker/parsers/config"
|
13
18
|
require "invoker/parsers/option_parser"
|
@@ -16,3 +21,23 @@ require "invoker/command_worker"
|
|
16
21
|
require "invoker/reactor"
|
17
22
|
require "invoker/event/manager"
|
18
23
|
require "invoker/process_printer"
|
24
|
+
|
25
|
+
module Invoker
|
26
|
+
def self.darwin?
|
27
|
+
ruby_platform.downcase.include?("darwin")
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.ruby_platform
|
31
|
+
RUBY_PLATFORM
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.can_run_balancer?(throw_warning = true)
|
35
|
+
return false unless darwin?
|
36
|
+
return true if File.exists?(Invoker::Power::Config::CONFIG_LOCATION)
|
37
|
+
|
38
|
+
if throw_warning
|
39
|
+
Invoker::Logger.puts("Invoker has detected setup has not been run. Domain feature will not work without running setup command.".color(:red))
|
40
|
+
end
|
41
|
+
false
|
42
|
+
end
|
43
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Invoker
|
2
2
|
class CommandWorker
|
3
3
|
attr_accessor :command_label, :pipe_end, :pid, :color
|
4
|
-
|
4
|
+
|
5
5
|
def initialize(command_label, pipe_end, pid, color)
|
6
6
|
@command_label = command_label
|
7
7
|
@pipe_end = pipe_end
|
@@ -24,7 +24,7 @@ module Invoker
|
|
24
24
|
|
25
25
|
# Print the lines received over the network
|
26
26
|
def receive_line(line)
|
27
|
-
Invoker::Logger.puts "#{@command_label.
|
27
|
+
Invoker::Logger.puts "#{@command_label.color(color)} : #{line}"
|
28
28
|
end
|
29
29
|
|
30
30
|
def to_h
|
data/lib/invoker/commander.rb
CHANGED
@@ -5,17 +5,17 @@ require "json"
|
|
5
5
|
module Invoker
|
6
6
|
class Commander
|
7
7
|
MAX_PROCESS_COUNT = 10
|
8
|
-
LABEL_COLORS = [
|
8
|
+
LABEL_COLORS = [:green, :yellow, :blue, :magenta, :cyan]
|
9
9
|
attr_accessor :reactor, :workers, :thread_group, :open_pipes
|
10
10
|
attr_accessor :event_manager, :runnables
|
11
|
-
|
11
|
+
|
12
12
|
def initialize
|
13
13
|
# mapping between open pipes and worker classes
|
14
14
|
@open_pipes = {}
|
15
15
|
|
16
16
|
# mapping between command label and worker classes
|
17
17
|
@workers = {}
|
18
|
-
|
18
|
+
|
19
19
|
@thread_group = ThreadGroup.new()
|
20
20
|
@worker_mutex = Mutex.new()
|
21
21
|
|
@@ -35,7 +35,9 @@ module Invoker
|
|
35
35
|
install_interrupt_handler()
|
36
36
|
unix_server_thread = Thread.new { Invoker::CommandListener::Server.new() }
|
37
37
|
thread_group.add(unix_server_thread)
|
38
|
+
run_power_server()
|
38
39
|
Invoker::CONFIG.processes.each { |process_info| add_command(process_info) }
|
40
|
+
at_exit { kill_workers }
|
39
41
|
start_event_loop()
|
40
42
|
end
|
41
43
|
|
@@ -65,9 +67,7 @@ module Invoker
|
|
65
67
|
#
|
66
68
|
# @param command_label [String] Command label of process specified in config file.
|
67
69
|
def add_command_by_label(command_label)
|
68
|
-
process_info = Invoker::CONFIG.
|
69
|
-
pconfig.label == command_label
|
70
|
-
}
|
70
|
+
process_info = Invoker::CONFIG.process(command_label)
|
71
71
|
if process_info
|
72
72
|
add_command(process_info)
|
73
73
|
end
|
@@ -97,7 +97,7 @@ module Invoker
|
|
97
97
|
return false unless worker
|
98
98
|
signal_to_use = rest_args ? Array(rest_args).first : 'INT'
|
99
99
|
|
100
|
-
Invoker::Logger.puts("Removing #{command_label} with signal #{signal_to_use}".red)
|
100
|
+
Invoker::Logger.puts("Removing #{command_label} with signal #{signal_to_use}".color(:red))
|
101
101
|
kill_or_remove_process(worker.pid, signal_to_use, command_label)
|
102
102
|
end
|
103
103
|
|
@@ -129,7 +129,19 @@ module Invoker
|
|
129
129
|
end
|
130
130
|
@runnables = []
|
131
131
|
end
|
132
|
-
|
132
|
+
|
133
|
+
def run_power_server
|
134
|
+
return unless Invoker.can_run_balancer?(false)
|
135
|
+
|
136
|
+
powerup_id = Invoker::Power::Powerup.fork_and_start()
|
137
|
+
wait_on_pid("powerup_manager", powerup_id)
|
138
|
+
at_exit {
|
139
|
+
begin
|
140
|
+
Process.kill("INT", powerup_id)
|
141
|
+
rescue Errno::ESRCH; end
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
133
145
|
private
|
134
146
|
def start_event_loop
|
135
147
|
loop do
|
@@ -152,7 +164,7 @@ module Invoker
|
|
152
164
|
remove_worker(command_label, false)
|
153
165
|
false
|
154
166
|
end
|
155
|
-
|
167
|
+
|
156
168
|
def process_kill(pid, signal_to_use)
|
157
169
|
if signal_to_use.to_i == 0
|
158
170
|
Process.kill(signal_to_use, pid)
|
@@ -166,7 +178,7 @@ module Invoker
|
|
166
178
|
LABEL_COLORS.push(selected_color)
|
167
179
|
selected_color
|
168
180
|
end
|
169
|
-
|
181
|
+
|
170
182
|
# Remove worker from all collections
|
171
183
|
def remove_worker(command_label, trigger_event = true)
|
172
184
|
worker = @workers[command_label]
|
@@ -210,7 +222,7 @@ module Invoker
|
|
210
222
|
thread = Thread.new do
|
211
223
|
Process.wait(pid)
|
212
224
|
message = "Process with command #{command_label} exited with status #{$?.exitstatus}"
|
213
|
-
Invoker::Logger.puts("\n#{message}".red)
|
225
|
+
Invoker::Logger.puts("\n#{message}".color(:red))
|
214
226
|
notify_user(message)
|
215
227
|
event_manager.trigger(command_label, :exit)
|
216
228
|
end
|
@@ -228,7 +240,7 @@ module Invoker
|
|
228
240
|
end
|
229
241
|
|
230
242
|
def check_and_notify_with_terminal_notifier(message)
|
231
|
-
return unless
|
243
|
+
return unless Invoker.darwin?
|
232
244
|
|
233
245
|
command_path = `which terminal-notifier`
|
234
246
|
if command_path && !command_path.empty?
|
@@ -238,15 +250,19 @@ module Invoker
|
|
238
250
|
|
239
251
|
def install_interrupt_handler
|
240
252
|
Signal.trap("INT") do
|
241
|
-
|
242
|
-
begin
|
243
|
-
Process.kill("INT", worker.pid)
|
244
|
-
rescue Errno::ESRCH
|
245
|
-
end
|
246
|
-
}
|
253
|
+
kill_workers()
|
247
254
|
exit(0)
|
248
255
|
end
|
249
256
|
end
|
250
257
|
|
258
|
+
def kill_workers
|
259
|
+
@workers.each {|key,worker|
|
260
|
+
begin
|
261
|
+
Process.kill("INT", worker.pid)
|
262
|
+
rescue Errno::ESRCH
|
263
|
+
end
|
264
|
+
}
|
265
|
+
@workers = {}
|
266
|
+
end
|
251
267
|
end
|
252
268
|
end
|
data/lib/invoker/errors.rb
CHANGED
@@ -3,10 +3,30 @@ require 'iniparse'
|
|
3
3
|
module Invoker
|
4
4
|
module Parsers
|
5
5
|
class Config
|
6
|
-
|
7
|
-
|
6
|
+
PORT_REGEX = /\$PORT/
|
7
|
+
attr_accessor :processes, :power_config
|
8
|
+
|
9
|
+
def initialize(filename, port)
|
8
10
|
@ini_content = File.read(filename)
|
11
|
+
@port = port
|
9
12
|
@processes = process_ini(@ini_content)
|
13
|
+
if Invoker.can_run_balancer?
|
14
|
+
@power_config = Invoker::Power::Config.load_config()
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def http_port
|
19
|
+
power_config && power_config.http_port
|
20
|
+
end
|
21
|
+
|
22
|
+
def dns_port
|
23
|
+
power_config && power_config.dns_port
|
24
|
+
end
|
25
|
+
|
26
|
+
def process(label)
|
27
|
+
processes.detect {|pconfig|
|
28
|
+
pconfig.label == label
|
29
|
+
}
|
10
30
|
end
|
11
31
|
|
12
32
|
private
|
@@ -14,16 +34,60 @@ module Invoker
|
|
14
34
|
document = IniParse.parse(ini_content)
|
15
35
|
document.map do |section|
|
16
36
|
check_directory(section["directory"])
|
17
|
-
|
37
|
+
if supports_subdomain?(section)
|
38
|
+
port = pick_port(section)
|
39
|
+
make_option_for_subdomain(section, port)
|
40
|
+
else
|
41
|
+
make_option(section)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def pick_port(section)
|
47
|
+
if section['command'] =~ PORT_REGEX
|
48
|
+
@port += 1
|
49
|
+
elsif section['port']
|
50
|
+
section['port']
|
51
|
+
else
|
52
|
+
nil
|
18
53
|
end
|
19
54
|
end
|
20
55
|
|
56
|
+
def make_option_for_subdomain(section, port)
|
57
|
+
OpenStruct.new(
|
58
|
+
port: port,
|
59
|
+
label: section.key,
|
60
|
+
dir: section["directory"],
|
61
|
+
cmd: replace_port_in_command(section["command"], port)
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
def make_option(section)
|
66
|
+
OpenStruct.new(
|
67
|
+
label: section.key,
|
68
|
+
dir: section["directory"],
|
69
|
+
cmd: section["command"]
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
def supports_subdomain?(section)
|
74
|
+
(section['command'] =~ PORT_REGEX) || section['port']
|
75
|
+
end
|
76
|
+
|
21
77
|
def check_directory(app_dir)
|
22
78
|
if app_dir && !app_dir.empty? && !File.directory?(app_dir)
|
23
79
|
raise Invoker::Errors::InvalidConfig.new("Invalid directory #{app_dir}")
|
24
80
|
end
|
25
81
|
end
|
82
|
+
|
83
|
+
def replace_port_in_command(command, port)
|
84
|
+
if command =~ PORT_REGEX
|
85
|
+
command.gsub(PORT_REGEX, port.to_s)
|
86
|
+
else
|
87
|
+
command
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
26
91
|
end
|
27
92
|
end
|
28
93
|
end
|
29
|
-
|
@@ -6,16 +6,29 @@ module Invoker
|
|
6
6
|
class OptionParser
|
7
7
|
def self.parse(args)
|
8
8
|
selected_command = nil
|
9
|
-
|
10
9
|
opts = Slop.parse(args, help: true) do
|
11
10
|
on :v, "Print the version" do
|
12
11
|
Invoker::Logger.puts Invoker::VERSION
|
13
12
|
end
|
13
|
+
on :p, :port=, "Port series to be used for starting rack servers", as: Integer
|
14
14
|
|
15
15
|
command 'start' do
|
16
16
|
banner "Usage : invoker start config.ini \n Start Invoker Process Manager"
|
17
|
+
on :p, :port=, "Port series to be used for starting rack servers", as: Integer
|
17
18
|
run do |cmd_opts, cmd_args|
|
18
|
-
|
19
|
+
port = cmd_opts.to_hash[:port] || 9000
|
20
|
+
selected_command = OpenStruct.new(
|
21
|
+
:command => 'start',
|
22
|
+
:file => cmd_args.first,
|
23
|
+
:port => port
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
command 'setup' do
|
29
|
+
banner "Usage : invoker setup \n Sets up firewall rules for subdomains"
|
30
|
+
run do |cmd_opts, cmd_args|
|
31
|
+
selected_command = OpenStruct.new(:command => 'setup')
|
19
32
|
end
|
20
33
|
end
|
21
34
|
|
@@ -54,7 +67,7 @@ module Invoker
|
|
54
67
|
run do |cmd_opts, cmd_args|
|
55
68
|
signal_to_use = cmd_opts.to_hash[:signal] || 'INT'
|
56
69
|
selected_command = OpenStruct.new(
|
57
|
-
:command => 'reload',
|
70
|
+
:command => 'reload',
|
58
71
|
:command_key => cmd_args.first,
|
59
72
|
:signal => signal_to_use
|
60
73
|
)
|
@@ -65,7 +78,6 @@ module Invoker
|
|
65
78
|
selected_command || create_default_command(args, opts)
|
66
79
|
end
|
67
80
|
|
68
|
-
|
69
81
|
# If user specifies no command either show help message or start the invoker
|
70
82
|
# process supervisor.
|
71
83
|
#
|
@@ -74,7 +86,8 @@ module Invoker
|
|
74
86
|
# @return [OpenStruct, false] returns default command or nil
|
75
87
|
def self.create_default_command(args, opts)
|
76
88
|
if args.first && File.exists?(args.first) && File.file?(args.first)
|
77
|
-
|
89
|
+
port = opts.to_hash[:port] || 9000
|
90
|
+
OpenStruct.new(:command => "start", :file => args.first, :port => port)
|
78
91
|
else
|
79
92
|
Invoker::Logger.puts opts.inspect
|
80
93
|
false
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Invoker
|
2
|
+
module Power
|
3
|
+
class BalancerConnection < EventMachine::ProxyServer::Connection
|
4
|
+
attr_accessor :host, :ip, :port
|
5
|
+
def set_host(host, selected_backend)
|
6
|
+
self.host = host
|
7
|
+
self.ip = selected_backend[:host]
|
8
|
+
self.port = selected_backend[:port]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class BalancerParser
|
13
|
+
attr_accessor :host, :parser
|
14
|
+
def initialize
|
15
|
+
@parser = HTTP::Parser.new()
|
16
|
+
@header = {}
|
17
|
+
@parser.on_headers_complete { headers_received() }
|
18
|
+
@parser.on_header_field { |field_name|
|
19
|
+
@last_key = field_name
|
20
|
+
}
|
21
|
+
@parser.on_header_value { |value| header_value_received(value) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def headers_received
|
25
|
+
@header_completion_callback.call(@header)
|
26
|
+
end
|
27
|
+
|
28
|
+
def header_value_received(value)
|
29
|
+
@header[@last_key] = value
|
30
|
+
end
|
31
|
+
|
32
|
+
def on_headers_complete(&block)
|
33
|
+
@header_completion_callback = block
|
34
|
+
end
|
35
|
+
|
36
|
+
def reset; @parser.reset(); end
|
37
|
+
|
38
|
+
def <<(data)
|
39
|
+
@parser << data
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Balancer
|
44
|
+
attr_accessor :connection, :http_parser, :session
|
45
|
+
|
46
|
+
def self.run(options = {})
|
47
|
+
EventMachine.start_server('0.0.0.0', Invoker::CONFIG.http_port,
|
48
|
+
BalancerConnection, options) do |connection|
|
49
|
+
balancer = Balancer.new(connection)
|
50
|
+
balancer.install_callbacks
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(connection)
|
55
|
+
@connection = connection
|
56
|
+
@http_parser = BalancerParser.new()
|
57
|
+
@session = nil
|
58
|
+
@buffer = []
|
59
|
+
end
|
60
|
+
|
61
|
+
def install_callbacks
|
62
|
+
http_parser.on_headers_complete { |header| headers_received(header) }
|
63
|
+
connection.on_data {|data| upstream_data(data) }
|
64
|
+
connection.on_response { |backend, data| backend_data(backend, data) }
|
65
|
+
connection.on_finish { |backend, name| frontend_disconnect(backend, name) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def headers_received(header)
|
69
|
+
@session = UUID.generate()
|
70
|
+
config = select_backend_config(header['Host'])
|
71
|
+
if config
|
72
|
+
connection.server(session, host: '0.0.0.0', port: config.port)
|
73
|
+
connection.relay_to_servers(@buffer.join)
|
74
|
+
@buffer = []
|
75
|
+
else
|
76
|
+
connection.unbind
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def upstream_data(data)
|
81
|
+
unless session
|
82
|
+
@buffer << data
|
83
|
+
append_for_http_parsing(data)
|
84
|
+
nil
|
85
|
+
else
|
86
|
+
data
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def append_for_http_parsing(data)
|
91
|
+
http_parser << data
|
92
|
+
rescue HTTP::Parser::Error
|
93
|
+
http_parser.reset
|
94
|
+
connection.close_connection_after_writing
|
95
|
+
end
|
96
|
+
|
97
|
+
def backend_data(backend, data)
|
98
|
+
data
|
99
|
+
end
|
100
|
+
|
101
|
+
def frontend_disconnect(backend, name)
|
102
|
+
http_parser.reset()
|
103
|
+
connection.close_connection_after_writing() if backend == session
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
def select_backend_config(host)
|
108
|
+
matching_string = host.match(/(\w+)\.dev(\:\d+)?$/)
|
109
|
+
return nil unless matching_string
|
110
|
+
if selected_app = matching_string[1]
|
111
|
+
Invoker::CONFIG.process(selected_app)
|
112
|
+
else
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|