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,34 @@
1
+ module Invoker
2
+ # This class defines sockets which are open for watching log files
3
+ class CLI::TailWatcher
4
+ attr_accessor :tail_watchers
5
+
6
+ def initialize
7
+ @tail_mutex = Mutex.new
8
+ @tail_watchers = Hash.new { |h, k| h[k] = [] }
9
+ end
10
+
11
+ def [](process_name)
12
+ @tail_mutex.synchronize { tail_watchers[process_name] }
13
+ end
14
+
15
+ def add(names, socket)
16
+ @tail_mutex.synchronize do
17
+ names.each { |name| tail_watchers[name] << socket }
18
+ end
19
+ end
20
+
21
+ def remove(name, socket)
22
+ @tail_mutex.synchronize do
23
+ client_list = tail_watchers[name]
24
+ client_list.delete(socket)
25
+ purge(name, socket) if client_list.empty?
26
+ end
27
+ end
28
+
29
+ def purge(name, socket)
30
+ tail_watchers.delete(name)
31
+ Invoker.close_socket(socket)
32
+ end
33
+ end
34
+ end
@@ -24,11 +24,37 @@ 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.color(color)} : #{line}"
27
+ tail_watchers = Invoker.tail_watchers[@command_label]
28
+ color_line = "#{@command_label.color(color)} : #{line}"
29
+ if tail_watchers && !tail_watchers.empty?
30
+ json_encoded_tail_response = tail_response(color_line)
31
+ if json_encoded_tail_response
32
+ tail_watchers.each { |tail_socket| send_data(tail_socket, json_encoded_tail_response) }
33
+ end
34
+ else
35
+ Invoker::Logger.puts color_line
36
+ end
28
37
  end
29
38
 
30
39
  def to_h
31
- {:command_label => command_label, :pid => pid.to_s}
40
+ { command_label: command_label, pid: pid.to_s }
41
+ end
42
+
43
+ def send_data(socket, data)
44
+ socket.write(data)
45
+ rescue
46
+ Invoker::Logger.puts "Removing #{@command_label} watcher #{socket} from list"
47
+ Invoker.tail_watchers.remove(@command_label, socket)
48
+ end
49
+
50
+ private
51
+
52
+ # Encode current line as json and send the response.
53
+ def tail_response(line)
54
+ tail_response = Invoker::IPC::Message::TailResponse.new(tail_line: line)
55
+ tail_response.encoded_message
56
+ rescue
57
+ nil
32
58
  end
33
59
  end
34
60
  end
@@ -2,129 +2,48 @@ require "io/console"
2
2
  require 'pty'
3
3
  require "json"
4
4
  require "dotenv"
5
+ require "forwardable"
5
6
 
6
7
  module Invoker
7
8
  class Commander
8
- MAX_PROCESS_COUNT = 10
9
- LABEL_COLORS = [:green, :yellow, :blue, :magenta, :cyan]
10
- attr_accessor :reactor, :workers, :thread_group, :open_pipes
11
- attr_accessor :event_manager, :runnables
9
+ attr_accessor :reactor, :process_manager
10
+ attr_accessor :event_manager, :runnables, :thread_group
11
+ extend Forwardable
12
12
 
13
- def initialize
14
- # mapping between open pipes and worker classes
15
- @open_pipes = {}
13
+ def_delegators :@process_manager, :start_process_by_name, :stop_process
14
+ def_delegators :@process_manager, :restart_process, :get_worker_from_fd, :process_list
16
15
 
17
- # mapping between command label and worker classes
18
- @workers = {}
16
+ def_delegators :@event_manager, :schedule_event, :trigger
17
+ def_delegator :@reactor, :watch_for_read
19
18
 
20
- @thread_group = ThreadGroup.new()
21
- @worker_mutex = Mutex.new()
19
+ def initialize
20
+ @thread_group = ThreadGroup.new
21
+ @runnable_mutex = Mutex.new
22
22
 
23
- @event_manager = Invoker::Event::Manager.new()
23
+ @event_manager = Invoker::Event::Manager.new
24
24
  @runnables = []
25
25
 
26
26
  @reactor = Invoker::Reactor.new
27
+ @process_manager = Invoker::ProcessManager.new
27
28
  Thread.abort_on_exception = true
28
29
  end
29
30
 
30
31
  # Start the invoker process supervisor. This method starts a unix server
31
32
  # in separate thread that listens for incoming commands.
32
33
  def start_manager
33
- if !Invoker::CONFIG.processes || Invoker::CONFIG.processes.empty?
34
- raise Invoker::Errors::InvalidConfig.new("No processes configured in config file")
35
- end
36
- install_interrupt_handler()
37
- unix_server_thread = Thread.new { Invoker::CommandListener::Server.new() }
38
- thread_group.add(unix_server_thread)
39
- run_power_server()
40
- Invoker::CONFIG.processes.each { |process_info| add_command(process_info) }
41
- at_exit { kill_workers }
42
- start_event_loop()
43
- end
44
-
45
- # Start given command and start a background thread to wait on the process
46
- #
47
- # @param process_info [OpenStruct(command, directory)]
48
- def add_command(process_info)
49
- m, s = PTY.open
50
- s.raw! # disable newline conversion.
51
-
52
- pid = run_command(process_info, s)
53
-
54
- s.close()
55
-
56
- worker = Invoker::CommandWorker.new(process_info.label, m, pid, select_color())
57
-
58
- add_worker(worker)
59
- wait_on_pid(process_info.label,pid)
60
- end
61
-
62
- # List currently running commands
63
- def list_commands
64
- Invoker::ProcessPrinter.to_json(workers)
65
- end
66
-
67
- # Start executing given command by their label name.
68
- #
69
- # @param command_label [String] Command label of process specified in config file.
70
- def add_command_by_label(command_label)
71
- if process_running?(command_label)
72
- Invoker::Logger.puts "\nProcess '#{command_label}' is already running".color(:red)
73
- return false
74
- end
75
-
76
- process_info = Invoker::CONFIG.process(command_label)
77
- if process_info
78
- add_command(process_info)
79
- end
80
- end
81
-
82
- # Reload a process given by command label
83
- #
84
- # @params command_label [String] Command label of process specified in config file.
85
- def reload_command(command_label, rest_args)
86
- if remove_command(command_label, rest_args)
87
- event_manager.schedule_event(command_label, :worker_removed) {
88
- add_command_by_label(command_label)
89
- }
90
- else
91
- add_command_by_label(command_label)
92
- end
93
- end
94
-
95
- # Remove a process from list of processes managed by invoker supervisor.It also
96
- # kills the process before removing it from the list.
97
- #
98
- # @param command_label [String] Command label of process specified in config file
99
- # @param rest_args [Array] Additional option arguments, such as signal that can be used.
100
- # @return [Boolean] if process existed and was removed else false
101
- def remove_command(command_label, rest_args)
102
- worker = workers[command_label]
103
- return false unless worker
104
- signal_to_use = rest_args ? Array(rest_args).first : 'INT'
105
-
106
- Invoker::Logger.puts("Removing #{command_label} with signal #{signal_to_use}".color(:red))
107
- kill_or_remove_process(worker.pid, signal_to_use, command_label)
108
- end
109
-
110
- # Given a file descriptor returns the worker object
111
- #
112
- # @param fd [IO] an IO object with valid file descriptor
113
- # @return [Invoker::CommandWorker] The worker object which is associated with this fd
114
- def get_worker_from_fd(fd)
115
- open_pipes[fd.fileno]
116
- end
117
-
118
- # Given a command label returns the associated worker object
119
- #
120
- # @param label [String] Command label of the command
121
- # @return [Invoker::CommandWorker] The worker object which is associated with this command
122
- def get_worker_from_label(label)
123
- workers[label]
34
+ verify_process_configuration
35
+ daemonize_app if Invoker.daemonize?
36
+ install_interrupt_handler
37
+ unix_server_thread = Thread.new { Invoker::IPC::Server.new }
38
+ @thread_group.add(unix_server_thread)
39
+ process_manager.run_power_server
40
+ Invoker.config.processes.each { |process_info| process_manager.start_process(process_info) }
41
+ at_exit { process_manager.kill_workers }
42
+ start_event_loop
124
43
  end
125
44
 
126
45
  def on_next_tick(*args, &block)
127
- @worker_mutex.synchronize do
46
+ @runnable_mutex.synchronize do
128
47
  @runnables << OpenStruct.new(:args => args, :block => block)
129
48
  end
130
49
  end
@@ -136,158 +55,37 @@ module Invoker
136
55
  @runnables = []
137
56
  end
138
57
 
139
- def run_power_server
140
- return unless Invoker.can_run_balancer?(false)
141
-
142
- powerup_id = Invoker::Power::Powerup.fork_and_start()
143
- wait_on_pid("powerup_manager", powerup_id)
144
- at_exit {
145
- begin
146
- Process.kill("INT", powerup_id)
147
- rescue Errno::ESRCH; end
148
- }
149
- end
58
+ private
150
59
 
151
- def load_env(directory = nil)
152
- directory ||= ENV['PWD']
153
- default_env = File.join(directory, '.env')
154
- if File.exist?(default_env)
155
- Dotenv::Environment.new(default_env)
156
- else
157
- {}
60
+ def verify_process_configuration
61
+ if !Invoker.config.processes || Invoker.config.processes.empty?
62
+ raise Invoker::Errors::InvalidConfig.new("No processes configured in config file")
158
63
  end
159
64
  end
160
65
 
161
- private
162
66
  def start_event_loop
163
67
  loop do
164
- reactor.watch_on_pipe()
165
- run_runnables()
166
- run_scheduled_events()
68
+ reactor.monitor_for_fd_events
69
+ run_runnables
70
+ run_scheduled_events
167
71
  end
168
72
  end
169
73
 
170
74
  def run_scheduled_events
171
75
  event_manager.run_scheduled_events do |event|
172
- event.block.call()
173
- end
174
- end
175
-
176
- def kill_or_remove_process(pid, signal_to_use, command_label)
177
- process_kill(pid, signal_to_use)
178
- true
179
- rescue Errno::ESRCH
180
- Invoker::Logger.puts("Killing process with #{pid} and name #{command_label} failed".color(:red))
181
- remove_worker(command_label, false)
182
- false
183
- end
184
-
185
- def process_kill(pid, signal_to_use)
186
- if signal_to_use.to_i == 0
187
- Process.kill(signal_to_use, -Process.getpgid(pid))
188
- else
189
- Process.kill(signal_to_use.to_i, -Process.getpgid(pid))
190
- end
191
- end
192
-
193
- def select_color
194
- selected_color = LABEL_COLORS.shift()
195
- LABEL_COLORS.push(selected_color)
196
- selected_color
197
- end
198
-
199
- # Remove worker from all collections
200
- def remove_worker(command_label, trigger_event = true)
201
- worker = @workers[command_label]
202
- if worker
203
- @open_pipes.delete(worker.pipe_end.fileno)
204
- @workers.delete(command_label)
205
- end
206
- if trigger_event
207
- event_manager.trigger(command_label, :worker_removed)
208
- end
209
- end
210
-
211
- # add worker to global collections
212
- def add_worker(worker)
213
- @open_pipes[worker.pipe_end.fileno] = worker
214
- @workers[worker.command_label] = worker
215
- @reactor.add_to_monitor(worker.pipe_end)
216
- end
217
-
218
- def run_command(process_info, write_pipe)
219
- command_label = process_info.label
220
-
221
- event_manager.schedule_event(command_label, :exit) { remove_worker(command_label) }
222
-
223
- env_options = load_env(process_info.dir)
224
-
225
- spawn_options = {
226
- :chdir => process_info.dir || ENV['PWD'], :out => write_pipe, :err => write_pipe,
227
- :pgroup => true, :close_others => true, :in => :close
228
- }
229
-
230
- if defined?(Bundler)
231
- Bundler.with_clean_env do
232
- spawn(env_options, process_info.cmd, spawn_options)
233
- end
234
- else
235
- spawn(env_options, process_info.cmd, spawn_options)
236
- end
237
- end
238
-
239
- def wait_on_pid(command_label,pid)
240
- raise Invoker::Errors::ToomanyOpenConnections if @thread_group.enclosed?
241
-
242
- thread = Thread.new do
243
- Process.wait(pid)
244
- message = "Process with command #{command_label} exited with status #{$?.exitstatus}"
245
- Invoker::Logger.puts("\n#{message}".color(:red))
246
- notify_user(message)
247
- event_manager.trigger(command_label, :exit)
248
- end
249
- @thread_group.add(thread)
250
- end
251
-
252
- def notify_user(message)
253
- if defined?(Bundler)
254
- Bundler.with_clean_env do
255
- check_and_notify_with_terminal_notifier(message)
256
- end
257
- else
258
- check_and_notify_with_terminal_notifier(message)
259
- end
260
- end
261
-
262
- def check_and_notify_with_terminal_notifier(message)
263
- return unless Invoker.darwin?
264
-
265
- command_path = `which terminal-notifier`
266
- if command_path && !command_path.empty?
267
- system("terminal-notifier -message '#{message}' -title Invoker")
76
+ event.block.call
268
77
  end
269
78
  end
270
79
 
271
80
  def install_interrupt_handler
272
81
  Signal.trap("INT") do
273
- kill_workers()
82
+ process_manager.kill_workers
274
83
  exit(0)
275
84
  end
276
85
  end
277
86
 
278
- def kill_workers
279
- @workers.each {|key,worker|
280
- begin
281
- Process.kill("INT", -Process.getpgid(worker.pid))
282
- rescue Errno::ESRCH
283
- puts "Error killing #{key}"
284
- end
285
- }
286
- @workers = {}
287
- end
288
-
289
- def process_running?(command_label)
290
- !!workers[command_label]
87
+ def daemonize_app
88
+ Invoker.daemon.start
291
89
  end
292
90
  end
293
91
  end
@@ -0,0 +1,5 @@
1
+ module Invoker
2
+ module Config
3
+
4
+ end
5
+ end
@@ -0,0 +1,126 @@
1
+ module Invoker
2
+ # rip off from borg
3
+ # https://github.com/code-mancers/borg/blob/master/lib/borg/borg_daemon.rb
4
+ class Daemon
5
+ attr_reader :process_name
6
+
7
+ def initialize
8
+ @process_name = 'invoker'
9
+ end
10
+
11
+ def start
12
+ if running?
13
+ Invoker::Logger.puts "Invoker daemon is already running"
14
+ exit(0)
15
+ elsif dead?
16
+ File.delete(pid_file) if File.exists?(pid_file)
17
+ end
18
+ Invoker::Logger.puts "Running Invoker daemon"
19
+ daemonize
20
+ end
21
+
22
+ def stop
23
+ kill_process
24
+ end
25
+
26
+ def pid_file
27
+ File.join(Dir.home, ".invoker", "#{process_name}.pid")
28
+ end
29
+
30
+ def pid
31
+ File.read(pid_file).strip.to_i
32
+ end
33
+
34
+ def log_file
35
+ File.join(Dir.home, ".invoker", "#{process_name}.log")
36
+ end
37
+
38
+ def daemonize
39
+ if fork
40
+ sleep(2)
41
+ exit(0)
42
+ else
43
+ Process.setsid
44
+ File.open(pid_file, "w") do |file|
45
+ file.write(Process.pid.to_s)
46
+ end
47
+ Invoker::Logger.puts "Invoker daemon log is available at #{log_file}"
48
+ redirect_io(log_file)
49
+ $0 = process_name
50
+ end
51
+ end
52
+
53
+ def kill_process
54
+ pgid = Process.getpgid(pid)
55
+ Process.kill('-TERM', pgid)
56
+ File.delete(pid_file) if File.exist?(pid_file)
57
+ Invoker::Logger.puts "Stopped Invoker daemon"
58
+ end
59
+
60
+ def process_running?
61
+ Process.kill(0, pid)
62
+ true
63
+ rescue Errno::ESRCH
64
+ false
65
+ end
66
+
67
+ def status
68
+ @status ||= check_process_status
69
+ end
70
+
71
+ def pidfile_exists?
72
+ File.exist?(pid_file)
73
+ end
74
+
75
+ def running?
76
+ status == 0
77
+ end
78
+
79
+ # pidfile exists but process isn't running
80
+ def dead?
81
+ status == 1
82
+ end
83
+
84
+ private
85
+
86
+ def check_process_status
87
+ if pidfile_exists? && process_running?
88
+ 0
89
+ elsif pidfile_exists? # but not process_running
90
+ 1
91
+ else
92
+ 3
93
+ end
94
+ end
95
+
96
+ def redirect_io(logfile_name = nil)
97
+ redirect_file_to_target($stdin)
98
+ redirect_stdout(logfile_name)
99
+ redirect_stderr
100
+ end
101
+
102
+ def redirect_stderr
103
+ redirect_file_to_target($stderr, $stdout)
104
+ $stderr.sync = true
105
+ end
106
+
107
+ def redirect_stdout(logfile_name)
108
+ if logfile_name
109
+ begin
110
+ $stdout.reopen logfile_name, "a"
111
+ $stdout.sync = true
112
+ rescue StandardError
113
+ redirect_file_to_target($stdout)
114
+ end
115
+ else
116
+ redirect_file_to_target($stdout)
117
+ end
118
+ end
119
+
120
+ def redirect_file_to_target(file, target = "/dev/null")
121
+ begin
122
+ file.reopen(target)
123
+ rescue; end
124
+ end
125
+ end
126
+ end