meepo 1.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +29 -0
  6. data/.travis.yml +10 -0
  7. data/Dockerfile +7 -0
  8. data/Gemfile +13 -0
  9. data/MIT-LICENSE +20 -0
  10. data/Rakefile +15 -0
  11. data/TODO +5 -0
  12. data/bin/invoker +7 -0
  13. data/contrib/completion/invoker-completion.bash +70 -0
  14. data/contrib/completion/invoker-completion.zsh +62 -0
  15. data/examples/hello_sinatra.rb +26 -0
  16. data/examples/sample.ini +3 -0
  17. data/invoker.gemspec +43 -0
  18. data/lib/invoker.rb +152 -0
  19. data/lib/invoker/cli.rb +159 -0
  20. data/lib/invoker/cli/pinger.rb +23 -0
  21. data/lib/invoker/cli/question.rb +15 -0
  22. data/lib/invoker/cli/tail.rb +34 -0
  23. data/lib/invoker/cli/tail_watcher.rb +34 -0
  24. data/lib/invoker/command_worker.rb +60 -0
  25. data/lib/invoker/commander.rb +95 -0
  26. data/lib/invoker/daemon.rb +126 -0
  27. data/lib/invoker/dns_cache.rb +23 -0
  28. data/lib/invoker/errors.rb +17 -0
  29. data/lib/invoker/event/manager.rb +79 -0
  30. data/lib/invoker/ipc.rb +45 -0
  31. data/lib/invoker/ipc/add_command.rb +12 -0
  32. data/lib/invoker/ipc/add_http_command.rb +10 -0
  33. data/lib/invoker/ipc/base_command.rb +24 -0
  34. data/lib/invoker/ipc/client_handler.rb +26 -0
  35. data/lib/invoker/ipc/dns_check_command.rb +17 -0
  36. data/lib/invoker/ipc/list_command.rb +11 -0
  37. data/lib/invoker/ipc/message.rb +170 -0
  38. data/lib/invoker/ipc/message/list_response.rb +35 -0
  39. data/lib/invoker/ipc/message/tail_response.rb +10 -0
  40. data/lib/invoker/ipc/ping_command.rb +10 -0
  41. data/lib/invoker/ipc/reload_command.rb +12 -0
  42. data/lib/invoker/ipc/remove_command.rb +12 -0
  43. data/lib/invoker/ipc/server.rb +26 -0
  44. data/lib/invoker/ipc/tail_command.rb +11 -0
  45. data/lib/invoker/ipc/unix_client.rb +60 -0
  46. data/lib/invoker/logger.rb +13 -0
  47. data/lib/invoker/parsers/config.rb +184 -0
  48. data/lib/invoker/parsers/procfile.rb +86 -0
  49. data/lib/invoker/power/balancer.rb +131 -0
  50. data/lib/invoker/power/config.rb +77 -0
  51. data/lib/invoker/power/dns.rb +38 -0
  52. data/lib/invoker/power/http_parser.rb +68 -0
  53. data/lib/invoker/power/http_response.rb +81 -0
  54. data/lib/invoker/power/pf_migrate.rb +64 -0
  55. data/lib/invoker/power/port_finder.rb +49 -0
  56. data/lib/invoker/power/power.rb +3 -0
  57. data/lib/invoker/power/powerup.rb +29 -0
  58. data/lib/invoker/power/setup.rb +90 -0
  59. data/lib/invoker/power/setup/distro/arch.rb +15 -0
  60. data/lib/invoker/power/setup/distro/base.rb +57 -0
  61. data/lib/invoker/power/setup/distro/debian.rb +11 -0
  62. data/lib/invoker/power/setup/distro/mint.rb +10 -0
  63. data/lib/invoker/power/setup/distro/opensuse.rb +11 -0
  64. data/lib/invoker/power/setup/distro/redhat.rb +11 -0
  65. data/lib/invoker/power/setup/distro/ubuntu.rb +10 -0
  66. data/lib/invoker/power/setup/files/invoker_forwarder.sh.erb +17 -0
  67. data/lib/invoker/power/setup/files/socat_invoker.service +12 -0
  68. data/lib/invoker/power/setup/linux_setup.rb +105 -0
  69. data/lib/invoker/power/setup/osx_setup.rb +137 -0
  70. data/lib/invoker/power/templates/400.html +40 -0
  71. data/lib/invoker/power/templates/404.html +40 -0
  72. data/lib/invoker/power/templates/503.html +40 -0
  73. data/lib/invoker/power/url_rewriter.rb +40 -0
  74. data/lib/invoker/process_manager.rb +198 -0
  75. data/lib/invoker/process_printer.rb +43 -0
  76. data/lib/invoker/reactor.rb +37 -0
  77. data/lib/invoker/reactor/reader.rb +54 -0
  78. data/lib/invoker/version.rb +47 -0
  79. data/readme.md +25 -0
  80. data/spec/invoker/cli/pinger_spec.rb +22 -0
  81. data/spec/invoker/cli/tail_watcher_spec.rb +39 -0
  82. data/spec/invoker/cli_spec.rb +27 -0
  83. data/spec/invoker/command_worker_spec.rb +45 -0
  84. data/spec/invoker/commander_spec.rb +152 -0
  85. data/spec/invoker/config_spec.rb +361 -0
  86. data/spec/invoker/daemon_spec.rb +34 -0
  87. data/spec/invoker/event/manager_spec.rb +67 -0
  88. data/spec/invoker/invoker_spec.rb +71 -0
  89. data/spec/invoker/ipc/client_handler_spec.rb +54 -0
  90. data/spec/invoker/ipc/dns_check_command_spec.rb +32 -0
  91. data/spec/invoker/ipc/message/list_response_spec.rb +24 -0
  92. data/spec/invoker/ipc/message_spec.rb +49 -0
  93. data/spec/invoker/ipc/unix_client_spec.rb +29 -0
  94. data/spec/invoker/power/balancer_spec.rb +22 -0
  95. data/spec/invoker/power/config_spec.rb +18 -0
  96. data/spec/invoker/power/http_parser_spec.rb +32 -0
  97. data/spec/invoker/power/http_response_spec.rb +34 -0
  98. data/spec/invoker/power/pf_migrate_spec.rb +87 -0
  99. data/spec/invoker/power/port_finder_spec.rb +16 -0
  100. data/spec/invoker/power/setup/linux_setup_spec.rb +103 -0
  101. data/spec/invoker/power/setup/osx_setup_spec.rb +105 -0
  102. data/spec/invoker/power/setup_spec.rb +4 -0
  103. data/spec/invoker/power/url_rewriter_spec.rb +70 -0
  104. data/spec/invoker/power/web_sockets_spec.rb +61 -0
  105. data/spec/invoker/process_manager_spec.rb +130 -0
  106. data/spec/invoker/reactor_spec.rb +6 -0
  107. data/spec/spec_helper.rb +43 -0
  108. metadata +389 -0
@@ -0,0 +1,137 @@
1
+ module Invoker
2
+ module Power
3
+ class OsxSetup < Setup
4
+ FIREWALL_PLIST_FILE = "/Library/LaunchDaemons/com.codemancers.invoker.firewall.plist"
5
+ RESOLVER_DIR = "/etc/resolver"
6
+
7
+ def resolver_file
8
+ File.join(RESOLVER_DIR, tld)
9
+ end
10
+
11
+ def setup_invoker
12
+ if setup_resolver_file
13
+ find_open_ports
14
+ install_resolver(port_finder.dns_port)
15
+ install_firewall(port_finder.http_port, port_finder.https_port)
16
+ # Before writing the config file, drop down to a normal user
17
+ drop_to_normal_user
18
+ create_config_file
19
+ else
20
+ Invoker::Logger.puts("Invoker is not configured to serve from subdomains".color(:red))
21
+ end
22
+ self
23
+ end
24
+
25
+ def uninstall_invoker
26
+ uninstall_invoker_flag = Invoker::CLI::Question.agree("Are you sure you want to uninstall firewall rules created by setup (y/n) : ")
27
+
28
+ if uninstall_invoker_flag
29
+ remove_resolver_file
30
+ unload_firewall_rule(true)
31
+ Invoker::Power::Config.delete
32
+ Invoker::Logger.puts("Firewall rules were removed")
33
+ end
34
+ end
35
+
36
+ def build_power_config
37
+ config = super
38
+ config[:dns_port] = port_finder.dns_port
39
+ config
40
+ end
41
+
42
+ def install_resolver(dns_port)
43
+ open_resolver_for_write { |fl| fl.write(resolve_string(dns_port)) }
44
+ rescue Errno::EACCES
45
+ Invoker::Logger.puts("Running setup requires root access, please rerun it with sudo".color(:red))
46
+ raise
47
+ end
48
+
49
+ def install_firewall(http_port, https_port)
50
+ File.open(FIREWALL_PLIST_FILE, "w") { |fl|
51
+ fl.write(plist_string(http_port, https_port))
52
+ }
53
+ unload_firewall_rule
54
+ load_firewall_rule
55
+ end
56
+
57
+ def load_firewall_rule
58
+ system("launchctl load -Fw #{FIREWALL_PLIST_FILE} 2>/dev/null")
59
+ end
60
+
61
+ def unload_firewall_rule(remove = false)
62
+ system("pfctl -a com.apple/250.InvokerFirewall -F nat 2>/dev/null")
63
+ system("launchctl unload -w #{FIREWALL_PLIST_FILE} 2>/dev/null")
64
+ system("rm -rf #{FIREWALL_PLIST_FILE}") if remove
65
+ end
66
+
67
+ # Ripped from POW code
68
+ def plist_string(http_port, https_port)
69
+ plist =<<-EOD
70
+ <?xml version="1.0" encoding="UTF-8"?>
71
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
72
+ <plist version="1.0">
73
+ <dict>
74
+ <key>Label</key>
75
+ <string>com.codemancers.invoker</string>
76
+ <key>ProgramArguments</key>
77
+ <array>
78
+ <string>sh</string>
79
+ <string>-c</string>
80
+ <string>#{firewall_command(http_port, https_port)}</string>
81
+ </array>
82
+ <key>RunAtLoad</key>
83
+ <true/>
84
+ <key>UserName</key>
85
+ <string>root</string>
86
+ </dict>
87
+ </plist>
88
+ EOD
89
+ plist
90
+ end
91
+
92
+ def resolve_string(dns_port)
93
+ string =<<-EOD
94
+ nameserver 127.0.0.1
95
+ port #{dns_port}
96
+ EOD
97
+ string
98
+ end
99
+
100
+ # Ripped from Pow code
101
+ def firewall_command(http_port, https_port)
102
+ rules = [
103
+ "rdr pass on lo0 inet proto tcp from any to any port 80 -> 127.0.0.1 port #{http_port}",
104
+ "rdr pass on lo0 inet proto tcp from any to any port 443 -> 127.0.0.1 port #{https_port}"
105
+ ].join("\n")
106
+ "echo \"#{rules}\" | pfctl -a 'com.apple/250.InvokerFirewall' -f - -E"
107
+ end
108
+
109
+ def setup_resolver_file
110
+ return true unless File.exist?(resolver_file)
111
+
112
+ Invoker::Logger.puts "Invoker has detected an existing Pow installation. We recommend "\
113
+ "that you uninstall pow and rerun this setup.".color(:red)
114
+ Invoker::Logger.puts "If you have already uninstalled Pow, proceed with installation"\
115
+ " by pressing y/n."
116
+ replace_resolver_flag = Invoker::CLI::Question.agree("Replace Pow configuration (y/n) : ")
117
+
118
+ if replace_resolver_flag
119
+ Invoker::Logger.puts "Invoker has overwritten one or more files created by Pow. "\
120
+ "If .#{tld} domains still don't resolve locally, try turning off the wi-fi"\
121
+ " and turning it on. It'll force OS X to reload network configuration".color(:green)
122
+ end
123
+ replace_resolver_flag
124
+ end
125
+
126
+ private
127
+
128
+ def open_resolver_for_write
129
+ FileUtils.mkdir(RESOLVER_DIR) unless Dir.exist?(RESOLVER_DIR)
130
+ fl = File.open(resolver_file, "w")
131
+ yield fl
132
+ ensure
133
+ fl && fl.close
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,40 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Invoker</title>
6
+ <style>
7
+ body {
8
+ margin: 0;
9
+ padding: 0;
10
+ background: #fff;
11
+ line-height: 18px;
12
+ }
13
+ div.page {
14
+ padding: 36px 90px;
15
+ }
16
+ h1, h2, p, li {
17
+ font-family: Helvetica, sans-serif;
18
+ font-size: 13px;
19
+ }
20
+ h1 {
21
+ line-height: 45px;
22
+ font-size: 36px;
23
+ margin: 0;
24
+ }
25
+ h2 {
26
+ line-height: 27px;
27
+ font-size: 18px;
28
+ font-weight: normal;
29
+ margin: 0;
30
+ }
31
+ </style>
32
+ </head>
33
+ <body class="">
34
+ <div class="page">
35
+ <h1>Bad request</h1>
36
+ <hr>
37
+ <h2>Invoker couldn't understand the request</h2>
38
+ </div>
39
+ </body>
40
+ </html>
@@ -0,0 +1,40 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Invoker</title>
6
+ <style>
7
+ body {
8
+ margin: 0;
9
+ padding: 0;
10
+ background: #fff;
11
+ line-height: 18px;
12
+ }
13
+ div.page {
14
+ padding: 36px 90px;
15
+ }
16
+ h1, h2, p, li {
17
+ font-family: Helvetica, sans-serif;
18
+ font-size: 13px;
19
+ }
20
+ h1 {
21
+ line-height: 45px;
22
+ font-size: 36px;
23
+ margin: 0;
24
+ }
25
+ h2 {
26
+ line-height: 27px;
27
+ font-size: 18px;
28
+ font-weight: normal;
29
+ margin: 0;
30
+ }
31
+ </style>
32
+ </head>
33
+ <body class="">
34
+ <div class="page">
35
+ <h1>Application not found</h1>
36
+ <hr>
37
+ <h2>Invoker could not find the application. Please check the configuration file.<h2>
38
+ </div>
39
+ </body>
40
+ </html>
@@ -0,0 +1,40 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Invoker</title>
6
+ <style>
7
+ body {
8
+ margin: 0;
9
+ padding: 0;
10
+ background: #fff;
11
+ line-height: 18px;
12
+ }
13
+ div.page {
14
+ padding: 36px 90px;
15
+ }
16
+ h1, h2, p, li {
17
+ font-family: Helvetica, sans-serif;
18
+ font-size: 13px;
19
+ }
20
+ h1 {
21
+ line-height: 45px;
22
+ font-size: 36px;
23
+ margin: 0;
24
+ }
25
+ h2 {
26
+ line-height: 27px;
27
+ font-size: 18px;
28
+ font-weight: normal;
29
+ margin: 0;
30
+ }
31
+ </style>
32
+ </head>
33
+ <body class="">
34
+ <div class="page">
35
+ <h1>Application not running</h1>
36
+ <hr>
37
+ <h2>Invoker did not get any response. Please check if the application is running.<h2>
38
+ </div>
39
+ </body>
40
+ </html>
@@ -0,0 +1,40 @@
1
+ module Invoker
2
+ module Power
3
+ class UrlRewriter
4
+ def select_backend_config(complete_path)
5
+ possible_matches = extract_host_from_domain(complete_path)
6
+ exact_match = nil
7
+ possible_matches.each do |match|
8
+ if match
9
+ exact_match = dns_check(process_name: match)
10
+ break if exact_match.port
11
+ end
12
+ end
13
+ exact_match
14
+ end
15
+
16
+ def extract_host_from_domain(complete_path)
17
+ matching_strings = []
18
+ tld_match_regex.map do |regexp|
19
+ if (match_result = complete_path.match(regexp))
20
+ matching_strings << match_result[1]
21
+ end
22
+ end
23
+ matching_strings.uniq
24
+ end
25
+
26
+ private
27
+
28
+ def tld_match_regex
29
+ tld = Invoker.config.tld
30
+ [/([\w.-]+)\.#{tld}(\:\d+)?$/, /([\w-]+)\.#{tld}(\:\d+)?$/]
31
+ end
32
+
33
+ def dns_check(dns_args)
34
+ Invoker::IPC::UnixClient.send_command("dns_check", dns_args) do |dns_response|
35
+ dns_response
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,198 @@
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
+
92
+ if !directory || directory.empty? || !Dir.exist?(directory)
93
+ return {}
94
+ end
95
+
96
+ default_env = File.join(directory, '.env')
97
+ local_env = File.join(directory, '.env.local')
98
+ env = {}
99
+
100
+ if File.exist?(default_env)
101
+ env.merge!(Dotenv::Environment.new(default_env))
102
+ end
103
+
104
+ if File.exist?(local_env)
105
+ env.merge!(Dotenv::Environment.new(local_env))
106
+ end
107
+
108
+ env
109
+ end
110
+
111
+ def kill_workers
112
+ @workers.each do |key, worker|
113
+ kill_or_remove_process(worker.pid, "INT", worker.command_label)
114
+ end
115
+ @workers = {}
116
+ end
117
+
118
+ # List currently running commands
119
+ def process_list
120
+ Invoker::IPC::Message::ListResponse.from_workers(workers)
121
+ end
122
+
123
+ private
124
+
125
+ def wait_on_pid(command_label, pid)
126
+ raise Invoker::Errors::ToomanyOpenConnections if @thread_group.enclosed?
127
+
128
+ thread = Thread.new do
129
+ Process.wait(pid)
130
+ message = "Process with command #{command_label} exited with status #{$?.exitstatus}"
131
+ Invoker::Logger.puts("\n#{message}".color(:red))
132
+ Invoker.notify_user(message)
133
+ Invoker.commander.trigger(command_label, :exit)
134
+ end
135
+ @thread_group.add(thread)
136
+ end
137
+
138
+ def select_color
139
+ selected_color = LABEL_COLORS.shift
140
+ LABEL_COLORS.push(selected_color)
141
+ selected_color
142
+ end
143
+
144
+ def process_running?(command_label)
145
+ !!workers[command_label]
146
+ end
147
+
148
+ def kill_or_remove_process(pid, signal_to_use, command_label)
149
+ process_kill(pid, signal_to_use)
150
+ true
151
+ rescue Errno::ESRCH
152
+ Invoker::Logger.puts("Killing process with #{pid} and name #{command_label} failed".color(:red))
153
+ remove_worker(command_label, false)
154
+ false
155
+ end
156
+
157
+ def process_kill(pid, signal_to_use)
158
+ if signal_to_use.to_i == 0
159
+ Process.kill(signal_to_use, -Process.getpgid(pid))
160
+ else
161
+ Process.kill(signal_to_use.to_i, -Process.getpgid(pid))
162
+ end
163
+ end
164
+
165
+ # Remove worker from all collections
166
+ def remove_worker(command_label, trigger_event = true)
167
+ worker = @workers[command_label]
168
+ if worker
169
+ @open_pipes.delete(worker.pipe_end.fileno)
170
+ @workers.delete(command_label)
171
+ end
172
+ if trigger_event
173
+ Invoker.commander.trigger(command_label, :worker_removed)
174
+ end
175
+ end
176
+
177
+ # add worker to global collections
178
+ def add_worker(worker)
179
+ @open_pipes[worker.pipe_end.fileno] = worker
180
+ @workers[worker.command_label] = worker
181
+ Invoker.commander.watch_for_read(worker.pipe_end)
182
+ end
183
+
184
+ def run_command(process_info, write_pipe)
185
+ command_label = process_info.label
186
+
187
+ Invoker.commander.schedule_event(command_label, :exit) { remove_worker(command_label) }
188
+
189
+ env_options = load_env(process_info.dir)
190
+
191
+ spawn_options = {
192
+ :chdir => process_info.dir || ENV['PWD'], :out => write_pipe, :err => write_pipe,
193
+ :pgroup => true, :close_others => true, :in => :close
194
+ }
195
+ Invoker.run_without_bundler { spawn(env_options, process_info.cmd, spawn_options) }
196
+ end
197
+ end
198
+ end