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.
@@ -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