invoker 0.1.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +14 -6
- data/.gitignore +3 -0
- data/Gemfile +0 -1
- data/TODO +5 -0
- data/bin/invoker +0 -3
- data/invoker.gemspec +6 -1
- data/lib/invoker.rb +26 -1
- data/lib/invoker/command_listener/client.rb +1 -1
- data/lib/invoker/command_worker.rb +2 -2
- data/lib/invoker/commander.rb +34 -18
- data/lib/invoker/errors.rb +1 -0
- data/lib/invoker/parsers/config.rb +68 -4
- data/lib/invoker/parsers/option_parser.rb +18 -5
- data/lib/invoker/power.rb +6 -0
- data/lib/invoker/power/balancer.rb +118 -0
- data/lib/invoker/power/config.rb +53 -0
- data/lib/invoker/power/dns.rb +37 -0
- data/lib/invoker/power/port_finder.rb +49 -0
- data/lib/invoker/power/powerup.rb +26 -0
- data/lib/invoker/power/setup.rb +134 -0
- data/lib/invoker/runner.rb +8 -2
- data/lib/invoker/version.rb +1 -1
- data/readme.md +29 -0
- data/spec/invoker/commander_spec.rb +2 -2
- data/spec/invoker/config_spec.rb +106 -2
- data/spec/invoker/invoker_spec.rb +53 -0
- data/spec/invoker/power/config_spec.rb +24 -0
- data/spec/invoker/power/port_finder_spec.rb +16 -0
- data/spec/invoker/power/setup_spec.rb +87 -0
- metadata +104 -18
@@ -0,0 +1,53 @@
|
|
1
|
+
require "yaml"
|
2
|
+
module Invoker
|
3
|
+
module Power
|
4
|
+
# Save and Load Invoker::Power config
|
5
|
+
class ConfigExists < StandardError; end
|
6
|
+
class Config
|
7
|
+
CONFIG_LOCATION = File.join(ENV['HOME'], ".invoker")
|
8
|
+
def self.has_config?
|
9
|
+
File.exists?(CONFIG_LOCATION)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.create(options = {})
|
13
|
+
if has_config?
|
14
|
+
raise ConfigExists, "Config file already exists at location #{CONFIG_LOCATION}"
|
15
|
+
end
|
16
|
+
config = new(options)
|
17
|
+
config.save
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(options = {})
|
21
|
+
@config = options
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.load_config
|
25
|
+
config_hash = File.open(CONFIG_LOCATION, "r") { |fl| YAML.load(fl) }
|
26
|
+
new(config_hash)
|
27
|
+
end
|
28
|
+
|
29
|
+
def dns_port=(dns_port)
|
30
|
+
@config[:dns_port] = dns_port
|
31
|
+
end
|
32
|
+
|
33
|
+
def http_port=(http_port)
|
34
|
+
@config[:http_port] = http_port
|
35
|
+
end
|
36
|
+
|
37
|
+
def ipfw_rule_number=(ipfw_rule_number)
|
38
|
+
@config[:ipfw_rule_number] = ipfw_rule_number
|
39
|
+
end
|
40
|
+
|
41
|
+
def dns_port; @config[:dns_port]; end
|
42
|
+
def http_port; @config[:http_port]; end
|
43
|
+
def ipfw_rule_number; @config[:ipfw_rule_number]; end
|
44
|
+
|
45
|
+
def save
|
46
|
+
File.open(CONFIG_LOCATION, "w") do |fl|
|
47
|
+
YAML.dump(@config, fl)
|
48
|
+
end
|
49
|
+
self
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module Invoker
|
4
|
+
|
5
|
+
module Power
|
6
|
+
|
7
|
+
class DNS
|
8
|
+
IN = Resolv::DNS::Resource::IN
|
9
|
+
def self.server_ports
|
10
|
+
[
|
11
|
+
[:udp, '127.0.0.1', Invoker::CONFIG.dns_port],
|
12
|
+
[:tcp, '127.0.0.1', Invoker::CONFIG.dns_port]
|
13
|
+
]
|
14
|
+
end
|
15
|
+
|
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
|
26
|
+
|
27
|
+
# Default DNS handler
|
28
|
+
otherwise do |transaction|
|
29
|
+
transaction.failure!(:NXDomain)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Invoker
|
2
|
+
module Power
|
3
|
+
class PortFinder
|
4
|
+
STARTING_PORT = 23400
|
5
|
+
attr_accessor :dns_port, :http_port, :starting_port
|
6
|
+
def initialize
|
7
|
+
@starting_port = STARTING_PORT
|
8
|
+
@ports = []
|
9
|
+
@dns_port = nil
|
10
|
+
@http_port = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def find_ports
|
14
|
+
STARTING_PORT.upto(STARTING_PORT + 100) do |port|
|
15
|
+
break if @ports.size > 2
|
16
|
+
if check_if_port_is_open(port)
|
17
|
+
@ports << port
|
18
|
+
else
|
19
|
+
next
|
20
|
+
end
|
21
|
+
end
|
22
|
+
@dns_port = @ports[0]
|
23
|
+
@http_port = @ports[1]
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def check_if_port_is_open(port)
|
28
|
+
socket_flag = true
|
29
|
+
sockets = nil
|
30
|
+
begin
|
31
|
+
sockets = Socket.tcp_server_sockets(port)
|
32
|
+
socket_flag = false if sockets.size <= 1
|
33
|
+
rescue Errno::EADDRINUSE
|
34
|
+
socket_flag = false
|
35
|
+
end
|
36
|
+
sockets && close_socket_pairs(sockets)
|
37
|
+
socket_flag
|
38
|
+
end
|
39
|
+
|
40
|
+
def close_socket_pairs(sockets)
|
41
|
+
sockets.each { |s| s.close }
|
42
|
+
rescue
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Invoker
|
2
|
+
# power is really a stupid pun on pow.
|
3
|
+
module Power
|
4
|
+
class Powerup
|
5
|
+
def self.fork_and_start
|
6
|
+
powerup = new()
|
7
|
+
fork { powerup.run }
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
EM.epoll
|
12
|
+
EM.run {
|
13
|
+
trap("TERM") { stop }
|
14
|
+
trap("INT") { stop }
|
15
|
+
DNS.run_dns()
|
16
|
+
Balancer.run()
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def stop
|
21
|
+
Invoker::Logger.puts "Terminating Proxy/Server"
|
22
|
+
EventMachine.stop
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require "highline/import"
|
2
|
+
|
3
|
+
module Invoker
|
4
|
+
module Power
|
5
|
+
class Setup
|
6
|
+
RESOLVER_FILE = "/etc/resolver/dev"
|
7
|
+
FIREWALL_PLIST_FILE = "/Library/LaunchDaemons/com.codemancers.invoker.firewall.plist"
|
8
|
+
def self.install
|
9
|
+
installer = new
|
10
|
+
unless installer.check_if_already_setup?
|
11
|
+
installer.setup_invoker
|
12
|
+
else
|
13
|
+
Invoker::Logger.puts("The setup has been already run.".color(:red))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def setup_invoker
|
18
|
+
if setup_resolver_file
|
19
|
+
find_open_ports
|
20
|
+
install_resolver(port_finder.dns_port)
|
21
|
+
install_firewall(port_finder.http_port)
|
22
|
+
flush_dns_rules()
|
23
|
+
# Before writing the config file, drop down to a normal user
|
24
|
+
drop_to_normal_user()
|
25
|
+
create_config_file()
|
26
|
+
else
|
27
|
+
Invoker::Logger.puts("Invoker is not configured to serve from subdomains".color(:red))
|
28
|
+
end
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def drop_to_normal_user
|
33
|
+
EventMachine.set_effective_user(ENV["SUDO_USER"])
|
34
|
+
end
|
35
|
+
|
36
|
+
def flush_dns_rules
|
37
|
+
system("dscacheutil -flushcache")
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_config_file
|
41
|
+
Invoker::Power::Config.create(
|
42
|
+
dns_port: port_finder.dns_port,
|
43
|
+
http_port: port_finder.http_port
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_open_ports
|
48
|
+
port_finder.find_ports()
|
49
|
+
end
|
50
|
+
|
51
|
+
def port_finder
|
52
|
+
@port_finder ||= Invoker::Power::PortFinder.new()
|
53
|
+
end
|
54
|
+
|
55
|
+
def install_resolver(dns_port)
|
56
|
+
File.open(RESOLVER_FILE, "w") { |fl|
|
57
|
+
fl.write(resolve_string(dns_port))
|
58
|
+
}
|
59
|
+
rescue Errno::EACCES
|
60
|
+
Invoker::Logger.puts("Running setup requires root access, please rerun it with sudo".color(:red))
|
61
|
+
raise
|
62
|
+
end
|
63
|
+
|
64
|
+
def check_if_already_setup?
|
65
|
+
File.exists?(Invoker::Power::Config::CONFIG_LOCATION)
|
66
|
+
end
|
67
|
+
|
68
|
+
def install_firewall(balancer_port)
|
69
|
+
File.open(FIREWALL_PLIST_FILE, "w") { |fl|
|
70
|
+
fl.write(plist_string(balancer_port))
|
71
|
+
}
|
72
|
+
system("launchctl unload -w #{FIREWALL_PLIST_FILE} 2>/dev/null")
|
73
|
+
system("launchctl load -Fw #{FIREWALL_PLIST_FILE} 2>/dev/null")
|
74
|
+
end
|
75
|
+
|
76
|
+
# Ripped from POW code
|
77
|
+
def plist_string(balancer_port)
|
78
|
+
plist =<<-EOD
|
79
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
80
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
81
|
+
<plist version="1.0">
|
82
|
+
<dict>
|
83
|
+
<key>Label</key>
|
84
|
+
<string>com.codemancers.invoker</string>
|
85
|
+
<key>ProgramArguments</key>
|
86
|
+
<array>
|
87
|
+
<string>sh</string>
|
88
|
+
<string>-c</string>
|
89
|
+
<string>#{firewall_command(balancer_port)}</string>
|
90
|
+
</array>
|
91
|
+
<key>RunAtLoad</key>
|
92
|
+
<true/>
|
93
|
+
<key>UserName</key>
|
94
|
+
<string>root</string>
|
95
|
+
</dict>
|
96
|
+
</plist>
|
97
|
+
EOD
|
98
|
+
plist
|
99
|
+
end
|
100
|
+
|
101
|
+
def resolve_string(dns_port)
|
102
|
+
string =<<-EOD
|
103
|
+
nameserver 127.0.0.1
|
104
|
+
port #{dns_port}
|
105
|
+
EOD
|
106
|
+
string
|
107
|
+
end
|
108
|
+
|
109
|
+
# Ripped from Pow code
|
110
|
+
def firewall_command(balancer_port)
|
111
|
+
"ipfw add fwd 127.0.0.1,#{balancer_port} tcp from any to me dst-port 80 in"\
|
112
|
+
"&& sysctl -w net.inet.ip.forwarding=1"
|
113
|
+
end
|
114
|
+
|
115
|
+
def setup_resolver_file
|
116
|
+
return true unless File.exists?(RESOLVER_FILE)
|
117
|
+
Invoker::Logger.puts "Invoker has detected an existing Pow installation. We recommend "\
|
118
|
+
"that you uninstall pow and rerun this setup.".color(:red)
|
119
|
+
|
120
|
+
Invoker::Logger.puts "If you have already uninstalled Pow, proceed with installation"\
|
121
|
+
" by pressing y/n."
|
122
|
+
|
123
|
+
replace_resolver_flag = agree("Replace Pow configuration (y/n) : ")
|
124
|
+
|
125
|
+
if replace_resolver_flag
|
126
|
+
Invoker::Logger.puts "Invoker has overwritten one or more files created by Pow. "\
|
127
|
+
"If .dev domains still don't resolve locally. Try turning off the wi-fi"\
|
128
|
+
" and turning it on. It will force OSX to reload network configuration".color(:green)
|
129
|
+
end
|
130
|
+
replace_resolver_flag
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/lib/invoker/runner.rb
CHANGED
@@ -14,6 +14,8 @@ module Invoker
|
|
14
14
|
def self.run_command(selected_command)
|
15
15
|
return unless selected_command
|
16
16
|
case selected_command.command
|
17
|
+
when 'setup'
|
18
|
+
setup_pow(selected_command)
|
17
19
|
when 'start'
|
18
20
|
start_server(selected_command)
|
19
21
|
when 'add'
|
@@ -29,8 +31,12 @@ module Invoker
|
|
29
31
|
end
|
30
32
|
end
|
31
33
|
|
34
|
+
def self.setup_pow(selected_command)
|
35
|
+
Invoker::Power::Setup.install()
|
36
|
+
end
|
37
|
+
|
32
38
|
def self.start_server(selected_command)
|
33
|
-
config = Invoker::Parsers::Config.new(selected_command.file)
|
39
|
+
config = Invoker::Parsers::Config.new(selected_command.file, selected_command.port)
|
34
40
|
Invoker.const_set(:CONFIG, config)
|
35
41
|
warn_about_terminal_notifier()
|
36
42
|
commander = Invoker::Commander.new()
|
@@ -71,7 +77,7 @@ module Invoker
|
|
71
77
|
if RUBY_PLATFORM.downcase.include?("darwin")
|
72
78
|
command_path = `which terminal-notifier`
|
73
79
|
if !command_path || command_path.empty?
|
74
|
-
Invoker::Logger.puts("You can enable OSX notification for processes by installing terminal-notifier gem".red)
|
80
|
+
Invoker::Logger.puts("You can enable OSX notification for processes by installing terminal-notifier gem".color(:red))
|
75
81
|
end
|
76
82
|
end
|
77
83
|
end
|
data/lib/invoker/version.rb
CHANGED
data/readme.md
CHANGED
@@ -26,6 +26,35 @@ You need to start by creating a `ini` file which will define processes you want
|
|
26
26
|
directory = /home/gnufied/god_particle
|
27
27
|
command = zsh -c 'bundle exec ruby script/event_server'
|
28
28
|
|
29
|
+
## Invoker as Pow alternative
|
30
|
+
|
31
|
+
Invoker now supports pow like `.dev` subdomain. It will automatically
|
32
|
+
make all your app servers defined on subdomain `label.dev`.
|
33
|
+
|
34
|
+
To make it work though, you need to run following command, just once from anywhere:
|
35
|
+
|
36
|
+
~> sudo invoker setup
|
37
|
+
|
38
|
+
Now because invoker is making your app server available on a subdomain. It requires
|
39
|
+
control over port on which your applications will be listening. This can be simply
|
40
|
+
done by replacing specific port number in `ini` file with `$PORT`. For example:
|
41
|
+
|
42
|
+
[spree]
|
43
|
+
directory = /home/gnufied/spree
|
44
|
+
command = zsh -c 'bundle exec rails s -p $PORT'
|
45
|
+
|
46
|
+
[api]
|
47
|
+
directory = /home/gnufied/api
|
48
|
+
command = zsh -c 'bundle exec rails s -p $PORT'
|
49
|
+
|
50
|
+
[events]
|
51
|
+
directory = /home/gnufied/god_particle
|
52
|
+
command = zsh -c 'bundle exec ruby script/event_server'
|
53
|
+
|
54
|
+
The subdomain feature won't work for apps which don't use `$PORT` placeholder.
|
55
|
+
|
56
|
+
## Running Invoker
|
57
|
+
|
29
58
|
After that you can start process manager via:
|
30
59
|
|
31
60
|
~> invoker start invoker.ini
|
@@ -23,6 +23,7 @@ describe "Invoker::Commander" do
|
|
23
23
|
|
24
24
|
it "should find command by label and start it, if found" do
|
25
25
|
invoker_config.stubs(:processes).returns([OpenStruct.new(:label => "resque", :cmd => "foo", :dir => "bar")])
|
26
|
+
invoker_config.expects(:process).returns(OpenStruct.new(:label => "resque", :cmd => "foo", :dir => "bar"))
|
26
27
|
@commander.expects(:add_command).returns(true)
|
27
28
|
|
28
29
|
@commander.add_command_by_label("resque")
|
@@ -97,8 +98,7 @@ describe "Invoker::Commander" do
|
|
97
98
|
|
98
99
|
worker.should.not.equal nil
|
99
100
|
worker.command_label.should.equal "sleep"
|
100
|
-
worker.color.should.equal
|
101
|
-
|
101
|
+
worker.color.should.equal :green
|
102
102
|
|
103
103
|
pipe_end_worker = @commander.open_pipes[worker.pipe_end.fileno]
|
104
104
|
pipe_end_worker.should.not.equal nil
|
data/spec/invoker/config_spec.rb
CHANGED
@@ -16,12 +16,116 @@ command = ruby try_sleep.rb
|
|
16
16
|
file.write(config_data)
|
17
17
|
file.close
|
18
18
|
lambda {
|
19
|
-
Invoker::Parsers::Config.new(file.path)
|
19
|
+
Invoker::Parsers::Config.new(file.path, 9000)
|
20
20
|
}.should.raise(Invoker::Errors::InvalidConfig)
|
21
21
|
ensure
|
22
22
|
file.unlink()
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|
26
|
-
end
|
27
26
|
|
27
|
+
describe "for ports" do
|
28
|
+
it "should replace port in commands" do
|
29
|
+
begin
|
30
|
+
file = Tempfile.new("invalid_config.ini")
|
31
|
+
|
32
|
+
config_data =<<-EOD
|
33
|
+
[try_sleep]
|
34
|
+
directory = /tmp
|
35
|
+
command = ruby try_sleep.rb -p $PORT
|
36
|
+
|
37
|
+
[ls]
|
38
|
+
directory = /tmp
|
39
|
+
command = ls -p $PORT
|
40
|
+
|
41
|
+
[noport]
|
42
|
+
directory = /tmp
|
43
|
+
command = ls
|
44
|
+
EOD
|
45
|
+
file.write(config_data)
|
46
|
+
file.close
|
47
|
+
|
48
|
+
config = Invoker::Parsers::Config.new(file.path, 9000)
|
49
|
+
command1 = config.processes.first
|
50
|
+
|
51
|
+
command1.port.should == 9001
|
52
|
+
command1.cmd.should =~ /9001/
|
53
|
+
|
54
|
+
command2 = config.processes[1]
|
55
|
+
|
56
|
+
command2.port.should == 9002
|
57
|
+
command2.cmd.should =~ /9002/
|
58
|
+
|
59
|
+
command2 = config.processes[2]
|
60
|
+
|
61
|
+
command2.port.should == nil
|
62
|
+
ensure
|
63
|
+
file.unlink()
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should use port from separate option" do
|
68
|
+
begin
|
69
|
+
file = Tempfile.new("invalid_config.ini")
|
70
|
+
config_data =<<-EOD
|
71
|
+
[try_sleep]
|
72
|
+
directory = /tmp
|
73
|
+
command = ruby try_sleep.rb -p $PORT
|
74
|
+
|
75
|
+
[ls]
|
76
|
+
directory = /tmp
|
77
|
+
port = 3000
|
78
|
+
command = pwd
|
79
|
+
|
80
|
+
[noport]
|
81
|
+
directory = /tmp
|
82
|
+
command = ls
|
83
|
+
EOD
|
84
|
+
file.write(config_data)
|
85
|
+
file.close
|
86
|
+
|
87
|
+
config = Invoker::Parsers::Config.new(file.path, 9000)
|
88
|
+
command1 = config.processes.first
|
89
|
+
|
90
|
+
command1.port.should == 9001
|
91
|
+
command1.cmd.should =~ /9001/
|
92
|
+
|
93
|
+
command2 = config.processes[1]
|
94
|
+
|
95
|
+
command2.port.should == 3000
|
96
|
+
|
97
|
+
command2 = config.processes[2]
|
98
|
+
|
99
|
+
command2.port.should == nil
|
100
|
+
ensure
|
101
|
+
file.unlink()
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "loading power config" do
|
107
|
+
before do
|
108
|
+
@file = Tempfile.new("config.ini")
|
109
|
+
end
|
110
|
+
|
111
|
+
it "does not load config if platform is not darwin" do
|
112
|
+
Invoker.expects(:darwin?).returns(false)
|
113
|
+
Invoker::Power::Config.expects(:load_config).never
|
114
|
+
Invoker::Parsers::Config.new(@file.path, 9000)
|
115
|
+
end
|
116
|
+
|
117
|
+
it "does not load config if platform is darwin but there is no power config file" do
|
118
|
+
Invoker.expects(:darwin?).returns(true)
|
119
|
+
File.expects(:exists?).with(Invoker::Power::Config::CONFIG_LOCATION).returns(false)
|
120
|
+
Invoker::Power::Config.expects(:load_config).never
|
121
|
+
Invoker::Parsers::Config.new(@file.path, 9000)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "loads config if platform is darwin and power config file exists" do
|
125
|
+
Invoker.expects(:darwin?).returns(true)
|
126
|
+
File.expects(:exists?).with(Invoker::Power::Config::CONFIG_LOCATION).returns(true)
|
127
|
+
Invoker::Power::Config.expects(:load_config).once
|
128
|
+
Invoker::Parsers::Config.new(@file.path, 9000)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|