Class: Legs
- Inherits:
-
Object
- Object
- Legs
- Defined in:
- lib/legs.rb
Defined Under Namespace
Classes: AsyncData, RequestError, StartBlockError
Class Attribute Summary collapse
-
.log ⇒ Object
(also: log?)
Returns the value of attribute log.
-
.messages_mutex ⇒ Object
readonly
Returns the value of attribute messages_mutex.
-
.server_object ⇒ Object
readonly
Returns the value of attribute server_object.
-
.terminator ⇒ Object
Returns the value of attribute terminator.
-
.users ⇒ Object
readonly
Returns the value of attribute users.
-
.users_mutex ⇒ Object
readonly
Returns the value of attribute users_mutex.
Instance Attribute Summary collapse
-
#meta ⇒ Object
readonly
Returns the value of attribute meta.
-
#parent ⇒ Object
readonly
Returns the value of attribute parent.
-
#socket ⇒ Object
readonly
Returns the value of attribute socket.
Class Method Summary collapse
-
.__data!(data, from) ⇒ Object
gets called to handle all incomming messages (RPC requests).
-
.broadcast(method, *args) ⇒ Object
sends a notification message to all connected clients.
-
.connections(direction = :both) ⇒ Object
gives you an array of all the instances of Legs which are still connected direction can be :both, :in, or :out.
-
.event(name, sender, *extras) ⇒ Object
add an event call to the server’s message queue.
-
.find_user_by(property, value) ⇒ Object
Finds a user by the value of a certain property…
- .find_users_by(property, *values) ⇒ Object
- .initializer ⇒ Object
-
.open(*args, &blk) ⇒ Object
creates a legs client, and passes it to supplied block, closes client after block finishes running I wouldn’t have added this method to keep shoes small, but users insist comedic value makes it worthwhile.
-
.start(port = 30274, &blk) ⇒ Object
starts the server, pass nil for port to make a ‘server’ that doesn’t actually accept connections This is useful for adding methods to Legs so that systems you connect to can call methods back on you.
-
.started? ⇒ Boolean
returns true if server is running.
-
.stop ⇒ Object
stops the server, disconnects the clients.
Instance Method Summary collapse
-
#close! ⇒ Object
closes the connection and the threads and stuff for this user.
-
#connected? ⇒ Boolean
I think you can guess this one.
-
#initialize(host = 'localhost', port = 30274) ⇒ Legs
constructor
Legs.new for a client, subclass to make a server, .new then makes server and client!.
- #inspect ⇒ Object
-
#method_missing(method, *args) ⇒ Object
maps undefined methods in to rpc calls.
-
#notify!(method, *args) ⇒ Object
send a notification to this user.
-
#send(method, *args) ⇒ Object
hacks the send method so ancestor methods instead become rpc calls too if you want to use a method in a Legs superclass, prefix with __.
-
#send!(method, *args) ⇒ Object
sends a normal RPC request that has a response.
-
#send_async!(method, *args, &blk) ⇒ Object
does an async request which calls a block when response arrives.
-
#send_data!(data) ⇒ Object
sends raw object over the socket.
Constructor Details
#initialize(host = 'localhost', port = 30274) ⇒ Legs
Legs.new for a client, subclass to make a server, .new then makes server and client!
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/legs.rb', line 13 def initialize(host = 'localhost', port = 30274) self.class.start(port) if self.class != Legs && !self.class.started? ObjectSpace.define_finalizer(self) { self.close! } @socket = TCPSocket.new(host, port) and @parent = false if host.instance_of?(String) @socket = host and @parent = port if host.instance_of?(TCPSocket) @responses = Hash.new; @meta = {}; @closed = false @responses_mutex = Mutex.new; @socket_mutex = Mutex.new @handle_data = Proc.new do |data| data = self.__json_restore(JSON.parse(data)) if data['method'] == '**remote__disconnecting**' self.close! elsif @parent and data['method'] @parent.__data!(data, self) elsif data['error'] and data['id'].nil? raise data['error'] else @responses_mutex.synchronize { @responses[data['id']] = data } end end @thread = Thread.new do while connected? begin self.close! if @socket.eof? data = nil @socket_mutex.synchronize { data = @socket.gets(self.class.terminator) } if data.nil? self.close! else @handle_data[data] end rescue JSON::ParserError => e self.send_data!({"error" => "JSON provided is invalid. See http://json.org/ to see how to format correctly."}) rescue IOError => e self.close! end end end end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(method, *args) ⇒ Object
maps undefined methods in to rpc calls
120 121 122 123 |
# File 'lib/legs.rb', line 120 def method_missing(method, *args) return self.send(method, *args) if method.to_s =~ /^__/ send!(method, *args) end |
Class Attribute Details
.log ⇒ Object Also known as: log?
Returns the value of attribute log.
205 206 207 |
# File 'lib/legs.rb', line 205 def log @log end |
.messages_mutex ⇒ Object (readonly)
Returns the value of attribute messages_mutex.
206 207 208 |
# File 'lib/legs.rb', line 206 def @messages_mutex end |
.server_object ⇒ Object (readonly)
Returns the value of attribute server_object.
206 207 208 |
# File 'lib/legs.rb', line 206 def server_object @server_object end |
.terminator ⇒ Object
Returns the value of attribute terminator.
205 206 207 |
# File 'lib/legs.rb', line 205 def terminator @terminator end |
.users ⇒ Object (readonly)
Returns the value of attribute users.
206 207 208 |
# File 'lib/legs.rb', line 206 def users @users end |
.users_mutex ⇒ Object (readonly)
Returns the value of attribute users_mutex.
206 207 208 |
# File 'lib/legs.rb', line 206 def users_mutex @users_mutex end |
Instance Attribute Details
#meta ⇒ Object (readonly)
Returns the value of attribute meta.
10 11 12 |
# File 'lib/legs.rb', line 10 def @meta end |
#parent ⇒ Object (readonly)
Returns the value of attribute parent.
10 11 12 |
# File 'lib/legs.rb', line 10 def parent @parent end |
#socket ⇒ Object (readonly)
Returns the value of attribute socket.
10 11 12 |
# File 'lib/legs.rb', line 10 def socket @socket end |
Class Method Details
.__data!(data, from) ⇒ Object
gets called to handle all incomming messages (RPC requests)
322 323 324 |
# File 'lib/legs.rb', line 322 def __data!(data, from) @messages.enq [data, from] end |
.broadcast(method, *args) ⇒ Object
sends a notification message to all connected clients
289 290 291 |
# File 'lib/legs.rb', line 289 def broadcast(method, *args) @users.each { |user| user.notify!(method, *args) } end |
.connections(direction = :both) ⇒ Object
gives you an array of all the instances of Legs which are still connected direction can be :both, :in, or :out
304 305 306 307 308 309 310 311 312 313 |
# File 'lib/legs.rb', line 304 def connections direction = :both return @users if direction == :in list = Array.new ObjectSpace.each_object(self) do |leg| next if list.include?(leg) unless leg.connected? next unless leg.parent == false if direction == :out list.push leg end return list end |
.event(name, sender, *extras) ⇒ Object
add an event call to the server’s message queue
316 317 318 319 |
# File 'lib/legs.rb', line 316 def event(name, sender, *extras) return unless @server_object.respond_to?("on_#{name}") __data!({'method' => "on_#{name}", 'params' => extras.to_a, 'id' => nil}, sender) end |
.find_user_by(property, value) ⇒ Object
Finds a user by the value of a certain property… like find_user_by :object_id, 12345
294 295 296 |
# File 'lib/legs.rb', line 294 def find_user_by property, value @users.find { |user| user.__send(property) == value } end |
.find_users_by(property, *values) ⇒ Object
298 299 300 |
# File 'lib/legs.rb', line 298 def find_users_by property, *values @users.select { |user| user.__send(property) == value } end |
.initializer ⇒ Object
209 210 211 212 213 |
# File 'lib/legs.rb', line 209 def initializer ObjectSpace.define_finalizer(self) { self.stop! } @users = []; @messages = Queue.new; @terminator = "\n"; @log = true @users_mutex = Mutex.new end |
.open(*args, &blk) ⇒ Object
creates a legs client, and passes it to supplied block, closes client after block finishes running I wouldn’t have added this method to keep shoes small, but users insist comedic value makes it worthwhile
331 332 333 334 335 |
# File 'lib/legs.rb', line 331 def open(*args, &blk) client = Legs.new(*args) blk[client] client.close! end |
.start(port = 30274, &blk) ⇒ Object
starts the server, pass nil for port to make a ‘server’ that doesn’t actually accept connections This is useful for adding methods to Legs so that systems you connect to can call methods back on you
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/legs.rb', line 218 def start(port=30274, &blk) return if started? raise "Legs.start requires a block" unless blk @started = true # make the fake class @server_class = Class.new @server_class.module_eval { private; attr_reader :server, :caller; public } @server_class.module_eval(&blk) @server_object = @server_class.allocate @server_object.instance_variable_set(:@server, self) @server_object.instance_eval { initialize } @message_processor = Thread.new do while started? sleep(0.01) and next if @messages.empty? data, from = @messages.deq method = data['method']; params = data['params'] methods = @server_object.public_methods(false) begin raise "Supplied method is not a String" unless method.is_a?(String) raise "Supplied params object is not an Array" unless params.is_a?(Array) raise "Cannot run '#{method}' because it is not defined in this server" unless methods.include?(method.to_s) or methods.include? :method_missing puts "Call #{method}(#{params.map { |i| i.inspect }.join(', ')})" if log? @server_object.instance_variable_set(:@caller, from) result = nil @users_mutex.synchronize do if methods.include?(method.to_s) result = @server_object.__send__(method.to_s, *params) else result = @server_object.method_missing(method.to_s, *params) end end puts ">> #{method} #=> #{result.inspect}" if log? from.send_data!({'id' => data['id'], 'result' => result}) unless data['id'].nil? rescue Exception => e from.send_data!({'error' => e, 'id' => data['id']}) unless data['id'].nil? puts "Backtrace: \n" + e.backtrace.map { |i| " #{i}" }.join("\n") if log? end end end unless port.nil? or port == false @listener = TCPServer.new(port) @acceptor_thread = Thread.new do while started? user = Legs.new(@listener.accept, self) @users_mutex.synchronize { @users.push user } puts "User #{user.object_id} connected, number of users: #{@users.length}" if log? self.event :connect, user end end end end |
.started? ⇒ Boolean
returns true if server is running
327 |
# File 'lib/legs.rb', line 327 def started?; @started; end |
.stop ⇒ Object
stops the server, disconnects the clients
283 284 285 286 |
# File 'lib/legs.rb', line 283 def stop @started = false @users.each { |user| user.close! } end |
Instance Method Details
#close! ⇒ Object
closes the connection and the threads and stuff for this user
59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/legs.rb', line 59 def close! @closed = true puts "User #{self.inspect} disconnecting" if self.class.log? @parent.event(:disconnect, self) if @parent # notify the remote side notify!('**remote__disconnecting**') rescue nil @parent.users_mutex.synchronize { @parent.users.delete(self) } if @parent @socket.close rescue nil end |
#connected? ⇒ Boolean
I think you can guess this one
56 |
# File 'lib/legs.rb', line 56 def connected?; @socket.closed? == false and @closed == false; end |
#inspect ⇒ Object
98 99 100 |
# File 'lib/legs.rb', line 98 def inspect "<Legs:#{__object_id} Meta: #{@meta.inspect}>" end |
#notify!(method, *args) ⇒ Object
send a notification to this user
73 74 75 76 |
# File 'lib/legs.rb', line 73 def notify!(method, *args) puts "Notify #{self.__inspect}: #{method}(#{args.map { |i| i.inspect }.join(', ')})" if self.__class.log? self.__send_data!({'method' => method.to_s, 'params' => args, 'id' => nil}) end |
#send(method, *args) ⇒ Object
hacks the send method so ancestor methods instead become rpc calls too if you want to use a method in a Legs superclass, prefix with __
127 128 129 130 131 |
# File 'lib/legs.rb', line 127 def send(method, *args) return super(method.to_s.sub(/^__/, ''), *args) if method.to_s =~ /^__/ return super(method, *args) if self.__public_methods(false).include?(method) super('send!', method.to_s, *args) end |
#send!(method, *args) ⇒ Object
sends a normal RPC request that has a response
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/legs.rb', line 79 def send!(method, *args) puts "Call #{self.__inspect}: #{method}(#{args.map { |i| i.inspect }.join(', ')})" if self.__class.log? id = self.__get_unique_number self.send_data! 'method' => method.to_s, 'params' => args, 'id' => id while @responses_mutex.synchronize { @responses.keys.include?(id) } == false sleep(0.01) end data = @responses_mutex.synchronize { @responses.delete(id) } error = data['error'] raise error unless error.nil? puts ">> #{method} #=> #{data['result'].inspect}" if self.__class.log? return data['result'] end |
#send_async!(method, *args, &blk) ⇒ Object
does an async request which calls a block when response arrives
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/legs.rb', line 103 def send_async!(method, *args, &blk) puts "Call #{self.__inspect}: #{method}(#{args.map { |i| i.inspect }.join(', ')})" if self.__class.log? id = self.__get_unique_number self.send_data! 'method' => method.to_s, 'params' => args, 'id' => id Thread.new do while @responses_mutex.synchronize { @responses.keys.include?(id) } == false sleep(0.05) end data = @responses_mutex.synchronize { @responses.delete(id) } puts ">> #{method} #=> #{data['result'].inspect}" if self.__class.log? unless data['error'] blk[Legs::AsyncData.new(data)] end end |
#send_data!(data) ⇒ Object
sends raw object over the socket
134 135 136 137 |
# File 'lib/legs.rb', line 134 def send_data!(data) raise "Lost remote connection" unless connected? @socket_mutex.synchronize { @socket.write(JSON.generate(__json_marshal(data)) + self.__class.terminator) } end |