Class: Legs

Inherits:
Object
  • Object
show all
Defined in:
lib/legs.rb

Defined Under Namespace

Classes: AsyncData, RequestError, StartBlockError

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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

.logObject Also known as: log?

Returns the value of attribute log.



205
206
207
# File 'lib/legs.rb', line 205

def log
  @log
end

.messages_mutexObject (readonly)

Returns the value of attribute messages_mutex.



206
207
208
# File 'lib/legs.rb', line 206

def messages_mutex
  @messages_mutex
end

.server_objectObject (readonly)

Returns the value of attribute server_object.



206
207
208
# File 'lib/legs.rb', line 206

def server_object
  @server_object
end

.terminatorObject

Returns the value of attribute terminator.



205
206
207
# File 'lib/legs.rb', line 205

def terminator
  @terminator
end

.usersObject (readonly)

Returns the value of attribute users.



206
207
208
# File 'lib/legs.rb', line 206

def users
  @users
end

.users_mutexObject (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

#metaObject (readonly)

Returns the value of attribute meta.



10
11
12
# File 'lib/legs.rb', line 10

def meta
  @meta
end

#parentObject (readonly)

Returns the value of attribute parent.



10
11
12
# File 'lib/legs.rb', line 10

def parent
  @parent
end

#socketObject (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

.initializerObject



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

Returns:

  • (Boolean)


327
# File 'lib/legs.rb', line 327

def started?; @started; end

.stopObject

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

Returns:

  • (Boolean)


56
# File 'lib/legs.rb', line 56

def connected?; @socket.closed? == false and @closed == false; end

#inspectObject



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