invoker 0.1.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ "&amp;&amp; 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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Invoker
2
- VERSION = "0.1.2"
2
+ VERSION = "1.0.0"
3
3
  end
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 "green"
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
@@ -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