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 CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: cb5db9a4721bdd2207ddcc8df052b13535bb9ff2
4
- data.tar.gz: 1595dbd8535de9debb91405e6538eaa2ecc94533
5
- SHA512:
6
- metadata.gz: 799b0482eef58b4c28c4db4bcc6d643488ee37a11597960b1d866b8a8d5469381cc22aba13ca39a6457006242224dff401050979775acddb3061cf2b317a65b8
7
- data.tar.gz: 614f1643928310cc009886cc42cc5552508391fd553c7b555c60e3e867bd4d8a65a7ad45181215c968850d942dc6bf68d8dad2fac0ae73c944cad4d1b9749b90
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
@@ -3,3 +3,6 @@ config/
3
3
  *.gem
4
4
  Gemfile.lock
5
5
  pkg/
6
+ local.ini
7
+ multi.ini
8
+ tags
data/Gemfile CHANGED
@@ -3,4 +3,3 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem 'pry'
6
-
data/TODO ADDED
@@ -0,0 +1,5 @@
1
+ Todos for current release
2
+
3
+ * Implement port support via config file
4
+ * Fix setup command and make it eaiser for people who have pow configured.
5
+ * Possibly add support for foreman?
data/bin/invoker CHANGED
@@ -9,6 +9,3 @@ rescue LoadError
9
9
  end
10
10
 
11
11
  Invoker::Runner.run(ARGV)
12
-
13
-
14
-
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
@@ -37,7 +37,7 @@ module Invoker
37
37
  reload_command(b_command_label, b_rest_args)
38
38
  }
39
39
  else
40
- Invoker::Logger.puts("\n Invalid command".red)
40
+ Invoker::Logger.puts("\n Invalid command".color(:red))
41
41
  end
42
42
  end
43
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.send(color)} : #{line}"
27
+ Invoker::Logger.puts "#{@command_label.color(color)} : #{line}"
28
28
  end
29
29
 
30
30
  def to_h
@@ -5,17 +5,17 @@ require "json"
5
5
  module Invoker
6
6
  class Commander
7
7
  MAX_PROCESS_COUNT = 10
8
- LABEL_COLORS = ['green', 'yellow', 'blue', 'magenta', 'cyan']
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.processes.detect {|pconfig|
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 RUBY_PLATFORM.downcase.include?("darwin")
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
- @workers.each {|key,worker|
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
@@ -9,6 +9,7 @@ module Invoker
9
9
  end
10
10
  end
11
11
 
12
+ class NoValidPortFound < StandardError; end
12
13
  class InvalidConfig < StandardError; end
13
14
  end
14
15
  end
@@ -3,10 +3,30 @@ require 'iniparse'
3
3
  module Invoker
4
4
  module Parsers
5
5
  class Config
6
- attr_accessor :processes
7
- def initialize(filename)
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
- OpenStruct.new(label: section.key, dir: section["directory"], cmd: section["command"])
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
- selected_command = OpenStruct.new(:command => 'start', :file => cmd_args.first)
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
- OpenStruct.new(:command => "start", :file => args.first)
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,6 @@
1
+ require "invoker/power/config"
2
+ require "invoker/power/port_finder"
3
+ require "invoker/power/setup"
4
+ require "invoker/power/dns"
5
+ require "invoker/power/balancer"
6
+ require "invoker/power/powerup"
@@ -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