invoker 1.0.2 → 1.0.3
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.
- 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
|