journeta 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ == 0.0.2 2008-08-19
2
+
3
+ * First working RubyForge release:
4
+ * See README.txt for general info.
5
+ * Adding functional chat room example. (See examples/)
6
+
7
+ == 0.0.1 2007-07-25
8
+
9
+ * Initial release:
10
+ * Basic port from original Java code.
11
+ * NOT YET FUNCTIONAL!
@@ -0,0 +1,11 @@
1
+ Copyright (c) 2007, OpenRain, LLC. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ -Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ -Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ -Neither the name of the OpenRain, LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,27 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ lib/journeta.rb
7
+ examples/instant_messenger.rb
8
+ lib/journeta/logger.rb
9
+ lib/journeta/version.rb
10
+ lib/journeta/asynchronous.rb
11
+ lib/journeta/session_listener.rb
12
+ lib/journeta/session_handler.rb
13
+ lib/journeta/event_broadcaster.rb
14
+ lib/journeta/event_listener.rb
15
+ lib/journeta/presence_message.rb
16
+ lib/journeta/peer_registry.rb
17
+ lib/journeta/peer_connection.rb
18
+ lib/journeta/journeta_engine.rb
19
+ scripts/txt2html
20
+ setup.rb
21
+ test/test_journeta.rb
22
+ test/test_helper.rb
23
+ website/index.html
24
+ website/index.txt
25
+ website/javascripts/rounded_corners_lite.inc.js
26
+ website/stylesheets/screen.css
27
+ website/template.rhtml
@@ -0,0 +1,41 @@
1
+ = Journeta
2
+
3
+ == About
4
+
5
+
6
+ Journeta is a dirt simple peer discovery and message passing library for processes on the same LAN,
7
+ requiring no advanced networking knowledge to use.
8
+
9
+ Only core Ruby libraries are required, making the library fairly light. As all data is sent accross
10
+ the wire in YAML form, any arbitrary Ruby object can be sent to peers, provided they..
11
+
12
+ * Are running a compatible Journeta version, and
13
+ * Have access to the same class definitions if you are sending your own custom objects.
14
+ * Do not have a firewall preventing
15
+
16
+ Journeta uses Ruby threading to manage the asynchonous nature of peer-to-peer I/O.
17
+ For insight into events internal to the library, start ruby with the `--debug` options.
18
+
19
+
20
+ == Use
21
+
22
+
23
+ examples/instant_messenger.rb
24
+
25
+ A completely distributed, zero-configuration-required chat room script.
26
+ Fire up several instances in separate terminals. Multiple instances on the same machine is ok.
27
+ Everything you type will automatically be sent to all other instances on the LAN!
28
+ Use `ruby --debug examples/instant_messenger.rb` for detailed internal event details.
29
+
30
+
31
+ == Author
32
+
33
+ Preston Lee <preston.lee at openrain d0t com>
34
+ http://www.prestonlee.com
35
+ http://www.openrain.com
36
+
37
+
38
+ == Links
39
+
40
+ How Journeta discovers peers using UDP multicasting..
41
+ http://onestepback.org/index.cgi/Tech/Ruby/MulticastingInRuby.red
@@ -0,0 +1,123 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/rubyforgepublisher'
9
+ require 'fileutils'
10
+ require 'hoe'
11
+
12
+ include FileUtils
13
+ require File.join(File.dirname(__FILE__), 'lib', 'journeta', 'version')
14
+
15
+ AUTHOR = 'Preston Lee' # can also be an array of Authors
16
+ EMAIL = "preston.lee@openrain.com"
17
+ DESCRIPTION = "A zero-configuration-required peer-to-peer (P2P) discovery and communications library for closed networks."
18
+ GEM_NAME = 'journeta' # what ppl will type to install your gem
19
+
20
+ @config_file = "~/.rubyforge/user-config.yml"
21
+ @config = nil
22
+ def rubyforge_username
23
+ unless @config
24
+ begin
25
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
26
+ rescue
27
+ puts <<-EOS
28
+ ERROR: No rubyforge config file found: #{@config_file}"
29
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
30
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
31
+ EOS
32
+ exit
33
+ end
34
+ end
35
+ @rubyforge_username ||= @config["username"]
36
+ end
37
+
38
+ RUBYFORGE_PROJECT = 'journeta' # The unix name for your project
39
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
40
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
41
+
42
+ NAME = "journeta"
43
+ REV = nil
44
+ # UNCOMMENT IF REQUIRED:
45
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
46
+ VERS = Journeta::VERSION::STRING + (REV ? ".#{REV}" : "")
47
+ CLEAN.include ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store']
48
+ RDOC_OPTS = ['--quiet', '--title', 'journeta documentation',
49
+ "--opname", "index.html",
50
+ "--line-numbers",
51
+ "--main", "README",
52
+ "--inline-source"]
53
+
54
+ class Hoe
55
+ def extra_deps
56
+ @extra_deps.reject { |x| Array(x).first == 'hoe' }
57
+ end
58
+ end
59
+
60
+ # Generate all the Rake tasks
61
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
62
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
63
+ p.author = AUTHOR
64
+ p.description = DESCRIPTION
65
+ p.email = EMAIL
66
+ p.summary = DESCRIPTION
67
+ p.url = HOMEPATH
68
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
69
+ p.test_globs = ["test/**/test_*.rb"]
70
+ p.clean_globs |= CLEAN #An array of file patterns to delete on clean.
71
+
72
+ # == Optional
73
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
74
+ #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
75
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
76
+ end
77
+
78
+ CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\n\n")
79
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
80
+ hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
81
+
82
+ desc 'Generate website files'
83
+ task :website_generate do
84
+ Dir['website/**/*.txt'].each do |txt|
85
+ sh %{ ruby scripts/txt2html #{txt} > #{txt.gsub(/txt$/,'html')} }
86
+ end
87
+ end
88
+
89
+ desc 'Upload website files to rubyforge'
90
+ task :website_upload do
91
+ host = "#{rubyforge_username}@rubyforge.org"
92
+ remote_dir = "/var/www/gforge-projects/#{PATH}/"
93
+ local_dir = 'website'
94
+ sh %{rsync -aCv #{local_dir}/ #{host}:#{remote_dir}}
95
+ end
96
+
97
+ desc 'Generate and upload website files'
98
+ task :website => [:website_generate, :website_upload, :publish_docs]
99
+
100
+ desc 'Release the website and new gem version'
101
+ task :deploy => [:check_version, :website, :release] do
102
+ puts "Remember to create SVN tag:"
103
+ puts "svn copy svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/trunk " +
104
+ "svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/tags/REL-#{VERS} "
105
+ puts "Suggested comment:"
106
+ puts "Tagging release #{CHANGES}"
107
+ end
108
+
109
+ desc 'Runs tasks website_generate and install_gem as a local deployment of the gem'
110
+ task :local_deploy => [:website_generate, :install_gem]
111
+
112
+ task :check_version do
113
+ unless ENV['VERSION']
114
+ puts 'Must pass a VERSION=x.y.z release version'
115
+ exit
116
+ end
117
+ unless ENV['VERSION'] == VERS
118
+ puts "Please update your version.rb to match the release version, currently #{VERS}"
119
+ exit
120
+ end
121
+ end
122
+
123
+
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby
2
+ current_dir = File.dirname(File.expand_path(__FILE__))
3
+ lib_path = File.join(current_dir, '..', 'lib')
4
+ $LOAD_PATH.unshift lib_path
5
+
6
+ # Load up the library!
7
+ require 'journeta'
8
+ include Journeta
9
+
10
+
11
+ # Any arbitrary object can be sent to peers as long as it's serializable to YAML.
12
+ # We'll create an ordinary class with a couple typical-looking fields to send to our peers.
13
+ class ExampleMessage
14
+ attr_accessor :name
15
+ attr_accessor :text
16
+ end
17
+
18
+ # A message handler will be called by the engine every time a message is received.
19
+ # This code will be customized for your application-specific needs.
20
+ class ExampleHandler < Journeta::DefaultSessionHandler
21
+ def handle(message)
22
+ puts "#{message.name.chop}: #{message.text}"
23
+ end
24
+ end
25
+
26
+ # Now we'll create an instance of the Journeta P2P engine.
27
+ # We'll change the default incoming session port to a
28
+ # pseudo-randomly generated number so multiple instances
29
+ # may be started on the same machine.
30
+ #
31
+ # You'll need to find an unused port if..
32
+ # (1) you intend to run multiple peers on the same machine, or
33
+ # (2) the default port (Journeta::JournetaEngine::DEFAULT_SESSION_PORT)
34
+ # is otherwise already taken on your machine.
35
+ session_port = (2048 + rand( 2 ** 8))
36
+ journeta = Journeta::JournetaEngine.new(:session_port => session_port, :session_handler => ExampleHandler.new)
37
+
38
+
39
+ # Let the magic begin!
40
+ journeta.start
41
+
42
+ puts "What's your name?"
43
+ name = gets
44
+
45
+ # The `known_peers` call allows you to access the registry of known available peers on the network.
46
+ # The UUID associated which each peer will be unique accross the network.
47
+ peers = journeta.known_peers
48
+ if peers.size > 0
49
+ puts 'The following peers IDs are online..'
50
+ peers.each do |uuid, peer|
51
+ puts " #{uuid}; version #{peer.version}"
52
+ end
53
+ else
54
+ puts 'No peers known. (Start another client!)'
55
+ end
56
+
57
+
58
+ # Sit around are watch events at the console until the user hits <enter>
59
+ puts 'Text you enter here will automatically be shown on peers terminals.'
60
+ begin
61
+ loop do
62
+ input = gets
63
+ m = ExampleMessage.new
64
+ m.name = name
65
+ m.text = input
66
+ journeta.send_to_known_peers(m)
67
+ end
68
+ ensure
69
+ end
70
+
71
+ # Please stop the engine when shutting down. This broadcasts a message
72
+ # stating you are going offline as a courtesy to your peers.
73
+ journeta.stop
74
+
75
+ # The engine can be restarted and stopped as many times as you'd like.
76
+ journeta.start
77
+ journeta.stop
@@ -0,0 +1,16 @@
1
+
2
+ require 'yaml'
3
+
4
+
5
+ require 'journeta/logger'
6
+ require 'journeta/version'
7
+ require 'journeta/asynchronous'
8
+
9
+ require 'journeta/session_listener'
10
+ require 'journeta/session_handler'
11
+ require 'journeta/event_broadcaster'
12
+ require 'journeta/event_listener'
13
+ require 'journeta/presence_message'
14
+ require 'journeta/peer_registry'
15
+ require 'journeta/peer_connection'
16
+ require 'journeta/journeta_engine'
@@ -0,0 +1,34 @@
1
+ require 'journeta/logger'
2
+
3
+ module Journeta
4
+
5
+ class Asynchronous
6
+
7
+ include Logger
8
+
9
+ attr_accessor :thread, :engine
10
+
11
+ def initialize(engine)
12
+ @engine = engine
13
+ end
14
+
15
+ def start
16
+ putsd "Creating asynchronous thread for I/O: #{self.class.to_s}."
17
+ @thread = Thread.new {
18
+ go # @engine
19
+ }
20
+ end
21
+
22
+ def stop
23
+ Thread.kill(@thread)
24
+ @thread.join
25
+ @thread = nil
26
+ end
27
+
28
+ def go #(engine)
29
+ raise NotImplementedException
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,36 @@
1
+ #require 'yaml'
2
+
3
+ require 'journeta/asynchronous'
4
+
5
+
6
+ module Journeta
7
+
8
+ class EventBroadcaster < Journeta::Asynchronous
9
+
10
+ attr_accessor :thread
11
+
12
+ def go #(engine)
13
+ address = @engine.configuration[:event_address]
14
+ port = @engine.configuration[:event_port]
15
+ delay = @engine.configuration[:event_period]
16
+ uuid = @engine.configuration[:uuid]
17
+ session_port = @engine.configuration[:session_port]
18
+ begin
19
+ socket = UDPSocket.open
20
+ socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_TTL, [1].pack('i'))
21
+ loop do
22
+ putsd "Sending presence event."
23
+ note = PresenceMessage.new uuid, session_port
24
+ socket.send(note.to_yaml, 0, address, port)
25
+ sleep delay
26
+ end
27
+ ensure
28
+ putsd "Closing event broadcaster socket."
29
+ socket.close
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
@@ -0,0 +1,57 @@
1
+ require 'yaml'
2
+ require 'socket'
3
+ require 'ipaddr'
4
+ require 'journeta/asynchronous'
5
+ require 'journeta/peer_connection'
6
+
7
+ module Journeta
8
+
9
+ # uses the fucked up socket api to listen for events broadcast from peers
10
+ class EventListener < Journeta::Asynchronous
11
+
12
+
13
+ def go #(engine)
14
+ event_address = @engine.configuration[:event_address]
15
+ port = @engine.configuration[:event_port]
16
+ addresses = IPAddr.new(event_address).hton + IPAddr.new("0.0.0.0").hton
17
+ begin
18
+ socket = UDPSocket.new
19
+ # Remember how i said this was fucked up? yeaahhhhhh. i hope you like C.
20
+ # `man setsockopt` for details.
21
+ # SO_REUSEPORT is needed so multiple peers can be run on the same machine.
22
+ socket.setsockopt( Socket::SOL_SOCKET, Socket::SO_REUSEPORT, [1].pack("i_") )
23
+ # socket.setsockopt( Socket::SOL_SOCKET, Socket::SO_REUSEADDR, [1].pack("i_") )
24
+ socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, addresses)
25
+ socket.bind(Socket::INADDR_ANY, port)
26
+ putsd "Waiting for presence events."
27
+ loop do
28
+ # Why 1024? umm.. because it's Thursday!
29
+ data, meta = socket.recvfrom 1024
30
+ Thread.new(data) {
31
+ event = YAML.load(data)
32
+ if event.uuid != @engine.configuration[:uuid]
33
+ # putsd "New Event: #{data} #{meta.inspect}"
34
+ # Update registry
35
+ m = YAML::load(data)
36
+ peer = PeerConnection.new
37
+ # require 'pp'
38
+ # pp m
39
+ # Why is this always [2]? Not sure.. they should have returned a hash instead.
40
+ peer.ip_address = meta[2]
41
+ peer.session_port = m.session_port
42
+ peer.uuid = m.uuid
43
+ peer.version = m.version
44
+ @engine.register_peer peer
45
+ end
46
+ }
47
+ # putsd "Event received!"
48
+ end
49
+ ensure
50
+ putsd "Closing event listener socket."
51
+ socket.close
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,114 @@
1
+ # Copyright © 2007 OpenRain, LLC. All rights reserved.
2
+ #
3
+ # Preston Lee <preston.lee@openrain.com>
4
+
5
+
6
+ module Journeta
7
+
8
+ class JournetaEngine
9
+
10
+ include Logger
11
+
12
+ # Continuously sends out "i'm here" presence messages to the local network
13
+ attr_accessor :event_broadcaster
14
+ # continuously listens for "i'm here" presence messages from other peers
15
+ attr_accessor :event_listener
16
+
17
+ # Constantly listens for incoming peer sessions
18
+ attr_accessor :session_listener
19
+ # Application logic which processes session data.
20
+ attr_accessor :session_handler
21
+
22
+ # Authoritative peer availability database.
23
+ attr_accessor :peer_registry
24
+
25
+ # Instance-specific configuration which may be overriden at initialization time by the application
26
+ attr_reader :configuration
27
+
28
+ # The universally-unique ID of this instance.
29
+ attr_reader :uuid
30
+
31
+ @@DEFAULT_SESSION_PORT = 31338
32
+ @@DEFAULT_EVENT_PORT = 31337
33
+ @@DEFAULT_SESSION_HANDLER = DefaultSessionHandler.new
34
+
35
+ # Addresses 224.0.0.0 through 239.255.255.255 are reserved for multicast messages.
36
+ @@DEFAULT_EVENT_NETWORK = '224.220.221.222'
37
+ @@DEFAULT_EVENT_PERIOD = 4
38
+
39
+
40
+ def initialize(configuration ={})
41
+ putsd "CON: #{configuration}"
42
+ @configuration = Hash.new
43
+
44
+ # A supposedly universally unique id for this instance. not technically gauranteed but close enough for now.
45
+ # TODO make guaranteed to be unique.
46
+ @configuration[:uuid] = configuration[:uuid] || rand(2 ** 31)
47
+ # the tcp port to use for direct peer connections
48
+ @configuration[:session_port] = configuration[:session_port] || @@DEFAULT_SESSION_PORT
49
+
50
+ @session_handler = configuration[:session_handler] || @@DEFAULT_SESSION_HANDLER
51
+
52
+ # The UDP port for event broadcast messages.
53
+ @configuration[:event_port] = configuration[:event_port] || @@DEFAULT_EVENT_PORT
54
+
55
+ # The UDP network address used for broadcast messages.
56
+ @configuration[:event_address] = configuration[:event_address] || @@DEFAULT_EVENT_NETWORK
57
+
58
+ # The delay, in seconds, between presence notification broadcasts.
59
+ @configuration[:event_period] = configuration[:event_period] || @@DEFAULT_EVENT_PERIOD
60
+
61
+ # Initialize sub-components.
62
+ @session_listener = Journeta::SessionListener.new self
63
+ @event_listener = EventListener.new self
64
+ @event_broadcaster = EventBroadcaster.new self
65
+ @peer_registry = PeerRegistry.new self
66
+ end
67
+
68
+ def start
69
+ # start a session listener first so we don't risk missing a connection attempt
70
+ putsd "Starting #{@session_listener.class.to_s}"
71
+ @session_listener.start
72
+
73
+ # start listening for peer events
74
+ putsd "Starting #{@event_listener.class.to_s}"
75
+ @event_listener.start
76
+
77
+ # start sending our own events
78
+ putsd "Starting #{@event_broadcaster.class.to_s}"
79
+ @event_broadcaster.start
80
+ end
81
+
82
+ def stop
83
+ # stop broadcasting events
84
+ @event_broadcaster.stop
85
+ # stop listener for events
86
+ @event_listener.stop
87
+ # stop listening for incoming peer sessions
88
+ @session_listener.stop
89
+ end
90
+
91
+ def send_to_known_peers(payload)
92
+ # Delegate directly.
93
+ peer_registry.send_to_known_peers(payload)
94
+ end
95
+
96
+ def send_to_peer(peer_uuid, payload)
97
+ # Delegate directly.
98
+ peer_registry.send_to_peer(peer_uuid, payload)
99
+ end
100
+
101
+ # Returns metadata on all known peers in a hash, keyed by the uuid of each.
102
+ # A record corresponding to this peer is not included.
103
+ def known_peers()
104
+ peer_registry.peers # FIXME Returns objects outside of synchronized context.
105
+ end
106
+
107
+ # Adds (or updates) the given +PeerConnection+.
108
+ def register_peer(peer)
109
+ peer_registry.add(peer)
110
+ end
111
+
112
+ end
113
+
114
+ end