legs 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,62 @@
1
+ = About Legs
2
+
3
+ This is Legs, the little networking doodad somewhat inspired by Camping but not all that much really. It uses a lot of ruby trickery to give you one class, Legs, which can act as a server, a client, or a peer depending on how you use it. Oh, and the protocol it uses is {JSON-RPC 1.0}[http://json-rpc.org/wiki/specification], so quite platform and language agnostic. :)
4
+
5
+ If you give Legs some methods, then anyone you connect to will be able to make calls back to those methods to notify you of things. Otherwise, your instance of legs will only be able to connect out to a legs server and call methods in that server, but not the other way around. If that's all you need, learning Camping[http://github.com/why/camping/tree/master] and using http might be a better way to go, as http is a more popular and more standard protocol. :)
6
+
7
+ == To connect to a Legs server running on your own computer
8
+
9
+ server = Legs.new
10
+ - OR -
11
+ server = Legs.new('localhost')
12
+
13
+ == To connect to a remote server
14
+
15
+ server = Legs.new('web.address.to.server')
16
+
17
+ == To connect to a server on port 1234
18
+
19
+ server = Legs.new('web.address', 1234)
20
+
21
+ == To create a legs server
22
+
23
+ Legs.start do
24
+ def callable_method(a, b, c = 'default')
25
+ return "You called with params #{a}, #{b}, and #{c}"
26
+ end
27
+ end
28
+
29
+ sleep # Stops ruby from closing as soon as Legs starts.
30
+ # don't put sleep if you're embedding the server in a shoes UI or something.
31
+
32
+ == To add methods to legs, without creating a server
33
+
34
+ Legs.start(false) do
35
+ def callable_method(a, b, c = 'default')
36
+ return "You called with params #{a}, #{b}, and #{c}"
37
+ end
38
+ end
39
+
40
+ You should do this before creating instances of Legs with Legs.new. Adding methods like this is useful, because the "TCP" network connections Legs makes work in both directions, so if you have some methods, server's you connect to will be able to call your methods to notify you of events and stuff. If you don't give it +false+ on that first line, it will start up a server as well, so other instances of Legs can connect in to you, deliciously peery. It's important to know, though, that the <tt>Legs.users</tt> only includes connections who connected to you, not outgoing connections you make with Legs.new syntax. You could make your own array of outgoing connections to keep track ot them if you want. :)
41
+
42
+ Inside of your Legs.start block, there are two magic methods you can use in your code, these are +server+ and +caller+. +server+ references the Legs server object, which has methods like +users+ and some other stuff you can use to your advantage. The +caller+ method gives you the Legs instance representing that network connection (doesn't matter if they connected to you or vice versa).
43
+
44
+ Legs instances, like the one you get from +caller+, and the array of them you get from <tt>server.users</tt> have a useful method called +meta+ which gives you a hash you can store whatever you like inside of. Things you might put in there include the user's name, or if they have any special powers, so you can control which methods they can use and what they can do. Here's an example: :)
45
+
46
+ Legs.start do
47
+ def count
48
+ caller.meta[:counter] ||= 0
49
+ caller.meta[:counter] += 1
50
+ end
51
+ end
52
+
53
+ sleep
54
+
55
+ When you connect to this server, you can call the +count+ method. Each time you call it, it will give you a number, starting with 1 and growing higher each time. Because it stores the 'counter' in the caller's meta hash, if another person connects, they will start out with the number 1 as well. You could connect to this with :)
56
+
57
+ server = Legs.new('localhost')
58
+ puts "#{server.count}, #{server.count}, #{server.count}"
59
+
60
+ And if you run that script, you should see in your terminal: "1, 2, 3". That's pretty much how Legs works. An instance of legs has a few of it's own special methods: <tt>close!</tt>, <tt>notify!</tt>, <tt>send!</tt>, <tt>send_async!</tt>, <tt>connected?</tt>, +socket+, +parent+, +meta+, and <tt>send_data!</tt>. If you need to actually run a method on your server with one of these names, do: <tt>server.send!(:connected?, a_param, another_param)</tt> or whatever, and it'll run that method on the server for you. If you want to let the server know something, but don't care about the method's response, or any errors, you can do the same thing with <tt>legs.notify!</tt>, which will make your program run faster, especially if it's running through the web. :)
61
+
62
+ Finally, if you're making a program for running over the internet, and want to make your app more responsive, you can call methods asyncronously. What this means is that the method won't return immidiately, but instead will send the request out to your network, and your program will continue to run, and then when the server responds, a block you provide will be run. The block is passed an object, call that object's +result+ or +value+ method to get the server response, or it will raise an error if the server had an error, so you can use it as though it were a syncronous response. Error is only raised the first time you call it. To do this, just give the thing a block like this: <tt>server.some_method { |result| ... do stuff with result.value ... }</tt>.
@@ -0,0 +1,111 @@
1
+ require '../lib/legs'
2
+
3
+ # this is a work in progress, api's will change and break, one day there will be a functional matching
4
+ # client in shoes or something
5
+
6
+ Legs.start do
7
+ def initialize
8
+ @rooms = Hash.new { {'users' => [], 'messages' => [], 'topic' => 'No topic set'} }
9
+ @rooms['Lobby'] = {'topic' => 'General Chit Chat', 'messages' => [], 'users' => []}
10
+ end
11
+
12
+ # returns a list of available rooms
13
+ def rooms
14
+ room_list = Hash.new
15
+ @rooms.keys.each { |rn| room_list[rn] = room_object rn, :remote, :topic, :users, :messages }
16
+ room_list
17
+ end
18
+
19
+ # joins/creates a room
20
+ def join(room_name)
21
+ unless @rooms.keys.include?(room_name)
22
+ @rooms[room_name.to_s] = @rooms[room_name]
23
+ broadcast :room_created, room_name
24
+ end
25
+
26
+ # room = room_object(room_name)
27
+ #
28
+ # unless room['users'].include?(caller)
29
+ # broadcast room['users'], 'user_joined', room_name, user_object(caller)
30
+ # room['users'].push(caller)
31
+ # end
32
+
33
+ room_object room_name, :remote
34
+ end
35
+
36
+ # leaves a room
37
+ def leave(room_name)
38
+ room = @rooms[room_name.to_s]
39
+ room['users'].delete(caller)
40
+ broadcast room['users'], 'user_left', room_name, user_object(caller)
41
+ true
42
+ end
43
+
44
+ # sets the room topic message
45
+ def topic=(room, message)
46
+ room = @rooms[room.to_s]
47
+ room['topic'] = message.to_s
48
+ broadcast room['users'], 'room_changed', room_object(room, :remote, :name, :topic)
49
+ end
50
+
51
+ # sets the user's name
52
+ def name=(name)
53
+ caller.meta[:name] = name.to_s
54
+ user_rooms(caller).each do |room_name|
55
+ broadcast @rooms[room_name]['users'], 'user_changed', user_object(caller)
56
+ end
57
+ true
58
+ end
59
+
60
+ # returns information about ones self, clients thusly can find out their user 'id' number
61
+ def user(object_id = nil)
62
+ user = user_object( object_id.nil? ? caller : find_user_by_object_id(object_id) }.first )
63
+ user['rooms'] = user_rooms(user)
64
+ return user
65
+ end
66
+
67
+ # posts a message to a room
68
+ def post_message(room_name, message)
69
+ room = room_object(room_name)
70
+ room['messages'].push(msg = {'user' => user_object(caller), 'time' => Time.now.to_i, 'message' => message.to_s} )
71
+ trim_messages room
72
+ broadcast room['users'], 'message', room_name.to_s, msg
73
+ return msg
74
+ end
75
+
76
+ private
77
+
78
+ # trims the message backlog
79
+ def trim_messages room
80
+ room = room_object(room) if room.is_a?(String)
81
+ while room['messages'].length > 250
82
+ room['messages'].shift
83
+ end
84
+ end
85
+
86
+ # makes a user object suitable for sending back with meta info and stuff
87
+ def user_object user
88
+ object = {'id' => user.object_id}
89
+ user.meta.each_pair do |key, value|
90
+ object[key.to_s] = value
91
+ end
92
+ return object
93
+ end
94
+
95
+ def room_object room_name, target = :local, *only
96
+ object = @rooms[room_name.to_s].dup
97
+ object['users'].delete_if {|user| user.connected? == false }
98
+ object['users'] = object['users'].map { |user| user_object(user) } if target == :remote
99
+ object['topic'] = 'No topic set' if object['topic'].nil? or object['topic'].empty?
100
+ object['name'] = room_name.to_s if target == :remote
101
+ object.delete_if { |key, value| only.include?(key.to_sym) == false } unless only.empty?
102
+ object
103
+ end
104
+
105
+ # returns all the room names the user is in.
106
+ def user_rooms user
107
+ @rooms.values.select { |room| room['users'].include?(user) }.map { |room| @rooms.index(room) }
108
+ end
109
+ end
110
+
111
+ sleep
@@ -0,0 +1,24 @@
1
+ require '../lib/legs'
2
+
3
+ # This is how simple a Legs server can look.
4
+
5
+ Legs.start do
6
+ def echo(text)
7
+ return text
8
+ end
9
+ end
10
+
11
+ sleep
12
+
13
+ # To test this server, first, run it in ruby, and then open another terminal, telnet localhost 30274
14
+ # Then enter:
15
+ # {"method":"echo","params":["Hello World"],"id":1}
16
+ # result should be:
17
+ # {"result":"Hello World","id":1}
18
+ # however the properties may appear in a different order, this makes no difference.
19
+
20
+ # Test to ensure there are no security flaws...
21
+ # Try {"method":"object_id","params":[],"id":1}
22
+ # Should return: {"error":"Cannot run 'object_id' because it is not defined in this server","id":1}
23
+ # And try: {"method":"caller","params":[],"id":1}
24
+ # Should recieve a similar error response. :)
@@ -0,0 +1,159 @@
1
+ # Shoes Chat Client is a generic irc-like chat system, made in Legs and Shoes, as a way to find
2
+ # any troublesome parts in Legs and learn stuff about Shoes. :)
3
+
4
+ #HOSTNAME = 'localhost'
5
+ HOSTNAME = 'bluebie.creativepony.com'
6
+
7
+
8
+ Shoes.setup do
9
+ gem 'json_pure >= 1.1.1'
10
+ end
11
+
12
+ require 'json/pure.rb'
13
+ require '../lib/legs'
14
+
15
+ # This add's some methods the server can call on to notify us of events like a new message
16
+ Legs.start false do
17
+ attr_accessor :app, :rooms
18
+
19
+ def user_left room_name, user
20
+ @rooms[room_name]['users'].delete_if { |u| u['id'] == user['id'] }
21
+ end
22
+
23
+ def user_joined room_name, user
24
+ @rooms[room_name]['users'].push user unless @rooms[room_name]['users'].include?(user)
25
+ end
26
+
27
+ def room_created room
28
+ @app.add_room room
29
+ end
30
+
31
+ def room_changed room
32
+ @app.update_room room.delete('name'), room
33
+ end
34
+
35
+ def user_changed user
36
+ @rooms.each do |room|
37
+ room['users'].map! do |room_user|
38
+ if user['id'] == room_user['id']
39
+ user
40
+ else
41
+ room_user
42
+ end
43
+ end
44
+ end
45
+ #@app.update_user_list
46
+ end
47
+
48
+ def room_message room, message
49
+ @app.add_message message
50
+ end
51
+ end
52
+
53
+
54
+ class LegsChat < Shoes
55
+ url '/', :index
56
+ url '/room/(\w+)', :room
57
+
58
+ Chat = Legs.new(HOSTNAME)
59
+
60
+ def index
61
+ #Legs.server_object.app = self
62
+ #name = ask("What's your name?")
63
+ #Chat.notify! :set_name, name
64
+
65
+ @@room_data = Chat.rooms
66
+ @@joined_rooms = []
67
+ @@available_rooms = @@room_data.keys
68
+ visit('/room/Lobby')
69
+ end
70
+
71
+ def room name
72
+ unless @@joined_rooms.include? name
73
+ @@room_data[name] = Chat.join(name)
74
+ @@joined_rooms.push name
75
+ end
76
+
77
+ @room = name
78
+ timer(0.5) { layout } # don't know why I need this, but layout goes all weird without delay
79
+ end
80
+
81
+ def layout
82
+ clear do
83
+ background white
84
+
85
+ @rooms_list = stack :width => 180, :height => 1.0
86
+ stack do
87
+ @log = stack :width => -360, :height => -30, :scroll => true, :margin_right => gutter
88
+
89
+ flow :margin_right => gutter, :height => 30 do
90
+ @msg_input = edit_line(:width => -100)
91
+
92
+ submit = Proc.new do
93
+ add_message @room, Chat.post_message(@room, @msg_input.text)
94
+ @msg_input.text = ''
95
+ end
96
+
97
+ button("Send", :width => 100, &submit)
98
+ end
99
+
100
+ end
101
+ @users_list = stack :width => 180, :height => 1.0
102
+ end
103
+
104
+ @@available_rooms.each { |r| add_room r }
105
+ @@room_data[@room]['messages'].each { |m| add_message @room, m }
106
+ end
107
+
108
+
109
+ # adds a message to the display
110
+ def add_message room_name, message
111
+ unless @@room_data[room_name]['messages'].include? message
112
+ messages = @@room_data[room_name]['messages']
113
+ messages.push message
114
+ @@room_data[room_name]['messages'] = messages[-500,500] if messages.length > 500
115
+ end
116
+
117
+ return if room_name != @room
118
+ scroll_down = @log.scroll_top >= @log.scroll_max - 10
119
+ @log.append do
120
+ flow do
121
+ para strong(message['user']['name'] || message ['user']['id']), ': ', message['message']
122
+ end
123
+ end
124
+
125
+ while @log.contents.length > 500
126
+ @log.contents.first.remove
127
+ end
128
+
129
+ @log.scroll_top = @log.scroll_max
130
+ end
131
+
132
+
133
+ # adds a room to the sidebar
134
+ def add_room room_name
135
+ @@available_rooms.push room_name unless @@available_rooms.include? room_name
136
+ @rooms_list.append do
137
+ flow :margin => 5 do
138
+ # _why assures me the :checked style will work in the next release
139
+ joined = check :checked => @@joined_rooms.include?(room_name) do |chk|
140
+ @@joined_rooms.push(room_name) and @@room_data[room_name].merge! Chat.join(room_name) if chk.checked?
141
+ Chat.leave(room_name) and @@joined_rooms.delete(room_name) unless chk.checked?
142
+ end
143
+
144
+ para room_name, :underline => (@room == room_name ? :one : false)
145
+
146
+ click do
147
+ visit("/room/#{room_name}")
148
+ end unless @room == room_name
149
+ end
150
+ end
151
+ end
152
+
153
+ def update_room room_name, data
154
+ @@room_data[room_name].merge! data
155
+ end
156
+ end
157
+
158
+ Shoes.app(:title => "Legs Chat", :width => 700, :height => 350)
159
+
@@ -0,0 +1,13 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "legs"
3
+ s.version = "0.6.2"
4
+ s.date = "2008-07-17"
5
+ s.summary = "Simple fun open networking for newbies and quick hacks"
6
+ s.email = "a@creativepony.com"
7
+ s.homepage = "http://github.com/Bluebie/legs"
8
+ s.description = "Legs is a really simple fun networking library that uses 'json-rpc' formated messages over a tcp connection to really easily built peery or server-clienty sorts of apps, for ruby newbies and hackers to build fun little things."
9
+ s.has_rdoc = false
10
+ s.authors = ["Jenna Fox"]
11
+ s.files = ["README.rdoc", "legs.gemspec", "lib/legs.rb", "examples/echo-server.rb", "examples/chat-server.rb", "examples/shoes-chat-client.rb", "test/test_legs.rb"]
12
+ s.add_dependency("json_pure", ["> 1.1.0"])
13
+ end
@@ -0,0 +1,404 @@
1
+ # Legs take you places, a networking companion
2
+ ['rubygems', 'socket', 'thread'].each { |i| require i }
3
+ require 'json' unless self.class.const_defined? 'JSON'
4
+
5
+ class Legs
6
+ # general getters
7
+ attr_reader :socket, :parent, :meta
8
+ def inspect; "<Legs:#{object_id} Meta: #{@meta.inspect}>"; end
9
+
10
+ # Legs.new for a client, subclass to make a server, .new then makes server and client!
11
+ def initialize(host = 'localhost', port = 30274)
12
+ self.class.start(port) if self.class != Legs && !self.class.started?
13
+ ObjectSpace.define_finalizer(self) { self.close! }
14
+ @parent = false; @responses = Hash.new; @meta = {}; @disconnected = false
15
+ @responses_mutex = Mutex.new; @socket_mutex = Mutex.new
16
+
17
+ if host.instance_of?(TCPSocket)
18
+ @socket = host
19
+ @parent = port unless port.instance_of?(Numeric)
20
+ elsif host.instance_of?(String)
21
+ @socket = TCPSocket.new(host, port)
22
+ self.class.outgoing_mutex.synchronize { self.class.outgoing.push self }
23
+ else
24
+ raise "First argument needs to be a hostname, ip, or socket"
25
+ end
26
+
27
+
28
+ @handle_data = Proc.new do |data|
29
+ data = json_restore(JSON.parse(data))
30
+
31
+ if data['method']
32
+ (@parent || self.class).__data!(data, self)
33
+ elsif data['error'] and data['id'].nil?
34
+ raise data['error']
35
+ else
36
+ @responses_mutex.synchronize { @responses[data['id']] = data }
37
+ end
38
+ end
39
+
40
+ @thread = Thread.new do
41
+ until @socket.closed?
42
+ begin
43
+ close! if @socket.eof?
44
+ data = nil
45
+ @socket_mutex.synchronize { data = @socket.gets(self.class.terminator) rescue nil }
46
+ if data.nil?
47
+ close!
48
+ else
49
+ @handle_data[data]
50
+ end
51
+ rescue JSON::ParserError => e
52
+ send_data!({"error" => "JSON provided is invalid. See http://json.org/ to see how to format correctly."})
53
+ rescue IOError => e
54
+ close!
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ # I think you can guess this one
61
+ def connected?; self.class.connections.include?(self); end
62
+
63
+ # closes the connection and the threads and stuff for this user
64
+ def close!
65
+ return if @disconnected == true
66
+
67
+ @disconnected = true
68
+ puts "User #{inspect} disconnecting" if self.class.log?
69
+
70
+ # notify the remote side
71
+ notify!('**remote__disconnecting**') rescue nil
72
+
73
+ if @parent
74
+ @parent.event(:disconnect, self)
75
+ @parent.incoming_mutex.synchronize { @parent.incoming.delete(self) }
76
+ else
77
+ self.class.outgoing_mutex.synchronize { self.class.outgoing.delete(self) }
78
+ end
79
+
80
+ Thread.new { sleep(1); @socket.close rescue nil }
81
+ end
82
+
83
+ # send a notification to this user
84
+ def notify!(method, *args, &blk)
85
+ puts "Notify #{inspect}: #{method}(#{args.map(&:inspect).join(', ')})" if self.class.log?
86
+ send_data!({'method' => method.to_s, 'params' => args, 'id' => nil})
87
+ end
88
+
89
+ # sends a normal RPC request that has a response
90
+ def send!(method, *args, &blk)
91
+ puts "Call #{inspect}: #{method}(#{args.map(&:inspect).join(', ')})" if self.class.log?
92
+ id = get_unique_number
93
+ send_data! 'method' => method.to_s, 'params' => args, 'id' => id
94
+
95
+ worker = Proc.new do
96
+ sleep 0.1 until @responses_mutex.synchronize { @responses.keys.include?(id) }
97
+
98
+ result = Legs::Result.new(@responses_mutex.synchronize { @responses.delete(id) })
99
+ puts ">> #{method} #=> #{result.data['result'].inspect}" if self.class.log?
100
+ result
101
+ end
102
+
103
+ if blk.respond_to?(:call); Thread.new { blk[worker.call] }
104
+ else; worker.call.value; end
105
+ end
106
+
107
+ # catch all the rogue calls and make them work niftily
108
+ alias_method :method_missing, :send!
109
+
110
+ # sends raw object over the socket
111
+ def send_data!(data)
112
+ raise "Lost remote connection" unless connected?
113
+ raw = JSON.generate(json_marshal(data)) + self.class.terminator
114
+ @socket_mutex.synchronize { @socket.write(raw) }
115
+ end
116
+
117
+
118
+ private
119
+
120
+ # takes a ruby object, and converts it if needed in to marshalled hashes
121
+ def json_marshal(object)
122
+ case object
123
+ when Bignum, Fixnum, Integer, Float, TrueClass, FalseClass, String, NilClass
124
+ return object
125
+ when Hash
126
+ out = Hash.new
127
+ object.each_pair { |k,v| out[k.to_s] = json_marshal(v) }
128
+ return out
129
+ when Array
130
+ return object.map { |v| json_marshal(v) }
131
+ when Symbol
132
+ return {'__jsonclass__' => ['Legs', '__make_symbol', object.to_s]}
133
+ when Exception
134
+ return {'__jsonclass__' => ['Legs::RemoteError', 'new', "<#{object.class.name}> #{object.message}", object.backtrace]}
135
+ else
136
+ return {'__jsonclass__' => [object.class.name, '_load', object._dump]} if object.respond_to?(:_dump)
137
+
138
+ # the default marshalling behaviour
139
+ instance_vars = {}
140
+ object.instance_variables.each do |var_name|
141
+ instance_vars[var_name.to_s.sub(/@/, '')] = json_marshal(object.instance_variable_get(var_name))
142
+ end
143
+
144
+ return {'__jsonclass__' => [object.class.name, 'new']}.merge(instance_vars)
145
+ end
146
+ end
147
+
148
+ SAFE_CONSTRUCTORS = ['new', 'allocate', '_load']
149
+
150
+ # takes an object from the network, and decodes any marshalled hashes back in to ruby objects
151
+ def json_restore(object)
152
+ case object
153
+ when Hash
154
+ if object.keys.include? '__jsonclass__'
155
+ constructor = object.delete('__jsonclass__')
156
+ class_name = constructor.shift.to_s
157
+
158
+ # find the constant through the heirachy
159
+ object_class = Module
160
+ class_name.split(/::/).each { |piece_of_const| object_class = object_class.const_get(piece_of_const) } rescue false
161
+
162
+ if object_class
163
+ unless constructor.empty?
164
+ raise "Unsafe marshaling constructor method: #{constructor.first}" unless (object_class == Legs and constructor.first =~ /^__make_/) or SAFE_CONSTRUCTORS.include?(constructor.first)
165
+ raise "#{class_name} doesn't support the #{constructor.first} constructor" unless object_class.respond_to?(constructor.first)
166
+ instance = object_class.__send__(*constructor)
167
+ else
168
+ instance = object_class.allocate
169
+ end
170
+
171
+ object.each_pair do |key, value|
172
+ instance.instance_variable_set("@#{key}", json_restore(value))
173
+ end
174
+ return instance
175
+ else
176
+ raise "Response contains a #{class_name} but that class is not loaded locally, it needs to be."
177
+ end
178
+ else
179
+ hash = Hash.new
180
+ object.each_pair { |k,v| hash[k] = json_restore(v) }
181
+ return hash
182
+ end
183
+ when Array
184
+ return object.map { |i| json_restore(i) }
185
+ else
186
+ return object
187
+ end
188
+ end
189
+
190
+ # gets a unique number that we can use to match requests to responses
191
+ def get_unique_number; @unique_id ||= 0; @unique_id += 1; end
192
+ end
193
+
194
+ # undef's the superclass's methods so they won't get in the way
195
+ removal_list = Legs.instance_methods(true)
196
+ removal_list -= %w{JSON new class object_id send __send__ __id__ < <= <=> => > == === yield raise}
197
+ removal_list -= Legs.instance_methods(false)
198
+ Legs.class_eval { removal_list.each { |m| undef_method m } }
199
+
200
+
201
+ # the server is started by subclassing Legs, then SubclassName.start
202
+ class << Legs
203
+ attr_accessor :terminator, :log
204
+ attr_reader :incoming, :outgoing, :server_object, :incoming_mutex, :outgoing_mutex, :messages_mutex
205
+ alias_method :log?, :log
206
+ alias_method :users, :incoming
207
+ def started?; @started; end
208
+
209
+ def initializer
210
+ ObjectSpace.define_finalizer(self) { self.stop! }
211
+ @incoming = []; @outgoing = []; @messages = Queue.new; @terminator = "\n"; @log = false
212
+ @incoming_mutex = Mutex.new; @outgoing_mutex = Mutex.new; @started = false
213
+ end
214
+
215
+
216
+ # starts the server, pass nil for port to make a 'server' that doesn't actually accept connections
217
+ # This is useful for adding methods to Legs so that systems you connect to can call methods back on you
218
+ def start(port=30274, &blk)
219
+ return @server_class.module_eval(&blk) if started? and blk.respond_to? :call
220
+ @started = true
221
+
222
+ # makes a nice clean class to hold all the server methods.
223
+ if @server_class.nil?
224
+ @server_class = Class.new
225
+ @server_class.module_eval do
226
+ private
227
+ attr_reader :server, :caller
228
+
229
+ # sends a notification message to all connected clients
230
+ def broadcast(*args)
231
+ if args.first.is_a?(Array)
232
+ list = args.shift
233
+ method = args.shift
234
+ elsif args.first.is_a?(String) or args.first.is_a?(Symbol)
235
+ list = server.incoming
236
+ method = args.shift
237
+ else
238
+ raise "You need to specify a 'method' to broadcast out to"
239
+ end
240
+
241
+ list.each { |user| user.notify!(method, *args) }
242
+ end
243
+
244
+ # Finds a user by the value of a certain property... like find_user_by :object_id, 12345
245
+ def find_user_by_object_id value
246
+ server.incoming.find { |user| user.object_id == value }
247
+ end
248
+
249
+ # finds user's with the specified meta keys matching the specified values, can use regexps and stuff, like a case block
250
+ def find_users_by_meta hash = nil
251
+ raise "You need to give find_users_by_meta a hash to check the user's meta hash against" if hash.nil?
252
+ server.incoming.select do |user|
253
+ hash.all? { |key, value| value === user.meta[key] }
254
+ end
255
+ end
256
+
257
+ public # makes it public again for the user code
258
+ end
259
+ end
260
+
261
+ @server_class.module_eval(&blk) if blk.respond_to?(:call)
262
+
263
+ if @server_object.nil?
264
+ @server_object = @server_class.allocate
265
+ @server_object.instance_variable_set(:@server, self)
266
+ @server_object.instance_eval { initialize }
267
+ end
268
+
269
+ @message_processor = Thread.new do
270
+ while started?
271
+ sleep 0.01 while @messages.empty?
272
+ data, from = @messages.deq
273
+ method = data['method']; params = data['params']
274
+ methods = @server_object.public_methods(false)
275
+
276
+ # close dead connections
277
+ if data['method'] == '**remote__disconnecting**'
278
+ from.close!
279
+ next
280
+ else
281
+ begin
282
+ raise "Supplied method is not a String" unless method.is_a?(String)
283
+ raise "Supplied params object is not an Array" unless params.is_a?(Array)
284
+ raise "Cannot run '#{method}' because it is not defined in this server" unless methods.include?(method.to_s) or methods.include? :method_missing
285
+
286
+ puts "Call #{method}(#{params.map(&:inspect).join(', ')})" if log?
287
+
288
+ @server_object.instance_variable_set(:@caller, from)
289
+
290
+ result = nil
291
+
292
+ @incoming_mutex.synchronize do
293
+ if methods.include?(method.to_s)
294
+ result = @server_object.__send__(method.to_s, *params)
295
+ else
296
+ result = @server_object.method_missing(method.to_s, *params)
297
+ end
298
+ end
299
+
300
+ puts ">> #{method} #=> #{result.inspect}" if log?
301
+
302
+ from.send_data!({'id' => data['id'], 'result' => result}) unless data['id'].nil?
303
+
304
+ rescue Exception => e
305
+ from.send_data!({'error' => e, 'id' => data['id']}) unless data['id'].nil?
306
+ puts "Error: #{e}\nBacktrace: " + e.backtrace.join("\n ") if log?
307
+ end
308
+ end
309
+ end
310
+ end unless @message_processor and @message_processor.alive?
311
+
312
+ if ( port.nil? or port == false ) == false and @listener.nil?
313
+ @listener = TCPServer.new(port)
314
+
315
+ @acceptor_thread = Thread.new do
316
+ while started?
317
+ user = Legs.new(@listener.accept, self)
318
+ @incoming_mutex.synchronize { @incoming.push user }
319
+ puts "User #{user.object_id} connected, number of users: #{@incoming.length}" if log?
320
+ self.event :connect, user
321
+ end
322
+ end
323
+ end
324
+ end
325
+
326
+ # stops the server, disconnects the clients
327
+ def stop
328
+ @started = false
329
+ @incoming.each { |user| user.close! }
330
+ end
331
+
332
+ # returns an array of all connections
333
+ def connections
334
+ @incoming + @outgoing
335
+ end
336
+
337
+ # add an event call to the server's message queue
338
+ def event(name, sender, *extras)
339
+ return unless @server_object.respond_to?("on_#{name}")
340
+ __data!({'method' => "on_#{name}", 'params' => extras.to_a, 'id' => nil}, sender)
341
+ end
342
+
343
+ # gets called to handle all incoming messages (RPC requests)
344
+ def __data!(data, from)
345
+ @messages.enq [data, from]
346
+ end
347
+
348
+ # People say this syntax is too funny not to have... whatever. Works like IO and File and what have you
349
+ def open(*args)
350
+ client = Legs.new(*args)
351
+ yield(client)
352
+ client.close!
353
+ end
354
+
355
+ # add's a method to the 'server' class, bound in to that class
356
+ def define_method(name, &blk); @server_class.class_eval { define_method(name, &blk) }; end
357
+
358
+ # add's a block to the 'server' class in a way that retains it's old bindings.
359
+ # the block will be passed the caller object, followed by the args.
360
+ def add_block(name, &block)
361
+ @server_class.class_eval do
362
+ define_method(name) do |*args|
363
+ block.call caller, *args
364
+ end
365
+ end
366
+ end
367
+
368
+ # lets the marshaler transport symbols
369
+ def __make_symbol(name); name.to_sym; end
370
+
371
+ # hooks up these methods so you can use them off the main object too!
372
+ [:broadcast, :find_user_by_object_id, :find_users_by_meta].each do |name|
373
+ define_method name do |*args|
374
+ @incoming_mutex.synchronize do; @outgoing_mutex.synchronize do
375
+ @server_object.__send__(name, *args)
376
+ end; end
377
+ end
378
+ end
379
+ end
380
+
381
+ Legs.initializer
382
+
383
+ # represents the data response, handles throwing of errors and stuff
384
+ class Legs::Result
385
+ attr_reader :data
386
+ def initialize(data); @data = data; end
387
+ def result
388
+ unless @data['error'].nil? or @errored
389
+ @errored = true
390
+ raise @data['error']
391
+ end
392
+ @data['result']
393
+ end
394
+ alias_method :value, :result
395
+ end
396
+
397
+ class Legs::StartBlockError < StandardError; end
398
+ class Legs::RequestError < StandardError; end
399
+ class Legs::RemoteError < StandardError
400
+ def initialize(msg, backtrace)
401
+ super(msg)
402
+ set_backtrace(backtrace)
403
+ end
404
+ end
@@ -0,0 +1,206 @@
1
+ # Makes use of ZenTest. Install the 'ZenTest' gem, then run this ruby script in a terminal to see the results!
2
+ require 'test/unit' unless defined? $ZENTEST and $ZENTEST
3
+ require '../lib/legs'
4
+
5
+ # want to see errors, don't want to see excessively verbose logging normally
6
+ Thread.abort_on_exception = true
7
+ #Legs.log = true
8
+
9
+ # class to test the marshaling
10
+ class Marshal::TesterClass; attr_accessor :a, :b, :c; end
11
+
12
+ # a simple server to test with
13
+ Legs.start(6425) do
14
+ def echo(text)
15
+ return text
16
+ end
17
+
18
+ def count
19
+ caller.meta[:counter] ||= 0
20
+ caller.meta[:counter] += 1
21
+ end
22
+
23
+ def methods
24
+ 'overridden'
25
+ end
26
+
27
+ def error
28
+ raise "This is a fake error"
29
+ end
30
+
31
+ def notified
32
+ $notified = true
33
+ end
34
+
35
+ def marshal
36
+ obj = Marshal::TesterClass.new
37
+ obj.a = 1; obj.b = 2; obj.c = 3
38
+ return obj
39
+ end
40
+
41
+ def on_connect
42
+ $server_instance = caller
43
+ end
44
+
45
+ def on_some_event
46
+ $some_event_ran = true
47
+ end
48
+
49
+ # tests that we can call stuff back over the caller's socket
50
+ def bidirectional; caller.notify!(:bidirectional_test_reciever); end
51
+ def bidirectional_test_reciever; $bidirectional_worked = true; end
52
+ end
53
+
54
+ Remote = Legs.new('localhost', 6425)
55
+
56
+
57
+ class TestLegsObject < Test::Unit::TestCase
58
+ def test_class_broadcast
59
+ $notified = false
60
+ Legs.broadcast(:notified)
61
+ sleep 0.5
62
+ assert_equal(true, $notified)
63
+ end
64
+
65
+ def test_class_connections
66
+ assert_equal(2, Legs.connections.length)
67
+ end
68
+
69
+ def test_class_event
70
+ Legs.event :some_event, nil
71
+ sleep 0.1
72
+ assert_equal(true, $some_event_ran)
73
+ end
74
+
75
+ def test_class_find_user_by_object_id
76
+ assert_equal(Legs.incoming.first, Legs.find_user_by_object_id(Legs.incoming.first.object_id))
77
+ end
78
+
79
+ def test_class_find_users_by_meta
80
+ Legs.incoming.first.meta[:id] = 'This is the incoming legs instance'
81
+ assert_equal(true, Legs.find_users_by_meta(:id => /incoming legs/).include?(Legs.incoming.first))
82
+ end
83
+
84
+ def test_class_incoming
85
+ assert_equal(true, Legs.incoming.length > 0)
86
+ end
87
+
88
+ def test_class_outgoing
89
+ assert_equal(true, Legs.outgoing.is_a?(Array))
90
+ assert_equal(1, Legs.outgoing.length)
91
+ end
92
+
93
+ def test_class_open
94
+ instance = nil
95
+ Legs.open('localhost', 6425) do |i|
96
+ instance = i
97
+ end
98
+ assert_equal(Legs, instance.class)
99
+ end
100
+
101
+ def test_class_started_eh
102
+ assert_equal(true, Legs.started?)
103
+ end
104
+
105
+ def test_connected_eh
106
+ assert_equal(true, Remote.connected?)
107
+ end
108
+
109
+ def test_close_bang
110
+ ii = Legs.new('localhost', 6425)
111
+ assert_equal(2, Legs.outgoing.length)
112
+ ii.close!
113
+ assert_equal(1, Legs.outgoing.length)
114
+ end
115
+
116
+ def test_meta
117
+ assert_equal(true, Remote.meta.is_a?(Hash))
118
+ Remote.meta[:yada] = 'Yada'
119
+ assert_equal('Yada', Remote.meta[:yada])
120
+ end
121
+
122
+ def test_notify_bang
123
+ $notified = false
124
+ Remote.notify! :notified
125
+ sleep(0.2)
126
+ assert_equal(true, $notified)
127
+ end
128
+
129
+ def test_parent
130
+ assert_equal(false, Remote.parent)
131
+ assert_equal(Legs, Legs.incoming.first.parent)
132
+ end
133
+
134
+ def test_send_bang
135
+ assert_equal(123, Remote.echo(123))
136
+ assert_equal(123, Remote.send!(:echo, 123))
137
+
138
+ # check async
139
+ abc = 0
140
+ Remote.echo(123) { |r| abc = r.value }
141
+ sleep 0.2
142
+ assert_equal(123, abc)
143
+
144
+ # check it catches ancestor method calls
145
+ assert_equal('overridden', Remote.methods)
146
+ end
147
+
148
+ def test_symbol_marshaling
149
+ assert_equal(Symbol, Remote.echo(:test).class)
150
+ end
151
+
152
+ def test_socket
153
+ assert_equal(true, Remote.socket.is_a?(BasicSocket))
154
+ end
155
+
156
+ def test_marshaling
157
+ object = Remote.marshal
158
+ assert_equal(1, object.a)
159
+ assert_equal(2, object.b)
160
+ assert_equal(3, object.c)
161
+ end
162
+
163
+ def test_bidirectional
164
+ $bidirectional_worked = false; Remote.bidirectional; sleep 0.2
165
+ assert_equal(true, $bidirectional_worked)
166
+ end
167
+
168
+ # makes sure the block adding thingos work
169
+ def test_adding_block
170
+ @bound_var = bound_var = 'Ladedadedah'
171
+ Legs.define_method(:defined_meth) { bound_var }
172
+ assert_equal(bound_var, Remote.defined_meth)
173
+
174
+ Legs.add_block(:unbound_meth) { @bound_var }
175
+ assert_equal(bound_var, Remote.unbound_meth)
176
+ end
177
+
178
+ # this is to make sure we can run the start method a ton of times without bad side effects
179
+ def test_start_again_and_again
180
+ Legs.start
181
+ Legs.start { def adding_another_method; true; end }
182
+ assert_equal(true, Remote.adding_another_method)
183
+ end
184
+ end
185
+
186
+ module TestLegs
187
+ class TestResult < Test::Unit::TestCase
188
+ def test_data
189
+ result = nil
190
+ Remote.echo(123) { |r| result = r }
191
+ sleep(0.2)
192
+
193
+ assert_equal(123, result.data['result'])
194
+ end
195
+
196
+ def test_result
197
+ normal_result = Legs::Result.new({'id'=>1, 'result' => 'Hello World'})
198
+ error_result = Legs::Result.new({'id'=>2, 'error' => 'Uh oh Spagetti-o\'s'})
199
+
200
+ assert_equal(normal_result.value, 'Hello World')
201
+ assert_equal(normal_result.result, 'Hello World')
202
+ assert_equal((error_result.value rescue :good), :good)
203
+ end
204
+ end
205
+ end
206
+
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: legs
3
+ version: !ruby/object:Gem::Version
4
+ hash: 3
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 6
9
+ - 2
10
+ version: 0.6.2
11
+ platform: ruby
12
+ authors:
13
+ - Jenna Fox
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2008-07-17 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: json_pure
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">"
27
+ - !ruby/object:Gem::Version
28
+ hash: 19
29
+ segments:
30
+ - 1
31
+ - 1
32
+ - 0
33
+ version: 1.1.0
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ description: Legs is a really simple fun networking library that uses 'json-rpc' formated messages over a tcp connection to really easily built peery or server-clienty sorts of apps, for ruby newbies and hackers to build fun little things.
37
+ email: a@creativepony.com
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ extra_rdoc_files: []
43
+
44
+ files:
45
+ - README.rdoc
46
+ - legs.gemspec
47
+ - lib/legs.rb
48
+ - examples/echo-server.rb
49
+ - examples/chat-server.rb
50
+ - examples/shoes-chat-client.rb
51
+ - test/test_legs.rb
52
+ homepage: http://github.com/Bluebie/legs
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options: []
57
+
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ hash: 3
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ hash: 3
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.8.5
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Simple fun open networking for newbies and quick hacks
85
+ test_files: []
86
+