puggernaut 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ .DS_Store
2
+ *.gem
3
+ coverage
4
+ log/*
5
+ pkg
6
+ tmp
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2010
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ Puggernaut
2
+ ==========
3
+
4
+ Simple server push implementation using eventmachine and long polling.
5
+
6
+ ![Puggernaut](/winton/puggernaut/raw/master/puggernaut.png)
7
+
8
+ Requirements
9
+ ------------
10
+
11
+ <pre>
12
+ gem install puggernaut
13
+ </pre>
14
+
15
+ How it works
16
+ ------------
17
+
18
+ Puggernaut consists of three pieces:
19
+
20
+ * TCP client to send push messages
21
+ * TCP server to receive push messages
22
+ * HTTP server to deliver long poll requests
23
+
24
+ Start it up
25
+ -----------
26
+
27
+ Run the <code>puggernaut</code> binary with optional port numbers:
28
+
29
+ <pre>
30
+ puggernaut &lt;http port&gt; &lt;tcp port&gt;
31
+ </pre>
32
+
33
+ The default HTTP and TCP ports are 8000 and 8001, respectively.
34
+
35
+ Set up proxy pass
36
+ -----------------
37
+
38
+ You will need to set up a URL on your public facing web server that points to the Puggernaut HTTP server.
39
+
40
+ If you do not see your web server below, [Google](http://google.com) is your friend.
41
+
42
+ ### Apache
43
+
44
+ *http.conf*
45
+
46
+ <pre>
47
+ ProxyPass /long_poll http://localhost:8000/
48
+ ProxyPassReverse /long_poll http://localhost:8000/
49
+ </pre>
50
+
51
+ ### Nginx
52
+
53
+ *nginx.conf*
54
+
55
+ <pre>
56
+ location /long_poll {
57
+ proxy_pass http://localhost:8000/;
58
+ }
59
+ </pre>
60
+
61
+ Send push messages
62
+ ------------------
63
+
64
+ <pre>
65
+ require 'puggernaut'
66
+
67
+ client = Puggernaut::Client.new("localhost:8001", "localhost:9001")
68
+ client.push :channel => "message"
69
+ client.push :channel => [ "message 1", "message 2" ], :channel_2 => "message"
70
+ </pre>
71
+
72
+ The <code>Client.new</code> initializer accepts any number of TCP server addresses.
73
+
74
+ Receive push messages
75
+ ---------------------
76
+
77
+ Include [puggernaut.js](/winton/puggernaut/public/puggernaut.js) into to your HTML page.
78
+
79
+ Javascript client example:
80
+
81
+ <pre>
82
+ Puggernaut.path = '/long_poll';
83
+
84
+ Puggernaut
85
+ .watch('channel', function(e, message) {
86
+ // do something with message
87
+ })
88
+ .watch('channel_2', function(e, message) {
89
+ // do something with message
90
+ });
91
+
92
+ Puggernaut.unwatch('channel');
93
+ </pre>
data/Rakefile ADDED
@@ -0,0 +1,90 @@
1
+ require File.dirname(__FILE__) + '/lib/puggernaut/gems'
2
+
3
+ Puggernaut::Gems.activate %w(rake rspec)
4
+
5
+ require 'rake'
6
+ require 'spec/rake/spectask'
7
+
8
+ def gemspec
9
+ @gemspec ||= begin
10
+ file = File.expand_path('../puggernaut.gemspec', __FILE__)
11
+ eval(File.read(file), binding, file)
12
+ end
13
+ end
14
+
15
+ if defined?(Spec::Rake::SpecTask)
16
+ desc "Run specs"
17
+ Spec::Rake::SpecTask.new do |t|
18
+ t.spec_files = FileList['spec/**/*_spec.rb']
19
+ t.spec_opts = %w(-fs --color)
20
+ t.warning = true
21
+ end
22
+ task :spec
23
+ task :default => :spec
24
+ end
25
+
26
+ desc "Build gem(s)"
27
+ task :gem do
28
+ old_gemset = ENV['GEMSET']
29
+ root = File.expand_path('../', __FILE__)
30
+ pkg = "#{root}/pkg"
31
+ system "rm -Rf #{pkg}"
32
+ Puggernaut::Gems.gemset_names.each do |gemset|
33
+ ENV['GEMSET'] = gemset.to_s
34
+ system "cd #{root} && gem build puggernaut.gemspec"
35
+ system "mkdir -p #{pkg} && mv *.gem pkg"
36
+ end
37
+ ENV['GEMSET'] = old_gemset
38
+ end
39
+
40
+ namespace :gem do
41
+ desc "Install gem(s)"
42
+ task :install do
43
+ Rake::Task['gem'].invoke
44
+ Dir["#{File.dirname(__FILE__)}/pkg/*.gem"].each do |pkg|
45
+ system "gem install #{pkg} --no-ri --no-rdoc"
46
+ end
47
+ end
48
+
49
+ desc "Push gem(s)"
50
+ task :push do
51
+ Rake::Task['gem'].invoke
52
+ Dir["#{File.dirname(__FILE__)}/pkg/*.gem"].each do |pkg|
53
+ system "gem push #{pkg}"
54
+ end
55
+ end
56
+ end
57
+
58
+ namespace :gems do
59
+ desc "Install gem dependencies (DEV=0 DOCS=0 GEMSPEC=default SUDO=0)"
60
+ task :install do
61
+ dev = ENV['DEV'] == '1'
62
+ docs = ENV['DOCS'] == '1' ? '' : '--no-ri --no-rdoc'
63
+ gemset = ENV['GEMSET']
64
+ sudo = ENV['SUDO'] == '1' ? 'sudo' : ''
65
+
66
+ Puggernaut::Gems.gemset = gemset if gemset
67
+
68
+ if dev
69
+ gems = Puggernaut::Gems.gemspec.development_dependencies
70
+ else
71
+ gems = Puggernaut::Gems.gemspec.dependencies
72
+ end
73
+
74
+ gems.each do |name|
75
+ name = name.to_s
76
+ version = Puggernaut::Gems.versions[name]
77
+ if Gem.source_index.find_name(name, version).empty?
78
+ version = version ? "-v #{version}" : ''
79
+ system "#{sudo} gem install #{name} #{version} #{docs}"
80
+ else
81
+ puts "already installed: #{name} #{version}"
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ desc "Validate the gemspec"
88
+ task :gemspec do
89
+ gemspec.validate
90
+ end
data/bin/puggernaut ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/puggernaut")
4
+
5
+ Puggernaut::Server.new(*ARGV)
data/config.ru ADDED
@@ -0,0 +1,3 @@
1
+ require "#{File.dirname(__FILE__)}/lib/puggernaut/spec_server"
2
+
3
+ run SpecServer
@@ -0,0 +1,5 @@
1
+ puggernaut:
2
+ eventmachine: ~>0.12.10
3
+ rake: >=0.8.7
4
+ rspec: ~>1.0
5
+ sinatra: ~>1.1.0
@@ -0,0 +1,11 @@
1
+ name: puggernaut
2
+ version: 0.1.0
3
+ authors:
4
+ - Winton Welsh
5
+ email: mail@wintoni.us
6
+ homepage: http://github.com/winton/puggernaut
7
+ summary: Simple server push implementation using eventmachine and long polling
8
+ description: Simple server push implementation using eventmachine and long polling
9
+ dependencies:
10
+ - eventmachine
11
+ development_dependencies: null
data/lib/puggernaut.rb ADDED
@@ -0,0 +1,14 @@
1
+ require File.dirname(__FILE__) + '/puggernaut/gems'
2
+
3
+ Puggernaut::Gems.activate %w(eventmachine)
4
+
5
+ require 'eventmachine'
6
+
7
+ $:.unshift File.dirname(__FILE__)
8
+
9
+ require 'puggernaut/logger'
10
+ require 'puggernaut/client'
11
+ require 'puggernaut/server'
12
+
13
+ module Puggernaut
14
+ end
@@ -0,0 +1,66 @@
1
+ require "#{File.dirname(__FILE__)}/logger"
2
+ require 'socket'
3
+
4
+ module Puggernaut
5
+ class Client
6
+
7
+ include Logger
8
+
9
+ attr_accessor :connections
10
+
11
+ def initialize(*servers)
12
+ @connections = {}
13
+ @retry = []
14
+ @servers = servers.collect { |s| s.split(':') }
15
+ end
16
+
17
+ def close
18
+ @connections.each do |host_port, connection|
19
+ connection.close
20
+ logger.info "Client#close - #{host_port}"
21
+ end
22
+ end
23
+
24
+ def push(messages)
25
+ messages = messages.collect do |channel, message|
26
+ if message.is_a?(::Array)
27
+ message.collect { |m| "#{channel}|#{m}" }.join("\n")
28
+ else
29
+ "#{channel}|#{message}"
30
+ end
31
+ end
32
+ unless messages.empty?
33
+ @servers.each do |(host, port)|
34
+ send host, port, messages.join("\n")
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def send(host, port, data, try_retry=true)
42
+ try if try_retry
43
+ begin
44
+ host_port = "#{host}:#{port}"
45
+ logger.info "Client#send - #{host_port} - #{data}"
46
+ connection = @connections[host_port] ||= TCPSocket.open(host, port)
47
+ connection.print(data)
48
+ response = connection.gets
49
+ raise 'not ok' if !response || !response.include?('OK')
50
+ rescue Exception => e
51
+ logger.info "Client#send - Exception - #{e.message} - #{host_port} - #{data}"
52
+ @retry << [ host, port, data ]
53
+ @retry.shift if @retry.length > 10
54
+ end
55
+ try if try_retry
56
+ end
57
+
58
+ def try
59
+ @retry.length.times do
60
+ host, port, data = @retry.shift
61
+ @connections.delete("#{host}:#{port}")
62
+ send host, port, data, false
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,154 @@
1
+ unless defined?(Puggernaut::Gems)
2
+
3
+ require 'yaml'
4
+
5
+ module Puggernaut
6
+ module Gems
7
+ class <<self
8
+
9
+ attr_accessor :config
10
+ attr_reader :gemset, :gemsets, :versions
11
+
12
+ class SimpleStruct
13
+ attr_reader :hash
14
+
15
+ def initialize(hash)
16
+ @hash = hash
17
+ @hash.each do |key, value|
18
+ self.class.send(:define_method, key) { @hash[key] }
19
+ self.class.send(:define_method, "#{key}=") { |v| @hash[key] = v }
20
+ end
21
+ end
22
+ end
23
+
24
+ Gems.config = SimpleStruct.new(
25
+ :gemsets => [ "#{File.expand_path('../../../', __FILE__)}/config/gemsets.yml" ],
26
+ :gemspec => "#{File.expand_path('../../../', __FILE__)}/config/gemspec.yml",
27
+ :warn => true
28
+ )
29
+
30
+ def activate(*gems)
31
+ begin
32
+ require 'rubygems' unless defined?(::Gem)
33
+ rescue LoadError
34
+ puts "rubygems library could not be required" if @config.warn
35
+ end
36
+
37
+ self.gemset ||= gemset_from_loaded_specs
38
+
39
+ gems.flatten.collect(&:to_sym).each do |name|
40
+ version = @versions[name]
41
+ vendor = File.expand_path("../../../vendor/#{name}/lib", __FILE__)
42
+ if File.exists?(vendor)
43
+ $:.unshift vendor
44
+ elsif defined?(gem)
45
+ gem name.to_s, version
46
+ else
47
+ puts "#{name} #{"(#{version})" if version} failed to activate" if @config.warn
48
+ end
49
+ end
50
+ end
51
+
52
+ def dependencies
53
+ dependency_filter(@gemspec.dependencies, @gemset)
54
+ end
55
+
56
+ def development_dependencies
57
+ dependency_filter(@gemspec.development_dependencies, @gemset)
58
+ end
59
+
60
+ def gemset=(gemset)
61
+ if gemset
62
+ @gemset = gemset.to_sym
63
+
64
+ @gemsets = @config.gemsets.reverse.collect { |config|
65
+ if config.is_a?(::String)
66
+ YAML::load(File.read(config)) rescue {}
67
+ elsif config.is_a?(::Hash)
68
+ config
69
+ end
70
+ }.inject({}) do |hash, config|
71
+ deep_merge(hash, symbolize_keys(config))
72
+ end
73
+
74
+ @versions = (@gemsets[gemspec.name.to_sym] || {}).inject({}) do |hash, (key, value)|
75
+ if !value.is_a?(::Hash) && value
76
+ hash[key] = value
77
+ elsif key == @gemset
78
+ (value || {}).each { |k, v| hash[k] = v }
79
+ end
80
+ hash
81
+ end
82
+ else
83
+ @gemset = nil
84
+ @gemsets = nil
85
+ @versions = nil
86
+ end
87
+ end
88
+
89
+ def gemset_names
90
+ (
91
+ [ :default ] +
92
+ @gemsets[gemspec.name.to_sym].inject([]) { |array, (key, value)|
93
+ array.push(key) if value.is_a?(::Hash) || value.nil?
94
+ array
95
+ }
96
+ ).uniq
97
+ end
98
+
99
+ def gemspec(reload=false)
100
+ if @gemspec && !reload
101
+ @gemspec
102
+ else
103
+ data = YAML::load(File.read(@config.gemspec)) rescue {}
104
+ @gemspec = SimpleStruct.new(data)
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ def deep_merge(first, second)
111
+ merger = lambda do |key, v1, v2|
112
+ Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2
113
+ end
114
+ first.merge(second, &merger)
115
+ end
116
+
117
+ def dependency_filter(dependencies, match)
118
+ (dependencies || []).inject([]) { |array, value|
119
+ if value.is_a?(::Hash)
120
+ array += value[match.to_s] if value[match.to_s]
121
+ else
122
+ array << value
123
+ end
124
+ array
125
+ }.uniq.collect(&:to_sym)
126
+ end
127
+
128
+ def gemset_from_loaded_specs
129
+ if defined?(Gem)
130
+ Gem.loaded_specs.each do |name, spec|
131
+ if name == gemspec.name
132
+ return :default
133
+ elsif name[0..gemspec.name.length] == "#{gemspec.name}-"
134
+ return name[gemspec.name.length+1..-1].to_sym
135
+ end
136
+ end
137
+ :default
138
+ else
139
+ :none
140
+ end
141
+ end
142
+
143
+ def symbolize_keys(hash)
144
+ return {} unless hash.is_a?(::Hash)
145
+ hash.inject({}) do |options, (key, value)|
146
+ value = symbolize_keys(value) if value.is_a?(::Hash)
147
+ options[(key.to_sym rescue key) || key] = value
148
+ options
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end