invoker 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -0
  3. data/.rubocop.yml +30 -0
  4. data/.travis.yml +1 -0
  5. data/Gemfile +1 -0
  6. data/bin/invoker +4 -8
  7. data/invoker.gemspec +10 -11
  8. data/lib/invoker.rb +95 -21
  9. data/lib/invoker/cli.rb +126 -0
  10. data/lib/invoker/cli/pinger.rb +23 -0
  11. data/lib/invoker/cli/question.rb +15 -0
  12. data/lib/invoker/cli/tail.rb +34 -0
  13. data/lib/invoker/cli/tail_watcher.rb +34 -0
  14. data/lib/invoker/command_worker.rb +28 -2
  15. data/lib/invoker/commander.rb +34 -236
  16. data/lib/invoker/config.rb +5 -0
  17. data/lib/invoker/daemon.rb +126 -0
  18. data/lib/invoker/dns_cache.rb +23 -0
  19. data/lib/invoker/errors.rb +1 -0
  20. data/lib/invoker/ipc.rb +45 -0
  21. data/lib/invoker/ipc/add_command.rb +12 -0
  22. data/lib/invoker/ipc/add_http_command.rb +10 -0
  23. data/lib/invoker/ipc/base_command.rb +24 -0
  24. data/lib/invoker/ipc/client_handler.rb +26 -0
  25. data/lib/invoker/ipc/dns_check_command.rb +16 -0
  26. data/lib/invoker/ipc/list_command.rb +11 -0
  27. data/lib/invoker/ipc/message.rb +170 -0
  28. data/lib/invoker/ipc/message/list_response.rb +33 -0
  29. data/lib/invoker/ipc/message/tail_response.rb +10 -0
  30. data/lib/invoker/ipc/ping_command.rb +10 -0
  31. data/lib/invoker/ipc/reload_command.rb +12 -0
  32. data/lib/invoker/ipc/remove_command.rb +12 -0
  33. data/lib/invoker/{command_listener → ipc}/server.rb +6 -11
  34. data/lib/invoker/ipc/tail_command.rb +11 -0
  35. data/lib/invoker/ipc/unix_client.rb +60 -0
  36. data/lib/invoker/parsers/config.rb +1 -0
  37. data/lib/invoker/power/balancer.rb +17 -7
  38. data/lib/invoker/power/config.rb +6 -3
  39. data/lib/invoker/power/dns.rb +22 -21
  40. data/lib/invoker/power/http_response.rb +1 -1
  41. data/lib/invoker/power/power.rb +3 -0
  42. data/lib/invoker/power/powerup.rb +3 -2
  43. data/lib/invoker/power/setup.rb +6 -4
  44. data/lib/invoker/process_manager.rb +187 -0
  45. data/lib/invoker/process_printer.rb +27 -38
  46. data/lib/invoker/reactor.rb +19 -38
  47. data/lib/invoker/reactor/reader.rb +53 -0
  48. data/lib/invoker/version.rb +1 -1
  49. data/readme.md +1 -1
  50. data/spec/invoker/cli/pinger_spec.rb +22 -0
  51. data/spec/invoker/cli/tail_watcher_spec.rb +39 -0
  52. data/spec/invoker/cli_spec.rb +27 -0
  53. data/spec/invoker/command_worker_spec.rb +30 -0
  54. data/spec/invoker/commander_spec.rb +57 -127
  55. data/spec/invoker/config_spec.rb +21 -0
  56. data/spec/invoker/daemon_spec.rb +34 -0
  57. data/spec/invoker/invoker_spec.rb +31 -0
  58. data/spec/invoker/ipc/client_handler_spec.rb +44 -0
  59. data/spec/invoker/ipc/dns_check_command_spec.rb +32 -0
  60. data/spec/invoker/ipc/message/list_response_spec.rb +22 -0
  61. data/spec/invoker/ipc/message_spec.rb +45 -0
  62. data/spec/invoker/ipc/unix_client_spec.rb +29 -0
  63. data/spec/invoker/power/setup_spec.rb +1 -1
  64. data/spec/invoker/process_manager_spec.rb +98 -0
  65. data/spec/invoker/reactor_spec.rb +6 -0
  66. data/spec/spec_helper.rb +15 -24
  67. metadata +107 -77
  68. data/lib/invoker/command_listener/client.rb +0 -45
  69. data/lib/invoker/parsers/option_parser.rb +0 -106
  70. data/lib/invoker/power.rb +0 -7
  71. data/lib/invoker/runner.rb +0 -98
  72. data/spec/invoker/command_listener/client_spec.rb +0 -52
@@ -0,0 +1,11 @@
1
+ module Invoker
2
+ module IPC
3
+ class TailCommand < BaseCommand
4
+ def run_command(message_object)
5
+ Invoker::Logger.puts("Adding #{message_object.process_names.inspect}")
6
+ Invoker.tail_watchers.add(message_object.process_names, client_socket)
7
+ false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,60 @@
1
+ module Invoker
2
+ module IPC
3
+ class UnixClient
4
+ def send_command(command, message = {})
5
+ message_object = get_message_object(command, message)
6
+ open_client_socket do |socket|
7
+ send_json_message(socket, message_object)
8
+ socket.flush
9
+ if block_given?
10
+ response_object = Invoker::IPC.message_from_io(socket)
11
+ yield response_object
12
+ end
13
+ end
14
+ end
15
+
16
+ def send_and_receive(command, message = {})
17
+ response = nil
18
+ message_object = get_message_object(command, message)
19
+ open_client_socket(false) do |socket|
20
+ send_json_message(socket, message_object)
21
+ socket.flush
22
+ response = Invoker::IPC.message_from_io(socket)
23
+ end
24
+ response
25
+ end
26
+
27
+ def send_and_wait(command, message = {})
28
+ begin
29
+ socket = Socket.unix(Invoker::IPC::Server::SOCKET_PATH)
30
+ rescue
31
+ abort("Invoker does not seem to be running".color(:red))
32
+ end
33
+ message_object = get_message_object(command, message)
34
+ send_json_message(socket, message_object)
35
+ socket.flush
36
+ socket
37
+ end
38
+
39
+ def self.send_command(command, message_arguments = {}, &block)
40
+ new.send_command(command, message_arguments, &block)
41
+ end
42
+
43
+ private
44
+
45
+ def get_message_object(command, message_arguments)
46
+ Invoker::IPC::Message.const_get(Invoker::IPC.camelize(command)).new(message_arguments)
47
+ end
48
+
49
+ def open_client_socket(abort_if_not_running = true)
50
+ Socket.unix(Invoker::IPC::Server::SOCKET_PATH) { |socket| yield socket }
51
+ rescue
52
+ abort_if_not_running && abort("Invoker does not seem to be running".color(:red))
53
+ end
54
+
55
+ def send_json_message(socket, message_object)
56
+ socket.write(message_object.encoded_message)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -28,6 +28,7 @@ module Invoker
28
28
  end
29
29
 
30
30
  private
31
+
31
32
  def load_config
32
33
  if is_ini?
33
34
  process_ini
@@ -1,3 +1,6 @@
1
+ require 'em-proxy'
2
+ require 'http-parser'
3
+
1
4
  module Invoker
2
5
  module Power
3
6
  class BalancerConnection < EventMachine::ProxyServer::Connection
@@ -45,7 +48,7 @@ module Invoker
45
48
  DEV_MATCH_REGEX = /([\w-]+)\.dev(\:\d+)?$/
46
49
 
47
50
  def self.run(options = {})
48
- EventMachine.start_server('0.0.0.0', Invoker::CONFIG.http_port,
51
+ EventMachine.start_server('0.0.0.0', Invoker.config.http_port,
49
52
  BalancerConnection, options) do |connection|
50
53
  balancer = Balancer.new(connection)
51
54
  balancer.install_callbacks
@@ -68,9 +71,9 @@ module Invoker
68
71
 
69
72
  def headers_received(header)
70
73
  @session = UUID.generate()
71
- config = select_backend_config(header['Host'])
72
- if config
73
- connection.server(session, host: '0.0.0.0', port: config.port)
74
+ dns_check_response = select_backend_config(header['Host'])
75
+ if dns_check_response && dns_check_response.port
76
+ connection.server(session, host: '0.0.0.0', port: dns_check_response.port)
74
77
  connection.relay_to_servers(@buffer.join)
75
78
  @buffer = []
76
79
  else
@@ -102,13 +105,13 @@ module Invoker
102
105
  end
103
106
 
104
107
  def frontend_disconnect(backend, name)
105
- http_parser.reset()
108
+ http_parser.reset
106
109
  unless @backend_data
107
110
  Invoker::Logger.puts("\nApplication not running. Returning error page.".color(:red))
108
111
  return_error_page(503)
109
112
  end
110
113
  @backend_data = false
111
- connection.close_connection_after_writing() if backend == session
114
+ connection.close_connection_after_writing if backend == session
112
115
  end
113
116
 
114
117
  def extract_host_from_domain(host)
@@ -116,16 +119,23 @@ module Invoker
116
119
  end
117
120
 
118
121
  private
122
+
119
123
  def select_backend_config(host)
120
124
  matching_string = extract_host_from_domain(host)
121
125
  return nil unless matching_string
122
126
  if selected_app = matching_string[1]
123
- Invoker::CONFIG.process(selected_app)
127
+ dns_check(process_name: selected_app)
124
128
  else
125
129
  nil
126
130
  end
127
131
  end
128
132
 
133
+ def dns_check(dns_args)
134
+ Invoker::IPC::UnixClient.send_command("dns_check", dns_args) do |dns_response|
135
+ dns_response
136
+ end
137
+ end
138
+
129
139
  def return_error_page(status)
130
140
  http_response = Invoker::Power::HttpResponse.new()
131
141
  http_response.status = status
@@ -1,12 +1,15 @@
1
1
  require "yaml"
2
+
2
3
  module Invoker
3
4
  module Power
4
5
  # Save and Load Invoker::Power config
5
6
  class ConfigExists < StandardError; end
7
+
6
8
  class Config
7
- CONFIG_LOCATION = File.join(ENV['HOME'], ".invoker")
9
+ CONFIG_LOCATION = File.join(Dir.home, ".invoker", "config")
10
+
8
11
  def self.has_config?
9
- File.exists?(CONFIG_LOCATION)
12
+ File.exist?(CONFIG_LOCATION)
10
13
  end
11
14
 
12
15
  def self.create(options = {})
@@ -18,7 +21,7 @@ module Invoker
18
21
  end
19
22
 
20
23
  def self.delete
21
- if File.exists?(CONFIG_LOCATION)
24
+ if File.exist?(CONFIG_LOCATION)
22
25
  File.delete(CONFIG_LOCATION)
23
26
  end
24
27
  end
@@ -1,37 +1,38 @@
1
1
  require "logger"
2
+ require 'rubydns'
2
3
 
3
4
  module Invoker
4
-
5
5
  module Power
6
-
7
- class DNS
8
- IN = Resolv::DNS::Resource::IN
6
+ class DNS < RubyDNS::Server
9
7
  def self.server_ports
10
8
  [
11
- [:udp, '127.0.0.1', Invoker::CONFIG.dns_port],
12
- [:tcp, '127.0.0.1', Invoker::CONFIG.dns_port]
9
+ [:udp, '127.0.0.1', Invoker.config.dns_port],
10
+ [:tcp, '127.0.0.1', Invoker.config.dns_port]
13
11
  ]
14
12
  end
15
13
 
16
- def self.run_dns
17
- RubyDNS::run_server(:listen => server_ports) do
18
- on(:start) do
19
- @logger.level = ::Logger::WARN
20
- end
21
-
22
- # For this exact address record, return an IP address
23
- match(/.*\.dev/, IN::A) do |transaction|
24
- transaction.respond!("127.0.0.1")
25
- end
14
+ def initialize
15
+ @logger = ::Logger.new($stderr)
16
+ @logger.level = ::Logger::FATAL
17
+ end
26
18
 
27
- # Default DNS handler
28
- otherwise do |transaction|
29
- transaction.failure!(:NXDomain)
30
- end
19
+ def process(name, resource_class, transaction)
20
+ if name_matches?(name) && resource_class_matches?(resource_class)
21
+ transaction.respond!("127.0.0.1")
22
+ else
23
+ transaction.fail!(:NXDomain)
31
24
  end
32
25
  end
33
26
 
34
- end
27
+ private
35
28
 
29
+ def resource_class_matches?(resource_class)
30
+ resource_class == Resolv::DNS::Resource::IN::A
31
+ end
32
+
33
+ def name_matches?(name)
34
+ name =~ /.*\.dev/
35
+ end
36
+ end
36
37
  end
37
38
  end
@@ -56,7 +56,7 @@ module Invoker
56
56
  file_content = File.read(file_name)
57
57
  self.body = file_content
58
58
  else
59
- raise Invoker::Errors:InvalidFile, "Invalid file as body"
59
+ raise Invoker::Errors::InvalidFile, "Invalid file as body"
60
60
  end
61
61
  end
62
62
 
@@ -0,0 +1,3 @@
1
+ require "invoker/power/http_response"
2
+ require "invoker/power/dns"
3
+ require "invoker/power/balancer"
@@ -8,12 +8,13 @@ module Invoker
8
8
  end
9
9
 
10
10
  def run
11
+ require "invoker/power/power"
11
12
  EM.epoll
12
13
  EM.run {
13
14
  trap("TERM") { stop }
14
15
  trap("INT") { stop }
15
- DNS.run_dns()
16
- Balancer.run()
16
+ DNS.new.run(listen: DNS.server_ports)
17
+ Balancer.run
17
18
  }
18
19
  end
19
20
 
@@ -1,4 +1,4 @@
1
- require "highline/import"
1
+ require "eventmachine"
2
2
 
3
3
  module Invoker
4
4
  module Power
@@ -42,7 +42,7 @@ module Invoker
42
42
  end
43
43
 
44
44
  def uninstall_invoker
45
- uninstall_invoker_flag = agree("Are you sure you want to uninstall firewall rules created by setup (y/n) : ")
45
+ uninstall_invoker_flag = Invoker::CLI::Question.agree("Are you sure you want to uninstall firewall rules created by setup (y/n) : ")
46
46
 
47
47
  if uninstall_invoker_flag
48
48
  remove_resolver_file
@@ -62,6 +62,7 @@ module Invoker
62
62
  end
63
63
 
64
64
  def create_config_file
65
+ Invoker.setup_config_location
65
66
  Invoker::Power::Config.create(
66
67
  dns_port: port_finder.dns_port,
67
68
  http_port: port_finder.http_port
@@ -162,7 +163,7 @@ port #{dns_port}
162
163
  Invoker::Logger.puts "If you have already uninstalled Pow, proceed with installation"\
163
164
  " by pressing y/n."
164
165
 
165
- replace_resolver_flag = agree("Replace Pow configuration (y/n) : ")
166
+ replace_resolver_flag = Invoker::CLI::Question.agree("Replace Pow configuration (y/n) : ")
166
167
 
167
168
  if replace_resolver_flag
168
169
  Invoker::Logger.puts "Invoker has overwritten one or more files created by Pow. "\
@@ -173,12 +174,13 @@ port #{dns_port}
173
174
  end
174
175
 
175
176
  private
177
+
176
178
  def open_resolver_for_write
177
179
  FileUtils.mkdir(RESOLVER_DIR) unless Dir.exists?(RESOLVER_DIR)
178
180
  fl = File.open(RESOLVER_FILE, "w")
179
181
  yield fl
180
182
  ensure
181
- fl && fl.close()
183
+ fl && fl.close
182
184
  end
183
185
 
184
186
  end
@@ -0,0 +1,187 @@
1
+ module Invoker
2
+ # Class is responsible for managing all the processes Invoker is supposed
3
+ # to manage. Takes care of starting, stopping and restarting processes.
4
+ class ProcessManager
5
+ LABEL_COLORS = [:green, :yellow, :blue, :magenta, :cyan]
6
+ attr_accessor :open_pipes, :workers
7
+
8
+ def initialize
9
+ @open_pipes = {}
10
+ @workers = {}
11
+ @worker_mutex = Mutex.new
12
+ @thread_group = ThreadGroup.new
13
+ end
14
+
15
+ def start_process(process_info)
16
+ m, s = PTY.open
17
+ s.raw! # disable newline conversion.
18
+
19
+ pid = run_command(process_info, s)
20
+
21
+ s.close
22
+
23
+ worker = CommandWorker.new(process_info.label, m, pid, select_color)
24
+
25
+ add_worker(worker)
26
+ wait_on_pid(process_info.label, pid)
27
+ end
28
+
29
+ # Start a process given their name
30
+ # @param process_name [String] Command label of process specified in config file.
31
+ def start_process_by_name(process_name)
32
+ if process_running?(process_name)
33
+ Invoker::Logger.puts "\nProcess '#{process_name}' is already running".color(:red)
34
+ return false
35
+ end
36
+
37
+ process_info = Invoker.config.process(process_name)
38
+ start_process(process_info) if process_info
39
+ end
40
+
41
+ # Remove a process from list of processes managed by invoker supervisor.It also
42
+ # kills the process before removing it from the list.
43
+ #
44
+ # @param remove_message [Invoker::IPC::Message::Remove]
45
+ # @return [Boolean] if process existed and was removed else false
46
+ def stop_process(remove_message)
47
+ worker = workers[remove_message.process_name]
48
+ command_label = remove_message.process_name
49
+ return false unless worker
50
+ signal_to_use = remove_message.signal || 'INT'
51
+
52
+ Invoker::Logger.puts("Removing #{command_label} with signal #{signal_to_use}".color(:red))
53
+ kill_or_remove_process(worker.pid, signal_to_use, command_label)
54
+ end
55
+
56
+ # Receive a message from user to restart a Process
57
+ # @param [Invoker::IPC::Message::Reload]
58
+ def restart_process(reload_message)
59
+ command_label = reload_message.process_name
60
+ if stop_process(reload_message.remove_message)
61
+ Invoker.commander.schedule_event(command_label, :worker_removed) do
62
+ start_process_by_name(command_label)
63
+ end
64
+ else
65
+ start_process_by_name(command_label)
66
+ end
67
+ end
68
+
69
+ def run_power_server
70
+ return unless Invoker.can_run_balancer?(false)
71
+
72
+ powerup_id = Invoker::Power::Powerup.fork_and_start
73
+ wait_on_pid("powerup_manager", powerup_id)
74
+ at_exit do
75
+ begin
76
+ Process.kill("INT", powerup_id)
77
+ rescue Errno::ESRCH; end
78
+ end
79
+ end
80
+
81
+ # Given a file descriptor returns the worker object
82
+ #
83
+ # @param fd [IO] an IO object with valid file descriptor
84
+ # @return [Invoker::CommandWorker] The worker object which is associated with this fd
85
+ def get_worker_from_fd(fd)
86
+ open_pipes[fd.fileno]
87
+ end
88
+
89
+ def load_env(directory = nil)
90
+ directory ||= ENV['PWD']
91
+ default_env = File.join(directory, '.env')
92
+ return {} unless File.exist?(default_env)
93
+ Dotenv::Environment.new(default_env)
94
+ end
95
+
96
+ def kill_workers
97
+ @workers.each do |key, worker|
98
+ begin
99
+ Process.kill("INT", -Process.getpgid(worker.pid))
100
+ rescue Errno::ESRCH
101
+ Invoker::Logger.puts "Error killing #{key}"
102
+ end
103
+ end
104
+ @workers = {}
105
+ end
106
+
107
+ # List currently running commands
108
+ def process_list
109
+ Invoker::IPC::Message::ListResponse.from_workers(workers)
110
+ end
111
+
112
+ private
113
+
114
+ def wait_on_pid(command_label, pid)
115
+ raise Invoker::Errors::ToomanyOpenConnections if @thread_group.enclosed?
116
+
117
+ thread = Thread.new do
118
+ Process.wait(pid)
119
+ message = "Process with command #{command_label} exited with status #{$?.exitstatus}"
120
+ Invoker::Logger.puts("\n#{message}".color(:red))
121
+ Invoker.notify_user(message)
122
+ Invoker.commander.trigger(command_label, :exit)
123
+ end
124
+ @thread_group.add(thread)
125
+ end
126
+
127
+ def select_color
128
+ selected_color = LABEL_COLORS.shift
129
+ LABEL_COLORS.push(selected_color)
130
+ selected_color
131
+ end
132
+
133
+ def process_running?(command_label)
134
+ !!workers[command_label]
135
+ end
136
+
137
+ def kill_or_remove_process(pid, signal_to_use, command_label)
138
+ process_kill(pid, signal_to_use)
139
+ true
140
+ rescue Errno::ESRCH
141
+ Invoker::Logger.puts("Killing process with #{pid} and name #{command_label} failed".color(:red))
142
+ remove_worker(command_label, false)
143
+ false
144
+ end
145
+
146
+ def process_kill(pid, signal_to_use)
147
+ if signal_to_use.to_i == 0
148
+ Process.kill(signal_to_use, -Process.getpgid(pid))
149
+ else
150
+ Process.kill(signal_to_use.to_i, -Process.getpgid(pid))
151
+ end
152
+ end
153
+
154
+ # Remove worker from all collections
155
+ def remove_worker(command_label, trigger_event = true)
156
+ worker = @workers[command_label]
157
+ if worker
158
+ @open_pipes.delete(worker.pipe_end.fileno)
159
+ @workers.delete(command_label)
160
+ end
161
+ if trigger_event
162
+ Invoker.commander.trigger(command_label, :worker_removed)
163
+ end
164
+ end
165
+
166
+ # add worker to global collections
167
+ def add_worker(worker)
168
+ @open_pipes[worker.pipe_end.fileno] = worker
169
+ @workers[worker.command_label] = worker
170
+ Invoker.commander.watch_for_read(worker.pipe_end)
171
+ end
172
+
173
+ def run_command(process_info, write_pipe)
174
+ command_label = process_info.label
175
+
176
+ Invoker.commander.schedule_event(command_label, :exit) { remove_worker(command_label) }
177
+
178
+ env_options = load_env(process_info.dir)
179
+
180
+ spawn_options = {
181
+ :chdir => process_info.dir || ENV['PWD'], :out => write_pipe, :err => write_pipe,
182
+ :pgroup => true, :close_others => true, :in => :close
183
+ }
184
+ Invoker.run_without_bundler { spawn(env_options, process_info.cmd, spawn_options) }
185
+ end
186
+ end
187
+ end