Bluebie-legs 0.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -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
@@ -0,0 +1,119 @@
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
+ class User; attr_accessor :id, :name; end
6
+
7
+ Legs.start do
8
+ def initialize
9
+ @rooms = Hash.new { {'users' => [], 'messages' => [], 'topic' => 'No topic set'} }
10
+ @rooms['Lobby'] = {'topic' => 'General Chit Chat', 'messages' => [], 'users' => []}
11
+ end
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
+ server.broadcast :room_created, room_name
24
+ end
25
+
26
+ # room = room_object(room_name)
27
+ #
28
+ # unless room['users'].include?(caller)
29
+ # broadcast_to room, '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_to room, 'user_left', room_name, user_object(caller)
41
+ true
42
+ end
43
+
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)
48
+ end
49
+
50
+ # sets the user's name
51
+ def set_name(name)
52
+ caller.meta[:name] = name.to_s
53
+ user_rooms(caller).each do |room_name|
54
+ broadcast_to room_name, 'user_changed', user_object(caller)
55
+ end
56
+ true
57
+ end
58
+
59
+ # 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 )
62
+ user['rooms'] = user_rooms(user)
63
+ return user
64
+ end
65
+
66
+ # posts a message to a room
67
+ def post_message(room_name, message)
68
+ room = room_object(room_name)
69
+ room['messages'].push(msg = {'user' => user_object(caller), 'time' => Time.now.to_i, 'message' => message.to_s} )
70
+ trim_messages room
71
+ broadcast_to room, 'message', room_name.to_s, msg
72
+ return msg
73
+ end
74
+
75
+ private
76
+
77
+ # trims the message backlog
78
+ def trim_messages room
79
+ room = room_object(room) if room.is_a?(String)
80
+ while room['messages'].length > 250
81
+ room['messages'].shift
82
+ end
83
+ end
84
+
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
+ # makes a user object suitable for sending back with meta info and stuff
95
+ def user_object user
96
+ object = {'id' => user.object_id}
97
+ user.meta.each_pair do |key, value|
98
+ object[key.to_s] = value
99
+ end
100
+ return object
101
+ end
102
+
103
+ def room_object room_name, target = :local, *only
104
+ object = @rooms[room_name.to_s].dup
105
+ object['users'].delete_if {|user| user.connected? == false }
106
+ object['users'] = object['users'].map { |user| user_object(user) } if target == :remote
107
+ object['topic'] = 'No topic set' if object['topic'].nil? or object['topic'].empty?
108
+ object['name'] = room_name.to_s if target == :remote
109
+ object.delete_if { |key, value| only.include?(key.to_sym) == false } unless only.empty?
110
+ object
111
+ end
112
+
113
+ # returns all the room names the user is in.
114
+ def user_rooms user
115
+ @rooms.values.select { |room| room['users'].include?(user) }.map { |room| @rooms.index(room) }
116
+ end
117
+ end
118
+
119
+ 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
+
data/legs.gemspec ADDED
@@ -0,0 +1,13 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "legs"
3
+ s.version = "0.6"
4
+ s.date = "2008-07-12"
5
+ s.summary = "Simple fun open networking for newbies and quick hacks"
6
+ s.email = "blue@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 'Bluebie' 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/tester.rb"]
12
+ s.add_dependency("json_pure", ["> 1.1.0"])
13
+ end
data/lib/legs.rb ADDED
@@ -0,0 +1,351 @@
1
+ # Legs take you places, a networking companion to Shoes
2
+ require 'rubygems'
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
+
9
+ class Legs
10
+ attr_reader :socket, :parent, :meta
11
+
12
+ # Legs.new for a client, subclass to make a server, .new then makes server and client!
13
+ def initialize(host = 'localhost', port = 30274)
14
+ self.class.start(port) if self.class != Legs && !self.class.started?
15
+ 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
19
+ @responses_mutex = Mutex.new; @socket_mutex = Mutex.new
20
+
21
+ @handle_data = Proc.new do |data|
22
+ data = self.__json_restore(JSON.parse(data))
23
+
24
+ if data['method'] == '**remote__disconnecting**'
25
+ self.close!
26
+ elsif @parent and data['method']
27
+ @parent.__data!(data, self)
28
+ elsif data['error'] and data['id'].nil?
29
+ raise data['error']
30
+ else
31
+ @responses_mutex.synchronize { @responses[data['id']] = data }
32
+ end
33
+ end
34
+
35
+ @thread = Thread.new do
36
+ while connected?
37
+ begin
38
+ self.close! if @socket.eof?
39
+ data = nil
40
+ @socket_mutex.synchronize { data = @socket.gets(self.class.terminator) }
41
+ if data.nil?
42
+ self.close!
43
+ else
44
+ @handle_data[data]
45
+ end
46
+ rescue JSON::ParserError => e
47
+ self.send_data!({"error" => "JSON provided is invalid. See http://json.org/ to see how to format correctly."})
48
+ rescue IOError => e
49
+ self.close!
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ # I think you can guess this one
56
+ def connected?; @socket.closed? == false and @closed == false; end
57
+
58
+ # closes the connection and the threads and stuff for this user
59
+ def close!
60
+ @closed = true
61
+ puts "User #{self.inspect} disconnecting" if self.class.log?
62
+ @parent.event(:disconnect, self) if @parent
63
+
64
+ # notify the remote side
65
+ notify!('**remote__disconnecting**') rescue nil
66
+
67
+ @parent.users_mutex.synchronize { @parent.users.delete(self) } if @parent
68
+
69
+ @socket.close rescue nil
70
+ end
71
+
72
+ # 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})
76
+ end
77
+
78
+ # 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
87
+
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
112
+
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)]
116
+ end
117
+ end
118
+
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
124
+
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
+ # sends raw object over the socket
134
+ def send_data!(data)
135
+ raise "Lost remote connection" unless connected?
136
+ @socket_mutex.synchronize { @socket.write(JSON.generate(__json_marshal(data)) + self.__class.terminator) }
137
+ end
138
+
139
+
140
+ private
141
+
142
+ # takes a ruby object, and converts it if needed in to marshalled hashes
143
+ def json_marshal(object)
144
+ case object
145
+ when Bignum, Fixnum, Integer, Float, TrueClass, FalseClass, String, NilClass
146
+ return object
147
+ when Hash
148
+ out = Hash.new
149
+ object.each_pair { |k,v| out[k.to_s] = __json_marshal(v) }
150
+ return out
151
+ when Array
152
+ return object.map { |v| __json_marshal(v) }
153
+ else
154
+ return {'__jsonclass__' => [object.class.name, object._dump]} if object.respond_to?(:_dump)
155
+
156
+ # the default marshalling behaviour
157
+ instance_vars = {}
158
+ object.instance_variables.each do |var_name|
159
+ instance_vars[var_name.to_s.sub(/@/, '')] = self.__json_marshal(object.instance_variable_get(var_name))
160
+ end
161
+
162
+ return {'__jsonclass__' => [object.class.name]}.merge(instance_vars)
163
+ end
164
+ end
165
+
166
+ # takes an object from the network, and decodes any marshalled hashes back in to ruby objects
167
+ def json_restore(object)
168
+ case object
169
+ when Hash
170
+ if object.keys.include? '__jsonclass__'
171
+ constructor = object.delete('__jsonclass__')
172
+ class_name = constructor.shift.to_s
173
+ object_class = Module.const_get(class_name) rescue false
174
+
175
+ if object_class.name == class_name
176
+ return object_class._load(*constructor) if object_class.respond_to?(:_load) unless constructor.empty?
177
+
178
+ instance = object_class.allocate
179
+ object.each_pair do |key, value|
180
+ instance.instance_variable_set("@#{key}", self.__json_restore(value))
181
+ end
182
+ return instance
183
+ else
184
+ raise "Response contains a #{class_name} but that class is not loaded locally."
185
+ end
186
+ else
187
+ hash = Hash.new
188
+ object.each_pair { |k,v| hash[k] = self.__json_restore(v) }
189
+ return hash
190
+ end
191
+ when Array
192
+ return object.map { |i| self.__json_restore(i) }
193
+ else
194
+ return object
195
+ end
196
+ end
197
+
198
+ # gets a unique number that we can use to match requests to responses
199
+ def get_unique_number; @unique_id ||= 0; @unique_id += 1; end
200
+ end
201
+
202
+
203
+ # the server is started by subclassing Legs, then SubclassName.start
204
+ class << Legs
205
+ attr_accessor :terminator, :log
206
+ attr_reader :users, :server_object, :users_mutex, :messages_mutex
207
+ alias_method :log?, :log
208
+
209
+ def initializer
210
+ ObjectSpace.define_finalizer(self) { self.stop! }
211
+ @users = []; @messages = Queue.new; @terminator = "\n"; @log = true
212
+ @users_mutex = Mutex.new
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 if started?
220
+ raise "Legs.start requires a block" unless blk
221
+ @started = true
222
+
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 }
230
+
231
+ @message_processor = Thread.new do
232
+ while started?
233
+ sleep(0.01) and next if @messages.empty?
234
+ data, from = @messages.deq
235
+ method = data['method']; params = data['params']
236
+ methods = @server_object.public_methods(false)
237
+
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)
254
+ end
255
+ 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
+ end
265
+ end
266
+ end
267
+
268
+ unless port.nil? or port == false
269
+ @listener = TCPServer.new(port)
270
+
271
+ @acceptor_thread = Thread.new do
272
+ while started?
273
+ 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?
276
+ self.event :connect, user
277
+ end
278
+ end
279
+ end
280
+ end
281
+
282
+ # stops the server, disconnects the clients
283
+ def stop
284
+ @started = false
285
+ @users.each { |user| user.close! }
286
+ end
287
+
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
313
+ end
314
+
315
+ # add an event call to the server's message queue
316
+ def event(name, sender, *extras)
317
+ return unless @server_object.respond_to?("on_#{name}")
318
+ __data!({'method' => "on_#{name}", 'params' => extras.to_a, 'id' => nil}, sender)
319
+ end
320
+
321
+ # gets called to handle all incomming messages (RPC requests)
322
+ def __data!(data, from)
323
+ @messages.enq [data, from]
324
+ end
325
+
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)
332
+ client = Legs.new(*args)
333
+ blk[client]
334
+ client.close!
335
+ end
336
+ end
337
+
338
+ Legs.initializer
339
+
340
+
341
+ class Legs::AsyncData
342
+ def initialize(data); @data = data; end
343
+ def result
344
+ @errored = true and raise @data['error'] if @data['error'] unless @errored
345
+ return @data['result']
346
+ end
347
+ alias_method :value, :result
348
+ end
349
+
350
+ class Legs::StartBlockError < StandardError; end
351
+ class Legs::RequestError < StandardError; end
data/test/tester.rb ADDED
@@ -0,0 +1,108 @@
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"
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: Bluebie-legs
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.6"
5
+ platform: ruby
6
+ authors:
7
+ - Jenna 'Bluebie' Fox
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-07-12 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json_pure
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">"
21
+ - !ruby/object:Gem::Version
22
+ version: 1.1.0
23
+ version:
24
+ 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.
25
+ email: blue@creativepony.com
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files: []
31
+
32
+ files:
33
+ - README.rdoc
34
+ - legs.gemspec
35
+ - lib/legs.rb
36
+ - examples/echo-server.rb
37
+ - examples/chat-server.rb
38
+ - examples/shoes-chat-client.rb
39
+ - test/tester.rb
40
+ has_rdoc: false
41
+ homepage: http://github.com/Bluebie/legs
42
+ post_install_message:
43
+ rdoc_options: []
44
+
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.2.0
63
+ signing_key:
64
+ specification_version: 2
65
+ summary: Simple fun open networking for newbies and quick hacks
66
+ test_files: []
67
+