invoker 1.0.4 → 1.1.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.
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