invoker 1.0.2 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +6 -14
- data/.gitignore +2 -0
- data/.rspec +2 -0
- data/Rakefile +7 -5
- data/invoker.gemspec +3 -3
- data/lib/invoker.rb +2 -0
- data/lib/invoker/commander.rb +12 -9
- data/lib/invoker/errors.rb +1 -0
- data/lib/invoker/parsers/config.rb +49 -14
- data/lib/invoker/parsers/option_parser.rb +1 -1
- data/lib/invoker/parsers/procfile.rb +86 -0
- data/lib/invoker/power.rb +1 -0
- data/lib/invoker/power/balancer.rb +16 -1
- data/lib/invoker/power/http_response.rb +81 -0
- data/lib/invoker/power/setup.rb +13 -1
- data/lib/invoker/power/templates/404.html +40 -0
- data/lib/invoker/power/templates/503.html +40 -0
- data/lib/invoker/runner.rb +6 -0
- data/lib/invoker/version.rb +1 -1
- data/spec/invoker/command_listener/client_spec.rb +1 -1
- data/spec/invoker/command_worker_spec.rb +1 -1
- data/spec/invoker/commander_spec.rb +10 -10
- data/spec/invoker/config_spec.rb +55 -17
- data/spec/invoker/event/manager_spec.rb +13 -18
- data/spec/invoker/invoker_spec.rb +5 -5
- data/spec/invoker/power/config_spec.rb +4 -4
- data/spec/invoker/power/http_response_spec.rb +34 -0
- data/spec/invoker/power/port_finder_spec.rb +2 -2
- data/spec/invoker/power/setup_spec.rb +22 -30
- data/spec/spec_helper.rb +58 -8
- metadata +26 -19
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
metadata.gz: !binary |-
|
9
|
-
MjllZmVkMTIxNmIxYTZlMjFjNjQ4NjIxNGQwNTFkMzcxZWFiYTg3YjdjNjk3
|
10
|
-
ZTI2YzM5ODk5Mjc5NTRmNWUyYjA4MjExZjQ3MTg2OTg2NTM2MGMxNGI0ODcz
|
11
|
-
ZTFiZDk1NTU0ZTc2YTIwOGE5OGUxY2E0NjlkNDIzNGJkYTI0ZjU=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
ZjhhNDM5YjJhNjgzMjcxODJhMGI3ZmI1YmM2ZDY0OTI1NTQzN2NlMjBjNGRj
|
14
|
-
YmIwMTljMDMwNjlkMjI2OGY2YWI0ZmNiOTgyYWQyOTRjMWRiN2JjNTBlNjcy
|
15
|
-
ODQ2YWU3ZWQ5M2IxZTZiNGVhMzczN2NlNWEyNmZhMWJiMzljZGQ=
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8949242941b92eea8a78f0a528b62c59dc831b2e
|
4
|
+
data.tar.gz: bd867429fa2a66e625549de2a8d2ab532ffe2c6c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b6732d806f3ecfcc9a9d2275baa9068eff0207e789d63fe3b92d19bdcb5474513565b5f7a0d5a3438c4efcb41649c4249304cbf6a16459ddf111707b0cf1c183
|
7
|
+
data.tar.gz: 67c02e15182e736bdefb13b2f09e7eda386a9ecf7fd23f7dc44a1c6943ef73965ad5c039bc31cb952688dd46d9114004dcf1ea5d0f932517db2ec6326983eaad
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/Rakefile
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
task :test => :spec
|
data/invoker.gemspec
CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |s|
|
|
22
22
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
23
|
s.require_paths = ["lib"]
|
24
24
|
|
25
|
-
s.homepage = %q{http://
|
25
|
+
s.homepage = %q{http://invoker.codemancers.com}
|
26
26
|
s.licenses = ["MIT"]
|
27
27
|
s.require_paths = ["lib"]
|
28
28
|
s.summary = %q{Something small for Process management}
|
@@ -35,8 +35,8 @@ Gem::Specification.new do |s|
|
|
35
35
|
s.add_dependency("uuid", "~> 2.3.7")
|
36
36
|
s.add_dependency("highline", "~> 1.6.19")
|
37
37
|
s.add_dependency("http-parser-lite", "~> 0.6.0")
|
38
|
-
s.
|
38
|
+
s.add_dependency("dotenv", "~> 0.9.0")
|
39
|
+
s.add_development_dependency("rspec")
|
39
40
|
s.add_development_dependency("mocha")
|
40
|
-
s.add_development_dependency("mocha-on-bacon")
|
41
41
|
s.add_development_dependency("rake")
|
42
42
|
end
|
data/lib/invoker.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
$: << File.dirname(__FILE__) unless $:.include?(File.expand_path(File.dirname(__FILE__)))
|
2
2
|
|
3
|
+
require "fileutils"
|
3
4
|
require "formatador"
|
4
5
|
require 'rubydns'
|
5
6
|
require 'em-proxy'
|
@@ -14,6 +15,7 @@ require "invoker/command_listener/server"
|
|
14
15
|
require "invoker/command_listener/client"
|
15
16
|
require "invoker/power"
|
16
17
|
require "invoker/errors"
|
18
|
+
require "invoker/parsers/procfile"
|
17
19
|
require "invoker/parsers/config"
|
18
20
|
require "invoker/parsers/option_parser"
|
19
21
|
require "invoker/commander"
|
data/lib/invoker/commander.rb
CHANGED
@@ -161,15 +161,16 @@ module Invoker
|
|
161
161
|
process_kill(pid, signal_to_use)
|
162
162
|
true
|
163
163
|
rescue Errno::ESRCH
|
164
|
+
Invoker::Logger.puts("Killing process with #{pid} and name #{command_label} failed".color(:red))
|
164
165
|
remove_worker(command_label, false)
|
165
166
|
false
|
166
167
|
end
|
167
168
|
|
168
169
|
def process_kill(pid, signal_to_use)
|
169
170
|
if signal_to_use.to_i == 0
|
170
|
-
Process.kill(signal_to_use, pid)
|
171
|
+
Process.kill(signal_to_use, -Process.getpgid(pid))
|
171
172
|
else
|
172
|
-
Process.kill(signal_to_use.to_i, pid)
|
173
|
+
Process.kill(signal_to_use.to_i, -Process.getpgid(pid))
|
173
174
|
end
|
174
175
|
end
|
175
176
|
|
@@ -203,16 +204,17 @@ module Invoker
|
|
203
204
|
|
204
205
|
event_manager.schedule_event(command_label, :exit) { remove_worker(command_label) }
|
205
206
|
|
207
|
+
spawn_options = {
|
208
|
+
:chdir => process_info.dir || ENV['PWD'], :out => write_pipe, :err => write_pipe,
|
209
|
+
:pgroup => true, :close_others => true, :in => :close
|
210
|
+
}
|
211
|
+
|
206
212
|
if defined?(Bundler)
|
207
213
|
Bundler.with_clean_env do
|
208
|
-
spawn(process_info.cmd,
|
209
|
-
:chdir => process_info.dir || "/", :out => write_pipe, :err => write_pipe
|
210
|
-
)
|
214
|
+
spawn(process_info.cmd, spawn_options)
|
211
215
|
end
|
212
216
|
else
|
213
|
-
spawn(process_info.cmd,
|
214
|
-
:chdir => process_info.dir || "/", :out => write_pipe, :err => write_pipe
|
215
|
-
)
|
217
|
+
spawn(process_info.cmd, spawn_options)
|
216
218
|
end
|
217
219
|
end
|
218
220
|
|
@@ -258,8 +260,9 @@ module Invoker
|
|
258
260
|
def kill_workers
|
259
261
|
@workers.each {|key,worker|
|
260
262
|
begin
|
261
|
-
Process.kill("INT", worker.pid)
|
263
|
+
Process.kill("INT", -Process.getpgid(worker.pid))
|
262
264
|
rescue Errno::ESRCH
|
265
|
+
puts "Error killing #{key}"
|
263
266
|
end
|
264
267
|
}
|
265
268
|
@workers = {}
|
data/lib/invoker/errors.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'iniparse'
|
2
|
+
require 'dotenv'
|
2
3
|
|
3
4
|
module Invoker
|
4
5
|
module Parsers
|
@@ -7,9 +8,9 @@ module Invoker
|
|
7
8
|
attr_accessor :processes, :power_config
|
8
9
|
|
9
10
|
def initialize(filename, port)
|
10
|
-
@
|
11
|
+
@filename = filename
|
11
12
|
@port = port
|
12
|
-
@processes =
|
13
|
+
@processes = load_config
|
13
14
|
if Invoker.can_run_balancer?
|
14
15
|
@power_config = Invoker::Power::Config.load_config()
|
15
16
|
end
|
@@ -24,25 +25,52 @@ module Invoker
|
|
24
25
|
end
|
25
26
|
|
26
27
|
def process(label)
|
27
|
-
processes.detect {|pconfig|
|
28
|
-
pconfig.label == label
|
29
|
-
}
|
28
|
+
processes.detect { |pconfig| pconfig.label == label }
|
30
29
|
end
|
31
30
|
|
32
31
|
private
|
33
|
-
def
|
32
|
+
def load_config
|
33
|
+
if is_ini?
|
34
|
+
process_ini
|
35
|
+
elsif is_procfile?
|
36
|
+
process_procfile
|
37
|
+
else
|
38
|
+
Invoker::Logger.puts("\n Invalid config file. Invoker requires an ini or a Procfile.".color(:red))
|
39
|
+
abort
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def process_ini
|
44
|
+
ini_content = File.read(@filename)
|
34
45
|
document = IniParse.parse(ini_content)
|
35
46
|
document.map do |section|
|
36
47
|
check_directory(section["directory"])
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
48
|
+
process_command_from_section(section)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def process_procfile
|
53
|
+
load_env
|
54
|
+
procfile = Invoker::Parsers::Procfile.new(@filename)
|
55
|
+
procfile.entries.map do |name, command|
|
56
|
+
section = { "label" => name, "command" => command }
|
57
|
+
process_command_from_section(section)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def process_command_from_section(section)
|
62
|
+
if supports_subdomain?(section)
|
63
|
+
port = pick_port(section)
|
64
|
+
make_option_for_subdomain(section, port)
|
65
|
+
else
|
66
|
+
make_option(section)
|
43
67
|
end
|
44
68
|
end
|
45
69
|
|
70
|
+
def load_env
|
71
|
+
Dotenv.load
|
72
|
+
end
|
73
|
+
|
46
74
|
def pick_port(section)
|
47
75
|
if section['command'] =~ PORT_REGEX
|
48
76
|
@port += 1
|
@@ -56,7 +84,7 @@ module Invoker
|
|
56
84
|
def make_option_for_subdomain(section, port)
|
57
85
|
OpenStruct.new(
|
58
86
|
port: port,
|
59
|
-
label: section.key,
|
87
|
+
label: section["label"] || section.key,
|
60
88
|
dir: section["directory"],
|
61
89
|
cmd: replace_port_in_command(section["command"], port)
|
62
90
|
)
|
@@ -64,7 +92,7 @@ module Invoker
|
|
64
92
|
|
65
93
|
def make_option(section)
|
66
94
|
OpenStruct.new(
|
67
|
-
label: section.key,
|
95
|
+
label: section["label"] || section.key,
|
68
96
|
dir: section["directory"],
|
69
97
|
cmd: section["command"]
|
70
98
|
)
|
@@ -88,6 +116,13 @@ module Invoker
|
|
88
116
|
end
|
89
117
|
end
|
90
118
|
|
119
|
+
def is_ini?
|
120
|
+
File.extname(@filename) == '.ini'
|
121
|
+
end
|
122
|
+
|
123
|
+
def is_procfile?
|
124
|
+
@filename =~ /Procfile/
|
125
|
+
end
|
91
126
|
end
|
92
127
|
end
|
93
128
|
end
|
@@ -8,7 +8,7 @@ module Invoker
|
|
8
8
|
selected_command = nil
|
9
9
|
opts = Slop.parse(args, help: true) do
|
10
10
|
on :v, "Print the version" do
|
11
|
-
|
11
|
+
selected_command = OpenStruct.new(:command => 'version')
|
12
12
|
end
|
13
13
|
on :p, :port=, "Port series to be used for starting rack servers", as: Integer
|
14
14
|
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Invoker
|
2
|
+
module Parsers
|
3
|
+
# rip off from foreman
|
4
|
+
class Procfile
|
5
|
+
# Initialize a Procfile
|
6
|
+
#
|
7
|
+
# @param [String] filename (nil) An optional filename to read from
|
8
|
+
#
|
9
|
+
def initialize(filename=nil)
|
10
|
+
@entries = []
|
11
|
+
load(filename) if filename
|
12
|
+
end
|
13
|
+
|
14
|
+
# Yield each +Procfile+ entry in order
|
15
|
+
#
|
16
|
+
def entries
|
17
|
+
return @entries unless block_given?
|
18
|
+
@entries.each do |(name, command)|
|
19
|
+
yield name, command
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Retrieve a +Procfile+ command by name
|
24
|
+
#
|
25
|
+
# @param [String] name The name of the Procfile entry to retrieve
|
26
|
+
#
|
27
|
+
def [](name)
|
28
|
+
@entries.detect { |n,c| name == n }.last
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create a +Procfile+ entry
|
32
|
+
#
|
33
|
+
# @param [String] name The name of the +Procfile+ entry to create
|
34
|
+
# @param [String] command The command of the +Procfile+ entry to create
|
35
|
+
#
|
36
|
+
def []=(name, command)
|
37
|
+
delete name
|
38
|
+
@entries << [name, command]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Remove a +Procfile+ entry
|
42
|
+
#
|
43
|
+
# @param [String] name The name of the +Procfile+ entry to remove
|
44
|
+
#
|
45
|
+
def delete(name)
|
46
|
+
@entries.reject! { |n,c| name == n }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Load a Procfile from a file
|
50
|
+
#
|
51
|
+
# @param [String] filename The filename of the +Procfile+ to load
|
52
|
+
#
|
53
|
+
def load(filename)
|
54
|
+
@entries.replace parse(filename)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Save a Procfile to a file
|
58
|
+
#
|
59
|
+
# @param [String] filename Save the +Procfile+ to this file
|
60
|
+
#
|
61
|
+
def save(filename)
|
62
|
+
File.open(filename, 'w') do |file|
|
63
|
+
file.puts self.to_s
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get the +Procfile+ as a +String+
|
68
|
+
#
|
69
|
+
def to_s
|
70
|
+
@entries.map do |name, command|
|
71
|
+
[ name, command ].join(": ")
|
72
|
+
end.join("\n")
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def parse(filename)
|
78
|
+
File.read(filename).gsub("\r\n","\n").split("\n").map do |line|
|
79
|
+
if line =~ /^([A-Za-z0-9_-]+):\s*(.+)$/
|
80
|
+
[$1, $2]
|
81
|
+
end
|
82
|
+
end.compact
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/invoker/power.rb
CHANGED
@@ -73,7 +73,8 @@ module Invoker
|
|
73
73
|
connection.relay_to_servers(@buffer.join)
|
74
74
|
@buffer = []
|
75
75
|
else
|
76
|
-
|
76
|
+
return_error_page(404)
|
77
|
+
connection.close_connection_after_writing
|
77
78
|
end
|
78
79
|
end
|
79
80
|
|
@@ -95,11 +96,17 @@ module Invoker
|
|
95
96
|
end
|
96
97
|
|
97
98
|
def backend_data(backend, data)
|
99
|
+
@backend_data = true
|
98
100
|
data
|
99
101
|
end
|
100
102
|
|
101
103
|
def frontend_disconnect(backend, name)
|
102
104
|
http_parser.reset()
|
105
|
+
unless @backend_data
|
106
|
+
Invoker::Logger.puts("\nApplication not running. Returning error page.".color(:red))
|
107
|
+
return_error_page(503)
|
108
|
+
end
|
109
|
+
@backend_data = false
|
103
110
|
connection.close_connection_after_writing() if backend == session
|
104
111
|
end
|
105
112
|
|
@@ -113,6 +120,14 @@ module Invoker
|
|
113
120
|
nil
|
114
121
|
end
|
115
122
|
end
|
123
|
+
|
124
|
+
def return_error_page(status)
|
125
|
+
http_response = Invoker::Power::HttpResponse.new()
|
126
|
+
http_response.status = status
|
127
|
+
http_response['Content-Type'] = "text/html; charset=utf-8"
|
128
|
+
http_response.use_file_as_body(File.join(File.dirname(__FILE__), "templates/#{status}.html"))
|
129
|
+
connection.send_data(http_response.http_string)
|
130
|
+
end
|
116
131
|
end
|
117
132
|
end
|
118
133
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Invoker
|
4
|
+
module Power
|
5
|
+
class HttpResponse
|
6
|
+
STATUS_MAPS = {
|
7
|
+
200 => "OK",
|
8
|
+
201 => "Created",
|
9
|
+
202 => "Accepted",
|
10
|
+
204 => "No Content",
|
11
|
+
205 => "Reset Content",
|
12
|
+
206 => "Partial Content",
|
13
|
+
301 => "Moved Permanently",
|
14
|
+
302 => "Found",
|
15
|
+
304 => "Not Modified",
|
16
|
+
400 => "Bad Request",
|
17
|
+
401 => "Unauthorized",
|
18
|
+
402 => "Payment Required",
|
19
|
+
403 => "Forbidden",
|
20
|
+
404 => "Not Found",
|
21
|
+
411 => "Length Required",
|
22
|
+
500 => "Internal Server Error",
|
23
|
+
501 => "Not Implemented",
|
24
|
+
502 => "Bad Gateway",
|
25
|
+
503 => "Service Unavailable",
|
26
|
+
504 => "Gateway Timeout"
|
27
|
+
}
|
28
|
+
|
29
|
+
HTTP_HEADER_FIELDS = [
|
30
|
+
'Cache-Control', 'Connection', 'Date',
|
31
|
+
'Pragma', 'Trailer', 'Transfer-Encoding',
|
32
|
+
'Accept-Ranges', 'Age', 'Etag',
|
33
|
+
'Server', 'Location', 'Allow',
|
34
|
+
'Content-Encoding', 'Content-Language', 'Content-Location',
|
35
|
+
'Content-MD5', 'Content-Range',
|
36
|
+
'Content-Type', 'Expires',
|
37
|
+
'Last-Modified', 'extension-header'
|
38
|
+
]
|
39
|
+
|
40
|
+
attr_accessor :header, :body, :status
|
41
|
+
|
42
|
+
def initialize
|
43
|
+
@header = {}
|
44
|
+
header['Server'] = "Invoker 1.1"
|
45
|
+
header['Date'] = Time.now.httpdate
|
46
|
+
@status = 200
|
47
|
+
@body = ""
|
48
|
+
end
|
49
|
+
|
50
|
+
def []=(key, value)
|
51
|
+
header[key] = value
|
52
|
+
end
|
53
|
+
|
54
|
+
def use_file_as_body(file_name)
|
55
|
+
if file_name && File.exists?(file_name)
|
56
|
+
file_content = File.read(file_name)
|
57
|
+
self.body = file_content
|
58
|
+
else
|
59
|
+
raise Invoker::Errors:InvalidFile, "Invalid file as body"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def http_string
|
64
|
+
final_string = []
|
65
|
+
final_string << "HTTP/1.1 #{status} #{STATUS_MAPS[status]}"
|
66
|
+
|
67
|
+
if header['Transfer-Encoding'].nil? && body.empty?
|
68
|
+
header['Content-Length'] = body.length
|
69
|
+
end
|
70
|
+
|
71
|
+
HTTP_HEADER_FIELDS.each do |key|
|
72
|
+
if value = header[key]
|
73
|
+
final_string << "#{key}: #{value}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
final_string.join("\r\n") + "\r\n\r\n" + body
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|