Bluebie-legs 0.6 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +1 -1
- data/examples/chat-server.rb +13 -21
- data/legs.gemspec +1 -1
- data/lib/legs.rb +213 -170
- metadata +1 -1
- data/test/tester.rb +0 -108
data/README.rdoc
CHANGED
@@ -59,4 +59,4 @@ When you connect to this server, you can call the +count+ method. Each time you
|
|
59
59
|
|
60
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
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
|
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>.
|
data/examples/chat-server.rb
CHANGED
@@ -2,13 +2,13 @@ require '../lib/legs'
|
|
2
2
|
|
3
3
|
# this is a work in progress, api's will change and break, one day there will be a functional matching
|
4
4
|
# client in shoes or something
|
5
|
-
class User; attr_accessor :id, :name; end
|
6
5
|
|
7
6
|
Legs.start do
|
8
7
|
def initialize
|
9
8
|
@rooms = Hash.new { {'users' => [], 'messages' => [], 'topic' => 'No topic set'} }
|
10
9
|
@rooms['Lobby'] = {'topic' => 'General Chit Chat', 'messages' => [], 'users' => []}
|
11
10
|
end
|
11
|
+
|
12
12
|
# returns a list of available rooms
|
13
13
|
def rooms
|
14
14
|
room_list = Hash.new
|
@@ -20,13 +20,13 @@ Legs.start do
|
|
20
20
|
def join(room_name)
|
21
21
|
unless @rooms.keys.include?(room_name)
|
22
22
|
@rooms[room_name.to_s] = @rooms[room_name]
|
23
|
-
|
23
|
+
broadcast :room_created, room_name
|
24
24
|
end
|
25
25
|
|
26
26
|
# room = room_object(room_name)
|
27
27
|
#
|
28
28
|
# unless room['users'].include?(caller)
|
29
|
-
#
|
29
|
+
# broadcast room['users'], 'user_joined', room_name, user_object(caller)
|
30
30
|
# room['users'].push(caller)
|
31
31
|
# end
|
32
32
|
|
@@ -37,28 +37,29 @@ Legs.start do
|
|
37
37
|
def leave(room_name)
|
38
38
|
room = @rooms[room_name.to_s]
|
39
39
|
room['users'].delete(caller)
|
40
|
-
|
40
|
+
broadcast room['users'], 'user_left', room_name, user_object(caller)
|
41
41
|
true
|
42
42
|
end
|
43
43
|
|
44
44
|
# sets the room topic message
|
45
|
-
def
|
46
|
-
@rooms[room.to_s]
|
47
|
-
|
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)
|
48
49
|
end
|
49
50
|
|
50
51
|
# sets the user's name
|
51
|
-
def
|
52
|
+
def name=(name)
|
52
53
|
caller.meta[:name] = name.to_s
|
53
54
|
user_rooms(caller).each do |room_name|
|
54
|
-
|
55
|
+
broadcast @rooms[room_name]['users'], 'user_changed', user_object(caller)
|
55
56
|
end
|
56
57
|
true
|
57
58
|
end
|
58
59
|
|
59
60
|
# returns information about ones self, clients thusly can find out their user 'id' number
|
60
|
-
def
|
61
|
-
user = user_object( object_id.nil? ? caller :
|
61
|
+
def user(object_id = nil)
|
62
|
+
user = user_object( object_id.nil? ? caller : find_user_by_object_id(object_id) }.first )
|
62
63
|
user['rooms'] = user_rooms(user)
|
63
64
|
return user
|
64
65
|
end
|
@@ -68,7 +69,7 @@ Legs.start do
|
|
68
69
|
room = room_object(room_name)
|
69
70
|
room['messages'].push(msg = {'user' => user_object(caller), 'time' => Time.now.to_i, 'message' => message.to_s} )
|
70
71
|
trim_messages room
|
71
|
-
|
72
|
+
broadcast room['users'], 'message', room_name.to_s, msg
|
72
73
|
return msg
|
73
74
|
end
|
74
75
|
|
@@ -82,15 +83,6 @@ Legs.start do
|
|
82
83
|
end
|
83
84
|
end
|
84
85
|
|
85
|
-
# sends a notification to members of a room
|
86
|
-
def broadcast_to room, *args
|
87
|
-
room = @rooms[room.to_s] if room.is_a? String
|
88
|
-
room['users'].each do |user|
|
89
|
-
user.notify! *args
|
90
|
-
end
|
91
|
-
return true
|
92
|
-
end
|
93
|
-
|
94
86
|
# makes a user object suitable for sending back with meta info and stuff
|
95
87
|
def user_object user
|
96
88
|
object = {'id' => user.object_id}
|
data/legs.gemspec
CHANGED
data/lib/legs.rb
CHANGED
@@ -1,30 +1,35 @@
|
|
1
|
-
# Legs take you places, a networking companion
|
2
|
-
|
1
|
+
# Legs take you places, a networking companion
|
2
|
+
['rubygems', 'socket', 'thread'].each { |i| require i }
|
3
3
|
require 'json' unless self.class.const_defined? 'JSON'
|
4
|
-
require 'socket'
|
5
|
-
require 'thread'
|
6
|
-
|
7
|
-
Thread.abort_on_exception = true # Should be able to run without this, hopefully. Helps with debugging though
|
8
4
|
|
9
5
|
class Legs
|
6
|
+
# general getters
|
10
7
|
attr_reader :socket, :parent, :meta
|
8
|
+
def inspect; "<Legs:#{object_id} Meta: #{@meta.inspect}>"; end
|
11
9
|
|
12
10
|
# Legs.new for a client, subclass to make a server, .new then makes server and client!
|
13
11
|
def initialize(host = 'localhost', port = 30274)
|
14
12
|
self.class.start(port) if self.class != Legs && !self.class.started?
|
15
13
|
ObjectSpace.define_finalizer(self) { self.close! }
|
16
|
-
@
|
17
|
-
@socket = host and @parent = port if host.instance_of?(TCPSocket)
|
18
|
-
@responses = Hash.new; @meta = {}; @closed = false
|
14
|
+
@parent = false; @responses = Hash.new; @meta = {}; @disconnected = false
|
19
15
|
@responses_mutex = Mutex.new; @socket_mutex = Mutex.new
|
20
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
|
+
|
21
28
|
@handle_data = Proc.new do |data|
|
22
|
-
data =
|
29
|
+
data = json_restore(JSON.parse(data))
|
23
30
|
|
24
|
-
if data['method']
|
25
|
-
self.
|
26
|
-
elsif @parent and data['method']
|
27
|
-
@parent.__data!(data, self)
|
31
|
+
if data['method']
|
32
|
+
(@parent || self.class).__data!(data, self)
|
28
33
|
elsif data['error'] and data['id'].nil?
|
29
34
|
raise data['error']
|
30
35
|
else
|
@@ -33,107 +38,80 @@ class Legs
|
|
33
38
|
end
|
34
39
|
|
35
40
|
@thread = Thread.new do
|
36
|
-
|
41
|
+
until @socket.closed?
|
37
42
|
begin
|
38
|
-
|
43
|
+
close! if @socket.eof?
|
39
44
|
data = nil
|
40
|
-
@socket_mutex.synchronize { data = @socket.gets(self.class.terminator) }
|
45
|
+
@socket_mutex.synchronize { data = @socket.gets(self.class.terminator) rescue nil }
|
41
46
|
if data.nil?
|
42
|
-
|
47
|
+
close!
|
43
48
|
else
|
44
49
|
@handle_data[data]
|
45
50
|
end
|
46
51
|
rescue JSON::ParserError => e
|
47
|
-
|
52
|
+
send_data!({"error" => "JSON provided is invalid. See http://json.org/ to see how to format correctly."})
|
48
53
|
rescue IOError => e
|
49
|
-
|
54
|
+
close!
|
50
55
|
end
|
51
56
|
end
|
52
57
|
end
|
53
58
|
end
|
54
59
|
|
55
60
|
# I think you can guess this one
|
56
|
-
def connected?;
|
61
|
+
def connected?; self.class.connections.include?(self); end
|
57
62
|
|
58
63
|
# closes the connection and the threads and stuff for this user
|
59
64
|
def close!
|
60
|
-
@
|
61
|
-
|
62
|
-
@
|
65
|
+
return if @disconnected == true
|
66
|
+
|
67
|
+
@disconnected = true
|
68
|
+
puts "User #{inspect} disconnecting" if self.class.log?
|
63
69
|
|
64
70
|
# notify the remote side
|
65
71
|
notify!('**remote__disconnecting**') rescue nil
|
66
72
|
|
67
|
-
|
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
|
68
79
|
|
69
|
-
@socket.close rescue nil
|
80
|
+
Thread.new { sleep(1); @socket.close rescue nil }
|
70
81
|
end
|
71
82
|
|
72
83
|
# send a notification to this user
|
73
|
-
def notify!(method, *args)
|
74
|
-
puts "Notify #{
|
75
|
-
|
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})
|
76
87
|
end
|
77
88
|
|
78
89
|
# sends a normal RPC request that has a response
|
79
|
-
def send!(method, *args)
|
80
|
-
puts "Call #{
|
81
|
-
id =
|
82
|
-
|
83
|
-
|
84
|
-
while @responses_mutex.synchronize { @responses.keys.include?(id) } == false
|
85
|
-
sleep(0.01)
|
86
|
-
end
|
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
|
87
94
|
|
88
|
-
|
89
|
-
|
90
|
-
error = data['error']
|
91
|
-
raise error unless error.nil?
|
92
|
-
|
93
|
-
puts ">> #{method} #=> #{data['result'].inspect}" if self.__class.log?
|
94
|
-
|
95
|
-
return data['result']
|
96
|
-
end
|
97
|
-
|
98
|
-
def inspect
|
99
|
-
"<Legs:#{__object_id} Meta: #{@meta.inspect}>"
|
100
|
-
end
|
101
|
-
|
102
|
-
# does an async request which calls a block when response arrives
|
103
|
-
def send_async!(method, *args, &blk)
|
104
|
-
puts "Call #{self.__inspect}: #{method}(#{args.map { |i| i.inspect }.join(', ')})" if self.__class.log?
|
105
|
-
id = self.__get_unique_number
|
106
|
-
self.send_data! 'method' => method.to_s, 'params' => args, 'id' => id
|
107
|
-
|
108
|
-
Thread.new do
|
109
|
-
while @responses_mutex.synchronize { @responses.keys.include?(id) } == false
|
110
|
-
sleep(0.05)
|
111
|
-
end
|
95
|
+
worker = Proc.new do
|
96
|
+
sleep 0.1 until @responses_mutex.synchronize { @responses.keys.include?(id) }
|
112
97
|
|
113
|
-
|
114
|
-
puts ">> #{method} #=> #{data['result'].inspect}" if self.
|
115
|
-
|
98
|
+
result = Legs::Result.new(@responses_mutex.synchronize { @responses.delete(id) })
|
99
|
+
puts ">> #{method} #=> #{result.data['result'].inspect}" if self.class.log?
|
100
|
+
result
|
116
101
|
end
|
102
|
+
|
103
|
+
if blk.respond_to?(:call); Thread.new { blk[worker.call] }
|
104
|
+
else; worker.call.value; end
|
117
105
|
end
|
118
106
|
|
119
|
-
#
|
120
|
-
|
121
|
-
return self.send(method, *args) if method.to_s =~ /^__/
|
122
|
-
send!(method, *args)
|
123
|
-
end
|
107
|
+
# catch all the rogue calls and make them work niftily
|
108
|
+
alias_method :method_missing, :send!
|
124
109
|
|
125
|
-
# hacks the send method so ancestor methods instead become rpc calls too
|
126
|
-
# if you want to use a method in a Legs superclass, prefix with __
|
127
|
-
def send(method, *args)
|
128
|
-
return super(method.to_s.sub(/^__/, ''), *args) if method.to_s =~ /^__/
|
129
|
-
return super(method, *args) if self.__public_methods(false).include?(method)
|
130
|
-
super('send!', method.to_s, *args)
|
131
|
-
end
|
132
|
-
|
133
110
|
# sends raw object over the socket
|
134
111
|
def send_data!(data)
|
135
112
|
raise "Lost remote connection" unless connected?
|
136
|
-
|
113
|
+
raw = JSON.generate(json_marshal(data)) + self.class.terminator
|
114
|
+
@socket_mutex.synchronize { @socket.write(raw) }
|
137
115
|
end
|
138
116
|
|
139
117
|
|
@@ -146,23 +124,29 @@ class Legs
|
|
146
124
|
return object
|
147
125
|
when Hash
|
148
126
|
out = Hash.new
|
149
|
-
object.each_pair { |k,v| out[k.to_s] =
|
127
|
+
object.each_pair { |k,v| out[k.to_s] = json_marshal(v) }
|
150
128
|
return out
|
151
129
|
when Array
|
152
|
-
return object.map { |v|
|
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]}
|
153
135
|
else
|
154
|
-
return {'__jsonclass__' => [object.class.name, object._dump]} if object.respond_to?(:_dump)
|
136
|
+
return {'__jsonclass__' => [object.class.name, '_load', object._dump]} if object.respond_to?(:_dump)
|
155
137
|
|
156
138
|
# the default marshalling behaviour
|
157
139
|
instance_vars = {}
|
158
140
|
object.instance_variables.each do |var_name|
|
159
|
-
instance_vars[var_name.to_s.sub(/@/, '')] =
|
141
|
+
instance_vars[var_name.to_s.sub(/@/, '')] = json_marshal(object.instance_variable_get(var_name))
|
160
142
|
end
|
161
143
|
|
162
|
-
return {'__jsonclass__' => [object.class.name]}.merge(instance_vars)
|
144
|
+
return {'__jsonclass__' => [object.class.name, 'new']}.merge(instance_vars)
|
163
145
|
end
|
164
146
|
end
|
165
147
|
|
148
|
+
SAFE_CONSTRUCTORS = ['new', 'allocate', '_load']
|
149
|
+
|
166
150
|
# takes an object from the network, and decodes any marshalled hashes back in to ruby objects
|
167
151
|
def json_restore(object)
|
168
152
|
case object
|
@@ -170,26 +154,34 @@ class Legs
|
|
170
154
|
if object.keys.include? '__jsonclass__'
|
171
155
|
constructor = object.delete('__jsonclass__')
|
172
156
|
class_name = constructor.shift.to_s
|
173
|
-
object_class = Module.const_get(class_name) rescue false
|
174
157
|
|
175
|
-
|
176
|
-
|
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
|
177
170
|
|
178
|
-
instance = object_class.allocate
|
179
171
|
object.each_pair do |key, value|
|
180
|
-
instance.instance_variable_set("@#{key}",
|
172
|
+
instance.instance_variable_set("@#{key}", json_restore(value))
|
181
173
|
end
|
182
174
|
return instance
|
183
175
|
else
|
184
|
-
raise "Response contains a #{class_name} but that class is not loaded locally."
|
176
|
+
raise "Response contains a #{class_name} but that class is not loaded locally, it needs to be."
|
185
177
|
end
|
186
178
|
else
|
187
179
|
hash = Hash.new
|
188
|
-
object.each_pair { |k,v| hash[k] =
|
180
|
+
object.each_pair { |k,v| hash[k] = json_restore(v) }
|
189
181
|
return hash
|
190
182
|
end
|
191
183
|
when Array
|
192
|
-
return object.map { |i|
|
184
|
+
return object.map { |i| json_restore(i) }
|
193
185
|
else
|
194
186
|
return object
|
195
187
|
end
|
@@ -199,80 +191,132 @@ class Legs
|
|
199
191
|
def get_unique_number; @unique_id ||= 0; @unique_id += 1; end
|
200
192
|
end
|
201
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
|
+
|
202
200
|
|
203
201
|
# the server is started by subclassing Legs, then SubclassName.start
|
204
202
|
class << Legs
|
205
203
|
attr_accessor :terminator, :log
|
206
|
-
attr_reader :
|
204
|
+
attr_reader :incoming, :outgoing, :server_object, :incoming_mutex, :outgoing_mutex, :messages_mutex
|
207
205
|
alias_method :log?, :log
|
206
|
+
alias_method :users, :incoming
|
207
|
+
def started?; @started; end
|
208
208
|
|
209
209
|
def initializer
|
210
210
|
ObjectSpace.define_finalizer(self) { self.stop! }
|
211
|
-
@
|
212
|
-
@
|
211
|
+
@incoming = []; @outgoing = []; @messages = Queue.new; @terminator = "\n"; @log = false
|
212
|
+
@incoming_mutex = Mutex.new; @outgoing_mutex = Mutex.new; @started = false
|
213
213
|
end
|
214
214
|
|
215
215
|
|
216
216
|
# starts the server, pass nil for port to make a 'server' that doesn't actually accept connections
|
217
217
|
# This is useful for adding methods to Legs so that systems you connect to can call methods back on you
|
218
218
|
def start(port=30274, &blk)
|
219
|
-
return if started?
|
220
|
-
raise "Legs.start requires a block" unless blk
|
219
|
+
return @server_class.module_eval(&blk) if started? and blk.respond_to? :call
|
221
220
|
@started = true
|
222
221
|
|
223
|
-
#
|
224
|
-
@server_class
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
230
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
|
+
|
231
269
|
@message_processor = Thread.new do
|
232
270
|
while started?
|
233
|
-
sleep
|
271
|
+
sleep 0.01 while @messages.empty?
|
234
272
|
data, from = @messages.deq
|
235
273
|
method = data['method']; params = data['params']
|
236
274
|
methods = @server_object.public_methods(false)
|
237
275
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
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
|
254
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?
|
255
307
|
end
|
256
|
-
|
257
|
-
puts ">> #{method} #=> #{result.inspect}" if log?
|
258
|
-
|
259
|
-
from.send_data!({'id' => data['id'], 'result' => result}) unless data['id'].nil?
|
260
|
-
|
261
|
-
rescue Exception => e
|
262
|
-
from.send_data!({'error' => e, 'id' => data['id']}) unless data['id'].nil?
|
263
|
-
puts "Backtrace: \n" + e.backtrace.map { |i| " #{i}" }.join("\n") if log?
|
264
308
|
end
|
265
309
|
end
|
266
|
-
end
|
310
|
+
end unless @message_processor and @message_processor.alive?
|
267
311
|
|
268
|
-
|
312
|
+
if ( port.nil? or port == false ) == false and @listener.nil?
|
269
313
|
@listener = TCPServer.new(port)
|
270
314
|
|
271
315
|
@acceptor_thread = Thread.new do
|
272
316
|
while started?
|
273
317
|
user = Legs.new(@listener.accept, self)
|
274
|
-
@
|
275
|
-
puts "User #{user.object_id} connected, number of users: #{@
|
318
|
+
@incoming_mutex.synchronize { @incoming.push user }
|
319
|
+
puts "User #{user.object_id} connected, number of users: #{@incoming.length}" if log?
|
276
320
|
self.event :connect, user
|
277
321
|
end
|
278
322
|
end
|
@@ -282,34 +326,12 @@ class << Legs
|
|
282
326
|
# stops the server, disconnects the clients
|
283
327
|
def stop
|
284
328
|
@started = false
|
285
|
-
@
|
329
|
+
@incoming.each { |user| user.close! }
|
286
330
|
end
|
287
331
|
|
288
|
-
#
|
289
|
-
def
|
290
|
-
@
|
291
|
-
end
|
292
|
-
|
293
|
-
# Finds a user by the value of a certain property... like find_user_by :object_id, 12345
|
294
|
-
def find_user_by property, value
|
295
|
-
@users.find { |user| user.__send(property) == value }
|
296
|
-
end
|
297
|
-
|
298
|
-
def find_users_by property, *values
|
299
|
-
@users.select { |user| user.__send(property) == value }
|
300
|
-
end
|
301
|
-
|
302
|
-
# gives you an array of all the instances of Legs which are still connected
|
303
|
-
# direction can be :both, :in, or :out
|
304
|
-
def connections direction = :both
|
305
|
-
return @users if direction == :in
|
306
|
-
list = Array.new
|
307
|
-
ObjectSpace.each_object(self) do |leg|
|
308
|
-
next if list.include?(leg) unless leg.connected?
|
309
|
-
next unless leg.parent == false if direction == :out
|
310
|
-
list.push leg
|
311
|
-
end
|
312
|
-
return list
|
332
|
+
# returns an array of all connections
|
333
|
+
def connections
|
334
|
+
@incoming + @outgoing
|
313
335
|
end
|
314
336
|
|
315
337
|
# add an event call to the server's message queue
|
@@ -318,34 +340,55 @@ class << Legs
|
|
318
340
|
__data!({'method' => "on_#{name}", 'params' => extras.to_a, 'id' => nil}, sender)
|
319
341
|
end
|
320
342
|
|
321
|
-
# gets called to handle all
|
343
|
+
# gets called to handle all incoming messages (RPC requests)
|
322
344
|
def __data!(data, from)
|
323
345
|
@messages.enq [data, from]
|
324
346
|
end
|
325
347
|
|
326
|
-
#
|
327
|
-
def
|
328
|
-
|
329
|
-
# creates a legs client, and passes it to supplied block, closes client after block finishes running
|
330
|
-
# I wouldn't have added this method to keep shoes small, but users insist comedic value makes it worthwhile
|
331
|
-
def open(*args, &blk)
|
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)
|
332
350
|
client = Legs.new(*args)
|
333
|
-
|
351
|
+
yield(client)
|
334
352
|
client.close!
|
335
353
|
end
|
354
|
+
|
355
|
+
# add's a method to the 'server'
|
356
|
+
def define_method(name, &blk); @server_class.class_eval { define_method(name, &blk) }; end
|
357
|
+
|
358
|
+
# lets the marshaler transport symbols
|
359
|
+
def __make_symbol(name); name.to_sym; end
|
360
|
+
|
361
|
+
# hooks up these methods so you can use them off the main object too!
|
362
|
+
[:broadcast, :find_user_by_object_id, :find_users_by_meta].each do |name|
|
363
|
+
define_method name do |*args|
|
364
|
+
@incoming_mutex.synchronize do; @outgoing_mutex.synchronize do
|
365
|
+
@server_object.__send__(name, *args)
|
366
|
+
end; end
|
367
|
+
end
|
368
|
+
end
|
336
369
|
end
|
337
370
|
|
338
371
|
Legs.initializer
|
339
372
|
|
340
|
-
|
341
|
-
class Legs::
|
373
|
+
# represents the data response, handles throwing of errors and stuff
|
374
|
+
class Legs::Result
|
375
|
+
attr_reader :data
|
342
376
|
def initialize(data); @data = data; end
|
343
377
|
def result
|
344
|
-
|
345
|
-
|
378
|
+
unless @data['error'].nil? or @errored
|
379
|
+
@errored = true
|
380
|
+
raise @data['error']
|
381
|
+
end
|
382
|
+
@data['result']
|
346
383
|
end
|
347
384
|
alias_method :value, :result
|
348
385
|
end
|
349
386
|
|
350
387
|
class Legs::StartBlockError < StandardError; end
|
351
388
|
class Legs::RequestError < StandardError; end
|
389
|
+
class Legs::RemoteError < StandardError
|
390
|
+
def initialize(msg, backtrace)
|
391
|
+
super(msg)
|
392
|
+
set_backtrace(backtrace)
|
393
|
+
end
|
394
|
+
end
|
metadata
CHANGED
data/test/tester.rb
DELETED
@@ -1,108 +0,0 @@
|
|
1
|
-
require '../lib/legs.rb'
|
2
|
-
|
3
|
-
# class to test the marshaling
|
4
|
-
class Testing
|
5
|
-
attr_accessor :a, :b, :c
|
6
|
-
end
|
7
|
-
|
8
|
-
Legs.log = false
|
9
|
-
|
10
|
-
# a simple server to test with
|
11
|
-
Legs.start(6425) do
|
12
|
-
def echo(text)
|
13
|
-
return text
|
14
|
-
end
|
15
|
-
|
16
|
-
def count
|
17
|
-
caller.meta[:counter] ||= 0
|
18
|
-
caller.meta[:counter] += 1
|
19
|
-
end
|
20
|
-
|
21
|
-
def error
|
22
|
-
raise StandardError.new("This is a fake error")
|
23
|
-
end
|
24
|
-
|
25
|
-
def test_notify
|
26
|
-
puts "Success"
|
27
|
-
end
|
28
|
-
|
29
|
-
def marshal
|
30
|
-
obj = Testing.new
|
31
|
-
obj.a = 1; obj.b = 2; obj.c = 3
|
32
|
-
return obj
|
33
|
-
end
|
34
|
-
|
35
|
-
def on_disconnect
|
36
|
-
puts "#{caller.inspect} disconnected"
|
37
|
-
end
|
38
|
-
|
39
|
-
def on_connect
|
40
|
-
$server_instance = caller
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
## connects and tests a bunch of things
|
45
|
-
puts "Testing syncronous echo"
|
46
|
-
i = Legs.new('localhost',6425)
|
47
|
-
i.meta[:name] = 'first tester client'
|
48
|
-
puts i.echo('Hello World') == 'Hello World' ?"Success":"Failure"
|
49
|
-
|
50
|
-
puts "Testing Count"
|
51
|
-
puts i.count==1 && i.count == 2 && i.count == 3 ?'Success':'Failure'
|
52
|
-
|
53
|
-
puts "Testing count resets correctly"
|
54
|
-
ii = Legs.new('localhost',6425)
|
55
|
-
ii.meta[:name] = 'second tester client'
|
56
|
-
puts ii.count == 1 && ii.count == 2 && ii.count == 3 ?'Success':'Failure'
|
57
|
-
ii.close!
|
58
|
-
|
59
|
-
sleep(0.5)
|
60
|
-
puts "Testing disconnection went well"
|
61
|
-
puts ii.connected? == false && $server_instance.connected? == false ?'Success':'Failure'
|
62
|
-
|
63
|
-
ii = nil
|
64
|
-
ObjectSpace.garbage_collect
|
65
|
-
|
66
|
-
puts "Testing server disconnect worked correctly"
|
67
|
-
sleep(0.5)
|
68
|
-
puts Legs.users.length == 1 ?'Success':"Failure: #{Legs.users.length}"
|
69
|
-
|
70
|
-
puts "Testing async call..."
|
71
|
-
i.send_async!(:echo, 'Testing') do |r|
|
72
|
-
puts r.result == 'Testing' ?'Success':'Failure'
|
73
|
-
end
|
74
|
-
sleep(0.5)
|
75
|
-
|
76
|
-
puts "Testing async error..."
|
77
|
-
i.send_async!(:error) do |r|
|
78
|
-
begin
|
79
|
-
v = r.result
|
80
|
-
puts "Failure"
|
81
|
-
rescue
|
82
|
-
puts "Success"
|
83
|
-
end
|
84
|
-
end
|
85
|
-
sleep(0.5)
|
86
|
-
|
87
|
-
puts "Testing regular error"
|
88
|
-
v = i.error rescue :good
|
89
|
-
puts v == :good ?'Success':'Failure'
|
90
|
-
|
91
|
-
puts "Testing notify!"
|
92
|
-
i.test_notify
|
93
|
-
|
94
|
-
puts "Testing marshalling"
|
95
|
-
m = i.marshal
|
96
|
-
puts m.a == 1 && m.b == 2 && m.c == 3 ?'Success':'Failure'
|
97
|
-
|
98
|
-
puts "Testing Legs.connections"
|
99
|
-
puts (Legs.connections(:out) == i and Legs.connections.length == 2 and Legs.connections(:in) == Legs.users.first) ?'Success':'Failure'
|
100
|
-
puts ":in => #{Legs.connections(:in).map{ |l| l.inspect }}"
|
101
|
-
puts ":out => #{Legs.connections(:out).map{ |l| l.inspect }}"
|
102
|
-
puts ":both => #{Legs.connections.map{ |l| l.inspect }}"
|
103
|
-
|
104
|
-
puts "All: "
|
105
|
-
ObjectSpace.each_object(Legs) { |l| puts " #{l.inspect}" }
|
106
|
-
|
107
|
-
puts
|
108
|
-
puts "Done"
|