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 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>.
@@ -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
- server.broadcast :room_created, room_name
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
- # broadcast_to room, 'user_joined', room_name, user_object(caller)
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
- broadcast_to room, 'user_left', room_name, user_object(caller)
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 set_topic(room, message)
46
- @rooms[room.to_s]['topic'] = message.to_s
47
- broadcast_to room, 'room_changed', room_object(room, :remote, :name, :topic)
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 set_name(name)
52
+ def name=(name)
52
53
  caller.meta[:name] = name.to_s
53
54
  user_rooms(caller).each do |room_name|
54
- broadcast_to room_name, 'user_changed', user_object(caller)
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 get_user(object_id = nil)
61
- user = user_object( object_id.nil? ? caller : users.select { |u| u.object_id == object_id.to_i }.first )
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
- broadcast_to room, 'message', room_name.to_s, msg
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
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "legs"
3
- s.version = "0.6"
3
+ s.version = "0.6.1"
4
4
  s.date = "2008-07-12"
5
5
  s.summary = "Simple fun open networking for newbies and quick hacks"
6
6
  s.email = "blue@creativepony.com"
data/lib/legs.rb CHANGED
@@ -1,30 +1,35 @@
1
- # Legs take you places, a networking companion to Shoes
2
- require 'rubygems'
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
- @socket = TCPSocket.new(host, port) and @parent = false if host.instance_of?(String)
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 = self.__json_restore(JSON.parse(data))
29
+ data = json_restore(JSON.parse(data))
23
30
 
24
- if data['method'] == '**remote__disconnecting**'
25
- self.close!
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
- while connected?
41
+ until @socket.closed?
37
42
  begin
38
- self.close! if @socket.eof?
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
- self.close!
47
+ close!
43
48
  else
44
49
  @handle_data[data]
45
50
  end
46
51
  rescue JSON::ParserError => e
47
- self.send_data!({"error" => "JSON provided is invalid. See http://json.org/ to see how to format correctly."})
52
+ send_data!({"error" => "JSON provided is invalid. See http://json.org/ to see how to format correctly."})
48
53
  rescue IOError => e
49
- self.close!
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?; @socket.closed? == false and @closed == false; end
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
- @closed = true
61
- puts "User #{self.inspect} disconnecting" if self.class.log?
62
- @parent.event(:disconnect, self) if @parent
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
- @parent.users_mutex.synchronize { @parent.users.delete(self) } if @parent
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 #{self.__inspect}: #{method}(#{args.map { |i| i.inspect }.join(', ')})" if self.__class.log?
75
- self.__send_data!({'method' => method.to_s, 'params' => args, 'id' => nil})
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 #{self.__inspect}: #{method}(#{args.map { |i| i.inspect }.join(', ')})" if self.__class.log?
81
- id = self.__get_unique_number
82
- self.send_data! 'method' => method.to_s, 'params' => args, 'id' => id
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
- data = @responses_mutex.synchronize { @responses.delete(id) }
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
- data = @responses_mutex.synchronize { @responses.delete(id) }
114
- puts ">> #{method} #=> #{data['result'].inspect}" if self.__class.log? unless data['error']
115
- blk[Legs::AsyncData.new(data)]
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
- # maps undefined methods in to rpc calls
120
- def method_missing(method, *args)
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
- @socket_mutex.synchronize { @socket.write(JSON.generate(__json_marshal(data)) + self.__class.terminator) }
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] = __json_marshal(v) }
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| __json_marshal(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(/@/, '')] = self.__json_marshal(object.instance_variable_get(var_name))
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
- if object_class.name == class_name
176
- return object_class._load(*constructor) if object_class.respond_to?(:_load) unless constructor.empty?
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}", self.__json_restore(value))
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] = self.__json_restore(v) }
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| self.__json_restore(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 :users, :server_object, :users_mutex, :messages_mutex
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
- @users = []; @messages = Queue.new; @terminator = "\n"; @log = true
212
- @users_mutex = Mutex.new
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
- # make the fake class
224
- @server_class = Class.new
225
- @server_class.module_eval { private; attr_reader :server, :caller; public }
226
- @server_class.module_eval(&blk)
227
- @server_object = @server_class.allocate
228
- @server_object.instance_variable_set(:@server, self)
229
- @server_object.instance_eval { initialize }
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(0.01) and next if @messages.empty?
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
- begin
239
- raise "Supplied method is not a String" unless method.is_a?(String)
240
- raise "Supplied params object is not an Array" unless params.is_a?(Array)
241
- raise "Cannot run '#{method}' because it is not defined in this server" unless methods.include?(method.to_s) or methods.include? :method_missing
242
-
243
- puts "Call #{method}(#{params.map { |i| i.inspect }.join(', ')})" if log?
244
-
245
- @server_object.instance_variable_set(:@caller, from)
246
-
247
- result = nil
248
-
249
- @users_mutex.synchronize do
250
- if methods.include?(method.to_s)
251
- result = @server_object.__send__(method.to_s, *params)
252
- else
253
- result = @server_object.method_missing(method.to_s, *params)
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
- unless port.nil? or port == false
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
- @users_mutex.synchronize { @users.push user }
275
- puts "User #{user.object_id} connected, number of users: #{@users.length}" if log?
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
- @users.each { |user| user.close! }
329
+ @incoming.each { |user| user.close! }
286
330
  end
287
331
 
288
- # sends a notification message to all connected clients
289
- def broadcast(method, *args)
290
- @users.each { |user| user.notify!(method, *args) }
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 incomming messages (RPC requests)
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
- # returns true if server is running
327
- def started?; @started; end
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
- blk[client]
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::AsyncData
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
- @errored = true and raise @data['error'] if @data['error'] unless @errored
345
- return @data['result']
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
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: Bluebie-legs
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.6"
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jenna 'Bluebie' Fox
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"