invoker 0.1.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|