invoker 0.1.2 → 1.0.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.
- 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
|